@planningcenter/chat-react-native 2.0.0 → 2.1.0-rc.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/conversation/message.d.ts.map +1 -1
- package/build/components/conversation/message.js +7 -2
- package/build/components/conversation/message.js.map +1 -1
- package/build/components/conversation/message_reaction.d.ts +1 -1
- package/build/components/conversation/message_reaction.d.ts.map +1 -1
- package/build/components/conversation/message_reaction.js +1 -1
- package/build/components/conversation/message_reaction.js.map +1 -1
- package/build/components/conversations.d.ts.map +1 -1
- package/build/components/conversations.js +76 -30
- package/build/components/conversations.js.map +1 -1
- package/build/components/display/badge.d.ts +2 -6
- package/build/components/display/badge.d.ts.map +1 -1
- package/build/components/display/badge.js +1 -5
- package/build/components/display/badge.js.map +1 -1
- package/build/components/display/tabs.d.ts +17 -0
- package/build/components/display/tabs.d.ts.map +1 -0
- package/build/components/display/tabs.js +97 -0
- package/build/components/display/tabs.js.map +1 -0
- package/build/contexts/api_provider.js +2 -2
- package/build/contexts/api_provider.js.map +1 -1
- package/build/hooks/use_conversation_jolt_events.d.ts +2 -0
- package/build/hooks/use_conversation_jolt_events.d.ts.map +1 -0
- package/build/hooks/use_conversation_jolt_events.js +47 -0
- package/build/hooks/use_conversation_jolt_events.js.map +1 -0
- package/build/hooks/use_conversation_messages.d.ts +2 -18
- package/build/hooks/use_conversation_messages.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages.js +2 -2
- package/build/hooks/use_conversation_messages.js.map +1 -1
- package/build/hooks/use_conversations.d.ts +37 -0
- package/build/hooks/use_conversations.d.ts.map +1 -0
- package/build/hooks/use_conversations.js +48 -0
- package/build/hooks/use_conversations.js.map +1 -0
- package/build/hooks/use_jolt.d.ts +9 -0
- package/build/hooks/use_jolt.d.ts.map +1 -0
- package/build/hooks/use_jolt.js +71 -0
- package/build/hooks/use_jolt.js.map +1 -0
- package/build/hooks/use_suspense_api.d.ts +7 -2
- package/build/hooks/use_suspense_api.d.ts.map +1 -1
- package/build/hooks/use_suspense_api.js +7 -2
- package/build/hooks/use_suspense_api.js.map +1 -1
- package/build/navigation/index.d.ts +5 -0
- package/build/navigation/index.d.ts.map +1 -1
- package/build/navigation/index.js +7 -2
- package/build/navigation/index.js.map +1 -1
- package/build/screens/message_actions_screen.d.ts +1 -1
- package/build/screens/message_actions_screen.d.ts.map +1 -1
- package/build/screens/message_actions_screen.js +1 -1
- package/build/screens/message_actions_screen.js.map +1 -1
- package/build/screens/reactions_screen.d.ts +11 -0
- package/build/screens/reactions_screen.d.ts.map +1 -0
- package/build/screens/reactions_screen.js +83 -0
- package/build/screens/reactions_screen.js.map +1 -0
- package/build/types/resources/app_name.d.ts +2 -0
- package/build/types/resources/app_name.d.ts.map +1 -0
- package/build/types/resources/app_name.js +2 -0
- package/build/types/resources/app_name.js.map +1 -0
- package/build/types/resources/conversation.d.ts +18 -10
- package/build/types/resources/conversation.d.ts.map +1 -1
- package/build/types/resources/conversation.js.map +1 -1
- package/build/types/resources/conversation_badge.d.ts +12 -0
- package/build/types/resources/conversation_badge.d.ts.map +1 -0
- package/build/types/resources/conversation_badge.js +2 -0
- package/build/types/resources/conversation_badge.js.map +1 -0
- package/build/types/resources/group_resource.d.ts +12 -0
- package/build/types/resources/group_resource.d.ts.map +1 -0
- package/build/types/resources/group_resource.js +2 -0
- package/build/types/resources/group_resource.js.map +1 -0
- package/build/types/resources/index.d.ts +2 -1
- package/build/types/resources/index.d.ts.map +1 -1
- package/build/types/resources/index.js +2 -1
- package/build/types/resources/index.js.map +1 -1
- package/build/types/resources/member.d.ts +23 -0
- package/build/types/resources/member.d.ts.map +1 -0
- package/build/types/resources/member.js +2 -0
- package/build/types/resources/member.js.map +1 -0
- package/build/types/resources/member_ability.d.ts +6 -0
- package/build/types/resources/member_ability.d.ts.map +1 -0
- package/build/types/resources/member_ability.js +2 -0
- package/build/types/resources/member_ability.js.map +1 -0
- package/build/types/resources/reaction.d.ts +1 -1
- package/build/types/resources/reaction.js.map +1 -1
- package/build/utils/cache/page_mutations.d.ts +19 -2
- package/build/utils/cache/page_mutations.d.ts.map +1 -1
- package/build/utils/cache/page_mutations.js +21 -7
- package/build/utils/cache/page_mutations.js.map +1 -1
- package/build/utils/client/client.d.ts +1 -1
- package/build/utils/client/client.d.ts.map +1 -1
- package/build/utils/client/client.js +1 -1
- package/build/utils/client/client.js.map +1 -1
- package/build/utils/date.d.ts +4 -0
- package/build/utils/date.d.ts.map +1 -0
- package/build/utils/date.js +23 -0
- package/build/utils/date.js.map +1 -0
- package/build/utils/session.d.ts +0 -6
- package/build/utils/session.d.ts.map +1 -1
- package/build/utils/session.js +0 -6
- package/build/utils/session.js.map +1 -1
- package/build/utils/uri.d.ts +1 -1
- package/build/utils/uri.d.ts.map +1 -1
- package/build/utils/uri.js +1 -1
- package/build/utils/uri.js.map +1 -1
- package/package.json +7 -3
- package/src/__tests__/utils/cache/page_mutations.ts +7 -46
- package/src/components/conversation/message.tsx +8 -3
- package/src/components/conversation/message_reaction.tsx +6 -2
- package/src/components/conversations.tsx +95 -32
- package/src/components/display/badge.tsx +3 -8
- package/src/components/display/tabs.tsx +142 -0
- package/src/contexts/api_provider.tsx +3 -3
- package/src/hooks/use_conversation_jolt_events.ts +67 -0
- package/src/hooks/use_conversation_messages.ts +6 -2
- package/src/hooks/use_conversations.ts +53 -0
- package/src/hooks/use_jolt.ts +101 -0
- package/src/hooks/use_suspense_api.ts +10 -3
- package/src/navigation/index.tsx +10 -2
- package/src/screens/message_actions_screen.tsx +1 -1
- package/src/screens/reactions_screen.tsx +131 -0
- package/src/types/resources/app_name.ts +1 -0
- package/src/types/resources/conversation.ts +18 -10
- package/src/types/resources/conversation_badge.ts +10 -0
- package/src/types/resources/group_resource.ts +10 -0
- package/src/types/resources/index.ts +2 -1
- package/src/types/resources/member.ts +24 -0
- package/src/types/resources/member_ability.ts +5 -0
- package/src/types/resources/reaction.ts +1 -1
- package/src/utils/cache/page_mutations.ts +32 -9
- package/src/utils/client/client.ts +1 -1
- package/src/utils/date.ts +25 -0
- package/src/utils/session.ts +0 -7
- package/src/utils/uri.ts +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { ConversationResource } from '../types'
|
|
3
|
+
import { GetRequest } from '../utils/client/types'
|
|
4
|
+
import { useSuspensePaginator } from './use_suspense_api'
|
|
5
|
+
|
|
6
|
+
export const getConversationsRequestArgs = (): GetRequest => ({
|
|
7
|
+
url: '/me/conversations',
|
|
8
|
+
data: {
|
|
9
|
+
perPage: 20,
|
|
10
|
+
order: '-last_message',
|
|
11
|
+
fields: {
|
|
12
|
+
Conversation: [
|
|
13
|
+
'created_at',
|
|
14
|
+
'badges',
|
|
15
|
+
'groups',
|
|
16
|
+
'last_message_author_id',
|
|
17
|
+
'last_message_author_name',
|
|
18
|
+
'last_message_created_at',
|
|
19
|
+
'last_message_text_preview',
|
|
20
|
+
'preview_avatar_urls',
|
|
21
|
+
'member_ability',
|
|
22
|
+
'muted',
|
|
23
|
+
'replies_disabled',
|
|
24
|
+
'title',
|
|
25
|
+
'unread_count',
|
|
26
|
+
'updated_at',
|
|
27
|
+
],
|
|
28
|
+
ConversationBadge: ['app_name', 'pco_resource_type', 'text'],
|
|
29
|
+
},
|
|
30
|
+
include: ['badges'],
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export function useConversations() {
|
|
35
|
+
const requestArgs = getConversationsRequestArgs()
|
|
36
|
+
const { data, ...rest } = useSuspensePaginator<ConversationResource>(requestArgs)
|
|
37
|
+
|
|
38
|
+
const conversations = useMemo(
|
|
39
|
+
() =>
|
|
40
|
+
data.sort((a, b) => {
|
|
41
|
+
const dateA = a.lastMessageCreatedAt || a.createdAt
|
|
42
|
+
const dateB = b.lastMessageCreatedAt || b.createdAt
|
|
43
|
+
if (a.lastMessageCreatedAt && !b.lastMessageCreatedAt) return 1
|
|
44
|
+
if (!a.lastMessageCreatedAt && b.lastMessageCreatedAt) return -1
|
|
45
|
+
if (dateB > dateA) return 1
|
|
46
|
+
if (dateB < dateA) return -1
|
|
47
|
+
return 0
|
|
48
|
+
}),
|
|
49
|
+
[data]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return { conversations, ...rest }
|
|
53
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
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'
|
|
6
|
+
import {
|
|
7
|
+
FetchSubscribeToken,
|
|
8
|
+
JoltSubscription,
|
|
9
|
+
} from '@planningcenter/jolt-client/dist/types/JoltSubscription'
|
|
10
|
+
import { CustomMessage } from '@planningcenter/jolt-client/dist/types/JoltConnection'
|
|
11
|
+
|
|
12
|
+
interface JoltResponse {
|
|
13
|
+
type: 'JoltToken'
|
|
14
|
+
id: string
|
|
15
|
+
wssUrl: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const useJoltClient = (): JoltClient | undefined => {
|
|
19
|
+
const { client } = useContext(ChatContext)
|
|
20
|
+
const { data: joltToken } = useSuspenseQuery<ApiResource<JoltResponse>>({
|
|
21
|
+
refetchOnMount: false,
|
|
22
|
+
queryKey: ['jolt-token'],
|
|
23
|
+
queryFn: () => {
|
|
24
|
+
return client.post({
|
|
25
|
+
url: '/me/jolt_authorize',
|
|
26
|
+
data: {
|
|
27
|
+
data: {
|
|
28
|
+
type: 'JoltToken',
|
|
29
|
+
attributes: {},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const fetchAuthTokenFn = async () => {
|
|
37
|
+
return joltToken.data.id || ''
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const fetchSubscribeTokenFn: FetchSubscribeToken = (channel: string, connectionId: string) => {
|
|
41
|
+
return client
|
|
42
|
+
.post({
|
|
43
|
+
url: '/me/jolt_subscribe',
|
|
44
|
+
data: {
|
|
45
|
+
data: {
|
|
46
|
+
type: 'JoltSubscribeToken',
|
|
47
|
+
attributes: { channel, cid: connectionId },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
.then((res: ApiResource<JoltResponse>) => res.data.id)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { data: joltClient } = useQuery({
|
|
55
|
+
refetchOnMount: false,
|
|
56
|
+
refetchOnWindowFocus: false,
|
|
57
|
+
refetchOnReconnect: false,
|
|
58
|
+
enabled: Boolean(joltToken),
|
|
59
|
+
queryKey: ['jolt-client'],
|
|
60
|
+
queryFn: async () => {
|
|
61
|
+
if (!joltToken) return undefined
|
|
62
|
+
|
|
63
|
+
return new JoltClient(
|
|
64
|
+
joltToken?.data.wssUrl,
|
|
65
|
+
{
|
|
66
|
+
fetchAuthTokenFn,
|
|
67
|
+
fetchSubscribeTokenFn,
|
|
68
|
+
},
|
|
69
|
+
{ logToConsole: true }
|
|
70
|
+
)
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return joltClient
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function useJoltChannel(channelName: string) {
|
|
78
|
+
const [joltChannel, setJoltChannel] = useState<JoltSubscription>()
|
|
79
|
+
const jolt = useJoltClient()
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
setJoltChannel(jolt?.subscribe(channelName))
|
|
83
|
+
return () => jolt?.unsubscribe(channelName)
|
|
84
|
+
}, [channelName, jolt])
|
|
85
|
+
|
|
86
|
+
return joltChannel
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type UserCallbackFn<T> = (_event: T) => void
|
|
90
|
+
|
|
91
|
+
export function useJoltEvent<T extends CustomMessage>(
|
|
92
|
+
channel: JoltSubscription | undefined,
|
|
93
|
+
eventName: string,
|
|
94
|
+
callback: UserCallbackFn<T>
|
|
95
|
+
) {
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!channel) return () => {}
|
|
98
|
+
|
|
99
|
+
return channel.bind(eventName, e => callback(e as T))
|
|
100
|
+
}, [channel, eventName, callback])
|
|
101
|
+
}
|
|
@@ -10,10 +10,10 @@ import { ApiCollection, ApiResource, ResourceObject } from '../types'
|
|
|
10
10
|
import { GetRequest, RequestData } from '../utils/client/types'
|
|
11
11
|
|
|
12
12
|
export const useSuspenseGet = <T extends ResourceObject | ResourceObject[]>(args: GetRequest) => {
|
|
13
|
-
type Resource =
|
|
13
|
+
type Resource = ApiResource<T>
|
|
14
14
|
|
|
15
15
|
const { data, ...query } = useSuspenseQuery<Resource, Response>({
|
|
16
|
-
queryKey:
|
|
16
|
+
queryKey: getRequestQueryKey(args),
|
|
17
17
|
})
|
|
18
18
|
|
|
19
19
|
return { ...data, ...query }
|
|
@@ -41,7 +41,7 @@ export const useSuspensePaginator = <T extends ResourceObject>(
|
|
|
41
41
|
any,
|
|
42
42
|
Partial<RequestData> | undefined
|
|
43
43
|
>({
|
|
44
|
-
queryKey:
|
|
44
|
+
queryKey: getRequestQueryKey(args),
|
|
45
45
|
queryFn: ({ pageParam }) => {
|
|
46
46
|
const pageParmWhere = pageParam?.where || {}
|
|
47
47
|
const argsWhere = args.data.where || {}
|
|
@@ -72,3 +72,10 @@ export const useSuspensePaginator = <T extends ResourceObject>(
|
|
|
72
72
|
|
|
73
73
|
return { ...query, data }
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
export type RequestQueryKey = [GetRequest['url'], GetRequest['data'], GetRequest['headers']]
|
|
77
|
+
export const getRequestQueryKey = (args: GetRequest): RequestQueryKey => [
|
|
78
|
+
args.url,
|
|
79
|
+
args.data,
|
|
80
|
+
args.headers,
|
|
81
|
+
]
|
package/src/navigation/index.tsx
CHANGED
|
@@ -7,7 +7,11 @@ import { ConversationsScreen } from '../screens/conversations_screen'
|
|
|
7
7
|
import { ConversationScreen } from '../screens/conversation_screen'
|
|
8
8
|
import { HeaderBackButton, HeaderButton } from '@react-navigation/elements'
|
|
9
9
|
import { Icon } from '../components'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
MessageActionsScreen,
|
|
12
|
+
MessageActionsScreenOptions,
|
|
13
|
+
} from '../screens/message_actions_screen'
|
|
14
|
+
import { ReactionsScreen, ReactionsScreenOptions } from '../screens/reactions_screen'
|
|
11
15
|
|
|
12
16
|
export const ChatStack = createNativeStackNavigator({
|
|
13
17
|
screenLayout: ScreenLayout,
|
|
@@ -35,7 +39,11 @@ export const ChatStack = createNativeStackNavigator({
|
|
|
35
39
|
MessageActions: {
|
|
36
40
|
screen: MessageActionsScreen,
|
|
37
41
|
// Something about sheetAllowedDetents declared inline breaks TS
|
|
38
|
-
options:
|
|
42
|
+
options: MessageActionsScreenOptions,
|
|
43
|
+
},
|
|
44
|
+
Reactions: {
|
|
45
|
+
screen: ReactionsScreen,
|
|
46
|
+
options: ReactionsScreenOptions,
|
|
39
47
|
},
|
|
40
48
|
NotFound: {
|
|
41
49
|
screen: NotFound,
|
|
@@ -15,7 +15,7 @@ import { ReactionCountResource } from '../types/resources/reaction'
|
|
|
15
15
|
import { updateRecordInPagesData } from '../utils'
|
|
16
16
|
import { Clipboard } from '../utils/native_adapters'
|
|
17
17
|
|
|
18
|
-
export const
|
|
18
|
+
export const MessageActionsScreenOptions: NativeStackNavigationOptions = {
|
|
19
19
|
presentation: 'formSheet',
|
|
20
20
|
headerShown: false,
|
|
21
21
|
sheetAllowedDetents: [0.25],
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { StaticScreenProps } from '@react-navigation/native'
|
|
2
|
+
import { NativeStackNavigationOptions } from '@react-navigation/native-stack'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { FlatList, StyleSheet, useWindowDimensions, View } from 'react-native'
|
|
5
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
|
6
|
+
import { Avatar, Text } from '../components'
|
|
7
|
+
import { REACTION_EMOJIS, useReactionStyles } from '../components/conversation/message_reaction'
|
|
8
|
+
import { Tabs } from '../components/display/tabs'
|
|
9
|
+
import { useSuspenseGet, useTheme } from '../hooks'
|
|
10
|
+
import { useConversationMessages } from '../hooks/use_conversation_messages'
|
|
11
|
+
import { MemberResource } from '../types'
|
|
12
|
+
import { ReactionCountResource } from '../types/resources/reaction'
|
|
13
|
+
|
|
14
|
+
export const ReactionsScreenOptions: NativeStackNavigationOptions = {
|
|
15
|
+
presentation: 'formSheet',
|
|
16
|
+
headerShown: false,
|
|
17
|
+
sheetAllowedDetents: [0.5],
|
|
18
|
+
sheetGrabberVisible: true,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ReactionScreenProps = StaticScreenProps<{
|
|
22
|
+
message_id: string
|
|
23
|
+
conversation_id: string
|
|
24
|
+
reaction_value?: string
|
|
25
|
+
}>
|
|
26
|
+
|
|
27
|
+
export function ReactionsScreen({ route }: ReactionScreenProps) {
|
|
28
|
+
const { conversation_id, message_id, reaction_value } = route.params
|
|
29
|
+
const styles = useStyles()
|
|
30
|
+
|
|
31
|
+
const { messages } = useConversationMessages({ conversation_id }, { refetchOnMount: false })
|
|
32
|
+
const { data: members } = useSuspenseGet<MemberResource[]>({
|
|
33
|
+
url: `/me/conversations/${conversation_id}/members`,
|
|
34
|
+
data: {
|
|
35
|
+
fields: {
|
|
36
|
+
Member: ['id', 'name', 'avatar', 'badges', 'child', 'role'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
const message = messages.find(m => m.id === message_id)
|
|
41
|
+
const reactionCounts = message?.reactionCounts || []
|
|
42
|
+
const initialReactionCount =
|
|
43
|
+
reactionCounts.find(r => r.value === reaction_value) || reactionCounts[0]
|
|
44
|
+
const [reactionCount, setReactionCount] =
|
|
45
|
+
React.useState<ReactionCountResource>(initialReactionCount)
|
|
46
|
+
|
|
47
|
+
const authorIds = reactionCount.authorIds
|
|
48
|
+
const authors = members.filter(member => authorIds.includes(member.id))
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<View style={styles.container}>
|
|
52
|
+
<Tabs
|
|
53
|
+
data={reactionCounts}
|
|
54
|
+
activeTab={reactionCount}
|
|
55
|
+
onTabPress={setReactionCount}
|
|
56
|
+
renderItem={({ item }) => (
|
|
57
|
+
<Reaction
|
|
58
|
+
active={reactionCount.id === item.id}
|
|
59
|
+
reaction={item}
|
|
60
|
+
onPress={() => setReactionCount(item)}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
style={styles.actions}
|
|
64
|
+
/>
|
|
65
|
+
<FlatList
|
|
66
|
+
data={authors}
|
|
67
|
+
keyExtractor={item => item.id}
|
|
68
|
+
renderItem={({ item: author }) => (
|
|
69
|
+
<View style={styles.authorList}>
|
|
70
|
+
<Avatar size={'md'} sourceUri={author.avatar} />
|
|
71
|
+
<Text key={author.id}>{author.name}</Text>
|
|
72
|
+
</View>
|
|
73
|
+
)}
|
|
74
|
+
/>
|
|
75
|
+
</View>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const Reaction = ({
|
|
80
|
+
reaction,
|
|
81
|
+
}: {
|
|
82
|
+
active: boolean
|
|
83
|
+
reaction: ReactionCountResource
|
|
84
|
+
onPress: () => void
|
|
85
|
+
}) => {
|
|
86
|
+
const styles = useStyles()
|
|
87
|
+
const reactionStyles = useReactionStyles({ mine: reaction.mine ? 1 : 0 })
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<View key={reaction.value} style={styles.reaction}>
|
|
91
|
+
<Text style={reactionStyles.reactionEmoji}>{REACTION_EMOJIS[reaction.value]}</Text>
|
|
92
|
+
<Text style={reactionStyles.reactionEmoji}>{reaction.count}</Text>
|
|
93
|
+
</View>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const useStyles = () => {
|
|
98
|
+
const theme = useTheme()
|
|
99
|
+
const { height } = useWindowDimensions()
|
|
100
|
+
const { bottom } = useSafeAreaInsets()
|
|
101
|
+
|
|
102
|
+
return StyleSheet.create({
|
|
103
|
+
container: {
|
|
104
|
+
justifyContent: 'flex-start',
|
|
105
|
+
paddingTop: 12,
|
|
106
|
+
paddingBottom: bottom,
|
|
107
|
+
width: '100%',
|
|
108
|
+
backgroundColor: theme.colors.fillColorNeutral100Inverted,
|
|
109
|
+
height,
|
|
110
|
+
gap: 8,
|
|
111
|
+
},
|
|
112
|
+
authorList: {
|
|
113
|
+
flexDirection: 'row',
|
|
114
|
+
alignItems: 'center',
|
|
115
|
+
gap: 8,
|
|
116
|
+
paddingHorizontal: 12,
|
|
117
|
+
paddingVertical: 12,
|
|
118
|
+
},
|
|
119
|
+
reaction: {
|
|
120
|
+
paddingVertical: 12,
|
|
121
|
+
paddingHorizontal: 12,
|
|
122
|
+
flexDirection: 'row',
|
|
123
|
+
gap: 4,
|
|
124
|
+
},
|
|
125
|
+
actions: {
|
|
126
|
+
minHeight: 48,
|
|
127
|
+
borderBottomColor: theme.colors.fillColorNeutral040,
|
|
128
|
+
borderBottomWidth: 1,
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type AppName = 'Services' | 'Groups'
|
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ConversationBadgeResource } from './conversation_badge'
|
|
2
|
+
import { GroupResource } from './group_resource'
|
|
3
|
+
import { MemberAbilityResource } from './member_ability'
|
|
2
4
|
|
|
3
|
-
export interface ConversationResource
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
export interface ConversationResource {
|
|
6
|
+
type: 'Conversation'
|
|
7
|
+
id: string
|
|
8
|
+
badges?: ConversationBadgeResource[]
|
|
6
9
|
createdAt: string
|
|
7
|
-
|
|
10
|
+
deleted?: boolean
|
|
11
|
+
groups?: GroupResource[]
|
|
12
|
+
previewAvatarUrls?: string[]
|
|
13
|
+
lastMessageAuthorId?: string
|
|
14
|
+
lastMessageAuthorName?: string
|
|
15
|
+
lastMessageCreatedAt?: string
|
|
16
|
+
lastMessageTextPreview?: string
|
|
17
|
+
memberAbility?: MemberAbilityResource
|
|
18
|
+
muted: boolean
|
|
8
19
|
repliesDisabled: boolean
|
|
9
|
-
|
|
10
|
-
lastMessageAuthorName: string
|
|
11
|
-
lastMessageCreatedAt: string
|
|
12
|
-
lastMessageTextPreview: string
|
|
20
|
+
title: string
|
|
13
21
|
unreadCount: number
|
|
14
|
-
|
|
22
|
+
updatedAt: string
|
|
15
23
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface MemberResource {
|
|
2
|
+
type: 'Member'
|
|
3
|
+
id: string
|
|
4
|
+
name: string
|
|
5
|
+
avatar: string
|
|
6
|
+
badges: MemberBadge[]
|
|
7
|
+
child: boolean
|
|
8
|
+
role?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface MemberResourceWithPerson extends MemberResource {
|
|
12
|
+
person: {
|
|
13
|
+
firstName: string
|
|
14
|
+
lastName: string
|
|
15
|
+
avatarUrl: string
|
|
16
|
+
child: boolean
|
|
17
|
+
id: string
|
|
18
|
+
type: 'Person'
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MemberBadge {
|
|
23
|
+
title: string
|
|
24
|
+
}
|
|
@@ -17,7 +17,7 @@ export function updateRecordInPagesData<T extends ResourceObject>({
|
|
|
17
17
|
}: {
|
|
18
18
|
data?: { pages: ApiCollection<T>[]; pageParams: any }
|
|
19
19
|
record: T
|
|
20
|
-
processRecord?: (
|
|
20
|
+
processRecord?: (_next: T, _prev?: T) => T
|
|
21
21
|
}) {
|
|
22
22
|
if (!data) return data
|
|
23
23
|
|
|
@@ -26,7 +26,7 @@ export function updateRecordInPagesData<T extends ResourceObject>({
|
|
|
26
26
|
const newData = page.data.map(message => {
|
|
27
27
|
if (message.id === record.id) {
|
|
28
28
|
foundRecord = true
|
|
29
|
-
return processRecord(record)
|
|
29
|
+
return processRecord(record, message)
|
|
30
30
|
}
|
|
31
31
|
return message
|
|
32
32
|
})
|
|
@@ -37,9 +37,9 @@ export function updateRecordInPagesData<T extends ResourceObject>({
|
|
|
37
37
|
if (!foundRecord) {
|
|
38
38
|
// Can be used to add as well but it's not at all efficient. It's better to use addRecordInPagesData.
|
|
39
39
|
// This is a fallback for when the record is not found in the cache.
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
newPages[
|
|
40
|
+
const firstPage = { ...newPages[0] }
|
|
41
|
+
firstPage.data = [processRecord(record), ...firstPage.data]
|
|
42
|
+
newPages[0] = firstPage
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
return { ...data, pages: newPages }
|
|
@@ -52,15 +52,38 @@ export function addRecordInPagesData<T extends ResourceObject>({
|
|
|
52
52
|
}: {
|
|
53
53
|
data?: { pages: ApiCollection<T>[]; pageParams: any }
|
|
54
54
|
record: T
|
|
55
|
-
processRecord?: (
|
|
55
|
+
processRecord?: (_next: T, _prev?: T) => T
|
|
56
56
|
}) {
|
|
57
57
|
if (!data) return data
|
|
58
58
|
|
|
59
59
|
const newPages = [...data.pages]
|
|
60
|
-
const
|
|
61
|
-
|
|
60
|
+
const firstPage = { ...newPages[0] }
|
|
61
|
+
firstPage.data = [processRecord(record), ...firstPage.data]
|
|
62
62
|
|
|
63
|
-
newPages[
|
|
63
|
+
newPages[0] = firstPage
|
|
64
|
+
|
|
65
|
+
return { ...data, pages: newPages }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* deleteRecordInPagesData
|
|
70
|
+
*/
|
|
71
|
+
export function deleteRecordInPagesData<T extends ResourceObject>({
|
|
72
|
+
data,
|
|
73
|
+
record,
|
|
74
|
+
}: {
|
|
75
|
+
data?: { pages: ApiCollection<T>[]; pageParams: any }
|
|
76
|
+
record: T
|
|
77
|
+
}) {
|
|
78
|
+
if (!data) return data
|
|
79
|
+
|
|
80
|
+
const newPages = data.pages.map(page => {
|
|
81
|
+
const newData = page.data.filter(message => {
|
|
82
|
+
return message.id !== record.id
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
return { ...page, data: newData }
|
|
86
|
+
})
|
|
64
87
|
|
|
65
88
|
return { ...data, pages: newPages }
|
|
66
89
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { date as formatDate } from '@planningcenter/datetime-fmt'
|
|
2
|
+
import moment from 'moment-timezone'
|
|
3
|
+
|
|
4
|
+
type DateProps = string | number | Date
|
|
5
|
+
|
|
6
|
+
export function formatDatePreview(date?: DateProps) {
|
|
7
|
+
if (!date) return ''
|
|
8
|
+
|
|
9
|
+
const now = moment()
|
|
10
|
+
const isToday = now.isSame(date, 'day')
|
|
11
|
+
const isThisWeek = now.isSame(date, 'week')
|
|
12
|
+
const isThisYear = now.isSame(date, 'year')
|
|
13
|
+
|
|
14
|
+
if (isToday) return moment(date).format('h:mm a')
|
|
15
|
+
if (isThisWeek) return formatDate(date, { style: 'relative-short' })
|
|
16
|
+
if (isThisYear) return formatDate(date, { style: 'abbreviated' })
|
|
17
|
+
|
|
18
|
+
// TODO: Org date format
|
|
19
|
+
return formatShorterDate(date)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const formatShorterDate = (date: DateProps) => {
|
|
23
|
+
// Drop the century from the year
|
|
24
|
+
return formatDate(date, { style: 'short', year: true }).replace(/20(\d{2})/, '$1')
|
|
25
|
+
}
|
package/src/utils/session.ts
CHANGED
|
@@ -2,18 +2,11 @@ import { OAuthToken } from '../types'
|
|
|
2
2
|
|
|
3
3
|
export type ENV = 'production' | 'staging' | 'development'
|
|
4
4
|
|
|
5
|
-
export const baseUrlMap = {
|
|
6
|
-
production: 'api.planningcenteronline.com',
|
|
7
|
-
staging: 'api-staging.planningcenteronline.com',
|
|
8
|
-
development: 'api.pco.test',
|
|
9
|
-
}
|
|
10
|
-
|
|
11
5
|
type SessionProps = { env?: ENV; token?: OAuthToken } | undefined
|
|
12
6
|
|
|
13
7
|
/**
|
|
14
8
|
* Session class to track the environment and token
|
|
15
9
|
* Not intended to make network requests or handle authentication
|
|
16
|
-
* - returns urls for convenience only
|
|
17
10
|
*/
|
|
18
11
|
export class Session {
|
|
19
12
|
env: ENV
|
package/src/utils/uri.ts
CHANGED