@planningcenter/chat-react-native 2.1.1 → 2.2.0-rc.1

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 (126) hide show
  1. package/build/components/conversation/message_form.d.ts.map +1 -1
  2. package/build/components/conversation/message_form.js +4 -4
  3. package/build/components/conversation/message_form.js.map +1 -1
  4. package/build/components/display/badge.d.ts +2 -9
  5. package/build/components/display/badge.d.ts.map +1 -1
  6. package/build/components/display/badge.js +8 -40
  7. package/build/components/display/badge.js.map +1 -1
  8. package/build/components/display/banner.d.ts +29 -0
  9. package/build/components/display/banner.d.ts.map +1 -0
  10. package/build/components/display/banner.js +16 -0
  11. package/build/components/display/banner.js.map +1 -0
  12. package/build/components/display/button.d.ts +1 -1
  13. package/build/components/display/button.d.ts.map +1 -1
  14. package/build/components/display/button.js +1 -1
  15. package/build/components/display/button.js.map +1 -1
  16. package/build/components/display/icon_button.d.ts +1 -1
  17. package/build/components/display/icon_button.d.ts.map +1 -1
  18. package/build/components/display/icon_button.js +1 -1
  19. package/build/components/display/icon_button.js.map +1 -1
  20. package/build/components/display/index.d.ts +1 -0
  21. package/build/components/display/index.d.ts.map +1 -1
  22. package/build/components/display/index.js +1 -0
  23. package/build/components/display/index.js.map +1 -1
  24. package/build/components/display/text_button.d.ts +1 -1
  25. package/build/components/display/text_button.d.ts.map +1 -1
  26. package/build/components/display/text_button.js +1 -1
  27. package/build/components/display/text_button.js.map +1 -1
  28. package/build/components/display/text_inline_button.d.ts +2 -2
  29. package/build/components/display/text_inline_button.d.ts.map +1 -1
  30. package/build/components/display/text_inline_button.js +3 -2
  31. package/build/components/display/text_inline_button.js.map +1 -1
  32. package/build/components/display/{button_color_utils.d.ts → utils/button_colors.d.ts} +1 -1
  33. package/build/components/display/utils/button_colors.d.ts.map +1 -0
  34. package/build/components/display/{button_color_utils.js → utils/button_colors.js} +2 -2
  35. package/build/components/display/utils/button_colors.js.map +1 -0
  36. package/build/components/display/utils/status_colors.d.ts +17 -0
  37. package/build/components/display/utils/status_colors.d.ts.map +1 -0
  38. package/build/components/display/utils/status_colors.js +49 -0
  39. package/build/components/display/utils/status_colors.js.map +1 -0
  40. package/build/components/primitive/banner_primitive.d.ts +38 -0
  41. package/build/components/primitive/banner_primitive.d.ts.map +1 -0
  42. package/build/components/primitive/banner_primitive.js +112 -0
  43. package/build/components/primitive/banner_primitive.js.map +1 -0
  44. package/build/contexts/api_provider.js +4 -3
  45. package/build/contexts/api_provider.js.map +1 -1
  46. package/build/contexts/chat_context.d.ts +2 -2
  47. package/build/contexts/chat_context.d.ts.map +1 -1
  48. package/build/contexts/chat_context.js +3 -15
  49. package/build/contexts/chat_context.js.map +1 -1
  50. package/build/hooks/use_api_client.d.ts +6 -0
  51. package/build/hooks/use_api_client.d.ts.map +1 -0
  52. package/build/hooks/use_api_client.js +18 -0
  53. package/build/hooks/use_api_client.js.map +1 -0
  54. package/build/hooks/use_conversation.d.ts +122 -0
  55. package/build/hooks/use_conversation.d.ts.map +1 -0
  56. package/build/hooks/use_conversation.js +103 -0
  57. package/build/hooks/use_conversation.js.map +1 -0
  58. package/build/hooks/use_conversation_jolt_events.d.ts.map +1 -1
  59. package/build/hooks/use_conversation_jolt_events.js +3 -4
  60. package/build/hooks/use_conversation_jolt_events.js.map +1 -1
  61. package/build/hooks/use_font_scale.d.ts +1 -1
  62. package/build/hooks/use_font_scale.d.ts.map +1 -1
  63. package/build/hooks/use_font_scale.js +1 -1
  64. package/build/hooks/use_font_scale.js.map +1 -1
  65. package/build/hooks/use_jolt.d.ts +1 -1
  66. package/build/hooks/use_jolt.d.ts.map +1 -1
  67. package/build/hooks/use_jolt.js +6 -6
  68. package/build/hooks/use_jolt.js.map +1 -1
  69. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  70. package/build/hooks/use_suspense_api.js +3 -4
  71. package/build/hooks/use_suspense_api.js.map +1 -1
  72. package/build/navigation/header.d.ts +10 -0
  73. package/build/navigation/header.d.ts.map +1 -0
  74. package/build/navigation/header.js +16 -0
  75. package/build/navigation/header.js.map +1 -0
  76. package/build/navigation/index.d.ts +17 -4
  77. package/build/navigation/index.d.ts.map +1 -1
  78. package/build/navigation/index.js +18 -6
  79. package/build/navigation/index.js.map +1 -1
  80. package/build/screens/conversation_details_screen.d.ts +7 -0
  81. package/build/screens/conversation_details_screen.d.ts.map +1 -0
  82. package/build/screens/conversation_details_screen.js +155 -0
  83. package/build/screens/conversation_details_screen.js.map +1 -0
  84. package/build/screens/conversation_screen.d.ts +5 -3
  85. package/build/screens/conversation_screen.d.ts.map +1 -1
  86. package/build/screens/conversation_screen.js +43 -15
  87. package/build/screens/conversation_screen.js.map +1 -1
  88. package/build/screens/design_system_screen.d.ts.map +1 -1
  89. package/build/screens/design_system_screen.js +24 -1
  90. package/build/screens/design_system_screen.js.map +1 -1
  91. package/build/screens/message_actions_screen.d.ts.map +1 -1
  92. package/build/screens/message_actions_screen.js +7 -7
  93. package/build/screens/message_actions_screen.js.map +1 -1
  94. package/build/utils/client/request_helpers.d.ts +2 -1
  95. package/build/utils/client/request_helpers.d.ts.map +1 -1
  96. package/build/utils/client/request_helpers.js +17 -0
  97. package/build/utils/client/request_helpers.js.map +1 -1
  98. package/package.json +2 -2
  99. package/src/components/conversation/message_form.tsx +4 -4
  100. package/src/components/display/badge.tsx +9 -53
  101. package/src/components/display/banner.tsx +56 -0
  102. package/src/components/display/button.tsx +2 -2
  103. package/src/components/display/icon_button.tsx +2 -2
  104. package/src/components/display/index.ts +1 -0
  105. package/src/components/display/text_button.tsx +2 -2
  106. package/src/components/display/text_inline_button.tsx +4 -2
  107. package/src/components/display/{button_color_utils.ts → utils/button_colors.ts} +1 -1
  108. package/src/components/display/utils/status_colors.ts +85 -0
  109. package/src/components/primitive/banner_primitive.tsx +247 -0
  110. package/src/contexts/api_provider.tsx +5 -5
  111. package/src/contexts/chat_context.tsx +4 -21
  112. package/src/hooks/use_api_client.ts +29 -0
  113. package/src/hooks/use_conversation.ts +113 -0
  114. package/src/hooks/use_conversation_jolt_events.ts +3 -4
  115. package/src/hooks/use_font_scale.ts +3 -1
  116. package/src/hooks/use_jolt.ts +9 -9
  117. package/src/hooks/use_suspense_api.ts +3 -4
  118. package/src/navigation/header.tsx +24 -0
  119. package/src/navigation/index.tsx +20 -6
  120. package/src/screens/conversation_details_screen.tsx +205 -0
  121. package/src/screens/conversation_screen.tsx +65 -20
  122. package/src/screens/design_system_screen.tsx +56 -0
  123. package/src/screens/message_actions_screen.tsx +7 -7
  124. package/src/utils/client/request_helpers.ts +21 -1
  125. package/build/components/display/button_color_utils.d.ts.map +0 -1
  126. package/build/components/display/button_color_utils.js.map +0 -1
@@ -0,0 +1,29 @@
1
+ import { useContext, useMemo } from 'react'
2
+ import { ChatContext } from '../contexts'
3
+ import { Client } from '../utils'
4
+
5
+ type App = 'chat' | 'groups'
6
+ const apps: App[] = ['chat', 'groups']
7
+ export type ApiClient = Record<App, Client>
8
+
9
+ export const useApiClient = () => {
10
+ const { session, onTokenExpired } = useContext(ChatContext)
11
+ const api = useMemo(
12
+ () =>
13
+ apps.reduce(
14
+ (acc, app) => {
15
+ acc[app] = new Client({
16
+ app,
17
+ session,
18
+ version: '2018-11-01',
19
+ onTokenExpired,
20
+ })
21
+ return acc
22
+ },
23
+ {} as Record<App, Client>
24
+ ),
25
+ [session, onTokenExpired]
26
+ )
27
+
28
+ return api
29
+ }
@@ -0,0 +1,113 @@
1
+ import { useMutation } from '@tanstack/react-query'
2
+ import { ApiResource, ConversationResource } from '../types'
3
+ import { getRequestQueryKey, useSuspenseGet } from './use_suspense_api'
4
+ import { queryClient } from '../contexts'
5
+ import { useApiClient } from './use_api_client'
6
+ import { transformGetToPost } from '../utils/client/request_helpers'
7
+ import { useState } from 'react'
8
+
9
+ export const getConversationRequestArgs = ({ conversation_id }: { conversation_id: string }) => ({
10
+ url: `/me/conversations/${conversation_id}`,
11
+ data: {
12
+ fields: {
13
+ Conversation: [
14
+ 'created_at',
15
+ 'badges',
16
+ 'groups',
17
+ 'last_message_author_id',
18
+ 'last_message_author_name',
19
+ 'last_message_created_at',
20
+ 'last_message_text_preview',
21
+ 'preview_avatar_urls',
22
+ 'member_ability',
23
+ 'muted',
24
+ 'replies_disabled',
25
+ 'title',
26
+ 'unread_count',
27
+ 'updated_at',
28
+ ],
29
+ MemberAbility: ['can_update', 'can_delete'],
30
+ ConversationBadge: ['app_name', 'pco_resource_type', 'text'],
31
+ },
32
+ include: ['badges', 'member_ability'],
33
+ },
34
+ })
35
+
36
+ export const useConversation = ({ conversation_id }) => {
37
+ return useSuspenseGet<ConversationResource>(getConversationRequestArgs({ conversation_id }))
38
+ }
39
+
40
+ export const useConversationMute = ({ conversation_id }: { conversation_id: string }) => {
41
+ const apiClient = useApiClient()
42
+ const requestArgs = getConversationRequestArgs({ conversation_id })
43
+ const queryKey = getRequestQueryKey(requestArgs)
44
+ const { data: conversation } = useConversation({ conversation_id })
45
+ const [value, setValue] = useState<boolean>(conversation.muted)
46
+
47
+ const { mutate: setMuted, ...mutation } = useMutation({
48
+ onMutate: async (muted: boolean) => {
49
+ return queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, prev => {
50
+ if (!prev?.data) return prev
51
+ setValue(muted)
52
+ prev.data.muted = muted
53
+
54
+ return prev
55
+ })
56
+ },
57
+ mutationKey: ['muteConversation'],
58
+ mutationFn: async (muted: boolean) => {
59
+ const action = muted ? 'mute' : 'unmute'
60
+
61
+ return apiClient.chat.post({
62
+ url: `/me/conversations/${conversation_id}/${action}`,
63
+ data: { data: { type: '', attributes: {} }, fields: { Conversation: 'muted' } },
64
+ })
65
+ },
66
+ onSuccess: (response: ApiResource<ConversationResource>) => {
67
+ queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, prev => {
68
+ if (!prev?.data) return prev
69
+
70
+ // Posting to the mute action endpoint can't return all the fields
71
+ // so we need to set only the fields we require
72
+ prev.data.muted = response.data.muted
73
+ setValue(response.data.muted)
74
+
75
+ return prev
76
+ })
77
+ },
78
+ })
79
+
80
+ return { muted: value, setMuted, ...mutation }
81
+ }
82
+
83
+ export const useConversationUpdate = ({ conversation_id }: { conversation_id: string }) => {
84
+ const apiClient = useApiClient()
85
+ const requestArgs = getConversationRequestArgs({ conversation_id })
86
+ const queryKey = getRequestQueryKey(requestArgs)
87
+
88
+ return useMutation({
89
+ onMutate: async (update: Partial<ConversationResource>) => {
90
+ queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, prev => {
91
+ if (prev?.data) {
92
+ prev.data = {
93
+ ...prev.data,
94
+ ...update,
95
+ }
96
+ }
97
+
98
+ return prev
99
+ })
100
+ },
101
+ mutationKey: ['mutateConversation'],
102
+ mutationFn: async (update: Partial<ConversationResource>) => {
103
+ const postArgs = transformGetToPost(requestArgs).data
104
+ return apiClient.chat.patch({
105
+ url: `/me/conversations/${conversation_id}/`,
106
+ data: { data: { type: '', attributes: update }, ...postArgs },
107
+ })
108
+ },
109
+ onSuccess: (response: ApiResource<ConversationResource>) => {
110
+ queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, () => response)
111
+ },
112
+ })
113
+ }
@@ -1,9 +1,8 @@
1
1
  import { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'
2
2
  import { InfiniteData, useQueryClient } from '@tanstack/react-query'
3
- import { useContext } from 'react'
4
- import { ChatContext } from '../contexts'
5
3
  import { ApiCollection, ApiResource, ConversationResource } from '../types'
6
4
  import { deleteRecordInPagesData, updateRecordInPagesData } from '../utils'
5
+ import { useApiClient } from './use_api_client'
7
6
  import { getConversationsRequestArgs } from './use_conversations'
8
7
  import { useCurrentPerson } from './use_current_person'
9
8
  import { useJoltChannel, useJoltEvent } from './use_jolt'
@@ -17,7 +16,7 @@ interface JoltConversationsEvent extends CustomMessage {
17
16
  }
18
17
 
19
18
  export function useConversationsJoltEvents() {
20
- const { client } = useContext(ChatContext)
19
+ const apiClient = useApiClient()
21
20
  const queryClient = useQueryClient()
22
21
  const currentPerson = useCurrentPerson()
23
22
  const joltChannel = useJoltChannel(`chat.people.${currentPerson.id}`)
@@ -27,7 +26,7 @@ export function useConversationsJoltEvents() {
27
26
 
28
27
  const fetchConversation = async ({ id }: ConversationResource) => {
29
28
  const { data: argsData } = conversationsRequestArgs
30
- const { data } = await client.get<ApiResource<ConversationResource>>({
29
+ const { data } = await apiClient.chat.get<ApiResource<ConversationResource>>({
31
30
  url: `/me/conversations/${id}`,
32
31
  data: {
33
32
  fields: argsData.fields,
@@ -1,6 +1,8 @@
1
1
  import { useWindowDimensions } from 'react-native'
2
2
 
3
- export const useFontScale = ({ maxFontSizeMultiplier }: { maxFontSizeMultiplier?: number }) => {
3
+ export const useFontScale = ({
4
+ maxFontSizeMultiplier,
5
+ }: { maxFontSizeMultiplier?: number } = {}) => {
4
6
  const { fontScale: nativeFontScale } = useWindowDimensions()
5
7
  const scaleLimit = maxFontSizeMultiplier || nativeFontScale
6
8
  const fontScale = Math.min(scaleLimit, nativeFontScale)
@@ -1,13 +1,13 @@
1
1
  import JoltClient from '@planningcenter/jolt-client'
2
- import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
3
- import { useContext, useEffect, useState } from 'react'
4
- import { ChatContext } from '../contexts'
5
- import { ApiResource } from '../types'
2
+ import { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'
6
3
  import {
7
4
  FetchSubscribeToken,
8
5
  JoltSubscription,
9
6
  } from '@planningcenter/jolt-client/dist/types/JoltSubscription'
10
- import { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'
7
+ import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
8
+ import { useEffect, useState } from 'react'
9
+ import { ApiResource } from '../types'
10
+ import { useApiClient } from './use_api_client'
11
11
 
12
12
  interface JoltResponse {
13
13
  type: 'JoltToken'
@@ -16,12 +16,12 @@ interface JoltResponse {
16
16
  }
17
17
 
18
18
  export const useJoltClient = (): JoltClient | undefined => {
19
- const { client } = useContext(ChatContext)
19
+ const apiClient = useApiClient()
20
20
  const { data: joltToken } = useSuspenseQuery<ApiResource<JoltResponse>>({
21
21
  refetchOnMount: false,
22
22
  queryKey: ['jolt-token'],
23
23
  queryFn: () => {
24
- return client.post({
24
+ return apiClient.chat.post({
25
25
  url: '/me/jolt_authorize',
26
26
  data: {
27
27
  data: {
@@ -38,7 +38,7 @@ export const useJoltClient = (): JoltClient | undefined => {
38
38
  }
39
39
 
40
40
  const fetchSubscribeTokenFn: FetchSubscribeToken = (channel: string, connectionId: string) => {
41
- return client
41
+ return apiClient.chat
42
42
  .post({
43
43
  url: '/me/jolt_subscribe',
44
44
  data: {
@@ -66,7 +66,7 @@ export const useJoltClient = (): JoltClient | undefined => {
66
66
  fetchAuthTokenFn,
67
67
  fetchSubscribeTokenFn,
68
68
  },
69
- { logToConsole: true }
69
+ { logToConsole: false }
70
70
  )
71
71
  },
72
72
  })
@@ -4,10 +4,9 @@ import {
4
4
  useSuspenseInfiniteQuery,
5
5
  useSuspenseQuery,
6
6
  } from '@tanstack/react-query'
7
- import { useContext } from 'react'
8
- import { ChatContext } from '../contexts'
9
7
  import { ApiCollection, ApiResource, ResourceObject } from '../types'
10
8
  import { GetRequest, RequestData } from '../utils/client/types'
9
+ import { useApiClient } from './use_api_client'
11
10
 
12
11
  export const useSuspenseGet = <T extends ResourceObject | ResourceObject[]>(args: GetRequest) => {
13
12
  type Resource = ApiResource<T>
@@ -33,7 +32,7 @@ export const useSuspensePaginator = <T extends ResourceObject>(
33
32
  args: GetRequest,
34
33
  opts?: SuspensePaginatorOptions
35
34
  ) => {
36
- const { client } = useContext(ChatContext)
35
+ const apiClient = useApiClient()
37
36
  const query = useSuspenseInfiniteQuery<
38
37
  ApiCollection<T>,
39
38
  Response,
@@ -50,7 +49,7 @@ export const useSuspensePaginator = <T extends ResourceObject>(
50
49
  const offset = pageParam?.offset || args.data.offset
51
50
  const data = { ...args.data, where, offset }
52
51
 
53
- return client.get({
52
+ return apiClient.chat.get({
54
53
  url: args.url,
55
54
  data,
56
55
  })
@@ -0,0 +1,24 @@
1
+ import React from 'react'
2
+ import { HeaderButton, HeaderButtonProps, Text } from '@react-navigation/elements'
3
+ import { StyleSheet, TextProps, TextStyle } from 'react-native'
4
+
5
+ type HeaderRightButtonProps = HeaderButtonProps & {
6
+ children: TextProps['children']
7
+ textStyle?: TextStyle
8
+ }
9
+
10
+ export const HeaderRightButton = (props: HeaderRightButtonProps) => {
11
+ const styles = StyleSheet.create({
12
+ text: {
13
+ fontSize: 16,
14
+ },
15
+ })
16
+
17
+ return (
18
+ <HeaderButton {...props} style={props.style}>
19
+ <Text style={[styles.text, props.textStyle]} numberOfLines={1}>
20
+ {props.children}
21
+ </Text>
22
+ </HeaderButton>
23
+ )
24
+ }
@@ -1,21 +1,23 @@
1
- import React from 'react'
1
+ import { HeaderBackButton, HeaderButton } from '@react-navigation/elements'
2
2
  import { StaticParamList } from '@react-navigation/native'
3
3
  import {
4
4
  createNativeStackNavigator,
5
5
  NativeStackHeaderLeftProps,
6
6
  NativeStackHeaderRightProps,
7
7
  } from '@react-navigation/native-stack'
8
- import { NotFound } from '../screens/not_found'
9
- import { ScreenLayout } from './screenLayout'
10
- import { ConversationsScreen } from '../screens/conversations_screen'
11
- import { ConversationScreen } from '../screens/conversation_screen'
12
- import { HeaderBackButton, HeaderButton } from '@react-navigation/elements'
8
+ import React from 'react'
13
9
  import { Icon } from '../components'
10
+ import { ConversationDetailsScreen } from '../screens/conversation_details_screen'
11
+ import { ConversationScreen } from '../screens/conversation_screen'
12
+ import { ConversationsScreen } from '../screens/conversations_screen'
14
13
  import {
15
14
  MessageActionsScreen,
16
15
  MessageActionsScreenOptions,
17
16
  } from '../screens/message_actions_screen'
17
+ import { NotFound } from '../screens/not_found'
18
18
  import { ReactionsScreen, ReactionsScreenOptions } from '../screens/reactions_screen'
19
+ import { HeaderRightButton } from './header'
20
+ import { ScreenLayout } from './screenLayout'
19
21
 
20
22
  export const ChatStack = createNativeStackNavigator({
21
23
  screenOptions: {
@@ -44,6 +46,18 @@ export const ChatStack = createNativeStackNavigator({
44
46
  Conversation: {
45
47
  screen: ConversationScreen,
46
48
  },
49
+ ConversationDetails: {
50
+ screen: ConversationDetailsScreen,
51
+ options: ({ navigation }) => ({
52
+ presentation: 'modal',
53
+ title: 'Conversation details',
54
+ headerRight: (props: NativeStackHeaderRightProps) => (
55
+ <HeaderRightButton {...props} onPress={navigation.goBack}>
56
+ Done
57
+ </HeaderRightButton>
58
+ ),
59
+ }),
60
+ },
47
61
  MessageActions: {
48
62
  screen: MessageActionsScreen,
49
63
  // Something about sheetAllowedDetents declared inline breaks TS
@@ -0,0 +1,205 @@
1
+ import {
2
+ StaticScreenProps,
3
+ useNavigation,
4
+ useTheme as useNavigationTheme,
5
+ } from '@react-navigation/native'
6
+ import React, { PropsWithChildren, useCallback, useEffect, useState } from 'react'
7
+ import { FlatList, StyleSheet, TextInput, View } from 'react-native'
8
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
9
+ import { Avatar, Badge, Heading, Icon, Switch, Text } from '../components'
10
+ import { useSuspenseGet, useTheme } from '../hooks'
11
+ import {
12
+ useConversation,
13
+ useConversationMute,
14
+ useConversationUpdate,
15
+ } from '../hooks/use_conversation'
16
+ import { MemberResource } from '../types'
17
+ import { HeaderRightButton } from '../navigation/header'
18
+
19
+ export type ConversationDetailsScreenProps = StaticScreenProps<{
20
+ conversation_id: string
21
+ }>
22
+
23
+ export function ConversationDetailsScreen({ route }: ConversationDetailsScreenProps) {
24
+ const styles = useStyles()
25
+ const navigation = useNavigation()
26
+ const { data: conversation } = useConversation(route.params)
27
+ const canUpdate = conversation.memberAbility?.canUpdate
28
+
29
+ const { data: members } = useSuspenseGet<MemberResource[]>({
30
+ url: `/me/conversations/${route.params.conversation_id}/members`,
31
+ data: {
32
+ include: ['person'],
33
+ fields: {
34
+ Member: ['avatar', 'name', 'first_name', 'last_name', 'child', 'badges'],
35
+ },
36
+ },
37
+ })
38
+
39
+ const { muted, setMuted } = useConversationMute(route.params)
40
+ const { mutate: saveTitle } = useConversationUpdate(route.params)
41
+ const [title, setTitle] = useState(conversation.title)
42
+ const HeaderRight = useCallback(() => {
43
+ return (
44
+ <HeaderRightButton
45
+ onPress={() => {
46
+ saveTitle({ title })
47
+ navigation.goBack()
48
+ }}
49
+ >
50
+ Done
51
+ </HeaderRightButton>
52
+ )
53
+ }, [navigation, saveTitle, title])
54
+
55
+ useEffect(() => {
56
+ navigation.setOptions({
57
+ headerRight: HeaderRight,
58
+ })
59
+ }, [HeaderRight, navigation])
60
+
61
+ return (
62
+ <View style={styles.container}>
63
+ <SectionList>
64
+ <View style={styles.titleContainer}>
65
+ <View style={[styles.titleLabelContainer, !canUpdate && styles.titleInputDisabled]}>
66
+ <Text variant="tertiary">Title</Text>
67
+ {!canUpdate && <Icon name="general.lock" />}
68
+ </View>
69
+ <TextInput
70
+ editable={canUpdate}
71
+ onChangeText={setTitle}
72
+ style={[styles.titleInput, !canUpdate && styles.titleInputDisabled]}
73
+ value={title}
74
+ />
75
+ </View>
76
+ </SectionList>
77
+ <SectionList>
78
+ <SectionListHeader>Settings</SectionListHeader>
79
+ <View style={styles.muteContainer}>
80
+ <Text variant="plain" style={styles.muteText}>
81
+ Mute
82
+ </Text>
83
+ <Switch value={muted} onChange={() => setMuted(!muted)} />
84
+ </View>
85
+ </SectionList>
86
+ <SectionList>
87
+ <SectionListHeader divider={false}>Members</SectionListHeader>
88
+ <FlatList
89
+ data={members}
90
+ renderItem={({ item }) => (
91
+ <View style={styles.member}>
92
+ <Avatar sourceUri={item.avatar} />
93
+ <View style={styles.memberBody}>
94
+ <Text style={styles.memberName}>{item.name}</Text>
95
+ <View style={styles.memberBadges}>
96
+ {item.badges?.map((badge, index) => (
97
+ <View key={index} style={styles.memberBadge}>
98
+ <Badge label={badge.title} />
99
+ </View>
100
+ ))}
101
+ </View>
102
+ </View>
103
+ </View>
104
+ )}
105
+ keyExtractor={item => item.id}
106
+ contentContainerStyle={styles.listContainer}
107
+ />
108
+ </SectionList>
109
+ </View>
110
+ )
111
+ }
112
+
113
+ const useStyles = () => {
114
+ const theme = useTheme()
115
+ const { bottom } = useSafeAreaInsets()
116
+
117
+ return StyleSheet.create({
118
+ container: {
119
+ flex: 1,
120
+ paddingTop: 16,
121
+ paddingHorizontal: 16,
122
+ backgroundColor: theme.colors.fillColorNeutral080,
123
+ paddingBottom: bottom,
124
+ gap: 16,
125
+ },
126
+ listContainer: {
127
+ gap: 12,
128
+ },
129
+ member: {
130
+ flexDirection: 'row',
131
+ gap: 8,
132
+ },
133
+ memberBody: {
134
+ gap: 4,
135
+ },
136
+ memberName: {
137
+ lineHeight: 16,
138
+ },
139
+ memberBadges: {
140
+ flexDirection: 'row',
141
+ gap: 8,
142
+ },
143
+ memberBadge: {},
144
+ muteContainer: {
145
+ flexDirection: 'row',
146
+ justifyContent: 'space-between',
147
+ alignItems: 'center',
148
+ },
149
+ muteText: {
150
+ lineHeight: 20,
151
+ },
152
+ titleContainer: {},
153
+ titleLabel: {},
154
+ titleLabelContainer: {
155
+ flexDirection: 'row',
156
+ alignItems: 'center',
157
+ gap: 8,
158
+ },
159
+ titleInput: {
160
+ color: theme.colors.textColorDefaultPrimary,
161
+ },
162
+ titleInputDisabled: { opacity: 0.7 },
163
+ })
164
+ }
165
+
166
+ const SectionList = ({ children }: PropsWithChildren) => {
167
+ const styles = useSectionListStyles()
168
+
169
+ return <View style={styles.container}>{children}</View>
170
+ }
171
+
172
+ const SectionListHeader = ({
173
+ children,
174
+ divider = true,
175
+ }: PropsWithChildren & { divider?: boolean }) => {
176
+ const styles = useSectionListStyles()
177
+
178
+ return (
179
+ <Heading variant="h3" style={[styles.heading, divider && styles.headingDivider]}>
180
+ {children}
181
+ </Heading>
182
+ )
183
+ }
184
+
185
+ const useSectionListStyles = () => {
186
+ const theme = useTheme()
187
+ const navigationTheme = useNavigationTheme()
188
+
189
+ return StyleSheet.create({
190
+ container: {
191
+ padding: 16,
192
+ backgroundColor: navigationTheme.colors.card,
193
+ borderRadius: 8,
194
+ flexDirection: 'column',
195
+ gap: 8,
196
+ },
197
+ heading: {
198
+ paddingBottom: 12,
199
+ },
200
+ headingDivider: {
201
+ borderBottomWidth: 1,
202
+ borderBottomColor: theme.colors.fillColorNeutral050Base,
203
+ },
204
+ })
205
+ }
@@ -1,44 +1,46 @@
1
+ import { HeaderTitle, HeaderTitleProps, PlatformPressable } from '@react-navigation/elements'
1
2
  import {
2
3
  StaticScreenProps,
3
4
  useNavigation,
4
5
  useTheme as useNavigationTheme,
6
+ useRoute,
5
7
  } from '@react-navigation/native'
6
- import React, { useEffect } from 'react'
7
- import { FlatList, StyleSheet, View } from 'react-native'
8
+ import React, { useCallback, useEffect } from 'react'
9
+ import { FlatList, Platform, StyleSheet, View } from 'react-native'
8
10
  import { useSafeAreaInsets } from 'react-native-safe-area-context'
11
+ import { Icon, Text } from '../components'
9
12
  import { Message } from '../components/conversation/message'
10
13
  import { MessageForm } from '../components/conversation/message_form'
11
14
  import { KeyboardView } from '../components/display/keyboard_view'
15
+ import { useConversation } from '../hooks/use_conversation'
12
16
  import { useConversationMessages } from '../hooks/use_conversation_messages'
13
- import { useSuspenseGet } from '../hooks/use_suspense_api'
14
- import { ConversationResource } from '../types'
15
17
 
16
- export type ConversationScreenProps = StaticScreenProps<{
18
+ type ConversationRouteProps = {
17
19
  conversation_id: string
18
- chat_group_graph_id: string
19
- }>
20
+ chat_group_graph_id?: string
21
+ }
22
+
23
+ export type ConversationScreenProps = StaticScreenProps<ConversationRouteProps>
20
24
 
21
25
  export function ConversationScreen({ route }: ConversationScreenProps) {
22
- const navigation = useNavigation()
23
- const { conversation_id } = route.params
24
26
  const styles = useStyles()
27
+ const navigation = useNavigation()
25
28
 
26
- const { data: conversation } = useSuspenseGet<ConversationResource>({
27
- url: `/me/conversations/${conversation_id}`,
28
- data: {
29
- fields: {
30
- Conversation: ['title'],
31
- },
32
- },
33
- })
34
-
29
+ const { conversation_id } = route.params
30
+ const { data: conversation } = useConversation(route.params)
35
31
  const { messages, refetch, isRefetching, fetchNextPage } = useConversationMessages({
36
32
  conversation_id,
37
33
  })
38
34
 
35
+ // Seems to be necessary to define this way so we get the route picked up
36
+ const headerTitle = useCallback(
37
+ (props: HeaderTitleProps) => <PressableHeaderTitle {...props} />,
38
+ []
39
+ )
40
+
39
41
  useEffect(() => {
40
- navigation.setOptions({ title: conversation?.title })
41
- }, [conversation, conversation_id, navigation])
42
+ navigation.setOptions({ headerTitle, title: conversation?.title })
43
+ }, [conversation, conversation_id, navigation, headerTitle, conversation?.title])
42
44
 
43
45
  return (
44
46
  <View style={styles.container}>
@@ -64,6 +66,49 @@ export function ConversationScreen({ route }: ConversationScreenProps) {
64
66
  )
65
67
  }
66
68
 
69
+ const PressableHeaderTitle = ({ style, children }: HeaderTitleProps) => {
70
+ const styles = usePressableHeaderStyle()
71
+ const navigation = useNavigation()
72
+
73
+ const route = useRoute() as ConversationScreenProps['route']
74
+
75
+ const { data: conversation } = useConversation(route.params)
76
+ const badge = conversation.badges?.[0]
77
+ const subtitle = [badge?.pcoResourceType, badge?.text].filter(f => f).join(': ')
78
+
79
+ return (
80
+ <PlatformPressable
81
+ style={styles.container}
82
+ onPress={() =>
83
+ navigation.navigate('ConversationDetails', { conversation_id: conversation?.id })
84
+ }
85
+ >
86
+ <View style={styles.titleWrapper}>
87
+ <HeaderTitle style={[styles.title, style]}>{children}</HeaderTitle>
88
+ <Icon name="general.downChevron" size={12} />
89
+ </View>
90
+ <Text variant="tertiary">{subtitle}</Text>
91
+ </PlatformPressable>
92
+ )
93
+ }
94
+
95
+ const usePressableHeaderStyle = () => {
96
+ return StyleSheet.create({
97
+ container: {
98
+ alignItems: Platform.select({ android: 'flex-start', default: 'center' }),
99
+ },
100
+ titleWrapper: {
101
+ alignItems: 'center',
102
+ columnGap: 8,
103
+ flexDirection: 'row',
104
+ },
105
+ title: {
106
+ padding: 0,
107
+ margin: 0,
108
+ },
109
+ })
110
+ }
111
+
67
112
  const useStyles = () => {
68
113
  const navigationTheme = useNavigationTheme()
69
114
  const { bottom } = useSafeAreaInsets()