Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: handle invites to projects #70

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
87 changes: 87 additions & 0 deletions src/renderer/src/components/InviteListener.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react'
import { ClientApiProvider } from '@comapeo/core-react'
import type { Invite } from '@comapeo/core/dist/invite-api'
import { createMapeoClient } from '@comapeo/ipc'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import {
RouterProvider,
createMemoryHistory,
createRouter,
} from '@tanstack/react-router'
import { act, render } from '@testing-library/react'
import { IntlProvider } from 'react-intl'
import { expect, test, vi } from 'vitest'

import { routeTree } from '../routeTree.gen'

type InviteEventHandler = (invite: Invite) => void

let inviteReceivedHandler: ((invite: InviteEventHandler) => void) | undefined

export type MapeoClientApi = ReturnType<typeof createMapeoClient>

const mockInvite = {
addListener: vi.fn(
(event: string, handler: (invite: InviteEventHandler) => void) => {
if (event === 'invite-received') {
inviteReceivedHandler = handler
}
},
),
removeListener: vi.fn(
(event: string, handler: (invite: InviteEventHandler) => void) => {
if (event === 'invite-received' && inviteReceivedHandler === handler) {
inviteReceivedHandler = undefined
}
},
),
emit: vi.fn((event: string, data: InviteEventHandler) => {
if (event === 'invite-received' && inviteReceivedHandler) {
inviteReceivedHandler(data)
}
}),
getPending: vi.fn(async () => {
return []
}),
}

const mockClientApi = {
invite: mockInvite,
} as unknown as MapeoClientApi
const queryClient = new QueryClient()
const testRouter = createRouter({
routeTree,
context: {},
history: createMemoryHistory({
initialEntries: ['/Onboarding/CreateJoinProjectScreen'],
}),
})

function setup() {
return render(
<IntlProvider locale="en">
<QueryClientProvider client={queryClient}>
<ClientApiProvider clientApi={mockClientApi}>
<RouterProvider router={testRouter} />
</ClientApiProvider>
</QueryClientProvider>
</IntlProvider>,
)
}

test('invite listener responds to invite-received', () => {
setup()
act(() => {
mockClientApi.invite.emit('invite-received', {
inviteId: 'test-invite-id',
projectName: 'Mock Project',
receivedAt: Date.now(),
projectInviteId: 'test-project-invite-id',
invitorName: 'Test Inviter',
})
})

expect(testRouter.state.location.pathname).toEqual(
'/Onboarding/JoinProjectScreen/test-invite-id',
)
})
34 changes: 34 additions & 0 deletions src/renderer/src/components/InviteListener.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useEffect } from 'react'
import { useClientApi } from '@comapeo/core-react'
import { useLocation, useNavigate } from '@tanstack/react-router'

import { useRejectInvite } from '../hooks/mutations/invites'

export function InviteListener() {
const clientApi = useClientApi()
const location = useLocation()
const navigate = useNavigate()
const rejectInvite = useRejectInvite()

useEffect(() => {
function onInviteReceived(invite: { inviteId: string }) {
if (location.pathname === '/Onboarding/CreateJoinProjectScreen') {
navigate({
to: '/Onboarding/JoinProjectScreen/$inviteId',
params: { inviteId: invite.inviteId },
})
} else {
// Reject all invites received while not on the join project screen
rejectInvite.mutate({ inviteId: invite.inviteId })
}
}

clientApi.invite.addListener('invite-received', onInviteReceived)

return () => {
clientApi.invite.removeListener('invite-received', onInviteReceived)
}
}, [clientApi, location.pathname, navigate, rejectInvite])

return null
}
34 changes: 34 additions & 0 deletions src/renderer/src/hooks/mutations/invites.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {
getInvitesQueryKey,
getPendingInvitesQueryKey,
useClientApi,
} from '@comapeo/core-react'
import { useMutation, useQueryClient } from '@tanstack/react-query'

export function useAcceptInvite() {
const clientApi = useClientApi()
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ inviteId }: { inviteId: string }) => {
return clientApi.invite.accept({ inviteId })
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: getPendingInvitesQueryKey() })
queryClient.invalidateQueries({ queryKey: getInvitesQueryKey() })
},
})
}

export function useRejectInvite() {
const clientApi = useClientApi()
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ inviteId }: { inviteId: string }) => {
return clientApi.invite.reject({ inviteId })
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: getPendingInvitesQueryKey() })
queryClient.invalidateQueries({ queryKey: getInvitesQueryKey() })
},
})
}
9 changes: 9 additions & 0 deletions src/renderer/src/hooks/usePendingInvites.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { pendingInvitesQueryOptions, useClientApi } from '@comapeo/core-react'
import { useSuspenseQuery } from '@tanstack/react-query'

export function usePendingInvites() {
const clientApi = useClientApi()
return useSuspenseQuery({
...pendingInvitesQueryOptions({ clientApi }),
})
}
57 changes: 29 additions & 28 deletions src/renderer/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ import { Route as WelcomeImport } from './routes/Welcome'
import { Route as IndexImport } from './routes/index'
import { Route as OnboardingIndexImport } from './routes/Onboarding/index'
import { Route as OnboardingPrivacyPolicyScreenImport } from './routes/Onboarding/PrivacyPolicyScreen'
import { Route as OnboardingJoinProjectScreenImport } from './routes/Onboarding/JoinProjectScreen'
import { Route as OnboardingDeviceNamingScreenImport } from './routes/Onboarding/DeviceNamingScreen'
import { Route as OnboardingDataPrivacyImport } from './routes/Onboarding/DataPrivacy'
import { Route as OnboardingCreateProjectScreenImport } from './routes/Onboarding/CreateProjectScreen'
import { Route as OnboardingCreateJoinProjectScreenImport } from './routes/Onboarding/CreateJoinProjectScreen'
import { Route as MapTabsMapImport } from './routes/(MapTabs)/_Map'
import { Route as OnboardingJoinProjectScreenInviteIdImport } from './routes/Onboarding/JoinProjectScreen.$inviteId'
import { Route as MapTabsMapTab2Import } from './routes/(MapTabs)/_Map.tab2'
import { Route as MapTabsMapTab1Import } from './routes/(MapTabs)/_Map.tab1'

Expand Down Expand Up @@ -62,13 +62,6 @@ const OnboardingPrivacyPolicyScreenRoute =
getParentRoute: () => rootRoute,
} as any)

const OnboardingJoinProjectScreenRoute =
OnboardingJoinProjectScreenImport.update({
id: '/Onboarding/JoinProjectScreen',
path: '/Onboarding/JoinProjectScreen',
getParentRoute: () => rootRoute,
} as any)

const OnboardingDeviceNamingScreenRoute =
OnboardingDeviceNamingScreenImport.update({
id: '/Onboarding/DeviceNamingScreen',
Expand Down Expand Up @@ -101,6 +94,13 @@ const MapTabsMapRoute = MapTabsMapImport.update({
getParentRoute: () => MapTabsRoute,
} as any)

const OnboardingJoinProjectScreenInviteIdRoute =
OnboardingJoinProjectScreenInviteIdImport.update({
id: '/Onboarding/JoinProjectScreen/$inviteId',
path: '/Onboarding/JoinProjectScreen/$inviteId',
getParentRoute: () => rootRoute,
} as any)

const MapTabsMapTab2Route = MapTabsMapTab2Import.update({
id: '/tab2',
path: '/tab2',
Expand Down Expand Up @@ -173,13 +173,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof OnboardingDeviceNamingScreenImport
parentRoute: typeof rootRoute
}
'/Onboarding/JoinProjectScreen': {
id: '/Onboarding/JoinProjectScreen'
path: '/Onboarding/JoinProjectScreen'
fullPath: '/Onboarding/JoinProjectScreen'
preLoaderRoute: typeof OnboardingJoinProjectScreenImport
parentRoute: typeof rootRoute
}
'/Onboarding/PrivacyPolicyScreen': {
id: '/Onboarding/PrivacyPolicyScreen'
path: '/Onboarding/PrivacyPolicyScreen'
Expand Down Expand Up @@ -208,6 +201,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof MapTabsMapTab2Import
parentRoute: typeof MapTabsMapImport
}
'/Onboarding/JoinProjectScreen/$inviteId': {
id: '/Onboarding/JoinProjectScreen/$inviteId'
path: '/Onboarding/JoinProjectScreen/$inviteId'
fullPath: '/Onboarding/JoinProjectScreen/$inviteId'
preLoaderRoute: typeof OnboardingJoinProjectScreenInviteIdImport
parentRoute: typeof rootRoute
}
}
}

Expand Down Expand Up @@ -245,11 +245,11 @@ export interface FileRoutesByFullPath {
'/Onboarding/CreateProjectScreen': typeof OnboardingCreateProjectScreenRoute
'/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute
'/Onboarding/DeviceNamingScreen': typeof OnboardingDeviceNamingScreenRoute
'/Onboarding/JoinProjectScreen': typeof OnboardingJoinProjectScreenRoute
'/Onboarding/PrivacyPolicyScreen': typeof OnboardingPrivacyPolicyScreenRoute
'/Onboarding': typeof OnboardingIndexRoute
'/tab1': typeof MapTabsMapTab1Route
'/tab2': typeof MapTabsMapTab2Route
'/Onboarding/JoinProjectScreen/$inviteId': typeof OnboardingJoinProjectScreenInviteIdRoute
}

export interface FileRoutesByTo {
Expand All @@ -259,11 +259,11 @@ export interface FileRoutesByTo {
'/Onboarding/CreateProjectScreen': typeof OnboardingCreateProjectScreenRoute
'/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute
'/Onboarding/DeviceNamingScreen': typeof OnboardingDeviceNamingScreenRoute
'/Onboarding/JoinProjectScreen': typeof OnboardingJoinProjectScreenRoute
'/Onboarding/PrivacyPolicyScreen': typeof OnboardingPrivacyPolicyScreenRoute
'/Onboarding': typeof OnboardingIndexRoute
'/tab1': typeof MapTabsMapTab1Route
'/tab2': typeof MapTabsMapTab2Route
'/Onboarding/JoinProjectScreen/$inviteId': typeof OnboardingJoinProjectScreenInviteIdRoute
}

export interface FileRoutesById {
Expand All @@ -276,11 +276,11 @@ export interface FileRoutesById {
'/Onboarding/CreateProjectScreen': typeof OnboardingCreateProjectScreenRoute
'/Onboarding/DataPrivacy': typeof OnboardingDataPrivacyRoute
'/Onboarding/DeviceNamingScreen': typeof OnboardingDeviceNamingScreenRoute
'/Onboarding/JoinProjectScreen': typeof OnboardingJoinProjectScreenRoute
'/Onboarding/PrivacyPolicyScreen': typeof OnboardingPrivacyPolicyScreenRoute
'/Onboarding/': typeof OnboardingIndexRoute
'/(MapTabs)/_Map/tab1': typeof MapTabsMapTab1Route
'/(MapTabs)/_Map/tab2': typeof MapTabsMapTab2Route
'/Onboarding/JoinProjectScreen/$inviteId': typeof OnboardingJoinProjectScreenInviteIdRoute
}

export interface FileRouteTypes {
Expand All @@ -292,11 +292,11 @@ export interface FileRouteTypes {
| '/Onboarding/CreateProjectScreen'
| '/Onboarding/DataPrivacy'
| '/Onboarding/DeviceNamingScreen'
| '/Onboarding/JoinProjectScreen'
| '/Onboarding/PrivacyPolicyScreen'
| '/Onboarding'
| '/tab1'
| '/tab2'
| '/Onboarding/JoinProjectScreen/$inviteId'
fileRoutesByTo: FileRoutesByTo
to:
| '/'
Expand All @@ -305,11 +305,11 @@ export interface FileRouteTypes {
| '/Onboarding/CreateProjectScreen'
| '/Onboarding/DataPrivacy'
| '/Onboarding/DeviceNamingScreen'
| '/Onboarding/JoinProjectScreen'
| '/Onboarding/PrivacyPolicyScreen'
| '/Onboarding'
| '/tab1'
| '/tab2'
| '/Onboarding/JoinProjectScreen/$inviteId'
id:
| '__root__'
| '/'
Expand All @@ -320,11 +320,11 @@ export interface FileRouteTypes {
| '/Onboarding/CreateProjectScreen'
| '/Onboarding/DataPrivacy'
| '/Onboarding/DeviceNamingScreen'
| '/Onboarding/JoinProjectScreen'
| '/Onboarding/PrivacyPolicyScreen'
| '/Onboarding/'
| '/(MapTabs)/_Map/tab1'
| '/(MapTabs)/_Map/tab2'
| '/Onboarding/JoinProjectScreen/$inviteId'
fileRoutesById: FileRoutesById
}

Expand All @@ -336,9 +336,9 @@ export interface RootRouteChildren {
OnboardingCreateProjectScreenRoute: typeof OnboardingCreateProjectScreenRoute
OnboardingDataPrivacyRoute: typeof OnboardingDataPrivacyRoute
OnboardingDeviceNamingScreenRoute: typeof OnboardingDeviceNamingScreenRoute
OnboardingJoinProjectScreenRoute: typeof OnboardingJoinProjectScreenRoute
OnboardingPrivacyPolicyScreenRoute: typeof OnboardingPrivacyPolicyScreenRoute
OnboardingIndexRoute: typeof OnboardingIndexRoute
OnboardingJoinProjectScreenInviteIdRoute: typeof OnboardingJoinProjectScreenInviteIdRoute
}

const rootRouteChildren: RootRouteChildren = {
Expand All @@ -350,9 +350,10 @@ const rootRouteChildren: RootRouteChildren = {
OnboardingCreateProjectScreenRoute: OnboardingCreateProjectScreenRoute,
OnboardingDataPrivacyRoute: OnboardingDataPrivacyRoute,
OnboardingDeviceNamingScreenRoute: OnboardingDeviceNamingScreenRoute,
OnboardingJoinProjectScreenRoute: OnboardingJoinProjectScreenRoute,
OnboardingPrivacyPolicyScreenRoute: OnboardingPrivacyPolicyScreenRoute,
OnboardingIndexRoute: OnboardingIndexRoute,
OnboardingJoinProjectScreenInviteIdRoute:
OnboardingJoinProjectScreenInviteIdRoute,
}

export const routeTree = rootRoute
Expand All @@ -372,9 +373,9 @@ export const routeTree = rootRoute
"/Onboarding/CreateProjectScreen",
"/Onboarding/DataPrivacy",
"/Onboarding/DeviceNamingScreen",
"/Onboarding/JoinProjectScreen",
"/Onboarding/PrivacyPolicyScreen",
"/Onboarding/"
"/Onboarding/",
"/Onboarding/JoinProjectScreen/$inviteId"
]
},
"/": {
Expand Down Expand Up @@ -409,9 +410,6 @@ export const routeTree = rootRoute
"/Onboarding/DeviceNamingScreen": {
"filePath": "Onboarding/DeviceNamingScreen.tsx"
},
"/Onboarding/JoinProjectScreen": {
"filePath": "Onboarding/JoinProjectScreen.tsx"
},
"/Onboarding/PrivacyPolicyScreen": {
"filePath": "Onboarding/PrivacyPolicyScreen.tsx"
},
Expand All @@ -425,6 +423,9 @@ export const routeTree = rootRoute
"/(MapTabs)/_Map/tab2": {
"filePath": "(MapTabs)/_Map.tab2.tsx",
"parent": "/(MapTabs)/_Map"
},
"/Onboarding/JoinProjectScreen/$inviteId": {
"filePath": "Onboarding/JoinProjectScreen.$inviteId.tsx"
}
}
}
Expand Down
Loading
Loading