@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.
Files changed (130) hide show
  1. package/build/components/conversation/message.d.ts.map +1 -1
  2. package/build/components/conversation/message.js +7 -2
  3. package/build/components/conversation/message.js.map +1 -1
  4. package/build/components/conversation/message_reaction.d.ts +1 -1
  5. package/build/components/conversation/message_reaction.d.ts.map +1 -1
  6. package/build/components/conversation/message_reaction.js +1 -1
  7. package/build/components/conversation/message_reaction.js.map +1 -1
  8. package/build/components/conversations.d.ts.map +1 -1
  9. package/build/components/conversations.js +76 -30
  10. package/build/components/conversations.js.map +1 -1
  11. package/build/components/display/badge.d.ts +2 -6
  12. package/build/components/display/badge.d.ts.map +1 -1
  13. package/build/components/display/badge.js +1 -5
  14. package/build/components/display/badge.js.map +1 -1
  15. package/build/components/display/tabs.d.ts +17 -0
  16. package/build/components/display/tabs.d.ts.map +1 -0
  17. package/build/components/display/tabs.js +97 -0
  18. package/build/components/display/tabs.js.map +1 -0
  19. package/build/contexts/api_provider.js +2 -2
  20. package/build/contexts/api_provider.js.map +1 -1
  21. package/build/hooks/use_conversation_jolt_events.d.ts +2 -0
  22. package/build/hooks/use_conversation_jolt_events.d.ts.map +1 -0
  23. package/build/hooks/use_conversation_jolt_events.js +47 -0
  24. package/build/hooks/use_conversation_jolt_events.js.map +1 -0
  25. package/build/hooks/use_conversation_messages.d.ts +2 -18
  26. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  27. package/build/hooks/use_conversation_messages.js +2 -2
  28. package/build/hooks/use_conversation_messages.js.map +1 -1
  29. package/build/hooks/use_conversations.d.ts +37 -0
  30. package/build/hooks/use_conversations.d.ts.map +1 -0
  31. package/build/hooks/use_conversations.js +48 -0
  32. package/build/hooks/use_conversations.js.map +1 -0
  33. package/build/hooks/use_jolt.d.ts +9 -0
  34. package/build/hooks/use_jolt.d.ts.map +1 -0
  35. package/build/hooks/use_jolt.js +71 -0
  36. package/build/hooks/use_jolt.js.map +1 -0
  37. package/build/hooks/use_suspense_api.d.ts +7 -2
  38. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  39. package/build/hooks/use_suspense_api.js +7 -2
  40. package/build/hooks/use_suspense_api.js.map +1 -1
  41. package/build/navigation/index.d.ts +5 -0
  42. package/build/navigation/index.d.ts.map +1 -1
  43. package/build/navigation/index.js +7 -2
  44. package/build/navigation/index.js.map +1 -1
  45. package/build/screens/message_actions_screen.d.ts +1 -1
  46. package/build/screens/message_actions_screen.d.ts.map +1 -1
  47. package/build/screens/message_actions_screen.js +1 -1
  48. package/build/screens/message_actions_screen.js.map +1 -1
  49. package/build/screens/reactions_screen.d.ts +11 -0
  50. package/build/screens/reactions_screen.d.ts.map +1 -0
  51. package/build/screens/reactions_screen.js +83 -0
  52. package/build/screens/reactions_screen.js.map +1 -0
  53. package/build/types/resources/app_name.d.ts +2 -0
  54. package/build/types/resources/app_name.d.ts.map +1 -0
  55. package/build/types/resources/app_name.js +2 -0
  56. package/build/types/resources/app_name.js.map +1 -0
  57. package/build/types/resources/conversation.d.ts +18 -10
  58. package/build/types/resources/conversation.d.ts.map +1 -1
  59. package/build/types/resources/conversation.js.map +1 -1
  60. package/build/types/resources/conversation_badge.d.ts +12 -0
  61. package/build/types/resources/conversation_badge.d.ts.map +1 -0
  62. package/build/types/resources/conversation_badge.js +2 -0
  63. package/build/types/resources/conversation_badge.js.map +1 -0
  64. package/build/types/resources/group_resource.d.ts +12 -0
  65. package/build/types/resources/group_resource.d.ts.map +1 -0
  66. package/build/types/resources/group_resource.js +2 -0
  67. package/build/types/resources/group_resource.js.map +1 -0
  68. package/build/types/resources/index.d.ts +2 -1
  69. package/build/types/resources/index.d.ts.map +1 -1
  70. package/build/types/resources/index.js +2 -1
  71. package/build/types/resources/index.js.map +1 -1
  72. package/build/types/resources/member.d.ts +23 -0
  73. package/build/types/resources/member.d.ts.map +1 -0
  74. package/build/types/resources/member.js +2 -0
  75. package/build/types/resources/member.js.map +1 -0
  76. package/build/types/resources/member_ability.d.ts +6 -0
  77. package/build/types/resources/member_ability.d.ts.map +1 -0
  78. package/build/types/resources/member_ability.js +2 -0
  79. package/build/types/resources/member_ability.js.map +1 -0
  80. package/build/types/resources/reaction.d.ts +1 -1
  81. package/build/types/resources/reaction.js.map +1 -1
  82. package/build/utils/cache/page_mutations.d.ts +19 -2
  83. package/build/utils/cache/page_mutations.d.ts.map +1 -1
  84. package/build/utils/cache/page_mutations.js +21 -7
  85. package/build/utils/cache/page_mutations.js.map +1 -1
  86. package/build/utils/client/client.d.ts +1 -1
  87. package/build/utils/client/client.d.ts.map +1 -1
  88. package/build/utils/client/client.js +1 -1
  89. package/build/utils/client/client.js.map +1 -1
  90. package/build/utils/date.d.ts +4 -0
  91. package/build/utils/date.d.ts.map +1 -0
  92. package/build/utils/date.js +23 -0
  93. package/build/utils/date.js.map +1 -0
  94. package/build/utils/session.d.ts +0 -6
  95. package/build/utils/session.d.ts.map +1 -1
  96. package/build/utils/session.js +0 -6
  97. package/build/utils/session.js.map +1 -1
  98. package/build/utils/uri.d.ts +1 -1
  99. package/build/utils/uri.d.ts.map +1 -1
  100. package/build/utils/uri.js +1 -1
  101. package/build/utils/uri.js.map +1 -1
  102. package/package.json +7 -3
  103. package/src/__tests__/utils/cache/page_mutations.ts +7 -46
  104. package/src/components/conversation/message.tsx +8 -3
  105. package/src/components/conversation/message_reaction.tsx +6 -2
  106. package/src/components/conversations.tsx +95 -32
  107. package/src/components/display/badge.tsx +3 -8
  108. package/src/components/display/tabs.tsx +142 -0
  109. package/src/contexts/api_provider.tsx +3 -3
  110. package/src/hooks/use_conversation_jolt_events.ts +67 -0
  111. package/src/hooks/use_conversation_messages.ts +6 -2
  112. package/src/hooks/use_conversations.ts +53 -0
  113. package/src/hooks/use_jolt.ts +101 -0
  114. package/src/hooks/use_suspense_api.ts +10 -3
  115. package/src/navigation/index.tsx +10 -2
  116. package/src/screens/message_actions_screen.tsx +1 -1
  117. package/src/screens/reactions_screen.tsx +131 -0
  118. package/src/types/resources/app_name.ts +1 -0
  119. package/src/types/resources/conversation.ts +18 -10
  120. package/src/types/resources/conversation_badge.ts +10 -0
  121. package/src/types/resources/group_resource.ts +10 -0
  122. package/src/types/resources/index.ts +2 -1
  123. package/src/types/resources/member.ts +24 -0
  124. package/src/types/resources/member_ability.ts +5 -0
  125. package/src/types/resources/reaction.ts +1 -1
  126. package/src/utils/cache/page_mutations.ts +32 -9
  127. package/src/utils/client/client.ts +1 -1
  128. package/src/utils/date.ts +25 -0
  129. package/src/utils/session.ts +0 -7
  130. 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 = T extends ResourceObject ? ApiResource<T> : ApiCollection<T>
13
+ type Resource = ApiResource<T>
14
14
 
15
15
  const { data, ...query } = useSuspenseQuery<Resource, Response>({
16
- queryKey: [args],
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: [args.url, args.data],
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
+ ]
@@ -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 { MessageActionsScreen, ReactScreenOptions } from '../screens/message_actions_screen'
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: ReactScreenOptions,
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 ReactScreenOptions: NativeStackNavigationOptions = {
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 type { ResourceObject } from '../api_primitives'
1
+ import { ConversationBadgeResource } from './conversation_badge'
2
+ import { GroupResource } from './group_resource'
3
+ import { MemberAbilityResource } from './member_ability'
2
4
 
3
- export interface ConversationResource extends ResourceObject {
4
- title: string
5
- subtitle: string
5
+ export interface ConversationResource {
6
+ type: 'Conversation'
7
+ id: string
8
+ badges?: ConversationBadgeResource[]
6
9
  createdAt: string
7
- updatedAt: string
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
- lastMessageAuthorId: string
10
- lastMessageAuthorName: string
11
- lastMessageCreatedAt: string
12
- lastMessageTextPreview: string
20
+ title: string
13
21
  unreadCount: number
14
- muted: boolean
22
+ updatedAt: string
15
23
  }
@@ -0,0 +1,10 @@
1
+ import { AppName } from './app_name'
2
+
3
+ export interface ConversationBadgeResource {
4
+ type: 'ConversationBadge'
5
+ id: string
6
+ links: { self: string }
7
+ text: string | null
8
+ appName: AppName
9
+ pcoResourceType: string
10
+ }
@@ -0,0 +1,10 @@
1
+ import { AppName } from './app_name'
2
+
3
+ export interface GroupResource {
4
+ type: 'Group'
5
+ id: string
6
+ links: { self: string }
7
+ name: string | null
8
+ sourceAppName: AppName
9
+ sourceType: string
10
+ }
@@ -1,4 +1,5 @@
1
1
  export * from './conversation'
2
+ export * from './member'
2
3
  export * from './message'
3
- export * from './person'
4
4
  export * from './oauth_token'
5
+ export * from './person'
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ export interface MemberAbilityResource {
2
+ type: 'MemberAbility'
3
+ canUpdate: boolean
4
+ canDelete: boolean
5
+ }
@@ -5,5 +5,5 @@ export interface ReactionCountResource {
5
5
  count: number
6
6
  mine: number
7
7
  messageId: string
8
- authorIds: number[]
8
+ authorIds: string[]
9
9
  }
@@ -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?: (_record: T) => T
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 lastPage = { ...newPages[newPages.length - 1] }
41
- lastPage.data = [...lastPage.data, processRecord(record)]
42
- newPages[newPages.length - 1] = lastPage
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?: (_record: T) => T
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 lastPage = { ...newPages[newPages.length - 1] }
61
- lastPage.data = [...lastPage.data, processRecord(record)]
60
+ const firstPage = { ...newPages[0] }
61
+ firstPage.data = [processRecord(record), ...firstPage.data]
62
62
 
63
- newPages[newPages.length - 1] = lastPage
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
  }
@@ -1,6 +1,6 @@
1
1
  import { ApiCollection, ApiResource } from '../../types'
2
2
  import { Session } from '../session'
3
- import Uri from '../uri'
3
+ import { Uri } from '../uri'
4
4
  import {
5
5
  concatRecords,
6
6
  ensureNoQueryParamsInDev,
@@ -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
+ }
@@ -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
@@ -7,7 +7,7 @@ const systemVersion = DeviceInfo.getSystemVersion()
7
7
  const readableVersion = DeviceInfo.getReadableVersion()
8
8
  const appName = DeviceInfo.getApplicationName()
9
9
 
10
- export default class Uri {
10
+ export class Uri {
11
11
  session: Session
12
12
  app?: string
13
13