@planningcenter/chat-react-native 1.5.0 → 1.5.1-qa-22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/components/conversations.d.ts.map +1 -1
- package/build/components/conversations.js +29 -8
- package/build/components/conversations.js.map +1 -1
- package/build/components/index.d.ts +1 -0
- package/build/components/index.d.ts.map +1 -1
- package/build/components/index.js +1 -0
- package/build/components/index.js.map +1 -1
- package/build/contexts/api_provider.d.ts +4 -6
- package/build/contexts/api_provider.d.ts.map +1 -1
- package/build/contexts/api_provider.js +12 -20
- package/build/contexts/api_provider.js.map +1 -1
- package/build/contexts/chat_context.d.ts +7 -5
- package/build/contexts/chat_context.d.ts.map +1 -1
- package/build/contexts/chat_context.js +40 -4
- package/build/contexts/chat_context.js.map +1 -1
- package/build/hooks/index.d.ts +2 -0
- package/build/hooks/index.d.ts.map +1 -1
- package/build/hooks/index.js +2 -0
- package/build/hooks/index.js.map +1 -1
- package/build/hooks/use_api.d.ts +61 -0
- package/build/hooks/use_api.d.ts.map +1 -0
- package/build/hooks/use_api.js +39 -0
- package/build/hooks/use_api.js.map +1 -0
- package/build/hooks/use_current_person.d.ts +3 -0
- package/build/hooks/use_current_person.d.ts.map +1 -0
- package/build/hooks/use_current_person.js +13 -0
- package/build/hooks/use_current_person.js.map +1 -0
- package/build/navigation/index.d.ts +1 -0
- package/build/navigation/index.d.ts.map +1 -1
- package/build/navigation/index.js +7 -4
- package/build/navigation/index.js.map +1 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +59 -6
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/screens/not_found.js +1 -1
- package/build/screens/not_found.js.map +1 -1
- package/build/types/api_primitives.d.ts +23 -0
- package/build/types/api_primitives.d.ts.map +1 -0
- package/build/types/api_primitives.js +2 -0
- package/build/types/api_primitives.js.map +1 -0
- package/build/types/index.d.ts +4 -0
- package/build/types/index.d.ts.map +1 -0
- package/build/types/index.js +4 -0
- package/build/types/index.js.map +1 -0
- package/build/types/resources/conversation.d.ts +15 -0
- package/build/types/resources/conversation.d.ts.map +1 -0
- package/build/types/resources/conversation.js +2 -0
- package/build/types/resources/conversation.js.map +1 -0
- package/build/types/resources/index.d.ts +5 -0
- package/build/types/resources/index.d.ts.map +1 -0
- package/build/types/resources/index.js +5 -0
- package/build/types/resources/index.js.map +1 -0
- package/build/types/resources/message.d.ts +16 -0
- package/build/types/resources/message.d.ts.map +1 -0
- package/build/types/resources/message.js +2 -0
- package/build/types/resources/message.js.map +1 -0
- package/build/types/resources/oauth_token.d.ts +9 -0
- package/build/types/resources/oauth_token.d.ts.map +1 -0
- package/build/types/resources/oauth_token.js +2 -0
- package/build/types/resources/oauth_token.js.map +1 -0
- package/build/types/resources/person.d.ts +9 -0
- package/build/types/resources/person.d.ts.map +1 -0
- package/build/types/resources/person.js +2 -0
- package/build/types/resources/person.js.map +1 -0
- package/build/types/resources/reaction.d.ts +10 -0
- package/build/types/resources/reaction.d.ts.map +1 -0
- package/build/types/resources/reaction.js +2 -0
- package/build/types/resources/reaction.js.map +1 -0
- package/build/types/utils/index.d.ts +4 -0
- package/build/types/utils/index.d.ts.map +1 -0
- package/build/types/utils/index.js +4 -0
- package/build/types/utils/index.js.map +1 -0
- package/build/utils/client/client.d.ts +21 -12
- package/build/utils/client/client.d.ts.map +1 -1
- package/build/utils/client/client.js +24 -22
- package/build/utils/client/client.js.map +1 -1
- package/build/utils/session.d.ts +0 -5
- package/build/utils/session.d.ts.map +1 -1
- package/build/utils/session.js +0 -10
- package/build/utils/session.js.map +1 -1
- package/package.json +2 -3
- package/src/__tests__/client.ts +72 -19
- package/src/__tests__/session.ts +0 -11
- package/src/__utils__/handlers.ts +1 -1
- package/src/components/conversations.tsx +33 -11
- package/src/components/index.tsx +1 -0
- package/src/contexts/api_provider.tsx +17 -26
- package/src/contexts/chat_context.tsx +61 -7
- package/src/hooks/index.ts +2 -0
- package/src/hooks/use_api.ts +60 -0
- package/src/hooks/use_current_person.ts +15 -0
- package/src/navigation/index.tsx +14 -4
- package/src/screens/conversation_screen.tsx +83 -7
- package/src/screens/not_found.tsx +1 -1
- package/src/types/api_primitives.ts +24 -0
- package/src/types/index.ts +3 -0
- package/src/types/resources/conversation.ts +15 -0
- package/src/types/resources/index.ts +4 -0
- package/src/types/resources/message.ts +18 -0
- package/src/types/resources/oauth_token.ts +8 -0
- package/src/types/resources/person.ts +9 -0
- package/src/types/resources/reaction.ts +9 -0
- package/src/types/utils/index.ts +6 -0
- package/src/utils/client/client.ts +41 -34
- package/src/utils/client/types.d.ts +2 -0
- package/src/utils/session.ts +0 -13
- package/build/utils/api.d.ts +0 -9
- package/build/utils/api.d.ts.map +0 -1
- package/build/utils/api.js +0 -36
- package/build/utils/api.js.map +0 -1
- package/src/types.d.ts +0 -35
- package/src/utils/api.ts +0 -47
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/chat-react-native",
|
|
3
|
-
"version": "1.5.0",
|
|
3
|
+
"version": "1.5.1-qa-22.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
"prepublishOnly": "expo-module prepublishOnly"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@planningcenter/chat-core": "^1.5.0",
|
|
21
20
|
"fast-text-encoding": "^1.0.6",
|
|
22
21
|
"jest-fetch-mock": "^3.0.3",
|
|
23
22
|
"lodash-inflection": "^1.5.0",
|
|
@@ -45,5 +44,5 @@
|
|
|
45
44
|
"prettier": "^3.4.2",
|
|
46
45
|
"typescript": "<5.6.0"
|
|
47
46
|
},
|
|
48
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "e15259014cecb56e087edf1f905dce3c059fcfde"
|
|
49
48
|
}
|
package/src/__tests__/client.ts
CHANGED
|
@@ -3,26 +3,13 @@ import Client from '../utils/client/client'
|
|
|
3
3
|
import { Session } from '../utils/session'
|
|
4
4
|
import DefaultFixtures from '../__utils__/fixtures/defaults'
|
|
5
5
|
import { BASE_URL } from '../__utils__/handlers'
|
|
6
|
+
import { OAuthToken } from '../types'
|
|
6
7
|
|
|
7
8
|
const APP_BASE_URL = BASE_URL
|
|
8
9
|
|
|
9
|
-
let session
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
app: 'chat',
|
|
13
|
-
version: '2018-11-01',
|
|
14
|
-
session,
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
const clientWithDefaultHeaders = new Client({
|
|
18
|
-
app: 'chat',
|
|
19
|
-
version: '2018-11-01',
|
|
20
|
-
defaultHeaders: {
|
|
21
|
-
'X-Custom-Default-Header': 'important data',
|
|
22
|
-
},
|
|
23
|
-
session,
|
|
24
|
-
})
|
|
25
|
-
|
|
10
|
+
let session: Session
|
|
11
|
+
let client: Client
|
|
12
|
+
let clientWithDefaultHeaders: Client
|
|
26
13
|
let fetchSpy: jest.SpyInstance
|
|
27
14
|
|
|
28
15
|
beforeAll(() => MockServer.server().listen())
|
|
@@ -32,6 +19,22 @@ afterAll(() => MockServer.server().close())
|
|
|
32
19
|
beforeEach(() => {
|
|
33
20
|
jest.clearAllMocks()
|
|
34
21
|
fetchSpy = jest.spyOn(globalThis, 'fetch')
|
|
22
|
+
session = new Session({ env: 'development' })
|
|
23
|
+
client = new Client({
|
|
24
|
+
app: 'chat',
|
|
25
|
+
version: '2018-11-01',
|
|
26
|
+
session,
|
|
27
|
+
onTokenExpired: () => {},
|
|
28
|
+
})
|
|
29
|
+
clientWithDefaultHeaders = new Client({
|
|
30
|
+
app: 'chat',
|
|
31
|
+
version: '2018-11-01',
|
|
32
|
+
defaultHeaders: {
|
|
33
|
+
'X-Custom-Default-Header': 'important data',
|
|
34
|
+
},
|
|
35
|
+
session,
|
|
36
|
+
onTokenExpired: () => {},
|
|
37
|
+
})
|
|
35
38
|
})
|
|
36
39
|
|
|
37
40
|
describe('get', () => {
|
|
@@ -341,13 +344,14 @@ describe('url switching', () => {
|
|
|
341
344
|
const prodBase = 'https://api.planningcenteronline.com/chat/v2'
|
|
342
345
|
const stagingBase = 'https://api-staging.planningcenteronline.com/chat/v2'
|
|
343
346
|
|
|
344
|
-
MockServer.get(prodBase + '/records', {}, 200)
|
|
345
|
-
MockServer.get(stagingBase + '/records', {}, 200)
|
|
347
|
+
MockServer.get(prodBase + '/records', {}, 200, { once: true })
|
|
348
|
+
MockServer.get(stagingBase + '/records', {}, 200, { once: true })
|
|
346
349
|
|
|
347
350
|
const newClient = new Client({
|
|
348
351
|
app: 'chat',
|
|
349
352
|
session,
|
|
350
353
|
version: '2018-11-01',
|
|
354
|
+
onTokenExpired: () => {},
|
|
351
355
|
})
|
|
352
356
|
|
|
353
357
|
await newClient.get({
|
|
@@ -385,4 +389,53 @@ describe('url switching', () => {
|
|
|
385
389
|
expect.any(Object)
|
|
386
390
|
)
|
|
387
391
|
})
|
|
392
|
+
|
|
393
|
+
it('Changes to session switches the base url with custom headers', async () => {
|
|
394
|
+
const token: OAuthToken = {
|
|
395
|
+
access_token: 'foo',
|
|
396
|
+
token_type: undefined,
|
|
397
|
+
created_at: 0,
|
|
398
|
+
expires_in: undefined,
|
|
399
|
+
scope: '',
|
|
400
|
+
refresh_token: undefined,
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
session.token = token
|
|
404
|
+
|
|
405
|
+
await client.get({
|
|
406
|
+
url: '/records',
|
|
407
|
+
data: { fields: { Record: ['id'] } },
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
requestHeadersShouldContain({
|
|
411
|
+
...fetchSpy.mock.calls[0][1],
|
|
412
|
+
key: 'Authorization',
|
|
413
|
+
value: 'foo',
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
session.token.access_token = 'bar'
|
|
417
|
+
|
|
418
|
+
await client.get({
|
|
419
|
+
url: '/records',
|
|
420
|
+
data: { fields: { Record: ['id'] } },
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
requestHeadersShouldContain({
|
|
424
|
+
...fetchSpy.mock.calls[1][1],
|
|
425
|
+
key: 'Authorization',
|
|
426
|
+
value: 'bar',
|
|
427
|
+
})
|
|
428
|
+
})
|
|
388
429
|
})
|
|
430
|
+
|
|
431
|
+
const requestHeadersShouldContain = ({
|
|
432
|
+
headers,
|
|
433
|
+
key,
|
|
434
|
+
value,
|
|
435
|
+
}: {
|
|
436
|
+
headers: Record<string, unknown>
|
|
437
|
+
key: string
|
|
438
|
+
value: unknown
|
|
439
|
+
}) => {
|
|
440
|
+
expect(headers[key]).toContain(value)
|
|
441
|
+
}
|
package/src/__tests__/session.ts
CHANGED
|
@@ -25,17 +25,6 @@ describe('Session', () => {
|
|
|
25
25
|
})
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
describe('urls', () => {
|
|
29
|
-
it('should return the correct base url', () => {
|
|
30
|
-
const session = new Session({ env: 'production' })
|
|
31
|
-
expect(session.baseUrl).toBe('https://api.planningcenteronline.com')
|
|
32
|
-
session.env = 'development'
|
|
33
|
-
expect(session.baseUrl).toBe('http://api.pco.test')
|
|
34
|
-
session.env = 'staging'
|
|
35
|
-
expect(session.baseUrl).toBe('https://api-staging.planningcenteronline.com')
|
|
36
|
-
})
|
|
37
|
-
})
|
|
38
|
-
|
|
39
28
|
describe('hydrate', () => {
|
|
40
29
|
describe('success', () => {
|
|
41
30
|
it('should return a hydrated Session instance', () => {
|
|
@@ -1,27 +1,43 @@
|
|
|
1
|
-
import { ConversationResource, JSONAPICollection } from '@planningcenter/chat-core'
|
|
2
1
|
import { useNavigation } from '@react-navigation/native'
|
|
3
|
-
import { useSuspenseQuery } from '@tanstack/react-query'
|
|
4
2
|
import React from 'react'
|
|
5
3
|
import { FlatList, Pressable, StyleSheet } from 'react-native'
|
|
6
4
|
import { useTheme } from '../hooks'
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
import { useSuspensePaginator } from '../hooks/use_api'
|
|
6
|
+
import { ConversationResource } from '../types'
|
|
7
|
+
import { GetRequest } from '../utils/client/types'
|
|
8
|
+
import { Heading, Text } from './display'
|
|
10
9
|
|
|
11
10
|
export const Conversations = () => {
|
|
12
11
|
const styles = useStyles()
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
const request: GetRequest = {
|
|
13
|
+
url: '/me/conversations',
|
|
14
|
+
data: {
|
|
15
|
+
perPage: 20,
|
|
16
|
+
order: '-last_message',
|
|
17
|
+
fields: {
|
|
18
|
+
Conversation: [
|
|
19
|
+
'title',
|
|
20
|
+
'last_message_created_at',
|
|
21
|
+
'last_message_author_name',
|
|
22
|
+
'last_message_text_preview',
|
|
23
|
+
'unread_count',
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
const { data: conversations, fetchNextPage } = useSuspensePaginator<ConversationResource>(request)
|
|
29
|
+
|
|
30
|
+
// TODO: Filter using the API
|
|
31
|
+
const nonEmptyConversations = conversations.filter(c => c.lastMessageTextPreview) || []
|
|
32
|
+
|
|
16
33
|
const navigation = useNavigation()
|
|
17
34
|
|
|
18
35
|
return (
|
|
19
36
|
<FlatList
|
|
20
|
-
data={
|
|
37
|
+
data={nonEmptyConversations}
|
|
21
38
|
contentContainerStyle={styles.container}
|
|
22
39
|
style={styles.scrollView}
|
|
23
40
|
ListEmptyComponent={<Text>No conversations found</Text>}
|
|
24
|
-
ListHeaderComponent={<Text style={styles.foo}>Conversations</Text>}
|
|
25
41
|
renderItem={({ item }) => (
|
|
26
42
|
<Pressable
|
|
27
43
|
onPress={() =>
|
|
@@ -31,9 +47,15 @@ export const Conversations = () => {
|
|
|
31
47
|
})
|
|
32
48
|
}
|
|
33
49
|
>
|
|
34
|
-
<
|
|
50
|
+
<Heading numberOfLines={1} variant="h3">
|
|
51
|
+
{item.title}
|
|
52
|
+
</Heading>
|
|
53
|
+
<Text style={styles.listItem}>
|
|
54
|
+
{item.lastMessageAuthorName}: {item.lastMessageTextPreview}
|
|
55
|
+
</Text>
|
|
35
56
|
</Pressable>
|
|
36
57
|
)}
|
|
58
|
+
onEndReached={() => fetchNextPage()}
|
|
37
59
|
/>
|
|
38
60
|
)
|
|
39
61
|
}
|
package/src/components/index.tsx
CHANGED
|
@@ -1,30 +1,19 @@
|
|
|
1
|
-
import { JSONAPIResponse } from '@planningcenter/chat-core'
|
|
2
1
|
import { QueryClient, QueryClientProvider, QueryKey } from '@tanstack/react-query'
|
|
3
2
|
import React, { useEffect } from 'react'
|
|
4
3
|
import { ViewProps } from 'react-native'
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import { ENV, session } from '../utils/session'
|
|
8
|
-
import Uri from '../utils/uri'
|
|
4
|
+
import { Client } from '../utils'
|
|
5
|
+
import { GetRequest } from '../utils/client/types'
|
|
9
6
|
|
|
10
|
-
let
|
|
7
|
+
let apiClient: Client | undefined
|
|
11
8
|
|
|
12
9
|
const defaultQueryFn = ({ queryKey }: { queryKey: QueryKey }) => {
|
|
13
|
-
if (!
|
|
10
|
+
if (!apiClient) {
|
|
14
11
|
throw new Error('No token present')
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return apiRequest<JSONAPIResponse>(url)
|
|
21
|
-
.then(r => r.json)
|
|
22
|
-
.catch(error => {
|
|
23
|
-
if (error.message === 'Token expired') {
|
|
24
|
-
handleTokenExpired()
|
|
25
|
-
}
|
|
26
|
-
return null
|
|
27
|
-
})
|
|
14
|
+
const data = queryKey[0] as GetRequest
|
|
15
|
+
|
|
16
|
+
return apiClient.get(data)
|
|
28
17
|
}
|
|
29
18
|
|
|
30
19
|
export const queryClient = new QueryClient({
|
|
@@ -37,17 +26,19 @@ export const queryClient = new QueryClient({
|
|
|
37
26
|
|
|
38
27
|
export function ApiProvider({
|
|
39
28
|
children,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
handleTokenExpired = onTokenExpired
|
|
29
|
+
client,
|
|
30
|
+
sessionChanged,
|
|
31
|
+
}: ViewProps & { client: Client; sessionChanged: boolean }) {
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
apiClient = client
|
|
34
|
+
}, [client])
|
|
47
35
|
|
|
36
|
+
// TODO: CREATE CALLBACK TO INVALIDATE QUERIES
|
|
48
37
|
useEffect(() => {
|
|
38
|
+
if (!sessionChanged) return
|
|
39
|
+
|
|
49
40
|
queryClient.invalidateQueries()
|
|
50
|
-
}, [
|
|
41
|
+
}, [sessionChanged])
|
|
51
42
|
|
|
52
43
|
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
53
44
|
}
|
|
@@ -1,39 +1,93 @@
|
|
|
1
1
|
import { merge } from 'lodash'
|
|
2
|
-
import React, { createContext, useMemo } from 'react'
|
|
2
|
+
import React, { createContext, useEffect, useMemo, useRef } from 'react'
|
|
3
3
|
import { ColorSchemeName, useColorScheme } from 'react-native'
|
|
4
4
|
import { DeepPartial, OAuthToken } from '../types'
|
|
5
5
|
import { defaultTheme, DefaultTheme } from '../utils/theme'
|
|
6
6
|
import { aliasTokensColorMap } from '../vendor/tapestry/alias_tokens_color_map'
|
|
7
7
|
import { ApiProvider } from './api_provider'
|
|
8
|
+
import { Client, ENV, Session } from '../utils'
|
|
8
9
|
|
|
9
|
-
type
|
|
10
|
+
type ChatContextValue = {
|
|
10
11
|
token?: OAuthToken
|
|
11
12
|
onTokenExpired: () => void
|
|
12
13
|
theme: any
|
|
13
|
-
env?:
|
|
14
|
+
env?: ENV
|
|
15
|
+
client: Client
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
const defaultChatClient = new Client({
|
|
19
|
+
app: 'chat',
|
|
20
|
+
session: new Session(),
|
|
21
|
+
version: '2018-11-01',
|
|
22
|
+
onTokenExpired: () => null,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
export const ChatContext = createContext<ChatContextValue>({
|
|
17
26
|
theme: undefined,
|
|
18
27
|
token: undefined,
|
|
19
28
|
env: undefined,
|
|
20
29
|
onTokenExpired: () => {},
|
|
30
|
+
client: defaultChatClient,
|
|
21
31
|
})
|
|
22
32
|
|
|
23
|
-
export function ChatProvider({
|
|
33
|
+
export function ChatProvider({
|
|
34
|
+
children,
|
|
35
|
+
value,
|
|
36
|
+
}: {
|
|
37
|
+
children: any
|
|
38
|
+
value: Omit<ChatContextValue, 'client'>
|
|
39
|
+
}) {
|
|
40
|
+
const { env, token, onTokenExpired } = value
|
|
24
41
|
const theme = useCreateChatTheme(value.theme)
|
|
42
|
+
const session = useMemo(() => new Session({ token, env }), [env, token])
|
|
43
|
+
const client = useMemo(
|
|
44
|
+
() =>
|
|
45
|
+
new Client({
|
|
46
|
+
app: 'chat',
|
|
47
|
+
session,
|
|
48
|
+
version: '2018-11-01',
|
|
49
|
+
onTokenExpired,
|
|
50
|
+
}),
|
|
51
|
+
[onTokenExpired, session]
|
|
52
|
+
)
|
|
53
|
+
const sessionChanged = useSessionChanged({ token, env })
|
|
54
|
+
|
|
55
|
+
const contextValue: ChatContextValue = {
|
|
56
|
+
env,
|
|
57
|
+
token,
|
|
58
|
+
onTokenExpired,
|
|
59
|
+
theme,
|
|
60
|
+
client,
|
|
61
|
+
}
|
|
25
62
|
|
|
26
63
|
if (!Object.keys(value.token || {}).length) return null
|
|
27
64
|
|
|
28
65
|
return (
|
|
29
|
-
<ChatContext.Provider value={
|
|
30
|
-
<ApiProvider
|
|
66
|
+
<ChatContext.Provider value={contextValue}>
|
|
67
|
+
<ApiProvider sessionChanged={sessionChanged} client={client}>
|
|
31
68
|
{children}
|
|
32
69
|
</ApiProvider>
|
|
33
70
|
</ChatContext.Provider>
|
|
34
71
|
)
|
|
35
72
|
}
|
|
36
73
|
|
|
74
|
+
function useSessionChanged(value: Pick<ChatContextValue, 'token' | 'env'>): boolean {
|
|
75
|
+
const { token: newToken, env: newEnv } = value
|
|
76
|
+
const { token: prevToken, env: prevEnv } = usePrevious<typeof value>(value)
|
|
77
|
+
|
|
78
|
+
return Boolean(prevToken && newToken !== prevToken) || Boolean(prevEnv && newEnv !== prevEnv)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function usePrevious<T>(value) {
|
|
82
|
+
const ref = useRef<T>(value)
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
ref.current = value
|
|
86
|
+
}, [value])
|
|
87
|
+
|
|
88
|
+
return ref.current
|
|
89
|
+
}
|
|
90
|
+
|
|
37
91
|
interface CreateChatThemeProps {
|
|
38
92
|
theme?: DeepPartial<DefaultTheme>
|
|
39
93
|
colorScheme?: ColorSchemeName
|
package/src/hooks/index.ts
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { InfiniteData, useSuspenseInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query'
|
|
2
|
+
import { useContext } from 'react'
|
|
3
|
+
import { ChatContext } from '../contexts'
|
|
4
|
+
import { ApiCollection, ApiResource, ResourceObject } from '../types'
|
|
5
|
+
import { GetRequest, RequestData } from '../utils/client/types'
|
|
6
|
+
|
|
7
|
+
export const useSuspenseGet = <T extends ResourceObject | ResourceObject[]>(args: GetRequest) => {
|
|
8
|
+
type Resource = T extends ResourceObject ? ApiResource<T> : ApiCollection<T>
|
|
9
|
+
|
|
10
|
+
const { data, ...query } = useSuspenseQuery<Resource, Response>({
|
|
11
|
+
queryKey: [args],
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
return { ...data, ...query }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type NextMeta = Partial<{
|
|
18
|
+
offset: string
|
|
19
|
+
idLt: string
|
|
20
|
+
}>
|
|
21
|
+
|
|
22
|
+
export const useSuspensePaginator = <T extends ResourceObject>(args: GetRequest) => {
|
|
23
|
+
const { client } = useContext(ChatContext)
|
|
24
|
+
const query = useSuspenseInfiniteQuery<
|
|
25
|
+
ApiCollection<T>,
|
|
26
|
+
Response,
|
|
27
|
+
InfiniteData<ApiCollection<T>>,
|
|
28
|
+
any,
|
|
29
|
+
Partial<RequestData> | undefined
|
|
30
|
+
>({
|
|
31
|
+
queryKey: [args.url, args.data],
|
|
32
|
+
queryFn: ({ pageParam }) => {
|
|
33
|
+
const pageParmWhere = pageParam?.where || {}
|
|
34
|
+
const argsWhere = args.data.where || {}
|
|
35
|
+
const where = { ...argsWhere, ...pageParmWhere }
|
|
36
|
+
|
|
37
|
+
const offset = pageParam?.offset || args.data.offset
|
|
38
|
+
const data = { ...args.data, where, offset }
|
|
39
|
+
|
|
40
|
+
return client.get({
|
|
41
|
+
url: args.url,
|
|
42
|
+
data,
|
|
43
|
+
})
|
|
44
|
+
},
|
|
45
|
+
initialPageParam: {} as Partial<RequestData>,
|
|
46
|
+
getNextPageParam: lastPage => {
|
|
47
|
+
const next: NextMeta = lastPage.meta?.next || {}
|
|
48
|
+
const { offset, idLt } = next
|
|
49
|
+
|
|
50
|
+
if (idLt) return { where: { id_lt: idLt } }
|
|
51
|
+
if (offset) return { offset: Number(offset) }
|
|
52
|
+
|
|
53
|
+
return undefined
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const data: T[] = query.data?.pages.flatMap(page => page.data) || []
|
|
58
|
+
|
|
59
|
+
return { ...query, data }
|
|
60
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PersonResource } from '../types'
|
|
2
|
+
import { useSuspenseGet } from './use_api'
|
|
3
|
+
|
|
4
|
+
export const useCurrentPerson = () => {
|
|
5
|
+
const { data: person } = useSuspenseGet<PersonResource>({
|
|
6
|
+
url: '/me',
|
|
7
|
+
data: {
|
|
8
|
+
fields: {
|
|
9
|
+
Person: ['id', 'name', 'avatar', 'unread_count', 'pco_chat_enabled'],
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
return person
|
|
15
|
+
}
|
package/src/navigation/index.tsx
CHANGED
|
@@ -5,7 +5,8 @@ import { NotFound } from '../screens/not_found'
|
|
|
5
5
|
import { ScreenLayout } from './screenLayout'
|
|
6
6
|
import { ConversationsScreen } from '../screens/conversations_screen'
|
|
7
7
|
import { ConversationScreen } from '../screens/conversation_screen'
|
|
8
|
-
import { HeaderBackButton } from '@react-navigation/elements'
|
|
8
|
+
import { HeaderBackButton, PlatformPressable } from '@react-navigation/elements'
|
|
9
|
+
import { Icon } from '../components'
|
|
9
10
|
|
|
10
11
|
export const ChatStack = createNativeStackNavigator({
|
|
11
12
|
screenLayout: ScreenLayout,
|
|
@@ -13,9 +14,18 @@ export const ChatStack = createNativeStackNavigator({
|
|
|
13
14
|
Conversations: {
|
|
14
15
|
screen: ConversationsScreen,
|
|
15
16
|
options: ({ route, navigation }) => ({
|
|
16
|
-
headerTitle: (route.params as { title?: string })?.title ?? '
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
headerTitle: (route.params as { title?: string })?.title ?? 'Chat',
|
|
18
|
+
headerLeft: () => (
|
|
19
|
+
<PlatformPressable onPress={() => null}>
|
|
20
|
+
<Icon name="general.threeReducingHorizontalBars" size={18} />
|
|
21
|
+
</PlatformPressable>
|
|
22
|
+
),
|
|
23
|
+
headerRight: () => (
|
|
24
|
+
<HeaderBackButton
|
|
25
|
+
onPress={navigation.goBack}
|
|
26
|
+
backImage={() => <Icon name="general.x" size={18} />}
|
|
27
|
+
/>
|
|
28
|
+
),
|
|
19
29
|
}),
|
|
20
30
|
},
|
|
21
31
|
Conversation: {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { StaticScreenProps } from '@react-navigation/native'
|
|
2
|
-
import React from 'react'
|
|
3
|
-
import { StyleSheet, View } from 'react-native'
|
|
1
|
+
import { StaticScreenProps, useNavigation } from '@react-navigation/native'
|
|
2
|
+
import React, { useEffect } from 'react'
|
|
3
|
+
import { FlatList, SafeAreaView, StyleSheet, TextInput, View } from 'react-native'
|
|
4
4
|
import { Text } from '../components/display'
|
|
5
|
+
import { useSuspenseGet, useSuspensePaginator } from '../hooks/use_api'
|
|
6
|
+
import { ConversationResource, MessageResource } from '../types'
|
|
7
|
+
import { useTheme } from '../hooks'
|
|
5
8
|
|
|
6
9
|
export type ConversationScreenProps = StaticScreenProps<{
|
|
7
10
|
conversation_id: string
|
|
@@ -9,17 +12,90 @@ export type ConversationScreenProps = StaticScreenProps<{
|
|
|
9
12
|
}>
|
|
10
13
|
|
|
11
14
|
export function ConversationScreen({ route }: ConversationScreenProps) {
|
|
15
|
+
const navigation = useNavigation()
|
|
16
|
+
const { conversation_id } = route.params
|
|
17
|
+
|
|
18
|
+
const { data: conversation } = useSuspenseGet<ConversationResource>({
|
|
19
|
+
url: `/me/conversations/${conversation_id}`,
|
|
20
|
+
data: {
|
|
21
|
+
fields: {
|
|
22
|
+
Conversation: ['title'],
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const { data, refetch, isRefetching, fetchNextPage } = useSuspensePaginator<MessageResource>({
|
|
28
|
+
url: `/me/conversations/${conversation_id}/messages`,
|
|
29
|
+
data: {
|
|
30
|
+
perPage: 25,
|
|
31
|
+
fields: {
|
|
32
|
+
Message: ['text', 'mine'],
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
navigation.setOptions({ title: conversation?.title })
|
|
39
|
+
}, [conversation, conversation_id, navigation])
|
|
40
|
+
|
|
12
41
|
return (
|
|
13
|
-
<
|
|
14
|
-
<
|
|
15
|
-
|
|
42
|
+
<SafeAreaView style={styles.container}>
|
|
43
|
+
<FlatList
|
|
44
|
+
inverted
|
|
45
|
+
contentContainerStyle={styles.listContainer}
|
|
46
|
+
refreshing={isRefetching}
|
|
47
|
+
onRefresh={refetch}
|
|
48
|
+
data={data}
|
|
49
|
+
keyExtractor={item => item.id}
|
|
50
|
+
renderItem={({ item }) => <Message {...item} />}
|
|
51
|
+
onEndReached={() => fetchNextPage()}
|
|
52
|
+
/>
|
|
53
|
+
<View style={styles.textInputContainer}>
|
|
54
|
+
<TextInput
|
|
55
|
+
aria-disabled={true}
|
|
56
|
+
placeholder="Send a message"
|
|
57
|
+
onChangeText={() => console.log('TODO: Implement sending a message')}
|
|
58
|
+
value=""
|
|
59
|
+
style={styles.textInput}
|
|
60
|
+
/>
|
|
61
|
+
</View>
|
|
62
|
+
</SafeAreaView>
|
|
16
63
|
)
|
|
17
64
|
}
|
|
18
65
|
|
|
66
|
+
function Message(message: MessageResource) {
|
|
67
|
+
const { text } = message
|
|
68
|
+
const styles = useMessageStyles(message)
|
|
69
|
+
|
|
70
|
+
return <Text style={styles.message}>{text}</Text>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const useMessageStyles = ({ mine }: MessageResource) => {
|
|
74
|
+
const { colors } = useTheme()
|
|
75
|
+
|
|
76
|
+
return StyleSheet.create({
|
|
77
|
+
message: {
|
|
78
|
+
alignSelf: mine ? 'flex-end' : 'flex-start',
|
|
79
|
+
backgroundColor: mine ? colors.fillColorNeutral040 : colors.fillColorNeutral050Base,
|
|
80
|
+
borderRadius: 16,
|
|
81
|
+
borderBottomLeftRadius: mine ? 16 : 0,
|
|
82
|
+
borderBottomRightRadius: mine ? 0 : 16,
|
|
83
|
+
padding: 12,
|
|
84
|
+
color: colors.textColorDefaultPrimary,
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
19
89
|
const styles = StyleSheet.create({
|
|
20
90
|
container: {
|
|
21
91
|
flex: 1,
|
|
22
92
|
justifyContent: 'center',
|
|
23
|
-
gap: 8,
|
|
24
93
|
},
|
|
94
|
+
listContainer: {
|
|
95
|
+
gap: 12,
|
|
96
|
+
paddingHorizontal: 16,
|
|
97
|
+
paddingTop: 12,
|
|
98
|
+
},
|
|
99
|
+
textInputContainer: { borderTopWidth: 1, padding: 12 },
|
|
100
|
+
textInput: { borderRadius: 16, borderWidth: 1, padding: 12 },
|
|
25
101
|
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface ResourceObject {
|
|
2
|
+
id: string
|
|
3
|
+
type: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type ApiResource<Type = ResourceObject> = {
|
|
7
|
+
data: Type
|
|
8
|
+
links: Record<string, string>
|
|
9
|
+
meta: Record<string, unknown>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ApiCollection<Type = ResourceObject> = {
|
|
13
|
+
data: Type[]
|
|
14
|
+
links: Record<string, string>
|
|
15
|
+
meta: CollectionMeta
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface CollectionMeta {
|
|
19
|
+
count: number
|
|
20
|
+
totalCount: number
|
|
21
|
+
next?: Record<string, unknown>
|
|
22
|
+
parent?: ResourceObject
|
|
23
|
+
[attributeName: string]: unknown
|
|
24
|
+
}
|