@planningcenter/chat-react-native 3.42.2-rc.0 → 3.42.2

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 (52) hide show
  1. package/build/components/conversations/conversation_preview.d.ts.map +1 -1
  2. package/build/components/conversations/conversation_preview.js +46 -12
  3. package/build/components/conversations/conversation_preview.js.map +1 -1
  4. package/build/components/conversations/unread_count_badge.d.ts +2 -1
  5. package/build/components/conversations/unread_count_badge.d.ts.map +1 -1
  6. package/build/components/conversations/unread_count_badge.js +5 -4
  7. package/build/components/conversations/unread_count_badge.js.map +1 -1
  8. package/build/hooks/use_conversation.d.ts.map +1 -1
  9. package/build/hooks/use_conversation.js +2 -0
  10. package/build/hooks/use_conversation.js.map +1 -1
  11. package/build/hooks/use_conversation_jolt_events.d.ts.map +1 -1
  12. package/build/hooks/use_conversation_jolt_events.js +8 -15
  13. package/build/hooks/use_conversation_jolt_events.js.map +1 -1
  14. package/build/hooks/use_conversations_actions.d.ts.map +1 -1
  15. package/build/hooks/use_conversations_actions.js +15 -2
  16. package/build/hooks/use_conversations_actions.js.map +1 -1
  17. package/build/hooks/use_mark_latest_message_read.d.ts.map +1 -1
  18. package/build/hooks/use_mark_latest_message_read.js +3 -4
  19. package/build/hooks/use_mark_latest_message_read.js.map +1 -1
  20. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  21. package/build/hooks/use_suspense_api.js +1 -7
  22. package/build/hooks/use_suspense_api.js.map +1 -1
  23. package/build/types/jolt_events/reaction_events.d.ts +1 -0
  24. package/build/types/jolt_events/reaction_events.d.ts.map +1 -1
  25. package/build/types/jolt_events/reaction_events.js.map +1 -1
  26. package/build/types/resources/conversation.d.ts +10 -1
  27. package/build/types/resources/conversation.d.ts.map +1 -1
  28. package/build/types/resources/conversation.js.map +1 -1
  29. package/build/utils/client/client.d.ts.map +1 -1
  30. package/build/utils/client/client.js +2 -1
  31. package/build/utils/client/client.js.map +1 -1
  32. package/build/utils/request/conversation.d.ts.map +1 -1
  33. package/build/utils/request/conversation.js +2 -0
  34. package/build/utils/request/conversation.js.map +1 -1
  35. package/build/utils/response_error.d.ts +1 -0
  36. package/build/utils/response_error.d.ts.map +1 -1
  37. package/build/utils/response_error.js +20 -1
  38. package/build/utils/response_error.js.map +1 -1
  39. package/package.json +3 -3
  40. package/src/components/conversations/conversation_preview.tsx +58 -23
  41. package/src/components/conversations/unread_count_badge.tsx +13 -4
  42. package/src/hooks/use_conversation.ts +2 -0
  43. package/src/hooks/use_conversation_jolt_events.ts +6 -17
  44. package/src/hooks/use_conversations_actions.ts +16 -2
  45. package/src/hooks/use_mark_latest_message_read.ts +3 -4
  46. package/src/hooks/use_suspense_api.ts +1 -9
  47. package/src/types/jolt_events/reaction_events.ts +1 -0
  48. package/src/types/resources/conversation.ts +11 -1
  49. package/src/utils/__tests__/response_error.test.ts +47 -0
  50. package/src/utils/client/client.ts +2 -1
  51. package/src/utils/request/conversation.ts +2 -0
  52. package/src/utils/response_error.ts +23 -1
@@ -17,9 +17,9 @@ export function useMarkLatestMessageRead({ conversation }: Props) {
17
17
  [markRead]
18
18
  )
19
19
  const unreadCount = conversation.unreadCount
20
- const unreadReactionCount = conversation.unreadReactionCount || 0
20
+ const unreadReaction = conversation.unreadReaction
21
21
 
22
- const shouldMarkRead = Boolean(unreadCount >= 1 || unreadReactionCount > 0 || !firedOnce.current)
22
+ const shouldMarkRead = Boolean(unreadCount >= 1 || unreadReaction || !firedOnce.current)
23
23
  const appState = useAppState()
24
24
  const isActive = appState === 'active'
25
25
 
@@ -29,6 +29,5 @@ export function useMarkLatestMessageRead({ conversation }: Props) {
29
29
  firedOnce.current = true
30
30
 
31
31
  debouncedMarkRead(true)
32
- // keeping unreadReactionCount in the dependency array to watch for changes
33
- }, [debouncedMarkRead, isActive, shouldMarkRead, unreadReactionCount])
32
+ }, [debouncedMarkRead, isActive, shouldMarkRead, unreadReaction])
34
33
  }
@@ -9,7 +9,7 @@ import { ApiCollection, ApiResource, ResourceObject } from '../types'
9
9
  import { FailedResponse } from '../types/api_primitives'
10
10
  import { Log } from '../utils'
11
11
  import { GetRequest, RequestData } from '../utils/client'
12
- import { ResponseError } from '../utils/response_error'
12
+ import { throwResponseError } from '../utils/response_error'
13
13
  import { getNextPageParamFromMeta } from './paginator_meta'
14
14
  import { App, useApiClient } from './use_api_client'
15
15
 
@@ -90,14 +90,6 @@ export const useSuspensePaginator = <T extends ResourceObject>(
90
90
  return { ...query, data, totalCount }
91
91
  }
92
92
 
93
- const throwResponseError = (error: unknown) => {
94
- if (error instanceof Response) {
95
- throw new ResponseError(error as FailedResponse)
96
- }
97
-
98
- return Promise.reject(error)
99
- }
100
-
101
93
  export type RequestQueryKey = [
102
94
  SuspenseGetOptions['url'],
103
95
  SuspenseGetOptions['data'],
@@ -6,6 +6,7 @@ interface BaseReactionEventData extends Record<string, unknown> {
6
6
  author_id: number
7
7
  conversation_id: number
8
8
  message_sort_key: string
9
+ message_author_id: number
9
10
  reply_root_id?: string | null
10
11
  created_at: string
11
12
  organization_id: number
@@ -3,15 +3,25 @@ import { ConversationBadgeResource } from './conversation_badge'
3
3
  import { ConversationMembershipResource } from './conversation_membership'
4
4
  import { GroupResource } from './group_resource'
5
5
  import { MemberAbilityResource } from './member_ability'
6
+ import { ReactionCountResource } from './reaction'
6
7
 
7
8
  export type ConversationDisabledReason = 'safety_check' | 'direct_messages_disabled'
8
9
 
10
+ export interface ConversationPreviewData {
11
+ kind: 'reaction' | 'message'
12
+ authorName: string
13
+ value: ReactionCountResource['value'] | null
14
+ text: string
15
+ createdAt: string
16
+ }
17
+
9
18
  export interface ConversationResource {
10
19
  type: 'Conversation'
11
20
  id: number
12
21
  badges?: ConversationBadgeResource[]
13
22
  analyticsMetadata?: AnalyticsMetadataResource
14
23
  conversationMembership?: Partial<ConversationMembershipResource>
24
+ conversationPreview: ConversationPreviewData | null
15
25
  createdAt: string
16
26
  deleted?: boolean
17
27
  directMessage?: boolean
@@ -34,6 +44,6 @@ export interface ConversationResource {
34
44
  repliesDisabled: boolean
35
45
  title: string
36
46
  unreadCount: number
37
- unreadReactionCount?: number // Derived from Jolt events
47
+ unreadReaction: boolean
38
48
  updatedAt: string
39
49
  }
@@ -0,0 +1,47 @@
1
+ import { isResponseLike, ResponseError, throwResponseError } from '../response_error'
2
+
3
+ // Mimics Expo's winter `FetchResponse`: the full Response shape, but NOT an
4
+ // instance of the global `Response` (so `instanceof Response` returns false).
5
+ const fetchResponseLike = (status: number) => ({
6
+ status,
7
+ ok: status >= 200 && status < 300,
8
+ statusText: 'unauthorized',
9
+ url: 'https://api.planningcenteronline.com/chat/v2/me',
10
+ redirected: false,
11
+ headers: {},
12
+ clone() {
13
+ return this
14
+ },
15
+ json: async () => ({ errors: [] }),
16
+ text: async () => '',
17
+ arrayBuffer: async () => new ArrayBuffer(0),
18
+ })
19
+
20
+ describe('isResponseLike', () => {
21
+ it('recognizes a Response by shape even when it is not an instanceof Response', () => {
22
+ const notARealResponse = fetchResponseLike(401)
23
+ expect(notARealResponse instanceof Response).toBe(false)
24
+ expect(isResponseLike(notARealResponse)).toBe(true)
25
+ })
26
+
27
+ it('rejects things that are not responses', () => {
28
+ expect(isResponseLike(null)).toBe(false)
29
+ expect(isResponseLike(new Error('boom'))).toBe(false)
30
+ expect(isResponseLike(new ResponseError({ status: 401 } as never))).toBe(false)
31
+ expect(isResponseLike({ status: 401 })).toBe(false)
32
+ expect(isResponseLike({ status: 401, ok: false, clone() {}, json: async () => ({}) })).toBe(
33
+ false
34
+ )
35
+ })
36
+ })
37
+
38
+ describe('throwResponseError', () => {
39
+ it('wraps a Response-like rejection into a ResponseError', () => {
40
+ expect(() => throwResponseError(fetchResponseLike(401))).toThrow(ResponseError)
41
+ })
42
+
43
+ it('passes through non-response errors untouched', async () => {
44
+ const error = new Error('network down')
45
+ await expect(throwResponseError(error)).rejects.toBe(error)
46
+ })
47
+ })
@@ -1,4 +1,5 @@
1
1
  import { ApiCollection, ApiResource, FailedResponse } from '../../types'
2
+ import { isResponseLike } from '../response_error'
2
3
  import {
3
4
  concatRecords,
4
5
  ensureNoQueryParamsInDev,
@@ -106,7 +107,7 @@ export class Client {
106
107
  }
107
108
 
108
109
  handleNotOk = async (response: Response | Error) => {
109
- if (!(response instanceof Response)) return Promise.reject(response)
110
+ if (!isResponseLike(response)) return Promise.reject(response)
110
111
 
111
112
  const errorData = await this.parseErrorResponse(response)
112
113
  const notOkResponse = response.clone() as FailedResponse
@@ -38,6 +38,7 @@ export const getConversationsRequestArgs = ({
38
38
  Conversation: [
39
39
  'created_at',
40
40
  'badges',
41
+ 'conversation_preview',
41
42
  'disabled',
42
43
  'disabled_reason',
43
44
  'groups',
@@ -55,6 +56,7 @@ export const getConversationsRequestArgs = ({
55
56
  'replies_disabled',
56
57
  'title',
57
58
  'unread_count',
59
+ 'unread_reaction',
58
60
  'updated_at',
59
61
  ],
60
62
  AnalyticsMetadata: ['metadata'],
@@ -16,8 +16,30 @@ export class ResponseError extends Error {
16
16
  }
17
17
  }
18
18
 
19
+ // Detect a fetch Response by shape, not `instanceof`: Expo's winter runtime
20
+ // swaps in a `fetch` whose `FetchResponse` isn't an instance of the global
21
+ // `Response`, so `instanceof` silently misses it under Expo.
22
+ export const isResponseLike = (value: unknown): value is Response => {
23
+ if (typeof value !== 'object' || value === null) return false
24
+
25
+ const candidate = value as Record<string, unknown>
26
+ return (
27
+ typeof candidate.status === 'number' &&
28
+ typeof candidate.statusText === 'string' &&
29
+ typeof candidate.ok === 'boolean' &&
30
+ typeof candidate.redirected === 'boolean' &&
31
+ typeof candidate.url === 'string' &&
32
+ typeof candidate.headers === 'object' &&
33
+ candidate.headers !== null &&
34
+ typeof candidate.clone === 'function' &&
35
+ typeof candidate.json === 'function' &&
36
+ typeof candidate.text === 'function' &&
37
+ typeof candidate.arrayBuffer === 'function'
38
+ )
39
+ }
40
+
19
41
  export const throwResponseError = (error: unknown) => {
20
- if (error instanceof Response) {
42
+ if (isResponseLike(error)) {
21
43
  throw new ResponseError(error as FailedResponse)
22
44
  }
23
45