@planningcenter/chat-react-native 3.42.2-qa-staging.2 → 3.42.2-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 (60) hide show
  1. package/build/components/conversations/conversation_preview.d.ts.map +1 -1
  2. package/build/components/conversations/conversation_preview.js +12 -46
  3. package/build/components/conversations/conversation_preview.js.map +1 -1
  4. package/build/components/conversations/unread_count_badge.d.ts +1 -2
  5. package/build/components/conversations/unread_count_badge.d.ts.map +1 -1
  6. package/build/components/conversations/unread_count_badge.js +4 -5
  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 +0 -2
  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 +19 -4
  13. package/build/hooks/use_conversation_jolt_events.js.map +1 -1
  14. package/build/hooks/use_conversation_messages.d.ts +1 -0
  15. package/build/hooks/use_conversation_messages.d.ts.map +1 -1
  16. package/build/hooks/use_conversation_messages.js +2 -2
  17. package/build/hooks/use_conversation_messages.js.map +1 -1
  18. package/build/hooks/use_conversations_actions.d.ts.map +1 -1
  19. package/build/hooks/use_conversations_actions.js +1 -3
  20. package/build/hooks/use_conversations_actions.js.map +1 -1
  21. package/build/hooks/use_mark_latest_message_read.d.ts.map +1 -1
  22. package/build/hooks/use_mark_latest_message_read.js +4 -3
  23. package/build/hooks/use_mark_latest_message_read.js.map +1 -1
  24. package/build/hooks/use_suspense_api.d.ts.map +1 -1
  25. package/build/hooks/use_suspense_api.js +7 -1
  26. package/build/hooks/use_suspense_api.js.map +1 -1
  27. package/build/screens/conversation_screen.d.ts.map +1 -1
  28. package/build/screens/conversation_screen.js +2 -2
  29. package/build/screens/conversation_screen.js.map +1 -1
  30. package/build/screens/conversations/components/list_header_component.js +1 -1
  31. package/build/screens/conversations/components/list_header_component.js.map +1 -1
  32. package/build/types/resources/conversation.d.ts +1 -10
  33. package/build/types/resources/conversation.d.ts.map +1 -1
  34. package/build/types/resources/conversation.js.map +1 -1
  35. package/build/utils/client/client.d.ts.map +1 -1
  36. package/build/utils/client/client.js +1 -2
  37. package/build/utils/client/client.js.map +1 -1
  38. package/build/utils/request/conversation.d.ts.map +1 -1
  39. package/build/utils/request/conversation.js +0 -2
  40. package/build/utils/request/conversation.js.map +1 -1
  41. package/build/utils/response_error.d.ts +0 -1
  42. package/build/utils/response_error.d.ts.map +1 -1
  43. package/build/utils/response_error.js +1 -10
  44. package/build/utils/response_error.js.map +1 -1
  45. package/package.json +3 -3
  46. package/src/components/conversations/conversation_preview.tsx +23 -58
  47. package/src/components/conversations/unread_count_badge.tsx +4 -13
  48. package/src/hooks/use_conversation.ts +0 -2
  49. package/src/hooks/use_conversation_jolt_events.ts +19 -3
  50. package/src/hooks/use_conversation_messages.ts +6 -5
  51. package/src/hooks/use_conversations_actions.ts +1 -3
  52. package/src/hooks/use_mark_latest_message_read.ts +4 -3
  53. package/src/hooks/use_suspense_api.ts +9 -1
  54. package/src/screens/conversation_screen.tsx +4 -2
  55. package/src/screens/conversations/components/list_header_component.tsx +6 -6
  56. package/src/types/resources/conversation.ts +1 -11
  57. package/src/utils/client/client.ts +1 -2
  58. package/src/utils/request/conversation.ts +0 -2
  59. package/src/utils/response_error.ts +1 -12
  60. package/src/utils/__tests__/response_error.test.ts +0 -40
@@ -6,10 +6,8 @@ import {
6
6
  useFontScale,
7
7
  useScalableNumberOfLines,
8
8
  } from '../../hooks'
9
- import { ConversationPreviewData, ConversationResource } from '../../types'
10
- import { REACTION_EMOJIS, platformFontWeightBold } from '../../utils'
9
+ import { ConversationResource } from '../../types'
11
10
  import { formatDatePreview } from '../../utils/date'
12
- import { tokens } from '../../vendor/tapestry/tokens'
13
11
  import { AvatarGroup, ConversationAvatar, Heading, Text, Badge, type BadgeProps } from '../display'
14
12
  import { ConversationActions } from './conversation_actions'
15
13
  import { MuteIndicator } from './mute_indicator'
@@ -32,26 +30,25 @@ export const ConversationPreview = ({
32
30
  }: ConversationPreviewProps) => {
33
31
  const styles = useStyles()
34
32
  const {
35
- conversationPreview,
33
+ lastMessageAuthorName,
36
34
  lastMessageCreatedAt,
35
+ lastMessageTextPreview,
37
36
  title,
38
37
  unreadCount,
39
38
  badges,
40
39
  muted,
41
40
  disabled,
42
- unreadReaction,
43
41
  } = conversation
44
42
 
45
43
  const emptyConversation = !lastMessageCreatedAt
46
44
  const fallbackIconName = emptyConversation ? 'people.noTextMessage' : 'general.person'
47
45
  const hasMetaContent = lastMessageCreatedAt || unreadCount > 0 || muted
48
- const isUnread = unreadCount > 0 || unreadReaction
49
46
 
50
47
  const numberOfLinesScaledTitle = useScalableNumberOfLines(1)
51
48
  const numberOfLinesScaledPreviewText = useScalableNumberOfLines(2)
52
49
 
53
- const conversationPreviewText = conversationPreview
54
- ? formatConversationPreviewText(conversationPreview)
50
+ const conversationPreviewText = lastMessageTextPreview
51
+ ? `${lastMessageAuthorName}: ${lastMessageTextPreview}`
55
52
  : EMPTY_CONVERSATION_PREVIEW_TEXT
56
53
 
57
54
  const conversationAccessibilityLabel = getConversationAccessibilityLabel({
@@ -72,20 +69,13 @@ export const ConversationPreview = ({
72
69
  fallbackIconName={fallbackIconName}
73
70
  />
74
71
  <View style={styles.conversationBody}>
75
- <Heading
76
- numberOfLines={numberOfLinesScaledTitle}
77
- variant="h3"
78
- style={[styles.title, isUnread && styles.unreadTitle]}
79
- >
72
+ <Heading numberOfLines={numberOfLinesScaledTitle} variant="h3" style={styles.title}>
80
73
  {title}
81
74
  </Heading>
82
75
  <Text
83
76
  variant="tertiary"
84
77
  numberOfLines={numberOfLinesScaledPreviewText}
85
- style={[
86
- emptyConversation && styles.emptyConversationPreviewText,
87
- isUnread && styles.unreadText,
88
- ]}
78
+ style={emptyConversation && styles.emptyConversationPreviewText}
89
79
  >
90
80
  {conversationPreviewText}
91
81
  </Text>
@@ -93,9 +83,9 @@ export const ConversationPreview = ({
93
83
  </View>
94
84
  {hasMetaContent && (
95
85
  <View style={styles.metaContainer}>
96
- <Text variant="tertiary">{formatDatePreview(conversationPreview?.createdAt)}</Text>
86
+ <Text variant="tertiary">{formatDatePreview(lastMessageCreatedAt)}</Text>
97
87
  <View style={styles.statusContainer}>
98
- <UnreadCountBadge count={unreadCount} unreadReaction={unreadReaction} showDot={muted} />
88
+ <UnreadCountBadge count={unreadCount} showDot={muted} />
99
89
  <MuteIndicator muted={muted} />
100
90
  </View>
101
91
  </View>
@@ -162,30 +152,6 @@ export const ConversationPreviewSkeleton = () => {
162
152
  )
163
153
  }
164
154
 
165
- function reactionEmoji(value: ConversationPreviewData['value']): string | null {
166
- if (!value) return null
167
- return REACTION_EMOJIS[value] ?? null
168
- }
169
-
170
- function formatConversationPreviewText(preview: ConversationPreviewData) {
171
- if (preview.kind === 'reaction') {
172
- const emoji = reactionEmoji(preview.value)
173
- return `${preview.authorName}${emoji ? ` ${emoji}` : ' reacted'} to: "${preview.text}"`
174
- }
175
-
176
- return `${preview.authorName}: ${preview.text}`
177
- }
178
-
179
- function formatConversationPreviewAccessibilityLabel(preview: ConversationPreviewData) {
180
- const date = formatDatePreview(preview.createdAt)
181
-
182
- if (preview.kind === 'reaction') {
183
- return `${preview.authorName} reacted to your message sent ${date} and says ${preview.text}`
184
- }
185
-
186
- return `Last message sent ${date} from ${preview.authorName} and says ${preview.text}`
187
- }
188
-
189
155
  interface GetConversationAccessibilityLabelProps {
190
156
  conversation: ConversationResource
191
157
  showBadges: boolean
@@ -194,24 +160,29 @@ const getConversationAccessibilityLabel = ({
194
160
  conversation,
195
161
  showBadges,
196
162
  }: GetConversationAccessibilityLabelProps) => {
197
- const { badges, title, conversationPreview, muted, disabled, unreadCount, unreadReaction } =
198
- conversation
163
+ const {
164
+ badges,
165
+ title,
166
+ lastMessageAuthorName,
167
+ lastMessageTextPreview,
168
+ muted,
169
+ disabled,
170
+ unreadCount,
171
+ lastMessageCreatedAt,
172
+ } = conversation
199
173
 
200
174
  const hasBadges = badges && showBadges
201
175
  const hasUnreadMessages = unreadCount > 0
176
+ const hasLastMessage = lastMessageCreatedAt
202
177
 
203
178
  const label = {
204
179
  title,
205
180
  badge: hasBadges ? `for ${badges[0]?.pcoResourceType} ${badges[0]?.text || ''}` : '',
206
181
  disabled: disabled ? 'Disabled' : '',
207
182
  muted: muted ? 'Muted' : '',
208
- readStatus: hasUnreadMessages
209
- ? `${unreadCount} unread messages`
210
- : unreadReaction
211
- ? 'unread reaction'
212
- : '',
213
- lastMessage: conversationPreview
214
- ? formatConversationPreviewAccessibilityLabel(conversationPreview)
183
+ readStatus: hasUnreadMessages ? `${unreadCount} unread messages` : '',
184
+ lastMessage: hasLastMessage
185
+ ? `Last message sent ${formatDatePreview(lastMessageCreatedAt)} from ${lastMessageAuthorName} and says ${lastMessageTextPreview}`
215
186
  : EMPTY_CONVERSATION_PREVIEW_TEXT,
216
187
  }
217
188
 
@@ -251,12 +222,6 @@ const useStyles = () => {
251
222
  fontSize: 16,
252
223
  color: colors.textColorDefaultPrimary,
253
224
  },
254
- unreadTitle: {
255
- fontWeight: tokens.fontWeightBold,
256
- },
257
- unreadText: {
258
- fontWeight: platformFontWeightBold,
259
- },
260
225
  titleLoading: {
261
226
  width: 120,
262
227
  height: 20,
@@ -4,21 +4,12 @@ import { useFontScale, useTheme } from '../../hooks'
4
4
  import { MAX_FONT_SIZE_MULTIPLIER, platformFontWeightBold } from '../../utils'
5
5
  import { Text } from '../display'
6
6
 
7
- export const UnreadCountBadge = ({
8
- count,
9
- unreadReaction,
10
- showDot,
11
- }: {
12
- count: number
13
- unreadReaction: boolean
14
- showDot: boolean
15
- }) => {
7
+ export const UnreadCountBadge = ({ count, showDot }: { count: number; showDot: boolean }) => {
16
8
  const styles = useStyles()
17
9
  const displayCount = count > 99 ? '99+' : count
18
- const isUnread = count > 0 || unreadReaction
19
10
 
20
- if (!isUnread) return null
21
- if (showDot || count === 0) return <View style={styles.dot} />
11
+ if (count === 0) return null
12
+ if (showDot) return <View style={styles.dot} />
22
13
 
23
14
  return (
24
15
  <View style={styles.badge}>
@@ -33,7 +24,7 @@ const useStyles = () => {
33
24
  const { colors } = useTheme()
34
25
  const dotScaleLimit = MAX_FONT_SIZE_MULTIPLIER * 1.5
35
26
  const fontScale = useFontScale({ maxFontSizeMultiplier: dotScaleLimit })
36
- const size = 10 * fontScale
27
+ const size = 8 * fontScale
37
28
 
38
29
  return StyleSheet.create({
39
30
  badge: {
@@ -16,7 +16,6 @@ export const getConversationRequestArgs = ({ conversation_id }: { conversation_i
16
16
  'created_at',
17
17
  'badges',
18
18
  'conversation_membership',
19
- 'conversation_preview',
20
19
  'gender_option',
21
20
  'groups',
22
21
  'last_message_author_id',
@@ -37,7 +36,6 @@ export const getConversationRequestArgs = ({ conversation_id }: { conversation_i
37
36
  'replies_disabled',
38
37
  'title',
39
38
  'unread_count',
40
- 'unread_reaction',
41
39
  'updated_at',
42
40
  ],
43
41
  AnalyticsMetadata: ['metadata'],
@@ -63,7 +63,7 @@ export function useConversationJoltEvents({ conversationId }: Props) {
63
63
  data: {
64
64
  ...prev.data,
65
65
  latestReadMessageSortKey: latest_read_message_sort_key,
66
- unreadReaction: false,
66
+ unreadReactionCount: 0,
67
67
  },
68
68
  }
69
69
  })
@@ -87,9 +87,25 @@ export function useConversationJoltEvents({ conversationId }: Props) {
87
87
 
88
88
  const handleReactionJoltEvent = useCallback(
89
89
  (event: JoltReactionEvent) => {
90
- if (event.data.data.author_id === currentPersonId) return
90
+ queryClient.setQueryData<ApiResource<ConversationResource>>(queryKey, prev => {
91
+ if (!prev?.data) return prev
91
92
 
92
- queryClient.invalidateQueries({ queryKey })
93
+ if (event.data.data.author_id === currentPersonId) {
94
+ return prev
95
+ }
96
+
97
+ return {
98
+ ...prev,
99
+ data: {
100
+ ...prev.data,
101
+ // Not a real count, just a derived value from Jolt events so we can
102
+ // determine if we should mark the conversation as read
103
+ unreadReactionCount: prev.data.unreadReactionCount
104
+ ? prev.data.unreadReactionCount + 1
105
+ : 1,
106
+ },
107
+ }
108
+ })
93
109
  },
94
110
  [queryClient, queryKey, currentPersonId]
95
111
  )
@@ -7,10 +7,11 @@ export const useConversationMessages = (
7
7
  { conversation_id, reply_root_id }: { conversation_id: number; reply_root_id?: string | null },
8
8
  opts?: SuspensePaginatorOptions
9
9
  ) => {
10
- const { data, refetch, isRefetching, fetchNextPage } = useSuspensePaginator<MessageResource>(
11
- getMessagesRequestArgs({ conversation_id, reply_root_id }),
12
- opts
13
- )
10
+ const { data, refetch, isRefetching, fetchNextPage, hasNextPage } =
11
+ useSuspensePaginator<MessageResource>(
12
+ getMessagesRequestArgs({ conversation_id, reply_root_id }),
13
+ opts
14
+ )
14
15
  const queryKey = getMessagesQueryKey({ conversation_id, reply_root_id })
15
16
  const messages = useMemo(
16
17
  () =>
@@ -24,5 +25,5 @@ export const useConversationMessages = (
24
25
  [data]
25
26
  )
26
27
 
27
- return { messages, refetch, isRefetching, fetchNextPage, queryKey }
28
+ return { messages, refetch, isRefetching, fetchNextPage, hasNextPage, queryKey }
28
29
  }
@@ -20,7 +20,7 @@ export const useConversationsMarkRead = ({
20
20
  currentPersonCache.update({}, person => {
21
21
  const currentUnread = person.unreadCount
22
22
  const updatedUnread = read ? Math.max(currentUnread - 1, 0) : currentUnread + 1
23
- return { ...person, unreadCount: updatedUnread }
23
+ return { ...person, unreadCount: updatedUnread, unreadReactionCount: 0 }
24
24
  })
25
25
  }
26
26
 
@@ -29,7 +29,6 @@ export const useConversationsMarkRead = ({
29
29
  update({
30
30
  ...conversation,
31
31
  unreadCount: read ? 0 : 1,
32
- ...(read ? { unreadReaction: false } : {}),
33
32
  })
34
33
  handlePersonUnreadCount(read)
35
34
  },
@@ -107,7 +106,6 @@ export const useMarkAllRead = () => {
107
106
  onMutate: () => {
108
107
  updateAll({
109
108
  unreadCount: 0,
110
- unreadReaction: false,
111
109
  })
112
110
  },
113
111
  mutationKey: ['markAllRead', args],
@@ -17,9 +17,9 @@ export function useMarkLatestMessageRead({ conversation }: Props) {
17
17
  [markRead]
18
18
  )
19
19
  const unreadCount = conversation.unreadCount
20
- const unreadReaction = conversation.unreadReaction
20
+ const unreadReactionCount = conversation.unreadReactionCount || 0
21
21
 
22
- const shouldMarkRead = Boolean(unreadCount >= 1 || unreadReaction || !firedOnce.current)
22
+ const shouldMarkRead = Boolean(unreadCount >= 1 || unreadReactionCount > 0 || !firedOnce.current)
23
23
  const appState = useAppState()
24
24
  const isActive = appState === 'active'
25
25
 
@@ -29,5 +29,6 @@ export function useMarkLatestMessageRead({ conversation }: Props) {
29
29
  firedOnce.current = true
30
30
 
31
31
  debouncedMarkRead(true)
32
- }, [debouncedMarkRead, isActive, shouldMarkRead, unreadReaction])
32
+ // keeping unreadReactionCount in the dependency array to watch for changes
33
+ }, [debouncedMarkRead, isActive, shouldMarkRead, unreadReactionCount])
33
34
  }
@@ -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 { throwResponseError } from '../utils/response_error'
12
+ import { ResponseError } from '../utils/response_error'
13
13
  import { getNextPageParamFromMeta } from './paginator_meta'
14
14
  import { App, useApiClient } from './use_api_client'
15
15
 
@@ -90,6 +90,14 @@ 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
+
93
101
  export type RequestQueryKey = [
94
102
  SuspenseGetOptions['url'],
95
103
  SuspenseGetOptions['data'],
@@ -89,7 +89,7 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
89
89
  const { conversation_id, editing_message_id, reply_root_id, reply_root_author_name } =
90
90
  route.params
91
91
  const { data: conversation } = useConversation(route.params)
92
- const { messages, fetchNextPage } = useConversationMessages({
92
+ const { messages, fetchNextPage, hasNextPage } = useConversationMessages({
93
93
  conversation_id,
94
94
  reply_root_id,
95
95
  })
@@ -181,7 +181,9 @@ function ConversationScreenContent({ route }: ConversationScreenProps) {
181
181
  onScroll={trackScroll}
182
182
  scrollEventThrottle={10}
183
183
  ListFooterComponent={
184
- <ConversationStartBanner conversation={conversation} hasMessages={true} />
184
+ hasNextPage ? null : (
185
+ <ConversationStartBanner conversation={conversation} hasMessages={true} />
186
+ )
185
187
  }
186
188
  renderItem={({ item }) => {
187
189
  if (item.type === 'DateSeparator') {
@@ -158,6 +158,12 @@ export const ListHeaderComponent = () => {
158
158
  </TextButton>
159
159
  </View>
160
160
  <View style={styles.actionsContainer}>
161
+ <ListHeaderActionButton
162
+ name="general.cog"
163
+ onPress={handleChatSettings}
164
+ accessibilityLabel="Chat settings"
165
+ maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
166
+ />
161
167
  {canCreateConversations && (
162
168
  <ListHeaderActionButton
163
169
  name="churchCenter.signups"
@@ -167,12 +173,6 @@ export const ListHeaderComponent = () => {
167
173
  maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
168
174
  />
169
175
  )}
170
- <ListHeaderActionButton
171
- name="general.cog"
172
- onPress={handleChatSettings}
173
- accessibilityLabel="Chat settings"
174
- maxFontSizeMultiplier={MAX_FONT_SIZE_MULTIPLIER_LANDMARK}
175
- />
176
176
  {shouldShowHeaderFiltersButton && <ToggleButton {...moreFiltersButtonProps} />}
177
177
  </View>
178
178
  </View>
@@ -3,25 +3,15 @@ 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'
7
6
 
8
7
  export type ConversationDisabledReason = 'safety_check' | 'direct_messages_disabled'
9
8
 
10
- export interface ConversationPreviewData {
11
- kind: 'reaction' | 'message'
12
- authorName: string
13
- value: ReactionCountResource['value'] | null
14
- text: string
15
- createdAt: string
16
- }
17
-
18
9
  export interface ConversationResource {
19
10
  type: 'Conversation'
20
11
  id: number
21
12
  badges?: ConversationBadgeResource[]
22
13
  analyticsMetadata?: AnalyticsMetadataResource
23
14
  conversationMembership?: Partial<ConversationMembershipResource>
24
- conversationPreview: ConversationPreviewData | null
25
15
  createdAt: string
26
16
  deleted?: boolean
27
17
  directMessage?: boolean
@@ -44,6 +34,6 @@ export interface ConversationResource {
44
34
  repliesDisabled: boolean
45
35
  title: string
46
36
  unreadCount: number
47
- unreadReaction: boolean
37
+ unreadReactionCount?: number // Derived from Jolt events
48
38
  updatedAt: string
49
39
  }
@@ -1,5 +1,4 @@
1
1
  import { ApiCollection, ApiResource, FailedResponse } from '../../types'
2
- import { isResponseLike } from '../response_error'
3
2
  import {
4
3
  concatRecords,
5
4
  ensureNoQueryParamsInDev,
@@ -107,7 +106,7 @@ export class Client {
107
106
  }
108
107
 
109
108
  handleNotOk = async (response: Response | Error) => {
110
- if (!isResponseLike(response)) return Promise.reject(response)
109
+ if (!(response instanceof Response)) return Promise.reject(response)
111
110
 
112
111
  const errorData = await this.parseErrorResponse(response)
113
112
  const notOkResponse = response.clone() as FailedResponse
@@ -38,7 +38,6 @@ export const getConversationsRequestArgs = ({
38
38
  Conversation: [
39
39
  'created_at',
40
40
  'badges',
41
- 'conversation_preview',
42
41
  'disabled',
43
42
  'disabled_reason',
44
43
  'groups',
@@ -56,7 +55,6 @@ export const getConversationsRequestArgs = ({
56
55
  'replies_disabled',
57
56
  'title',
58
57
  'unread_count',
59
- 'unread_reaction',
60
58
  'updated_at',
61
59
  ],
62
60
  AnalyticsMetadata: ['metadata'],
@@ -16,19 +16,8 @@ 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
- typeof value === 'object' &&
24
- value !== null &&
25
- typeof (value as Response).status === 'number' &&
26
- typeof (value as Response).ok === 'boolean' &&
27
- typeof (value as Response).clone === 'function' &&
28
- typeof (value as Response).json === 'function'
29
-
30
19
  export const throwResponseError = (error: unknown) => {
31
- if (isResponseLike(error)) {
20
+ if (error instanceof Response) {
32
21
  throw new ResponseError(error as FailedResponse)
33
22
  }
34
23
 
@@ -1,40 +0,0 @@
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
- clone() {
11
- return this
12
- },
13
- json: async () => ({ errors: [] }),
14
- })
15
-
16
- describe('isResponseLike', () => {
17
- it('recognizes a Response by shape even when it is not an instanceof Response', () => {
18
- const notARealResponse = fetchResponseLike(401)
19
- expect(notARealResponse instanceof Response).toBe(false)
20
- expect(isResponseLike(notARealResponse)).toBe(true)
21
- })
22
-
23
- it('rejects things that are not responses', () => {
24
- expect(isResponseLike(null)).toBe(false)
25
- expect(isResponseLike(new Error('boom'))).toBe(false)
26
- expect(isResponseLike(new ResponseError({ status: 401 } as never))).toBe(false)
27
- expect(isResponseLike({ status: 401 })).toBe(false)
28
- })
29
- })
30
-
31
- describe('throwResponseError', () => {
32
- it('wraps a Response-like rejection into a ResponseError', () => {
33
- expect(() => throwResponseError(fetchResponseLike(401))).toThrow(ResponseError)
34
- })
35
-
36
- it('passes through non-response errors untouched', async () => {
37
- const error = new Error('network down')
38
- await expect(throwResponseError(error)).rejects.toBe(error)
39
- })
40
- })