@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.
Files changed (112) hide show
  1. package/build/components/conversations.d.ts.map +1 -1
  2. package/build/components/conversations.js +29 -8
  3. package/build/components/conversations.js.map +1 -1
  4. package/build/components/index.d.ts +1 -0
  5. package/build/components/index.d.ts.map +1 -1
  6. package/build/components/index.js +1 -0
  7. package/build/components/index.js.map +1 -1
  8. package/build/contexts/api_provider.d.ts +4 -6
  9. package/build/contexts/api_provider.d.ts.map +1 -1
  10. package/build/contexts/api_provider.js +12 -20
  11. package/build/contexts/api_provider.js.map +1 -1
  12. package/build/contexts/chat_context.d.ts +7 -5
  13. package/build/contexts/chat_context.d.ts.map +1 -1
  14. package/build/contexts/chat_context.js +40 -4
  15. package/build/contexts/chat_context.js.map +1 -1
  16. package/build/hooks/index.d.ts +2 -0
  17. package/build/hooks/index.d.ts.map +1 -1
  18. package/build/hooks/index.js +2 -0
  19. package/build/hooks/index.js.map +1 -1
  20. package/build/hooks/use_api.d.ts +61 -0
  21. package/build/hooks/use_api.d.ts.map +1 -0
  22. package/build/hooks/use_api.js +39 -0
  23. package/build/hooks/use_api.js.map +1 -0
  24. package/build/hooks/use_current_person.d.ts +3 -0
  25. package/build/hooks/use_current_person.d.ts.map +1 -0
  26. package/build/hooks/use_current_person.js +13 -0
  27. package/build/hooks/use_current_person.js.map +1 -0
  28. package/build/navigation/index.d.ts +1 -0
  29. package/build/navigation/index.d.ts.map +1 -1
  30. package/build/navigation/index.js +7 -4
  31. package/build/navigation/index.js.map +1 -1
  32. package/build/screens/conversation_screen.d.ts.map +1 -1
  33. package/build/screens/conversation_screen.js +59 -6
  34. package/build/screens/conversation_screen.js.map +1 -1
  35. package/build/screens/not_found.js +1 -1
  36. package/build/screens/not_found.js.map +1 -1
  37. package/build/types/api_primitives.d.ts +23 -0
  38. package/build/types/api_primitives.d.ts.map +1 -0
  39. package/build/types/api_primitives.js +2 -0
  40. package/build/types/api_primitives.js.map +1 -0
  41. package/build/types/index.d.ts +4 -0
  42. package/build/types/index.d.ts.map +1 -0
  43. package/build/types/index.js +4 -0
  44. package/build/types/index.js.map +1 -0
  45. package/build/types/resources/conversation.d.ts +15 -0
  46. package/build/types/resources/conversation.d.ts.map +1 -0
  47. package/build/types/resources/conversation.js +2 -0
  48. package/build/types/resources/conversation.js.map +1 -0
  49. package/build/types/resources/index.d.ts +5 -0
  50. package/build/types/resources/index.d.ts.map +1 -0
  51. package/build/types/resources/index.js +5 -0
  52. package/build/types/resources/index.js.map +1 -0
  53. package/build/types/resources/message.d.ts +16 -0
  54. package/build/types/resources/message.d.ts.map +1 -0
  55. package/build/types/resources/message.js +2 -0
  56. package/build/types/resources/message.js.map +1 -0
  57. package/build/types/resources/oauth_token.d.ts +9 -0
  58. package/build/types/resources/oauth_token.d.ts.map +1 -0
  59. package/build/types/resources/oauth_token.js +2 -0
  60. package/build/types/resources/oauth_token.js.map +1 -0
  61. package/build/types/resources/person.d.ts +9 -0
  62. package/build/types/resources/person.d.ts.map +1 -0
  63. package/build/types/resources/person.js +2 -0
  64. package/build/types/resources/person.js.map +1 -0
  65. package/build/types/resources/reaction.d.ts +10 -0
  66. package/build/types/resources/reaction.d.ts.map +1 -0
  67. package/build/types/resources/reaction.js +2 -0
  68. package/build/types/resources/reaction.js.map +1 -0
  69. package/build/types/utils/index.d.ts +4 -0
  70. package/build/types/utils/index.d.ts.map +1 -0
  71. package/build/types/utils/index.js +4 -0
  72. package/build/types/utils/index.js.map +1 -0
  73. package/build/utils/client/client.d.ts +21 -12
  74. package/build/utils/client/client.d.ts.map +1 -1
  75. package/build/utils/client/client.js +24 -22
  76. package/build/utils/client/client.js.map +1 -1
  77. package/build/utils/session.d.ts +0 -5
  78. package/build/utils/session.d.ts.map +1 -1
  79. package/build/utils/session.js +0 -10
  80. package/build/utils/session.js.map +1 -1
  81. package/package.json +2 -3
  82. package/src/__tests__/client.ts +72 -19
  83. package/src/__tests__/session.ts +0 -11
  84. package/src/__utils__/handlers.ts +1 -1
  85. package/src/components/conversations.tsx +33 -11
  86. package/src/components/index.tsx +1 -0
  87. package/src/contexts/api_provider.tsx +17 -26
  88. package/src/contexts/chat_context.tsx +61 -7
  89. package/src/hooks/index.ts +2 -0
  90. package/src/hooks/use_api.ts +60 -0
  91. package/src/hooks/use_current_person.ts +15 -0
  92. package/src/navigation/index.tsx +14 -4
  93. package/src/screens/conversation_screen.tsx +83 -7
  94. package/src/screens/not_found.tsx +1 -1
  95. package/src/types/api_primitives.ts +24 -0
  96. package/src/types/index.ts +3 -0
  97. package/src/types/resources/conversation.ts +15 -0
  98. package/src/types/resources/index.ts +4 -0
  99. package/src/types/resources/message.ts +18 -0
  100. package/src/types/resources/oauth_token.ts +8 -0
  101. package/src/types/resources/person.ts +9 -0
  102. package/src/types/resources/reaction.ts +9 -0
  103. package/src/types/utils/index.ts +6 -0
  104. package/src/utils/client/client.ts +41 -34
  105. package/src/utils/client/types.d.ts +2 -0
  106. package/src/utils/session.ts +0 -13
  107. package/build/utils/api.d.ts +0 -9
  108. package/build/utils/api.d.ts.map +0 -1
  109. package/build/utils/api.js +0 -36
  110. package/build/utils/api.js.map +0 -1
  111. package/src/types.d.ts +0 -35
  112. 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": "66c448543e4702f8f692332f1abef17017994d1e"
47
+ "gitHead": "e15259014cecb56e087edf1f905dce3c059fcfde"
49
48
  }
@@ -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 = new Session({ env: 'development' })
10
-
11
- const client = new Client({
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
+ }
@@ -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,6 +1,6 @@
1
1
  import { http, HttpResponse } from 'msw'
2
2
  import DefaultFixtures from './fixtures/defaults'
3
- import { JSONAPIResponse } from '@planningcenter/chat-core'
3
+ import { JSONAPIResponse } from '../types'
4
4
 
5
5
  export const BASE_URL = 'http://api.pco.test/chat/v2'
6
6
 
@@ -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 { Text } from './display'
8
-
9
- type ConversationsResponse = JSONAPICollection<ConversationResource>
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 { data: conversations } = useSuspenseQuery<ConversationsResponse>({
14
- queryKey: ['/chat/v2/me/conversations'],
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={conversations?.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
- <Text style={styles.listItem}>{item.attributes.title}</Text>
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
  }
@@ -1,2 +1,3 @@
1
1
  export * from './conversations'
2
2
  export * from './error_boundary'
3
+ export * from './display'
@@ -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 { OAuthToken } from '../types'
6
- import apiRequest from '../utils/api'
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 handleTokenExpired: () => void
7
+ let apiClient: Client | undefined
11
8
 
12
9
  const defaultQueryFn = ({ queryKey }: { queryKey: QueryKey }) => {
13
- if (!session.token) {
10
+ if (!apiClient) {
14
11
  throw new Error('No token present')
15
12
  }
16
13
 
17
- const uri = new Uri({ session })
18
- const url = uri.api(queryKey[0])
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
- env = 'production',
41
- token,
42
- onTokenExpired,
43
- }: ViewProps & { env?: ENV; token?: OAuthToken; onTokenExpired: () => void }) {
44
- session.env = env
45
- session.token = token
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
- }, [env, token])
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 ContextValue = {
10
+ type ChatContextValue = {
10
11
  token?: OAuthToken
11
12
  onTokenExpired: () => void
12
13
  theme: any
13
- env?: 'production' | 'staging' | 'development'
14
+ env?: ENV
15
+ client: Client
14
16
  }
15
17
 
16
- export const ChatContext = createContext<ContextValue>({
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({ children, value }: { children: any; value: ContextValue }) {
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={{ ...value, theme }}>
30
- <ApiProvider env={value.env} token={value.token} onTokenExpired={value.onTokenExpired}>
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
@@ -1,2 +1,4 @@
1
1
  export * from './use_async_storage'
2
2
  export * from './use_theme'
3
+ export * from './use_api'
4
+ export * from './use_current_person'
@@ -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
+ }
@@ -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 ?? 'Conversations',
17
- // This goes back on the parent so it doesn't automatically show up on the first screen
18
- headerLeft: () => <HeaderBackButton onPress={navigation.goBack} />,
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
- <View style={styles.container}>
14
- <Text>{JSON.stringify(route.params, null, 2)}</Text>
15
- </View>
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
  })
@@ -6,7 +6,7 @@ export function NotFound() {
6
6
  return (
7
7
  <View style={styles.container}>
8
8
  <Text>404</Text>
9
- <Button screen="Conversations">Go to Home</Button>
9
+ <Button href="Conversations">Go to Home</Button>
10
10
  </View>
11
11
  )
12
12
  }
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ export * from './api_primitives'
2
+ export * from './resources'
3
+ export * from './utils'