@planningcenter/chat-react-native 3.35.0-rc.3 → 3.35.0-rc.4
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/README.md +1 -1
- package/build/components/conversation/jump_to_bottom_button.d.ts +2 -1
- package/build/components/conversation/jump_to_bottom_button.d.ts.map +1 -1
- package/build/components/conversation/jump_to_bottom_button.js +39 -7
- package/build/components/conversation/jump_to_bottom_button.js.map +1 -1
- package/build/components/conversation/reply_shadow_message.d.ts +1 -2
- package/build/components/conversation/reply_shadow_message.d.ts.map +1 -1
- package/build/components/conversation/reply_shadow_message.js.map +1 -1
- package/build/components/conversation/unread_divider.d.ts +6 -0
- package/build/components/conversation/unread_divider.d.ts.map +1 -0
- package/build/components/conversation/unread_divider.js +59 -0
- package/build/components/conversation/unread_divider.js.map +1 -0
- package/build/contexts/conversation_context.d.ts +2 -0
- package/build/contexts/conversation_context.d.ts.map +1 -1
- package/build/contexts/conversation_context.js +13 -5
- package/build/contexts/conversation_context.js.map +1 -1
- package/build/hooks/use_conversation_messages.d.ts +2 -0
- package/build/hooks/use_conversation_messages.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages.js +9 -5
- package/build/hooks/use_conversation_messages.js.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.d.ts.map +1 -1
- package/build/hooks/use_conversation_messages_jolt_events.js +4 -4
- package/build/hooks/use_conversation_messages_jolt_events.js.map +1 -1
- package/build/hooks/use_conversations_actions.d.ts +5 -0
- package/build/hooks/use_conversations_actions.d.ts.map +1 -1
- package/build/hooks/use_conversations_actions.js +12 -0
- package/build/hooks/use_conversations_actions.js.map +1 -1
- package/build/hooks/use_flat_list_viewability.d.ts +20 -0
- package/build/hooks/use_flat_list_viewability.d.ts.map +1 -0
- package/build/hooks/use_flat_list_viewability.js +30 -0
- package/build/hooks/use_flat_list_viewability.js.map +1 -0
- package/build/hooks/use_jump_to_bottom_action.d.ts +9 -0
- package/build/hooks/use_jump_to_bottom_action.d.ts.map +1 -0
- package/build/hooks/use_jump_to_bottom_action.js +62 -0
- package/build/hooks/use_jump_to_bottom_action.js.map +1 -0
- package/build/hooks/use_jump_to_unread_anchor.d.ts +20 -0
- package/build/hooks/use_jump_to_unread_anchor.d.ts.map +1 -0
- package/build/hooks/use_jump_to_unread_anchor.js +53 -0
- package/build/hooks/use_jump_to_unread_anchor.js.map +1 -0
- package/build/hooks/use_jump_to_unread_gates.d.ts +5 -0
- package/build/hooks/use_jump_to_unread_gates.d.ts.map +1 -0
- package/build/hooks/use_jump_to_unread_gates.js +10 -0
- package/build/hooks/use_jump_to_unread_gates.js.map +1 -0
- package/build/hooks/use_mark_latest_message_read.d.ts +1 -1
- package/build/hooks/use_mark_latest_message_read.d.ts.map +1 -1
- package/build/hooks/use_mark_latest_message_read.js +17 -1
- package/build/hooks/use_mark_latest_message_read.js.map +1 -1
- package/build/hooks/use_scroll_tracking.d.ts +13 -0
- package/build/hooks/use_scroll_tracking.d.ts.map +1 -0
- package/build/hooks/use_scroll_tracking.js +45 -0
- package/build/hooks/use_scroll_tracking.js.map +1 -0
- package/build/hooks/use_track_highest_seen_message.d.ts +4 -0
- package/build/hooks/use_track_highest_seen_message.d.ts.map +1 -0
- package/build/hooks/use_track_highest_seen_message.js +35 -0
- package/build/hooks/use_track_highest_seen_message.js.map +1 -0
- package/build/navigation/index.d.ts.map +1 -1
- package/build/screens/conversation_screen.d.ts.map +1 -1
- package/build/screens/conversation_screen.js +87 -44
- package/build/screens/conversation_screen.js.map +1 -1
- package/build/utils/cache/messages_cache.d.ts +1 -0
- package/build/utils/cache/messages_cache.d.ts.map +1 -1
- package/build/utils/cache/messages_cache.js +4 -0
- package/build/utils/cache/messages_cache.js.map +1 -1
- package/build/utils/group_messages.d.ts +9 -2
- package/build/utils/group_messages.d.ts.map +1 -1
- package/build/utils/group_messages.js +20 -1
- package/build/utils/group_messages.js.map +1 -1
- package/build/utils/highest_seen_tracker.d.ts +12 -0
- package/build/utils/highest_seen_tracker.d.ts.map +1 -0
- package/build/utils/highest_seen_tracker.js +37 -0
- package/build/utils/highest_seen_tracker.js.map +1 -0
- package/build/utils/message_viewability.d.ts +24 -0
- package/build/utils/message_viewability.d.ts.map +1 -0
- package/build/utils/message_viewability.js +29 -0
- package/build/utils/message_viewability.js.map +1 -0
- package/build/utils/unread_divider_helpers.d.ts +18 -0
- package/build/utils/unread_divider_helpers.d.ts.map +1 -0
- package/build/utils/unread_divider_helpers.js +13 -0
- package/build/utils/unread_divider_helpers.js.map +1 -0
- package/package.json +10 -4
- package/src/__tests__/contexts/session_context.tsx +1 -1
- package/src/__tests__/hooks/use_async_storage.test.tsx +1 -1
- package/src/__tests__/hooks/use_attachment_uploader.test.tsx +1 -1
- package/src/__tests__/hooks/use_chat_configuration.test.tsx +1 -1
- package/src/__tests__/hooks/use_conversation_messages.test.tsx +1 -1
- package/src/__tests__/hooks/use_mark_latest_message_read.test.tsx +154 -0
- package/src/__tests__/utils/cache/messages_cache.test.ts +54 -0
- package/src/components/conversation/jump_to_bottom_button.tsx +57 -8
- package/src/components/conversation/reply_shadow_message.tsx +4 -2
- package/src/components/conversation/unread_divider.tsx +90 -0
- package/src/contexts/conversation_context.tsx +15 -13
- package/src/hooks/use_conversation_messages.ts +19 -3
- package/src/hooks/use_conversation_messages_jolt_events.ts +4 -3
- package/src/hooks/use_conversations_actions.ts +15 -0
- package/src/hooks/use_flat_list_viewability.ts +50 -0
- package/src/hooks/use_jump_to_bottom_action.ts +75 -0
- package/src/hooks/use_jump_to_unread_anchor.ts +68 -0
- package/src/hooks/use_jump_to_unread_gates.ts +10 -0
- package/src/hooks/use_mark_latest_message_read.ts +16 -2
- package/src/hooks/use_scroll_tracking.ts +64 -0
- package/src/hooks/use_track_highest_seen_message.ts +43 -0
- package/src/screens/conversation_screen.tsx +173 -70
- package/src/utils/__tests__/group_messages.test.ts +71 -0
- package/src/utils/__tests__/highest_seen_tracker.test.ts +82 -0
- package/src/utils/__tests__/message_viewability.test.ts +168 -0
- package/src/utils/__tests__/unread_divider_helpers.test.ts +85 -0
- package/src/utils/cache/messages_cache.ts +5 -0
- package/src/utils/group_messages.ts +42 -2
- package/src/utils/highest_seen_tracker.ts +42 -0
- package/src/utils/message_viewability.ts +49 -0
- package/src/utils/unread_divider_helpers.ts +25 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const FLUSH_DELAY_MS = 2000;
|
|
2
|
+
export function makeHighestSeenTracker(conversationId, send, flushDelayMs = FLUSH_DELAY_MS) {
|
|
3
|
+
let highest = null;
|
|
4
|
+
let lastSent = null;
|
|
5
|
+
let timer = null;
|
|
6
|
+
const fire = () => {
|
|
7
|
+
timer = null;
|
|
8
|
+
if (!highest || highest === lastSent)
|
|
9
|
+
return;
|
|
10
|
+
lastSent = highest;
|
|
11
|
+
send({ conversationId, sortKey: highest });
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
onSeen(sortKey) {
|
|
15
|
+
if (highest && sortKey.localeCompare(highest) <= 0)
|
|
16
|
+
return;
|
|
17
|
+
highest = sortKey;
|
|
18
|
+
if (timer)
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
timer = setTimeout(fire, flushDelayMs);
|
|
21
|
+
},
|
|
22
|
+
flushNow() {
|
|
23
|
+
if (timer) {
|
|
24
|
+
clearTimeout(timer);
|
|
25
|
+
timer = null;
|
|
26
|
+
}
|
|
27
|
+
fire();
|
|
28
|
+
},
|
|
29
|
+
cancel() {
|
|
30
|
+
if (timer) {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
timer = null;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=highest_seen_tracker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"highest_seen_tracker.js","sourceRoot":"","sources":["../../src/utils/highest_seen_tracker.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAA;AAIlC,MAAM,UAAU,sBAAsB,CACpC,cAAsB,EACtB,IAAY,EACZ,eAAuB,cAAc;IAErC,IAAI,OAAO,GAAkB,IAAI,CAAA;IACjC,IAAI,QAAQ,GAAkB,IAAI,CAAA;IAClC,IAAI,KAAK,GAAyC,IAAI,CAAA;IAEtD,MAAM,IAAI,GAAG,GAAG,EAAE;QAChB,KAAK,GAAG,IAAI,CAAA;QACZ,IAAI,CAAC,OAAO,IAAI,OAAO,KAAK,QAAQ;YAAE,OAAM;QAC5C,QAAQ,GAAG,OAAO,CAAA;QAClB,IAAI,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAA;IAC5C,CAAC,CAAA;IAED,OAAO;QACL,MAAM,CAAC,OAAe;YACpB,IAAI,OAAO,IAAI,OAAO,CAAC,aAAa,CAAC,OAAO,CAAC,IAAI,CAAC;gBAAE,OAAM;YAC1D,OAAO,GAAG,OAAO,CAAA;YACjB,IAAI,KAAK;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAA;YAC9B,KAAK,GAAG,UAAU,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;QACxC,CAAC;QACD,QAAQ;YACN,IAAI,KAAK,EAAE,CAAC;gBACV,YAAY,CAAC,KAAK,CAAC,CAAA;gBACnB,KAAK,GAAG,IAAI,CAAA;YACd,CAAC;YACD,IAAI,EAAE,CAAA;QACR,CAAC;QACD,MAAM;YACJ,IAAI,KAAK,EAAE,CAAC;gBACV,YAAY,CAAC,KAAK,CAAC,CAAA;gBACnB,KAAK,GAAG,IAAI,CAAA;YACd,CAAC;QACH,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["export const FLUSH_DELAY_MS = 2000\n\ntype SendFn = (args: { conversationId: number; sortKey: string }) => void\n\nexport function makeHighestSeenTracker(\n conversationId: number,\n send: SendFn,\n flushDelayMs: number = FLUSH_DELAY_MS\n) {\n let highest: string | null = null\n let lastSent: string | null = null\n let timer: ReturnType<typeof setTimeout> | null = null\n\n const fire = () => {\n timer = null\n if (!highest || highest === lastSent) return\n lastSent = highest\n send({ conversationId, sortKey: highest })\n }\n\n return {\n onSeen(sortKey: string) {\n if (highest && sortKey.localeCompare(highest) <= 0) return\n highest = sortKey\n if (timer) clearTimeout(timer)\n timer = setTimeout(fire, flushDelayMs)\n },\n flushNow() {\n if (timer) {\n clearTimeout(timer)\n timer = null\n }\n fire()\n },\n cancel() {\n if (timer) {\n clearTimeout(timer)\n timer = null\n }\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface ViewableEntry<Item> {
|
|
2
|
+
key: string;
|
|
3
|
+
isViewable: boolean;
|
|
4
|
+
item: Item;
|
|
5
|
+
}
|
|
6
|
+
export interface ViewabilityEvent<Item> {
|
|
7
|
+
viewableItems: ViewableEntry<Item>[];
|
|
8
|
+
changed: ViewableEntry<Item>[];
|
|
9
|
+
userHasScrolled: boolean;
|
|
10
|
+
}
|
|
11
|
+
export type ViewabilityObserver<Item> = (event: ViewabilityEvent<Item>) => void;
|
|
12
|
+
export declare function reportViewableMessages<Item extends {
|
|
13
|
+
id?: string;
|
|
14
|
+
type?: string;
|
|
15
|
+
}>(onMessageSeen: (id: string) => void): ViewabilityObserver<Item>;
|
|
16
|
+
export declare function detectDividerExitTowardNewer<Item extends {
|
|
17
|
+
id?: string;
|
|
18
|
+
type?: string;
|
|
19
|
+
}>({ dividerKey, initialMessageId, onExited, }: {
|
|
20
|
+
dividerKey: string;
|
|
21
|
+
initialMessageId: string | null;
|
|
22
|
+
onExited: () => void;
|
|
23
|
+
}): ViewabilityObserver<Item>;
|
|
24
|
+
//# sourceMappingURL=message_viewability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message_viewability.d.ts","sourceRoot":"","sources":["../../src/utils/message_viewability.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,aAAa,CAAC,IAAI;IACjC,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,OAAO,CAAA;IACnB,IAAI,EAAE,IAAI,CAAA;CACX;AAED,MAAM,WAAW,gBAAgB,CAAC,IAAI;IACpC,aAAa,EAAE,aAAa,CAAC,IAAI,CAAC,EAAE,CAAA;IACpC,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,EAAE,CAAA;IAC9B,eAAe,EAAE,OAAO,CAAA;CACzB;AAED,MAAM,MAAM,mBAAmB,CAAC,IAAI,IAAI,CAAC,KAAK,EAAE,gBAAgB,CAAC,IAAI,CAAC,KAAK,IAAI,CAAA;AAE/E,wBAAgB,sBAAsB,CAAC,IAAI,SAAS;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAChF,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,GAClC,mBAAmB,CAAC,IAAI,CAAC,CAS3B;AAED,wBAAgB,4BAA4B,CAAC,IAAI,SAAS;IAAE,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,EACxF,UAAU,EACV,gBAAgB,EAChB,QAAQ,GACT,EAAE;IACD,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IAC/B,QAAQ,EAAE,MAAM,IAAI,CAAA;CACrB,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAW5B"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { dividerExitedTowardNewer } from './unread_divider_helpers';
|
|
2
|
+
export function reportViewableMessages(onMessageSeen) {
|
|
3
|
+
return ({ viewableItems, userHasScrolled }) => {
|
|
4
|
+
if (!userHasScrolled)
|
|
5
|
+
return;
|
|
6
|
+
for (const entry of viewableItems) {
|
|
7
|
+
if (entry.item?.type !== 'Message')
|
|
8
|
+
continue;
|
|
9
|
+
const id = entry.item?.id;
|
|
10
|
+
if (typeof id === 'string')
|
|
11
|
+
onMessageSeen(id);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function detectDividerExitTowardNewer({ dividerKey, initialMessageId, onExited, }) {
|
|
16
|
+
return ({ viewableItems, changed, userHasScrolled }) => {
|
|
17
|
+
if (!userHasScrolled || !initialMessageId)
|
|
18
|
+
return;
|
|
19
|
+
const exited = dividerExitedTowardNewer({
|
|
20
|
+
changed,
|
|
21
|
+
viewableItems,
|
|
22
|
+
dividerKey,
|
|
23
|
+
initialMessageId,
|
|
24
|
+
});
|
|
25
|
+
if (exited)
|
|
26
|
+
onExited();
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=message_viewability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"message_viewability.js","sourceRoot":"","sources":["../../src/utils/message_viewability.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAA;AAgBnE,MAAM,UAAU,sBAAsB,CACpC,aAAmC;IAEnC,OAAO,CAAC,EAAE,aAAa,EAAE,eAAe,EAAE,EAAE,EAAE;QAC5C,IAAI,CAAC,eAAe;YAAE,OAAM;QAC5B,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;YAClC,IAAI,KAAK,CAAC,IAAI,EAAE,IAAI,KAAK,SAAS;gBAAE,SAAQ;YAC5C,MAAM,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE,EAAE,CAAA;YACzB,IAAI,OAAO,EAAE,KAAK,QAAQ;gBAAE,aAAa,CAAC,EAAE,CAAC,CAAA;QAC/C,CAAC;IACH,CAAC,CAAA;AACH,CAAC;AAED,MAAM,UAAU,4BAA4B,CAA8C,EACxF,UAAU,EACV,gBAAgB,EAChB,QAAQ,GAKT;IACC,OAAO,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,eAAe,EAAE,EAAE,EAAE;QACrD,IAAI,CAAC,eAAe,IAAI,CAAC,gBAAgB;YAAE,OAAM;QACjD,MAAM,MAAM,GAAG,wBAAwB,CAAC;YACtC,OAAO;YACP,aAAa;YACb,UAAU;YACV,gBAAgB;SACjB,CAAC,CAAA;QACF,IAAI,MAAM;YAAE,QAAQ,EAAE,CAAA;IACxB,CAAC,CAAA;AACH,CAAC","sourcesContent":["import { dividerExitedTowardNewer } from './unread_divider_helpers'\n\nexport interface ViewableEntry<Item> {\n key: string\n isViewable: boolean\n item: Item\n}\n\nexport interface ViewabilityEvent<Item> {\n viewableItems: ViewableEntry<Item>[]\n changed: ViewableEntry<Item>[]\n userHasScrolled: boolean\n}\n\nexport type ViewabilityObserver<Item> = (event: ViewabilityEvent<Item>) => void\n\nexport function reportViewableMessages<Item extends { id?: string; type?: string }>(\n onMessageSeen: (id: string) => void\n): ViewabilityObserver<Item> {\n return ({ viewableItems, userHasScrolled }) => {\n if (!userHasScrolled) return\n for (const entry of viewableItems) {\n if (entry.item?.type !== 'Message') continue\n const id = entry.item?.id\n if (typeof id === 'string') onMessageSeen(id)\n }\n }\n}\n\nexport function detectDividerExitTowardNewer<Item extends { id?: string; type?: string }>({\n dividerKey,\n initialMessageId,\n onExited,\n}: {\n dividerKey: string\n initialMessageId: string | null\n onExited: () => void\n}): ViewabilityObserver<Item> {\n return ({ viewableItems, changed, userHasScrolled }) => {\n if (!userHasScrolled || !initialMessageId) return\n const exited = dividerExitedTowardNewer({\n changed,\n viewableItems,\n dividerKey,\n initialMessageId,\n })\n if (exited) onExited()\n }\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type ViewableChangeEntry = {
|
|
2
|
+
key: string;
|
|
3
|
+
isViewable: boolean;
|
|
4
|
+
};
|
|
5
|
+
type ViewableItem = {
|
|
6
|
+
item: {
|
|
7
|
+
id?: string;
|
|
8
|
+
type?: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export declare function dividerExitedTowardNewer({ changed, viewableItems, dividerKey, initialMessageId, }: {
|
|
12
|
+
changed: ViewableChangeEntry[];
|
|
13
|
+
viewableItems: ViewableItem[];
|
|
14
|
+
dividerKey: string;
|
|
15
|
+
initialMessageId: string;
|
|
16
|
+
}): boolean;
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=unread_divider_helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unread_divider_helpers.d.ts","sourceRoot":"","sources":["../../src/utils/unread_divider_helpers.ts"],"names":[],"mappings":"AAAA,KAAK,mBAAmB,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAAA;AAC/D,KAAK,YAAY,GAAG;IAAE,IAAI,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAA;AAE5D,wBAAgB,wBAAwB,CAAC,EACvC,OAAO,EACP,aAAa,EACb,UAAU,EACV,gBAAgB,GACjB,EAAE;IACD,OAAO,EAAE,mBAAmB,EAAE,CAAA;IAC9B,aAAa,EAAE,YAAY,EAAE,CAAA;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,EAAE,MAAM,CAAA;CACzB,GAAG,OAAO,CAWV"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function dividerExitedTowardNewer({ changed, viewableItems, dividerKey, initialMessageId, }) {
|
|
2
|
+
const dividerExited = changed.some(c => c.key === dividerKey && !c.isViewable);
|
|
3
|
+
if (!dividerExited)
|
|
4
|
+
return false;
|
|
5
|
+
const visibleMessageIds = viewableItems
|
|
6
|
+
.filter(v => v.item?.type === 'Message')
|
|
7
|
+
.map(v => v.item?.id)
|
|
8
|
+
.filter((id) => !!id);
|
|
9
|
+
if (visibleMessageIds.length === 0)
|
|
10
|
+
return false;
|
|
11
|
+
return visibleMessageIds.every(id => id.localeCompare(initialMessageId) > 0);
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=unread_divider_helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unread_divider_helpers.js","sourceRoot":"","sources":["../../src/utils/unread_divider_helpers.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,wBAAwB,CAAC,EACvC,OAAO,EACP,aAAa,EACb,UAAU,EACV,gBAAgB,GAMjB;IACC,MAAM,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,UAAU,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,CAAA;IAC9E,IAAI,CAAC,aAAa;QAAE,OAAO,KAAK,CAAA;IAEhC,MAAM,iBAAiB,GAAG,aAAa;SACpC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC;SACvC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,EAAE,EAAgB,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IACrC,IAAI,iBAAiB,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAEhD,OAAO,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,aAAa,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAA;AAC9E,CAAC","sourcesContent":["type ViewableChangeEntry = { key: string; isViewable: boolean }\ntype ViewableItem = { item: { id?: string; type?: string } }\n\nexport function dividerExitedTowardNewer({\n changed,\n viewableItems,\n dividerKey,\n initialMessageId,\n}: {\n changed: ViewableChangeEntry[]\n viewableItems: ViewableItem[]\n dividerKey: string\n initialMessageId: string\n}): boolean {\n const dividerExited = changed.some(c => c.key === dividerKey && !c.isViewable)\n if (!dividerExited) return false\n\n const visibleMessageIds = viewableItems\n .filter(v => v.item?.type === 'Message')\n .map(v => v.item?.id)\n .filter((id): id is string => !!id)\n if (visibleMessageIds.length === 0) return false\n\n return visibleMessageIds.every(id => id.localeCompare(initialMessageId) > 0)\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/chat-react-native",
|
|
3
|
-
"version": "3.35.0-rc.
|
|
3
|
+
"version": "3.35.0-rc.4",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -52,9 +52,12 @@
|
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@react-native/eslint-config": "~0.83.0",
|
|
55
|
-
"@testing-library/react-
|
|
55
|
+
"@testing-library/react-native": "^13.2.0",
|
|
56
|
+
"@types/color": "^3.0.0",
|
|
56
57
|
"@types/jest": "^29.5.14",
|
|
57
|
-
"@typescript-eslint/parser": "^8.
|
|
58
|
+
"@typescript-eslint/parser": "^8.36.0",
|
|
59
|
+
"color": "^3.1.2",
|
|
60
|
+
"eslint": "8.57.1",
|
|
58
61
|
"eslint-plugin-react-compiler": "^19.1.0-rc.2",
|
|
59
62
|
"expo-module-scripts": "^55.0.0",
|
|
60
63
|
"fast-text-encoding": "^1.0.6",
|
|
@@ -62,8 +65,11 @@
|
|
|
62
65
|
"jest-fetch-mock": "^3.0.3",
|
|
63
66
|
"msw": "^2.7.3",
|
|
64
67
|
"prettier": "^3.4.2",
|
|
68
|
+
"react": "19.2.0",
|
|
69
|
+
"react-native": "0.83.6",
|
|
70
|
+
"react-native-svg": "15.15.3",
|
|
65
71
|
"react-native-url-polyfill": "^2.0.0",
|
|
66
72
|
"typescript": "~5.9.2"
|
|
67
73
|
},
|
|
68
|
-
"gitHead": "
|
|
74
|
+
"gitHead": "033e05c3a604adc1e36af2b8673719455e9e0f86"
|
|
69
75
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
2
2
|
import { QueryClientProvider } from '@tanstack/react-query'
|
|
3
|
-
import { renderHook, act } from '@testing-library/react-
|
|
3
|
+
import { renderHook, act } from '@testing-library/react-native'
|
|
4
4
|
import React, { useContext, Suspense } from 'react'
|
|
5
5
|
import { buildTestQueryClient } from '../../__utils__/query_client'
|
|
6
6
|
import {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
2
2
|
import { QueryClientProvider } from '@tanstack/react-query'
|
|
3
|
-
import { renderHook, act } from '@testing-library/react-
|
|
3
|
+
import { renderHook, act } from '@testing-library/react-native'
|
|
4
4
|
import React, { Suspense } from 'react'
|
|
5
5
|
import { buildTestQueryClient } from '../../__utils__/query_client'
|
|
6
6
|
import { useAsyncStorage } from '../../hooks/use_async_storage'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
-
import { renderHook, act } from '@testing-library/react-
|
|
2
|
+
import { renderHook, act } from '@testing-library/react-native'
|
|
3
3
|
import React from 'react'
|
|
4
4
|
import { buildTestQueryClient } from '../../__utils__/query_client'
|
|
5
5
|
import { useApiClient } from '../../hooks/use_api_client'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
-
import { renderHook, act } from '@testing-library/react-
|
|
2
|
+
import { renderHook, act } from '@testing-library/react-native'
|
|
3
3
|
import React, { Suspense } from 'react'
|
|
4
4
|
import { buildTestQueryClient } from '../../__utils__/query_client'
|
|
5
5
|
import {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
-
import { act, renderHook } from '@testing-library/react-
|
|
2
|
+
import { act, renderHook } from '@testing-library/react-native'
|
|
3
3
|
import React, { Suspense } from 'react'
|
|
4
4
|
import { buildTestQueryClient } from '../../__utils__/query_client'
|
|
5
5
|
import { ConversationContextProvider } from '../../contexts/conversation_context'
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
+
import { renderHook } from '@testing-library/react-native'
|
|
3
|
+
import React, { useEffect } from 'react'
|
|
4
|
+
import { buildTestQueryClient } from '../../__utils__/query_client'
|
|
5
|
+
import {
|
|
6
|
+
ConversationContextProvider,
|
|
7
|
+
useConversationContext,
|
|
8
|
+
} from '../../contexts/conversation_context'
|
|
9
|
+
import * as appStateModule from '../../hooks/use_app_state'
|
|
10
|
+
import * as conversationsActionsModule from '../../hooks/use_conversations_actions'
|
|
11
|
+
import * as featuresModule from '../../hooks/use_features'
|
|
12
|
+
import { useMarkLatestMessageRead } from '../../hooks/use_mark_latest_message_read'
|
|
13
|
+
import { ConversationResource } from '../../types'
|
|
14
|
+
|
|
15
|
+
const conversation = {
|
|
16
|
+
id: 1,
|
|
17
|
+
type: 'Conversation',
|
|
18
|
+
unreadCount: 3,
|
|
19
|
+
unreadReactionCount: 0,
|
|
20
|
+
conversationMembership: { lastReadMessageSortKey: '01A' },
|
|
21
|
+
} as unknown as ConversationResource
|
|
22
|
+
|
|
23
|
+
interface WrapperProps {
|
|
24
|
+
replyRootId?: string | null
|
|
25
|
+
initialMessageIdIsAnchor?: boolean
|
|
26
|
+
atEndOfMessageHistory?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const createWrapper = ({
|
|
30
|
+
replyRootId = null,
|
|
31
|
+
initialMessageIdIsAnchor = false,
|
|
32
|
+
atEndOfMessageHistory = !initialMessageIdIsAnchor,
|
|
33
|
+
}: WrapperProps = {}) => {
|
|
34
|
+
const queryClient = buildTestQueryClient()
|
|
35
|
+
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
36
|
+
<QueryClientProvider client={queryClient}>
|
|
37
|
+
<ConversationContextProvider
|
|
38
|
+
conversationId={1}
|
|
39
|
+
currentPageReplyRootId={replyRootId}
|
|
40
|
+
initialMessageId={initialMessageIdIsAnchor ? '01A' : null}
|
|
41
|
+
initialMessageIdIsAnchor={initialMessageIdIsAnchor}
|
|
42
|
+
>
|
|
43
|
+
<AtEndPrimer atEndOfMessageHistory={atEndOfMessageHistory}>{children}</AtEndPrimer>
|
|
44
|
+
</ConversationContextProvider>
|
|
45
|
+
</QueryClientProvider>
|
|
46
|
+
)
|
|
47
|
+
return Wrapper
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const AtEndPrimer = ({
|
|
51
|
+
atEndOfMessageHistory,
|
|
52
|
+
children,
|
|
53
|
+
}: {
|
|
54
|
+
atEndOfMessageHistory: boolean
|
|
55
|
+
children: React.ReactNode
|
|
56
|
+
}) => {
|
|
57
|
+
const { setAtEndOfMessageHistory } = useConversationContext()
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
setAtEndOfMessageHistory(atEndOfMessageHistory)
|
|
60
|
+
}, [atEndOfMessageHistory, setAtEndOfMessageHistory])
|
|
61
|
+
return <>{children}</>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const mockFeatures = (enabled: boolean) => {
|
|
65
|
+
jest.spyOn(featuresModule, 'useFeatures').mockReturnValue({
|
|
66
|
+
features: [],
|
|
67
|
+
featureEnabled: () => enabled,
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const mockMarkRead = () => {
|
|
72
|
+
const markRead = jest.fn()
|
|
73
|
+
jest.spyOn(conversationsActionsModule, 'useConversationsMarkRead').mockReturnValue({
|
|
74
|
+
markRead,
|
|
75
|
+
read: false,
|
|
76
|
+
} as unknown as ReturnType<typeof conversationsActionsModule.useConversationsMarkRead>)
|
|
77
|
+
return markRead
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const mockAppState = (state: string = 'active') => {
|
|
81
|
+
jest.spyOn(appStateModule, 'useAppState').mockReturnValue(state)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe('useMarkLatestMessageRead', () => {
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
jest.restoreAllMocks()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('fires markRead when JTU is off (backward compat preserved)', () => {
|
|
90
|
+
mockAppState()
|
|
91
|
+
mockFeatures(false)
|
|
92
|
+
const markRead = mockMarkRead()
|
|
93
|
+
|
|
94
|
+
renderHook(() => useMarkLatestMessageRead({ conversation }), {
|
|
95
|
+
wrapper: createWrapper(),
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
expect(markRead).toHaveBeenCalledWith(true)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('does NOT fire markRead when in a reply view (parity fix, ships flag-off)', () => {
|
|
102
|
+
mockAppState()
|
|
103
|
+
mockFeatures(false)
|
|
104
|
+
const markRead = mockMarkRead()
|
|
105
|
+
|
|
106
|
+
renderHook(() => useMarkLatestMessageRead({ conversation }), {
|
|
107
|
+
wrapper: createWrapper({ replyRootId: 'root-1' }),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
expect(markRead).not.toHaveBeenCalled()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('does NOT fire markRead when JTU is active and user is not at end of history', () => {
|
|
114
|
+
mockAppState()
|
|
115
|
+
mockFeatures(true)
|
|
116
|
+
const markRead = mockMarkRead()
|
|
117
|
+
|
|
118
|
+
renderHook(() => useMarkLatestMessageRead({ conversation }), {
|
|
119
|
+
wrapper: createWrapper({
|
|
120
|
+
initialMessageIdIsAnchor: true,
|
|
121
|
+
atEndOfMessageHistory: false,
|
|
122
|
+
}),
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
expect(markRead).not.toHaveBeenCalled()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('fires markRead when JTU is active and user reaches end of history', () => {
|
|
129
|
+
mockAppState()
|
|
130
|
+
mockFeatures(true)
|
|
131
|
+
const markRead = mockMarkRead()
|
|
132
|
+
|
|
133
|
+
renderHook(() => useMarkLatestMessageRead({ conversation }), {
|
|
134
|
+
wrapper: createWrapper({
|
|
135
|
+
initialMessageIdIsAnchor: true,
|
|
136
|
+
atEndOfMessageHistory: true,
|
|
137
|
+
}),
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
expect(markRead).toHaveBeenCalledWith(true)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('does NOT fire when app state is not active', () => {
|
|
144
|
+
mockAppState('background')
|
|
145
|
+
mockFeatures(false)
|
|
146
|
+
const markRead = mockMarkRead()
|
|
147
|
+
|
|
148
|
+
renderHook(() => useMarkLatestMessageRead({ conversation }), {
|
|
149
|
+
wrapper: createWrapper(),
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
expect(markRead).not.toHaveBeenCalled()
|
|
153
|
+
})
|
|
154
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { buildTestQueryClient } from '../../../__utils__/query_client'
|
|
2
|
+
import { hasUnloadedNewerPages } from '../../../utils/cache/messages_cache'
|
|
3
|
+
|
|
4
|
+
const queryKey = ['messages-test']
|
|
5
|
+
|
|
6
|
+
const buildClient = (data: unknown) => {
|
|
7
|
+
const client = buildTestQueryClient()
|
|
8
|
+
client.setQueryData(queryKey, data)
|
|
9
|
+
return client
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('hasUnloadedNewerPages', () => {
|
|
13
|
+
it('returns false when the query has no cached data', () => {
|
|
14
|
+
const client = buildTestQueryClient()
|
|
15
|
+
expect(hasUnloadedNewerPages(client, queryKey)).toBe(false)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('returns false when pages[0] has no next.idGt cursor', () => {
|
|
19
|
+
const client = buildClient({
|
|
20
|
+
pageParams: [{}],
|
|
21
|
+
pages: [{ data: [], links: {}, meta: { count: 0, totalCount: 0 } }],
|
|
22
|
+
})
|
|
23
|
+
expect(hasUnloadedNewerPages(client, queryKey)).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('returns false when next exists but idGt is absent', () => {
|
|
27
|
+
const client = buildClient({
|
|
28
|
+
pageParams: [{}],
|
|
29
|
+
pages: [{ data: [], links: {}, meta: { count: 0, totalCount: 0, next: { idLt: '01A' } } }],
|
|
30
|
+
})
|
|
31
|
+
expect(hasUnloadedNewerPages(client, queryKey)).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('returns true when pages[0].meta.next.idGt is set', () => {
|
|
35
|
+
const client = buildClient({
|
|
36
|
+
pageParams: [{}],
|
|
37
|
+
pages: [
|
|
38
|
+
{ data: [], links: {}, meta: { count: 0, totalCount: 0, next: { idGt: '01KQSTAY' } } },
|
|
39
|
+
],
|
|
40
|
+
})
|
|
41
|
+
expect(hasUnloadedNewerPages(client, queryKey)).toBe(true)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('inspects only the first page (the newest side after sort)', () => {
|
|
45
|
+
const client = buildClient({
|
|
46
|
+
pageParams: [{}, {}],
|
|
47
|
+
pages: [
|
|
48
|
+
{ data: [], links: {}, meta: { count: 0, totalCount: 0 } },
|
|
49
|
+
{ data: [], links: {}, meta: { count: 0, totalCount: 0, next: { idGt: '01KQSTAY' } } },
|
|
50
|
+
],
|
|
51
|
+
})
|
|
52
|
+
expect(hasUnloadedNewerPages(client, queryKey)).toBe(false)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { useEffect } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { ActivityIndicator, Pressable, StyleSheet, View } from 'react-native'
|
|
3
3
|
import Animated, {
|
|
4
|
-
useSharedValue,
|
|
5
|
-
useAnimatedStyle,
|
|
6
|
-
interpolate,
|
|
7
4
|
Extrapolation,
|
|
5
|
+
interpolate,
|
|
8
6
|
ReduceMotion,
|
|
7
|
+
useAnimatedStyle,
|
|
8
|
+
useSharedValue,
|
|
9
9
|
withSpring,
|
|
10
|
+
withTiming,
|
|
10
11
|
} from 'react-native-reanimated'
|
|
11
12
|
import { useTheme } from '../../hooks'
|
|
12
13
|
import { platformFontWeightMedium } from '../../utils'
|
|
@@ -15,11 +16,17 @@ import { Icon, Text } from '../display'
|
|
|
15
16
|
interface JumpToBottomButtonProps {
|
|
16
17
|
onPress: () => void
|
|
17
18
|
visible: boolean
|
|
19
|
+
loading?: boolean
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
export const JumpToBottomButton = ({
|
|
22
|
+
export const JumpToBottomButton = ({
|
|
23
|
+
onPress,
|
|
24
|
+
visible,
|
|
25
|
+
loading = false,
|
|
26
|
+
}: JumpToBottomButtonProps) => {
|
|
21
27
|
const styles = useStyles()
|
|
22
28
|
const progress = useSharedValue(0)
|
|
29
|
+
const loadingProgress = useSharedValue(0)
|
|
23
30
|
|
|
24
31
|
useEffect(() => {
|
|
25
32
|
progress.value = withSpring(visible ? 1 : 0, {
|
|
@@ -31,6 +38,13 @@ export const JumpToBottomButton = ({ onPress, visible }: JumpToBottomButtonProps
|
|
|
31
38
|
})
|
|
32
39
|
}, [visible, progress])
|
|
33
40
|
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
loadingProgress.value = withTiming(loading ? 1 : 0, {
|
|
43
|
+
duration: 750,
|
|
44
|
+
reduceMotion: ReduceMotion.System,
|
|
45
|
+
})
|
|
46
|
+
}, [loading, loadingProgress])
|
|
47
|
+
|
|
34
48
|
const animatedStyle = useAnimatedStyle(() => {
|
|
35
49
|
return {
|
|
36
50
|
opacity: progress.value,
|
|
@@ -45,16 +59,34 @@ export const JumpToBottomButton = ({ onPress, visible }: JumpToBottomButtonProps
|
|
|
45
59
|
}
|
|
46
60
|
})
|
|
47
61
|
|
|
62
|
+
const iconStyle = useAnimatedStyle(() => ({ opacity: 1 - loadingProgress.value }))
|
|
63
|
+
const spinnerStyle = useAnimatedStyle(() => ({ opacity: loadingProgress.value }))
|
|
64
|
+
|
|
48
65
|
return (
|
|
49
66
|
<View>
|
|
50
|
-
<Animated.View
|
|
67
|
+
<Animated.View
|
|
68
|
+
style={[styles.container, animatedStyle]}
|
|
69
|
+
pointerEvents={visible ? 'auto' : 'none'}
|
|
70
|
+
>
|
|
51
71
|
<Pressable
|
|
52
72
|
onPress={onPress}
|
|
73
|
+
disabled={loading}
|
|
74
|
+
accessibilityRole="button"
|
|
75
|
+
accessibilityLabel="Jump to most recent message"
|
|
76
|
+
accessibilityState={{ busy: loading }}
|
|
77
|
+
hitSlop={hitSlop}
|
|
53
78
|
style={({ pressed }) => [styles.button, pressed && styles.pressed]}
|
|
54
79
|
>
|
|
55
|
-
<
|
|
80
|
+
<View style={styles.glyph}>
|
|
81
|
+
<Animated.View style={[styles.glyphLayer, iconStyle]}>
|
|
82
|
+
<Icon name="general.downArrow" style={styles.icon} />
|
|
83
|
+
</Animated.View>
|
|
84
|
+
<Animated.View style={[styles.glyphLayer, spinnerStyle]}>
|
|
85
|
+
<ActivityIndicator size="small" color={styles.icon.color} style={styles.spinner} />
|
|
86
|
+
</Animated.View>
|
|
87
|
+
</View>
|
|
56
88
|
<Text variant="tertiary" style={styles.text}>
|
|
57
|
-
Jump to bottom
|
|
89
|
+
{loading ? 'Jumping to latest…' : 'Jump to bottom'}
|
|
58
90
|
</Text>
|
|
59
91
|
</Pressable>
|
|
60
92
|
</Animated.View>
|
|
@@ -62,6 +94,8 @@ export const JumpToBottomButton = ({ onPress, visible }: JumpToBottomButtonProps
|
|
|
62
94
|
)
|
|
63
95
|
}
|
|
64
96
|
|
|
97
|
+
const hitSlop = { top: 12, bottom: 12, left: 12, right: 12 }
|
|
98
|
+
|
|
65
99
|
const useStyles = () => {
|
|
66
100
|
const { colors } = useTheme()
|
|
67
101
|
|
|
@@ -93,10 +127,25 @@ const useStyles = () => {
|
|
|
93
127
|
color: colors.fillColorNeutral100Inverted,
|
|
94
128
|
fontWeight: platformFontWeightMedium,
|
|
95
129
|
},
|
|
130
|
+
glyph: {
|
|
131
|
+
width: 14,
|
|
132
|
+
height: 14,
|
|
133
|
+
alignItems: 'center',
|
|
134
|
+
justifyContent: 'center',
|
|
135
|
+
},
|
|
136
|
+
glyphLayer: {
|
|
137
|
+
position: 'absolute',
|
|
138
|
+
alignItems: 'center',
|
|
139
|
+
justifyContent: 'center',
|
|
140
|
+
},
|
|
96
141
|
icon: {
|
|
97
142
|
color: colors.fillColorNeutral100Inverted,
|
|
98
143
|
fontSize: 14,
|
|
99
144
|
},
|
|
145
|
+
spinner: {
|
|
146
|
+
width: 14,
|
|
147
|
+
height: 14,
|
|
148
|
+
},
|
|
100
149
|
pressed: {
|
|
101
150
|
transform: [{ scale: 0.95 }],
|
|
102
151
|
},
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
import { Avatar, Icon, IconProps, Image, Text } from '../display'
|
|
29
29
|
import { TheirReplyConnector, MyReplyConnector } from './reply_connectors'
|
|
30
30
|
|
|
31
|
-
interface ReplyShadowMessageProps
|
|
31
|
+
interface ReplyShadowMessageProps {
|
|
32
32
|
messageId: string
|
|
33
33
|
conversation_id: number
|
|
34
34
|
inReplyScreen?: boolean
|
|
@@ -86,7 +86,9 @@ function ShadowMessageContent({ conversation_id, ...message }: ShadowMessageCont
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
const attachmentLabel = some(attachments) ? pluralize(attachments.length, 'attachment') : ''
|
|
89
|
-
const accessibilityLabel = `${author?.name || ''} Reply Preview ${attachmentLabel} ${
|
|
89
|
+
const accessibilityLabel = `${author?.name || ''} Reply Preview ${attachmentLabel} ${
|
|
90
|
+
messageText || ''
|
|
91
|
+
} ${timestamp} ${replyCountText}`
|
|
90
92
|
|
|
91
93
|
return (
|
|
92
94
|
<Pressable
|