From a03dc921802eeed0d03abc56dae5660a0d20b96a Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Fri, 26 Jun 2026 18:07:05 -0300 Subject: [PATCH 001/144] docs: rename ubiquitous language glossary to CONTEXT and add message state vocabulary Rename UBIQUITOUS_LANGUAGE.md to CONTEXT.md (history preserved) and update its three references in CLAUDE.md, the VoIP docs README, and the RoomView architecture doc. Add the "Message Interaction & Position State" section distinguishing Interaction state (selection plus the reply/quote/edit/react action, owned by the per-Room interaction store) from Positional state (highlight plus jump/scroll position, owned by the Room view scroll machinery), since the two are migrated independently. Ref: NATIVE-1345 Claude-Session: https://claude.ai/code/session_01Cj1jHhEbiC9mWGe8dpqQeS --- CLAUDE.md | 2 +- UBIQUITOUS_LANGUAGE.md => CONTEXT.md | 9 +++++++++ app/lib/services/voip/docs/README.md | 2 +- app/views/RoomView/docs/ARCHITECTURE.md | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) rename UBIQUITOUS_LANGUAGE.md => CONTEXT.md (94%) diff --git a/CLAUDE.md b/CLAUDE.md index 9af5b2b0b40..2f701da32b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ Rocket.Chat React Native mobile client. Single-package React Native app (not a m - React 19.1, React Native 0.81, Expo 54 - TypeScript with strict mode, baseUrl set to `app/` (imports resolve from there) - Node: engines `>=18`, volta pins 24.13.1 -- Read UBIQUITOUS_LANGUAGE.md +- Read CONTEXT.md ## Commands diff --git a/UBIQUITOUS_LANGUAGE.md b/CONTEXT.md similarity index 94% rename from UBIQUITOUS_LANGUAGE.md rename to CONTEXT.md index 2b35c62a1d2..474444eb06d 100644 --- a/UBIQUITOUS_LANGUAGE.md +++ b/CONTEXT.md @@ -52,6 +52,15 @@ | **Room History** | Older Messages of a Room fetched on demand from the server (distinct from **Server History**) | Message history | | **Jump to Message** | Re-position the Room view onto a target Message that may be far from the Live Tail or not yet synced — fetches a surrounding Chunk | Scroll to message | +## Message Interaction & Position State + +Two distinct kinds of transient per-Room state drive how the Room view renders Messages. They have different owners and are migrated independently, so keep them apart. + +| Term | Definition | Owner | Scope | +| --------------------- | --------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------- | +| **Interaction state** | Which Message is selected and the current Message action — reply, quote, edit, or react | A per-Room interaction store | Migrated to the per-Room store as part of the Message row work | +| **Positional state** | Which Message is highlighted and the jump or scroll position | The Room view scroll machinery | Stays with the Room view scroll machinery and is migrated separately | + ## Users & Roles | Term | Definition | Aliases to avoid | diff --git a/app/lib/services/voip/docs/README.md b/app/lib/services/voip/docs/README.md index f39207f939e..f76c518a737 100644 --- a/app/lib/services/voip/docs/README.md +++ b/app/lib/services/voip/docs/README.md @@ -12,4 +12,4 @@ Entry point for documentation of the peer-to-peer audio call subsystem. VoIP is ## Glossary -Domain terms used throughout these docs are defined in the project glossary at `../../../../UBIQUITOUS_LANGUAGE.md` under "Video & Voice". +Domain terms used throughout these docs are defined in the project glossary at `../../../../CONTEXT.md` under "Video & Voice". diff --git a/app/views/RoomView/docs/ARCHITECTURE.md b/app/views/RoomView/docs/ARCHITECTURE.md index 2bad93e189f..c3dfadd9960 100644 --- a/app/views/RoomView/docs/ARCHITECTURE.md +++ b/app/views/RoomView/docs/ARCHITECTURE.md @@ -1,6 +1,6 @@ # Room Message Loading Architecture -Load-bearing reference for how the Room view loads, observes, and re-positions Messages. Read this before `FLOWS.md` — that document assumes the vocabulary defined here. Domain terms (Message Window, Live Tail, Anchored Window, Chunk, Gap, Loader Row) are defined in the repo glossary `UBIQUITOUS_LANGUAGE.md` under "Message Loading"; this document uses them as defined there. +Load-bearing reference for how the Room view loads, observes, and re-positions Messages. Read this before `FLOWS.md` — that document assumes the vocabulary defined here. Domain terms (Message Window, Live Tail, Anchored Window, Chunk, Gap, Loader Row) are defined in the repo glossary `CONTEXT.md` under "Message Loading"; this document uses them as defined there. ## Overview From 6a8682c44d44c5e79ee9ec617a8ca1a17ef380a6 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Fri, 26 Jun 2026 18:12:59 -0300 Subject: [PATCH 002/144] perf: memoize @json model fields Switch every @json-decorated WatermelonDB model field to the memoized form ({ memo: true }) so an unchanged nested JSON value keeps its reference identity across reads, enabling per-field re-render bailout once consumers adopt it. The memo cache is invalidated only when the raw column string changes, so in-place mutation of a memoized value would silently corrupt it. Convert the two remaining in-place mutations to replacement: - ReactionsList sorts a copy ([...reactions].sort) instead of the field. - encryption quote-attachment decryption reassigns message.attachments instead of pushing into it. Ref: NATIVE-1346 Claude-Session: https://claude.ai/code/session_01Cj1jHhEbiC9mWGe8dpqQeS --- app/containers/ReactionsList/index.tsx | 2 +- app/lib/database/model/CustomEmoji.js | 2 +- app/lib/database/model/Message.js | 26 +++++++-------- app/lib/database/model/Permission.js | 2 +- app/lib/database/model/Room.js | 10 +++--- app/lib/database/model/Setting.js | 2 +- app/lib/database/model/Subscription.js | 42 ++++++++++++------------ app/lib/database/model/Thread.js | 22 ++++++------- app/lib/database/model/ThreadMessage.js | 22 ++++++------- app/lib/database/model/User.js | 2 +- app/lib/database/model/servers/Server.js | 2 +- app/lib/database/model/servers/User.js | 2 +- app/lib/encryption/room.ts | 7 ++-- 13 files changed, 71 insertions(+), 72 deletions(-) diff --git a/app/containers/ReactionsList/index.tsx b/app/containers/ReactionsList/index.tsx index 3057aa2c8d3..6354a7200e5 100644 --- a/app/containers/ReactionsList/index.tsx +++ b/app/containers/ReactionsList/index.tsx @@ -30,7 +30,7 @@ const useRoutes = (reactions: IReaction[] | undefined) => { }; } - const sortedReactions = reactions?.sort((reaction1, reaction2) => reaction2.usernames.length - reaction1.usernames.length); + const sortedReactions = [...reactions].sort((reaction1, reaction2) => reaction2.usernames.length - reaction1.usernames.length); const routes: IRoute[] = sortedReactions.map(reaction => ({ key: reaction.emoji, title: reaction.emoji, diff --git a/app/lib/database/model/CustomEmoji.js b/app/lib/database/model/CustomEmoji.js index c8fdedd057c..b3ce7362aec 100644 --- a/app/lib/database/model/CustomEmoji.js +++ b/app/lib/database/model/CustomEmoji.js @@ -10,7 +10,7 @@ export default class CustomEmoji extends Model { @field('name') name; - @json('aliases', sanitizer) aliases; + @json('aliases', sanitizer, { memo: true }) aliases; @field('extension') extension; diff --git a/app/lib/database/model/Message.js b/app/lib/database/model/Message.js index 336f9c39351..d8693866ccd 100644 --- a/app/lib/database/model/Message.js +++ b/app/lib/database/model/Message.js @@ -18,13 +18,13 @@ export default class Message extends Model { @date('ts') ts; - @json('u', sanitizer) u; + @json('u', sanitizer, { memo: true }) u; @relation('subscriptions', 'rid') subscription; @field('alias') alias; - @json('parse_urls', sanitizer) parseUrls; + @json('parse_urls', sanitizer, { memo: true }) parseUrls; @field('groupable') groupable; @@ -32,9 +32,9 @@ export default class Message extends Model { @field('emoji') emoji; - @json('attachments', sanitizer) attachments; + @json('attachments', sanitizer, { memo: true }) attachments; - @json('urls', sanitizer) urls; + @json('urls', sanitizer, { memo: true }) urls; @date('_updated_at') _updatedAt; @@ -44,9 +44,9 @@ export default class Message extends Model { @field('starred') starred; - @json('edited_by', sanitizer) editedBy; + @json('edited_by', sanitizer, { memo: true }) editedBy; - @json('reactions', sanitizer) reactions; + @json('reactions', sanitizer, { memo: true }) reactions; @field('role') role; @@ -62,29 +62,29 @@ export default class Message extends Model { @date('tlm') tlm; - @json('replies', sanitizer) replies; + @json('replies', sanitizer, { memo: true }) replies; - @json('mentions', sanitizer) mentions; + @json('mentions', sanitizer, { memo: true }) mentions; - @json('channels', sanitizer) channels; + @json('channels', sanitizer, { memo: true }) channels; @field('unread') unread; @field('auto_translate') autoTranslate; - @json('translations', sanitizer) translations; + @json('translations', sanitizer, { memo: true }) translations; @field('tmsg') tmsg; - @json('blocks', sanitizer) blocks; + @json('blocks', sanitizer, { memo: true }) blocks; @field('e2e') e2e; @field('tshow') tshow; - @json('md', sanitizer) md; + @json('md', sanitizer, { memo: true }) md; - @json('content', sanitizer) content; + @json('content', sanitizer, { memo: true }) content; @field('comment') comment; diff --git a/app/lib/database/model/Permission.js b/app/lib/database/model/Permission.js index c397f079a35..d1ebb7b9020 100644 --- a/app/lib/database/model/Permission.js +++ b/app/lib/database/model/Permission.js @@ -8,7 +8,7 @@ export const PERMISSIONS_TABLE = 'permissions'; export default class Permission extends Model { static table = PERMISSIONS_TABLE; - @json('roles', sanitizer) roles; + @json('roles', sanitizer, { memo: true }) roles; @date('_updated_at') _updatedAt; } diff --git a/app/lib/database/model/Room.js b/app/lib/database/model/Room.js index e2a1127bf56..2dfadbd1c7b 100644 --- a/app/lib/database/model/Room.js +++ b/app/lib/database/model/Room.js @@ -8,7 +8,7 @@ export const ROOMS_TABLE = 'rooms'; export default class Room extends Model { static table = ROOMS_TABLE; - @json('custom_fields', sanitizer) customFields; + @json('custom_fields', sanitizer, { memo: true }) customFields; @field('broadcast') broadcast; @@ -18,15 +18,15 @@ export default class Room extends Model { @field('ro') ro; - @json('v', sanitizer) v; + @json('v', sanitizer, { memo: true }) v; - @json('served_by', sanitizer) servedBy; + @json('served_by', sanitizer, { memo: true }) servedBy; @field('department_id') departmentId; - @json('livechat_data', sanitizer) livechatData; + @json('livechat_data', sanitizer, { memo: true }) livechatData; - @json('tags', sanitizer) tags; + @json('tags', sanitizer, { memo: true }) tags; @field('avatar_etag') avatarETag; } diff --git a/app/lib/database/model/Setting.js b/app/lib/database/model/Setting.js index 1597272bdb1..59c175a947b 100644 --- a/app/lib/database/model/Setting.js +++ b/app/lib/database/model/Setting.js @@ -14,7 +14,7 @@ export default class Setting extends Model { @field('value_as_number') valueAsNumber; - @json('value_as_array', sanitizer) valueAsArray; + @json('value_as_array', sanitizer, { memo: true }) valueAsArray; @date('_updated_at') _updatedAt; } diff --git a/app/lib/database/model/Subscription.js b/app/lib/database/model/Subscription.js index 8c0db9d18ca..4c137faf466 100644 --- a/app/lib/database/model/Subscription.js +++ b/app/lib/database/model/Subscription.js @@ -37,7 +37,7 @@ export default class Subscription extends Model { @field('alert') alert; - @json('roles', sanitizer) roles; + @json('roles', sanitizer, { memo: true }) roles; @field('unread') unread; @@ -45,11 +45,11 @@ export default class Subscription extends Model { @field('group_mentions') groupMentions; - @json('tunread', sanitizer) tunread; + @json('tunread', sanitizer, { memo: true }) tunread; - @json('tunread_user', sanitizer) tunreadUser; + @json('tunread_user', sanitizer, { memo: true }) tunreadUser; - @json('tunread_group', sanitizer) tunreadGroup; + @json('tunread_group', sanitizer, { memo: true }) tunreadGroup; @date('room_updated_at') roomUpdatedAt; @@ -77,11 +77,11 @@ export default class Subscription extends Model { @field('notifications') notifications; - @json('muted', sanitizer) muted; + @json('muted', sanitizer, { memo: true }) muted; - @json('unmuted', sanitizer) unmuted; + @json('unmuted', sanitizer, { memo: true }) unmuted; - @json('ignored', sanitizer) ignored; + @json('ignored', sanitizer, { memo: true }) ignored; @field('broadcast') broadcast; @@ -97,7 +97,7 @@ export default class Subscription extends Model { @field('auto_translate_language') autoTranslateLanguage; - @json('last_message', sanitizer) lastMessage; + @json('last_message', sanitizer, { memo: true }) lastMessage; @children('messages') messages; @@ -109,25 +109,25 @@ export default class Subscription extends Model { @field('hide_mention_status') hideMentionStatus; - @json('sys_mes', sanitizer) sysMes; + @json('sys_mes', sanitizer, { memo: true }) sysMes; - @json('uids', sanitizer) uids; + @json('uids', sanitizer, { memo: true }) uids; - @json('usernames', sanitizer) usernames; + @json('usernames', sanitizer, { memo: true }) usernames; - @json('visitor', sanitizer) visitor; + @json('visitor', sanitizer, { memo: true }) visitor; @field('department_id') departmentId; - @json('served_by', sanitizer) servedBy; + @json('served_by', sanitizer, { memo: true }) servedBy; - @json('livechat_data', sanitizer) livechatData; + @json('livechat_data', sanitizer, { memo: true }) livechatData; - @json('tags', sanitizer) tags; + @json('tags', sanitizer, { memo: true }) tags; @field('e2e_key') E2EKey; - @json('old_room_keys', sanitizer) oldRoomKeys; + @json('old_room_keys', sanitizer, { memo: true }) oldRoomKeys; @field('e2e_suggested_key') E2ESuggestedKey; @@ -135,7 +135,7 @@ export default class Subscription extends Model { @field('e2e_key_id') e2eKeyId; - @json('users_waiting_for_e2e_keys', sanitizer) usersWaitingForE2EKeys; + @json('users_waiting_for_e2e_keys', sanitizer, { memo: true }) usersWaitingForE2EKeys; @field('avatar_etag') avatarETag; @@ -147,19 +147,19 @@ export default class Subscription extends Model { @field('users_count') usersCount; - @json('source', sanitizer) source; + @json('source', sanitizer, { memo: true }) source; @field('disable_notifications') disableNotifications; @field('federated') federated; - @json('abac_attributes', sanitizer) abacAttributes; + @json('abac_attributes', sanitizer, { memo: true }) abacAttributes; - @json('federation', sanitizer) federation; + @json('federation', sanitizer, { memo: true }) federation; @field('status') status; - @json('inviter', sanitizer) inviter; + @json('inviter', sanitizer, { memo: true }) inviter; asPlain() { return { diff --git a/app/lib/database/model/Thread.js b/app/lib/database/model/Thread.js index a04e589bbda..4830cb12417 100644 --- a/app/lib/database/model/Thread.js +++ b/app/lib/database/model/Thread.js @@ -18,13 +18,13 @@ export default class Thread extends Model { @date('ts') ts; - @json('u', sanitizer) u; + @json('u', sanitizer, { memo: true }) u; @relation('subscriptions', 'rid') subscription; @field('alias') alias; - @json('parse_urls', sanitizer) parseUrls; + @json('parse_urls', sanitizer, { memo: true }) parseUrls; @field('groupable') groupable; @@ -32,9 +32,9 @@ export default class Thread extends Model { @field('emoji') emoji; - @json('attachments', sanitizer) attachments; + @json('attachments', sanitizer, { memo: true }) attachments; - @json('urls', sanitizer) urls; + @json('urls', sanitizer, { memo: true }) urls; @date('_updated_at') _updatedAt; @@ -44,9 +44,9 @@ export default class Thread extends Model { @field('starred') starred; - @json('edited_by', sanitizer) editedBy; + @json('edited_by', sanitizer, { memo: true }) editedBy; - @json('reactions', sanitizer) reactions; + @json('reactions', sanitizer, { memo: true }) reactions; @field('role') role; @@ -62,19 +62,19 @@ export default class Thread extends Model { @date('tlm') tlm; - @json('replies', sanitizer) replies; + @json('replies', sanitizer, { memo: true }) replies; - @json('mentions', sanitizer) mentions; + @json('mentions', sanitizer, { memo: true }) mentions; - @json('channels', sanitizer) channels; + @json('channels', sanitizer, { memo: true }) channels; @field('unread') unread; @field('auto_translate') autoTranslate; - @json('translations', sanitizer) translations; + @json('translations', sanitizer, { memo: true }) translations; - @json('content', sanitizer) content; + @json('content', sanitizer, { memo: true }) content; @field('e2e') e2e; diff --git a/app/lib/database/model/ThreadMessage.js b/app/lib/database/model/ThreadMessage.js index 8bb364b7edf..2bbd11e612d 100644 --- a/app/lib/database/model/ThreadMessage.js +++ b/app/lib/database/model/ThreadMessage.js @@ -18,7 +18,7 @@ export default class ThreadMessage extends Model { @date('ts') ts; - @json('u', sanitizer) u; + @json('u', sanitizer, { memo: true }) u; @relation('subscriptions', 'subscription_id') subscription; @@ -26,7 +26,7 @@ export default class ThreadMessage extends Model { @field('alias') alias; - @json('parse_urls', sanitizer) parseUrls; + @json('parse_urls', sanitizer, { memo: true }) parseUrls; @field('groupable') groupable; @@ -34,9 +34,9 @@ export default class ThreadMessage extends Model { @field('emoji') emoji; - @json('attachments', sanitizer) attachments; + @json('attachments', sanitizer, { memo: true }) attachments; - @json('urls', sanitizer) urls; + @json('urls', sanitizer, { memo: true }) urls; @date('_updated_at') _updatedAt; @@ -46,9 +46,9 @@ export default class ThreadMessage extends Model { @field('starred') starred; - @json('edited_by', sanitizer) editedBy; + @json('edited_by', sanitizer, { memo: true }) editedBy; - @json('reactions', sanitizer) reactions; + @json('reactions', sanitizer, { memo: true }) reactions; @field('role') role; @@ -62,23 +62,23 @@ export default class ThreadMessage extends Model { @date('tlm') tlm; - @json('replies', sanitizer) replies; + @json('replies', sanitizer, { memo: true }) replies; - @json('mentions', sanitizer) mentions; + @json('mentions', sanitizer, { memo: true }) mentions; - @json('channels', sanitizer) channels; + @json('channels', sanitizer, { memo: true }) channels; @field('unread') unread; @field('auto_translate') autoTranslate; - @json('translations', sanitizer) translations; + @json('translations', sanitizer, { memo: true }) translations; @field('draft_message') draftMessage; @field('e2e') e2e; - @json('content', sanitizer) content; + @json('content', sanitizer, { memo: true }) content; asPlain() { return { diff --git a/app/lib/database/model/User.js b/app/lib/database/model/User.js index 23978d1caf3..8c9531f610d 100644 --- a/app/lib/database/model/User.js +++ b/app/lib/database/model/User.js @@ -16,5 +16,5 @@ export default class User extends Model { @field('avatar_etag') avatarETag; - @json('roles', sanitizer) roles; + @json('roles', sanitizer, { memo: true }) roles; } diff --git a/app/lib/database/model/servers/Server.js b/app/lib/database/model/servers/Server.js index 2d57113e414..d9673269188 100644 --- a/app/lib/database/model/servers/Server.js +++ b/app/lib/database/model/servers/Server.js @@ -35,7 +35,7 @@ export default class Server extends Model { @field('e2e_enable') E2E_Enable; - @json('supported_versions', sanitizer) supportedVersions; + @json('supported_versions', sanitizer, { memo: true }) supportedVersions; @date('supported_versions_warning_at') supportedVersionsWarningAt; diff --git a/app/lib/database/model/servers/User.js b/app/lib/database/model/servers/User.js index bf32f82e911..4acacbe385e 100644 --- a/app/lib/database/model/servers/User.js +++ b/app/lib/database/model/servers/User.js @@ -20,7 +20,7 @@ export default class User extends Model { @field('statusText') statusText; - @json('roles', sanitizer) roles; + @json('roles', sanitizer, { memo: true }) roles; @field('avatar_etag') avatarETag; diff --git a/app/lib/encryption/room.ts b/app/lib/encryption/room.ts index 04058659758..ce68f11074c 100644 --- a/app/lib/encryption/room.ts +++ b/app/lib/encryption/room.ts @@ -727,9 +727,9 @@ export default class EncryptionRoom { const messageFromDB = await getMessageById(messageId); if (messageFromDB && messageFromDB.e2e === 'done') { const decryptedQuoteMessage = mapMessageFromDB(messageFromDB); - message.attachments = message.attachments || []; const quoteAttachment = createQuoteAttachment(decryptedQuoteMessage, url); - return message.attachments.push(quoteAttachment); + message.attachments = [...(message.attachments || []), quoteAttachment]; + return; } // From API @@ -738,9 +738,8 @@ export default class EncryptionRoom { return; } const decryptedQuoteMessage = await this.decrypt(mapMessageFromAPI(quotedMessageObject)); - message.attachments = message.attachments || []; const quoteAttachment = createQuoteAttachment(decryptedQuoteMessage as IMessage, url); - return message.attachments.push(quoteAttachment); + message.attachments = [...(message.attachments || []), quoteAttachment]; }) ); return message; From b2709f2d7666d7ae4f1123ab404f1faa8d675475 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Mon, 29 Jun 2026 12:24:59 -0300 Subject: [PATCH 003/144] feat: add reactive useMessage snapshot hook (NATIVE-1347) Introduce useMessage, a reactive hook that takes a live WatermelonDB Message model and returns a plain Message Snapshot via useSyncExternalStore. It subscribes to model mutations, paints synchronously on first render with no flash, and reads the memoized @json getters directly so unchanged fields keep their reference for per-field re-render bailout. All WatermelonDB specifics are confined to the hook; it is not yet wired into the row container. --- .../message/hooks/useMessage.test.ts | 127 ++++++++++++++++++ app/containers/message/hooks/useMessage.ts | 121 +++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 app/containers/message/hooks/useMessage.test.ts create mode 100644 app/containers/message/hooks/useMessage.ts diff --git a/app/containers/message/hooks/useMessage.test.ts b/app/containers/message/hooks/useMessage.test.ts new file mode 100644 index 00000000000..8c71787a40b --- /dev/null +++ b/app/containers/message/hooks/useMessage.test.ts @@ -0,0 +1,127 @@ +import { act, renderHook } from '@testing-library/react-native'; + +import { type TAnyMessageModel } from 'definitions'; + +import { useMessage } from './useMessage'; + +type Subscriber = () => void; + +const buildFakeModel = (overrides: Partial = {}): TAnyMessageModel & { _emit: () => void } => { + const subscribers: Subscriber[] = []; + + const model = { + id: 'msg-1', + msg: 'Hello world', + t: undefined, + ts: new Date('2024-01-01T00:00:00Z'), + u: { _id: 'u1', username: 'alice' }, + alias: undefined, + groupable: true, + avatar: undefined, + emoji: undefined, + attachments: [{ image_url: 'https://example.com/img.png' }], + urls: [], + status: 0, + pinned: false, + editedBy: null, + reactions: { '👍': { usernames: ['alice'] } }, + role: undefined, + drid: undefined, + dcount: undefined, + dlm: undefined, + tmid: undefined, + tcount: undefined, + tlm: undefined, + replies: ['u1'], + mentions: [], + channels: [], + unread: false, + autoTranslate: false, + translations: null, + tmsg: undefined, + blocks: null, + e2e: undefined, + md: [{ type: 'PARAGRAPH' }], + comment: undefined, + ...overrides, + experimentalSubscribe(cb: Subscriber) { + subscribers.push(cb); + return () => { + const idx = subscribers.indexOf(cb); + if (idx !== -1) subscribers.splice(idx, 1); + }; + }, + _emit() { + subscribers.forEach(cb => cb()); + } + } as unknown as TAnyMessageModel & { _emit: () => void }; + + return model; +}; + +describe('useMessage', () => { + it('returns a snapshot with the correct initial field values', () => { + const model = buildFakeModel(); + const { result } = renderHook(() => useMessage(model)); + + expect(result.current.id).toBe('msg-1'); + expect(result.current.msg).toBe('Hello world'); + expect(result.current.u).toEqual({ _id: 'u1', username: 'alice' }); + expect(result.current.ts).toEqual(new Date('2024-01-01T00:00:00Z')); + expect(result.current.attachments).toBe(model.attachments); + expect(result.current.reactions).toBe(model.reactions); + expect(result.current.md).toBe(model.md); + expect(result.current.replies).toBe(model.replies); + }); + + it('returns a new snapshot reflecting the changed field after a model mutation', () => { + const model = buildFakeModel(); + const { result } = renderHook(() => useMessage(model)); + const before = result.current; + + act(() => { + (model as any).msg = 'Updated message'; + model._emit(); + }); + + expect(result.current).not.toBe(before); + expect(result.current.msg).toBe('Updated message'); + }); + + it('keeps the same reference for an unchanged @json field across an unrelated mutation', () => { + const model = buildFakeModel(); + const { result } = renderHook(() => useMessage(model)); + + const attachmentsBefore = result.current.attachments; + const reactionsBefore = result.current.reactions; + const mdBefore = result.current.md; + + act(() => { + (model as any).msg = 'Changed msg only'; + model._emit(); + }); + + expect(result.current.attachments).toBe(attachmentsBefore); + expect(result.current.reactions).toBe(reactionsBefore); + expect(result.current.md).toBe(mdBefore); + }); + + it('returns the new model snapshot synchronously when item identity changes', () => { + const modelA = buildFakeModel({ id: 'msg-a', msg: 'Model A' }); + const modelB = buildFakeModel({ id: 'msg-b', msg: 'Model B' }); + + const { result, rerender } = renderHook(({ model }: { model: typeof modelA }) => useMessage(model), { + initialProps: { model: modelA } + }); + + expect(result.current.id).toBe('msg-a'); + expect(result.current.msg).toBe('Model A'); + + act(() => { + rerender({ model: modelB }); + }); + + expect(result.current.id).toBe('msg-b'); + expect(result.current.msg).toBe('Model B'); + }); +}); diff --git a/app/containers/message/hooks/useMessage.ts b/app/containers/message/hooks/useMessage.ts new file mode 100644 index 00000000000..e14e111b03f --- /dev/null +++ b/app/containers/message/hooks/useMessage.ts @@ -0,0 +1,121 @@ +import { useRef, useSyncExternalStore } from 'react'; +import { type Root } from '@rocket.chat/message-parser'; + +import { + type IAttachment, + type IEditedBy, + type IMessage, + type IMessageTranslations, + type IReaction, + type IUrl, + type IUserChannel, + type IUserMention, + type IUserMessage, + type TAnyMessageModel +} from 'definitions'; + +export type IMessageSnapshot = Pick< + IMessage, + | 'id' + | 'msg' + | 't' + | 'ts' + | 'alias' + | 'groupable' + | 'avatar' + | 'emoji' + | 'status' + | 'pinned' + | 'role' + | 'drid' + | 'dcount' + | 'dlm' + | 'tmid' + | 'tcount' + | 'tlm' + | 'unread' + | 'autoTranslate' + | 'tmsg' + | 'blocks' + | 'e2e' + | 'comment' +> & { + u: IUserMessage | undefined; + attachments: IAttachment[] | undefined; + urls: IUrl[] | undefined; + editedBy: IEditedBy | undefined; + reactions: IReaction[] | undefined; + replies: string[] | undefined; + mentions: IUserMention[] | undefined; + channels: IUserChannel[] | undefined; + translations: IMessageTranslations[] | undefined; + md: Root | undefined; +}; + +const readSnapshot = (item: TAnyMessageModel): IMessageSnapshot => ({ + id: item.id, + msg: item.msg, + t: item.t, + ts: item.ts, + u: item.u, + alias: item.alias, + groupable: item.groupable, + avatar: item.avatar, + emoji: item.emoji, + attachments: item.attachments, + urls: item.urls, + status: item.status, + pinned: item.pinned, + editedBy: item.editedBy, + reactions: item.reactions, + role: item.role, + drid: item.drid, + dcount: item.dcount, + dlm: item.dlm, + tmid: item.tmid, + tcount: item.tcount, + tlm: item.tlm, + replies: item.replies, + mentions: item.mentions, + channels: item.channels, + unread: item.unread, + autoTranslate: item.autoTranslate, + translations: item.translations, + tmsg: item.tmsg, + blocks: item.blocks, + e2e: item.e2e, + md: item.md, + comment: item.comment +}); + +export const useMessage = (item: TAnyMessageModel): IMessageSnapshot => { + 'use memo'; + + // prevItemRef lets us detect item identity changes so getSnapshot always + // reflects the CURRENT item synchronously — even before the new subscription + // fires its first callback. + const prevItemRef = useRef(null); + + // Cache the latest snapshot so getSnapshot is referentially stable between + // emissions; useSyncExternalStore requires it to return the same object when + // nothing has changed, otherwise React will infinite-loop. + const snapshotRef = useRef(null); + + if (prevItemRef.current !== item) { + prevItemRef.current = item; + snapshotRef.current = readSnapshot(item); + } + + const subscribe = (onStoreChange: () => void): (() => void) => { + // @ts-ignore: experimentalSubscribe is not yet in WatermelonDB's TS types + const unsubscribe = item.experimentalSubscribe(() => { + snapshotRef.current = readSnapshot(item); + onStoreChange(); + }); + return unsubscribe; + }; + + const getSnapshot = (): IMessageSnapshot => snapshotRef.current as IMessageSnapshot; + + return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); +}; From 0db07c7068f4bb4a0b9738c14c06bebe455647b3 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Mon, 29 Jun 2026 14:34:19 -0300 Subject: [PATCH 004/144] feat: migrate Message row container to function component (NATIVE-1348) Convert MessageContainer from a class to a function component. - Replace manual experimentalSubscribe + forceUpdate with the reactive useMessage snapshot hook; model mutations now re-render through useSyncExternalStore. - Replace shouldComponentUpdate with React.memo and an equivalent custom comparator over the same nine props. - Move isManualUnignored to useState, theming to useTheme, and defaultProps to destructuring parameter defaults. - Keep the debounced onPress as a single lifetime instance via a ref, always invoking the latest closure. Render output is unchanged: the existing Message snapshot suite passes without updates. Adds a behavior test for the row container. --- app/containers/message/index.test.tsx | 93 ++++ app/containers/message/index.tsx | 680 +++++++++++--------------- 2 files changed, 388 insertions(+), 385 deletions(-) create mode 100644 app/containers/message/index.test.tsx diff --git a/app/containers/message/index.test.tsx b/app/containers/message/index.test.tsx new file mode 100644 index 00000000000..b12fa6329e9 --- /dev/null +++ b/app/containers/message/index.test.tsx @@ -0,0 +1,93 @@ +import { Provider } from 'react-redux'; +import { render, fireEvent } from '@testing-library/react-native'; + +import MessageContainer from './index'; +import { mockedStore } from '../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../definitions'; + +jest.mock('./Touch', () => { + const { forwardRef } = require('react'); + const { TouchableOpacity } = require('react-native'); + return forwardRef(({ children, onPress, onLongPress, ...props }: any, ref: any) => ( + + {children} + + )); +}); + +const createMockMessage = (overrides: Record = {}): TAnyMessageModel => + ({ + id: 'msg-1', + msg: 'Hello World', + t: undefined, + ts: new Date(), + u: { _id: 'user-1', username: 'testuser', name: 'Test User' }, + alias: undefined, + groupable: true, + avatar: undefined, + emoji: undefined, + attachments: undefined, + urls: undefined, + status: undefined, + pinned: undefined, + editedBy: undefined, + reactions: undefined, + role: undefined, + drid: undefined, + dcount: undefined, + dlm: undefined, + tmid: undefined, + tcount: undefined, + tlm: undefined, + replies: undefined, + mentions: undefined, + channels: undefined, + unread: undefined, + autoTranslate: undefined, + translations: undefined, + tmsg: undefined, + blocks: undefined, + e2e: undefined, + md: undefined, + comment: undefined, + experimentalSubscribe: jest.fn(() => jest.fn()), + ...overrides + } as unknown as TAnyMessageModel); + +const baseProps = { + user: { id: 'user-1', username: 'testuser', token: 'token' }, + rid: 'room-1', + baseUrl: 'https://open.rocket.chat', + getCustomEmoji: jest.fn(() => null) +}; + +const renderContainer = (itemOverrides: Record = {}, propOverrides: Record = {}) => + render( + + + + ); + +it('renders a normal message without crashing', () => { + const { getByTestId } = renderContainer(); + expect(getByTestId('message-msg-1')).toBeTruthy(); +}); + +it('renders the message text', () => { + const { getByText } = renderContainer({ msg: 'Hello World' }); + expect(getByText('Hello World')).toBeTruthy(); +}); + +it('reveals an ignored message when tapped', () => { + const { getByTestId, queryByTestId } = renderContainer({ msg: 'Hello World' }, { isIgnored: true }); + expect(getByTestId('message-ignored-Hello World')).toBeTruthy(); + fireEvent.press(getByTestId('message-ignored-Hello World')); + expect(queryByTestId('message-ignored-Hello World')).toBeNull(); +}); + +it('fires the onLongPress callback when long-pressed', () => { + const onLongPress = jest.fn(); + const { getByTestId } = renderContainer({}, { onLongPress }); + fireEvent(getByTestId('message-msg-1'), 'longPress'); + expect(onLongPress).toHaveBeenCalledTimes(1); +}); diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index df0e3ee8a57..16f74ef196c 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -1,17 +1,18 @@ -import { Component } from 'react'; +import { memo, useRef, useState } from 'react'; import { Keyboard } from 'react-native'; import Message from './Message'; import MessageContext from './Context'; import { debounce } from '../../lib/methods/helpers'; import { getMessageTranslation } from './utils'; -import { type TSupportedThemes, withTheme } from '../../theme'; +import { type TSupportedThemes, useTheme } from '../../theme'; import openLink from '../../lib/methods/helpers/openLink'; import { type IAttachment, type TAnyMessageModel, type TGetCustomEmoji } from '../../definitions'; import { type IRoomInfoParam } from '../../views/SearchMessagesView'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; import { messagesStatus } from '../../lib/constants/messagesStatus'; import MessageSeparator from '../MessageSeparator'; +import { useMessage } from './hooks/useMessage'; interface IMessageContainerProps { item: TAnyMessageModel; @@ -65,453 +66,362 @@ interface IMessageContainerProps { showUnreadSeparator?: boolean; } -interface IMessageContainerState { - isManualUnignored: boolean; +function areEqual(prev: IMessageContainerProps, next: IMessageContainerProps): boolean { + return ( + prev.showUnreadSeparator === next.showUnreadSeparator && + prev.dateSeparator === next.dateSeparator && + prev.highlighted === next.highlighted && + prev.threadBadgeColor === next.threadBadgeColor && + prev.isIgnored === next.isIgnored && + prev.previousItem?._id === next.previousItem?._id && + prev.isBeingEdited === next.isBeingEdited && + prev.autoTranslateRoom === next.autoTranslateRoom && + prev.autoTranslateLanguage === next.autoTranslateLanguage + ); } -class MessageContainer extends Component { - static defaultProps = { - getCustomEmoji: () => null, - onLongPress: () => {}, - blockAction: () => {}, - archived: false, - broadcast: false, - isIgnored: false, - theme: 'light' as TSupportedThemes - }; - - state = { isManualUnignored: false }; - - private subscription?: Function; - - componentDidMount() { - const { item } = this.props; - // @ts-ignore - if (item && item.experimentalSubscribe) { - // TODO: Update watermelonDB to recognize experimentalSubscribe at types - // experimentalSubscribe(subscriber: (isDeleted: boolean) => void, debugInfo?: any): Unsubscribe - // @ts-ignore - this.subscription = item.experimentalSubscribe(() => { - this.forceUpdate(); - }); - } - } - - shouldComponentUpdate(nextProps: IMessageContainerProps, nextState: IMessageContainerState) { - const { isManualUnignored } = this.state; - const { - threadBadgeColor, - isIgnored, - highlighted, - previousItem, - autoTranslateRoom, - autoTranslateLanguage, - isBeingEdited, - showUnreadSeparator, - dateSeparator - } = this.props; - - if (nextProps.showUnreadSeparator !== showUnreadSeparator) { - return true; - } - if (nextProps.dateSeparator !== dateSeparator) { - return true; - } - if (nextProps.highlighted !== highlighted) { - return true; - } - if (nextProps.threadBadgeColor !== threadBadgeColor) { +const MessageContainer = ({ + item, + user, + rid, + timeFormat, + archived = false, + broadcast = false, + previousItem, + baseUrl, + Message_GroupingPeriod, + isReadReceiptEnabled, + isThreadRoom, + useRealName, + autoTranslateRoom, + autoTranslateLanguage, + isIgnored: isIgnoredProp = false, + highlighted, + getCustomEmoji = () => null, + onLongPress: onLongPressProp = () => {}, + onReactionPress: onReactionPressProp, + onEncryptedPress: onEncryptedPressProp, + onDiscussionPress: onDiscussionPressProp, + onThreadPress: onThreadPressProp, + errorActionsShow, + replyBroadcast: replyBroadcastProp, + reactionInit: reactionInitProp, + fetchThreadName, + showAttachment, + onReactionLongPress: onReactionLongPressProp, + navToRoomInfo = () => {}, + handleEnterCall, + blockAction = () => {}, + onAnswerButtonPress: onAnswerButtonPressProp, + threadBadgeColor, + toggleFollowThread, + jumpToMessage, + onPress: onPressProp, + closeEmojiAndAction, + isBeingEdited, + isPreview, + dateSeparator, + showUnreadSeparator +}: IMessageContainerProps) => { + 'use memo'; + + const message = useMessage(item); + const { theme } = useTheme(); + const [isManualUnignored, setIsManualUnignored] = useState(false); + + // Derived values (formerly getters) + const hasError = message.status === messagesStatus.ERROR; + + const isHeader = (() => { + if (hasError || (previousItem && previousItem.status === messagesStatus.ERROR)) { return true; } - if (nextProps.isIgnored !== isIgnored) { - return true; - } - if (nextState.isManualUnignored !== isManualUnignored) { - return true; - } - if (nextProps.previousItem?._id !== previousItem?._id) { + try { + if ( + previousItem && + // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense + previousItem.ts.toDateString() === message.ts.toDateString() && + previousItem.u?.username === message.u?.username && + !(previousItem.groupable === false || message.groupable === false || broadcast === true) && + // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense + message.ts - previousItem.ts < Message_GroupingPeriod * 1000 && + previousItem.tmid === message.tmid && + message.t !== 'rm' && + previousItem.t !== 'rm' + ) { + return false; + } return true; - } - if (isBeingEdited !== nextProps.isBeingEdited) { + } catch (error) { return true; } - if (nextProps.autoTranslateRoom !== autoTranslateRoom) { - return true; + })(); + + const isThreadReply = (() => { + if (isThreadRoom) { + return false; } - if (nextProps.autoTranslateRoom !== autoTranslateRoom || nextProps.autoTranslateLanguage !== autoTranslateLanguage) { + if (previousItem && message.tmid && previousItem.tmid !== message.tmid && previousItem.id !== message.tmid) { return true; } return false; - } + })(); - componentWillUnmount() { - if (this.subscription) { - this.subscription(); + const isThreadSequential = (() => { + if (isThreadRoom) { + return false; } - } + return !!message.tmid; + })(); - onPressAction = () => { - const { closeEmojiAndAction } = this.props; + const isEncrypted = message.t === E2E_MESSAGE_TYPE && message.e2e !== E2E_STATUS.DONE; - if (closeEmojiAndAction) { - return closeEmojiAndAction(this.onPress); + const isInfo: string | boolean = (() => { + if (['e2e', 'discussion-created', 'jitsi_call_started', 'videoconf'].includes(message.t)) { + return false; } + return message.t; + })(); - return this.onPress(); - }; - - onPress = debounce( - () => { - const { onPress } = this.props; - if (this.isIgnored) { - return this.onIgnoredMessagePress(); - } - - if (onPress) { - return onPress(); - } - - const { item, isThreadRoom } = this.props; - Keyboard.dismiss(); - - if ((item.tlm || item.tmid) && !isThreadRoom) { - this.onThreadPress(); - } + const isTemp = message.status === messagesStatus.TEMP || message.status === messagesStatus.ERROR; - const { onDiscussionPress } = this.props; + const isIgnored = isManualUnignored ? false : isIgnoredProp ?? false; - if (item.dlm && onDiscussionPress) { - onDiscussionPress(item); - } - }, - 300, - true - ); - - onLongPress = () => { - const { archived, onLongPress, item } = this.props; - if (this.isInfo || this.hasError || this.isEncrypted || archived) { - return; - } - if (onLongPress) { - onLongPress(item); - } + // Event handlers + const onIgnoredMessagePress = () => { + setIsManualUnignored(true); }; - onErrorPress = () => { - const { errorActionsShow, item } = this.props; + const onErrorPress = () => { if (errorActionsShow) { errorActionsShow(item); } }; - onReactionPress = (emoji: string) => { - const { onReactionPress, item } = this.props; - if (onReactionPress) { - onReactionPress(emoji, item.id); + const onReactionPress = (emoji: string) => { + if (onReactionPressProp) { + onReactionPressProp(emoji, message.id); } }; - onReactionLongPress = () => { - const { onReactionLongPress, item } = this.props; - if (onReactionLongPress) { - onReactionLongPress(item); + const onReactionLongPress = () => { + if (onReactionLongPressProp) { + onReactionLongPressProp(item); } }; - onEncryptedPress = () => { - const { onEncryptedPress } = this.props; - if (onEncryptedPress) { - onEncryptedPress(); + const onEncryptedPress = () => { + if (onEncryptedPressProp) { + onEncryptedPressProp(); } }; - onDiscussionPress = () => { - const { onDiscussionPress, item } = this.props; - if (onDiscussionPress) { - onDiscussionPress(item); + const onDiscussionPress = () => { + if (onDiscussionPressProp) { + onDiscussionPressProp(item); } }; - onThreadPress = () => { - const { onThreadPress, item } = this.props; - if (onThreadPress) { - onThreadPress(item); + const onThreadPress = () => { + if (onThreadPressProp) { + onThreadPressProp(item); } }; - onAnswerButtonPress = (msg: string) => { - const { onAnswerButtonPress } = this.props; - if (onAnswerButtonPress) { - onAnswerButtonPress(msg, false); + const onAnswerButtonPress = (msg: string) => { + if (onAnswerButtonPressProp) { + onAnswerButtonPressProp(msg, false); } }; - onIgnoredMessagePress = () => { - this.setState({ isManualUnignored: true }); + const reactionInit = () => { + if (reactionInitProp) { + reactionInitProp(message.id); + } }; - get isHeader(): boolean { - const { item, previousItem, broadcast, Message_GroupingPeriod } = this.props; - if (this.hasError || (previousItem && previousItem.status === messagesStatus.ERROR)) { - return true; - } - try { - if ( - previousItem && - // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense - previousItem.ts.toDateString() === item.ts.toDateString() && - previousItem.u.username === item.u.username && - !(previousItem.groupable === false || item.groupable === false || broadcast === true) && - // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense - item.ts - previousItem.ts < Message_GroupingPeriod * 1000 && - previousItem.tmid === item.tmid && - item.t !== 'rm' && - previousItem.t !== 'rm' - ) { - return false; - } - return true; - } catch (error) { - return true; + const replyBroadcast = () => { + if (replyBroadcastProp) { + replyBroadcastProp(item); } - } + }; - get isThreadReply(): boolean { - const { item, previousItem, isThreadRoom } = this.props; - if (isThreadRoom) { - return false; - } - if (previousItem && item.tmid && previousItem.tmid !== item.tmid && previousItem.id !== item.tmid) { - return true; + const onLinkPress = (link: string): void => { + const isMessageLink = message.attachments?.findIndex((att: IAttachment) => att?.message_link === link) !== -1; + if (isMessageLink && jumpToMessage) { + return jumpToMessage(link); } - return false; - } + openLink(link, theme); + }; - get isThreadSequential(): boolean { - const { item, isThreadRoom } = this.props; - if (isThreadRoom) { - return false; + const onLongPress = () => { + if (isInfo || hasError || isEncrypted || archived) { + return; } - return !!item.tmid; - } - - get isEncrypted(): boolean { - const { item } = this.props; - const { t, e2e } = item; - return t === E2E_MESSAGE_TYPE && e2e !== E2E_STATUS.DONE; - } - - get isInfo(): string | boolean { - const { item } = this.props; - if (['e2e', 'discussion-created', 'jitsi_call_started', 'videoconf'].includes(item.t)) { - return false; + if (onLongPressProp) { + onLongPressProp(item); } - return item.t; - } + }; - get isTemp(): boolean { - const { item } = this.props; - return item.status === messagesStatus.TEMP || item.status === messagesStatus.ERROR; - } + // Stable debounced onPress — one instance for the component lifetime, + // always invoking the latest closure via the ref. + const pressHandlerRef = useRef<() => void>(() => {}); + pressHandlerRef.current = () => { + if (isIgnored) { + return onIgnoredMessagePress(); + } - get isIgnored(): boolean { - const { isManualUnignored } = this.state; - const { isIgnored } = this.props; - if (isManualUnignored) { - return false; + if (onPressProp) { + return onPressProp(); } - return isIgnored ?? false; - } - get hasError(): boolean { - const { item } = this.props; - return item.status === messagesStatus.ERROR; - } + Keyboard.dismiss(); - reactionInit = () => { - const { reactionInit, item } = this.props; - if (reactionInit) { - reactionInit(item.id); + if ((message.tlm || message.tmid) && !isThreadRoom) { + onThreadPress(); } - }; - replyBroadcast = () => { - const { replyBroadcast, item } = this.props; - if (replyBroadcast) { - replyBroadcast(item); + if (message.dlm && onDiscussionPressProp) { + onDiscussionPressProp(item); } }; + const onPress = useRef(debounce(() => pressHandlerRef.current(), 300, true)).current; - onLinkPress = (link: string): void => { - const { item, jumpToMessage, theme } = this.props; - const isMessageLink = item?.attachments?.findIndex((att: IAttachment) => att?.message_link === link) !== -1; - if (isMessageLink && jumpToMessage) { - return jumpToMessage(link); + const onPressAction = () => { + if (closeEmojiAndAction) { + return closeEmojiAndAction(onPress); } - openLink(link, theme); + return onPress(); }; - render() { - const { - item, - user, - archived, - baseUrl, - useRealName, - broadcast, - fetchThreadName, - showAttachment, - timeFormat, - isReadReceiptEnabled, - autoTranslateRoom, - autoTranslateLanguage, - navToRoomInfo = () => {}, - getCustomEmoji, - isThreadRoom, - handleEnterCall, - blockAction, - rid, - threadBadgeColor, - toggleFollowThread, - jumpToMessage, - highlighted, - isBeingEdited, - isPreview, - showUnreadSeparator, - dateSeparator - } = this.props; - const { - id, - msg, - ts, - attachments, - urls, - reactions, - t, - avatar, - emoji, - u, - alias, - editedBy, - role, - drid, - dcount, - dlm, - tmid, - tcount, - tlm, - tmsg, - mentions, - channels, - unread, - blocks, - autoTranslate: autoTranslateMessage, - replies, - md, - comment, - pinned - } = item; - - let message = msg; - let isTranslated = false; - const otherUserMessage = u?.username !== user?.username; - // "autoTranslateRoom" and "autoTranslateLanguage" are properties from the subscription - // "autoTranslateMessage" is a toggle between "View Original" and "Translate" state - if (autoTranslateRoom && autoTranslateMessage && autoTranslateLanguage && otherUserMessage) { - const messageTranslated = getMessageTranslation(item, autoTranslateLanguage); - isTranslated = !!messageTranslated; - message = messageTranslated || message; - } - - const canTranslateMessage = autoTranslateRoom && autoTranslateLanguage && autoTranslateMessage !== false && otherUserMessage; - - return ( - - {/* @ts-ignore*/} - - - - ); + const { + id, + msg, + ts, + attachments, + urls, + reactions, + t, + avatar, + emoji, + u, + alias, + editedBy, + role, + drid, + dcount, + dlm, + tmid, + tcount, + tlm, + tmsg, + mentions, + channels, + unread, + blocks, + autoTranslate: autoTranslateMessage, + replies, + md, + comment, + pinned + } = message; + + let messageText = msg; + let isTranslated = false; + const otherUserMessage = u?.username !== user?.username; + if (autoTranslateRoom && autoTranslateMessage && autoTranslateLanguage && otherUserMessage) { + const messageTranslated = getMessageTranslation(item, autoTranslateLanguage); + isTranslated = !!messageTranslated; + messageText = messageTranslated || messageText; } -} -export default withTheme(MessageContainer); + const canTranslateMessage = autoTranslateRoom && autoTranslateLanguage && autoTranslateMessage !== false && otherUserMessage; + + return ( + + {/* @ts-ignore*/} + + + + ); +}; + +export default memo(MessageContainer, areEqual); From 1209090f00f0a27912198afca69c0e8c6d3a5b64 Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Mon, 29 Jun 2026 16:23:39 -0300 Subject: [PATCH 005/144] perf: move Message interaction state to a per-Room store (NATIVE-1349) Move `action` and `selectedMessages` out of the RoomView class state into a per-Room Zustand store (InteractionStore), mirroring the MessageComposer store topology. Message rows now read their edit state via a `useIsBeingEdited` selector instead of a prop, so starting or cancelling an edit re-renders only the affected row instead of triggering a RoomView render and a FlatList walk. Composer consumers read the interaction values from the store while keeping their handlers on RoomContext, so the RoomContext value no longer changes when interaction state changes. ShareView provides its own store for its composer subtree. Behavior is unchanged. --- .../MessageComposer/MessageComposer.test.tsx | 50 ++++--- .../MessageComposer/MessageComposer.tsx | 5 +- .../MessageComposer/components/CancelEdit.tsx | 4 +- .../components/ComposerInput.tsx | 5 +- .../components/Quotes/Quotes.tsx | 5 +- .../MessageComposer/hooks/useAutoSaveDraft.ts | 5 +- .../hooks/useChooseMedia.test.tsx | 11 +- .../MessageComposer/hooks/useChooseMedia.ts | 5 +- app/containers/message/Message.tsx | 1 + .../__snapshots__/Message.test.tsx.snap | 2 + app/containers/message/index.test.tsx | 33 +++++ app/containers/message/index.tsx | 5 +- app/views/RoomView/InteractionStore.tsx | 79 ++++++++++ app/views/RoomView/context.ts | 2 - app/views/RoomView/definitions.ts | 5 +- app/views/RoomView/index.tsx | 137 +++++++++--------- app/views/ShareView/index.tsx | 59 ++++---- 17 files changed, 279 insertions(+), 134 deletions(-) create mode 100644 app/views/RoomView/InteractionStore.tsx diff --git a/app/containers/MessageComposer/MessageComposer.test.tsx b/app/containers/MessageComposer/MessageComposer.test.tsx index e9cb34b73f0..e511f287dc2 100644 --- a/app/containers/MessageComposer/MessageComposer.test.tsx +++ b/app/containers/MessageComposer/MessageComposer.test.tsx @@ -12,7 +12,8 @@ import { mockedStore } from '../../reducers/mockedStore'; import { type IPermissionsState } from '../../reducers/permissions'; import { type IMessage, type IShareAttachment } from '../../definitions'; import { colors } from '../../lib/constants/colors'; -import { type IRoomContext, RoomContext } from '../../views/RoomView/context'; +import { type IRoomContext, RoomContext, type TMessageAction } from '../../views/RoomView/context'; +import { InteractionProvider } from '../../views/RoomView/InteractionStore'; import * as EmojiKeyboardHook from './hooks/useEmojiKeyboard'; import { initStore } from '../../lib/store/auxStore'; import { search } from '../../lib/methods/search'; @@ -109,24 +110,32 @@ const initialContext = { federated: false }, sharing: false, - action: null, - selectedMessages: [], editCancel: jest.fn(), editRequest: jest.fn(), onSendMessage: jest.fn(), onRemoveQuoteMessage: jest.fn() }; -const Render = ({ context, children }: { context?: Partial; children?: ReactElement }) => ( +const Render = ({ + context, + interactionState, + children +}: { + context?: Partial; + interactionState?: { action?: TMessageAction; selectedMessages?: string[] }; + children?: ReactElement; +}) => ( - - - <> - - {children} - - - + + + + <> + + {children} + + + + ); @@ -638,7 +647,10 @@ describe('MessageComposer', () => { const id = 'messageId'; beforeEach(() => { return renderAndFlush( - + ); }); test('init', async () => { @@ -670,7 +682,9 @@ describe('MessageComposer', () => { const editRequest = jest.fn(); const id = 'image'; test('edit image', async () => { - await renderAndFlush(); + await renderAndFlush( + + ); await screen.findByTestId('message-composer'); await user.press(screen.getByTestId('message-composer-send')); expect(editRequest).toHaveBeenCalledWith({ id, msg: `Attachment description for ${id}`, rid: 'rid' }); @@ -702,14 +716,14 @@ describe('MessageComposer', () => { describe('Quote', () => { test('Add quote `abc`', async () => { - render(); + render(); await screen.findByTestId('composer-quote-abc'); expect(screen.queryByTestId('composer-quote-abc')).toBeOnTheScreen(); expect(screen.toJSON()).toMatchSnapshot(); }); test('Add quote `def`', async () => { - render(); + render(); await screen.findByTestId('composer-quote-abc'); expect(screen.queryByTestId('composer-quote-abc')).toBeOnTheScreen(); expect(screen.queryByTestId('composer-quote-def')).toBeOnTheScreen(); @@ -718,7 +732,9 @@ describe('MessageComposer', () => { test('Remove a quote', async () => { const onRemoveQuoteMessage = jest.fn(); - render(); + render( + + ); await screen.findByTestId('composer-quote-def'); await user.press(screen.getByTestId('composer-quote-remove-def')); expect(onRemoveQuoteMessage).toHaveBeenCalledTimes(1); diff --git a/app/containers/MessageComposer/MessageComposer.tsx b/app/containers/MessageComposer/MessageComposer.tsx index f9367890e4d..1fda1b7954b 100644 --- a/app/containers/MessageComposer/MessageComposer.tsx +++ b/app/containers/MessageComposer/MessageComposer.tsx @@ -5,6 +5,7 @@ import { Q } from '@nozbe/watermelondb'; import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; import { useRoomContext } from '../../views/RoomView/context'; +import { useMessageAction, useSelectedMessages } from '../../views/RoomView/InteractionStore'; import { Autocomplete } from './components'; import { MIN_HEIGHT } from './constants'; import { @@ -54,7 +55,9 @@ export const MessageComposer = ({ }); const contentHeight = useSharedValue(MIN_HEIGHT); useCloseKeyboardWhenOrientationChanges(); - const { rid, tmid, action, selectedMessages, sharing, editRequest, onSendMessage, setQuotesAndText } = useRoomContext(); + const { rid, tmid, sharing, editRequest, onSendMessage, setQuotesAndText } = useRoomContext(); + const action = useMessageAction(); + const selectedMessages = useSelectedMessages(); const alsoSendThreadToChannel = useAlsoSendThreadToChannel(); const { showEmojiKeyboard, showEmojiSearchbar, openEmojiSearchbar, resetKeyboard, keyboardHeight } = useEmojiKeyboard(); const { setAlsoSendThreadToChannel, setAutocompleteParams, clearAttachments } = useMessageComposerApi(); diff --git a/app/containers/MessageComposer/components/CancelEdit.tsx b/app/containers/MessageComposer/components/CancelEdit.tsx index 04da5354405..8c65d7bbc3e 100644 --- a/app/containers/MessageComposer/components/CancelEdit.tsx +++ b/app/containers/MessageComposer/components/CancelEdit.tsx @@ -1,11 +1,13 @@ import { BaseButton } from './Buttons'; import { useRoomContext } from '../../../views/RoomView/context'; +import { useMessageAction } from '../../../views/RoomView/InteractionStore'; import { Gap } from './Gap'; export const CancelEdit = () => { 'use memo'; - const { action, editCancel } = useRoomContext(); + const { editCancel } = useRoomContext(); + const action = useMessageAction(); if (action !== 'edit') { return null; diff --git a/app/containers/MessageComposer/components/ComposerInput.tsx b/app/containers/MessageComposer/components/ComposerInput.tsx index 8bc8eed21cd..90e2c5df4b4 100644 --- a/app/containers/MessageComposer/components/ComposerInput.tsx +++ b/app/containers/MessageComposer/components/ComposerInput.tsx @@ -33,6 +33,7 @@ import database from '../../../lib/database'; import Navigation from '../../../lib/navigation/appNavigation'; import { emitter } from '../../../lib/methods/helpers/emitter'; import { useRoomContext } from '../../../views/RoomView/context'; +import { useMessageAction, useSelectedMessages } from '../../../views/RoomView/InteractionStore'; import { getMessageById } from '../../../lib/database/services/Message'; import { generateTriggerId } from '../../../lib/methods/actions'; import { executeCommandPreview } from '../../../lib/services/restApi'; @@ -50,7 +51,9 @@ const defaultSelection: IInputSelection = { start: 0, end: 0 }; export const ComposerInput = memo( forwardRef(({ inputRef }, ref) => { const { colors, theme } = useTheme(); - const { rid, tmid, sharing, action, selectedMessages, setQuotesAndText, room } = useRoomContext(); + const { rid, tmid, sharing, setQuotesAndText, room } = useRoomContext(); + const action = useMessageAction(); + const selectedMessages = useSelectedMessages(); const focused = useFocused(); const { setFocused, setMicOrSend, setAutocompleteParams } = useMessageComposerApi(); const autocompleteType = useAutocompleteParams()?.type; diff --git a/app/containers/MessageComposer/components/Quotes/Quotes.tsx b/app/containers/MessageComposer/components/Quotes/Quotes.tsx index 7eadcf4f5ed..377d07f6d5e 100644 --- a/app/containers/MessageComposer/components/Quotes/Quotes.tsx +++ b/app/containers/MessageComposer/components/Quotes/Quotes.tsx @@ -2,12 +2,13 @@ import { useEffect, useRef, type ReactElement } from 'react'; import { FlatList } from 'react-native'; import { Quote } from './Quote'; -import { useRoomContext } from '../../../../views/RoomView/context'; +import { useMessageAction, useSelectedMessages } from '../../../../views/RoomView/InteractionStore'; export const Quotes = (): ReactElement | null => { 'use memo'; - const { selectedMessages, action } = useRoomContext(); + const selectedMessages = useSelectedMessages(); + const action = useMessageAction(); const nQuotesRef = useRef(0); const listRef = useRef(null); diff --git a/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts b/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts index 5c547542366..2f7c2c0c5f2 100644 --- a/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts +++ b/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts @@ -3,13 +3,16 @@ import { useCallback, useEffect, useRef } from 'react'; import { saveDraftMessage } from '../../../lib/methods/draftMessage'; import { useRoomContext } from '../../../views/RoomView/context'; +import { useMessageAction, useSelectedMessages } from '../../../views/RoomView/InteractionStore'; import { useFocused } from '../context'; export const useAutoSaveDraft = (text = '') => { 'use memo'; const route = useRoute(); - const { rid, tmid, action, selectedMessages } = useRoomContext(); + const { rid, tmid } = useRoomContext(); + const action = useMessageAction(); + const selectedMessages = useSelectedMessages(); const focused = useFocused(); const oldText = useRef(''); const intervalRef = useRef(null); diff --git a/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx b/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx index a1f52c7d728..cbd4be40d0a 100644 --- a/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx +++ b/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx @@ -18,6 +18,11 @@ jest.mock('../../../views/RoomView/context', () => ({ useRoomContext: jest.fn() })); +jest.mock('../../../views/RoomView/InteractionStore', () => ({ + useMessageAction: jest.fn(), + useSelectedMessages: jest.fn() +})); + jest.mock('../../../lib/hooks/useAltTextSupported', () => ({ useAltTextSupported: jest.fn() })); @@ -46,6 +51,8 @@ const mockGetDocumentAsync = require('expo-document-picker').getDocumentAsync as const mockUseAppSelector = require('../../../lib/hooks/useAppSelector').useAppSelector as jest.Mock; const mockUseMessageComposerApi = require('../context').useMessageComposerApi as jest.Mock; const mockUseRoomContext = require('../../../views/RoomView/context').useRoomContext as jest.Mock; +const mockUseMessageAction = require('../../../views/RoomView/InteractionStore').useMessageAction as jest.Mock; +const mockUseSelectedMessages = require('../../../views/RoomView/InteractionStore').useSelectedMessages as jest.Mock; const mockUseAltTextSupported = require('../../../lib/hooks/useAltTextSupported').useAltTextSupported as jest.Mock; const mockGetSubscriptionByRoomId = require('../../../lib/database/services/Subscription').getSubscriptionByRoomId as jest.Mock; const mockGetThreadById = require('../../../lib/database/services/Thread').getThreadById as jest.Mock; @@ -67,11 +74,11 @@ describe('useChooseMedia', () => { ); mockUseMessageComposerApi.mockReturnValue({ addAttachments }); mockUseRoomContext.mockReturnValue({ - action: null, setQuotesAndText: jest.fn(), - selectedMessages: [], getText: jest.fn(() => 'draft') }); + mockUseMessageAction.mockReturnValue(null); + mockUseSelectedMessages.mockReturnValue([]); mockGetSubscriptionByRoomId.mockResolvedValue({ rid: 'room-id', t: 'c' }); mockGetThreadById.mockResolvedValue({ id: 'thread-id' }); }); diff --git a/app/containers/MessageComposer/hooks/useChooseMedia.ts b/app/containers/MessageComposer/hooks/useChooseMedia.ts index dad98c7be54..068b3e0581e 100644 --- a/app/containers/MessageComposer/hooks/useChooseMedia.ts +++ b/app/containers/MessageComposer/hooks/useChooseMedia.ts @@ -10,6 +10,7 @@ import { getThreadById } from '../../../lib/database/services/Thread'; import Navigation from '../../../lib/navigation/appNavigation'; import { useAppSelector } from '../../../lib/hooks/useAppSelector'; import { useRoomContext } from '../../../views/RoomView/context'; +import { useMessageAction, useSelectedMessages } from '../../../views/RoomView/InteractionStore'; import { type IShareAttachment } from '../../../definitions'; import ImagePicker, { type ImageOrVideo } from '../../../lib/methods/helpers/ImagePicker/ImagePicker'; import { useMessageComposerApi } from '../context'; @@ -31,7 +32,9 @@ export const useChooseMedia = ({ const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = useAppSelector(state => state.settings); const { addAttachments } = useMessageComposerApi(); - const { action, setQuotesAndText, selectedMessages, getText } = useRoomContext(); + const { setQuotesAndText, getText } = useRoomContext(); + const action = useMessageAction(); + const selectedMessages = useSelectedMessages(); const altTextSupported = useAltTextSupported(); const allowList = FileUpload_MediaTypeWhiteList as string; const maxFileSize = FileUpload_MaxFileSize as number; diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index ec465eec8da..127a85ccf32 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -210,6 +210,7 @@ const MessageTouchable = memo((props: IMessageTouchable & IMessage) => { onPress={onPress} disabled={isDisabled} style={{ backgroundColor }} + testID={props.isBeingEdited ? `message-editing-${props.id}` : undefined} accessible accessibilityRole='button' accessibilityLabel={accessibilityLabelValue} diff --git a/app/containers/message/__snapshots__/Message.test.tsx.snap b/app/containers/message/__snapshots__/Message.test.tsx.snap index 2f9d43e479d..a05b7035932 100644 --- a/app/containers/message/__snapshots__/Message.test.tsx.snap +++ b/app/containers/message/__snapshots__/Message.test.tsx.snap @@ -32901,6 +32901,7 @@ exports[`Story Snapshots: Editing should match snapshot 1`] = ` "opacity": 1, } } + testID="message-editing-undefined" > { const { forwardRef } = require('react'); @@ -91,3 +93,34 @@ it('fires the onLongPress callback when long-pressed', () => { fireEvent(getByTestId('message-msg-1'), 'longPress'); expect(onLongPress).toHaveBeenCalledTimes(1); }); + +describe('edit highlight reacts to InteractionStore', () => { + it('shows editing testID when the store marks this message as being edited', () => { + const store = createInteractionStore({ action: 'edit', selectedMessages: ['msg-1'] }); + const item = createMockMessage({ id: 'msg-1' }); + const { getByTestId } = render( + + + + + + ); + expect(getByTestId('message-editing-msg-1')).toBeTruthy(); + }); + + it('removes editing testID when the store resets', async () => { + const store = createInteractionStore({ action: 'edit', selectedMessages: ['msg-1'] }); + const item = createMockMessage({ id: 'msg-1' }); + const { queryByTestId } = render( + + + + + + ); + await act(() => { + store.getState().actions.reset(); + }); + expect(queryByTestId('message-editing-msg-1')).toBeNull(); + }); +}); diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 16f74ef196c..1e179b8aeb9 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -13,6 +13,7 @@ import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; import { messagesStatus } from '../../lib/constants/messagesStatus'; import MessageSeparator from '../MessageSeparator'; import { useMessage } from './hooks/useMessage'; +import { useIsBeingEdited } from '../../views/RoomView/InteractionStore'; interface IMessageContainerProps { item: TAnyMessageModel; @@ -60,7 +61,6 @@ interface IMessageContainerProps { onPress?: () => void; theme?: TSupportedThemes; closeEmojiAndAction?: (action?: Function, params?: any) => void; - isBeingEdited?: boolean; isPreview?: boolean; dateSeparator?: Date | string | null; showUnreadSeparator?: boolean; @@ -74,7 +74,6 @@ function areEqual(prev: IMessageContainerProps, next: IMessageContainerProps): b prev.threadBadgeColor === next.threadBadgeColor && prev.isIgnored === next.isIgnored && prev.previousItem?._id === next.previousItem?._id && - prev.isBeingEdited === next.isBeingEdited && prev.autoTranslateRoom === next.autoTranslateRoom && prev.autoTranslateLanguage === next.autoTranslateLanguage ); @@ -118,7 +117,6 @@ const MessageContainer = ({ jumpToMessage, onPress: onPressProp, closeEmojiAndAction, - isBeingEdited, isPreview, dateSeparator, showUnreadSeparator @@ -126,6 +124,7 @@ const MessageContainer = ({ 'use memo'; const message = useMessage(item); + const isBeingEdited = useIsBeingEdited(item.id); const { theme } = useTheme(); const [isManualUnignored, setIsManualUnignored] = useState(false); diff --git a/app/views/RoomView/InteractionStore.tsx b/app/views/RoomView/InteractionStore.tsx new file mode 100644 index 00000000000..1af2f0c0589 --- /dev/null +++ b/app/views/RoomView/InteractionStore.tsx @@ -0,0 +1,79 @@ +import { createContext, type ReactElement, useContext, useState } from 'react'; +import { createStore, useStore } from 'zustand'; + +import { type TMessageAction } from './context'; + +type TInteractionActions = { + setEditing(messageId: string): void; + initQuote(messageId: string): void; + appendQuote(messageId: string): void; + removeQuote(messageId: string): void; + setReacting(messageId: string): void; + setQuotes(messageIds: string[]): void; + reset(): void; +}; + +type InteractionState = { + action: TMessageAction; + selectedMessages: string[]; + // Built once inside the store initializer — stable ref for consumers. + actions: TInteractionActions; +}; + +export const createInteractionStore = (initialState?: { action?: TMessageAction; selectedMessages?: string[] }) => + createStore()(set => ({ + action: initialState?.action ?? null, + selectedMessages: initialState?.selectedMessages ?? [], + actions: { + setEditing: messageId => set({ action: 'edit', selectedMessages: [messageId] }), + initQuote: messageId => set({ action: 'quote', selectedMessages: [messageId] }), + appendQuote: messageId => set(state => ({ selectedMessages: [...state.selectedMessages, messageId] })), + removeQuote: messageId => + set(state => { + const msgs = state.selectedMessages.filter(m => m !== messageId); + return { selectedMessages: msgs, action: msgs.length ? 'quote' : null }; + }), + setReacting: messageId => set({ action: 'react', selectedMessages: [messageId] }), + setQuotes: messageIds => set({ action: messageIds.length ? 'quote' : null, selectedMessages: messageIds }), + reset: () => set({ action: null, selectedMessages: [] }) + } + })); + +export type InteractionStore = ReturnType; + +export const InteractionStoreContext = createContext(null); + +// Rows rendered outside a RoomView (search, pinned) can never be in edit mode. +// Using a module-level fallback avoids a conditional hook call in useIsBeingEdited. +const fallbackStore = createInteractionStore(); + +const useInteractionStore = (selector: (state: InteractionState) => T): T => { + const store = useContext(InteractionStoreContext); + if (!store) { + throw new Error('Interaction hooks must be used within an InteractionProvider'); + } + return useStore(store, selector); +}; + +export const useMessageAction = (): TMessageAction => useInteractionStore(s => s.action); +export const useSelectedMessages = (): string[] => useInteractionStore(s => s.selectedMessages); + +export const useIsBeingEdited = (messageId: string): boolean => { + const store = useContext(InteractionStoreContext) ?? fallbackStore; + return useStore(store, s => s.action === 'edit' && s.selectedMessages[0] === messageId); +}; + +export const useInteractionApi = (): TInteractionActions => useInteractionStore(s => s.actions); + +export const InteractionProvider = ({ + initialState, + children +}: { + initialState?: { action?: TMessageAction; selectedMessages?: string[] }; + children: ReactElement; +}): ReactElement => { + 'use memo'; + + const [store] = useState(() => createInteractionStore(initialState)); + return {children}; +}; diff --git a/app/views/RoomView/context.ts b/app/views/RoomView/context.ts index eb2e2d3d2db..16c04d8e11c 100644 --- a/app/views/RoomView/context.ts +++ b/app/views/RoomView/context.ts @@ -8,9 +8,7 @@ export interface IRoomContext { tmid?: string; room: any; // FIXME: type it properly after we migrate RoomView to hooks sharing?: boolean; - action?: TMessageAction; isAutocompleteVisible?: boolean; - selectedMessages: string[]; editCancel?: () => void; editRequest?: (message: any) => void; onRemoveQuoteMessage?: (messageId: string) => void; diff --git a/app/views/RoomView/definitions.ts b/app/views/RoomView/definitions.ts index e583db6a387..e38ccfef85c 100644 --- a/app/views/RoomView/definitions.ts +++ b/app/views/RoomView/definitions.ts @@ -6,8 +6,7 @@ import { type ILastMessage, type ILoggedUser, type TSubscriptionModel, - type ICustomEmojis, - type TMessageAction + type ICustomEmojis } from '../../definitions'; import { type IActionSheetProvider } from '../../containers/ActionSheet'; @@ -68,8 +67,6 @@ export interface IRoomViewState { readOnly: boolean; unreadsCount: number | null; roomUserId?: string | null; - action: TMessageAction; - selectedMessages: string[]; isAutocompleteVisible: boolean; showMissingE2EEKey: boolean; showE2EEDisabledRoom: boolean; diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index f2b3c882a1d..06075e345ed 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -98,6 +98,7 @@ import { withActionSheet } from '../../containers/ActionSheet'; import { goRoom, type TGoRoomItem } from '../../lib/methods/helpers/goRoom'; import { ComposerAttachments, type IMessageComposerRef, MessageComposerContainer } from '../../containers/MessageComposer'; import { RoomContext } from './context'; +import { createInteractionStore, InteractionStoreContext, type InteractionStore } from './InteractionStore'; import AudioManager from '../../lib/methods/AudioManager'; import { type IListContainerRef, type TListRef } from './List/definitions'; import { resolveJumpAnchor } from './services/resolveJumpAnchor'; @@ -142,6 +143,7 @@ class RoomView extends Component { }; private sub?: RoomClass; private unsubscribeBlur?: () => void; + private interactionStore: InteractionStore; constructor(props: IRoomViewProps) { super(props); @@ -167,6 +169,10 @@ class RoomView extends Component { this.jumpToThreadId = props.route.params?.jumpToThreadId; const roomUserId = props.route.params?.roomUserId ?? getUidDirectMessage(room); const selectedMessages = props.route.params?.messageId ? [props.route.params.messageId] : []; + this.interactionStore = createInteractionStore({ + selectedMessages, + action: selectedMessages.length ? 'quote' : null + }); this.state = { joined: true, room, @@ -174,8 +180,6 @@ class RoomView extends Component { member: {}, lastOpen: null, reactionsModalVisible: false, - selectedMessages, - action: selectedMessages.length ? 'quote' : null, canAutoTranslate: false, loading: true, replyWithMention: false, @@ -222,7 +226,7 @@ class RoomView extends Component { componentDidMount() { const { navigation, dispatch } = this.props; - const { selectedMessages } = this.state; + const { selectedMessages } = this.interactionStore.getState(); dispatch(clearInAppFeedback()); this.mounted = true; this.didMountInteraction = InteractionManager.runAfterInteractions(() => { @@ -294,9 +298,6 @@ class RoomView extends Component { if (stateUpdated) { return true; } - if (!dequal(nextState.selectedMessages, state.selectedMessages)) { - return true; - } if (!dequal(nextProps.insets, insets)) { return true; } @@ -791,11 +792,11 @@ class RoomView extends Component { }; onEditInit = (messageId: string) => { - const { action } = this.state; + const { action, actions } = this.interactionStore.getState(); if (action) { return; } - this.setState({ selectedMessages: [messageId], action: 'edit' }); + actions.setEditing(messageId); }; onEditCancel = () => { @@ -837,32 +838,30 @@ class RoomView extends Component { }; onQuoteInit = (messageId: string) => { - const { action } = this.state; + const { action, selectedMessages, actions } = this.interactionStore.getState(); if (action === 'quote') { - if (!this.state.selectedMessages.includes(messageId)) { - this.setState(({ selectedMessages }) => ({ selectedMessages: [...selectedMessages, messageId] })); + if (!selectedMessages.includes(messageId)) { + actions.appendQuote(messageId); } return; } if (action) { return; } - this.setState({ selectedMessages: [messageId], action: 'quote' }); + actions.initQuote(messageId); }; onRemoveQuoteMessage = (messageId: string) => { - const { selectedMessages } = this.state; - const newSelectedMessages = selectedMessages.filter(item => item !== messageId); - this.setState({ selectedMessages: newSelectedMessages, action: newSelectedMessages.length ? 'quote' : null }); + this.interactionStore.getState().actions.removeQuote(messageId); }; resetAction = () => { - this.setState({ action: null, selectedMessages: [] }); + this.interactionStore.getState().actions.reset(); }; showReactionPicker = () => { const { showActionSheet } = this.props; - const { selectedMessages } = this.state; + const { selectedMessages } = this.interactionStore.getState(); setTimeout(() => { showActionSheet({ children: ( @@ -881,11 +880,12 @@ class RoomView extends Component { }; onReactionInit = (messageId: string) => { - if (this.state.action) { + if (this.interactionStore.getState().action) { return; } this.handleCloseEmoji(() => { - this.setState({ selectedMessages: [messageId], action: 'react' }, this.showReactionPicker); + this.interactionStore.getState().actions.setReacting(messageId); + this.showReactionPicker(); }); }; @@ -896,7 +896,7 @@ class RoomView extends Component { }; onMessageLongPress = (message: TAnyMessageModel) => { - const { action } = this.state; + const { action } = this.interactionStore.getState(); if (action && action !== 'quote') { return; } @@ -1389,11 +1389,7 @@ class RoomView extends Component { }; setQuotesAndText = (text: string, quotes: string[]) => { - if (quotes.length) { - this.setState({ selectedMessages: quotes, action: 'quote' }); - } else { - this.setState({ action: null, selectedMessages: [] }); - } + this.interactionStore.getState().actions.setQuotes(quotes); this.messageComposerRef.current?.setInput(text || ''); }; @@ -1429,10 +1425,8 @@ class RoomView extends Component { theme, inAppFeedback } = this.props; - const { action, selectedMessages } = this.state; let dateSeparator = null; let showUnreadSeparator = false; - const isBeingEdited = action === 'edit' && item.id === selectedMessages[0]; const federated = 'id' in room && isRoomFederated(room); if (!previousItem) { @@ -1514,7 +1508,6 @@ class RoomView extends Component { highlighted={highlightedMessage === item.id} theme={theme} closeEmojiAndAction={this.handleCloseEmoji} - isBeingEdited={isBeingEdited} dateSeparator={dateSeparator} showUnreadSeparator={showUnreadSeparator} /> @@ -1654,7 +1647,7 @@ class RoomView extends Component { render() { console.count(`${this.constructor.name}.render calls`); - const { room, action, selectedMessages, isAutocompleteVisible, showMissingE2EEKey, showE2EEDisabledRoom } = this.state; + const { room, isAutocompleteVisible, showMissingE2EEKey, showE2EEDisabledRoom } = this.state; const { user, baseUrl, theme, width, serverVersion, navigation } = this.props; const { rid, t } = room; let bannerClosed; @@ -1686,50 +1679,50 @@ class RoomView extends Component { } return ( - - - {!this.tmid ? ( - + + + {!this.tmid ? ( + + ) : null} + - ) : null} - - {this.renderFooter()} - {this.renderActions()} - - - - + {this.renderFooter()} + {this.renderActions()} + + + + + ); } } diff --git a/app/views/ShareView/index.tsx b/app/views/ShareView/index.tsx index f6cf1436956..39c561d7a3d 100644 --- a/app/views/ShareView/index.tsx +++ b/app/views/ShareView/index.tsx @@ -39,6 +39,7 @@ import { sendAttachments } from '../../lib/methods/sendFileMessage/sendAttachmen import { sendMessage } from '../../lib/methods/sendMessage'; import { hasPermission, isAndroid, canUploadFile, isReadOnly, isBlocked } from '../../lib/methods/helpers'; import { RoomContext } from '../RoomView/context'; +import { createInteractionStore, InteractionStoreContext, type InteractionStore } from '../RoomView/InteractionStore'; import { appStart } from '../../actions/app'; interface IShareViewState { @@ -80,6 +81,7 @@ class ShareView extends Component { private serverInfo: TShareServerInfo; private finishShareView: (text?: string, selectedMessages?: string[]) => void; private sentMessage: boolean; + private interactionStore: InteractionStore; constructor(props: IShareViewProps) { super(props); @@ -89,6 +91,7 @@ class ShareView extends Component { this.serverInfo = props.route.params?.serverInfo ?? {}; this.finishShareView = props.route.params?.finishShareView; this.sentMessage = false; + this.interactionStore = createInteractionStore({ action: props.route.params?.action, selectedMessages: [] }); this.state = { selected: {} as IShareAttachment, @@ -237,6 +240,7 @@ class ShareView extends Component { await new Promise(resolve => setTimeout(resolve, 100)); this.messageComposerRef.current?.setInput(text); this.setState({ selectedMessages }); + this.interactionStore.getState().actions.setQuotes(selectedMessages); } }; @@ -382,39 +386,40 @@ class ShareView extends Component { const { selectedMessages } = this.state; const newSelectedMessages = selectedMessages.filter(item => item !== messageId); this.setState({ selectedMessages: newSelectedMessages, action: newSelectedMessages.length ? 'quote' : null }); + this.interactionStore.getState().actions.setQuotes(newSelectedMessages); }; renderContent = () => { - const { attachments, selected, text, room, thread, selectedMessages } = this.state; - const { theme, route } = this.props; + const { attachments, selected, text, room, thread } = this.state; + const { theme } = this.props; if (attachments.length) { return ( - - - - - - - - + + + + + + + + + + ); } From 45ea86df71b2b4d9301f2bd025eaa3d871771fac Mon Sep 17 00:00:00 2001 From: Diego Mello Date: Mon, 29 Jun 2026 17:58:01 -0300 Subject: [PATCH 006/144] refactor: narrow Message component props contract (NATIVE-1350) Remove the umbrella IMessage/IMessageInner interfaces and the {...props} bag spread from the message component tree. Each presentational component now declares the narrow interface it consumes and receives explicit props. Shared handlers (getCustomEmoji, navToRoomInfo, showAttachment, blockAction, handleEnterCall, fetchThreadName) are read from MessageContext rather than threaded as props, and MessageContext is now typed (IMessageContext) in place of `any`. Behavior is unchanged. --- .rnstorybook/preview.tsx | 8 +- app/containers/message/Blocks.ts | 7 +- app/containers/message/Broadcast.tsx | 2 +- app/containers/message/CallButton.tsx | 8 +- .../Attachments/AttachedActions.tsx | 2 +- .../Attachments/Attachments.stories.tsx | 14 +- .../Components/Attachments/Attachments.tsx | 5 +- .../message/Components/Attachments/Audio.tsx | 4 +- .../Attachments/CollapsibleQuote/index.tsx | 4 +- .../Attachments/Image/Container.tsx | 2 +- .../message/Components/Attachments/Quote.tsx | 15 +- .../message/Components/Attachments/Reply.tsx | 29 +- .../message/Components/Attachments/Video.tsx | 2 +- app/containers/message/Content.test.tsx | 12 +- app/containers/message/Content.tsx | 21 +- app/containers/message/Context.ts | 40 +- app/containers/message/Message.stories.tsx | 85 ++-- app/containers/message/Message.tsx | 439 ++++++++++++++++-- app/containers/message/MessageAvatar.tsx | 10 +- app/containers/message/Reactions.test.tsx | 9 +- app/containers/message/Reactions.tsx | 17 +- app/containers/message/RepliedThread.tsx | 6 +- app/containers/message/Thread.tsx | 4 +- app/containers/message/Urls.tsx | 4 +- app/containers/message/User.tsx | 21 +- .../message/hooks/useMediaAutoDownload.tsx | 12 +- .../useMessageAccessibilityLabel.test.ts | 5 +- .../hooks/useMessageAccessibilityLabel.ts | 19 +- app/containers/message/index.tsx | 15 +- app/containers/message/interfaces.ts | 52 +-- 30 files changed, 627 insertions(+), 246 deletions(-) diff --git a/.rnstorybook/preview.tsx b/.rnstorybook/preview.tsx index 6cc89495278..134f80e7ac3 100644 --- a/.rnstorybook/preview.tsx +++ b/.rnstorybook/preview.tsx @@ -35,7 +35,13 @@ const preview: Preview = { onReactionPress: () => {}, onDiscussionPress: () => {}, onReactionLongPress: () => {}, - threadBadgeColor: themes.light.badgeBackgroundLevel1 + threadBadgeColor: themes.light.badgeBackgroundLevel1, + getCustomEmoji: () => null, + navToRoomInfo: () => {}, + showAttachment: undefined, + blockAction: undefined, + handleEnterCall: undefined, + fetchThreadName: undefined }}> diff --git a/app/containers/message/Blocks.ts b/app/containers/message/Blocks.ts index c37fbfe230c..62ad320cbf5 100644 --- a/app/containers/message/Blocks.ts +++ b/app/containers/message/Blocks.ts @@ -1,11 +1,14 @@ -import { createElement } from 'react'; +import { createElement, useContext } from 'react'; import { messageBlockWithContext } from '../UIKit/MessageBlock'; import { type IMessageBlocks } from './interfaces'; +import MessageContext from './Context'; -const Blocks = ({ blocks, id: mid, rid, blockAction }: IMessageBlocks) => { +const Blocks = ({ blocks, id: mid, rid }: IMessageBlocks) => { 'use memo'; + const { blockAction } = useContext(MessageContext); + if (blocks && blocks.length > 0) { const appId = blocks[0]?.appId || ''; return createElement( diff --git a/app/containers/message/Broadcast.tsx b/app/containers/message/Broadcast.tsx index 406900c5ecf..b494a5cee3a 100644 --- a/app/containers/message/Broadcast.tsx +++ b/app/containers/message/Broadcast.tsx @@ -17,7 +17,7 @@ const Broadcast = memo(({ author, broadcast }: IMessageBroadcast) => { const { user, replyBroadcast } = useContext(MessageContext); const { theme } = useTheme(); - const isOwn = author?._id === user.id; + const isOwn = author?._id === user?.id; if (broadcast && !isOwn) { return ( diff --git a/app/containers/message/CallButton.tsx b/app/containers/message/CallButton.tsx index ae3b841109a..66545459d04 100644 --- a/app/containers/message/CallButton.tsx +++ b/app/containers/message/CallButton.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { memo, useContext } from 'react'; import { Text, View } from 'react-native'; import Touchable from './Touchable'; @@ -7,13 +7,13 @@ import styles from './styles'; import I18n from '../../i18n'; import { CustomIcon } from '../CustomIcon'; import { themes } from '../../lib/constants/colors'; -import { type IMessageCallButton } from './interfaces'; import { useTheme } from '../../theme'; +import MessageContext from './Context'; -// TODO: Create a reusable button component for message -const CallButton = memo(({ handleEnterCall }: IMessageCallButton) => { +const CallButton = memo(() => { 'use memo'; + const { handleEnterCall } = useContext(MessageContext); const { theme } = useTheme(); return ( diff --git a/app/containers/message/Components/Attachments/AttachedActions.tsx b/app/containers/message/Components/Attachments/AttachedActions.tsx index fdb804af534..9d534a2a9ee 100644 --- a/app/containers/message/Components/Attachments/AttachedActions.tsx +++ b/app/containers/message/Components/Attachments/AttachedActions.tsx @@ -25,7 +25,7 @@ const AttachedActions = ({ attachment, getCustomEmoji }: { attachment: IAttachme const attachedButtons = attachment.actions.map((element: TElement) => { const onPress = () => { if (element.msg) { - onAnswerButtonPress(element.msg); + onAnswerButtonPress?.(element.msg); } if (element.url) { diff --git a/app/containers/message/Components/Attachments/Attachments.stories.tsx b/app/containers/message/Components/Attachments/Attachments.stories.tsx index 43947a2a374..d3567c24ca0 100644 --- a/app/containers/message/Components/Attachments/Attachments.stories.tsx +++ b/app/containers/message/Components/Attachments/Attachments.stories.tsx @@ -11,7 +11,9 @@ const mockMessageContext = { baseUrl: 'https://open.rocket.chat', user: { id: 'user-id', username: 'rocket.cat', token: 'token' }, onLongPress: () => {}, - translateLanguage: undefined + translateLanguage: undefined, + getCustomEmoji: () => null, + showAttachment: undefined }; const MOCK_IMAGE_1 = { @@ -54,7 +56,7 @@ export const SingleImageOldServer = () => ( - null} timeFormat='LT' /> + @@ -65,7 +67,7 @@ export const SingleImageNewServer = () => ( - null} timeFormat='LT' /> + @@ -76,7 +78,7 @@ export const SingleImageNoAlt = () => ( - null} timeFormat='LT' /> + @@ -87,7 +89,7 @@ export const MultipleImagesNewServer = () => ( - null} timeFormat='LT' /> + @@ -98,7 +100,7 @@ export const MultipleImagesOldServer = () => ( - null} timeFormat='LT' /> + diff --git a/app/containers/message/Components/Attachments/Attachments.tsx b/app/containers/message/Components/Attachments/Attachments.tsx index 329c0e48eec..4b3f2bd4680 100644 --- a/app/containers/message/Components/Attachments/Attachments.tsx +++ b/app/containers/message/Components/Attachments/Attachments.tsx @@ -14,10 +14,10 @@ import { getMessageFromAttachment } from '../../utils'; import { isContentAttachment } from './utils'; const Attachments: FC = memo( - ({ attachments, timeFormat, showAttachment, getCustomEmoji, author }: IMessageAttachments) => { + ({ attachments, timeFormat, author }: IMessageAttachments) => { 'use memo'; - const { translateLanguage } = useContext(MessageContext); + const { translateLanguage, showAttachment, getCustomEmoji = () => null } = useContext(MessageContext); const nonQuoteAttachments = attachments?.filter(isContentAttachment); @@ -87,7 +87,6 @@ const Attachments: FC = memo( attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} - showAttachment={showAttachment} msg={msg} /> ); diff --git a/app/containers/message/Components/Attachments/Audio.tsx b/app/containers/message/Components/Attachments/Audio.tsx index 37bbf5206f4..90a6cef6844 100644 --- a/app/containers/message/Components/Attachments/Audio.tsx +++ b/app/containers/message/Components/Attachments/Audio.tsx @@ -23,8 +23,8 @@ const MessageAudio = ({ file, getCustomEmoji, author, msg }: IMessageAudioProps) return ( - {msg ? : null} - + {msg ? : null} + ); }; diff --git a/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx b/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx index 202c0a32b5b..844a4d0bd47 100644 --- a/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx +++ b/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx @@ -93,7 +93,7 @@ const AttText = memo( return null; } - return ; + return ; }, (prevProps, nextProps) => prevProps.text === nextProps.text ); @@ -116,7 +116,7 @@ const Fields = memo( {field.title} - + ))} diff --git a/app/containers/message/Components/Attachments/Image/Container.tsx b/app/containers/message/Components/Attachments/Image/Container.tsx index bd288bcc046..c751b51e605 100644 --- a/app/containers/message/Components/Attachments/Image/Container.tsx +++ b/app/containers/message/Components/Attachments/Image/Container.tsx @@ -52,7 +52,7 @@ const ImageContainer = ({ if (msg) { return ( - + {image} ); diff --git a/app/containers/message/Components/Attachments/Quote.tsx b/app/containers/message/Components/Attachments/Quote.tsx index 5d45a1e034b..8fa3bf52c49 100644 --- a/app/containers/message/Components/Attachments/Quote.tsx +++ b/app/containers/message/Components/Attachments/Quote.tsx @@ -10,10 +10,10 @@ import { getMessageFromAttachment } from '../../utils'; import { isQuoteAttachment } from './utils'; const Quote: FC = memo( - ({ attachments, timeFormat, showAttachment, getCustomEmoji }: IMessageAttachments) => { + ({ attachments, timeFormat }: IMessageAttachments) => { 'use memo'; - const { translateLanguage } = useContext(MessageContext); + const { translateLanguage, getCustomEmoji = () => null } = useContext(MessageContext); const quotes = attachments?.filter(isQuoteAttachment); @@ -24,16 +24,7 @@ const Quote: FC = memo( const quotesElements = quotes.map((file: IAttachment, index: number) => { const msg = getMessageFromAttachment(file, translateLanguage); - return ( - - ); + return ; }); return {quotesElements}; diff --git a/app/containers/message/Components/Attachments/Reply.tsx b/app/containers/message/Components/Attachments/Reply.tsx index da15a41b9dd..9e8631406b2 100644 --- a/app/containers/message/Components/Attachments/Reply.tsx +++ b/app/containers/message/Components/Attachments/Reply.tsx @@ -97,7 +97,6 @@ interface IMessageReply { timeFormat?: string; getCustomEmoji: TGetCustomEmoji; msg?: string; - showAttachment?: (file: IAttachment) => void; } const Title = memo( @@ -140,7 +139,7 @@ const Description = memo( return ; } - return ; + return ; }, (prevProps, nextProps) => { if (prevProps.attachment.text !== nextProps.attachment.text) { @@ -166,7 +165,7 @@ const UrlImage = memo( return null; } - image = image.includes('http') ? image : `${baseUrl}/${image}?rc_uid=${user.id}&rc_token=${user.token}`; + image = image.includes('http') ? image : `${baseUrl}/${image}?rc_uid=${user?.id ?? ''}&rc_token=${user?.token ?? ''}`; return ; }, (prevProps, nextProps) => prevProps.image === nextProps.image @@ -195,7 +194,7 @@ const Fields = memo( {attachment.fields.map(field => ( {field.title} - + ))} @@ -206,7 +205,7 @@ const Fields = memo( ); const Reply = memo( - ({ attachment, timeFormat, getCustomEmoji, msg, showAttachment }: IMessageReply) => { + ({ attachment, timeFormat, getCustomEmoji, msg }: IMessageReply) => { 'use memo'; const [loading, setLoading] = useState(false); @@ -224,8 +223,8 @@ const Reply = memo( } if (attachment.type === 'file' && attachment.title_link) { setLoading(true); - url = formatAttachmentUrl(attachment.title_link, user.id, user.token, baseUrl); - await fileDownloadAndPreview(url, attachment, id); + url = formatAttachmentUrl(attachment.title_link, user?.id ?? '', user?.token ?? '', baseUrl ?? ''); + await fileDownloadAndPreview(url, attachment, id ?? ''); setLoading(false); return; } @@ -253,18 +252,8 @@ const Reply = memo( <Description attachment={attachment} getCustomEmoji={getCustomEmoji} /> - <Quote - attachments={attachment.attachments} - getCustomEmoji={getCustomEmoji} - timeFormat={timeFormat} - showAttachment={showAttachment} - /> - <Attachments - attachments={attachment.attachments} - getCustomEmoji={getCustomEmoji} - timeFormat={timeFormat} - showAttachment={showAttachment} - /> + <Quote attachments={attachment.attachments} timeFormat={timeFormat} /> + <Attachments attachments={attachment.attachments} timeFormat={timeFormat} /> <Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} /> {loading ? ( <View style={styles.backdrop}> @@ -280,7 +269,7 @@ const Reply = memo( <UrlImage image={attachment.thumb_url} /> </View> </Touchable> - {msg ? <Markdown msg={msg} username={user.username} getCustomEmoji={getCustomEmoji} /> : null} + {msg ? <Markdown msg={msg} username={user?.username} getCustomEmoji={getCustomEmoji} /> : null} </View> ); }, diff --git a/app/containers/message/Components/Attachments/Video.tsx b/app/containers/message/Components/Attachments/Video.tsx index 7d9c90054ad..4feb5eee7c4 100644 --- a/app/containers/message/Components/Attachments/Video.tsx +++ b/app/containers/message/Components/Attachments/Video.tsx @@ -101,7 +101,7 @@ const Video = ({ file, showAttachment, getCustomEmoji, author, msg }: IMessageVi return ( <View style={{ gap: 4 }}> - {msg ? <Markdown msg={msg} username={user.username} getCustomEmoji={getCustomEmoji} /> : null} + {msg ? <Markdown msg={msg} username={user?.username} getCustomEmoji={getCustomEmoji} /> : null} <Touchable onPress={_onPress} style={messageStyles.image}> <Thumbnail status={status} encrypted={isEncrypted} /> </Touchable> diff --git a/app/containers/message/Content.test.tsx b/app/containers/message/Content.test.tsx index 3013e84f872..60b46dc0cb3 100644 --- a/app/containers/message/Content.test.tsx +++ b/app/containers/message/Content.test.tsx @@ -27,9 +27,7 @@ const baseProps: Partial<IMessageContent> = { isIgnored: false, isTranslated: false, isHeader: false, - hasError: false, - getCustomEmoji: jest.fn(), - navToRoomInfo: jest.fn() + hasError: false }; const renderContent = (overrides: Partial<IMessageContent> & { attachments?: any[]; autoTranslateLanguage?: string }) => @@ -38,7 +36,9 @@ const renderContent = (overrides: Partial<IMessageContent> & { attachments?: any <MessageContext.Provider value={{ user: { username: 'john' }, - onLinkPress: jest.fn() + onLinkPress: jest.fn(), + getCustomEmoji: jest.fn(), + navToRoomInfo: jest.fn() }}> <Content {...(baseProps as IMessageContent)} {...(overrides as IMessageContent)} /> </MessageContext.Provider> @@ -50,7 +50,9 @@ const tree = (overrides: Partial<IMessageContent> & { attachments?: any[]; autoT <MessageContext.Provider value={{ user: { username: 'john' }, - onLinkPress: jest.fn() + onLinkPress: jest.fn(), + getCustomEmoji: jest.fn(), + navToRoomInfo: jest.fn() }}> <Content {...(baseProps as IMessageContent)} {...(overrides as IMessageContent)} /> </MessageContext.Provider> diff --git a/app/containers/message/Content.tsx b/app/containers/message/Content.tsx index c6f132ff7be..7f87ac15a06 100644 --- a/app/containers/message/Content.tsx +++ b/app/containers/message/Content.tsx @@ -18,7 +18,7 @@ const Content = memo( 'use memo'; const { theme } = useTheme(); - const { user, onLinkPress } = useContext(MessageContext); + const { user, onLinkPress, getCustomEmoji = () => null, navToRoomInfo } = useContext(MessageContext); if (props.isInfo) { // @ts-ignore @@ -32,7 +32,18 @@ const Content = memo( if (messageHaveAuthorName(props.type as MessageTypesValues)) { return ( <Text> - <User {...props} /> {renderMessageContent} + <User + hasError={props.hasError} + type={props.type} + isEdited={props.isEdited} + isTranslated={props.isTranslated} + isHeader={props.isHeader} + useRealName={props.useRealName} + author={props.author} + alias={props.alias} + pinned={props.pinned} + />{' '} + {renderMessageContent} </Text> ); } @@ -64,11 +75,11 @@ const Content = memo( <Markdown msg={props.msg} md={props.type !== 'e2e' ? props.md : undefined} - getCustomEmoji={props.getCustomEmoji} - username={user.username} + getCustomEmoji={getCustomEmoji} + username={user?.username ?? ''} channels={props.channels} mentions={props.mentions} - navToRoomInfo={props.navToRoomInfo} + navToRoomInfo={navToRoomInfo} useRealName={props.useRealName} onLinkPress={onLinkPress} isTranslated={props.isTranslated} diff --git a/app/containers/message/Context.ts b/app/containers/message/Context.ts index 3f2784fce01..5b8f41f05e8 100644 --- a/app/containers/message/Context.ts +++ b/app/containers/message/Context.ts @@ -1,4 +1,40 @@ import { createContext } from 'react'; -// @ts-ignore -const MessageContext = createContext<any>(); + +import { type TGetCustomEmoji } from '../../definitions/IEmoji'; +import { type IAttachment } from '../../definitions'; +import { type IRoomInfoParam } from '../../views/SearchMessagesView'; + +export interface IMessageContext { + id?: string; + rid?: string; + user?: { id?: string; username?: string; token?: string }; + baseUrl?: string; + onPress?: () => void; + onLongPress?: () => void; + reactionInit?: () => void; + onErrorPress?: () => void; + replyBroadcast?: () => void; + onReactionPress?: (emoji: string) => void; + onEncryptedPress?: () => void; + onDiscussionPress?: () => void; + onThreadPress?: () => void; + onReactionLongPress?: () => void; + onLinkPress?: (link: string) => void; + onAnswerButtonPress?: (msg: string) => void; + jumpToMessage?: (link: string) => void; + threadBadgeColor?: string; + toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise<void>; + replies?: string[]; + translateLanguage?: string; + isEncrypted?: boolean; + e2e?: string; + getCustomEmoji?: TGetCustomEmoji; + navToRoomInfo?: (navParam: IRoomInfoParam) => void; + showAttachment?: (file: IAttachment) => void; + blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void; + handleEnterCall?: () => void; + fetchThreadName?: (tmid: string, id: string) => Promise<string | undefined>; +} + +const MessageContext = createContext<IMessageContext>({}); export default MessageContext; diff --git a/app/containers/message/Message.stories.tsx b/app/containers/message/Message.stories.tsx index 0320d52459f..9b262b9481e 100644 --- a/app/containers/message/Message.stories.tsx +++ b/app/containers/message/Message.stories.tsx @@ -2,6 +2,8 @@ import { ScrollView } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import MessageComponent from './Message'; +import MessageContext from './Context'; +import { type ICustomEmoji } from '../../definitions/IEmoji'; import { E2E_MESSAGE_TYPE } from '../../lib/constants/keys'; import { messagesStatus } from '../../lib/constants/messagesStatus'; import { themes } from '../../lib/constants/colors'; @@ -62,13 +64,38 @@ const responsiveLayoutProviderLargeFontValue = (fontScale: number) => ({ height: 800 }); -const getCustomEmoji = (content: string) => { +const getCustomEmoji = (content: string): ICustomEmoji | null => { const customEmoji = { marioparty: { name: content, extension: 'gif' }, react_rocket: { name: content, extension: 'png' }, nyan_rocket: { name: content, extension: 'png' } }[content]; - return customEmoji; + return customEmoji ?? null; +}; + +const storyContextValue = { + user, + baseUrl, + onPress: () => {}, + onLongPress: () => {}, + reactionInit: () => {}, + onErrorPress: () => {}, + replyBroadcast: () => {}, + onReactionPress: () => {}, + onEncryptedPress: () => {}, + onDiscussionPress: () => {}, + onThreadPress: () => {}, + onReactionLongPress: () => {}, + onLinkPress: () => {}, + onAnswerButtonPress: () => {}, + jumpToMessage: () => {}, + threadBadgeColor: themes[_theme].badgeBackgroundLevel1, + getCustomEmoji, + navToRoomInfo: () => {}, + showAttachment: undefined, + blockAction: undefined, + handleEnterCall: undefined, + fetchThreadName: undefined }; export default { @@ -85,36 +112,38 @@ export default { }; export const Message = (props: any) => ( - <ResponsiveLayoutContext.Provider value={responsiveLayoutProviderLargeFontValue(1)}> - <MessageComponent - baseUrl={baseUrl} - user={user} - author={author} - ts={date} - timeFormat='LT' - isHeader - getCustomEmoji={getCustomEmoji} - theme={_theme} - {...props} - /> - </ResponsiveLayoutContext.Provider> + <MessageContext.Provider value={storyContextValue}> + <ResponsiveLayoutContext.Provider value={responsiveLayoutProviderLargeFontValue(1)}> + <MessageComponent + baseUrl={baseUrl} + user={user} + author={author} + ts={date} + timeFormat='LT' + isHeader + theme={_theme} + {...props} + /> + </ResponsiveLayoutContext.Provider> + </MessageContext.Provider> ); // The large font components are not perfect because the text's font scale increases only with the device's font size setting. const MessageLargeFont = (props: any) => ( - <ResponsiveLayoutContext.Provider value={responsiveLayoutProviderLargeFontValue(FONT_SCALE_LIMIT)}> - <MessageComponent - baseUrl={baseUrl} - user={user} - author={author} - ts={date} - timeFormat='LT' - isHeader - getCustomEmoji={getCustomEmoji} - theme={_theme} - {...props} - /> - </ResponsiveLayoutContext.Provider> + <MessageContext.Provider value={storyContextValue}> + <ResponsiveLayoutContext.Provider value={responsiveLayoutProviderLargeFontValue(FONT_SCALE_LIMIT)}> + <MessageComponent + baseUrl={baseUrl} + user={user} + author={author} + ts={date} + timeFormat='LT' + isHeader + theme={_theme} + {...props} + /> + </ResponsiveLayoutContext.Provider> + </MessageContext.Provider> ); export const Basic = () => ( diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index 127a85ccf32..62b9b9849b4 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -16,7 +16,16 @@ import Broadcast from './Broadcast'; import Discussion from './Discussion'; import Content from './Content'; import CallButton from './CallButton'; -import { type IMessage, type IMessageInner, type IMessageTouchable } from './interfaces'; +import { + type IMessageAttachments, + type IMessageAvatar, + type IMessageBlocks, + type IMessageBroadcast, + type IMessageContent, + type IMessageRepliedThread, + type IMessageThread, + type IMessageTouchable +} from './interfaces'; import { useTheme } from '../../theme'; import RightIcons from './Components/RightIcons'; import { WidthAwareView } from './Components/WidthAwareView'; @@ -29,7 +38,29 @@ import { useMessageAccessibilityLabel } from './hooks/useMessageAccessibilityLab import { useMessageAccessibilityActions } from './hooks/useMessageAccessibilityActions'; import { useMessageAccessibilityHint } from './hooks/useMessageAccessibilityHint'; -const MessageInner = memo((props: IMessageInner) => { +type TMessageProps = IMessageContent & + IMessageAttachments & + IMessageThread & + IMessageBlocks & + IMessageBroadcast & + IMessageAvatar & + IMessageRepliedThread & + IMessageTouchable & { + isReadReceiptEnabled?: boolean; + unread?: boolean; + dcount?: number; + dlm?: Date | string; + isThreadSequential: boolean; + isPreview?: boolean; + }; + +interface IMessageA11y { + accessibilityActions?: AccessibilityActionInfo[]; + onAccessibilityAction?: (event: AccessibilityActionEvent) => void; + handleLongPress?: () => void; +} + +const MessageInner = memo((props: TMessageProps) => { const { isLargeFontScale } = useResponsiveLayout(); const showTimeLarge = isLargeFontScale && props.isHeader; @@ -37,14 +68,52 @@ const MessageInner = memo((props: IMessageInner) => { if (props.isPreview) { content = ( <> - <User {...props} /> - {showTimeLarge ? <MessageTime {...props} /> : null} + <User + isHeader={props.isHeader} + hasError={props.hasError} + useRealName={props.useRealName} + author={props.author} + alias={props.alias} + ts={props.ts as Date | undefined} + timeFormat={props.timeFormat} + isEdited={props.isEdited} + isReadReceiptEnabled={props.isReadReceiptEnabled} + unread={props.unread} + pinned={props.pinned} + isTranslated={props.isTranslated} + type={props.type} + /> + {showTimeLarge ? <MessageTime ts={props.ts as Date | undefined} timeFormat={props.timeFormat} /> : null} <> - <Quote {...props} /> - <Content {...props} /> - <Attachments {...props} /> + <Quote attachments={props.attachments} timeFormat={props.timeFormat} author={props.author} /> + <Content + isTemp={props.isTemp} + isInfo={props.isInfo} + tmid={props.tmid} + isThreadRoom={props.isThreadRoom} + msg={props.msg} + md={props.md} + isEdited={props.isEdited} + isEncrypted={props.isEncrypted} + channels={props.channels} + mentions={props.mentions} + useRealName={props.useRealName} + isIgnored={props.isIgnored} + type={props.type} + comment={props.comment} + hasError={props.hasError} + isHeader={props.isHeader} + isTranslated={props.isTranslated} + pinned={props.pinned} + attachments={props.attachments} + autoTranslateLanguage={props.autoTranslateLanguage} + author={props.author} + alias={props.alias} + role={props.role} + /> + <Attachments attachments={props.attachments} timeFormat={props.timeFormat} author={props.author} /> </> - <Urls {...props} /> + <Urls urls={props.urls} /> </> ); } @@ -52,9 +121,23 @@ const MessageInner = memo((props: IMessageInner) => { if (props.type === 'discussion-created') { content = ( <> - <User {...props} /> - {showTimeLarge ? <MessageTime {...props} /> : null} - <Discussion {...props} /> + <User + isHeader={props.isHeader} + hasError={props.hasError} + useRealName={props.useRealName} + author={props.author} + alias={props.alias} + ts={props.ts as Date | undefined} + timeFormat={props.timeFormat} + isEdited={props.isEdited} + isReadReceiptEnabled={props.isReadReceiptEnabled} + unread={props.unread} + pinned={props.pinned} + isTranslated={props.isTranslated} + type={props.type} + /> + {showTimeLarge ? <MessageTime ts={props.ts as Date | undefined} timeFormat={props.timeFormat} /> : null} + <Discussion msg={props.msg} dcount={props.dcount} dlm={props.dlm} /> </> ); } @@ -62,10 +145,48 @@ const MessageInner = memo((props: IMessageInner) => { if (props.type === 'jitsi_call_started') { content = ( <> - <User {...props} /> - <Content {...props} isInfo /> - <CallButton {...props} /> - {showTimeLarge ? <MessageTime {...props} /> : null} + <User + isHeader={props.isHeader} + hasError={props.hasError} + useRealName={props.useRealName} + author={props.author} + alias={props.alias} + ts={props.ts as Date | undefined} + timeFormat={props.timeFormat} + isEdited={props.isEdited} + isReadReceiptEnabled={props.isReadReceiptEnabled} + unread={props.unread} + pinned={props.pinned} + isTranslated={props.isTranslated} + type={props.type} + /> + <Content + isTemp={props.isTemp} + isInfo + tmid={props.tmid} + isThreadRoom={props.isThreadRoom} + msg={props.msg} + md={props.md} + isEdited={props.isEdited} + isEncrypted={props.isEncrypted} + channels={props.channels} + mentions={props.mentions} + useRealName={props.useRealName} + isIgnored={props.isIgnored} + type={props.type} + comment={props.comment} + hasError={props.hasError} + isHeader={props.isHeader} + isTranslated={props.isTranslated} + pinned={props.pinned} + attachments={props.attachments} + autoTranslateLanguage={props.autoTranslateLanguage} + author={props.author} + alias={props.alias} + role={props.role} + /> + <CallButton /> + {showTimeLarge ? <MessageTime ts={props.ts as Date | undefined} timeFormat={props.timeFormat} /> : null} </> ); } @@ -73,11 +194,25 @@ const MessageInner = memo((props: IMessageInner) => { if (props.blocks && props.blocks.length) { content = ( <> - <User {...props} /> - <Blocks {...props} /> - <Thread {...props} /> - <Reactions {...props} /> - {showTimeLarge ? <MessageTime {...props} /> : null} + <User + isHeader={props.isHeader} + hasError={props.hasError} + useRealName={props.useRealName} + author={props.author} + alias={props.alias} + ts={props.ts as Date | undefined} + timeFormat={props.timeFormat} + isEdited={props.isEdited} + isReadReceiptEnabled={props.isReadReceiptEnabled} + unread={props.unread} + pinned={props.pinned} + isTranslated={props.isTranslated} + type={props.type} + /> + <Blocks blocks={props.blocks} id={props.id} rid={props.rid} /> + <Thread msg={props.msg} tcount={props.tcount} tlm={props.tlm} id={props.id} isThreadRoom={props.isThreadRoom} /> + <Reactions reactions={props.reactions} /> + {showTimeLarge ? <MessageTime ts={props.ts as Date | undefined} timeFormat={props.timeFormat} /> : null} </> ); } @@ -85,16 +220,54 @@ const MessageInner = memo((props: IMessageInner) => { if (!content) { content = ( <> - <User {...props} /> - {showTimeLarge ? <MessageTime {...props} /> : null} + <User + isHeader={props.isHeader} + hasError={props.hasError} + useRealName={props.useRealName} + author={props.author} + alias={props.alias} + ts={props.ts as Date | undefined} + timeFormat={props.timeFormat} + isEdited={props.isEdited} + isReadReceiptEnabled={props.isReadReceiptEnabled} + unread={props.unread} + pinned={props.pinned} + isTranslated={props.isTranslated} + type={props.type} + /> + {showTimeLarge ? <MessageTime ts={props.ts as Date | undefined} timeFormat={props.timeFormat} /> : null} <View style={{ gap: 4 }}> - <Quote {...props} /> - <Content {...props} /> - <Attachments {...props} /> - <Urls {...props} /> - <Thread {...props} /> - <Reactions {...props} /> - <Broadcast {...props} /> + <Quote attachments={props.attachments} timeFormat={props.timeFormat} author={props.author} /> + <Content + isTemp={props.isTemp} + isInfo={props.isInfo} + tmid={props.tmid} + isThreadRoom={props.isThreadRoom} + msg={props.msg} + md={props.md} + isEdited={props.isEdited} + isEncrypted={props.isEncrypted} + channels={props.channels} + mentions={props.mentions} + useRealName={props.useRealName} + isIgnored={props.isIgnored} + type={props.type} + comment={props.comment} + hasError={props.hasError} + isHeader={props.isHeader} + isTranslated={props.isTranslated} + pinned={props.pinned} + attachments={props.attachments} + autoTranslateLanguage={props.autoTranslateLanguage} + author={props.author} + alias={props.alias} + role={props.role} + /> + <Attachments attachments={props.attachments} timeFormat={props.timeFormat} author={props.author} /> + <Urls urls={props.urls} /> + <Thread msg={props.msg} tcount={props.tcount} tlm={props.tlm} id={props.id} isThreadRoom={props.isThreadRoom} /> + <Reactions reactions={props.reactions} /> + <Broadcast author={props.author} broadcast={props.broadcast} /> </View> </> ); @@ -104,21 +277,23 @@ const MessageInner = memo((props: IMessageInner) => { }); MessageInner.displayName = 'MessageInner'; -interface IMessageA11y { - accessibilityActions?: AccessibilityActionInfo[]; - onAccessibilityAction?: (event: AccessibilityActionEvent) => void; - handleLongPress?: () => void; -} -const Message = memo((props: IMessageTouchable & IMessage & IMessageA11y) => { +const Message = memo((props: TMessageProps & IMessageA11y) => { if (props.isThreadReply || props.isThreadSequential || props.isInfo || props.isIgnored) { - const thread = props.isThreadReply ? <RepliedThread {...props} /> : null; - // Prevent misalignment of info when the font size is increased. + const thread = props.isThreadReply ? ( + <RepliedThread + tmid={props.tmid} + tmsg={props.tmsg} + id={props.id} + isHeader={props.isHeader} + isEncrypted={props.isEncrypted} + /> + ) : null; const infoStyle: ViewStyle = props.isInfo ? { alignItems: 'center' } : {}; return ( <View style={[styles.container, { marginTop: 4 }]}> {thread} <View style={[styles.flex, infoStyle]}> - <MessageAvatar small {...props} /> + <MessageAvatar isHeader={props.isHeader} avatar={props.avatar} emoji={props.emoji} author={props.author} small /> <A11y.Index accessible={props.isTranslated} accessibilityLabel={props?.msg || ''} @@ -126,10 +301,34 @@ const Message = memo((props: IMessageTouchable & IMessage & IMessageA11y) => { index={2} style={{ flex: 1 }}> <View style={styles.messageContent}> - <Content {...props} /> + <Content + isTemp={props.isTemp} + isInfo={props.isInfo} + tmid={props.tmid} + isThreadRoom={props.isThreadRoom} + msg={props.msg} + md={props.md} + isEdited={props.isEdited} + isEncrypted={props.isEncrypted} + channels={props.channels} + mentions={props.mentions} + useRealName={props.useRealName} + isIgnored={props.isIgnored} + type={props.type} + comment={props.comment} + hasError={props.hasError} + isHeader={props.isHeader} + isTranslated={props.isTranslated} + pinned={props.pinned} + attachments={props.attachments} + autoTranslateLanguage={props.autoTranslateLanguage} + author={props.author} + alias={props.alias} + role={props.role} + /> {props.isInfo && props.type === 'message_pinned' ? ( <View pointerEvents='none'> - <Attachments {...props} /> + <Attachments attachments={props.attachments} timeFormat={props.timeFormat} author={props.author} /> </View> ) : null} </View> @@ -147,9 +346,58 @@ const Message = memo((props: IMessageTouchable & IMessage & IMessageA11y) => { accessibilityLanguage={props.autoTranslateLanguage} index={2}> <View style={styles.flex}> - <MessageAvatar {...props} /> + <MessageAvatar isHeader={props.isHeader} avatar={props.avatar} emoji={props.emoji} author={props.author} /> <View style={styles.messageContent}> - <MessageInner {...props} /> + <MessageInner + id={props.id} + rid={props.rid} + msg={props.msg} + md={props.md} + type={props.type} + attachments={props.attachments} + blocks={props.blocks} + urls={props.urls} + reactions={props.reactions} + alias={props.alias} + avatar={props.avatar} + emoji={props.emoji} + timeFormat={props.timeFormat} + archived={props.archived} + broadcast={props.broadcast} + useRealName={props.useRealName} + isReadReceiptEnabled={props.isReadReceiptEnabled} + unread={props.unread} + role={props.role} + drid={props.drid} + dcount={props.dcount} + dlm={props.dlm} + tmid={props.tmid} + tcount={props.tcount} + tlm={props.tlm} + tmsg={props.tmsg} + mentions={props.mentions} + channels={props.channels} + isIgnored={props.isIgnored} + isEdited={props.isEdited} + isHeader={props.isHeader} + isThreadReply={props.isThreadReply} + isThreadSequential={props.isThreadSequential} + isThreadRoom={props.isThreadRoom} + isInfo={props.isInfo} + isTemp={props.isTemp} + isEncrypted={props.isEncrypted} + hasError={props.hasError} + highlighted={props.highlighted} + comment={props.comment} + isTranslated={props.isTranslated} + isBeingEdited={props.isBeingEdited} + isPreview={props.isPreview} + pinned={props.pinned} + autoTranslateLanguage={props.autoTranslateLanguage} + author={props.author} + ts={props.ts} + small={props.small} + /> </View> {!props.isHeader ? ( <RightIcons @@ -170,7 +418,7 @@ const Message = memo((props: IMessageTouchable & IMessage & IMessageA11y) => { }); Message.displayName = 'Message'; -const MessageTouchable = memo((props: IMessageTouchable & IMessage) => { +const MessageTouchable = memo((props: TMessageProps) => { const { onPress, onLongPress } = useContext(MessageContext); const { colors } = useTheme(); const { ref: touchRef, markAsLastFocused } = useLastFocusedMessageRef(); @@ -191,14 +439,63 @@ const MessageTouchable = memo((props: IMessageTouchable & IMessage) => { if (props.hasError || props.isInfo) { return ( <A11y.Order> - <Message {...props} /> + <Message + id={props.id} + rid={props.rid} + msg={props.msg} + md={props.md} + type={props.type} + attachments={props.attachments} + blocks={props.blocks} + urls={props.urls} + reactions={props.reactions} + alias={props.alias} + avatar={props.avatar} + emoji={props.emoji} + timeFormat={props.timeFormat} + archived={props.archived} + broadcast={props.broadcast} + useRealName={props.useRealName} + isReadReceiptEnabled={props.isReadReceiptEnabled} + unread={props.unread} + role={props.role} + drid={props.drid} + dcount={props.dcount} + dlm={props.dlm} + tmid={props.tmid} + tcount={props.tcount} + tlm={props.tlm} + tmsg={props.tmsg} + mentions={props.mentions} + channels={props.channels} + isIgnored={props.isIgnored} + isEdited={props.isEdited} + isHeader={props.isHeader} + isThreadReply={props.isThreadReply} + isThreadSequential={props.isThreadSequential} + isThreadRoom={props.isThreadRoom} + isInfo={props.isInfo} + isTemp={props.isTemp} + isEncrypted={props.isEncrypted} + hasError={props.hasError} + highlighted={props.highlighted} + comment={props.comment} + isTranslated={props.isTranslated} + isBeingEdited={props.isBeingEdited} + isPreview={props.isPreview} + pinned={props.pinned} + autoTranslateLanguage={props.autoTranslateLanguage} + author={props.author} + ts={props.ts} + small={props.small} + /> </A11y.Order> ); } const handleLongPress = () => { markAsLastFocused(); - onLongPress(); + onLongPress?.(); }; return ( @@ -219,7 +516,57 @@ const MessageTouchable = memo((props: IMessageTouchable & IMessage) => { onAccessibilityAction={e => { if (e.nativeEvent.actionName === 'showActions') handleLongPress(); }}> - <Message {...props} handleLongPress={!isDisabled ? handleLongPress : undefined} /> + <Message + id={props.id} + rid={props.rid} + msg={props.msg} + md={props.md} + type={props.type} + attachments={props.attachments} + blocks={props.blocks} + urls={props.urls} + reactions={props.reactions} + alias={props.alias} + avatar={props.avatar} + emoji={props.emoji} + timeFormat={props.timeFormat} + archived={props.archived} + broadcast={props.broadcast} + useRealName={props.useRealName} + isReadReceiptEnabled={props.isReadReceiptEnabled} + unread={props.unread} + role={props.role} + drid={props.drid} + dcount={props.dcount} + dlm={props.dlm} + tmid={props.tmid} + tcount={props.tcount} + tlm={props.tlm} + tmsg={props.tmsg} + mentions={props.mentions} + channels={props.channels} + isIgnored={props.isIgnored} + isEdited={props.isEdited} + isHeader={props.isHeader} + isThreadReply={props.isThreadReply} + isThreadSequential={props.isThreadSequential} + isThreadRoom={props.isThreadRoom} + isInfo={props.isInfo} + isTemp={props.isTemp} + isEncrypted={props.isEncrypted} + hasError={props.hasError} + highlighted={props.highlighted} + comment={props.comment} + isTranslated={props.isTranslated} + isBeingEdited={props.isBeingEdited} + isPreview={props.isPreview} + pinned={props.pinned} + autoTranslateLanguage={props.autoTranslateLanguage} + author={props.author} + ts={props.ts} + small={props.small} + handleLongPress={!isDisabled ? handleLongPress : undefined} + /> </Touch> </A11y.Index> </A11y.Order> diff --git a/app/containers/message/MessageAvatar.tsx b/app/containers/message/MessageAvatar.tsx index 3727b2c4729..bfb43834599 100644 --- a/app/containers/message/MessageAvatar.tsx +++ b/app/containers/message/MessageAvatar.tsx @@ -16,10 +16,10 @@ export const AvatarContainer = ({ children }: { children?: ReactElement | null } return <View style={{ width, alignItems: 'flex-end' }}>{children}</View>; }; -const MessageAvatar = memo(({ isHeader, avatar, author, small, navToRoomInfo, emoji, getCustomEmoji }: IMessageAvatar) => { +const MessageAvatar = memo(({ isHeader, avatar, author, small, emoji }: IMessageAvatar) => { 'use memo'; - const { user } = useContext(MessageContext); + const { user, navToRoomInfo, getCustomEmoji } = useContext(MessageContext); const { fontScaleLimited } = useResponsiveLayout(); const smallSize = 20 * fontScaleLimited; const normalSize = AVATAR_BASE_SIZE * fontScaleLimited; @@ -27,10 +27,10 @@ const MessageAvatar = memo(({ isHeader, avatar, author, small, navToRoomInfo, em if (isHeader && author) { const onPress = () => - navToRoomInfo({ + navToRoomInfo?.({ t: SubscriptionType.DIRECT, rid: author._id, - itsMe: author._id === user.id + itsMe: author._id === user?.id }); return ( @@ -41,7 +41,7 @@ const MessageAvatar = memo(({ isHeader, avatar, author, small, navToRoomInfo, em size={size} borderRadius={4} onPress={onPress} - getCustomEmoji={getCustomEmoji} + getCustomEmoji={getCustomEmoji ?? (() => null)} avatar={avatar} emoji={emoji} /> diff --git a/app/containers/message/Reactions.test.tsx b/app/containers/message/Reactions.test.tsx index 05d1fe8b081..6007415c48f 100644 --- a/app/containers/message/Reactions.test.tsx +++ b/app/containers/message/Reactions.test.tsx @@ -22,6 +22,8 @@ const initialMockedStoreState = () => { initialMockedStoreState(); +const getCustomEmoji = jest.fn(); + const TestWrapper = ({ initialReactions }: { initialReactions: IReaction[] }) => { const [reactions, setReactions] = useState<IReaction[]>(initialReactions); const handleReactionPress = (emoji: string) => { @@ -35,16 +37,15 @@ const TestWrapper = ({ initialReactions }: { initialReactions: IReaction[] }) => user: { username: 'john' }, reactionInit: jest.fn(), onReactionPress: handleReactionPress, - onReactionLongPress: jest.fn() + onReactionLongPress: jest.fn(), + getCustomEmoji }}> - <Reactions reactions={reactions} getCustomEmoji={getCustomEmoji} /> + <Reactions reactions={reactions} /> </MessageContext.Provider> </Provider> ); }; -const getCustomEmoji = jest.fn(); - it('renders all reactions and AddReaction button', () => { const reactions: IReaction[] = [ { _id: '1', emoji: '👍', usernames: ['john', 'alice'], names: [] }, diff --git a/app/containers/message/Reactions.tsx b/app/containers/message/Reactions.tsx index aaadbe5b369..e8f6d5f8853 100644 --- a/app/containers/message/Reactions.tsx +++ b/app/containers/message/Reactions.tsx @@ -10,7 +10,6 @@ import { BUTTON_HIT_SLOP } from './utils'; import { themes } from '../../lib/constants/colors'; import { type TSupportedThemes, useTheme } from '../../theme'; import MessageContext from './Context'; -import { type TGetCustomEmoji } from '../../definitions/IEmoji'; interface IReaction { _id: string; @@ -20,13 +19,11 @@ interface IReaction { interface IMessageReaction { reaction: IReaction; - getCustomEmoji: TGetCustomEmoji; theme: TSupportedThemes; } interface IMessageReactions { reactions?: IReaction[]; - getCustomEmoji: TGetCustomEmoji; } const AddReaction = memo(({ theme }: { theme: TSupportedThemes }) => { @@ -52,16 +49,16 @@ const AddReaction = memo(({ theme }: { theme: TSupportedThemes }) => { ); }); -const Reaction = memo(({ reaction, getCustomEmoji, theme }: IMessageReaction) => { +const Reaction = memo(({ reaction, theme }: IMessageReaction) => { 'use memo'; - const { onReactionPress, onReactionLongPress, user } = useContext(MessageContext); + const { onReactionPress, onReactionLongPress, user, getCustomEmoji } = useContext(MessageContext); const { fontScale } = useWindowDimensions(); const height = 28 * fontScale; - const reacted = reaction.usernames.findIndex((item: string) => item === user.username) !== -1; + const reacted = reaction.usernames.findIndex((item: string) => item === user?.username) !== -1; return ( <Touchable - onPress={() => onReactionPress(reaction.emoji)} + onPress={() => onReactionPress?.(reaction.emoji)} onLongPress={onReactionLongPress} key={reaction.emoji} testID={`message-reaction-${reaction.emoji}`} @@ -80,7 +77,7 @@ const Reaction = memo(({ reaction, getCustomEmoji, theme }: IMessageReaction) => content={reaction.emoji} standardEmojiStyle={styles.reactionEmoji} customEmojiStyle={styles.reactionCustomEmoji} - getCustomEmoji={getCustomEmoji} + getCustomEmoji={getCustomEmoji ?? (() => null)} /> <Text style={[styles.reactionCount, { color: themes[theme].badgeBackgroundLevel2 }]}>{reaction.usernames.length}</Text> </View> @@ -88,7 +85,7 @@ const Reaction = memo(({ reaction, getCustomEmoji, theme }: IMessageReaction) => ); }); -const Reactions = memo(({ reactions, getCustomEmoji }: IMessageReactions) => { +const Reactions = memo(({ reactions }: IMessageReactions) => { 'use memo'; const { theme } = useTheme(); @@ -99,7 +96,7 @@ const Reactions = memo(({ reactions, getCustomEmoji }: IMessageReactions) => { return ( <View style={styles.reactionsContainer}> {reactions.map(reaction => ( - <Reaction key={reaction.emoji} reaction={reaction} getCustomEmoji={getCustomEmoji} theme={theme} /> + <Reaction key={reaction.emoji} reaction={reaction} theme={theme} /> ))} <AddReaction theme={theme} /> </View> diff --git a/app/containers/message/RepliedThread.tsx b/app/containers/message/RepliedThread.tsx index 4f398297dc4..bc4da08edd8 100644 --- a/app/containers/message/RepliedThread.tsx +++ b/app/containers/message/RepliedThread.tsx @@ -1,4 +1,4 @@ -import { memo, useEffect, useState } from 'react'; +import { memo, useContext, useEffect, useState } from 'react'; import { View } from 'react-native'; import { CustomIcon } from '../CustomIcon'; @@ -8,11 +8,13 @@ import { MarkdownPreview } from '../markdown'; import { type IMessageRepliedThread } from './interfaces'; import { useTheme } from '../../theme'; import { AvatarContainer } from './MessageAvatar'; +import MessageContext from './Context'; -const RepliedThread = memo(({ tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted }: IMessageRepliedThread) => { +const RepliedThread = memo(({ tmid, tmsg, isHeader, id, isEncrypted }: IMessageRepliedThread) => { 'use memo'; const { colors } = useTheme(); + const { fetchThreadName } = useContext(MessageContext); const [msg, setMsg] = useState(isEncrypted ? I18n.t('Encrypted_message') : tmsg); useEffect(() => { diff --git a/app/containers/message/Thread.tsx b/app/containers/message/Thread.tsx index e1ebf3cdfb0..e68744721db 100644 --- a/app/containers/message/Thread.tsx +++ b/app/containers/message/Thread.tsx @@ -39,9 +39,9 @@ const Thread = memo( replies, id }} - user={user} + user={{ id: user?.id ?? '' }} badgeColor={threadBadgeColor} - toggleFollowThread={toggleFollowThread} + toggleFollowThread={toggleFollowThread as Function} style={styles.threadDetails} /> </View> diff --git a/app/containers/message/Urls.tsx b/app/containers/message/Urls.tsx index 3bdf1d3d653..c92365d49ec 100644 --- a/app/containers/message/Urls.tsx +++ b/app/containers/message/Urls.tsx @@ -130,8 +130,8 @@ const Url = ({ url }: { url: IUrl }) => { if (!_imageUrl) return null; if (_imageUrl.startsWith('http')) return _imageUrl; - return `${baseUrl}/${_imageUrl}?rc_uid=${user.id}&rc_token=${user.token}`; - }, [url.image, url.url, baseUrl, user.id, user.token]); + return `${baseUrl}/${_imageUrl}?rc_uid=${user?.id ?? ''}&rc_token=${user?.token ?? ''}`; + }, [url.image, url.url, baseUrl, user?.id, user?.token]); useEffect(() => { const verifyUrlIsImage = async () => { diff --git a/app/containers/message/User.tsx b/app/containers/message/User.tsx index bcb80f3f645..470afce1c4a 100644 --- a/app/containers/message/User.tsx +++ b/app/containers/message/User.tsx @@ -3,7 +3,6 @@ import { Pressable, StyleSheet, Text, View } from 'react-native'; import { type MessageType, type MessageTypesValues, SubscriptionType } from '../../definitions'; import { useTheme } from '../../theme'; -import { type IRoomInfoParam } from '../../views/SearchMessagesView'; import sharedStyles from '../../views/Styles'; import RightIcons from './Components/RightIcons'; import MessageContext from './Context'; @@ -54,9 +53,8 @@ interface IMessageUser { username?: string; }; alias?: string; - ts?: Date; + ts?: Date | string; timeFormat?: string; - navToRoomInfo?: (navParam: IRoomInfoParam) => void; type: MessageType; isEdited: boolean; isReadReceiptEnabled?: boolean; @@ -74,22 +72,23 @@ const User = memo( ts, timeFormat, hasError, - navToRoomInfo, type, isEdited, isTranslated, - ...props + isReadReceiptEnabled, + unread, + pinned }: IMessageUser) => { 'use memo'; - const { user } = useContext(MessageContext); + const { user, navToRoomInfo } = useContext(MessageContext); const { colors } = useTheme(); const { isLargeFontScale } = useResponsiveLayout(); if (isHeader) { const username = (useRealName && author?.name) || author?.username; const aliasUsername = alias ? <Text style={[styles.alias, { color: colors.fontSecondaryInfo }]}> @{username}</Text> : null; - const itsMe = author?._id === user.id; + const itsMe = author?._id === user?.id; const onUserPress = () => { navToRoomInfo?.({ @@ -120,15 +119,15 @@ const User = memo( <Text style={[styles.username, { color: colors.fontTitlesLabels }]} numberOfLines={1}> {textContent} </Text> - {isLargeFontScale ? null : <MessageTime timeFormat={timeFormat} ts={ts} />} + {isLargeFontScale ? null : <MessageTime timeFormat={timeFormat} ts={ts as Date | undefined} />} </Pressable> <RightIcons type={type} isEdited={isEdited} hasError={hasError} - isReadReceiptEnabled={props.isReadReceiptEnabled} - unread={props.unread} - pinned={props.pinned} + isReadReceiptEnabled={isReadReceiptEnabled} + unread={unread} + pinned={pinned} isTranslated={isTranslated} /> </View> diff --git a/app/containers/message/hooks/useMediaAutoDownload.tsx b/app/containers/message/hooks/useMediaAutoDownload.tsx index e681f8d3517..b2accf2c3e0 100644 --- a/app/containers/message/hooks/useMediaAutoDownload.tsx +++ b/app/containers/message/hooks/useMediaAutoDownload.tsx @@ -62,13 +62,13 @@ export const useMediaAutoDownload = ({ const fileType = getFileType(file) ?? 'image'; const { id, baseUrl, user } = useContext(MessageContext); const [status, setStatus] = useState<TDownloadState>('to-download'); - const [currentFile, setCurrentFile] = useFile(file, id); + const [currentFile, setCurrentFile] = useFile(file, id ?? ''); const originalUrl = getOriginalURL(file); const url = formatAttachmentUrl( file.title_link || getFileProperty(currentFile, fileType, 'url'), - user.id, - user.token, - baseUrl, + user?.id ?? '', + user?.token ?? '', + baseUrl ?? '', originalUrl ); const isEncrypted = currentFile.e2e === 'pending'; @@ -108,7 +108,7 @@ export const useMediaAutoDownload = ({ }; const tryAutoDownload = async () => { - const isCurrentUserAuthor = author?._id === user.id; + const isCurrentUserAuthor = author?._id === user?.id; const isAutoDownloadEnabled = fetchAutoDownloadEnabled(`${fileType}PreferenceDownload`); if (isAutoDownloadEnabled || isCurrentUserAuthor) { await download(); @@ -121,7 +121,7 @@ export const useMediaAutoDownload = ({ try { setStatus('loading'); const uri = await downloadMediaFile({ - messageId: id, + messageId: id ?? '', downloadUrl: url, type: fileType, mimeType: getFileProperty(currentFile, fileType, 'type'), diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.test.ts b/app/containers/message/hooks/useMessageAccessibilityLabel.test.ts index 227fe9d9f72..149acec7f27 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.test.ts +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.test.ts @@ -1,13 +1,14 @@ import { renderHook } from '@testing-library/react-native'; import { useMessageAccessibilityLabel } from './useMessageAccessibilityLabel'; -import { type IMessage, type IMessageTouchable } from '../interfaces'; +import { type IMessageContent, type IMessageTouchable } from '../interfaces'; jest.mock('../../../lib/hooks/useAltTextSupported', () => ({ useAltTextSupported: () => false })); -type TProps = IMessage & IMessageTouchable; +type TProps = IMessageContent & + IMessageTouchable & { isThreadSequential?: boolean; isReadReceiptEnabled?: boolean; unread?: boolean | null }; const FIXED_TS = new Date('2024-01-15T12:34:56Z'); const HOUR = FIXED_TS.toLocaleTimeString(); diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.ts b/app/containers/message/hooks/useMessageAccessibilityLabel.ts index bec1d4971aa..06d5b45f7de 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.ts +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.ts @@ -1,17 +1,24 @@ import i18n from '../../../i18n'; import translationLanguages from '../../../lib/constants/translationLanguages'; import { useImageDescriptionLabel } from './useImageDescriptionLabel'; -import { type IMessage, type IMessageTouchable } from '../interfaces'; +import { type IMessageContent, type IMessageTouchable } from '../interfaces'; import { getInfoMessage } from '../utils'; -const stripMentions = (label: string, mentions: IMessage['mentions'] = [], channels: IMessage['channels'] = []) => { +type TMessageLabelProps = IMessageContent & + IMessageTouchable & { + isThreadSequential?: boolean; + isReadReceiptEnabled?: boolean; + unread?: boolean | null; + }; + +const stripMentions = (label: string, mentions: IMessageContent['mentions'] = [], channels: IMessageContent['channels'] = []) => { let result = label; - mentions.forEach(item => { + mentions?.forEach(item => { if (item?.username) { result = result.replaceAll(`@${item.username}`, item.username); } }); - channels.forEach(item => { + channels?.forEach(item => { if (item?.name) { result = result.replaceAll(`#${item.name}`, item.name); } @@ -19,7 +26,7 @@ const stripMentions = (label: string, mentions: IMessage['mentions'] = [], chann return result; }; -export const useMessageAccessibilityLabel = (props: IMessage & IMessageTouchable): string => { +export const useMessageAccessibilityLabel = (props: TMessageLabelProps): string => { const imageDescriptionLabel = useImageDescriptionLabel(props.attachments, props.msg); const msg = props?.msg || ''; const threadMessageLabel = i18n.t('Thread_message', { msg }); @@ -39,7 +46,7 @@ export const useMessageAccessibilityLabel = (props: IMessage & IMessageTouchable } label = stripMentions(label, props.mentions, props.channels); - const hour = props.ts ? new Date(props.ts).toLocaleTimeString() : ''; + const hour = props.ts ? new Date(props.ts as Date).toLocaleTimeString() : ''; const user = props.useRealName ? props.author?.name : props.author?.username || ''; const readOrUnreadLabel = !props.unread && props.unread !== null ? i18n.t('Message_was_read') : i18n.t('Message_was_not_read'); const readReceipt = props.isReadReceiptEnabled && !props.isInfo ? readOrUnreadLabel : ''; diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 1e179b8aeb9..2d9bcab4354 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -360,9 +360,14 @@ const MessageContainer = ({ toggleFollowThread, replies, translateLanguage: canTranslateMessage ? autoTranslateLanguage : undefined, - isEncrypted + isEncrypted, + getCustomEmoji, + navToRoomInfo, + showAttachment, + blockAction, + handleEnterCall, + fetchThreadName }}> - {/* @ts-ignore*/} <Message id={id} msg={messageText} @@ -392,7 +397,6 @@ const MessageContainer = ({ tcount={tcount} tlm={tlm} tmsg={tmsg} - fetchThreadName={fetchThreadName} mentions={mentions} channels={channels} isIgnored={isIgnored} @@ -405,11 +409,6 @@ const MessageContainer = ({ isTemp={isTemp} isEncrypted={isEncrypted} hasError={hasError} - showAttachment={showAttachment} - getCustomEmoji={getCustomEmoji} - navToRoomInfo={navToRoomInfo} - handleEnterCall={handleEnterCall} - blockAction={blockAction} highlighted={highlighted} comment={comment} isTranslated={isTranslated} diff --git a/app/containers/message/interfaces.ts b/app/containers/message/interfaces.ts index 6e33a9583bb..694474fce7f 100644 --- a/app/containers/message/interfaces.ts +++ b/app/containers/message/interfaces.ts @@ -10,16 +10,12 @@ import { type IUrl, type IUserMention, type IUserMessage, - type MessageType, - type TAnyMessageModel + type MessageType } from '../../definitions'; -import { type IRoomInfoParam } from '../../views/SearchMessagesView'; export interface IMessageAttachments { attachments?: IAttachment[]; timeFormat?: string; - showAttachment?: (file: IAttachment) => void; - getCustomEmoji: TGetCustomEmoji; author?: IUserMessage; } @@ -29,15 +25,12 @@ export interface IMessageAvatar { emoji?: string; author?: IUserMessage; small?: boolean; - navToRoomInfo: (navParam: IRoomInfoParam) => void; - getCustomEmoji: TGetCustomEmoji; } export interface IMessageBlocks { - blocks: { appId?: string }[]; + blocks?: { appId?: string }[]; id: string; rid: string; - blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void; } export interface IMessageBroadcast { @@ -45,12 +38,8 @@ export interface IMessageBroadcast { broadcast?: boolean; } -export interface IMessageCallButton { - handleEnterCall?: () => void; -} - export interface IMessageContent { - _id: string; + _id?: string; isTemp: boolean; isInfo: string | boolean; tmid?: string; @@ -59,10 +48,8 @@ export interface IMessageContent { md?: Root; isEdited: boolean; isEncrypted: boolean; - getCustomEmoji: TGetCustomEmoji; channels?: IUserChannel[]; mentions?: IUserMention[]; - navToRoomInfo: (navParam: IRoomInfoParam) => void; useRealName?: boolean; isIgnored: boolean; type: MessageType; @@ -73,6 +60,9 @@ export interface IMessageContent { pinned?: boolean; attachments?: IAttachment[]; autoTranslateLanguage?: string; + author?: IUserMessage; + alias?: string; + role?: string; } export interface IMessageEmoji { @@ -104,35 +94,5 @@ export interface IMessageTouchable { export interface IMessageRepliedThread extends Pick<IThread, 'tmid' | 'tmsg' | 'id'> { isHeader: boolean; - fetchThreadName?: (tmid: string, id: string) => Promise<string | undefined>; isEncrypted: boolean; } - -export interface IMessageInner - extends IMessageContent, - IMessageCallButton, - IMessageBlocks, - IMessageThread, - IMessageAttachments, - IMessageBroadcast { - type: MessageType; - blocks: []; - urls?: IUrl[]; - isPreview?: boolean; -} - -export interface IMessage extends IMessageRepliedThread, IMessageInner, IMessageAvatar { - isThreadReply: boolean; - isThreadSequential: boolean; - isInfo: string | boolean; - isTemp: boolean; - isHeader: boolean; - hasError: boolean; - onLongPress?: (item: TAnyMessageModel) => void; - isReadReceiptEnabled?: boolean; - unread?: boolean; - isIgnored: boolean; - dcount: number | undefined; - dlm: string | Date | undefined; - autoTranslateLanguage?: string; -} From c33f71dd3ebe53c472071abce5c7fbb4bea0111e Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 29 Jun 2026 18:12:24 -0300 Subject: [PATCH 007/144] refactor: annotate message presentational layers for React Compiler (NATIVE-1351) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the `'use memo'` opt-in directive to the three presentational memo components in Message.tsx (MessageInner, Message, MessageTouchable), keeping their explicit React.memo gate. This mirrors the existing precedent in message/index.tsx (MessageContainer) under the project's React Compiler annotation mode. Clean Message.stories.tsx to pass only the explicit data props each story exercises: drop the per-story handler props (onReactionPress, onErrorPress, replyBroadcast) and the dead `theme` prop, all of which are now supplied through the MessageContext provider value. Behavior is unchanged — the snapshot suite passes without updates. --- app/containers/message/Message.stories.tsx | 75 +++------------------- app/containers/message/Message.tsx | 6 ++ 2 files changed, 16 insertions(+), 65 deletions(-) diff --git a/app/containers/message/Message.stories.tsx b/app/containers/message/Message.stories.tsx index 9b262b9481e..b3786b887df 100644 --- a/app/containers/message/Message.stories.tsx +++ b/app/containers/message/Message.stories.tsx @@ -114,16 +114,7 @@ export default { export const Message = (props: any) => ( <MessageContext.Provider value={storyContextValue}> <ResponsiveLayoutContext.Provider value={responsiveLayoutProviderLargeFontValue(1)}> - <MessageComponent - baseUrl={baseUrl} - user={user} - author={author} - ts={date} - timeFormat='LT' - isHeader - theme={_theme} - {...props} - /> + <MessageComponent baseUrl={baseUrl} user={user} author={author} ts={date} timeFormat='LT' isHeader {...props} /> </ResponsiveLayoutContext.Provider> </MessageContext.Provider> ); @@ -132,16 +123,7 @@ export const Message = (props: any) => ( const MessageLargeFont = (props: any) => ( <MessageContext.Provider value={storyContextValue}> <ResponsiveLayoutContext.Provider value={responsiveLayoutProviderLargeFontValue(FONT_SCALE_LIMIT)}> - <MessageComponent - baseUrl={baseUrl} - user={user} - author={author} - ts={date} - timeFormat='LT' - isHeader - theme={_theme} - {...props} - /> + <MessageComponent baseUrl={baseUrl} user={user} author={author} ts={date} timeFormat='LT' isHeader {...props} /> </ResponsiveLayoutContext.Provider> </MessageContext.Provider> ); @@ -310,19 +292,12 @@ export const Encrypted = () => ( usernames: [user.username] } ]} - onReactionPress={() => {}} type='e2e' /> <Message msg='Thread reply encrypted' tmid='1' tmsg='Thread with emoji :) :joy:' isThreadReply type='e2e' /> <Message msg='Temp message encrypted' status={messagesStatus.TEMP} isTemp type='e2e' /> <Message msg='Message Edited encrypted' edited type='e2e' /> - <Message - hasError - msg='This message has error and is encrypted' - status={messagesStatus.ERROR} - onErrorPress={() => alert('Error pressed')} - type='e2e' - /> + <Message hasError msg='This message has error and is encrypted' status={messagesStatus.ERROR} type='e2e' /> <Message msg='Read Receipt encrypted with Header' isReadReceiptEnabled read type='e2e' /> <Message msg='Read Receipt encrypted without Header' isReadReceiptEnabled read isHeader={false} type='e2e' /> </> @@ -348,19 +323,12 @@ export const EncryptedLargeFont = () => ( usernames: [user.username] } ]} - onReactionPress={() => {}} type='e2e' /> <MessageLargeFont msg='Thread reply encrypted' tmid='1' tmsg='Thread with emoji :) :joy:' isThreadReply type='e2e' /> <MessageLargeFont msg='Temp message encrypted' status={messagesStatus.TEMP} isTemp type='e2e' /> <MessageLargeFont msg='Message Edited encrypted' edited type='e2e' /> - <MessageLargeFont - hasError - msg='This message has error and is encrypted' - status={messagesStatus.ERROR} - onErrorPress={() => alert('Error pressed')} - type='e2e' - /> + <MessageLargeFont hasError msg='This message has error and is encrypted' status={messagesStatus.ERROR} type='e2e' /> <MessageLargeFont msg='Read Receipt encrypted with Header' isReadReceiptEnabled read type='e2e' /> <MessageLargeFont msg='Read Receipt encrypted without Header' isReadReceiptEnabled read isHeader={false} type='e2e' /> </> @@ -618,7 +586,6 @@ export const Reactions = () => ( usernames: new Array(9999) } ]} - onReactionPress={() => {}} /> <Message msg='Multiple Reactions' @@ -656,7 +623,6 @@ export const Reactions = () => ( usernames: [user.username] } ]} - onReactionPress={() => {}} /> </> ); @@ -683,7 +649,6 @@ export const ReactionsLargeFont = () => ( usernames: new Array(9999) } ]} - onReactionPress={() => {}} /> <MessageLargeFont msg='Multiple Reactions' @@ -721,7 +686,6 @@ export const ReactionsLargeFont = () => ( usernames: [user.username] } ]} - onReactionPress={() => {}} /> </> ); @@ -2280,11 +2244,9 @@ export const ColoredAttachmentsLargeFont = () => ( /> ); -export const Broadcast = () => <Message msg='Broadcasted message' broadcast replyBroadcast={() => alert('broadcast!')} />; +export const Broadcast = () => <Message msg='Broadcasted message' broadcast />; -export const BroadcastLargeFont = () => ( - <MessageLargeFont msg='Broadcasted message' broadcast replyBroadcast={() => alert('broadcast!')} /> -); +export const BroadcastLargeFont = () => <MessageLargeFont msg='Broadcasted message' broadcast />; export const Archived = () => <Message msg='This message is inside an archived room' archived />; @@ -2292,32 +2254,15 @@ export const ArchivedLargeFont = () => <MessageLargeFont msg='This message is in export const Error = () => ( <> - <Message hasError msg='This message has error' status={messagesStatus.ERROR} onErrorPress={() => alert('Error pressed')} /> - <Message - hasError - msg='This message has error too' - status={messagesStatus.ERROR} - onErrorPress={() => alert('Error pressed')} - isHeader={false} - /> + <Message hasError msg='This message has error' status={messagesStatus.ERROR} /> + <Message hasError msg='This message has error too' status={messagesStatus.ERROR} isHeader={false} /> </> ); export const ErrorLargeFont = () => ( <> - <MessageLargeFont - hasError - msg='This message has error' - status={messagesStatus.ERROR} - onErrorPress={() => alert('Error pressed')} - /> - <MessageLargeFont - hasError - msg='This message has error too' - status={messagesStatus.ERROR} - onErrorPress={() => alert('Error pressed')} - isHeader={false} - /> + <MessageLargeFont hasError msg='This message has error' status={messagesStatus.ERROR} /> + <MessageLargeFont hasError msg='This message has error too' status={messagesStatus.ERROR} isHeader={false} /> </> ); diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index 62b9b9849b4..6bc51a15963 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -61,6 +61,8 @@ interface IMessageA11y { } const MessageInner = memo((props: TMessageProps) => { + 'use memo'; + const { isLargeFontScale } = useResponsiveLayout(); const showTimeLarge = isLargeFontScale && props.isHeader; @@ -278,6 +280,8 @@ const MessageInner = memo((props: TMessageProps) => { MessageInner.displayName = 'MessageInner'; const Message = memo((props: TMessageProps & IMessageA11y) => { + 'use memo'; + if (props.isThreadReply || props.isThreadSequential || props.isInfo || props.isIgnored) { const thread = props.isThreadReply ? ( <RepliedThread @@ -419,6 +423,8 @@ const Message = memo((props: TMessageProps & IMessageA11y) => { Message.displayName = 'Message'; const MessageTouchable = memo((props: TMessageProps) => { + 'use memo'; + const { onPress, onLongPress } = useContext(MessageContext); const { colors } = useTheme(); const { ref: touchRef, markAsLastFocused } = useLastFocusedMessageRef(); From 7738cc379d16eea89e31b492a7f7eb6f50cf4e0a Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 29 Jun 2026 18:22:37 -0300 Subject: [PATCH 008/144] refactor: remove dead RoomView message state (NATIVE-1352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop three confirmed-dead members from the RoomView container state: the unused reaction-modal visibility flag (reactionsModalVisible), the unused reply-with-mention flag (replyWithMention), and the redundant on-hold flag (isOnHold). reactionsModalVisible and replyWithMention were only ever initialized and type-declared, never read or re-set. isOnHold was set alongside roomUpdate and only compared in shouldComponentUpdate; that comparison is redundant because roomUpdate already carries onHold (it is part of roomAttrsUpdate), so on-hold transitions still trigger a re-render through the existing roomUpdate diff. No behavior change — the full test suite passes without snapshot updates. --- app/views/RoomView/constants.ts | 1 - app/views/RoomView/definitions.ts | 2 -- app/views/RoomView/index.tsx | 10 ++-------- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/app/views/RoomView/constants.ts b/app/views/RoomView/constants.ts index a94401bbc80..4f790e05832 100644 --- a/app/views/RoomView/constants.ts +++ b/app/views/RoomView/constants.ts @@ -3,7 +3,6 @@ import { type TRoomUpdate, type TStateAttrsUpdate } from './definitions'; export const stateAttrsUpdate = [ 'joined', 'lastOpen', - 'reactionsModalVisible', 'canAutoTranslate', 'loading', 'editing', diff --git a/app/views/RoomView/definitions.ts b/app/views/RoomView/definitions.ts index e38ccfef85c..cd71822b594 100644 --- a/app/views/RoomView/definitions.ts +++ b/app/views/RoomView/definitions.ts @@ -60,10 +60,8 @@ export interface IRoomViewState { }; member: any; lastOpen: Date | null; - reactionsModalVisible: boolean; canAutoTranslate: boolean; loading: boolean; - replyWithMention: boolean; readOnly: boolean; unreadsCount: number | null; roomUserId?: string | null; diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 06075e345ed..ba865ab7eca 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -179,10 +179,8 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { roomUpdate: {}, member: {}, lastOpen: null, - reactionsModalVisible: false, canAutoTranslate: false, loading: true, - replyWithMention: false, readOnly: false, unreadsCount: null, roomUserId, @@ -190,7 +188,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { canForwardGuest: false, canReturnQueue: false, canPlaceLivechatOnHold: false, - isOnHold: false, isAutocompleteVisible: false, showMissingE2EEKey: false, showE2EEDisabledRoom: false @@ -268,7 +265,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { shouldComponentUpdate(nextProps: IRoomViewProps, nextState: IRoomViewState) { const { state } = this; - const { roomUpdate, member, isOnHold, isAutocompleteVisible, showMissingE2EEKey, showE2EEDisabledRoom } = state; + const { roomUpdate, member, isAutocompleteVisible, showMissingE2EEKey, showE2EEDisabledRoom } = state; const { theme, insets, route, encryptionEnabled, airGappedRestrictionRemainingDays } = this.props; if (theme !== nextProps.theme) { return true; @@ -282,9 +279,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { if (member.statusText !== nextState.member.statusText) { return true; } - if (isOnHold !== nextState.isOnHold) { - return true; - } if (isAutocompleteVisible !== nextState.isAutocompleteVisible) { return true; } @@ -763,7 +757,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { return ret; }, {}); if (this.mounted) { - this.internalSetState({ room: changes, roomUpdate, isOnHold: !!changes?.onHold }); + this.internalSetState({ room: changes, roomUpdate }); } else { // @ts-ignore this.state.room = changes; From d8c1881f1e0844a8e69fee425c4e72f9d6643773 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 30 Jun 2026 14:14:14 -0300 Subject: [PATCH 009/144] fix: prevent quote attachment loss under concurrent decryption Collect quote attachments from Promise.all and assign message.attachments once after it resolves, instead of racing read-modify-write across the concurrent decrypt branches (which dropped quotes on multi-quote messages). --- app/lib/encryption/room.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/lib/encryption/room.ts b/app/lib/encryption/room.ts index ce68f11074c..a837847923b 100644 --- a/app/lib/encryption/room.ts +++ b/app/lib/encryption/room.ts @@ -715,33 +715,34 @@ export default class EncryptionRoom { async decryptQuoteAttachment(message: IMessage) { const urls = message?.msg?.match(getMessageUrlRegex()) || []; - await Promise.all( - urls.map(async (url: string) => { + const quoteAttachments = await Promise.all( + urls.map(async (url: string): Promise<IAttachment | null> => { const parsedUrl = parse(url, true); const messageId = parsedUrl.query?.msg; if (!messageId) { - return; + return null; } // From local db const messageFromDB = await getMessageById(messageId); if (messageFromDB && messageFromDB.e2e === 'done') { const decryptedQuoteMessage = mapMessageFromDB(messageFromDB); - const quoteAttachment = createQuoteAttachment(decryptedQuoteMessage, url); - message.attachments = [...(message.attachments || []), quoteAttachment]; - return; + return createQuoteAttachment(decryptedQuoteMessage, url); } // From API const quotedMessageObject = await getSingleMessage(messageId); if (!quotedMessageObject) { - return; + return null; } const decryptedQuoteMessage = await this.decrypt(mapMessageFromAPI(quotedMessageObject)); - const quoteAttachment = createQuoteAttachment(decryptedQuoteMessage as IMessage, url); - message.attachments = [...(message.attachments || []), quoteAttachment]; + return createQuoteAttachment(decryptedQuoteMessage as IMessage, url); }) ); + message.attachments = [ + ...(message.attachments || []), + ...quoteAttachments.filter((attachment): attachment is IAttachment => attachment !== null) + ]; return message; } From 9387cbc415b8e6480456791156718ceb32acd935 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 30 Jun 2026 14:14:18 -0300 Subject: [PATCH 010/144] refactor: remove unused useInteractionApi export Dead export with zero callers; consumers use the narrower hooks or read getState().actions directly. --- app/views/RoomView/InteractionStore.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/views/RoomView/InteractionStore.tsx b/app/views/RoomView/InteractionStore.tsx index 1af2f0c0589..1680f7470da 100644 --- a/app/views/RoomView/InteractionStore.tsx +++ b/app/views/RoomView/InteractionStore.tsx @@ -63,8 +63,6 @@ export const useIsBeingEdited = (messageId: string): boolean => { return useStore(store, s => s.action === 'edit' && s.selectedMessages[0] === messageId); }; -export const useInteractionApi = (): TInteractionActions => useInteractionStore(s => s.actions); - export const InteractionProvider = ({ initialState, children From 50e3a4405b8724cfaf64e7772ab043943b3e237a Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 30 Jun 2026 14:14:25 -0300 Subject: [PATCH 011/144] fix: harden message row reactivity and correct memo/link/translation reads - Guard the model subscription so plain REST objects (search, starred, pinned, mentions, files) no longer throw 'experimentalSubscribe is not a function' on mount; serve a static snapshot when the model is not observable - Remove render-body ref writes from MessageContainer and useMessage so the React Compiler memoizes both (stable subscribe/getSnapshot, snapshot cached by item identity; debounced press handler created once, latest closure via effect) - areEqual compares previousItem.id (was ._id, always undefined on WatermelonDB models) and documents why item is intentionally omitted - isMessageLink no longer treats plain links as message links when attachments is undefined - read the translation from the useMessage snapshot instead of the live model - drop redundant/historical comments Adds a useMessage regression test for the non-observable plain-object path. --- .../message/hooks/useMessage.test.ts | 52 ++++++++ app/containers/message/hooks/useMessage.ts | 54 ++++---- app/containers/message/index.tsx | 122 ++++++++++-------- app/containers/message/utils.ts | 5 +- 4 files changed, 158 insertions(+), 75 deletions(-) diff --git a/app/containers/message/hooks/useMessage.test.ts b/app/containers/message/hooks/useMessage.test.ts index 8c71787a40b..79de40fbf77 100644 --- a/app/containers/message/hooks/useMessage.test.ts +++ b/app/containers/message/hooks/useMessage.test.ts @@ -124,4 +124,56 @@ describe('useMessage', () => { expect(result.current.id).toBe('msg-b'); expect(result.current.msg).toBe('Model B'); }); + + it('does not throw and returns a correct snapshot for a plain REST object without experimentalSubscribe', () => { + const plainItem = { + id: 'rest-msg-1', + msg: 'Plain REST message', + t: undefined, + ts: new Date('2024-06-01T00:00:00Z'), + u: { _id: 'u2', username: 'bob' }, + alias: undefined, + groupable: true, + avatar: undefined, + emoji: undefined, + attachments: undefined, + urls: [], + status: 0, + pinned: false, + editedBy: undefined, + reactions: undefined, + role: undefined, + drid: undefined, + dcount: undefined, + dlm: undefined, + tmid: undefined, + tcount: undefined, + tlm: undefined, + replies: undefined, + mentions: [], + channels: [], + unread: false, + autoTranslate: false, + translations: undefined, + tmsg: undefined, + blocks: undefined, + e2e: undefined, + md: undefined, + comment: undefined + } as unknown as TAnyMessageModel; + + let caughtError: unknown; + let hookResult: ReturnType<typeof useMessage> | undefined; + + try { + const { result } = renderHook(() => useMessage(plainItem)); + hookResult = result.current; + } catch (e) { + caughtError = e; + } + + expect(caughtError).toBeUndefined(); + expect(hookResult?.id).toBe('rest-msg-1'); + expect(hookResult?.msg).toBe('Plain REST message'); + }); }); diff --git a/app/containers/message/hooks/useMessage.ts b/app/containers/message/hooks/useMessage.ts index e14e111b03f..6dca3dc9183 100644 --- a/app/containers/message/hooks/useMessage.ts +++ b/app/containers/message/hooks/useMessage.ts @@ -1,4 +1,4 @@ -import { useRef, useSyncExternalStore } from 'react'; +import { useCallback, useRef, useSyncExternalStore } from 'react'; import { type Root } from '@rocket.chat/message-parser'; import { @@ -88,34 +88,42 @@ const readSnapshot = (item: TAnyMessageModel): IMessageSnapshot => ({ comment: item.comment }); +type SnapshotCache = { item: TAnyMessageModel; snapshot: IMessageSnapshot }; + export const useMessage = (item: TAnyMessageModel): IMessageSnapshot => { 'use memo'; - // prevItemRef lets us detect item identity changes so getSnapshot always - // reflects the CURRENT item synchronously — even before the new subscription - // fires its first callback. - const prevItemRef = useRef<TAnyMessageModel | null>(null); - - // Cache the latest snapshot so getSnapshot is referentially stable between - // emissions; useSyncExternalStore requires it to return the same object when - // nothing has changed, otherwise React will infinite-loop. - const snapshotRef = useRef<IMessageSnapshot | null>(null); + // Cache the latest snapshot so getSnapshot returns the same object reference + // when nothing has changed — useSyncExternalStore requires this to avoid loops. + const cacheRef = useRef<SnapshotCache | null>(null); - if (prevItemRef.current !== item) { - prevItemRef.current = item; - snapshotRef.current = readSnapshot(item); - } + // getSnapshot and subscribe both close over item directly and are recreated + // when item identity changes (useCallback with [item] dep). This lets + // useSyncExternalStore resubscribe on item change without any render-body + // ref writes. - const subscribe = (onStoreChange: () => void): (() => void) => { - // @ts-ignore: experimentalSubscribe is not yet in WatermelonDB's TS types - const unsubscribe = item.experimentalSubscribe(() => { - snapshotRef.current = readSnapshot(item); - onStoreChange(); - }); - return unsubscribe; - }; + const getSnapshot = useCallback((): IMessageSnapshot => { + if (!cacheRef.current || cacheRef.current.item !== item) { + cacheRef.current = { item, snapshot: readSnapshot(item) }; + } + return cacheRef.current.snapshot; + }, [item]); - const getSnapshot = (): IMessageSnapshot => snapshotRef.current as IMessageSnapshot; + const subscribe = useCallback( + (onStoreChange: () => void): (() => void) => { + // @ts-ignore: experimentalSubscribe is not yet in WatermelonDB's TS types + if (typeof item.experimentalSubscribe !== 'function') { + return () => {}; + } + // @ts-ignore + const unsubscribe = item.experimentalSubscribe(() => { + cacheRef.current = { item, snapshot: readSnapshot(item) }; + onStoreChange(); + }); + return unsubscribe; + }, + [item] + ); return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); }; diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 2d9bcab4354..1f4c668ebfc 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -1,4 +1,4 @@ -import { memo, useRef, useState } from 'react'; +import { memo, useEffect, useRef, useState } from 'react'; import { Keyboard } from 'react-native'; import Message from './Message'; @@ -12,7 +12,7 @@ import { type IRoomInfoParam } from '../../views/SearchMessagesView'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; import { messagesStatus } from '../../lib/constants/messagesStatus'; import MessageSeparator from '../MessageSeparator'; -import { useMessage } from './hooks/useMessage'; +import { type IMessageSnapshot, useMessage } from './hooks/useMessage'; import { useIsBeingEdited } from '../../views/RoomView/InteractionStore'; interface IMessageContainerProps { @@ -66,6 +66,42 @@ interface IMessageContainerProps { showUnreadSeparator?: boolean; } +// Extracted at module scope so the try/catch does not prevent React Compiler from +// memoising MessageContainer (the compiler cannot optimise try/catch in render). +function computeIsHeader( + previousItem: TAnyMessageModel | undefined, + message: IMessageSnapshot, + broadcast: boolean, + Message_GroupingPeriod: number | undefined, + hasError: boolean +): boolean { + if (hasError || (previousItem && previousItem.status === messagesStatus.ERROR)) { + return true; + } + try { + if ( + previousItem && + // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense + previousItem.ts.toDateString() === message.ts.toDateString() && + previousItem.u?.username === message.u?.username && + !(previousItem.groupable === false || message.groupable === false || broadcast === true) && + // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense + message.ts - previousItem.ts < Message_GroupingPeriod * 1000 && + previousItem.tmid === message.tmid && + message.t !== 'rm' && + previousItem.t !== 'rm' + ) { + return false; + } + return true; + } catch { + return true; + } +} + +// `item` is intentionally omitted: the FlatList keys each row by item.id, so a +// different message remounts rather than re-rendering with a new item prop, and +// useMessage recomputes its snapshot on item-identity change. function areEqual(prev: IMessageContainerProps, next: IMessageContainerProps): boolean { return ( prev.showUnreadSeparator === next.showUnreadSeparator && @@ -73,7 +109,7 @@ function areEqual(prev: IMessageContainerProps, next: IMessageContainerProps): b prev.highlighted === next.highlighted && prev.threadBadgeColor === next.threadBadgeColor && prev.isIgnored === next.isIgnored && - prev.previousItem?._id === next.previousItem?._id && + prev.previousItem?.id === next.previousItem?.id && prev.autoTranslateRoom === next.autoTranslateRoom && prev.autoTranslateLanguage === next.autoTranslateLanguage ); @@ -128,33 +164,10 @@ const MessageContainer = ({ const { theme } = useTheme(); const [isManualUnignored, setIsManualUnignored] = useState(false); - // Derived values (formerly getters) + // Derived values const hasError = message.status === messagesStatus.ERROR; - const isHeader = (() => { - if (hasError || (previousItem && previousItem.status === messagesStatus.ERROR)) { - return true; - } - try { - if ( - previousItem && - // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense - previousItem.ts.toDateString() === message.ts.toDateString() && - previousItem.u?.username === message.u?.username && - !(previousItem.groupable === false || message.groupable === false || broadcast === true) && - // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense - message.ts - previousItem.ts < Message_GroupingPeriod * 1000 && - previousItem.tmid === message.tmid && - message.t !== 'rm' && - previousItem.t !== 'rm' - ) { - return false; - } - return true; - } catch (error) { - return true; - } - })(); + const isHeader = computeIsHeader(previousItem, message, broadcast, Message_GroupingPeriod, hasError); const isThreadReply = (() => { if (isThreadRoom) { @@ -186,7 +199,6 @@ const MessageContainer = ({ const isIgnored = isManualUnignored ? false : isIgnoredProp ?? false; - // Event handlers const onIgnoredMessagePress = () => { setIsManualUnignored(true); }; @@ -246,7 +258,7 @@ const MessageContainer = ({ }; const onLinkPress = (link: string): void => { - const isMessageLink = message.attachments?.findIndex((att: IAttachment) => att?.message_link === link) !== -1; + const isMessageLink = !!message.attachments?.some((att: IAttachment) => att?.message_link === link); if (isMessageLink && jumpToMessage) { return jumpToMessage(link); } @@ -262,35 +274,43 @@ const MessageContainer = ({ } }; - // Stable debounced onPress — one instance for the component lifetime, - // always invoking the latest closure via the ref. + // pressHandlerRef holds the latest press logic; updated after every render via + // useEffect so the render body stays free of ref writes. const pressHandlerRef = useRef<() => void>(() => {}); - pressHandlerRef.current = () => { - if (isIgnored) { - return onIgnoredMessagePress(); - } + useEffect(() => { + pressHandlerRef.current = () => { + if (isIgnored) { + return onIgnoredMessagePress(); + } - if (onPressProp) { - return onPressProp(); - } + if (onPressProp) { + return onPressProp(); + } - Keyboard.dismiss(); + Keyboard.dismiss(); - if ((message.tlm || message.tmid) && !isThreadRoom) { - onThreadPress(); - } + if ((message.tlm || message.tmid) && !isThreadRoom) { + onThreadPress(); + } - if (message.dlm && onDiscussionPressProp) { - onDiscussionPressProp(item); - } - }; - const onPress = useRef(debounce(() => pressHandlerRef.current(), 300, true)).current; + if (message.dlm && onDiscussionPressProp) { + onDiscussionPressProp(item); + } + }; + }); + // onPressRef holds the single debounced instance for the component lifetime. + // Initialised to a noop; replaced with the real debounced fn in the first effect + // so that pressHandlerRef.current is never read during render. + const onPressRef = useRef(debounce(() => {}, 300, true)); + useEffect(() => { + onPressRef.current = debounce(() => pressHandlerRef.current?.(), 300, true); + }, []); const onPressAction = () => { if (closeEmojiAndAction) { - return closeEmojiAndAction(onPress); + return closeEmojiAndAction(onPressRef.current); } - return onPress(); + return onPressRef.current(); }; const { @@ -329,7 +349,7 @@ const MessageContainer = ({ let isTranslated = false; const otherUserMessage = u?.username !== user?.username; if (autoTranslateRoom && autoTranslateMessage && autoTranslateLanguage && otherUserMessage) { - const messageTranslated = getMessageTranslation(item, autoTranslateLanguage); + const messageTranslated = getMessageTranslation(message, autoTranslateLanguage); isTranslated = !!messageTranslated; messageText = messageTranslated || messageText; } diff --git a/app/containers/message/utils.ts b/app/containers/message/utils.ts index ae5e778311a..08dceb44947 100644 --- a/app/containers/message/utils.ts +++ b/app/containers/message/utils.ts @@ -189,7 +189,10 @@ export const getInfoMessage = ({ type, role, msg, author, comment }: TInfoMessag } }; -export const getMessageTranslation = (message: TMessageModel, autoTranslateLanguage: string): string | null => { +export const getMessageTranslation = ( + message: TMessageModel | { translations?: TMessageModel['translations'] }, + autoTranslateLanguage: string +): string | null => { if (!autoTranslateLanguage) { return null; } From ad71bdd498a99ddea8c22dc6949a1fccc70e0e5a Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 30 Jun 2026 15:57:12 -0300 Subject: [PATCH 012/144] refactor: drop redundant React.memo from compiled message subtree The message render tree is annotated for the React Compiler ('use memo'). A compiled parent emits referentially-stable child elements, so React.memo on a child whose parent is also compiled is redundant. Remove the memo wrappers and their hand-written comparators (both dequal deep-compares and scalar shouldComponentUpdate-era allowlists) across the presentational message components, leaving compilation as the single change-detection mechanism. The deep-compare removals are safe because the @json model fields (attachments, urls, md, mentions, channels, reactions, replies) resolve to stable references, making reference comparison equivalent to dequal. MessageContainer (index.tsx) keeps its memo: its parent is RoomView's renderItem class method, which the compiler never compiles, so that wrapper is load-bearing. Removing Thread's tcount-only comparator also fixes a latent bug: a tlm change with unchanged tcount no longer suppresses the re-render that toggles the thread button's visibility. Add Thread.test.tsx as a regression guard. Also delete dead StyleSheet keys the memo wrappers had hidden from react-native/no-unused-styles. --- app/containers/message/Broadcast.tsx | 6 +- app/containers/message/CallButton.tsx | 6 +- .../Components/Attachments/Attachments.tsx | 150 +++++---- .../Attachments/CollapsibleQuote/index.tsx | 188 +++++------ .../Components/Attachments/Image/Image.tsx | 6 +- .../message/Components/Attachments/Quote.tsx | 32 +- .../message/Components/Attachments/Reply.tsx | 301 ++++++++---------- .../message/Components/RightIcons/Edited.tsx | 5 +- .../Components/RightIcons/Encrypted.tsx | 6 +- .../Components/RightIcons/MessageError.tsx | 31 +- .../Components/RightIcons/ReadReceipt.tsx | 6 +- .../Components/RightIcons/Translated.tsx | 5 +- app/containers/message/Content.tsx | 196 +++++------- app/containers/message/Discussion.tsx | 70 ++-- app/containers/message/Emoji.tsx | 24 +- app/containers/message/Message.tsx | 14 +- app/containers/message/MessageAvatar.tsx | 6 +- app/containers/message/Reactions.tsx | 14 +- app/containers/message/RepliedThread.tsx | 6 +- app/containers/message/Thread.test.tsx | 41 +++ app/containers/message/Thread.tsx | 82 +++-- app/containers/message/Touchable.tsx | 6 +- app/containers/message/Urls.tsx | 20 +- app/containers/message/User.tsx | 132 ++++---- 24 files changed, 623 insertions(+), 730 deletions(-) create mode 100644 app/containers/message/Thread.test.tsx diff --git a/app/containers/message/Broadcast.tsx b/app/containers/message/Broadcast.tsx index b494a5cee3a..a58467c9d50 100644 --- a/app/containers/message/Broadcast.tsx +++ b/app/containers/message/Broadcast.tsx @@ -1,4 +1,4 @@ -import { useContext, memo } from 'react'; +import { useContext } from 'react'; import { Text, View } from 'react-native'; import Touchable from './Touchable'; @@ -12,7 +12,7 @@ import { type IMessageBroadcast } from './interfaces'; import { useTheme } from '../../theme'; // TODO: Create a reusable button component for message -const Broadcast = memo(({ author, broadcast }: IMessageBroadcast) => { +const Broadcast = ({ author, broadcast }: IMessageBroadcast) => { 'use memo'; const { user, replyBroadcast } = useContext(MessageContext); @@ -36,7 +36,7 @@ const Broadcast = memo(({ author, broadcast }: IMessageBroadcast) => { ); } return null; -}); +}; Broadcast.displayName = 'MessageBroadcast'; diff --git a/app/containers/message/CallButton.tsx b/app/containers/message/CallButton.tsx index 66545459d04..4677d39da16 100644 --- a/app/containers/message/CallButton.tsx +++ b/app/containers/message/CallButton.tsx @@ -1,4 +1,4 @@ -import { memo, useContext } from 'react'; +import { useContext } from 'react'; import { Text, View } from 'react-native'; import Touchable from './Touchable'; @@ -10,7 +10,7 @@ import { themes } from '../../lib/constants/colors'; import { useTheme } from '../../theme'; import MessageContext from './Context'; -const CallButton = memo(() => { +const CallButton = () => { 'use memo'; const { handleEnterCall } = useContext(MessageContext); @@ -28,7 +28,7 @@ const CallButton = memo(() => { </Touchable> </View> ); -}); +}; CallButton.displayName = 'CallButton'; diff --git a/app/containers/message/Components/Attachments/Attachments.tsx b/app/containers/message/Components/Attachments/Attachments.tsx index 4b3f2bd4680..4f51b81024b 100644 --- a/app/containers/message/Components/Attachments/Attachments.tsx +++ b/app/containers/message/Components/Attachments/Attachments.tsx @@ -1,5 +1,4 @@ -import { useContext, memo, type FC } from 'react'; -import { dequal } from 'dequal'; +import { useContext, type FC } from 'react'; import { View } from 'react-native'; import Image from './Image'; @@ -13,92 +12,89 @@ import { type IMessageAttachments } from '../../interfaces'; import { getMessageFromAttachment } from '../../utils'; import { isContentAttachment } from './utils'; -const Attachments: FC<IMessageAttachments> = memo( - ({ attachments, timeFormat, author }: IMessageAttachments) => { - 'use memo'; +const Attachments: FC<IMessageAttachments> = ({ attachments, timeFormat, author }: IMessageAttachments) => { + 'use memo'; - const { translateLanguage, showAttachment, getCustomEmoji = () => null } = useContext(MessageContext); + const { translateLanguage, showAttachment, getCustomEmoji = () => null } = useContext(MessageContext); - const nonQuoteAttachments = attachments?.filter(isContentAttachment); + const nonQuoteAttachments = attachments?.filter(isContentAttachment); - if (!nonQuoteAttachments || nonQuoteAttachments.length === 0) { - return null; - } + if (!nonQuoteAttachments || nonQuoteAttachments.length === 0) { + return null; + } - const attachmentsElements = nonQuoteAttachments.map((file, index) => { - const msg = getMessageFromAttachment(file, translateLanguage); + const attachmentsElements = nonQuoteAttachments.map((file, index) => { + const msg = getMessageFromAttachment(file, translateLanguage); - if (file.image_url) { - return ( - <Image - key={file.image_url} - file={file} - showAttachment={showAttachment} - getCustomEmoji={getCustomEmoji} - author={author} - msg={msg} - imagePreview={file.image_preview} - imageType={file.image_type} - /> - ); - } + if (file.image_url) { + return ( + <Image + key={file.image_url} + file={file} + showAttachment={showAttachment} + getCustomEmoji={getCustomEmoji} + author={author} + msg={msg} + imagePreview={file.image_preview} + imageType={file.image_type} + /> + ); + } - if (file.audio_url) { - return <Audio key={file.audio_url} file={file} getCustomEmoji={getCustomEmoji} author={author} msg={msg} />; - } + if (file.audio_url) { + return <Audio key={file.audio_url} file={file} getCustomEmoji={getCustomEmoji} author={author} msg={msg} />; + } - if (file.video_url) { - return ( - <Video - key={file.video_url} - file={file} - showAttachment={showAttachment} - getCustomEmoji={getCustomEmoji} - author={author} - msg={msg} - /> - ); - } + if (file.video_url) { + return ( + <Video + key={file.video_url} + file={file} + showAttachment={showAttachment} + getCustomEmoji={getCustomEmoji} + author={author} + msg={msg} + /> + ); + } - if (file.actions && file.actions.length > 0) { - return ( - <AttachedActions - key={file.title_link || file.message_link || `actions-${index}`} - attachment={file} - getCustomEmoji={getCustomEmoji} - /> - ); - } - if (typeof file.collapsed === 'boolean') { - return ( - <CollapsibleQuote - key={file.title_link || file.message_link || `collapsible-${index}`} - attachment={file} - timeFormat={timeFormat} - getCustomEmoji={getCustomEmoji} - /> - ); - } + if (file.actions && file.actions.length > 0) { + return ( + <AttachedActions + key={file.title_link || file.message_link || `actions-${index}`} + attachment={file} + getCustomEmoji={getCustomEmoji} + /> + ); + } + if (typeof file.collapsed === 'boolean') { + return ( + <CollapsibleQuote + key={file.title_link || file.message_link || `collapsible-${index}`} + attachment={file} + timeFormat={timeFormat} + getCustomEmoji={getCustomEmoji} + /> + ); + } - if (file.attachments?.length) { - return ( - <Reply - key={file.title_link || file.message_link || `reply-${index}`} - attachment={file} - timeFormat={timeFormat} - getCustomEmoji={getCustomEmoji} - msg={msg} - /> - ); - } + if (file.attachments?.length) { + return ( + <Reply + key={file.title_link || file.message_link || `reply-${index}`} + attachment={file} + timeFormat={timeFormat} + getCustomEmoji={getCustomEmoji} + msg={msg} + /> + ); + } - return null; - }); + return null; + }); - return <View style={{ gap: 4 }}>{attachmentsElements}</View>; - }, - (prevProps, nextProps) => dequal(prevProps.attachments, nextProps.attachments) -); + return <View style={{ gap: 4 }}>{attachmentsElements}</View>; +}; Attachments.displayName = 'MessageAttachments'; diff --git a/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx b/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx index 844a4d0bd47..4e7e1d99060 100644 --- a/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx +++ b/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx @@ -1,6 +1,5 @@ import { transparentize } from 'color2k'; -import { dequal } from 'dequal'; -import { useContext, useState, memo } from 'react'; +import { useContext, useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { themes } from '../../../../../lib/constants/colors'; @@ -35,18 +34,10 @@ const styles = StyleSheet.create({ paddingLeft: 10, paddingVertical: 10 }, - fieldText: { - fontSize: 15, - padding: 10, - ...sharedStyles.textRegular - }, fieldTitle: { fontSize: 15, ...sharedStyles.textBold }, - marginTop: { - marginTop: 4 - }, marginBottom: { marginBottom: 4 }, @@ -83,113 +74,104 @@ interface IMessageReply { getCustomEmoji: TGetCustomEmoji; } -const AttText = memo( - ({ text, getCustomEmoji }: IMessageAttText) => { - 'use memo'; +const AttText = ({ text, getCustomEmoji }: IMessageAttText) => { + 'use memo'; - const { user } = useContext(MessageContext); - - if (!text) { - return null; - } + const { user } = useContext(MessageContext); - return <Markdown msg={text} username={user?.username} getCustomEmoji={getCustomEmoji} />; - }, - (prevProps, nextProps) => prevProps.text === nextProps.text -); + if (!text) { + return null; + } -const Fields = memo( - ({ attachment, getCustomEmoji }: IMessageFields) => { - 'use memo'; + return <Markdown msg={text} username={user?.username} getCustomEmoji={getCustomEmoji} />; +}; - const { theme } = useTheme(); - const { user } = useContext(MessageContext); +const Fields = ({ attachment, getCustomEmoji }: IMessageFields) => { + 'use memo'; - if (!attachment.fields) { - return null; - } + const { theme } = useTheme(); + const { user } = useContext(MessageContext); - return ( - <> - {attachment.fields.map(field => ( - <View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}> - <Text testID='collapsibleQuoteTouchableFieldTitle' style={[styles.fieldTitle, { color: themes[theme].fontDefault }]}> - {field.title} - </Text> - <Markdown msg={field?.value || ''} username={user?.username} getCustomEmoji={getCustomEmoji} /> - </View> - ))} - </> - ); - }, - (prevProps, nextProps) => dequal(prevProps.attachment.fields, nextProps.attachment.fields) -); + if (!attachment.fields) { + return null; + } -const CollapsibleQuote = memo( - ({ attachment, getCustomEmoji }: IMessageReply) => { - 'use memo'; + return ( + <> + {attachment.fields.map(field => ( + <View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}> + <Text testID='collapsibleQuoteTouchableFieldTitle' style={[styles.fieldTitle, { color: themes[theme].fontDefault }]}> + {field.title} + </Text> + <Markdown msg={field?.value || ''} username={user?.username} getCustomEmoji={getCustomEmoji} /> + </View> + ))} + </> + ); +}; + +const CollapsibleQuote = ({ attachment, getCustomEmoji }: IMessageReply) => { + 'use memo'; + + const { theme } = useTheme(); + const [collapsed, setCollapsed] = useState(attachment?.collapsed); + + if (!attachment) { + return null; + } - const { theme } = useTheme(); - const [collapsed, setCollapsed] = useState(attachment?.collapsed); + const onPress = () => { + setCollapsed(!collapsed); + }; - if (!attachment) { - return null; - } + let { strokeExtraLight, surfaceTint: backgroundColor, strokeLight, strokeMedium, fontSecondaryInfo } = themes[theme]; - const onPress = () => { - setCollapsed(!collapsed); - }; - - let { strokeExtraLight, surfaceTint: backgroundColor, strokeLight, strokeMedium, fontSecondaryInfo } = themes[theme]; - - try { - if (attachment.color) { - backgroundColor = transparentize(attachment.color, 0.8); - strokeExtraLight = attachment.color; - strokeLight = attachment.color; - strokeMedium = attachment.color; - fontSecondaryInfo = fontSecondaryInfo; - } - } catch (e) { - // fallback to default + try { + if (attachment.color) { + backgroundColor = transparentize(attachment.color, 0.8); + strokeExtraLight = attachment.color; + strokeLight = attachment.color; + strokeMedium = attachment.color; + fontSecondaryInfo = fontSecondaryInfo; } + } catch (e) { + // fallback to default + } - return ( - <> - <Touchable - testID={`collapsibleQuoteTouchable-${attachment.title}`} - onPress={onPress} - style={[ - styles.button, - attachment.description && styles.marginBottom, - { - backgroundColor, - borderLeftColor: strokeLight, - borderTopColor: strokeExtraLight, - borderRightColor: strokeExtraLight, - borderBottomColor: strokeExtraLight, - borderLeftWidth: 2 - } - ]} - hitSlop={BUTTON_HIT_SLOP}> - <View style={styles.touchableContainer}> - <View style={styles.attachmentContainer}> - <View style={styles.authorContainer}> - <Text style={[styles.title, { color: fontSecondaryInfo }]}>{attachment.title}</Text> - </View> - {!collapsed && <AttText text={attachment.text} getCustomEmoji={getCustomEmoji} />} - {!collapsed && <Fields attachment={attachment} getCustomEmoji={getCustomEmoji} />} - </View> - <View style={styles.iconContainer}> - <CustomIcon name={!collapsed ? 'chevron-up' : 'chevron-down'} size={22} color={strokeMedium} /> + return ( + <> + <Touchable + testID={`collapsibleQuoteTouchable-${attachment.title}`} + onPress={onPress} + style={[ + styles.button, + attachment.description && styles.marginBottom, + { + backgroundColor, + borderLeftColor: strokeLight, + borderTopColor: strokeExtraLight, + borderRightColor: strokeExtraLight, + borderBottomColor: strokeExtraLight, + borderLeftWidth: 2 + } + ]} + hitSlop={BUTTON_HIT_SLOP}> + <View style={styles.touchableContainer}> + <View style={styles.attachmentContainer}> + <View style={styles.authorContainer}> + <Text style={[styles.title, { color: fontSecondaryInfo }]}>{attachment.title}</Text> </View> + {!collapsed && <AttText text={attachment.text} getCustomEmoji={getCustomEmoji} />} + {!collapsed && <Fields attachment={attachment} getCustomEmoji={getCustomEmoji} />} </View> - </Touchable> - </> - ); - }, - (prevProps, nextProps) => dequal(prevProps.attachment, nextProps.attachment) -); + <View style={styles.iconContainer}> + <CustomIcon name={!collapsed ? 'chevron-up' : 'chevron-down'} size={22} color={strokeMedium} /> + </View> + </View> + </Touchable> + </> + ); +}; CollapsibleQuote.displayName = 'CollapsibleQuote'; Fields.displayName = 'CollapsibleQuoteFields'; diff --git a/app/containers/message/Components/Attachments/Image/Image.tsx b/app/containers/message/Components/Attachments/Image/Image.tsx index 2a76e527f1c..0156276037f 100644 --- a/app/containers/message/Components/Attachments/Image/Image.tsx +++ b/app/containers/message/Components/Attachments/Image/Image.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState, memo } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { View, type ViewStyle } from 'react-native'; import { Image } from 'expo-image'; @@ -13,7 +13,7 @@ import { AUTOPLAY_GIFS_PREFERENCES_KEY } from '../../../../../lib/constants/keys import ImageBadge from './ImageBadge'; import log from '../../../../../lib/methods/helpers/log'; -export const MessageImage = memo(({ uri, status, encrypted = false, imagePreview, imageType }: IMessageImage) => { +export const MessageImage = ({ uri, status, encrypted = false, imagePreview, imageType }: IMessageImage) => { 'use memo'; const { colors } = useTheme(); @@ -97,6 +97,6 @@ export const MessageImage = memo(({ uri, status, encrypted = false, imagePreview <View style={styles.badgeContainer}>{isGif ? <ImageBadge title='GIF' /> : null}</View> </> ); -}); +}; MessageImage.displayName = 'MessageImage'; diff --git a/app/containers/message/Components/Attachments/Quote.tsx b/app/containers/message/Components/Attachments/Quote.tsx index 8fa3bf52c49..a3723108067 100644 --- a/app/containers/message/Components/Attachments/Quote.tsx +++ b/app/containers/message/Components/Attachments/Quote.tsx @@ -1,5 +1,4 @@ -import { useContext, memo, type FC } from 'react'; -import { dequal } from 'dequal'; +import { useContext, type FC } from 'react'; import { View } from 'react-native'; import { Reply } from './components'; @@ -9,28 +8,25 @@ import { type IAttachment } from '../../../../definitions'; import { getMessageFromAttachment } from '../../utils'; import { isQuoteAttachment } from './utils'; -const Quote: FC<IMessageAttachments> = memo( - ({ attachments, timeFormat }: IMessageAttachments) => { - 'use memo'; +const Quote: FC<IMessageAttachments> = ({ attachments, timeFormat }: IMessageAttachments) => { + 'use memo'; - const { translateLanguage, getCustomEmoji = () => null } = useContext(MessageContext); + const { translateLanguage, getCustomEmoji = () => null } = useContext(MessageContext); - const quotes = attachments?.filter(isQuoteAttachment); + const quotes = attachments?.filter(isQuoteAttachment); - if (!quotes || !quotes?.length) { - return null; - } + if (!quotes || !quotes?.length) { + return null; + } - const quotesElements = quotes.map((file: IAttachment, index: number) => { - const msg = getMessageFromAttachment(file, translateLanguage); + const quotesElements = quotes.map((file: IAttachment, index: number) => { + const msg = getMessageFromAttachment(file, translateLanguage); - return <Reply key={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} msg={msg} />; - }); + return <Reply key={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} msg={msg} />; + }); - return <View style={{ gap: 4 }}>{quotesElements}</View>; - }, - (prevProps, nextProps) => dequal(prevProps.attachments, nextProps.attachments) -); + return <View style={{ gap: 4 }}>{quotesElements}</View>; +}; Quote.displayName = 'MessageQuote'; diff --git a/app/containers/message/Components/Attachments/Reply.tsx b/app/containers/message/Components/Attachments/Reply.tsx index 9e8631406b2..aa210329c75 100644 --- a/app/containers/message/Components/Attachments/Reply.tsx +++ b/app/containers/message/Components/Attachments/Reply.tsx @@ -1,5 +1,4 @@ -import { dequal } from 'dequal'; -import { useContext, useState, memo } from 'react'; +import { useContext, useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { Image } from 'expo-image'; @@ -67,16 +66,6 @@ const styles = StyleSheet.create({ fontSize: 14, ...sharedStyles.textSemibold }, - fieldValue: { - fontSize: 14, - ...sharedStyles.textRegular - }, - marginTop: { - marginTop: 4 - }, - marginBottom: { - marginBottom: 4 - }, image: { height: 80, width: 80, @@ -99,182 +88,156 @@ interface IMessageReply { msg?: string; } -const Title = memo( - ({ attachment, timeFormat, theme }: { attachment: IAttachment; timeFormat?: string; theme: TSupportedThemes }) => { - 'use memo'; - - const time = attachment.message_link && attachment.ts ? dayjs(attachment.ts).format(timeFormat) : null; - return ( - <View style={styles.authorContainer}> - {attachment.author_name ? ( - <Text numberOfLines={1} style={[styles.author, { color: themes[theme].fontHint }]}> - {attachment.author_name} - </Text> - ) : null} - {time ? <Text style={[messageStyles.time, { color: themes[theme].fontSecondaryInfo }]}>{time}</Text> : null} - {attachment.title ? <Text style={[styles.title, { color: themes[theme].fontDefault }]}>{attachment.title}</Text> : null} - </View> - ); +const Title = ({ attachment, timeFormat, theme }: { attachment: IAttachment; timeFormat?: string; theme: TSupportedThemes }) => { + 'use memo'; + + const time = attachment.message_link && attachment.ts ? dayjs(attachment.ts).format(timeFormat) : null; + return ( + <View style={styles.authorContainer}> + {attachment.author_name ? ( + <Text numberOfLines={1} style={[styles.author, { color: themes[theme].fontHint }]}> + {attachment.author_name} + </Text> + ) : null} + {time ? <Text style={[messageStyles.time, { color: themes[theme].fontSecondaryInfo }]}>{time}</Text> : null} + {attachment.title ? <Text style={[styles.title, { color: themes[theme].fontDefault }]}>{attachment.title}</Text> : null} + </View> + ); +}; + +const Description = ({ attachment, getCustomEmoji }: { attachment: IAttachment; getCustomEmoji: TGetCustomEmoji }) => { + 'use memo'; + + const { user } = useContext(MessageContext); + const text = attachment.text || attachment.title; + + if (!text) { + return null; } -); -const Description = memo( - ({ attachment, getCustomEmoji }: { attachment: IAttachment; getCustomEmoji: TGetCustomEmoji }) => { - 'use memo'; + // For file attachments without explicit text, the title is just a filename (e.g., "test.py"). + // We use MarkdownPreview to avoid markdown parsing treating filenames as URLs or markdown syntax. + // For other attachments (message quotes, embeds), the text may contain actual markdown formatting, + // so we use the full Markdown component to preserve styling. + const isFileName = attachment.type === 'file' && !attachment.text; - const { user } = useContext(MessageContext); - const text = attachment.text || attachment.title; + if (isFileName) { + return <MarkdownPreview msg={text} numberOfLines={0} />; + } - if (!text) { - return null; - } + return <Markdown msg={text} username={user?.username} getCustomEmoji={getCustomEmoji} />; +}; - // For file attachments without explicit text, the title is just a filename (e.g., "test.py"). - // We use MarkdownPreview to avoid markdown parsing treating filenames as URLs or markdown syntax. - // For other attachments (message quotes, embeds), the text may contain actual markdown formatting, - // so we use the full Markdown component to preserve styling. - const isFileName = attachment.type === 'file' && !attachment.text; +const UrlImage = ({ image }: { image?: string }) => { + 'use memo'; - if (isFileName) { - return <MarkdownPreview msg={text} numberOfLines={0} />; - } + const { baseUrl, user } = useContext(MessageContext); - return <Markdown msg={text} username={user?.username} getCustomEmoji={getCustomEmoji} />; - }, - (prevProps, nextProps) => { - if (prevProps.attachment.text !== nextProps.attachment.text) { - return false; - } - if (prevProps.attachment.title !== nextProps.attachment.title) { - return false; - } - if (prevProps.attachment.type !== nextProps.attachment.type) { - return false; - } - return true; + if (!image) { + return null; } -); -const UrlImage = memo( - ({ image }: { image?: string }) => { - 'use memo'; + image = image.includes('http') ? image : `${baseUrl}/${image}?rc_uid=${user?.id ?? ''}&rc_token=${user?.token ?? ''}`; + return <Image source={{ uri: image }} style={styles.image} contentFit='cover' />; +}; - const { baseUrl, user } = useContext(MessageContext); - - if (!image) { - return null; - } +const Fields = ({ + attachment, + theme, + getCustomEmoji +}: { + attachment: IAttachment; + theme: TSupportedThemes; + getCustomEmoji: TGetCustomEmoji; +}) => { + 'use memo'; - image = image.includes('http') ? image : `${baseUrl}/${image}?rc_uid=${user?.id ?? ''}&rc_token=${user?.token ?? ''}`; - return <Image source={{ uri: image }} style={styles.image} contentFit='cover' />; - }, - (prevProps, nextProps) => prevProps.image === nextProps.image -); + const { user } = useContext(MessageContext); -const Fields = memo( - ({ - attachment, - theme, - getCustomEmoji - }: { - attachment: IAttachment; - theme: TSupportedThemes; - getCustomEmoji: TGetCustomEmoji; - }) => { - 'use memo'; + if (!attachment.fields) { + return null; + } - const { user } = useContext(MessageContext); + return ( + <View style={styles.fieldsContainer}> + {attachment.fields.map(field => ( + <View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}> + <Text style={[styles.fieldTitle, { color: themes[theme].fontDefault }]}>{field.title}</Text> + <Markdown msg={field?.value || ''} username={user?.username} getCustomEmoji={getCustomEmoji} /> + </View> + ))} + </View> + ); +}; + +const Reply = ({ attachment, timeFormat, getCustomEmoji, msg }: IMessageReply) => { + 'use memo'; + + const [loading, setLoading] = useState(false); + const { theme } = useTheme(); + const { baseUrl, user, id, e2e, isEncrypted } = useContext(MessageContext); + + if (!attachment || (isEncrypted && !e2e)) { + return null; + } - if (!attachment.fields) { - return null; + const onPress = async () => { + let url = attachment.title_link || attachment.author_link; + if (!url) { + return; } - - return ( - <View style={styles.fieldsContainer}> - {attachment.fields.map(field => ( - <View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}> - <Text style={[styles.fieldTitle, { color: themes[theme].fontDefault }]}>{field.title}</Text> - <Markdown msg={field?.value || ''} username={user?.username} getCustomEmoji={getCustomEmoji} /> - </View> - ))} - </View> - ); - }, - (prevProps, nextProps) => - dequal(prevProps.attachment.fields, nextProps.attachment.fields) && prevProps.theme === nextProps.theme -); - -const Reply = memo( - ({ attachment, timeFormat, getCustomEmoji, msg }: IMessageReply) => { - 'use memo'; - - const [loading, setLoading] = useState(false); - const { theme } = useTheme(); - const { baseUrl, user, id, e2e, isEncrypted } = useContext(MessageContext); - - if (!attachment || (isEncrypted && !e2e)) { - return null; + if (attachment.type === 'file' && attachment.title_link) { + setLoading(true); + url = formatAttachmentUrl(attachment.title_link, user?.id ?? '', user?.token ?? '', baseUrl ?? ''); + await fileDownloadAndPreview(url, attachment, id ?? ''); + setLoading(false); + return; } + openLink(url, theme); + }; - const onPress = async () => { - let url = attachment.title_link || attachment.author_link; - if (!url) { - return; - } - if (attachment.type === 'file' && attachment.title_link) { - setLoading(true); - url = formatAttachmentUrl(attachment.title_link, user?.id ?? '', user?.token ?? '', baseUrl ?? ''); - await fileDownloadAndPreview(url, attachment, id ?? ''); - setLoading(false); - return; - } - openLink(url, theme); - }; - - let { strokeLight } = themes[theme]; - if (attachment.color) { - strokeLight = attachment.color; - } + let { strokeLight } = themes[theme]; + if (attachment.color) { + strokeLight = attachment.color; + } - return ( - <View style={{ gap: 4 }}> - <Touchable - testID={`reply-${attachment?.author_name}-${attachment?.text}`} - onPress={onPress} - style={[ - styles.button, - { - borderColor: strokeLight - } - ]} - disabled={!!(loading || attachment.message_link)}> - <View style={styles.attachmentContainer}> - <View style={styles.titleAndDescriptionContainer}> - <Title attachment={attachment} timeFormat={timeFormat} theme={theme} /> - <Description attachment={attachment} getCustomEmoji={getCustomEmoji} /> - <Quote attachments={attachment.attachments} timeFormat={timeFormat} /> - <Attachments attachments={attachment.attachments} timeFormat={timeFormat} /> - <Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} /> - {loading ? ( - <View style={styles.backdrop}> - <View - style={[ - styles.backdrop, - { backgroundColor: themes[theme].surfaceNeutral, opacity: themes[theme].attachmentLoadingOpacity } - ]}></View> - <RCActivityIndicator /> - </View> - ) : null} - </View> - <UrlImage image={attachment.thumb_url} /> + return ( + <View style={{ gap: 4 }}> + <Touchable + testID={`reply-${attachment?.author_name}-${attachment?.text}`} + onPress={onPress} + style={[ + styles.button, + { + borderColor: strokeLight + } + ]} + disabled={!!(loading || attachment.message_link)}> + <View style={styles.attachmentContainer}> + <View style={styles.titleAndDescriptionContainer}> + <Title attachment={attachment} timeFormat={timeFormat} theme={theme} /> + <Description attachment={attachment} getCustomEmoji={getCustomEmoji} /> + <Quote attachments={attachment.attachments} timeFormat={timeFormat} /> + <Attachments attachments={attachment.attachments} timeFormat={timeFormat} /> + <Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} /> + {loading ? ( + <View style={styles.backdrop}> + <View + style={[ + styles.backdrop, + { backgroundColor: themes[theme].surfaceNeutral, opacity: themes[theme].attachmentLoadingOpacity } + ]}></View> + <RCActivityIndicator /> + </View> + ) : null} </View> - </Touchable> - {msg ? <Markdown msg={msg} username={user?.username} getCustomEmoji={getCustomEmoji} /> : null} - </View> - ); - }, - (prevProps, nextProps) => dequal(prevProps.attachment, nextProps.attachment) -); + <UrlImage image={attachment.thumb_url} /> + </View> + </Touchable> + {msg ? <Markdown msg={msg} username={user?.username} getCustomEmoji={getCustomEmoji} /> : null} + </View> + ); +}; Reply.displayName = 'MessageReply'; Title.displayName = 'MessageReplyTitle'; diff --git a/app/containers/message/Components/RightIcons/Edited.tsx b/app/containers/message/Components/RightIcons/Edited.tsx index d75d78ed3fe..f6aed2828d9 100644 --- a/app/containers/message/Components/RightIcons/Edited.tsx +++ b/app/containers/message/Components/RightIcons/Edited.tsx @@ -1,10 +1,9 @@ -import { memo } from 'react'; import { View } from 'react-native'; import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; -const Edited = memo(({ isEdited, testID }: { isEdited: boolean; testID?: string }) => { +const Edited = ({ isEdited, testID }: { isEdited: boolean; testID?: string }) => { 'use memo'; if (!isEdited) { @@ -16,6 +15,6 @@ const Edited = memo(({ isEdited, testID }: { isEdited: boolean; testID?: string <CustomIcon name='edit' size={16} /> </View> ); -}); +}; export default Edited; diff --git a/app/containers/message/Components/RightIcons/Encrypted.tsx b/app/containers/message/Components/RightIcons/Encrypted.tsx index 449451b5dc7..2c1fb26d55f 100644 --- a/app/containers/message/Components/RightIcons/Encrypted.tsx +++ b/app/containers/message/Components/RightIcons/Encrypted.tsx @@ -1,4 +1,4 @@ -import { useContext, memo } from 'react'; +import { useContext } from 'react'; import Touchable from '../../Touchable'; import { CustomIcon } from '../../../CustomIcon'; @@ -7,7 +7,7 @@ import MessageContext from '../../Context'; import styles from '../../styles'; import { E2E_MESSAGE_TYPE } from '../../../../lib/constants/keys'; -const Encrypted = memo(({ type }: { type: string }) => { +const Encrypted = ({ type }: { type: string }) => { 'use memo'; const { onEncryptedPress } = useContext(MessageContext); @@ -21,6 +21,6 @@ const Encrypted = memo(({ type }: { type: string }) => { <CustomIcon name='encrypted' size={16} /> </Touchable> ); -}); +}; export default Encrypted; diff --git a/app/containers/message/Components/RightIcons/MessageError.tsx b/app/containers/message/Components/RightIcons/MessageError.tsx index 8c5fbd5a629..53cadbfc323 100644 --- a/app/containers/message/Components/RightIcons/MessageError.tsx +++ b/app/containers/message/Components/RightIcons/MessageError.tsx @@ -1,4 +1,4 @@ -import { useContext, memo } from 'react'; +import { useContext } from 'react'; import Touchable from '../../Touchable'; import { CustomIcon } from '../../../CustomIcon'; @@ -8,25 +8,22 @@ import { themes } from '../../../../lib/constants/colors'; import MessageContext from '../../Context'; import { useTheme } from '../../../../theme'; -const MessageError = memo( - ({ hasError }: { hasError: boolean }) => { - 'use memo'; +const MessageError = ({ hasError }: { hasError: boolean }) => { + 'use memo'; - const { theme } = useTheme(); - const { onErrorPress } = useContext(MessageContext); + const { theme } = useTheme(); + const { onErrorPress } = useContext(MessageContext); - if (!hasError) { - return null; - } + if (!hasError) { + return null; + } - return ( - <Touchable onPress={onErrorPress} style={styles.rightIcons} hitSlop={BUTTON_HIT_SLOP}> - <CustomIcon name='warning' color={themes[theme].buttonBackgroundDangerDefault} size={16} /> - </Touchable> - ); - }, - (prevProps, nextProps) => prevProps.hasError === nextProps.hasError -); + return ( + <Touchable onPress={onErrorPress} style={styles.rightIcons} hitSlop={BUTTON_HIT_SLOP}> + <CustomIcon name='warning' color={themes[theme].buttonBackgroundDangerDefault} size={16} /> + </Touchable> + ); +}; MessageError.displayName = 'MessageError'; diff --git a/app/containers/message/Components/RightIcons/ReadReceipt.tsx b/app/containers/message/Components/RightIcons/ReadReceipt.tsx index ce1d0e4fa49..215249c4316 100644 --- a/app/containers/message/Components/RightIcons/ReadReceipt.tsx +++ b/app/containers/message/Components/RightIcons/ReadReceipt.tsx @@ -1,10 +1,8 @@ -import { memo } from 'react'; - import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; import { useTheme } from '../../../../theme'; -const ReadReceipt = memo(({ isReadReceiptEnabled, unread }: { isReadReceiptEnabled?: boolean; unread?: boolean }) => { +const ReadReceipt = ({ isReadReceiptEnabled, unread }: { isReadReceiptEnabled?: boolean; unread?: boolean }) => { 'use memo'; const { colors } = useTheme(); @@ -25,7 +23,7 @@ const ReadReceipt = memo(({ isReadReceiptEnabled, unread }: { isReadReceiptEnabl ); } return null; -}); +}; ReadReceipt.displayName = 'MessageReadReceipt'; export default ReadReceipt; diff --git a/app/containers/message/Components/RightIcons/Translated.tsx b/app/containers/message/Components/RightIcons/Translated.tsx index fa1393b68fe..6e35131bc2a 100644 --- a/app/containers/message/Components/RightIcons/Translated.tsx +++ b/app/containers/message/Components/RightIcons/Translated.tsx @@ -1,10 +1,9 @@ -import { memo } from 'react'; import { View } from 'react-native'; import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; -const Translated = memo(({ isTranslated }: { isTranslated: boolean }) => { +const Translated = ({ isTranslated }: { isTranslated: boolean }) => { 'use memo'; if (!isTranslated) { @@ -16,6 +15,6 @@ const Translated = memo(({ isTranslated }: { isTranslated: boolean }) => { <CustomIcon name='language' size={16} /> </View> ); -}); +}; export default Translated; diff --git a/app/containers/message/Content.tsx b/app/containers/message/Content.tsx index 7f87ac15a06..f1b047d5638 100644 --- a/app/containers/message/Content.tsx +++ b/app/containers/message/Content.tsx @@ -1,6 +1,5 @@ -import { useContext, memo } from 'react'; +import { useContext } from 'react'; import { Text, View } from 'react-native'; -import { dequal } from 'dequal'; import I18n from '../../i18n'; import styles from './styles'; @@ -13,134 +12,93 @@ import { useTheme } from '../../theme'; import { themes } from '../../lib/constants/colors'; import { type MessageTypesValues } from '../../definitions'; -const Content = memo( - (props: IMessageContent) => { - 'use memo'; +const Content = (props: IMessageContent) => { + 'use memo'; - const { theme } = useTheme(); - const { user, onLinkPress, getCustomEmoji = () => null, navToRoomInfo } = useContext(MessageContext); + const { theme } = useTheme(); + const { user, onLinkPress, getCustomEmoji = () => null, navToRoomInfo } = useContext(MessageContext); - if (props.isInfo) { - // @ts-ignore - const infoMessage = getInfoMessage({ ...props }); + if (props.isInfo) { + // @ts-ignore + const infoMessage = getInfoMessage({ ...props }); - const renderMessageContent = ( - <Text style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} accessibilityLabel={infoMessage}> - {infoMessage} + const renderMessageContent = ( + <Text style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} accessibilityLabel={infoMessage}> + {infoMessage} + </Text> + ); + if (messageHaveAuthorName(props.type as MessageTypesValues)) { + return ( + <Text> + <User + hasError={props.hasError} + type={props.type} + isEdited={props.isEdited} + isTranslated={props.isTranslated} + isHeader={props.isHeader} + useRealName={props.useRealName} + author={props.author} + alias={props.alias} + pinned={props.pinned} + />{' '} + {renderMessageContent} </Text> ); - if (messageHaveAuthorName(props.type as MessageTypesValues)) { - return ( - <Text> - <User - hasError={props.hasError} - type={props.type} - isEdited={props.isEdited} - isTranslated={props.isTranslated} - isHeader={props.isHeader} - useRealName={props.useRealName} - author={props.author} - alias={props.alias} - pinned={props.pinned} - />{' '} - {renderMessageContent} - </Text> - ); - } - - return renderMessageContent; } - const isPreview = props.tmid && !props.isThreadRoom; - let content = null; + return renderMessageContent; + } - if (props.isEncrypted) { - content = ( - <Text - style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} - accessibilityLabel={I18n.t('Encrypted_message')} - testID='message-encrypted'> - {I18n.t('Encrypted_message')} - </Text> - ); - } else if (isPreview) { - const previewMsg = - props.msg || - (props.attachments?.length - ? getPreviewMessageFromAttachment(props.attachments[0], props.autoTranslateLanguage) - : undefined); - content = previewMsg ? <MarkdownPreview testID={`message-preview-${previewMsg}`} msg={previewMsg} /> : null; - } else if (props.msg) { - content = ( - <Markdown - msg={props.msg} - md={props.type !== 'e2e' ? props.md : undefined} - getCustomEmoji={getCustomEmoji} - username={user?.username ?? ''} - channels={props.channels} - mentions={props.mentions} - navToRoomInfo={navToRoomInfo} - useRealName={props.useRealName} - onLinkPress={onLinkPress} - isTranslated={props.isTranslated} - /> - ); - } + const isPreview = props.tmid && !props.isThreadRoom; + let content = null; - if (props.isIgnored) { - content = ( - <Text style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} testID={`message-ignored-${props.msg}`}> - {I18n.t('Message_Ignored')} - </Text> - ); - } + if (props.isEncrypted) { + content = ( + <Text + style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} + accessibilityLabel={I18n.t('Encrypted_message')} + testID='message-encrypted'> + {I18n.t('Encrypted_message')} + </Text> + ); + } else if (isPreview) { + const previewMsg = + props.msg || + (props.attachments?.length + ? getPreviewMessageFromAttachment(props.attachments[0], props.autoTranslateLanguage) + : undefined); + content = previewMsg ? <MarkdownPreview testID={`message-preview-${previewMsg}`} msg={previewMsg} /> : null; + } else if (props.msg) { + content = ( + <Markdown + msg={props.msg} + md={props.type !== 'e2e' ? props.md : undefined} + getCustomEmoji={getCustomEmoji} + username={user?.username ?? ''} + channels={props.channels} + mentions={props.mentions} + navToRoomInfo={navToRoomInfo} + useRealName={props.useRealName} + onLinkPress={onLinkPress} + isTranslated={props.isTranslated} + /> + ); + } - return content ? ( - <View style={props.isTemp && styles.temp} testID={`message-content-${props.msg || ''}`}> - {content} - </View> - ) : null; - }, - (prevProps, nextProps) => { - if (prevProps.isTemp !== nextProps.isTemp) { - return false; - } - if (prevProps.msg !== nextProps.msg) { - return false; - } - if (prevProps.type !== nextProps.type) { - return false; - } - if (prevProps.isEncrypted !== nextProps.isEncrypted) { - return false; - } - if (prevProps.isIgnored !== nextProps.isIgnored) { - return false; - } - if (prevProps.tmid !== nextProps.tmid) { - return false; - } - if (prevProps.isThreadRoom !== nextProps.isThreadRoom) { - return false; - } - if (prevProps.autoTranslateLanguage !== nextProps.autoTranslateLanguage) { - return false; - } - if (!dequal(prevProps.md, nextProps.md)) { - return false; - } - if (!dequal(prevProps.mentions, nextProps.mentions)) { - return false; - } - if (!dequal(prevProps.channels, nextProps.channels)) { - return false; - } - if (!dequal(prevProps.attachments, nextProps.attachments)) { - return false; - } - return true; + if (props.isIgnored) { + content = ( + <Text style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} testID={`message-ignored-${props.msg}`}> + {I18n.t('Message_Ignored')} + </Text> + ); } -); + + return content ? ( + <View style={props.isTemp && styles.temp} testID={`message-content-${props.msg || ''}`}> + {content} + </View> + ) : null; +}; Content.displayName = 'MessageContent'; diff --git a/app/containers/message/Discussion.tsx b/app/containers/message/Discussion.tsx index 15ecc2830e5..cb58ebde82f 100644 --- a/app/containers/message/Discussion.tsx +++ b/app/containers/message/Discussion.tsx @@ -1,4 +1,4 @@ -import { useContext, memo } from 'react'; +import { useContext } from 'react'; import { Text, View } from 'react-native'; import Touchable from './Touchable'; @@ -13,49 +13,35 @@ import { type IMessage } from '../../definitions'; import { useTheme } from '../../theme'; // TODO: Create a reusable button component for message -const Discussion = memo( - ({ msg, dcount, dlm }: Pick<IMessage, 'msg' | 'dcount' | 'dlm'>) => { - 'use memo'; +const Discussion = ({ msg, dcount, dlm }: Pick<IMessage, 'msg' | 'dcount' | 'dlm'>) => { + 'use memo'; - const { colors } = useTheme(); - let time; - if (dlm) { - time = formatDateThreads(dlm); - } - const buttonText = formatMessageCount(dcount, DISCUSSION); - const { onDiscussionPress } = useContext(MessageContext); - return ( - <View style={{ gap: 4 }}> - <Text style={[styles.startedDiscussion, { color: colors.fontSecondaryInfo }]}>{I18n.t('Started_discussion')}</Text> - <Text style={[styles.discussionText, { color: colors.fontDefault }]}>{msg}</Text> - <View style={[styles.buttonContainer, { gap: 8 }]}> - <Touchable - onPress={onDiscussionPress} - style={[styles.button, { backgroundColor: colors.badgeBackgroundLevel2 }]} - hitSlop={BUTTON_HIT_SLOP}> - <View style={styles.buttonInnerContainer}> - <CustomIcon name='discussions' size={16} color={colors.fontWhite} /> - <Text style={[styles.buttonText, { color: colors.fontWhite }]}>{buttonText}</Text> - </View> - </Touchable> - <Text style={[styles.time, { color: colors.fontSecondaryInfo }]}>{time}</Text> - </View> - </View> - ); - }, - (prevProps, nextProps) => { - if (prevProps.msg !== nextProps.msg) { - return false; - } - if (prevProps.dcount !== nextProps.dcount) { - return false; - } - if (prevProps.dlm !== nextProps.dlm) { - return false; - } - return true; + const { colors } = useTheme(); + let time; + if (dlm) { + time = formatDateThreads(dlm); } -); + const buttonText = formatMessageCount(dcount, DISCUSSION); + const { onDiscussionPress } = useContext(MessageContext); + return ( + <View style={{ gap: 4 }}> + <Text style={[styles.startedDiscussion, { color: colors.fontSecondaryInfo }]}>{I18n.t('Started_discussion')}</Text> + <Text style={[styles.discussionText, { color: colors.fontDefault }]}>{msg}</Text> + <View style={[styles.buttonContainer, { gap: 8 }]}> + <Touchable + onPress={onDiscussionPress} + style={[styles.button, { backgroundColor: colors.badgeBackgroundLevel2 }]} + hitSlop={BUTTON_HIT_SLOP}> + <View style={styles.buttonInnerContainer}> + <CustomIcon name='discussions' size={16} color={colors.fontWhite} /> + <Text style={[styles.buttonText, { color: colors.fontWhite }]}>{buttonText}</Text> + </View> + </Touchable> + <Text style={[styles.time, { color: colors.fontSecondaryInfo }]}>{time}</Text> + </View> + </View> + ); +}; Discussion.displayName = 'MessageDiscussion'; diff --git a/app/containers/message/Emoji.tsx b/app/containers/message/Emoji.tsx index 0540a7c86ef..64ed5e46c6a 100644 --- a/app/containers/message/Emoji.tsx +++ b/app/containers/message/Emoji.tsx @@ -1,24 +1,20 @@ -import { memo } from 'react'; import { Text } from 'react-native'; import useShortnameToUnicode from '../../lib/hooks/useShortnameToUnicode'; import CustomEmoji from '../EmojiPicker/CustomEmoji'; import { type IMessageEmoji } from './interfaces'; -const Emoji = memo( - ({ content, standardEmojiStyle, customEmojiStyle, getCustomEmoji }: IMessageEmoji) => { - 'use memo'; +const Emoji = ({ content, standardEmojiStyle, customEmojiStyle, getCustomEmoji }: IMessageEmoji) => { + 'use memo'; - const parsedContent = content.replace(/^:|:$/g, ''); - const emoji = getCustomEmoji(parsedContent); - const { formatShortnameToUnicode } = useShortnameToUnicode(); - if (emoji) { - return <CustomEmoji key={content} style={customEmojiStyle} emoji={emoji} />; - } - return <Text style={standardEmojiStyle}>{formatShortnameToUnicode(content)}</Text>; - }, - () => true -); + const parsedContent = content.replace(/^:|:$/g, ''); + const emoji = getCustomEmoji(parsedContent); + const { formatShortnameToUnicode } = useShortnameToUnicode(); + if (emoji) { + return <CustomEmoji key={content} style={customEmojiStyle} emoji={emoji} />; + } + return <Text style={standardEmojiStyle}>{formatShortnameToUnicode(content)}</Text>; +}; Emoji.displayName = 'MessageEmoji'; diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index 6bc51a15963..181547d9495 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -1,4 +1,4 @@ -import { useContext, memo } from 'react'; +import { useContext } from 'react'; import { View, type ViewStyle, type AccessibilityActionEvent, type AccessibilityActionInfo } from 'react-native'; import { A11y } from 'react-native-a11y-order'; @@ -60,7 +60,7 @@ interface IMessageA11y { handleLongPress?: () => void; } -const MessageInner = memo((props: TMessageProps) => { +const MessageInner = (props: TMessageProps) => { 'use memo'; const { isLargeFontScale } = useResponsiveLayout(); @@ -276,10 +276,10 @@ const MessageInner = memo((props: TMessageProps) => { } return <WidthAwareView>{content}</WidthAwareView>; -}); +}; MessageInner.displayName = 'MessageInner'; -const Message = memo((props: TMessageProps & IMessageA11y) => { +const Message = (props: TMessageProps & IMessageA11y) => { 'use memo'; if (props.isThreadReply || props.isThreadSequential || props.isInfo || props.isIgnored) { @@ -419,10 +419,10 @@ const Message = memo((props: TMessageProps & IMessageA11y) => { </A11y.Index> </View> ); -}); +}; Message.displayName = 'Message'; -const MessageTouchable = memo((props: TMessageProps) => { +const MessageTouchable = (props: TMessageProps) => { 'use memo'; const { onPress, onLongPress } = useContext(MessageContext); @@ -577,7 +577,7 @@ const MessageTouchable = memo((props: TMessageProps) => { </A11y.Index> </A11y.Order> ); -}); +}; MessageTouchable.displayName = 'MessageTouchable'; diff --git a/app/containers/message/MessageAvatar.tsx b/app/containers/message/MessageAvatar.tsx index bfb43834599..6f51b12d421 100644 --- a/app/containers/message/MessageAvatar.tsx +++ b/app/containers/message/MessageAvatar.tsx @@ -1,4 +1,4 @@ -import { useContext, memo, type ReactElement } from 'react'; +import { useContext, type ReactElement } from 'react'; import { View } from 'react-native'; import Avatar from '../Avatar'; @@ -16,7 +16,7 @@ export const AvatarContainer = ({ children }: { children?: ReactElement | null } return <View style={{ width, alignItems: 'flex-end' }}>{children}</View>; }; -const MessageAvatar = memo(({ isHeader, avatar, author, small, emoji }: IMessageAvatar) => { +const MessageAvatar = ({ isHeader, avatar, author, small, emoji }: IMessageAvatar) => { 'use memo'; const { user, navToRoomInfo, getCustomEmoji } = useContext(MessageContext); @@ -49,7 +49,7 @@ const MessageAvatar = memo(({ isHeader, avatar, author, small, emoji }: IMessage ); } return <AvatarContainer />; -}); +}; MessageAvatar.displayName = 'MessageAvatar'; diff --git a/app/containers/message/Reactions.tsx b/app/containers/message/Reactions.tsx index e8f6d5f8853..36d691e540e 100644 --- a/app/containers/message/Reactions.tsx +++ b/app/containers/message/Reactions.tsx @@ -1,4 +1,4 @@ -import { useContext, memo } from 'react'; +import { useContext } from 'react'; import { Text, useWindowDimensions, View } from 'react-native'; import I18n from '../../i18n'; @@ -26,7 +26,7 @@ interface IMessageReactions { reactions?: IReaction[]; } -const AddReaction = memo(({ theme }: { theme: TSupportedThemes }) => { +const AddReaction = ({ theme }: { theme: TSupportedThemes }) => { 'use memo'; const { reactionInit } = useContext(MessageContext); @@ -47,9 +47,9 @@ const AddReaction = memo(({ theme }: { theme: TSupportedThemes }) => { </View> </Touchable> ); -}); +}; -const Reaction = memo(({ reaction, theme }: IMessageReaction) => { +const Reaction = ({ reaction, theme }: IMessageReaction) => { 'use memo'; const { onReactionPress, onReactionLongPress, user, getCustomEmoji } = useContext(MessageContext); @@ -83,9 +83,9 @@ const Reaction = memo(({ reaction, theme }: IMessageReaction) => { </View> </Touchable> ); -}); +}; -const Reactions = memo(({ reactions }: IMessageReactions) => { +const Reactions = ({ reactions }: IMessageReactions) => { 'use memo'; const { theme } = useTheme(); @@ -101,7 +101,7 @@ const Reactions = memo(({ reactions }: IMessageReactions) => { <AddReaction theme={theme} /> </View> ); -}); +}; Reaction.displayName = 'MessageReaction'; Reactions.displayName = 'MessageReactions'; diff --git a/app/containers/message/RepliedThread.tsx b/app/containers/message/RepliedThread.tsx index bc4da08edd8..8ab82a58f79 100644 --- a/app/containers/message/RepliedThread.tsx +++ b/app/containers/message/RepliedThread.tsx @@ -1,4 +1,4 @@ -import { memo, useContext, useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { View } from 'react-native'; import { CustomIcon } from '../CustomIcon'; @@ -10,7 +10,7 @@ import { useTheme } from '../../theme'; import { AvatarContainer } from './MessageAvatar'; import MessageContext from './Context'; -const RepliedThread = memo(({ tmid, tmsg, isHeader, id, isEncrypted }: IMessageRepliedThread) => { +const RepliedThread = ({ tmid, tmsg, isHeader, id, isEncrypted }: IMessageRepliedThread) => { 'use memo'; const { colors } = useTheme(); @@ -47,7 +47,7 @@ const RepliedThread = memo(({ tmid, tmsg, isHeader, id, isEncrypted }: IMessageR </View> </View> ); -}); +}; RepliedThread.displayName = 'MessageRepliedThread'; diff --git a/app/containers/message/Thread.test.tsx b/app/containers/message/Thread.test.tsx new file mode 100644 index 00000000000..c49160438e8 --- /dev/null +++ b/app/containers/message/Thread.test.tsx @@ -0,0 +1,41 @@ +import { render } from '@testing-library/react-native'; + +import Thread from './Thread'; +import MessageContext from './Context'; + +const baseContextValue = { + threadBadgeColor: undefined, + toggleFollowThread: jest.fn(), + user: { id: 'user1', username: 'user1' }, + replies: [], + onThreadPress: jest.fn() +}; + +const renderThread = (props: Parameters<typeof Thread>[0]) => + render( + <MessageContext.Provider value={baseContextValue}> + <Thread {...props} /> + </MessageContext.Provider> + ); + +describe('Thread — tlm-only update regression', () => { + test('renders null when tlm is undefined, then shows button after tlm arrives (same tcount)', () => { + const { queryByTestId, rerender, getByTestId } = renderThread({ + msg: 'hello', + tcount: 3, + tlm: undefined, + isThreadRoom: false, + id: 'msg1' + }); + + expect(queryByTestId('message-thread-button-hello')).toBeNull(); + + rerender( + <MessageContext.Provider value={baseContextValue}> + <Thread msg='hello' tcount={3} tlm={new Date('2024-01-01T00:00:00Z')} isThreadRoom={false} id='msg1' /> + </MessageContext.Provider> + ); + + expect(getByTestId('message-thread-button-hello')).toBeTruthy(); + }); +}); diff --git a/app/containers/message/Thread.tsx b/app/containers/message/Thread.tsx index e68744721db..6efd29b88d2 100644 --- a/app/containers/message/Thread.tsx +++ b/app/containers/message/Thread.tsx @@ -1,4 +1,4 @@ -import { useContext, memo } from 'react'; +import { useContext } from 'react'; import { Text, View } from 'react-native'; import styles from './styles'; @@ -9,51 +9,43 @@ import { type IMessageThread } from './interfaces'; import { useTheme } from '../../theme'; import Touchable from './Touchable'; -const Thread = memo( - ({ msg, tcount, tlm, isThreadRoom, id }: IMessageThread) => { - 'use memo'; - - const { theme, colors } = useTheme(); - const { threadBadgeColor, toggleFollowThread, user, replies, onThreadPress } = useContext(MessageContext); - - const backgroundColor = threadBadgeColor ? colors.badgeBackgroundLevel2 : colors.buttonBackgroundSecondaryDefault; - const textColor = threadBadgeColor || theme !== 'light' ? colors.fontWhite : colors.fontPureBlack; - - if (!tlm || isThreadRoom || tcount === null) { - return null; - } - - return ( - <View style={styles.buttonContainer}> - <Touchable - onPress={onThreadPress} - accessibilityRole='button' - accessibilityLabel={I18n.t('View_Thread')} - style={[styles.button, { backgroundColor }]} - testID={`message-thread-button-${msg}`}> - <Text style={[styles.buttonText, { color: textColor }]}>{I18n.t('View_Thread')}</Text> - </Touchable> - <ThreadDetails - item={{ - tcount, - replies, - id - }} - user={{ id: user?.id ?? '' }} - badgeColor={threadBadgeColor} - toggleFollowThread={toggleFollowThread as Function} - style={styles.threadDetails} - /> - </View> - ); - }, - (prevProps, nextProps) => { - if (prevProps.tcount !== nextProps.tcount) { - return false; - } - return true; +const Thread = ({ msg, tcount, tlm, isThreadRoom, id }: IMessageThread) => { + 'use memo'; + + const { theme, colors } = useTheme(); + const { threadBadgeColor, toggleFollowThread, user, replies, onThreadPress } = useContext(MessageContext); + + const backgroundColor = threadBadgeColor ? colors.badgeBackgroundLevel2 : colors.buttonBackgroundSecondaryDefault; + const textColor = threadBadgeColor || theme !== 'light' ? colors.fontWhite : colors.fontPureBlack; + + if (!tlm || isThreadRoom || tcount === null) { + return null; } -); + + return ( + <View style={styles.buttonContainer}> + <Touchable + onPress={onThreadPress} + accessibilityRole='button' + accessibilityLabel={I18n.t('View_Thread')} + style={[styles.button, { backgroundColor }]} + testID={`message-thread-button-${msg}`}> + <Text style={[styles.buttonText, { color: textColor }]}>{I18n.t('View_Thread')}</Text> + </Touchable> + <ThreadDetails + item={{ + tcount, + replies, + id + }} + user={{ id: user?.id ?? '' }} + badgeColor={threadBadgeColor} + toggleFollowThread={toggleFollowThread as Function} + style={styles.threadDetails} + /> + </View> + ); +}; Thread.displayName = 'MessageThread'; diff --git a/app/containers/message/Touchable.tsx b/app/containers/message/Touchable.tsx index ad53493b8be..18924ed6214 100644 --- a/app/containers/message/Touchable.tsx +++ b/app/containers/message/Touchable.tsx @@ -1,4 +1,4 @@ -import { useContext, memo, type ReactNode, type FC } from 'react'; +import { useContext, type ReactNode, type FC } from 'react'; import { Pressable, type PressableProps } from 'react-native'; import MessageContext from './Context'; @@ -8,7 +8,7 @@ interface IProps extends PressableProps { onLongPress?: () => void; } -const RCTouchable: FC<IProps> = memo(({ children, ...props }) => { +const RCTouchable: FC<IProps> = ({ children, ...props }) => { 'use memo'; const { onLongPress } = useContext(MessageContext); @@ -18,6 +18,6 @@ const RCTouchable: FC<IProps> = memo(({ children, ...props }) => { {children} </Pressable> ); -}); +}; export default RCTouchable; diff --git a/app/containers/message/Urls.tsx b/app/containers/message/Urls.tsx index c92365d49ec..4a9422eb2de 100644 --- a/app/containers/message/Urls.tsx +++ b/app/containers/message/Urls.tsx @@ -1,8 +1,7 @@ -import { useCallback, useContext, useEffect, useLayoutEffect, useState, memo, type ReactElement } from 'react'; +import { useCallback, useContext, useEffect, useLayoutEffect, useState, type ReactElement } from 'react'; import { StyleSheet, Text, View, type ViewStyle } from 'react-native'; import Clipboard from '@react-native-clipboard/clipboard'; import { Image } from 'expo-image'; -import { dequal } from 'dequal'; import axios from 'axios'; import { useAppSelector } from '../../lib/hooks/useAppSelector'; @@ -185,18 +184,15 @@ const Url = ({ url }: { url: IUrl }) => { </Touchable> ); }; -const Urls = memo( - ({ urls }: { urls?: IUrl[] }): ReactElement[] | null => { - 'use memo'; +const Urls = ({ urls }: { urls?: IUrl[] }): ReactElement[] | null => { + 'use memo'; - if (!urls || urls.length === 0) { - return null; - } + if (!urls || urls.length === 0) { + return null; + } - return urls.map((url: IUrl) => <Url url={url} key={url.url} />); - }, - (oldProps, newProps) => dequal(oldProps.urls, newProps.urls) -); + return urls.map((url: IUrl) => <Url url={url} key={url.url} />); +}; UrlContent.displayName = 'MessageUrlContent'; UrlImage.displayName = 'MessageUrlImage'; diff --git a/app/containers/message/User.tsx b/app/containers/message/User.tsx index 470afce1c4a..99de3cf6dd0 100644 --- a/app/containers/message/User.tsx +++ b/app/containers/message/User.tsx @@ -1,4 +1,4 @@ -import { useContext, memo } from 'react'; +import { useContext } from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import { type MessageType, type MessageTypesValues, SubscriptionType } from '../../definitions'; @@ -17,10 +17,6 @@ const styles = StyleSheet.create({ justifyContent: 'space-between', alignItems: 'center' }, - actionIcons: { - flexDirection: 'row', - alignItems: 'center' - }, username: { flexShrink: 1, fontSize: 16, @@ -63,79 +59,77 @@ interface IMessageUser { isTranslated: boolean; } -const User = memo( - ({ - isHeader, - useRealName, - author, - alias, - ts, - timeFormat, - hasError, - type, - isEdited, - isTranslated, - isReadReceiptEnabled, - unread, - pinned - }: IMessageUser) => { - 'use memo'; +const User = ({ + isHeader, + useRealName, + author, + alias, + ts, + timeFormat, + hasError, + type, + isEdited, + isTranslated, + isReadReceiptEnabled, + unread, + pinned +}: IMessageUser) => { + 'use memo'; - const { user, navToRoomInfo } = useContext(MessageContext); - const { colors } = useTheme(); - const { isLargeFontScale } = useResponsiveLayout(); + const { user, navToRoomInfo } = useContext(MessageContext); + const { colors } = useTheme(); + const { isLargeFontScale } = useResponsiveLayout(); - if (isHeader) { - const username = (useRealName && author?.name) || author?.username; - const aliasUsername = alias ? <Text style={[styles.alias, { color: colors.fontSecondaryInfo }]}> @{username}</Text> : null; - const itsMe = author?._id === user?.id; + if (isHeader) { + const username = (useRealName && author?.name) || author?.username; + const aliasUsername = alias ? <Text style={[styles.alias, { color: colors.fontSecondaryInfo }]}> @{username}</Text> : null; + const itsMe = author?._id === user?.id; - const onUserPress = () => { - navToRoomInfo?.({ - t: SubscriptionType.DIRECT, - rid: author?._id || '', - itsMe - }); - }; - - const textContent = ( - <> - {alias || username} - {aliasUsername} - </> - ); + const onUserPress = () => { + navToRoomInfo?.({ + t: SubscriptionType.DIRECT, + rid: author?._id || '', + itsMe + }); + }; - if (messageHaveAuthorName(type as MessageTypesValues)) { - return ( - <Text style={[styles.usernameInfoMessage, { color: colors.fontTitlesLabels }]} onPress={onUserPress}> - {textContent} - </Text> - ); - } + const textContent = ( + <> + {alias || username} + {aliasUsername} + </> + ); + if (messageHaveAuthorName(type as MessageTypesValues)) { return ( - <View style={styles.container}> - <Pressable testID={`username-header-${username}`} style={styles.titleContainer} onPress={onUserPress}> - <Text style={[styles.username, { color: colors.fontTitlesLabels }]} numberOfLines={1}> - {textContent} - </Text> - {isLargeFontScale ? null : <MessageTime timeFormat={timeFormat} ts={ts as Date | undefined} />} - </Pressable> - <RightIcons - type={type} - isEdited={isEdited} - hasError={hasError} - isReadReceiptEnabled={isReadReceiptEnabled} - unread={unread} - pinned={pinned} - isTranslated={isTranslated} - /> - </View> + <Text style={[styles.usernameInfoMessage, { color: colors.fontTitlesLabels }]} onPress={onUserPress}> + {textContent} + </Text> ); } - return null; + + return ( + <View style={styles.container}> + <Pressable testID={`username-header-${username}`} style={styles.titleContainer} onPress={onUserPress}> + <Text style={[styles.username, { color: colors.fontTitlesLabels }]} numberOfLines={1}> + {textContent} + </Text> + {isLargeFontScale ? null : <MessageTime timeFormat={timeFormat} ts={ts as Date | undefined} />} + </Pressable> + <RightIcons + type={type} + isEdited={isEdited} + hasError={hasError} + isReadReceiptEnabled={isReadReceiptEnabled} + unread={unread} + pinned={pinned} + isTranslated={isTranslated} + /> + </View> + ); } -); + return null; +}; User.displayName = 'MessageUser'; From 425a635a861aa4b1c952326910e3d9a31dbd833e Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 30 Jun 2026 22:12:47 -0300 Subject: [PATCH 013/144] refactor: replace useMessage snapshot hook with per-message store Message rendering funneled every field of a message through a single useMessage hook that snapshotted the whole record and prop-drilled it down the subtree, so a change to any one field re-rendered the entire message. Introduce a per-message store (MessageStore) created once per MessageProvider and ticked by the WatermelonDB record's experimentalSubscribe. Leaves read only the fields they need through granular selector hooks that bail via Object.is for a single field or useShallow for a domain group, so a field change re-renders just its consumers. Delete useMessage and drop the props that existed only to carry its snapshot. Attachments and Quote keep attachments/author as props: Reply renders them recursively with the nested attachment array, so a message-level hook there would recurse infinitely. Claude-Session: https://claude.ai/code/session_01Q27JMv1gF6V9urfyvGpWqn --- app/containers/message/Blocks.ts | 4 +- app/containers/message/Broadcast.tsx | 4 +- .../message/Components/RightIcons/Edited.tsx | 5 +- .../Components/RightIcons/Encrypted.tsx | 4 +- .../Components/RightIcons/MessageError.tsx | 4 +- .../message/Components/RightIcons/Pinned.tsx | 5 +- .../Components/RightIcons/ReadReceipt.tsx | 4 +- .../Components/RightIcons/Translated.tsx | 5 +- app/containers/message/Content.test.tsx | 51 +- app/containers/message/Content.tsx | 51 +- app/containers/message/Context.ts | 5 + app/containers/message/Discussion.tsx | 8 +- app/containers/message/Message.stories.tsx | 168 +- app/containers/message/Message.tsx | 460 +- app/containers/message/MessageAvatar.tsx | 6 +- app/containers/message/MessageStore.test.tsx | 497 ++ app/containers/message/MessageStore.tsx | 242 + app/containers/message/Reactions.test.tsx | 83 +- app/containers/message/Reactions.tsx | 4 +- app/containers/message/RepliedThread.tsx | 5 +- app/containers/message/Thread.test.tsx | 25 +- app/containers/message/Thread.tsx | 7 +- app/containers/message/Time.tsx | 4 +- app/containers/message/Urls.tsx | 5 +- app/containers/message/User.tsx | 46 +- .../__snapshots__/Message.test.tsx.snap | 7486 ++++++++++------- .../message/hooks/useMessage.test.ts | 179 - app/containers/message/hooks/useMessage.ts | 129 - .../hooks/useMessageAccessibilityHint.test.ts | 20 - .../useMessageAccessibilityHint.test.tsx | 43 + .../hooks/useMessageAccessibilityHint.ts | 14 +- .../useMessageAccessibilityLabel.test.ts | 158 - .../useMessageAccessibilityLabel.test.tsx | 149 + .../hooks/useMessageAccessibilityLabel.ts | 76 +- app/containers/message/index.tsx | 220 +- app/containers/message/interfaces.ts | 58 +- .../__snapshots__/LoadMore.test.tsx.snap | 84 +- 37 files changed, 5807 insertions(+), 4511 deletions(-) create mode 100644 app/containers/message/MessageStore.test.tsx create mode 100644 app/containers/message/MessageStore.tsx delete mode 100644 app/containers/message/hooks/useMessage.test.ts delete mode 100644 app/containers/message/hooks/useMessage.ts delete mode 100644 app/containers/message/hooks/useMessageAccessibilityHint.test.ts create mode 100644 app/containers/message/hooks/useMessageAccessibilityHint.test.tsx delete mode 100644 app/containers/message/hooks/useMessageAccessibilityLabel.test.ts create mode 100644 app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx diff --git a/app/containers/message/Blocks.ts b/app/containers/message/Blocks.ts index 62ad320cbf5..e13aaddd708 100644 --- a/app/containers/message/Blocks.ts +++ b/app/containers/message/Blocks.ts @@ -3,11 +3,13 @@ import { createElement, useContext } from 'react'; import { messageBlockWithContext } from '../UIKit/MessageBlock'; import { type IMessageBlocks } from './interfaces'; import MessageContext from './Context'; +import { useBlocks } from './MessageStore'; -const Blocks = ({ blocks, id: mid, rid }: IMessageBlocks) => { +const Blocks = ({ rid }: IMessageBlocks) => { 'use memo'; const { blockAction } = useContext(MessageContext); + const { blocks, id: mid } = useBlocks(); if (blocks && blocks.length > 0) { const appId = blocks[0]?.appId || ''; diff --git a/app/containers/message/Broadcast.tsx b/app/containers/message/Broadcast.tsx index a58467c9d50..e77dd1e23bd 100644 --- a/app/containers/message/Broadcast.tsx +++ b/app/containers/message/Broadcast.tsx @@ -10,13 +10,15 @@ import { themes } from '../../lib/constants/colors'; import MessageContext from './Context'; import { type IMessageBroadcast } from './interfaces'; import { useTheme } from '../../theme'; +import { useMessageAuthor } from './MessageStore'; // TODO: Create a reusable button component for message -const Broadcast = ({ author, broadcast }: IMessageBroadcast) => { +const Broadcast = ({ broadcast }: IMessageBroadcast) => { 'use memo'; const { user, replyBroadcast } = useContext(MessageContext); const { theme } = useTheme(); + const { u: author } = useMessageAuthor(); const isOwn = author?._id === user?.id; if (broadcast && !isOwn) { diff --git a/app/containers/message/Components/RightIcons/Edited.tsx b/app/containers/message/Components/RightIcons/Edited.tsx index f6aed2828d9..4eb685f47a7 100644 --- a/app/containers/message/Components/RightIcons/Edited.tsx +++ b/app/containers/message/Components/RightIcons/Edited.tsx @@ -2,10 +2,13 @@ import { View } from 'react-native'; import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; +import { useIsEdited } from '../../MessageStore'; -const Edited = ({ isEdited, testID }: { isEdited: boolean; testID?: string }) => { +const Edited = ({ testID }: { isEdited: boolean; testID?: string }) => { 'use memo'; + const isEdited = useIsEdited(); + if (!isEdited) { return null; } diff --git a/app/containers/message/Components/RightIcons/Encrypted.tsx b/app/containers/message/Components/RightIcons/Encrypted.tsx index 2c1fb26d55f..7b4c4920ac4 100644 --- a/app/containers/message/Components/RightIcons/Encrypted.tsx +++ b/app/containers/message/Components/RightIcons/Encrypted.tsx @@ -6,11 +6,13 @@ import { BUTTON_HIT_SLOP } from '../../utils'; import MessageContext from '../../Context'; import styles from '../../styles'; import { E2E_MESSAGE_TYPE } from '../../../../lib/constants/keys'; +import { useMessageField } from '../../MessageStore'; -const Encrypted = ({ type }: { type: string }) => { +const Encrypted = (_props: { type: string }) => { 'use memo'; const { onEncryptedPress } = useContext(MessageContext); + const type = useMessageField(item => item.t); if (type !== E2E_MESSAGE_TYPE) { return null; diff --git a/app/containers/message/Components/RightIcons/MessageError.tsx b/app/containers/message/Components/RightIcons/MessageError.tsx index 53cadbfc323..821c7333b86 100644 --- a/app/containers/message/Components/RightIcons/MessageError.tsx +++ b/app/containers/message/Components/RightIcons/MessageError.tsx @@ -7,12 +7,14 @@ import { BUTTON_HIT_SLOP } from '../../utils'; import { themes } from '../../../../lib/constants/colors'; import MessageContext from '../../Context'; import { useTheme } from '../../../../theme'; +import { useMessageStatus } from '../../MessageStore'; -const MessageError = ({ hasError }: { hasError: boolean }) => { +const MessageError = (_props: { hasError: boolean }) => { 'use memo'; const { theme } = useTheme(); const { onErrorPress } = useContext(MessageContext); + const { hasError } = useMessageStatus(); if (!hasError) { return null; diff --git a/app/containers/message/Components/RightIcons/Pinned.tsx b/app/containers/message/Components/RightIcons/Pinned.tsx index 61aaf44fa83..343ca8021dc 100644 --- a/app/containers/message/Components/RightIcons/Pinned.tsx +++ b/app/containers/message/Components/RightIcons/Pinned.tsx @@ -2,10 +2,13 @@ import { type ReactElement } from 'react'; import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; +import { useMessageField } from '../../MessageStore'; -const Pinned = ({ pinned, testID }: { pinned?: boolean; testID?: string }): ReactElement | null => { +const Pinned = ({ testID }: { pinned?: boolean; testID?: string }): ReactElement | null => { 'use memo'; + const pinned = useMessageField(item => item.pinned); + if (pinned) return <CustomIcon testID={testID} name='pin' size={16} style={styles.rightIcons} />; return null; }; diff --git a/app/containers/message/Components/RightIcons/ReadReceipt.tsx b/app/containers/message/Components/RightIcons/ReadReceipt.tsx index 215249c4316..9816ea4029f 100644 --- a/app/containers/message/Components/RightIcons/ReadReceipt.tsx +++ b/app/containers/message/Components/RightIcons/ReadReceipt.tsx @@ -1,11 +1,13 @@ import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; import { useTheme } from '../../../../theme'; +import { useMessageField } from '../../MessageStore'; -const ReadReceipt = ({ isReadReceiptEnabled, unread }: { isReadReceiptEnabled?: boolean; unread?: boolean }) => { +const ReadReceipt = ({ isReadReceiptEnabled }: { isReadReceiptEnabled?: boolean; unread?: boolean }) => { 'use memo'; const { colors } = useTheme(); + const unread = useMessageField(item => item.unread); const isUnread = unread || unread === null; const iconName = isUnread ? 'check-single' : 'check-double'; const iconColor = isUnread ? colors.fontAnnotation : colors.fontInfo; diff --git a/app/containers/message/Components/RightIcons/Translated.tsx b/app/containers/message/Components/RightIcons/Translated.tsx index 6e35131bc2a..92d280910b7 100644 --- a/app/containers/message/Components/RightIcons/Translated.tsx +++ b/app/containers/message/Components/RightIcons/Translated.tsx @@ -2,10 +2,13 @@ import { View } from 'react-native'; import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; +import { useMessageText } from '../../MessageStore'; -const Translated = ({ isTranslated }: { isTranslated: boolean }) => { +const Translated = (_props: { isTranslated: boolean }) => { 'use memo'; + const { isTranslated } = useMessageText(); + if (!isTranslated) { return null; } diff --git a/app/containers/message/Content.test.tsx b/app/containers/message/Content.test.tsx index 60b46dc0cb3..618c59ae0ae 100644 --- a/app/containers/message/Content.test.tsx +++ b/app/containers/message/Content.test.tsx @@ -3,8 +3,11 @@ import { render } from '@testing-library/react-native'; import Content from './Content'; import MessageContext from './Context'; +import { MessageProvider } from './MessageStore'; import { setUser } from '../../actions/login'; import { mockedStore } from '../../reducers/mockedStore'; +import { type IAttachment, type TAnyMessageModel } from '../../definitions'; +import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; import { type IMessageContent } from './interfaces'; mockedStore.dispatch( @@ -17,7 +20,20 @@ mockedStore.dispatch( }) ); -const baseProps: Partial<IMessageContent> = { +type TOverrides = Partial<IMessageContent> & { + attachments?: IAttachment[]; + isEncrypted?: boolean; + msg?: string; + _id?: string; + isTemp?: boolean; + isInfo?: boolean; + isEdited?: boolean; + isTranslated?: boolean; + isHeader?: boolean; + hasError?: boolean; +}; + +const baseProps: TOverrides = { _id: 'msg-1', isTemp: false, isInfo: false, @@ -30,8 +46,18 @@ const baseProps: Partial<IMessageContent> = { hasError: false }; -const renderContent = (overrides: Partial<IMessageContent> & { attachments?: any[]; autoTranslateLanguage?: string }) => - render( +const buildItem = (msg: string | undefined, attachments: IAttachment[] | undefined, isEncrypted: boolean | undefined) => + ({ + id: 'msg-1', + msg, + attachments, + t: isEncrypted ? E2E_MESSAGE_TYPE : undefined, + e2e: isEncrypted ? E2E_STATUS.PENDING : undefined + } as unknown as TAnyMessageModel); + +const tree = (overrides: TOverrides) => { + const { msg, attachments, isEncrypted, ...contentProps } = overrides; + return ( <Provider store={mockedStore}> <MessageContext.Provider value={{ @@ -40,24 +66,15 @@ const renderContent = (overrides: Partial<IMessageContent> & { attachments?: any getCustomEmoji: jest.fn(), navToRoomInfo: jest.fn() }}> - <Content {...(baseProps as IMessageContent)} {...(overrides as IMessageContent)} /> + <MessageProvider item={buildItem(msg, attachments, isEncrypted)}> + <Content {...(baseProps as IMessageContent)} {...(contentProps as IMessageContent)} /> + </MessageProvider> </MessageContext.Provider> </Provider> ); +}; -const tree = (overrides: Partial<IMessageContent> & { attachments?: any[]; autoTranslateLanguage?: string }) => ( - <Provider store={mockedStore}> - <MessageContext.Provider - value={{ - user: { username: 'john' }, - onLinkPress: jest.fn(), - getCustomEmoji: jest.fn(), - navToRoomInfo: jest.fn() - }}> - <Content {...(baseProps as IMessageContent)} {...(overrides as IMessageContent)} /> - </MessageContext.Provider> - </Provider> -); +const renderContent = (overrides: TOverrides) => render(tree(overrides)); describe('Content preview branch — Thread Message Attachment fallback', () => { test('renders the Attachment title as a preview line when msg is empty and an image-type Attachment has only a title', () => { diff --git a/app/containers/message/Content.tsx b/app/containers/message/Content.tsx index f1b047d5638..205da02dd99 100644 --- a/app/containers/message/Content.tsx +++ b/app/containers/message/Content.tsx @@ -11,6 +11,7 @@ import { type IMessageContent } from './interfaces'; import { useTheme } from '../../theme'; import { themes } from '../../lib/constants/colors'; import { type MessageTypesValues } from '../../definitions'; +import { useContentData, useIsEncrypted, useIsInfo, useMessageAuthor, useMessageStatus, useMessageText } from './MessageStore'; const Content = (props: IMessageContent) => { 'use memo'; @@ -18,30 +19,26 @@ const Content = (props: IMessageContent) => { const { theme } = useTheme(); const { user, onLinkPress, getCustomEmoji = () => null, navToRoomInfo } = useContext(MessageContext); - if (props.isInfo) { + const isInfo = useIsInfo(); + const isEncrypted = useIsEncrypted(); + const { md, mentions, channels, comment, attachments, t: type } = useContentData(); + const { isTemp } = useMessageStatus(); + const { messageText, isTranslated } = useMessageText(); + const { u: author, role } = useMessageAuthor(); + + if (isInfo) { // @ts-ignore - const infoMessage = getInfoMessage({ ...props }); + const infoMessage = getInfoMessage({ type, role, msg: messageText, author, comment }); const renderMessageContent = ( <Text style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} accessibilityLabel={infoMessage}> {infoMessage} </Text> ); - if (messageHaveAuthorName(props.type as MessageTypesValues)) { + if (messageHaveAuthorName(type as MessageTypesValues)) { return ( <Text> - <User - hasError={props.hasError} - type={props.type} - isEdited={props.isEdited} - isTranslated={props.isTranslated} - isHeader={props.isHeader} - useRealName={props.useRealName} - author={props.author} - alias={props.alias} - pinned={props.pinned} - />{' '} - {renderMessageContent} + <User useRealName={props.useRealName} /> {renderMessageContent} </Text> ); } @@ -52,7 +49,7 @@ const Content = (props: IMessageContent) => { const isPreview = props.tmid && !props.isThreadRoom; let content = null; - if (props.isEncrypted) { + if (isEncrypted) { content = ( <Text style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} @@ -63,38 +60,36 @@ const Content = (props: IMessageContent) => { ); } else if (isPreview) { const previewMsg = - props.msg || - (props.attachments?.length - ? getPreviewMessageFromAttachment(props.attachments[0], props.autoTranslateLanguage) - : undefined); + messageText || + (attachments?.length ? getPreviewMessageFromAttachment(attachments[0], props.autoTranslateLanguage) : undefined); content = previewMsg ? <MarkdownPreview testID={`message-preview-${previewMsg}`} msg={previewMsg} /> : null; - } else if (props.msg) { + } else if (messageText) { content = ( <Markdown - msg={props.msg} - md={props.type !== 'e2e' ? props.md : undefined} + msg={messageText} + md={type !== 'e2e' ? md : undefined} getCustomEmoji={getCustomEmoji} username={user?.username ?? ''} - channels={props.channels} - mentions={props.mentions} + channels={channels} + mentions={mentions} navToRoomInfo={navToRoomInfo} useRealName={props.useRealName} onLinkPress={onLinkPress} - isTranslated={props.isTranslated} + isTranslated={isTranslated} /> ); } if (props.isIgnored) { content = ( - <Text style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} testID={`message-ignored-${props.msg}`}> + <Text style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} testID={`message-ignored-${messageText}`}> {I18n.t('Message_Ignored')} </Text> ); } return content ? ( - <View style={props.isTemp && styles.temp} testID={`message-content-${props.msg || ''}`}> + <View style={isTemp && styles.temp} testID={`message-content-${messageText || ''}`}> {content} </View> ) : null; diff --git a/app/containers/message/Context.ts b/app/containers/message/Context.ts index 5b8f41f05e8..cb38a88186e 100644 --- a/app/containers/message/Context.ts +++ b/app/containers/message/Context.ts @@ -34,6 +34,11 @@ export interface IMessageContext { blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void; handleEnterCall?: () => void; fetchThreadName?: (tmid: string, id: string) => Promise<string | undefined>; + broadcast?: boolean; + Message_GroupingPeriod?: number; + isThreadRoom?: boolean; + autoTranslateRoom?: boolean; + autoTranslateLanguage?: string; } const MessageContext = createContext<IMessageContext>({}); diff --git a/app/containers/message/Discussion.tsx b/app/containers/message/Discussion.tsx index cb58ebde82f..04f8d988242 100644 --- a/app/containers/message/Discussion.tsx +++ b/app/containers/message/Discussion.tsx @@ -9,14 +9,16 @@ import { CustomIcon } from '../CustomIcon'; import { DISCUSSION } from './constants'; import MessageContext from './Context'; import { formatDateThreads } from '../../lib/methods/helpers/room'; -import { type IMessage } from '../../definitions'; import { useTheme } from '../../theme'; +import { useDiscussion, useMessageText } from './MessageStore'; // TODO: Create a reusable button component for message -const Discussion = ({ msg, dcount, dlm }: Pick<IMessage, 'msg' | 'dcount' | 'dlm'>) => { +const Discussion = () => { 'use memo'; const { colors } = useTheme(); + const { dcount, dlm } = useDiscussion(); + const { messageText } = useMessageText(); let time; if (dlm) { time = formatDateThreads(dlm); @@ -26,7 +28,7 @@ const Discussion = ({ msg, dcount, dlm }: Pick<IMessage, 'msg' | 'dcount' | 'dlm return ( <View style={{ gap: 4 }}> <Text style={[styles.startedDiscussion, { color: colors.fontSecondaryInfo }]}>{I18n.t('Started_discussion')}</Text> - <Text style={[styles.discussionText, { color: colors.fontDefault }]}>{msg}</Text> + <Text style={[styles.discussionText, { color: colors.fontDefault }]}>{messageText}</Text> <View style={[styles.buttonContainer, { gap: 8 }]}> <Touchable onPress={onDiscussionPress} diff --git a/app/containers/message/Message.stories.tsx b/app/containers/message/Message.stories.tsx index b3786b887df..cc0c303e0df 100644 --- a/app/containers/message/Message.stories.tsx +++ b/app/containers/message/Message.stories.tsx @@ -1,9 +1,9 @@ import { ScrollView } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; -import MessageComponent from './Message'; -import MessageContext from './Context'; +import MessageContainer from './index'; import { type ICustomEmoji } from '../../definitions/IEmoji'; +import { type TAnyMessageModel } from '../../definitions'; import { E2E_MESSAGE_TYPE } from '../../lib/constants/keys'; import { messagesStatus } from '../../lib/constants/messagesStatus'; import { themes } from '../../lib/constants/colors'; @@ -16,6 +16,7 @@ import { } from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; import { mockedStore as store } from '../../reducers/mockedStore'; import { updateSettings } from '../../actions/settings'; +import { createInteractionStore, InteractionStoreContext } from '../../views/RoomView/InteractionStore'; const _theme = 'light'; @@ -111,22 +112,145 @@ export default { ] }; -export const Message = (props: any) => ( - <MessageContext.Provider value={storyContextValue}> - <ResponsiveLayoutContext.Provider value={responsiveLayoutProviderLargeFontValue(1)}> - <MessageComponent baseUrl={baseUrl} user={user} author={author} ts={date} timeFormat='LT' isHeader {...props} /> +const rid = 'GENERAL'; + +const otherUser = { _id: 'other-user', username: 'other.user', name: 'Other User' }; + +const editedByFixture = { _id: 'edited-by', username: 'diego.mello' }; + +let storyMessageIdCounter = 0; + +const buildMessage = (overrides: Record<string, any> = {}): TAnyMessageModel => { + const definedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== undefined)); + return { + id: `story-message-${++storyMessageIdCounter}`, + msg: undefined, + t: undefined, + ts: date, + u: author, + alias: undefined, + groupable: true, + avatar: undefined, + emoji: undefined, + attachments: undefined, + urls: undefined, + status: undefined, + pinned: undefined, + editedBy: undefined, + reactions: undefined, + role: undefined, + drid: undefined, + dcount: undefined, + dlm: undefined, + tmid: undefined, + tcount: undefined, + tlm: undefined, + replies: undefined, + mentions: undefined, + channels: undefined, + unread: undefined, + autoTranslate: undefined, + translations: undefined, + tmsg: undefined, + blocks: undefined, + e2e: undefined, + md: undefined, + comment: undefined, + ...definedOverrides + } as unknown as TAnyMessageModel; +}; + +// Builds the message model for a single <Message>/<MessageLargeFont> call, pulling the +// scalar display flags (author/type/hasError/status/isEdited/isTranslated) out of the +// props that used to be passed straight to the leaf component. +const buildItem = (props: any): TAnyMessageModel => { + const { author: authorOverride, type, hasError, status, isEdited, isTranslated, ...itemProps } = props; + const resolvedAuthor = authorOverride ?? (isTranslated ? otherUser : author); + return buildMessage({ + ...itemProps, + u: resolvedAuthor, + t: type, + status: status ?? (hasError ? messagesStatus.ERROR : undefined), + editedBy: isEdited ? editedByFixture : undefined, + msg: isTranslated ? 'Untranslated message' : itemProps.msg, + autoTranslate: isTranslated ? true : undefined, + translations: isTranslated ? [{ _id: 'translation-1', language: 'en', value: itemProps.msg ?? 'Message' }] : undefined + }); +}; + +// Synthesizes a sibling message so MessageContainer's real grouping logic (computeIsHeader) +// derives isHeader=false the same way the old isHeader={false} prop used to force it. +const buildGroupedPreviousItem = (item: TAnyMessageModel): TAnyMessageModel => + buildMessage({ + id: `${item.id}-prev`, + u: item.u, + ts: item.ts, + tmid: item.tmid, + t: item.t, + groupable: item.groupable, + status: item.status + }); + +// Synthesizes a non-matching sibling thread message so isThreadReply derives true, +// the same way the old isThreadReply prop used to force it. +const buildThreadReplyPreviousItem = (item: TAnyMessageModel): TAnyMessageModel => + buildMessage({ id: `${item.id}-prev`, u: otherUser, ts: item.ts }); + +const resolvePreviousItem = (item: TAnyMessageModel, isHeader: boolean | undefined, isThreadReply: boolean | undefined) => { + if (isHeader === false) { + return buildGroupedPreviousItem(item); + } + if (isThreadReply) { + return buildThreadReplyPreviousItem(item); + } + return undefined; +}; + +const renderMessageStory = (fontScale: number, props: any) => { + const { + isHeader, + isThreadReply, + previousItem: previousItemOverride, + broadcast, + archived, + timeFormat = 'LT', + useRealName, + isReadReceiptEnabled, + isIgnored, + isTranslated, + autoTranslateRoom, + autoTranslateLanguage, + ...itemProps + } = props; + + const item = buildItem({ ...itemProps, isTranslated }); + const previousItem = previousItemOverride ?? resolvePreviousItem(item, isHeader, isThreadReply); + + return ( + <ResponsiveLayoutContext.Provider value={responsiveLayoutProviderLargeFontValue(fontScale)}> + <MessageContainer + item={item} + previousItem={previousItem} + rid={rid} + Message_GroupingPeriod={300} + timeFormat={timeFormat} + broadcast={broadcast} + archived={archived} + isIgnored={isIgnored} + useRealName={useRealName} + isReadReceiptEnabled={isReadReceiptEnabled} + autoTranslateRoom={autoTranslateRoom ?? (isTranslated ? true : undefined)} + autoTranslateLanguage={autoTranslateLanguage ?? (isTranslated ? 'en' : undefined)} + {...storyContextValue} + /> </ResponsiveLayoutContext.Provider> - </MessageContext.Provider> -); + ); +}; + +export const Message = (props: any) => renderMessageStory(1, props); // The large font components are not perfect because the text's font scale increases only with the device's font size setting. -const MessageLargeFont = (props: any) => ( - <MessageContext.Provider value={storyContextValue}> - <ResponsiveLayoutContext.Provider value={responsiveLayoutProviderLargeFontValue(FONT_SCALE_LIMIT)}> - <MessageComponent baseUrl={baseUrl} user={user} author={author} ts={date} timeFormat='LT' isHeader {...props} /> - </ResponsiveLayoutContext.Provider> - </MessageContext.Provider> -); +const MessageLargeFont = (props: any) => renderMessageStory(FONT_SCALE_LIMIT, props); export const Basic = () => ( <> @@ -2270,9 +2394,19 @@ export const Temp = () => <Message msg='Temp message' status={messagesStatus.TEM export const TempLargeFont = () => <MessageLargeFont msg='Temp message' status={messagesStatus.TEMP} isTemp />; -export const Editing = () => <Message msg='Message being edited' isBeingEdited />; +const editingStore = createInteractionStore({ action: 'edit', selectedMessages: ['editing-message'] }); -export const EditingLargeFont = () => <MessageLargeFont msg='Message being edited' isBeingEdited />; +export const Editing = () => ( + <InteractionStoreContext.Provider value={editingStore}> + <Message id='editing-message' msg='Message being edited' /> + </InteractionStoreContext.Provider> +); + +export const EditingLargeFont = () => ( + <InteractionStoreContext.Provider value={editingStore}> + <MessageLargeFont id='editing-message' msg='Message being edited' /> + </InteractionStoreContext.Provider> +); export const SystemMessages = () => ( <> diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index 181547d9495..f7e58e2ef3a 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -16,16 +16,6 @@ import Broadcast from './Broadcast'; import Discussion from './Discussion'; import Content from './Content'; import CallButton from './CallButton'; -import { - type IMessageAttachments, - type IMessageAvatar, - type IMessageBlocks, - type IMessageBroadcast, - type IMessageContent, - type IMessageRepliedThread, - type IMessageThread, - type IMessageTouchable -} from './interfaces'; import { useTheme } from '../../theme'; import RightIcons from './Components/RightIcons'; import { WidthAwareView } from './Components/WidthAwareView'; @@ -37,22 +27,36 @@ import { useLastFocusedMessageRef } from '../../lib/a11y/useLastFocusedMessageRe import { useMessageAccessibilityLabel } from './hooks/useMessageAccessibilityLabel'; import { useMessageAccessibilityActions } from './hooks/useMessageAccessibilityActions'; import { useMessageAccessibilityHint } from './hooks/useMessageAccessibilityHint'; +import { + useBlocks, + useContentData, + useIsEdited, + useIsInfo, + useMessageAuthor, + useMessageField, + useMessageGrouping, + useMessageMeta, + useMessageStatus, + useMessageText, + useThreadData, + useThreadPosition +} from './MessageStore'; -type TMessageProps = IMessageContent & - IMessageAttachments & - IMessageThread & - IMessageBlocks & - IMessageBroadcast & - IMessageAvatar & - IMessageRepliedThread & - IMessageTouchable & { - isReadReceiptEnabled?: boolean; - unread?: boolean; - dcount?: number; - dlm?: Date | string; - isThreadSequential: boolean; - isPreview?: boolean; - }; +type TMessageProps = { + rid: string; + timeFormat?: string; + archived?: boolean; + broadcast?: boolean; + useRealName?: boolean; + isReadReceiptEnabled?: boolean; + isThreadRoom: boolean; + isPreview?: boolean; + highlighted?: boolean; + isIgnored: boolean; + isBeingEdited?: boolean; + autoTranslateLanguage?: string; + small?: boolean; +}; interface IMessageA11y { accessibilityActions?: AccessibilityActionInfo[]; @@ -64,157 +68,70 @@ const MessageInner = (props: TMessageProps) => { 'use memo'; const { isLargeFontScale } = useResponsiveLayout(); - const showTimeLarge = isLargeFontScale && props.isHeader; + const isHeader = useMessageGrouping(); + const { attachments, t: type } = useContentData(); + const { blocks } = useBlocks(); + const { tmid } = useThreadData(); + const { u: author } = useMessageAuthor(); + const showTimeLarge = isLargeFontScale && isHeader; let content; if (props.isPreview) { content = ( <> - <User - isHeader={props.isHeader} - hasError={props.hasError} - useRealName={props.useRealName} - author={props.author} - alias={props.alias} - ts={props.ts as Date | undefined} - timeFormat={props.timeFormat} - isEdited={props.isEdited} - isReadReceiptEnabled={props.isReadReceiptEnabled} - unread={props.unread} - pinned={props.pinned} - isTranslated={props.isTranslated} - type={props.type} - /> - {showTimeLarge ? <MessageTime ts={props.ts as Date | undefined} timeFormat={props.timeFormat} /> : null} + <User useRealName={props.useRealName} timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> + {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} <> - <Quote attachments={props.attachments} timeFormat={props.timeFormat} author={props.author} /> + <Quote attachments={attachments} timeFormat={props.timeFormat} author={author} /> <Content - isTemp={props.isTemp} - isInfo={props.isInfo} - tmid={props.tmid} + tmid={tmid} isThreadRoom={props.isThreadRoom} - msg={props.msg} - md={props.md} - isEdited={props.isEdited} - isEncrypted={props.isEncrypted} - channels={props.channels} - mentions={props.mentions} useRealName={props.useRealName} isIgnored={props.isIgnored} - type={props.type} - comment={props.comment} - hasError={props.hasError} - isHeader={props.isHeader} - isTranslated={props.isTranslated} - pinned={props.pinned} - attachments={props.attachments} autoTranslateLanguage={props.autoTranslateLanguage} - author={props.author} - alias={props.alias} - role={props.role} /> - <Attachments attachments={props.attachments} timeFormat={props.timeFormat} author={props.author} /> + <Attachments attachments={attachments} timeFormat={props.timeFormat} author={author} /> </> - <Urls urls={props.urls} /> + <Urls /> </> ); } - if (props.type === 'discussion-created') { + if (type === 'discussion-created') { content = ( <> - <User - isHeader={props.isHeader} - hasError={props.hasError} - useRealName={props.useRealName} - author={props.author} - alias={props.alias} - ts={props.ts as Date | undefined} - timeFormat={props.timeFormat} - isEdited={props.isEdited} - isReadReceiptEnabled={props.isReadReceiptEnabled} - unread={props.unread} - pinned={props.pinned} - isTranslated={props.isTranslated} - type={props.type} - /> - {showTimeLarge ? <MessageTime ts={props.ts as Date | undefined} timeFormat={props.timeFormat} /> : null} - <Discussion msg={props.msg} dcount={props.dcount} dlm={props.dlm} /> + <User useRealName={props.useRealName} timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> + {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} + <Discussion /> </> ); } - if (props.type === 'jitsi_call_started') { + if (type === 'jitsi_call_started') { content = ( <> - <User - isHeader={props.isHeader} - hasError={props.hasError} - useRealName={props.useRealName} - author={props.author} - alias={props.alias} - ts={props.ts as Date | undefined} - timeFormat={props.timeFormat} - isEdited={props.isEdited} - isReadReceiptEnabled={props.isReadReceiptEnabled} - unread={props.unread} - pinned={props.pinned} - isTranslated={props.isTranslated} - type={props.type} - /> + <User useRealName={props.useRealName} timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> <Content - isTemp={props.isTemp} - isInfo - tmid={props.tmid} + tmid={tmid} isThreadRoom={props.isThreadRoom} - msg={props.msg} - md={props.md} - isEdited={props.isEdited} - isEncrypted={props.isEncrypted} - channels={props.channels} - mentions={props.mentions} useRealName={props.useRealName} isIgnored={props.isIgnored} - type={props.type} - comment={props.comment} - hasError={props.hasError} - isHeader={props.isHeader} - isTranslated={props.isTranslated} - pinned={props.pinned} - attachments={props.attachments} autoTranslateLanguage={props.autoTranslateLanguage} - author={props.author} - alias={props.alias} - role={props.role} /> <CallButton /> - {showTimeLarge ? <MessageTime ts={props.ts as Date | undefined} timeFormat={props.timeFormat} /> : null} + {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} </> ); } - if (props.blocks && props.blocks.length) { + if (blocks && blocks.length) { content = ( <> - <User - isHeader={props.isHeader} - hasError={props.hasError} - useRealName={props.useRealName} - author={props.author} - alias={props.alias} - ts={props.ts as Date | undefined} - timeFormat={props.timeFormat} - isEdited={props.isEdited} - isReadReceiptEnabled={props.isReadReceiptEnabled} - unread={props.unread} - pinned={props.pinned} - isTranslated={props.isTranslated} - type={props.type} - /> - <Blocks blocks={props.blocks} id={props.id} rid={props.rid} /> - <Thread msg={props.msg} tcount={props.tcount} tlm={props.tlm} id={props.id} isThreadRoom={props.isThreadRoom} /> - <Reactions reactions={props.reactions} /> - {showTimeLarge ? <MessageTime ts={props.ts as Date | undefined} timeFormat={props.timeFormat} /> : null} + <User useRealName={props.useRealName} timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> + <Blocks rid={props.rid} /> + <Thread isThreadRoom={props.isThreadRoom} /> + <Reactions /> + {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} </> ); } @@ -222,54 +139,22 @@ const MessageInner = (props: TMessageProps) => { if (!content) { content = ( <> - <User - isHeader={props.isHeader} - hasError={props.hasError} - useRealName={props.useRealName} - author={props.author} - alias={props.alias} - ts={props.ts as Date | undefined} - timeFormat={props.timeFormat} - isEdited={props.isEdited} - isReadReceiptEnabled={props.isReadReceiptEnabled} - unread={props.unread} - pinned={props.pinned} - isTranslated={props.isTranslated} - type={props.type} - /> - {showTimeLarge ? <MessageTime ts={props.ts as Date | undefined} timeFormat={props.timeFormat} /> : null} + <User useRealName={props.useRealName} timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> + {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} <View style={{ gap: 4 }}> - <Quote attachments={props.attachments} timeFormat={props.timeFormat} author={props.author} /> + <Quote attachments={attachments} timeFormat={props.timeFormat} author={author} /> <Content - isTemp={props.isTemp} - isInfo={props.isInfo} - tmid={props.tmid} + tmid={tmid} isThreadRoom={props.isThreadRoom} - msg={props.msg} - md={props.md} - isEdited={props.isEdited} - isEncrypted={props.isEncrypted} - channels={props.channels} - mentions={props.mentions} useRealName={props.useRealName} isIgnored={props.isIgnored} - type={props.type} - comment={props.comment} - hasError={props.hasError} - isHeader={props.isHeader} - isTranslated={props.isTranslated} - pinned={props.pinned} - attachments={props.attachments} autoTranslateLanguage={props.autoTranslateLanguage} - author={props.author} - alias={props.alias} - role={props.role} /> - <Attachments attachments={props.attachments} timeFormat={props.timeFormat} author={props.author} /> - <Urls urls={props.urls} /> - <Thread msg={props.msg} tcount={props.tcount} tlm={props.tlm} id={props.id} isThreadRoom={props.isThreadRoom} /> - <Reactions reactions={props.reactions} /> - <Broadcast author={props.author} broadcast={props.broadcast} /> + <Attachments attachments={attachments} timeFormat={props.timeFormat} author={author} /> + <Urls /> + <Thread isThreadRoom={props.isThreadRoom} /> + <Reactions /> + <Broadcast broadcast={props.broadcast} /> </View> </> ); @@ -282,57 +167,43 @@ MessageInner.displayName = 'MessageInner'; const Message = (props: TMessageProps & IMessageA11y) => { 'use memo'; - if (props.isThreadReply || props.isThreadSequential || props.isInfo || props.isIgnored) { - const thread = props.isThreadReply ? ( - <RepliedThread - tmid={props.tmid} - tmsg={props.tmsg} - id={props.id} - isHeader={props.isHeader} - isEncrypted={props.isEncrypted} - /> - ) : null; - const infoStyle: ViewStyle = props.isInfo ? { alignItems: 'center' } : {}; + const isHeader = useMessageGrouping(); + const { isThreadReply, isThreadSequential } = useThreadPosition(); + const isInfo = useIsInfo(); + const { messageText, isTranslated } = useMessageText(); + const { attachments, t: type } = useContentData(); + const { tmid } = useThreadData(); + const { u: author } = useMessageAuthor(); + const id = useMessageField(item => item.id); + const isEdited = useIsEdited(); + const { hasError } = useMessageStatus(); + const { unread, pinned } = useMessageMeta(); + + if (isThreadReply || isThreadSequential || isInfo || props.isIgnored) { + const thread = isThreadReply ? <RepliedThread isHeader={isHeader} /> : null; + const infoStyle: ViewStyle = isInfo ? { alignItems: 'center' } : {}; return ( <View style={[styles.container, { marginTop: 4 }]}> {thread} <View style={[styles.flex, infoStyle]}> - <MessageAvatar isHeader={props.isHeader} avatar={props.avatar} emoji={props.emoji} author={props.author} small /> + <MessageAvatar small /> <A11y.Index - accessible={props.isTranslated} - accessibilityLabel={props?.msg || ''} + accessible={isTranslated} + accessibilityLabel={messageText || ''} accessibilityLanguage={props.autoTranslateLanguage} index={2} style={{ flex: 1 }}> <View style={styles.messageContent}> <Content - isTemp={props.isTemp} - isInfo={props.isInfo} - tmid={props.tmid} + tmid={tmid} isThreadRoom={props.isThreadRoom} - msg={props.msg} - md={props.md} - isEdited={props.isEdited} - isEncrypted={props.isEncrypted} - channels={props.channels} - mentions={props.mentions} useRealName={props.useRealName} isIgnored={props.isIgnored} - type={props.type} - comment={props.comment} - hasError={props.hasError} - isHeader={props.isHeader} - isTranslated={props.isTranslated} - pinned={props.pinned} - attachments={props.attachments} autoTranslateLanguage={props.autoTranslateLanguage} - author={props.author} - alias={props.alias} - role={props.role} /> - {props.isInfo && props.type === 'message_pinned' ? ( + {isInfo && type === 'message_pinned' ? ( <View pointerEvents='none'> - <Attachments attachments={props.attachments} timeFormat={props.timeFormat} author={props.author} /> + <Attachments attachments={attachments} timeFormat={props.timeFormat} author={author} /> </View> ) : null} </View> @@ -343,76 +214,41 @@ const Message = (props: TMessageProps & IMessageA11y) => { } return ( - <View testID={`message-${props.id}`} style={styles.container}> + <View testID={`message-${id}`} style={styles.container}> <A11y.Index - accessible={props.isTranslated} - accessibilityLabel={props?.msg || ''} + accessible={isTranslated} + accessibilityLabel={messageText || ''} accessibilityLanguage={props.autoTranslateLanguage} index={2}> <View style={styles.flex}> - <MessageAvatar isHeader={props.isHeader} avatar={props.avatar} emoji={props.emoji} author={props.author} /> + <MessageAvatar /> <View style={styles.messageContent}> <MessageInner - id={props.id} rid={props.rid} - msg={props.msg} - md={props.md} - type={props.type} - attachments={props.attachments} - blocks={props.blocks} - urls={props.urls} - reactions={props.reactions} - alias={props.alias} - avatar={props.avatar} - emoji={props.emoji} timeFormat={props.timeFormat} archived={props.archived} broadcast={props.broadcast} useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} - unread={props.unread} - role={props.role} - drid={props.drid} - dcount={props.dcount} - dlm={props.dlm} - tmid={props.tmid} - tcount={props.tcount} - tlm={props.tlm} - tmsg={props.tmsg} - mentions={props.mentions} - channels={props.channels} - isIgnored={props.isIgnored} - isEdited={props.isEdited} - isHeader={props.isHeader} - isThreadReply={props.isThreadReply} - isThreadSequential={props.isThreadSequential} isThreadRoom={props.isThreadRoom} - isInfo={props.isInfo} - isTemp={props.isTemp} - isEncrypted={props.isEncrypted} - hasError={props.hasError} + isPreview={props.isPreview} highlighted={props.highlighted} - comment={props.comment} - isTranslated={props.isTranslated} + isIgnored={props.isIgnored} isBeingEdited={props.isBeingEdited} - isPreview={props.isPreview} - pinned={props.pinned} autoTranslateLanguage={props.autoTranslateLanguage} - author={props.author} - ts={props.ts} small={props.small} /> </View> - {!props.isHeader ? ( + {!isHeader ? ( <RightIcons - type={props.type} - msg={props.msg} - isEdited={props.isEdited} - hasError={props.hasError} + type={type} + msg={messageText} + isEdited={isEdited} + hasError={hasError} isReadReceiptEnabled={props.isReadReceiptEnabled} - unread={props.unread} - pinned={props.pinned} - isTranslated={props.isTranslated} + unread={unread} + pinned={pinned} + isTranslated={isTranslated} /> ) : null} </View> @@ -428,11 +264,19 @@ const MessageTouchable = (props: TMessageProps) => { const { onPress, onLongPress } = useContext(MessageContext); const { colors } = useTheme(); const { ref: touchRef, markAsLastFocused } = useLastFocusedMessageRef(); - const accessibilityLabelValue = useMessageAccessibilityLabel(props); - const isDisabled = - (props.isInfo && !props.isThreadReply) || props.archived || props.isTemp || props.type === 'jitsi_call_started'; + const { isThreadReply } = useThreadPosition(); + const isInfo = useIsInfo(); + const { hasError, isTemp } = useMessageStatus(); + const type = useMessageField(item => item.t); + const id = useMessageField(item => item.id); + const accessibilityLabelValue = useMessageAccessibilityLabel({ + useRealName: props.useRealName, + isReadReceiptEnabled: props.isReadReceiptEnabled, + autoTranslateLanguage: props.autoTranslateLanguage + }); + const isDisabled = (isInfo && !isThreadReply) || props.archived || isTemp || type === 'jitsi_call_started'; const accessibilityActions = useMessageAccessibilityActions(isDisabled); - const accessibilityHint = useMessageAccessibilityHint(props); + const accessibilityHint = useMessageAccessibilityHint(); let backgroundColor = undefined; if (props.isBeingEdited) { @@ -442,57 +286,22 @@ const MessageTouchable = (props: TMessageProps) => { backgroundColor = colors.surfaceNeutral; } - if (props.hasError || props.isInfo) { + if (hasError || isInfo) { return ( <A11y.Order> <Message - id={props.id} rid={props.rid} - msg={props.msg} - md={props.md} - type={props.type} - attachments={props.attachments} - blocks={props.blocks} - urls={props.urls} - reactions={props.reactions} - alias={props.alias} - avatar={props.avatar} - emoji={props.emoji} timeFormat={props.timeFormat} archived={props.archived} broadcast={props.broadcast} useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} - unread={props.unread} - role={props.role} - drid={props.drid} - dcount={props.dcount} - dlm={props.dlm} - tmid={props.tmid} - tcount={props.tcount} - tlm={props.tlm} - tmsg={props.tmsg} - mentions={props.mentions} - channels={props.channels} - isIgnored={props.isIgnored} - isEdited={props.isEdited} - isHeader={props.isHeader} - isThreadReply={props.isThreadReply} - isThreadSequential={props.isThreadSequential} isThreadRoom={props.isThreadRoom} - isInfo={props.isInfo} - isTemp={props.isTemp} - isEncrypted={props.isEncrypted} - hasError={props.hasError} + isPreview={props.isPreview} highlighted={props.highlighted} - comment={props.comment} - isTranslated={props.isTranslated} + isIgnored={props.isIgnored} isBeingEdited={props.isBeingEdited} - isPreview={props.isPreview} - pinned={props.pinned} autoTranslateLanguage={props.autoTranslateLanguage} - author={props.author} - ts={props.ts} small={props.small} /> </A11y.Order> @@ -513,7 +322,7 @@ const MessageTouchable = (props: TMessageProps) => { onPress={onPress} disabled={isDisabled} style={{ backgroundColor }} - testID={props.isBeingEdited ? `message-editing-${props.id}` : undefined} + testID={props.isBeingEdited ? `message-editing-${id}` : undefined} accessible accessibilityRole='button' accessibilityLabel={accessibilityLabelValue} @@ -523,53 +332,18 @@ const MessageTouchable = (props: TMessageProps) => { if (e.nativeEvent.actionName === 'showActions') handleLongPress(); }}> <Message - id={props.id} rid={props.rid} - msg={props.msg} - md={props.md} - type={props.type} - attachments={props.attachments} - blocks={props.blocks} - urls={props.urls} - reactions={props.reactions} - alias={props.alias} - avatar={props.avatar} - emoji={props.emoji} timeFormat={props.timeFormat} archived={props.archived} broadcast={props.broadcast} useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} - unread={props.unread} - role={props.role} - drid={props.drid} - dcount={props.dcount} - dlm={props.dlm} - tmid={props.tmid} - tcount={props.tcount} - tlm={props.tlm} - tmsg={props.tmsg} - mentions={props.mentions} - channels={props.channels} - isIgnored={props.isIgnored} - isEdited={props.isEdited} - isHeader={props.isHeader} - isThreadReply={props.isThreadReply} - isThreadSequential={props.isThreadSequential} isThreadRoom={props.isThreadRoom} - isInfo={props.isInfo} - isTemp={props.isTemp} - isEncrypted={props.isEncrypted} - hasError={props.hasError} + isPreview={props.isPreview} highlighted={props.highlighted} - comment={props.comment} - isTranslated={props.isTranslated} + isIgnored={props.isIgnored} isBeingEdited={props.isBeingEdited} - isPreview={props.isPreview} - pinned={props.pinned} autoTranslateLanguage={props.autoTranslateLanguage} - author={props.author} - ts={props.ts} small={props.small} handleLongPress={!isDisabled ? handleLongPress : undefined} /> diff --git a/app/containers/message/MessageAvatar.tsx b/app/containers/message/MessageAvatar.tsx index 6f51b12d421..40737ebed05 100644 --- a/app/containers/message/MessageAvatar.tsx +++ b/app/containers/message/MessageAvatar.tsx @@ -7,6 +7,7 @@ import MessageContext from './Context'; import { type IMessageAvatar } from './interfaces'; import { SubscriptionType } from '../../definitions'; import { useResponsiveLayout } from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; +import { useAvatar, useMessageAuthor, useMessageGrouping } from './MessageStore'; const AVATAR_BASE_SIZE = 36; @@ -16,11 +17,14 @@ export const AvatarContainer = ({ children }: { children?: ReactElement | null } return <View style={{ width, alignItems: 'flex-end' }}>{children}</View>; }; -const MessageAvatar = ({ isHeader, avatar, author, small, emoji }: IMessageAvatar) => { +const MessageAvatar = ({ small }: IMessageAvatar) => { 'use memo'; const { user, navToRoomInfo, getCustomEmoji } = useContext(MessageContext); const { fontScaleLimited } = useResponsiveLayout(); + const { avatar, emoji } = useAvatar(); + const { u: author } = useMessageAuthor(); + const isHeader = useMessageGrouping(); const smallSize = 20 * fontScaleLimited; const normalSize = AVATAR_BASE_SIZE * fontScaleLimited; const size = small ? smallSize : normalSize; diff --git a/app/containers/message/MessageStore.test.tsx b/app/containers/message/MessageStore.test.tsx new file mode 100644 index 00000000000..772384087f3 --- /dev/null +++ b/app/containers/message/MessageStore.test.tsx @@ -0,0 +1,497 @@ +import { act, render } from '@testing-library/react-native'; + +import { type TAnyMessageModel } from '../../definitions'; +import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; +import { messagesStatus } from '../../lib/constants/messagesStatus'; +import MessageContext, { type IMessageContext } from './Context'; +import { + type MessagePrev, + MessageProvider, + useAvatar, + useBlocks, + useContentData, + useDiscussion, + useIsEdited, + useIsEncrypted, + useIsInfo, + useMessageAuthor, + useMessageCtx, + useMessageField, + useMessageGrouping, + useMessageMeta, + useMessageStatus, + useMessageText, + useReactions, + useRepliedThreadData, + useThreadData, + useThreadPosition, + useUrls +} from './MessageStore'; + +type Subscriber = () => void; + +type FakeModel = TAnyMessageModel & { _emit: () => void; experimentalSubscribe: (cb: Subscriber) => () => void }; + +const buildFakeModel = (overrides: Partial<TAnyMessageModel> = {}): FakeModel => { + const subscribers: Subscriber[] = []; + + const model = { + id: 'msg-1', + msg: 'Hello world', + t: undefined, + u: { _id: 'u1', username: 'alice' }, + alias: undefined, + role: undefined, + avatar: undefined, + emoji: undefined, + attachments: [{ image_url: 'https://example.com/img.png' }], + urls: [], + reactions: [{ emoji: '👍', usernames: ['alice'] }], + blocks: null, + dcount: undefined, + dlm: undefined, + tmid: undefined, + tcount: undefined, + tlm: undefined, + tmsg: undefined, + mentions: [], + channels: [], + comment: undefined, + pinned: false, + md: [{ type: 'PARAGRAPH' }], + ...overrides, + experimentalSubscribe(cb: Subscriber) { + subscribers.push(cb); + return () => { + const idx = subscribers.indexOf(cb); + if (idx !== -1) subscribers.splice(idx, 1); + }; + }, + _emit() { + subscribers.forEach(cb => cb()); + } + } as unknown as FakeModel; + + return model; +}; + +// One probe per `probes` entry; mirrors how each leaf calls a single domain hook. +const renderMessage = (item: TAnyMessageModel, probes: Record<string, () => unknown>) => { + const probeSpies: Record<string, jest.Mock> = {}; + + const probeElements = Object.entries(probes).map(([name, useHook]) => { + const spy = jest.fn(); + probeSpies[name] = spy; + const Probe = () => { + spy(useHook()); + return null; + }; + return <Probe key={name} />; + }); + + render( + <MessageProvider item={item}> + <>{probeElements}</> + </MessageProvider> + ); + + const renderCount = (name: string) => probeSpies[name].mock.calls.length; + const latestValue = (name: string) => { + const { calls } = probeSpies[name].mock; + return calls[calls.length - 1]?.[0]; + }; + + return { renderCount, latestValue }; +}; + +const MsgValueProbe = ({ item, spy }: { item: TAnyMessageModel; spy: jest.Mock }) => ( + <MessageProvider item={item}> + <MsgValueReporter spy={spy} /> + </MessageProvider> +); + +const MsgValueReporter = ({ spy }: { spy: jest.Mock }) => { + spy(useMessageField(m => m.msg)); + return null; +}; + +// Derived hooks read both the per-row store/item/prev and the config in MessageContext, +// so the probe needs both providers mounted. +const renderDerived = ( + item: TAnyMessageModel, + useHook: () => unknown, + { prev, config }: { prev?: MessagePrev; config?: Partial<IMessageContext> } = {} +) => { + const spy = jest.fn(); + const Probe = () => { + spy(useHook()); + return null; + }; + render( + <MessageContext.Provider value={(config ?? {}) as IMessageContext}> + <MessageProvider item={item} prev={prev}> + <Probe /> + </MessageProvider> + </MessageContext.Provider> + ); + const latest = () => { + const { calls } = spy.mock; + return calls[calls.length - 1]?.[0]; + }; + return { latest, spy }; +}; + +describe('MessageStore', () => { + describe('field hooks', () => { + it('return the current field values', () => { + const model = buildFakeModel(); + const { latestValue } = renderMessage(model, { reactions: useReactions, urls: useUrls, author: useMessageAuthor }); + + expect(latestValue('reactions')).toBe(model.reactions); + expect(latestValue('urls')).toBe(model.urls); + expect(latestValue('author')).toEqual({ u: model.u, alias: model.alias, role: model.role }); + }); + + it('return the new value after a model mutation followed by an experimentalSubscribe emit', () => { + const model = buildFakeModel(); + const { latestValue } = renderMessage(model, { urls: useUrls }); + + act(() => { + (model as any).urls = [{ url: 'https://rocket.chat' }]; + model._emit(); + }); + + expect(latestValue('urls')).toEqual([{ url: 'https://rocket.chat' }]); + }); + + it('does not throw and returns current values for a plain object without experimentalSubscribe', () => { + const plainItem = { + id: 'rest-1', + msg: 'Plain REST message', + reactions: [{ emoji: '👍', usernames: ['alice'] }] + } as unknown as TAnyMessageModel; + + expect(() => renderMessage(plainItem, { reactions: useReactions })).not.toThrow(); + }); + + it('reflects an item identity change synchronously', () => { + const modelA = buildFakeModel({ id: 'msg-a', msg: 'Model A' }); + const modelB = buildFakeModel({ id: 'msg-b', msg: 'Model B' }); + const spy = jest.fn(); + + const { rerender } = render(<MsgValueProbe item={modelA} spy={spy} />); + expect(spy).toHaveBeenLastCalledWith('Model A'); + + act(() => rerender(<MsgValueProbe item={modelB} spy={spy} />)); + + expect(spy).toHaveBeenLastCalledWith('Model B'); + }); + }); + + describe('domain hooks', () => { + it('useBlocks selects blocks and id', () => { + const model = buildFakeModel({ blocks: [{ appId: 'app-1' }] }); + const { latestValue } = renderMessage(model, { blocks: useBlocks }); + expect(latestValue('blocks')).toEqual({ blocks: model.blocks, id: model.id }); + }); + + it('useDiscussion selects dcount and dlm', () => { + const model = buildFakeModel({ msg: 'discuss', dcount: 2, dlm: new Date('2024-01-01') }); + const { latestValue } = renderMessage(model, { discussion: useDiscussion }); + expect(latestValue('discussion')).toEqual({ dcount: model.dcount, dlm: model.dlm }); + }); + + it('useThreadData selects tcount, tlm, tmid and id', () => { + const model = buildFakeModel({ tmid: 't1', tcount: 3, tlm: new Date('2024-01-02') }); + const { latestValue } = renderMessage(model, { thread: useThreadData }); + expect(latestValue('thread')).toEqual({ + tcount: model.tcount, + tlm: model.tlm, + tmid: model.tmid, + id: model.id + }); + }); + + it('useRepliedThreadData selects tmid, tmsg and id', () => { + const model = buildFakeModel({ tmid: 't1', tmsg: 'replied' }); + const { latestValue } = renderMessage(model, { repliedThread: useRepliedThreadData }); + expect(latestValue('repliedThread')).toEqual({ tmid: model.tmid, tmsg: model.tmsg, id: model.id }); + }); + + it('useAvatar selects avatar and emoji', () => { + const model = buildFakeModel({ avatar: 'avatar.png', emoji: ':smile:' }); + const { latestValue } = renderMessage(model, { avatar: useAvatar }); + expect(latestValue('avatar')).toEqual({ avatar: model.avatar, emoji: model.emoji }); + }); + + it('useContentData selects the raw content fields', () => { + const model = buildFakeModel(); + const { latestValue } = renderMessage(model, { content: useContentData }); + expect(latestValue('content')).toEqual({ + md: model.md, + mentions: model.mentions, + channels: model.channels, + comment: model.comment, + attachments: model.attachments, + t: model.t + }); + }); + }); + + describe('granularity', () => { + it('re-renders only the consumer of the field that changed', () => { + const model = buildFakeModel(); + const { renderCount } = renderMessage(model, { reactions: useReactions, urls: useUrls }); + + const reactionsBaseline = renderCount('reactions'); + const urlsBaseline = renderCount('urls'); + + act(() => { + (model as any).urls = [{ url: 'https://rocket.chat' }]; + model._emit(); + }); + + expect(renderCount('urls')).toBeGreaterThan(urlsBaseline); + expect(renderCount('reactions')).toBe(reactionsBaseline); + }); + + it('bails a multi-field (useShallow) consumer when an unrelated field changes', () => { + const model = buildFakeModel(); + const { renderCount } = renderMessage(model, { author: useMessageAuthor, reactions: useReactions }); + + const authorBaseline = renderCount('author'); + const reactionsBaseline = renderCount('reactions'); + + act(() => { + (model as any).reactions = [{ emoji: '🎉', usernames: ['bob'] }]; + model._emit(); + }); + + expect(renderCount('reactions')).toBeGreaterThan(reactionsBaseline); + expect(renderCount('author')).toBe(authorBaseline); + }); + }); + + describe('per-instance isolation', () => { + it('keeps state independent across provider instances', () => { + const modelA = buildFakeModel({ id: 'msg-a', msg: 'Model A' }); + const modelB = buildFakeModel({ id: 'msg-b', msg: 'Model B' }); + + const spyA = jest.fn(); + const spyB = jest.fn(); + + render( + <> + <MsgValueProbe item={modelA} spy={spyA} /> + <MsgValueProbe item={modelB} spy={spyB} /> + </> + ); + + expect(spyA).toHaveBeenLastCalledWith('Model A'); + expect(spyB).toHaveBeenLastCalledWith('Model B'); + + act(() => { + (modelA as any).msg = 'Model A updated'; + modelA._emit(); + }); + + expect(spyA).toHaveBeenLastCalledWith('Model A updated'); + expect(spyB).toHaveBeenLastCalledWith('Model B'); + }); + }); + + describe('derived hooks', () => { + it('useMessageStatus returns hasError false and isTemp false for a normal model', () => { + const model = buildFakeModel(); + const { latest } = renderDerived(model, useMessageStatus); + expect(latest()).toEqual({ hasError: false, isTemp: false }); + }); + + it('useMessageStatus returns hasError true and isTemp true for an ERROR status model', () => { + const model = buildFakeModel({ status: messagesStatus.ERROR }); + const { latest } = renderDerived(model, useMessageStatus); + expect(latest()).toEqual({ hasError: true, isTemp: true }); + }); + + it('useMessageStatus returns hasError false and isTemp true for a TEMP status model', () => { + const model = buildFakeModel({ status: messagesStatus.TEMP }); + const { latest } = renderDerived(model, useMessageStatus); + expect(latest()).toEqual({ hasError: false, isTemp: true }); + }); + + it('useIsEncrypted returns true for a pending e2e message', () => { + const model = buildFakeModel({ t: E2E_MESSAGE_TYPE, e2e: 'pending' }); + const { latest } = renderDerived(model, useIsEncrypted); + expect(latest()).toBe(true); + }); + + it('useIsEncrypted returns false for a done e2e message', () => { + const model = buildFakeModel({ t: E2E_MESSAGE_TYPE, e2e: E2E_STATUS.DONE }); + const { latest } = renderDerived(model, useIsEncrypted); + expect(latest()).toBe(false); + }); + + it('useIsEncrypted returns false for a plain text message', () => { + const model = buildFakeModel(); + const { latest } = renderDerived(model, useIsEncrypted); + expect(latest()).toBe(false); + }); + + it('useIsInfo returns false for a discussion-created message', () => { + const model = buildFakeModel({ t: 'discussion-created' }); + const { latest } = renderDerived(model, useIsInfo); + expect(latest()).toBe(false); + }); + + it('useIsInfo returns the type for an info message', () => { + const model = buildFakeModel({ t: 'room_changed_topic' }); + const { latest } = renderDerived(model, useIsInfo); + expect(latest()).toBe('room_changed_topic'); + }); + + it('useIsInfo returns undefined for a default text message', () => { + const model = buildFakeModel(); + const { latest } = renderDerived(model, useIsInfo); + expect(latest()).toBeUndefined(); + }); + + it('useIsEdited returns false for a default model', () => { + const model = buildFakeModel(); + const { latest } = renderDerived(model, useIsEdited); + expect(latest()).toBe(false); + }); + + it('useIsEdited returns true for a model with editedBy', () => { + const model = buildFakeModel({ editedBy: { _id: 'u2', username: 'bob' } }); + const { latest } = renderDerived(model, useIsEdited); + expect(latest()).toBe(true); + }); + + it('useThreadPosition returns false/false when isThreadRoom is true', () => { + const model = buildFakeModel({ tmid: 't1' }); + const { latest } = renderDerived(model, useThreadPosition, { config: { isThreadRoom: true } }); + expect(latest()).toEqual({ isThreadReply: false, isThreadSequential: false }); + }); + + it('useThreadPosition returns isThreadReply true when the message replies to a different thread than prev', () => { + const model = buildFakeModel({ tmid: 't1' }); + const { latest } = renderDerived(model, useThreadPosition, { prev: { tmid: 't0', id: 'p0' } }); + expect(latest()).toEqual({ isThreadReply: true, isThreadSequential: true }); + }); + + it('useThreadPosition returns isThreadReply false when prev is in the same thread', () => { + const model = buildFakeModel({ tmid: 't1' }); + const { latest } = renderDerived(model, useThreadPosition, { prev: { tmid: 't1' } }); + expect(latest()).toEqual({ isThreadReply: false, isThreadSequential: true }); + }); + + it('useMessageGrouping returns true (header) when there is no prev', () => { + const model = buildFakeModel(); + const { latest } = renderDerived(model, useMessageGrouping); + expect(latest()).toBe(true); + }); + + it('useMessageGrouping returns true (header) when prev has an ERROR status', () => { + const model = buildFakeModel(); + const { latest } = renderDerived(model, useMessageGrouping, { prev: { status: messagesStatus.ERROR } }); + expect(latest()).toBe(true); + }); + + it('useMessageGrouping returns false when prev matches author/day/period/thread', () => { + const ts = new Date('2024-01-01T10:00:00Z'); + const prevTs = new Date('2024-01-01T09:59:50Z'); + const model = buildFakeModel({ ts, tmid: 't1' }); + const { latest } = renderDerived(model, useMessageGrouping, { + prev: { ts: prevTs, username: 'alice', tmid: 't1' }, + config: { broadcast: false, Message_GroupingPeriod: 300 } + }); + expect(latest()).toBe(false); + }); + + it('useMessageGrouping returns true (header) when the room is broadcast, even if prev would otherwise group', () => { + const ts = new Date('2024-01-01T10:00:00Z'); + const prevTs = new Date('2024-01-01T09:59:50Z'); + const model = buildFakeModel({ ts, tmid: 't1' }); + const { latest } = renderDerived(model, useMessageGrouping, { + prev: { ts: prevTs, username: 'alice', tmid: 't1' }, + config: { broadcast: true, Message_GroupingPeriod: 300 } + }); + expect(latest()).toBe(true); + }); + + it('useMessageGrouping returns true (header) when groupable is false, even if prev would otherwise group', () => { + const ts = new Date('2024-01-01T10:00:00Z'); + const prevTs = new Date('2024-01-01T09:59:50Z'); + const model = buildFakeModel({ ts, tmid: 't1', groupable: false }); + const { latest } = renderDerived(model, useMessageGrouping, { + prev: { ts: prevTs, username: 'alice', tmid: 't1' }, + config: { broadcast: false, Message_GroupingPeriod: 300 } + }); + expect(latest()).toBe(true); + }); + + it('useMessageGrouping returns true (header) for a removed message, even if prev would otherwise group', () => { + const ts = new Date('2024-01-01T10:00:00Z'); + const prevTs = new Date('2024-01-01T09:59:50Z'); + const model = buildFakeModel({ ts, tmid: 't1', t: 'rm' }); + const { latest } = renderDerived(model, useMessageGrouping, { + prev: { ts: prevTs, username: 'alice', tmid: 't1' }, + config: { broadcast: false, Message_GroupingPeriod: 300 } + }); + expect(latest()).toBe(true); + }); + + it('useMessageText returns the raw message when there is no auto-translation', () => { + const model = buildFakeModel(); + const { latest } = renderDerived(model, useMessageText); + expect(latest()).toEqual({ messageText: model.msg, isTranslated: false }); + }); + + it('useMessageText returns the translated message when auto-translate conditions are met', () => { + const model = buildFakeModel({ + autoTranslate: true, + translations: [{ _id: 't1', language: 'pt-BR', value: 'Olá mundo' }], + u: { _id: 'u2', username: 'bob' } + }); + const { latest } = renderDerived(model, useMessageText, { + config: { autoTranslateRoom: true, autoTranslateLanguage: 'pt-BR', user: { username: 'alice' } } + }); + expect(latest()).toEqual({ messageText: 'Olá mundo', isTranslated: true }); + }); + + it('useMessageMeta returns ts, unread, pinned and t', () => { + const ts = new Date('2024-01-01T10:00:00Z'); + const model = buildFakeModel({ ts, unread: true, pinned: true, t: 'room_changed_topic' }); + const { latest } = renderDerived(model, useMessageMeta); + expect(latest()).toEqual({ ts: model.ts, unread: model.unread, pinned: model.pinned, t: model.t }); + }); + }); + + describe('outside a MessageProvider', () => { + let consoleErrorSpy: jest.SpyInstance; + + beforeEach(() => { + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + }); + + it('useMessageCtx throws', () => { + const Probe = () => { + useMessageCtx(); + return null; + }; + expect(() => render(<Probe />)).toThrow('Message hooks must be used within a MessageProvider'); + }); + + it('useMessageField throws', () => { + const Probe = () => { + useReactions(); + return null; + }; + expect(() => render(<Probe />)).toThrow('Message hooks must be used within a MessageProvider'); + }); + }); +}); diff --git a/app/containers/message/MessageStore.tsx b/app/containers/message/MessageStore.tsx new file mode 100644 index 00000000000..16c958e94b0 --- /dev/null +++ b/app/containers/message/MessageStore.tsx @@ -0,0 +1,242 @@ +import { createContext, useContext, useEffect, useState, type ReactElement, type ReactNode } from 'react'; +import { createStore, useStore } from 'zustand'; +import { useShallow } from 'zustand/react/shallow'; + +import { type TAnyMessageModel } from '../../definitions'; +import MessageContext from './Context'; +import { getMessageTranslation } from './utils'; +import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; +import { messagesStatus } from '../../lib/constants/messagesStatus'; + +const createMessageStore = () => createStore(() => ({ tick: 0 })); + +type MessageStore = ReturnType<typeof createMessageStore>; + +export type MessagePrev = { + id?: string; + status?: number; + ts?: TAnyMessageModel['ts']; + username?: string; + groupable?: boolean; + tmid?: string; + t?: TAnyMessageModel['t']; +}; + +type MessageCtxValue = { store: MessageStore; item: TAnyMessageModel; prev?: MessagePrev }; + +const MessageStoreContext = createContext<MessageCtxValue | null>(null); + +export const useMessageCtx = (): MessageCtxValue => { + const ctx = useContext(MessageStoreContext); + if (!ctx) { + throw new Error('Message hooks must be used within a MessageProvider'); + } + return ctx; +}; + +// The selector must return a referentially stable value for an unchanged field (Object.is bail); +// for multiple fields use a focused domain hook with useShallow, never call this hook repeatedly. +// Stability of JSON fields depends on the model's @json(..., { memo: true }) decorators +// (app/lib/database/model/Message.js); a field that drops memo:true returns a fresh ref every +// access and would defeat both the Object.is and useShallow bail. +export const useMessageField = <T,>(selector: (item: TAnyMessageModel) => T): T => { + const { store, item } = useMessageCtx(); + return useStore(store, () => selector(item)); +}; + +export const MessageProvider = ({ + item, + prev, + children +}: { + item: TAnyMessageModel; + prev?: MessagePrev; + children: ReactNode; +}): ReactElement => { + 'use memo'; + + const [store] = useState(createMessageStore); + + useEffect(() => { + // Plain REST objects (no experimentalSubscribe) never emit again after the initial render. + // @ts-ignore: experimentalSubscribe is not yet in WatermelonDB's TS types + if (typeof item.experimentalSubscribe !== 'function') { + return undefined; + } + // @ts-ignore: experimentalSubscribe is not yet in WatermelonDB's TS types + return item.experimentalSubscribe(() => store.setState(s => ({ tick: s.tick + 1 }))); + }, [item, store]); + + return <MessageStoreContext.Provider value={{ store, item, prev }}>{children}</MessageStoreContext.Provider>; +}; + +export const useReactions = (): TAnyMessageModel['reactions'] => useMessageField(item => item.reactions); + +export const useUrls = (): TAnyMessageModel['urls'] => useMessageField(item => item.urls); + +export const useBlocks = (): Pick<TAnyMessageModel, 'blocks' | 'id'> => { + const { store, item } = useMessageCtx(); + return useStore( + store, + useShallow(() => ({ blocks: item.blocks, id: item.id })) + ); +}; + +export const useDiscussion = (): Pick<TAnyMessageModel, 'dcount' | 'dlm'> => { + const { store, item } = useMessageCtx(); + return useStore( + store, + useShallow(() => ({ dcount: item.dcount, dlm: item.dlm })) + ); +}; + +export const useThreadData = (): Pick<TAnyMessageModel, 'tcount' | 'tlm' | 'tmid' | 'id'> => { + const { store, item } = useMessageCtx(); + return useStore( + store, + useShallow(() => ({ tcount: item.tcount, tlm: item.tlm, tmid: item.tmid, id: item.id })) + ); +}; + +export const useRepliedThreadData = (): Pick<TAnyMessageModel, 'tmid' | 'tmsg' | 'id'> => { + const { store, item } = useMessageCtx(); + return useStore( + store, + useShallow(() => ({ tmid: item.tmid, tmsg: item.tmsg, id: item.id })) + ); +}; + +export const useMessageAuthor = (): Pick<TAnyMessageModel, 'u' | 'alias' | 'role'> => { + const { store, item } = useMessageCtx(); + return useStore( + store, + useShallow(() => ({ u: item.u, alias: item.alias, role: item.role })) + ); +}; + +export const useAvatar = (): Pick<TAnyMessageModel, 'avatar' | 'emoji'> => { + const { store, item } = useMessageCtx(); + return useStore( + store, + useShallow(() => ({ avatar: item.avatar, emoji: item.emoji })) + ); +}; + +export const useContentData = (): Pick<TAnyMessageModel, 'md' | 'mentions' | 'channels' | 'comment' | 'attachments' | 't'> => { + const { store, item } = useMessageCtx(); + return useStore( + store, + useShallow(() => ({ + md: item.md, + mentions: item.mentions, + channels: item.channels, + comment: item.comment, + attachments: item.attachments, + t: item.t + })) + ); +}; + +export const useMessageMeta = (): Pick<TAnyMessageModel, 'ts' | 'unread' | 'pinned' | 't'> => { + const { store, item } = useMessageCtx(); + return useStore( + store, + useShallow(() => ({ ts: item.ts, unread: item.unread, pinned: item.pinned, t: item.t })) + ); +}; + +const computeIsHeader = ( + prev: MessagePrev | undefined, + item: TAnyMessageModel, + broadcast: boolean, + Message_GroupingPeriod: number | undefined, + hasError: boolean +): boolean => { + if (hasError || (prev && prev.status === messagesStatus.ERROR)) { + return true; + } + try { + if ( + prev && + // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense + prev.ts.toDateString() === item.ts.toDateString() && + prev.username === item.u?.username && + !(prev.groupable === false || item.groupable === false || broadcast === true) && + // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense + item.ts - prev.ts < Message_GroupingPeriod * 1000 && + prev.tmid === item.tmid && + item.t !== 'rm' && + prev.t !== 'rm' + ) { + return false; + } + return true; + } catch { + return true; + } +}; + +export const useMessageGrouping = (): boolean => { + const { store, item, prev } = useMessageCtx(); + const { broadcast, Message_GroupingPeriod } = useContext(MessageContext); + return useStore(store, () => + computeIsHeader(prev, item, !!broadcast, Message_GroupingPeriod, item.status === messagesStatus.ERROR) + ); +}; + +export const useThreadPosition = (): { isThreadReply: boolean; isThreadSequential: boolean } => { + const { store, item, prev } = useMessageCtx(); + const { isThreadRoom } = useContext(MessageContext); + return useStore( + store, + useShallow(() => { + if (isThreadRoom) { + return { isThreadReply: false, isThreadSequential: false }; + } + const isThreadReply = !!(prev && item.tmid && prev.tmid !== item.tmid && prev.id !== item.tmid); + return { isThreadReply, isThreadSequential: !!item.tmid }; + }) + ); +}; + +export const useMessageStatus = (): { hasError: boolean; isTemp: boolean } => { + const { store, item } = useMessageCtx(); + return useStore( + store, + useShallow(() => ({ + hasError: item.status === messagesStatus.ERROR, + isTemp: item.status === messagesStatus.TEMP || item.status === messagesStatus.ERROR + })) + ); +}; + +export const useIsEncrypted = (): boolean => useMessageField(item => item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE); + +export const useIsInfo = (): string | boolean => + useMessageField(item => { + if (['e2e', 'discussion-created', 'jitsi_call_started', 'videoconf'].includes(item.t as string)) { + return false; + } + return item.t; + }); + +export const useIsEdited = (): boolean => useMessageField(item => (item.editedBy && !!item.editedBy.username) ?? false); + +export const useMessageText = (): { messageText: TAnyMessageModel['msg']; isTranslated: boolean } => { + const { store, item } = useMessageCtx(); + const { user, autoTranslateRoom, autoTranslateLanguage } = useContext(MessageContext); + return useStore( + store, + useShallow(() => { + let messageText = item.msg; + let isTranslated = false; + const otherUserMessage = item.u?.username !== user?.username; + if (autoTranslateRoom && item.autoTranslate && autoTranslateLanguage && otherUserMessage) { + const translated = getMessageTranslation(item, autoTranslateLanguage); + isTranslated = !!translated; + messageText = translated || messageText; + } + return { messageText, isTranslated }; + }) + ); +}; diff --git a/app/containers/message/Reactions.test.tsx b/app/containers/message/Reactions.test.tsx index 6007415c48f..91db9381329 100644 --- a/app/containers/message/Reactions.test.tsx +++ b/app/containers/message/Reactions.test.tsx @@ -1,12 +1,12 @@ -import { useState } from 'react'; import { Provider } from 'react-redux'; import { render, fireEvent } from '@testing-library/react-native'; import Reactions from './Reactions'; import MessageContext from './Context'; +import { MessageProvider } from './MessageStore'; import { setUser } from '../../actions/login'; import { mockedStore } from '../../reducers/mockedStore'; -import { type IReaction } from '../../definitions'; +import { type IReaction, type TAnyMessageModel } from '../../definitions'; const initialMockedStoreState = () => { mockedStore.dispatch( @@ -24,35 +24,60 @@ initialMockedStoreState(); const getCustomEmoji = jest.fn(); -const TestWrapper = ({ initialReactions }: { initialReactions: IReaction[] }) => { - const [reactions, setReactions] = useState<IReaction[]>(initialReactions); - const handleReactionPress = (emoji: string) => { - setReactions(prev => prev.filter(r => r.emoji !== emoji)); - }; - - return ( - <Provider store={mockedStore}> - <MessageContext.Provider - value={{ - user: { username: 'john' }, - reactionInit: jest.fn(), - onReactionPress: handleReactionPress, - onReactionLongPress: jest.fn(), - getCustomEmoji - }}> - <Reactions reactions={reactions} /> - </MessageContext.Provider> - </Provider> - ); +type FakeItem = TAnyMessageModel & { reactions: IReaction[]; _emit: () => void }; + +// Mirrors the MessageStore.test.tsx fake-model pattern: a stable item mutated in place, +// with experimentalSubscribe/_emit standing in for WatermelonDB's reactivity. +const buildItem = (reactions: IReaction[]): FakeItem => { + const subscribers: Array<() => void> = []; + return { + id: 'msg-1', + reactions, + experimentalSubscribe(cb: () => void) { + subscribers.push(cb); + return () => { + const idx = subscribers.indexOf(cb); + if (idx !== -1) subscribers.splice(idx, 1); + }; + }, + _emit() { + subscribers.forEach(cb => cb()); + } + } as unknown as FakeItem; +}; + +// onReactionPress is built outside the component (test scope) and mutates `item` in +// place, mirroring how a real WatermelonDB write would be observed via experimentalSubscribe. +const buildOnReactionPress = (item: FakeItem) => (emoji: string) => { + item.reactions = item.reactions.filter(r => r.emoji !== emoji); + item._emit(); }; +const TestWrapper = ({ item, onReactionPress }: { item: FakeItem; onReactionPress: (emoji: string) => void }) => ( + <Provider store={mockedStore}> + <MessageContext.Provider + value={{ + user: { username: 'john' }, + reactionInit: jest.fn(), + onReactionPress, + onReactionLongPress: jest.fn(), + getCustomEmoji + }}> + <MessageProvider item={item}> + <Reactions reactions={item.reactions} /> + </MessageProvider> + </MessageContext.Provider> + </Provider> +); + it('renders all reactions and AddReaction button', () => { const reactions: IReaction[] = [ { _id: '1', emoji: '👍', usernames: ['john', 'alice'], names: [] }, { _id: '2', emoji: '😂', usernames: ['bob'], names: [] } ]; + const item = buildItem(reactions); - const { getByTestId } = render(<TestWrapper initialReactions={reactions} />); + const { getByTestId } = render(<TestWrapper item={item} onReactionPress={buildOnReactionPress(item)} />); expect(getByTestId('message-reaction-👍')).toBeTruthy(); expect(getByTestId('message-reaction-😂')).toBeTruthy(); @@ -61,7 +86,9 @@ it('renders all reactions and AddReaction button', () => { it('should render unicode emoji reaction', () => { const reactions = [{ _id: '1', emoji: ':)', usernames: ['john', 'alice'], names: [] }]; - const { getByTestId } = render(<TestWrapper initialReactions={reactions} />); + const item = buildItem(reactions); + + const { getByTestId } = render(<TestWrapper item={item} onReactionPress={buildOnReactionPress(item)} />); expect(getByTestId('message-reaction-:)')).toBeTruthy(); expect(getByTestId('message-add-reaction')).toBeTruthy(); @@ -69,7 +96,9 @@ it('should render unicode emoji reaction', () => { it('should render custom emoji reaction', () => { const reactions = [{ _id: '1', emoji: ':aaaaa:', usernames: ['john', 'alice'], names: [] }]; - const { getByTestId } = render(<TestWrapper initialReactions={reactions} />); + const item = buildItem(reactions); + + const { getByTestId } = render(<TestWrapper item={item} onReactionPress={buildOnReactionPress(item)} />); expect(getByTestId('message-reaction-:aaaaa:')).toBeTruthy(); expect(getByTestId('message-add-reaction')).toBeTruthy(); @@ -80,7 +109,9 @@ it('should remove reaction', () => { { _id: '1', emoji: ':thumbsup:', usernames: ['john'], names: [] }, { _id: '1', emoji: ':heart_eyes:', usernames: ['john', 'alice'], names: [] } ]; - const { getByTestId, queryByTestId } = render(<TestWrapper initialReactions={reactions} />); + const item = buildItem(reactions); + + const { getByTestId, queryByTestId } = render(<TestWrapper item={item} onReactionPress={buildOnReactionPress(item)} />); expect(getByTestId('message-reaction-:thumbsup:')).toBeTruthy(); expect(getByTestId('message-reaction-:heart_eyes:')).toBeTruthy(); diff --git a/app/containers/message/Reactions.tsx b/app/containers/message/Reactions.tsx index 36d691e540e..be3db0c7ee4 100644 --- a/app/containers/message/Reactions.tsx +++ b/app/containers/message/Reactions.tsx @@ -10,6 +10,7 @@ import { BUTTON_HIT_SLOP } from './utils'; import { themes } from '../../lib/constants/colors'; import { type TSupportedThemes, useTheme } from '../../theme'; import MessageContext from './Context'; +import { useReactions } from './MessageStore'; interface IReaction { _id: string; @@ -85,10 +86,11 @@ const Reaction = ({ reaction, theme }: IMessageReaction) => { ); }; -const Reactions = ({ reactions }: IMessageReactions) => { +const Reactions = (_props: IMessageReactions) => { 'use memo'; const { theme } = useTheme(); + const reactions = useReactions(); if (!Array.isArray(reactions) || reactions.length === 0) { return null; diff --git a/app/containers/message/RepliedThread.tsx b/app/containers/message/RepliedThread.tsx index 8ab82a58f79..1a3fe1aae8f 100644 --- a/app/containers/message/RepliedThread.tsx +++ b/app/containers/message/RepliedThread.tsx @@ -9,12 +9,15 @@ import { type IMessageRepliedThread } from './interfaces'; import { useTheme } from '../../theme'; import { AvatarContainer } from './MessageAvatar'; import MessageContext from './Context'; +import { useIsEncrypted, useRepliedThreadData } from './MessageStore'; -const RepliedThread = ({ tmid, tmsg, isHeader, id, isEncrypted }: IMessageRepliedThread) => { +const RepliedThread = ({ isHeader }: IMessageRepliedThread) => { 'use memo'; const { colors } = useTheme(); const { fetchThreadName } = useContext(MessageContext); + const { tmid, tmsg, id } = useRepliedThreadData(); + const isEncrypted = useIsEncrypted(); const [msg, setMsg] = useState(isEncrypted ? I18n.t('Encrypted_message') : tmsg); useEffect(() => { diff --git a/app/containers/message/Thread.test.tsx b/app/containers/message/Thread.test.tsx index c49160438e8..280cd8a9ff1 100644 --- a/app/containers/message/Thread.test.tsx +++ b/app/containers/message/Thread.test.tsx @@ -2,6 +2,8 @@ import { render } from '@testing-library/react-native'; import Thread from './Thread'; import MessageContext from './Context'; +import { MessageProvider } from './MessageStore'; +import { type TAnyMessageModel } from '../../definitions'; const baseContextValue = { threadBadgeColor: undefined, @@ -11,28 +13,33 @@ const baseContextValue = { onThreadPress: jest.fn() }; -const renderThread = (props: Parameters<typeof Thread>[0]) => +const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel => + ({ id: 'msg1', msg: 'hello', tcount: 3, tlm: undefined, tmid: undefined, ...overrides } as unknown as TAnyMessageModel); + +const renderThread = (item: TAnyMessageModel, props: Parameters<typeof Thread>[0]) => render( <MessageContext.Provider value={baseContextValue}> - <Thread {...props} /> + <MessageProvider item={item}> + <Thread {...props} /> + </MessageProvider> </MessageContext.Provider> ); describe('Thread — tlm-only update regression', () => { test('renders null when tlm is undefined, then shows button after tlm arrives (same tcount)', () => { - const { queryByTestId, rerender, getByTestId } = renderThread({ - msg: 'hello', - tcount: 3, - tlm: undefined, - isThreadRoom: false, - id: 'msg1' + const item = buildItem(); + const { queryByTestId, rerender, getByTestId } = renderThread(item, { + isThreadRoom: false }); expect(queryByTestId('message-thread-button-hello')).toBeNull(); + const updatedItem = buildItem({ tlm: new Date('2024-01-01T00:00:00Z') }); rerender( <MessageContext.Provider value={baseContextValue}> - <Thread msg='hello' tcount={3} tlm={new Date('2024-01-01T00:00:00Z')} isThreadRoom={false} id='msg1' /> + <MessageProvider item={updatedItem}> + <Thread isThreadRoom={false} /> + </MessageProvider> </MessageContext.Provider> ); diff --git a/app/containers/message/Thread.tsx b/app/containers/message/Thread.tsx index 6efd29b88d2..6570303ac0b 100644 --- a/app/containers/message/Thread.tsx +++ b/app/containers/message/Thread.tsx @@ -8,12 +8,15 @@ import I18n from '../../i18n'; import { type IMessageThread } from './interfaces'; import { useTheme } from '../../theme'; import Touchable from './Touchable'; +import { useMessageText, useThreadData } from './MessageStore'; -const Thread = ({ msg, tcount, tlm, isThreadRoom, id }: IMessageThread) => { +const Thread = ({ isThreadRoom }: IMessageThread) => { 'use memo'; const { theme, colors } = useTheme(); const { threadBadgeColor, toggleFollowThread, user, replies, onThreadPress } = useContext(MessageContext); + const { tcount, tlm, id } = useThreadData(); + const { messageText } = useMessageText(); const backgroundColor = threadBadgeColor ? colors.badgeBackgroundLevel2 : colors.buttonBackgroundSecondaryDefault; const textColor = threadBadgeColor || theme !== 'light' ? colors.fontWhite : colors.fontPureBlack; @@ -29,7 +32,7 @@ const Thread = ({ msg, tcount, tlm, isThreadRoom, id }: IMessageThread) => { accessibilityRole='button' accessibilityLabel={I18n.t('View_Thread')} style={[styles.button, { backgroundColor }]} - testID={`message-thread-button-${msg}`}> + testID={`message-thread-button-${messageText}`}> <Text style={[styles.buttonText, { color: textColor }]}>{I18n.t('View_Thread')}</Text> </Touchable> <ThreadDetails diff --git a/app/containers/message/Time.tsx b/app/containers/message/Time.tsx index 73256665efb..6bc7033a84f 100644 --- a/app/containers/message/Time.tsx +++ b/app/containers/message/Time.tsx @@ -3,6 +3,7 @@ import { Text, type TextStyle } from 'react-native'; import dayjs from '../../lib/dayjs'; import { useTheme } from '../../theme'; import messageStyles from './styles'; +import { useMessageField } from './MessageStore'; interface IMessageTime { ts?: Date; @@ -10,10 +11,11 @@ interface IMessageTime { style?: TextStyle; } -const MessageTime = ({ timeFormat, ts, style }: IMessageTime) => { +const MessageTime = ({ timeFormat, style }: IMessageTime) => { 'use memo'; const { colors } = useTheme(); + const ts = useMessageField(item => item.ts); const time = dayjs(ts).format(timeFormat); diff --git a/app/containers/message/Urls.tsx b/app/containers/message/Urls.tsx index 4a9422eb2de..bdbf412d05c 100644 --- a/app/containers/message/Urls.tsx +++ b/app/containers/message/Urls.tsx @@ -15,6 +15,7 @@ import I18n from '../../i18n'; import MessageContext from './Context'; import { type IUrl } from '../../definitions'; import { WidthAwareContext } from './Components/WidthAwareView'; +import { useUrls } from './MessageStore'; const styles = StyleSheet.create({ container: { @@ -184,9 +185,11 @@ const Url = ({ url }: { url: IUrl }) => { </Touchable> ); }; -const Urls = ({ urls }: { urls?: IUrl[] }): ReactElement[] | null => { +const Urls = (_props: { urls?: IUrl[] }): ReactElement[] | null => { 'use memo'; + const urls = useUrls(); + if (!urls || urls.length === 0) { return null; } diff --git a/app/containers/message/User.tsx b/app/containers/message/User.tsx index 99de3cf6dd0..c873ce30910 100644 --- a/app/containers/message/User.tsx +++ b/app/containers/message/User.tsx @@ -1,7 +1,7 @@ import { useContext } from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; -import { type MessageType, type MessageTypesValues, SubscriptionType } from '../../definitions'; +import { type MessageTypesValues, SubscriptionType } from '../../definitions'; import { useTheme } from '../../theme'; import sharedStyles from '../../views/Styles'; import RightIcons from './Components/RightIcons'; @@ -9,6 +9,14 @@ import MessageContext from './Context'; import { messageHaveAuthorName } from './utils'; import MessageTime from './Time'; import { useResponsiveLayout } from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; +import { + useIsEdited, + useMessageAuthor, + useMessageGrouping, + useMessageMeta, + useMessageStatus, + useMessageText +} from './MessageStore'; const styles = StyleSheet.create({ container: { @@ -40,45 +48,23 @@ const styles = StyleSheet.create({ }); interface IMessageUser { - isHeader?: boolean; - hasError: boolean; useRealName?: boolean; - author?: { - _id: string; - name?: string; - username?: string; - }; - alias?: string; - ts?: Date | string; timeFormat?: string; - type: MessageType; - isEdited: boolean; isReadReceiptEnabled?: boolean; - unread?: boolean; - pinned?: boolean; - isTranslated: boolean; } -const User = ({ - isHeader, - useRealName, - author, - alias, - ts, - timeFormat, - hasError, - type, - isEdited, - isTranslated, - isReadReceiptEnabled, - unread, - pinned -}: IMessageUser) => { +const User = ({ useRealName, timeFormat, isReadReceiptEnabled }: IMessageUser) => { 'use memo'; const { user, navToRoomInfo } = useContext(MessageContext); const { colors } = useTheme(); const { isLargeFontScale } = useResponsiveLayout(); + const isHeader = useMessageGrouping(); + const { u: author, alias } = useMessageAuthor(); + const { hasError } = useMessageStatus(); + const isEdited = useIsEdited(); + const { ts, unread, pinned, t: type } = useMessageMeta(); + const { isTranslated } = useMessageText(); if (isHeader) { const username = (useRealName && author?.name) || author?.username; diff --git a/app/containers/message/__snapshots__/Message.test.tsx.snap b/app/containers/message/__snapshots__/Message.test.tsx.snap index a05b7035932..10590e7b688 100644 --- a/app/containers/message/__snapshots__/Message.test.tsx.snap +++ b/app/containers/message/__snapshots__/Message.test.tsx.snap @@ -101,10 +101,11 @@ exports[`Story Snapshots: Archived should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-1" > <A11yIndexView accessibilityLabel="This message is inside an archived room" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -389,6 +390,7 @@ exports[`Story Snapshots: Archived should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This message is inside an archived room" > <View @@ -563,10 +565,11 @@ exports[`Story Snapshots: ArchivedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-2" > <A11yIndexView accessibilityLabel="This message is inside an archived room" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -851,6 +854,7 @@ exports[`Story Snapshots: ArchivedLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This message is inside an archived room" > <View @@ -1033,10 +1037,11 @@ exports[`Story Snapshots: AttachmentWithTextAndLink should match snapshot 1`] = "width": "100%", } } - testID="message-undefined" + testID="message-story-message-3" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -1804,10 +1809,11 @@ exports[`Story Snapshots: AttachmentWithTextAndLinkLargeFont should match snapsh "width": "100%", } } - testID="message-undefined" + testID="message-story-message-4" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -2575,10 +2581,11 @@ exports[`Story Snapshots: Avatar should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-5" > <A11yIndexView accessibilityLabel="This message has no avatar." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -2863,6 +2870,7 @@ exports[`Story Snapshots: Avatar should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This message has no avatar." > <View @@ -3032,10 +3040,11 @@ exports[`Story Snapshots: Avatar should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-6" > <A11yIndexView accessibilityLabel="This message uses the user's profile picture as the avatar." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -3320,6 +3329,7 @@ exports[`Story Snapshots: Avatar should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This message uses the user's profile picture as the avatar." > <View @@ -3489,10 +3499,11 @@ exports[`Story Snapshots: Avatar should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-7" > <A11yIndexView accessibilityLabel="This message uses a Unicode emoji as the avatar." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -3769,6 +3780,7 @@ exports[`Story Snapshots: Avatar should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This message uses a Unicode emoji as the avatar." > <View @@ -3938,10 +3950,11 @@ exports[`Story Snapshots: Avatar should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-8" > <A11yIndexView accessibilityLabel="This message uses a custom animated emoji as the avatar." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -4234,6 +4247,7 @@ exports[`Story Snapshots: Avatar should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This message uses a custom animated emoji as the avatar." > <View @@ -4403,10 +4417,11 @@ exports[`Story Snapshots: Avatar should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-9" > <A11yIndexView accessibilityLabel="This message uses a custom static emoji as the avatar." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -4699,6 +4714,7 @@ exports[`Story Snapshots: Avatar should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This message uses a custom static emoji as the avatar." > <View @@ -4881,10 +4897,11 @@ exports[`Story Snapshots: Basic should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-10" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -5169,6 +5186,7 @@ exports[`Story Snapshots: Basic should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message" > <View @@ -5338,10 +5356,11 @@ exports[`Story Snapshots: Basic should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-11" > <A11yIndexView accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -5626,6 +5645,7 @@ exports[`Story Snapshots: Basic should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." > <View @@ -5808,10 +5828,11 @@ exports[`Story Snapshots: BasicLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-12" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -6096,6 +6117,7 @@ exports[`Story Snapshots: BasicLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message" > <View @@ -6265,10 +6287,11 @@ exports[`Story Snapshots: BasicLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-13" > <A11yIndexView accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -6553,6 +6576,7 @@ exports[`Story Snapshots: BasicLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." > <View @@ -6735,10 +6759,11 @@ exports[`Story Snapshots: BlockQuote should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-14" > <A11yIndexView accessibilityLabel="> Testing block quote" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -7023,6 +7048,7 @@ exports[`Story Snapshots: BlockQuote should match snapshot 1`] = ` } > <View + style={false} testID="message-content-> Testing block quote" > <View @@ -7224,11 +7250,12 @@ Testing block quote." "width": "100%", } } - testID="message-undefined" + testID="message-story-message-15" > <A11yIndexView accessibilityLabel="> Testing block quote Testing block quote" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -7513,6 +7540,7 @@ Testing block quote" } > <View + style={false} testID="message-content-> Testing block quote Testing block quote" > @@ -7774,10 +7802,11 @@ exports[`Story Snapshots: BlockQuoteLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-16" > <A11yIndexView accessibilityLabel="> Testing block quote" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -8062,6 +8091,7 @@ exports[`Story Snapshots: BlockQuoteLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-> Testing block quote" > <View @@ -8263,11 +8293,12 @@ Testing block quote." "width": "100%", } } - testID="message-undefined" + testID="message-story-message-17" > <A11yIndexView accessibilityLabel="> Testing block quote Testing block quote" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -8552,6 +8583,7 @@ Testing block quote" } > <View + style={false} testID="message-content-> Testing block quote Testing block quote" > @@ -8813,10 +8845,11 @@ exports[`Story Snapshots: Broadcast should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-18" > <A11yIndexView accessibilityLabel="Broadcasted message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -9101,6 +9134,7 @@ exports[`Story Snapshots: Broadcast should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Broadcasted message" > <View @@ -9403,10 +9437,11 @@ exports[`Story Snapshots: BroadcastLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-19" > <A11yIndexView accessibilityLabel="Broadcasted message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -9691,6 +9726,7 @@ exports[`Story Snapshots: BroadcastLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Broadcasted message" > <View @@ -9993,10 +10029,11 @@ exports[`Story Snapshots: CollapsedAttachments should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-20" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -10281,6 +10318,7 @@ exports[`Story Snapshots: CollapsedAttachments should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message" > <View @@ -10602,10 +10640,11 @@ exports[`Story Snapshots: CollapsedAttachments should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-21" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -11244,6 +11283,7 @@ exports[`Story Snapshots: CollapsedAttachments should match snapshot 1`] = ` </View> </View> <View + style={false} testID="message-content-Message" > <View @@ -11426,10 +11466,11 @@ exports[`Story Snapshots: CollapsedAttachmentsLargeFont should match snapshot 1` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-22" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -11714,6 +11755,7 @@ exports[`Story Snapshots: CollapsedAttachmentsLargeFont should match snapshot 1` } > <View + style={false} testID="message-content-Message" > <View @@ -12035,10 +12077,11 @@ exports[`Story Snapshots: CollapsedAttachmentsLargeFont should match snapshot 1` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-23" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -12677,6 +12720,7 @@ exports[`Story Snapshots: CollapsedAttachmentsLargeFont should match snapshot 1` </View> </View> <View + style={false} testID="message-content-Message" > <View @@ -12859,10 +12903,11 @@ exports[`Story Snapshots: CollapsibleAttachmentWithText should match snapshot 1` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-24" > <A11yIndexView accessibilityLabel="This is the main message body." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -13147,6 +13192,7 @@ exports[`Story Snapshots: CollapsibleAttachmentWithText should match snapshot 1` } > <View + style={false} testID="message-content-This is the main message body." > <View @@ -13470,10 +13516,11 @@ exports[`Story Snapshots: CollapsibleAttachmentWithText should match snapshot 1` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-25" > <A11yIndexView accessibilityLabel="This is the main message body." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -14253,6 +14300,7 @@ exports[`Story Snapshots: CollapsibleAttachmentWithText should match snapshot 1` </View> </View> <View + style={false} testID="message-content-This is the main message body." > <View @@ -14435,10 +14483,11 @@ exports[`Story Snapshots: CollapsibleAttachmentWithTextLargeFont should match sn "width": "100%", } } - testID="message-undefined" + testID="message-story-message-26" > <A11yIndexView accessibilityLabel="This is the main message body." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -14723,6 +14772,7 @@ exports[`Story Snapshots: CollapsibleAttachmentWithTextLargeFont should match sn } > <View + style={false} testID="message-content-This is the main message body." > <View @@ -15046,10 +15096,11 @@ exports[`Story Snapshots: CollapsibleAttachmentWithTextLargeFont should match sn "width": "100%", } } - testID="message-undefined" + testID="message-story-message-27" > <A11yIndexView accessibilityLabel="This is the main message body." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -15829,6 +15880,7 @@ exports[`Story Snapshots: CollapsibleAttachmentWithTextLargeFont should match sn </View> </View> <View + style={false} testID="message-content-This is the main message body." > <View @@ -16011,10 +16063,11 @@ exports[`Story Snapshots: ColoredAttachments should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-28" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -17243,10 +17296,11 @@ exports[`Story Snapshots: ColoredAttachmentsLargeFont should match snapshot 1`] "width": "100%", } } - testID="message-undefined" + testID="message-story-message-29" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -18475,10 +18529,11 @@ exports[`Story Snapshots: CustomFields should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-30" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -19118,6 +19173,7 @@ exports[`Story Snapshots: CustomFields should match snapshot 1`] = ` </View> </View> <View + style={false} testID="message-content-Message" > <View @@ -19287,10 +19343,11 @@ exports[`Story Snapshots: CustomFields should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-31" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -20188,6 +20245,7 @@ exports[`Story Snapshots: CustomFields should match snapshot 1`] = ` </View> </View> <View + style={false} testID="message-content-Message" > <View @@ -20370,10 +20428,11 @@ exports[`Story Snapshots: CustomFieldsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-32" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -21013,6 +21072,7 @@ exports[`Story Snapshots: CustomFieldsLargeFont should match snapshot 1`] = ` </View> </View> <View + style={false} testID="message-content-Message" > <View @@ -21182,10 +21242,11 @@ exports[`Story Snapshots: CustomFieldsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-33" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -22083,6 +22144,7 @@ exports[`Story Snapshots: CustomFieldsLargeFont should match snapshot 1`] = ` </View> </View> <View + style={false} testID="message-content-Message" > <View @@ -22265,10 +22327,11 @@ exports[`Story Snapshots: DateAndUnreadSeparators should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-34" > <A11yIndexView accessibilityLabel="Fourth message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -22553,6 +22616,7 @@ exports[`Story Snapshots: DateAndUnreadSeparators should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Fourth message" > <View @@ -22784,10 +22848,11 @@ exports[`Story Snapshots: DateAndUnreadSeparators should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-35" > <A11yIndexView accessibilityLabel="Third message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -23072,6 +23137,7 @@ exports[`Story Snapshots: DateAndUnreadSeparators should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Third message" > <View @@ -23285,10 +23351,11 @@ exports[`Story Snapshots: DateAndUnreadSeparators should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-36" > <A11yIndexView accessibilityLabel="Second message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -23333,6 +23400,7 @@ exports[`Story Snapshots: DateAndUnreadSeparators should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Second message" > <View @@ -23509,10 +23577,11 @@ exports[`Story Snapshots: DateAndUnreadSeparators should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-38" > <A11yIndexView accessibilityLabel="Second message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -23797,6 +23866,7 @@ exports[`Story Snapshots: DateAndUnreadSeparators should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Second message" > <View @@ -24023,10 +24093,11 @@ exports[`Story Snapshots: DateAndUnreadSeparators should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-39" > <A11yIndexView accessibilityLabel="First message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -24311,6 +24382,7 @@ exports[`Story Snapshots: DateAndUnreadSeparators should match snapshot 1`] = ` } > <View + style={false} testID="message-content-First message" > <View @@ -24493,10 +24565,11 @@ exports[`Story Snapshots: DateAndUnreadSeparatorsLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-40" > <A11yIndexView accessibilityLabel="Fourth message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -24781,6 +24854,7 @@ exports[`Story Snapshots: DateAndUnreadSeparatorsLargeFont should match snapshot } > <View + style={false} testID="message-content-Fourth message" > <View @@ -25012,10 +25086,11 @@ exports[`Story Snapshots: DateAndUnreadSeparatorsLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-41" > <A11yIndexView accessibilityLabel="Third message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -25300,6 +25375,7 @@ exports[`Story Snapshots: DateAndUnreadSeparatorsLargeFont should match snapshot } > <View + style={false} testID="message-content-Third message" > <View @@ -25513,10 +25589,11 @@ exports[`Story Snapshots: DateAndUnreadSeparatorsLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-42" > <A11yIndexView accessibilityLabel="Second message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -25561,6 +25638,7 @@ exports[`Story Snapshots: DateAndUnreadSeparatorsLargeFont should match snapshot } > <View + style={false} testID="message-content-Second message" > <View @@ -25737,10 +25815,11 @@ exports[`Story Snapshots: DateAndUnreadSeparatorsLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-44" > <A11yIndexView accessibilityLabel="Second message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -26025,6 +26104,7 @@ exports[`Story Snapshots: DateAndUnreadSeparatorsLargeFont should match snapshot } > <View + style={false} testID="message-content-Second message" > <View @@ -26251,10 +26331,11 @@ exports[`Story Snapshots: DateAndUnreadSeparatorsLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-45" > <A11yIndexView accessibilityLabel="First message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -26539,6 +26620,7 @@ exports[`Story Snapshots: DateAndUnreadSeparatorsLargeFont should match snapshot } > <View + style={false} testID="message-content-First message" > <View @@ -26721,10 +26803,11 @@ exports[`Story Snapshots: Discussion should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-46" > <A11yIndexView accessibilityLabel="This is a discussion" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -27299,10 +27382,11 @@ exports[`Story Snapshots: Discussion should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-47" > <A11yIndexView accessibilityLabel="This is a discussion" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -27879,10 +27963,11 @@ exports[`Story Snapshots: Discussion should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-48" > <A11yIndexView accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -28459,10 +28544,11 @@ exports[`Story Snapshots: Discussion should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-49" > <A11yIndexView accessibilityLabel="This is a discussion" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -29052,10 +29138,11 @@ exports[`Story Snapshots: DiscussionLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-50" > <A11yIndexView accessibilityLabel="This is a discussion" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -29630,10 +29717,11 @@ exports[`Story Snapshots: DiscussionLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-51" > <A11yIndexView accessibilityLabel="This is a discussion" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -30210,10 +30298,11 @@ exports[`Story Snapshots: DiscussionLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-52" > <A11yIndexView accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -30790,10 +30879,11 @@ exports[`Story Snapshots: DiscussionLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-53" > <A11yIndexView accessibilityLabel="This is a discussion" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -31383,10 +31473,11 @@ exports[`Story Snapshots: Edited should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-54" > <A11yIndexView accessibilityLabel="Message header" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -31707,6 +31798,7 @@ exports[`Story Snapshots: Edited should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message header" > <View @@ -31876,10 +31968,11 @@ exports[`Story Snapshots: Edited should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-55" > <A11yIndexView accessibilityLabel="Message without header" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -31924,6 +32017,7 @@ exports[`Story Snapshots: Edited should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message without header" > <View @@ -32149,10 +32243,11 @@ exports[`Story Snapshots: EditedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-57" > <A11yIndexView accessibilityLabel="Message header" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -32473,6 +32568,7 @@ exports[`Story Snapshots: EditedLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message header" > <View @@ -32642,10 +32738,11 @@ exports[`Story Snapshots: EditedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-58" > <A11yIndexView accessibilityLabel="Message without header" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -32690,6 +32787,7 @@ exports[`Story Snapshots: EditedLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message without header" > <View @@ -32901,7 +32999,7 @@ exports[`Story Snapshots: Editing should match snapshot 1`] = ` "opacity": 1, } } - testID="message-editing-undefined" + testID="message-editing-editing-message" > <View style={{}} @@ -32916,10 +33014,11 @@ exports[`Story Snapshots: Editing should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-editing-message" > <A11yIndexView accessibilityLabel="Message being edited" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -33204,6 +33303,7 @@ exports[`Story Snapshots: Editing should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message being edited" > <View @@ -33372,7 +33472,7 @@ exports[`Story Snapshots: EditingLargeFont should match snapshot 1`] = ` "opacity": 1, } } - testID="message-editing-undefined" + testID="message-editing-editing-message" > <View style={{}} @@ -33387,10 +33487,11 @@ exports[`Story Snapshots: EditingLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-editing-message" > <A11yIndexView accessibilityLabel="Message being edited" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -33675,6 +33776,7 @@ exports[`Story Snapshots: EditingLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message being edited" > <View @@ -33857,10 +33959,11 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-62" > <A11yIndexView accessibilityLabel="👊🤙👏" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -34145,6 +34248,7 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` } > <View + style={false} testID="message-content-👊🤙👏" > <View @@ -34313,10 +34417,11 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-63" > <A11yIndexView accessibilityLabel="👏" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -34601,6 +34706,7 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` } > <View + style={false} testID="message-content-👏" > <View @@ -34750,10 +34856,11 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-64" > <A11yIndexView accessibilityLabel=":react_rocket: :nyan_rocket: :marioparty:" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -35038,6 +35145,7 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` } > <View + style={false} testID="message-content-:react_rocket: :nyan_rocket: :marioparty:" > <View @@ -35300,10 +35408,11 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-65" > <A11yIndexView accessibilityLabel=":react_rocket:" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -35588,6 +35697,7 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` } > <View + style={false} testID="message-content-:react_rocket:" > <View @@ -35762,10 +35872,11 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-66" > <A11yIndexView accessibilityLabel="🤙:react_rocket:" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -36050,6 +36161,7 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` } > <View + style={false} testID="message-content-🤙:react_rocket:" > <View @@ -36243,10 +36355,11 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-67" > <A11yIndexView accessibilityLabel="🤙:react_rocket:🤙🤙" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -36531,6 +36644,7 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` } > <View + style={false} testID="message-content-🤙:react_rocket:🤙🤙" > <View @@ -36770,10 +36884,11 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-68" > <A11yIndexView accessibilityLabel="👊🤙👏" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -37058,6 +37173,7 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-👊🤙👏" > <View @@ -37226,10 +37342,11 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-69" > <A11yIndexView accessibilityLabel="👏" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -37514,6 +37631,7 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-👏" > <View @@ -37663,10 +37781,11 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-70" > <A11yIndexView accessibilityLabel=":react_rocket: :nyan_rocket: :marioparty:" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -37951,6 +38070,7 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-:react_rocket: :nyan_rocket: :marioparty:" > <View @@ -38213,10 +38333,11 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-71" > <A11yIndexView accessibilityLabel=":react_rocket:" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -38501,6 +38622,7 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-:react_rocket:" > <View @@ -38675,10 +38797,11 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-72" > <A11yIndexView accessibilityLabel="🤙:react_rocket:" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -38963,6 +39086,7 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-🤙:react_rocket:" > <View @@ -39156,10 +39280,11 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-73" > <A11yIndexView accessibilityLabel="🤙:react_rocket:🤙🤙" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -39444,6 +39569,7 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-🤙:react_rocket:🤙🤙" > <View @@ -39623,7 +39749,7 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM Message." + accessibilityLabel="diego.mello 10:00:00 AM Encrypted message. Encrypted message" accessibilityRole="button" accessibilityState={ { @@ -39683,10 +39809,11 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-74" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -40043,63 +40170,29 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Message" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Message - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -40152,7 +40245,7 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM Message Encrypted without Header." + accessibilityLabel="diego.mello 10:00:00 AM Encrypted message. Encrypted message" accessibilityRole="button" accessibilityState={ { @@ -40212,10 +40305,11 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-75" > <A11yIndexView accessibilityLabel="Message Encrypted without Header" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -40260,63 +40354,29 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message Encrypted without Header" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Message Encrypted without Header" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Message Encrypted without Header - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -40448,7 +40508,7 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM Message Encrypted with Reactions." + accessibilityLabel="diego.mello 10:00:00 AM Encrypted message. Encrypted message" accessibilityRole="button" accessibilityState={ { @@ -40508,10 +40568,11 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-77" > <A11yIndexView accessibilityLabel="Message Encrypted with Reactions" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -40868,63 +40929,29 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message Encrypted with Reactions" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Message Encrypted with Reactions" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Message Encrypted with Reactions - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> <View style={ @@ -41418,7 +41445,7 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message Thread reply encrypted." + accessibilityLabel="diego.mello 10:00:00 AM Encrypted message. Encrypted message" accessibilityRole="button" accessibilityState={ { @@ -41492,7 +41519,7 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` "gap": 10, } } - testID="message-thread-replied-on-Thread with emoji :) :joy:" + testID="message-thread-replied-on-Encrypted message" > <View style={ @@ -41530,7 +41557,7 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` </Text> </View> <Text - accessibilityLabel="Thread with emoji :) 😂" + accessibilityLabel="Encrypted message" numberOfLines={1} style={ [ @@ -41559,9 +41586,9 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` }, ] } - testID="markdown-preview-Thread with emoji :) 😂" + testID="markdown-preview-Encrypted message" > - Thread with emoji :) 😂 + Encrypted message </Text> <View style={ @@ -41759,6 +41786,7 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="Thread reply encrypted" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -41778,11 +41806,11 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Thread reply encrypted" > <Text - accessibilityLabel="Thread reply encrypted" - numberOfLines={1} + accessibilityLabel="Encrypted message" style={ [ { @@ -41790,18 +41818,16 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` "fontFamily": "Inter", "fontSize": 16, "fontWeight": "400", - "lineHeight": 22, "textAlign": "left", }, { - "color": "#2F343D", - "lineHeight": undefined, + "color": "#6C727A", }, ] } - testID="message-preview-Thread reply encrypted" + testID="message-encrypted" > - Thread reply encrypted + Encrypted message </Text> </View> </View> @@ -41845,7 +41871,7 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` } > <View - accessibilityLabel="diego.mello 10:00:00 AM Temp message encrypted." + accessibilityLabel="diego.mello 10:00:00 AM Encrypted message. Encrypted message" accessibilityRole="button" accessibilityState={ { @@ -41905,10 +41931,11 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-80" > <A11yIndexView accessibilityLabel="Temp message encrypted" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -42272,61 +42299,26 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` } testID="message-content-Temp message encrypted" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Temp message encrypted" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Temp message encrypted - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -42379,7 +42371,7 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM Message Edited encrypted." + accessibilityLabel="diego.mello 10:00:00 AM Encrypted message. Encrypted message" accessibilityRole="button" accessibilityState={ { @@ -42439,10 +42431,11 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-81" > <A11yIndexView accessibilityLabel="Message Edited encrypted" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -42799,63 +42792,29 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message Edited encrypted" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Message Edited encrypted" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Message Edited encrypted - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -42881,10 +42840,11 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-82" > <A11yIndexView accessibilityLabel="This message has error and is encrypted" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -43312,63 +43272,33 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` } > <View + style={ + { + "opacity": 0.3, + } + } testID="message-content-This message has error and is encrypted" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="This message has error and is encrypted" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - This message has error and is encrypted - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -43417,7 +43347,7 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM Read Receipt encrypted with Header. Message was read" + accessibilityLabel="diego.mello 10:00:00 AM Encrypted message. Encrypted message Message was read" accessibilityRole="button" accessibilityState={ { @@ -43477,10 +43407,11 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-83" > <A11yIndexView accessibilityLabel="Read Receipt encrypted with Header" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -43867,63 +43798,29 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Read Receipt encrypted with Header" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Read Receipt encrypted with Header" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Read Receipt encrypted with Header - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -43976,7 +43873,7 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM Read Receipt encrypted without Header. Message was read" + accessibilityLabel="diego.mello 10:00:00 AM Encrypted message. Encrypted message Message was read" accessibilityRole="button" accessibilityState={ { @@ -44036,10 +43933,11 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-84" > <A11yIndexView accessibilityLabel="Read Receipt encrypted without Header" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -44084,63 +43982,29 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Read Receipt encrypted without Header" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Read Receipt encrypted without Header" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Read Receipt encrypted without Header - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -44315,7 +44179,7 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM Message." + accessibilityLabel="diego.mello 10:00:00 AM Encrypted message. Encrypted message" accessibilityRole="button" accessibilityState={ { @@ -44375,10 +44239,11 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-86" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -44735,63 +44600,29 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Message" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Message - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -44844,7 +44675,7 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM Message Encrypted without Header." + accessibilityLabel="diego.mello 10:00:00 AM Encrypted message. Encrypted message" accessibilityRole="button" accessibilityState={ { @@ -44904,10 +44735,11 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-87" > <A11yIndexView accessibilityLabel="Message Encrypted without Header" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -44952,63 +44784,29 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message Encrypted without Header" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Message Encrypted without Header" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Message Encrypted without Header - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -45140,7 +44938,7 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM Message Encrypted with Reactions." + accessibilityLabel="diego.mello 10:00:00 AM Encrypted message. Encrypted message" accessibilityRole="button" accessibilityState={ { @@ -45200,10 +44998,11 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-89" > <A11yIndexView accessibilityLabel="Message Encrypted with Reactions" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -45560,63 +45359,29 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message Encrypted with Reactions" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Message Encrypted with Reactions" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Message Encrypted with Reactions - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> <View style={ @@ -46110,7 +45875,7 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message Thread reply encrypted." + accessibilityLabel="diego.mello 10:00:00 AM Encrypted message. Encrypted message" accessibilityRole="button" accessibilityState={ { @@ -46184,7 +45949,7 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` "gap": 10, } } - testID="message-thread-replied-on-Thread with emoji :) :joy:" + testID="message-thread-replied-on-Encrypted message" > <View style={ @@ -46222,7 +45987,7 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` </Text> </View> <Text - accessibilityLabel="Thread with emoji :) 😂" + accessibilityLabel="Encrypted message" numberOfLines={1} style={ [ @@ -46251,9 +46016,9 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` }, ] } - testID="markdown-preview-Thread with emoji :) 😂" + testID="markdown-preview-Encrypted message" > - Thread with emoji :) 😂 + Encrypted message </Text> <View style={ @@ -46451,6 +46216,7 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="Thread reply encrypted" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -46470,11 +46236,11 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Thread reply encrypted" > <Text - accessibilityLabel="Thread reply encrypted" - numberOfLines={1} + accessibilityLabel="Encrypted message" style={ [ { @@ -46482,18 +46248,16 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` "fontFamily": "Inter", "fontSize": 16, "fontWeight": "400", - "lineHeight": 22, "textAlign": "left", }, { - "color": "#2F343D", - "lineHeight": undefined, + "color": "#6C727A", }, ] } - testID="message-preview-Thread reply encrypted" + testID="message-encrypted" > - Thread reply encrypted + Encrypted message </Text> </View> </View> @@ -46537,7 +46301,7 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` } > <View - accessibilityLabel="diego.mello 10:00:00 AM Temp message encrypted." + accessibilityLabel="diego.mello 10:00:00 AM Encrypted message. Encrypted message" accessibilityRole="button" accessibilityState={ { @@ -46597,10 +46361,11 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-92" > <A11yIndexView accessibilityLabel="Temp message encrypted" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -46964,61 +46729,26 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` } testID="message-content-Temp message encrypted" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Temp message encrypted" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Temp message encrypted - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -47071,7 +46801,7 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM Message Edited encrypted." + accessibilityLabel="diego.mello 10:00:00 AM Encrypted message. Encrypted message" accessibilityRole="button" accessibilityState={ { @@ -47131,10 +46861,11 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-93" > <A11yIndexView accessibilityLabel="Message Edited encrypted" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -47491,63 +47222,29 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message Edited encrypted" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Message Edited encrypted" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Message Edited encrypted - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -47573,10 +47270,11 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-94" > <A11yIndexView accessibilityLabel="This message has error and is encrypted" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -48004,63 +47702,33 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` } > <View + style={ + { + "opacity": 0.3, + } + } testID="message-content-This message has error and is encrypted" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="This message has error and is encrypted" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - This message has error and is encrypted - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -48109,7 +47777,7 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM Read Receipt encrypted with Header. Message was read" + accessibilityLabel="diego.mello 10:00:00 AM Encrypted message. Encrypted message Message was read" accessibilityRole="button" accessibilityState={ { @@ -48169,10 +47837,11 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-95" > <A11yIndexView accessibilityLabel="Read Receipt encrypted with Header" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -48559,63 +48228,29 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Read Receipt encrypted with Header" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Read Receipt encrypted with Header" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Read Receipt encrypted with Header - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -48668,7 +48303,7 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM Read Receipt encrypted without Header. Message was read" + accessibilityLabel="diego.mello 10:00:00 AM Encrypted message. Encrypted message Message was read" accessibilityRole="button" accessibilityState={ { @@ -48728,10 +48363,11 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-96" > <A11yIndexView accessibilityLabel="Read Receipt encrypted without Header" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -48776,63 +48412,29 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Read Receipt encrypted without Header" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Read Receipt encrypted without Header" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Read Receipt encrypted without Header - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -48980,10 +48582,11 @@ exports[`Story Snapshots: Error should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-98" > <A11yIndexView accessibilityLabel="This message has error" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -49340,6 +48943,11 @@ exports[`Story Snapshots: Error should match snapshot 1`] = ` } > <View + style={ + { + "opacity": 0.3, + } + } testID="message-content-This message has error" > <View @@ -49418,10 +49026,11 @@ exports[`Story Snapshots: Error should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-99" > <A11yIndexView accessibilityLabel="This message has error too" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -49441,7 +49050,147 @@ exports[`Story Snapshots: Error should match snapshot 1`] = ` "width": 36, } } - /> + > + <View + accessible={true} + style={ + [ + { + "borderRadius": 4, + "height": 36, + "width": 36, + }, + { + "marginTop": 4, + }, + ] + } + testID="avatar" + > + <ExternalKeyboardView + canBeFocused={true} + enableA11yFocus={false} + enableContextMenu={false} + group={false} + haloEffect={true} + hasKeyDownPress={false} + hasKeyUpPress={true} + hasOnFocusChanged={true} + lockFocus={0} + onContextMenuPress={[Function]} + onFocusChange={[Function]} + onKeyUpPress={[Function]} + orderIndex={-1} + screenAutoA11yFocusDelay={300} + style={ + [ + undefined, + undefined, + ] + } + > + <RNGestureHandlerButton + activeOpacity={1} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={-1} + handlerType="NativeViewGestureHandler" + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[Function]} + rippleColor="#E4E7EA" + style={ + [ + { + "backgroundColor": undefined, + "borderRadius": undefined, + "margin": undefined, + "marginBottom": undefined, + "marginEnd": undefined, + "marginHorizontal": undefined, + "marginLeft": undefined, + "marginRight": undefined, + "marginStart": undefined, + "marginTop": undefined, + "marginVertical": undefined, + }, + { + "cursor": undefined, + }, + ] + } + underlayColor="#E4E7EA" + > + <View + collapsable={false} + style={ + { + "backgroundColor": "#E4E7EA", + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "bottom": 0, + "left": 0, + "opacity": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + /> + <View + accessibilityLabel="diego.mello's avatar" + accessible={true} + style={{}} + > + <ViewManagerAdapter_ExpoImage + borderRadius={4} + containerViewRef={{"current":"Object"}} + contentFit="cover" + contentPosition={ + { + "left": "50%", + "top": "50%", + } + } + height={36} + nativeViewRef={{"current":"NativeComponent"}} + onError={[Function]} + onLoad={[Function]} + onLoadStart={[Function]} + onProgress={[Function]} + placeholder={[]} + priority="high" + source={ + [ + { + "headers": { + "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", + }, + "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=72", + }, + ] + } + style={ + { + "borderRadius": 4, + "height": 36, + "width": 36, + } + } + transition={null} + width={36} + /> + </View> + </RNGestureHandlerButton> + </ExternalKeyboardView> + </View> + </View> <View style={ { @@ -49458,6 +49207,178 @@ exports[`Story Snapshots: Error should match snapshot 1`] = ` } } > + <View + style={ + { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + "justifyContent": "space-between", + } + } + > + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, + } + } + testID="username-header-diego.mello" + > + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#1F2329", + }, + ] + } + > + diego.mello + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + undefined, + ] + } + > + 10:00 AM + </Text> + </View> + <View + style={ + { + "flexDirection": "row", + } + } + > + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "paddingLeft": 5, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#EC0D2A", + "fontSize": 16, + }, + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </View> + </View> <View style={ { @@ -49466,6 +49387,11 @@ exports[`Story Snapshots: Error should match snapshot 1`] = ` } > <View + style={ + { + "opacity": 0.3, + } + } testID="message-content-This message has error too" > <View @@ -49527,85 +49453,6 @@ exports[`Story Snapshots: Error should match snapshot 1`] = ` </View> </View> </View> - <View - style={ - { - "flexDirection": "row", - } - } - > - <View - accessibilityState={ - { - "busy": undefined, - "checked": undefined, - "disabled": undefined, - "expanded": undefined, - "selected": undefined, - } - } - accessibilityValue={ - { - "max": undefined, - "min": undefined, - "now": undefined, - "text": undefined, - } - } - accessible={true} - collapsable={false} - focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - onResponderGrant={[Function]} - onResponderMove={[Function]} - onResponderRelease={[Function]} - onResponderTerminate={[Function]} - onResponderTerminationRequest={[Function]} - onStartShouldSetResponder={[Function]} - style={ - { - "paddingLeft": 5, - } - } - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#EC0D2A", - "fontSize": 16, - }, - [ - { - "lineHeight": 16, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> - </View> - </View> </View> </A11yIndexView> </View> @@ -49636,10 +49483,11 @@ exports[`Story Snapshots: ErrorLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-101" > <A11yIndexView accessibilityLabel="This message has error" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -49996,6 +49844,11 @@ exports[`Story Snapshots: ErrorLargeFont should match snapshot 1`] = ` } > <View + style={ + { + "opacity": 0.3, + } + } testID="message-content-This message has error" > <View @@ -50074,10 +49927,11 @@ exports[`Story Snapshots: ErrorLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-102" > <A11yIndexView accessibilityLabel="This message has error too" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -50097,7 +49951,147 @@ exports[`Story Snapshots: ErrorLargeFont should match snapshot 1`] = ` "width": 46.800000000000004, } } - /> + > + <View + accessible={true} + style={ + [ + { + "borderRadius": 4, + "height": 46.800000000000004, + "width": 46.800000000000004, + }, + { + "marginTop": 4, + }, + ] + } + testID="avatar" + > + <ExternalKeyboardView + canBeFocused={true} + enableA11yFocus={false} + enableContextMenu={false} + group={false} + haloEffect={true} + hasKeyDownPress={false} + hasKeyUpPress={true} + hasOnFocusChanged={true} + lockFocus={0} + onContextMenuPress={[Function]} + onFocusChange={[Function]} + onKeyUpPress={[Function]} + orderIndex={-1} + screenAutoA11yFocusDelay={300} + style={ + [ + undefined, + undefined, + ] + } + > + <RNGestureHandlerButton + activeOpacity={1} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={-1} + handlerType="NativeViewGestureHandler" + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[Function]} + rippleColor="#E4E7EA" + style={ + [ + { + "backgroundColor": undefined, + "borderRadius": undefined, + "margin": undefined, + "marginBottom": undefined, + "marginEnd": undefined, + "marginHorizontal": undefined, + "marginLeft": undefined, + "marginRight": undefined, + "marginStart": undefined, + "marginTop": undefined, + "marginVertical": undefined, + }, + { + "cursor": undefined, + }, + ] + } + underlayColor="#E4E7EA" + > + <View + collapsable={false} + style={ + { + "backgroundColor": "#E4E7EA", + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "bottom": 0, + "left": 0, + "opacity": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + /> + <View + accessibilityLabel="diego.mello's avatar" + accessible={true} + style={{}} + > + <ViewManagerAdapter_ExpoImage + borderRadius={4} + containerViewRef={{"current":"Object"}} + contentFit="cover" + contentPosition={ + { + "left": "50%", + "top": "50%", + } + } + height={46.800000000000004} + nativeViewRef={{"current":"NativeComponent"}} + onError={[Function]} + onLoad={[Function]} + onLoadStart={[Function]} + onProgress={[Function]} + placeholder={[]} + priority="high" + source={ + [ + { + "headers": { + "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", + }, + "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=93.60000000000001", + }, + ] + } + style={ + { + "borderRadius": 4, + "height": 46.800000000000004, + "width": 46.800000000000004, + } + } + transition={null} + width={46.800000000000004} + /> + </View> + </RNGestureHandlerButton> + </ExternalKeyboardView> + </View> + </View> <View style={ { @@ -50114,6 +50108,178 @@ exports[`Story Snapshots: ErrorLargeFont should match snapshot 1`] = ` } } > + <View + style={ + { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + "justifyContent": "space-between", + } + } + > + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, + } + } + testID="username-header-diego.mello" + > + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#1F2329", + }, + ] + } + > + diego.mello + </Text> + </View> + <View + style={ + { + "flexDirection": "row", + } + } + > + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "paddingLeft": 5, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#EC0D2A", + "fontSize": 20.8, + }, + [ + { + "lineHeight": 20.8, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </View> + </View> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + undefined, + ] + } + > + 10:00 AM + </Text> <View style={ { @@ -50122,6 +50288,11 @@ exports[`Story Snapshots: ErrorLargeFont should match snapshot 1`] = ` } > <View + style={ + { + "opacity": 0.3, + } + } testID="message-content-This message has error too" > <View @@ -50183,85 +50354,6 @@ exports[`Story Snapshots: ErrorLargeFont should match snapshot 1`] = ` </View> </View> </View> - <View - style={ - { - "flexDirection": "row", - } - } - > - <View - accessibilityState={ - { - "busy": undefined, - "checked": undefined, - "disabled": undefined, - "expanded": undefined, - "selected": undefined, - } - } - accessibilityValue={ - { - "max": undefined, - "min": undefined, - "now": undefined, - "text": undefined, - } - } - accessible={true} - collapsable={false} - focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - onResponderGrant={[Function]} - onResponderMove={[Function]} - onResponderRelease={[Function]} - onResponderTerminate={[Function]} - onResponderTerminationRequest={[Function]} - onStartShouldSetResponder={[Function]} - style={ - { - "paddingLeft": 5, - } - } - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#EC0D2A", - "fontSize": 20.8, - }, - [ - { - "lineHeight": 20.8, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> - </View> - </View> </View> </A11yIndexView> </View> @@ -50379,10 +50471,11 @@ exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] "width": "100%", } } - testID="message-undefined" + testID="message-story-message-104" > <A11yIndexView accessibilityLabel="Check out this Python file" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -50806,6 +50899,7 @@ exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] </View> </View> <View + style={false} testID="message-content-Check out this Python file" > <View @@ -50975,10 +51069,11 @@ exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] "width": "100%", } } - testID="message-undefined" + testID="message-story-message-105" > <A11yIndexView accessibilityLabel="TypeScript component" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -51402,6 +51497,7 @@ exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] </View> </View> <View + style={false} testID="message-content-TypeScript component" > <View @@ -51571,10 +51667,11 @@ exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] "width": "100%", } } - testID="message-undefined" + testID="message-story-message-106" > <A11yIndexView accessibilityLabel="Configuration file" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -51998,6 +52095,7 @@ exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] </View> </View> <View + style={false} testID="message-content-Configuration file" > <View @@ -52167,10 +52265,11 @@ exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] "width": "100%", } } - testID="message-undefined" + testID="message-story-message-107" > <A11yIndexView accessibilityLabel="File with description" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -52671,6 +52770,7 @@ exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] </View> </View> <View + style={false} testID="message-content-File with description" > <View @@ -52840,10 +52940,11 @@ exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] "width": "100%", } } - testID="message-undefined" + testID="message-story-message-108" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -53288,10 +53389,11 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna "width": "100%", } } - testID="message-undefined" + testID="message-story-message-110" > <A11yIndexView accessibilityLabel="Check out this Python file" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -53715,6 +53817,7 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna </View> </View> <View + style={false} testID="message-content-Check out this Python file" > <View @@ -53884,10 +53987,11 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna "width": "100%", } } - testID="message-undefined" + testID="message-story-message-111" > <A11yIndexView accessibilityLabel="TypeScript component" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -54311,6 +54415,7 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna </View> </View> <View + style={false} testID="message-content-TypeScript component" > <View @@ -54480,10 +54585,11 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna "width": "100%", } } - testID="message-undefined" + testID="message-story-message-112" > <A11yIndexView accessibilityLabel="Configuration file" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -54907,6 +55013,7 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna </View> </View> <View + style={false} testID="message-content-Configuration file" > <View @@ -55076,10 +55183,11 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna "width": "100%", } } - testID="message-undefined" + testID="message-story-message-113" > <A11yIndexView accessibilityLabel="File with description" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -55580,6 +55688,7 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna </View> </View> <View + style={false} testID="message-content-File with description" > <View @@ -55749,10 +55858,11 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna "width": "100%", } } - testID="message-undefined" + testID="message-story-message-114" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -56184,10 +56294,11 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna "width": "100%", } } - testID="message-undefined" + testID="message-story-message-116" > <A11yIndexView accessibilityLabel="multi file" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -56766,6 +56877,7 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna </View> </View> <View + style={false} testID="message-content-multi file" > <View @@ -56948,10 +57060,11 @@ exports[`Story Snapshots: FullName should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-117" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -57236,6 +57349,7 @@ exports[`Story Snapshots: FullName should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message" > <View @@ -57418,10 +57532,11 @@ exports[`Story Snapshots: FullNameLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-118" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -57706,6 +57821,7 @@ exports[`Story Snapshots: FullNameLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message" > <View @@ -57888,10 +58004,11 @@ exports[`Story Snapshots: GroupedMessages should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-119" > <A11yIndexView accessibilityLabel="..." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -58176,6 +58293,7 @@ exports[`Story Snapshots: GroupedMessages should match snapshot 1`] = ` } > <View + style={false} testID="message-content-..." > <View @@ -58345,10 +58463,11 @@ exports[`Story Snapshots: GroupedMessages should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-120" > <A11yIndexView accessibilityLabel="Different user" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -58633,6 +58752,7 @@ exports[`Story Snapshots: GroupedMessages should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Different user" > <View @@ -58802,10 +58922,11 @@ exports[`Story Snapshots: GroupedMessages should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-121" > <A11yIndexView accessibilityLabel="This is the third message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -58850,6 +58971,7 @@ exports[`Story Snapshots: GroupedMessages should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This is the third message" > <View @@ -59026,10 +59148,11 @@ exports[`Story Snapshots: GroupedMessages should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-123" > <A11yIndexView accessibilityLabel="This is the second message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -59074,6 +59197,7 @@ exports[`Story Snapshots: GroupedMessages should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This is the second message" > <View @@ -59250,10 +59374,11 @@ exports[`Story Snapshots: GroupedMessages should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-125" > <A11yIndexView accessibilityLabel="This is the first message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -59538,6 +59663,7 @@ exports[`Story Snapshots: GroupedMessages should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This is the first message" > <View @@ -59720,10 +59846,11 @@ exports[`Story Snapshots: GroupedMessagesLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-126" > <A11yIndexView accessibilityLabel="..." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -60008,6 +60135,7 @@ exports[`Story Snapshots: GroupedMessagesLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-..." > <View @@ -60177,10 +60305,11 @@ exports[`Story Snapshots: GroupedMessagesLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-127" > <A11yIndexView accessibilityLabel="Different user" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -60465,6 +60594,7 @@ exports[`Story Snapshots: GroupedMessagesLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Different user" > <View @@ -60634,10 +60764,11 @@ exports[`Story Snapshots: GroupedMessagesLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-128" > <A11yIndexView accessibilityLabel="This is the third message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -60682,6 +60813,7 @@ exports[`Story Snapshots: GroupedMessagesLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This is the third message" > <View @@ -60858,10 +60990,11 @@ exports[`Story Snapshots: GroupedMessagesLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-130" > <A11yIndexView accessibilityLabel="This is the second message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -60906,6 +61039,7 @@ exports[`Story Snapshots: GroupedMessagesLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This is the second message" > <View @@ -61082,10 +61216,11 @@ exports[`Story Snapshots: GroupedMessagesLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-132" > <A11yIndexView accessibilityLabel="This is the first message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -61370,6 +61505,7 @@ exports[`Story Snapshots: GroupedMessagesLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This is the first message" > <View @@ -61716,6 +61852,7 @@ exports[`Story Snapshots: Ignored should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -61735,6 +61872,7 @@ exports[`Story Snapshots: Ignored should match snapshot 1`] = ` } > <View + style={false} testID="message-content-" > <Text @@ -62043,6 +62181,7 @@ exports[`Story Snapshots: IgnoredLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -62062,6 +62201,7 @@ exports[`Story Snapshots: IgnoredLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-" > <Text @@ -62206,10 +62346,11 @@ exports[`Story Snapshots: InlineKatex should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-135" > <A11yIndexView accessibilityLabel="\\(xˆ2 + yˆ2 - zˆ2\\)" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -62494,6 +62635,7 @@ exports[`Story Snapshots: InlineKatex should match snapshot 1`] = ` } > <View + style={false} testID="message-content-\\(xˆ2 + yˆ2 - zˆ2\\)" > <View @@ -62662,10 +62804,11 @@ exports[`Story Snapshots: InlineKatexLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-136" > <A11yIndexView accessibilityLabel="\\(xˆ2 + yˆ2 - zˆ2\\)" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -62950,6 +63093,7 @@ exports[`Story Snapshots: InlineKatexLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-\\(xˆ2 + yˆ2 - zˆ2\\)" > <View @@ -63118,10 +63262,11 @@ exports[`Story Snapshots: Katex should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-137" > <A11yIndexView accessibilityLabel="\\[ \\color{black} \\colorbox{white}{ \\boxed{ \\begin{matrix} \\;\\;\\;\\; \\overlinesegment {\\underlinesegment{ \\Huge Test\\; Test \\; in \\; Test} } & \\\\ \\;\\;\\textit{develop}\\end{matrix} } }\\]" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -63406,6 +63551,7 @@ exports[`Story Snapshots: Katex should match snapshot 1`] = ` } > <View + style={false} testID="message-content-\\[ \\color{black} \\colorbox{white}{ \\boxed{ \\begin{matrix} \\;\\;\\;\\; \\overlinesegment {\\underlinesegment{ \\Huge Test\\; Test \\; in \\; Test} } & \\\\ \\;\\;\\textit{develop}\\end{matrix} } }\\]" > <View @@ -63542,10 +63688,11 @@ exports[`Story Snapshots: KatexArray should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-138" > <A11yIndexView accessibilityLabel="\\begin{array}{|c|c|c|c|c|c|} \\hline \\text{...} & \\text{...} \\\\ \\hline \\end{array}" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -63830,6 +63977,7 @@ exports[`Story Snapshots: KatexArray should match snapshot 1`] = ` } > <View + style={false} testID="message-content-\\begin{array}{|c|c|c|c|c|c|} \\hline \\text{...} & \\text{...} \\\\ \\hline \\end{array}" > <View @@ -63966,10 +64114,11 @@ exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-139" > <A11yIndexView accessibilityLabel="\\begin{array}{|c|c|c|c|c|c|} \\hline \\text{...} & \\text{...} \\\\ \\hline \\end{array}" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -64254,6 +64403,7 @@ exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-\\begin{array}{|c|c|c|c|c|c|} \\hline \\text{...} & \\text{...} \\\\ \\hline \\end{array}" > <View @@ -64390,10 +64540,11 @@ exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-140" > <A11yIndexView accessibilityLabel="\\[ \\color{black} \\colorbox{white}{ \\boxed{ \\begin{matrix} \\;\\;\\;\\; \\overlinesegment {\\underlinesegment{ \\Huge Test\\; Test \\; in \\; Test} } & \\\\ \\;\\;\\textit{develop}\\end{matrix} } }\\]" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -64678,6 +64829,7 @@ exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-\\[ \\color{black} \\colorbox{white}{ \\boxed{ \\begin{matrix} \\;\\;\\;\\; \\overlinesegment {\\underlinesegment{ \\Huge Test\\; Test \\; in \\; Test} } & \\\\ \\;\\;\\textit{develop}\\end{matrix} } }\\]" > <View @@ -64816,12 +64968,13 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-141" > <A11yIndexView accessibilityLabel="* Dogs * cats - cats" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -65106,6 +65259,7 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` } > <View + style={false} testID="message-content-* Dogs * cats - cats" @@ -65454,11 +65608,12 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-142" > <A11yIndexView accessibilityLabel="1. Dogs 2. Cats" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -65743,6 +65898,7 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` } > <View + style={false} testID="message-content-1. Dogs 2. Cats" > @@ -66042,10 +66198,11 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-143" > <A11yIndexView accessibilityLabel="1. Dogs" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -66330,6 +66487,7 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` } > <View + style={false} testID="message-content-1. Dogs" > <View @@ -66541,10 +66699,11 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-144" > <A11yIndexView accessibilityLabel="2. Cats" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -66589,6 +66748,7 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` } > <View + style={false} testID="message-content-2. Cats" > <View @@ -66822,12 +66982,13 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-146" > <A11yIndexView accessibilityLabel="* Dogs * cats - cats" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -67112,6 +67273,7 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-* Dogs * cats - cats" @@ -67460,11 +67622,12 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-147" > <A11yIndexView accessibilityLabel="1. Dogs 2. Cats" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -67749,6 +67912,7 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-1. Dogs 2. Cats" > @@ -68048,10 +68212,11 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-148" > <A11yIndexView accessibilityLabel="1. Dogs" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -68336,6 +68501,7 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-1. Dogs" > <View @@ -68547,10 +68713,11 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-149" > <A11yIndexView accessibilityLabel="2. Cats" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -68595,6 +68762,7 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-2. Cats" > <View @@ -68826,10 +68994,11 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-151" > <A11yIndexView accessibilityLabel="this is a normal message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -69114,6 +69283,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` } > <View + style={false} testID="message-content-this is a normal message" > <View @@ -69283,10 +69453,11 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-152" > <A11yIndexView accessibilityLabel="Edited message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -69607,6 +69778,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Edited message" > <View @@ -69776,10 +69948,11 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-153" > <A11yIndexView accessibilityLabel="Translated message" + accessibilityLanguage="en" accessible={true} importantForAccessibility="yes" orderFocusType={0} @@ -70100,6 +70273,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Translated message" > <View @@ -70209,7 +70383,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` }, ] } - accessibilityLabel="johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername 10:00:00 AM Encrypted message." + accessibilityLabel="johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername 10:00:00 AM Encrypted message. Encrypted message" accessibilityRole="button" accessibilityState={ { @@ -70269,10 +70443,11 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-154" > <A11yIndexView accessibilityLabel="Encrypted message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -70629,63 +70804,29 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Encrypted message" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Encrypted message" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Encrypted message - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -70711,10 +70852,11 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-155" > <A11yIndexView accessibilityLabel="Error message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -71071,6 +71213,11 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` } > <View + style={ + { + "opacity": 0.3, + } + } testID="message-content-Error message" > <View @@ -71236,10 +71383,11 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-156" > <A11yIndexView accessibilityLabel="Message with read receipt" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -71555,6 +71703,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message with read receipt" > <View @@ -71664,7 +71813,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` }, ] } - accessibilityLabel="johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername 10:00:00 AM Message with read receipt. Message was read" + accessibilityLabel="johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername 10:00:00 AM Encrypted message. Encrypted message Message was read" accessibilityRole="button" accessibilityState={ { @@ -71724,10 +71873,11 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-157" > <A11yIndexView accessibilityLabel="Message with read receipt" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -72114,63 +72264,29 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message with read receipt" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Message with read receipt" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Message with read receipt - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -72196,10 +72312,11 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-158" > <A11yIndexView accessibilityLabel="Show all icons " + accessibilityLanguage="en" accessible={true} importantForAccessibility="yes" orderFocusType={0} @@ -72727,63 +72844,33 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` } > <View + style={ + { + "opacity": 0.3, + } + } testID="message-content-Show all icons " > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Show all icons " - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Show all icons - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -72805,10 +72892,11 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-159" > <A11yIndexView accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + accessibilityLanguage="en" accessible={true} importantForAccessibility="yes" orderFocusType={0} @@ -72829,7 +72917,147 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": 36, } } - /> + > + <View + accessible={true} + style={ + [ + { + "borderRadius": 4, + "height": 36, + "width": 36, + }, + { + "marginTop": 4, + }, + ] + } + testID="avatar" + > + <ExternalKeyboardView + canBeFocused={true} + enableA11yFocus={false} + enableContextMenu={false} + group={false} + haloEffect={true} + hasKeyDownPress={false} + hasKeyUpPress={true} + hasOnFocusChanged={true} + lockFocus={0} + onContextMenuPress={[Function]} + onFocusChange={[Function]} + onKeyUpPress={[Function]} + orderIndex={-1} + screenAutoA11yFocusDelay={300} + style={ + [ + undefined, + undefined, + ] + } + > + <RNGestureHandlerButton + activeOpacity={1} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={-1} + handlerType="NativeViewGestureHandler" + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[Function]} + rippleColor="#E4E7EA" + style={ + [ + { + "backgroundColor": undefined, + "borderRadius": undefined, + "margin": undefined, + "marginBottom": undefined, + "marginEnd": undefined, + "marginHorizontal": undefined, + "marginLeft": undefined, + "marginRight": undefined, + "marginStart": undefined, + "marginTop": undefined, + "marginVertical": undefined, + }, + { + "cursor": undefined, + }, + ] + } + underlayColor="#E4E7EA" + > + <View + collapsable={false} + style={ + { + "backgroundColor": "#E4E7EA", + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "bottom": 0, + "left": 0, + "opacity": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + /> + <View + accessibilityLabel="johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername's avatar" + accessible={true} + style={{}} + > + <ViewManagerAdapter_ExpoImage + borderRadius={4} + containerViewRef={{"current":"Object"}} + contentFit="cover" + contentPosition={ + { + "left": "50%", + "top": "50%", + } + } + height={36} + nativeViewRef={{"current":"NativeComponent"}} + onError={[Function]} + onLoad={[Function]} + onLoadStart={[Function]} + onProgress={[Function]} + placeholder={[]} + priority="high" + source={ + [ + { + "headers": { + "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", + }, + "uri": "https://open.rocket.chat/avatar/johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername?format=png&size=72", + }, + ] + } + style={ + { + "borderRadius": 4, + "height": 36, + "width": 36, + } + } + transition={null} + width={36} + /> + </View> + </RNGestureHandlerButton> + </ExternalKeyboardView> + </View> + </View> <View style={ { @@ -72849,320 +73077,383 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` <View style={ { - "gap": 4, + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + "justifyContent": "space-between", } } > <View - testID="message-content-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, + } + } + testID="username-header-johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername" + > + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#1F2329", + }, + ] + } + > + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + undefined, + ] + } + > + 10:00 AM + </Text> + </View> + <View + style={ + { + "flexDirection": "row", + } + } > <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} style={ { - "gap": 2, + "paddingLeft": 5, } } > <Text + allowFontScaling={false} + selectable={false} style={ [ { - "backgroundColor": "transparent", - "fontFamily": "Inter", + "color": "#2F343D", "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", }, + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "paddingLeft": 5, + } + } + testID="undefined-edited" + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ { "color": "#2F343D", + "fontSize": 16, + }, + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", }, + {}, ] } > - <Text - style={ +  + </Text> + </View> + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "paddingLeft": 5, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - </Text> - </Text> + "color": "#EC0D2A", + "fontSize": 16, + }, + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  </Text> </View> + <View + style={ + { + "paddingLeft": 5, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 16, + }, + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#095AD2", + "fontSize": 25, + }, + [ + { + "lineHeight": 25, + }, + { + "marginTop": -5, + "paddingLeft": 5, + }, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + testID="read-receipt-read" + > +  + </Text> </View> </View> - </View> - </View> - <View - style={ - { - "flexDirection": "row", - } - } - > - <View - accessibilityState={ - { - "busy": undefined, - "checked": undefined, - "disabled": undefined, - "expanded": undefined, - "selected": undefined, - } - } - accessibilityValue={ - { - "max": undefined, - "min": undefined, - "now": undefined, - "text": undefined, - } - } - accessible={true} - collapsable={false} - focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - onResponderGrant={[Function]} - onResponderMove={[Function]} - onResponderRelease={[Function]} - onResponderTerminate={[Function]} - onResponderTerminationRequest={[Function]} - onStartShouldSetResponder={[Function]} - style={ - { - "paddingLeft": 5, - } - } - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#2F343D", - "fontSize": 16, - }, - [ - { - "lineHeight": 16, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> - </View> - <View - style={ - { - "paddingLeft": 5, - } - } - testID="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.-edited" - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#2F343D", - "fontSize": 16, - }, - [ - { - "lineHeight": 16, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> - </View> - <View - accessibilityState={ - { - "busy": undefined, - "checked": undefined, - "disabled": undefined, - "expanded": undefined, - "selected": undefined, - } - } - accessibilityValue={ - { - "max": undefined, - "min": undefined, - "now": undefined, - "text": undefined, - } - } - accessible={true} - collapsable={false} - focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - onResponderGrant={[Function]} - onResponderMove={[Function]} - onResponderRelease={[Function]} - onResponderTerminate={[Function]} - onResponderTerminationRequest={[Function]} - onStartShouldSetResponder={[Function]} - style={ - { - "paddingLeft": 5, - } - } - > - <Text - allowFontScaling={false} - selectable={false} + <View style={ - [ - { - "color": "#EC0D2A", - "fontSize": 16, - }, - [ - { - "lineHeight": 16, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] + { + "gap": 4, + } } > -  - </Text> - </View> - <View - style={ - { - "paddingLeft": 5, - } - } - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#2F343D", - "fontSize": 16, - }, - [ - { - "lineHeight": 16, - }, - undefined, - ], + <View + style={ { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> + "opacity": 0.3, + } + } + testID="message-content-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + > + <Text + accessibilityLabel="Encrypted message" + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + testID="message-encrypted" + > + Encrypted message + </Text> + </View> + </View> </View> - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#095AD2", - "fontSize": 25, - }, - [ - { - "lineHeight": 25, - }, - { - "marginTop": -5, - "paddingLeft": 5, - }, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - testID="read-receipt-read" - > -  - </Text> </View> </View> </A11yIndexView> @@ -73181,10 +73472,11 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-161" > <A11yIndexView accessibilityLabel="small message" + accessibilityLanguage="en" accessible={true} importantForAccessibility="yes" orderFocusType={0} @@ -73205,7 +73497,147 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": 36, } } - /> + > + <View + accessible={true} + style={ + [ + { + "borderRadius": 4, + "height": 36, + "width": 36, + }, + { + "marginTop": 4, + }, + ] + } + testID="avatar" + > + <ExternalKeyboardView + canBeFocused={true} + enableA11yFocus={false} + enableContextMenu={false} + group={false} + haloEffect={true} + hasKeyDownPress={false} + hasKeyUpPress={true} + hasOnFocusChanged={true} + lockFocus={0} + onContextMenuPress={[Function]} + onFocusChange={[Function]} + onKeyUpPress={[Function]} + orderIndex={-1} + screenAutoA11yFocusDelay={300} + style={ + [ + undefined, + undefined, + ] + } + > + <RNGestureHandlerButton + activeOpacity={1} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={-1} + handlerType="NativeViewGestureHandler" + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[Function]} + rippleColor="#E4E7EA" + style={ + [ + { + "backgroundColor": undefined, + "borderRadius": undefined, + "margin": undefined, + "marginBottom": undefined, + "marginEnd": undefined, + "marginHorizontal": undefined, + "marginLeft": undefined, + "marginRight": undefined, + "marginStart": undefined, + "marginTop": undefined, + "marginVertical": undefined, + }, + { + "cursor": undefined, + }, + ] + } + underlayColor="#E4E7EA" + > + <View + collapsable={false} + style={ + { + "backgroundColor": "#E4E7EA", + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "bottom": 0, + "left": 0, + "opacity": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + /> + <View + accessibilityLabel="johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername's avatar" + accessible={true} + style={{}} + > + <ViewManagerAdapter_ExpoImage + borderRadius={4} + containerViewRef={{"current":"Object"}} + contentFit="cover" + contentPosition={ + { + "left": "50%", + "top": "50%", + } + } + height={36} + nativeViewRef={{"current":"NativeComponent"}} + onError={[Function]} + onLoad={[Function]} + onLoadStart={[Function]} + onProgress={[Function]} + placeholder={[]} + priority="high" + source={ + [ + { + "headers": { + "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", + }, + "uri": "https://open.rocket.chat/avatar/johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername?format=png&size=72", + }, + ] + } + style={ + { + "borderRadius": 4, + "height": 36, + "width": 36, + } + } + transition={null} + width={36} + /> + </View> + </RNGestureHandlerButton> + </ExternalKeyboardView> + </View> + </View> <View style={ { @@ -73225,320 +73657,383 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` <View style={ { - "gap": 4, + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + "justifyContent": "space-between", } } > <View - testID="message-content-small message" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, + } + } + testID="username-header-johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername" + > + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#1F2329", + }, + ] + } + > + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + undefined, + ] + } + > + 10:00 AM + </Text> + </View> + <View + style={ + { + "flexDirection": "row", + } + } > <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} style={ { - "gap": 2, + "paddingLeft": 5, } } > <Text + allowFontScaling={false} + selectable={false} style={ [ { - "backgroundColor": "transparent", - "fontFamily": "Inter", + "color": "#2F343D", "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", }, + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "paddingLeft": 5, + } + } + testID="undefined-edited" + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ { "color": "#2F343D", + "fontSize": 16, + }, + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", }, + {}, ] } > - <Text - style={ +  + </Text> + </View> + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "paddingLeft": 5, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="small message" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - small message - </Text> - </Text> + "color": "#EC0D2A", + "fontSize": 16, + }, + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  </Text> </View> + <View + style={ + { + "paddingLeft": 5, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 16, + }, + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#095AD2", + "fontSize": 25, + }, + [ + { + "lineHeight": 25, + }, + { + "marginTop": -5, + "paddingLeft": 5, + }, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + testID="read-receipt-read" + > +  + </Text> </View> </View> - </View> - </View> - <View - style={ - { - "flexDirection": "row", - } - } - > - <View - accessibilityState={ - { - "busy": undefined, - "checked": undefined, - "disabled": undefined, - "expanded": undefined, - "selected": undefined, - } - } - accessibilityValue={ - { - "max": undefined, - "min": undefined, - "now": undefined, - "text": undefined, - } - } - accessible={true} - collapsable={false} - focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - onResponderGrant={[Function]} - onResponderMove={[Function]} - onResponderRelease={[Function]} - onResponderTerminate={[Function]} - onResponderTerminationRequest={[Function]} - onStartShouldSetResponder={[Function]} - style={ - { - "paddingLeft": 5, - } - } - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#2F343D", - "fontSize": 16, - }, - [ - { - "lineHeight": 16, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> - </View> - <View - style={ - { - "paddingLeft": 5, - } - } - testID="small message-edited" - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#2F343D", - "fontSize": 16, - }, - [ - { - "lineHeight": 16, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> - </View> - <View - accessibilityState={ - { - "busy": undefined, - "checked": undefined, - "disabled": undefined, - "expanded": undefined, - "selected": undefined, - } - } - accessibilityValue={ - { - "max": undefined, - "min": undefined, - "now": undefined, - "text": undefined, - } - } - accessible={true} - collapsable={false} - focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - onResponderGrant={[Function]} - onResponderMove={[Function]} - onResponderRelease={[Function]} - onResponderTerminate={[Function]} - onResponderTerminationRequest={[Function]} - onStartShouldSetResponder={[Function]} - style={ - { - "paddingLeft": 5, - } - } - > - <Text - allowFontScaling={false} - selectable={false} + <View style={ - [ - { - "color": "#EC0D2A", - "fontSize": 16, - }, - [ - { - "lineHeight": 16, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] + { + "gap": 4, + } } > -  - </Text> - </View> - <View - style={ - { - "paddingLeft": 5, - } - } - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#2F343D", - "fontSize": 16, - }, - [ - { - "lineHeight": 16, - }, - undefined, - ], + <View + style={ { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> + "opacity": 0.3, + } + } + testID="message-content-small message" + > + <Text + accessibilityLabel="Encrypted message" + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + testID="message-encrypted" + > + Encrypted message + </Text> + </View> + </View> </View> - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#095AD2", - "fontSize": 25, - }, - [ - { - "lineHeight": 25, - }, - { - "marginTop": -5, - "paddingLeft": 5, - }, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - testID="read-receipt-read" - > -  - </Text> </View> </View> </A11yIndexView> @@ -73657,10 +74152,11 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-163" > <A11yIndexView accessibilityLabel="this is a normal message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -73945,6 +74441,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-this is a normal message" > <View @@ -74114,10 +74611,11 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-164" > <A11yIndexView accessibilityLabel="Edited message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -74438,6 +74936,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Edited message" > <View @@ -74607,10 +75106,11 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-165" > <A11yIndexView accessibilityLabel="Translated message" + accessibilityLanguage="en" accessible={true} importantForAccessibility="yes" orderFocusType={0} @@ -74931,6 +75431,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Translated message" > <View @@ -75040,7 +75541,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` }, ] } - accessibilityLabel="johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername 10:00:00 AM Encrypted message." + accessibilityLabel="johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername 10:00:00 AM Encrypted message. Encrypted message" accessibilityRole="button" accessibilityState={ { @@ -75100,10 +75601,11 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-166" > <A11yIndexView accessibilityLabel="Encrypted message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -75460,63 +75962,29 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Encrypted message" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Encrypted message" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Encrypted message - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -75542,10 +76010,11 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-167" > <A11yIndexView accessibilityLabel="Error message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -75902,6 +76371,11 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` } > <View + style={ + { + "opacity": 0.3, + } + } testID="message-content-Error message" > <View @@ -76067,10 +76541,11 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-168" > <A11yIndexView accessibilityLabel="Message with read receipt" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -76386,6 +76861,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message with read receipt" > <View @@ -76495,7 +76971,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` }, ] } - accessibilityLabel="johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername 10:00:00 AM Message with read receipt. Message was read" + accessibilityLabel="johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername 10:00:00 AM Encrypted message. Encrypted message Message was read" accessibilityRole="button" accessibilityState={ { @@ -76555,10 +77031,11 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-169" > <A11yIndexView accessibilityLabel="Message with read receipt" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -76945,63 +77422,29 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message with read receipt" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Message with read receipt" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Message with read receipt - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -77027,10 +77470,11 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-170" > <A11yIndexView accessibilityLabel="Show all icons " + accessibilityLanguage="en" accessible={true} importantForAccessibility="yes" orderFocusType={0} @@ -77558,819 +78002,1197 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` } > <View - testID="message-content-Show all icons " - > - <View - style={ - { - "gap": 2, - } - } - > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Show all icons " - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Show all icons - </Text> - </Text> - </Text> - </View> - </View> - </View> - </View> - </View> - </View> - </A11yIndexView> - </View> - </A11yOrderView> - <A11yOrderView - orderKey="«r4k»" - > - <View - style={ - { - "flexDirection": "column", - "gap": 8, - "paddingHorizontal": 12, - "paddingVertical": 4, - "width": "100%", - } - } - testID="message-undefined" - > - <A11yIndexView - accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - accessible={true} - importantForAccessibility="yes" - orderFocusType={0} - orderIndex={2} - orderKey="«r4k»" - > - <View - style={ - { - "flexDirection": "row", - } - } - > - <View - style={ - { - "alignItems": "flex-end", - "width": 46.800000000000004, - } - } - /> - <View - style={ - { - "flex": 1, - "marginLeft": 10, - } - } - > - <View - onLayout={[Function]} - style={ - { - "flex": 1, - } - } - > - <View - style={ - { - "gap": 4, - } - } - > - <View - testID="message-content-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - > - <View - style={ - { - "gap": 2, - } - } - > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - </Text> - </Text> - </Text> - </View> - </View> - </View> - </View> - </View> - <View - style={ - { - "flexDirection": "row", - } - } - > - <View - accessibilityState={ - { - "busy": undefined, - "checked": undefined, - "disabled": undefined, - "expanded": undefined, - "selected": undefined, - } - } - accessibilityValue={ - { - "max": undefined, - "min": undefined, - "now": undefined, - "text": undefined, - } - } - accessible={true} - collapsable={false} - focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - onResponderGrant={[Function]} - onResponderMove={[Function]} - onResponderRelease={[Function]} - onResponderTerminate={[Function]} - onResponderTerminationRequest={[Function]} - onStartShouldSetResponder={[Function]} - style={ - { - "paddingLeft": 5, - } - } - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#2F343D", - "fontSize": 20.8, - }, - [ - { - "lineHeight": 20.8, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> - </View> - <View - style={ - { - "paddingLeft": 5, - } - } - testID="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.-edited" - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#2F343D", - "fontSize": 20.8, - }, - [ - { - "lineHeight": 20.8, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> - </View> - <View - accessibilityState={ - { - "busy": undefined, - "checked": undefined, - "disabled": undefined, - "expanded": undefined, - "selected": undefined, - } - } - accessibilityValue={ - { - "max": undefined, - "min": undefined, - "now": undefined, - "text": undefined, - } - } - accessible={true} - collapsable={false} - focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - onResponderGrant={[Function]} - onResponderMove={[Function]} - onResponderRelease={[Function]} - onResponderTerminate={[Function]} - onResponderTerminationRequest={[Function]} - onStartShouldSetResponder={[Function]} - style={ - { - "paddingLeft": 5, - } - } - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#EC0D2A", - "fontSize": 20.8, - }, - [ - { - "lineHeight": 20.8, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> - </View> - <View - style={ - { - "paddingLeft": 5, - } - } - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#2F343D", - "fontSize": 20.8, - }, - [ - { - "lineHeight": 20.8, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> - </View> - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#095AD2", - "fontSize": 32.5, - }, - [ - { - "lineHeight": 32.5, - }, + style={ + { + "opacity": 0.3, + } + } + testID="message-content-Show all icons " + > + <Text + accessibilityLabel="Encrypted message" + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + testID="message-encrypted" + > + Encrypted message + </Text> + </View> + </View> + </View> + </View> + </View> + </A11yIndexView> + </View> + </A11yOrderView> + <A11yOrderView + orderKey="«r4k»" + > + <View + style={ + { + "flexDirection": "column", + "gap": 8, + "paddingHorizontal": 12, + "paddingVertical": 4, + "width": "100%", + } + } + testID="message-story-message-171" + > + <A11yIndexView + accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + accessibilityLanguage="en" + accessible={true} + importantForAccessibility="yes" + orderFocusType={0} + orderIndex={2} + orderKey="«r4k»" + > + <View + style={ + { + "flexDirection": "row", + } + } + > + <View + style={ + { + "alignItems": "flex-end", + "width": 46.800000000000004, + } + } + > + <View + accessible={true} + style={ + [ + { + "borderRadius": 4, + "height": 46.800000000000004, + "width": 46.800000000000004, + }, + { + "marginTop": 4, + }, + ] + } + testID="avatar" + > + <ExternalKeyboardView + canBeFocused={true} + enableA11yFocus={false} + enableContextMenu={false} + group={false} + haloEffect={true} + hasKeyDownPress={false} + hasKeyUpPress={true} + hasOnFocusChanged={true} + lockFocus={0} + onContextMenuPress={[Function]} + onFocusChange={[Function]} + onKeyUpPress={[Function]} + orderIndex={-1} + screenAutoA11yFocusDelay={300} + style={ + [ + undefined, + undefined, + ] + } + > + <RNGestureHandlerButton + activeOpacity={1} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={-1} + handlerType="NativeViewGestureHandler" + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[Function]} + rippleColor="#E4E7EA" + style={ + [ + { + "backgroundColor": undefined, + "borderRadius": undefined, + "margin": undefined, + "marginBottom": undefined, + "marginEnd": undefined, + "marginHorizontal": undefined, + "marginLeft": undefined, + "marginRight": undefined, + "marginStart": undefined, + "marginTop": undefined, + "marginVertical": undefined, + }, + { + "cursor": undefined, + }, + ] + } + underlayColor="#E4E7EA" + > + <View + collapsable={false} + style={ + { + "backgroundColor": "#E4E7EA", + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "bottom": 0, + "left": 0, + "opacity": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + /> + <View + accessibilityLabel="johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername's avatar" + accessible={true} + style={{}} + > + <ViewManagerAdapter_ExpoImage + borderRadius={4} + containerViewRef={{"current":"Object"}} + contentFit="cover" + contentPosition={ + { + "left": "50%", + "top": "50%", + } + } + height={46.800000000000004} + nativeViewRef={{"current":"NativeComponent"}} + onError={[Function]} + onLoad={[Function]} + onLoadStart={[Function]} + onProgress={[Function]} + placeholder={[]} + priority="high" + source={ + [ + { + "headers": { + "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", + }, + "uri": "https://open.rocket.chat/avatar/johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername?format=png&size=93.60000000000001", + }, + ] + } + style={ + { + "borderRadius": 4, + "height": 46.800000000000004, + "width": 46.800000000000004, + } + } + transition={null} + width={46.800000000000004} + /> + </View> + </RNGestureHandlerButton> + </ExternalKeyboardView> + </View> + </View> + <View + style={ + { + "flex": 1, + "marginLeft": 10, + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + "justifyContent": "space-between", + } + } + > + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, + } + } + testID="username-header-johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername" + > + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#1F2329", + }, + ] + } + > + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + </Text> + </View> + <View + style={ + { + "flexDirection": "row", + } + } + > + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "paddingLeft": 5, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 20.8, + }, + [ + { + "lineHeight": 20.8, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "paddingLeft": 5, + } + } + testID="undefined-edited" + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 20.8, + }, + [ + { + "lineHeight": 20.8, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "paddingLeft": 5, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#EC0D2A", + "fontSize": 20.8, + }, + [ + { + "lineHeight": 20.8, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "paddingLeft": 5, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 20.8, + }, + [ + { + "lineHeight": 20.8, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#095AD2", + "fontSize": 32.5, + }, + [ + { + "lineHeight": 32.5, + }, + { + "marginTop": -5, + "paddingLeft": 5, + }, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + testID="read-receipt-read" + > +  + </Text> + </View> + </View> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + undefined, + ] + } + > + 10:00 AM + </Text> + <View + style={ + { + "gap": 4, + } + } + > + <View + style={ + { + "opacity": 0.3, + } + } + testID="message-content-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + > + <Text + accessibilityLabel="Encrypted message" + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + testID="message-encrypted" + > + Encrypted message + </Text> + </View> + </View> + </View> + </View> + </View> + </A11yIndexView> + </View> + </A11yOrderView> + <A11yOrderView + orderKey="«r4l»" + > + <View + style={ + { + "flexDirection": "column", + "gap": 8, + "paddingHorizontal": 12, + "paddingVertical": 4, + "width": "100%", + } + } + testID="message-story-message-173" + > + <A11yIndexView + accessibilityLabel="small message" + accessibilityLanguage="en" + accessible={true} + importantForAccessibility="yes" + orderFocusType={0} + orderIndex={2} + orderKey="«r4l»" + > + <View + style={ + { + "flexDirection": "row", + } + } + > + <View + style={ + { + "alignItems": "flex-end", + "width": 46.800000000000004, + } + } + > + <View + accessible={true} + style={ + [ + { + "borderRadius": 4, + "height": 46.800000000000004, + "width": 46.800000000000004, + }, + { + "marginTop": 4, + }, + ] + } + testID="avatar" + > + <ExternalKeyboardView + canBeFocused={true} + enableA11yFocus={false} + enableContextMenu={false} + group={false} + haloEffect={true} + hasKeyDownPress={false} + hasKeyUpPress={true} + hasOnFocusChanged={true} + lockFocus={0} + onContextMenuPress={[Function]} + onFocusChange={[Function]} + onKeyUpPress={[Function]} + orderIndex={-1} + screenAutoA11yFocusDelay={300} + style={ + [ + undefined, + undefined, + ] + } + > + <RNGestureHandlerButton + activeOpacity={1} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={-1} + handlerType="NativeViewGestureHandler" + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[Function]} + rippleColor="#E4E7EA" + style={ + [ + { + "backgroundColor": undefined, + "borderRadius": undefined, + "margin": undefined, + "marginBottom": undefined, + "marginEnd": undefined, + "marginHorizontal": undefined, + "marginLeft": undefined, + "marginRight": undefined, + "marginStart": undefined, + "marginTop": undefined, + "marginVertical": undefined, + }, + { + "cursor": undefined, + }, + ] + } + underlayColor="#E4E7EA" + > + <View + collapsable={false} + style={ + { + "backgroundColor": "#E4E7EA", + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "bottom": 0, + "left": 0, + "opacity": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + /> + <View + accessibilityLabel="johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername's avatar" + accessible={true} + style={{}} + > + <ViewManagerAdapter_ExpoImage + borderRadius={4} + containerViewRef={{"current":"Object"}} + contentFit="cover" + contentPosition={ + { + "left": "50%", + "top": "50%", + } + } + height={46.800000000000004} + nativeViewRef={{"current":"NativeComponent"}} + onError={[Function]} + onLoad={[Function]} + onLoadStart={[Function]} + onProgress={[Function]} + placeholder={[]} + priority="high" + source={ + [ + { + "headers": { + "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", + }, + "uri": "https://open.rocket.chat/avatar/johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername?format=png&size=93.60000000000001", + }, + ] + } + style={ + { + "borderRadius": 4, + "height": 46.800000000000004, + "width": 46.800000000000004, + } + } + transition={null} + width={46.800000000000004} + /> + </View> + </RNGestureHandlerButton> + </ExternalKeyboardView> + </View> + </View> + <View + style={ + { + "flex": 1, + "marginLeft": 10, + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + "justifyContent": "space-between", + } + } + > + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, + } + } + testID="username-header-johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername" + > + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#1F2329", + }, + ] + } + > + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + </Text> + </View> + <View + style={ + { + "flexDirection": "row", + } + } + > + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "paddingLeft": 5, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 20.8, + }, + [ + { + "lineHeight": 20.8, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "paddingLeft": 5, + } + } + testID="undefined-edited" + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 20.8, + }, + [ + { + "lineHeight": 20.8, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "paddingLeft": 5, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#EC0D2A", + "fontSize": 20.8, + }, + [ + { + "lineHeight": 20.8, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "paddingLeft": 5, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 20.8, + }, + [ + { + "lineHeight": 20.8, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#095AD2", + "fontSize": 32.5, + }, + [ + { + "lineHeight": 32.5, + }, + { + "marginTop": -5, + "paddingLeft": 5, + }, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + testID="read-receipt-read" + > +  + </Text> + </View> + </View> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + undefined, + ] + } + > + 10:00 AM + </Text> + <View + style={ + { + "gap": 4, + } + } + > + <View + style={ { - "marginTop": -5, - "paddingLeft": 5, - }, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - testID="read-receipt-read" - > -  - </Text> - </View> - </View> - </A11yIndexView> - </View> - </A11yOrderView> - <A11yOrderView - orderKey="«r4l»" - > - <View - style={ - { - "flexDirection": "column", - "gap": 8, - "paddingHorizontal": 12, - "paddingVertical": 4, - "width": "100%", - } - } - testID="message-undefined" - > - <A11yIndexView - accessibilityLabel="small message" - accessible={true} - importantForAccessibility="yes" - orderFocusType={0} - orderIndex={2} - orderKey="«r4l»" - > - <View - style={ - { - "flexDirection": "row", - } - } - > - <View - style={ - { - "alignItems": "flex-end", - "width": 46.800000000000004, - } - } - /> - <View - style={ - { - "flex": 1, - "marginLeft": 10, - } - } - > - <View - onLayout={[Function]} - style={ - { - "flex": 1, - } - } - > - <View - style={ - { - "gap": 4, + "opacity": 0.3, + } } - } - > - <View testID="message-content-small message" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="small message" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - small message - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> </View> - <View - style={ - { - "flexDirection": "row", - } - } - > - <View - accessibilityState={ - { - "busy": undefined, - "checked": undefined, - "disabled": undefined, - "expanded": undefined, - "selected": undefined, - } - } - accessibilityValue={ - { - "max": undefined, - "min": undefined, - "now": undefined, - "text": undefined, - } - } - accessible={true} - collapsable={false} - focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - onResponderGrant={[Function]} - onResponderMove={[Function]} - onResponderRelease={[Function]} - onResponderTerminate={[Function]} - onResponderTerminationRequest={[Function]} - onStartShouldSetResponder={[Function]} - style={ - { - "paddingLeft": 5, - } - } - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#2F343D", - "fontSize": 20.8, - }, - [ - { - "lineHeight": 20.8, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> - </View> - <View - style={ - { - "paddingLeft": 5, - } - } - testID="small message-edited" - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#2F343D", - "fontSize": 20.8, - }, - [ - { - "lineHeight": 20.8, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> - </View> - <View - accessibilityState={ - { - "busy": undefined, - "checked": undefined, - "disabled": undefined, - "expanded": undefined, - "selected": undefined, - } - } - accessibilityValue={ - { - "max": undefined, - "min": undefined, - "now": undefined, - "text": undefined, - } - } - accessible={true} - collapsable={false} - focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - onResponderGrant={[Function]} - onResponderMove={[Function]} - onResponderRelease={[Function]} - onResponderTerminate={[Function]} - onResponderTerminationRequest={[Function]} - onStartShouldSetResponder={[Function]} - style={ - { - "paddingLeft": 5, - } - } - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#EC0D2A", - "fontSize": 20.8, - }, - [ - { - "lineHeight": 20.8, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> - </View> - <View - style={ - { - "paddingLeft": 5, - } - } - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#2F343D", - "fontSize": 20.8, - }, - [ - { - "lineHeight": 20.8, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> - </View> - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#095AD2", - "fontSize": 32.5, - }, - [ - { - "lineHeight": 32.5, - }, - { - "marginTop": -5, - "paddingLeft": 5, - }, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - testID="read-receipt-read" - > -  - </Text> - </View> </View> </A11yIndexView> </View> @@ -78488,10 +79310,11 @@ exports[`Story Snapshots: Mentions should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-175" > <A11yIndexView accessibilityLabel="@rocket.cat @diego.mello @all @here #general @team" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -78776,6 +79599,7 @@ exports[`Story Snapshots: Mentions should match snapshot 1`] = ` } > <View + style={false} testID="message-content-@rocket.cat @diego.mello @all @here #general @team" > <View @@ -79124,10 +79948,11 @@ exports[`Story Snapshots: Mentions should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-176" > <A11yIndexView accessibilityLabel="@rocket.cat Lorem ipsum dolor @diego.mello sit amet, @all consectetur adipiscing @here elit, sed do eiusmod tempor #general incididunt ut labore et dolore magna aliqua." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -79412,6 +80237,7 @@ exports[`Story Snapshots: Mentions should match snapshot 1`] = ` } > <View + style={false} testID="message-content-@rocket.cat Lorem ipsum dolor @diego.mello sit amet, @all consectetur adipiscing @here elit, sed do eiusmod tempor #general incididunt ut labore et dolore magna aliqua." > <View @@ -79755,10 +80581,11 @@ exports[`Story Snapshots: MentionsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-177" > <A11yIndexView accessibilityLabel="@rocket.cat @diego.mello @all @here #general @team" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -80043,6 +80870,7 @@ exports[`Story Snapshots: MentionsLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-@rocket.cat @diego.mello @all @here #general @team" > <View @@ -80391,10 +81219,11 @@ exports[`Story Snapshots: MentionsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-178" > <A11yIndexView accessibilityLabel="@rocket.cat Lorem ipsum dolor @diego.mello sit amet, @all consectetur adipiscing @here elit, sed do eiusmod tempor #general incididunt ut labore et dolore magna aliqua." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -80679,6 +81508,7 @@ exports[`Story Snapshots: MentionsLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-@rocket.cat Lorem ipsum dolor @diego.mello sit amet, @all consectetur adipiscing @here elit, sed do eiusmod tempor #general incididunt ut labore et dolore magna aliqua." > <View @@ -81022,10 +81852,11 @@ exports[`Story Snapshots: Message should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-179" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -81432,10 +82263,11 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-180" > <A11yIndexView accessibilityLabel="Forwarded message with file inside" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -81720,6 +82552,7 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` } > <View + style={false} testID="message-content-Forwarded message with file inside" > <View @@ -82184,10 +83017,11 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-181" > <A11yIndexView accessibilityLabel="Forwarded message with nested image" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -82472,6 +83306,7 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` } > <View + style={false} testID="message-content-Forwarded message with nested image" > <View @@ -82990,10 +83825,11 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-182" > <A11yIndexView accessibilityLabel="Forwarded message with nested audio" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -83278,6 +84114,7 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` } > <View + style={false} testID="message-content-Forwarded message with nested audio" > <View @@ -83808,10 +84645,11 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-183" > <A11yIndexView accessibilityLabel="Forwarded message with nested video" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -84096,6 +84934,7 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` } > <View + style={false} testID="message-content-Forwarded message with nested video" > <View @@ -84517,10 +85356,11 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-184" > <A11yIndexView accessibilityLabel="Message forwarded twice with file inside" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -84805,6 +85645,7 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` } > <View + style={false} testID="message-content-Message forwarded twice with file inside" > <View @@ -85418,10 +86259,11 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-185" > <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -85737,6 +86579,7 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` } > <View + style={false} testID="message-content-I'm fine!" > <View @@ -85906,10 +86749,11 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-186" > <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -85954,6 +86798,7 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` } > <View + style={false} testID="message-content-I'm fine!" > <View @@ -86161,10 +87006,11 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-188" > <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -86480,6 +87326,7 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` } > <View + style={false} testID="message-content-I'm fine!" > <View @@ -86649,10 +87496,11 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-189" > <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -86697,6 +87545,7 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` } > <View + style={false} testID="message-content-I'm fine!" > <View @@ -86917,10 +87766,11 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-191" > <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -87236,6 +88086,7 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot } > <View + style={false} testID="message-content-I'm fine!" > <View @@ -87405,10 +88256,11 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-192" > <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -87453,6 +88305,7 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot } > <View + style={false} testID="message-content-I'm fine!" > <View @@ -87660,10 +88513,11 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-194" > <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -87979,6 +88833,7 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot } > <View + style={false} testID="message-content-I'm fine!" > <View @@ -88148,10 +89003,11 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-195" > <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -88196,6 +89052,7 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot } > <View + style={false} testID="message-content-I'm fine!" > <View @@ -88416,10 +89273,11 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-197" > <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -88876,6 +89734,7 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` </View> </View> <View + style={false} testID="message-content-I'm fine!" > <View @@ -89045,10 +89904,11 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-198" > <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -89812,6 +90672,7 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` </View> </View> <View + style={false} testID="message-content-I'm fine!" > <View @@ -89981,10 +90842,11 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-199" > <A11yIndexView accessibilityLabel="Looks cool!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -90269,6 +91131,7 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Looks cool!" > <View @@ -90787,10 +91650,11 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-200" > <A11yIndexView accessibilityLabel="Yes, I am" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -91075,6 +91939,7 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Yes, I am" > <View @@ -91667,10 +92532,11 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-201" > <A11yIndexView accessibilityLabel="Here is the file" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -92114,6 +92980,7 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` </View> </View> <View + style={false} testID="message-content-Here is the file" > <View @@ -92283,10 +93150,11 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-202" > <A11yIndexView accessibilityLabel="Got it!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -92807,6 +93675,7 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` </View> </View> <View + style={false} testID="message-content-Got it!" > <View @@ -92976,10 +93845,11 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-203" > <A11yIndexView accessibilityLabel="Multiple files" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -93574,6 +94444,7 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` </View> </View> <View + style={false} testID="message-content-Multiple files" > <View @@ -93756,10 +94627,11 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-204" > <A11yIndexView accessibilityLabel="Here is the file" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -94203,6 +95075,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot </View> </View> <View + style={false} testID="message-content-Here is the file" > <View @@ -94372,10 +95245,11 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-205" > <A11yIndexView accessibilityLabel="Got it!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -94896,6 +95770,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot </View> </View> <View + style={false} testID="message-content-Got it!" > <View @@ -95065,10 +95940,11 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-206" > <A11yIndexView accessibilityLabel="Multiple files" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -95663,6 +96539,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot </View> </View> <View + style={false} testID="message-content-Multiple files" > <View @@ -95832,10 +96709,11 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-207" > <A11yIndexView accessibilityLabel="Forwarded message with nested audio" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -96120,6 +96998,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot } > <View + style={false} testID="message-content-Forwarded message with nested audio" > <View @@ -96650,10 +97529,11 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-208" > <A11yIndexView accessibilityLabel="Forwarded message with nested video" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -96938,6 +97818,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot } > <View + style={false} testID="message-content-Forwarded message with nested video" > <View @@ -97359,10 +98240,11 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-209" > <A11yIndexView accessibilityLabel="Message forwarded twice with file inside" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -97647,6 +98529,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot } > <View + style={false} testID="message-content-Message forwarded twice with file inside" > <View @@ -98260,10 +99143,11 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = "width": "100%", } } - testID="message-undefined" + testID="message-story-message-210" > <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -98720,6 +99604,7 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = </View> </View> <View + style={false} testID="message-content-I'm fine!" > <View @@ -98889,10 +99774,11 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = "width": "100%", } } - testID="message-undefined" + testID="message-story-message-211" > <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -99393,6 +100279,7 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = </View> </View> <View + style={false} testID="message-content-I'm fine!" > <View @@ -99562,10 +100449,11 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = "width": "100%", } } - testID="message-undefined" + testID="message-story-message-212" > <A11yIndexView accessibilityLabel="Looks cool!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -99850,6 +100738,7 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = } > <View + style={false} testID="message-content-Looks cool!" > <View @@ -100368,10 +101257,11 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = "width": "100%", } } - testID="message-undefined" + testID="message-story-message-213" > <A11yIndexView accessibilityLabel="Yes, I am" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -100656,6 +101546,7 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = } > <View + style={false} testID="message-content-Yes, I am" > <View @@ -101249,10 +102140,11 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-214" > <A11yIndexView accessibilityLabel="How are you?" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -101537,6 +102429,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` } > <View + style={false} testID="message-content-How are you?" > <View @@ -101995,7 +102888,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message I'm fine!." + accessibilityLabel="diego.mello 10:00:00 AM thread message I'm fine!." accessibilityRole="button" accessibilityState={ { @@ -102336,6 +103229,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -102355,6 +103249,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` } > <View + style={false} testID="message-content-I'm fine!" > <Text @@ -102430,7 +103325,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message I'm fine!." + accessibilityLabel="diego.mello 10:00:00 AM thread message I'm fine!." accessibilityRole="button" accessibilityState={ { @@ -102771,6 +103666,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -102790,6 +103686,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` } > <View + style={false} testID="message-content-I'm fine!" > <Text @@ -102865,7 +103762,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message I'm fine!." + accessibilityLabel="diego.mello 10:00:00 AM thread message I'm fine!." accessibilityRole="button" accessibilityState={ { @@ -103206,6 +104103,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -103225,6 +104123,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` } > <View + style={false} testID="message-content-I'm fine!" > <Text @@ -103300,7 +104199,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.." + accessibilityLabel="diego.mello 10:00:00 AM thread message Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.." accessibilityRole="button" accessibilityState={ { @@ -103641,6 +104540,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -103660,6 +104560,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." > <Text @@ -103735,7 +104636,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.." + accessibilityLabel="diego.mello 10:00:00 AM thread message Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.." accessibilityRole="button" accessibilityState={ { @@ -104076,6 +104977,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -104095,6 +104997,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." > <Text @@ -104170,7 +105073,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message ." + accessibilityLabel="diego.mello 10:00:00 AM thread message ." accessibilityRole="button" accessibilityState={ { @@ -104511,6 +105414,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -104530,6 +105434,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` } > <View + style={false} testID="message-content-" > <Text @@ -104605,7 +105510,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message ." + accessibilityLabel="diego.mello 10:00:00 AM thread message ." accessibilityRole="button" accessibilityState={ { @@ -104946,6 +105851,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -104965,6 +105871,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` } > <View + style={false} testID="message-content-" > <Text @@ -105040,7 +105947,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message ." + accessibilityLabel="diego.mello 10:00:00 AM thread message ." accessibilityRole="button" accessibilityState={ { @@ -105381,6 +106288,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -105400,6 +106308,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` } > <View + style={false} testID="message-content-" > <Text @@ -105475,7 +106384,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message ." + accessibilityLabel="diego.mello 10:00:00 AM thread message ." accessibilityRole="button" accessibilityState={ { @@ -105816,6 +106725,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -105835,6 +106745,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` } > <View + style={false} testID="message-content-" > <Text @@ -105984,10 +106895,11 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = "width": "100%", } } - testID="message-undefined" + testID="message-story-message-233" > <A11yIndexView accessibilityLabel="How are you?" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -106272,6 +107184,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = } > <View + style={false} testID="message-content-How are you?" > <View @@ -106730,7 +107643,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message I'm fine!." + accessibilityLabel="diego.mello 10:00:00 AM thread message I'm fine!." accessibilityRole="button" accessibilityState={ { @@ -107071,6 +107984,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </View> <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -107090,6 +108004,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = } > <View + style={false} testID="message-content-I'm fine!" > <Text @@ -107165,7 +108080,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message I'm fine!." + accessibilityLabel="diego.mello 10:00:00 AM thread message I'm fine!." accessibilityRole="button" accessibilityState={ { @@ -107506,6 +108421,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </View> <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -107525,6 +108441,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = } > <View + style={false} testID="message-content-I'm fine!" > <Text @@ -107600,7 +108517,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message I'm fine!." + accessibilityLabel="diego.mello 10:00:00 AM thread message I'm fine!." accessibilityRole="button" accessibilityState={ { @@ -107941,6 +108858,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </View> <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -107960,6 +108878,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = } > <View + style={false} testID="message-content-I'm fine!" > <Text @@ -108035,7 +108954,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.." + accessibilityLabel="diego.mello 10:00:00 AM thread message Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.." accessibilityRole="button" accessibilityState={ { @@ -108376,6 +109295,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </View> <A11yIndexView accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -108395,6 +109315,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = } > <View + style={false} testID="message-content-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." > <Text @@ -108470,7 +109391,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.." + accessibilityLabel="diego.mello 10:00:00 AM thread message Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.." accessibilityRole="button" accessibilityState={ { @@ -108811,6 +109732,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </View> <A11yIndexView accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -108830,6 +109752,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = } > <View + style={false} testID="message-content-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." > <Text @@ -108905,7 +109828,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message ." + accessibilityLabel="diego.mello 10:00:00 AM thread message ." accessibilityRole="button" accessibilityState={ { @@ -109246,6 +110169,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -109265,6 +110189,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = } > <View + style={false} testID="message-content-" > <Text @@ -109340,7 +110265,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message ." + accessibilityLabel="diego.mello 10:00:00 AM thread message ." accessibilityRole="button" accessibilityState={ { @@ -109681,6 +110606,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -109700,6 +110626,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = } > <View + style={false} testID="message-content-" > <Text @@ -109775,7 +110702,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message ." + accessibilityLabel="diego.mello 10:00:00 AM thread message ." accessibilityRole="button" accessibilityState={ { @@ -110116,6 +111043,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -110135,6 +111063,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = } > <View + style={false} testID="message-content-" > <Text @@ -110210,7 +111139,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message ." + accessibilityLabel="diego.mello 10:00:00 AM thread message ." accessibilityRole="button" accessibilityState={ { @@ -110551,6 +111480,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -110570,6 +111500,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = } > <View + style={false} testID="message-content-" > <Text @@ -110718,10 +111649,11 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-252" > <A11yIndexView accessibilityLabel="Small name (2 chars)" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -111006,6 +111938,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Small name (2 chars)" > <View @@ -111088,10 +112021,11 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-253" > <A11yIndexView accessibilityLabel="Small name with icons" + accessibilityLanguage="en" accessible={true} importantForAccessibility="yes" orderFocusType={0} @@ -111648,63 +112582,33 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` } > <View + style={ + { + "opacity": 0.3, + } + } testID="message-content-Small name with icons" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Small name with icons" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Small name with icons - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -111813,10 +112717,11 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-254" > <A11yIndexView accessibilityLabel="Medium name (8 chars)" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -112101,6 +113006,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Medium name (8 chars)" > <View @@ -112183,10 +113089,11 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-255" > <A11yIndexView accessibilityLabel="Medium name with icons" + accessibilityLanguage="en" accessible={true} importantForAccessibility="yes" orderFocusType={0} @@ -112743,63 +113650,33 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` } > <View + style={ + { + "opacity": 0.3, + } + } testID="message-content-Medium name with icons" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Medium name with icons" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Medium name with icons - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -112908,10 +113785,11 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-256" > <A11yIndexView accessibilityLabel="Long name (22 chars) - should truncate" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -113196,6 +114074,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Long name (22 chars) - should truncate" > <View @@ -113278,10 +114157,11 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-257" > <A11yIndexView accessibilityLabel="Long name with icons - timestamp visible" + accessibilityLanguage="en" accessible={true} importantForAccessibility="yes" orderFocusType={0} @@ -113838,63 +114718,33 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` } > <View + style={ + { + "opacity": 0.3, + } + } testID="message-content-Long name with icons - timestamp visible" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Long name with icons - timestamp visible" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Long name with icons - timestamp visible - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -114016,10 +114866,11 @@ exports[`Story Snapshots: Pinned should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-258" > <A11yIndexView accessibilityLabel="Message header" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -114334,6 +115185,7 @@ exports[`Story Snapshots: Pinned should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message header" > <View @@ -114503,10 +115355,11 @@ exports[`Story Snapshots: Pinned should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-259" > <A11yIndexView accessibilityLabel="Message without header" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -114551,6 +115404,7 @@ exports[`Story Snapshots: Pinned should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message without header" > <View @@ -114770,10 +115624,11 @@ exports[`Story Snapshots: PinnedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-261" > <A11yIndexView accessibilityLabel="Message header" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -115088,6 +115943,7 @@ exports[`Story Snapshots: PinnedLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message header" > <View @@ -115257,10 +116113,11 @@ exports[`Story Snapshots: PinnedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-262" > <A11yIndexView accessibilityLabel="Message without header" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -115305,6 +116162,7 @@ exports[`Story Snapshots: PinnedLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message without header" > <View @@ -115524,10 +116382,11 @@ exports[`Story Snapshots: Reactions should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-264" > <A11yIndexView accessibilityLabel="Reactions" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -115812,6 +116671,7 @@ exports[`Story Snapshots: Reactions should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Reactions" > <View @@ -116525,10 +117385,11 @@ exports[`Story Snapshots: Reactions should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-265" > <A11yIndexView accessibilityLabel="Multiple Reactions" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -116813,6 +117674,7 @@ exports[`Story Snapshots: Reactions should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Multiple Reactions" > <View @@ -117995,10 +118857,11 @@ exports[`Story Snapshots: ReactionsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-266" > <A11yIndexView accessibilityLabel="Reactions" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -118283,6 +119146,7 @@ exports[`Story Snapshots: ReactionsLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Reactions" > <View @@ -118996,10 +119860,11 @@ exports[`Story Snapshots: ReactionsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-267" > <A11yIndexView accessibilityLabel="Multiple Reactions" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -119284,6 +120149,7 @@ exports[`Story Snapshots: ReactionsLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Multiple Reactions" > <View @@ -120467,10 +121333,11 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m "width": "100%", } } - testID="message-undefined" + testID="message-story-message-268" > <A11yIndexView accessibilityLabel="How are you?" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -120755,6 +121622,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m } > <View + style={false} testID="message-content-How are you?" > <View @@ -121437,6 +122305,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m </View> <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -121456,6 +122325,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m } > <View + style={false} testID="message-content-I'm fine!" > <Text @@ -121755,6 +122625,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m </View> <A11yIndexView accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -121774,6 +122645,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m } > <View + style={false} testID="message-content-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." > <Text @@ -122073,6 +122945,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -122092,6 +122965,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m } > <View + style={false} testID="message-content-" > <Text @@ -122241,10 +123115,11 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont "width": "100%", } } - testID="message-undefined" + testID="message-story-message-272" > <A11yIndexView accessibilityLabel="How are you?" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -122529,6 +123404,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont } > <View + style={false} testID="message-content-How are you?" > <View @@ -123211,6 +124087,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont </View> <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -123230,6 +124107,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont } > <View + style={false} testID="message-content-I'm fine!" > <Text @@ -123529,6 +124407,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont </View> <A11yIndexView accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -123548,6 +124427,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont } > <View + style={false} testID="message-content-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." > <Text @@ -123847,6 +124727,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -123866,6 +124747,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont } > <View + style={false} testID="message-content-" > <Text @@ -123954,7 +124836,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message I'm fine!." + accessibilityLabel="diego.mello 10:00:00 AM thread message I'm fine!." accessibilityRole="button" accessibilityState={ { @@ -124295,6 +125177,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma </View> <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -124314,6 +125197,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma } > <View + style={false} testID="message-content-I'm fine!" > <Text @@ -124613,6 +125497,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma </View> <A11yIndexView accessibilityLabel="Cool!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -124632,6 +125517,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma } > <View + style={false} testID="message-content-Cool!" > <Text @@ -124931,6 +125817,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma </View> <A11yIndexView accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -124950,6 +125837,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma } > <View + style={false} testID="message-content-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." > <Text @@ -125249,6 +126137,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -125268,6 +126157,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma } > <View + style={false} testID="message-content-" > <Text @@ -125356,7 +126246,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont }, ] } - accessibilityLabel="diego.mello 10:00:00 AM replying to thread message I'm fine!." + accessibilityLabel="diego.mello 10:00:00 AM thread message I'm fine!." accessibilityRole="button" accessibilityState={ { @@ -125697,6 +126587,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont </View> <A11yIndexView accessibilityLabel="I'm fine!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -125716,6 +126607,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont } > <View + style={false} testID="message-content-I'm fine!" > <Text @@ -126015,6 +126907,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont </View> <A11yIndexView accessibilityLabel="Cool!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -126034,6 +126927,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont } > <View + style={false} testID="message-content-Cool!" > <Text @@ -126333,6 +127227,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont </View> <A11yIndexView accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -126352,6 +127247,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont } > <View + style={false} testID="message-content-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." > <Text @@ -126651,6 +127547,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -126670,6 +127567,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont } > <View + style={false} testID="message-content-" > <Text @@ -126818,10 +127716,11 @@ exports[`Story Snapshots: ShowButtonAsAttachment should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-286" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -127349,10 +128248,11 @@ exports[`Story Snapshots: ShowButtonAsAttachment should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-287" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -128058,10 +128958,11 @@ exports[`Story Snapshots: ShowButtonAsAttachmentLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-288" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -128589,10 +129490,11 @@ exports[`Story Snapshots: ShowButtonAsAttachmentLargeFont should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-289" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -129298,10 +130200,11 @@ exports[`Story Snapshots: StaticAvatar should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-290" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -129586,6 +130489,7 @@ exports[`Story Snapshots: StaticAvatar should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message" > <View @@ -129768,10 +130672,11 @@ exports[`Story Snapshots: StaticAvatarLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-291" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -130056,6 +130961,7 @@ exports[`Story Snapshots: StaticAvatarLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message" > <View @@ -130317,6 +131223,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -130560,6 +131467,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -130803,6 +131711,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="New name" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -131049,6 +131958,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -131292,6 +132202,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="rocket.cat" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -131535,6 +132446,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="rocket.cat" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -131778,6 +132690,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="rocket.cat" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -132021,6 +132934,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="rocket.cat" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -132264,6 +133178,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="rocket.cat" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -132507,6 +133422,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="rocket.cat" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -132750,6 +133666,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="New name" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -132993,6 +133910,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="new description" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -133236,6 +134154,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="new announcement" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -133479,6 +134398,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="new topic" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -133722,6 +134642,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="public" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -133965,6 +134886,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -134208,6 +135130,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -134451,6 +135374,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="rocket.cat" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -134694,6 +135618,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="rocket.cat" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -134937,6 +135862,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="channel-name" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -135180,6 +136106,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="channel-name" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -135423,6 +136350,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="channel-name" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -135666,6 +136594,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="channel-name" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -135909,6 +136838,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="channel-name" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -136152,6 +137082,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -136395,6 +137326,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -136629,6 +137561,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -136872,6 +137805,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -137115,6 +138049,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="New name" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -137361,6 +138296,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -137604,6 +138540,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="rocket.cat" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -137847,6 +138784,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="rocket.cat" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -138090,6 +139028,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="rocket.cat" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -138333,6 +139272,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="rocket.cat" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -138576,6 +139516,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="rocket.cat" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -138819,6 +139760,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="rocket.cat" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -139062,6 +140004,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="New name" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -139305,6 +140248,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="new description" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -139548,6 +140492,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="new announcement" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -139791,6 +140736,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="new topic" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -140034,6 +140980,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="public" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -140277,6 +141224,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -140520,6 +141468,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -140763,6 +141712,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="rocket.cat" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -141006,6 +141956,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="rocket.cat" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -141249,6 +142200,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="channel-name" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -141492,6 +142444,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="channel-name" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -141735,6 +142688,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="channel-name" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -141978,6 +142932,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="channel-name" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -142221,6 +143176,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="channel-name" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -142464,6 +143420,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -142707,6 +143664,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -142854,10 +143812,11 @@ exports[`Story Snapshots: Temp should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-344" > <A11yIndexView accessibilityLabel="Temp message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -143321,10 +144280,11 @@ exports[`Story Snapshots: TempLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-345" > <A11yIndexView accessibilityLabel="Temp message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -143796,10 +144756,11 @@ exports[`Story Snapshots: ThumbnailFromServer should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-346" > <A11yIndexView accessibilityLabel="this is a thumbnail" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -144295,6 +145256,7 @@ exports[`Story Snapshots: ThumbnailFromServer should match snapshot 1`] = ` </View> </View> <View + style={false} testID="message-content-this is a thumbnail" > <View @@ -144477,10 +145439,11 @@ exports[`Story Snapshots: ThumbnailFromServerLargeFont should match snapshot 1`] "width": "100%", } } - testID="message-undefined" + testID="message-story-message-347" > <A11yIndexView accessibilityLabel="this is a thumbnail" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -144976,6 +145939,7 @@ exports[`Story Snapshots: ThumbnailFromServerLargeFont should match snapshot 1`] </View> </View> <View + style={false} testID="message-content-this is a thumbnail" > <View @@ -145158,10 +146122,11 @@ exports[`Story Snapshots: TimeFormat should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-348" > <A11yIndexView accessibilityLabel="Testing" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -145446,6 +146411,7 @@ exports[`Story Snapshots: TimeFormat should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Testing" > <View @@ -145628,10 +146594,11 @@ exports[`Story Snapshots: TimeFormatLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-349" > <A11yIndexView accessibilityLabel="Testing" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -145916,6 +146883,7 @@ exports[`Story Snapshots: TimeFormatLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Testing" > <View @@ -146038,7 +147006,7 @@ exports[`Story Snapshots: Translated should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM Message translated into English." + accessibilityLabel="other.user 10:00:00 AM Message translated into English." accessibilityRole="button" accessibilityState={ { @@ -146098,10 +147066,11 @@ exports[`Story Snapshots: Translated should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-350" > <A11yIndexView accessibilityLabel="Message translated" + accessibilityLanguage="en" accessible={true} importantForAccessibility="yes" orderFocusType={0} @@ -146216,7 +147185,7 @@ exports[`Story Snapshots: Translated should match snapshot 1`] = ` } /> <View - accessibilityLabel="diego.mello's avatar" + accessibilityLabel="other.user's avatar" accessible={true} style={{}} > @@ -146244,7 +147213,7 @@ exports[`Story Snapshots: Translated should match snapshot 1`] = ` "headers": { "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", }, - "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=72", + "uri": "https://open.rocket.chat/avatar/other.user?format=png&size=72", }, ] } @@ -146327,7 +147296,7 @@ exports[`Story Snapshots: Translated should match snapshot 1`] = ` "gap": 4, } } - testID="username-header-diego.mello" + testID="username-header-other.user" > <Text numberOfLines={1} @@ -146348,7 +147317,7 @@ exports[`Story Snapshots: Translated should match snapshot 1`] = ` ] } > - diego.mello + other.user </Text> <Text style={ @@ -146422,6 +147391,7 @@ exports[`Story Snapshots: Translated should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message translated" > <View @@ -146544,7 +147514,7 @@ exports[`Story Snapshots: TranslatedLargeFont should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM Message translated into English." + accessibilityLabel="other.user 10:00:00 AM Message translated into English." accessibilityRole="button" accessibilityState={ { @@ -146604,10 +147574,11 @@ exports[`Story Snapshots: TranslatedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-351" > <A11yIndexView accessibilityLabel="Message translated" + accessibilityLanguage="en" accessible={true} importantForAccessibility="yes" orderFocusType={0} @@ -146722,7 +147693,7 @@ exports[`Story Snapshots: TranslatedLargeFont should match snapshot 1`] = ` } /> <View - accessibilityLabel="diego.mello's avatar" + accessibilityLabel="other.user's avatar" accessible={true} style={{}} > @@ -146750,7 +147721,7 @@ exports[`Story Snapshots: TranslatedLargeFont should match snapshot 1`] = ` "headers": { "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", }, - "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=93.60000000000001", + "uri": "https://open.rocket.chat/avatar/other.user?format=png&size=93.60000000000001", }, ] } @@ -146833,7 +147804,7 @@ exports[`Story Snapshots: TranslatedLargeFont should match snapshot 1`] = ` "gap": 4, } } - testID="username-header-diego.mello" + testID="username-header-other.user" > <Text numberOfLines={1} @@ -146854,7 +147825,7 @@ exports[`Story Snapshots: TranslatedLargeFont should match snapshot 1`] = ` ] } > - diego.mello + other.user </Text> </View> <View @@ -146928,6 +147899,7 @@ exports[`Story Snapshots: TranslatedLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message translated" > <View @@ -147110,10 +148082,11 @@ exports[`Story Snapshots: TwoShortCustomFieldsWithMarkdown should match snapshot "width": "100%", } } - testID="message-undefined" + testID="message-story-message-352" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -148114,6 +149087,7 @@ exports[`Story Snapshots: TwoShortCustomFieldsWithMarkdown should match snapshot </View> </View> <View + style={false} testID="message-content-Message" > <View @@ -148296,10 +149270,11 @@ exports[`Story Snapshots: TwoShortCustomFieldsWithMarkdownLargeFont should match "width": "100%", } } - testID="message-undefined" + testID="message-story-message-353" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -149300,6 +150275,7 @@ exports[`Story Snapshots: TwoShortCustomFieldsWithMarkdownLargeFont should match </View> </View> <View + style={false} testID="message-content-Message" > <View @@ -149482,10 +150458,11 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-354" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -150076,10 +151053,11 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-355" > <A11yIndexView accessibilityLabel="Message :nyan_rocket:" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -150364,6 +151342,7 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message :nyan_rocket:" > <View @@ -150675,10 +151654,11 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-356" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -150951,10 +151931,11 @@ exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-358" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -151447,10 +152428,11 @@ exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-359" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -151899,10 +152881,11 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-360" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -152395,10 +153378,11 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-361" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -152847,10 +153831,11 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-362" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -153441,10 +154426,11 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-363" > <A11yIndexView accessibilityLabel="Message :nyan_rocket:" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -153729,6 +154715,7 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message :nyan_rocket:" > <View @@ -154040,10 +155027,11 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-364" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -154316,10 +155304,11 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-366" > <A11yIndexView accessibilityLabel="Small username (2 chars)" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -154604,6 +155593,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Small username (2 chars)" > <View @@ -154686,10 +155676,11 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-367" > <A11yIndexView accessibilityLabel="Small username with icons" + accessibilityLanguage="en" accessible={true} importantForAccessibility="yes" orderFocusType={0} @@ -155246,63 +156237,33 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` } > <View + style={ + { + "opacity": 0.3, + } + } testID="message-content-Small username with icons" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Small username with icons" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Small username with icons - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -155411,10 +156372,11 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-368" > <A11yIndexView accessibilityLabel="Medium username (8 chars)" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -155699,6 +156661,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Medium username (8 chars)" > <View @@ -155781,10 +156744,11 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-369" > <A11yIndexView accessibilityLabel="Medium username with icons" + accessibilityLanguage="en" accessible={true} importantForAccessibility="yes" orderFocusType={0} @@ -156341,63 +157305,33 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` } > <View + style={ + { + "opacity": 0.3, + } + } testID="message-content-Medium username with icons" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Medium username with icons" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Medium username with icons - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -156506,10 +157440,11 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-370" > <A11yIndexView accessibilityLabel="Long username (22 chars) - should truncate" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -156794,6 +157729,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Long username (22 chars) - should truncate" > <View @@ -156876,10 +157812,11 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-371" > <A11yIndexView accessibilityLabel="Long username with icons - timestamp visible" + accessibilityLanguage="en" accessible={true} importantForAccessibility="yes" orderFocusType={0} @@ -157436,63 +158373,33 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` } > <View + style={ + { + "opacity": 0.3, + } + } testID="message-content-Long username with icons - timestamp visible" > - <View + <Text + accessibilityLabel="Encrypted message" style={ - { - "gap": 2, - } + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] } + testID="message-encrypted" > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Long username with icons - timestamp visible" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Long username with icons - timestamp visible - </Text> - </Text> - </Text> - </View> + Encrypted message + </Text> </View> </View> </View> @@ -157614,10 +158521,11 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-372" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -157921,6 +158829,7 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message" > <View @@ -158090,10 +158999,11 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-373" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -158397,6 +159307,7 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message" > <View @@ -158579,10 +159490,11 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-374" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -158886,6 +159798,7 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message" > <View @@ -159055,10 +159968,11 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-375" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -159362,6 +160276,7 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message" > <View @@ -159544,10 +160459,11 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-376" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -160285,10 +161201,11 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-377" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -160749,10 +161666,11 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-379" > <A11yIndexView accessibilityLabel="multi file" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -160797,6 +161715,7 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` } > <View + style={false} testID="message-content-multi file" > <View @@ -161453,10 +162372,11 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-381" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -162446,10 +163366,11 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-383" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -163187,10 +164108,11 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-384" > <A11yIndexView accessibilityLabel="First message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -163235,6 +164157,7 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-First message" > <View @@ -163411,10 +164334,11 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-386" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -163875,10 +164799,11 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-388" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -164284,10 +165209,11 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-390" > <A11yIndexView accessibilityLabel="multi file" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -164332,6 +165258,7 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-multi file" > <View @@ -165336,10 +166263,11 @@ exports[`Story Snapshots: WithFile should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-392" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -165984,10 +166912,11 @@ exports[`Story Snapshots: WithFile should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-393" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -166654,10 +167583,11 @@ exports[`Story Snapshots: WithFileLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-395" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -167302,10 +168232,11 @@ exports[`Story Snapshots: WithFileLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-396" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -167730,10 +168661,11 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-398" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -168404,10 +169336,11 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-399" > <A11yIndexView accessibilityLabel="Message, not description" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -168692,6 +169625,7 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message, not description" > <View @@ -169566,10 +170500,11 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-400" > <A11yIndexView accessibilityLabel="multi file" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -169854,6 +170789,7 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` } > <View + style={false} testID="message-content-multi file" > <View @@ -170353,10 +171289,11 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-401" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -171027,10 +171964,11 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-402" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -171657,10 +172595,11 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-403" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -172054,10 +172993,11 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-405" > <A11yIndexView accessibilityLabel="multi file" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -172342,6 +173282,7 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-multi file" > <View @@ -172841,10 +173782,11 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-406" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -173515,10 +174457,11 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-407" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -174147,10 +175090,11 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-408" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -174502,10 +175446,11 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-410" > <A11yIndexView accessibilityLabel="multi file" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -174790,6 +175735,7 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` } > <View + style={false} testID="message-content-multi file" > <View @@ -175234,10 +176180,11 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-411" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -175866,10 +176813,11 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-412" > <A11yIndexView accessibilityLabel="" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -176399,10 +177347,11 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-413" > <A11yIndexView accessibilityLabel="multi file" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -176687,6 +177636,7 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-multi file" > <View @@ -177131,10 +178081,11 @@ exports[`Story Snapshots: WithoutHeader should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-414" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -177179,6 +178130,7 @@ exports[`Story Snapshots: WithoutHeader should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message" > <View @@ -177368,10 +178320,11 @@ exports[`Story Snapshots: WithoutHeaderLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-416" > <A11yIndexView accessibilityLabel="Message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -177416,6 +178369,7 @@ exports[`Story Snapshots: WithoutHeaderLargeFont should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Message" > <View diff --git a/app/containers/message/hooks/useMessage.test.ts b/app/containers/message/hooks/useMessage.test.ts deleted file mode 100644 index 79de40fbf77..00000000000 --- a/app/containers/message/hooks/useMessage.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -import { act, renderHook } from '@testing-library/react-native'; - -import { type TAnyMessageModel } from 'definitions'; - -import { useMessage } from './useMessage'; - -type Subscriber = () => void; - -const buildFakeModel = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel & { _emit: () => void } => { - const subscribers: Subscriber[] = []; - - const model = { - id: 'msg-1', - msg: 'Hello world', - t: undefined, - ts: new Date('2024-01-01T00:00:00Z'), - u: { _id: 'u1', username: 'alice' }, - alias: undefined, - groupable: true, - avatar: undefined, - emoji: undefined, - attachments: [{ image_url: 'https://example.com/img.png' }], - urls: [], - status: 0, - pinned: false, - editedBy: null, - reactions: { '👍': { usernames: ['alice'] } }, - role: undefined, - drid: undefined, - dcount: undefined, - dlm: undefined, - tmid: undefined, - tcount: undefined, - tlm: undefined, - replies: ['u1'], - mentions: [], - channels: [], - unread: false, - autoTranslate: false, - translations: null, - tmsg: undefined, - blocks: null, - e2e: undefined, - md: [{ type: 'PARAGRAPH' }], - comment: undefined, - ...overrides, - experimentalSubscribe(cb: Subscriber) { - subscribers.push(cb); - return () => { - const idx = subscribers.indexOf(cb); - if (idx !== -1) subscribers.splice(idx, 1); - }; - }, - _emit() { - subscribers.forEach(cb => cb()); - } - } as unknown as TAnyMessageModel & { _emit: () => void }; - - return model; -}; - -describe('useMessage', () => { - it('returns a snapshot with the correct initial field values', () => { - const model = buildFakeModel(); - const { result } = renderHook(() => useMessage(model)); - - expect(result.current.id).toBe('msg-1'); - expect(result.current.msg).toBe('Hello world'); - expect(result.current.u).toEqual({ _id: 'u1', username: 'alice' }); - expect(result.current.ts).toEqual(new Date('2024-01-01T00:00:00Z')); - expect(result.current.attachments).toBe(model.attachments); - expect(result.current.reactions).toBe(model.reactions); - expect(result.current.md).toBe(model.md); - expect(result.current.replies).toBe(model.replies); - }); - - it('returns a new snapshot reflecting the changed field after a model mutation', () => { - const model = buildFakeModel(); - const { result } = renderHook(() => useMessage(model)); - const before = result.current; - - act(() => { - (model as any).msg = 'Updated message'; - model._emit(); - }); - - expect(result.current).not.toBe(before); - expect(result.current.msg).toBe('Updated message'); - }); - - it('keeps the same reference for an unchanged @json field across an unrelated mutation', () => { - const model = buildFakeModel(); - const { result } = renderHook(() => useMessage(model)); - - const attachmentsBefore = result.current.attachments; - const reactionsBefore = result.current.reactions; - const mdBefore = result.current.md; - - act(() => { - (model as any).msg = 'Changed msg only'; - model._emit(); - }); - - expect(result.current.attachments).toBe(attachmentsBefore); - expect(result.current.reactions).toBe(reactionsBefore); - expect(result.current.md).toBe(mdBefore); - }); - - it('returns the new model snapshot synchronously when item identity changes', () => { - const modelA = buildFakeModel({ id: 'msg-a', msg: 'Model A' }); - const modelB = buildFakeModel({ id: 'msg-b', msg: 'Model B' }); - - const { result, rerender } = renderHook(({ model }: { model: typeof modelA }) => useMessage(model), { - initialProps: { model: modelA } - }); - - expect(result.current.id).toBe('msg-a'); - expect(result.current.msg).toBe('Model A'); - - act(() => { - rerender({ model: modelB }); - }); - - expect(result.current.id).toBe('msg-b'); - expect(result.current.msg).toBe('Model B'); - }); - - it('does not throw and returns a correct snapshot for a plain REST object without experimentalSubscribe', () => { - const plainItem = { - id: 'rest-msg-1', - msg: 'Plain REST message', - t: undefined, - ts: new Date('2024-06-01T00:00:00Z'), - u: { _id: 'u2', username: 'bob' }, - alias: undefined, - groupable: true, - avatar: undefined, - emoji: undefined, - attachments: undefined, - urls: [], - status: 0, - pinned: false, - editedBy: undefined, - reactions: undefined, - role: undefined, - drid: undefined, - dcount: undefined, - dlm: undefined, - tmid: undefined, - tcount: undefined, - tlm: undefined, - replies: undefined, - mentions: [], - channels: [], - unread: false, - autoTranslate: false, - translations: undefined, - tmsg: undefined, - blocks: undefined, - e2e: undefined, - md: undefined, - comment: undefined - } as unknown as TAnyMessageModel; - - let caughtError: unknown; - let hookResult: ReturnType<typeof useMessage> | undefined; - - try { - const { result } = renderHook(() => useMessage(plainItem)); - hookResult = result.current; - } catch (e) { - caughtError = e; - } - - expect(caughtError).toBeUndefined(); - expect(hookResult?.id).toBe('rest-msg-1'); - expect(hookResult?.msg).toBe('Plain REST message'); - }); -}); diff --git a/app/containers/message/hooks/useMessage.ts b/app/containers/message/hooks/useMessage.ts deleted file mode 100644 index 6dca3dc9183..00000000000 --- a/app/containers/message/hooks/useMessage.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { useCallback, useRef, useSyncExternalStore } from 'react'; -import { type Root } from '@rocket.chat/message-parser'; - -import { - type IAttachment, - type IEditedBy, - type IMessage, - type IMessageTranslations, - type IReaction, - type IUrl, - type IUserChannel, - type IUserMention, - type IUserMessage, - type TAnyMessageModel -} from 'definitions'; - -export type IMessageSnapshot = Pick< - IMessage, - | 'id' - | 'msg' - | 't' - | 'ts' - | 'alias' - | 'groupable' - | 'avatar' - | 'emoji' - | 'status' - | 'pinned' - | 'role' - | 'drid' - | 'dcount' - | 'dlm' - | 'tmid' - | 'tcount' - | 'tlm' - | 'unread' - | 'autoTranslate' - | 'tmsg' - | 'blocks' - | 'e2e' - | 'comment' -> & { - u: IUserMessage | undefined; - attachments: IAttachment[] | undefined; - urls: IUrl[] | undefined; - editedBy: IEditedBy | undefined; - reactions: IReaction[] | undefined; - replies: string[] | undefined; - mentions: IUserMention[] | undefined; - channels: IUserChannel[] | undefined; - translations: IMessageTranslations[] | undefined; - md: Root | undefined; -}; - -const readSnapshot = (item: TAnyMessageModel): IMessageSnapshot => ({ - id: item.id, - msg: item.msg, - t: item.t, - ts: item.ts, - u: item.u, - alias: item.alias, - groupable: item.groupable, - avatar: item.avatar, - emoji: item.emoji, - attachments: item.attachments, - urls: item.urls, - status: item.status, - pinned: item.pinned, - editedBy: item.editedBy, - reactions: item.reactions, - role: item.role, - drid: item.drid, - dcount: item.dcount, - dlm: item.dlm, - tmid: item.tmid, - tcount: item.tcount, - tlm: item.tlm, - replies: item.replies, - mentions: item.mentions, - channels: item.channels, - unread: item.unread, - autoTranslate: item.autoTranslate, - translations: item.translations, - tmsg: item.tmsg, - blocks: item.blocks, - e2e: item.e2e, - md: item.md, - comment: item.comment -}); - -type SnapshotCache = { item: TAnyMessageModel; snapshot: IMessageSnapshot }; - -export const useMessage = (item: TAnyMessageModel): IMessageSnapshot => { - 'use memo'; - - // Cache the latest snapshot so getSnapshot returns the same object reference - // when nothing has changed — useSyncExternalStore requires this to avoid loops. - const cacheRef = useRef<SnapshotCache | null>(null); - - // getSnapshot and subscribe both close over item directly and are recreated - // when item identity changes (useCallback with [item] dep). This lets - // useSyncExternalStore resubscribe on item change without any render-body - // ref writes. - - const getSnapshot = useCallback((): IMessageSnapshot => { - if (!cacheRef.current || cacheRef.current.item !== item) { - cacheRef.current = { item, snapshot: readSnapshot(item) }; - } - return cacheRef.current.snapshot; - }, [item]); - - const subscribe = useCallback( - (onStoreChange: () => void): (() => void) => { - // @ts-ignore: experimentalSubscribe is not yet in WatermelonDB's TS types - if (typeof item.experimentalSubscribe !== 'function') { - return () => {}; - } - // @ts-ignore - const unsubscribe = item.experimentalSubscribe(() => { - cacheRef.current = { item, snapshot: readSnapshot(item) }; - onStoreChange(); - }); - return unsubscribe; - }, - [item] - ); - - return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); -}; diff --git a/app/containers/message/hooks/useMessageAccessibilityHint.test.ts b/app/containers/message/hooks/useMessageAccessibilityHint.test.ts deleted file mode 100644 index a93bf029caa..00000000000 --- a/app/containers/message/hooks/useMessageAccessibilityHint.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { renderHook } from '@testing-library/react-native'; - -import { useMessageAccessibilityHint } from './useMessageAccessibilityHint'; - -describe('useMessageAccessibilityHint', () => { - it('returns the view thread hint when the message has a thread', () => { - const { result } = renderHook(() => useMessageAccessibilityHint({ tlm: new Date(), tcount: 1, isThreadRoom: false })); - expect(result.current).toBe('Press to view thread'); - }); - - it('returns undefined when there is no thread', () => { - const { result } = renderHook(() => useMessageAccessibilityHint({ tlm: undefined, tcount: null, isThreadRoom: false })); - expect(result.current).toBeUndefined(); - }); - - it('returns undefined when rendered inside a thread room', () => { - const { result } = renderHook(() => useMessageAccessibilityHint({ tlm: new Date(), tcount: 1, isThreadRoom: true })); - expect(result.current).toBeUndefined(); - }); -}); diff --git a/app/containers/message/hooks/useMessageAccessibilityHint.test.tsx b/app/containers/message/hooks/useMessageAccessibilityHint.test.tsx new file mode 100644 index 00000000000..c2d86e24523 --- /dev/null +++ b/app/containers/message/hooks/useMessageAccessibilityHint.test.tsx @@ -0,0 +1,43 @@ +import { render } from '@testing-library/react-native'; + +import { useMessageAccessibilityHint } from './useMessageAccessibilityHint'; +import MessageContext, { type IMessageContext } from '../Context'; +import { MessageProvider } from '../MessageStore'; +import { type TAnyMessageModel } from '../../../definitions'; + +const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel => + ({ id: 'msg-1', ...overrides } as TAnyMessageModel); + +const renderHint = (item: TAnyMessageModel, config: Partial<IMessageContext> = {}) => { + const spy = jest.fn(); + const Probe = () => { + spy(useMessageAccessibilityHint()); + return null; + }; + render( + <MessageContext.Provider value={config as IMessageContext}> + <MessageProvider item={item}> + <Probe /> + </MessageProvider> + </MessageContext.Provider> + ); + const { calls } = spy.mock; + return calls[calls.length - 1][0]; +}; + +describe('useMessageAccessibilityHint', () => { + it('returns the view thread hint when the message has a thread', () => { + const result = renderHint(buildItem({ tlm: new Date(), tcount: 1 }), { isThreadRoom: false }); + expect(result).toBe('Press to view thread'); + }); + + it('returns undefined when there is no thread', () => { + const result = renderHint(buildItem({ tlm: undefined, tcount: null }), { isThreadRoom: false }); + expect(result).toBeUndefined(); + }); + + it('returns undefined when rendered inside a thread room', () => { + const result = renderHint(buildItem({ tlm: new Date(), tcount: 1 }), { isThreadRoom: true }); + expect(result).toBeUndefined(); + }); +}); diff --git a/app/containers/message/hooks/useMessageAccessibilityHint.ts b/app/containers/message/hooks/useMessageAccessibilityHint.ts index 15ace38b9f1..31988d90ee8 100644 --- a/app/containers/message/hooks/useMessageAccessibilityHint.ts +++ b/app/containers/message/hooks/useMessageAccessibilityHint.ts @@ -1,11 +1,15 @@ +import { useContext } from 'react'; + import i18n from '../../../i18n'; -import { type IMessageThread } from '../interfaces'; +import MessageContext from '../Context'; +import { useThreadData } from '../MessageStore'; -export const useMessageAccessibilityHint = ( - props: Pick<IMessageThread, 'tlm' | 'tcount' | 'isThreadRoom'> -): string | undefined => { +export const useMessageAccessibilityHint = (): string | undefined => { 'use memo'; - const hasThread = !!props.tlm && !props.isThreadRoom && props.tcount !== null; + const { tlm, tcount } = useThreadData(); + const { isThreadRoom } = useContext(MessageContext); + + const hasThread = !!tlm && !isThreadRoom && tcount !== null; return hasThread ? i18n.t('A11y_press_to_view_thread') : undefined; }; diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.test.ts b/app/containers/message/hooks/useMessageAccessibilityLabel.test.ts deleted file mode 100644 index 149acec7f27..00000000000 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { renderHook } from '@testing-library/react-native'; - -import { useMessageAccessibilityLabel } from './useMessageAccessibilityLabel'; -import { type IMessageContent, type IMessageTouchable } from '../interfaces'; - -jest.mock('../../../lib/hooks/useAltTextSupported', () => ({ - useAltTextSupported: () => false -})); - -type TProps = IMessageContent & - IMessageTouchable & { isThreadSequential?: boolean; isReadReceiptEnabled?: boolean; unread?: boolean | null }; - -const FIXED_TS = new Date('2024-01-15T12:34:56Z'); -const HOUR = FIXED_TS.toLocaleTimeString(); - -const buildProps = (overrides: Partial<TProps> = {}): TProps => - ({ - msg: 'hello world', - ts: FIXED_TS, - author: { _id: 'u1', username: 'alice', name: 'Alice' }, - isInfo: false, - isThreadReply: false, - isThreadSequential: false, - isEncrypted: false, - isTranslated: false, - isReadReceiptEnabled: false, - unread: false, - useRealName: false, - ...overrides - } as TProps); - -describe('useMessageAccessibilityLabel', () => { - it('builds the default label with user, hour and message', () => { - const { result } = renderHook(() => useMessageAccessibilityLabel(buildProps())); - expect(result.current).toBe(`alice ${HOUR} hello world.`); - }); - - it('prefixes the message with "thread message" when tmid is set', () => { - const { result } = renderHook(() => useMessageAccessibilityLabel(buildProps({ tmid: 't1' }))); - expect(result.current).toBe(`alice ${HOUR} thread message hello world.`); - }); - - it('uses "replying to" for a thread reply', () => { - const { result } = renderHook(() => useMessageAccessibilityLabel(buildProps({ isThreadReply: true }))); - expect(result.current).toBe(`alice ${HOUR} replying to hello world.`); - }); - - it('uses "replying to thread message" for a thread reply with tmid', () => { - const { result } = renderHook(() => useMessageAccessibilityLabel(buildProps({ isThreadReply: true, tmid: 't1' }))); - expect(result.current).toBe(`alice ${HOUR} replying to thread message hello world.`); - }); - - it('uses "thread message" for a sequential thread message', () => { - const { result } = renderHook(() => useMessageAccessibilityLabel(buildProps({ isThreadSequential: true }))); - expect(result.current).toBe(`alice ${HOUR} thread message hello world.`); - }); - - it('replaces the body with the encrypted placeholder and appends it to the suffix', () => { - const { result } = renderHook(() => useMessageAccessibilityLabel(buildProps({ isEncrypted: true }))); - expect(result.current).toBe(`alice ${HOUR} Encrypted message. Encrypted message`); - }); - - it('uses the author real name when useRealName is true', () => { - const { result } = renderHook(() => useMessageAccessibilityLabel(buildProps({ useRealName: true }))); - expect(result.current).toBe(`Alice ${HOUR} hello world.`); - }); - - it('omits the hour when ts is missing', () => { - const { result } = renderHook(() => useMessageAccessibilityLabel(buildProps({ ts: undefined }))); - expect(result.current).toBe('alice hello world.'); - }); - - it('strips @username and #channel sigils via mentions/channels lists', () => { - const { result } = renderHook(() => - useMessageAccessibilityLabel( - buildProps({ - msg: 'hey @alice check #general', - mentions: [{ _id: 'u1', username: 'alice', name: 'Alice', type: 'user' }], - channels: [{ _id: 'c1', name: 'general' }] - }) - ) - ); - expect(result.current).toBe(`alice ${HOUR} hey alice check general.`); - }); - - it('appends "Message was read" when read receipts are enabled and the message is read', () => { - const { result } = renderHook(() => useMessageAccessibilityLabel(buildProps({ isReadReceiptEnabled: true, unread: false }))); - expect(result.current).toBe(`alice ${HOUR} hello world. Message was read`); - }); - - it('appends "Message was not read" when read receipts are enabled and the message is unread', () => { - const { result } = renderHook(() => useMessageAccessibilityLabel(buildProps({ isReadReceiptEnabled: true, unread: true }))); - expect(result.current).toBe(`alice ${HOUR} hello world. Message was not read`); - }); - - it('omits the read-receipt suffix for info messages', () => { - const { result } = renderHook(() => - useMessageAccessibilityLabel( - buildProps({ isReadReceiptEnabled: true, isInfo: true, type: 'uj', author: { _id: 'u1', username: 'alice' } }) - ) - ); - expect(result.current).toBe(`alice ${HOUR} joined the channel.`); - }); - - it('appends the image description to the suffix', () => { - const { result } = renderHook(() => - useMessageAccessibilityLabel( - buildProps({ - msg: 'caption', - attachments: [{ image_url: 'https://example.com/img.png', altText: 'A wavy pattern' }] - }) - ) - ); - expect(result.current).toBe(`alice ${HOUR} caption. Image description: A wavy pattern`); - }); - - it('does not announce "undefined" for attachment-only messages', () => { - const { result } = renderHook(() => - useMessageAccessibilityLabel( - buildProps({ - msg: undefined, - attachments: [{ image_url: 'https://example.com/img.png', altText: 'A wavy pattern' }] - }) - ) - ); - expect(result.current).toBe(`alice ${HOUR}. Image description: A wavy pattern`); - }); - - it('builds a translated message with only user, hour and translated marker as prefix', () => { - const { result } = renderHook(() => - useMessageAccessibilityLabel(buildProps({ isTranslated: true, autoTranslateLanguage: 'en' })) - ); - expect(result.current).toBe(`alice ${HOUR} Message translated into English.`); - }); - - it('still appends suffix metadata (image description, encryption, read receipt) on translated messages', () => { - const { result } = renderHook(() => - useMessageAccessibilityLabel( - buildProps({ - msg: 'caption', - isTranslated: true, - autoTranslateLanguage: 'en', - isReadReceiptEnabled: true, - unread: true, - attachments: [{ image_url: 'https://example.com/img.png', altText: 'A wavy pattern' }] - }) - ) - ); - expect(result.current).toBe( - `alice ${HOUR} Message translated into English. Image description: A wavy pattern Message was not read` - ); - }); - - it('falls back to English when autoTranslateLanguage is missing', () => { - const { result } = renderHook(() => useMessageAccessibilityLabel(buildProps({ isTranslated: true }))); - expect(result.current).toBe(`alice ${HOUR} Message translated into English.`); - }); -}); diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx b/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx new file mode 100644 index 00000000000..06cdf179930 --- /dev/null +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx @@ -0,0 +1,149 @@ +import { render } from '@testing-library/react-native'; + +import { useMessageAccessibilityLabel } from './useMessageAccessibilityLabel'; +import MessageContext, { type IMessageContext } from '../Context'; +import { MessageProvider, type MessagePrev } from '../MessageStore'; +import { type TAnyMessageModel } from '../../../definitions'; +import { E2E_MESSAGE_TYPE } from '../../../lib/constants/keys'; + +jest.mock('../../../lib/hooks/useAltTextSupported', () => ({ + useAltTextSupported: () => false +})); + +type TArgs = { useRealName?: boolean; isReadReceiptEnabled?: boolean; autoTranslateLanguage?: string }; + +const FIXED_TS = new Date('2024-01-15T12:34:56Z'); +const HOUR = FIXED_TS.toLocaleTimeString(); + +const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel => + ({ + id: 'msg-1', + msg: 'hello world', + ts: FIXED_TS, + u: { _id: 'u1', username: 'alice', name: 'Alice' }, + unread: false, + ...overrides + } as TAnyMessageModel); + +const renderLabel = (item: TAnyMessageModel, args: TArgs = {}, config: Partial<IMessageContext> = {}, prev?: MessagePrev) => { + const spy = jest.fn(); + const Probe = () => { + spy(useMessageAccessibilityLabel(args)); + return null; + }; + render( + <MessageContext.Provider value={config as IMessageContext}> + <MessageProvider item={item} prev={prev}> + <Probe /> + </MessageProvider> + </MessageContext.Provider> + ); + const { calls } = spy.mock; + return calls[calls.length - 1][0]; +}; + +describe('useMessageAccessibilityLabel', () => { + it('builds the default label with user, hour and message', () => { + expect(renderLabel(buildItem())).toBe(`alice ${HOUR} hello world.`); + }); + + it('prefixes the message with "thread message" when the message has a tmid', () => { + expect(renderLabel(buildItem({ tmid: 't1' }))).toBe(`alice ${HOUR} thread message hello world.`); + }); + + it('replaces the body with the encrypted placeholder and appends it to the suffix', () => { + expect(renderLabel(buildItem({ t: E2E_MESSAGE_TYPE, e2e: 'pending' }))).toBe( + `alice ${HOUR} Encrypted message. Encrypted message` + ); + }); + + it('uses the author real name when useRealName is true', () => { + expect(renderLabel(buildItem(), { useRealName: true })).toBe(`Alice ${HOUR} hello world.`); + }); + + it('omits the hour when ts is missing', () => { + expect(renderLabel(buildItem({ ts: undefined }))).toBe('alice hello world.'); + }); + + it('strips @username and #channel sigils via mentions/channels lists', () => { + expect( + renderLabel( + buildItem({ + msg: 'hey @alice check #general', + mentions: [{ _id: 'u1', username: 'alice', name: 'Alice', type: 'user' }], + channels: [{ _id: 'c1', name: 'general' }] + }) + ) + ).toBe(`alice ${HOUR} hey alice check general.`); + }); + + it('appends "Message was read" when read receipts are enabled and the message is read', () => { + expect(renderLabel(buildItem({ unread: false }), { isReadReceiptEnabled: true })).toBe( + `alice ${HOUR} hello world. Message was read` + ); + }); + + it('appends "Message was not read" when read receipts are enabled and the message is unread', () => { + expect(renderLabel(buildItem({ unread: true }), { isReadReceiptEnabled: true })).toBe( + `alice ${HOUR} hello world. Message was not read` + ); + }); + + it('omits the read-receipt suffix for info messages', () => { + expect(renderLabel(buildItem({ t: 'uj', u: { _id: 'u1', username: 'alice' } }), { isReadReceiptEnabled: true })).toBe( + `alice ${HOUR} joined the channel.` + ); + }); + + it('appends the image description to the suffix', () => { + expect( + renderLabel( + buildItem({ msg: 'caption', attachments: [{ image_url: 'https://example.com/img.png', altText: 'A wavy pattern' }] }) + ) + ).toBe(`alice ${HOUR} caption. Image description: A wavy pattern`); + }); + + it('does not announce "undefined" for attachment-only messages', () => { + expect( + renderLabel( + buildItem({ msg: undefined, attachments: [{ image_url: 'https://example.com/img.png', altText: 'A wavy pattern' }] }) + ) + ).toBe(`alice ${HOUR}. Image description: A wavy pattern`); + }); + + it('builds a translated message with only user, hour and translated marker as prefix', () => { + expect( + renderLabel( + buildItem({ autoTranslate: true, translations: [{ _id: 't1', language: 'en', value: 'translated text' }] }), + { autoTranslateLanguage: 'en' }, + { autoTranslateRoom: true, autoTranslateLanguage: 'en', user: { username: 'bob' } } + ) + ).toBe(`alice ${HOUR} Message translated into English.`); + }); + + it('still appends suffix metadata (image description, encryption, read receipt) on translated messages', () => { + expect( + renderLabel( + buildItem({ + msg: 'caption', + autoTranslate: true, + translations: [{ _id: 't1', language: 'en', value: 'translated text' }], + unread: true, + attachments: [{ image_url: 'https://example.com/img.png', altText: 'A wavy pattern' }] + }), + { autoTranslateLanguage: 'en', isReadReceiptEnabled: true }, + { autoTranslateRoom: true, autoTranslateLanguage: 'en', user: { username: 'bob' } } + ) + ).toBe(`alice ${HOUR} Message translated into English. Image description: A wavy pattern Message was not read`); + }); + + it('falls back to English when autoTranslateLanguage is missing', () => { + expect( + renderLabel( + buildItem({ autoTranslate: true, translations: [{ _id: 't1', language: 'en', value: 'translated text' }] }), + {}, + { autoTranslateRoom: true, autoTranslateLanguage: 'en', user: { username: 'bob' } } + ) + ).toBe(`alice ${HOUR} Message translated into English.`); + }); +}); diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.ts b/app/containers/message/hooks/useMessageAccessibilityLabel.ts index 06d5b45f7de..8980b803e79 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.ts +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.ts @@ -1,17 +1,20 @@ import i18n from '../../../i18n'; import translationLanguages from '../../../lib/constants/translationLanguages'; import { useImageDescriptionLabel } from './useImageDescriptionLabel'; -import { type IMessageContent, type IMessageTouchable } from '../interfaces'; import { getInfoMessage } from '../utils'; +import { type IUserChannel, type IUserMention } from '../../../definitions'; +import { + useContentData, + useIsEncrypted, + useIsInfo, + useMessageAuthor, + useMessageMeta, + useMessageText, + useThreadData, + useThreadPosition +} from '../MessageStore'; -type TMessageLabelProps = IMessageContent & - IMessageTouchable & { - isThreadSequential?: boolean; - isReadReceiptEnabled?: boolean; - unread?: boolean | null; - }; - -const stripMentions = (label: string, mentions: IMessageContent['mentions'] = [], channels: IMessageContent['channels'] = []) => { +const stripMentions = (label: string, mentions: IUserMention[] = [], channels: IUserChannel[] = []) => { let result = label; mentions?.forEach(item => { if (item?.username) { @@ -26,36 +29,53 @@ const stripMentions = (label: string, mentions: IMessageContent['mentions'] = [] return result; }; -export const useMessageAccessibilityLabel = (props: TMessageLabelProps): string => { - const imageDescriptionLabel = useImageDescriptionLabel(props.attachments, props.msg); - const msg = props?.msg || ''; +export const useMessageAccessibilityLabel = ({ + useRealName, + isReadReceiptEnabled, + autoTranslateLanguage +}: { + useRealName?: boolean; + isReadReceiptEnabled?: boolean; + autoTranslateLanguage?: string; +}): string => { + const { attachments, mentions, channels, comment, t: type } = useContentData(); + const { u: author, role } = useMessageAuthor(); + const { messageText, isTranslated } = useMessageText(); + const { tmid } = useThreadData(); + const { isThreadReply, isThreadSequential } = useThreadPosition(); + const isInfo = useIsInfo(); + const isEncrypted = useIsEncrypted(); + const { ts, unread } = useMessageMeta(); + + const imageDescriptionLabel = useImageDescriptionLabel(attachments, messageText); + const msg = messageText || ''; const threadMessageLabel = i18n.t('Thread_message', { msg }); - let label = props.isInfo ? msg : `${props.tmid ? threadMessageLabel : msg}`; - if (props.isThreadReply) { - label = i18n.t('Thread_reply', { msg: props.tmid ? threadMessageLabel : msg }); + let label = isInfo ? msg : `${tmid ? threadMessageLabel : msg}`; + if (isThreadReply) { + label = i18n.t('Thread_reply', { msg: tmid ? threadMessageLabel : msg }); } - if (props.isThreadSequential) { + if (isThreadSequential) { label = threadMessageLabel; } - if (props.isEncrypted) { + if (isEncrypted) { label = i18n.t('Encrypted_message'); } - if (props.isInfo) { + if (isInfo) { // @ts-ignore - label = getInfoMessage({ ...props }); + label = getInfoMessage({ type, role, msg, author, comment }); } - label = stripMentions(label, props.mentions, props.channels); + label = stripMentions(label, mentions, channels); - const hour = props.ts ? new Date(props.ts as Date).toLocaleTimeString() : ''; - const user = props.useRealName ? props.author?.name : props.author?.username || ''; - const readOrUnreadLabel = !props.unread && props.unread !== null ? i18n.t('Message_was_read') : i18n.t('Message_was_not_read'); - const readReceipt = props.isReadReceiptEnabled && !props.isInfo ? readOrUnreadLabel : ''; - const encryptedMessageLabel = props.isEncrypted ? i18n.t('Encrypted_message') : ''; - const translatedLanguage = translationLanguages[props?.autoTranslateLanguage || 'en']; - const translated = props.isTranslated ? i18n.t('Message_translated_into_idiom', { idiom: translatedLanguage }) : ''; + const hour = ts ? new Date(ts as Date).toLocaleTimeString() : ''; + const user = useRealName ? author?.name : author?.username || ''; + const readOrUnreadLabel = !unread && unread !== null ? i18n.t('Message_was_read') : i18n.t('Message_was_not_read'); + const readReceipt = isReadReceiptEnabled && !isInfo ? readOrUnreadLabel : ''; + const encryptedMessageLabel = isEncrypted ? i18n.t('Encrypted_message') : ''; + const translatedLanguage = translationLanguages[autoTranslateLanguage || 'en']; + const translated = isTranslated ? i18n.t('Message_translated_into_idiom', { idiom: translatedLanguage }) : ''; // For translated messages, the translated body is announced by the inner A11y.Index node, so the outer label // only carries the metadata (user, hour, translated marker) and the suffix (image description, encryption, read receipt). - const prefix = props.isTranslated + const prefix = isTranslated ? [user, hour, translated].filter(Boolean).join(' ') : [user, hour, translated, label].filter(Boolean).join(' '); const suffix = [imageDescriptionLabel, encryptedMessageLabel, readReceipt].filter(Boolean).join(' '); diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 1f4c668ebfc..89c1a2e7a1a 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -4,16 +4,13 @@ import { Keyboard } from 'react-native'; import Message from './Message'; import MessageContext from './Context'; import { debounce } from '../../lib/methods/helpers'; -import { getMessageTranslation } from './utils'; import { type TSupportedThemes, useTheme } from '../../theme'; import openLink from '../../lib/methods/helpers/openLink'; import { type IAttachment, type TAnyMessageModel, type TGetCustomEmoji } from '../../definitions'; import { type IRoomInfoParam } from '../../views/SearchMessagesView'; -import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; -import { messagesStatus } from '../../lib/constants/messagesStatus'; import MessageSeparator from '../MessageSeparator'; -import { type IMessageSnapshot, useMessage } from './hooks/useMessage'; import { useIsBeingEdited } from '../../views/RoomView/InteractionStore'; +import { MessageProvider, type MessagePrev, useIsEncrypted, useIsInfo, useMessageStatus } from './MessageStore'; interface IMessageContainerProps { item: TAnyMessageModel; @@ -66,63 +63,38 @@ interface IMessageContainerProps { showUnreadSeparator?: boolean; } -// Extracted at module scope so the try/catch does not prevent React Compiler from -// memoising MessageContainer (the compiler cannot optimise try/catch in render). -function computeIsHeader( - previousItem: TAnyMessageModel | undefined, - message: IMessageSnapshot, - broadcast: boolean, - Message_GroupingPeriod: number | undefined, - hasError: boolean -): boolean { - if (hasError || (previousItem && previousItem.status === messagesStatus.ERROR)) { - return true; - } - try { - if ( - previousItem && - // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense - previousItem.ts.toDateString() === message.ts.toDateString() && - previousItem.u?.username === message.u?.username && - !(previousItem.groupable === false || message.groupable === false || broadcast === true) && - // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense - message.ts - previousItem.ts < Message_GroupingPeriod * 1000 && - previousItem.tmid === message.tmid && - message.t !== 'rm' && - previousItem.t !== 'rm' - ) { - return false; - } - return true; - } catch { - return true; - } -} - // `item` is intentionally omitted: the FlatList keys each row by item.id, so a -// different message remounts rather than re-rendering with a new item prop, and -// useMessage recomputes its snapshot on item-identity change. +// different message remounts rather than re-rendering with a new item prop; in-place +// field updates on the same item are handled by the per-message Zustand store (see +// MessageProvider/MessageStore), not by this comparison. function areEqual(prev: IMessageContainerProps, next: IMessageContainerProps): boolean { + const p = prev.previousItem; + const n = next.previousItem; return ( prev.showUnreadSeparator === next.showUnreadSeparator && prev.dateSeparator === next.dateSeparator && prev.highlighted === next.highlighted && prev.threadBadgeColor === next.threadBadgeColor && prev.isIgnored === next.isIgnored && - prev.previousItem?.id === next.previousItem?.id && + p?.id === n?.id && + p?.status === n?.status && + p?.ts === n?.ts && + p?.u?.username === n?.u?.username && + p?.groupable === n?.groupable && + p?.tmid === n?.tmid && + p?.t === n?.t && prev.autoTranslateRoom === next.autoTranslateRoom && prev.autoTranslateLanguage === next.autoTranslateLanguage ); } -const MessageContainer = ({ +const MessageContainerInner = ({ item, user, rid, timeFormat, archived = false, broadcast = false, - previousItem, baseUrl, Message_GroupingPeriod, isReadReceiptEnabled, @@ -159,43 +131,15 @@ const MessageContainer = ({ }: IMessageContainerProps) => { 'use memo'; - const message = useMessage(item); const isBeingEdited = useIsBeingEdited(item.id); const { theme } = useTheme(); const [isManualUnignored, setIsManualUnignored] = useState(false); - // Derived values - const hasError = message.status === messagesStatus.ERROR; - - const isHeader = computeIsHeader(previousItem, message, broadcast, Message_GroupingPeriod, hasError); - - const isThreadReply = (() => { - if (isThreadRoom) { - return false; - } - if (previousItem && message.tmid && previousItem.tmid !== message.tmid && previousItem.id !== message.tmid) { - return true; - } - return false; - })(); - - const isThreadSequential = (() => { - if (isThreadRoom) { - return false; - } - return !!message.tmid; - })(); - - const isEncrypted = message.t === E2E_MESSAGE_TYPE && message.e2e !== E2E_STATUS.DONE; - - const isInfo: string | boolean = (() => { - if (['e2e', 'discussion-created', 'jitsi_call_started', 'videoconf'].includes(message.t)) { - return false; - } - return message.t; - })(); - - const isTemp = message.status === messagesStatus.TEMP || message.status === messagesStatus.ERROR; + // Sourced via the per-message store (rather than read directly off `item`) so this + // component re-renders and refreshes the Context value/closures below when they change. + const { hasError } = useMessageStatus(); + const isEncrypted = useIsEncrypted(); + const isInfo = useIsInfo(); const isIgnored = isManualUnignored ? false : isIgnoredProp ?? false; @@ -211,7 +155,7 @@ const MessageContainer = ({ const onReactionPress = (emoji: string) => { if (onReactionPressProp) { - onReactionPressProp(emoji, message.id); + onReactionPressProp(emoji, item.id); } }; @@ -247,7 +191,7 @@ const MessageContainer = ({ const reactionInit = () => { if (reactionInitProp) { - reactionInitProp(message.id); + reactionInitProp(item.id); } }; @@ -258,7 +202,7 @@ const MessageContainer = ({ }; const onLinkPress = (link: string): void => { - const isMessageLink = !!message.attachments?.some((att: IAttachment) => att?.message_link === link); + const isMessageLink = !!item.attachments?.some((att: IAttachment) => att?.message_link === link); if (isMessageLink && jumpToMessage) { return jumpToMessage(link); } @@ -289,11 +233,11 @@ const MessageContainer = ({ Keyboard.dismiss(); - if ((message.tlm || message.tmid) && !isThreadRoom) { + if ((item.tlm || item.tmid) && !isThreadRoom) { onThreadPress(); } - if (message.dlm && onDiscussionPressProp) { + if (item.dlm && onDiscussionPressProp) { onDiscussionPressProp(item); } }; @@ -313,53 +257,13 @@ const MessageContainer = ({ return onPressRef.current(); }; - const { - id, - msg, - ts, - attachments, - urls, - reactions, - t, - avatar, - emoji, - u, - alias, - editedBy, - role, - drid, - dcount, - dlm, - tmid, - tcount, - tlm, - tmsg, - mentions, - channels, - unread, - blocks, - autoTranslate: autoTranslateMessage, - replies, - md, - comment, - pinned - } = message; - - let messageText = msg; - let isTranslated = false; - const otherUserMessage = u?.username !== user?.username; - if (autoTranslateRoom && autoTranslateMessage && autoTranslateLanguage && otherUserMessage) { - const messageTranslated = getMessageTranslation(message, autoTranslateLanguage); - isTranslated = !!messageTranslated; - messageText = messageTranslated || messageText; - } - - const canTranslateMessage = autoTranslateRoom && autoTranslateLanguage && autoTranslateMessage !== false && otherUserMessage; + const otherUserMessage = item.u?.username !== user?.username; + const canTranslateMessage = autoTranslateRoom && autoTranslateLanguage && item.autoTranslate !== false && otherUserMessage; return ( <MessageContext.Provider value={{ - id, + id: item.id, rid, user, baseUrl, @@ -378,7 +282,7 @@ const MessageContainer = ({ jumpToMessage, threadBadgeColor, toggleFollowThread, - replies, + replies: item.replies, translateLanguage: canTranslateMessage ? autoTranslateLanguage : undefined, isEncrypted, getCustomEmoji, @@ -386,55 +290,25 @@ const MessageContainer = ({ showAttachment, blockAction, handleEnterCall, - fetchThreadName + fetchThreadName, + broadcast, + Message_GroupingPeriod, + isThreadRoom: !!isThreadRoom, + autoTranslateRoom, + autoTranslateLanguage }}> <Message - id={id} - msg={messageText} - md={md} rid={rid} - author={u} - ts={ts} - type={t} - attachments={attachments} - blocks={blocks} - urls={urls} - reactions={reactions} - alias={alias} - avatar={avatar} - emoji={emoji} timeFormat={timeFormat} archived={archived} broadcast={broadcast} useRealName={useRealName} isReadReceiptEnabled={isReadReceiptEnabled} - unread={unread} - role={role} - drid={drid} - dcount={dcount} - dlm={dlm} - tmid={tmid} - tcount={tcount} - tlm={tlm} - tmsg={tmsg} - mentions={mentions} - channels={channels} - isIgnored={isIgnored} - isEdited={(editedBy && !!editedBy.username) ?? false} - isHeader={isHeader} - isThreadReply={isThreadReply} - isThreadSequential={isThreadSequential} isThreadRoom={!!isThreadRoom} - isInfo={isInfo} - isTemp={isTemp} - isEncrypted={isEncrypted} - hasError={hasError} + isPreview={isPreview} highlighted={highlighted} - comment={comment} - isTranslated={isTranslated} + isIgnored={isIgnored} isBeingEdited={isBeingEdited} - isPreview={isPreview} - pinned={pinned} autoTranslateLanguage={autoTranslateLanguage} /> <MessageSeparator ts={dateSeparator} unread={showUnreadSeparator} /> @@ -442,4 +316,26 @@ const MessageContainer = ({ ); }; +const MessageContainer = (props: IMessageContainerProps) => { + 'use memo'; + + const { item, previousItem } = props; + const prev: MessagePrev | undefined = previousItem + ? { + id: previousItem.id, + status: previousItem.status, + ts: previousItem.ts, + username: previousItem.u?.username, + groupable: previousItem.groupable, + tmid: previousItem.tmid, + t: previousItem.t + } + : undefined; + return ( + <MessageProvider item={item} prev={prev}> + <MessageContainerInner {...props} /> + </MessageProvider> + ); +}; + export default memo(MessageContainer, areEqual); diff --git a/app/containers/message/interfaces.ts b/app/containers/message/interfaces.ts index 694474fce7f..d1cb55061c0 100644 --- a/app/containers/message/interfaces.ts +++ b/app/containers/message/interfaces.ts @@ -1,17 +1,8 @@ -import { type Root } from '@rocket.chat/message-parser'; import { type StyleProp } from 'react-native'; import { type ImageStyle } from 'expo-image'; -import { type IUserChannel } from '../markdown/interfaces'; import { type TGetCustomEmoji } from '../../definitions/IEmoji'; -import { - type IAttachment, - type IThread, - type IUrl, - type IUserMention, - type IUserMessage, - type MessageType -} from '../../definitions'; +import { type IAttachment, type IUserMessage } from '../../definitions'; export interface IMessageAttachments { attachments?: IAttachment[]; @@ -20,49 +11,23 @@ export interface IMessageAttachments { } export interface IMessageAvatar { - isHeader: boolean; - avatar?: string; - emoji?: string; - author?: IUserMessage; small?: boolean; } export interface IMessageBlocks { - blocks?: { appId?: string }[]; - id: string; rid: string; } export interface IMessageBroadcast { - author?: IUserMessage; broadcast?: boolean; } export interface IMessageContent { - _id?: string; - isTemp: boolean; - isInfo: string | boolean; tmid?: string; isThreadRoom: boolean; - msg?: string; - md?: Root; - isEdited: boolean; - isEncrypted: boolean; - channels?: IUserChannel[]; - mentions?: IUserMention[]; useRealName?: boolean; isIgnored: boolean; - type: MessageType; - comment?: string; - hasError: boolean; - isHeader: boolean; - isTranslated: boolean; - pinned?: boolean; - attachments?: IAttachment[]; autoTranslateLanguage?: string; - author?: IUserMessage; - alias?: string; - role?: string; } export interface IMessageEmoji { @@ -72,27 +37,10 @@ export interface IMessageEmoji { getCustomEmoji: TGetCustomEmoji; } -export interface IMessageThread extends Pick<IThread, 'msg' | 'tcount' | 'tlm' | 'id'> { +export interface IMessageThread { isThreadRoom: boolean; } -export interface IMessageTouchable { - hasError: boolean; - isInfo: string | boolean; - isThreadReply: boolean; - isTemp: boolean; - archived?: boolean; - highlighted?: boolean; - ts?: string | Date; - urls?: IUrl[]; - reactions?: any; - alias?: string; - role?: string; - drid?: string; - isBeingEdited?: boolean; -} - -export interface IMessageRepliedThread extends Pick<IThread, 'tmid' | 'tmsg' | 'id'> { +export interface IMessageRepliedThread { isHeader: boolean; - isEncrypted: boolean; } diff --git a/app/views/RoomView/LoadMore/__snapshots__/LoadMore.test.tsx.snap b/app/views/RoomView/LoadMore/__snapshots__/LoadMore.test.tsx.snap index 0a5abbdf57c..643fc3e4b79 100644 --- a/app/views/RoomView/LoadMore/__snapshots__/LoadMore.test.tsx.snap +++ b/app/views/RoomView/LoadMore/__snapshots__/LoadMore.test.tsx.snap @@ -629,10 +629,11 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-1" > <A11yIndexView accessibilityLabel="Hey!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -917,6 +918,7 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Hey!" > <View @@ -1086,10 +1088,11 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-2" > <A11yIndexView accessibilityLabel="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -1134,6 +1137,7 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries" > <View @@ -1310,10 +1314,11 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-4" > <A11yIndexView accessibilityLabel="Older message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -1358,6 +1363,7 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Older message" > <View @@ -1746,10 +1752,11 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-6" > <A11yIndexView accessibilityLabel="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -2034,6 +2041,7 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries" > <View @@ -2203,10 +2211,11 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-7" > <A11yIndexView accessibilityLabel="This is the third message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -2251,6 +2260,7 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This is the third message" > <View @@ -2427,10 +2437,11 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-9" > <A11yIndexView accessibilityLabel="This is the second message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -2475,6 +2486,7 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This is the second message" > <View @@ -2651,10 +2663,11 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-11" > <A11yIndexView accessibilityLabel="This is the first message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -2939,6 +2952,7 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This is the first message" > <View @@ -3227,10 +3241,11 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-12" > <A11yIndexView accessibilityLabel="Hey!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -3515,6 +3530,7 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Hey!" > <View @@ -3684,10 +3700,11 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-13" > <A11yIndexView accessibilityLabel="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -3732,6 +3749,7 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries" > <View @@ -3908,10 +3926,11 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-15" > <A11yIndexView accessibilityLabel="Older message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -3956,6 +3975,7 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Older message" > <View @@ -4344,10 +4364,11 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-17" > <A11yIndexView accessibilityLabel="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -4632,6 +4653,7 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries" > <View @@ -4801,10 +4823,11 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-18" > <A11yIndexView accessibilityLabel="This is the third message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -4849,6 +4872,7 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This is the third message" > <View @@ -5025,10 +5049,11 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-20" > <A11yIndexView accessibilityLabel="This is the second message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -5073,6 +5098,7 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This is the second message" > <View @@ -5249,10 +5275,11 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-22" > <A11yIndexView accessibilityLabel="This is the first message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -5537,6 +5564,7 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This is the first message" > <View @@ -5825,10 +5853,11 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-23" > <A11yIndexView accessibilityLabel="Hey!" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -6113,6 +6142,7 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Hey!" > <View @@ -6282,10 +6312,11 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-24" > <A11yIndexView accessibilityLabel="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -6330,6 +6361,7 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries" > <View @@ -6506,10 +6538,11 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-26" > <A11yIndexView accessibilityLabel="Older message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -6554,6 +6587,7 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Older message" > <View @@ -6942,10 +6976,11 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-28" > <A11yIndexView accessibilityLabel="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -7230,6 +7265,7 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industrys standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries" > <View @@ -7399,10 +7435,11 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-29" > <A11yIndexView accessibilityLabel="This is the third message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -7447,6 +7484,7 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This is the third message" > <View @@ -7623,10 +7661,11 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-31" > <A11yIndexView accessibilityLabel="This is the second message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -7671,6 +7710,7 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This is the second message" > <View @@ -7847,10 +7887,11 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` "width": "100%", } } - testID="message-undefined" + testID="message-story-message-33" > <A11yIndexView accessibilityLabel="This is the first message" + accessible={false} importantForAccessibility="yes" orderFocusType={0} orderIndex={2} @@ -8135,6 +8176,7 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` } > <View + style={false} testID="message-content-This is the first message" > <View From abaf4c57d8f877b3ec19047eb067a5492e6a3ca5 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 11:58:48 -0300 Subject: [PATCH 014/144] refactor: make message header grouping reactive via previous-record subscription MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Row grouping depended on the previous row's mutable scalars, propagated only by the areEqual memo comparator — which cannot observe an in-place mutation of a stable model instance, so a previous message going Sent to Error never re-derived the next message's header until an unrelated re-render. Subscribe each per-message store to both its own record and the previous record; grouping and thread-position derivations read the previous model live. Delete areEqual and the MessagePrev scalar snapshot; the row keeps memo with the default comparator as a perf-only guard. Claude-Session: https://claude.ai/code/session_01PNjqp2s36bgagFz7Lo6xsV --- CONTEXT.md | 9 ++ app/containers/message/MessageStore.test.tsx | 93 ++++++++++++++++--- app/containers/message/MessageStore.tsx | 52 +++++------ .../useMessageAccessibilityLabel.test.tsx | 11 ++- app/containers/message/index.tsx | 42 +-------- 5 files changed, 125 insertions(+), 82 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 474444eb06d..b6f02d280ae 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -26,6 +26,15 @@ | **Draft Message** | A user's unsent composition stored on a Subscription or Thread (`draftMessage` field) | Unsent message | | **Snippet** | A saved excerpt from a Message | — | +## Message Grouping + +| Term | Definition | Aliases to avoid | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------- | +| **Message Header** | The author block (avatar, name, timestamp) shown on the first Message of a grouped run; a Message either shows a Header or is grouped under the one above it | Title, byline | +| **Grouped Message** | A Message rendered without its own Header because it continues a run from the same author within the Grouping Period, visually attached to the Message above | Sequential, collapsed | +| **Grouping Period** | The maximum time gap between consecutive same-author Messages for them to share one Header (server setting `Message_GroupingPeriod`) | — | +| **Previous Message** | Relative to a given Message, the adjacent older Message; whether that Message shows a Header is derived from it (author, time, status, thread) | Prior message, neighbor | + ## Message Status | Term | Definition | Aliases to avoid | diff --git a/app/containers/message/MessageStore.test.tsx b/app/containers/message/MessageStore.test.tsx index 772384087f3..945abb47ba2 100644 --- a/app/containers/message/MessageStore.test.tsx +++ b/app/containers/message/MessageStore.test.tsx @@ -5,7 +5,6 @@ import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; import { messagesStatus } from '../../lib/constants/messagesStatus'; import MessageContext, { type IMessageContext } from './Context'; import { - type MessagePrev, MessageProvider, useAvatar, useBlocks, @@ -115,12 +114,12 @@ const MsgValueReporter = ({ spy }: { spy: jest.Mock }) => { return null; }; -// Derived hooks read both the per-row store/item/prev and the config in MessageContext, +// Derived hooks read both the per-row store/item/previousItem and the config in MessageContext, // so the probe needs both providers mounted. const renderDerived = ( item: TAnyMessageModel, useHook: () => unknown, - { prev, config }: { prev?: MessagePrev; config?: Partial<IMessageContext> } = {} + { previousItem, config }: { previousItem?: TAnyMessageModel; config?: Partial<IMessageContext> } = {} ) => { const spy = jest.fn(); const Probe = () => { @@ -129,7 +128,7 @@ const renderDerived = ( }; render( <MessageContext.Provider value={(config ?? {}) as IMessageContext}> - <MessageProvider item={item} prev={prev}> + <MessageProvider item={item} previousItem={previousItem}> <Probe /> </MessageProvider> </MessageContext.Provider> @@ -375,13 +374,13 @@ describe('MessageStore', () => { it('useThreadPosition returns isThreadReply true when the message replies to a different thread than prev', () => { const model = buildFakeModel({ tmid: 't1' }); - const { latest } = renderDerived(model, useThreadPosition, { prev: { tmid: 't0', id: 'p0' } }); + const { latest } = renderDerived(model, useThreadPosition, { previousItem: buildFakeModel({ id: 'p0', tmid: 't0' }) }); expect(latest()).toEqual({ isThreadReply: true, isThreadSequential: true }); }); it('useThreadPosition returns isThreadReply false when prev is in the same thread', () => { const model = buildFakeModel({ tmid: 't1' }); - const { latest } = renderDerived(model, useThreadPosition, { prev: { tmid: 't1' } }); + const { latest } = renderDerived(model, useThreadPosition, { previousItem: buildFakeModel({ id: 'p0', tmid: 't1' }) }); expect(latest()).toEqual({ isThreadReply: false, isThreadSequential: true }); }); @@ -393,7 +392,9 @@ describe('MessageStore', () => { it('useMessageGrouping returns true (header) when prev has an ERROR status', () => { const model = buildFakeModel(); - const { latest } = renderDerived(model, useMessageGrouping, { prev: { status: messagesStatus.ERROR } }); + const { latest } = renderDerived(model, useMessageGrouping, { + previousItem: buildFakeModel({ id: 'p0', status: messagesStatus.ERROR }) + }); expect(latest()).toBe(true); }); @@ -402,7 +403,7 @@ describe('MessageStore', () => { const prevTs = new Date('2024-01-01T09:59:50Z'); const model = buildFakeModel({ ts, tmid: 't1' }); const { latest } = renderDerived(model, useMessageGrouping, { - prev: { ts: prevTs, username: 'alice', tmid: 't1' }, + previousItem: buildFakeModel({ id: 'p0', ts: prevTs, tmid: 't1' }), config: { broadcast: false, Message_GroupingPeriod: 300 } }); expect(latest()).toBe(false); @@ -413,7 +414,7 @@ describe('MessageStore', () => { const prevTs = new Date('2024-01-01T09:59:50Z'); const model = buildFakeModel({ ts, tmid: 't1' }); const { latest } = renderDerived(model, useMessageGrouping, { - prev: { ts: prevTs, username: 'alice', tmid: 't1' }, + previousItem: buildFakeModel({ id: 'p0', ts: prevTs, tmid: 't1' }), config: { broadcast: true, Message_GroupingPeriod: 300 } }); expect(latest()).toBe(true); @@ -424,7 +425,7 @@ describe('MessageStore', () => { const prevTs = new Date('2024-01-01T09:59:50Z'); const model = buildFakeModel({ ts, tmid: 't1', groupable: false }); const { latest } = renderDerived(model, useMessageGrouping, { - prev: { ts: prevTs, username: 'alice', tmid: 't1' }, + previousItem: buildFakeModel({ id: 'p0', ts: prevTs, tmid: 't1' }), config: { broadcast: false, Message_GroupingPeriod: 300 } }); expect(latest()).toBe(true); @@ -435,9 +436,79 @@ describe('MessageStore', () => { const prevTs = new Date('2024-01-01T09:59:50Z'); const model = buildFakeModel({ ts, tmid: 't1', t: 'rm' }); const { latest } = renderDerived(model, useMessageGrouping, { - prev: { ts: prevTs, username: 'alice', tmid: 't1' }, + previousItem: buildFakeModel({ id: 'p0', ts: prevTs, tmid: 't1' }), + config: { broadcast: false, Message_GroupingPeriod: 300 } + }); + expect(latest()).toBe(true); + }); + + it('flips to header when the previous message transitions to ERROR in place, without re-passing props', () => { + const ts = new Date('2024-01-01T10:00:00Z'); + const prevTs = new Date('2024-01-01T09:59:50Z'); + const model = buildFakeModel({ ts, tmid: 't1' }); + const previousItem = buildFakeModel({ id: 'p0', ts: prevTs, tmid: 't1' }); + const { latest } = renderDerived(model, useMessageGrouping, { + previousItem, config: { broadcast: false, Message_GroupingPeriod: 300 } }); + + expect(latest()).toBe(false); + + act(() => { + (previousItem as any).status = messagesStatus.ERROR; + previousItem._emit(); + }); + + expect(latest()).toBe(true); + }); + + it('unsubscribes the old previous model and subscribes the new one on a temp→server swap', () => { + const ts = new Date('2024-01-01T10:00:00Z'); + const prevTs = new Date('2024-01-01T09:59:50Z'); + const item = buildFakeModel({ id: 'msg-1', ts, tmid: 't1' }); + const previousTemp = buildFakeModel({ id: 'p-temp', ts: prevTs, tmid: 't1' }); + const previousServer = buildFakeModel({ id: 'p-server', ts: prevTs, tmid: 't1' }); + + const unsubscribeSpy = jest.fn(); + const realSubscribe = previousTemp.experimentalSubscribe.bind(previousTemp); + previousTemp.experimentalSubscribe = ((cb: Subscriber) => { + const realUnsub = realSubscribe(cb); + return () => { + unsubscribeSpy(); + realUnsub(); + }; + }) as FakeModel['experimentalSubscribe']; + + const spy = jest.fn(); + const Probe = () => { + spy(useMessageGrouping()); + return null; + }; + const latest = () => { + const { calls } = spy.mock; + return calls[calls.length - 1]?.[0]; + }; + const wrap = (previousItem: TAnyMessageModel) => ( + <MessageContext.Provider value={{ broadcast: false, Message_GroupingPeriod: 300 } as IMessageContext}> + <MessageProvider item={item} previousItem={previousItem}> + <Probe /> + </MessageProvider> + </MessageContext.Provider> + ); + + const { rerender } = render(wrap(previousTemp)); + expect(latest()).toBe(false); + expect(unsubscribeSpy).not.toHaveBeenCalled(); + + act(() => rerender(wrap(previousServer))); + expect(unsubscribeSpy).toHaveBeenCalledTimes(1); + expect(latest()).toBe(false); + + // An in-place change on the NEW previous model re-derives grouping, proving it is subscribed. + act(() => { + (previousServer as any).status = messagesStatus.ERROR; + previousServer._emit(); + }); expect(latest()).toBe(true); }); diff --git a/app/containers/message/MessageStore.tsx b/app/containers/message/MessageStore.tsx index 16c958e94b0..6cef3722c68 100644 --- a/app/containers/message/MessageStore.tsx +++ b/app/containers/message/MessageStore.tsx @@ -12,17 +12,7 @@ const createMessageStore = () => createStore(() => ({ tick: 0 })); type MessageStore = ReturnType<typeof createMessageStore>; -export type MessagePrev = { - id?: string; - status?: number; - ts?: TAnyMessageModel['ts']; - username?: string; - groupable?: boolean; - tmid?: string; - t?: TAnyMessageModel['t']; -}; - -type MessageCtxValue = { store: MessageStore; item: TAnyMessageModel; prev?: MessagePrev }; +type MessageCtxValue = { store: MessageStore; item: TAnyMessageModel; previousItem?: TAnyMessageModel }; const MessageStoreContext = createContext<MessageCtxValue | null>(null); @@ -44,30 +34,34 @@ export const useMessageField = <T,>(selector: (item: TAnyMessageModel) => T): T return useStore(store, () => selector(item)); }; +// Plain REST objects (no experimentalSubscribe) never emit again after the initial render. +const subscribeModel = (m: TAnyMessageModel, store: MessageStore) => { + // @ts-ignore: experimentalSubscribe is not yet in WatermelonDB's TS types + if (typeof m.experimentalSubscribe !== 'function') return undefined; + // @ts-ignore: experimentalSubscribe is not yet in WatermelonDB's TS types + return m.experimentalSubscribe(() => store.setState(s => ({ tick: s.tick + 1 }))); +}; + export const MessageProvider = ({ item, - prev, + previousItem, children }: { item: TAnyMessageModel; - prev?: MessagePrev; + previousItem?: TAnyMessageModel; children: ReactNode; }): ReactElement => { 'use memo'; const [store] = useState(createMessageStore); - useEffect(() => { - // Plain REST objects (no experimentalSubscribe) never emit again after the initial render. - // @ts-ignore: experimentalSubscribe is not yet in WatermelonDB's TS types - if (typeof item.experimentalSubscribe !== 'function') { - return undefined; - } - // @ts-ignore: experimentalSubscribe is not yet in WatermelonDB's TS types - return item.experimentalSubscribe(() => store.setState(s => ({ tick: s.tick + 1 }))); - }, [item, store]); + // Header grouping and thread position depend on the previous record too, so each effect + // subscribes one record; both feed the same tick. Keeping them separate means changing + // previousItem does not tear down and recreate item's subscription. + useEffect(() => subscribeModel(item, store), [item, store]); + useEffect(() => (previousItem ? subscribeModel(previousItem, store) : undefined), [previousItem, store]); - return <MessageStoreContext.Provider value={{ store, item, prev }}>{children}</MessageStoreContext.Provider>; + return <MessageStoreContext.Provider value={{ store, item, previousItem }}>{children}</MessageStoreContext.Provider>; }; export const useReactions = (): TAnyMessageModel['reactions'] => useMessageField(item => item.reactions); @@ -146,7 +140,7 @@ export const useMessageMeta = (): Pick<TAnyMessageModel, 'ts' | 'unread' | 'pinn }; const computeIsHeader = ( - prev: MessagePrev | undefined, + prev: TAnyMessageModel | undefined, item: TAnyMessageModel, broadcast: boolean, Message_GroupingPeriod: number | undefined, @@ -160,7 +154,7 @@ const computeIsHeader = ( prev && // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense prev.ts.toDateString() === item.ts.toDateString() && - prev.username === item.u?.username && + prev.u?.username === item.u?.username && !(prev.groupable === false || item.groupable === false || broadcast === true) && // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense item.ts - prev.ts < Message_GroupingPeriod * 1000 && @@ -177,15 +171,15 @@ const computeIsHeader = ( }; export const useMessageGrouping = (): boolean => { - const { store, item, prev } = useMessageCtx(); + const { store, item, previousItem } = useMessageCtx(); const { broadcast, Message_GroupingPeriod } = useContext(MessageContext); return useStore(store, () => - computeIsHeader(prev, item, !!broadcast, Message_GroupingPeriod, item.status === messagesStatus.ERROR) + computeIsHeader(previousItem, item, !!broadcast, Message_GroupingPeriod, item.status === messagesStatus.ERROR) ); }; export const useThreadPosition = (): { isThreadReply: boolean; isThreadSequential: boolean } => { - const { store, item, prev } = useMessageCtx(); + const { store, item, previousItem } = useMessageCtx(); const { isThreadRoom } = useContext(MessageContext); return useStore( store, @@ -193,7 +187,7 @@ export const useThreadPosition = (): { isThreadReply: boolean; isThreadSequentia if (isThreadRoom) { return { isThreadReply: false, isThreadSequential: false }; } - const isThreadReply = !!(prev && item.tmid && prev.tmid !== item.tmid && prev.id !== item.tmid); + const isThreadReply = !!(previousItem && item.tmid && previousItem.tmid !== item.tmid && previousItem.id !== item.tmid); return { isThreadReply, isThreadSequential: !!item.tmid }; }) ); diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx b/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx index 06cdf179930..b63343e4bc3 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx @@ -2,7 +2,7 @@ import { render } from '@testing-library/react-native'; import { useMessageAccessibilityLabel } from './useMessageAccessibilityLabel'; import MessageContext, { type IMessageContext } from '../Context'; -import { MessageProvider, type MessagePrev } from '../MessageStore'; +import { MessageProvider } from '../MessageStore'; import { type TAnyMessageModel } from '../../../definitions'; import { E2E_MESSAGE_TYPE } from '../../../lib/constants/keys'; @@ -25,7 +25,12 @@ const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel ...overrides } as TAnyMessageModel); -const renderLabel = (item: TAnyMessageModel, args: TArgs = {}, config: Partial<IMessageContext> = {}, prev?: MessagePrev) => { +const renderLabel = ( + item: TAnyMessageModel, + args: TArgs = {}, + config: Partial<IMessageContext> = {}, + previousItem?: TAnyMessageModel +) => { const spy = jest.fn(); const Probe = () => { spy(useMessageAccessibilityLabel(args)); @@ -33,7 +38,7 @@ const renderLabel = (item: TAnyMessageModel, args: TArgs = {}, config: Partial<I }; render( <MessageContext.Provider value={config as IMessageContext}> - <MessageProvider item={item} prev={prev}> + <MessageProvider item={item} previousItem={previousItem}> <Probe /> </MessageProvider> </MessageContext.Provider> diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 89c1a2e7a1a..a91e5eff3b1 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -10,7 +10,7 @@ import { type IAttachment, type TAnyMessageModel, type TGetCustomEmoji } from '. import { type IRoomInfoParam } from '../../views/SearchMessagesView'; import MessageSeparator from '../MessageSeparator'; import { useIsBeingEdited } from '../../views/RoomView/InteractionStore'; -import { MessageProvider, type MessagePrev, useIsEncrypted, useIsInfo, useMessageStatus } from './MessageStore'; +import { MessageProvider, useIsEncrypted, useIsInfo, useMessageStatus } from './MessageStore'; interface IMessageContainerProps { item: TAnyMessageModel; @@ -63,31 +63,6 @@ interface IMessageContainerProps { showUnreadSeparator?: boolean; } -// `item` is intentionally omitted: the FlatList keys each row by item.id, so a -// different message remounts rather than re-rendering with a new item prop; in-place -// field updates on the same item are handled by the per-message Zustand store (see -// MessageProvider/MessageStore), not by this comparison. -function areEqual(prev: IMessageContainerProps, next: IMessageContainerProps): boolean { - const p = prev.previousItem; - const n = next.previousItem; - return ( - prev.showUnreadSeparator === next.showUnreadSeparator && - prev.dateSeparator === next.dateSeparator && - prev.highlighted === next.highlighted && - prev.threadBadgeColor === next.threadBadgeColor && - prev.isIgnored === next.isIgnored && - p?.id === n?.id && - p?.status === n?.status && - p?.ts === n?.ts && - p?.u?.username === n?.u?.username && - p?.groupable === n?.groupable && - p?.tmid === n?.tmid && - p?.t === n?.t && - prev.autoTranslateRoom === next.autoTranslateRoom && - prev.autoTranslateLanguage === next.autoTranslateLanguage - ); -} - const MessageContainerInner = ({ item, user, @@ -320,22 +295,11 @@ const MessageContainer = (props: IMessageContainerProps) => { 'use memo'; const { item, previousItem } = props; - const prev: MessagePrev | undefined = previousItem - ? { - id: previousItem.id, - status: previousItem.status, - ts: previousItem.ts, - username: previousItem.u?.username, - groupable: previousItem.groupable, - tmid: previousItem.tmid, - t: previousItem.t - } - : undefined; return ( - <MessageProvider item={item} prev={prev}> + <MessageProvider item={item} previousItem={previousItem}> <MessageContainerInner {...props} /> </MessageProvider> ); }; -export default memo(MessageContainer, areEqual); +export default memo(MessageContainer); From 6f3682d5031d2202657e8603e4e8eb4e61f38a52 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 12:13:42 -0300 Subject: [PATCH 015/144] test: refresh time-drifted Timestamp story snapshot The Timestamp story renders a dayjs fromNow relative to the real current date; the stored snapshot ('a year ago') drifted to '2 years ago', failing the ESLint and Test job on every run after the boundary. Claude-Session: https://claude.ai/code/session_01PNjqp2s36bgagFz7Lo6xsV --- app/containers/markdown/__snapshots__/Markdown.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap index 009e5615a36..2517ef4eb26 100644 --- a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap +++ b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap @@ -5882,7 +5882,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - a year ago + 2 years ago </Text> </Text> </Text> From 1f045e60b3beaa956f28e085f43f4f14e1a18824 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 14:53:36 -0300 Subject: [PATCH 016/144] feat: add per-instance MessageRoomStore and provider (NATIVE-1376) --- app/containers/message/MessageRoomStore.tsx | 82 +++++++++++++++++++++ app/views/MessagesView/index.tsx | 28 ++++--- app/views/RoomView/index.tsx | 44 ++++++++--- app/views/SearchMessagesView/index.tsx | 34 ++++++--- 4 files changed, 155 insertions(+), 33 deletions(-) create mode 100644 app/containers/message/MessageRoomStore.tsx diff --git a/app/containers/message/MessageRoomStore.tsx b/app/containers/message/MessageRoomStore.tsx new file mode 100644 index 00000000000..bba19661f77 --- /dev/null +++ b/app/containers/message/MessageRoomStore.tsx @@ -0,0 +1,82 @@ +import { createContext, useContext, useEffect, useState, type ReactElement, type ReactNode } from 'react'; +import { createStore, useStore } from 'zustand'; +import { useShallow } from 'zustand/react/shallow'; + +import { type TGetCustomEmoji } from '../../definitions/IEmoji'; +import { type IAttachment } from '../../definitions'; +import { type IRoomInfoParam } from '../../views/SearchMessagesView'; +import { type TSupportedThemes } from '../../theme'; + +type MessageRoomState = { + // stable handlers + getCustomEmoji?: TGetCustomEmoji; + navToRoomInfo?: (navParam: IRoomInfoParam) => void; + showAttachment?: (file: IAttachment) => void; + blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void; + handleEnterCall?: () => void; + fetchThreadName?: (tmid: string, id: string) => Promise<string | undefined>; + toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise<void>; + jumpToMessage?: (link: string) => void; + // room constants + rid?: string; + user?: { id?: string; username?: string; token?: string }; + baseUrl?: string; + broadcast?: boolean; + isThreadRoom?: boolean; + Message_GroupingPeriod?: number; + // reactive tail (provider keeps current) + autoTranslateRoom?: boolean; + autoTranslateLanguage?: string; + theme?: TSupportedThemes; +}; + +export const createMessageRoomStore = (initial: MessageRoomState) => createStore<MessageRoomState>(() => ({ ...initial })); + +export type MessageRoomStore = ReturnType<typeof createMessageRoomStore>; + +export const MessageRoomStoreContext = createContext<MessageRoomStore | null>(null); + +const useMessageRoomStore = <T,>(selector: (state: MessageRoomState) => T): T => { + const store = useContext(MessageRoomStoreContext); + if (!store) { + throw new Error('Message room hooks must be used within a MessageRoomProvider'); + } + return useStore(store, selector); +}; + +export const MessageRoomProvider = ({ children, ...state }: { children: ReactNode } & MessageRoomState): ReactElement => { + 'use memo'; + + const [store] = useState(() => createMessageRoomStore(state)); + + // No dependency array: the store must mirror the provider's props every render. + // Handlers/constants are stable refs, so unchanged slices won't trigger a notify. + useEffect(() => { + store.setState(state); + }); + + return <MessageRoomStoreContext.Provider value={store}>{children}</MessageRoomStoreContext.Provider>; +}; + +export const useGetCustomEmoji = (): TGetCustomEmoji | undefined => useMessageRoomStore(s => s.getCustomEmoji); +export const useNavToRoomInfo = (): ((navParam: IRoomInfoParam) => void) | undefined => useMessageRoomStore(s => s.navToRoomInfo); +export const useShowAttachment = (): ((file: IAttachment) => void) | undefined => useMessageRoomStore(s => s.showAttachment); +export const useBlockAction = (): MessageRoomState['blockAction'] => useMessageRoomStore(s => s.blockAction); +export const useHandleEnterCall = (): (() => void) | undefined => useMessageRoomStore(s => s.handleEnterCall); +export const useFetchThreadName = (): MessageRoomState['fetchThreadName'] => useMessageRoomStore(s => s.fetchThreadName); +export const useToggleFollowThread = (): MessageRoomState['toggleFollowThread'] => useMessageRoomStore(s => s.toggleFollowThread); +export const useJumpToMessage = (): ((link: string) => void) | undefined => useMessageRoomStore(s => s.jumpToMessage); + +export const useRid = (): string | undefined => useMessageRoomStore(s => s.rid); +export const useMessageUser = (): MessageRoomState['user'] => useMessageRoomStore(s => s.user); +export const useBaseUrl = (): string | undefined => useMessageRoomStore(s => s.baseUrl); +export const useBroadcast = (): boolean | undefined => useMessageRoomStore(s => s.broadcast); +export const useIsThreadRoom = (): boolean | undefined => useMessageRoomStore(s => s.isThreadRoom); +export const useMessageGroupingPeriod = (): number | undefined => useMessageRoomStore(s => s.Message_GroupingPeriod); + +export const useMessageTheme = (): TSupportedThemes | undefined => useMessageRoomStore(s => s.theme); + +export const useAutoTranslate = (): { autoTranslateRoom?: boolean; autoTranslateLanguage?: string } => + useMessageRoomStore( + useShallow(s => ({ autoTranslateRoom: s.autoTranslateRoom, autoTranslateLanguage: s.autoTranslateLanguage })) + ); diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index 5ac8fc574fd..60e13953f28 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -7,6 +7,7 @@ import { type CompositeNavigationProp, type RouteProp } from '@react-navigation/ import { type MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import Message from '../../containers/message'; +import { MessageRoomProvider } from '../../containers/message/MessageRoomStore'; import ActivityIndicator from '../../containers/ActivityIndicator'; import I18n from '../../i18n'; import getFileUrlAndTypeFromMessage from './getFileUrlAndTypeFromMessage'; @@ -358,7 +359,7 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { render() { const { messages, loading } = this.state; - const { theme } = this.props; + const { theme, user, baseUrl } = this.props; if (!loading && messages.length === 0) { return this.renderEmpty(); @@ -366,14 +367,23 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { return ( <SafeAreaView style={{ backgroundColor: themes[theme].surfaceRoom }} testID={this.content.testID}> - <FlatList - data={messages} - renderItem={this.renderItem} - style={[styles.list, { backgroundColor: themes[theme].surfaceRoom }]} - keyExtractor={item => item._id} - onEndReached={this.load} - ListFooterComponent={loading ? <ActivityIndicator /> : null} - /> + <MessageRoomProvider + getCustomEmoji={this.getCustomEmoji} + navToRoomInfo={this.navToRoomInfo} + showAttachment={this.showAttachment} + user={user} + baseUrl={baseUrl} + isThreadRoom + theme={theme}> + <FlatList + data={messages} + renderItem={this.renderItem} + style={[styles.list, { backgroundColor: themes[theme].surfaceRoom }]} + keyExtractor={item => item._id} + onEndReached={this.load} + ListFooterComponent={loading ? <ActivityIndicator /> : null} + /> + </MessageRoomProvider> </SafeAreaView> ); } diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index ba865ab7eca..61af9007395 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -99,6 +99,7 @@ import { goRoom, type TGoRoomItem } from '../../lib/methods/helpers/goRoom'; import { ComposerAttachments, type IMessageComposerRef, MessageComposerContainer } from '../../containers/MessageComposer'; import { RoomContext } from './context'; import { createInteractionStore, InteractionStoreContext, type InteractionStore } from './InteractionStore'; +import { MessageRoomProvider } from '../../containers/message/MessageRoomStore'; import AudioManager from '../../lib/methods/AudioManager'; import { type IListContainerRef, type TListRef } from './List/definitions'; import { resolveJumpAnchor } from './services/resolveJumpAnchor'; @@ -1641,8 +1642,8 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { render() { console.count(`${this.constructor.name}.render calls`); - const { room, isAutocompleteVisible, showMissingE2EEKey, showE2EEDisabledRoom } = this.state; - const { user, baseUrl, theme, width, serverVersion, navigation } = this.props; + const { room, isAutocompleteVisible, showMissingE2EEKey, showE2EEDisabledRoom, canAutoTranslate } = this.state; + const { user, baseUrl, theme, width, serverVersion, navigation, Message_GroupingPeriod } = this.props; const { rid, t } = room; let bannerClosed; let announcement; @@ -1699,17 +1700,36 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { closeBanner={this.closeBanner} /> ) : null} - <List - ref={this.list} - listRef={this.flatList} + <MessageRoomProvider + getCustomEmoji={this.getCustomEmoji} + navToRoomInfo={this.navToRoomInfo} + showAttachment={this.showAttachment} + blockAction={this.blockAction} + handleEnterCall={this.handleEnterCall} + fetchThreadName={this.fetchThreadName} + toggleFollowThread={this.toggleFollowThread} + jumpToMessage={this.jumpToMessageByUrl} rid={rid} - t={t as RoomType} - tmid={this.tmid} - renderRow={this.renderItem} - hideSystemMessages={this.hideSystemMessages} - showMessageInMainThread={user.showMessageInMainThread ?? false} - serverVersion={serverVersion} - /> + user={user as any} + baseUrl={baseUrl} + broadcast={'id' in room && room.broadcast} + isThreadRoom={!!this.tmid} + Message_GroupingPeriod={Message_GroupingPeriod} + autoTranslateRoom={canAutoTranslate && 'id' in room && room.autoTranslate} + autoTranslateLanguage={'id' in room ? room.autoTranslateLanguage : undefined} + theme={theme}> + <List + ref={this.list} + listRef={this.flatList} + rid={rid} + t={t as RoomType} + tmid={this.tmid} + renderRow={this.renderItem} + hideSystemMessages={this.hideSystemMessages} + showMessageInMainThread={user.showMessageInMainThread ?? false} + serverVersion={serverVersion} + /> + </MessageRoomProvider> {this.renderFooter()} {this.renderActions()} <UploadProgress rid={rid} user={user} baseUrl={baseUrl} width={width} /> diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index e931573cb7f..17466b2c615 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -10,6 +10,7 @@ import { FormTextInput } from '../../containers/TextInput'; import ActivityIndicator from '../../containers/ActivityIndicator'; import Markdown from '../../containers/markdown'; import Message from '../../containers/message'; +import { MessageRoomProvider } from '../../containers/message/MessageRoomStore'; import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps'; import I18n from '../../i18n'; import log from '../../lib/methods/helpers/log'; @@ -307,24 +308,33 @@ class SearchMessagesView extends Component<ISearchMessagesViewProps, ISearchMess renderList = () => { const { messages, loading, searchText } = this.state; - const { theme } = this.props; + const { theme, user, baseUrl } = this.props; if (!loading && messages.length === 0 && searchText.length) { return this.renderEmpty(); } return ( - <FlatList - data={messages} - renderItem={this.renderItem} - style={[styles.list, { backgroundColor: themes[theme].surfaceRoom }]} - keyExtractor={item => item._id} - onEndReached={this.onEndReached} - ListFooterComponent={loading ? <ActivityIndicator /> : null} - onEndReachedThreshold={0.5} - removeClippedSubviews={isIOS} - {...scrollPersistTaps} - /> + <MessageRoomProvider + getCustomEmoji={this.getCustomEmoji} + navToRoomInfo={this.navToRoomInfo} + showAttachment={this.showAttachment} + user={user} + baseUrl={baseUrl} + isThreadRoom + theme={theme}> + <FlatList + data={messages} + renderItem={this.renderItem} + style={[styles.list, { backgroundColor: themes[theme].surfaceRoom }]} + keyExtractor={item => item._id} + onEndReached={this.onEndReached} + ListFooterComponent={loading ? <ActivityIndicator /> : null} + onEndReachedThreshold={0.5} + removeClippedSubviews={isIOS} + {...scrollPersistTaps} + /> + </MessageRoomProvider> ); }; From 39167bd271cf6957efd5c92b2e09db79d58432ef Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 14:56:51 -0300 Subject: [PATCH 017/144] feat: add per-message data selectors for translate/id/replies (NATIVE-1376) --- app/containers/message/MessageStore.tsx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/containers/message/MessageStore.tsx b/app/containers/message/MessageStore.tsx index 6cef3722c68..b85264d4e59 100644 --- a/app/containers/message/MessageStore.tsx +++ b/app/containers/message/MessageStore.tsx @@ -7,6 +7,7 @@ import MessageContext from './Context'; import { getMessageTranslation } from './utils'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; import { messagesStatus } from '../../lib/constants/messagesStatus'; +import { useAutoTranslate, useMessageUser } from './MessageRoomStore'; const createMessageStore = () => createStore(() => ({ tick: 0 })); @@ -216,6 +217,21 @@ export const useIsInfo = (): string | boolean => export const useIsEdited = (): boolean => useMessageField(item => (item.editedBy && !!item.editedBy.username) ?? false); +export const useMessageId = (): TAnyMessageModel['id'] => useMessageField(item => item.id); + +export const useReplies = (): TAnyMessageModel['replies'] => useMessageField(item => item.replies); + +export const useTranslateLanguage = (): string | undefined => { + const { store, item } = useMessageCtx(); + const { autoTranslateRoom, autoTranslateLanguage } = useAutoTranslate(); + const user = useMessageUser(); + return useStore(store, () => { + const otherUserMessage = item.u?.username !== user?.username; + const canTranslate = autoTranslateRoom && autoTranslateLanguage && item.autoTranslate !== false && otherUserMessage; + return canTranslate ? autoTranslateLanguage : undefined; + }); +}; + export const useMessageText = (): { messageText: TAnyMessageModel['msg']; isTranslated: boolean } => { const { store, item } = useMessageCtx(); const { user, autoTranslateRoom, autoTranslateLanguage } = useContext(MessageContext); From 854e457207a1c36907c73d381b12f22ac036ac01 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 15:08:36 -0300 Subject: [PATCH 018/144] test: shared message render helper wiring MessageRoomProvider (NATIVE-1376) Adds pickMessageRoomState + a MessageProviders/renderWithMessageProviders helper and adopts it in the container test and stories so the room store is mounted alongside MessageContext during the dual-run. No production behavior change; 328 tests / 128 snapshots unchanged. --- app/containers/message/Message.stories.tsx | 35 ++++++++------ app/containers/message/MessageRoomStore.tsx | 35 +++++++++++++- app/containers/message/index.test.tsx | 40 ++++++++-------- app/containers/message/testHelpers.tsx | 52 +++++++++++++++++++++ 4 files changed, 125 insertions(+), 37 deletions(-) create mode 100644 app/containers/message/testHelpers.tsx diff --git a/app/containers/message/Message.stories.tsx b/app/containers/message/Message.stories.tsx index cc0c303e0df..7a9a1f3c7a7 100644 --- a/app/containers/message/Message.stories.tsx +++ b/app/containers/message/Message.stories.tsx @@ -17,6 +17,7 @@ import { import { mockedStore as store } from '../../reducers/mockedStore'; import { updateSettings } from '../../actions/settings'; import { createInteractionStore, InteractionStoreContext } from '../../views/RoomView/InteractionStore'; +import { MessageRoomProvider, pickMessageRoomState } from './MessageRoomStore'; const _theme = 'light'; @@ -226,23 +227,27 @@ const renderMessageStory = (fontScale: number, props: any) => { const item = buildItem({ ...itemProps, isTranslated }); const previousItem = previousItemOverride ?? resolvePreviousItem(item, isHeader, isThreadReply); + const containerProps = { + item, + previousItem, + rid, + Message_GroupingPeriod: 300, + timeFormat, + broadcast, + archived, + isIgnored, + useRealName, + isReadReceiptEnabled, + autoTranslateRoom: autoTranslateRoom ?? (isTranslated ? true : undefined), + autoTranslateLanguage: autoTranslateLanguage ?? (isTranslated ? 'en' : undefined), + ...storyContextValue + }; + return ( <ResponsiveLayoutContext.Provider value={responsiveLayoutProviderLargeFontValue(fontScale)}> - <MessageContainer - item={item} - previousItem={previousItem} - rid={rid} - Message_GroupingPeriod={300} - timeFormat={timeFormat} - broadcast={broadcast} - archived={archived} - isIgnored={isIgnored} - useRealName={useRealName} - isReadReceiptEnabled={isReadReceiptEnabled} - autoTranslateRoom={autoTranslateRoom ?? (isTranslated ? true : undefined)} - autoTranslateLanguage={autoTranslateLanguage ?? (isTranslated ? 'en' : undefined)} - {...storyContextValue} - /> + <MessageRoomProvider {...pickMessageRoomState(containerProps)}> + <MessageContainer {...containerProps} /> + </MessageRoomProvider> </ResponsiveLayoutContext.Provider> ); }; diff --git a/app/containers/message/MessageRoomStore.tsx b/app/containers/message/MessageRoomStore.tsx index bba19661f77..9ac263350fb 100644 --- a/app/containers/message/MessageRoomStore.tsx +++ b/app/containers/message/MessageRoomStore.tsx @@ -7,7 +7,7 @@ import { type IAttachment } from '../../definitions'; import { type IRoomInfoParam } from '../../views/SearchMessagesView'; import { type TSupportedThemes } from '../../theme'; -type MessageRoomState = { +export type MessageRoomState = { // stable handlers getCustomEmoji?: TGetCustomEmoji; navToRoomInfo?: (navParam: IRoomInfoParam) => void; @@ -30,6 +30,39 @@ type MessageRoomState = { theme?: TSupportedThemes; }; +// The room-scoped fields this store owns. Tests and stories use pickMessageRoomState to +// mirror the same values into MessageRoomProvider and the legacy MessageContext during the +// dual-run migration. Add a field to MessageRoomState → add its key here. +const ROOM_STATE_KEYS: (keyof MessageRoomState)[] = [ + 'getCustomEmoji', + 'navToRoomInfo', + 'showAttachment', + 'blockAction', + 'handleEnterCall', + 'fetchThreadName', + 'toggleFollowThread', + 'jumpToMessage', + 'rid', + 'user', + 'baseUrl', + 'broadcast', + 'isThreadRoom', + 'Message_GroupingPeriod', + 'autoTranslateRoom', + 'autoTranslateLanguage', + 'theme' +]; + +export const pickMessageRoomState = (source: Record<string, any> = {}): Partial<MessageRoomState> => { + const state: Partial<MessageRoomState> = {}; + ROOM_STATE_KEYS.forEach(key => { + if (source[key] !== undefined) { + (state as Record<string, unknown>)[key] = source[key]; + } + }); + return state; +}; + export const createMessageRoomStore = (initial: MessageRoomState) => createStore<MessageRoomState>(() => ({ ...initial })); export type MessageRoomStore = ReturnType<typeof createMessageRoomStore>; diff --git a/app/containers/message/index.test.tsx b/app/containers/message/index.test.tsx index 50a738a987b..fd9739f5201 100644 --- a/app/containers/message/index.test.tsx +++ b/app/containers/message/index.test.tsx @@ -1,11 +1,11 @@ import { act } from 'react'; -import { Provider } from 'react-redux'; -import { render, fireEvent } from '@testing-library/react-native'; +import { fireEvent } from '@testing-library/react-native'; import MessageContainer from './index'; -import { mockedStore } from '../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../definitions'; import { createInteractionStore, InteractionStoreContext } from '../../views/RoomView/InteractionStore'; +import { renderWithMessageProviders } from './testHelpers'; +import { pickMessageRoomState } from './MessageRoomStore'; jest.mock('./Touch', () => { const { forwardRef } = require('react'); @@ -63,12 +63,12 @@ const baseProps = { getCustomEmoji: jest.fn(() => null) }; -const renderContainer = (itemOverrides: Record<string, any> = {}, propOverrides: Record<string, any> = {}) => - render( - <Provider store={mockedStore}> - <MessageContainer item={createMockMessage(itemOverrides)} {...baseProps} {...propOverrides} /> - </Provider> - ); +const renderContainer = (itemOverrides: Record<string, any> = {}, propOverrides: Record<string, any> = {}) => { + const props = { ...baseProps, ...propOverrides }; + return renderWithMessageProviders(<MessageContainer item={createMockMessage(itemOverrides)} {...props} />, { + room: pickMessageRoomState(props) + }); +}; it('renders a normal message without crashing', () => { const { getByTestId } = renderContainer(); @@ -98,12 +98,11 @@ describe('edit highlight reacts to InteractionStore', () => { it('shows editing testID when the store marks this message as being edited', () => { const store = createInteractionStore({ action: 'edit', selectedMessages: ['msg-1'] }); const item = createMockMessage({ id: 'msg-1' }); - const { getByTestId } = render( - <Provider store={mockedStore}> - <InteractionStoreContext.Provider value={store}> - <MessageContainer item={item} {...baseProps} /> - </InteractionStoreContext.Provider> - </Provider> + const { getByTestId } = renderWithMessageProviders( + <InteractionStoreContext.Provider value={store}> + <MessageContainer item={item} {...baseProps} /> + </InteractionStoreContext.Provider>, + { room: pickMessageRoomState(baseProps) } ); expect(getByTestId('message-editing-msg-1')).toBeTruthy(); }); @@ -111,12 +110,11 @@ describe('edit highlight reacts to InteractionStore', () => { it('removes editing testID when the store resets', async () => { const store = createInteractionStore({ action: 'edit', selectedMessages: ['msg-1'] }); const item = createMockMessage({ id: 'msg-1' }); - const { queryByTestId } = render( - <Provider store={mockedStore}> - <InteractionStoreContext.Provider value={store}> - <MessageContainer item={item} {...baseProps} /> - </InteractionStoreContext.Provider> - </Provider> + const { queryByTestId } = renderWithMessageProviders( + <InteractionStoreContext.Provider value={store}> + <MessageContainer item={item} {...baseProps} /> + </InteractionStoreContext.Provider>, + { room: pickMessageRoomState(baseProps) } ); await act(() => { store.getState().actions.reset(); diff --git a/app/containers/message/testHelpers.tsx b/app/containers/message/testHelpers.tsx new file mode 100644 index 00000000000..29c098dc288 --- /dev/null +++ b/app/containers/message/testHelpers.tsx @@ -0,0 +1,52 @@ +import { type ReactElement, type ReactNode } from 'react'; +import { Provider } from 'react-redux'; +import { render } from '@testing-library/react-native'; + +import { type TAnyMessageModel } from '../../definitions'; +import { mockedStore } from '../../reducers/mockedStore'; +import MessageContext, { type IMessageContext } from './Context'; +import { MessageProvider } from './MessageStore'; +import { MessageRoomProvider, pickMessageRoomState, type MessageRoomState } from './MessageRoomStore'; + +interface IMessageProvidersOptions { + item?: TAnyMessageModel; + previousItem?: TAnyMessageModel; + context?: Partial<IMessageContext>; + room?: Partial<MessageRoomState>; + withRedux?: boolean; +} + +// Provider stack every message-subtree test needs during the MessageContext → room-store dual-run: +// Redux → MessageRoomProvider (new) → MessageContext.Provider (legacy, only when `context` given) +// → MessageProvider (per-message store, only when `item` given). `room` defaults to the room-scoped +// slice of `context`, so a single source (`context`) feeds both providers with identical values. +export const MessageProviders = ({ + item, + previousItem, + context, + room, + withRedux = true, + children +}: IMessageProvidersOptions & { children: ReactNode }): ReactElement => { + const roomState = room ?? pickMessageRoomState(context ?? {}); + + let tree: ReactNode = children; + if (item) { + tree = ( + <MessageProvider item={item} previousItem={previousItem}> + {tree} + </MessageProvider> + ); + } + if (context !== undefined) { + tree = <MessageContext.Provider value={context as IMessageContext}>{tree}</MessageContext.Provider>; + } + tree = <MessageRoomProvider {...roomState}>{tree}</MessageRoomProvider>; + if (withRedux) { + tree = <Provider store={mockedStore}>{tree}</Provider>; + } + return tree as ReactElement; +}; + +export const renderWithMessageProviders = (ui: ReactElement, options: IMessageProvidersOptions = {}) => + render(<MessageProviders {...options}>{ui}</MessageProviders>); From 263729b6412c17ba2ed49d75fe272bea6a15521d Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 15:13:36 -0300 Subject: [PATCH 019/144] refactor: read fetchThreadName via room-store selector in RepliedThread (NATIVE-1376) First stable-field consumer off MessageContext. RepliedThread now subscribes only to fetchThreadName (a stable ref) via useFetchThreadName(), so it no longer re-renders on this row's send/decrypt/status transitions. --- app/containers/message/RepliedThread.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/containers/message/RepliedThread.tsx b/app/containers/message/RepliedThread.tsx index 1a3fe1aae8f..f8fde67b15e 100644 --- a/app/containers/message/RepliedThread.tsx +++ b/app/containers/message/RepliedThread.tsx @@ -1,4 +1,4 @@ -import { useContext, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { View } from 'react-native'; import { CustomIcon } from '../CustomIcon'; @@ -8,14 +8,14 @@ import { MarkdownPreview } from '../markdown'; import { type IMessageRepliedThread } from './interfaces'; import { useTheme } from '../../theme'; import { AvatarContainer } from './MessageAvatar'; -import MessageContext from './Context'; +import { useFetchThreadName } from './MessageRoomStore'; import { useIsEncrypted, useRepliedThreadData } from './MessageStore'; const RepliedThread = ({ isHeader }: IMessageRepliedThread) => { 'use memo'; const { colors } = useTheme(); - const { fetchThreadName } = useContext(MessageContext); + const fetchThreadName = useFetchThreadName(); const { tmid, tmsg, id } = useRepliedThreadData(); const isEncrypted = useIsEncrypted(); const [msg, setMsg] = useState(isEncrypted ? I18n.t('Encrypted_message') : tmsg); From 1bc61ac42765ba3e45388efa07d606420f74b3c0 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 15:22:50 -0300 Subject: [PATCH 020/144] refactor: read grouping/thread/translate config via room store in MessageStore hooks (NATIVE-1376) useMessageGrouping, useThreadPosition and useMessageText now read broadcast, Message_GroupingPeriod, isThreadRoom, user and auto-translate settings from the per-instance MessageRoomStore selectors instead of useContext(MessageContext). Consumer tests (Content, Thread, useMessageAccessibilityLabel, MessageStore) mount MessageRoomProvider alongside the legacy MessageContext.Provider, mirroring values via pickMessageRoomState during the dual-run migration. --- app/containers/message/Content.test.tsx | 25 +++++++++++-------- app/containers/message/MessageStore.test.tsx | 16 ++++++------ app/containers/message/MessageStore.tsx | 11 ++++---- app/containers/message/Thread.test.tsx | 25 +++++++++++-------- .../useMessageAccessibilityLabel.test.tsx | 13 ++++++---- 5 files changed, 51 insertions(+), 39 deletions(-) diff --git a/app/containers/message/Content.test.tsx b/app/containers/message/Content.test.tsx index 618c59ae0ae..6cc99af01a8 100644 --- a/app/containers/message/Content.test.tsx +++ b/app/containers/message/Content.test.tsx @@ -4,6 +4,7 @@ import { render } from '@testing-library/react-native'; import Content from './Content'; import MessageContext from './Context'; import { MessageProvider } from './MessageStore'; +import { MessageRoomProvider, pickMessageRoomState } from './MessageRoomStore'; import { setUser } from '../../actions/login'; import { mockedStore } from '../../reducers/mockedStore'; import { type IAttachment, type TAnyMessageModel } from '../../definitions'; @@ -57,19 +58,21 @@ const buildItem = (msg: string | undefined, attachments: IAttachment[] | undefin const tree = (overrides: TOverrides) => { const { msg, attachments, isEncrypted, ...contentProps } = overrides; + const contextValue = { + user: { username: 'john' }, + onLinkPress: jest.fn(), + getCustomEmoji: jest.fn(), + navToRoomInfo: jest.fn() + }; return ( <Provider store={mockedStore}> - <MessageContext.Provider - value={{ - user: { username: 'john' }, - onLinkPress: jest.fn(), - getCustomEmoji: jest.fn(), - navToRoomInfo: jest.fn() - }}> - <MessageProvider item={buildItem(msg, attachments, isEncrypted)}> - <Content {...(baseProps as IMessageContent)} {...(contentProps as IMessageContent)} /> - </MessageProvider> - </MessageContext.Provider> + <MessageRoomProvider {...pickMessageRoomState(contextValue)}> + <MessageContext.Provider value={contextValue}> + <MessageProvider item={buildItem(msg, attachments, isEncrypted)}> + <Content {...(baseProps as IMessageContent)} {...(contentProps as IMessageContent)} /> + </MessageProvider> + </MessageContext.Provider> + </MessageRoomProvider> </Provider> ); }; diff --git a/app/containers/message/MessageStore.test.tsx b/app/containers/message/MessageStore.test.tsx index 945abb47ba2..2b2c5f6f22a 100644 --- a/app/containers/message/MessageStore.test.tsx +++ b/app/containers/message/MessageStore.test.tsx @@ -3,7 +3,7 @@ import { act, render } from '@testing-library/react-native'; import { type TAnyMessageModel } from '../../definitions'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; import { messagesStatus } from '../../lib/constants/messagesStatus'; -import MessageContext, { type IMessageContext } from './Context'; +import { MessageRoomProvider, pickMessageRoomState, type MessageRoomState } from './MessageRoomStore'; import { MessageProvider, useAvatar, @@ -114,12 +114,12 @@ const MsgValueReporter = ({ spy }: { spy: jest.Mock }) => { return null; }; -// Derived hooks read both the per-row store/item/previousItem and the config in MessageContext, -// so the probe needs both providers mounted. +// Derived hooks read both the per-row store/item/previousItem and the room state in +// MessageRoomProvider, so the probe needs both providers mounted. const renderDerived = ( item: TAnyMessageModel, useHook: () => unknown, - { previousItem, config }: { previousItem?: TAnyMessageModel; config?: Partial<IMessageContext> } = {} + { previousItem, config }: { previousItem?: TAnyMessageModel; config?: Partial<MessageRoomState> } = {} ) => { const spy = jest.fn(); const Probe = () => { @@ -127,11 +127,11 @@ const renderDerived = ( return null; }; render( - <MessageContext.Provider value={(config ?? {}) as IMessageContext}> + <MessageRoomProvider {...pickMessageRoomState(config ?? {})}> <MessageProvider item={item} previousItem={previousItem}> <Probe /> </MessageProvider> - </MessageContext.Provider> + </MessageRoomProvider> ); const latest = () => { const { calls } = spy.mock; @@ -489,11 +489,11 @@ describe('MessageStore', () => { return calls[calls.length - 1]?.[0]; }; const wrap = (previousItem: TAnyMessageModel) => ( - <MessageContext.Provider value={{ broadcast: false, Message_GroupingPeriod: 300 } as IMessageContext}> + <MessageRoomProvider {...pickMessageRoomState({ broadcast: false, Message_GroupingPeriod: 300 })}> <MessageProvider item={item} previousItem={previousItem}> <Probe /> </MessageProvider> - </MessageContext.Provider> + </MessageRoomProvider> ); const { rerender } = render(wrap(previousTemp)); diff --git a/app/containers/message/MessageStore.tsx b/app/containers/message/MessageStore.tsx index b85264d4e59..bcb565ecc7e 100644 --- a/app/containers/message/MessageStore.tsx +++ b/app/containers/message/MessageStore.tsx @@ -3,11 +3,10 @@ import { createStore, useStore } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; import { type TAnyMessageModel } from '../../definitions'; -import MessageContext from './Context'; import { getMessageTranslation } from './utils'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; import { messagesStatus } from '../../lib/constants/messagesStatus'; -import { useAutoTranslate, useMessageUser } from './MessageRoomStore'; +import { useAutoTranslate, useBroadcast, useIsThreadRoom, useMessageGroupingPeriod, useMessageUser } from './MessageRoomStore'; const createMessageStore = () => createStore(() => ({ tick: 0 })); @@ -173,7 +172,8 @@ const computeIsHeader = ( export const useMessageGrouping = (): boolean => { const { store, item, previousItem } = useMessageCtx(); - const { broadcast, Message_GroupingPeriod } = useContext(MessageContext); + const broadcast = useBroadcast(); + const Message_GroupingPeriod = useMessageGroupingPeriod(); return useStore(store, () => computeIsHeader(previousItem, item, !!broadcast, Message_GroupingPeriod, item.status === messagesStatus.ERROR) ); @@ -181,7 +181,7 @@ export const useMessageGrouping = (): boolean => { export const useThreadPosition = (): { isThreadReply: boolean; isThreadSequential: boolean } => { const { store, item, previousItem } = useMessageCtx(); - const { isThreadRoom } = useContext(MessageContext); + const isThreadRoom = useIsThreadRoom(); return useStore( store, useShallow(() => { @@ -234,7 +234,8 @@ export const useTranslateLanguage = (): string | undefined => { export const useMessageText = (): { messageText: TAnyMessageModel['msg']; isTranslated: boolean } => { const { store, item } = useMessageCtx(); - const { user, autoTranslateRoom, autoTranslateLanguage } = useContext(MessageContext); + const user = useMessageUser(); + const { autoTranslateRoom, autoTranslateLanguage } = useAutoTranslate(); return useStore( store, useShallow(() => { diff --git a/app/containers/message/Thread.test.tsx b/app/containers/message/Thread.test.tsx index 280cd8a9ff1..dfba3d636c1 100644 --- a/app/containers/message/Thread.test.tsx +++ b/app/containers/message/Thread.test.tsx @@ -3,6 +3,7 @@ import { render } from '@testing-library/react-native'; import Thread from './Thread'; import MessageContext from './Context'; import { MessageProvider } from './MessageStore'; +import { MessageRoomProvider, pickMessageRoomState } from './MessageRoomStore'; import { type TAnyMessageModel } from '../../definitions'; const baseContextValue = { @@ -18,11 +19,13 @@ const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel const renderThread = (item: TAnyMessageModel, props: Parameters<typeof Thread>[0]) => render( - <MessageContext.Provider value={baseContextValue}> - <MessageProvider item={item}> - <Thread {...props} /> - </MessageProvider> - </MessageContext.Provider> + <MessageRoomProvider {...pickMessageRoomState(baseContextValue)}> + <MessageContext.Provider value={baseContextValue}> + <MessageProvider item={item}> + <Thread {...props} /> + </MessageProvider> + </MessageContext.Provider> + </MessageRoomProvider> ); describe('Thread — tlm-only update regression', () => { @@ -36,11 +39,13 @@ describe('Thread — tlm-only update regression', () => { const updatedItem = buildItem({ tlm: new Date('2024-01-01T00:00:00Z') }); rerender( - <MessageContext.Provider value={baseContextValue}> - <MessageProvider item={updatedItem}> - <Thread isThreadRoom={false} /> - </MessageProvider> - </MessageContext.Provider> + <MessageRoomProvider {...pickMessageRoomState(baseContextValue)}> + <MessageContext.Provider value={baseContextValue}> + <MessageProvider item={updatedItem}> + <Thread isThreadRoom={false} /> + </MessageProvider> + </MessageContext.Provider> + </MessageRoomProvider> ); expect(getByTestId('message-thread-button-hello')).toBeTruthy(); diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx b/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx index b63343e4bc3..12e596dfa5a 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx @@ -3,6 +3,7 @@ import { render } from '@testing-library/react-native'; import { useMessageAccessibilityLabel } from './useMessageAccessibilityLabel'; import MessageContext, { type IMessageContext } from '../Context'; import { MessageProvider } from '../MessageStore'; +import { MessageRoomProvider, pickMessageRoomState } from '../MessageRoomStore'; import { type TAnyMessageModel } from '../../../definitions'; import { E2E_MESSAGE_TYPE } from '../../../lib/constants/keys'; @@ -37,11 +38,13 @@ const renderLabel = ( return null; }; render( - <MessageContext.Provider value={config as IMessageContext}> - <MessageProvider item={item} previousItem={previousItem}> - <Probe /> - </MessageProvider> - </MessageContext.Provider> + <MessageRoomProvider {...pickMessageRoomState(config)}> + <MessageContext.Provider value={config as IMessageContext}> + <MessageProvider item={item} previousItem={previousItem}> + <Probe /> + </MessageProvider> + </MessageContext.Provider> + </MessageRoomProvider> ); const { calls } = spy.mock; return calls[calls.length - 1][0]; From 8e7ce17ef7c5f9267037ca2a037320774190f594 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 15:25:21 -0300 Subject: [PATCH 021/144] refactor: read stable message fields via room-store selectors in leaf components (NATIVE-1376) MessageAvatar, User, Urls, CallButton, Video, Audio and Quote now read user, navToRoomInfo, getCustomEmoji, handleEnterCall, baseUrl, rid (room store) plus id and translateLanguage (per-message store) through selector hooks instead of useContext(MessageContext). No test or story changes needed: these leaves render under Message.stories and the container test, which already mount MessageRoomProvider and MessageProvider. --- app/containers/message/CallButton.tsx | 5 ++--- app/containers/message/Components/Attachments/Audio.tsx | 8 +++++--- app/containers/message/Components/Attachments/Quote.tsx | 8 +++++--- app/containers/message/Components/Attachments/Video.tsx | 6 +++--- app/containers/message/MessageAvatar.tsx | 8 +++++--- app/containers/message/Urls.tsx | 5 +++-- app/containers/message/User.tsx | 6 +++--- 7 files changed, 26 insertions(+), 20 deletions(-) diff --git a/app/containers/message/CallButton.tsx b/app/containers/message/CallButton.tsx index 4677d39da16..bc198477baa 100644 --- a/app/containers/message/CallButton.tsx +++ b/app/containers/message/CallButton.tsx @@ -1,4 +1,3 @@ -import { useContext } from 'react'; import { Text, View } from 'react-native'; import Touchable from './Touchable'; @@ -8,12 +7,12 @@ import I18n from '../../i18n'; import { CustomIcon } from '../CustomIcon'; import { themes } from '../../lib/constants/colors'; import { useTheme } from '../../theme'; -import MessageContext from './Context'; +import { useHandleEnterCall } from './MessageRoomStore'; const CallButton = () => { 'use memo'; - const { handleEnterCall } = useContext(MessageContext); + const handleEnterCall = useHandleEnterCall(); const { theme } = useTheme(); return ( <View style={styles.buttonContainer}> diff --git a/app/containers/message/Components/Attachments/Audio.tsx b/app/containers/message/Components/Attachments/Audio.tsx index 90a6cef6844..498776c7c28 100644 --- a/app/containers/message/Components/Attachments/Audio.tsx +++ b/app/containers/message/Components/Attachments/Audio.tsx @@ -1,12 +1,12 @@ -import { useContext } from 'react'; import { View } from 'react-native'; import { type IAttachment, type IUserMessage } from '../../../../definitions'; import { type TGetCustomEmoji } from '../../../../definitions/IEmoji'; import AudioPlayer from '../../../AudioPlayer'; import Markdown from '../../../markdown'; -import MessageContext from '../../Context'; import { useMediaAutoDownload } from '../../hooks/useMediaAutoDownload'; +import { useMessageUser, useRid } from '../../MessageRoomStore'; +import { useMessageId } from '../../MessageStore'; interface IMessageAudioProps { file: IAttachment; @@ -18,7 +18,9 @@ interface IMessageAudioProps { const MessageAudio = ({ file, getCustomEmoji, author, msg }: IMessageAudioProps) => { 'use memo'; - const { user, id, rid } = useContext(MessageContext); + const user = useMessageUser(); + const id = useMessageId(); + const rid = useRid(); const { status, onPress, url } = useMediaAutoDownload({ file, author }); return ( diff --git a/app/containers/message/Components/Attachments/Quote.tsx b/app/containers/message/Components/Attachments/Quote.tsx index a3723108067..7bb3884bfff 100644 --- a/app/containers/message/Components/Attachments/Quote.tsx +++ b/app/containers/message/Components/Attachments/Quote.tsx @@ -1,17 +1,19 @@ -import { useContext, type FC } from 'react'; +import { type FC } from 'react'; import { View } from 'react-native'; import { Reply } from './components'; -import MessageContext from '../../Context'; import { type IMessageAttachments } from '../../interfaces'; import { type IAttachment } from '../../../../definitions'; import { getMessageFromAttachment } from '../../utils'; import { isQuoteAttachment } from './utils'; +import { useGetCustomEmoji } from '../../MessageRoomStore'; +import { useTranslateLanguage } from '../../MessageStore'; const Quote: FC<IMessageAttachments> = ({ attachments, timeFormat }: IMessageAttachments) => { 'use memo'; - const { translateLanguage, getCustomEmoji = () => null } = useContext(MessageContext); + const translateLanguage = useTranslateLanguage(); + const getCustomEmoji = useGetCustomEmoji() ?? (() => null); const quotes = attachments?.filter(isQuoteAttachment); diff --git a/app/containers/message/Components/Attachments/Video.tsx b/app/containers/message/Components/Attachments/Video.tsx index 4feb5eee7c4..fe04fc2bb93 100644 --- a/app/containers/message/Components/Attachments/Video.tsx +++ b/app/containers/message/Components/Attachments/Video.tsx @@ -1,4 +1,4 @@ -import { useContext, type ReactElement } from 'react'; +import { type ReactElement } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { type IUserMessage } from '../../../../definitions'; @@ -12,9 +12,9 @@ import sharedStyles from '../../../../views/Styles'; import { type TIconsName } from '../../../CustomIcon'; import { LISTENER } from '../../../Toast'; import Markdown from '../../../markdown'; -import MessageContext from '../../Context'; import Touchable from '../../Touchable'; import { useMediaAutoDownload } from '../../hooks/useMediaAutoDownload'; +import { useMessageUser } from '../../MessageRoomStore'; import BlurComponent from '../OverlayComponent'; import { type TDownloadState } from '../../../../lib/methods/handleMediaDownload'; import messageStyles from '../../styles'; @@ -73,7 +73,7 @@ const Thumbnail = ({ status, encrypted = false }: { status: TDownloadState; encr const Video = ({ file, showAttachment, getCustomEmoji, author, msg }: IMessageVideo): ReactElement | null => { 'use memo'; - const { user } = useContext(MessageContext); + const user = useMessageUser(); const { status, onPress, url, isEncrypted, currentFile } = useMediaAutoDownload({ file, author, showAttachment }); const _onPress = async () => { diff --git a/app/containers/message/MessageAvatar.tsx b/app/containers/message/MessageAvatar.tsx index 40737ebed05..4a29c4d5d94 100644 --- a/app/containers/message/MessageAvatar.tsx +++ b/app/containers/message/MessageAvatar.tsx @@ -1,13 +1,13 @@ -import { useContext, type ReactElement } from 'react'; +import { type ReactElement } from 'react'; import { View } from 'react-native'; import Avatar from '../Avatar'; import styles from './styles'; -import MessageContext from './Context'; import { type IMessageAvatar } from './interfaces'; import { SubscriptionType } from '../../definitions'; import { useResponsiveLayout } from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; import { useAvatar, useMessageAuthor, useMessageGrouping } from './MessageStore'; +import { useGetCustomEmoji, useMessageUser, useNavToRoomInfo } from './MessageRoomStore'; const AVATAR_BASE_SIZE = 36; @@ -20,7 +20,9 @@ export const AvatarContainer = ({ children }: { children?: ReactElement | null } const MessageAvatar = ({ small }: IMessageAvatar) => { 'use memo'; - const { user, navToRoomInfo, getCustomEmoji } = useContext(MessageContext); + const user = useMessageUser(); + const navToRoomInfo = useNavToRoomInfo(); + const getCustomEmoji = useGetCustomEmoji(); const { fontScaleLimited } = useResponsiveLayout(); const { avatar, emoji } = useAvatar(); const { u: author } = useMessageAuthor(); diff --git a/app/containers/message/Urls.tsx b/app/containers/message/Urls.tsx index bdbf412d05c..36b45f28328 100644 --- a/app/containers/message/Urls.tsx +++ b/app/containers/message/Urls.tsx @@ -12,10 +12,10 @@ import { useTheme } from '../../theme'; import { LISTENER } from '../Toast'; import EventEmitter from '../../lib/methods/helpers/events'; import I18n from '../../i18n'; -import MessageContext from './Context'; import { type IUrl } from '../../definitions'; import { WidthAwareContext } from './Components/WidthAwareView'; import { useUrls } from './MessageStore'; +import { useBaseUrl, useMessageUser } from './MessageRoomStore'; const styles = StyleSheet.create({ container: { @@ -121,7 +121,8 @@ const Url = ({ url }: { url: IUrl }) => { 'use memo'; const { colors, theme } = useTheme(); - const { baseUrl, user } = useContext(MessageContext); + const baseUrl = useBaseUrl(); + const user = useMessageUser(); const API_Embed = useAppSelector(state => state.settings.API_Embed); const [imageUrl, setImageUrl] = useState<string | null>(null); diff --git a/app/containers/message/User.tsx b/app/containers/message/User.tsx index c873ce30910..f1f0826dbb1 100644 --- a/app/containers/message/User.tsx +++ b/app/containers/message/User.tsx @@ -1,11 +1,9 @@ -import { useContext } from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import { type MessageTypesValues, SubscriptionType } from '../../definitions'; import { useTheme } from '../../theme'; import sharedStyles from '../../views/Styles'; import RightIcons from './Components/RightIcons'; -import MessageContext from './Context'; import { messageHaveAuthorName } from './utils'; import MessageTime from './Time'; import { useResponsiveLayout } from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; @@ -17,6 +15,7 @@ import { useMessageStatus, useMessageText } from './MessageStore'; +import { useMessageUser, useNavToRoomInfo } from './MessageRoomStore'; const styles = StyleSheet.create({ container: { @@ -56,7 +55,8 @@ interface IMessageUser { const User = ({ useRealName, timeFormat, isReadReceiptEnabled }: IMessageUser) => { 'use memo'; - const { user, navToRoomInfo } = useContext(MessageContext); + const user = useMessageUser(); + const navToRoomInfo = useNavToRoomInfo(); const { colors } = useTheme(); const { isLargeFontScale } = useResponsiveLayout(); const isHeader = useMessageGrouping(); From 2e91b79512be46eaa984d4b5d69dab2c9d9f02cc Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 15:30:51 -0300 Subject: [PATCH 022/144] refactor: read stable fields via room/message selectors in attachments and hooks (NATIVE-1376) Attachments, Image/Container, CollapsibleQuote, useMediaAutoDownload and useMessageAccessibilityHint now read translateLanguage/id (per-message store) and user/baseUrl/showAttachment/getCustomEmoji/isThreadRoom (room store) via selector hooks instead of useContext(MessageContext). Their hand-written tests and stories mount MessageRoomProvider (and MessageProvider where a per-message hook is exercised) alongside the legacy MessageContext.Provider, mirroring values through pickMessageRoomState during the dual-run. --- .../Attachments/Attachments.stories.tsx | 76 ++++++++++--------- .../Components/Attachments/Attachments.tsx | 9 ++- .../CollapsibleQuote.stories.tsx | 17 +++-- .../CollapsibleQuote.test.tsx | 18 +++-- .../Attachments/CollapsibleQuote/index.tsx | 8 +- .../Attachments/Image/Container.test.tsx | 28 ++++--- .../Attachments/Image/Container.tsx | 6 +- .../message/hooks/useMediaAutoDownload.tsx | 9 ++- .../useMessageAccessibilityHint.test.tsx | 13 ++-- .../hooks/useMessageAccessibilityHint.ts | 6 +- 10 files changed, 107 insertions(+), 83 deletions(-) diff --git a/app/containers/message/Components/Attachments/Attachments.stories.tsx b/app/containers/message/Components/Attachments/Attachments.stories.tsx index d3567c24ca0..a3cfc04eab2 100644 --- a/app/containers/message/Components/Attachments/Attachments.stories.tsx +++ b/app/containers/message/Components/Attachments/Attachments.stories.tsx @@ -1,9 +1,13 @@ +import { type ReactNode } from 'react'; import { View } from 'react-native'; import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { selectServerSuccess } from '../../../../actions/server'; +import { type TAnyMessageModel } from '../../../../definitions'; import MessageContext from '../../Context'; +import { MessageRoomProvider, pickMessageRoomState } from '../../MessageRoomStore'; +import { MessageProvider } from '../../MessageStore'; import Attachments from './Attachments'; const mockMessageContext = { @@ -16,6 +20,18 @@ const mockMessageContext = { showAttachment: undefined }; +const mockItem = { id: 'msg-id', msg: '', u: { username: 'rocket.cat' }, autoTranslate: false } as unknown as TAnyMessageModel; + +const StoryWrapper = ({ store, children }: { store: any; children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState(mockMessageContext)}> + <MessageProvider item={mockItem}> + <MessageContext.Provider value={mockMessageContext}>{children}</MessageContext.Provider> + </MessageProvider> + </MessageRoomProvider> + </Provider> +); + const MOCK_IMAGE_1 = { image_url: 'https://picsum.photos/seed/1/400/300', image_type: 'image/jpeg', @@ -53,55 +69,45 @@ export default { // Single image on server < 8.4: renders with Markdown caption if description is set export const SingleImageOldServer = () => ( - <Provider store={oldServerStore}> - <MessageContext.Provider value={mockMessageContext}> - <View style={{ padding: 10, width: 350 }}> - <Attachments attachments={[MOCK_IMAGE_WITH_ALT]} timeFormat='LT' /> - </View> - </MessageContext.Provider> - </Provider> + <StoryWrapper store={oldServerStore}> + <View style={{ padding: 10, width: 350 }}> + <Attachments attachments={[MOCK_IMAGE_WITH_ALT]} timeFormat='LT' /> + </View> + </StoryWrapper> ); // Single image on server >= 8.4: renders with AltTextLabel pill below image export const SingleImageNewServer = () => ( - <Provider store={newServerStore}> - <MessageContext.Provider value={mockMessageContext}> - <View style={{ padding: 10, width: 350 }}> - <Attachments attachments={[MOCK_IMAGE_WITH_ALT]} timeFormat='LT' /> - </View> - </MessageContext.Provider> - </Provider> + <StoryWrapper store={newServerStore}> + <View style={{ padding: 10, width: 350 }}> + <Attachments attachments={[MOCK_IMAGE_WITH_ALT]} timeFormat='LT' /> + </View> + </StoryWrapper> ); // Single image without alt text on new server: no label export const SingleImageNoAlt = () => ( - <Provider store={newServerStore}> - <MessageContext.Provider value={mockMessageContext}> - <View style={{ padding: 10, width: 350 }}> - <Attachments attachments={[MOCK_IMAGE_1]} timeFormat='LT' /> - </View> - </MessageContext.Provider> - </Provider> + <StoryWrapper store={newServerStore}> + <View style={{ padding: 10, width: 350 }}> + <Attachments attachments={[MOCK_IMAGE_1]} timeFormat='LT' /> + </View> + </StoryWrapper> ); // Multiple images on server >= 8.4: each attachment renders as its own image row export const MultipleImagesNewServer = () => ( - <Provider store={newServerStore}> - <MessageContext.Provider value={mockMessageContext}> - <View style={{ padding: 10, width: 350 }}> - <Attachments attachments={MOCK_MULTIPLE_IMAGES} timeFormat='LT' /> - </View> - </MessageContext.Provider> - </Provider> + <StoryWrapper store={newServerStore}> + <View style={{ padding: 10, width: 350 }}> + <Attachments attachments={MOCK_MULTIPLE_IMAGES} timeFormat='LT' /> + </View> + </StoryWrapper> ); // 4 images on server < 8.4: renders stacked (existing behavior) export const MultipleImagesOldServer = () => ( - <Provider store={oldServerStore}> - <MessageContext.Provider value={mockMessageContext}> - <View style={{ padding: 10, width: 350 }}> - <Attachments attachments={MOCK_MULTIPLE_IMAGES.slice(0, 4)} timeFormat='LT' /> - </View> - </MessageContext.Provider> - </Provider> + <StoryWrapper store={oldServerStore}> + <View style={{ padding: 10, width: 350 }}> + <Attachments attachments={MOCK_MULTIPLE_IMAGES.slice(0, 4)} timeFormat='LT' /> + </View> + </StoryWrapper> ); diff --git a/app/containers/message/Components/Attachments/Attachments.tsx b/app/containers/message/Components/Attachments/Attachments.tsx index 4f51b81024b..bd47386e8f5 100644 --- a/app/containers/message/Components/Attachments/Attachments.tsx +++ b/app/containers/message/Components/Attachments/Attachments.tsx @@ -1,4 +1,4 @@ -import { useContext, type FC } from 'react'; +import { type FC } from 'react'; import { View } from 'react-native'; import Image from './Image'; @@ -7,7 +7,8 @@ import Video from './Video'; import CollapsibleQuote from './CollapsibleQuote'; import AttachedActions from './AttachedActions'; import Reply from './Reply'; -import MessageContext from '../../Context'; +import { useShowAttachment, useGetCustomEmoji } from '../../MessageRoomStore'; +import { useTranslateLanguage } from '../../MessageStore'; import { type IMessageAttachments } from '../../interfaces'; import { getMessageFromAttachment } from '../../utils'; import { isContentAttachment } from './utils'; @@ -15,7 +16,9 @@ import { isContentAttachment } from './utils'; const Attachments: FC<IMessageAttachments> = ({ attachments, timeFormat, author }: IMessageAttachments) => { 'use memo'; - const { translateLanguage, showAttachment, getCustomEmoji = () => null } = useContext(MessageContext); + const translateLanguage = useTranslateLanguage(); + const showAttachment = useShowAttachment(); + const getCustomEmoji = useGetCustomEmoji() ?? (() => null); const nonQuoteAttachments = attachments?.filter(isContentAttachment); diff --git a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx b/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx index 72a56c70f4c..e0ae9a2fb3b 100644 --- a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx +++ b/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx @@ -1,6 +1,7 @@ import { View } from 'react-native'; import MessageContext from '../../../Context'; +import { MessageRoomProvider, pickMessageRoomState } from '../../../MessageRoomStore'; import CollapsibleQuote from '.'; const testAttachment = { @@ -23,12 +24,14 @@ export default { export const Item = () => ( <View style={{ padding: 10 }}> - <MessageContext.Provider - value={{ - onLongPress: () => {}, - user: { username: 'Marcos' } - }}> - <CollapsibleQuote attachment={testAttachment} getCustomEmoji={() => null} timeFormat='LT' /> - </MessageContext.Provider> + <MessageRoomProvider {...pickMessageRoomState({ onLongPress: () => {}, user: { username: 'Marcos' } })}> + <MessageContext.Provider + value={{ + onLongPress: () => {}, + user: { username: 'Marcos' } + }}> + <CollapsibleQuote attachment={testAttachment} getCustomEmoji={() => null} timeFormat='LT' /> + </MessageContext.Provider> + </MessageRoomProvider> </View> ); diff --git a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx b/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx index b86d3135cc6..8678861b9e5 100644 --- a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx +++ b/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx @@ -4,6 +4,7 @@ import { Provider } from 'react-redux'; import { setUser } from '../../../../../actions/login'; import { mockedStore } from '../../../../../reducers/mockedStore'; import MessageContext from '../../../Context'; +import { MessageRoomProvider, pickMessageRoomState } from '../../../MessageRoomStore'; import CollapsibleQuote from '.'; const testAttachment = { @@ -37,15 +38,18 @@ const initialMockedStoreState = () => { initialMockedStoreState(); +const contextValue = { + onLongPress: () => {}, + user: { username: 'Marcos' } +}; + const Render = () => ( <Provider store={mockedStore}> - <MessageContext.Provider - value={{ - onLongPress: () => {}, - user: { username: 'Marcos' } - }}> - <CollapsibleQuote attachment={testAttachment} getCustomEmoji={mockFn} timeFormat='LT' /> - </MessageContext.Provider> + <MessageRoomProvider {...pickMessageRoomState(contextValue)}> + <MessageContext.Provider value={contextValue}> + <CollapsibleQuote attachment={testAttachment} getCustomEmoji={mockFn} timeFormat='LT' /> + </MessageContext.Provider> + </MessageRoomProvider> </Provider> ); diff --git a/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx b/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx index 4e7e1d99060..b06c5947996 100644 --- a/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx +++ b/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx @@ -1,5 +1,5 @@ import { transparentize } from 'color2k'; -import { useContext, useState } from 'react'; +import { useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { themes } from '../../../../../lib/constants/colors'; @@ -9,7 +9,7 @@ import { CustomIcon } from '../../../../CustomIcon'; import { useTheme } from '../../../../../theme'; import sharedStyles from '../../../../../views/Styles'; import Markdown from '../../../../markdown'; -import MessageContext from '../../../Context'; +import { useMessageUser } from '../../../MessageRoomStore'; import Touchable from '../../../Touchable'; import { BUTTON_HIT_SLOP } from '../../../utils'; @@ -77,7 +77,7 @@ interface IMessageReply { const AttText = ({ text, getCustomEmoji }: IMessageAttText) => { 'use memo'; - const { user } = useContext(MessageContext); + const user = useMessageUser(); if (!text) { return null; @@ -90,7 +90,7 @@ const Fields = ({ attachment, getCustomEmoji }: IMessageFields) => { 'use memo'; const { theme } = useTheme(); - const { user } = useContext(MessageContext); + const user = useMessageUser(); if (!attachment.fields) { return null; diff --git a/app/containers/message/Components/Attachments/Image/Container.test.tsx b/app/containers/message/Components/Attachments/Image/Container.test.tsx index c0fe3978f2c..f610fb250f2 100644 --- a/app/containers/message/Components/Attachments/Image/Container.test.tsx +++ b/app/containers/message/Components/Attachments/Image/Container.test.tsx @@ -3,6 +3,7 @@ import { type ComponentProps } from 'react'; import { A11y } from 'react-native-a11y-order'; import MessageContext from '../../../Context'; +import { MessageRoomProvider, pickMessageRoomState } from '../../../MessageRoomStore'; import ImageContainer from './Container'; jest.mock('../../../../markdown', () => { @@ -30,21 +31,24 @@ jest.mock('../../../../../lib/hooks/useAltTextSupported', () => ({ useAltTextSupported: () => mockUseAltTextSupported() })); -const renderImageContainer = (props?: Partial<ComponentProps<typeof ImageContainer>>) => - render( +const renderImageContainer = (props?: Partial<ComponentProps<typeof ImageContainer>>) => { + const contextValue = { + id: 'message-id', + baseUrl: 'https://open.rocket.chat', + user: { id: 'user-id', username: 'rocket.cat', token: 'token' }, + onLongPress: jest.fn(), + translateLanguage: undefined + }; + return render( <A11y.Order> - <MessageContext.Provider - value={{ - id: 'message-id', - baseUrl: 'https://open.rocket.chat', - user: { id: 'user-id', username: 'rocket.cat', token: 'token' }, - onLongPress: jest.fn(), - translateLanguage: undefined - }}> - <ImageContainer file={{ image_url: 'https://open.rocket.chat/image.png', image_type: 'image/png' }} {...props} /> - </MessageContext.Provider> + <MessageRoomProvider {...pickMessageRoomState(contextValue)}> + <MessageContext.Provider value={contextValue}> + <ImageContainer file={{ image_url: 'https://open.rocket.chat/image.png', image_type: 'image/png' }} {...props} /> + </MessageContext.Provider> + </MessageRoomProvider> </A11y.Order> ); +}; describe('ImageContainer', () => { beforeEach(() => { diff --git a/app/containers/message/Components/Attachments/Image/Container.tsx b/app/containers/message/Components/Attachments/Image/Container.tsx index c751b51e605..c7264cfd84b 100644 --- a/app/containers/message/Components/Attachments/Image/Container.tsx +++ b/app/containers/message/Components/Attachments/Image/Container.tsx @@ -1,4 +1,4 @@ -import { useContext, type ReactElement } from 'react'; +import { type ReactElement } from 'react'; import { View } from 'react-native'; import Markdown from '../../../../markdown'; @@ -7,7 +7,7 @@ import { Button } from './Button'; import { MessageImage } from './Image'; import AltTextLabel from '../../../../AltTextLabel'; import { type IImageContainer } from './definitions'; -import MessageContext from '../../../Context'; +import { useMessageUser } from '../../../MessageRoomStore'; import { WidthAwareView } from '../../WidthAwareView'; import { useAltTextSupported } from '../../../../../lib/hooks/useAltTextSupported'; import I18n from '../../../../../i18n'; @@ -23,7 +23,7 @@ const ImageContainer = ({ }: IImageContainer): ReactElement | null => { 'use memo'; - const { user } = useContext(MessageContext); + const user = useMessageUser(); const { status, onPress, url, isEncrypted } = useMediaAutoDownload({ file, author, showAttachment }); const isAltTextSupported = useAltTextSupported(); const altText = file.altText || (isAltTextSupported ? msg : undefined); diff --git a/app/containers/message/hooks/useMediaAutoDownload.tsx b/app/containers/message/hooks/useMediaAutoDownload.tsx index b2accf2c3e0..29302939675 100644 --- a/app/containers/message/hooks/useMediaAutoDownload.tsx +++ b/app/containers/message/hooks/useMediaAutoDownload.tsx @@ -1,4 +1,4 @@ -import { useCallback, useContext, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { type IAttachment, type IUserMessage } from '../../../definitions'; import { isImageBase64 } from '../../../lib/methods/isImageBase64'; @@ -13,7 +13,8 @@ import { } from '../../../lib/methods/handleMediaDownload'; import { emitter } from '../../../lib/methods/helpers/emitter'; import { formatAttachmentUrl } from '../../../lib/methods/helpers/formatAttachmentUrl'; -import MessageContext from '../Context'; +import { useBaseUrl, useMessageUser } from '../MessageRoomStore'; +import { useMessageId } from '../MessageStore'; import { useFile } from './useFile'; const getFileType = (file: IAttachment): MediaTypes | null => { @@ -60,7 +61,9 @@ export const useMediaAutoDownload = ({ 'use memo'; const fileType = getFileType(file) ?? 'image'; - const { id, baseUrl, user } = useContext(MessageContext); + const id = useMessageId(); + const baseUrl = useBaseUrl(); + const user = useMessageUser(); const [status, setStatus] = useState<TDownloadState>('to-download'); const [currentFile, setCurrentFile] = useFile(file, id ?? ''); const originalUrl = getOriginalURL(file); diff --git a/app/containers/message/hooks/useMessageAccessibilityHint.test.tsx b/app/containers/message/hooks/useMessageAccessibilityHint.test.tsx index c2d86e24523..bb349251c9f 100644 --- a/app/containers/message/hooks/useMessageAccessibilityHint.test.tsx +++ b/app/containers/message/hooks/useMessageAccessibilityHint.test.tsx @@ -2,6 +2,7 @@ import { render } from '@testing-library/react-native'; import { useMessageAccessibilityHint } from './useMessageAccessibilityHint'; import MessageContext, { type IMessageContext } from '../Context'; +import { MessageRoomProvider, pickMessageRoomState } from '../MessageRoomStore'; import { MessageProvider } from '../MessageStore'; import { type TAnyMessageModel } from '../../../definitions'; @@ -15,11 +16,13 @@ const renderHint = (item: TAnyMessageModel, config: Partial<IMessageContext> = { return null; }; render( - <MessageContext.Provider value={config as IMessageContext}> - <MessageProvider item={item}> - <Probe /> - </MessageProvider> - </MessageContext.Provider> + <MessageRoomProvider {...pickMessageRoomState(config)}> + <MessageContext.Provider value={config as IMessageContext}> + <MessageProvider item={item}> + <Probe /> + </MessageProvider> + </MessageContext.Provider> + </MessageRoomProvider> ); const { calls } = spy.mock; return calls[calls.length - 1][0]; diff --git a/app/containers/message/hooks/useMessageAccessibilityHint.ts b/app/containers/message/hooks/useMessageAccessibilityHint.ts index 31988d90ee8..feb81526e4d 100644 --- a/app/containers/message/hooks/useMessageAccessibilityHint.ts +++ b/app/containers/message/hooks/useMessageAccessibilityHint.ts @@ -1,14 +1,12 @@ -import { useContext } from 'react'; - import i18n from '../../../i18n'; -import MessageContext from '../Context'; +import { useIsThreadRoom } from '../MessageRoomStore'; import { useThreadData } from '../MessageStore'; export const useMessageAccessibilityHint = (): string | undefined => { 'use memo'; const { tlm, tcount } = useThreadData(); - const { isThreadRoom } = useContext(MessageContext); + const isThreadRoom = useIsThreadRoom(); const hasThread = !!tlm && !isThreadRoom && tcount !== null; return hasThread ? i18n.t('A11y_press_to_view_thread') : undefined; From c49a81bcb9802faf0399aec60725c52a7297828d Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 15:44:09 -0300 Subject: [PATCH 023/144] fix: wrap forwarded-message preview in MessageRoomProvider (NATIVE-1376) MessagePreview renders the message container outside RoomView/MessagesView/SearchMessagesView, which each already mount a MessageRoomProvider. After the message container's grouping/thread/translate hooks were repointed to the room store, this path had no provider and threw 'Message room hooks must be used within a MessageRoomProvider' on render, crashing ForwardMessageView. Seed a MessageRoomProvider from the props MessagePreview already holds (user, baseUrl, getCustomEmoji, rid); the remaining room fields stay unset to preserve the legacy preview defaults. Adds a regression test rendering MessagePreview under Redux only. --- app/containers/message/Preview.test.tsx | 48 +++++++++++++++++++++++++ app/containers/message/Preview.tsx | 23 ++++++------ 2 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 app/containers/message/Preview.test.tsx diff --git a/app/containers/message/Preview.test.tsx b/app/containers/message/Preview.test.tsx new file mode 100644 index 00000000000..81ed39005e5 --- /dev/null +++ b/app/containers/message/Preview.test.tsx @@ -0,0 +1,48 @@ +import { Provider } from 'react-redux'; +import { render } from '@testing-library/react-native'; + +import MessagePreview from './Preview'; +import { mockedStore } from '../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../definitions'; + +jest.mock('./Touch', () => { + const { forwardRef } = require('react'); + const { TouchableOpacity } = require('react-native'); + return forwardRef(({ children, onPress, onLongPress, ...props }: any, ref: any) => ( + <TouchableOpacity ref={ref} onPress={onPress} onLongPress={onLongPress} {...props}> + {children} + </TouchableOpacity> + )); +}); + +const mockMessage = { + id: 'msg-1', + rid: 'room-1', + msg: 'Hello World', + u: { _id: 'user-1', username: 'testuser', name: 'Test User' }, + ts: new Date(), + experimentalSubscribe: jest.fn(() => jest.fn()) +} as unknown as TAnyMessageModel; + +// MessagePreview renders <Message> as the only surface outside RoomView/MessagesView/SearchMessagesView. +// The message subtree's room hooks throw when mounted without a MessageRoomProvider, so it must self-provide. +describe('MessagePreview', () => { + it('renders without a MessageRoomProvider crash', () => { + expect(() => + render( + <Provider store={mockedStore}> + <MessagePreview message={mockMessage} /> + </Provider> + ) + ).not.toThrow(); + }); + + it('renders the message text', () => { + const { getByText } = render( + <Provider store={mockedStore}> + <MessagePreview message={mockMessage} /> + </Provider> + ); + expect(getByText('Hello World')).toBeTruthy(); + }); +}); diff --git a/app/containers/message/Preview.tsx b/app/containers/message/Preview.tsx index c8705b32fab..cceb49208e1 100644 --- a/app/containers/message/Preview.tsx +++ b/app/containers/message/Preview.tsx @@ -1,4 +1,5 @@ import Message from './index'; +import { MessageRoomProvider } from './MessageRoomStore'; import { useAppSelector } from '../../lib/hooks/useAppSelector'; import { getUserSelector } from '../../selectors/login'; import { type TAnyMessageModel, type TGetCustomEmoji } from '../../definitions'; @@ -19,16 +20,18 @@ const MessagePreview = ({ message }: { message: TAnyMessageModel }) => { return emoji ?? null; }; return ( - <Message - item={message} - user={user} - rid={message.rid} - baseUrl={baseUrl} - getCustomEmoji={getCustomEmoji} - timeFormat={Message_TimeFormat} - useRealName={useRealName} - isPreview - /> + <MessageRoomProvider user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} rid={message.rid}> + <Message + item={message} + user={user} + rid={message.rid} + baseUrl={baseUrl} + getCustomEmoji={getCustomEmoji} + timeFormat={Message_TimeFormat} + useRealName={useRealName} + isPreview + /> + </MessageRoomProvider> ); }; From dac3e90e28bcd736cf9076842a75e911abb349c7 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 15:47:33 -0300 Subject: [PATCH 024/144] refactor: read blockAction and reply fields via selectors (NATIVE-1376) Blocks reads blockAction from the room store (useBlockAction); Reply reads user/baseUrl (room store) and id/isEncrypted (per-message store) via selector hooks instead of useContext(MessageContext). Drops the dead e2e read: the message MessageContext value built in index.tsx never sets e2e, so the guard (isEncrypted && !e2e) was always equivalent to isEncrypted. This retires the last stable/per-message-data consumers; only closure handlers remain on MessageContext. --- app/containers/message/Blocks.ts | 6 +++--- .../message/Components/Attachments/Reply.tsx | 19 ++++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/containers/message/Blocks.ts b/app/containers/message/Blocks.ts index e13aaddd708..de2eb64bce9 100644 --- a/app/containers/message/Blocks.ts +++ b/app/containers/message/Blocks.ts @@ -1,14 +1,14 @@ -import { createElement, useContext } from 'react'; +import { createElement } from 'react'; import { messageBlockWithContext } from '../UIKit/MessageBlock'; import { type IMessageBlocks } from './interfaces'; -import MessageContext from './Context'; +import { useBlockAction } from './MessageRoomStore'; import { useBlocks } from './MessageStore'; const Blocks = ({ rid }: IMessageBlocks) => { 'use memo'; - const { blockAction } = useContext(MessageContext); + const blockAction = useBlockAction(); const { blocks, id: mid } = useBlocks(); if (blocks && blocks.length > 0) { diff --git a/app/containers/message/Components/Attachments/Reply.tsx b/app/containers/message/Components/Attachments/Reply.tsx index aa210329c75..019ceecc039 100644 --- a/app/containers/message/Components/Attachments/Reply.tsx +++ b/app/containers/message/Components/Attachments/Reply.tsx @@ -1,4 +1,4 @@ -import { useContext, useState } from 'react'; +import { useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { Image } from 'expo-image'; @@ -13,7 +13,8 @@ import RCActivityIndicator from '../../../ActivityIndicator'; import Markdown, { MarkdownPreview } from '../../../markdown'; import { Attachments } from './components'; import Quote from './Quote'; -import MessageContext from '../../Context'; +import { useBaseUrl, useMessageUser } from '../../MessageRoomStore'; +import { useIsEncrypted, useMessageId } from '../../MessageStore'; import Touchable from '../../Touchable'; import messageStyles from '../../styles'; import dayjs from '../../../../lib/dayjs'; @@ -108,7 +109,7 @@ const Title = ({ attachment, timeFormat, theme }: { attachment: IAttachment; tim const Description = ({ attachment, getCustomEmoji }: { attachment: IAttachment; getCustomEmoji: TGetCustomEmoji }) => { 'use memo'; - const { user } = useContext(MessageContext); + const user = useMessageUser(); const text = attachment.text || attachment.title; if (!text) { @@ -131,7 +132,8 @@ const Description = ({ attachment, getCustomEmoji }: { attachment: IAttachment; const UrlImage = ({ image }: { image?: string }) => { 'use memo'; - const { baseUrl, user } = useContext(MessageContext); + const baseUrl = useBaseUrl(); + const user = useMessageUser(); if (!image) { return null; @@ -152,7 +154,7 @@ const Fields = ({ }) => { 'use memo'; - const { user } = useContext(MessageContext); + const user = useMessageUser(); if (!attachment.fields) { return null; @@ -175,9 +177,12 @@ const Reply = ({ attachment, timeFormat, getCustomEmoji, msg }: IMessageReply) = const [loading, setLoading] = useState(false); const { theme } = useTheme(); - const { baseUrl, user, id, e2e, isEncrypted } = useContext(MessageContext); + const baseUrl = useBaseUrl(); + const user = useMessageUser(); + const id = useMessageId(); + const isEncrypted = useIsEncrypted(); - if (!attachment || (isEncrypted && !e2e)) { + if (!attachment || isEncrypted) { return null; } From 6010df438b06546c4c7bf1f74253b196d7321c7f Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 16:02:24 -0300 Subject: [PATCH 025/144] refactor: add room-store fields for message row action handlers (NATIVE-1376) Step 4a of ADR 0018. Adds the room-global row handlers (onReactionPress, onReactionLongPress, reactionInit, onDiscussionPress, onThreadPress, replyBroadcast, errorActionsShow, onAnswerButtonPress, onEncryptedPress) plus archived to MessageRoomState, ROOM_STATE_KEYS and matching selector hooks, and seeds them from RoomView's MessageRoomProvider. Purely additive: no consumer reads them yet, MessageContext still carries the closures. MessagesView and SearchMessagesView provide none of these handlers, so they stay unwired. --- app/containers/message/MessageRoomStore.tsx | 34 ++++++++++++++++++++- app/views/RoomView/index.tsx | 10 ++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/app/containers/message/MessageRoomStore.tsx b/app/containers/message/MessageRoomStore.tsx index 9ac263350fb..1120bcaf1fd 100644 --- a/app/containers/message/MessageRoomStore.tsx +++ b/app/containers/message/MessageRoomStore.tsx @@ -3,7 +3,7 @@ import { createStore, useStore } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; import { type TGetCustomEmoji } from '../../definitions/IEmoji'; -import { type IAttachment } from '../../definitions'; +import { type IAttachment, type TAnyMessageModel } from '../../definitions'; import { type IRoomInfoParam } from '../../views/SearchMessagesView'; import { type TSupportedThemes } from '../../theme'; @@ -17,6 +17,17 @@ export type MessageRoomState = { fetchThreadName?: (tmid: string, id: string) => Promise<string | undefined>; toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise<void>; jumpToMessage?: (link: string) => void; + // row action handlers + onReactionPress?: (emoji: string, id: string) => void; + onReactionLongPress?: (item: TAnyMessageModel) => void; + reactionInit?: (messageId: string) => void; + onDiscussionPress?: (item: TAnyMessageModel) => void; + onThreadPress?: (item: TAnyMessageModel) => void; + replyBroadcast?: (item: TAnyMessageModel) => void; + errorActionsShow?: (item: TAnyMessageModel) => void; + onAnswerButtonPress?: Function; + onEncryptedPress?: () => void; + archived?: boolean; // room constants rid?: string; user?: { id?: string; username?: string; token?: string }; @@ -42,6 +53,16 @@ const ROOM_STATE_KEYS: (keyof MessageRoomState)[] = [ 'fetchThreadName', 'toggleFollowThread', 'jumpToMessage', + 'onReactionPress', + 'onReactionLongPress', + 'reactionInit', + 'onDiscussionPress', + 'onThreadPress', + 'replyBroadcast', + 'errorActionsShow', + 'onAnswerButtonPress', + 'onEncryptedPress', + 'archived', 'rid', 'user', 'baseUrl', @@ -100,6 +121,17 @@ export const useFetchThreadName = (): MessageRoomState['fetchThreadName'] => use export const useToggleFollowThread = (): MessageRoomState['toggleFollowThread'] => useMessageRoomStore(s => s.toggleFollowThread); export const useJumpToMessage = (): ((link: string) => void) | undefined => useMessageRoomStore(s => s.jumpToMessage); +export const useOnReactionPress = (): MessageRoomState['onReactionPress'] => useMessageRoomStore(s => s.onReactionPress); +export const useOnReactionLongPress = (): MessageRoomState['onReactionLongPress'] => useMessageRoomStore(s => s.onReactionLongPress); +export const useReactionInit = (): MessageRoomState['reactionInit'] => useMessageRoomStore(s => s.reactionInit); +export const useOnDiscussionPress = (): MessageRoomState['onDiscussionPress'] => useMessageRoomStore(s => s.onDiscussionPress); +export const useOnThreadPress = (): MessageRoomState['onThreadPress'] => useMessageRoomStore(s => s.onThreadPress); +export const useReplyBroadcast = (): MessageRoomState['replyBroadcast'] => useMessageRoomStore(s => s.replyBroadcast); +export const useErrorActionsShow = (): MessageRoomState['errorActionsShow'] => useMessageRoomStore(s => s.errorActionsShow); +export const useOnAnswerButtonPress = (): MessageRoomState['onAnswerButtonPress'] => useMessageRoomStore(s => s.onAnswerButtonPress); +export const useOnEncryptedPress = (): MessageRoomState['onEncryptedPress'] => useMessageRoomStore(s => s.onEncryptedPress); +export const useArchived = (): boolean | undefined => useMessageRoomStore(s => s.archived); + export const useRid = (): string | undefined => useMessageRoomStore(s => s.rid); export const useMessageUser = (): MessageRoomState['user'] => useMessageRoomStore(s => s.user); export const useBaseUrl = (): string | undefined => useMessageRoomStore(s => s.baseUrl); diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 61af9007395..3392597bca0 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1709,6 +1709,16 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { fetchThreadName={this.fetchThreadName} toggleFollowThread={this.toggleFollowThread} jumpToMessage={this.jumpToMessageByUrl} + onReactionPress={this.onReactionPress} + onReactionLongPress={this.onReactionLongPress} + reactionInit={this.onReactionInit} + onDiscussionPress={this.onDiscussionPress} + onThreadPress={this.onThreadPress} + replyBroadcast={this.replyBroadcast} + errorActionsShow={this.errorActionsShow} + onAnswerButtonPress={this.handleSendMessage} + onEncryptedPress={this.onEncryptedPress} + archived={'id' in room && room.archived} rid={rid} user={user as any} baseUrl={baseUrl} From 6dedc8c7aa5e56edc532ea96610e3dd4de9c6007 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 16:09:48 -0300 Subject: [PATCH 026/144] refactor: add per-message store fields and composite press hooks (NATIVE-1376) Step 4b of ADR 0018. Extends the per-message zustand store with onPress, onLongPress and threadBadgeColor (mirrored from MessageProvider props every render, same pattern as MessageRoomProvider) and adds the composite hooks useMessageLongPress, useMessagePress and useOnLinkPress as a faithful relocation of the closures still living in MessageContainerInner. Adds closeEmojiAndAction to the room store (it was RoomView-internal, never in MessageContext) so useMessagePress can read it. Purely additive: the new hooks have no consumers yet and MessageContainerInner still builds the legacy Context closures, so behaviour and snapshots are unchanged. --- app/containers/message/MessageRoomStore.tsx | 10 +- app/containers/message/MessageStore.tsx | 132 +++++++++++++++++++- app/containers/message/index.tsx | 7 +- app/views/RoomView/index.tsx | 1 + 4 files changed, 143 insertions(+), 7 deletions(-) diff --git a/app/containers/message/MessageRoomStore.tsx b/app/containers/message/MessageRoomStore.tsx index 1120bcaf1fd..9000a864d1c 100644 --- a/app/containers/message/MessageRoomStore.tsx +++ b/app/containers/message/MessageRoomStore.tsx @@ -17,6 +17,7 @@ export type MessageRoomState = { fetchThreadName?: (tmid: string, id: string) => Promise<string | undefined>; toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise<void>; jumpToMessage?: (link: string) => void; + closeEmojiAndAction?: (action?: Function, params?: any) => void; // row action handlers onReactionPress?: (emoji: string, id: string) => void; onReactionLongPress?: (item: TAnyMessageModel) => void; @@ -53,6 +54,7 @@ const ROOM_STATE_KEYS: (keyof MessageRoomState)[] = [ 'fetchThreadName', 'toggleFollowThread', 'jumpToMessage', + 'closeEmojiAndAction', 'onReactionPress', 'onReactionLongPress', 'reactionInit', @@ -120,15 +122,19 @@ export const useHandleEnterCall = (): (() => void) | undefined => useMessageRoom export const useFetchThreadName = (): MessageRoomState['fetchThreadName'] => useMessageRoomStore(s => s.fetchThreadName); export const useToggleFollowThread = (): MessageRoomState['toggleFollowThread'] => useMessageRoomStore(s => s.toggleFollowThread); export const useJumpToMessage = (): ((link: string) => void) | undefined => useMessageRoomStore(s => s.jumpToMessage); +export const useCloseEmojiAndAction = (): MessageRoomState['closeEmojiAndAction'] => + useMessageRoomStore(s => s.closeEmojiAndAction); export const useOnReactionPress = (): MessageRoomState['onReactionPress'] => useMessageRoomStore(s => s.onReactionPress); -export const useOnReactionLongPress = (): MessageRoomState['onReactionLongPress'] => useMessageRoomStore(s => s.onReactionLongPress); +export const useOnReactionLongPress = (): MessageRoomState['onReactionLongPress'] => + useMessageRoomStore(s => s.onReactionLongPress); export const useReactionInit = (): MessageRoomState['reactionInit'] => useMessageRoomStore(s => s.reactionInit); export const useOnDiscussionPress = (): MessageRoomState['onDiscussionPress'] => useMessageRoomStore(s => s.onDiscussionPress); export const useOnThreadPress = (): MessageRoomState['onThreadPress'] => useMessageRoomStore(s => s.onThreadPress); export const useReplyBroadcast = (): MessageRoomState['replyBroadcast'] => useMessageRoomStore(s => s.replyBroadcast); export const useErrorActionsShow = (): MessageRoomState['errorActionsShow'] => useMessageRoomStore(s => s.errorActionsShow); -export const useOnAnswerButtonPress = (): MessageRoomState['onAnswerButtonPress'] => useMessageRoomStore(s => s.onAnswerButtonPress); +export const useOnAnswerButtonPress = (): MessageRoomState['onAnswerButtonPress'] => + useMessageRoomStore(s => s.onAnswerButtonPress); export const useOnEncryptedPress = (): MessageRoomState['onEncryptedPress'] => useMessageRoomStore(s => s.onEncryptedPress); export const useArchived = (): boolean | undefined => useMessageRoomStore(s => s.archived); diff --git a/app/containers/message/MessageStore.tsx b/app/containers/message/MessageStore.tsx index bcb565ecc7e..bd74e7fd29c 100644 --- a/app/containers/message/MessageStore.tsx +++ b/app/containers/message/MessageStore.tsx @@ -1,14 +1,36 @@ -import { createContext, useContext, useEffect, useState, type ReactElement, type ReactNode } from 'react'; +import { createContext, useContext, useEffect, useRef, useState, type ReactElement, type ReactNode } from 'react'; import { createStore, useStore } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; +import { Keyboard } from 'react-native'; -import { type TAnyMessageModel } from '../../definitions'; +import { type IAttachment, type TAnyMessageModel } from '../../definitions'; import { getMessageTranslation } from './utils'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; import { messagesStatus } from '../../lib/constants/messagesStatus'; -import { useAutoTranslate, useBroadcast, useIsThreadRoom, useMessageGroupingPeriod, useMessageUser } from './MessageRoomStore'; +import { debounce } from '../../lib/methods/helpers'; +import openLink from '../../lib/methods/helpers/openLink'; +import { useTheme } from '../../theme'; +import { + useArchived, + useAutoTranslate, + useBroadcast, + useCloseEmojiAndAction, + useIsThreadRoom, + useJumpToMessage, + useMessageGroupingPeriod, + useMessageUser, + useOnDiscussionPress, + useOnThreadPress +} from './MessageRoomStore'; + +type MessageStoreState = { + tick: number; + onPress?: () => void; + onLongPress?: (item: TAnyMessageModel) => void; + threadBadgeColor?: string; +}; -const createMessageStore = () => createStore(() => ({ tick: 0 })); +const createMessageStore = () => createStore<MessageStoreState>(() => ({ tick: 0 })); type MessageStore = ReturnType<typeof createMessageStore>; @@ -45,10 +67,16 @@ const subscribeModel = (m: TAnyMessageModel, store: MessageStore) => { export const MessageProvider = ({ item, previousItem, + onPress, + onLongPress, + threadBadgeColor, children }: { item: TAnyMessageModel; previousItem?: TAnyMessageModel; + onPress?: () => void; + onLongPress?: (item: TAnyMessageModel) => void; + threadBadgeColor?: string; children: ReactNode; }): ReactElement => { 'use memo'; @@ -61,6 +89,11 @@ export const MessageProvider = ({ useEffect(() => subscribeModel(item, store), [item, store]); useEffect(() => (previousItem ? subscribeModel(previousItem, store) : undefined), [previousItem, store]); + // Mirror per-message row handlers so field-level selectors subscribe without churning the context value. + useEffect(() => { + store.setState({ onPress, onLongPress, threadBadgeColor }); + }); + return <MessageStoreContext.Provider value={{ store, item, previousItem }}>{children}</MessageStoreContext.Provider>; }; @@ -251,3 +284,94 @@ export const useMessageText = (): { messageText: TAnyMessageModel['msg']; isTran }) ); }; + +export const useOnPressRaw = (): MessageStoreState['onPress'] => { + const { store } = useMessageCtx(); + return useStore(store, s => s.onPress); +}; +export const useOnLongPressRaw = (): MessageStoreState['onLongPress'] => { + const { store } = useMessageCtx(); + return useStore(store, s => s.onLongPress); +}; +export const useThreadBadgeColor = (): string | undefined => { + const { store } = useMessageCtx(); + return useStore(store, s => s.threadBadgeColor); +}; + +export const useMessageLongPress = (): (() => void) => { + const { item } = useMessageCtx(); + const isInfo = useIsInfo(); + const { hasError } = useMessageStatus(); + const isEncrypted = useIsEncrypted(); + const archived = useArchived(); + const onLongPress = useOnLongPressRaw(); + return () => { + if (isInfo || hasError || isEncrypted || archived) { + return; + } + onLongPress?.(item); + }; +}; + +export const useOnLinkPress = (): ((link: string) => void) => { + const { item } = useMessageCtx(); + const jumpToMessage = useJumpToMessage(); + const { theme } = useTheme(); + return (link: string) => { + const isMessageLink = !!item.attachments?.some((att: IAttachment) => att?.message_link === link); + if (isMessageLink && jumpToMessage) { + return jumpToMessage(link); + } + openLink(link, theme); + }; +}; + +export const useMessagePress = ({ + isIgnored, + revealIgnored +}: { + isIgnored: boolean; + revealIgnored: () => void; +}): (() => void) => { + const { item } = useMessageCtx(); + const isThreadRoom = useIsThreadRoom(); + const onPress = useOnPressRaw(); + const onThreadPress = useOnThreadPress(); + const onDiscussionPress = useOnDiscussionPress(); + const closeEmojiAndAction = useCloseEmojiAndAction(); + + // pressHandlerRef holds the latest press logic; updated after every render via + // useEffect so the render body stays free of ref writes. + const pressHandlerRef = useRef<() => void>(() => {}); + useEffect(() => { + pressHandlerRef.current = () => { + if (isIgnored) { + return revealIgnored(); + } + if (onPress) { + return onPress(); + } + Keyboard.dismiss(); + if ((item.tlm || item.tmid) && !isThreadRoom) { + onThreadPress?.(item); + } + if (item.dlm && onDiscussionPress) { + onDiscussionPress(item); + } + }; + }); + // onPressRef holds the single debounced instance for the component lifetime. + // Initialised to a noop; replaced with the real debounced fn in the first effect + // so that pressHandlerRef.current is never read during render. + const onPressRef = useRef(debounce(() => {}, 300, true)); + useEffect(() => { + onPressRef.current = debounce(() => pressHandlerRef.current?.(), 300, true); + }, []); + + return () => { + if (closeEmojiAndAction) { + return closeEmojiAndAction(onPressRef.current); + } + return onPressRef.current(); + }; +}; diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index a91e5eff3b1..d57900c5a81 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -296,7 +296,12 @@ const MessageContainer = (props: IMessageContainerProps) => { const { item, previousItem } = props; return ( - <MessageProvider item={item} previousItem={previousItem}> + <MessageProvider + item={item} + previousItem={previousItem} + onPress={props.onPress} + onLongPress={props.onLongPress} + threadBadgeColor={props.threadBadgeColor}> <MessageContainerInner {...props} /> </MessageProvider> ); diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 3392597bca0..d2bef862785 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1709,6 +1709,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { fetchThreadName={this.fetchThreadName} toggleFollowThread={this.toggleFollowThread} jumpToMessage={this.jumpToMessageByUrl} + closeEmojiAndAction={this.handleCloseEmoji} onReactionPress={this.onReactionPress} onReactionLongPress={this.onReactionLongPress} reactionInit={this.onReactionInit} From 542d982a538ba2e600f05758181c47c85cf9ba47 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 16:27:48 -0300 Subject: [PATCH 027/144] refactor: migrate message row-action consumers to store selectors (NATIVE-1376) Move the 8 remaining MessageContext consumers (Discussion, Reactions, Broadcast, AttachedActions, MessageError, Encrypted, Thread, Content) onto the room-level and per-message store selector hooks, reconstructing each handler's argument from the per-message context. Seed the per-message store's initial state from the provider's props so the first render sees onPress/onLongPress/threadBadgeColor synchronously, matching MessageRoomStore and avoiding an extra mount render. Rewire Reactions.test onto the shared MessageProviders helper for the dual-run providers. --- app/containers/message/Broadcast.tsx | 11 +++---- .../Attachments/AttachedActions.tsx | 8 ++--- .../Components/RightIcons/Encrypted.tsx | 6 ++-- .../Components/RightIcons/MessageError.tsx | 11 ++++--- app/containers/message/Content.tsx | 18 +++++++++--- app/containers/message/Discussion.tsx | 10 +++---- app/containers/message/MessageStore.tsx | 5 ++-- app/containers/message/Reactions.test.tsx | 29 ++++++++----------- app/containers/message/Reactions.tsx | 27 ++++++++++++----- app/containers/message/Thread.tsx | 14 +++++---- 10 files changed, 78 insertions(+), 61 deletions(-) diff --git a/app/containers/message/Broadcast.tsx b/app/containers/message/Broadcast.tsx index e77dd1e23bd..8689f01132c 100644 --- a/app/containers/message/Broadcast.tsx +++ b/app/containers/message/Broadcast.tsx @@ -1,4 +1,3 @@ -import { useContext } from 'react'; import { Text, View } from 'react-native'; import Touchable from './Touchable'; @@ -7,16 +6,18 @@ import styles from './styles'; import { BUTTON_HIT_SLOP } from './utils'; import I18n from '../../i18n'; import { themes } from '../../lib/constants/colors'; -import MessageContext from './Context'; import { type IMessageBroadcast } from './interfaces'; import { useTheme } from '../../theme'; -import { useMessageAuthor } from './MessageStore'; +import { useMessageAuthor, useMessageCtx } from './MessageStore'; +import { useMessageUser, useReplyBroadcast } from './MessageRoomStore'; // TODO: Create a reusable button component for message const Broadcast = ({ broadcast }: IMessageBroadcast) => { 'use memo'; - const { user, replyBroadcast } = useContext(MessageContext); + const { item } = useMessageCtx(); + const user = useMessageUser(); + const replyBroadcast = useReplyBroadcast(); const { theme } = useTheme(); const { u: author } = useMessageAuthor(); const isOwn = author?._id === user?.id; @@ -25,7 +26,7 @@ const Broadcast = ({ broadcast }: IMessageBroadcast) => { return ( <View style={styles.buttonContainer}> <Touchable - onPress={replyBroadcast} + onPress={() => replyBroadcast?.(item)} style={[styles.button, { backgroundColor: themes[theme].badgeBackgroundLevel2 }]} hitSlop={BUTTON_HIT_SLOP} testID='message-broadcast-reply'> diff --git a/app/containers/message/Components/Attachments/AttachedActions.tsx b/app/containers/message/Components/Attachments/AttachedActions.tsx index 9d534a2a9ee..66ef4beb8ee 100644 --- a/app/containers/message/Components/Attachments/AttachedActions.tsx +++ b/app/containers/message/Components/Attachments/AttachedActions.tsx @@ -1,7 +1,5 @@ -import { useContext } from 'react'; - import Button from '../../../Button'; -import MessageContext from '../../Context'; +import { useOnAnswerButtonPress } from '../../MessageRoomStore'; import { type IAttachment, type TGetCustomEmoji } from '../../../../definitions'; import openLink from '../../../../lib/methods/helpers/openLink'; import Markdown from '../../../markdown'; @@ -16,7 +14,7 @@ export type TElement = { const AttachedActions = ({ attachment, getCustomEmoji }: { attachment: IAttachment; getCustomEmoji: TGetCustomEmoji }) => { 'use memo'; - const { onAnswerButtonPress } = useContext(MessageContext); + const onAnswerButtonPress = useOnAnswerButtonPress(); if (!attachment.actions) { return null; @@ -25,7 +23,7 @@ const AttachedActions = ({ attachment, getCustomEmoji }: { attachment: IAttachme const attachedButtons = attachment.actions.map((element: TElement) => { const onPress = () => { if (element.msg) { - onAnswerButtonPress?.(element.msg); + onAnswerButtonPress?.(element.msg, false); } if (element.url) { diff --git a/app/containers/message/Components/RightIcons/Encrypted.tsx b/app/containers/message/Components/RightIcons/Encrypted.tsx index 7b4c4920ac4..9cdfe01e893 100644 --- a/app/containers/message/Components/RightIcons/Encrypted.tsx +++ b/app/containers/message/Components/RightIcons/Encrypted.tsx @@ -1,17 +1,15 @@ -import { useContext } from 'react'; - import Touchable from '../../Touchable'; import { CustomIcon } from '../../../CustomIcon'; import { BUTTON_HIT_SLOP } from '../../utils'; -import MessageContext from '../../Context'; import styles from '../../styles'; import { E2E_MESSAGE_TYPE } from '../../../../lib/constants/keys'; import { useMessageField } from '../../MessageStore'; +import { useOnEncryptedPress } from '../../MessageRoomStore'; const Encrypted = (_props: { type: string }) => { 'use memo'; - const { onEncryptedPress } = useContext(MessageContext); + const onEncryptedPress = useOnEncryptedPress(); const type = useMessageField(item => item.t); if (type !== E2E_MESSAGE_TYPE) { diff --git a/app/containers/message/Components/RightIcons/MessageError.tsx b/app/containers/message/Components/RightIcons/MessageError.tsx index 821c7333b86..43dc5017d46 100644 --- a/app/containers/message/Components/RightIcons/MessageError.tsx +++ b/app/containers/message/Components/RightIcons/MessageError.tsx @@ -1,19 +1,18 @@ -import { useContext } from 'react'; - import Touchable from '../../Touchable'; import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; import { BUTTON_HIT_SLOP } from '../../utils'; import { themes } from '../../../../lib/constants/colors'; -import MessageContext from '../../Context'; import { useTheme } from '../../../../theme'; -import { useMessageStatus } from '../../MessageStore'; +import { useMessageCtx, useMessageStatus } from '../../MessageStore'; +import { useErrorActionsShow } from '../../MessageRoomStore'; const MessageError = (_props: { hasError: boolean }) => { 'use memo'; const { theme } = useTheme(); - const { onErrorPress } = useContext(MessageContext); + const { item } = useMessageCtx(); + const errorActionsShow = useErrorActionsShow(); const { hasError } = useMessageStatus(); if (!hasError) { @@ -21,7 +20,7 @@ const MessageError = (_props: { hasError: boolean }) => { } return ( - <Touchable onPress={onErrorPress} style={styles.rightIcons} hitSlop={BUTTON_HIT_SLOP}> + <Touchable onPress={() => errorActionsShow?.(item)} style={styles.rightIcons} hitSlop={BUTTON_HIT_SLOP}> <CustomIcon name='warning' color={themes[theme].buttonBackgroundDangerDefault} size={16} /> </Touchable> ); diff --git a/app/containers/message/Content.tsx b/app/containers/message/Content.tsx index 205da02dd99..3567fa6e57c 100644 --- a/app/containers/message/Content.tsx +++ b/app/containers/message/Content.tsx @@ -1,4 +1,3 @@ -import { useContext } from 'react'; import { Text, View } from 'react-native'; import I18n from '../../i18n'; @@ -6,18 +5,29 @@ import styles from './styles'; import Markdown, { MarkdownPreview } from '../markdown'; import User from './User'; import { messageHaveAuthorName, getInfoMessage, getPreviewMessageFromAttachment } from './utils'; -import MessageContext from './Context'; import { type IMessageContent } from './interfaces'; import { useTheme } from '../../theme'; import { themes } from '../../lib/constants/colors'; import { type MessageTypesValues } from '../../definitions'; -import { useContentData, useIsEncrypted, useIsInfo, useMessageAuthor, useMessageStatus, useMessageText } from './MessageStore'; +import { + useContentData, + useIsEncrypted, + useIsInfo, + useMessageAuthor, + useMessageStatus, + useMessageText, + useOnLinkPress +} from './MessageStore'; +import { useGetCustomEmoji, useMessageUser, useNavToRoomInfo } from './MessageRoomStore'; const Content = (props: IMessageContent) => { 'use memo'; const { theme } = useTheme(); - const { user, onLinkPress, getCustomEmoji = () => null, navToRoomInfo } = useContext(MessageContext); + const user = useMessageUser(); + const onLinkPress = useOnLinkPress(); + const getCustomEmoji = useGetCustomEmoji() ?? (() => null); + const navToRoomInfo = useNavToRoomInfo(); const isInfo = useIsInfo(); const isEncrypted = useIsEncrypted(); diff --git a/app/containers/message/Discussion.tsx b/app/containers/message/Discussion.tsx index 04f8d988242..0d83e324aac 100644 --- a/app/containers/message/Discussion.tsx +++ b/app/containers/message/Discussion.tsx @@ -1,4 +1,3 @@ -import { useContext } from 'react'; import { Text, View } from 'react-native'; import Touchable from './Touchable'; @@ -7,16 +6,17 @@ import styles from './styles'; import I18n from '../../i18n'; import { CustomIcon } from '../CustomIcon'; import { DISCUSSION } from './constants'; -import MessageContext from './Context'; import { formatDateThreads } from '../../lib/methods/helpers/room'; import { useTheme } from '../../theme'; -import { useDiscussion, useMessageText } from './MessageStore'; +import { useDiscussion, useMessageCtx, useMessageText } from './MessageStore'; +import { useOnDiscussionPress } from './MessageRoomStore'; // TODO: Create a reusable button component for message const Discussion = () => { 'use memo'; const { colors } = useTheme(); + const { item } = useMessageCtx(); const { dcount, dlm } = useDiscussion(); const { messageText } = useMessageText(); let time; @@ -24,14 +24,14 @@ const Discussion = () => { time = formatDateThreads(dlm); } const buttonText = formatMessageCount(dcount, DISCUSSION); - const { onDiscussionPress } = useContext(MessageContext); + const onDiscussionPress = useOnDiscussionPress(); return ( <View style={{ gap: 4 }}> <Text style={[styles.startedDiscussion, { color: colors.fontSecondaryInfo }]}>{I18n.t('Started_discussion')}</Text> <Text style={[styles.discussionText, { color: colors.fontDefault }]}>{messageText}</Text> <View style={[styles.buttonContainer, { gap: 8 }]}> <Touchable - onPress={onDiscussionPress} + onPress={() => onDiscussionPress?.(item)} style={[styles.button, { backgroundColor: colors.badgeBackgroundLevel2 }]} hitSlop={BUTTON_HIT_SLOP}> <View style={styles.buttonInnerContainer}> diff --git a/app/containers/message/MessageStore.tsx b/app/containers/message/MessageStore.tsx index bd74e7fd29c..0928fc7b789 100644 --- a/app/containers/message/MessageStore.tsx +++ b/app/containers/message/MessageStore.tsx @@ -30,7 +30,8 @@ type MessageStoreState = { threadBadgeColor?: string; }; -const createMessageStore = () => createStore<MessageStoreState>(() => ({ tick: 0 })); +const createMessageStore = (initial: Partial<MessageStoreState>) => + createStore<MessageStoreState>(() => ({ tick: 0, ...initial })); type MessageStore = ReturnType<typeof createMessageStore>; @@ -81,7 +82,7 @@ export const MessageProvider = ({ }): ReactElement => { 'use memo'; - const [store] = useState(createMessageStore); + const [store] = useState(() => createMessageStore({ onPress, onLongPress, threadBadgeColor })); // Header grouping and thread position depend on the previous record too, so each effect // subscribes one record; both feed the same tick. Keeping them separate means changing diff --git a/app/containers/message/Reactions.test.tsx b/app/containers/message/Reactions.test.tsx index 91db9381329..572ba669e61 100644 --- a/app/containers/message/Reactions.test.tsx +++ b/app/containers/message/Reactions.test.tsx @@ -1,9 +1,7 @@ -import { Provider } from 'react-redux'; import { render, fireEvent } from '@testing-library/react-native'; import Reactions from './Reactions'; -import MessageContext from './Context'; -import { MessageProvider } from './MessageStore'; +import { MessageProviders } from './testHelpers'; import { setUser } from '../../actions/login'; import { mockedStore } from '../../reducers/mockedStore'; import { type IReaction, type TAnyMessageModel } from '../../definitions'; @@ -54,20 +52,17 @@ const buildOnReactionPress = (item: FakeItem) => (emoji: string) => { }; const TestWrapper = ({ item, onReactionPress }: { item: FakeItem; onReactionPress: (emoji: string) => void }) => ( - <Provider store={mockedStore}> - <MessageContext.Provider - value={{ - user: { username: 'john' }, - reactionInit: jest.fn(), - onReactionPress, - onReactionLongPress: jest.fn(), - getCustomEmoji - }}> - <MessageProvider item={item}> - <Reactions reactions={item.reactions} /> - </MessageProvider> - </MessageContext.Provider> - </Provider> + <MessageProviders + item={item} + context={{ + user: { username: 'john' }, + reactionInit: jest.fn(), + onReactionPress, + onReactionLongPress: jest.fn(), + getCustomEmoji + }}> + <Reactions reactions={item.reactions} /> + </MessageProviders> ); it('renders all reactions and AddReaction button', () => { diff --git a/app/containers/message/Reactions.tsx b/app/containers/message/Reactions.tsx index be3db0c7ee4..fbb5d1c7866 100644 --- a/app/containers/message/Reactions.tsx +++ b/app/containers/message/Reactions.tsx @@ -1,4 +1,3 @@ -import { useContext } from 'react'; import { Text, useWindowDimensions, View } from 'react-native'; import I18n from '../../i18n'; @@ -9,8 +8,14 @@ import Emoji from './Emoji'; import { BUTTON_HIT_SLOP } from './utils'; import { themes } from '../../lib/constants/colors'; import { type TSupportedThemes, useTheme } from '../../theme'; -import MessageContext from './Context'; -import { useReactions } from './MessageStore'; +import { useMessageCtx, useMessageId, useReactions } from './MessageStore'; +import { + useGetCustomEmoji, + useMessageUser, + useOnReactionLongPress, + useOnReactionPress, + useReactionInit +} from './MessageRoomStore'; interface IReaction { _id: string; @@ -30,12 +35,13 @@ interface IMessageReactions { const AddReaction = ({ theme }: { theme: TSupportedThemes }) => { 'use memo'; - const { reactionInit } = useContext(MessageContext); + const reactionInit = useReactionInit(); + const id = useMessageId(); const { fontScale } = useWindowDimensions(); const height = 28 * fontScale; return ( <Touchable - onPress={reactionInit} + onPress={() => reactionInit?.(id)} key='message-add-reaction' testID='message-add-reaction' accessibilityRole='button' @@ -53,14 +59,19 @@ const AddReaction = ({ theme }: { theme: TSupportedThemes }) => { const Reaction = ({ reaction, theme }: IMessageReaction) => { 'use memo'; - const { onReactionPress, onReactionLongPress, user, getCustomEmoji } = useContext(MessageContext); + const { item } = useMessageCtx(); + const id = useMessageId(); + const onReactionPress = useOnReactionPress(); + const onReactionLongPress = useOnReactionLongPress(); + const user = useMessageUser(); + const getCustomEmoji = useGetCustomEmoji(); const { fontScale } = useWindowDimensions(); const height = 28 * fontScale; const reacted = reaction.usernames.findIndex((item: string) => item === user?.username) !== -1; return ( <Touchable - onPress={() => onReactionPress?.(reaction.emoji)} - onLongPress={onReactionLongPress} + onPress={() => onReactionPress?.(reaction.emoji, id)} + onLongPress={() => onReactionLongPress?.(item)} key={reaction.emoji} testID={`message-reaction-${reaction.emoji}`} accessibilityRole='button' diff --git a/app/containers/message/Thread.tsx b/app/containers/message/Thread.tsx index 6570303ac0b..4c2b43ccea5 100644 --- a/app/containers/message/Thread.tsx +++ b/app/containers/message/Thread.tsx @@ -1,20 +1,24 @@ -import { useContext } from 'react'; import { Text, View } from 'react-native'; import styles from './styles'; -import MessageContext from './Context'; import ThreadDetails from '../ThreadDetails'; import I18n from '../../i18n'; import { type IMessageThread } from './interfaces'; import { useTheme } from '../../theme'; import Touchable from './Touchable'; -import { useMessageText, useThreadData } from './MessageStore'; +import { useMessageCtx, useMessageText, useReplies, useThreadBadgeColor, useThreadData } from './MessageStore'; +import { useMessageUser, useOnThreadPress, useToggleFollowThread } from './MessageRoomStore'; const Thread = ({ isThreadRoom }: IMessageThread) => { 'use memo'; const { theme, colors } = useTheme(); - const { threadBadgeColor, toggleFollowThread, user, replies, onThreadPress } = useContext(MessageContext); + const { item } = useMessageCtx(); + const threadBadgeColor = useThreadBadgeColor(); + const toggleFollowThread = useToggleFollowThread(); + const user = useMessageUser(); + const replies = useReplies(); + const onThreadPress = useOnThreadPress(); const { tcount, tlm, id } = useThreadData(); const { messageText } = useMessageText(); @@ -28,7 +32,7 @@ const Thread = ({ isThreadRoom }: IMessageThread) => { return ( <View style={styles.buttonContainer}> <Touchable - onPress={onThreadPress} + onPress={() => onThreadPress?.(item)} accessibilityRole='button' accessibilityLabel={I18n.t('View_Thread')} style={[styles.button, { backgroundColor }]} From 3118d501fbd4ac91de47908026d5052cade8e8db Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 16:45:56 -0300 Subject: [PATCH 028/144] refactor: collapse MessageContainerInner and migrate press handlers to store hooks (NATIVE-1376) MessageContainer now renders the provider stack directly; the deleted MessageContainerInner's press and long-press logic moves into MessageTouchable via the useMessagePress and useMessageLongPress composite hooks instead of MessageContext. Relocate the manual-unignore state and the isBeingEdited lookup into MessageTouchable, the component that consumes them. RCTouchable reads the long-press guard from useMessageLongPress. MessageContext.Provider stays mounted with an empty value that no component reads, leaving step 5 a pure deletion. Wrap two attachment test harnesses in MessageProvider now that their Touchable subtree needs it. --- .../CollapsibleQuote.test.tsx | 8 +- .../Attachments/Image/Container.test.tsx | 7 +- app/containers/message/Message.tsx | 29 +- app/containers/message/Touchable.tsx | 6 +- app/containers/message/index.tsx | 259 ++---------------- 5 files changed, 56 insertions(+), 253 deletions(-) diff --git a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx b/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx index 8678861b9e5..a58e7af3604 100644 --- a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx +++ b/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx @@ -3,7 +3,9 @@ import { Provider } from 'react-redux'; import { setUser } from '../../../../../actions/login'; import { mockedStore } from '../../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../../definitions'; import MessageContext from '../../../Context'; +import { MessageProvider } from '../../../MessageStore'; import { MessageRoomProvider, pickMessageRoomState } from '../../../MessageRoomStore'; import CollapsibleQuote from '.'; @@ -43,11 +45,15 @@ const contextValue = { user: { username: 'Marcos' } }; +const item = { id: 'collapsible-quote-msg' } as unknown as TAnyMessageModel; + const Render = () => ( <Provider store={mockedStore}> <MessageRoomProvider {...pickMessageRoomState(contextValue)}> <MessageContext.Provider value={contextValue}> - <CollapsibleQuote attachment={testAttachment} getCustomEmoji={mockFn} timeFormat='LT' /> + <MessageProvider item={item}> + <CollapsibleQuote attachment={testAttachment} getCustomEmoji={mockFn} timeFormat='LT' /> + </MessageProvider> </MessageContext.Provider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/Components/Attachments/Image/Container.test.tsx b/app/containers/message/Components/Attachments/Image/Container.test.tsx index f610fb250f2..569ac8d054b 100644 --- a/app/containers/message/Components/Attachments/Image/Container.test.tsx +++ b/app/containers/message/Components/Attachments/Image/Container.test.tsx @@ -2,7 +2,9 @@ import { render } from '@testing-library/react-native'; import { type ComponentProps } from 'react'; import { A11y } from 'react-native-a11y-order'; +import { type TAnyMessageModel } from '../../../../../definitions'; import MessageContext from '../../../Context'; +import { MessageProvider } from '../../../MessageStore'; import { MessageRoomProvider, pickMessageRoomState } from '../../../MessageRoomStore'; import ImageContainer from './Container'; @@ -39,11 +41,14 @@ const renderImageContainer = (props?: Partial<ComponentProps<typeof ImageContain onLongPress: jest.fn(), translateLanguage: undefined }; + const item = { id: contextValue.id } as unknown as TAnyMessageModel; return render( <A11y.Order> <MessageRoomProvider {...pickMessageRoomState(contextValue)}> <MessageContext.Provider value={contextValue}> - <ImageContainer file={{ image_url: 'https://open.rocket.chat/image.png', image_type: 'image/png' }} {...props} /> + <MessageProvider item={item}> + <ImageContainer file={{ image_url: 'https://open.rocket.chat/image.png', image_type: 'image/png' }} {...props} /> + </MessageProvider> </MessageContext.Provider> </MessageRoomProvider> </A11y.Order> diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index f7e58e2ef3a..83304a2ec29 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -1,8 +1,7 @@ -import { useContext } from 'react'; +import { useState } from 'react'; import { View, type ViewStyle, type AccessibilityActionEvent, type AccessibilityActionInfo } from 'react-native'; import { A11y } from 'react-native-a11y-order'; -import MessageContext from './Context'; import User from './User'; import styles from './styles'; import RepliedThread from './RepliedThread'; @@ -27,6 +26,7 @@ import { useLastFocusedMessageRef } from '../../lib/a11y/useLastFocusedMessageRe import { useMessageAccessibilityLabel } from './hooks/useMessageAccessibilityLabel'; import { useMessageAccessibilityActions } from './hooks/useMessageAccessibilityActions'; import { useMessageAccessibilityHint } from './hooks/useMessageAccessibilityHint'; +import { useIsBeingEdited } from '../../views/RoomView/InteractionStore'; import { useBlocks, useContentData, @@ -35,7 +35,9 @@ import { useMessageAuthor, useMessageField, useMessageGrouping, + useMessageLongPress, useMessageMeta, + useMessagePress, useMessageStatus, useMessageText, useThreadData, @@ -261,7 +263,6 @@ Message.displayName = 'Message'; const MessageTouchable = (props: TMessageProps) => { 'use memo'; - const { onPress, onLongPress } = useContext(MessageContext); const { colors } = useTheme(); const { ref: touchRef, markAsLastFocused } = useLastFocusedMessageRef(); const { isThreadReply } = useThreadPosition(); @@ -269,6 +270,12 @@ const MessageTouchable = (props: TMessageProps) => { const { hasError, isTemp } = useMessageStatus(); const type = useMessageField(item => item.t); const id = useMessageField(item => item.id); + const isBeingEdited = useIsBeingEdited(id); + const [isManualUnignored, setIsManualUnignored] = useState(false); + const isIgnored = isManualUnignored ? false : props.isIgnored ?? false; + const revealIgnored = () => setIsManualUnignored(true); + const onPressAction = useMessagePress({ isIgnored, revealIgnored }); + const onLongPress = useMessageLongPress(); const accessibilityLabelValue = useMessageAccessibilityLabel({ useRealName: props.useRealName, isReadReceiptEnabled: props.isReadReceiptEnabled, @@ -279,7 +286,7 @@ const MessageTouchable = (props: TMessageProps) => { const accessibilityHint = useMessageAccessibilityHint(); let backgroundColor = undefined; - if (props.isBeingEdited) { + if (isBeingEdited) { backgroundColor = colors.statusBackgroundWarning2; } if (props.highlighted) { @@ -299,8 +306,8 @@ const MessageTouchable = (props: TMessageProps) => { isThreadRoom={props.isThreadRoom} isPreview={props.isPreview} highlighted={props.highlighted} - isIgnored={props.isIgnored} - isBeingEdited={props.isBeingEdited} + isIgnored={isIgnored} + isBeingEdited={isBeingEdited} autoTranslateLanguage={props.autoTranslateLanguage} small={props.small} /> @@ -310,7 +317,7 @@ const MessageTouchable = (props: TMessageProps) => { const handleLongPress = () => { markAsLastFocused(); - onLongPress?.(); + onLongPress(); }; return ( @@ -319,10 +326,10 @@ const MessageTouchable = (props: TMessageProps) => { <Touch componentRef={touchRef} onLongPress={handleLongPress} - onPress={onPress} + onPress={onPressAction} disabled={isDisabled} style={{ backgroundColor }} - testID={props.isBeingEdited ? `message-editing-${id}` : undefined} + testID={isBeingEdited ? `message-editing-${id}` : undefined} accessible accessibilityRole='button' accessibilityLabel={accessibilityLabelValue} @@ -341,8 +348,8 @@ const MessageTouchable = (props: TMessageProps) => { isThreadRoom={props.isThreadRoom} isPreview={props.isPreview} highlighted={props.highlighted} - isIgnored={props.isIgnored} - isBeingEdited={props.isBeingEdited} + isIgnored={isIgnored} + isBeingEdited={isBeingEdited} autoTranslateLanguage={props.autoTranslateLanguage} small={props.small} handleLongPress={!isDisabled ? handleLongPress : undefined} diff --git a/app/containers/message/Touchable.tsx b/app/containers/message/Touchable.tsx index 18924ed6214..d82b9775033 100644 --- a/app/containers/message/Touchable.tsx +++ b/app/containers/message/Touchable.tsx @@ -1,7 +1,7 @@ -import { useContext, type ReactNode, type FC } from 'react'; +import { type ReactNode, type FC } from 'react'; import { Pressable, type PressableProps } from 'react-native'; -import MessageContext from './Context'; +import { useMessageLongPress } from './MessageStore'; interface IProps extends PressableProps { children: ReactNode; @@ -11,7 +11,7 @@ interface IProps extends PressableProps { const RCTouchable: FC<IProps> = ({ children, ...props }) => { 'use memo'; - const { onLongPress } = useContext(MessageContext); + const onLongPress = useMessageLongPress(); return ( <Pressable onLongPress={onLongPress} {...props}> diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index d57900c5a81..eb1a8c65c15 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -1,16 +1,12 @@ -import { memo, useEffect, useRef, useState } from 'react'; -import { Keyboard } from 'react-native'; +import { memo } from 'react'; import Message from './Message'; -import MessageContext from './Context'; -import { debounce } from '../../lib/methods/helpers'; -import { type TSupportedThemes, useTheme } from '../../theme'; -import openLink from '../../lib/methods/helpers/openLink'; +import MessageContext, { type IMessageContext } from './Context'; +import { type TSupportedThemes } from '../../theme'; import { type IAttachment, type TAnyMessageModel, type TGetCustomEmoji } from '../../definitions'; import { type IRoomInfoParam } from '../../views/SearchMessagesView'; import MessageSeparator from '../MessageSeparator'; -import { useIsBeingEdited } from '../../views/RoomView/InteractionStore'; -import { MessageProvider, useIsEncrypted, useIsInfo, useMessageStatus } from './MessageStore'; +import { MessageProvider } from './MessageStore'; interface IMessageContainerProps { item: TAnyMessageModel; @@ -63,238 +59,12 @@ interface IMessageContainerProps { showUnreadSeparator?: boolean; } -const MessageContainerInner = ({ - item, - user, - rid, - timeFormat, - archived = false, - broadcast = false, - baseUrl, - Message_GroupingPeriod, - isReadReceiptEnabled, - isThreadRoom, - useRealName, - autoTranslateRoom, - autoTranslateLanguage, - isIgnored: isIgnoredProp = false, - highlighted, - getCustomEmoji = () => null, - onLongPress: onLongPressProp = () => {}, - onReactionPress: onReactionPressProp, - onEncryptedPress: onEncryptedPressProp, - onDiscussionPress: onDiscussionPressProp, - onThreadPress: onThreadPressProp, - errorActionsShow, - replyBroadcast: replyBroadcastProp, - reactionInit: reactionInitProp, - fetchThreadName, - showAttachment, - onReactionLongPress: onReactionLongPressProp, - navToRoomInfo = () => {}, - handleEnterCall, - blockAction = () => {}, - onAnswerButtonPress: onAnswerButtonPressProp, - threadBadgeColor, - toggleFollowThread, - jumpToMessage, - onPress: onPressProp, - closeEmojiAndAction, - isPreview, - dateSeparator, - showUnreadSeparator -}: IMessageContainerProps) => { - 'use memo'; - - const isBeingEdited = useIsBeingEdited(item.id); - const { theme } = useTheme(); - const [isManualUnignored, setIsManualUnignored] = useState(false); - - // Sourced via the per-message store (rather than read directly off `item`) so this - // component re-renders and refreshes the Context value/closures below when they change. - const { hasError } = useMessageStatus(); - const isEncrypted = useIsEncrypted(); - const isInfo = useIsInfo(); - - const isIgnored = isManualUnignored ? false : isIgnoredProp ?? false; - - const onIgnoredMessagePress = () => { - setIsManualUnignored(true); - }; - - const onErrorPress = () => { - if (errorActionsShow) { - errorActionsShow(item); - } - }; - - const onReactionPress = (emoji: string) => { - if (onReactionPressProp) { - onReactionPressProp(emoji, item.id); - } - }; - - const onReactionLongPress = () => { - if (onReactionLongPressProp) { - onReactionLongPressProp(item); - } - }; - - const onEncryptedPress = () => { - if (onEncryptedPressProp) { - onEncryptedPressProp(); - } - }; - - const onDiscussionPress = () => { - if (onDiscussionPressProp) { - onDiscussionPressProp(item); - } - }; - - const onThreadPress = () => { - if (onThreadPressProp) { - onThreadPressProp(item); - } - }; - - const onAnswerButtonPress = (msg: string) => { - if (onAnswerButtonPressProp) { - onAnswerButtonPressProp(msg, false); - } - }; - - const reactionInit = () => { - if (reactionInitProp) { - reactionInitProp(item.id); - } - }; - - const replyBroadcast = () => { - if (replyBroadcastProp) { - replyBroadcastProp(item); - } - }; - - const onLinkPress = (link: string): void => { - const isMessageLink = !!item.attachments?.some((att: IAttachment) => att?.message_link === link); - if (isMessageLink && jumpToMessage) { - return jumpToMessage(link); - } - openLink(link, theme); - }; - - const onLongPress = () => { - if (isInfo || hasError || isEncrypted || archived) { - return; - } - if (onLongPressProp) { - onLongPressProp(item); - } - }; - - // pressHandlerRef holds the latest press logic; updated after every render via - // useEffect so the render body stays free of ref writes. - const pressHandlerRef = useRef<() => void>(() => {}); - useEffect(() => { - pressHandlerRef.current = () => { - if (isIgnored) { - return onIgnoredMessagePress(); - } - - if (onPressProp) { - return onPressProp(); - } - - Keyboard.dismiss(); - - if ((item.tlm || item.tmid) && !isThreadRoom) { - onThreadPress(); - } - - if (item.dlm && onDiscussionPressProp) { - onDiscussionPressProp(item); - } - }; - }); - // onPressRef holds the single debounced instance for the component lifetime. - // Initialised to a noop; replaced with the real debounced fn in the first effect - // so that pressHandlerRef.current is never read during render. - const onPressRef = useRef(debounce(() => {}, 300, true)); - useEffect(() => { - onPressRef.current = debounce(() => pressHandlerRef.current?.(), 300, true); - }, []); - - const onPressAction = () => { - if (closeEmojiAndAction) { - return closeEmojiAndAction(onPressRef.current); - } - return onPressRef.current(); - }; - - const otherUserMessage = item.u?.username !== user?.username; - const canTranslateMessage = autoTranslateRoom && autoTranslateLanguage && item.autoTranslate !== false && otherUserMessage; - - return ( - <MessageContext.Provider - value={{ - id: item.id, - rid, - user, - baseUrl, - onPress: onPressAction, - onLongPress, - reactionInit, - onErrorPress, - replyBroadcast, - onReactionPress, - onEncryptedPress, - onDiscussionPress, - onThreadPress, - onReactionLongPress, - onLinkPress, - onAnswerButtonPress, - jumpToMessage, - threadBadgeColor, - toggleFollowThread, - replies: item.replies, - translateLanguage: canTranslateMessage ? autoTranslateLanguage : undefined, - isEncrypted, - getCustomEmoji, - navToRoomInfo, - showAttachment, - blockAction, - handleEnterCall, - fetchThreadName, - broadcast, - Message_GroupingPeriod, - isThreadRoom: !!isThreadRoom, - autoTranslateRoom, - autoTranslateLanguage - }}> - <Message - rid={rid} - timeFormat={timeFormat} - archived={archived} - broadcast={broadcast} - useRealName={useRealName} - isReadReceiptEnabled={isReadReceiptEnabled} - isThreadRoom={!!isThreadRoom} - isPreview={isPreview} - highlighted={highlighted} - isIgnored={isIgnored} - isBeingEdited={isBeingEdited} - autoTranslateLanguage={autoTranslateLanguage} - /> - <MessageSeparator ts={dateSeparator} unread={showUnreadSeparator} /> - </MessageContext.Provider> - ); -}; +const EMPTY: IMessageContext = {}; const MessageContainer = (props: IMessageContainerProps) => { 'use memo'; - const { item, previousItem } = props; + const { item, previousItem, dateSeparator, showUnreadSeparator } = props; return ( <MessageProvider item={item} @@ -302,7 +72,22 @@ const MessageContainer = (props: IMessageContainerProps) => { onPress={props.onPress} onLongPress={props.onLongPress} threadBadgeColor={props.threadBadgeColor}> - <MessageContainerInner {...props} /> + <MessageContext.Provider value={EMPTY}> + <Message + rid={props.rid} + timeFormat={props.timeFormat} + archived={props.archived ?? false} + broadcast={props.broadcast ?? false} + useRealName={props.useRealName} + isReadReceiptEnabled={props.isReadReceiptEnabled} + isThreadRoom={!!props.isThreadRoom} + isPreview={props.isPreview} + highlighted={props.highlighted} + isIgnored={props.isIgnored ?? false} + autoTranslateLanguage={props.autoTranslateLanguage} + /> + <MessageSeparator ts={dateSeparator} unread={showUnreadSeparator} /> + </MessageContext.Provider> </MessageProvider> ); }; From 66964e0befc98cf1d5f7012d571e12ae3092b2e6 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 16:50:53 -0300 Subject: [PATCH 029/144] refactor: drop dead theme prop from the message container (NATIVE-1376) MessageContainer no longer reads theme after the Inner collapse; leaves already receive it through the room store's useMessageTheme selector. Removing the prop stops a theme toggle from churning memo(MessageContainer) and removes the last theme forward into the message subtree from RoomView, MessagesView, SearchMessagesView, and the Message stories. --- app/containers/message/index.tsx | 2 -- app/views/MessagesView/index.tsx | 9 ++++----- app/views/RoomView/index.tsx | 2 -- app/views/SearchMessagesView/index.tsx | 3 +-- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index eb1a8c65c15..3815aeee20b 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -2,7 +2,6 @@ import { memo } from 'react'; import Message from './Message'; import MessageContext, { type IMessageContext } from './Context'; -import { type TSupportedThemes } from '../../theme'; import { type IAttachment, type TAnyMessageModel, type TGetCustomEmoji } from '../../definitions'; import { type IRoomInfoParam } from '../../views/SearchMessagesView'; import MessageSeparator from '../MessageSeparator'; @@ -52,7 +51,6 @@ interface IMessageContainerProps { toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise<void>; jumpToMessage?: (link: string) => void; onPress?: () => void; - theme?: TSupportedThemes; closeEmojiAndAction?: (action?: Function, params?: any) => void; isPreview?: boolean; dateSeparator?: Date | string | null; diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index 60e13953f28..579544154f4 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -162,7 +162,7 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { }; defineMessagesViewContent = (name: string) => { - const { user, baseUrl, theme, useRealName } = this.props; + const { user, baseUrl, useRealName } = this.props; const renderItemCommonProps = (item: TAnyMessageModel) => ({ item, baseUrl, @@ -197,7 +197,6 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { renderItem: (item: any) => ( <Message {...renderItemCommonProps(item)} - theme={theme} item={{ ...item, u: item.user, @@ -223,7 +222,7 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { }, noDataMsg: I18n.t('No_mentioned_messages'), testID: 'mentioned-messages-view', - renderItem: (item: TAnyMessageModel) => <Message {...renderItemCommonProps(item)} msg={item.msg} theme={theme} /> + renderItem: (item: TAnyMessageModel) => <Message {...renderItemCommonProps(item)} msg={item.msg} /> }, // Starred Messages Screen Starred: { @@ -235,7 +234,7 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { noDataMsg: I18n.t('No_starred_messages'), testID: 'starred-messages-view', renderItem: (item: TAnyMessageModel) => ( - <Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} theme={theme} /> + <Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} /> ), action: (message: IMessage) => ({ title: I18n.t('Unstar'), @@ -254,7 +253,7 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { noDataMsg: I18n.t('No_pinned_messages'), testID: 'pinned-messages-view', renderItem: (item: TAnyMessageModel) => ( - <Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} theme={theme} /> + <Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} /> ), action: () => ({ title: I18n.t('Unpin'), icon: 'pin', onPress: this.handleActionPress }), handleActionPress: (message: IMessage) => togglePinMessage(message._id, message.pinned) diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index d2bef862785..3bd0cbd67d4 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1417,7 +1417,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { useRealName, baseUrl, Message_Read_Receipt_Enabled, - theme, inAppFeedback } = this.props; let dateSeparator = null; @@ -1501,7 +1500,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { toggleFollowThread={this.toggleFollowThread} jumpToMessage={this.jumpToMessageByUrl} highlighted={highlightedMessage === item.id} - theme={theme} closeEmojiAndAction={this.handleCloseEmoji} dateSeparator={dateSeparator} showUnreadSeparator={showUnreadSeparator} diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index 17466b2c615..cbfdd0dd610 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -286,7 +286,7 @@ class SearchMessagesView extends Component<ISearchMessagesViewProps, ISearchMess renderItem = ({ item }: { item: IMessageFromServer | TMessageModel }) => { const message = item as TMessageModel; - const { user, baseUrl, theme, useRealName } = this.props; + const { user, baseUrl, useRealName } = this.props; return ( <Message item={message} @@ -298,7 +298,6 @@ class SearchMessagesView extends Component<ISearchMessagesViewProps, ISearchMess getCustomEmoji={this.getCustomEmoji} navToRoomInfo={this.navToRoomInfo} useRealName={useRealName} - theme={theme} onPress={() => this.jumpToMessage({ item })} jumpToMessage={() => this.jumpToMessage({ item })} rid={message.rid} From 4059a4971d7732aab8a3c9fc67c4b1fb37677b39 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 17:05:11 -0300 Subject: [PATCH 030/144] refactor: delete MessageContext and its inert provider (NATIVE-1376) MessageContext held no readers after row consumers moved to the room and per-message zustand stores. Remove Context.ts, the EMPTY provider in the container, and the inert legacy provider wrappers left in tests and stories; test subtrees now rely solely on MessageRoomProvider and MessageProvider. --- .rnstorybook/preview.tsx | 29 +----------- app/containers/UIKit/UiKitMessage.stories.tsx | 28 +----------- app/containers/UIKit/UiKitModal.stories.tsx | 31 +------------ .../Attachments/Attachments.stories.tsx | 5 +-- .../CollapsibleQuote.stories.tsx | 9 +--- .../CollapsibleQuote.test.tsx | 9 ++-- .../Attachments/Image/Container.test.tsx | 9 ++-- app/containers/message/Content.test.tsx | 9 ++-- app/containers/message/Context.ts | 45 ------------------- app/containers/message/MessageRoomStore.tsx | 4 +- app/containers/message/Thread.test.tsx | 17 +++---- .../useMessageAccessibilityHint.test.tsx | 11 ++--- .../useMessageAccessibilityLabel.test.tsx | 11 ++--- app/containers/message/index.tsx | 33 ++++++-------- app/containers/message/testHelpers.tsx | 10 +---- 15 files changed, 46 insertions(+), 214 deletions(-) delete mode 100644 app/containers/message/Context.ts diff --git a/.rnstorybook/preview.tsx b/.rnstorybook/preview.tsx index 134f80e7ac3..d89a5eb10b2 100644 --- a/.rnstorybook/preview.tsx +++ b/.rnstorybook/preview.tsx @@ -2,8 +2,6 @@ import React from 'react'; import type { Preview } from '@storybook/react'; import { Provider } from 'react-redux'; -import { themes } from '../app/lib/constants/colors'; -import MessageContext from '../app/containers/message/Context'; import { selectServerRequest } from '../app/actions/server'; import { mockedStore as store } from '../app/reducers/mockedStore'; import { setUser } from '../app/actions/login'; @@ -19,32 +17,7 @@ const preview: Preview = { decorators: [ Story => ( <Provider store={store}> - <MessageContext.Provider - value={{ - user: { - id: 'y8bd77ptZswPj3EW8', - username: 'diego.mello', - token: 'abc' - }, - baseUrl, - onPress: () => {}, - onLongPress: () => {}, - reactionInit: () => {}, - onErrorPress: () => {}, - replyBroadcast: () => {}, - onReactionPress: () => {}, - onDiscussionPress: () => {}, - onReactionLongPress: () => {}, - threadBadgeColor: themes.light.badgeBackgroundLevel1, - getCustomEmoji: () => null, - navToRoomInfo: () => {}, - showAttachment: undefined, - blockAction: undefined, - handleEnterCall: undefined, - fetchThreadName: undefined - }}> - <Story /> - </MessageContext.Provider> + <Story /> </Provider> ) ] diff --git a/app/containers/UIKit/UiKitMessage.stories.tsx b/app/containers/UIKit/UiKitMessage.stories.tsx index 88b8bb4f756..dc386700e54 100644 --- a/app/containers/UIKit/UiKitMessage.stories.tsx +++ b/app/containers/UIKit/UiKitMessage.stories.tsx @@ -1,9 +1,8 @@ import { ScrollView, View } from 'react-native'; import { type ReactElement } from 'react'; -import MessageContext from '../message/Context'; import { UiKitMessage } from '.'; -import { themes, colors } from '../../lib/constants/colors'; +import { colors } from '../../lib/constants/colors'; import { longText } from '../../../.rnstorybook/utils'; import { BASE_ROW_HEIGHT, @@ -12,14 +11,6 @@ import { } from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; import { ThemeContext, type TSupportedThemes } from '../../theme'; -const user = { - id: 'y8bd77ptZswPj3EW8', - username: 'diego.mello', - token: 'abc' -}; - -const baseUrl = 'https://open.rocket.chat'; - export default { title: 'UIKit/UiKitMessage', decorators: [ @@ -34,22 +25,7 @@ export default { width: 350, height: 800 }}> - <MessageContext.Provider - value={{ - user, - baseUrl, - onPress: () => {}, - onLongPress: () => {}, - reactionInit: () => {}, - onErrorPress: () => {}, - replyBroadcast: () => {}, - onReactionPress: () => {}, - onDiscussionPress: () => {}, - onReactionLongPress: () => {}, - threadBadgeColor: themes.light.fontInfo - }}> - <Story /> - </MessageContext.Provider> + <Story /> </ResponsiveLayoutContext.Provider> ) ] diff --git a/app/containers/UIKit/UiKitModal.stories.tsx b/app/containers/UIKit/UiKitModal.stories.tsx index 21ccea85885..8f571fb14d4 100644 --- a/app/containers/UIKit/UiKitModal.stories.tsx +++ b/app/containers/UIKit/UiKitModal.stories.tsx @@ -3,39 +3,10 @@ import { Button, View } from 'react-native'; import { UiKitComponent, UiKitModal } from '.'; import { KitContext, defaultContext } from './utils'; -import MessageContext from '../message/Context'; -import { themes } from '../../lib/constants/colors'; - -const user = { - id: 'y8bd77ptZswPj3EW8', - username: 'diego.mello', - token: 'abc' -}; - -const baseUrl = 'https://open.rocket.chat'; export default { title: 'UIKit/UiKitModal', - decorators: [ - (Story: any) => ( - <MessageContext.Provider - value={{ - user, - baseUrl, - onPress: () => {}, - onLongPress: () => {}, - reactionInit: () => {}, - onErrorPress: () => {}, - replyBroadcast: () => {}, - onReactionPress: () => {}, - onDiscussionPress: () => {}, - onReactionLongPress: () => {}, - threadBadgeColor: themes.light.fontInfo - }}> - <Story /> - </MessageContext.Provider> - ) - ] + decorators: [(Story: any) => <Story />] }; export const ModalSectionSelects = () => diff --git a/app/containers/message/Components/Attachments/Attachments.stories.tsx b/app/containers/message/Components/Attachments/Attachments.stories.tsx index a3cfc04eab2..9131427746c 100644 --- a/app/containers/message/Components/Attachments/Attachments.stories.tsx +++ b/app/containers/message/Components/Attachments/Attachments.stories.tsx @@ -5,7 +5,6 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { selectServerSuccess } from '../../../../actions/server'; import { type TAnyMessageModel } from '../../../../definitions'; -import MessageContext from '../../Context'; import { MessageRoomProvider, pickMessageRoomState } from '../../MessageRoomStore'; import { MessageProvider } from '../../MessageStore'; import Attachments from './Attachments'; @@ -25,9 +24,7 @@ const mockItem = { id: 'msg-id', msg: '', u: { username: 'rocket.cat' }, autoTra const StoryWrapper = ({ store, children }: { store: any; children: ReactNode }) => ( <Provider store={store}> <MessageRoomProvider {...pickMessageRoomState(mockMessageContext)}> - <MessageProvider item={mockItem}> - <MessageContext.Provider value={mockMessageContext}>{children}</MessageContext.Provider> - </MessageProvider> + <MessageProvider item={mockItem}>{children}</MessageProvider> </MessageRoomProvider> </Provider> ); diff --git a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx b/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx index e0ae9a2fb3b..1cdfcce259e 100644 --- a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx +++ b/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx @@ -1,6 +1,5 @@ import { View } from 'react-native'; -import MessageContext from '../../../Context'; import { MessageRoomProvider, pickMessageRoomState } from '../../../MessageRoomStore'; import CollapsibleQuote from '.'; @@ -25,13 +24,7 @@ export default { export const Item = () => ( <View style={{ padding: 10 }}> <MessageRoomProvider {...pickMessageRoomState({ onLongPress: () => {}, user: { username: 'Marcos' } })}> - <MessageContext.Provider - value={{ - onLongPress: () => {}, - user: { username: 'Marcos' } - }}> - <CollapsibleQuote attachment={testAttachment} getCustomEmoji={() => null} timeFormat='LT' /> - </MessageContext.Provider> + <CollapsibleQuote attachment={testAttachment} getCustomEmoji={() => null} timeFormat='LT' /> </MessageRoomProvider> </View> ); diff --git a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx b/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx index a58e7af3604..ae181120587 100644 --- a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx +++ b/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx @@ -4,7 +4,6 @@ import { Provider } from 'react-redux'; import { setUser } from '../../../../../actions/login'; import { mockedStore } from '../../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../../definitions'; -import MessageContext from '../../../Context'; import { MessageProvider } from '../../../MessageStore'; import { MessageRoomProvider, pickMessageRoomState } from '../../../MessageRoomStore'; import CollapsibleQuote from '.'; @@ -50,11 +49,9 @@ const item = { id: 'collapsible-quote-msg' } as unknown as TAnyMessageModel; const Render = () => ( <Provider store={mockedStore}> <MessageRoomProvider {...pickMessageRoomState(contextValue)}> - <MessageContext.Provider value={contextValue}> - <MessageProvider item={item}> - <CollapsibleQuote attachment={testAttachment} getCustomEmoji={mockFn} timeFormat='LT' /> - </MessageProvider> - </MessageContext.Provider> + <MessageProvider item={item}> + <CollapsibleQuote attachment={testAttachment} getCustomEmoji={mockFn} timeFormat='LT' /> + </MessageProvider> </MessageRoomProvider> </Provider> ); diff --git a/app/containers/message/Components/Attachments/Image/Container.test.tsx b/app/containers/message/Components/Attachments/Image/Container.test.tsx index 569ac8d054b..c9679dbd83d 100644 --- a/app/containers/message/Components/Attachments/Image/Container.test.tsx +++ b/app/containers/message/Components/Attachments/Image/Container.test.tsx @@ -3,7 +3,6 @@ import { type ComponentProps } from 'react'; import { A11y } from 'react-native-a11y-order'; import { type TAnyMessageModel } from '../../../../../definitions'; -import MessageContext from '../../../Context'; import { MessageProvider } from '../../../MessageStore'; import { MessageRoomProvider, pickMessageRoomState } from '../../../MessageRoomStore'; import ImageContainer from './Container'; @@ -45,11 +44,9 @@ const renderImageContainer = (props?: Partial<ComponentProps<typeof ImageContain return render( <A11y.Order> <MessageRoomProvider {...pickMessageRoomState(contextValue)}> - <MessageContext.Provider value={contextValue}> - <MessageProvider item={item}> - <ImageContainer file={{ image_url: 'https://open.rocket.chat/image.png', image_type: 'image/png' }} {...props} /> - </MessageProvider> - </MessageContext.Provider> + <MessageProvider item={item}> + <ImageContainer file={{ image_url: 'https://open.rocket.chat/image.png', image_type: 'image/png' }} {...props} /> + </MessageProvider> </MessageRoomProvider> </A11y.Order> ); diff --git a/app/containers/message/Content.test.tsx b/app/containers/message/Content.test.tsx index 6cc99af01a8..02d1780f531 100644 --- a/app/containers/message/Content.test.tsx +++ b/app/containers/message/Content.test.tsx @@ -2,7 +2,6 @@ import { Provider } from 'react-redux'; import { render } from '@testing-library/react-native'; import Content from './Content'; -import MessageContext from './Context'; import { MessageProvider } from './MessageStore'; import { MessageRoomProvider, pickMessageRoomState } from './MessageRoomStore'; import { setUser } from '../../actions/login'; @@ -67,11 +66,9 @@ const tree = (overrides: TOverrides) => { return ( <Provider store={mockedStore}> <MessageRoomProvider {...pickMessageRoomState(contextValue)}> - <MessageContext.Provider value={contextValue}> - <MessageProvider item={buildItem(msg, attachments, isEncrypted)}> - <Content {...(baseProps as IMessageContent)} {...(contentProps as IMessageContent)} /> - </MessageProvider> - </MessageContext.Provider> + <MessageProvider item={buildItem(msg, attachments, isEncrypted)}> + <Content {...(baseProps as IMessageContent)} {...(contentProps as IMessageContent)} /> + </MessageProvider> </MessageRoomProvider> </Provider> ); diff --git a/app/containers/message/Context.ts b/app/containers/message/Context.ts deleted file mode 100644 index cb38a88186e..00000000000 --- a/app/containers/message/Context.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { createContext } from 'react'; - -import { type TGetCustomEmoji } from '../../definitions/IEmoji'; -import { type IAttachment } from '../../definitions'; -import { type IRoomInfoParam } from '../../views/SearchMessagesView'; - -export interface IMessageContext { - id?: string; - rid?: string; - user?: { id?: string; username?: string; token?: string }; - baseUrl?: string; - onPress?: () => void; - onLongPress?: () => void; - reactionInit?: () => void; - onErrorPress?: () => void; - replyBroadcast?: () => void; - onReactionPress?: (emoji: string) => void; - onEncryptedPress?: () => void; - onDiscussionPress?: () => void; - onThreadPress?: () => void; - onReactionLongPress?: () => void; - onLinkPress?: (link: string) => void; - onAnswerButtonPress?: (msg: string) => void; - jumpToMessage?: (link: string) => void; - threadBadgeColor?: string; - toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise<void>; - replies?: string[]; - translateLanguage?: string; - isEncrypted?: boolean; - e2e?: string; - getCustomEmoji?: TGetCustomEmoji; - navToRoomInfo?: (navParam: IRoomInfoParam) => void; - showAttachment?: (file: IAttachment) => void; - blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void; - handleEnterCall?: () => void; - fetchThreadName?: (tmid: string, id: string) => Promise<string | undefined>; - broadcast?: boolean; - Message_GroupingPeriod?: number; - isThreadRoom?: boolean; - autoTranslateRoom?: boolean; - autoTranslateLanguage?: string; -} - -const MessageContext = createContext<IMessageContext>({}); -export default MessageContext; diff --git a/app/containers/message/MessageRoomStore.tsx b/app/containers/message/MessageRoomStore.tsx index 9000a864d1c..644fcbc194d 100644 --- a/app/containers/message/MessageRoomStore.tsx +++ b/app/containers/message/MessageRoomStore.tsx @@ -43,8 +43,8 @@ export type MessageRoomState = { }; // The room-scoped fields this store owns. Tests and stories use pickMessageRoomState to -// mirror the same values into MessageRoomProvider and the legacy MessageContext during the -// dual-run migration. Add a field to MessageRoomState → add its key here. +// derive MessageRoomProvider props from a single fixture object. Add a field to +// MessageRoomState → add its key here. const ROOM_STATE_KEYS: (keyof MessageRoomState)[] = [ 'getCustomEmoji', 'navToRoomInfo', diff --git a/app/containers/message/Thread.test.tsx b/app/containers/message/Thread.test.tsx index dfba3d636c1..04946302357 100644 --- a/app/containers/message/Thread.test.tsx +++ b/app/containers/message/Thread.test.tsx @@ -1,7 +1,6 @@ import { render } from '@testing-library/react-native'; import Thread from './Thread'; -import MessageContext from './Context'; import { MessageProvider } from './MessageStore'; import { MessageRoomProvider, pickMessageRoomState } from './MessageRoomStore'; import { type TAnyMessageModel } from '../../definitions'; @@ -20,11 +19,9 @@ const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel const renderThread = (item: TAnyMessageModel, props: Parameters<typeof Thread>[0]) => render( <MessageRoomProvider {...pickMessageRoomState(baseContextValue)}> - <MessageContext.Provider value={baseContextValue}> - <MessageProvider item={item}> - <Thread {...props} /> - </MessageProvider> - </MessageContext.Provider> + <MessageProvider item={item}> + <Thread {...props} /> + </MessageProvider> </MessageRoomProvider> ); @@ -40,11 +37,9 @@ describe('Thread — tlm-only update regression', () => { const updatedItem = buildItem({ tlm: new Date('2024-01-01T00:00:00Z') }); rerender( <MessageRoomProvider {...pickMessageRoomState(baseContextValue)}> - <MessageContext.Provider value={baseContextValue}> - <MessageProvider item={updatedItem}> - <Thread isThreadRoom={false} /> - </MessageProvider> - </MessageContext.Provider> + <MessageProvider item={updatedItem}> + <Thread isThreadRoom={false} /> + </MessageProvider> </MessageRoomProvider> ); diff --git a/app/containers/message/hooks/useMessageAccessibilityHint.test.tsx b/app/containers/message/hooks/useMessageAccessibilityHint.test.tsx index bb349251c9f..ed007395d2a 100644 --- a/app/containers/message/hooks/useMessageAccessibilityHint.test.tsx +++ b/app/containers/message/hooks/useMessageAccessibilityHint.test.tsx @@ -1,7 +1,6 @@ import { render } from '@testing-library/react-native'; import { useMessageAccessibilityHint } from './useMessageAccessibilityHint'; -import MessageContext, { type IMessageContext } from '../Context'; import { MessageRoomProvider, pickMessageRoomState } from '../MessageRoomStore'; import { MessageProvider } from '../MessageStore'; import { type TAnyMessageModel } from '../../../definitions'; @@ -9,7 +8,7 @@ import { type TAnyMessageModel } from '../../../definitions'; const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel => ({ id: 'msg-1', ...overrides } as TAnyMessageModel); -const renderHint = (item: TAnyMessageModel, config: Partial<IMessageContext> = {}) => { +const renderHint = (item: TAnyMessageModel, config: Record<string, any> = {}) => { const spy = jest.fn(); const Probe = () => { spy(useMessageAccessibilityHint()); @@ -17,11 +16,9 @@ const renderHint = (item: TAnyMessageModel, config: Partial<IMessageContext> = { }; render( <MessageRoomProvider {...pickMessageRoomState(config)}> - <MessageContext.Provider value={config as IMessageContext}> - <MessageProvider item={item}> - <Probe /> - </MessageProvider> - </MessageContext.Provider> + <MessageProvider item={item}> + <Probe /> + </MessageProvider> </MessageRoomProvider> ); const { calls } = spy.mock; diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx b/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx index 12e596dfa5a..9705f8ded25 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx @@ -1,7 +1,6 @@ import { render } from '@testing-library/react-native'; import { useMessageAccessibilityLabel } from './useMessageAccessibilityLabel'; -import MessageContext, { type IMessageContext } from '../Context'; import { MessageProvider } from '../MessageStore'; import { MessageRoomProvider, pickMessageRoomState } from '../MessageRoomStore'; import { type TAnyMessageModel } from '../../../definitions'; @@ -29,7 +28,7 @@ const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel const renderLabel = ( item: TAnyMessageModel, args: TArgs = {}, - config: Partial<IMessageContext> = {}, + config: Record<string, any> = {}, previousItem?: TAnyMessageModel ) => { const spy = jest.fn(); @@ -39,11 +38,9 @@ const renderLabel = ( }; render( <MessageRoomProvider {...pickMessageRoomState(config)}> - <MessageContext.Provider value={config as IMessageContext}> - <MessageProvider item={item} previousItem={previousItem}> - <Probe /> - </MessageProvider> - </MessageContext.Provider> + <MessageProvider item={item} previousItem={previousItem}> + <Probe /> + </MessageProvider> </MessageRoomProvider> ); const { calls } = spy.mock; diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 3815aeee20b..d7d7b87385e 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -1,7 +1,6 @@ import { memo } from 'react'; import Message from './Message'; -import MessageContext, { type IMessageContext } from './Context'; import { type IAttachment, type TAnyMessageModel, type TGetCustomEmoji } from '../../definitions'; import { type IRoomInfoParam } from '../../views/SearchMessagesView'; import MessageSeparator from '../MessageSeparator'; @@ -57,8 +56,6 @@ interface IMessageContainerProps { showUnreadSeparator?: boolean; } -const EMPTY: IMessageContext = {}; - const MessageContainer = (props: IMessageContainerProps) => { 'use memo'; @@ -70,22 +67,20 @@ const MessageContainer = (props: IMessageContainerProps) => { onPress={props.onPress} onLongPress={props.onLongPress} threadBadgeColor={props.threadBadgeColor}> - <MessageContext.Provider value={EMPTY}> - <Message - rid={props.rid} - timeFormat={props.timeFormat} - archived={props.archived ?? false} - broadcast={props.broadcast ?? false} - useRealName={props.useRealName} - isReadReceiptEnabled={props.isReadReceiptEnabled} - isThreadRoom={!!props.isThreadRoom} - isPreview={props.isPreview} - highlighted={props.highlighted} - isIgnored={props.isIgnored ?? false} - autoTranslateLanguage={props.autoTranslateLanguage} - /> - <MessageSeparator ts={dateSeparator} unread={showUnreadSeparator} /> - </MessageContext.Provider> + <Message + rid={props.rid} + timeFormat={props.timeFormat} + archived={props.archived ?? false} + broadcast={props.broadcast ?? false} + useRealName={props.useRealName} + isReadReceiptEnabled={props.isReadReceiptEnabled} + isThreadRoom={!!props.isThreadRoom} + isPreview={props.isPreview} + highlighted={props.highlighted} + isIgnored={props.isIgnored ?? false} + autoTranslateLanguage={props.autoTranslateLanguage} + /> + <MessageSeparator ts={dateSeparator} unread={showUnreadSeparator} /> </MessageProvider> ); }; diff --git a/app/containers/message/testHelpers.tsx b/app/containers/message/testHelpers.tsx index 29c098dc288..b146c447f5a 100644 --- a/app/containers/message/testHelpers.tsx +++ b/app/containers/message/testHelpers.tsx @@ -4,22 +4,17 @@ import { render } from '@testing-library/react-native'; import { type TAnyMessageModel } from '../../definitions'; import { mockedStore } from '../../reducers/mockedStore'; -import MessageContext, { type IMessageContext } from './Context'; import { MessageProvider } from './MessageStore'; import { MessageRoomProvider, pickMessageRoomState, type MessageRoomState } from './MessageRoomStore'; interface IMessageProvidersOptions { item?: TAnyMessageModel; previousItem?: TAnyMessageModel; - context?: Partial<IMessageContext>; + context?: Record<string, any>; room?: Partial<MessageRoomState>; withRedux?: boolean; } -// Provider stack every message-subtree test needs during the MessageContext → room-store dual-run: -// Redux → MessageRoomProvider (new) → MessageContext.Provider (legacy, only when `context` given) -// → MessageProvider (per-message store, only when `item` given). `room` defaults to the room-scoped -// slice of `context`, so a single source (`context`) feeds both providers with identical values. export const MessageProviders = ({ item, previousItem, @@ -38,9 +33,6 @@ export const MessageProviders = ({ </MessageProvider> ); } - if (context !== undefined) { - tree = <MessageContext.Provider value={context as IMessageContext}>{tree}</MessageContext.Provider>; - } tree = <MessageRoomProvider {...roomState}>{tree}</MessageRoomProvider>; if (withRedux) { tree = <Provider store={mockedStore}>{tree}</Provider>; From f9ab6a66f078551f2ae0482d104d0a3fc211d822 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 17:20:42 -0300 Subject: [PATCH 031/144] refactor: drop dead handler props from the message container interface (NATIVE-1376) Row consumers now read the room and per-message zustand stores, so the container no longer relays these handlers/constants. Remove them from IMessageContainerProps and stop passing them at the RoomView, MessagesView, and SearchMessagesView call sites, where the same handlers are already supplied via MessageRoomProvider. --- app/containers/message/Preview.tsx | 11 +------- app/containers/message/index.test.tsx | 5 +--- app/containers/message/index.tsx | 32 +---------------------- app/views/MessagesView/index.tsx | 17 +++--------- app/views/RoomView/index.tsx | 36 ++------------------------ app/views/SearchMessagesView/index.tsx | 8 +----- 6 files changed, 10 insertions(+), 99 deletions(-) diff --git a/app/containers/message/Preview.tsx b/app/containers/message/Preview.tsx index cceb49208e1..8289091e839 100644 --- a/app/containers/message/Preview.tsx +++ b/app/containers/message/Preview.tsx @@ -21,16 +21,7 @@ const MessagePreview = ({ message }: { message: TAnyMessageModel }) => { }; return ( <MessageRoomProvider user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} rid={message.rid}> - <Message - item={message} - user={user} - rid={message.rid} - baseUrl={baseUrl} - getCustomEmoji={getCustomEmoji} - timeFormat={Message_TimeFormat} - useRealName={useRealName} - isPreview - /> + <Message item={message} rid={message.rid} timeFormat={Message_TimeFormat} useRealName={useRealName} isPreview /> </MessageRoomProvider> ); }; diff --git a/app/containers/message/index.test.tsx b/app/containers/message/index.test.tsx index fd9739f5201..9bae579435c 100644 --- a/app/containers/message/index.test.tsx +++ b/app/containers/message/index.test.tsx @@ -57,10 +57,7 @@ const createMockMessage = (overrides: Record<string, any> = {}): TAnyMessageMode } as unknown as TAnyMessageModel); const baseProps = { - user: { id: 'user-1', username: 'testuser', token: 'token' }, - rid: 'room-1', - baseUrl: 'https://open.rocket.chat', - getCustomEmoji: jest.fn(() => null) + rid: 'room-1' }; const renderContainer = (itemOverrides: Record<string, any> = {}, propOverrides: Record<string, any> = {}) => { diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index d7d7b87385e..f92ebe6fb04 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -1,56 +1,26 @@ import { memo } from 'react'; import Message from './Message'; -import { type IAttachment, type TAnyMessageModel, type TGetCustomEmoji } from '../../definitions'; -import { type IRoomInfoParam } from '../../views/SearchMessagesView'; +import { type TAnyMessageModel } from '../../definitions'; import MessageSeparator from '../MessageSeparator'; import { MessageProvider } from './MessageStore'; interface IMessageContainerProps { item: TAnyMessageModel; - user: { - id: string; - username: string; - token: string; - }; - msg?: string; rid: string; timeFormat?: string; archived?: boolean; broadcast?: boolean; previousItem?: TAnyMessageModel; - baseUrl: string; - Message_GroupingPeriod?: number; isReadReceiptEnabled?: boolean; isThreadRoom?: boolean; - isSystemMessage?: boolean; useRealName?: boolean; - autoTranslateRoom?: boolean; autoTranslateLanguage?: string; - status?: number; isIgnored?: boolean; highlighted?: boolean; - getCustomEmoji: TGetCustomEmoji; onLongPress?: (item: TAnyMessageModel) => void; - onReactionPress?: (emoji: string, id: string) => void; - onEncryptedPress?: () => void; - onDiscussionPress?: (item: TAnyMessageModel) => void; - onThreadPress?: (item: TAnyMessageModel) => void; - errorActionsShow?: (item: TAnyMessageModel) => void; - replyBroadcast?: (item: TAnyMessageModel) => void; - reactionInit?: (messageId: string) => void; - fetchThreadName?: (tmid: string, id: string) => Promise<string | undefined>; - showAttachment?: (file: IAttachment) => void; - onReactionLongPress?: (item: TAnyMessageModel) => void; - navToRoomInfo?: (navParam: IRoomInfoParam) => void; - handleEnterCall?: () => void; - blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void; - onAnswerButtonPress?: Function; threadBadgeColor?: string; - toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise<void>; - jumpToMessage?: (link: string) => void; onPress?: () => void; - closeEmojiAndAction?: (action?: Function, params?: any) => void; isPreview?: boolean; dateSeparator?: Date | string | null; showUnreadSeparator?: boolean; diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index 579544154f4..5b780be36b1 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -162,21 +162,12 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { }; defineMessagesViewContent = (name: string) => { - const { user, baseUrl, useRealName } = this.props; + const { user, useRealName } = this.props; const renderItemCommonProps = (item: TAnyMessageModel) => ({ item, - baseUrl, - user, - author: item.u || item.user, timeFormat: 'MMM Do YYYY, h:mm:ss a', - isEdited: !!item.editedAt, - isHeader: true, isThreadRoom: true, - attachments: item.attachments || [], useRealName, - showAttachment: this.showAttachment, - getCustomEmoji: this.getCustomEmoji, - navToRoomInfo: this.navToRoomInfo, onPress: () => this.jumpToMessage({ item }), rid: this.rid }); @@ -222,7 +213,7 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { }, noDataMsg: I18n.t('No_mentioned_messages'), testID: 'mentioned-messages-view', - renderItem: (item: TAnyMessageModel) => <Message {...renderItemCommonProps(item)} msg={item.msg} /> + renderItem: (item: TAnyMessageModel) => <Message {...renderItemCommonProps(item)} /> }, // Starred Messages Screen Starred: { @@ -234,7 +225,7 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { noDataMsg: I18n.t('No_starred_messages'), testID: 'starred-messages-view', renderItem: (item: TAnyMessageModel) => ( - <Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} /> + <Message {...renderItemCommonProps(item)} onLongPress={() => this.onLongPress(item)} /> ), action: (message: IMessage) => ({ title: I18n.t('Unstar'), @@ -253,7 +244,7 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { noDataMsg: I18n.t('No_pinned_messages'), testID: 'pinned-messages-view', renderItem: (item: TAnyMessageModel) => ( - <Message {...renderItemCommonProps(item)} msg={item.msg} onLongPress={() => this.onLongPress(item)} /> + <Message {...renderItemCommonProps(item)} onLongPress={() => this.onLongPress(item)} /> ), action: () => ({ title: I18n.t('Unpin'), icon: 'pin', onPress: this.handleActionPress }), handleActionPress: (message: IMessage) => togglePinMessage(message._id, message.pinned) diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 3bd0cbd67d4..600cf0f39d8 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1409,16 +1409,8 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { }; renderItem = (item: TAnyMessageModel, previousItem: TAnyMessageModel, highlightedMessage?: string) => { - const { room, lastOpen, canAutoTranslate } = this.state; - const { - user, - Message_GroupingPeriod, - Message_TimeFormat, - useRealName, - baseUrl, - Message_Read_Receipt_Enabled, - inAppFeedback - } = this.props; + const { room, lastOpen } = this.state; + const { Message_TimeFormat, useRealName, Message_Read_Receipt_Enabled, inAppFeedback } = this.props; let dateSeparator = null; let showUnreadSeparator = false; const federated = 'id' in room && isRoomFederated(room); @@ -1464,43 +1456,19 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { content = ( <Message item={item} - user={user as any} rid={room.rid} archived={'id' in room && room.archived} broadcast={'id' in room && room.broadcast} - status={item.status} isThreadRoom={!!this.tmid} isIgnored={this.isIgnored(item)} previousItem={previousItem} - fetchThreadName={this.fetchThreadName} - onReactionPress={this.onReactionPress} - onReactionLongPress={this.onReactionLongPress} onLongPress={this.onMessageLongPress} - onEncryptedPress={this.onEncryptedPress} - onDiscussionPress={this.onDiscussionPress} - onThreadPress={this.onThreadPress} - onAnswerButtonPress={this.handleSendMessage} - showAttachment={this.showAttachment} - reactionInit={this.onReactionInit} - replyBroadcast={this.replyBroadcast} - errorActionsShow={this.errorActionsShow} - isSystemMessage={room.sysMes as boolean} - baseUrl={baseUrl} - Message_GroupingPeriod={Message_GroupingPeriod} timeFormat={Message_TimeFormat} useRealName={useRealName} isReadReceiptEnabled={Message_Read_Receipt_Enabled && !federated} - autoTranslateRoom={canAutoTranslate && 'id' in room && room.autoTranslate} autoTranslateLanguage={'id' in room ? room.autoTranslateLanguage : undefined} - navToRoomInfo={this.navToRoomInfo} - getCustomEmoji={this.getCustomEmoji} - handleEnterCall={this.handleEnterCall} - blockAction={this.blockAction} threadBadgeColor={this.getBadgeColor(item?.id)} - toggleFollowThread={this.toggleFollowThread} - jumpToMessage={this.jumpToMessageByUrl} highlighted={highlightedMessage === item.id} - closeEmojiAndAction={this.handleCloseEmoji} dateSeparator={dateSeparator} showUnreadSeparator={showUnreadSeparator} /> diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index cbfdd0dd610..895f0da68cf 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -286,20 +286,14 @@ class SearchMessagesView extends Component<ISearchMessagesViewProps, ISearchMess renderItem = ({ item }: { item: IMessageFromServer | TMessageModel }) => { const message = item as TMessageModel; - const { user, baseUrl, useRealName } = this.props; + const { useRealName } = this.props; return ( <Message item={message} - baseUrl={baseUrl} - user={user} timeFormat='MMM Do YYYY, h:mm:ss a' isThreadRoom - showAttachment={this.showAttachment} - getCustomEmoji={this.getCustomEmoji} - navToRoomInfo={this.navToRoomInfo} useRealName={useRealName} onPress={() => this.jumpToMessage({ item })} - jumpToMessage={() => this.jumpToMessage({ item })} rid={message.rid} /> ); From 4ddd704da92df1cc4adb45137530b5794b286499 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 17:54:56 -0300 Subject: [PATCH 032/144] refactor: remove unused message room theme slice (NATIVE-1376) --- app/containers/message/MessageRoomStore.tsx | 7 +------ app/views/MessagesView/index.tsx | 3 +-- app/views/RoomView/index.tsx | 3 +-- app/views/SearchMessagesView/index.tsx | 3 +-- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/app/containers/message/MessageRoomStore.tsx b/app/containers/message/MessageRoomStore.tsx index 644fcbc194d..ac83c9ac92e 100644 --- a/app/containers/message/MessageRoomStore.tsx +++ b/app/containers/message/MessageRoomStore.tsx @@ -5,7 +5,6 @@ import { useShallow } from 'zustand/react/shallow'; import { type TGetCustomEmoji } from '../../definitions/IEmoji'; import { type IAttachment, type TAnyMessageModel } from '../../definitions'; import { type IRoomInfoParam } from '../../views/SearchMessagesView'; -import { type TSupportedThemes } from '../../theme'; export type MessageRoomState = { // stable handlers @@ -39,7 +38,6 @@ export type MessageRoomState = { // reactive tail (provider keeps current) autoTranslateRoom?: boolean; autoTranslateLanguage?: string; - theme?: TSupportedThemes; }; // The room-scoped fields this store owns. Tests and stories use pickMessageRoomState to @@ -72,8 +70,7 @@ const ROOM_STATE_KEYS: (keyof MessageRoomState)[] = [ 'isThreadRoom', 'Message_GroupingPeriod', 'autoTranslateRoom', - 'autoTranslateLanguage', - 'theme' + 'autoTranslateLanguage' ]; export const pickMessageRoomState = (source: Record<string, any> = {}): Partial<MessageRoomState> => { @@ -145,8 +142,6 @@ export const useBroadcast = (): boolean | undefined => useMessageRoomStore(s => export const useIsThreadRoom = (): boolean | undefined => useMessageRoomStore(s => s.isThreadRoom); export const useMessageGroupingPeriod = (): number | undefined => useMessageRoomStore(s => s.Message_GroupingPeriod); -export const useMessageTheme = (): TSupportedThemes | undefined => useMessageRoomStore(s => s.theme); - export const useAutoTranslate = (): { autoTranslateRoom?: boolean; autoTranslateLanguage?: string } => useMessageRoomStore( useShallow(s => ({ autoTranslateRoom: s.autoTranslateRoom, autoTranslateLanguage: s.autoTranslateLanguage })) diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index 5b780be36b1..fad2361788e 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -363,8 +363,7 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { showAttachment={this.showAttachment} user={user} baseUrl={baseUrl} - isThreadRoom - theme={theme}> + isThreadRoom> <FlatList data={messages} renderItem={this.renderItem} diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 600cf0f39d8..1a22b6c77c8 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1693,8 +1693,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { isThreadRoom={!!this.tmid} Message_GroupingPeriod={Message_GroupingPeriod} autoTranslateRoom={canAutoTranslate && 'id' in room && room.autoTranslate} - autoTranslateLanguage={'id' in room ? room.autoTranslateLanguage : undefined} - theme={theme}> + autoTranslateLanguage={'id' in room ? room.autoTranslateLanguage : undefined}> <List ref={this.list} listRef={this.flatList} diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index 895f0da68cf..75faef4edd7 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -314,8 +314,7 @@ class SearchMessagesView extends Component<ISearchMessagesViewProps, ISearchMess showAttachment={this.showAttachment} user={user} baseUrl={baseUrl} - isThreadRoom - theme={theme}> + isThreadRoom> <FlatList data={messages} renderItem={this.renderItem} From a3fe56f03458a4171bb53ec5a8a6556f26d728eb Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 17:55:03 -0300 Subject: [PATCH 033/144] refactor: default getCustomEmoji consistently in message components (NATIVE-1376) --- app/containers/message/MessageAvatar.tsx | 2 +- app/containers/message/Reactions.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/containers/message/MessageAvatar.tsx b/app/containers/message/MessageAvatar.tsx index 4a29c4d5d94..8dc8ad71a0d 100644 --- a/app/containers/message/MessageAvatar.tsx +++ b/app/containers/message/MessageAvatar.tsx @@ -22,7 +22,7 @@ const MessageAvatar = ({ small }: IMessageAvatar) => { const user = useMessageUser(); const navToRoomInfo = useNavToRoomInfo(); - const getCustomEmoji = useGetCustomEmoji(); + const getCustomEmoji = useGetCustomEmoji() ?? (() => null); const { fontScaleLimited } = useResponsiveLayout(); const { avatar, emoji } = useAvatar(); const { u: author } = useMessageAuthor(); diff --git a/app/containers/message/Reactions.tsx b/app/containers/message/Reactions.tsx index fbb5d1c7866..13b87b2a057 100644 --- a/app/containers/message/Reactions.tsx +++ b/app/containers/message/Reactions.tsx @@ -64,7 +64,7 @@ const Reaction = ({ reaction, theme }: IMessageReaction) => { const onReactionPress = useOnReactionPress(); const onReactionLongPress = useOnReactionLongPress(); const user = useMessageUser(); - const getCustomEmoji = useGetCustomEmoji(); + const getCustomEmoji = useGetCustomEmoji() ?? (() => null); const { fontScale } = useWindowDimensions(); const height = 28 * fontScale; const reacted = reaction.usernames.findIndex((item: string) => item === user?.username) !== -1; From be3b34a529883f7c397a609edbe8e83db97c5749 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 1 Jul 2026 18:01:17 -0300 Subject: [PATCH 034/144] fix: provide message context to UIKit modal image blocks (NATIVE-1376) The old deleted MessageContext returned {} outside a provider (graceful degrade). The zustand room/message stores throw instead, so a UIKit modal with a top-level image block crashed at useMessageUser/useMessageId/ useMessageLongPress. Fix self-provides a MessageRoomProvider and an empty MessageProvider at ModalBlockView. --- app/containers/UIKit/UiKitModal.stories.tsx | 96 ++-- .../__snapshots__/UiKitModal.test.tsx.snap | 462 ++++++++++++++++++ app/views/ModalBlockView.tsx | 45 +- 3 files changed, 544 insertions(+), 59 deletions(-) diff --git a/app/containers/UIKit/UiKitModal.stories.tsx b/app/containers/UIKit/UiKitModal.stories.tsx index 8f571fb14d4..848ea0f17cf 100644 --- a/app/containers/UIKit/UiKitModal.stories.tsx +++ b/app/containers/UIKit/UiKitModal.stories.tsx @@ -3,10 +3,21 @@ import { Button, View } from 'react-native'; import { UiKitComponent, UiKitModal } from '.'; import { KitContext, defaultContext } from './utils'; +import { MessageRoomProvider } from '../message/MessageRoomStore'; +import { MessageProvider } from '../message/MessageStore'; +import { type TAnyMessageModel } from '../../definitions'; export default { title: 'UIKit/UiKitModal', - decorators: [(Story: any) => <Story />] + decorators: [ + (Story: any) => ( + <MessageRoomProvider user={{ id: 'u1', username: 'user', token: 't' }} baseUrl='https://open.rocket.chat'> + <MessageProvider item={{} as TAnyMessageModel}> + <Story /> + </MessageProvider> + </MessageRoomProvider> + ) + ] }; export const ModalSectionSelects = () => @@ -273,48 +284,47 @@ export const ModalFormTextArea = () => ]); ModalFormTextArea.storyName = 'Modal - Form TextArea'; -// FIXME: Commented out because it's breaking jest snapshots -// export const ModalImages = () => -// UiKitModal([ -// { -// type: 'image', -// title: { -// type: 'plain_text', -// text: 'Example Image', -// emoji: true -// }, -// imageUrl: 'https://raw.githubusercontent.com/RocketChat/Rocket.Chat.Artwork/master/Logos/icon-circle-256.png', -// alt_text: 'Example Image' -// }, -// { -// type: 'section', -// text: { -// type: 'mrkdwn', -// text: 'How could be the life in Mars?' -// } -// }, -// { -// type: 'context', -// elements: [ -// { -// type: 'image', -// imageUrl: 'https://raw.githubusercontent.com/RocketChat/Rocket.Chat.Artwork/master/Logos/icon-circle-256.png' -// }, -// { -// type: 'mrkdwn', -// text: 'November 25, 2019' -// } -// ] -// }, -// { -// type: 'section', -// text: { -// type: 'mrkdwn', -// text: '*Next stop, Mars!*\nMussum Ipsum, cacilds vidis litro abertis. Admodum accumsan disputationi eu sit. Vide electram sadipscing et per. Diuretics paradis num copo é motivis de denguis. Mais vale um bebadis conhecidiss, que um alcoolatra anonimis. Aenean aliquam molestie leo, vitae iaculis nisl.' -// } -// } -// ]); -// ModalImages.storyName = 'Modal - Images'; +export const ModalImages = () => + UiKitModal([ + { + type: 'image', + title: { + type: 'plain_text', + text: 'Example Image', + emoji: true + }, + imageUrl: 'https://raw.githubusercontent.com/RocketChat/Rocket.Chat.Artwork/master/Logos/icon-circle-256.png', + alt_text: 'Example Image' + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: 'How could be the life in Mars?' + } + }, + { + type: 'context', + elements: [ + { + type: 'image', + imageUrl: 'https://raw.githubusercontent.com/RocketChat/Rocket.Chat.Artwork/master/Logos/icon-circle-256.png' + }, + { + type: 'mrkdwn', + text: 'November 25, 2019' + } + ] + }, + { + type: 'section', + text: { + type: 'mrkdwn', + text: '*Next stop, Mars!*\nMussum Ipsum, cacilds vidis litro abertis. Admodum accumsan disputationi eu sit. Vide electram sadipscing et per. Diuretics paradis num copo é motivis de denguis. Mais vale um bebadis conhecidiss, que um alcoolatra anonimis. Aenean aliquam molestie leo, vitae iaculis nisl.' + } + } + ]); +ModalImages.storyName = 'Modal - Images'; export const ModalActions = () => UiKitModal([ diff --git a/app/containers/UIKit/__snapshots__/UiKitModal.test.tsx.snap b/app/containers/UIKit/__snapshots__/UiKitModal.test.tsx.snap index b5a459e7869..a098c73ec9f 100644 --- a/app/containers/UIKit/__snapshots__/UiKitModal.test.tsx.snap +++ b/app/containers/UIKit/__snapshots__/UiKitModal.test.tsx.snap @@ -3448,6 +3448,468 @@ exports[`Story Snapshots: ModalFormTextArea should match snapshot 1`] = ` ] `; +exports[`Story Snapshots: ModalImages should match snapshot 1`] = ` +[ + <View + accessibilityLabel="Image without description" + accessibilityRole="imagebutton" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "borderRadius": 4, + "flexDirection": "column", + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 54, + }, + [ + { + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + </View> + </View>, + <View + style={ + [ + { + "marginBottom": 8, + }, + { + "justifyContent": "center", + }, + ] + } + > + <View + style={ + { + "flex": 1, + "padding": 4, + } + } + > + <View + style={ + { + "gap": 2, + } + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + <Text + style={ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + } + } + > + <Text + accessibilityLabel="How could be the life in Mars?" + style={ + [ + { + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + { + "fontSize": 14, + }, + ] + } + > + How could be the life in Mars? + </Text> + </Text> + </Text> + </View> + </View> + </View>, + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + "minHeight": 36, + } + } + > + <View + style={ + { + "marginRight": 8, + } + } + > + <ViewManagerAdapter_ExpoImage + borderRadius={2} + containerViewRef={{"current":"Object"}} + contentFit="cover" + contentPosition={ + { + "left": "50%", + "top": "50%", + } + } + height={20} + nativeViewRef={{"current":"NativeComponent"}} + onError={[Function]} + onLoad={[Function]} + onLoadStart={[Function]} + onProgress={[Function]} + placeholder={[]} + source={ + [ + { + "uri": "https://raw.githubusercontent.com/RocketChat/Rocket.Chat.Artwork/master/Logos/icon-circle-256.png", + }, + ] + } + style={ + { + "borderRadius": 2, + "height": 20, + "width": 20, + } + } + transition={null} + width={20} + /> + </View> + <Text + accessibilityLabel="November 25, 2019" + numberOfLines={0} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#2F343D", + "lineHeight": undefined, + }, + ] + } + testID="markdown-preview-November 25, 2019" + > + November 25, 2019 + </Text> + </View>, + <View + style={ + [ + { + "marginBottom": 8, + }, + { + "justifyContent": "center", + }, + ] + } + > + <View + style={ + { + "flex": 1, + "padding": 4, + } + } + > + <View + style={ + { + "gap": 2, + } + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + <Text + style={ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + } + } + > + <Text + style={ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontWeight": "700", + "textAlign": "left", + } + } + > + <Text + accessibilityLabel="Next stop, Mars!" + style={ + [ + { + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + { + "fontSize": 14, + }, + ] + } + > + Next stop, Mars! + </Text> + </Text> + </Text> + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + <Text + style={ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + } + } + > + <Text + accessibilityLabel="Mussum Ipsum, cacilds vidis litro abertis. Admodum accumsan disputationi eu sit. Vide electram sadipscing et per. Diuretics paradis num copo é motivis de denguis. Mais vale um bebadis conhecidiss, que um alcoolatra anonimis. Aenean aliquam molestie leo, vitae iaculis nisl." + style={ + [ + { + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + { + "fontSize": 14, + }, + ] + } + > + Mussum Ipsum, cacilds vidis litro abertis. Admodum accumsan disputationi eu sit. Vide electram sadipscing et per. Diuretics paradis num copo é motivis de denguis. Mais vale um bebadis conhecidiss, que um alcoolatra anonimis. Aenean aliquam molestie leo, vitae iaculis nisl. + </Text> + </Text> + </Text> + </View> + </View> + </View>, +] +`; + exports[`Story Snapshots: ModalInputWithAddField should match snapshot 1`] = ` <View> <View diff --git a/app/views/ModalBlockView.tsx b/app/views/ModalBlockView.tsx index 3e4faf3ae66..3eb791e8517 100644 --- a/app/views/ModalBlockView.tsx +++ b/app/views/ModalBlockView.tsx @@ -14,8 +14,11 @@ import Navigation from '../lib/navigation/appNavigation'; import { type MasterDetailInsideStackParamList } from '../stacks/MasterDetailStack/types'; import { ContainerTypes, ModalActions, type TModalAction } from '../containers/UIKit/interfaces'; import { triggerBlockAction, triggerCancel, triggerSubmitView } from '../lib/methods/triggerActions'; -import { type IApplicationState } from '../definitions'; +import { type IApplicationState, type TAnyMessageModel } from '../definitions'; import KeyboardView from '../containers/KeyboardView'; +import { MessageRoomProvider } from '../containers/message/MessageRoomStore'; +import { MessageProvider } from '../containers/message/MessageStore'; +import { getUserSelector } from '../selectors/login'; const styles = StyleSheet.create({ content: { @@ -51,10 +54,8 @@ interface IModalBlockViewProps { route: RouteProp<MasterDetailInsideStackParamList, 'ModalBlockView'>; theme: TSupportedThemes; language: string; - user: { - id: string; - token: string; - }; + user: ReturnType<typeof getUserSelector>; + baseUrl: string; } // eslint-disable-next-line no-sequences @@ -93,6 +94,12 @@ const LoadingIndicator = ({ loading }: { loading: boolean }) => { return null; }; +// A UIKit modal is not a message, but the shared media components it can render +// (ImageContainer -> Button -> Touchable) assume a per-message context. Provide an +// empty one: no long-press target, no id-scoped cache. Images still load via the +// room provider's user/baseUrl. +const EMPTY_MESSAGE = {} as TAnyMessageModel; + class ModalBlockView extends Component<IModalBlockViewProps, IModalBlockViewState> { private submitting: boolean; @@ -252,7 +259,7 @@ class ModalBlockView extends Component<IModalBlockViewProps, IModalBlockViewStat render() { const { data, loading, errors } = this.state; - const { language } = this.props; + const { language, user, baseUrl } = this.props; const { values } = this; const { view } = data; const { blocks } = view; @@ -265,15 +272,19 @@ class ModalBlockView extends Component<IModalBlockViewProps, IModalBlockViewStat <KeyboardView> <ScrollView style={styles.content}> <Fragment key={modalKey}> - <ModalBlockWithContext - action={this.action} - state={this.changeState} - {...data} - blocks={blocks} - errors={errors} - language={language} - values={values} - /> + <MessageRoomProvider user={user} baseUrl={baseUrl}> + <MessageProvider item={EMPTY_MESSAGE}> + <ModalBlockWithContext + action={this.action} + state={this.changeState} + {...data} + blocks={blocks} + errors={errors} + language={language} + values={values} + /> + </MessageProvider> + </MessageRoomProvider> </Fragment> </ScrollView> <LoadingIndicator loading={loading} /> @@ -283,7 +294,9 @@ class ModalBlockView extends Component<IModalBlockViewProps, IModalBlockViewStat } const mapStateToProps = (state: IApplicationState) => ({ - language: state.login.user.language as string + language: state.login.user.language as string, + baseUrl: state.server.server, + user: getUserSelector(state) }); export default connect(mapStateToProps)(ModalBlockView); From 4708c13ff1e2dae233f97b318f8ec03b0f4a2329 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 16:03:54 -0300 Subject: [PATCH 035/144] fix: seed rid in message providers for the files and search views (NATIVE-1385) --- app/views/MessagesView/index.tsx | 1 + app/views/SearchMessagesView/index.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index fad2361788e..5956e701f82 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -363,6 +363,7 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { showAttachment={this.showAttachment} user={user} baseUrl={baseUrl} + rid={this.rid} isThreadRoom> <FlatList data={messages} diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index 75faef4edd7..3aae7898f1b 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -314,6 +314,7 @@ class SearchMessagesView extends Component<ISearchMessagesViewProps, ISearchMess showAttachment={this.showAttachment} user={user} baseUrl={baseUrl} + rid={this.rid} isThreadRoom> <FlatList data={messages} From 304b241ca9797f2ad7e00dcfdf98ffc1cd524677 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 16:06:01 -0300 Subject: [PATCH 036/144] refactor: read rid from the room store in the blocks leaf (NATIVE-1385) --- app/containers/message/Blocks.ts | 8 ++++---- app/containers/message/Message.tsx | 6 +----- app/containers/message/Preview.tsx | 2 +- app/containers/message/index.tsx | 2 -- app/containers/message/interfaces.ts | 4 ---- app/views/MessagesView/index.tsx | 3 +-- app/views/RoomView/index.tsx | 1 - app/views/SearchMessagesView/index.tsx | 1 - 8 files changed, 7 insertions(+), 20 deletions(-) diff --git a/app/containers/message/Blocks.ts b/app/containers/message/Blocks.ts index de2eb64bce9..8ebf648ee39 100644 --- a/app/containers/message/Blocks.ts +++ b/app/containers/message/Blocks.ts @@ -1,13 +1,13 @@ import { createElement } from 'react'; import { messageBlockWithContext } from '../UIKit/MessageBlock'; -import { type IMessageBlocks } from './interfaces'; -import { useBlockAction } from './MessageRoomStore'; +import { useBlockAction, useRid } from './MessageRoomStore'; import { useBlocks } from './MessageStore'; -const Blocks = ({ rid }: IMessageBlocks) => { +const Blocks = () => { 'use memo'; + const rid = useRid(); const blockAction = useBlockAction(); const { blocks, id: mid } = useBlocks(); @@ -22,7 +22,7 @@ const Blocks = ({ rid }: IMessageBlocks) => { appId, value, blockId, - rid, + rid: rid ?? '', mid }); } diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index 83304a2ec29..a03dd795f23 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -45,7 +45,6 @@ import { } from './MessageStore'; type TMessageProps = { - rid: string; timeFormat?: string; archived?: boolean; broadcast?: boolean; @@ -130,7 +129,7 @@ const MessageInner = (props: TMessageProps) => { content = ( <> <User useRealName={props.useRealName} timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> - <Blocks rid={props.rid} /> + <Blocks /> <Thread isThreadRoom={props.isThreadRoom} /> <Reactions /> {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} @@ -226,7 +225,6 @@ const Message = (props: TMessageProps & IMessageA11y) => { <MessageAvatar /> <View style={styles.messageContent}> <MessageInner - rid={props.rid} timeFormat={props.timeFormat} archived={props.archived} broadcast={props.broadcast} @@ -297,7 +295,6 @@ const MessageTouchable = (props: TMessageProps) => { return ( <A11y.Order> <Message - rid={props.rid} timeFormat={props.timeFormat} archived={props.archived} broadcast={props.broadcast} @@ -339,7 +336,6 @@ const MessageTouchable = (props: TMessageProps) => { if (e.nativeEvent.actionName === 'showActions') handleLongPress(); }}> <Message - rid={props.rid} timeFormat={props.timeFormat} archived={props.archived} broadcast={props.broadcast} diff --git a/app/containers/message/Preview.tsx b/app/containers/message/Preview.tsx index 8289091e839..18a159e56cd 100644 --- a/app/containers/message/Preview.tsx +++ b/app/containers/message/Preview.tsx @@ -21,7 +21,7 @@ const MessagePreview = ({ message }: { message: TAnyMessageModel }) => { }; return ( <MessageRoomProvider user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} rid={message.rid}> - <Message item={message} rid={message.rid} timeFormat={Message_TimeFormat} useRealName={useRealName} isPreview /> + <Message item={message} timeFormat={Message_TimeFormat} useRealName={useRealName} isPreview /> </MessageRoomProvider> ); }; diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index f92ebe6fb04..3fb578c6f08 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -7,7 +7,6 @@ import { MessageProvider } from './MessageStore'; interface IMessageContainerProps { item: TAnyMessageModel; - rid: string; timeFormat?: string; archived?: boolean; broadcast?: boolean; @@ -38,7 +37,6 @@ const MessageContainer = (props: IMessageContainerProps) => { onLongPress={props.onLongPress} threadBadgeColor={props.threadBadgeColor}> <Message - rid={props.rid} timeFormat={props.timeFormat} archived={props.archived ?? false} broadcast={props.broadcast ?? false} diff --git a/app/containers/message/interfaces.ts b/app/containers/message/interfaces.ts index d1cb55061c0..4c2ded0ac1a 100644 --- a/app/containers/message/interfaces.ts +++ b/app/containers/message/interfaces.ts @@ -14,10 +14,6 @@ export interface IMessageAvatar { small?: boolean; } -export interface IMessageBlocks { - rid: string; -} - export interface IMessageBroadcast { broadcast?: boolean; } diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index 5956e701f82..2598542fa2a 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -168,8 +168,7 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { timeFormat: 'MMM Do YYYY, h:mm:ss a', isThreadRoom: true, useRealName, - onPress: () => this.jumpToMessage({ item }), - rid: this.rid + onPress: () => this.jumpToMessage({ item }) }); return { diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 1a22b6c77c8..feef5f49099 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1456,7 +1456,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { content = ( <Message item={item} - rid={room.rid} archived={'id' in room && room.archived} broadcast={'id' in room && room.broadcast} isThreadRoom={!!this.tmid} diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index 3aae7898f1b..60940ab5049 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -294,7 +294,6 @@ class SearchMessagesView extends Component<ISearchMessagesViewProps, ISearchMess isThreadRoom useRealName={useRealName} onPress={() => this.jumpToMessage({ item })} - rid={message.rid} /> ); }; From d3c448d72512f69cba25e06dbb0c30931e6cb944 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 16:07:53 -0300 Subject: [PATCH 037/144] refactor: read broadcast from the room store in the broadcast leaf (NATIVE-1385) --- app/containers/message/Broadcast.tsx | 6 +++--- app/containers/message/Message.tsx | 6 +----- app/containers/message/index.tsx | 2 -- app/containers/message/interfaces.ts | 4 ---- app/views/RoomView/index.tsx | 1 - 5 files changed, 4 insertions(+), 15 deletions(-) diff --git a/app/containers/message/Broadcast.tsx b/app/containers/message/Broadcast.tsx index 8689f01132c..3a05ffbc478 100644 --- a/app/containers/message/Broadcast.tsx +++ b/app/containers/message/Broadcast.tsx @@ -6,18 +6,18 @@ import styles from './styles'; import { BUTTON_HIT_SLOP } from './utils'; import I18n from '../../i18n'; import { themes } from '../../lib/constants/colors'; -import { type IMessageBroadcast } from './interfaces'; import { useTheme } from '../../theme'; import { useMessageAuthor, useMessageCtx } from './MessageStore'; -import { useMessageUser, useReplyBroadcast } from './MessageRoomStore'; +import { useBroadcast, useMessageUser, useReplyBroadcast } from './MessageRoomStore'; // TODO: Create a reusable button component for message -const Broadcast = ({ broadcast }: IMessageBroadcast) => { +const Broadcast = () => { 'use memo'; const { item } = useMessageCtx(); const user = useMessageUser(); const replyBroadcast = useReplyBroadcast(); + const broadcast = useBroadcast(); const { theme } = useTheme(); const { u: author } = useMessageAuthor(); const isOwn = author?._id === user?.id; diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index a03dd795f23..bc194ef933f 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -47,7 +47,6 @@ import { type TMessageProps = { timeFormat?: string; archived?: boolean; - broadcast?: boolean; useRealName?: boolean; isReadReceiptEnabled?: boolean; isThreadRoom: boolean; @@ -155,7 +154,7 @@ const MessageInner = (props: TMessageProps) => { <Urls /> <Thread isThreadRoom={props.isThreadRoom} /> <Reactions /> - <Broadcast broadcast={props.broadcast} /> + <Broadcast /> </View> </> ); @@ -227,7 +226,6 @@ const Message = (props: TMessageProps & IMessageA11y) => { <MessageInner timeFormat={props.timeFormat} archived={props.archived} - broadcast={props.broadcast} useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} isThreadRoom={props.isThreadRoom} @@ -297,7 +295,6 @@ const MessageTouchable = (props: TMessageProps) => { <Message timeFormat={props.timeFormat} archived={props.archived} - broadcast={props.broadcast} useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} isThreadRoom={props.isThreadRoom} @@ -338,7 +335,6 @@ const MessageTouchable = (props: TMessageProps) => { <Message timeFormat={props.timeFormat} archived={props.archived} - broadcast={props.broadcast} useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} isThreadRoom={props.isThreadRoom} diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 3fb578c6f08..88b554a3703 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -9,7 +9,6 @@ interface IMessageContainerProps { item: TAnyMessageModel; timeFormat?: string; archived?: boolean; - broadcast?: boolean; previousItem?: TAnyMessageModel; isReadReceiptEnabled?: boolean; isThreadRoom?: boolean; @@ -39,7 +38,6 @@ const MessageContainer = (props: IMessageContainerProps) => { <Message timeFormat={props.timeFormat} archived={props.archived ?? false} - broadcast={props.broadcast ?? false} useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} isThreadRoom={!!props.isThreadRoom} diff --git a/app/containers/message/interfaces.ts b/app/containers/message/interfaces.ts index 4c2ded0ac1a..6fa35943356 100644 --- a/app/containers/message/interfaces.ts +++ b/app/containers/message/interfaces.ts @@ -14,10 +14,6 @@ export interface IMessageAvatar { small?: boolean; } -export interface IMessageBroadcast { - broadcast?: boolean; -} - export interface IMessageContent { tmid?: string; isThreadRoom: boolean; diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index feef5f49099..0fd88bd1f44 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1457,7 +1457,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { <Message item={item} archived={'id' in room && room.archived} - broadcast={'id' in room && room.broadcast} isThreadRoom={!!this.tmid} isIgnored={this.isIgnored(item)} previousItem={previousItem} From dda147c8e8a1ae20a5238ad498940212a44bc643 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 16:09:30 -0300 Subject: [PATCH 038/144] refactor: read archived from the room store in the message touchable (NATIVE-1385) --- app/containers/message/Message.tsx | 8 +++----- app/containers/message/index.tsx | 2 -- app/views/RoomView/index.tsx | 1 - 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index bc194ef933f..515abdf5616 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -27,6 +27,7 @@ import { useMessageAccessibilityLabel } from './hooks/useMessageAccessibilityLab import { useMessageAccessibilityActions } from './hooks/useMessageAccessibilityActions'; import { useMessageAccessibilityHint } from './hooks/useMessageAccessibilityHint'; import { useIsBeingEdited } from '../../views/RoomView/InteractionStore'; +import { useArchived } from './MessageRoomStore'; import { useBlocks, useContentData, @@ -46,7 +47,6 @@ import { type TMessageProps = { timeFormat?: string; - archived?: boolean; useRealName?: boolean; isReadReceiptEnabled?: boolean; isThreadRoom: boolean; @@ -225,7 +225,6 @@ const Message = (props: TMessageProps & IMessageA11y) => { <View style={styles.messageContent}> <MessageInner timeFormat={props.timeFormat} - archived={props.archived} useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} isThreadRoom={props.isThreadRoom} @@ -263,6 +262,7 @@ const MessageTouchable = (props: TMessageProps) => { const { ref: touchRef, markAsLastFocused } = useLastFocusedMessageRef(); const { isThreadReply } = useThreadPosition(); const isInfo = useIsInfo(); + const archived = useArchived(); const { hasError, isTemp } = useMessageStatus(); const type = useMessageField(item => item.t); const id = useMessageField(item => item.id); @@ -277,7 +277,7 @@ const MessageTouchable = (props: TMessageProps) => { isReadReceiptEnabled: props.isReadReceiptEnabled, autoTranslateLanguage: props.autoTranslateLanguage }); - const isDisabled = (isInfo && !isThreadReply) || props.archived || isTemp || type === 'jitsi_call_started'; + const isDisabled = (isInfo && !isThreadReply) || archived || isTemp || type === 'jitsi_call_started'; const accessibilityActions = useMessageAccessibilityActions(isDisabled); const accessibilityHint = useMessageAccessibilityHint(); @@ -294,7 +294,6 @@ const MessageTouchable = (props: TMessageProps) => { <A11y.Order> <Message timeFormat={props.timeFormat} - archived={props.archived} useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} isThreadRoom={props.isThreadRoom} @@ -334,7 +333,6 @@ const MessageTouchable = (props: TMessageProps) => { }}> <Message timeFormat={props.timeFormat} - archived={props.archived} useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} isThreadRoom={props.isThreadRoom} diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 88b554a3703..45999dea625 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -8,7 +8,6 @@ import { MessageProvider } from './MessageStore'; interface IMessageContainerProps { item: TAnyMessageModel; timeFormat?: string; - archived?: boolean; previousItem?: TAnyMessageModel; isReadReceiptEnabled?: boolean; isThreadRoom?: boolean; @@ -37,7 +36,6 @@ const MessageContainer = (props: IMessageContainerProps) => { threadBadgeColor={props.threadBadgeColor}> <Message timeFormat={props.timeFormat} - archived={props.archived ?? false} useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} isThreadRoom={!!props.isThreadRoom} diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 0fd88bd1f44..ec8fe6e4c35 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1456,7 +1456,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { content = ( <Message item={item} - archived={'id' in room && room.archived} isThreadRoom={!!this.tmid} isIgnored={this.isIgnored(item)} previousItem={previousItem} From 80bda4ede96546970066b8481ae8afd975c52ce7 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 16:12:44 -0300 Subject: [PATCH 039/144] refactor: read isThreadRoom from the room store in content and thread (NATIVE-1385) --- app/containers/message/Content.test.tsx | 1 - app/containers/message/Content.tsx | 5 +++-- app/containers/message/Message.tsx | 12 ++---------- app/containers/message/Thread.test.tsx | 10 ++++------ app/containers/message/Thread.tsx | 6 +++--- app/containers/message/index.tsx | 2 -- app/containers/message/interfaces.ts | 5 ----- app/views/MessagesView/index.tsx | 1 - app/views/RoomView/index.tsx | 1 - app/views/SearchMessagesView/index.tsx | 1 - 10 files changed, 12 insertions(+), 32 deletions(-) diff --git a/app/containers/message/Content.test.tsx b/app/containers/message/Content.test.tsx index 02d1780f531..5ab0f791f36 100644 --- a/app/containers/message/Content.test.tsx +++ b/app/containers/message/Content.test.tsx @@ -37,7 +37,6 @@ const baseProps: TOverrides = { _id: 'msg-1', isTemp: false, isInfo: false, - isThreadRoom: false, isEdited: false, isEncrypted: false, isIgnored: false, diff --git a/app/containers/message/Content.tsx b/app/containers/message/Content.tsx index 3567fa6e57c..c8bc6b164e2 100644 --- a/app/containers/message/Content.tsx +++ b/app/containers/message/Content.tsx @@ -18,7 +18,7 @@ import { useMessageText, useOnLinkPress } from './MessageStore'; -import { useGetCustomEmoji, useMessageUser, useNavToRoomInfo } from './MessageRoomStore'; +import { useGetCustomEmoji, useIsThreadRoom, useMessageUser, useNavToRoomInfo } from './MessageRoomStore'; const Content = (props: IMessageContent) => { 'use memo'; @@ -28,6 +28,7 @@ const Content = (props: IMessageContent) => { const onLinkPress = useOnLinkPress(); const getCustomEmoji = useGetCustomEmoji() ?? (() => null); const navToRoomInfo = useNavToRoomInfo(); + const isThreadRoom = useIsThreadRoom(); const isInfo = useIsInfo(); const isEncrypted = useIsEncrypted(); @@ -56,7 +57,7 @@ const Content = (props: IMessageContent) => { return renderMessageContent; } - const isPreview = props.tmid && !props.isThreadRoom; + const isPreview = props.tmid && !isThreadRoom; let content = null; if (isEncrypted) { diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index 515abdf5616..ee939444625 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -49,7 +49,6 @@ type TMessageProps = { timeFormat?: string; useRealName?: boolean; isReadReceiptEnabled?: boolean; - isThreadRoom: boolean; isPreview?: boolean; highlighted?: boolean; isIgnored: boolean; @@ -85,7 +84,6 @@ const MessageInner = (props: TMessageProps) => { <Quote attachments={attachments} timeFormat={props.timeFormat} author={author} /> <Content tmid={tmid} - isThreadRoom={props.isThreadRoom} useRealName={props.useRealName} isIgnored={props.isIgnored} autoTranslateLanguage={props.autoTranslateLanguage} @@ -113,7 +111,6 @@ const MessageInner = (props: TMessageProps) => { <User useRealName={props.useRealName} timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> <Content tmid={tmid} - isThreadRoom={props.isThreadRoom} useRealName={props.useRealName} isIgnored={props.isIgnored} autoTranslateLanguage={props.autoTranslateLanguage} @@ -129,7 +126,7 @@ const MessageInner = (props: TMessageProps) => { <> <User useRealName={props.useRealName} timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> <Blocks /> - <Thread isThreadRoom={props.isThreadRoom} /> + <Thread /> <Reactions /> {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} </> @@ -145,14 +142,13 @@ const MessageInner = (props: TMessageProps) => { <Quote attachments={attachments} timeFormat={props.timeFormat} author={author} /> <Content tmid={tmid} - isThreadRoom={props.isThreadRoom} useRealName={props.useRealName} isIgnored={props.isIgnored} autoTranslateLanguage={props.autoTranslateLanguage} /> <Attachments attachments={attachments} timeFormat={props.timeFormat} author={author} /> <Urls /> - <Thread isThreadRoom={props.isThreadRoom} /> + <Thread /> <Reactions /> <Broadcast /> </View> @@ -196,7 +192,6 @@ const Message = (props: TMessageProps & IMessageA11y) => { <View style={styles.messageContent}> <Content tmid={tmid} - isThreadRoom={props.isThreadRoom} useRealName={props.useRealName} isIgnored={props.isIgnored} autoTranslateLanguage={props.autoTranslateLanguage} @@ -227,7 +222,6 @@ const Message = (props: TMessageProps & IMessageA11y) => { timeFormat={props.timeFormat} useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} - isThreadRoom={props.isThreadRoom} isPreview={props.isPreview} highlighted={props.highlighted} isIgnored={props.isIgnored} @@ -296,7 +290,6 @@ const MessageTouchable = (props: TMessageProps) => { timeFormat={props.timeFormat} useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} - isThreadRoom={props.isThreadRoom} isPreview={props.isPreview} highlighted={props.highlighted} isIgnored={isIgnored} @@ -335,7 +328,6 @@ const MessageTouchable = (props: TMessageProps) => { timeFormat={props.timeFormat} useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} - isThreadRoom={props.isThreadRoom} isPreview={props.isPreview} highlighted={props.highlighted} isIgnored={isIgnored} diff --git a/app/containers/message/Thread.test.tsx b/app/containers/message/Thread.test.tsx index 04946302357..590e8f193a2 100644 --- a/app/containers/message/Thread.test.tsx +++ b/app/containers/message/Thread.test.tsx @@ -16,11 +16,11 @@ const baseContextValue = { const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel => ({ id: 'msg1', msg: 'hello', tcount: 3, tlm: undefined, tmid: undefined, ...overrides } as unknown as TAnyMessageModel); -const renderThread = (item: TAnyMessageModel, props: Parameters<typeof Thread>[0]) => +const renderThread = (item: TAnyMessageModel) => render( <MessageRoomProvider {...pickMessageRoomState(baseContextValue)}> <MessageProvider item={item}> - <Thread {...props} /> + <Thread /> </MessageProvider> </MessageRoomProvider> ); @@ -28,9 +28,7 @@ const renderThread = (item: TAnyMessageModel, props: Parameters<typeof Thread>[0 describe('Thread — tlm-only update regression', () => { test('renders null when tlm is undefined, then shows button after tlm arrives (same tcount)', () => { const item = buildItem(); - const { queryByTestId, rerender, getByTestId } = renderThread(item, { - isThreadRoom: false - }); + const { queryByTestId, rerender, getByTestId } = renderThread(item); expect(queryByTestId('message-thread-button-hello')).toBeNull(); @@ -38,7 +36,7 @@ describe('Thread — tlm-only update regression', () => { rerender( <MessageRoomProvider {...pickMessageRoomState(baseContextValue)}> <MessageProvider item={updatedItem}> - <Thread isThreadRoom={false} /> + <Thread /> </MessageProvider> </MessageRoomProvider> ); diff --git a/app/containers/message/Thread.tsx b/app/containers/message/Thread.tsx index 4c2b43ccea5..7e2921e13ac 100644 --- a/app/containers/message/Thread.tsx +++ b/app/containers/message/Thread.tsx @@ -3,17 +3,17 @@ import { Text, View } from 'react-native'; import styles from './styles'; import ThreadDetails from '../ThreadDetails'; import I18n from '../../i18n'; -import { type IMessageThread } from './interfaces'; import { useTheme } from '../../theme'; import Touchable from './Touchable'; import { useMessageCtx, useMessageText, useReplies, useThreadBadgeColor, useThreadData } from './MessageStore'; -import { useMessageUser, useOnThreadPress, useToggleFollowThread } from './MessageRoomStore'; +import { useIsThreadRoom, useMessageUser, useOnThreadPress, useToggleFollowThread } from './MessageRoomStore'; -const Thread = ({ isThreadRoom }: IMessageThread) => { +const Thread = () => { 'use memo'; const { theme, colors } = useTheme(); const { item } = useMessageCtx(); + const isThreadRoom = useIsThreadRoom(); const threadBadgeColor = useThreadBadgeColor(); const toggleFollowThread = useToggleFollowThread(); const user = useMessageUser(); diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 45999dea625..23afdd30d4b 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -10,7 +10,6 @@ interface IMessageContainerProps { timeFormat?: string; previousItem?: TAnyMessageModel; isReadReceiptEnabled?: boolean; - isThreadRoom?: boolean; useRealName?: boolean; autoTranslateLanguage?: string; isIgnored?: boolean; @@ -38,7 +37,6 @@ const MessageContainer = (props: IMessageContainerProps) => { timeFormat={props.timeFormat} useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} - isThreadRoom={!!props.isThreadRoom} isPreview={props.isPreview} highlighted={props.highlighted} isIgnored={props.isIgnored ?? false} diff --git a/app/containers/message/interfaces.ts b/app/containers/message/interfaces.ts index 6fa35943356..c80c3b375f0 100644 --- a/app/containers/message/interfaces.ts +++ b/app/containers/message/interfaces.ts @@ -16,7 +16,6 @@ export interface IMessageAvatar { export interface IMessageContent { tmid?: string; - isThreadRoom: boolean; useRealName?: boolean; isIgnored: boolean; autoTranslateLanguage?: string; @@ -29,10 +28,6 @@ export interface IMessageEmoji { getCustomEmoji: TGetCustomEmoji; } -export interface IMessageThread { - isThreadRoom: boolean; -} - export interface IMessageRepliedThread { isHeader: boolean; } diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index 2598542fa2a..f09f33cc533 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -166,7 +166,6 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { const renderItemCommonProps = (item: TAnyMessageModel) => ({ item, timeFormat: 'MMM Do YYYY, h:mm:ss a', - isThreadRoom: true, useRealName, onPress: () => this.jumpToMessage({ item }) }); diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index ec8fe6e4c35..ad9b6747db1 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1456,7 +1456,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { content = ( <Message item={item} - isThreadRoom={!!this.tmid} isIgnored={this.isIgnored(item)} previousItem={previousItem} onLongPress={this.onMessageLongPress} diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index 60940ab5049..82303824c9c 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -291,7 +291,6 @@ class SearchMessagesView extends Component<ISearchMessagesViewProps, ISearchMess <Message item={message} timeFormat='MMM Do YYYY, h:mm:ss a' - isThreadRoom useRealName={useRealName} onPress={() => this.jumpToMessage({ item })} /> From 6110f85cb76f6e18ad181170a19af449f9561241 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 16:19:46 -0300 Subject: [PATCH 040/144] refactor: read autoTranslateLanguage from the room store where needed (NATIVE-1385) --- app/containers/message/Content.test.tsx | 6 ++- app/containers/message/Content.tsx | 5 ++- app/containers/message/Message.tsx | 42 ++++--------------- .../hooks/useMessageAccessibilityLabel.ts | 6 +-- app/containers/message/index.tsx | 2 - app/containers/message/interfaces.ts | 1 - app/views/RoomView/index.tsx | 1 - 7 files changed, 19 insertions(+), 44 deletions(-) diff --git a/app/containers/message/Content.test.tsx b/app/containers/message/Content.test.tsx index 5ab0f791f36..4eed8525e4e 100644 --- a/app/containers/message/Content.test.tsx +++ b/app/containers/message/Content.test.tsx @@ -31,6 +31,7 @@ type TOverrides = Partial<IMessageContent> & { isTranslated?: boolean; isHeader?: boolean; hasError?: boolean; + autoTranslateLanguage?: string; }; const baseProps: TOverrides = { @@ -55,12 +56,13 @@ const buildItem = (msg: string | undefined, attachments: IAttachment[] | undefin } as unknown as TAnyMessageModel); const tree = (overrides: TOverrides) => { - const { msg, attachments, isEncrypted, ...contentProps } = overrides; + const { msg, attachments, isEncrypted, autoTranslateLanguage, ...contentProps } = overrides; const contextValue = { user: { username: 'john' }, onLinkPress: jest.fn(), getCustomEmoji: jest.fn(), - navToRoomInfo: jest.fn() + navToRoomInfo: jest.fn(), + autoTranslateLanguage }; return ( <Provider store={mockedStore}> diff --git a/app/containers/message/Content.tsx b/app/containers/message/Content.tsx index c8bc6b164e2..e20a4132830 100644 --- a/app/containers/message/Content.tsx +++ b/app/containers/message/Content.tsx @@ -18,7 +18,7 @@ import { useMessageText, useOnLinkPress } from './MessageStore'; -import { useGetCustomEmoji, useIsThreadRoom, useMessageUser, useNavToRoomInfo } from './MessageRoomStore'; +import { useAutoTranslate, useGetCustomEmoji, useIsThreadRoom, useMessageUser, useNavToRoomInfo } from './MessageRoomStore'; const Content = (props: IMessageContent) => { 'use memo'; @@ -29,6 +29,7 @@ const Content = (props: IMessageContent) => { const getCustomEmoji = useGetCustomEmoji() ?? (() => null); const navToRoomInfo = useNavToRoomInfo(); const isThreadRoom = useIsThreadRoom(); + const { autoTranslateLanguage } = useAutoTranslate(); const isInfo = useIsInfo(); const isEncrypted = useIsEncrypted(); @@ -72,7 +73,7 @@ const Content = (props: IMessageContent) => { } else if (isPreview) { const previewMsg = messageText || - (attachments?.length ? getPreviewMessageFromAttachment(attachments[0], props.autoTranslateLanguage) : undefined); + (attachments?.length ? getPreviewMessageFromAttachment(attachments[0], autoTranslateLanguage) : undefined); content = previewMsg ? <MarkdownPreview testID={`message-preview-${previewMsg}`} msg={previewMsg} /> : null; } else if (messageText) { content = ( diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index ee939444625..373241ea11f 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -27,7 +27,7 @@ import { useMessageAccessibilityLabel } from './hooks/useMessageAccessibilityLab import { useMessageAccessibilityActions } from './hooks/useMessageAccessibilityActions'; import { useMessageAccessibilityHint } from './hooks/useMessageAccessibilityHint'; import { useIsBeingEdited } from '../../views/RoomView/InteractionStore'; -import { useArchived } from './MessageRoomStore'; +import { useArchived, useAutoTranslate } from './MessageRoomStore'; import { useBlocks, useContentData, @@ -53,7 +53,6 @@ type TMessageProps = { highlighted?: boolean; isIgnored: boolean; isBeingEdited?: boolean; - autoTranslateLanguage?: string; small?: boolean; }; @@ -82,12 +81,7 @@ const MessageInner = (props: TMessageProps) => { {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} <> <Quote attachments={attachments} timeFormat={props.timeFormat} author={author} /> - <Content - tmid={tmid} - useRealName={props.useRealName} - isIgnored={props.isIgnored} - autoTranslateLanguage={props.autoTranslateLanguage} - /> + <Content tmid={tmid} useRealName={props.useRealName} isIgnored={props.isIgnored} /> <Attachments attachments={attachments} timeFormat={props.timeFormat} author={author} /> </> <Urls /> @@ -109,12 +103,7 @@ const MessageInner = (props: TMessageProps) => { content = ( <> <User useRealName={props.useRealName} timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> - <Content - tmid={tmid} - useRealName={props.useRealName} - isIgnored={props.isIgnored} - autoTranslateLanguage={props.autoTranslateLanguage} - /> + <Content tmid={tmid} useRealName={props.useRealName} isIgnored={props.isIgnored} /> <CallButton /> {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} </> @@ -140,12 +129,7 @@ const MessageInner = (props: TMessageProps) => { {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} <View style={{ gap: 4 }}> <Quote attachments={attachments} timeFormat={props.timeFormat} author={author} /> - <Content - tmid={tmid} - useRealName={props.useRealName} - isIgnored={props.isIgnored} - autoTranslateLanguage={props.autoTranslateLanguage} - /> + <Content tmid={tmid} useRealName={props.useRealName} isIgnored={props.isIgnored} /> <Attachments attachments={attachments} timeFormat={props.timeFormat} author={author} /> <Urls /> <Thread /> @@ -174,6 +158,7 @@ const Message = (props: TMessageProps & IMessageA11y) => { const isEdited = useIsEdited(); const { hasError } = useMessageStatus(); const { unread, pinned } = useMessageMeta(); + const { autoTranslateLanguage } = useAutoTranslate(); if (isThreadReply || isThreadSequential || isInfo || props.isIgnored) { const thread = isThreadReply ? <RepliedThread isHeader={isHeader} /> : null; @@ -186,16 +171,11 @@ const Message = (props: TMessageProps & IMessageA11y) => { <A11y.Index accessible={isTranslated} accessibilityLabel={messageText || ''} - accessibilityLanguage={props.autoTranslateLanguage} + accessibilityLanguage={autoTranslateLanguage} index={2} style={{ flex: 1 }}> <View style={styles.messageContent}> - <Content - tmid={tmid} - useRealName={props.useRealName} - isIgnored={props.isIgnored} - autoTranslateLanguage={props.autoTranslateLanguage} - /> + <Content tmid={tmid} useRealName={props.useRealName} isIgnored={props.isIgnored} /> {isInfo && type === 'message_pinned' ? ( <View pointerEvents='none'> <Attachments attachments={attachments} timeFormat={props.timeFormat} author={author} /> @@ -213,7 +193,7 @@ const Message = (props: TMessageProps & IMessageA11y) => { <A11y.Index accessible={isTranslated} accessibilityLabel={messageText || ''} - accessibilityLanguage={props.autoTranslateLanguage} + accessibilityLanguage={autoTranslateLanguage} index={2}> <View style={styles.flex}> <MessageAvatar /> @@ -226,7 +206,6 @@ const Message = (props: TMessageProps & IMessageA11y) => { highlighted={props.highlighted} isIgnored={props.isIgnored} isBeingEdited={props.isBeingEdited} - autoTranslateLanguage={props.autoTranslateLanguage} small={props.small} /> </View> @@ -268,8 +247,7 @@ const MessageTouchable = (props: TMessageProps) => { const onLongPress = useMessageLongPress(); const accessibilityLabelValue = useMessageAccessibilityLabel({ useRealName: props.useRealName, - isReadReceiptEnabled: props.isReadReceiptEnabled, - autoTranslateLanguage: props.autoTranslateLanguage + isReadReceiptEnabled: props.isReadReceiptEnabled }); const isDisabled = (isInfo && !isThreadReply) || archived || isTemp || type === 'jitsi_call_started'; const accessibilityActions = useMessageAccessibilityActions(isDisabled); @@ -294,7 +272,6 @@ const MessageTouchable = (props: TMessageProps) => { highlighted={props.highlighted} isIgnored={isIgnored} isBeingEdited={isBeingEdited} - autoTranslateLanguage={props.autoTranslateLanguage} small={props.small} /> </A11y.Order> @@ -332,7 +309,6 @@ const MessageTouchable = (props: TMessageProps) => { highlighted={props.highlighted} isIgnored={isIgnored} isBeingEdited={isBeingEdited} - autoTranslateLanguage={props.autoTranslateLanguage} small={props.small} handleLongPress={!isDisabled ? handleLongPress : undefined} /> diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.ts b/app/containers/message/hooks/useMessageAccessibilityLabel.ts index 8980b803e79..4e2ff7c6106 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.ts +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.ts @@ -13,6 +13,7 @@ import { useThreadData, useThreadPosition } from '../MessageStore'; +import { useAutoTranslate } from '../MessageRoomStore'; const stripMentions = (label: string, mentions: IUserMention[] = [], channels: IUserChannel[] = []) => { let result = label; @@ -31,13 +32,12 @@ const stripMentions = (label: string, mentions: IUserMention[] = [], channels: I export const useMessageAccessibilityLabel = ({ useRealName, - isReadReceiptEnabled, - autoTranslateLanguage + isReadReceiptEnabled }: { useRealName?: boolean; isReadReceiptEnabled?: boolean; - autoTranslateLanguage?: string; }): string => { + const { autoTranslateLanguage } = useAutoTranslate(); const { attachments, mentions, channels, comment, t: type } = useContentData(); const { u: author, role } = useMessageAuthor(); const { messageText, isTranslated } = useMessageText(); diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 23afdd30d4b..33df27c7594 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -11,7 +11,6 @@ interface IMessageContainerProps { previousItem?: TAnyMessageModel; isReadReceiptEnabled?: boolean; useRealName?: boolean; - autoTranslateLanguage?: string; isIgnored?: boolean; highlighted?: boolean; onLongPress?: (item: TAnyMessageModel) => void; @@ -40,7 +39,6 @@ const MessageContainer = (props: IMessageContainerProps) => { isPreview={props.isPreview} highlighted={props.highlighted} isIgnored={props.isIgnored ?? false} - autoTranslateLanguage={props.autoTranslateLanguage} /> <MessageSeparator ts={dateSeparator} unread={showUnreadSeparator} /> </MessageProvider> diff --git a/app/containers/message/interfaces.ts b/app/containers/message/interfaces.ts index c80c3b375f0..cd2ad079c23 100644 --- a/app/containers/message/interfaces.ts +++ b/app/containers/message/interfaces.ts @@ -18,7 +18,6 @@ export interface IMessageContent { tmid?: string; useRealName?: boolean; isIgnored: boolean; - autoTranslateLanguage?: string; } export interface IMessageEmoji { diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index ad9b6747db1..08427a416a7 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1462,7 +1462,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { timeFormat={Message_TimeFormat} useRealName={useRealName} isReadReceiptEnabled={Message_Read_Receipt_Enabled && !federated} - autoTranslateLanguage={'id' in room ? room.autoTranslateLanguage : undefined} threadBadgeColor={this.getBadgeColor(item?.id)} highlighted={highlightedMessage === item.id} dateSeparator={dateSeparator} From c8cfc8474bb5b3d96b10b570d1006b8b17944b7f Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 16:28:45 -0300 Subject: [PATCH 041/144] refactor: read the display-name setting directly in message leaves (NATIVE-1385) --- app/containers/message/Content.tsx | 9 +++---- app/containers/message/Message.stories.tsx | 2 +- app/containers/message/Message.tsx | 23 +++++++----------- app/containers/message/Preview.tsx | 7 +++--- app/containers/message/User.tsx | 5 ++-- .../useMessageAccessibilityLabel.test.tsx | 24 +++++++++++++------ .../hooks/useMessageAccessibilityLabel.ts | 10 +++----- app/containers/message/index.tsx | 2 -- app/containers/message/interfaces.ts | 1 - app/views/MessagesView/index.tsx | 7 ++---- app/views/RoomView/definitions.ts | 1 - app/views/RoomView/index.tsx | 4 +--- app/views/SearchMessagesView/index.tsx | 12 +--------- 13 files changed, 45 insertions(+), 62 deletions(-) diff --git a/app/containers/message/Content.tsx b/app/containers/message/Content.tsx index e20a4132830..3a67f9275ff 100644 --- a/app/containers/message/Content.tsx +++ b/app/containers/message/Content.tsx @@ -8,6 +8,7 @@ import { messageHaveAuthorName, getInfoMessage, getPreviewMessageFromAttachment import { type IMessageContent } from './interfaces'; import { useTheme } from '../../theme'; import { themes } from '../../lib/constants/colors'; +import { useSetting } from '../../lib/hooks/useSetting'; import { type MessageTypesValues } from '../../definitions'; import { useContentData, @@ -24,6 +25,7 @@ const Content = (props: IMessageContent) => { 'use memo'; const { theme } = useTheme(); + const useRealName = useSetting('UI_Use_Real_Name') as boolean; const user = useMessageUser(); const onLinkPress = useOnLinkPress(); const getCustomEmoji = useGetCustomEmoji() ?? (() => null); @@ -50,7 +52,7 @@ const Content = (props: IMessageContent) => { if (messageHaveAuthorName(type as MessageTypesValues)) { return ( <Text> - <User useRealName={props.useRealName} /> {renderMessageContent} + <User /> {renderMessageContent} </Text> ); } @@ -72,8 +74,7 @@ const Content = (props: IMessageContent) => { ); } else if (isPreview) { const previewMsg = - messageText || - (attachments?.length ? getPreviewMessageFromAttachment(attachments[0], autoTranslateLanguage) : undefined); + messageText || (attachments?.length ? getPreviewMessageFromAttachment(attachments[0], autoTranslateLanguage) : undefined); content = previewMsg ? <MarkdownPreview testID={`message-preview-${previewMsg}`} msg={previewMsg} /> : null; } else if (messageText) { content = ( @@ -85,7 +86,7 @@ const Content = (props: IMessageContent) => { channels={channels} mentions={mentions} navToRoomInfo={navToRoomInfo} - useRealName={props.useRealName} + useRealName={useRealName} onLinkPress={onLinkPress} isTranslated={isTranslated} /> diff --git a/app/containers/message/Message.stories.tsx b/app/containers/message/Message.stories.tsx index 7a9a1f3c7a7..bbe0848c243 100644 --- a/app/containers/message/Message.stories.tsx +++ b/app/containers/message/Message.stories.tsx @@ -226,6 +226,7 @@ const renderMessageStory = (fontScale: number, props: any) => { const item = buildItem({ ...itemProps, isTranslated }); const previousItem = previousItemOverride ?? resolvePreviousItem(item, isHeader, isThreadReply); + store.dispatch(updateSettings('UI_Use_Real_Name', !!useRealName)); const containerProps = { item, @@ -236,7 +237,6 @@ const renderMessageStory = (fontScale: number, props: any) => { broadcast, archived, isIgnored, - useRealName, isReadReceiptEnabled, autoTranslateRoom: autoTranslateRoom ?? (isTranslated ? true : undefined), autoTranslateLanguage: autoTranslateLanguage ?? (isTranslated ? 'en' : undefined), diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index 373241ea11f..ee6947e8873 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -47,7 +47,6 @@ import { type TMessageProps = { timeFormat?: string; - useRealName?: boolean; isReadReceiptEnabled?: boolean; isPreview?: boolean; highlighted?: boolean; @@ -77,11 +76,11 @@ const MessageInner = (props: TMessageProps) => { if (props.isPreview) { content = ( <> - <User useRealName={props.useRealName} timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> + <User timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} <> <Quote attachments={attachments} timeFormat={props.timeFormat} author={author} /> - <Content tmid={tmid} useRealName={props.useRealName} isIgnored={props.isIgnored} /> + <Content tmid={tmid} isIgnored={props.isIgnored} /> <Attachments attachments={attachments} timeFormat={props.timeFormat} author={author} /> </> <Urls /> @@ -92,7 +91,7 @@ const MessageInner = (props: TMessageProps) => { if (type === 'discussion-created') { content = ( <> - <User useRealName={props.useRealName} timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> + <User timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} <Discussion /> </> @@ -102,8 +101,8 @@ const MessageInner = (props: TMessageProps) => { if (type === 'jitsi_call_started') { content = ( <> - <User useRealName={props.useRealName} timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> - <Content tmid={tmid} useRealName={props.useRealName} isIgnored={props.isIgnored} /> + <User timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> + <Content tmid={tmid} isIgnored={props.isIgnored} /> <CallButton /> {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} </> @@ -113,7 +112,7 @@ const MessageInner = (props: TMessageProps) => { if (blocks && blocks.length) { content = ( <> - <User useRealName={props.useRealName} timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> + <User timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> <Blocks /> <Thread /> <Reactions /> @@ -125,11 +124,11 @@ const MessageInner = (props: TMessageProps) => { if (!content) { content = ( <> - <User useRealName={props.useRealName} timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> + <User timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} <View style={{ gap: 4 }}> <Quote attachments={attachments} timeFormat={props.timeFormat} author={author} /> - <Content tmid={tmid} useRealName={props.useRealName} isIgnored={props.isIgnored} /> + <Content tmid={tmid} isIgnored={props.isIgnored} /> <Attachments attachments={attachments} timeFormat={props.timeFormat} author={author} /> <Urls /> <Thread /> @@ -175,7 +174,7 @@ const Message = (props: TMessageProps & IMessageA11y) => { index={2} style={{ flex: 1 }}> <View style={styles.messageContent}> - <Content tmid={tmid} useRealName={props.useRealName} isIgnored={props.isIgnored} /> + <Content tmid={tmid} isIgnored={props.isIgnored} /> {isInfo && type === 'message_pinned' ? ( <View pointerEvents='none'> <Attachments attachments={attachments} timeFormat={props.timeFormat} author={author} /> @@ -200,7 +199,6 @@ const Message = (props: TMessageProps & IMessageA11y) => { <View style={styles.messageContent}> <MessageInner timeFormat={props.timeFormat} - useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} isPreview={props.isPreview} highlighted={props.highlighted} @@ -246,7 +244,6 @@ const MessageTouchable = (props: TMessageProps) => { const onPressAction = useMessagePress({ isIgnored, revealIgnored }); const onLongPress = useMessageLongPress(); const accessibilityLabelValue = useMessageAccessibilityLabel({ - useRealName: props.useRealName, isReadReceiptEnabled: props.isReadReceiptEnabled }); const isDisabled = (isInfo && !isThreadReply) || archived || isTemp || type === 'jitsi_call_started'; @@ -266,7 +263,6 @@ const MessageTouchable = (props: TMessageProps) => { <A11y.Order> <Message timeFormat={props.timeFormat} - useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} isPreview={props.isPreview} highlighted={props.highlighted} @@ -303,7 +299,6 @@ const MessageTouchable = (props: TMessageProps) => { }}> <Message timeFormat={props.timeFormat} - useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} isPreview={props.isPreview} highlighted={props.highlighted} diff --git a/app/containers/message/Preview.tsx b/app/containers/message/Preview.tsx index 18a159e56cd..0330baf47d4 100644 --- a/app/containers/message/Preview.tsx +++ b/app/containers/message/Preview.tsx @@ -7,12 +7,11 @@ import { type TAnyMessageModel, type TGetCustomEmoji } from '../../definitions'; const MessagePreview = ({ message }: { message: TAnyMessageModel }) => { 'use memo'; - const { user, baseUrl, Message_TimeFormat, customEmojis, useRealName } = useAppSelector(state => ({ + const { user, baseUrl, Message_TimeFormat, customEmojis } = useAppSelector(state => ({ user: getUserSelector(state), baseUrl: state.server.server, Message_TimeFormat: state.settings.Message_TimeFormat as string, - customEmojis: state.customEmojis, - useRealName: state.settings.UI_Use_Real_Name as boolean + customEmojis: state.customEmojis })); const getCustomEmoji: TGetCustomEmoji = name => { @@ -21,7 +20,7 @@ const MessagePreview = ({ message }: { message: TAnyMessageModel }) => { }; return ( <MessageRoomProvider user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} rid={message.rid}> - <Message item={message} timeFormat={Message_TimeFormat} useRealName={useRealName} isPreview /> + <Message item={message} timeFormat={Message_TimeFormat} isPreview /> </MessageRoomProvider> ); }; diff --git a/app/containers/message/User.tsx b/app/containers/message/User.tsx index f1f0826dbb1..0d828c0fcfb 100644 --- a/app/containers/message/User.tsx +++ b/app/containers/message/User.tsx @@ -7,6 +7,7 @@ import RightIcons from './Components/RightIcons'; import { messageHaveAuthorName } from './utils'; import MessageTime from './Time'; import { useResponsiveLayout } from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; +import { useSetting } from '../../lib/hooks/useSetting'; import { useIsEdited, useMessageAuthor, @@ -47,14 +48,14 @@ const styles = StyleSheet.create({ }); interface IMessageUser { - useRealName?: boolean; timeFormat?: string; isReadReceiptEnabled?: boolean; } -const User = ({ useRealName, timeFormat, isReadReceiptEnabled }: IMessageUser) => { +const User = ({ timeFormat, isReadReceiptEnabled }: IMessageUser) => { 'use memo'; + const useRealName = useSetting('UI_Use_Real_Name') as boolean; const user = useMessageUser(); const navToRoomInfo = useNavToRoomInfo(); const { colors } = useTheme(); diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx b/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx index 9705f8ded25..78345e5a6f7 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx @@ -1,8 +1,11 @@ +import { Provider } from 'react-redux'; import { render } from '@testing-library/react-native'; import { useMessageAccessibilityLabel } from './useMessageAccessibilityLabel'; import { MessageProvider } from '../MessageStore'; import { MessageRoomProvider, pickMessageRoomState } from '../MessageRoomStore'; +import { updateSettings } from '../../../actions/settings'; +import { mockedStore } from '../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../definitions'; import { E2E_MESSAGE_TYPE } from '../../../lib/constants/keys'; @@ -10,7 +13,7 @@ jest.mock('../../../lib/hooks/useAltTextSupported', () => ({ useAltTextSupported: () => false })); -type TArgs = { useRealName?: boolean; isReadReceiptEnabled?: boolean; autoTranslateLanguage?: string }; +type TArgs = { isReadReceiptEnabled?: boolean; autoTranslateLanguage?: string }; const FIXED_TS = new Date('2024-01-15T12:34:56Z'); const HOUR = FIXED_TS.toLocaleTimeString(); @@ -37,11 +40,13 @@ const renderLabel = ( return null; }; render( - <MessageRoomProvider {...pickMessageRoomState(config)}> - <MessageProvider item={item} previousItem={previousItem}> - <Probe /> - </MessageProvider> - </MessageRoomProvider> + <Provider store={mockedStore}> + <MessageRoomProvider {...pickMessageRoomState(config)}> + <MessageProvider item={item} previousItem={previousItem}> + <Probe /> + </MessageProvider> + </MessageRoomProvider> + </Provider> ); const { calls } = spy.mock; return calls[calls.length - 1][0]; @@ -63,7 +68,12 @@ describe('useMessageAccessibilityLabel', () => { }); it('uses the author real name when useRealName is true', () => { - expect(renderLabel(buildItem(), { useRealName: true })).toBe(`Alice ${HOUR} hello world.`); + mockedStore.dispatch(updateSettings('UI_Use_Real_Name', true)); + try { + expect(renderLabel(buildItem())).toBe(`Alice ${HOUR} hello world.`); + } finally { + mockedStore.dispatch(updateSettings('UI_Use_Real_Name', false)); + } }); it('omits the hour when ts is missing', () => { diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.ts b/app/containers/message/hooks/useMessageAccessibilityLabel.ts index 4e2ff7c6106..f6e6c4fe67d 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.ts +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.ts @@ -14,6 +14,7 @@ import { useThreadPosition } from '../MessageStore'; import { useAutoTranslate } from '../MessageRoomStore'; +import { useSetting } from '../../../lib/hooks/useSetting'; const stripMentions = (label: string, mentions: IUserMention[] = [], channels: IUserChannel[] = []) => { let result = label; @@ -30,13 +31,8 @@ const stripMentions = (label: string, mentions: IUserMention[] = [], channels: I return result; }; -export const useMessageAccessibilityLabel = ({ - useRealName, - isReadReceiptEnabled -}: { - useRealName?: boolean; - isReadReceiptEnabled?: boolean; -}): string => { +export const useMessageAccessibilityLabel = ({ isReadReceiptEnabled }: { isReadReceiptEnabled?: boolean }): string => { + const useRealName = useSetting('UI_Use_Real_Name') as boolean; const { autoTranslateLanguage } = useAutoTranslate(); const { attachments, mentions, channels, comment, t: type } = useContentData(); const { u: author, role } = useMessageAuthor(); diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 33df27c7594..32798238530 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -10,7 +10,6 @@ interface IMessageContainerProps { timeFormat?: string; previousItem?: TAnyMessageModel; isReadReceiptEnabled?: boolean; - useRealName?: boolean; isIgnored?: boolean; highlighted?: boolean; onLongPress?: (item: TAnyMessageModel) => void; @@ -34,7 +33,6 @@ const MessageContainer = (props: IMessageContainerProps) => { threadBadgeColor={props.threadBadgeColor}> <Message timeFormat={props.timeFormat} - useRealName={props.useRealName} isReadReceiptEnabled={props.isReadReceiptEnabled} isPreview={props.isPreview} highlighted={props.highlighted} diff --git a/app/containers/message/interfaces.ts b/app/containers/message/interfaces.ts index cd2ad079c23..1397aad3c99 100644 --- a/app/containers/message/interfaces.ts +++ b/app/containers/message/interfaces.ts @@ -16,7 +16,6 @@ export interface IMessageAvatar { export interface IMessageContent { tmid?: string; - useRealName?: boolean; isIgnored: boolean; } diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index f09f33cc533..b348328f4fd 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -54,7 +54,6 @@ interface IMessagesViewProps { customEmojis: { [key: string]: ICustomEmoji }; theme: TSupportedThemes; showActionSheet: (params: { options: string[]; hasCancel: boolean }) => void; - useRealName: boolean; isMasterDetail: boolean; } @@ -162,11 +161,10 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { }; defineMessagesViewContent = (name: string) => { - const { user, useRealName } = this.props; + const { user } = this.props; const renderItemCommonProps = (item: TAnyMessageModel) => ({ item, timeFormat: 'MMM Do YYYY, h:mm:ss a', - useRealName, onPress: () => this.jumpToMessage({ item }) }); @@ -380,8 +378,7 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { const mapStateToProps = (state: IApplicationState) => ({ baseUrl: state.server.server, user: getUserSelector(state), - customEmojis: state.customEmojis, - useRealName: state.settings.UI_Use_Real_Name + customEmojis: state.customEmojis }); export default connect(mapStateToProps)(withTheme(withActionSheet(withMasterDetail(MessagesView)))); diff --git a/app/views/RoomView/definitions.ts b/app/views/RoomView/definitions.ts index cd71822b594..0f8e76261cb 100644 --- a/app/views/RoomView/definitions.ts +++ b/app/views/RoomView/definitions.ts @@ -12,7 +12,6 @@ import { type IActionSheetProvider } from '../../containers/ActionSheet'; export interface IRoomViewProps extends IActionSheetProvider, IBaseScreen<ChatsStackParamList, 'RoomView'> { user: Pick<ILoggedUser, 'id' | 'username' | 'token' | 'showMessageInMainThread'>; - useRealName?: boolean; isAuthenticated: boolean; Message_GroupingPeriod?: number; Message_TimeFormat?: string; diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 08427a416a7..dca4a40e1eb 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1410,7 +1410,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { renderItem = (item: TAnyMessageModel, previousItem: TAnyMessageModel, highlightedMessage?: string) => { const { room, lastOpen } = this.state; - const { Message_TimeFormat, useRealName, Message_Read_Receipt_Enabled, inAppFeedback } = this.props; + const { Message_TimeFormat, Message_Read_Receipt_Enabled, inAppFeedback } = this.props; let dateSeparator = null; let showUnreadSeparator = false; const federated = 'id' in room && isRoomFederated(room); @@ -1460,7 +1460,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { previousItem={previousItem} onLongPress={this.onMessageLongPress} timeFormat={Message_TimeFormat} - useRealName={useRealName} isReadReceiptEnabled={Message_Read_Receipt_Enabled && !federated} threadBadgeColor={this.getBadgeColor(item?.id)} highlighted={highlightedMessage === item.id} @@ -1714,7 +1713,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { const mapStateToProps = (state: IApplicationState) => ({ user: getUserSelector(state), - useRealName: state.settings.UI_Use_Real_Name as boolean, isAuthenticated: state.login.isAuthenticated, Message_GroupingPeriod: state.settings.Message_GroupingPeriod as number, Message_TimeFormat: state.settings.Message_TimeFormat as string, diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index 82303824c9c..e80ce757bb7 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -77,7 +77,6 @@ interface ISearchMessagesViewProps extends INavigationOption { [key: string]: ICustomEmoji; }; theme: TSupportedThemes; - useRealName: boolean; isMasterDetail: boolean; } class SearchMessagesView extends Component<ISearchMessagesViewProps, ISearchMessagesViewState> { @@ -286,15 +285,7 @@ class SearchMessagesView extends Component<ISearchMessagesViewProps, ISearchMess renderItem = ({ item }: { item: IMessageFromServer | TMessageModel }) => { const message = item as TMessageModel; - const { useRealName } = this.props; - return ( - <Message - item={message} - timeFormat='MMM Do YYYY, h:mm:ss a' - useRealName={useRealName} - onPress={() => this.jumpToMessage({ item })} - /> - ); + return <Message item={message} timeFormat='MMM Do YYYY, h:mm:ss a' onPress={() => this.jumpToMessage({ item })} />; }; renderList = () => { @@ -354,7 +345,6 @@ const mapStateToProps = (state: any) => ({ serverVersion: state.server.version, baseUrl: state.server.server, user: getUserSelector(state), - useRealName: state.settings.UI_Use_Real_Name, customEmojis: state.customEmojis }); From 96c8d7cea5a849283a840067f2f5fb07819a1838 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 16:36:47 -0300 Subject: [PATCH 042/144] test: drop vestigial autoTranslateLanguage args from accessibility-label tests (NATIVE-1385) --- .../hooks/useMessageAccessibilityLabel.test.tsx | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx b/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx index 78345e5a6f7..0462eea6771 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx @@ -13,7 +13,7 @@ jest.mock('../../../lib/hooks/useAltTextSupported', () => ({ useAltTextSupported: () => false })); -type TArgs = { isReadReceiptEnabled?: boolean; autoTranslateLanguage?: string }; +type TArgs = { isReadReceiptEnabled?: boolean }; const FIXED_TS = new Date('2024-01-15T12:34:56Z'); const HOUR = FIXED_TS.toLocaleTimeString(); @@ -130,7 +130,7 @@ describe('useMessageAccessibilityLabel', () => { expect( renderLabel( buildItem({ autoTranslate: true, translations: [{ _id: 't1', language: 'en', value: 'translated text' }] }), - { autoTranslateLanguage: 'en' }, + {}, { autoTranslateRoom: true, autoTranslateLanguage: 'en', user: { username: 'bob' } } ) ).toBe(`alice ${HOUR} Message translated into English.`); @@ -146,19 +146,9 @@ describe('useMessageAccessibilityLabel', () => { unread: true, attachments: [{ image_url: 'https://example.com/img.png', altText: 'A wavy pattern' }] }), - { autoTranslateLanguage: 'en', isReadReceiptEnabled: true }, + { isReadReceiptEnabled: true }, { autoTranslateRoom: true, autoTranslateLanguage: 'en', user: { username: 'bob' } } ) ).toBe(`alice ${HOUR} Message translated into English. Image description: A wavy pattern Message was not read`); }); - - it('falls back to English when autoTranslateLanguage is missing', () => { - expect( - renderLabel( - buildItem({ autoTranslate: true, translations: [{ _id: 't1', language: 'en', value: 'translated text' }] }), - {}, - { autoTranslateRoom: true, autoTranslateLanguage: 'en', user: { username: 'bob' } } - ) - ).toBe(`alice ${HOUR} Message translated into English.`); - }); }); From 8330912a7bcfa8dfe3e6cec5368995cd80d865e6 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 18:30:50 -0300 Subject: [PATCH 043/144] refactor: add timeFormat to the message room store (NATIVE-1385) --- app/containers/message/MessageRoomStore.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/containers/message/MessageRoomStore.tsx b/app/containers/message/MessageRoomStore.tsx index ac83c9ac92e..1859f829f32 100644 --- a/app/containers/message/MessageRoomStore.tsx +++ b/app/containers/message/MessageRoomStore.tsx @@ -35,6 +35,7 @@ export type MessageRoomState = { broadcast?: boolean; isThreadRoom?: boolean; Message_GroupingPeriod?: number; + timeFormat?: string; // reactive tail (provider keeps current) autoTranslateRoom?: boolean; autoTranslateLanguage?: string; @@ -69,6 +70,7 @@ const ROOM_STATE_KEYS: (keyof MessageRoomState)[] = [ 'broadcast', 'isThreadRoom', 'Message_GroupingPeriod', + 'timeFormat', 'autoTranslateRoom', 'autoTranslateLanguage' ]; @@ -139,6 +141,7 @@ export const useRid = (): string | undefined => useMessageRoomStore(s => s.rid); export const useMessageUser = (): MessageRoomState['user'] => useMessageRoomStore(s => s.user); export const useBaseUrl = (): string | undefined => useMessageRoomStore(s => s.baseUrl); export const useBroadcast = (): boolean | undefined => useMessageRoomStore(s => s.broadcast); +export const useTimeFormat = (): string | undefined => useMessageRoomStore(s => s.timeFormat); export const useIsThreadRoom = (): boolean | undefined => useMessageRoomStore(s => s.isThreadRoom); export const useMessageGroupingPeriod = (): number | undefined => useMessageRoomStore(s => s.Message_GroupingPeriod); From 7f5bffdeeb3edd5b2085a7e14c586a96af2491c8 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 18:33:10 -0300 Subject: [PATCH 044/144] refactor: seed timeFormat in the message room providers (NATIVE-1385) --- app/containers/message/Preview.tsx | 7 ++++++- app/views/MessagesView/index.tsx | 3 ++- app/views/RoomView/index.tsx | 3 ++- app/views/SearchMessagesView/index.tsx | 3 ++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/containers/message/Preview.tsx b/app/containers/message/Preview.tsx index 0330baf47d4..d4bd1b86e8c 100644 --- a/app/containers/message/Preview.tsx +++ b/app/containers/message/Preview.tsx @@ -19,7 +19,12 @@ const MessagePreview = ({ message }: { message: TAnyMessageModel }) => { return emoji ?? null; }; return ( - <MessageRoomProvider user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} rid={message.rid}> + <MessageRoomProvider + user={user} + baseUrl={baseUrl} + getCustomEmoji={getCustomEmoji} + rid={message.rid} + timeFormat={Message_TimeFormat}> <Message item={message} timeFormat={Message_TimeFormat} isPreview /> </MessageRoomProvider> ); diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index b348328f4fd..d97d85a23d5 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -360,7 +360,8 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { user={user} baseUrl={baseUrl} rid={this.rid} - isThreadRoom> + isThreadRoom + timeFormat={'MMM Do YYYY, h:mm:ss a'}> <FlatList data={messages} renderItem={this.renderItem} diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index dca4a40e1eb..d8e0275ff6a 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1603,7 +1603,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { render() { console.count(`${this.constructor.name}.render calls`); const { room, isAutocompleteVisible, showMissingE2EEKey, showE2EEDisabledRoom, canAutoTranslate } = this.state; - const { user, baseUrl, theme, width, serverVersion, navigation, Message_GroupingPeriod } = this.props; + const { user, baseUrl, theme, width, serverVersion, navigation, Message_GroupingPeriod, Message_TimeFormat } = this.props; const { rid, t } = room; let bannerClosed; let announcement; @@ -1686,6 +1686,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { broadcast={'id' in room && room.broadcast} isThreadRoom={!!this.tmid} Message_GroupingPeriod={Message_GroupingPeriod} + timeFormat={Message_TimeFormat} autoTranslateRoom={canAutoTranslate && 'id' in room && room.autoTranslate} autoTranslateLanguage={'id' in room ? room.autoTranslateLanguage : undefined}> <List diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index e80ce757bb7..a6552abe3db 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -304,7 +304,8 @@ class SearchMessagesView extends Component<ISearchMessagesViewProps, ISearchMess user={user} baseUrl={baseUrl} rid={this.rid} - isThreadRoom> + isThreadRoom + timeFormat={'MMM Do YYYY, h:mm:ss a'}> <FlatList data={messages} renderItem={this.renderItem} From 4ca8a769166cb76e9bff90313551a7c55d4429e7 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 18:46:09 -0300 Subject: [PATCH 045/144] refactor: read timeFormat from the room store in the message leaves (NATIVE-1385) --- .../Attachments/Attachments.stories.tsx | 10 +++--- .../Components/Attachments/Attachments.tsx | 4 +-- .../CollapsibleQuote.stories.tsx | 2 +- .../CollapsibleQuote.test.tsx | 2 +- .../Attachments/CollapsibleQuote/index.tsx | 1 - .../message/Components/Attachments/Quote.tsx | 4 +-- .../message/Components/Attachments/Reply.tsx | 14 ++++---- app/containers/message/Message.tsx | 34 ++++++++----------- app/containers/message/Preview.tsx | 2 +- app/containers/message/Time.tsx | 5 +-- app/containers/message/User.tsx | 5 ++- app/containers/message/index.tsx | 2 -- app/containers/message/interfaces.ts | 1 - app/views/MessagesView/index.tsx | 1 - app/views/RoomView/index.tsx | 3 +- app/views/SearchMessagesView/index.tsx | 2 +- 16 files changed, 40 insertions(+), 52 deletions(-) diff --git a/app/containers/message/Components/Attachments/Attachments.stories.tsx b/app/containers/message/Components/Attachments/Attachments.stories.tsx index 9131427746c..f9a8a68802c 100644 --- a/app/containers/message/Components/Attachments/Attachments.stories.tsx +++ b/app/containers/message/Components/Attachments/Attachments.stories.tsx @@ -68,7 +68,7 @@ export default { export const SingleImageOldServer = () => ( <StoryWrapper store={oldServerStore}> <View style={{ padding: 10, width: 350 }}> - <Attachments attachments={[MOCK_IMAGE_WITH_ALT]} timeFormat='LT' /> + <Attachments attachments={[MOCK_IMAGE_WITH_ALT]} /> </View> </StoryWrapper> ); @@ -77,7 +77,7 @@ export const SingleImageOldServer = () => ( export const SingleImageNewServer = () => ( <StoryWrapper store={newServerStore}> <View style={{ padding: 10, width: 350 }}> - <Attachments attachments={[MOCK_IMAGE_WITH_ALT]} timeFormat='LT' /> + <Attachments attachments={[MOCK_IMAGE_WITH_ALT]} /> </View> </StoryWrapper> ); @@ -86,7 +86,7 @@ export const SingleImageNewServer = () => ( export const SingleImageNoAlt = () => ( <StoryWrapper store={newServerStore}> <View style={{ padding: 10, width: 350 }}> - <Attachments attachments={[MOCK_IMAGE_1]} timeFormat='LT' /> + <Attachments attachments={[MOCK_IMAGE_1]} /> </View> </StoryWrapper> ); @@ -95,7 +95,7 @@ export const SingleImageNoAlt = () => ( export const MultipleImagesNewServer = () => ( <StoryWrapper store={newServerStore}> <View style={{ padding: 10, width: 350 }}> - <Attachments attachments={MOCK_MULTIPLE_IMAGES} timeFormat='LT' /> + <Attachments attachments={MOCK_MULTIPLE_IMAGES} /> </View> </StoryWrapper> ); @@ -104,7 +104,7 @@ export const MultipleImagesNewServer = () => ( export const MultipleImagesOldServer = () => ( <StoryWrapper store={oldServerStore}> <View style={{ padding: 10, width: 350 }}> - <Attachments attachments={MOCK_MULTIPLE_IMAGES.slice(0, 4)} timeFormat='LT' /> + <Attachments attachments={MOCK_MULTIPLE_IMAGES.slice(0, 4)} /> </View> </StoryWrapper> ); diff --git a/app/containers/message/Components/Attachments/Attachments.tsx b/app/containers/message/Components/Attachments/Attachments.tsx index bd47386e8f5..e05c8bba9c1 100644 --- a/app/containers/message/Components/Attachments/Attachments.tsx +++ b/app/containers/message/Components/Attachments/Attachments.tsx @@ -13,7 +13,7 @@ import { type IMessageAttachments } from '../../interfaces'; import { getMessageFromAttachment } from '../../utils'; import { isContentAttachment } from './utils'; -const Attachments: FC<IMessageAttachments> = ({ attachments, timeFormat, author }: IMessageAttachments) => { +const Attachments: FC<IMessageAttachments> = ({ attachments, author }: IMessageAttachments) => { 'use memo'; const translateLanguage = useTranslateLanguage(); @@ -75,7 +75,6 @@ const Attachments: FC<IMessageAttachments> = ({ attachments, timeFormat, author <CollapsibleQuote key={file.title_link || file.message_link || `collapsible-${index}`} attachment={file} - timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} /> ); @@ -86,7 +85,6 @@ const Attachments: FC<IMessageAttachments> = ({ attachments, timeFormat, author <Reply key={file.title_link || file.message_link || `reply-${index}`} attachment={file} - timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} msg={msg} /> diff --git a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx b/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx index 1cdfcce259e..7a851e97b87 100644 --- a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx +++ b/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx @@ -24,7 +24,7 @@ export default { export const Item = () => ( <View style={{ padding: 10 }}> <MessageRoomProvider {...pickMessageRoomState({ onLongPress: () => {}, user: { username: 'Marcos' } })}> - <CollapsibleQuote attachment={testAttachment} getCustomEmoji={() => null} timeFormat='LT' /> + <CollapsibleQuote attachment={testAttachment} getCustomEmoji={() => null} /> </MessageRoomProvider> </View> ); diff --git a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx b/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx index ae181120587..371310e33d2 100644 --- a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx +++ b/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx @@ -50,7 +50,7 @@ const Render = () => ( <Provider store={mockedStore}> <MessageRoomProvider {...pickMessageRoomState(contextValue)}> <MessageProvider item={item}> - <CollapsibleQuote attachment={testAttachment} getCustomEmoji={mockFn} timeFormat='LT' /> + <CollapsibleQuote attachment={testAttachment} getCustomEmoji={mockFn} /> </MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx b/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx index b06c5947996..60b8423837a 100644 --- a/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx +++ b/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx @@ -70,7 +70,6 @@ interface IMessageFields { interface IMessageReply { attachment: IAttachment; - timeFormat?: string; getCustomEmoji: TGetCustomEmoji; } diff --git a/app/containers/message/Components/Attachments/Quote.tsx b/app/containers/message/Components/Attachments/Quote.tsx index 7bb3884bfff..56cf1af872f 100644 --- a/app/containers/message/Components/Attachments/Quote.tsx +++ b/app/containers/message/Components/Attachments/Quote.tsx @@ -9,7 +9,7 @@ import { isQuoteAttachment } from './utils'; import { useGetCustomEmoji } from '../../MessageRoomStore'; import { useTranslateLanguage } from '../../MessageStore'; -const Quote: FC<IMessageAttachments> = ({ attachments, timeFormat }: IMessageAttachments) => { +const Quote: FC<IMessageAttachments> = ({ attachments }: IMessageAttachments) => { 'use memo'; const translateLanguage = useTranslateLanguage(); @@ -24,7 +24,7 @@ const Quote: FC<IMessageAttachments> = ({ attachments, timeFormat }: IMessageAtt const quotesElements = quotes.map((file: IAttachment, index: number) => { const msg = getMessageFromAttachment(file, translateLanguage); - return <Reply key={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} msg={msg} />; + return <Reply key={index} attachment={file} getCustomEmoji={getCustomEmoji} msg={msg} />; }); return <View style={{ gap: 4 }}>{quotesElements}</View>; diff --git a/app/containers/message/Components/Attachments/Reply.tsx b/app/containers/message/Components/Attachments/Reply.tsx index 019ceecc039..390fb48172b 100644 --- a/app/containers/message/Components/Attachments/Reply.tsx +++ b/app/containers/message/Components/Attachments/Reply.tsx @@ -13,7 +13,7 @@ import RCActivityIndicator from '../../../ActivityIndicator'; import Markdown, { MarkdownPreview } from '../../../markdown'; import { Attachments } from './components'; import Quote from './Quote'; -import { useBaseUrl, useMessageUser } from '../../MessageRoomStore'; +import { useBaseUrl, useMessageUser, useTimeFormat } from '../../MessageRoomStore'; import { useIsEncrypted, useMessageId } from '../../MessageStore'; import Touchable from '../../Touchable'; import messageStyles from '../../styles'; @@ -84,14 +84,14 @@ const styles = StyleSheet.create({ interface IMessageReply { attachment: IAttachment; - timeFormat?: string; getCustomEmoji: TGetCustomEmoji; msg?: string; } -const Title = ({ attachment, timeFormat, theme }: { attachment: IAttachment; timeFormat?: string; theme: TSupportedThemes }) => { +const Title = ({ attachment, theme }: { attachment: IAttachment; theme: TSupportedThemes }) => { 'use memo'; + const timeFormat = useTimeFormat(); const time = attachment.message_link && attachment.ts ? dayjs(attachment.ts).format(timeFormat) : null; return ( <View style={styles.authorContainer}> @@ -172,7 +172,7 @@ const Fields = ({ ); }; -const Reply = ({ attachment, timeFormat, getCustomEmoji, msg }: IMessageReply) => { +const Reply = ({ attachment, getCustomEmoji, msg }: IMessageReply) => { 'use memo'; const [loading, setLoading] = useState(false); @@ -220,10 +220,10 @@ const Reply = ({ attachment, timeFormat, getCustomEmoji, msg }: IMessageReply) = disabled={!!(loading || attachment.message_link)}> <View style={styles.attachmentContainer}> <View style={styles.titleAndDescriptionContainer}> - <Title attachment={attachment} timeFormat={timeFormat} theme={theme} /> + <Title attachment={attachment} theme={theme} /> <Description attachment={attachment} getCustomEmoji={getCustomEmoji} /> - <Quote attachments={attachment.attachments} timeFormat={timeFormat} /> - <Attachments attachments={attachment.attachments} timeFormat={timeFormat} /> + <Quote attachments={attachment.attachments} /> + <Attachments attachments={attachment.attachments} /> <Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} /> {loading ? ( <View style={styles.backdrop}> diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index ee6947e8873..ed62d3268a0 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -46,7 +46,6 @@ import { } from './MessageStore'; type TMessageProps = { - timeFormat?: string; isReadReceiptEnabled?: boolean; isPreview?: boolean; highlighted?: boolean; @@ -76,12 +75,12 @@ const MessageInner = (props: TMessageProps) => { if (props.isPreview) { content = ( <> - <User timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> - {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} + <User isReadReceiptEnabled={props.isReadReceiptEnabled} /> + {showTimeLarge ? <MessageTime /> : null} <> - <Quote attachments={attachments} timeFormat={props.timeFormat} author={author} /> + <Quote attachments={attachments} author={author} /> <Content tmid={tmid} isIgnored={props.isIgnored} /> - <Attachments attachments={attachments} timeFormat={props.timeFormat} author={author} /> + <Attachments attachments={attachments} author={author} /> </> <Urls /> </> @@ -91,8 +90,8 @@ const MessageInner = (props: TMessageProps) => { if (type === 'discussion-created') { content = ( <> - <User timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> - {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} + <User isReadReceiptEnabled={props.isReadReceiptEnabled} /> + {showTimeLarge ? <MessageTime /> : null} <Discussion /> </> ); @@ -101,10 +100,10 @@ const MessageInner = (props: TMessageProps) => { if (type === 'jitsi_call_started') { content = ( <> - <User timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> + <User isReadReceiptEnabled={props.isReadReceiptEnabled} /> <Content tmid={tmid} isIgnored={props.isIgnored} /> <CallButton /> - {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} + {showTimeLarge ? <MessageTime /> : null} </> ); } @@ -112,11 +111,11 @@ const MessageInner = (props: TMessageProps) => { if (blocks && blocks.length) { content = ( <> - <User timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> + <User isReadReceiptEnabled={props.isReadReceiptEnabled} /> <Blocks /> <Thread /> <Reactions /> - {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} + {showTimeLarge ? <MessageTime /> : null} </> ); } @@ -124,12 +123,12 @@ const MessageInner = (props: TMessageProps) => { if (!content) { content = ( <> - <User timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} /> - {showTimeLarge ? <MessageTime timeFormat={props.timeFormat} /> : null} + <User isReadReceiptEnabled={props.isReadReceiptEnabled} /> + {showTimeLarge ? <MessageTime /> : null} <View style={{ gap: 4 }}> - <Quote attachments={attachments} timeFormat={props.timeFormat} author={author} /> + <Quote attachments={attachments} author={author} /> <Content tmid={tmid} isIgnored={props.isIgnored} /> - <Attachments attachments={attachments} timeFormat={props.timeFormat} author={author} /> + <Attachments attachments={attachments} author={author} /> <Urls /> <Thread /> <Reactions /> @@ -177,7 +176,7 @@ const Message = (props: TMessageProps & IMessageA11y) => { <Content tmid={tmid} isIgnored={props.isIgnored} /> {isInfo && type === 'message_pinned' ? ( <View pointerEvents='none'> - <Attachments attachments={attachments} timeFormat={props.timeFormat} author={author} /> + <Attachments attachments={attachments} author={author} /> </View> ) : null} </View> @@ -198,7 +197,6 @@ const Message = (props: TMessageProps & IMessageA11y) => { <MessageAvatar /> <View style={styles.messageContent}> <MessageInner - timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} isPreview={props.isPreview} highlighted={props.highlighted} @@ -262,7 +260,6 @@ const MessageTouchable = (props: TMessageProps) => { return ( <A11y.Order> <Message - timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} isPreview={props.isPreview} highlighted={props.highlighted} @@ -298,7 +295,6 @@ const MessageTouchable = (props: TMessageProps) => { if (e.nativeEvent.actionName === 'showActions') handleLongPress(); }}> <Message - timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} isPreview={props.isPreview} highlighted={props.highlighted} diff --git a/app/containers/message/Preview.tsx b/app/containers/message/Preview.tsx index d4bd1b86e8c..0646128fc27 100644 --- a/app/containers/message/Preview.tsx +++ b/app/containers/message/Preview.tsx @@ -25,7 +25,7 @@ const MessagePreview = ({ message }: { message: TAnyMessageModel }) => { getCustomEmoji={getCustomEmoji} rid={message.rid} timeFormat={Message_TimeFormat}> - <Message item={message} timeFormat={Message_TimeFormat} isPreview /> + <Message item={message} isPreview /> </MessageRoomProvider> ); }; diff --git a/app/containers/message/Time.tsx b/app/containers/message/Time.tsx index 6bc7033a84f..b16475e6855 100644 --- a/app/containers/message/Time.tsx +++ b/app/containers/message/Time.tsx @@ -4,18 +4,19 @@ import dayjs from '../../lib/dayjs'; import { useTheme } from '../../theme'; import messageStyles from './styles'; import { useMessageField } from './MessageStore'; +import { useTimeFormat } from './MessageRoomStore'; interface IMessageTime { ts?: Date; - timeFormat?: string; style?: TextStyle; } -const MessageTime = ({ timeFormat, style }: IMessageTime) => { +const MessageTime = ({ style }: IMessageTime) => { 'use memo'; const { colors } = useTheme(); const ts = useMessageField(item => item.ts); + const timeFormat = useTimeFormat(); const time = dayjs(ts).format(timeFormat); diff --git a/app/containers/message/User.tsx b/app/containers/message/User.tsx index 0d828c0fcfb..37bc38a0e37 100644 --- a/app/containers/message/User.tsx +++ b/app/containers/message/User.tsx @@ -48,11 +48,10 @@ const styles = StyleSheet.create({ }); interface IMessageUser { - timeFormat?: string; isReadReceiptEnabled?: boolean; } -const User = ({ timeFormat, isReadReceiptEnabled }: IMessageUser) => { +const User = ({ isReadReceiptEnabled }: IMessageUser) => { 'use memo'; const useRealName = useSetting('UI_Use_Real_Name') as boolean; @@ -101,7 +100,7 @@ const User = ({ timeFormat, isReadReceiptEnabled }: IMessageUser) => { <Text style={[styles.username, { color: colors.fontTitlesLabels }]} numberOfLines={1}> {textContent} </Text> - {isLargeFontScale ? null : <MessageTime timeFormat={timeFormat} ts={ts as Date | undefined} />} + {isLargeFontScale ? null : <MessageTime ts={ts as Date | undefined} />} </Pressable> <RightIcons type={type} diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 32798238530..25135abbc71 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -7,7 +7,6 @@ import { MessageProvider } from './MessageStore'; interface IMessageContainerProps { item: TAnyMessageModel; - timeFormat?: string; previousItem?: TAnyMessageModel; isReadReceiptEnabled?: boolean; isIgnored?: boolean; @@ -32,7 +31,6 @@ const MessageContainer = (props: IMessageContainerProps) => { onLongPress={props.onLongPress} threadBadgeColor={props.threadBadgeColor}> <Message - timeFormat={props.timeFormat} isReadReceiptEnabled={props.isReadReceiptEnabled} isPreview={props.isPreview} highlighted={props.highlighted} diff --git a/app/containers/message/interfaces.ts b/app/containers/message/interfaces.ts index 1397aad3c99..75ddda2d121 100644 --- a/app/containers/message/interfaces.ts +++ b/app/containers/message/interfaces.ts @@ -6,7 +6,6 @@ import { type IAttachment, type IUserMessage } from '../../definitions'; export interface IMessageAttachments { attachments?: IAttachment[]; - timeFormat?: string; author?: IUserMessage; } diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index d97d85a23d5..c68c1412fa0 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -164,7 +164,6 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { const { user } = this.props; const renderItemCommonProps = (item: TAnyMessageModel) => ({ item, - timeFormat: 'MMM Do YYYY, h:mm:ss a', onPress: () => this.jumpToMessage({ item }) }); diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index d8e0275ff6a..3d787d5f95a 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1410,7 +1410,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { renderItem = (item: TAnyMessageModel, previousItem: TAnyMessageModel, highlightedMessage?: string) => { const { room, lastOpen } = this.state; - const { Message_TimeFormat, Message_Read_Receipt_Enabled, inAppFeedback } = this.props; + const { Message_Read_Receipt_Enabled, inAppFeedback } = this.props; let dateSeparator = null; let showUnreadSeparator = false; const federated = 'id' in room && isRoomFederated(room); @@ -1459,7 +1459,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { isIgnored={this.isIgnored(item)} previousItem={previousItem} onLongPress={this.onMessageLongPress} - timeFormat={Message_TimeFormat} isReadReceiptEnabled={Message_Read_Receipt_Enabled && !federated} threadBadgeColor={this.getBadgeColor(item?.id)} highlighted={highlightedMessage === item.id} diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index a6552abe3db..37c1ef1e943 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -285,7 +285,7 @@ class SearchMessagesView extends Component<ISearchMessagesViewProps, ISearchMess renderItem = ({ item }: { item: IMessageFromServer | TMessageModel }) => { const message = item as TMessageModel; - return <Message item={message} timeFormat='MMM Do YYYY, h:mm:ss a' onPress={() => this.jumpToMessage({ item })} />; + return <Message item={message} onPress={() => this.jumpToMessage({ item })} />; }; renderList = () => { From a7a8bc7cd78179300de87d86f9f132d4da5c2611 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 20:48:19 -0300 Subject: [PATCH 046/144] refactor: read attachments from the store in the quote and attachments leaves (NATIVE-1385) --- .../message/Components/Attachments/Attachments.tsx | 6 ++++-- .../message/Components/Attachments/Quote.tsx | 6 ++++-- .../message/Components/Attachments/Reply.tsx | 4 ++-- app/containers/message/Message.tsx | 14 +++++++------- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/app/containers/message/Components/Attachments/Attachments.tsx b/app/containers/message/Components/Attachments/Attachments.tsx index e05c8bba9c1..7d3f72a50be 100644 --- a/app/containers/message/Components/Attachments/Attachments.tsx +++ b/app/containers/message/Components/Attachments/Attachments.tsx @@ -8,7 +8,7 @@ import CollapsibleQuote from './CollapsibleQuote'; import AttachedActions from './AttachedActions'; import Reply from './Reply'; import { useShowAttachment, useGetCustomEmoji } from '../../MessageRoomStore'; -import { useTranslateLanguage } from '../../MessageStore'; +import { useContentData, useTranslateLanguage } from '../../MessageStore'; import { type IMessageAttachments } from '../../interfaces'; import { getMessageFromAttachment } from '../../utils'; import { isContentAttachment } from './utils'; @@ -19,8 +19,10 @@ const Attachments: FC<IMessageAttachments> = ({ attachments, author }: IMessageA const translateLanguage = useTranslateLanguage(); const showAttachment = useShowAttachment(); const getCustomEmoji = useGetCustomEmoji() ?? (() => null); + const { attachments: storeAttachments } = useContentData(); + const resolved = attachments ?? storeAttachments; - const nonQuoteAttachments = attachments?.filter(isContentAttachment); + const nonQuoteAttachments = resolved?.filter(isContentAttachment); if (!nonQuoteAttachments || nonQuoteAttachments.length === 0) { return null; diff --git a/app/containers/message/Components/Attachments/Quote.tsx b/app/containers/message/Components/Attachments/Quote.tsx index 56cf1af872f..2eda7f26107 100644 --- a/app/containers/message/Components/Attachments/Quote.tsx +++ b/app/containers/message/Components/Attachments/Quote.tsx @@ -7,15 +7,17 @@ import { type IAttachment } from '../../../../definitions'; import { getMessageFromAttachment } from '../../utils'; import { isQuoteAttachment } from './utils'; import { useGetCustomEmoji } from '../../MessageRoomStore'; -import { useTranslateLanguage } from '../../MessageStore'; +import { useContentData, useTranslateLanguage } from '../../MessageStore'; const Quote: FC<IMessageAttachments> = ({ attachments }: IMessageAttachments) => { 'use memo'; const translateLanguage = useTranslateLanguage(); const getCustomEmoji = useGetCustomEmoji() ?? (() => null); + const { attachments: storeAttachments } = useContentData(); + const resolved = attachments ?? storeAttachments; - const quotes = attachments?.filter(isQuoteAttachment); + const quotes = resolved?.filter(isQuoteAttachment); if (!quotes || !quotes?.length) { return null; diff --git a/app/containers/message/Components/Attachments/Reply.tsx b/app/containers/message/Components/Attachments/Reply.tsx index 390fb48172b..346d4b7e3de 100644 --- a/app/containers/message/Components/Attachments/Reply.tsx +++ b/app/containers/message/Components/Attachments/Reply.tsx @@ -222,8 +222,8 @@ const Reply = ({ attachment, getCustomEmoji, msg }: IMessageReply) => { <View style={styles.titleAndDescriptionContainer}> <Title attachment={attachment} theme={theme} /> <Description attachment={attachment} getCustomEmoji={getCustomEmoji} /> - <Quote attachments={attachment.attachments} /> - <Attachments attachments={attachment.attachments} /> + <Quote attachments={attachment.attachments ?? []} /> + <Attachments attachments={attachment.attachments ?? []} /> <Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} /> {loading ? ( <View style={styles.backdrop}> diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index ed62d3268a0..7fdbda7e783 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -65,7 +65,7 @@ const MessageInner = (props: TMessageProps) => { const { isLargeFontScale } = useResponsiveLayout(); const isHeader = useMessageGrouping(); - const { attachments, t: type } = useContentData(); + const { t: type } = useContentData(); const { blocks } = useBlocks(); const { tmid } = useThreadData(); const { u: author } = useMessageAuthor(); @@ -78,9 +78,9 @@ const MessageInner = (props: TMessageProps) => { <User isReadReceiptEnabled={props.isReadReceiptEnabled} /> {showTimeLarge ? <MessageTime /> : null} <> - <Quote attachments={attachments} author={author} /> + <Quote /> <Content tmid={tmid} isIgnored={props.isIgnored} /> - <Attachments attachments={attachments} author={author} /> + <Attachments author={author} /> </> <Urls /> </> @@ -126,9 +126,9 @@ const MessageInner = (props: TMessageProps) => { <User isReadReceiptEnabled={props.isReadReceiptEnabled} /> {showTimeLarge ? <MessageTime /> : null} <View style={{ gap: 4 }}> - <Quote attachments={attachments} author={author} /> + <Quote /> <Content tmid={tmid} isIgnored={props.isIgnored} /> - <Attachments attachments={attachments} author={author} /> + <Attachments author={author} /> <Urls /> <Thread /> <Reactions /> @@ -149,7 +149,7 @@ const Message = (props: TMessageProps & IMessageA11y) => { const { isThreadReply, isThreadSequential } = useThreadPosition(); const isInfo = useIsInfo(); const { messageText, isTranslated } = useMessageText(); - const { attachments, t: type } = useContentData(); + const { t: type } = useContentData(); const { tmid } = useThreadData(); const { u: author } = useMessageAuthor(); const id = useMessageField(item => item.id); @@ -176,7 +176,7 @@ const Message = (props: TMessageProps & IMessageA11y) => { <Content tmid={tmid} isIgnored={props.isIgnored} /> {isInfo && type === 'message_pinned' ? ( <View pointerEvents='none'> - <Attachments attachments={attachments} author={author} /> + <Attachments author={author} /> </View> ) : null} </View> From 941c09073f277427f80e217890889449cfb6900e Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 20:49:50 -0300 Subject: [PATCH 047/144] refactor: drop dead argument interfaces from the urls and reactions leaves (NATIVE-1385) --- app/containers/message/Reactions.test.tsx | 2 +- app/containers/message/Reactions.tsx | 6 +----- app/containers/message/Urls.tsx | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/containers/message/Reactions.test.tsx b/app/containers/message/Reactions.test.tsx index 572ba669e61..2d9e744e173 100644 --- a/app/containers/message/Reactions.test.tsx +++ b/app/containers/message/Reactions.test.tsx @@ -61,7 +61,7 @@ const TestWrapper = ({ item, onReactionPress }: { item: FakeItem; onReactionPres onReactionLongPress: jest.fn(), getCustomEmoji }}> - <Reactions reactions={item.reactions} /> + <Reactions /> </MessageProviders> ); diff --git a/app/containers/message/Reactions.tsx b/app/containers/message/Reactions.tsx index 13b87b2a057..c4f4055455b 100644 --- a/app/containers/message/Reactions.tsx +++ b/app/containers/message/Reactions.tsx @@ -28,10 +28,6 @@ interface IMessageReaction { theme: TSupportedThemes; } -interface IMessageReactions { - reactions?: IReaction[]; -} - const AddReaction = ({ theme }: { theme: TSupportedThemes }) => { 'use memo'; @@ -97,7 +93,7 @@ const Reaction = ({ reaction, theme }: IMessageReaction) => { ); }; -const Reactions = (_props: IMessageReactions) => { +const Reactions = () => { 'use memo'; const { theme } = useTheme(); diff --git a/app/containers/message/Urls.tsx b/app/containers/message/Urls.tsx index 36b45f28328..cc46ea2f0f0 100644 --- a/app/containers/message/Urls.tsx +++ b/app/containers/message/Urls.tsx @@ -186,7 +186,7 @@ const Url = ({ url }: { url: IUrl }) => { </Touchable> ); }; -const Urls = (_props: { urls?: IUrl[] }): ReactElement[] | null => { +const Urls = (): ReactElement[] | null => { 'use memo'; const urls = useUrls(); From 85cdf4098ac07fb674a73d97f15f0b5c909ac6ef Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 20:53:12 -0300 Subject: [PATCH 048/144] refactor: read tmid from the thread store in the content leaf (NATIVE-1385) --- app/containers/message/Content.test.tsx | 15 +++++++++++---- app/containers/message/Content.tsx | 6 ++++-- app/containers/message/Message.tsx | 11 ++++------- app/containers/message/interfaces.ts | 1 - 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/app/containers/message/Content.test.tsx b/app/containers/message/Content.test.tsx index 4eed8525e4e..ef722f3d1cc 100644 --- a/app/containers/message/Content.test.tsx +++ b/app/containers/message/Content.test.tsx @@ -32,6 +32,7 @@ type TOverrides = Partial<IMessageContent> & { isHeader?: boolean; hasError?: boolean; autoTranslateLanguage?: string; + tmid?: string; }; const baseProps: TOverrides = { @@ -46,17 +47,23 @@ const baseProps: TOverrides = { hasError: false }; -const buildItem = (msg: string | undefined, attachments: IAttachment[] | undefined, isEncrypted: boolean | undefined) => +const buildItem = ( + msg: string | undefined, + attachments: IAttachment[] | undefined, + isEncrypted: boolean | undefined, + tmid: string | undefined +) => ({ id: 'msg-1', msg, attachments, t: isEncrypted ? E2E_MESSAGE_TYPE : undefined, - e2e: isEncrypted ? E2E_STATUS.PENDING : undefined + e2e: isEncrypted ? E2E_STATUS.PENDING : undefined, + tmid } as unknown as TAnyMessageModel); const tree = (overrides: TOverrides) => { - const { msg, attachments, isEncrypted, autoTranslateLanguage, ...contentProps } = overrides; + const { msg, attachments, isEncrypted, autoTranslateLanguage, tmid, ...contentProps } = overrides; const contextValue = { user: { username: 'john' }, onLinkPress: jest.fn(), @@ -67,7 +74,7 @@ const tree = (overrides: TOverrides) => { return ( <Provider store={mockedStore}> <MessageRoomProvider {...pickMessageRoomState(contextValue)}> - <MessageProvider item={buildItem(msg, attachments, isEncrypted)}> + <MessageProvider item={buildItem(msg, attachments, isEncrypted, tmid)}> <Content {...(baseProps as IMessageContent)} {...(contentProps as IMessageContent)} /> </MessageProvider> </MessageRoomProvider> diff --git a/app/containers/message/Content.tsx b/app/containers/message/Content.tsx index 3a67f9275ff..a92d6896d78 100644 --- a/app/containers/message/Content.tsx +++ b/app/containers/message/Content.tsx @@ -17,7 +17,8 @@ import { useMessageAuthor, useMessageStatus, useMessageText, - useOnLinkPress + useOnLinkPress, + useThreadData } from './MessageStore'; import { useAutoTranslate, useGetCustomEmoji, useIsThreadRoom, useMessageUser, useNavToRoomInfo } from './MessageRoomStore'; @@ -31,6 +32,7 @@ const Content = (props: IMessageContent) => { const getCustomEmoji = useGetCustomEmoji() ?? (() => null); const navToRoomInfo = useNavToRoomInfo(); const isThreadRoom = useIsThreadRoom(); + const { tmid } = useThreadData(); const { autoTranslateLanguage } = useAutoTranslate(); const isInfo = useIsInfo(); @@ -60,7 +62,7 @@ const Content = (props: IMessageContent) => { return renderMessageContent; } - const isPreview = props.tmid && !isThreadRoom; + const isPreview = tmid && !isThreadRoom; let content = null; if (isEncrypted) { diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index 7fdbda7e783..4afde7f5c0f 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -41,7 +41,6 @@ import { useMessagePress, useMessageStatus, useMessageText, - useThreadData, useThreadPosition } from './MessageStore'; @@ -67,7 +66,6 @@ const MessageInner = (props: TMessageProps) => { const isHeader = useMessageGrouping(); const { t: type } = useContentData(); const { blocks } = useBlocks(); - const { tmid } = useThreadData(); const { u: author } = useMessageAuthor(); const showTimeLarge = isLargeFontScale && isHeader; @@ -79,7 +77,7 @@ const MessageInner = (props: TMessageProps) => { {showTimeLarge ? <MessageTime /> : null} <> <Quote /> - <Content tmid={tmid} isIgnored={props.isIgnored} /> + <Content isIgnored={props.isIgnored} /> <Attachments author={author} /> </> <Urls /> @@ -101,7 +99,7 @@ const MessageInner = (props: TMessageProps) => { content = ( <> <User isReadReceiptEnabled={props.isReadReceiptEnabled} /> - <Content tmid={tmid} isIgnored={props.isIgnored} /> + <Content isIgnored={props.isIgnored} /> <CallButton /> {showTimeLarge ? <MessageTime /> : null} </> @@ -127,7 +125,7 @@ const MessageInner = (props: TMessageProps) => { {showTimeLarge ? <MessageTime /> : null} <View style={{ gap: 4 }}> <Quote /> - <Content tmid={tmid} isIgnored={props.isIgnored} /> + <Content isIgnored={props.isIgnored} /> <Attachments author={author} /> <Urls /> <Thread /> @@ -150,7 +148,6 @@ const Message = (props: TMessageProps & IMessageA11y) => { const isInfo = useIsInfo(); const { messageText, isTranslated } = useMessageText(); const { t: type } = useContentData(); - const { tmid } = useThreadData(); const { u: author } = useMessageAuthor(); const id = useMessageField(item => item.id); const isEdited = useIsEdited(); @@ -173,7 +170,7 @@ const Message = (props: TMessageProps & IMessageA11y) => { index={2} style={{ flex: 1 }}> <View style={styles.messageContent}> - <Content tmid={tmid} isIgnored={props.isIgnored} /> + <Content isIgnored={props.isIgnored} /> {isInfo && type === 'message_pinned' ? ( <View pointerEvents='none'> <Attachments author={author} /> diff --git a/app/containers/message/interfaces.ts b/app/containers/message/interfaces.ts index 75ddda2d121..5bd7716f2c0 100644 --- a/app/containers/message/interfaces.ts +++ b/app/containers/message/interfaces.ts @@ -14,7 +14,6 @@ export interface IMessageAvatar { } export interface IMessageContent { - tmid?: string; isIgnored: boolean; } From 5e764093f1f11b280d1426a5775063789ce14733 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 21:12:58 -0300 Subject: [PATCH 049/144] refactor: add isReadReceiptEnabled to the message room store (NATIVE-1385) --- app/containers/message/MessageRoomStore.tsx | 3 +++ app/views/RoomView/index.tsx | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/containers/message/MessageRoomStore.tsx b/app/containers/message/MessageRoomStore.tsx index 1859f829f32..36d1c37049d 100644 --- a/app/containers/message/MessageRoomStore.tsx +++ b/app/containers/message/MessageRoomStore.tsx @@ -28,6 +28,7 @@ export type MessageRoomState = { onAnswerButtonPress?: Function; onEncryptedPress?: () => void; archived?: boolean; + isReadReceiptEnabled?: boolean; // room constants rid?: string; user?: { id?: string; username?: string; token?: string }; @@ -64,6 +65,7 @@ const ROOM_STATE_KEYS: (keyof MessageRoomState)[] = [ 'onAnswerButtonPress', 'onEncryptedPress', 'archived', + 'isReadReceiptEnabled', 'rid', 'user', 'baseUrl', @@ -136,6 +138,7 @@ export const useOnAnswerButtonPress = (): MessageRoomState['onAnswerButtonPress' useMessageRoomStore(s => s.onAnswerButtonPress); export const useOnEncryptedPress = (): MessageRoomState['onEncryptedPress'] => useMessageRoomStore(s => s.onEncryptedPress); export const useArchived = (): boolean | undefined => useMessageRoomStore(s => s.archived); +export const useIsReadReceiptEnabled = (): boolean | undefined => useMessageRoomStore(s => s.isReadReceiptEnabled); export const useRid = (): string | undefined => useMessageRoomStore(s => s.rid); export const useMessageUser = (): MessageRoomState['user'] => useMessageRoomStore(s => s.user); diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 3d787d5f95a..aef281a5d4c 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1602,7 +1602,17 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { render() { console.count(`${this.constructor.name}.render calls`); const { room, isAutocompleteVisible, showMissingE2EEKey, showE2EEDisabledRoom, canAutoTranslate } = this.state; - const { user, baseUrl, theme, width, serverVersion, navigation, Message_GroupingPeriod, Message_TimeFormat } = this.props; + const { + user, + baseUrl, + theme, + width, + serverVersion, + navigation, + Message_GroupingPeriod, + Message_TimeFormat, + Message_Read_Receipt_Enabled + } = this.props; const { rid, t } = room; let bannerClosed; let announcement; @@ -1632,6 +1642,8 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { } } + const federated = 'id' in room && isRoomFederated(room); + return ( <InteractionStoreContext.Provider value={this.interactionStore}> <RoomContext.Provider @@ -1679,6 +1691,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { onAnswerButtonPress={this.handleSendMessage} onEncryptedPress={this.onEncryptedPress} archived={'id' in room && room.archived} + isReadReceiptEnabled={Message_Read_Receipt_Enabled && !federated} rid={rid} user={user as any} baseUrl={baseUrl} From 4de1b09cd492742958a9f60c9c779f977673cdd3 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 22:20:28 -0300 Subject: [PATCH 050/144] refactor: read isReadReceiptEnabled from the room store in the message leaves (NATIVE-1385) --- .../message/Components/RightIcons/Edited.tsx | 2 +- .../Components/RightIcons/Encrypted.tsx | 2 +- .../Components/RightIcons/MessageError.tsx | 2 +- .../message/Components/RightIcons/Pinned.tsx | 2 +- .../Components/RightIcons/ReadReceipt.tsx | 4 ++- .../Components/RightIcons/Translated.tsx | 2 +- .../message/Components/RightIcons/index.tsx | 31 ++++------------ app/containers/message/Message.tsx | 36 ++++--------------- app/containers/message/User.tsx | 30 +++------------- .../useMessageAccessibilityLabel.test.tsx | 24 +++++-------- .../hooks/useMessageAccessibilityLabel.ts | 5 +-- app/containers/message/index.tsx | 8 +---- app/views/RoomView/index.tsx | 4 +-- 13 files changed, 39 insertions(+), 113 deletions(-) diff --git a/app/containers/message/Components/RightIcons/Edited.tsx b/app/containers/message/Components/RightIcons/Edited.tsx index 4eb685f47a7..69692424678 100644 --- a/app/containers/message/Components/RightIcons/Edited.tsx +++ b/app/containers/message/Components/RightIcons/Edited.tsx @@ -4,7 +4,7 @@ import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; import { useIsEdited } from '../../MessageStore'; -const Edited = ({ testID }: { isEdited: boolean; testID?: string }) => { +const Edited = ({ testID }: { testID?: string }) => { 'use memo'; const isEdited = useIsEdited(); diff --git a/app/containers/message/Components/RightIcons/Encrypted.tsx b/app/containers/message/Components/RightIcons/Encrypted.tsx index 9cdfe01e893..0bd32cc8bce 100644 --- a/app/containers/message/Components/RightIcons/Encrypted.tsx +++ b/app/containers/message/Components/RightIcons/Encrypted.tsx @@ -6,7 +6,7 @@ import { E2E_MESSAGE_TYPE } from '../../../../lib/constants/keys'; import { useMessageField } from '../../MessageStore'; import { useOnEncryptedPress } from '../../MessageRoomStore'; -const Encrypted = (_props: { type: string }) => { +const Encrypted = () => { 'use memo'; const onEncryptedPress = useOnEncryptedPress(); diff --git a/app/containers/message/Components/RightIcons/MessageError.tsx b/app/containers/message/Components/RightIcons/MessageError.tsx index 43dc5017d46..a7c4d9e1b5b 100644 --- a/app/containers/message/Components/RightIcons/MessageError.tsx +++ b/app/containers/message/Components/RightIcons/MessageError.tsx @@ -7,7 +7,7 @@ import { useTheme } from '../../../../theme'; import { useMessageCtx, useMessageStatus } from '../../MessageStore'; import { useErrorActionsShow } from '../../MessageRoomStore'; -const MessageError = (_props: { hasError: boolean }) => { +const MessageError = () => { 'use memo'; const { theme } = useTheme(); diff --git a/app/containers/message/Components/RightIcons/Pinned.tsx b/app/containers/message/Components/RightIcons/Pinned.tsx index 343ca8021dc..26f9a4abb96 100644 --- a/app/containers/message/Components/RightIcons/Pinned.tsx +++ b/app/containers/message/Components/RightIcons/Pinned.tsx @@ -4,7 +4,7 @@ import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; import { useMessageField } from '../../MessageStore'; -const Pinned = ({ testID }: { pinned?: boolean; testID?: string }): ReactElement | null => { +const Pinned = ({ testID }: { testID?: string }): ReactElement | null => { 'use memo'; const pinned = useMessageField(item => item.pinned); diff --git a/app/containers/message/Components/RightIcons/ReadReceipt.tsx b/app/containers/message/Components/RightIcons/ReadReceipt.tsx index 9816ea4029f..bcbee82e96c 100644 --- a/app/containers/message/Components/RightIcons/ReadReceipt.tsx +++ b/app/containers/message/Components/RightIcons/ReadReceipt.tsx @@ -2,12 +2,14 @@ import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; import { useTheme } from '../../../../theme'; import { useMessageField } from '../../MessageStore'; +import { useIsReadReceiptEnabled } from '../../MessageRoomStore'; -const ReadReceipt = ({ isReadReceiptEnabled }: { isReadReceiptEnabled?: boolean; unread?: boolean }) => { +const ReadReceipt = () => { 'use memo'; const { colors } = useTheme(); const unread = useMessageField(item => item.unread); + const isReadReceiptEnabled = useIsReadReceiptEnabled(); const isUnread = unread || unread === null; const iconName = isUnread ? 'check-single' : 'check-double'; const iconColor = isUnread ? colors.fontAnnotation : colors.fontInfo; diff --git a/app/containers/message/Components/RightIcons/Translated.tsx b/app/containers/message/Components/RightIcons/Translated.tsx index 92d280910b7..9163f424885 100644 --- a/app/containers/message/Components/RightIcons/Translated.tsx +++ b/app/containers/message/Components/RightIcons/Translated.tsx @@ -4,7 +4,7 @@ import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; import { useMessageText } from '../../MessageStore'; -const Translated = (_props: { isTranslated: boolean }) => { +const Translated = () => { 'use memo'; const { isTranslated } = useMessageText(); diff --git a/app/containers/message/Components/RightIcons/index.tsx b/app/containers/message/Components/RightIcons/index.tsx index 6504a981597..20dd7376532 100644 --- a/app/containers/message/Components/RightIcons/index.tsx +++ b/app/containers/message/Components/RightIcons/index.tsx @@ -1,7 +1,6 @@ import { type ReactElement } from 'react'; import { StyleSheet, View } from 'react-native'; -import { type MessageType } from '../../../../definitions'; import Edited from './Edited'; import Encrypted from './Encrypted'; import MessageError from './MessageError'; @@ -16,36 +15,20 @@ const styles = StyleSheet.create({ }); interface IRightIcons { - type: MessageType; msg?: string; - isEdited: boolean; - isReadReceiptEnabled?: boolean; - unread?: boolean; - hasError: boolean; - isTranslated: boolean; - pinned?: boolean; } -const RightIcons = ({ - type, - msg, - isEdited, - hasError, - isReadReceiptEnabled, - unread, - isTranslated, - pinned -}: IRightIcons): ReactElement => { +const RightIcons = ({ msg }: IRightIcons): ReactElement => { 'use memo'; return ( <View style={styles.actionIcons}> - <Pinned pinned={pinned} testID={`${msg}-pinned`} /> - <Encrypted type={type} /> - <Edited testID={`${msg}-edited`} isEdited={isEdited} /> - <MessageError hasError={hasError} /> - <Translated isTranslated={isTranslated} /> - <ReadReceipt isReadReceiptEnabled={isReadReceiptEnabled} unread={unread} /> + <Pinned testID={`${msg}-pinned`} /> + <Encrypted /> + <Edited testID={`${msg}-edited`} /> + <MessageError /> + <Translated /> + <ReadReceipt /> </View> ); }; diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index 4afde7f5c0f..027d480b6c0 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -31,13 +31,11 @@ import { useArchived, useAutoTranslate } from './MessageRoomStore'; import { useBlocks, useContentData, - useIsEdited, useIsInfo, useMessageAuthor, useMessageField, useMessageGrouping, useMessageLongPress, - useMessageMeta, useMessagePress, useMessageStatus, useMessageText, @@ -45,7 +43,6 @@ import { } from './MessageStore'; type TMessageProps = { - isReadReceiptEnabled?: boolean; isPreview?: boolean; highlighted?: boolean; isIgnored: boolean; @@ -73,7 +70,7 @@ const MessageInner = (props: TMessageProps) => { if (props.isPreview) { content = ( <> - <User isReadReceiptEnabled={props.isReadReceiptEnabled} /> + <User /> {showTimeLarge ? <MessageTime /> : null} <> <Quote /> @@ -88,7 +85,7 @@ const MessageInner = (props: TMessageProps) => { if (type === 'discussion-created') { content = ( <> - <User isReadReceiptEnabled={props.isReadReceiptEnabled} /> + <User /> {showTimeLarge ? <MessageTime /> : null} <Discussion /> </> @@ -98,7 +95,7 @@ const MessageInner = (props: TMessageProps) => { if (type === 'jitsi_call_started') { content = ( <> - <User isReadReceiptEnabled={props.isReadReceiptEnabled} /> + <User /> <Content isIgnored={props.isIgnored} /> <CallButton /> {showTimeLarge ? <MessageTime /> : null} @@ -109,7 +106,7 @@ const MessageInner = (props: TMessageProps) => { if (blocks && blocks.length) { content = ( <> - <User isReadReceiptEnabled={props.isReadReceiptEnabled} /> + <User /> <Blocks /> <Thread /> <Reactions /> @@ -121,7 +118,7 @@ const MessageInner = (props: TMessageProps) => { if (!content) { content = ( <> - <User isReadReceiptEnabled={props.isReadReceiptEnabled} /> + <User /> {showTimeLarge ? <MessageTime /> : null} <View style={{ gap: 4 }}> <Quote /> @@ -150,9 +147,6 @@ const Message = (props: TMessageProps & IMessageA11y) => { const { t: type } = useContentData(); const { u: author } = useMessageAuthor(); const id = useMessageField(item => item.id); - const isEdited = useIsEdited(); - const { hasError } = useMessageStatus(); - const { unread, pinned } = useMessageMeta(); const { autoTranslateLanguage } = useAutoTranslate(); if (isThreadReply || isThreadSequential || isInfo || props.isIgnored) { @@ -194,7 +188,6 @@ const Message = (props: TMessageProps & IMessageA11y) => { <MessageAvatar /> <View style={styles.messageContent}> <MessageInner - isReadReceiptEnabled={props.isReadReceiptEnabled} isPreview={props.isPreview} highlighted={props.highlighted} isIgnored={props.isIgnored} @@ -202,18 +195,7 @@ const Message = (props: TMessageProps & IMessageA11y) => { small={props.small} /> </View> - {!isHeader ? ( - <RightIcons - type={type} - msg={messageText} - isEdited={isEdited} - hasError={hasError} - isReadReceiptEnabled={props.isReadReceiptEnabled} - unread={unread} - pinned={pinned} - isTranslated={isTranslated} - /> - ) : null} + {!isHeader ? <RightIcons msg={messageText} /> : null} </View> </A11y.Index> </View> @@ -238,9 +220,7 @@ const MessageTouchable = (props: TMessageProps) => { const revealIgnored = () => setIsManualUnignored(true); const onPressAction = useMessagePress({ isIgnored, revealIgnored }); const onLongPress = useMessageLongPress(); - const accessibilityLabelValue = useMessageAccessibilityLabel({ - isReadReceiptEnabled: props.isReadReceiptEnabled - }); + const accessibilityLabelValue = useMessageAccessibilityLabel(); const isDisabled = (isInfo && !isThreadReply) || archived || isTemp || type === 'jitsi_call_started'; const accessibilityActions = useMessageAccessibilityActions(isDisabled); const accessibilityHint = useMessageAccessibilityHint(); @@ -257,7 +237,6 @@ const MessageTouchable = (props: TMessageProps) => { return ( <A11y.Order> <Message - isReadReceiptEnabled={props.isReadReceiptEnabled} isPreview={props.isPreview} highlighted={props.highlighted} isIgnored={isIgnored} @@ -292,7 +271,6 @@ const MessageTouchable = (props: TMessageProps) => { if (e.nativeEvent.actionName === 'showActions') handleLongPress(); }}> <Message - isReadReceiptEnabled={props.isReadReceiptEnabled} isPreview={props.isPreview} highlighted={props.highlighted} isIgnored={isIgnored} diff --git a/app/containers/message/User.tsx b/app/containers/message/User.tsx index 37bc38a0e37..7b03d8132af 100644 --- a/app/containers/message/User.tsx +++ b/app/containers/message/User.tsx @@ -8,14 +8,7 @@ import { messageHaveAuthorName } from './utils'; import MessageTime from './Time'; import { useResponsiveLayout } from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; import { useSetting } from '../../lib/hooks/useSetting'; -import { - useIsEdited, - useMessageAuthor, - useMessageGrouping, - useMessageMeta, - useMessageStatus, - useMessageText -} from './MessageStore'; +import { useMessageAuthor, useMessageGrouping, useMessageMeta } from './MessageStore'; import { useMessageUser, useNavToRoomInfo } from './MessageRoomStore'; const styles = StyleSheet.create({ @@ -47,11 +40,7 @@ const styles = StyleSheet.create({ } }); -interface IMessageUser { - isReadReceiptEnabled?: boolean; -} - -const User = ({ isReadReceiptEnabled }: IMessageUser) => { +const User = () => { 'use memo'; const useRealName = useSetting('UI_Use_Real_Name') as boolean; @@ -61,10 +50,7 @@ const User = ({ isReadReceiptEnabled }: IMessageUser) => { const { isLargeFontScale } = useResponsiveLayout(); const isHeader = useMessageGrouping(); const { u: author, alias } = useMessageAuthor(); - const { hasError } = useMessageStatus(); - const isEdited = useIsEdited(); - const { ts, unread, pinned, t: type } = useMessageMeta(); - const { isTranslated } = useMessageText(); + const { ts, t: type } = useMessageMeta(); if (isHeader) { const username = (useRealName && author?.name) || author?.username; @@ -102,15 +88,7 @@ const User = ({ isReadReceiptEnabled }: IMessageUser) => { </Text> {isLargeFontScale ? null : <MessageTime ts={ts as Date | undefined} />} </Pressable> - <RightIcons - type={type} - isEdited={isEdited} - hasError={hasError} - isReadReceiptEnabled={isReadReceiptEnabled} - unread={unread} - pinned={pinned} - isTranslated={isTranslated} - /> + <RightIcons /> </View> ); } diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx b/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx index 0462eea6771..6b98ccf1a09 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx @@ -13,8 +13,6 @@ jest.mock('../../../lib/hooks/useAltTextSupported', () => ({ useAltTextSupported: () => false })); -type TArgs = { isReadReceiptEnabled?: boolean }; - const FIXED_TS = new Date('2024-01-15T12:34:56Z'); const HOUR = FIXED_TS.toLocaleTimeString(); @@ -28,15 +26,10 @@ const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel ...overrides } as TAnyMessageModel); -const renderLabel = ( - item: TAnyMessageModel, - args: TArgs = {}, - config: Record<string, any> = {}, - previousItem?: TAnyMessageModel -) => { +const renderLabel = (item: TAnyMessageModel, config: Record<string, any> = {}, previousItem?: TAnyMessageModel) => { const spy = jest.fn(); const Probe = () => { - spy(useMessageAccessibilityLabel(args)); + spy(useMessageAccessibilityLabel()); return null; }; render( @@ -128,11 +121,11 @@ describe('useMessageAccessibilityLabel', () => { it('builds a translated message with only user, hour and translated marker as prefix', () => { expect( - renderLabel( - buildItem({ autoTranslate: true, translations: [{ _id: 't1', language: 'en', value: 'translated text' }] }), - {}, - { autoTranslateRoom: true, autoTranslateLanguage: 'en', user: { username: 'bob' } } - ) + renderLabel(buildItem({ autoTranslate: true, translations: [{ _id: 't1', language: 'en', value: 'translated text' }] }), { + autoTranslateRoom: true, + autoTranslateLanguage: 'en', + user: { username: 'bob' } + }) ).toBe(`alice ${HOUR} Message translated into English.`); }); @@ -146,8 +139,7 @@ describe('useMessageAccessibilityLabel', () => { unread: true, attachments: [{ image_url: 'https://example.com/img.png', altText: 'A wavy pattern' }] }), - { isReadReceiptEnabled: true }, - { autoTranslateRoom: true, autoTranslateLanguage: 'en', user: { username: 'bob' } } + { isReadReceiptEnabled: true, autoTranslateRoom: true, autoTranslateLanguage: 'en', user: { username: 'bob' } } ) ).toBe(`alice ${HOUR} Message translated into English. Image description: A wavy pattern Message was not read`); }); diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.ts b/app/containers/message/hooks/useMessageAccessibilityLabel.ts index f6e6c4fe67d..3c803bd2aa6 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.ts +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.ts @@ -13,7 +13,7 @@ import { useThreadData, useThreadPosition } from '../MessageStore'; -import { useAutoTranslate } from '../MessageRoomStore'; +import { useAutoTranslate, useIsReadReceiptEnabled } from '../MessageRoomStore'; import { useSetting } from '../../../lib/hooks/useSetting'; const stripMentions = (label: string, mentions: IUserMention[] = [], channels: IUserChannel[] = []) => { @@ -31,7 +31,7 @@ const stripMentions = (label: string, mentions: IUserMention[] = [], channels: I return result; }; -export const useMessageAccessibilityLabel = ({ isReadReceiptEnabled }: { isReadReceiptEnabled?: boolean }): string => { +export const useMessageAccessibilityLabel = (): string => { const useRealName = useSetting('UI_Use_Real_Name') as boolean; const { autoTranslateLanguage } = useAutoTranslate(); const { attachments, mentions, channels, comment, t: type } = useContentData(); @@ -42,6 +42,7 @@ export const useMessageAccessibilityLabel = ({ isReadReceiptEnabled }: { isReadR const isInfo = useIsInfo(); const isEncrypted = useIsEncrypted(); const { ts, unread } = useMessageMeta(); + const isReadReceiptEnabled = useIsReadReceiptEnabled(); const imageDescriptionLabel = useImageDescriptionLabel(attachments, messageText); const msg = messageText || ''; diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 25135abbc71..767cc0b8f57 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -8,7 +8,6 @@ import { MessageProvider } from './MessageStore'; interface IMessageContainerProps { item: TAnyMessageModel; previousItem?: TAnyMessageModel; - isReadReceiptEnabled?: boolean; isIgnored?: boolean; highlighted?: boolean; onLongPress?: (item: TAnyMessageModel) => void; @@ -30,12 +29,7 @@ const MessageContainer = (props: IMessageContainerProps) => { onPress={props.onPress} onLongPress={props.onLongPress} threadBadgeColor={props.threadBadgeColor}> - <Message - isReadReceiptEnabled={props.isReadReceiptEnabled} - isPreview={props.isPreview} - highlighted={props.highlighted} - isIgnored={props.isIgnored ?? false} - /> + <Message isPreview={props.isPreview} highlighted={props.highlighted} isIgnored={props.isIgnored ?? false} /> <MessageSeparator ts={dateSeparator} unread={showUnreadSeparator} /> </MessageProvider> ); diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index aef281a5d4c..94fd6d61842 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1410,10 +1410,9 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { renderItem = (item: TAnyMessageModel, previousItem: TAnyMessageModel, highlightedMessage?: string) => { const { room, lastOpen } = this.state; - const { Message_Read_Receipt_Enabled, inAppFeedback } = this.props; + const { inAppFeedback } = this.props; let dateSeparator = null; let showUnreadSeparator = false; - const federated = 'id' in room && isRoomFederated(room); if (!previousItem) { dateSeparator = item.ts; @@ -1459,7 +1458,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { isIgnored={this.isIgnored(item)} previousItem={previousItem} onLongPress={this.onMessageLongPress} - isReadReceiptEnabled={Message_Read_Receipt_Enabled && !federated} threadBadgeColor={this.getBadgeColor(item?.id)} highlighted={highlightedMessage === item.id} dateSeparator={dateSeparator} From 5ae983213f34dae3a7e724c21e65df472f03af85 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 2 Jul 2026 22:28:53 -0300 Subject: [PATCH 051/144] refactor: lift the ignored flag into the message store (NATIVE-1385) --- app/containers/message/Content.test.tsx | 22 +++---------- app/containers/message/Content.tsx | 7 ++-- app/containers/message/Message.tsx | 29 ++++++----------- app/containers/message/MessageStore.tsx | 43 +++++++++++++++++-------- app/containers/message/index.tsx | 5 +-- app/containers/message/interfaces.ts | 4 --- 6 files changed, 51 insertions(+), 59 deletions(-) diff --git a/app/containers/message/Content.test.tsx b/app/containers/message/Content.test.tsx index ef722f3d1cc..229c0f4c335 100644 --- a/app/containers/message/Content.test.tsx +++ b/app/containers/message/Content.test.tsx @@ -8,7 +8,6 @@ import { setUser } from '../../actions/login'; import { mockedStore } from '../../reducers/mockedStore'; import { type IAttachment, type TAnyMessageModel } from '../../definitions'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; -import { type IMessageContent } from './interfaces'; mockedStore.dispatch( setUser({ @@ -20,7 +19,7 @@ mockedStore.dispatch( }) ); -type TOverrides = Partial<IMessageContent> & { +type TOverrides = { attachments?: IAttachment[]; isEncrypted?: boolean; msg?: string; @@ -33,18 +32,7 @@ type TOverrides = Partial<IMessageContent> & { hasError?: boolean; autoTranslateLanguage?: string; tmid?: string; -}; - -const baseProps: TOverrides = { - _id: 'msg-1', - isTemp: false, - isInfo: false, - isEdited: false, - isEncrypted: false, - isIgnored: false, - isTranslated: false, - isHeader: false, - hasError: false + isIgnored?: boolean; }; const buildItem = ( @@ -63,7 +51,7 @@ const buildItem = ( } as unknown as TAnyMessageModel); const tree = (overrides: TOverrides) => { - const { msg, attachments, isEncrypted, autoTranslateLanguage, tmid, ...contentProps } = overrides; + const { msg, attachments, isEncrypted, autoTranslateLanguage, tmid, isIgnored } = overrides; const contextValue = { user: { username: 'john' }, onLinkPress: jest.fn(), @@ -74,8 +62,8 @@ const tree = (overrides: TOverrides) => { return ( <Provider store={mockedStore}> <MessageRoomProvider {...pickMessageRoomState(contextValue)}> - <MessageProvider item={buildItem(msg, attachments, isEncrypted, tmid)}> - <Content {...(baseProps as IMessageContent)} {...(contentProps as IMessageContent)} /> + <MessageProvider item={buildItem(msg, attachments, isEncrypted, tmid)} isIgnored={isIgnored}> + <Content /> </MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/Content.tsx b/app/containers/message/Content.tsx index a92d6896d78..bef5e89ca55 100644 --- a/app/containers/message/Content.tsx +++ b/app/containers/message/Content.tsx @@ -5,7 +5,6 @@ import styles from './styles'; import Markdown, { MarkdownPreview } from '../markdown'; import User from './User'; import { messageHaveAuthorName, getInfoMessage, getPreviewMessageFromAttachment } from './utils'; -import { type IMessageContent } from './interfaces'; import { useTheme } from '../../theme'; import { themes } from '../../lib/constants/colors'; import { useSetting } from '../../lib/hooks/useSetting'; @@ -15,6 +14,7 @@ import { useIsEncrypted, useIsInfo, useMessageAuthor, + useMessageIgnored, useMessageStatus, useMessageText, useOnLinkPress, @@ -22,7 +22,7 @@ import { } from './MessageStore'; import { useAutoTranslate, useGetCustomEmoji, useIsThreadRoom, useMessageUser, useNavToRoomInfo } from './MessageRoomStore'; -const Content = (props: IMessageContent) => { +const Content = () => { 'use memo'; const { theme } = useTheme(); @@ -41,6 +41,7 @@ const Content = (props: IMessageContent) => { const { isTemp } = useMessageStatus(); const { messageText, isTranslated } = useMessageText(); const { u: author, role } = useMessageAuthor(); + const isIgnored = useMessageIgnored(); if (isInfo) { // @ts-ignore @@ -95,7 +96,7 @@ const Content = (props: IMessageContent) => { ); } - if (props.isIgnored) { + if (isIgnored) { content = ( <Text style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} testID={`message-ignored-${messageText}`}> {I18n.t('Message_Ignored')} diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index 027d480b6c0..a187c6ee8dc 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import { View, type ViewStyle, type AccessibilityActionEvent, type AccessibilityActionInfo } from 'react-native'; import { A11y } from 'react-native-a11y-order'; @@ -35,6 +34,7 @@ import { useMessageAuthor, useMessageField, useMessageGrouping, + useMessageIgnored, useMessageLongPress, useMessagePress, useMessageStatus, @@ -45,7 +45,6 @@ import { type TMessageProps = { isPreview?: boolean; highlighted?: boolean; - isIgnored: boolean; isBeingEdited?: boolean; small?: boolean; }; @@ -74,7 +73,7 @@ const MessageInner = (props: TMessageProps) => { {showTimeLarge ? <MessageTime /> : null} <> <Quote /> - <Content isIgnored={props.isIgnored} /> + <Content /> <Attachments author={author} /> </> <Urls /> @@ -96,7 +95,7 @@ const MessageInner = (props: TMessageProps) => { content = ( <> <User /> - <Content isIgnored={props.isIgnored} /> + <Content /> <CallButton /> {showTimeLarge ? <MessageTime /> : null} </> @@ -122,7 +121,7 @@ const MessageInner = (props: TMessageProps) => { {showTimeLarge ? <MessageTime /> : null} <View style={{ gap: 4 }}> <Quote /> - <Content isIgnored={props.isIgnored} /> + <Content /> <Attachments author={author} /> <Urls /> <Thread /> @@ -148,8 +147,9 @@ const Message = (props: TMessageProps & IMessageA11y) => { const { u: author } = useMessageAuthor(); const id = useMessageField(item => item.id); const { autoTranslateLanguage } = useAutoTranslate(); + const isIgnored = useMessageIgnored(); - if (isThreadReply || isThreadSequential || isInfo || props.isIgnored) { + if (isThreadReply || isThreadSequential || isInfo || isIgnored) { const thread = isThreadReply ? <RepliedThread isHeader={isHeader} /> : null; const infoStyle: ViewStyle = isInfo ? { alignItems: 'center' } : {}; return ( @@ -164,7 +164,7 @@ const Message = (props: TMessageProps & IMessageA11y) => { index={2} style={{ flex: 1 }}> <View style={styles.messageContent}> - <Content isIgnored={props.isIgnored} /> + <Content /> {isInfo && type === 'message_pinned' ? ( <View pointerEvents='none'> <Attachments author={author} /> @@ -190,7 +190,6 @@ const Message = (props: TMessageProps & IMessageA11y) => { <MessageInner isPreview={props.isPreview} highlighted={props.highlighted} - isIgnored={props.isIgnored} isBeingEdited={props.isBeingEdited} small={props.small} /> @@ -215,10 +214,7 @@ const MessageTouchable = (props: TMessageProps) => { const type = useMessageField(item => item.t); const id = useMessageField(item => item.id); const isBeingEdited = useIsBeingEdited(id); - const [isManualUnignored, setIsManualUnignored] = useState(false); - const isIgnored = isManualUnignored ? false : props.isIgnored ?? false; - const revealIgnored = () => setIsManualUnignored(true); - const onPressAction = useMessagePress({ isIgnored, revealIgnored }); + const onPressAction = useMessagePress(); const onLongPress = useMessageLongPress(); const accessibilityLabelValue = useMessageAccessibilityLabel(); const isDisabled = (isInfo && !isThreadReply) || archived || isTemp || type === 'jitsi_call_started'; @@ -236,13 +232,7 @@ const MessageTouchable = (props: TMessageProps) => { if (hasError || isInfo) { return ( <A11y.Order> - <Message - isPreview={props.isPreview} - highlighted={props.highlighted} - isIgnored={isIgnored} - isBeingEdited={isBeingEdited} - small={props.small} - /> + <Message isPreview={props.isPreview} highlighted={props.highlighted} isBeingEdited={isBeingEdited} small={props.small} /> </A11y.Order> ); } @@ -273,7 +263,6 @@ const MessageTouchable = (props: TMessageProps) => { <Message isPreview={props.isPreview} highlighted={props.highlighted} - isIgnored={isIgnored} isBeingEdited={isBeingEdited} small={props.small} handleLongPress={!isDisabled ? handleLongPress : undefined} diff --git a/app/containers/message/MessageStore.tsx b/app/containers/message/MessageStore.tsx index 0928fc7b789..e6d9016ec94 100644 --- a/app/containers/message/MessageStore.tsx +++ b/app/containers/message/MessageStore.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useEffect, useRef, useState, type ReactElement, type ReactNode } from 'react'; +import { createContext, useContext, useCallback, useEffect, useRef, useState, type ReactElement, type ReactNode } from 'react'; import { createStore, useStore } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; import { Keyboard } from 'react-native'; @@ -25,17 +25,24 @@ import { type MessageStoreState = { tick: number; + manualUnignored: boolean; onPress?: () => void; onLongPress?: (item: TAnyMessageModel) => void; threadBadgeColor?: string; + ignoredSeed: boolean; }; const createMessageStore = (initial: Partial<MessageStoreState>) => - createStore<MessageStoreState>(() => ({ tick: 0, ...initial })); + createStore<MessageStoreState>(() => ({ tick: 0, manualUnignored: false, ...initial })); type MessageStore = ReturnType<typeof createMessageStore>; -type MessageCtxValue = { store: MessageStore; item: TAnyMessageModel; previousItem?: TAnyMessageModel }; +type MessageCtxValue = { + store: MessageStore; + item: TAnyMessageModel; + previousItem?: TAnyMessageModel; + revealIgnored: () => void; +}; const MessageStoreContext = createContext<MessageCtxValue | null>(null); @@ -71,6 +78,7 @@ export const MessageProvider = ({ onPress, onLongPress, threadBadgeColor, + isIgnored, children }: { item: TAnyMessageModel; @@ -78,11 +86,12 @@ export const MessageProvider = ({ onPress?: () => void; onLongPress?: (item: TAnyMessageModel) => void; threadBadgeColor?: string; + isIgnored?: boolean; children: ReactNode; }): ReactElement => { 'use memo'; - const [store] = useState(() => createMessageStore({ onPress, onLongPress, threadBadgeColor })); + const [store] = useState(() => createMessageStore({ onPress, onLongPress, threadBadgeColor, ignoredSeed: isIgnored ?? false })); // Header grouping and thread position depend on the previous record too, so each effect // subscribes one record; both feed the same tick. Keeping them separate means changing @@ -92,10 +101,14 @@ export const MessageProvider = ({ // Mirror per-message row handlers so field-level selectors subscribe without churning the context value. useEffect(() => { - store.setState({ onPress, onLongPress, threadBadgeColor }); + store.setState({ onPress, onLongPress, threadBadgeColor, ignoredSeed: isIgnored ?? false }); }); - return <MessageStoreContext.Provider value={{ store, item, previousItem }}>{children}</MessageStoreContext.Provider>; + const revealIgnored = useCallback(() => store.setState({ manualUnignored: true }), [store]); + + return ( + <MessageStoreContext.Provider value={{ store, item, previousItem, revealIgnored }}>{children}</MessageStoreContext.Provider> + ); }; export const useReactions = (): TAnyMessageModel['reactions'] => useMessageField(item => item.reactions); @@ -298,6 +311,14 @@ export const useThreadBadgeColor = (): string | undefined => { const { store } = useMessageCtx(); return useStore(store, s => s.threadBadgeColor); }; +export const useMessageIgnored = (): boolean => { + const { store } = useMessageCtx(); + return useStore(store, s => (s.manualUnignored ? false : s.ignoredSeed)); +}; +export const useRevealIgnored = (): (() => void) => { + const { revealIgnored } = useMessageCtx(); + return revealIgnored; +}; export const useMessageLongPress = (): (() => void) => { const { item } = useMessageCtx(); @@ -327,19 +348,15 @@ export const useOnLinkPress = (): ((link: string) => void) => { }; }; -export const useMessagePress = ({ - isIgnored, - revealIgnored -}: { - isIgnored: boolean; - revealIgnored: () => void; -}): (() => void) => { +export const useMessagePress = (): (() => void) => { const { item } = useMessageCtx(); const isThreadRoom = useIsThreadRoom(); const onPress = useOnPressRaw(); const onThreadPress = useOnThreadPress(); const onDiscussionPress = useOnDiscussionPress(); const closeEmojiAndAction = useCloseEmojiAndAction(); + const isIgnored = useMessageIgnored(); + const revealIgnored = useRevealIgnored(); // pressHandlerRef holds the latest press logic; updated after every render via // useEffect so the render body stays free of ref writes. diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 767cc0b8f57..4df540aa3f5 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -28,8 +28,9 @@ const MessageContainer = (props: IMessageContainerProps) => { previousItem={previousItem} onPress={props.onPress} onLongPress={props.onLongPress} - threadBadgeColor={props.threadBadgeColor}> - <Message isPreview={props.isPreview} highlighted={props.highlighted} isIgnored={props.isIgnored ?? false} /> + threadBadgeColor={props.threadBadgeColor} + isIgnored={props.isIgnored}> + <Message isPreview={props.isPreview} highlighted={props.highlighted} /> <MessageSeparator ts={dateSeparator} unread={showUnreadSeparator} /> </MessageProvider> ); diff --git a/app/containers/message/interfaces.ts b/app/containers/message/interfaces.ts index 5bd7716f2c0..25e2adfefbc 100644 --- a/app/containers/message/interfaces.ts +++ b/app/containers/message/interfaces.ts @@ -13,10 +13,6 @@ export interface IMessageAvatar { small?: boolean; } -export interface IMessageContent { - isIgnored: boolean; -} - export interface IMessageEmoji { content: string; standardEmojiStyle: { fontSize: number }; From 9c4386e86fff22b20d07315468f594dc0dc8900b Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 09:14:35 -0300 Subject: [PATCH 052/144] refactor: drop stale reusable-button todos from message components (NATIVE-1385) --- app/containers/message/Broadcast.tsx | 1 - app/containers/message/Discussion.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/app/containers/message/Broadcast.tsx b/app/containers/message/Broadcast.tsx index 3a05ffbc478..415bf423c1f 100644 --- a/app/containers/message/Broadcast.tsx +++ b/app/containers/message/Broadcast.tsx @@ -10,7 +10,6 @@ import { useTheme } from '../../theme'; import { useMessageAuthor, useMessageCtx } from './MessageStore'; import { useBroadcast, useMessageUser, useReplyBroadcast } from './MessageRoomStore'; -// TODO: Create a reusable button component for message const Broadcast = () => { 'use memo'; diff --git a/app/containers/message/Discussion.tsx b/app/containers/message/Discussion.tsx index 0d83e324aac..deadd3c43e1 100644 --- a/app/containers/message/Discussion.tsx +++ b/app/containers/message/Discussion.tsx @@ -11,7 +11,6 @@ import { useTheme } from '../../theme'; import { useDiscussion, useMessageCtx, useMessageText } from './MessageStore'; import { useOnDiscussionPress } from './MessageRoomStore'; -// TODO: Create a reusable button component for message const Discussion = () => { 'use memo'; From e4048707a9ee7fb0ea6f80904d587e993c4b31ae Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 10:38:28 -0300 Subject: [PATCH 053/144] refactor: drop manual memo and tighten ts-ignore notes in the message store (NATIVE-1376) The provider already opts into the React Compiler via 'use memo', so the hand-rolled useCallback on revealIgnored is redundant; inline it and let the compiler memoize. Replace the two stale `@ts-ignore TODO` comments with `@ts-expect-error` carrying the real reason (IMessage types ts as Date, IMessageFromServer as string; the date op is valid at runtime). @ts-expect-error also fails if the underlying type mismatch ever goes away, so the suppression cannot rot. --- app/containers/message/MessageStore.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/containers/message/MessageStore.tsx b/app/containers/message/MessageStore.tsx index e6d9016ec94..e3b27ed2e9a 100644 --- a/app/containers/message/MessageStore.tsx +++ b/app/containers/message/MessageStore.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useCallback, useEffect, useRef, useState, type ReactElement, type ReactNode } from 'react'; +import { createContext, useContext, useEffect, useRef, useState, type ReactElement, type ReactNode } from 'react'; import { createStore, useStore } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; import { Keyboard } from 'react-native'; @@ -104,7 +104,7 @@ export const MessageProvider = ({ store.setState({ onPress, onLongPress, threadBadgeColor, ignoredSeed: isIgnored ?? false }); }); - const revealIgnored = useCallback(() => store.setState({ manualUnignored: true }), [store]); + const revealIgnored = () => store.setState({ manualUnignored: true }); return ( <MessageStoreContext.Provider value={{ store, item, previousItem, revealIgnored }}>{children}</MessageStoreContext.Provider> @@ -199,11 +199,11 @@ const computeIsHeader = ( try { if ( prev && - // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense + // @ts-expect-error IMessage types ts as Date, IMessageFromServer as string; the date op is valid at runtime prev.ts.toDateString() === item.ts.toDateString() && prev.u?.username === item.u?.username && !(prev.groupable === false || item.groupable === false || broadcast === true) && - // @ts-ignore TODO: IMessage vs IMessageFromServer non-sense + // @ts-expect-error IMessage types ts as Date, IMessageFromServer as string; the date op is valid at runtime item.ts - prev.ts < Message_GroupingPeriod * 1000 && prev.tmid === item.tmid && item.t !== 'rm' && From 47fdf5f4a3c5dd4cdac2f8ecc26a42552cf3d392 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 12:15:01 -0300 Subject: [PATCH 054/144] refactor: make Quote and Attachments pure prop-driven Remove the useContentData store fallback from Quote and Attachments and pass the message's attachments explicitly from the Message.tsx callers, which already subscribe to the content bag. This drops the ?? [] laundering in Reply, where an undefined nested-attachment prop used to fall through to the current message's store attachments and render the wrong quote. --- .../message/Components/Attachments/Attachments.tsx | 6 ++---- .../message/Components/Attachments/Quote.tsx | 6 ++---- .../message/Components/Attachments/Reply.tsx | 4 ++-- app/containers/message/Message.tsx | 14 +++++++------- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/app/containers/message/Components/Attachments/Attachments.tsx b/app/containers/message/Components/Attachments/Attachments.tsx index 7d3f72a50be..e05c8bba9c1 100644 --- a/app/containers/message/Components/Attachments/Attachments.tsx +++ b/app/containers/message/Components/Attachments/Attachments.tsx @@ -8,7 +8,7 @@ import CollapsibleQuote from './CollapsibleQuote'; import AttachedActions from './AttachedActions'; import Reply from './Reply'; import { useShowAttachment, useGetCustomEmoji } from '../../MessageRoomStore'; -import { useContentData, useTranslateLanguage } from '../../MessageStore'; +import { useTranslateLanguage } from '../../MessageStore'; import { type IMessageAttachments } from '../../interfaces'; import { getMessageFromAttachment } from '../../utils'; import { isContentAttachment } from './utils'; @@ -19,10 +19,8 @@ const Attachments: FC<IMessageAttachments> = ({ attachments, author }: IMessageA const translateLanguage = useTranslateLanguage(); const showAttachment = useShowAttachment(); const getCustomEmoji = useGetCustomEmoji() ?? (() => null); - const { attachments: storeAttachments } = useContentData(); - const resolved = attachments ?? storeAttachments; - const nonQuoteAttachments = resolved?.filter(isContentAttachment); + const nonQuoteAttachments = attachments?.filter(isContentAttachment); if (!nonQuoteAttachments || nonQuoteAttachments.length === 0) { return null; diff --git a/app/containers/message/Components/Attachments/Quote.tsx b/app/containers/message/Components/Attachments/Quote.tsx index 2eda7f26107..56cf1af872f 100644 --- a/app/containers/message/Components/Attachments/Quote.tsx +++ b/app/containers/message/Components/Attachments/Quote.tsx @@ -7,17 +7,15 @@ import { type IAttachment } from '../../../../definitions'; import { getMessageFromAttachment } from '../../utils'; import { isQuoteAttachment } from './utils'; import { useGetCustomEmoji } from '../../MessageRoomStore'; -import { useContentData, useTranslateLanguage } from '../../MessageStore'; +import { useTranslateLanguage } from '../../MessageStore'; const Quote: FC<IMessageAttachments> = ({ attachments }: IMessageAttachments) => { 'use memo'; const translateLanguage = useTranslateLanguage(); const getCustomEmoji = useGetCustomEmoji() ?? (() => null); - const { attachments: storeAttachments } = useContentData(); - const resolved = attachments ?? storeAttachments; - const quotes = resolved?.filter(isQuoteAttachment); + const quotes = attachments?.filter(isQuoteAttachment); if (!quotes || !quotes?.length) { return null; diff --git a/app/containers/message/Components/Attachments/Reply.tsx b/app/containers/message/Components/Attachments/Reply.tsx index 346d4b7e3de..390fb48172b 100644 --- a/app/containers/message/Components/Attachments/Reply.tsx +++ b/app/containers/message/Components/Attachments/Reply.tsx @@ -222,8 +222,8 @@ const Reply = ({ attachment, getCustomEmoji, msg }: IMessageReply) => { <View style={styles.titleAndDescriptionContainer}> <Title attachment={attachment} theme={theme} /> <Description attachment={attachment} getCustomEmoji={getCustomEmoji} /> - <Quote attachments={attachment.attachments ?? []} /> - <Attachments attachments={attachment.attachments ?? []} /> + <Quote attachments={attachment.attachments} /> + <Attachments attachments={attachment.attachments} /> <Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} /> {loading ? ( <View style={styles.backdrop}> diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index a187c6ee8dc..83aae73b1a8 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -60,7 +60,7 @@ const MessageInner = (props: TMessageProps) => { const { isLargeFontScale } = useResponsiveLayout(); const isHeader = useMessageGrouping(); - const { t: type } = useContentData(); + const { t: type, attachments } = useContentData(); const { blocks } = useBlocks(); const { u: author } = useMessageAuthor(); const showTimeLarge = isLargeFontScale && isHeader; @@ -72,9 +72,9 @@ const MessageInner = (props: TMessageProps) => { <User /> {showTimeLarge ? <MessageTime /> : null} <> - <Quote /> + <Quote attachments={attachments} /> <Content /> - <Attachments author={author} /> + <Attachments attachments={attachments} author={author} /> </> <Urls /> </> @@ -120,9 +120,9 @@ const MessageInner = (props: TMessageProps) => { <User /> {showTimeLarge ? <MessageTime /> : null} <View style={{ gap: 4 }}> - <Quote /> + <Quote attachments={attachments} /> <Content /> - <Attachments author={author} /> + <Attachments attachments={attachments} author={author} /> <Urls /> <Thread /> <Reactions /> @@ -143,7 +143,7 @@ const Message = (props: TMessageProps & IMessageA11y) => { const { isThreadReply, isThreadSequential } = useThreadPosition(); const isInfo = useIsInfo(); const { messageText, isTranslated } = useMessageText(); - const { t: type } = useContentData(); + const { t: type, attachments } = useContentData(); const { u: author } = useMessageAuthor(); const id = useMessageField(item => item.id); const { autoTranslateLanguage } = useAutoTranslate(); @@ -167,7 +167,7 @@ const Message = (props: TMessageProps & IMessageA11y) => { <Content /> {isInfo && type === 'message_pinned' ? ( <View pointerEvents='none'> - <Attachments author={author} /> + <Attachments attachments={attachments} author={author} /> </View> ) : null} </View> From 06072275d808a7a7a2d6d4608d93e3d93c35b136 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 12:15:01 -0300 Subject: [PATCH 055/144] fix: default ignoredSeed in the message store factory The store factory left ignoredSeed possibly undefined, failing the tsc gate against the required boolean in MessageStoreState. --- app/containers/message/MessageStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/containers/message/MessageStore.tsx b/app/containers/message/MessageStore.tsx index e3b27ed2e9a..72760ad54ba 100644 --- a/app/containers/message/MessageStore.tsx +++ b/app/containers/message/MessageStore.tsx @@ -33,7 +33,7 @@ type MessageStoreState = { }; const createMessageStore = (initial: Partial<MessageStoreState>) => - createStore<MessageStoreState>(() => ({ tick: 0, manualUnignored: false, ...initial })); + createStore<MessageStoreState>(() => ({ tick: 0, manualUnignored: false, ignoredSeed: false, ...initial })); type MessageStore = ReturnType<typeof createMessageStore>; From f05a225050dca159b07fa22283382f7bf2a0ee58 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 13:01:09 -0300 Subject: [PATCH 056/144] fix: restore message-link jump in SearchMessagesView The MessageContext-to-store refactor moved the jumpToMessage handler into MessageRoomProvider but did not re-seed it in SearchMessagesView, so tapping an in-message permalink in search results fell through to openLink (browser) instead of jumping to the referenced message. Seed jumpToMessage with a handler that parses the message id from the permalink and jumps within the searched room, mirroring RoomView.jumpToMessageByUrl. --- app/views/SearchMessagesView/index.tsx | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index 37c1ef1e943..0e3e2c32730 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -5,6 +5,7 @@ import { Q } from '@nozbe/watermelondb'; import { connect } from 'react-redux'; import { dequal } from 'dequal'; import { Component } from 'react'; +import parse from 'url-parse'; import { FormTextInput } from '../../containers/TextInput'; import ActivityIndicator from '../../containers/ActivityIndicator'; @@ -259,6 +260,25 @@ class SearchMessagesView extends Component<ISearchMessagesViewProps, ISearchMess } }; + jumpToMessageByUrl = (messageUrl: string) => { + try { + const messageId = parse(messageUrl, true).query.msg; + if (!messageId) { + return; + } + const { isMasterDetail } = this.props; + Navigation.popToRoom(isMasterDetail); + Navigation.setParams({ + rid: this.rid, + jumpToMessageId: messageId, + t: this.t, + room: this.room as TSubscriptionModel + }); + } catch (e) { + log(e); + } + }; + onEndReached = async () => { const { serverVersion } = this.props; const { searchText, messages, loading } = this.state; @@ -301,6 +321,7 @@ class SearchMessagesView extends Component<ISearchMessagesViewProps, ISearchMess getCustomEmoji={this.getCustomEmoji} navToRoomInfo={this.navToRoomInfo} showAttachment={this.showAttachment} + jumpToMessage={this.jumpToMessageByUrl} user={user} baseUrl={baseUrl} rid={this.rid} From 430d920974d8e16f972143637cbd960719a53314 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 13:01:09 -0300 Subject: [PATCH 057/144] refactor: remove dead isBeingEdited and small props from Message TMessageProps carried isBeingEdited and small, but neither was ever read: MessageTouchable computes its own isBeingEdited from the interaction store, and small was only ever the hardcoded <MessageAvatar small /> literal. Drop both from the type and stop forwarding them, and the equally-unread highlighted, down into Message and MessageInner. --- app/containers/message/Message.tsx | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/app/containers/message/Message.tsx b/app/containers/message/Message.tsx index 83aae73b1a8..2ecf6150204 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/Message.tsx @@ -45,8 +45,6 @@ import { type TMessageProps = { isPreview?: boolean; highlighted?: boolean; - isBeingEdited?: boolean; - small?: boolean; }; interface IMessageA11y { @@ -187,12 +185,7 @@ const Message = (props: TMessageProps & IMessageA11y) => { <View style={styles.flex}> <MessageAvatar /> <View style={styles.messageContent}> - <MessageInner - isPreview={props.isPreview} - highlighted={props.highlighted} - isBeingEdited={props.isBeingEdited} - small={props.small} - /> + <MessageInner isPreview={props.isPreview} /> </View> {!isHeader ? <RightIcons msg={messageText} /> : null} </View> @@ -232,7 +225,7 @@ const MessageTouchable = (props: TMessageProps) => { if (hasError || isInfo) { return ( <A11y.Order> - <Message isPreview={props.isPreview} highlighted={props.highlighted} isBeingEdited={isBeingEdited} small={props.small} /> + <Message isPreview={props.isPreview} /> </A11y.Order> ); } @@ -260,13 +253,7 @@ const MessageTouchable = (props: TMessageProps) => { onAccessibilityAction={e => { if (e.nativeEvent.actionName === 'showActions') handleLongPress(); }}> - <Message - isPreview={props.isPreview} - highlighted={props.highlighted} - isBeingEdited={isBeingEdited} - small={props.small} - handleLongPress={!isDisabled ? handleLongPress : undefined} - /> + <Message isPreview={props.isPreview} handleLongPress={!isDisabled ? handleLongPress : undefined} /> </Touch> </A11y.Index> </A11y.Order> From 2f00bd2b38d0d96d21b77d2a639ff2982718854d Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 13:01:09 -0300 Subject: [PATCH 058/144] refactor: correct MessageRoomStore mirroring comment The mirroring effect's comment claimed unchanged slices would not trigger a notify; zustand setState notifies every listener on each render. Re-renders are avoided at the selector level: stable handler and constant refs keep each snapshot Object.is-equal. Fix the comment to describe the real mechanism. --- app/containers/message/MessageRoomStore.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/containers/message/MessageRoomStore.tsx b/app/containers/message/MessageRoomStore.tsx index 36d1c37049d..5e749dc6e50 100644 --- a/app/containers/message/MessageRoomStore.tsx +++ b/app/containers/message/MessageRoomStore.tsx @@ -107,7 +107,8 @@ export const MessageRoomProvider = ({ children, ...state }: { children: ReactNod const [store] = useState(() => createMessageRoomStore(state)); // No dependency array: the store must mirror the provider's props every render. - // Handlers/constants are stable refs, so unchanged slices won't trigger a notify. + // setState notifies every listener on each render, but handlers/constants are stable + // refs, so each selector's snapshot stays Object.is-equal and no consumer re-renders. useEffect(() => { store.setState(state); }); From 5ce3f4bb3e97e18df06404c31dd5a2a7570c4dce Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 13:10:51 -0300 Subject: [PATCH 059/144] test: cover ignored-message reveal, interaction fallback, room-store mirroring Add characterization tests for store behavior introduced by the message-hooks refactor that previously had no coverage: - useMessageIgnored/revealIgnored: the isIgnored seed, and that reveal is sticky because manualUnignored is never reset. - InteractionStore.useIsBeingEdited falls back to a module store (returns false) outside a provider; the action hooks throw without one. - MessageRoomProvider mirrors updated props into its store post-mount, so a regression in the mirroring effect dependencies would be caught. --- .../message/MessageRoomStore.test.tsx | 24 +++++++ app/containers/message/MessageStore.test.tsx | 60 ++++++++++++++++- app/views/RoomView/InteractionStore.test.tsx | 65 +++++++++++++++++++ 3 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 app/containers/message/MessageRoomStore.test.tsx create mode 100644 app/views/RoomView/InteractionStore.test.tsx diff --git a/app/containers/message/MessageRoomStore.test.tsx b/app/containers/message/MessageRoomStore.test.tsx new file mode 100644 index 00000000000..551e7abc03e --- /dev/null +++ b/app/containers/message/MessageRoomStore.test.tsx @@ -0,0 +1,24 @@ +import { act, render } from '@testing-library/react-native'; + +import { MessageRoomProvider, pickMessageRoomState, useTimeFormat } from './MessageRoomStore'; + +describe('MessageRoomStore', () => { + it('mirrors updated provider props into the store after mount', () => { + const spy = jest.fn(); + const Probe = () => { + spy(useTimeFormat()); + return null; + }; + const wrap = (timeFormat: string) => ( + <MessageRoomProvider {...pickMessageRoomState({ timeFormat })}> + <Probe /> + </MessageRoomProvider> + ); + + const { rerender } = render(wrap('MMM Do YYYY')); + expect(spy).toHaveBeenLastCalledWith('MMM Do YYYY'); + + act(() => rerender(wrap('h:mm a'))); + expect(spy).toHaveBeenLastCalledWith('h:mm a'); + }); +}); diff --git a/app/containers/message/MessageStore.test.tsx b/app/containers/message/MessageStore.test.tsx index 2b2c5f6f22a..55c22e1fa04 100644 --- a/app/containers/message/MessageStore.test.tsx +++ b/app/containers/message/MessageStore.test.tsx @@ -1,4 +1,5 @@ -import { act, render } from '@testing-library/react-native'; +import { act, fireEvent, render } from '@testing-library/react-native'; +import { Text } from 'react-native'; import { type TAnyMessageModel } from '../../definitions'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; @@ -17,11 +18,13 @@ import { useMessageCtx, useMessageField, useMessageGrouping, + useMessageIgnored, useMessageMeta, useMessageStatus, useMessageText, useReactions, useRepliedThreadData, + useRevealIgnored, useThreadData, useThreadPosition, useUrls @@ -565,4 +568,59 @@ describe('MessageStore', () => { expect(() => render(<Probe />)).toThrow('Message hooks must be used within a MessageProvider'); }); }); + + describe('ignored messages', () => { + it('useMessageIgnored reflects the isIgnored seed', () => { + const model = buildFakeModel(); + const ignoredSpy = jest.fn(); + const notIgnoredSpy = jest.fn(); + const IgnoredProbe = () => { + ignoredSpy(useMessageIgnored()); + return null; + }; + const NotIgnoredProbe = () => { + notIgnoredSpy(useMessageIgnored()); + return null; + }; + + render( + <MessageProvider item={model} isIgnored> + <IgnoredProbe /> + </MessageProvider> + ); + render( + <MessageProvider item={model}> + <NotIgnoredProbe /> + </MessageProvider> + ); + + expect(ignoredSpy).toHaveBeenLastCalledWith(true); + expect(notIgnoredSpy).toHaveBeenLastCalledWith(false); + }); + + it('revealIgnored flips useMessageIgnored to false and stays revealed across re-renders', () => { + const model = buildFakeModel(); + const spy = jest.fn(); + const Probe = () => { + spy(useMessageIgnored()); + const reveal = useRevealIgnored(); + return <Text testID='reveal' onPress={reveal} />; + }; + const wrap = () => ( + <MessageProvider item={model} isIgnored> + <Probe /> + </MessageProvider> + ); + + const { rerender, getByTestId } = render(wrap()); + expect(spy).toHaveBeenLastCalledWith(true); + + fireEvent.press(getByTestId('reveal')); + expect(spy).toHaveBeenLastCalledWith(false); + + // manualUnignored is never reset, so re-rendering with the same isIgnored seed keeps it revealed. + act(() => rerender(wrap())); + expect(spy).toHaveBeenLastCalledWith(false); + }); + }); }); diff --git a/app/views/RoomView/InteractionStore.test.tsx b/app/views/RoomView/InteractionStore.test.tsx new file mode 100644 index 00000000000..7199f757e47 --- /dev/null +++ b/app/views/RoomView/InteractionStore.test.tsx @@ -0,0 +1,65 @@ +import { render } from '@testing-library/react-native'; + +import { InteractionProvider, useIsBeingEdited, useMessageAction, useSelectedMessages } from './InteractionStore'; + +describe('InteractionStore', () => { + describe('useIsBeingEdited', () => { + it('returns false with no provider (module fallback store) and does not throw', () => { + const spy = jest.fn(); + const Probe = () => { + spy(useIsBeingEdited('msg-1')); + return null; + }; + + expect(() => render(<Probe />)).not.toThrow(); + expect(spy).toHaveBeenLastCalledWith(false); + }); + + it('returns true only for the message being edited under a provider', () => { + const editedSpy = jest.fn(); + const otherSpy = jest.fn(); + const Probe = () => { + editedSpy(useIsBeingEdited('msg-1')); + otherSpy(useIsBeingEdited('msg-2')); + return null; + }; + + render( + <InteractionProvider initialState={{ action: 'edit', selectedMessages: ['msg-1'] }}> + <Probe /> + </InteractionProvider> + ); + + expect(editedSpy).toHaveBeenLastCalledWith(true); + expect(otherSpy).toHaveBeenLastCalledWith(false); + }); + }); + + describe('action hooks without a provider', () => { + let consoleErrorSpy: jest.SpyInstance; + + beforeEach(() => { + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + }); + + it('useMessageAction throws', () => { + const Probe = () => { + useMessageAction(); + return null; + }; + expect(() => render(<Probe />)).toThrow('Interaction hooks must be used within an InteractionProvider'); + }); + + it('useSelectedMessages throws', () => { + const Probe = () => { + useSelectedMessages(); + return null; + }; + expect(() => render(<Probe />)).toThrow('Interaction hooks must be used within an InteractionProvider'); + }); + }); +}); From 273326bf3b410305b61b5684e216dd617354d90b Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 14:00:30 -0300 Subject: [PATCH 060/144] refactor: reorganize the message module into components, stores, tests and stories folders (NATIVE-1393) Move all presentational leaves into components/, the two zustand stores into stores/, and tests and stories into dedicated __tests__/ and components/stories/ folders. Rename the capital-cased Components/ to lowercase components/. Update all relative imports and the external importers of deep message paths. Moves only: snapshot content is byte-identical and the public entry app/containers/message is unchanged. --- app/containers/ReactionsList/AllTab.tsx | 2 +- app/containers/ReactionsList/index.tsx | 2 +- app/containers/UIKit/Image.tsx | 2 +- app/containers/UIKit/UiKitModal.stories.tsx | 4 +-- app/containers/message/Message.test.tsx | 12 -------- .../message/{ => __tests__}/index.test.tsx | 12 ++++---- .../message/{ => __tests__}/utils.test.ts | 2 +- .../Attachments/AttachedActions.tsx | 2 +- .../Attachments/Attachments.stories.tsx | 4 +-- .../Attachments/Attachments.tsx | 4 +-- .../Attachments/Audio.tsx | 4 +-- .../CollapsibleQuote.stories.tsx | 2 +- .../CollapsibleQuote.test.tsx | 4 +-- .../Attachments/CollapsibleQuote/index.tsx | 4 +-- .../Attachments/Image/Button.tsx | 2 +- .../Attachments/Image/Container.test.tsx | 4 +-- .../Attachments/Image/Container.tsx | 2 +- .../Attachments/Image/Image.stories.tsx | 0 .../Attachments/Image/Image.test.tsx | 0 .../Attachments/Image/Image.tsx | 0 .../Attachments/Image/ImageBadge.tsx | 0 .../Attachments/Image/definitions.ts | 0 .../Attachments/Image/index.tsx | 0 .../Attachments/Quote.tsx | 4 +-- .../Attachments/Reply.tsx | 6 ++-- .../Attachments/Video.tsx | 4 +-- .../Attachments/components.ts | 0 .../Attachments/index.ts | 0 .../Attachments/utils.test.ts | 0 .../Attachments/utils.ts | 0 .../message/{ => components}/Blocks.ts | 6 ++-- .../message/{ => components}/Broadcast.tsx | 16 +++++------ .../message/{ => components}/CallButton.tsx | 14 +++++----- .../message/{ => components}/Content.tsx | 26 ++++++++++------- .../message/{ => components}/Discussion.tsx | 18 ++++++------ .../message/{ => components}/Emoji.tsx | 6 ++-- .../message/{ => components}/Message.tsx | 28 +++++++++---------- .../{ => components}/MessageAvatar.tsx | 14 +++++----- .../OverlayComponent/index.tsx | 0 .../message/{ => components}/Preview.tsx | 10 +++---- .../message/{ => components}/Reactions.tsx | 16 +++++------ .../{ => components}/RepliedThread.tsx | 16 +++++------ .../RightIcons/Edited.tsx | 2 +- .../RightIcons/Encrypted.tsx | 6 ++-- .../RightIcons/MessageError.tsx | 6 ++-- .../RightIcons/Pinned.tsx | 2 +- .../RightIcons/ReadReceipt.tsx | 4 +-- .../RightIcons/Translated.tsx | 2 +- .../RightIcons/index.tsx | 0 .../message/{ => components}/Thread.tsx | 12 ++++---- .../message/{ => components}/Time.tsx | 10 +++---- .../message/{ => components}/Touch.tsx | 4 +-- .../message/{ => components}/Touchable.tsx | 2 +- .../message/{ => components}/Urls.tsx | 22 +++++++-------- .../message/{ => components}/User.tsx | 18 ++++++------ .../WidthAwareView.tsx | 0 .../__tests__}/Content.test.tsx | 14 +++++----- .../components/__tests__/Message.test.tsx | 12 ++++++++ .../__tests__}/Preview.test.tsx | 8 +++--- .../__tests__}/Reactions.test.tsx | 10 +++---- .../__tests__}/Thread.test.tsx | 8 +++--- .../__snapshots__/Message.test.tsx.snap | 0 .../stories}/Message.stories.tsx | 24 ++++++++-------- .../useImageDescriptionLabel.test.ts | 6 ++-- .../useMessageAccessibilityActions.test.ts | 2 +- .../useMessageAccessibilityHint.test.tsx | 8 +++--- .../useMessageAccessibilityLabel.test.tsx | 18 ++++++------ .../message/hooks/useMediaAutoDownload.tsx | 4 +-- .../hooks/useMessageAccessibilityHint.ts | 4 +-- .../hooks/useMessageAccessibilityLabel.ts | 4 +-- app/containers/message/index.tsx | 4 +-- .../message/{ => stores}/MessageRoomStore.tsx | 6 ++-- .../message/{ => stores}/MessageStore.tsx | 14 +++++----- .../__tests__}/MessageRoomStore.test.tsx | 2 +- .../__tests__}/MessageStore.test.tsx | 10 +++---- app/containers/message/testHelpers.tsx | 4 +-- app/views/ForwardMessageView/index.tsx | 2 +- app/views/MessagesView/index.tsx | 2 +- app/views/ModalBlockView.tsx | 4 +-- .../RoomView/LoadMore/LoadMore.stories.tsx | 2 +- app/views/RoomView/index.tsx | 2 +- app/views/SearchMessagesView/index.tsx | 2 +- 82 files changed, 262 insertions(+), 256 deletions(-) delete mode 100644 app/containers/message/Message.test.tsx rename app/containers/message/{ => __tests__}/index.test.tsx (92%) rename app/containers/message/{ => __tests__}/utils.test.ts (95%) rename app/containers/message/{Components => components}/Attachments/AttachedActions.tsx (93%) rename app/containers/message/{Components => components}/Attachments/Attachments.stories.tsx (97%) rename app/containers/message/{Components => components}/Attachments/Attachments.tsx (94%) rename app/containers/message/{Components => components}/Attachments/Audio.tsx (88%) rename app/containers/message/{Components => components}/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx (97%) rename app/containers/message/{Components => components}/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx (97%) rename app/containers/message/{Components => components}/Attachments/CollapsibleQuote/index.tsx (97%) rename app/containers/message/{Components => components}/Attachments/Image/Button.tsx (94%) rename app/containers/message/{Components => components}/Attachments/Image/Container.test.tsx (96%) rename app/containers/message/{Components => components}/Attachments/Image/Container.tsx (96%) rename app/containers/message/{Components => components}/Attachments/Image/Image.stories.tsx (100%) rename app/containers/message/{Components => components}/Attachments/Image/Image.test.tsx (100%) rename app/containers/message/{Components => components}/Attachments/Image/Image.tsx (100%) rename app/containers/message/{Components => components}/Attachments/Image/ImageBadge.tsx (100%) rename app/containers/message/{Components => components}/Attachments/Image/definitions.ts (100%) rename app/containers/message/{Components => components}/Attachments/Image/index.tsx (100%) rename app/containers/message/{Components => components}/Attachments/Quote.tsx (88%) rename app/containers/message/{Components => components}/Attachments/Reply.tsx (98%) rename app/containers/message/{Components => components}/Attachments/Video.tsx (97%) rename app/containers/message/{Components => components}/Attachments/components.ts (100%) rename app/containers/message/{Components => components}/Attachments/index.ts (100%) rename app/containers/message/{Components => components}/Attachments/utils.test.ts (100%) rename app/containers/message/{Components => components}/Attachments/utils.ts (100%) rename app/containers/message/{ => components}/Blocks.ts (79%) rename app/containers/message/{ => components}/Broadcast.tsx (75%) rename app/containers/message/{ => components}/CallButton.tsx (70%) rename app/containers/message/{ => components}/Content.tsx (84%) rename app/containers/message/{ => components}/Discussion.tsx (73%) rename app/containers/message/{ => components}/Emoji.tsx (75%) rename app/containers/message/{ => components}/Message.tsx (88%) rename app/containers/message/{ => components}/MessageAvatar.tsx (83%) rename app/containers/message/{Components => components}/OverlayComponent/index.tsx (100%) rename app/containers/message/{ => components}/Preview.tsx (76%) rename app/containers/message/{ => components}/Reactions.tsx (89%) rename app/containers/message/{ => components}/RepliedThread.tsx (75%) rename app/containers/message/{Components => components}/RightIcons/Edited.tsx (87%) rename app/containers/message/{Components => components}/RightIcons/Encrypted.tsx (77%) rename app/containers/message/{Components => components}/RightIcons/MessageError.tsx (80%) rename app/containers/message/{Components => components}/RightIcons/Pinned.tsx (87%) rename app/containers/message/{Components => components}/RightIcons/ReadReceipt.tsx (86%) rename app/containers/message/{Components => components}/RightIcons/Translated.tsx (86%) rename app/containers/message/{Components => components}/RightIcons/index.tsx (100%) rename app/containers/message/{ => components}/Thread.tsx (85%) rename app/containers/message/{ => components}/Time.tsx (66%) rename app/containers/message/{ => components}/Touch.tsx (97%) rename app/containers/message/{ => components}/Touchable.tsx (88%) rename app/containers/message/{ => components}/Urls.tsx (89%) rename app/containers/message/{ => components}/User.tsx (82%) rename app/containers/message/{Components => components}/WidthAwareView.tsx (100%) rename app/containers/message/{ => components/__tests__}/Content.test.tsx (92%) create mode 100644 app/containers/message/components/__tests__/Message.test.tsx rename app/containers/message/{ => components/__tests__}/Preview.test.tsx (87%) rename app/containers/message/{ => components/__tests__}/Reactions.test.tsx (92%) rename app/containers/message/{ => components/__tests__}/Thread.test.tsx (84%) rename app/containers/message/{ => components/__tests__}/__snapshots__/Message.test.tsx.snap (100%) rename app/containers/message/{ => components/stories}/Message.stories.tsx (99%) rename app/containers/message/hooks/{ => __tests__}/useImageDescriptionLabel.test.ts (95%) rename app/containers/message/hooks/{ => __tests__}/useMessageAccessibilityActions.test.ts (86%) rename app/containers/message/hooks/{ => __tests__}/useMessageAccessibilityHint.test.tsx (81%) rename app/containers/message/hooks/{ => __tests__}/useMessageAccessibilityLabel.test.tsx (89%) rename app/containers/message/{ => stores}/MessageRoomStore.tsx (97%) rename app/containers/message/{ => stores}/MessageStore.tsx (96%) rename app/containers/message/{ => stores/__tests__}/MessageRoomStore.test.tsx (95%) rename app/containers/message/{ => stores/__tests__}/MessageStore.test.tsx (98%) diff --git a/app/containers/ReactionsList/AllTab.tsx b/app/containers/ReactionsList/AllTab.tsx index 2184697e3d3..9e927b1b148 100644 --- a/app/containers/ReactionsList/AllTab.tsx +++ b/app/containers/ReactionsList/AllTab.tsx @@ -1,7 +1,7 @@ import { memo, type ReactElement } from 'react'; import { Text, View, FlatList } from 'react-native'; -import Emoji from '../message/Emoji'; +import Emoji from '../message/components/Emoji'; import { useTheme } from '../../theme'; import { type IReaction } from '../../definitions'; import { type TGetCustomEmoji } from '../../definitions/IEmoji'; diff --git a/app/containers/ReactionsList/index.tsx b/app/containers/ReactionsList/index.tsx index 6354a7200e5..d1beac082e9 100644 --- a/app/containers/ReactionsList/index.tsx +++ b/app/containers/ReactionsList/index.tsx @@ -7,7 +7,7 @@ import styles from './styles'; import AllTab from './AllTab'; import UsersList from './UsersList'; import { TabView } from '../TabView'; -import Emoji from '../message/Emoji'; +import Emoji from '../message/components/Emoji'; interface IReactionsListProps { getCustomEmoji: TGetCustomEmoji; diff --git a/app/containers/UIKit/Image.tsx b/app/containers/UIKit/Image.tsx index a2f7bd4e724..ce8539b10e9 100644 --- a/app/containers/UIKit/Image.tsx +++ b/app/containers/UIKit/Image.tsx @@ -2,7 +2,7 @@ import { StyleSheet, View } from 'react-native'; import { Image as ExpoImage } from 'expo-image'; import { BlockContext } from '@rocket.chat/ui-kit'; -import ImageContainer from '../message/Components/Attachments/Image'; +import ImageContainer from '../message/components/Attachments/Image'; import Navigation from '../../lib/navigation/appNavigation'; import { type IThumb, type IImage } from './interfaces'; import { type IAttachment } from '../../definitions'; diff --git a/app/containers/UIKit/UiKitModal.stories.tsx b/app/containers/UIKit/UiKitModal.stories.tsx index 848ea0f17cf..adee3d2d256 100644 --- a/app/containers/UIKit/UiKitModal.stories.tsx +++ b/app/containers/UIKit/UiKitModal.stories.tsx @@ -3,8 +3,8 @@ import { Button, View } from 'react-native'; import { UiKitComponent, UiKitModal } from '.'; import { KitContext, defaultContext } from './utils'; -import { MessageRoomProvider } from '../message/MessageRoomStore'; -import { MessageProvider } from '../message/MessageStore'; +import { MessageRoomProvider } from '../message/stores/MessageRoomStore'; +import { MessageProvider } from '../message/stores/MessageStore'; import { type TAnyMessageModel } from '../../definitions'; export default { diff --git a/app/containers/message/Message.test.tsx b/app/containers/message/Message.test.tsx deleted file mode 100644 index a047bf7a768..00000000000 --- a/app/containers/message/Message.test.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { generateSnapshots } from '../../../.rnstorybook/generateSnapshots'; -import * as stories from './Message.stories'; - -jest.mock('../../lib/methods/handleMediaDownload.ts', () => ({ - downloadMediaFile: jest.fn(), - getMediaCache: jest.fn(), - isDownloadActive: jest.fn() -})); - -jest.unmock('../../lib/hooks/useResponsiveLayout/useResponsiveLayout'); - -generateSnapshots(stories); diff --git a/app/containers/message/index.test.tsx b/app/containers/message/__tests__/index.test.tsx similarity index 92% rename from app/containers/message/index.test.tsx rename to app/containers/message/__tests__/index.test.tsx index 9bae579435c..a767c0712a5 100644 --- a/app/containers/message/index.test.tsx +++ b/app/containers/message/__tests__/index.test.tsx @@ -1,13 +1,13 @@ import { act } from 'react'; import { fireEvent } from '@testing-library/react-native'; -import MessageContainer from './index'; -import { type TAnyMessageModel } from '../../definitions'; -import { createInteractionStore, InteractionStoreContext } from '../../views/RoomView/InteractionStore'; -import { renderWithMessageProviders } from './testHelpers'; -import { pickMessageRoomState } from './MessageRoomStore'; +import MessageContainer from '../index'; +import { type TAnyMessageModel } from '../../../definitions'; +import { createInteractionStore, InteractionStoreContext } from '../../../views/RoomView/InteractionStore'; +import { renderWithMessageProviders } from '../testHelpers'; +import { pickMessageRoomState } from '../stores/MessageRoomStore'; -jest.mock('./Touch', () => { +jest.mock('../components/Touch', () => { const { forwardRef } = require('react'); const { TouchableOpacity } = require('react-native'); return forwardRef(({ children, onPress, onLongPress, ...props }: any, ref: any) => ( diff --git a/app/containers/message/utils.test.ts b/app/containers/message/__tests__/utils.test.ts similarity index 95% rename from app/containers/message/utils.test.ts rename to app/containers/message/__tests__/utils.test.ts index 186d55988d9..001f5609279 100644 --- a/app/containers/message/utils.test.ts +++ b/app/containers/message/__tests__/utils.test.ts @@ -1,4 +1,4 @@ -import { getPreviewMessageFromAttachment } from './utils'; +import { getPreviewMessageFromAttachment } from '../utils'; describe('getPreviewMessageFromAttachment', () => { test('returns the Attachment title when only title is set', () => { diff --git a/app/containers/message/Components/Attachments/AttachedActions.tsx b/app/containers/message/components/Attachments/AttachedActions.tsx similarity index 93% rename from app/containers/message/Components/Attachments/AttachedActions.tsx rename to app/containers/message/components/Attachments/AttachedActions.tsx index 66ef4beb8ee..2a9ac2dd9c5 100644 --- a/app/containers/message/Components/Attachments/AttachedActions.tsx +++ b/app/containers/message/components/Attachments/AttachedActions.tsx @@ -1,5 +1,5 @@ import Button from '../../../Button'; -import { useOnAnswerButtonPress } from '../../MessageRoomStore'; +import { useOnAnswerButtonPress } from '../../stores/MessageRoomStore'; import { type IAttachment, type TGetCustomEmoji } from '../../../../definitions'; import openLink from '../../../../lib/methods/helpers/openLink'; import Markdown from '../../../markdown'; diff --git a/app/containers/message/Components/Attachments/Attachments.stories.tsx b/app/containers/message/components/Attachments/Attachments.stories.tsx similarity index 97% rename from app/containers/message/Components/Attachments/Attachments.stories.tsx rename to app/containers/message/components/Attachments/Attachments.stories.tsx index f9a8a68802c..a128045a555 100644 --- a/app/containers/message/Components/Attachments/Attachments.stories.tsx +++ b/app/containers/message/components/Attachments/Attachments.stories.tsx @@ -5,8 +5,8 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { selectServerSuccess } from '../../../../actions/server'; import { type TAnyMessageModel } from '../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../MessageRoomStore'; -import { MessageProvider } from '../../MessageStore'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageProvider } from '../../stores/MessageStore'; import Attachments from './Attachments'; const mockMessageContext = { diff --git a/app/containers/message/Components/Attachments/Attachments.tsx b/app/containers/message/components/Attachments/Attachments.tsx similarity index 94% rename from app/containers/message/Components/Attachments/Attachments.tsx rename to app/containers/message/components/Attachments/Attachments.tsx index e05c8bba9c1..ca17ec8a2a2 100644 --- a/app/containers/message/Components/Attachments/Attachments.tsx +++ b/app/containers/message/components/Attachments/Attachments.tsx @@ -7,8 +7,8 @@ import Video from './Video'; import CollapsibleQuote from './CollapsibleQuote'; import AttachedActions from './AttachedActions'; import Reply from './Reply'; -import { useShowAttachment, useGetCustomEmoji } from '../../MessageRoomStore'; -import { useTranslateLanguage } from '../../MessageStore'; +import { useShowAttachment, useGetCustomEmoji } from '../../stores/MessageRoomStore'; +import { useTranslateLanguage } from '../../stores/MessageStore'; import { type IMessageAttachments } from '../../interfaces'; import { getMessageFromAttachment } from '../../utils'; import { isContentAttachment } from './utils'; diff --git a/app/containers/message/Components/Attachments/Audio.tsx b/app/containers/message/components/Attachments/Audio.tsx similarity index 88% rename from app/containers/message/Components/Attachments/Audio.tsx rename to app/containers/message/components/Attachments/Audio.tsx index 498776c7c28..db06d6c60e3 100644 --- a/app/containers/message/Components/Attachments/Audio.tsx +++ b/app/containers/message/components/Attachments/Audio.tsx @@ -5,8 +5,8 @@ import { type TGetCustomEmoji } from '../../../../definitions/IEmoji'; import AudioPlayer from '../../../AudioPlayer'; import Markdown from '../../../markdown'; import { useMediaAutoDownload } from '../../hooks/useMediaAutoDownload'; -import { useMessageUser, useRid } from '../../MessageRoomStore'; -import { useMessageId } from '../../MessageStore'; +import { useMessageUser, useRid } from '../../stores/MessageRoomStore'; +import { useMessageId } from '../../stores/MessageStore'; interface IMessageAudioProps { file: IAttachment; diff --git a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx b/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx similarity index 97% rename from app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx rename to app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx index 7a851e97b87..608f23fe19b 100644 --- a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx +++ b/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx @@ -1,6 +1,6 @@ import { View } from 'react-native'; -import { MessageRoomProvider, pickMessageRoomState } from '../../../MessageRoomStore'; +import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; import CollapsibleQuote from '.'; const testAttachment = { diff --git a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx b/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx similarity index 97% rename from app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx rename to app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx index 371310e33d2..6bab716906c 100644 --- a/app/containers/message/Components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx +++ b/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx @@ -4,8 +4,8 @@ import { Provider } from 'react-redux'; import { setUser } from '../../../../../actions/login'; import { mockedStore } from '../../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../../definitions'; -import { MessageProvider } from '../../../MessageStore'; -import { MessageRoomProvider, pickMessageRoomState } from '../../../MessageRoomStore'; +import { MessageProvider } from '../../../stores/MessageStore'; +import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; import CollapsibleQuote from '.'; const testAttachment = { diff --git a/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx b/app/containers/message/components/Attachments/CollapsibleQuote/index.tsx similarity index 97% rename from app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx rename to app/containers/message/components/Attachments/CollapsibleQuote/index.tsx index 60b8423837a..da948197c80 100644 --- a/app/containers/message/Components/Attachments/CollapsibleQuote/index.tsx +++ b/app/containers/message/components/Attachments/CollapsibleQuote/index.tsx @@ -9,8 +9,8 @@ import { CustomIcon } from '../../../../CustomIcon'; import { useTheme } from '../../../../../theme'; import sharedStyles from '../../../../../views/Styles'; import Markdown from '../../../../markdown'; -import { useMessageUser } from '../../../MessageRoomStore'; -import Touchable from '../../../Touchable'; +import { useMessageUser } from '../../../stores/MessageRoomStore'; +import Touchable from '../../Touchable'; import { BUTTON_HIT_SLOP } from '../../../utils'; const styles = StyleSheet.create({ diff --git a/app/containers/message/Components/Attachments/Image/Button.tsx b/app/containers/message/components/Attachments/Image/Button.tsx similarity index 94% rename from app/containers/message/Components/Attachments/Image/Button.tsx rename to app/containers/message/components/Attachments/Image/Button.tsx index 9f56c024321..f69396a6ec3 100644 --- a/app/containers/message/Components/Attachments/Image/Button.tsx +++ b/app/containers/message/components/Attachments/Image/Button.tsx @@ -1,7 +1,7 @@ import { type ReactElement } from 'react'; import { type PressableProps } from 'react-native'; -import Touchable from '../../../Touchable'; +import Touchable from '../../Touchable'; import styles from '../../../styles'; interface IMessageButton { diff --git a/app/containers/message/Components/Attachments/Image/Container.test.tsx b/app/containers/message/components/Attachments/Image/Container.test.tsx similarity index 96% rename from app/containers/message/Components/Attachments/Image/Container.test.tsx rename to app/containers/message/components/Attachments/Image/Container.test.tsx index c9679dbd83d..9d94585424f 100644 --- a/app/containers/message/Components/Attachments/Image/Container.test.tsx +++ b/app/containers/message/components/Attachments/Image/Container.test.tsx @@ -3,8 +3,8 @@ import { type ComponentProps } from 'react'; import { A11y } from 'react-native-a11y-order'; import { type TAnyMessageModel } from '../../../../../definitions'; -import { MessageProvider } from '../../../MessageStore'; -import { MessageRoomProvider, pickMessageRoomState } from '../../../MessageRoomStore'; +import { MessageProvider } from '../../../stores/MessageStore'; +import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; import ImageContainer from './Container'; jest.mock('../../../../markdown', () => { diff --git a/app/containers/message/Components/Attachments/Image/Container.tsx b/app/containers/message/components/Attachments/Image/Container.tsx similarity index 96% rename from app/containers/message/Components/Attachments/Image/Container.tsx rename to app/containers/message/components/Attachments/Image/Container.tsx index c7264cfd84b..af00157c4f0 100644 --- a/app/containers/message/Components/Attachments/Image/Container.tsx +++ b/app/containers/message/components/Attachments/Image/Container.tsx @@ -7,7 +7,7 @@ import { Button } from './Button'; import { MessageImage } from './Image'; import AltTextLabel from '../../../../AltTextLabel'; import { type IImageContainer } from './definitions'; -import { useMessageUser } from '../../../MessageRoomStore'; +import { useMessageUser } from '../../../stores/MessageRoomStore'; import { WidthAwareView } from '../../WidthAwareView'; import { useAltTextSupported } from '../../../../../lib/hooks/useAltTextSupported'; import I18n from '../../../../../i18n'; diff --git a/app/containers/message/Components/Attachments/Image/Image.stories.tsx b/app/containers/message/components/Attachments/Image/Image.stories.tsx similarity index 100% rename from app/containers/message/Components/Attachments/Image/Image.stories.tsx rename to app/containers/message/components/Attachments/Image/Image.stories.tsx diff --git a/app/containers/message/Components/Attachments/Image/Image.test.tsx b/app/containers/message/components/Attachments/Image/Image.test.tsx similarity index 100% rename from app/containers/message/Components/Attachments/Image/Image.test.tsx rename to app/containers/message/components/Attachments/Image/Image.test.tsx diff --git a/app/containers/message/Components/Attachments/Image/Image.tsx b/app/containers/message/components/Attachments/Image/Image.tsx similarity index 100% rename from app/containers/message/Components/Attachments/Image/Image.tsx rename to app/containers/message/components/Attachments/Image/Image.tsx diff --git a/app/containers/message/Components/Attachments/Image/ImageBadge.tsx b/app/containers/message/components/Attachments/Image/ImageBadge.tsx similarity index 100% rename from app/containers/message/Components/Attachments/Image/ImageBadge.tsx rename to app/containers/message/components/Attachments/Image/ImageBadge.tsx diff --git a/app/containers/message/Components/Attachments/Image/definitions.ts b/app/containers/message/components/Attachments/Image/definitions.ts similarity index 100% rename from app/containers/message/Components/Attachments/Image/definitions.ts rename to app/containers/message/components/Attachments/Image/definitions.ts diff --git a/app/containers/message/Components/Attachments/Image/index.tsx b/app/containers/message/components/Attachments/Image/index.tsx similarity index 100% rename from app/containers/message/Components/Attachments/Image/index.tsx rename to app/containers/message/components/Attachments/Image/index.tsx diff --git a/app/containers/message/Components/Attachments/Quote.tsx b/app/containers/message/components/Attachments/Quote.tsx similarity index 88% rename from app/containers/message/Components/Attachments/Quote.tsx rename to app/containers/message/components/Attachments/Quote.tsx index 56cf1af872f..273aa5a395a 100644 --- a/app/containers/message/Components/Attachments/Quote.tsx +++ b/app/containers/message/components/Attachments/Quote.tsx @@ -6,8 +6,8 @@ import { type IMessageAttachments } from '../../interfaces'; import { type IAttachment } from '../../../../definitions'; import { getMessageFromAttachment } from '../../utils'; import { isQuoteAttachment } from './utils'; -import { useGetCustomEmoji } from '../../MessageRoomStore'; -import { useTranslateLanguage } from '../../MessageStore'; +import { useGetCustomEmoji } from '../../stores/MessageRoomStore'; +import { useTranslateLanguage } from '../../stores/MessageStore'; const Quote: FC<IMessageAttachments> = ({ attachments }: IMessageAttachments) => { 'use memo'; diff --git a/app/containers/message/Components/Attachments/Reply.tsx b/app/containers/message/components/Attachments/Reply.tsx similarity index 98% rename from app/containers/message/Components/Attachments/Reply.tsx rename to app/containers/message/components/Attachments/Reply.tsx index 390fb48172b..770c2f2ae5f 100644 --- a/app/containers/message/Components/Attachments/Reply.tsx +++ b/app/containers/message/components/Attachments/Reply.tsx @@ -13,9 +13,9 @@ import RCActivityIndicator from '../../../ActivityIndicator'; import Markdown, { MarkdownPreview } from '../../../markdown'; import { Attachments } from './components'; import Quote from './Quote'; -import { useBaseUrl, useMessageUser, useTimeFormat } from '../../MessageRoomStore'; -import { useIsEncrypted, useMessageId } from '../../MessageStore'; -import Touchable from '../../Touchable'; +import { useBaseUrl, useMessageUser, useTimeFormat } from '../../stores/MessageRoomStore'; +import { useIsEncrypted, useMessageId } from '../../stores/MessageStore'; +import Touchable from '../Touchable'; import messageStyles from '../../styles'; import dayjs from '../../../../lib/dayjs'; diff --git a/app/containers/message/Components/Attachments/Video.tsx b/app/containers/message/components/Attachments/Video.tsx similarity index 97% rename from app/containers/message/Components/Attachments/Video.tsx rename to app/containers/message/components/Attachments/Video.tsx index fe04fc2bb93..e48f2fbb006 100644 --- a/app/containers/message/Components/Attachments/Video.tsx +++ b/app/containers/message/components/Attachments/Video.tsx @@ -12,9 +12,9 @@ import sharedStyles from '../../../../views/Styles'; import { type TIconsName } from '../../../CustomIcon'; import { LISTENER } from '../../../Toast'; import Markdown from '../../../markdown'; -import Touchable from '../../Touchable'; +import Touchable from '../Touchable'; import { useMediaAutoDownload } from '../../hooks/useMediaAutoDownload'; -import { useMessageUser } from '../../MessageRoomStore'; +import { useMessageUser } from '../../stores/MessageRoomStore'; import BlurComponent from '../OverlayComponent'; import { type TDownloadState } from '../../../../lib/methods/handleMediaDownload'; import messageStyles from '../../styles'; diff --git a/app/containers/message/Components/Attachments/components.ts b/app/containers/message/components/Attachments/components.ts similarity index 100% rename from app/containers/message/Components/Attachments/components.ts rename to app/containers/message/components/Attachments/components.ts diff --git a/app/containers/message/Components/Attachments/index.ts b/app/containers/message/components/Attachments/index.ts similarity index 100% rename from app/containers/message/Components/Attachments/index.ts rename to app/containers/message/components/Attachments/index.ts diff --git a/app/containers/message/Components/Attachments/utils.test.ts b/app/containers/message/components/Attachments/utils.test.ts similarity index 100% rename from app/containers/message/Components/Attachments/utils.test.ts rename to app/containers/message/components/Attachments/utils.test.ts diff --git a/app/containers/message/Components/Attachments/utils.ts b/app/containers/message/components/Attachments/utils.ts similarity index 100% rename from app/containers/message/Components/Attachments/utils.ts rename to app/containers/message/components/Attachments/utils.ts diff --git a/app/containers/message/Blocks.ts b/app/containers/message/components/Blocks.ts similarity index 79% rename from app/containers/message/Blocks.ts rename to app/containers/message/components/Blocks.ts index 8ebf648ee39..109491070d8 100644 --- a/app/containers/message/Blocks.ts +++ b/app/containers/message/components/Blocks.ts @@ -1,8 +1,8 @@ import { createElement } from 'react'; -import { messageBlockWithContext } from '../UIKit/MessageBlock'; -import { useBlockAction, useRid } from './MessageRoomStore'; -import { useBlocks } from './MessageStore'; +import { messageBlockWithContext } from '../../UIKit/MessageBlock'; +import { useBlockAction, useRid } from '../stores/MessageRoomStore'; +import { useBlocks } from '../stores/MessageStore'; const Blocks = () => { 'use memo'; diff --git a/app/containers/message/Broadcast.tsx b/app/containers/message/components/Broadcast.tsx similarity index 75% rename from app/containers/message/Broadcast.tsx rename to app/containers/message/components/Broadcast.tsx index 415bf423c1f..4bd5e28f3fe 100644 --- a/app/containers/message/Broadcast.tsx +++ b/app/containers/message/components/Broadcast.tsx @@ -1,14 +1,14 @@ import { Text, View } from 'react-native'; import Touchable from './Touchable'; -import { CustomIcon } from '../CustomIcon'; -import styles from './styles'; -import { BUTTON_HIT_SLOP } from './utils'; -import I18n from '../../i18n'; -import { themes } from '../../lib/constants/colors'; -import { useTheme } from '../../theme'; -import { useMessageAuthor, useMessageCtx } from './MessageStore'; -import { useBroadcast, useMessageUser, useReplyBroadcast } from './MessageRoomStore'; +import { CustomIcon } from '../../CustomIcon'; +import styles from '../styles'; +import { BUTTON_HIT_SLOP } from '../utils'; +import I18n from '../../../i18n'; +import { themes } from '../../../lib/constants/colors'; +import { useTheme } from '../../../theme'; +import { useMessageAuthor, useMessageCtx } from '../stores/MessageStore'; +import { useBroadcast, useMessageUser, useReplyBroadcast } from '../stores/MessageRoomStore'; const Broadcast = () => { 'use memo'; diff --git a/app/containers/message/CallButton.tsx b/app/containers/message/components/CallButton.tsx similarity index 70% rename from app/containers/message/CallButton.tsx rename to app/containers/message/components/CallButton.tsx index bc198477baa..53937e884ea 100644 --- a/app/containers/message/CallButton.tsx +++ b/app/containers/message/components/CallButton.tsx @@ -1,13 +1,13 @@ import { Text, View } from 'react-native'; import Touchable from './Touchable'; -import { BUTTON_HIT_SLOP } from './utils'; -import styles from './styles'; -import I18n from '../../i18n'; -import { CustomIcon } from '../CustomIcon'; -import { themes } from '../../lib/constants/colors'; -import { useTheme } from '../../theme'; -import { useHandleEnterCall } from './MessageRoomStore'; +import { BUTTON_HIT_SLOP } from '../utils'; +import styles from '../styles'; +import I18n from '../../../i18n'; +import { CustomIcon } from '../../CustomIcon'; +import { themes } from '../../../lib/constants/colors'; +import { useTheme } from '../../../theme'; +import { useHandleEnterCall } from '../stores/MessageRoomStore'; const CallButton = () => { 'use memo'; diff --git a/app/containers/message/Content.tsx b/app/containers/message/components/Content.tsx similarity index 84% rename from app/containers/message/Content.tsx rename to app/containers/message/components/Content.tsx index bef5e89ca55..56fabaf1087 100644 --- a/app/containers/message/Content.tsx +++ b/app/containers/message/components/Content.tsx @@ -1,14 +1,14 @@ import { Text, View } from 'react-native'; -import I18n from '../../i18n'; -import styles from './styles'; -import Markdown, { MarkdownPreview } from '../markdown'; +import I18n from '../../../i18n'; +import styles from '../styles'; +import Markdown, { MarkdownPreview } from '../../markdown'; import User from './User'; -import { messageHaveAuthorName, getInfoMessage, getPreviewMessageFromAttachment } from './utils'; -import { useTheme } from '../../theme'; -import { themes } from '../../lib/constants/colors'; -import { useSetting } from '../../lib/hooks/useSetting'; -import { type MessageTypesValues } from '../../definitions'; +import { messageHaveAuthorName, getInfoMessage, getPreviewMessageFromAttachment } from '../utils'; +import { useTheme } from '../../../theme'; +import { themes } from '../../../lib/constants/colors'; +import { useSetting } from '../../../lib/hooks/useSetting'; +import { type MessageTypesValues } from '../../../definitions'; import { useContentData, useIsEncrypted, @@ -19,8 +19,14 @@ import { useMessageText, useOnLinkPress, useThreadData -} from './MessageStore'; -import { useAutoTranslate, useGetCustomEmoji, useIsThreadRoom, useMessageUser, useNavToRoomInfo } from './MessageRoomStore'; +} from '../stores/MessageStore'; +import { + useAutoTranslate, + useGetCustomEmoji, + useIsThreadRoom, + useMessageUser, + useNavToRoomInfo +} from '../stores/MessageRoomStore'; const Content = () => { 'use memo'; diff --git a/app/containers/message/Discussion.tsx b/app/containers/message/components/Discussion.tsx similarity index 73% rename from app/containers/message/Discussion.tsx rename to app/containers/message/components/Discussion.tsx index deadd3c43e1..3c2b6631305 100644 --- a/app/containers/message/Discussion.tsx +++ b/app/containers/message/components/Discussion.tsx @@ -1,15 +1,15 @@ import { Text, View } from 'react-native'; import Touchable from './Touchable'; -import { BUTTON_HIT_SLOP, formatMessageCount } from './utils'; -import styles from './styles'; -import I18n from '../../i18n'; -import { CustomIcon } from '../CustomIcon'; -import { DISCUSSION } from './constants'; -import { formatDateThreads } from '../../lib/methods/helpers/room'; -import { useTheme } from '../../theme'; -import { useDiscussion, useMessageCtx, useMessageText } from './MessageStore'; -import { useOnDiscussionPress } from './MessageRoomStore'; +import { BUTTON_HIT_SLOP, formatMessageCount } from '../utils'; +import styles from '../styles'; +import I18n from '../../../i18n'; +import { CustomIcon } from '../../CustomIcon'; +import { DISCUSSION } from '../constants'; +import { formatDateThreads } from '../../../lib/methods/helpers/room'; +import { useTheme } from '../../../theme'; +import { useDiscussion, useMessageCtx, useMessageText } from '../stores/MessageStore'; +import { useOnDiscussionPress } from '../stores/MessageRoomStore'; const Discussion = () => { 'use memo'; diff --git a/app/containers/message/Emoji.tsx b/app/containers/message/components/Emoji.tsx similarity index 75% rename from app/containers/message/Emoji.tsx rename to app/containers/message/components/Emoji.tsx index 64ed5e46c6a..4c426e99162 100644 --- a/app/containers/message/Emoji.tsx +++ b/app/containers/message/components/Emoji.tsx @@ -1,8 +1,8 @@ import { Text } from 'react-native'; -import useShortnameToUnicode from '../../lib/hooks/useShortnameToUnicode'; -import CustomEmoji from '../EmojiPicker/CustomEmoji'; -import { type IMessageEmoji } from './interfaces'; +import useShortnameToUnicode from '../../../lib/hooks/useShortnameToUnicode'; +import CustomEmoji from '../../EmojiPicker/CustomEmoji'; +import { type IMessageEmoji } from '../interfaces'; const Emoji = ({ content, standardEmojiStyle, customEmojiStyle, getCustomEmoji }: IMessageEmoji) => { 'use memo'; diff --git a/app/containers/message/Message.tsx b/app/containers/message/components/Message.tsx similarity index 88% rename from app/containers/message/Message.tsx rename to app/containers/message/components/Message.tsx index 2ecf6150204..31c5cf90385 100644 --- a/app/containers/message/Message.tsx +++ b/app/containers/message/components/Message.tsx @@ -2,10 +2,10 @@ import { View, type ViewStyle, type AccessibilityActionEvent, type Accessibility import { A11y } from 'react-native-a11y-order'; import User from './User'; -import styles from './styles'; +import styles from '../styles'; import RepliedThread from './RepliedThread'; import MessageAvatar from './MessageAvatar'; -import Attachments from './Components/Attachments'; +import Attachments from './Attachments'; import Urls from './Urls'; import Thread from './Thread'; import Blocks from './Blocks'; @@ -14,19 +14,19 @@ import Broadcast from './Broadcast'; import Discussion from './Discussion'; import Content from './Content'; import CallButton from './CallButton'; -import { useTheme } from '../../theme'; -import RightIcons from './Components/RightIcons'; -import { WidthAwareView } from './Components/WidthAwareView'; +import { useTheme } from '../../../theme'; +import RightIcons from './RightIcons'; +import { WidthAwareView } from './WidthAwareView'; import MessageTime from './Time'; -import { useResponsiveLayout } from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; -import Quote from './Components/Attachments/Quote'; +import { useResponsiveLayout } from '../../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; +import Quote from './Attachments/Quote'; import Touch from './Touch'; -import { useLastFocusedMessageRef } from '../../lib/a11y/useLastFocusedMessageRef'; -import { useMessageAccessibilityLabel } from './hooks/useMessageAccessibilityLabel'; -import { useMessageAccessibilityActions } from './hooks/useMessageAccessibilityActions'; -import { useMessageAccessibilityHint } from './hooks/useMessageAccessibilityHint'; -import { useIsBeingEdited } from '../../views/RoomView/InteractionStore'; -import { useArchived, useAutoTranslate } from './MessageRoomStore'; +import { useLastFocusedMessageRef } from '../../../lib/a11y/useLastFocusedMessageRef'; +import { useMessageAccessibilityLabel } from '../hooks/useMessageAccessibilityLabel'; +import { useMessageAccessibilityActions } from '../hooks/useMessageAccessibilityActions'; +import { useMessageAccessibilityHint } from '../hooks/useMessageAccessibilityHint'; +import { useIsBeingEdited } from '../../../views/RoomView/InteractionStore'; +import { useArchived, useAutoTranslate } from '../stores/MessageRoomStore'; import { useBlocks, useContentData, @@ -40,7 +40,7 @@ import { useMessageStatus, useMessageText, useThreadPosition -} from './MessageStore'; +} from '../stores/MessageStore'; type TMessageProps = { isPreview?: boolean; diff --git a/app/containers/message/MessageAvatar.tsx b/app/containers/message/components/MessageAvatar.tsx similarity index 83% rename from app/containers/message/MessageAvatar.tsx rename to app/containers/message/components/MessageAvatar.tsx index 8dc8ad71a0d..3658e5ff67c 100644 --- a/app/containers/message/MessageAvatar.tsx +++ b/app/containers/message/components/MessageAvatar.tsx @@ -1,13 +1,13 @@ import { type ReactElement } from 'react'; import { View } from 'react-native'; -import Avatar from '../Avatar'; -import styles from './styles'; -import { type IMessageAvatar } from './interfaces'; -import { SubscriptionType } from '../../definitions'; -import { useResponsiveLayout } from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; -import { useAvatar, useMessageAuthor, useMessageGrouping } from './MessageStore'; -import { useGetCustomEmoji, useMessageUser, useNavToRoomInfo } from './MessageRoomStore'; +import Avatar from '../../Avatar'; +import styles from '../styles'; +import { type IMessageAvatar } from '../interfaces'; +import { SubscriptionType } from '../../../definitions'; +import { useResponsiveLayout } from '../../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; +import { useAvatar, useMessageAuthor, useMessageGrouping } from '../stores/MessageStore'; +import { useGetCustomEmoji, useMessageUser, useNavToRoomInfo } from '../stores/MessageRoomStore'; const AVATAR_BASE_SIZE = 36; diff --git a/app/containers/message/Components/OverlayComponent/index.tsx b/app/containers/message/components/OverlayComponent/index.tsx similarity index 100% rename from app/containers/message/Components/OverlayComponent/index.tsx rename to app/containers/message/components/OverlayComponent/index.tsx diff --git a/app/containers/message/Preview.tsx b/app/containers/message/components/Preview.tsx similarity index 76% rename from app/containers/message/Preview.tsx rename to app/containers/message/components/Preview.tsx index 0646128fc27..7f1b65382ee 100644 --- a/app/containers/message/Preview.tsx +++ b/app/containers/message/components/Preview.tsx @@ -1,8 +1,8 @@ -import Message from './index'; -import { MessageRoomProvider } from './MessageRoomStore'; -import { useAppSelector } from '../../lib/hooks/useAppSelector'; -import { getUserSelector } from '../../selectors/login'; -import { type TAnyMessageModel, type TGetCustomEmoji } from '../../definitions'; +import Message from '../index'; +import { MessageRoomProvider } from '../stores/MessageRoomStore'; +import { useAppSelector } from '../../../lib/hooks/useAppSelector'; +import { getUserSelector } from '../../../selectors/login'; +import { type TAnyMessageModel, type TGetCustomEmoji } from '../../../definitions'; const MessagePreview = ({ message }: { message: TAnyMessageModel }) => { 'use memo'; diff --git a/app/containers/message/Reactions.tsx b/app/containers/message/components/Reactions.tsx similarity index 89% rename from app/containers/message/Reactions.tsx rename to app/containers/message/components/Reactions.tsx index c4f4055455b..727ed60faa8 100644 --- a/app/containers/message/Reactions.tsx +++ b/app/containers/message/components/Reactions.tsx @@ -1,21 +1,21 @@ import { Text, useWindowDimensions, View } from 'react-native'; -import I18n from '../../i18n'; +import I18n from '../../../i18n'; import Touchable from './Touchable'; -import { CustomIcon } from '../CustomIcon'; -import styles from './styles'; +import { CustomIcon } from '../../CustomIcon'; +import styles from '../styles'; import Emoji from './Emoji'; -import { BUTTON_HIT_SLOP } from './utils'; -import { themes } from '../../lib/constants/colors'; -import { type TSupportedThemes, useTheme } from '../../theme'; -import { useMessageCtx, useMessageId, useReactions } from './MessageStore'; +import { BUTTON_HIT_SLOP } from '../utils'; +import { themes } from '../../../lib/constants/colors'; +import { type TSupportedThemes, useTheme } from '../../../theme'; +import { useMessageCtx, useMessageId, useReactions } from '../stores/MessageStore'; import { useGetCustomEmoji, useMessageUser, useOnReactionLongPress, useOnReactionPress, useReactionInit -} from './MessageRoomStore'; +} from '../stores/MessageRoomStore'; interface IReaction { _id: string; diff --git a/app/containers/message/RepliedThread.tsx b/app/containers/message/components/RepliedThread.tsx similarity index 75% rename from app/containers/message/RepliedThread.tsx rename to app/containers/message/components/RepliedThread.tsx index f8fde67b15e..dc344c18831 100644 --- a/app/containers/message/RepliedThread.tsx +++ b/app/containers/message/components/RepliedThread.tsx @@ -1,15 +1,15 @@ import { useEffect, useState } from 'react'; import { View } from 'react-native'; -import { CustomIcon } from '../CustomIcon'; -import styles from './styles'; -import I18n from '../../i18n'; -import { MarkdownPreview } from '../markdown'; -import { type IMessageRepliedThread } from './interfaces'; -import { useTheme } from '../../theme'; +import { CustomIcon } from '../../CustomIcon'; +import styles from '../styles'; +import I18n from '../../../i18n'; +import { MarkdownPreview } from '../../markdown'; +import { type IMessageRepliedThread } from '../interfaces'; +import { useTheme } from '../../../theme'; import { AvatarContainer } from './MessageAvatar'; -import { useFetchThreadName } from './MessageRoomStore'; -import { useIsEncrypted, useRepliedThreadData } from './MessageStore'; +import { useFetchThreadName } from '../stores/MessageRoomStore'; +import { useIsEncrypted, useRepliedThreadData } from '../stores/MessageStore'; const RepliedThread = ({ isHeader }: IMessageRepliedThread) => { 'use memo'; diff --git a/app/containers/message/Components/RightIcons/Edited.tsx b/app/containers/message/components/RightIcons/Edited.tsx similarity index 87% rename from app/containers/message/Components/RightIcons/Edited.tsx rename to app/containers/message/components/RightIcons/Edited.tsx index 69692424678..0e19ed3090c 100644 --- a/app/containers/message/Components/RightIcons/Edited.tsx +++ b/app/containers/message/components/RightIcons/Edited.tsx @@ -2,7 +2,7 @@ import { View } from 'react-native'; import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; -import { useIsEdited } from '../../MessageStore'; +import { useIsEdited } from '../../stores/MessageStore'; const Edited = ({ testID }: { testID?: string }) => { 'use memo'; diff --git a/app/containers/message/Components/RightIcons/Encrypted.tsx b/app/containers/message/components/RightIcons/Encrypted.tsx similarity index 77% rename from app/containers/message/Components/RightIcons/Encrypted.tsx rename to app/containers/message/components/RightIcons/Encrypted.tsx index 0bd32cc8bce..79f1ceb9d43 100644 --- a/app/containers/message/Components/RightIcons/Encrypted.tsx +++ b/app/containers/message/components/RightIcons/Encrypted.tsx @@ -1,10 +1,10 @@ -import Touchable from '../../Touchable'; +import Touchable from '../Touchable'; import { CustomIcon } from '../../../CustomIcon'; import { BUTTON_HIT_SLOP } from '../../utils'; import styles from '../../styles'; import { E2E_MESSAGE_TYPE } from '../../../../lib/constants/keys'; -import { useMessageField } from '../../MessageStore'; -import { useOnEncryptedPress } from '../../MessageRoomStore'; +import { useMessageField } from '../../stores/MessageStore'; +import { useOnEncryptedPress } from '../../stores/MessageRoomStore'; const Encrypted = () => { 'use memo'; diff --git a/app/containers/message/Components/RightIcons/MessageError.tsx b/app/containers/message/components/RightIcons/MessageError.tsx similarity index 80% rename from app/containers/message/Components/RightIcons/MessageError.tsx rename to app/containers/message/components/RightIcons/MessageError.tsx index a7c4d9e1b5b..8a07d06bb88 100644 --- a/app/containers/message/Components/RightIcons/MessageError.tsx +++ b/app/containers/message/components/RightIcons/MessageError.tsx @@ -1,11 +1,11 @@ -import Touchable from '../../Touchable'; +import Touchable from '../Touchable'; import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; import { BUTTON_HIT_SLOP } from '../../utils'; import { themes } from '../../../../lib/constants/colors'; import { useTheme } from '../../../../theme'; -import { useMessageCtx, useMessageStatus } from '../../MessageStore'; -import { useErrorActionsShow } from '../../MessageRoomStore'; +import { useMessageCtx, useMessageStatus } from '../../stores/MessageStore'; +import { useErrorActionsShow } from '../../stores/MessageRoomStore'; const MessageError = () => { 'use memo'; diff --git a/app/containers/message/Components/RightIcons/Pinned.tsx b/app/containers/message/components/RightIcons/Pinned.tsx similarity index 87% rename from app/containers/message/Components/RightIcons/Pinned.tsx rename to app/containers/message/components/RightIcons/Pinned.tsx index 26f9a4abb96..7f6187bf1b6 100644 --- a/app/containers/message/Components/RightIcons/Pinned.tsx +++ b/app/containers/message/components/RightIcons/Pinned.tsx @@ -2,7 +2,7 @@ import { type ReactElement } from 'react'; import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; -import { useMessageField } from '../../MessageStore'; +import { useMessageField } from '../../stores/MessageStore'; const Pinned = ({ testID }: { testID?: string }): ReactElement | null => { 'use memo'; diff --git a/app/containers/message/Components/RightIcons/ReadReceipt.tsx b/app/containers/message/components/RightIcons/ReadReceipt.tsx similarity index 86% rename from app/containers/message/Components/RightIcons/ReadReceipt.tsx rename to app/containers/message/components/RightIcons/ReadReceipt.tsx index bcbee82e96c..83421d79bf0 100644 --- a/app/containers/message/Components/RightIcons/ReadReceipt.tsx +++ b/app/containers/message/components/RightIcons/ReadReceipt.tsx @@ -1,8 +1,8 @@ import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; import { useTheme } from '../../../../theme'; -import { useMessageField } from '../../MessageStore'; -import { useIsReadReceiptEnabled } from '../../MessageRoomStore'; +import { useMessageField } from '../../stores/MessageStore'; +import { useIsReadReceiptEnabled } from '../../stores/MessageRoomStore'; const ReadReceipt = () => { 'use memo'; diff --git a/app/containers/message/Components/RightIcons/Translated.tsx b/app/containers/message/components/RightIcons/Translated.tsx similarity index 86% rename from app/containers/message/Components/RightIcons/Translated.tsx rename to app/containers/message/components/RightIcons/Translated.tsx index 9163f424885..a8e3f7a9452 100644 --- a/app/containers/message/Components/RightIcons/Translated.tsx +++ b/app/containers/message/components/RightIcons/Translated.tsx @@ -2,7 +2,7 @@ import { View } from 'react-native'; import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; -import { useMessageText } from '../../MessageStore'; +import { useMessageText } from '../../stores/MessageStore'; const Translated = () => { 'use memo'; diff --git a/app/containers/message/Components/RightIcons/index.tsx b/app/containers/message/components/RightIcons/index.tsx similarity index 100% rename from app/containers/message/Components/RightIcons/index.tsx rename to app/containers/message/components/RightIcons/index.tsx diff --git a/app/containers/message/Thread.tsx b/app/containers/message/components/Thread.tsx similarity index 85% rename from app/containers/message/Thread.tsx rename to app/containers/message/components/Thread.tsx index 7e2921e13ac..ac7da945808 100644 --- a/app/containers/message/Thread.tsx +++ b/app/containers/message/components/Thread.tsx @@ -1,12 +1,12 @@ import { Text, View } from 'react-native'; -import styles from './styles'; -import ThreadDetails from '../ThreadDetails'; -import I18n from '../../i18n'; -import { useTheme } from '../../theme'; +import styles from '../styles'; +import ThreadDetails from '../../ThreadDetails'; +import I18n from '../../../i18n'; +import { useTheme } from '../../../theme'; import Touchable from './Touchable'; -import { useMessageCtx, useMessageText, useReplies, useThreadBadgeColor, useThreadData } from './MessageStore'; -import { useIsThreadRoom, useMessageUser, useOnThreadPress, useToggleFollowThread } from './MessageRoomStore'; +import { useMessageCtx, useMessageText, useReplies, useThreadBadgeColor, useThreadData } from '../stores/MessageStore'; +import { useIsThreadRoom, useMessageUser, useOnThreadPress, useToggleFollowThread } from '../stores/MessageRoomStore'; const Thread = () => { 'use memo'; diff --git a/app/containers/message/Time.tsx b/app/containers/message/components/Time.tsx similarity index 66% rename from app/containers/message/Time.tsx rename to app/containers/message/components/Time.tsx index b16475e6855..4c814c1b8e3 100644 --- a/app/containers/message/Time.tsx +++ b/app/containers/message/components/Time.tsx @@ -1,10 +1,10 @@ import { Text, type TextStyle } from 'react-native'; -import dayjs from '../../lib/dayjs'; -import { useTheme } from '../../theme'; -import messageStyles from './styles'; -import { useMessageField } from './MessageStore'; -import { useTimeFormat } from './MessageRoomStore'; +import dayjs from '../../../lib/dayjs'; +import { useTheme } from '../../../theme'; +import messageStyles from '../styles'; +import { useMessageField } from '../stores/MessageStore'; +import { useTimeFormat } from '../stores/MessageRoomStore'; interface IMessageTime { ts?: Date; diff --git a/app/containers/message/Touch.tsx b/app/containers/message/components/Touch.tsx similarity index 97% rename from app/containers/message/Touch.tsx rename to app/containers/message/components/Touch.tsx index 882cb7a7c51..be7239ea2a9 100644 --- a/app/containers/message/Touch.tsx +++ b/app/containers/message/components/Touch.tsx @@ -12,8 +12,8 @@ import { } from 'react-native'; import { withKeyboardFocus } from 'react-native-external-keyboard'; -import { useTheme } from '../../theme'; -import { isIOS } from '../../lib/methods/helpers'; +import { useTheme } from '../../../theme'; +import { isIOS } from '../../../lib/methods/helpers'; export interface ITouchProps extends TouchableWithoutFeedbackProps { children: ReactNode; diff --git a/app/containers/message/Touchable.tsx b/app/containers/message/components/Touchable.tsx similarity index 88% rename from app/containers/message/Touchable.tsx rename to app/containers/message/components/Touchable.tsx index d82b9775033..1524dbfc341 100644 --- a/app/containers/message/Touchable.tsx +++ b/app/containers/message/components/Touchable.tsx @@ -1,7 +1,7 @@ import { type ReactNode, type FC } from 'react'; import { Pressable, type PressableProps } from 'react-native'; -import { useMessageLongPress } from './MessageStore'; +import { useMessageLongPress } from '../stores/MessageStore'; interface IProps extends PressableProps { children: ReactNode; diff --git a/app/containers/message/Urls.tsx b/app/containers/message/components/Urls.tsx similarity index 89% rename from app/containers/message/Urls.tsx rename to app/containers/message/components/Urls.tsx index cc46ea2f0f0..1d178e88910 100644 --- a/app/containers/message/Urls.tsx +++ b/app/containers/message/components/Urls.tsx @@ -4,18 +4,18 @@ import Clipboard from '@react-native-clipboard/clipboard'; import { Image } from 'expo-image'; import axios from 'axios'; -import { useAppSelector } from '../../lib/hooks/useAppSelector'; +import { useAppSelector } from '../../../lib/hooks/useAppSelector'; import Touchable from './Touchable'; -import openLink from '../../lib/methods/helpers/openLink'; -import sharedStyles from '../../views/Styles'; -import { useTheme } from '../../theme'; -import { LISTENER } from '../Toast'; -import EventEmitter from '../../lib/methods/helpers/events'; -import I18n from '../../i18n'; -import { type IUrl } from '../../definitions'; -import { WidthAwareContext } from './Components/WidthAwareView'; -import { useUrls } from './MessageStore'; -import { useBaseUrl, useMessageUser } from './MessageRoomStore'; +import openLink from '../../../lib/methods/helpers/openLink'; +import sharedStyles from '../../../views/Styles'; +import { useTheme } from '../../../theme'; +import { LISTENER } from '../../Toast'; +import EventEmitter from '../../../lib/methods/helpers/events'; +import I18n from '../../../i18n'; +import { type IUrl } from '../../../definitions'; +import { WidthAwareContext } from './WidthAwareView'; +import { useUrls } from '../stores/MessageStore'; +import { useBaseUrl, useMessageUser } from '../stores/MessageRoomStore'; const styles = StyleSheet.create({ container: { diff --git a/app/containers/message/User.tsx b/app/containers/message/components/User.tsx similarity index 82% rename from app/containers/message/User.tsx rename to app/containers/message/components/User.tsx index 7b03d8132af..4478e5bba46 100644 --- a/app/containers/message/User.tsx +++ b/app/containers/message/components/User.tsx @@ -1,15 +1,15 @@ import { Pressable, StyleSheet, Text, View } from 'react-native'; -import { type MessageTypesValues, SubscriptionType } from '../../definitions'; -import { useTheme } from '../../theme'; -import sharedStyles from '../../views/Styles'; -import RightIcons from './Components/RightIcons'; -import { messageHaveAuthorName } from './utils'; +import { type MessageTypesValues, SubscriptionType } from '../../../definitions'; +import { useTheme } from '../../../theme'; +import sharedStyles from '../../../views/Styles'; +import RightIcons from './RightIcons'; +import { messageHaveAuthorName } from '../utils'; import MessageTime from './Time'; -import { useResponsiveLayout } from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; -import { useSetting } from '../../lib/hooks/useSetting'; -import { useMessageAuthor, useMessageGrouping, useMessageMeta } from './MessageStore'; -import { useMessageUser, useNavToRoomInfo } from './MessageRoomStore'; +import { useResponsiveLayout } from '../../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; +import { useSetting } from '../../../lib/hooks/useSetting'; +import { useMessageAuthor, useMessageGrouping, useMessageMeta } from '../stores/MessageStore'; +import { useMessageUser, useNavToRoomInfo } from '../stores/MessageRoomStore'; const styles = StyleSheet.create({ container: { diff --git a/app/containers/message/Components/WidthAwareView.tsx b/app/containers/message/components/WidthAwareView.tsx similarity index 100% rename from app/containers/message/Components/WidthAwareView.tsx rename to app/containers/message/components/WidthAwareView.tsx diff --git a/app/containers/message/Content.test.tsx b/app/containers/message/components/__tests__/Content.test.tsx similarity index 92% rename from app/containers/message/Content.test.tsx rename to app/containers/message/components/__tests__/Content.test.tsx index 229c0f4c335..fa3d891a521 100644 --- a/app/containers/message/Content.test.tsx +++ b/app/containers/message/components/__tests__/Content.test.tsx @@ -1,13 +1,13 @@ import { Provider } from 'react-redux'; import { render } from '@testing-library/react-native'; -import Content from './Content'; -import { MessageProvider } from './MessageStore'; -import { MessageRoomProvider, pickMessageRoomState } from './MessageRoomStore'; -import { setUser } from '../../actions/login'; -import { mockedStore } from '../../reducers/mockedStore'; -import { type IAttachment, type TAnyMessageModel } from '../../definitions'; -import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; +import Content from '../Content'; +import { MessageProvider } from '../../stores/MessageStore'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { setUser } from '../../../../actions/login'; +import { mockedStore } from '../../../../reducers/mockedStore'; +import { type IAttachment, type TAnyMessageModel } from '../../../../definitions'; +import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../../../lib/constants/keys'; mockedStore.dispatch( setUser({ diff --git a/app/containers/message/components/__tests__/Message.test.tsx b/app/containers/message/components/__tests__/Message.test.tsx new file mode 100644 index 00000000000..743b38efa4f --- /dev/null +++ b/app/containers/message/components/__tests__/Message.test.tsx @@ -0,0 +1,12 @@ +import { generateSnapshots } from '../../../../../.rnstorybook/generateSnapshots'; +import * as stories from '../stories/Message.stories'; + +jest.mock('../../../../lib/methods/handleMediaDownload.ts', () => ({ + downloadMediaFile: jest.fn(), + getMediaCache: jest.fn(), + isDownloadActive: jest.fn() +})); + +jest.unmock('../../../../lib/hooks/useResponsiveLayout/useResponsiveLayout'); + +generateSnapshots(stories); diff --git a/app/containers/message/Preview.test.tsx b/app/containers/message/components/__tests__/Preview.test.tsx similarity index 87% rename from app/containers/message/Preview.test.tsx rename to app/containers/message/components/__tests__/Preview.test.tsx index 81ed39005e5..28ec84ba56e 100644 --- a/app/containers/message/Preview.test.tsx +++ b/app/containers/message/components/__tests__/Preview.test.tsx @@ -1,11 +1,11 @@ import { Provider } from 'react-redux'; import { render } from '@testing-library/react-native'; -import MessagePreview from './Preview'; -import { mockedStore } from '../../reducers/mockedStore'; -import { type TAnyMessageModel } from '../../definitions'; +import MessagePreview from '../Preview'; +import { mockedStore } from '../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../definitions'; -jest.mock('./Touch', () => { +jest.mock('../Touch', () => { const { forwardRef } = require('react'); const { TouchableOpacity } = require('react-native'); return forwardRef(({ children, onPress, onLongPress, ...props }: any, ref: any) => ( diff --git a/app/containers/message/Reactions.test.tsx b/app/containers/message/components/__tests__/Reactions.test.tsx similarity index 92% rename from app/containers/message/Reactions.test.tsx rename to app/containers/message/components/__tests__/Reactions.test.tsx index 2d9e744e173..6f052e3b332 100644 --- a/app/containers/message/Reactions.test.tsx +++ b/app/containers/message/components/__tests__/Reactions.test.tsx @@ -1,10 +1,10 @@ import { render, fireEvent } from '@testing-library/react-native'; -import Reactions from './Reactions'; -import { MessageProviders } from './testHelpers'; -import { setUser } from '../../actions/login'; -import { mockedStore } from '../../reducers/mockedStore'; -import { type IReaction, type TAnyMessageModel } from '../../definitions'; +import Reactions from '../Reactions'; +import { MessageProviders } from '../../testHelpers'; +import { setUser } from '../../../../actions/login'; +import { mockedStore } from '../../../../reducers/mockedStore'; +import { type IReaction, type TAnyMessageModel } from '../../../../definitions'; const initialMockedStoreState = () => { mockedStore.dispatch( diff --git a/app/containers/message/Thread.test.tsx b/app/containers/message/components/__tests__/Thread.test.tsx similarity index 84% rename from app/containers/message/Thread.test.tsx rename to app/containers/message/components/__tests__/Thread.test.tsx index 590e8f193a2..f3c95bddbc9 100644 --- a/app/containers/message/Thread.test.tsx +++ b/app/containers/message/components/__tests__/Thread.test.tsx @@ -1,9 +1,9 @@ import { render } from '@testing-library/react-native'; -import Thread from './Thread'; -import { MessageProvider } from './MessageStore'; -import { MessageRoomProvider, pickMessageRoomState } from './MessageRoomStore'; -import { type TAnyMessageModel } from '../../definitions'; +import Thread from '../Thread'; +import { MessageProvider } from '../../stores/MessageStore'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { type TAnyMessageModel } from '../../../../definitions'; const baseContextValue = { threadBadgeColor: undefined, diff --git a/app/containers/message/__snapshots__/Message.test.tsx.snap b/app/containers/message/components/__tests__/__snapshots__/Message.test.tsx.snap similarity index 100% rename from app/containers/message/__snapshots__/Message.test.tsx.snap rename to app/containers/message/components/__tests__/__snapshots__/Message.test.tsx.snap diff --git a/app/containers/message/Message.stories.tsx b/app/containers/message/components/stories/Message.stories.tsx similarity index 99% rename from app/containers/message/Message.stories.tsx rename to app/containers/message/components/stories/Message.stories.tsx index bbe0848c243..114e7629445 100644 --- a/app/containers/message/Message.stories.tsx +++ b/app/containers/message/components/stories/Message.stories.tsx @@ -1,23 +1,23 @@ import { ScrollView } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; -import MessageContainer from './index'; -import { type ICustomEmoji } from '../../definitions/IEmoji'; -import { type TAnyMessageModel } from '../../definitions'; -import { E2E_MESSAGE_TYPE } from '../../lib/constants/keys'; -import { messagesStatus } from '../../lib/constants/messagesStatus'; -import { themes } from '../../lib/constants/colors'; -import MessageSeparator from '../MessageSeparator'; +import MessageContainer from '../../index'; +import { type ICustomEmoji } from '../../../../definitions/IEmoji'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { E2E_MESSAGE_TYPE } from '../../../../lib/constants/keys'; +import { messagesStatus } from '../../../../lib/constants/messagesStatus'; +import { themes } from '../../../../lib/constants/colors'; +import MessageSeparator from '../../../MessageSeparator'; import { BASE_ROW_HEIGHT, BASE_ROW_HEIGHT_CONDENSED, FONT_SCALE_LIMIT, ResponsiveLayoutContext -} from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; -import { mockedStore as store } from '../../reducers/mockedStore'; -import { updateSettings } from '../../actions/settings'; -import { createInteractionStore, InteractionStoreContext } from '../../views/RoomView/InteractionStore'; -import { MessageRoomProvider, pickMessageRoomState } from './MessageRoomStore'; +} from '../../../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; +import { mockedStore as store } from '../../../../reducers/mockedStore'; +import { updateSettings } from '../../../../actions/settings'; +import { createInteractionStore, InteractionStoreContext } from '../../../../views/RoomView/InteractionStore'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; const _theme = 'light'; diff --git a/app/containers/message/hooks/useImageDescriptionLabel.test.ts b/app/containers/message/hooks/__tests__/useImageDescriptionLabel.test.ts similarity index 95% rename from app/containers/message/hooks/useImageDescriptionLabel.test.ts rename to app/containers/message/hooks/__tests__/useImageDescriptionLabel.test.ts index 759788da4b1..a22d91f5b1d 100644 --- a/app/containers/message/hooks/useImageDescriptionLabel.test.ts +++ b/app/containers/message/hooks/__tests__/useImageDescriptionLabel.test.ts @@ -1,9 +1,9 @@ import { renderHook } from '@testing-library/react-native'; -import { useImageDescriptionLabel } from './useImageDescriptionLabel'; -import { type IAttachment } from '../../../definitions'; +import { useImageDescriptionLabel } from '../useImageDescriptionLabel'; +import { type IAttachment } from '../../../../definitions'; -jest.mock('../../../lib/hooks/useAltTextSupported', () => ({ +jest.mock('../../../../lib/hooks/useAltTextSupported', () => ({ useAltTextSupported: () => false })); diff --git a/app/containers/message/hooks/useMessageAccessibilityActions.test.ts b/app/containers/message/hooks/__tests__/useMessageAccessibilityActions.test.ts similarity index 86% rename from app/containers/message/hooks/useMessageAccessibilityActions.test.ts rename to app/containers/message/hooks/__tests__/useMessageAccessibilityActions.test.ts index 8be877613bd..0c955a10fd5 100644 --- a/app/containers/message/hooks/useMessageAccessibilityActions.test.ts +++ b/app/containers/message/hooks/__tests__/useMessageAccessibilityActions.test.ts @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react-native'; -import { useMessageAccessibilityActions } from './useMessageAccessibilityActions'; +import { useMessageAccessibilityActions } from '../useMessageAccessibilityActions'; describe('useMessageAccessibilityActions', () => { it('returns undefined when disabled', () => { diff --git a/app/containers/message/hooks/useMessageAccessibilityHint.test.tsx b/app/containers/message/hooks/__tests__/useMessageAccessibilityHint.test.tsx similarity index 81% rename from app/containers/message/hooks/useMessageAccessibilityHint.test.tsx rename to app/containers/message/hooks/__tests__/useMessageAccessibilityHint.test.tsx index ed007395d2a..967f45fd093 100644 --- a/app/containers/message/hooks/useMessageAccessibilityHint.test.tsx +++ b/app/containers/message/hooks/__tests__/useMessageAccessibilityHint.test.tsx @@ -1,9 +1,9 @@ import { render } from '@testing-library/react-native'; -import { useMessageAccessibilityHint } from './useMessageAccessibilityHint'; -import { MessageRoomProvider, pickMessageRoomState } from '../MessageRoomStore'; -import { MessageProvider } from '../MessageStore'; -import { type TAnyMessageModel } from '../../../definitions'; +import { useMessageAccessibilityHint } from '../useMessageAccessibilityHint'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageProvider } from '../../stores/MessageStore'; +import { type TAnyMessageModel } from '../../../../definitions'; const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel => ({ id: 'msg-1', ...overrides } as TAnyMessageModel); diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx b/app/containers/message/hooks/__tests__/useMessageAccessibilityLabel.test.tsx similarity index 89% rename from app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx rename to app/containers/message/hooks/__tests__/useMessageAccessibilityLabel.test.tsx index 6b98ccf1a09..a273bd5f328 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.test.tsx +++ b/app/containers/message/hooks/__tests__/useMessageAccessibilityLabel.test.tsx @@ -1,15 +1,15 @@ import { Provider } from 'react-redux'; import { render } from '@testing-library/react-native'; -import { useMessageAccessibilityLabel } from './useMessageAccessibilityLabel'; -import { MessageProvider } from '../MessageStore'; -import { MessageRoomProvider, pickMessageRoomState } from '../MessageRoomStore'; -import { updateSettings } from '../../../actions/settings'; -import { mockedStore } from '../../../reducers/mockedStore'; -import { type TAnyMessageModel } from '../../../definitions'; -import { E2E_MESSAGE_TYPE } from '../../../lib/constants/keys'; - -jest.mock('../../../lib/hooks/useAltTextSupported', () => ({ +import { useMessageAccessibilityLabel } from '../useMessageAccessibilityLabel'; +import { MessageProvider } from '../../stores/MessageStore'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { updateSettings } from '../../../../actions/settings'; +import { mockedStore } from '../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { E2E_MESSAGE_TYPE } from '../../../../lib/constants/keys'; + +jest.mock('../../../../lib/hooks/useAltTextSupported', () => ({ useAltTextSupported: () => false })); diff --git a/app/containers/message/hooks/useMediaAutoDownload.tsx b/app/containers/message/hooks/useMediaAutoDownload.tsx index 29302939675..0b0ce4771da 100644 --- a/app/containers/message/hooks/useMediaAutoDownload.tsx +++ b/app/containers/message/hooks/useMediaAutoDownload.tsx @@ -13,8 +13,8 @@ import { } from '../../../lib/methods/handleMediaDownload'; import { emitter } from '../../../lib/methods/helpers/emitter'; import { formatAttachmentUrl } from '../../../lib/methods/helpers/formatAttachmentUrl'; -import { useBaseUrl, useMessageUser } from '../MessageRoomStore'; -import { useMessageId } from '../MessageStore'; +import { useBaseUrl, useMessageUser } from '../stores/MessageRoomStore'; +import { useMessageId } from '../stores/MessageStore'; import { useFile } from './useFile'; const getFileType = (file: IAttachment): MediaTypes | null => { diff --git a/app/containers/message/hooks/useMessageAccessibilityHint.ts b/app/containers/message/hooks/useMessageAccessibilityHint.ts index feb81526e4d..7d567c94038 100644 --- a/app/containers/message/hooks/useMessageAccessibilityHint.ts +++ b/app/containers/message/hooks/useMessageAccessibilityHint.ts @@ -1,6 +1,6 @@ import i18n from '../../../i18n'; -import { useIsThreadRoom } from '../MessageRoomStore'; -import { useThreadData } from '../MessageStore'; +import { useIsThreadRoom } from '../stores/MessageRoomStore'; +import { useThreadData } from '../stores/MessageStore'; export const useMessageAccessibilityHint = (): string | undefined => { 'use memo'; diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.ts b/app/containers/message/hooks/useMessageAccessibilityLabel.ts index 3c803bd2aa6..b29ba47183e 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.ts +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.ts @@ -12,8 +12,8 @@ import { useMessageText, useThreadData, useThreadPosition -} from '../MessageStore'; -import { useAutoTranslate, useIsReadReceiptEnabled } from '../MessageRoomStore'; +} from '../stores/MessageStore'; +import { useAutoTranslate, useIsReadReceiptEnabled } from '../stores/MessageRoomStore'; import { useSetting } from '../../../lib/hooks/useSetting'; const stripMentions = (label: string, mentions: IUserMention[] = [], channels: IUserChannel[] = []) => { diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 4df540aa3f5..e583c8b5abf 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -1,9 +1,9 @@ import { memo } from 'react'; -import Message from './Message'; +import Message from './components/Message'; import { type TAnyMessageModel } from '../../definitions'; import MessageSeparator from '../MessageSeparator'; -import { MessageProvider } from './MessageStore'; +import { MessageProvider } from './stores/MessageStore'; interface IMessageContainerProps { item: TAnyMessageModel; diff --git a/app/containers/message/MessageRoomStore.tsx b/app/containers/message/stores/MessageRoomStore.tsx similarity index 97% rename from app/containers/message/MessageRoomStore.tsx rename to app/containers/message/stores/MessageRoomStore.tsx index 5e749dc6e50..2d7918fc9b8 100644 --- a/app/containers/message/MessageRoomStore.tsx +++ b/app/containers/message/stores/MessageRoomStore.tsx @@ -2,9 +2,9 @@ import { createContext, useContext, useEffect, useState, type ReactElement, type import { createStore, useStore } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; -import { type TGetCustomEmoji } from '../../definitions/IEmoji'; -import { type IAttachment, type TAnyMessageModel } from '../../definitions'; -import { type IRoomInfoParam } from '../../views/SearchMessagesView'; +import { type TGetCustomEmoji } from '../../../definitions/IEmoji'; +import { type IAttachment, type TAnyMessageModel } from '../../../definitions'; +import { type IRoomInfoParam } from '../../../views/SearchMessagesView'; export type MessageRoomState = { // stable handlers diff --git a/app/containers/message/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx similarity index 96% rename from app/containers/message/MessageStore.tsx rename to app/containers/message/stores/MessageStore.tsx index 72760ad54ba..94aa66242bf 100644 --- a/app/containers/message/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -3,13 +3,13 @@ import { createStore, useStore } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; import { Keyboard } from 'react-native'; -import { type IAttachment, type TAnyMessageModel } from '../../definitions'; -import { getMessageTranslation } from './utils'; -import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; -import { messagesStatus } from '../../lib/constants/messagesStatus'; -import { debounce } from '../../lib/methods/helpers'; -import openLink from '../../lib/methods/helpers/openLink'; -import { useTheme } from '../../theme'; +import { type IAttachment, type TAnyMessageModel } from '../../../definitions'; +import { getMessageTranslation } from '../utils'; +import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../../lib/constants/keys'; +import { messagesStatus } from '../../../lib/constants/messagesStatus'; +import { debounce } from '../../../lib/methods/helpers'; +import openLink from '../../../lib/methods/helpers/openLink'; +import { useTheme } from '../../../theme'; import { useArchived, useAutoTranslate, diff --git a/app/containers/message/MessageRoomStore.test.tsx b/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx similarity index 95% rename from app/containers/message/MessageRoomStore.test.tsx rename to app/containers/message/stores/__tests__/MessageRoomStore.test.tsx index 551e7abc03e..6c21d177276 100644 --- a/app/containers/message/MessageRoomStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx @@ -1,6 +1,6 @@ import { act, render } from '@testing-library/react-native'; -import { MessageRoomProvider, pickMessageRoomState, useTimeFormat } from './MessageRoomStore'; +import { MessageRoomProvider, pickMessageRoomState, useTimeFormat } from '../MessageRoomStore'; describe('MessageRoomStore', () => { it('mirrors updated provider props into the store after mount', () => { diff --git a/app/containers/message/MessageStore.test.tsx b/app/containers/message/stores/__tests__/MessageStore.test.tsx similarity index 98% rename from app/containers/message/MessageStore.test.tsx rename to app/containers/message/stores/__tests__/MessageStore.test.tsx index 55c22e1fa04..a50a0c0e6f1 100644 --- a/app/containers/message/MessageStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageStore.test.tsx @@ -1,10 +1,10 @@ import { act, fireEvent, render } from '@testing-library/react-native'; import { Text } from 'react-native'; -import { type TAnyMessageModel } from '../../definitions'; -import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; -import { messagesStatus } from '../../lib/constants/messagesStatus'; -import { MessageRoomProvider, pickMessageRoomState, type MessageRoomState } from './MessageRoomStore'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../../../lib/constants/keys'; +import { messagesStatus } from '../../../../lib/constants/messagesStatus'; +import { MessageRoomProvider, pickMessageRoomState, type MessageRoomState } from '../MessageRoomStore'; import { MessageProvider, useAvatar, @@ -28,7 +28,7 @@ import { useThreadData, useThreadPosition, useUrls -} from './MessageStore'; +} from '../MessageStore'; type Subscriber = () => void; diff --git a/app/containers/message/testHelpers.tsx b/app/containers/message/testHelpers.tsx index b146c447f5a..d34ecd40dd9 100644 --- a/app/containers/message/testHelpers.tsx +++ b/app/containers/message/testHelpers.tsx @@ -4,8 +4,8 @@ import { render } from '@testing-library/react-native'; import { type TAnyMessageModel } from '../../definitions'; import { mockedStore } from '../../reducers/mockedStore'; -import { MessageProvider } from './MessageStore'; -import { MessageRoomProvider, pickMessageRoomState, type MessageRoomState } from './MessageRoomStore'; +import { MessageProvider } from './stores/MessageStore'; +import { MessageRoomProvider, pickMessageRoomState, type MessageRoomState } from './stores/MessageRoomStore'; interface IMessageProvidersOptions { item?: TAnyMessageModel; diff --git a/app/views/ForwardMessageView/index.tsx b/app/views/ForwardMessageView/index.tsx index 0ab9c0690c9..739819b91c0 100644 --- a/app/views/ForwardMessageView/index.tsx +++ b/app/views/ForwardMessageView/index.tsx @@ -15,7 +15,7 @@ import SelectPersonOrChannel from './SelectPersonOrChannel'; import { useAppSelector } from '../../lib/hooks/useAppSelector'; import { type NewMessageStackParamList } from '../../stacks/types'; import { postMessage } from '../../lib/services/restApi'; -import MessagePreview from '../../containers/message/Preview'; +import MessagePreview from '../../containers/message/components/Preview'; import EventEmitter from '../../lib/methods/helpers/events'; import { LISTENER } from '../../containers/Toast'; diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index c68c1412fa0..001eafb7ed2 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -7,7 +7,7 @@ import { type CompositeNavigationProp, type RouteProp } from '@react-navigation/ import { type MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import Message from '../../containers/message'; -import { MessageRoomProvider } from '../../containers/message/MessageRoomStore'; +import { MessageRoomProvider } from '../../containers/message/stores/MessageRoomStore'; import ActivityIndicator from '../../containers/ActivityIndicator'; import I18n from '../../i18n'; import getFileUrlAndTypeFromMessage from './getFileUrlAndTypeFromMessage'; diff --git a/app/views/ModalBlockView.tsx b/app/views/ModalBlockView.tsx index 3eb791e8517..3360c62098d 100644 --- a/app/views/ModalBlockView.tsx +++ b/app/views/ModalBlockView.tsx @@ -16,8 +16,8 @@ import { ContainerTypes, ModalActions, type TModalAction } from '../containers/U import { triggerBlockAction, triggerCancel, triggerSubmitView } from '../lib/methods/triggerActions'; import { type IApplicationState, type TAnyMessageModel } from '../definitions'; import KeyboardView from '../containers/KeyboardView'; -import { MessageRoomProvider } from '../containers/message/MessageRoomStore'; -import { MessageProvider } from '../containers/message/MessageStore'; +import { MessageRoomProvider } from '../containers/message/stores/MessageRoomStore'; +import { MessageProvider } from '../containers/message/stores/MessageStore'; import { getUserSelector } from '../selectors/login'; const styles = StyleSheet.create({ diff --git a/app/views/RoomView/LoadMore/LoadMore.stories.tsx b/app/views/RoomView/LoadMore/LoadMore.stories.tsx index 0aafe71153c..efed137e4dd 100644 --- a/app/views/RoomView/LoadMore/LoadMore.stories.tsx +++ b/app/views/RoomView/LoadMore/LoadMore.stories.tsx @@ -2,7 +2,7 @@ import { ScrollView } from 'react-native'; import { longText } from '../../../../.rnstorybook/utils'; import { ThemeContext, type TSupportedThemes } from '../../../theme'; -import { Message } from '../../../containers/message/Message.stories'; +import { Message } from '../../../containers/message/components/stories/Message.stories'; import { MessageTypeLoad } from '../../../lib/constants/messageTypeLoad'; import { themes } from '../../../lib/constants/colors'; import LoadMoreComponent from '.'; diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 94fd6d61842..aafcf6d0180 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -99,7 +99,7 @@ import { goRoom, type TGoRoomItem } from '../../lib/methods/helpers/goRoom'; import { ComposerAttachments, type IMessageComposerRef, MessageComposerContainer } from '../../containers/MessageComposer'; import { RoomContext } from './context'; import { createInteractionStore, InteractionStoreContext, type InteractionStore } from './InteractionStore'; -import { MessageRoomProvider } from '../../containers/message/MessageRoomStore'; +import { MessageRoomProvider } from '../../containers/message/stores/MessageRoomStore'; import AudioManager from '../../lib/methods/AudioManager'; import { type IListContainerRef, type TListRef } from './List/definitions'; import { resolveJumpAnchor } from './services/resolveJumpAnchor'; diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index 0e3e2c32730..505731f837a 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -11,7 +11,7 @@ import { FormTextInput } from '../../containers/TextInput'; import ActivityIndicator from '../../containers/ActivityIndicator'; import Markdown from '../../containers/markdown'; import Message from '../../containers/message'; -import { MessageRoomProvider } from '../../containers/message/MessageRoomStore'; +import { MessageRoomProvider } from '../../containers/message/stores/MessageRoomStore'; import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps'; import I18n from '../../i18n'; import log from '../../lib/methods/helpers/log'; From b503abd2180796b746a0576d295b174eebb435bb Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 14:29:59 -0300 Subject: [PATCH 061/144] refactor: remove reducible props from message leaves (NATIVE-1394) RightIcons: drop the msg prop. Pinned and Edited now self-read the message text via useMessageText to build their own testIDs, which also fixes a latent bug where the header path rendered undefined-pinned / undefined-edited testIDs (User rendered RightIcons without msg). Time: drop the dead ts and style props (ts is read from the store; no caller ever passed style). Preview time format: MessageRoomProvider defaults timeFormat from the Message_TimeFormat setting when the seed is absent, so RoomView and Preview stop passing it. MessagesView and SearchMessagesView keep their explicit full date-time override. Test suites that mount MessageRoomProvider directly are wrapped in a redux Provider since the provider now reads the setting. --- .../Attachments/Image/Container.test.tsx | 18 +- app/containers/message/components/Message.tsx | 2 +- app/containers/message/components/Preview.tsx | 10 +- .../message/components/RightIcons/Edited.tsx | 7 +- .../message/components/RightIcons/Pinned.tsx | 7 +- .../message/components/RightIcons/index.tsx | 10 +- app/containers/message/components/Time.tsx | 11 +- app/containers/message/components/User.tsx | 4 +- .../components/__tests__/Thread.test.tsx | 26 +- .../__snapshots__/Message.test.tsx.snap | 275 ++---------------- .../useMessageAccessibilityHint.test.tsx | 14 +- .../message/stores/MessageRoomStore.tsx | 7 +- .../__tests__/MessageRoomStore.test.tsx | 10 +- .../stores/__tests__/MessageStore.test.tsx | 26 +- .../__snapshots__/LoadMore.test.tsx.snap | 9 - app/views/RoomView/definitions.ts | 1 - app/views/RoomView/index.tsx | 15 +- 17 files changed, 109 insertions(+), 343 deletions(-) diff --git a/app/containers/message/components/Attachments/Image/Container.test.tsx b/app/containers/message/components/Attachments/Image/Container.test.tsx index 9d94585424f..d99f49a7a11 100644 --- a/app/containers/message/components/Attachments/Image/Container.test.tsx +++ b/app/containers/message/components/Attachments/Image/Container.test.tsx @@ -1,10 +1,12 @@ import { render } from '@testing-library/react-native'; import { type ComponentProps } from 'react'; import { A11y } from 'react-native-a11y-order'; +import { Provider } from 'react-redux'; import { type TAnyMessageModel } from '../../../../../definitions'; import { MessageProvider } from '../../../stores/MessageStore'; import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; +import { mockedStore } from '../../../../../reducers/mockedStore'; import ImageContainer from './Container'; jest.mock('../../../../markdown', () => { @@ -42,13 +44,15 @@ const renderImageContainer = (props?: Partial<ComponentProps<typeof ImageContain }; const item = { id: contextValue.id } as unknown as TAnyMessageModel; return render( - <A11y.Order> - <MessageRoomProvider {...pickMessageRoomState(contextValue)}> - <MessageProvider item={item}> - <ImageContainer file={{ image_url: 'https://open.rocket.chat/image.png', image_type: 'image/png' }} {...props} /> - </MessageProvider> - </MessageRoomProvider> - </A11y.Order> + <Provider store={mockedStore}> + <A11y.Order> + <MessageRoomProvider {...pickMessageRoomState(contextValue)}> + <MessageProvider item={item}> + <ImageContainer file={{ image_url: 'https://open.rocket.chat/image.png', image_type: 'image/png' }} {...props} /> + </MessageProvider> + </MessageRoomProvider> + </A11y.Order> + </Provider> ); }; diff --git a/app/containers/message/components/Message.tsx b/app/containers/message/components/Message.tsx index 31c5cf90385..c9791bca7f6 100644 --- a/app/containers/message/components/Message.tsx +++ b/app/containers/message/components/Message.tsx @@ -187,7 +187,7 @@ const Message = (props: TMessageProps & IMessageA11y) => { <View style={styles.messageContent}> <MessageInner isPreview={props.isPreview} /> </View> - {!isHeader ? <RightIcons msg={messageText} /> : null} + {!isHeader ? <RightIcons /> : null} </View> </A11y.Index> </View> diff --git a/app/containers/message/components/Preview.tsx b/app/containers/message/components/Preview.tsx index 7f1b65382ee..1917a880f49 100644 --- a/app/containers/message/components/Preview.tsx +++ b/app/containers/message/components/Preview.tsx @@ -7,10 +7,9 @@ import { type TAnyMessageModel, type TGetCustomEmoji } from '../../../definition const MessagePreview = ({ message }: { message: TAnyMessageModel }) => { 'use memo'; - const { user, baseUrl, Message_TimeFormat, customEmojis } = useAppSelector(state => ({ + const { user, baseUrl, customEmojis } = useAppSelector(state => ({ user: getUserSelector(state), baseUrl: state.server.server, - Message_TimeFormat: state.settings.Message_TimeFormat as string, customEmojis: state.customEmojis })); @@ -19,12 +18,7 @@ const MessagePreview = ({ message }: { message: TAnyMessageModel }) => { return emoji ?? null; }; return ( - <MessageRoomProvider - user={user} - baseUrl={baseUrl} - getCustomEmoji={getCustomEmoji} - rid={message.rid} - timeFormat={Message_TimeFormat}> + <MessageRoomProvider user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} rid={message.rid}> <Message item={message} isPreview /> </MessageRoomProvider> ); diff --git a/app/containers/message/components/RightIcons/Edited.tsx b/app/containers/message/components/RightIcons/Edited.tsx index 0e19ed3090c..0b865ed17d1 100644 --- a/app/containers/message/components/RightIcons/Edited.tsx +++ b/app/containers/message/components/RightIcons/Edited.tsx @@ -2,19 +2,20 @@ import { View } from 'react-native'; import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; -import { useIsEdited } from '../../stores/MessageStore'; +import { useIsEdited, useMessageText } from '../../stores/MessageStore'; -const Edited = ({ testID }: { testID?: string }) => { +const Edited = () => { 'use memo'; const isEdited = useIsEdited(); + const { messageText } = useMessageText(); if (!isEdited) { return null; } return ( - <View testID={testID} style={styles.rightIcons}> + <View testID={`${messageText}-edited`} style={styles.rightIcons}> <CustomIcon name='edit' size={16} /> </View> ); diff --git a/app/containers/message/components/RightIcons/Pinned.tsx b/app/containers/message/components/RightIcons/Pinned.tsx index 7f6187bf1b6..665cf53bfc4 100644 --- a/app/containers/message/components/RightIcons/Pinned.tsx +++ b/app/containers/message/components/RightIcons/Pinned.tsx @@ -2,14 +2,15 @@ import { type ReactElement } from 'react'; import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; -import { useMessageField } from '../../stores/MessageStore'; +import { useMessageField, useMessageText } from '../../stores/MessageStore'; -const Pinned = ({ testID }: { testID?: string }): ReactElement | null => { +const Pinned = (): ReactElement | null => { 'use memo'; const pinned = useMessageField(item => item.pinned); + const { messageText } = useMessageText(); - if (pinned) return <CustomIcon testID={testID} name='pin' size={16} style={styles.rightIcons} />; + if (pinned) return <CustomIcon testID={`${messageText}-pinned`} name='pin' size={16} style={styles.rightIcons} />; return null; }; diff --git a/app/containers/message/components/RightIcons/index.tsx b/app/containers/message/components/RightIcons/index.tsx index 20dd7376532..a4e6d206357 100644 --- a/app/containers/message/components/RightIcons/index.tsx +++ b/app/containers/message/components/RightIcons/index.tsx @@ -14,18 +14,14 @@ const styles = StyleSheet.create({ } }); -interface IRightIcons { - msg?: string; -} - -const RightIcons = ({ msg }: IRightIcons): ReactElement => { +const RightIcons = (): ReactElement => { 'use memo'; return ( <View style={styles.actionIcons}> - <Pinned testID={`${msg}-pinned`} /> + <Pinned /> <Encrypted /> - <Edited testID={`${msg}-edited`} /> + <Edited /> <MessageError /> <Translated /> <ReadReceipt /> diff --git a/app/containers/message/components/Time.tsx b/app/containers/message/components/Time.tsx index 4c814c1b8e3..b787f1d1ede 100644 --- a/app/containers/message/components/Time.tsx +++ b/app/containers/message/components/Time.tsx @@ -1,4 +1,4 @@ -import { Text, type TextStyle } from 'react-native'; +import { Text } from 'react-native'; import dayjs from '../../../lib/dayjs'; import { useTheme } from '../../../theme'; @@ -6,12 +6,7 @@ import messageStyles from '../styles'; import { useMessageField } from '../stores/MessageStore'; import { useTimeFormat } from '../stores/MessageRoomStore'; -interface IMessageTime { - ts?: Date; - style?: TextStyle; -} - -const MessageTime = ({ style }: IMessageTime) => { +const MessageTime = () => { 'use memo'; const { colors } = useTheme(); @@ -20,7 +15,7 @@ const MessageTime = ({ style }: IMessageTime) => { const time = dayjs(ts).format(timeFormat); - return <Text style={[messageStyles.time, { color: colors.fontSecondaryInfo }, style]}>{time}</Text>; + return <Text style={[messageStyles.time, { color: colors.fontSecondaryInfo }]}>{time}</Text>; }; export default MessageTime; diff --git a/app/containers/message/components/User.tsx b/app/containers/message/components/User.tsx index 4478e5bba46..04bd97edab7 100644 --- a/app/containers/message/components/User.tsx +++ b/app/containers/message/components/User.tsx @@ -50,7 +50,7 @@ const User = () => { const { isLargeFontScale } = useResponsiveLayout(); const isHeader = useMessageGrouping(); const { u: author, alias } = useMessageAuthor(); - const { ts, t: type } = useMessageMeta(); + const { t: type } = useMessageMeta(); if (isHeader) { const username = (useRealName && author?.name) || author?.username; @@ -86,7 +86,7 @@ const User = () => { <Text style={[styles.username, { color: colors.fontTitlesLabels }]} numberOfLines={1}> {textContent} </Text> - {isLargeFontScale ? null : <MessageTime ts={ts as Date | undefined} />} + {isLargeFontScale ? null : <MessageTime />} </Pressable> <RightIcons /> </View> diff --git a/app/containers/message/components/__tests__/Thread.test.tsx b/app/containers/message/components/__tests__/Thread.test.tsx index f3c95bddbc9..33d45065b6a 100644 --- a/app/containers/message/components/__tests__/Thread.test.tsx +++ b/app/containers/message/components/__tests__/Thread.test.tsx @@ -1,9 +1,11 @@ import { render } from '@testing-library/react-native'; +import { Provider } from 'react-redux'; import Thread from '../Thread'; import { MessageProvider } from '../../stores/MessageStore'; import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; import { type TAnyMessageModel } from '../../../../definitions'; +import { mockedStore } from '../../../../reducers/mockedStore'; const baseContextValue = { threadBadgeColor: undefined, @@ -18,11 +20,13 @@ const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel const renderThread = (item: TAnyMessageModel) => render( - <MessageRoomProvider {...pickMessageRoomState(baseContextValue)}> - <MessageProvider item={item}> - <Thread /> - </MessageProvider> - </MessageRoomProvider> + <Provider store={mockedStore}> + <MessageRoomProvider {...pickMessageRoomState(baseContextValue)}> + <MessageProvider item={item}> + <Thread /> + </MessageProvider> + </MessageRoomProvider> + </Provider> ); describe('Thread — tlm-only update regression', () => { @@ -34,11 +38,13 @@ describe('Thread — tlm-only update regression', () => { const updatedItem = buildItem({ tlm: new Date('2024-01-01T00:00:00Z') }); rerender( - <MessageRoomProvider {...pickMessageRoomState(baseContextValue)}> - <MessageProvider item={updatedItem}> - <Thread /> - </MessageProvider> - </MessageRoomProvider> + <Provider store={mockedStore}> + <MessageRoomProvider {...pickMessageRoomState(baseContextValue)}> + <MessageProvider item={updatedItem}> + <Thread /> + </MessageProvider> + </MessageRoomProvider> + </Provider> ); expect(getByTestId('message-thread-button-hello')).toBeTruthy(); diff --git a/app/containers/message/components/__tests__/__snapshots__/Message.test.tsx.snap b/app/containers/message/components/__tests__/__snapshots__/Message.test.tsx.snap index 10590e7b688..4bf8543433b 100644 --- a/app/containers/message/components/__tests__/__snapshots__/Message.test.tsx.snap +++ b/app/containers/message/components/__tests__/__snapshots__/Message.test.tsx.snap @@ -367,7 +367,6 @@ exports[`Story Snapshots: Archived should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -840,7 +839,6 @@ exports[`Story Snapshots: ArchivedLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -1303,7 +1301,6 @@ exports[`Story Snapshots: AttachmentWithTextAndLink should match snapshot 1`] = { "color": "#6C727A", }, - undefined, ] } > @@ -2084,7 +2081,6 @@ exports[`Story Snapshots: AttachmentWithTextAndLinkLargeFont should match snapsh { "color": "#6C727A", }, - undefined, ] } > @@ -2847,7 +2843,6 @@ exports[`Story Snapshots: Avatar should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -3306,7 +3301,6 @@ exports[`Story Snapshots: Avatar should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -3757,7 +3751,6 @@ exports[`Story Snapshots: Avatar should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -4224,7 +4217,6 @@ exports[`Story Snapshots: Avatar should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -4691,7 +4683,6 @@ exports[`Story Snapshots: Avatar should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -5163,7 +5154,6 @@ exports[`Story Snapshots: Basic should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -5622,7 +5612,6 @@ exports[`Story Snapshots: Basic should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -6103,7 +6092,6 @@ exports[`Story Snapshots: BasicLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -6562,7 +6550,6 @@ exports[`Story Snapshots: BasicLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -7025,7 +7012,6 @@ exports[`Story Snapshots: BlockQuote should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -7517,7 +7503,6 @@ Testing block quote" { "color": "#6C727A", }, - undefined, ] } > @@ -8077,7 +8062,6 @@ exports[`Story Snapshots: BlockQuoteLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -8569,7 +8553,6 @@ Testing block quote" { "color": "#6C727A", }, - undefined, ] } > @@ -9111,7 +9094,6 @@ exports[`Story Snapshots: Broadcast should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -9712,7 +9694,6 @@ exports[`Story Snapshots: BroadcastLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -10295,7 +10276,6 @@ exports[`Story Snapshots: CollapsedAttachments should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -10906,7 +10886,6 @@ exports[`Story Snapshots: CollapsedAttachments should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -11741,7 +11720,6 @@ exports[`Story Snapshots: CollapsedAttachmentsLargeFont should match snapshot 1` { "color": "#6C727A", }, - undefined, ] } > @@ -12352,7 +12330,6 @@ exports[`Story Snapshots: CollapsedAttachmentsLargeFont should match snapshot 1` { "color": "#6C727A", }, - undefined, ] } > @@ -13169,7 +13146,6 @@ exports[`Story Snapshots: CollapsibleAttachmentWithText should match snapshot 1` { "color": "#6C727A", }, - undefined, ] } > @@ -13782,7 +13758,6 @@ exports[`Story Snapshots: CollapsibleAttachmentWithText should match snapshot 1` { "color": "#6C727A", }, - undefined, ] } > @@ -14758,7 +14733,6 @@ exports[`Story Snapshots: CollapsibleAttachmentWithTextLargeFont should match sn { "color": "#6C727A", }, - undefined, ] } > @@ -15371,7 +15345,6 @@ exports[`Story Snapshots: CollapsibleAttachmentWithTextLargeFont should match sn { "color": "#6C727A", }, - undefined, ] } > @@ -16329,7 +16302,6 @@ exports[`Story Snapshots: ColoredAttachments should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -17571,7 +17543,6 @@ exports[`Story Snapshots: ColoredAttachmentsLargeFont should match snapshot 1`] { "color": "#6C727A", }, - undefined, ] } > @@ -18795,7 +18766,6 @@ exports[`Story Snapshots: CustomFields should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -19609,7 +19579,6 @@ exports[`Story Snapshots: CustomFields should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -20703,7 +20672,6 @@ exports[`Story Snapshots: CustomFieldsLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -21517,7 +21485,6 @@ exports[`Story Snapshots: CustomFieldsLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -22593,7 +22560,6 @@ exports[`Story Snapshots: DateAndUnreadSeparators should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -23114,7 +23080,6 @@ exports[`Story Snapshots: DateAndUnreadSeparators should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -23843,7 +23808,6 @@ exports[`Story Snapshots: DateAndUnreadSeparators should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -24359,7 +24323,6 @@ exports[`Story Snapshots: DateAndUnreadSeparators should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -24840,7 +24803,6 @@ exports[`Story Snapshots: DateAndUnreadSeparatorsLargeFont should match snapshot { "color": "#6C727A", }, - undefined, ] } > @@ -25361,7 +25323,6 @@ exports[`Story Snapshots: DateAndUnreadSeparatorsLargeFont should match snapshot { "color": "#6C727A", }, - undefined, ] } > @@ -26090,7 +26051,6 @@ exports[`Story Snapshots: DateAndUnreadSeparatorsLargeFont should match snapshot { "color": "#6C727A", }, - undefined, ] } > @@ -26606,7 +26566,6 @@ exports[`Story Snapshots: DateAndUnreadSeparatorsLargeFont should match snapshot { "color": "#6C727A", }, - undefined, ] } > @@ -27069,7 +27028,6 @@ exports[`Story Snapshots: Discussion should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -27648,7 +27606,6 @@ exports[`Story Snapshots: Discussion should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -28229,7 +28186,6 @@ exports[`Story Snapshots: Discussion should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -28810,7 +28766,6 @@ exports[`Story Snapshots: Discussion should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -29413,7 +29368,6 @@ exports[`Story Snapshots: DiscussionLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -29992,7 +29946,6 @@ exports[`Story Snapshots: DiscussionLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -30573,7 +30526,6 @@ exports[`Story Snapshots: DiscussionLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -31154,7 +31106,6 @@ exports[`Story Snapshots: DiscussionLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -31739,7 +31690,6 @@ exports[`Story Snapshots: Edited should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -31759,7 +31709,7 @@ exports[`Story Snapshots: Edited should match snapshot 1`] = ` "paddingLeft": 5, } } - testID="undefined-edited" + testID="Message header-edited" > <Text allowFontScaling={false} @@ -32509,7 +32459,7 @@ exports[`Story Snapshots: EditedLargeFont should match snapshot 1`] = ` "paddingLeft": 5, } } - testID="undefined-edited" + testID="Message header-edited" > <Text allowFontScaling={false} @@ -32554,7 +32504,6 @@ exports[`Story Snapshots: EditedLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -33280,7 +33229,6 @@ exports[`Story Snapshots: Editing should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -33762,7 +33710,6 @@ exports[`Story Snapshots: EditingLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -34225,7 +34172,6 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -34683,7 +34629,6 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -35122,7 +35067,6 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -35674,7 +35618,6 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -36138,7 +36081,6 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -36621,7 +36563,6 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -37159,7 +37100,6 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -37617,7 +37557,6 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -38056,7 +37995,6 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -38608,7 +38546,6 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -39072,7 +39009,6 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -39555,7 +39491,6 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -40075,7 +40010,6 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -40834,7 +40768,6 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -42197,7 +42130,6 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -42697,7 +42629,6 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -43106,7 +43037,6 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -43673,7 +43603,6 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -44586,7 +44515,6 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -45345,7 +45273,6 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -46708,7 +46635,6 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -47208,7 +47134,6 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -47688,7 +47613,6 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -48214,7 +48138,6 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -48848,7 +48771,6 @@ exports[`Story Snapshots: Error should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -49292,7 +49214,6 @@ exports[`Story Snapshots: Error should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -49830,7 +49751,6 @@ exports[`Story Snapshots: ErrorLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -50274,7 +50194,6 @@ exports[`Story Snapshots: ErrorLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -50737,7 +50656,6 @@ exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] { "color": "#6C727A", }, - undefined, ] } > @@ -51335,7 +51253,6 @@ exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] { "color": "#6C727A", }, - undefined, ] } > @@ -51933,7 +51850,6 @@ exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] { "color": "#6C727A", }, - undefined, ] } > @@ -52531,7 +52447,6 @@ exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] { "color": "#6C727A", }, - undefined, ] } > @@ -53664,7 +53579,6 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna { "color": "#6C727A", }, - undefined, ] } > @@ -54262,7 +54176,6 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna { "color": "#6C727A", }, - undefined, ] } > @@ -54860,7 +54773,6 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna { "color": "#6C727A", }, - undefined, ] } > @@ -55458,7 +55370,6 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna { "color": "#6C727A", }, - undefined, ] } > @@ -56569,7 +56480,6 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna { "color": "#6C727A", }, - undefined, ] } > @@ -57326,7 +57236,6 @@ exports[`Story Snapshots: FullName should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -57807,7 +57716,6 @@ exports[`Story Snapshots: FullNameLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -58270,7 +58178,6 @@ exports[`Story Snapshots: GroupedMessages should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -58729,7 +58636,6 @@ exports[`Story Snapshots: GroupedMessages should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -59640,7 +59546,6 @@ exports[`Story Snapshots: GroupedMessages should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -60121,7 +60026,6 @@ exports[`Story Snapshots: GroupedMessagesLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -60580,7 +60484,6 @@ exports[`Story Snapshots: GroupedMessagesLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -61491,7 +61394,6 @@ exports[`Story Snapshots: GroupedMessagesLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -62612,7 +62514,6 @@ exports[`Story Snapshots: InlineKatex should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -63079,7 +62980,6 @@ exports[`Story Snapshots: InlineKatexLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -63528,7 +63428,6 @@ exports[`Story Snapshots: Katex should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -63954,7 +63853,6 @@ exports[`Story Snapshots: KatexArray should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -64389,7 +64287,6 @@ exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -64815,7 +64712,6 @@ exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -65236,7 +65132,6 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -65875,7 +65770,6 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -66464,7 +66358,6 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -67259,7 +67152,6 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -67898,7 +67790,6 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -68487,7 +68378,6 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -69260,7 +69150,6 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -69719,7 +69608,6 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -69739,7 +69627,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "paddingLeft": 5, } } - testID="undefined-edited" + testID="Edited message-edited" > <Text allowFontScaling={false} @@ -70215,7 +70103,6 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -70709,7 +70596,6 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -71118,7 +71004,6 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -71649,7 +71534,6 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -72139,7 +72023,6 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -72579,7 +72462,6 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -72670,7 +72552,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "paddingLeft": 5, } } - testID="undefined-edited" + testID="Show all icons -edited" > <Text allowFontScaling={false} @@ -73159,7 +73041,6 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -73250,7 +73131,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "paddingLeft": 5, } } - testID="undefined-edited" + testID="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.-edited" > <Text allowFontScaling={false} @@ -73739,7 +73620,6 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -73830,7 +73710,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "paddingLeft": 5, } } - testID="undefined-edited" + testID="small message-edited" > <Text allowFontScaling={false} @@ -74427,7 +74307,6 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -74877,7 +74756,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "paddingLeft": 5, } } - testID="undefined-edited" + testID="Edited message-edited" > <Text allowFontScaling={false} @@ -74922,7 +74801,6 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -75417,7 +75295,6 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -75948,7 +75825,6 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -76357,7 +76233,6 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -76847,7 +76722,6 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -77408,7 +77282,6 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -77808,7 +77681,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "paddingLeft": 5, } } - testID="undefined-edited" + testID="Show all icons -edited" > <Text allowFontScaling={false} @@ -77988,7 +77861,6 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -78388,7 +78260,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "paddingLeft": 5, } } - testID="undefined-edited" + testID="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.-edited" > <Text allowFontScaling={false} @@ -78568,7 +78440,6 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -78968,7 +78839,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "paddingLeft": 5, } } - testID="undefined-edited" + testID="small message-edited" > <Text allowFontScaling={false} @@ -79148,7 +79019,6 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -79576,7 +79446,6 @@ exports[`Story Snapshots: Mentions should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -80214,7 +80083,6 @@ exports[`Story Snapshots: Mentions should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -80856,7 +80724,6 @@ exports[`Story Snapshots: MentionsLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -81494,7 +81361,6 @@ exports[`Story Snapshots: MentionsLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -82118,7 +81984,6 @@ exports[`Story Snapshots: Message should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -82529,7 +82394,6 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` { "color": "#6C727A", }, - undefined, ] } > @@ -83283,7 +83147,6 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` { "color": "#6C727A", }, - undefined, ] } > @@ -84091,7 +83954,6 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` { "color": "#6C727A", }, - undefined, ] } > @@ -84911,7 +84773,6 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` { "color": "#6C727A", }, - undefined, ] } > @@ -85622,7 +85483,6 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` { "color": "#6C727A", }, - undefined, ] } > @@ -86525,7 +86385,6 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -87272,7 +87131,6 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -88072,7 +87930,6 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot { "color": "#6C727A", }, - undefined, ] } > @@ -88819,7 +88676,6 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot { "color": "#6C727A", }, - undefined, ] } > @@ -89539,7 +89395,6 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -90170,7 +90025,6 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -91108,7 +90962,6 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -91916,7 +91769,6 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -92798,7 +92650,6 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -93416,7 +93267,6 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -94111,7 +93961,6 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -94902,7 +94751,6 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot { "color": "#6C727A", }, - undefined, ] } > @@ -95520,7 +95368,6 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot { "color": "#6C727A", }, - undefined, ] } > @@ -96215,7 +96062,6 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot { "color": "#6C727A", }, - undefined, ] } > @@ -96984,7 +96830,6 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot { "color": "#6C727A", }, - undefined, ] } > @@ -97804,7 +97649,6 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot { "color": "#6C727A", }, - undefined, ] } > @@ -98515,7 +98359,6 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot { "color": "#6C727A", }, - undefined, ] } > @@ -99418,7 +99261,6 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = { "color": "#6C727A", }, - undefined, ] } > @@ -100049,7 +99891,6 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = { "color": "#6C727A", }, - undefined, ] } > @@ -100724,7 +100565,6 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = { "color": "#6C727A", }, - undefined, ] } > @@ -101532,7 +101372,6 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = { "color": "#6C727A", }, - undefined, ] } > @@ -102406,7 +102245,6 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -107170,7 +107008,6 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = { "color": "#6C727A", }, - undefined, ] } > @@ -111915,7 +111752,6 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -112288,7 +112124,6 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -112327,7 +112162,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` {}, ] } - testID="undefined-pinned" + testID="Small name with icons-pinned" >  </Text> @@ -112408,7 +112243,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` "paddingLeft": 5, } } - testID="undefined-edited" + testID="Small name with icons-edited" > <Text allowFontScaling={false} @@ -112983,7 +112818,6 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -113356,7 +113190,6 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -113395,7 +113228,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` {}, ] } - testID="undefined-pinned" + testID="Medium name with icons-pinned" >  </Text> @@ -113476,7 +113309,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` "paddingLeft": 5, } } - testID="undefined-edited" + testID="Medium name with icons-edited" > <Text allowFontScaling={false} @@ -114051,7 +113884,6 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -114424,7 +114256,6 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -114463,7 +114294,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` {}, ] } - testID="undefined-pinned" + testID="Long name with icons - timestamp visible-pinned" >  </Text> @@ -114544,7 +114375,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` "paddingLeft": 5, } } - testID="undefined-edited" + testID="Long name with icons - timestamp visible-edited" > <Text allowFontScaling={false} @@ -115132,7 +114963,6 @@ exports[`Story Snapshots: Pinned should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -115171,7 +115001,7 @@ exports[`Story Snapshots: Pinned should match snapshot 1`] = ` {}, ] } - testID="undefined-pinned" + testID="Message header-pinned" >  </Text> @@ -115909,7 +115739,7 @@ exports[`Story Snapshots: PinnedLargeFont should match snapshot 1`] = ` {}, ] } - testID="undefined-pinned" + testID="Message header-pinned" >  </Text> @@ -115929,7 +115759,6 @@ exports[`Story Snapshots: PinnedLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -116648,7 +116477,6 @@ exports[`Story Snapshots: Reactions should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -117651,7 +117479,6 @@ exports[`Story Snapshots: Reactions should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -119132,7 +118959,6 @@ exports[`Story Snapshots: ReactionsLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -120135,7 +119961,6 @@ exports[`Story Snapshots: ReactionsLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -121599,7 +121424,6 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m { "color": "#6C727A", }, - undefined, ] } > @@ -123390,7 +123214,6 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont { "color": "#6C727A", }, - undefined, ] } > @@ -127982,7 +127805,6 @@ exports[`Story Snapshots: ShowButtonAsAttachment should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -128514,7 +128336,6 @@ exports[`Story Snapshots: ShowButtonAsAttachment should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -129233,7 +129054,6 @@ exports[`Story Snapshots: ShowButtonAsAttachmentLargeFont should match snapshot { "color": "#6C727A", }, - undefined, ] } > @@ -129765,7 +129585,6 @@ exports[`Story Snapshots: ShowButtonAsAttachmentLargeFont should match snapshot { "color": "#6C727A", }, - undefined, ] } > @@ -130466,7 +130285,6 @@ exports[`Story Snapshots: StaticAvatar should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -130947,7 +130765,6 @@ exports[`Story Snapshots: StaticAvatarLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -144078,7 +143895,6 @@ exports[`Story Snapshots: Temp should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -144555,7 +144371,6 @@ exports[`Story Snapshots: TempLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -145022,7 +144837,6 @@ exports[`Story Snapshots: ThumbnailFromServer should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -145714,7 +145528,6 @@ exports[`Story Snapshots: ThumbnailFromServerLargeFont should match snapshot 1`] { "color": "#6C727A", }, - undefined, ] } > @@ -146388,7 +146201,6 @@ exports[`Story Snapshots: TimeFormat should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -146869,7 +146681,6 @@ exports[`Story Snapshots: TimeFormatLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -147333,7 +147144,6 @@ exports[`Story Snapshots: Translated should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -147885,7 +147695,6 @@ exports[`Story Snapshots: TranslatedLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -148348,7 +148157,6 @@ exports[`Story Snapshots: TwoShortCustomFieldsWithMarkdown should match snapshot { "color": "#6C727A", }, - undefined, ] } > @@ -149545,7 +149353,6 @@ exports[`Story Snapshots: TwoShortCustomFieldsWithMarkdownLargeFont should match { "color": "#6C727A", }, - undefined, ] } > @@ -150724,7 +150531,6 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -151319,7 +151125,6 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -152197,7 +152002,6 @@ exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -152694,7 +152498,6 @@ exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -153156,7 +152959,6 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -153653,7 +153455,6 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -154106,7 +153907,6 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -154701,7 +154501,6 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -155570,7 +155369,6 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -155943,7 +155741,6 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -155982,7 +155779,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` {}, ] } - testID="undefined-pinned" + testID="Small username with icons-pinned" >  </Text> @@ -156063,7 +155860,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` "paddingLeft": 5, } } - testID="undefined-edited" + testID="Small username with icons-edited" > <Text allowFontScaling={false} @@ -156638,7 +156435,6 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -157011,7 +156807,6 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -157050,7 +156845,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` {}, ] } - testID="undefined-pinned" + testID="Medium username with icons-pinned" >  </Text> @@ -157131,7 +156926,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` "paddingLeft": 5, } } - testID="undefined-edited" + testID="Medium username with icons-edited" > <Text allowFontScaling={false} @@ -157706,7 +157501,6 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -158079,7 +157873,6 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -158118,7 +157911,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` {}, ] } - testID="undefined-pinned" + testID="Long username with icons - timestamp visible-pinned" >  </Text> @@ -158199,7 +157992,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` "paddingLeft": 5, } } - testID="undefined-edited" + testID="Long username with icons - timestamp visible-edited" > <Text allowFontScaling={false} @@ -158806,7 +158599,6 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -159284,7 +159076,6 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -159784,7 +159575,6 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -160262,7 +160052,6 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -160725,7 +160514,6 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -163641,7 +163429,6 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -166529,7 +166316,6 @@ exports[`Story Snapshots: WithFile should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -167858,7 +167644,6 @@ exports[`Story Snapshots: WithFileLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -168927,7 +168712,6 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -169602,7 +169386,6 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -170766,7 +170549,6 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -171555,7 +171337,6 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -172239,7 +172020,6 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -173259,7 +173039,6 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -174057,7 +173836,6 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -174723,7 +174501,6 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -175712,7 +175489,6 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -176455,7 +176231,6 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -177088,7 +176863,6 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -177622,7 +177396,6 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > diff --git a/app/containers/message/hooks/__tests__/useMessageAccessibilityHint.test.tsx b/app/containers/message/hooks/__tests__/useMessageAccessibilityHint.test.tsx index 967f45fd093..7ddd31303bc 100644 --- a/app/containers/message/hooks/__tests__/useMessageAccessibilityHint.test.tsx +++ b/app/containers/message/hooks/__tests__/useMessageAccessibilityHint.test.tsx @@ -1,9 +1,11 @@ import { render } from '@testing-library/react-native'; +import { Provider } from 'react-redux'; import { useMessageAccessibilityHint } from '../useMessageAccessibilityHint'; import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import { type TAnyMessageModel } from '../../../../definitions'; +import { mockedStore } from '../../../../reducers/mockedStore'; const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel => ({ id: 'msg-1', ...overrides } as TAnyMessageModel); @@ -15,11 +17,13 @@ const renderHint = (item: TAnyMessageModel, config: Record<string, any> = {}) => return null; }; render( - <MessageRoomProvider {...pickMessageRoomState(config)}> - <MessageProvider item={item}> - <Probe /> - </MessageProvider> - </MessageRoomProvider> + <Provider store={mockedStore}> + <MessageRoomProvider {...pickMessageRoomState(config)}> + <MessageProvider item={item}> + <Probe /> + </MessageProvider> + </MessageRoomProvider> + </Provider> ); const { calls } = spy.mock; return calls[calls.length - 1][0]; diff --git a/app/containers/message/stores/MessageRoomStore.tsx b/app/containers/message/stores/MessageRoomStore.tsx index 2d7918fc9b8..5c0df28b4c8 100644 --- a/app/containers/message/stores/MessageRoomStore.tsx +++ b/app/containers/message/stores/MessageRoomStore.tsx @@ -5,6 +5,7 @@ import { useShallow } from 'zustand/react/shallow'; import { type TGetCustomEmoji } from '../../../definitions/IEmoji'; import { type IAttachment, type TAnyMessageModel } from '../../../definitions'; import { type IRoomInfoParam } from '../../../views/SearchMessagesView'; +import { useSetting } from '../../../lib/hooks/useSetting'; export type MessageRoomState = { // stable handlers @@ -104,13 +105,15 @@ const useMessageRoomStore = <T,>(selector: (state: MessageRoomState) => T): T => export const MessageRoomProvider = ({ children, ...state }: { children: ReactNode } & MessageRoomState): ReactElement => { 'use memo'; - const [store] = useState(() => createMessageRoomStore(state)); + const Message_TimeFormat = useSetting('Message_TimeFormat') as string; + const resolvedState = { ...state, timeFormat: state.timeFormat ?? Message_TimeFormat }; + const [store] = useState(() => createMessageRoomStore(resolvedState)); // No dependency array: the store must mirror the provider's props every render. // setState notifies every listener on each render, but handlers/constants are stable // refs, so each selector's snapshot stays Object.is-equal and no consumer re-renders. useEffect(() => { - store.setState(state); + store.setState(resolvedState); }); return <MessageRoomStoreContext.Provider value={store}>{children}</MessageRoomStoreContext.Provider>; diff --git a/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx b/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx index 6c21d177276..ac3fd4af5b5 100644 --- a/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx @@ -1,6 +1,8 @@ import { act, render } from '@testing-library/react-native'; +import { Provider } from 'react-redux'; import { MessageRoomProvider, pickMessageRoomState, useTimeFormat } from '../MessageRoomStore'; +import { mockedStore } from '../../../../reducers/mockedStore'; describe('MessageRoomStore', () => { it('mirrors updated provider props into the store after mount', () => { @@ -10,9 +12,11 @@ describe('MessageRoomStore', () => { return null; }; const wrap = (timeFormat: string) => ( - <MessageRoomProvider {...pickMessageRoomState({ timeFormat })}> - <Probe /> - </MessageRoomProvider> + <Provider store={mockedStore}> + <MessageRoomProvider {...pickMessageRoomState({ timeFormat })}> + <Probe /> + </MessageRoomProvider> + </Provider> ); const { rerender } = render(wrap('MMM Do YYYY')); diff --git a/app/containers/message/stores/__tests__/MessageStore.test.tsx b/app/containers/message/stores/__tests__/MessageStore.test.tsx index a50a0c0e6f1..a3610d6aaeb 100644 --- a/app/containers/message/stores/__tests__/MessageStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageStore.test.tsx @@ -1,7 +1,9 @@ import { act, fireEvent, render } from '@testing-library/react-native'; import { Text } from 'react-native'; +import { Provider } from 'react-redux'; import { type TAnyMessageModel } from '../../../../definitions'; +import { mockedStore } from '../../../../reducers/mockedStore'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../../../lib/constants/keys'; import { messagesStatus } from '../../../../lib/constants/messagesStatus'; import { MessageRoomProvider, pickMessageRoomState, type MessageRoomState } from '../MessageRoomStore'; @@ -130,11 +132,13 @@ const renderDerived = ( return null; }; render( - <MessageRoomProvider {...pickMessageRoomState(config ?? {})}> - <MessageProvider item={item} previousItem={previousItem}> - <Probe /> - </MessageProvider> - </MessageRoomProvider> + <Provider store={mockedStore}> + <MessageRoomProvider {...pickMessageRoomState(config ?? {})}> + <MessageProvider item={item} previousItem={previousItem}> + <Probe /> + </MessageProvider> + </MessageRoomProvider> + </Provider> ); const latest = () => { const { calls } = spy.mock; @@ -492,11 +496,13 @@ describe('MessageStore', () => { return calls[calls.length - 1]?.[0]; }; const wrap = (previousItem: TAnyMessageModel) => ( - <MessageRoomProvider {...pickMessageRoomState({ broadcast: false, Message_GroupingPeriod: 300 })}> - <MessageProvider item={item} previousItem={previousItem}> - <Probe /> - </MessageProvider> - </MessageRoomProvider> + <Provider store={mockedStore}> + <MessageRoomProvider {...pickMessageRoomState({ broadcast: false, Message_GroupingPeriod: 300 })}> + <MessageProvider item={item} previousItem={previousItem}> + <Probe /> + </MessageProvider> + </MessageRoomProvider> + </Provider> ); const { rerender } = render(wrap(previousTemp)); diff --git a/app/views/RoomView/LoadMore/__snapshots__/LoadMore.test.tsx.snap b/app/views/RoomView/LoadMore/__snapshots__/LoadMore.test.tsx.snap index 643fc3e4b79..0bc989b37bd 100644 --- a/app/views/RoomView/LoadMore/__snapshots__/LoadMore.test.tsx.snap +++ b/app/views/RoomView/LoadMore/__snapshots__/LoadMore.test.tsx.snap @@ -895,7 +895,6 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` { "color": "#9EA2A8", }, - undefined, ] } > @@ -2018,7 +2017,6 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` { "color": "#9EA2A8", }, - undefined, ] } > @@ -2929,7 +2927,6 @@ exports[`Story Snapshots: BlackTheme should match snapshot 1`] = ` { "color": "#9EA2A8", }, - undefined, ] } > @@ -3507,7 +3504,6 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` { "color": "#9EA2A8", }, - undefined, ] } > @@ -4630,7 +4626,6 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` { "color": "#9EA2A8", }, - undefined, ] } > @@ -5541,7 +5536,6 @@ exports[`Story Snapshots: DarkTheme should match snapshot 1`] = ` { "color": "#9EA2A8", }, - undefined, ] } > @@ -6119,7 +6113,6 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -7242,7 +7235,6 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > @@ -8153,7 +8145,6 @@ exports[`Story Snapshots: LightTheme should match snapshot 1`] = ` { "color": "#6C727A", }, - undefined, ] } > diff --git a/app/views/RoomView/definitions.ts b/app/views/RoomView/definitions.ts index 0f8e76261cb..6aabaf7dd0e 100644 --- a/app/views/RoomView/definitions.ts +++ b/app/views/RoomView/definitions.ts @@ -14,7 +14,6 @@ export interface IRoomViewProps extends IActionSheetProvider, IBaseScreen<ChatsS user: Pick<ILoggedUser, 'id' | 'username' | 'token' | 'showMessageInMainThread'>; isAuthenticated: boolean; Message_GroupingPeriod?: number; - Message_TimeFormat?: string; Message_Read_Receipt_Enabled?: boolean; Hide_System_Messages?: string[]; baseUrl: string; diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index aafcf6d0180..a3f18a637eb 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1600,17 +1600,8 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { render() { console.count(`${this.constructor.name}.render calls`); const { room, isAutocompleteVisible, showMissingE2EEKey, showE2EEDisabledRoom, canAutoTranslate } = this.state; - const { - user, - baseUrl, - theme, - width, - serverVersion, - navigation, - Message_GroupingPeriod, - Message_TimeFormat, - Message_Read_Receipt_Enabled - } = this.props; + const { user, baseUrl, theme, width, serverVersion, navigation, Message_GroupingPeriod, Message_Read_Receipt_Enabled } = + this.props; const { rid, t } = room; let bannerClosed; let announcement; @@ -1696,7 +1687,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { broadcast={'id' in room && room.broadcast} isThreadRoom={!!this.tmid} Message_GroupingPeriod={Message_GroupingPeriod} - timeFormat={Message_TimeFormat} autoTranslateRoom={canAutoTranslate && 'id' in room && room.autoTranslate} autoTranslateLanguage={'id' in room ? room.autoTranslateLanguage : undefined}> <List @@ -1726,7 +1716,6 @@ const mapStateToProps = (state: IApplicationState) => ({ user: getUserSelector(state), isAuthenticated: state.login.isAuthenticated, Message_GroupingPeriod: state.settings.Message_GroupingPeriod as number, - Message_TimeFormat: state.settings.Message_TimeFormat as string, customEmojis: state.customEmojis, baseUrl: state.server.server, serverVersion: state.server.version, From 5d687aa526b541feabb2bf1e8e7cfa068c6c8586 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 14:36:42 -0300 Subject: [PATCH 062/144] refactor: carry the ignored seed in message context (NATIVE-1395) The per-message store kept an ignoredSeed field mirrored from the isIgnored prop every render. The seed never changes at runtime, so move it into the message context value and keep only the mutable manualUnignored latch in the store. useMessageIgnored now reads the seed from context and the latch from the store. Behavior is identical. --- app/containers/message/stores/MessageStore.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index 94aa66242bf..15c6ec9d1af 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -29,11 +29,10 @@ type MessageStoreState = { onPress?: () => void; onLongPress?: (item: TAnyMessageModel) => void; threadBadgeColor?: string; - ignoredSeed: boolean; }; const createMessageStore = (initial: Partial<MessageStoreState>) => - createStore<MessageStoreState>(() => ({ tick: 0, manualUnignored: false, ignoredSeed: false, ...initial })); + createStore<MessageStoreState>(() => ({ tick: 0, manualUnignored: false, ...initial })); type MessageStore = ReturnType<typeof createMessageStore>; @@ -42,6 +41,7 @@ type MessageCtxValue = { item: TAnyMessageModel; previousItem?: TAnyMessageModel; revealIgnored: () => void; + isIgnored: boolean; }; const MessageStoreContext = createContext<MessageCtxValue | null>(null); @@ -91,7 +91,7 @@ export const MessageProvider = ({ }): ReactElement => { 'use memo'; - const [store] = useState(() => createMessageStore({ onPress, onLongPress, threadBadgeColor, ignoredSeed: isIgnored ?? false })); + const [store] = useState(() => createMessageStore({ onPress, onLongPress, threadBadgeColor })); // Header grouping and thread position depend on the previous record too, so each effect // subscribes one record; both feed the same tick. Keeping them separate means changing @@ -101,13 +101,15 @@ export const MessageProvider = ({ // Mirror per-message row handlers so field-level selectors subscribe without churning the context value. useEffect(() => { - store.setState({ onPress, onLongPress, threadBadgeColor, ignoredSeed: isIgnored ?? false }); + store.setState({ onPress, onLongPress, threadBadgeColor }); }); const revealIgnored = () => store.setState({ manualUnignored: true }); return ( - <MessageStoreContext.Provider value={{ store, item, previousItem, revealIgnored }}>{children}</MessageStoreContext.Provider> + <MessageStoreContext.Provider value={{ store, item, previousItem, revealIgnored, isIgnored: isIgnored ?? false }}> + {children} + </MessageStoreContext.Provider> ); }; @@ -312,8 +314,9 @@ export const useThreadBadgeColor = (): string | undefined => { return useStore(store, s => s.threadBadgeColor); }; export const useMessageIgnored = (): boolean => { - const { store } = useMessageCtx(); - return useStore(store, s => (s.manualUnignored ? false : s.ignoredSeed)); + const { store, isIgnored } = useMessageCtx(); + const manualUnignored = useStore(store, s => s.manualUnignored); + return manualUnignored ? false : isIgnored; }; export const useRevealIgnored = (): (() => void) => { const { revealIgnored } = useMessageCtx(); From ff9f162f45c70d8de01f2394ef50d1047a01e173 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 15:07:05 -0300 Subject: [PATCH 063/144] test: add snapshot-oracle stories for message leaves (NATIVE-1396) Add a Storybook story per presentational leaf in the message module (12 top-level + 6 RightIcons), each seeding the minimal message/room store state to render a meaningful, deterministic branch. An aggregator test renders every story to a committed snapshot so each leaf gains a regression oracle. Hidden/own variants cover the null branches. --- .../components/stories/Broadcast.stories.tsx | 56 + .../components/stories/CallButton.stories.tsx | 40 + .../components/stories/Content.stories.tsx | 42 + .../components/stories/Discussion.stories.tsx | 43 + .../components/stories/Emoji.stories.tsx | 40 + .../stories/MessageAvatar.stories.tsx | 43 + .../components/stories/Reactions.stories.tsx | 48 + .../stories/RepliedThread.stories.tsx | 38 + .../stories/RightIcons/Edited.stories.tsx | 51 + .../stories/RightIcons/Encrypted.stories.tsx | 57 + .../RightIcons/MessageError.stories.tsx | 57 + .../stories/RightIcons/Pinned.stories.tsx | 52 + .../RightIcons/ReadReceipt.stories.tsx | 59 + .../stories/RightIcons/Translated.stories.tsx | 53 + .../components/stories/Thread.stories.tsx | 45 + .../components/stories/Time.stories.tsx | 41 + .../components/stories/Urls.stories.tsx | 51 + .../components/stories/User.stories.tsx | 45 + .../__snapshots__/leaves.test.tsx.snap | 2036 +++++++++++++++++ .../stories/__tests__/leaves.test.tsx | 38 + 20 files changed, 2935 insertions(+) create mode 100644 app/containers/message/components/stories/Broadcast.stories.tsx create mode 100644 app/containers/message/components/stories/CallButton.stories.tsx create mode 100644 app/containers/message/components/stories/Content.stories.tsx create mode 100644 app/containers/message/components/stories/Discussion.stories.tsx create mode 100644 app/containers/message/components/stories/Emoji.stories.tsx create mode 100644 app/containers/message/components/stories/MessageAvatar.stories.tsx create mode 100644 app/containers/message/components/stories/Reactions.stories.tsx create mode 100644 app/containers/message/components/stories/RepliedThread.stories.tsx create mode 100644 app/containers/message/components/stories/RightIcons/Edited.stories.tsx create mode 100644 app/containers/message/components/stories/RightIcons/Encrypted.stories.tsx create mode 100644 app/containers/message/components/stories/RightIcons/MessageError.stories.tsx create mode 100644 app/containers/message/components/stories/RightIcons/Pinned.stories.tsx create mode 100644 app/containers/message/components/stories/RightIcons/ReadReceipt.stories.tsx create mode 100644 app/containers/message/components/stories/RightIcons/Translated.stories.tsx create mode 100644 app/containers/message/components/stories/Thread.stories.tsx create mode 100644 app/containers/message/components/stories/Time.stories.tsx create mode 100644 app/containers/message/components/stories/Urls.stories.tsx create mode 100644 app/containers/message/components/stories/User.stories.tsx create mode 100644 app/containers/message/components/stories/__tests__/__snapshots__/leaves.test.tsx.snap create mode 100644 app/containers/message/components/stories/__tests__/leaves.test.tsx diff --git a/app/containers/message/components/stories/Broadcast.stories.tsx b/app/containers/message/components/stories/Broadcast.stories.tsx new file mode 100644 index 00000000000..1a9d898275a --- /dev/null +++ b/app/containers/message/components/stories/Broadcast.stories.tsx @@ -0,0 +1,56 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageProvider } from '../../stores/MessageStore'; +import BroadcastLeaf from '../Broadcast'; + +const store = createMockedStore(); + +const otherAuthorItem = { + id: 'msg-id', + msg: 'Broadcast message', + u: { _id: 'author-id', username: 'other.user' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const ownAuthorItem = { + id: 'msg-id', + msg: 'Broadcast message', + u: { _id: 'reader-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const room = { + broadcast: true, + user: { id: 'reader-id', username: 'rocket.cat' }, + replyBroadcast: () => {} +}; + +const StoryWrapper = ({ item, children }: { item: TAnyMessageModel; children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/Broadcast' +}; + +// A broadcast message from another user renders the reply button. +export const Broadcast = () => ( + <StoryWrapper item={otherAuthorItem}> + <BroadcastLeaf /> + </StoryWrapper> +); + +// A broadcast message authored by the reader renders nothing. +export const BroadcastOwn = () => ( + <StoryWrapper item={ownAuthorItem}> + <BroadcastLeaf /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/CallButton.stories.tsx b/app/containers/message/components/stories/CallButton.stories.tsx new file mode 100644 index 00000000000..724a3f98039 --- /dev/null +++ b/app/containers/message/components/stories/CallButton.stories.tsx @@ -0,0 +1,40 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageProvider } from '../../stores/MessageStore'; +import CallButtonLeaf from '../CallButton'; + +const store = createMockedStore(); + +const item = { + id: 'msg-id', + msg: '', + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const room = { + handleEnterCall: () => {} +}; + +const StoryWrapper = ({ children }: { children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/CallButton' +}; + +// The join-call button always renders its label and video icon. +export const CallButton = () => ( + <StoryWrapper> + <CallButtonLeaf /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/Content.stories.tsx b/app/containers/message/components/stories/Content.stories.tsx new file mode 100644 index 00000000000..6b9daaba139 --- /dev/null +++ b/app/containers/message/components/stories/Content.stories.tsx @@ -0,0 +1,42 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageProvider } from '../../stores/MessageStore'; +import ContentLeaf from '../Content'; + +const store = createMockedStore(); + +const item = { + id: 'msg-id', + msg: 'This is a plain text message', + t: undefined, + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const room = { + user: { id: 'reader-id', username: 'reader' }, + getCustomEmoji: () => null +}; + +const StoryWrapper = ({ children }: { children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/Content' +}; + +// A regular text message renders its Markdown body. +export const Content = () => ( + <StoryWrapper> + <ContentLeaf /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/Discussion.stories.tsx b/app/containers/message/components/stories/Discussion.stories.tsx new file mode 100644 index 00000000000..84d4a5d4095 --- /dev/null +++ b/app/containers/message/components/stories/Discussion.stories.tsx @@ -0,0 +1,43 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageProvider } from '../../stores/MessageStore'; +import DiscussionLeaf from '../Discussion'; + +const store = createMockedStore(); + +const item = { + id: 'msg-id', + msg: 'Discussion topic message', + dcount: 5, + dlm: new Date('2024-01-01T10:20:00.000Z'), + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const room = { + user: { id: 'reader-id', username: 'reader' }, + onDiscussionPress: () => {} +}; + +const StoryWrapper = ({ children }: { children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/Discussion' +}; + +// A discussion message renders the started-discussion label, topic, reply count and last-message time. +export const Discussion = () => ( + <StoryWrapper> + <DiscussionLeaf /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/Emoji.stories.tsx b/app/containers/message/components/stories/Emoji.stories.tsx new file mode 100644 index 00000000000..d9d985b2ef8 --- /dev/null +++ b/app/containers/message/components/stories/Emoji.stories.tsx @@ -0,0 +1,40 @@ +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../reducers/mockedStore'; +import { selectServerRequest } from '../../../../actions/server'; +import { type ICustomEmoji } from '../../../../definitions/IEmoji'; +import EmojiLeaf from '../Emoji'; + +const store = createMockedStore(); +store.dispatch(selectServerRequest('https://open.rocket.chat', '7.0.0')); + +const standardEmojiStyle = { fontSize: 30 }; +const customEmojiStyle = { width: 30, height: 30 }; + +export default { + title: 'Message/Emoji' +}; + +// A shortname with no custom-emoji match renders as unicode text. +export const Emoji = () => ( + <Provider store={store}> + <EmojiLeaf + content=':grinning:' + standardEmojiStyle={standardEmojiStyle} + customEmojiStyle={customEmojiStyle} + getCustomEmoji={() => null} + /> + </Provider> +); + +// A shortname that matches a custom emoji renders the custom image. +export const EmojiCustom = () => ( + <Provider store={store}> + <EmojiLeaf + content=':react_rocket:' + standardEmojiStyle={standardEmojiStyle} + customEmojiStyle={customEmojiStyle} + getCustomEmoji={(): ICustomEmoji => ({ name: 'react_rocket', extension: 'png' })} + /> + </Provider> +); diff --git a/app/containers/message/components/stories/MessageAvatar.stories.tsx b/app/containers/message/components/stories/MessageAvatar.stories.tsx new file mode 100644 index 00000000000..08d217559b3 --- /dev/null +++ b/app/containers/message/components/stories/MessageAvatar.stories.tsx @@ -0,0 +1,43 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../reducers/mockedStore'; +import { selectServerRequest } from '../../../../actions/server'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageProvider } from '../../stores/MessageStore'; +import MessageAvatarLeaf from '../MessageAvatar'; + +const store = createMockedStore(); +store.dispatch(selectServerRequest('https://open.rocket.chat', '7.0.0')); + +const item = { + id: 'msg-id', + msg: 'Message', + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const room = { + user: { id: 'reader-id', username: 'reader' }, + getCustomEmoji: () => null +}; + +const StoryWrapper = ({ children }: { children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/MessageAvatar' +}; + +// Header messages render the author avatar from the username. +export const MessageAvatar = () => ( + <StoryWrapper> + <MessageAvatarLeaf /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/Reactions.stories.tsx b/app/containers/message/components/stories/Reactions.stories.tsx new file mode 100644 index 00000000000..56f17b8a500 --- /dev/null +++ b/app/containers/message/components/stories/Reactions.stories.tsx @@ -0,0 +1,48 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageProvider } from '../../stores/MessageStore'; +import ReactionsLeaf from '../Reactions'; + +const store = createMockedStore(); + +const item = { + id: 'msg-id', + msg: 'Message with reactions', + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false, + reactions: [ + { emoji: ':joy:', usernames: ['rocket.cat', 'diego.mello'] }, + { emoji: ':heart:', usernames: ['diego.mello'] } + ] +} as unknown as TAnyMessageModel; + +const room = { + user: { id: 'reader-id', username: 'rocket.cat' }, + getCustomEmoji: () => null, + onReactionPress: () => {}, + onReactionLongPress: () => {}, + reactionInit: () => {} +}; + +const StoryWrapper = ({ children }: { children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/Reactions' +}; + +// A message with reactions renders each reaction pill plus the add-reaction button. +export const Reactions = () => ( + <StoryWrapper> + <ReactionsLeaf /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/RepliedThread.stories.tsx b/app/containers/message/components/stories/RepliedThread.stories.tsx new file mode 100644 index 00000000000..01a9a02f6a3 --- /dev/null +++ b/app/containers/message/components/stories/RepliedThread.stories.tsx @@ -0,0 +1,38 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageProvider } from '../../stores/MessageStore'; +import RepliedThreadLeaf from '../RepliedThread'; + +const store = createMockedStore(); + +const item = { + id: 'msg-id', + msg: 'Thread reply body', + tmid: 'thread-1', + tmsg: 'Original thread message', + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const StoryWrapper = ({ children }: { children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState({})}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/RepliedThread' +}; + +// A header message that replies to a thread renders the replied-thread preview. +export const RepliedThread = () => ( + <StoryWrapper> + <RepliedThreadLeaf isHeader /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/RightIcons/Edited.stories.tsx b/app/containers/message/components/stories/RightIcons/Edited.stories.tsx new file mode 100644 index 00000000000..746ada6e2bf --- /dev/null +++ b/app/containers/message/components/stories/RightIcons/Edited.stories.tsx @@ -0,0 +1,51 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../../definitions'; +import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; +import { MessageProvider } from '../../../stores/MessageStore'; +import EditedLeaf from '../../RightIcons/Edited'; + +const store = createMockedStore(); + +const editedItem = { + id: 'msg-id', + msg: 'Edited message', + editedBy: { username: 'rocket.cat' }, + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const plainItem = { + id: 'msg-id', + msg: 'Untouched message', + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const StoryWrapper = ({ item, children }: { item: TAnyMessageModel; children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState({})}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/RightIcons/Edited' +}; + +// An edited message renders the edit icon. +export const Edited = () => ( + <StoryWrapper item={editedItem}> + <EditedLeaf /> + </StoryWrapper> +); + +// A message that was never edited renders nothing. +export const EditedHidden = () => ( + <StoryWrapper item={plainItem}> + <EditedLeaf /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/RightIcons/Encrypted.stories.tsx b/app/containers/message/components/stories/RightIcons/Encrypted.stories.tsx new file mode 100644 index 00000000000..d55ba8763dd --- /dev/null +++ b/app/containers/message/components/stories/RightIcons/Encrypted.stories.tsx @@ -0,0 +1,57 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../../definitions'; +import { E2E_MESSAGE_TYPE } from '../../../../../lib/constants/keys'; +import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; +import { MessageProvider } from '../../../stores/MessageStore'; +import EncryptedLeaf from '../../RightIcons/Encrypted'; + +const store = createMockedStore(); + +const encryptedItem = { + id: 'msg-id', + msg: 'Encrypted message', + t: E2E_MESSAGE_TYPE, + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const plainItem = { + id: 'msg-id', + msg: 'Plain message', + t: undefined, + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const room = { + onEncryptedPress: () => {} +}; + +const StoryWrapper = ({ item, children }: { item: TAnyMessageModel; children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/RightIcons/Encrypted' +}; + +// An E2E message renders the encrypted icon. +export const Encrypted = () => ( + <StoryWrapper item={encryptedItem}> + <EncryptedLeaf /> + </StoryWrapper> +); + +// A non-encrypted message renders nothing. +export const EncryptedHidden = () => ( + <StoryWrapper item={plainItem}> + <EncryptedLeaf /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/RightIcons/MessageError.stories.tsx b/app/containers/message/components/stories/RightIcons/MessageError.stories.tsx new file mode 100644 index 00000000000..8b8ff91b5a4 --- /dev/null +++ b/app/containers/message/components/stories/RightIcons/MessageError.stories.tsx @@ -0,0 +1,57 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../../definitions'; +import { messagesStatus } from '../../../../../lib/constants/messagesStatus'; +import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; +import { MessageProvider } from '../../../stores/MessageStore'; +import MessageErrorLeaf from '../../RightIcons/MessageError'; + +const store = createMockedStore(); + +const errorItem = { + id: 'msg-id', + msg: 'Message that failed to send', + status: messagesStatus.ERROR, + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const okItem = { + id: 'msg-id', + msg: 'Message that sent fine', + status: messagesStatus.SENT, + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const room = { + errorActionsShow: () => {} +}; + +const StoryWrapper = ({ item, children }: { item: TAnyMessageModel; children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/RightIcons/MessageError' +}; + +// A message in error status renders the warning icon. +export const MessageError = () => ( + <StoryWrapper item={errorItem}> + <MessageErrorLeaf /> + </StoryWrapper> +); + +// A message without an error renders nothing. +export const MessageErrorHidden = () => ( + <StoryWrapper item={okItem}> + <MessageErrorLeaf /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/RightIcons/Pinned.stories.tsx b/app/containers/message/components/stories/RightIcons/Pinned.stories.tsx new file mode 100644 index 00000000000..b0e3463131c --- /dev/null +++ b/app/containers/message/components/stories/RightIcons/Pinned.stories.tsx @@ -0,0 +1,52 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../../definitions'; +import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; +import { MessageProvider } from '../../../stores/MessageStore'; +import PinnedLeaf from '../../RightIcons/Pinned'; + +const store = createMockedStore(); + +const pinnedItem = { + id: 'msg-id', + msg: 'Pinned message', + pinned: true, + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const unpinnedItem = { + id: 'msg-id', + msg: 'Regular message', + pinned: false, + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const StoryWrapper = ({ item, children }: { item: TAnyMessageModel; children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState({})}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/RightIcons/Pinned' +}; + +// A pinned message renders the pin icon. +export const Pinned = () => ( + <StoryWrapper item={pinnedItem}> + <PinnedLeaf /> + </StoryWrapper> +); + +// A non-pinned message renders nothing. +export const PinnedHidden = () => ( + <StoryWrapper item={unpinnedItem}> + <PinnedLeaf /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/RightIcons/ReadReceipt.stories.tsx b/app/containers/message/components/stories/RightIcons/ReadReceipt.stories.tsx new file mode 100644 index 00000000000..f4d7f0b2745 --- /dev/null +++ b/app/containers/message/components/stories/RightIcons/ReadReceipt.stories.tsx @@ -0,0 +1,59 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../../definitions'; +import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; +import { MessageProvider } from '../../../stores/MessageStore'; +import ReadReceiptLeaf from '../../RightIcons/ReadReceipt'; + +const store = createMockedStore(); + +const readItem = { + id: 'msg-id', + msg: 'Read message', + unread: false, + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const unreadItem = { + id: 'msg-id', + msg: 'Unread message', + unread: true, + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const StoryWrapper = ({ item, room, children }: { item: TAnyMessageModel; room: Record<string, any>; children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/RightIcons/ReadReceipt' +}; + +// With read receipts enabled, a read message renders the double-check icon. +export const ReadReceipt = () => ( + <StoryWrapper item={readItem} room={{ isReadReceiptEnabled: true }}> + <ReadReceiptLeaf /> + </StoryWrapper> +); + +// With read receipts enabled, an unread message renders the single-check icon. +export const ReadReceiptUnread = () => ( + <StoryWrapper item={unreadItem} room={{ isReadReceiptEnabled: true }}> + <ReadReceiptLeaf /> + </StoryWrapper> +); + +// With read receipts disabled, nothing renders. +export const ReadReceiptHidden = () => ( + <StoryWrapper item={readItem} room={{ isReadReceiptEnabled: false }}> + <ReadReceiptLeaf /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/RightIcons/Translated.stories.tsx b/app/containers/message/components/stories/RightIcons/Translated.stories.tsx new file mode 100644 index 00000000000..8e8a2d37564 --- /dev/null +++ b/app/containers/message/components/stories/RightIcons/Translated.stories.tsx @@ -0,0 +1,53 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../../definitions'; +import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; +import { MessageProvider } from '../../../stores/MessageStore'; +import TranslatedLeaf from '../../RightIcons/Translated'; + +const store = createMockedStore(); + +const translatedItem = { + id: 'msg-id', + msg: 'Original message', + autoTranslate: true, + translations: [{ _id: 'translation-1', language: 'en', value: 'Translated text' }], + u: { _id: 'author-id', username: 'other.user' } +} as unknown as TAnyMessageModel; + +const plainItem = { + id: 'msg-id', + msg: 'Original message', + autoTranslate: false, + u: { _id: 'author-id', username: 'other.user' } +} as unknown as TAnyMessageModel; + +const StoryWrapper = ({ item, room, children }: { item: TAnyMessageModel; room: Record<string, any>; children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/RightIcons/Translated' +}; + +// An auto-translated message from another user renders the language icon. +export const Translated = () => ( + <StoryWrapper + item={translatedItem} + room={{ user: { id: 'reader-id', username: 'rocket.cat' }, autoTranslateRoom: true, autoTranslateLanguage: 'en' }}> + <TranslatedLeaf /> + </StoryWrapper> +); + +// A message that is not translated renders nothing. +export const TranslatedHidden = () => ( + <StoryWrapper item={plainItem} room={{ user: { id: 'reader-id', username: 'rocket.cat' } }}> + <TranslatedLeaf /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/Thread.stories.tsx b/app/containers/message/components/stories/Thread.stories.tsx new file mode 100644 index 00000000000..23bc287e29c --- /dev/null +++ b/app/containers/message/components/stories/Thread.stories.tsx @@ -0,0 +1,45 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageProvider } from '../../stores/MessageStore'; +import ThreadLeaf from '../Thread'; + +const store = createMockedStore(); + +const item = { + id: 'msg-id', + msg: 'Thread parent message', + tcount: 3, + tlm: new Date('2024-01-01T10:20:00.000Z'), + replies: ['user-1', 'user-2', 'user-3'], + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const room = { + user: { id: 'reader-id', username: 'reader' }, + onThreadPress: () => {}, + isThreadRoom: false +}; + +const StoryWrapper = ({ children }: { children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/Thread' +}; + +// A message with thread replies renders the View Thread button and thread details. +export const Thread = () => ( + <StoryWrapper> + <ThreadLeaf /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/Time.stories.tsx b/app/containers/message/components/stories/Time.stories.tsx new file mode 100644 index 00000000000..edce66113a3 --- /dev/null +++ b/app/containers/message/components/stories/Time.stories.tsx @@ -0,0 +1,41 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageProvider } from '../../stores/MessageStore'; +import MessageTimeLeaf from '../Time'; + +const store = createMockedStore(); + +const item = { + id: 'msg-id', + msg: '', + ts: new Date('2024-01-01T10:20:00.000Z'), + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const room = { + timeFormat: 'LT' +}; + +const StoryWrapper = ({ children }: { children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/Time' +}; + +// The timestamp renders the message time using the room time format. +export const Time = () => ( + <StoryWrapper> + <MessageTimeLeaf /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/Urls.stories.tsx b/app/containers/message/components/stories/Urls.stories.tsx new file mode 100644 index 00000000000..b3ffba66f29 --- /dev/null +++ b/app/containers/message/components/stories/Urls.stories.tsx @@ -0,0 +1,51 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../reducers/mockedStore'; +import { updateSettings } from '../../../../actions/settings'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageProvider } from '../../stores/MessageStore'; +import UrlsLeaf from '../Urls'; + +const store = createMockedStore(); +store.dispatch(updateSettings('API_Embed', true)); + +const item = { + id: 'msg-id', + msg: '', + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false, + urls: [ + { + url: 'https://rocket.chat', + title: 'Rocket.Chat', + description: 'Have your own chat service on-premises or in the cloud', + image: '' + } + ] +} as unknown as TAnyMessageModel; + +const room = { + user: { id: 'reader-id', username: 'reader', token: 'token' }, + baseUrl: 'https://open.rocket.chat' +}; + +const StoryWrapper = ({ children }: { children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/Urls' +}; + +// A message with an embeddable URL renders the link preview card. +export const Urls = () => ( + <StoryWrapper> + <UrlsLeaf /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/User.stories.tsx b/app/containers/message/components/stories/User.stories.tsx new file mode 100644 index 00000000000..410ebe820af --- /dev/null +++ b/app/containers/message/components/stories/User.stories.tsx @@ -0,0 +1,45 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageProvider } from '../../stores/MessageStore'; +import UserLeaf from '../User'; + +const store = createMockedStore(); + +const item = { + id: 'msg-id', + msg: 'Message', + t: undefined, + ts: new Date('2024-01-01T10:20:00.000Z'), + u: { _id: 'author-id', username: 'rocket.cat', name: 'Rocket Cat' }, + alias: undefined, + role: undefined, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const room = { + user: { id: 'reader-id', username: 'reader' }, + timeFormat: 'LT' +}; + +const StoryWrapper = ({ children }: { children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +export default { + title: 'Message/User' +}; + +// The header renders the username, timestamp and right icons row. +export const User = () => ( + <StoryWrapper> + <UserLeaf /> + </StoryWrapper> +); diff --git a/app/containers/message/components/stories/__tests__/__snapshots__/leaves.test.tsx.snap b/app/containers/message/components/stories/__tests__/__snapshots__/leaves.test.tsx.snap new file mode 100644 index 00000000000..cb670e675f0 --- /dev/null +++ b/app/containers/message/components/stories/__tests__/__snapshots__/leaves.test.tsx.snap @@ -0,0 +1,2036 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Story Snapshots: Broadcast should match snapshot 1`] = ` +<View + style={ + { + "alignItems": "center", + "flexDirection": "row", + } + } +> + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignItems": "center", + "borderRadius": 4, + "flexDirection": "row", + "justifyContent": "center", + "paddingHorizontal": 12, + "paddingVertical": 8, + }, + { + "backgroundColor": "#1D74F5", + }, + ] + } + testID="message-broadcast-reply" + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + "gap": 8, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#FFFFFF", + "fontSize": 20, + }, + [ + { + "lineHeight": 20, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600", + "lineHeight": 16, + "textAlign": "left", + }, + { + "color": "#FFFFFF", + }, + ] + } + > + Reply + </Text> + </View> + </View> +</View> +`; + +exports[`Story Snapshots: BroadcastOwn should match snapshot 1`] = `null`; + +exports[`Story Snapshots: CallButton should match snapshot 1`] = ` +<View + style={ + { + "alignItems": "center", + "flexDirection": "row", + } + } +> + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignItems": "center", + "borderRadius": 4, + "flexDirection": "row", + "justifyContent": "center", + "paddingHorizontal": 12, + "paddingVertical": 8, + }, + { + "backgroundColor": "#1D74F5", + }, + ] + } + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + "gap": 8, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#FFFFFF", + "fontSize": 16, + }, + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600", + "lineHeight": 16, + "textAlign": "left", + }, + { + "color": "#FFFFFF", + }, + ] + } + > + Click to join! + </Text> + </View> + </View> +</View> +`; + +exports[`Story Snapshots: Content should match snapshot 1`] = ` +<View + style={false} + testID="message-content-This is a plain text message" +> + <View + style={ + { + "gap": 2, + } + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + <Text + style={ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + } + } + > + <Text + accessibilityLabel="This is a plain text message" + style={ + [ + { + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + ] + } + > + This is a plain text message + </Text> + </Text> + </Text> + </View> +</View> +`; + +exports[`Story Snapshots: Discussion should match snapshot 1`] = ` +<View + style={ + { + "gap": 4, + } + } +> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontStyle": "italic", + "fontWeight": "400", + "lineHeight": 20, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + > + Started a discussion: + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 20, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + Discussion topic message + </Text> + <View + style={ + [ + { + "alignItems": "center", + "flexDirection": "row", + }, + { + "gap": 8, + }, + ] + } + > + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignItems": "center", + "borderRadius": 4, + "flexDirection": "row", + "justifyContent": "center", + "paddingHorizontal": 12, + "paddingVertical": 8, + }, + { + "backgroundColor": "#1D74F5", + }, + ] + } + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + "gap": 8, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#FFFFFF", + "fontSize": 16, + }, + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600", + "lineHeight": 16, + "textAlign": "left", + }, + { + "color": "#FFFFFF", + }, + ] + } + > + 5 messages + </Text> + </View> + </View> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + > + January 1, 2024 + </Text> + </View> +</View> +`; + +exports[`Story Snapshots: Edited should match snapshot 1`] = ` +<View + style={ + { + "paddingLeft": 5, + } + } + testID="Edited message-edited" +> + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 16, + }, + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> +</View> +`; + +exports[`Story Snapshots: EditedHidden should match snapshot 1`] = `null`; + +exports[`Story Snapshots: Emoji should match snapshot 1`] = ` +<Text + style={ + { + "fontSize": 30, + } + } +> + 😀 +</Text> +`; + +exports[`Story Snapshots: EmojiCustom should match snapshot 1`] = ` +<ViewManagerAdapter_ExpoImage + containerViewRef={{"current":"Object"}} + contentFit="contain" + contentPosition={ + { + "left": "50%", + "top": "50%", + } + } + height={30} + nativeViewRef={{"current":"NativeComponent"}} + onError={[Function]} + onLoad={[Function]} + onLoadStart={[Function]} + onProgress={[Function]} + placeholder={[]} + source={ + [ + { + "uri": "https://open.rocket.chat/emoji-custom/react_rocket.png", + }, + ] + } + style={ + { + "height": 30, + "width": 30, + } + } + transition={null} + width={30} +/> +`; + +exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` +<View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "paddingLeft": 5, + } + } +> + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 16, + }, + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> +</View> +`; + +exports[`Story Snapshots: EncryptedHidden should match snapshot 1`] = `null`; + +exports[`Story Snapshots: MessageAvatar should match snapshot 1`] = ` +<View + style={ + { + "alignItems": "flex-end", + "width": 36, + } + } +> + <View + accessible={true} + style={ + [ + { + "borderRadius": 4, + "height": 36, + "width": 36, + }, + { + "marginTop": 4, + }, + ] + } + testID="avatar" + > + <ExternalKeyboardView + canBeFocused={true} + enableA11yFocus={false} + enableContextMenu={false} + group={false} + haloEffect={true} + hasKeyDownPress={false} + hasKeyUpPress={true} + hasOnFocusChanged={true} + lockFocus={0} + onContextMenuPress={[Function]} + onFocusChange={[Function]} + onKeyUpPress={[Function]} + orderIndex={-1} + screenAutoA11yFocusDelay={300} + style={ + [ + undefined, + undefined, + ] + } + > + <RNGestureHandlerButton + activeOpacity={1} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={-1} + handlerType="NativeViewGestureHandler" + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[Function]} + rippleColor="#E4E7EA" + style={ + [ + { + "backgroundColor": undefined, + "borderRadius": undefined, + "margin": undefined, + "marginBottom": undefined, + "marginEnd": undefined, + "marginHorizontal": undefined, + "marginLeft": undefined, + "marginRight": undefined, + "marginStart": undefined, + "marginTop": undefined, + "marginVertical": undefined, + }, + { + "cursor": undefined, + }, + ] + } + underlayColor="#E4E7EA" + > + <View + collapsable={false} + style={ + { + "backgroundColor": "#E4E7EA", + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "bottom": 0, + "left": 0, + "opacity": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + /> + <View + accessibilityLabel="rocket.cat's avatar" + accessible={true} + style={{}} + > + <ViewManagerAdapter_ExpoImage + borderRadius={4} + containerViewRef={{"current":"Object"}} + contentFit="cover" + contentPosition={ + { + "left": "50%", + "top": "50%", + } + } + height={36} + nativeViewRef={{"current":"NativeComponent"}} + onError={[Function]} + onLoad={[Function]} + onLoadStart={[Function]} + onProgress={[Function]} + placeholder={[]} + priority="high" + source={ + [ + { + "headers": { + "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", + }, + "uri": "https://open.rocket.chat/avatar/rocket.cat?format=png&size=72", + }, + ] + } + style={ + { + "borderRadius": 4, + "height": 36, + "width": 36, + } + } + transition={null} + width={36} + /> + </View> + </RNGestureHandlerButton> + </ExternalKeyboardView> + </View> +</View> +`; + +exports[`Story Snapshots: MessageError should match snapshot 1`] = ` +<View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "paddingLeft": 5, + } + } +> + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#EC0D2A", + "fontSize": 16, + }, + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> +</View> +`; + +exports[`Story Snapshots: MessageErrorHidden should match snapshot 1`] = `null`; + +exports[`Story Snapshots: Pinned should match snapshot 1`] = ` +<Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 16, + }, + [ + { + "lineHeight": 16, + }, + { + "paddingLeft": 5, + }, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + testID="Pinned message-pinned" +> +  +</Text> +`; + +exports[`Story Snapshots: PinnedHidden should match snapshot 1`] = `null`; + +exports[`Story Snapshots: Reactions should match snapshot 1`] = ` +<View + style={ + { + "flexDirection": "row", + "flexWrap": "wrap", + "gap": 4, + } + } +> + <View + accessibilityLabel=":joy:, 2" + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": true, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "borderRadius": 4, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + testID="message-reaction-:joy:" + > + <View + style={ + [ + { + "alignItems": "center", + "borderRadius": 4, + "borderWidth": 1, + "flexDirection": "row", + "gap": 4, + "justifyContent": "center", + "minWidth": 46.3, + "paddingHorizontal": 4, + }, + { + "borderColor": "#1D74F5", + "height": 28, + }, + ] + } + > + <Text + style={ + { + "color": "#ffffff", + "fontSize": 13, + } + } + > + 😂 + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#1D74F5", + }, + ] + } + > + 2 + </Text> + </View> + </View> + <View + accessibilityLabel=":heart:, 1" + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": false, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "borderRadius": 4, + }, + { + "backgroundColor": "#FFFFFF", + }, + ] + } + testID="message-reaction-:heart:" + > + <View + style={ + [ + { + "alignItems": "center", + "borderRadius": 4, + "borderWidth": 1, + "flexDirection": "row", + "gap": 4, + "justifyContent": "center", + "minWidth": 46.3, + "paddingHorizontal": 4, + }, + { + "borderColor": "#CBCED1", + "height": 28, + }, + ] + } + > + <Text + style={ + { + "color": "#ffffff", + "fontSize": 13, + } + } + > + ❤️ + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#1D74F5", + }, + ] + } + > + 1 + </Text> + </View> + </View> + <View + accessibilityLabel="Add reaction" + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "borderRadius": 4, + }, + { + "backgroundColor": "#FFFFFF", + }, + ] + } + testID="message-add-reaction" + > + <View + style={ + [ + { + "alignItems": "center", + "borderRadius": 4, + "borderWidth": 1, + "flexDirection": "row", + "gap": 4, + "justifyContent": "center", + "minWidth": 46.3, + "paddingHorizontal": 4, + }, + { + "borderColor": "#CBCED1", + "height": 28, + }, + ] + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#1D74F5", + "fontSize": 20, + }, + [ + { + "lineHeight": 20, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </View> +</View> +`; + +exports[`Story Snapshots: ReadReceipt should match snapshot 1`] = ` +<Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#095AD2", + "fontSize": 25, + }, + [ + { + "lineHeight": 25, + }, + { + "marginTop": -5, + "paddingLeft": 5, + }, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + testID="read-receipt-read" +> +  +</Text> +`; + +exports[`Story Snapshots: ReadReceiptHidden should match snapshot 1`] = `null`; + +exports[`Story Snapshots: ReadReceiptUnread should match snapshot 1`] = ` +<Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#9EA2A8", + "fontSize": 25, + }, + [ + { + "lineHeight": 25, + }, + { + "marginTop": -5, + "paddingLeft": 5, + }, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + testID="read-receipt-unread" +> +  +</Text> +`; + +exports[`Story Snapshots: RepliedThread should match snapshot 1`] = ` +<View + style={ + { + "alignItems": "center", + "flexDirection": "row", + "gap": 10, + } + } + testID="message-thread-replied-on-Original thread message" +> + <View + style={ + { + "alignItems": "flex-end", + "width": 36, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#095AD2", + "fontSize": 20, + }, + [ + { + "lineHeight": 20, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <Text + accessibilityLabel="Original thread message" + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#2F343D", + "lineHeight": undefined, + }, + { + "backgroundColor": "transparent", + "flex": 1, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#095AD2", + }, + ] + } + testID="markdown-preview-Original thread message" + > + Original thread message + </Text> + <View + style={ + { + "alignItems": "center", + "justifyContent": "center", + "marginLeft": 4, + "marginRight": 4, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#6C727A", + "fontSize": 20, + }, + [ + { + "lineHeight": 20, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> +</View> +`; + +exports[`Story Snapshots: Thread should match snapshot 1`] = ` +<View + style={ + { + "alignItems": "center", + "flexDirection": "row", + } + } +> + <View + accessibilityLabel="View thread" + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignItems": "center", + "borderRadius": 4, + "flexDirection": "row", + "justifyContent": "center", + "paddingHorizontal": 12, + "paddingVertical": 8, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + testID="message-thread-button-Thread parent message" + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600", + "lineHeight": 16, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + View thread + </Text> + </View> + <View + style={ + [ + { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + }, + { + "flex": 1, + "marginLeft": 12, + }, + ] + } + > + <View + style={ + { + "flex": 1, + "flexDirection": "row", + } + } + > + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + "marginRight": 8, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 24, + }, + [ + { + "lineHeight": 24, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "marginLeft": 2, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + testID="thread-count-3" + > + 3 + </Text> + </View> + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + "marginRight": 8, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 24, + }, + [ + { + "lineHeight": 24, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 10, + "fontWeight": "600", + "marginLeft": 2, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + > + 3 + </Text> + </View> + </View> + <View + style={ + { + "alignItems": "center", + "flexDirection": "row", + } + } + > + <ExternalKeyboardView + canBeFocused={true} + enableA11yFocus={false} + enableContextMenu={false} + group={false} + haloEffect={true} + hasKeyDownPress={false} + hasKeyUpPress={true} + hasOnFocusChanged={true} + lockFocus={0} + onContextMenuPress={[Function]} + onFocusChange={[Function]} + onKeyUpPress={[Function]} + orderIndex={-1} + screenAutoA11yFocusDelay={300} + style={ + [ + undefined, + undefined, + ] + } + > + <RNGestureHandlerButton + activeOpacity={1} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={-1} + handlerType="NativeViewGestureHandler" + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[Function]} + rippleColor="#E4E7EA" + style={ + [ + { + "backgroundColor": undefined, + "borderRadius": undefined, + "margin": undefined, + "marginBottom": undefined, + "marginEnd": undefined, + "marginHorizontal": undefined, + "marginLeft": undefined, + "marginRight": undefined, + "marginStart": undefined, + "marginTop": undefined, + "marginVertical": undefined, + }, + { + "cursor": undefined, + }, + ] + } + underlayColor="#E4E7EA" + > + <View + collapsable={false} + style={ + { + "backgroundColor": "#E4E7EA", + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "bottom": 0, + "left": 0, + "opacity": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + /> + <View + accessibilityLabel="Follow thread" + style={{}} + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 24, + }, + [ + { + "lineHeight": 24, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </RNGestureHandlerButton> + </ExternalKeyboardView> + </View> + </View> +</View> +`; + +exports[`Story Snapshots: Time should match snapshot 1`] = ` +<Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } +> + 10:20 AM +</Text> +`; + +exports[`Story Snapshots: Translated should match snapshot 1`] = ` +<View + style={ + { + "paddingLeft": 5, + } + } +> + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 16, + }, + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> +</View> +`; + +exports[`Story Snapshots: TranslatedHidden should match snapshot 1`] = `null`; + +exports[`Story Snapshots: Urls should match snapshot 1`] = ` +<View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "flex": 1, + "flexDirection": "column", + "gap": 4, + }, + { + "backgroundColor": "#F7F8FA", + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } +> + <View + style={ + { + "alignItems": "flex-start", + "flex": 1, + "flexDirection": "column", + "justifyContent": "flex-start", + "padding": 12, + } + } + > + <Text + numberOfLines={2} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "500", + "textAlign": "left", + }, + { + "color": "#095AD2", + }, + ] + } + > + Rocket.Chat + </Text> + <Text + numberOfLines={2} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + > + Have your own chat service on-premises or in the cloud + </Text> + </View> +</View> +`; + +exports[`Story Snapshots: User should match snapshot 1`] = ` +<View + style={ + { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + "justifyContent": "space-between", + } + } +> + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, + } + } + testID="username-header-rocket.cat" + > + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#1F2329", + }, + ] + } + > + rocket.cat + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + > + 10:20 AM + </Text> + </View> + <View + style={ + { + "flexDirection": "row", + } + } + /> +</View> +`; diff --git a/app/containers/message/components/stories/__tests__/leaves.test.tsx b/app/containers/message/components/stories/__tests__/leaves.test.tsx new file mode 100644 index 00000000000..e341085742d --- /dev/null +++ b/app/containers/message/components/stories/__tests__/leaves.test.tsx @@ -0,0 +1,38 @@ +import { generateSnapshots } from '../../../../../../.rnstorybook/generateSnapshots'; +import * as broadcast from '../Broadcast.stories'; +import * as callButton from '../CallButton.stories'; +import * as content from '../Content.stories'; +import * as discussion from '../Discussion.stories'; +import * as emoji from '../Emoji.stories'; +import * as messageAvatar from '../MessageAvatar.stories'; +import * as reactions from '../Reactions.stories'; +import * as repliedThread from '../RepliedThread.stories'; +import * as thread from '../Thread.stories'; +import * as time from '../Time.stories'; +import * as urls from '../Urls.stories'; +import * as user from '../User.stories'; +import * as edited from '../RightIcons/Edited.stories'; +import * as encrypted from '../RightIcons/Encrypted.stories'; +import * as messageError from '../RightIcons/MessageError.stories'; +import * as pinned from '../RightIcons/Pinned.stories'; +import * as readReceipt from '../RightIcons/ReadReceipt.stories'; +import * as translated from '../RightIcons/Translated.stories'; + +generateSnapshots(broadcast); +generateSnapshots(callButton); +generateSnapshots(content); +generateSnapshots(discussion); +generateSnapshots(emoji); +generateSnapshots(messageAvatar); +generateSnapshots(reactions); +generateSnapshots(repliedThread); +generateSnapshots(thread); +generateSnapshots(time); +generateSnapshots(urls); +generateSnapshots(user); +generateSnapshots(edited); +generateSnapshots(encrypted); +generateSnapshots(messageError); +generateSnapshots(pinned); +generateSnapshots(readReceipt); +generateSnapshots(translated); From 715c48a76ad7ed90f2dd396c26091fe36658c6c5 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 15:31:45 -0300 Subject: [PATCH 064/144] test: add unit tests for logic-bearing message units (NATIVE-1397) Cover the branching hooks and component the presentational stories do not: useFile (persisted vs forwarded-file state), useMediaAutoDownload (file-type resolution, cache/auto-download/download and onPress states), and Attachments/Reply (null gate, file-download vs open-link onPress, disabled handling, and sub-part conditionals). --- .../components/Attachments/Reply.test.tsx | 208 ++++++++++++++ .../message/hooks/__tests__/useFile.test.ts | 84 ++++++ .../__tests__/useMediaAutoDownload.test.tsx | 268 ++++++++++++++++++ 3 files changed, 560 insertions(+) create mode 100644 app/containers/message/components/Attachments/Reply.test.tsx create mode 100644 app/containers/message/hooks/__tests__/useFile.test.ts create mode 100644 app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx diff --git a/app/containers/message/components/Attachments/Reply.test.tsx b/app/containers/message/components/Attachments/Reply.test.tsx new file mode 100644 index 00000000000..320d5b79087 --- /dev/null +++ b/app/containers/message/components/Attachments/Reply.test.tsx @@ -0,0 +1,208 @@ +import { fireEvent, render, waitFor } from '@testing-library/react-native'; +import { Provider } from 'react-redux'; + +import Reply from './Reply'; +import { MessageProvider } from '../../stores/MessageStore'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { mockedStore } from '../../../../reducers/mockedStore'; +import { type IAttachment, type TAnyMessageModel } from '../../../../definitions'; +import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../../../lib/constants/keys'; +import { fileDownloadAndPreview } from '../../../../lib/methods/helpers'; +import openLink from '../../../../lib/methods/helpers/openLink'; +import { formatAttachmentUrl } from '../../../../lib/methods/helpers/formatAttachmentUrl'; + +jest.mock('../../../markdown', () => { + const React = require('react'); + const { Text } = require('react-native'); + return { + __esModule: true, + default: ({ msg }: { msg?: string }) => <Text testID='reply-markdown'>{msg}</Text>, + MarkdownPreview: ({ msg }: { msg?: string }) => <Text testID='reply-markdown-preview'>{msg}</Text> + }; +}); + +jest.mock('../../../../lib/methods/helpers', () => ({ + fileDownloadAndPreview: jest.fn(() => Promise.resolve()) +})); + +jest.mock('../../../../lib/methods/helpers/openLink', () => ({ + __esModule: true, + default: jest.fn() +})); + +jest.mock('../../../../lib/methods/helpers/formatAttachmentUrl', () => ({ + formatAttachmentUrl: jest.fn((url: string) => `formatted:${url}`) +})); + +jest.mock('expo-image', () => { + const { View } = require('react-native'); + const Image = () => <View testID='reply-url-image' />; + Image.loadAsync = jest.fn(); + return { Image }; +}); + +const mockFileDownloadAndPreview = fileDownloadAndPreview as jest.Mock; +const mockOpenLink = openLink as jest.Mock; +const mockFormatAttachmentUrl = formatAttachmentUrl as jest.Mock; + +const getCustomEmoji = jest.fn(); + +const buildItem = (isEncrypted?: boolean) => + ({ + id: 'msg-1', + t: isEncrypted ? E2E_MESSAGE_TYPE : undefined, + e2e: isEncrypted ? E2E_STATUS.PENDING : undefined + } as unknown as TAnyMessageModel); + +const renderReply = ({ + attachment, + msg, + isEncrypted, + ctx = {} +}: { + attachment?: IAttachment; + msg?: string; + isEncrypted?: boolean; + ctx?: Record<string, any>; +}) => { + const contextValue = { + user: { id: 'user-1', username: 'john', token: 'token' }, + baseUrl: 'https://open.rocket.chat', + timeFormat: 'HH:mm', + ...ctx + }; + return render( + <Provider store={mockedStore}> + <MessageRoomProvider {...pickMessageRoomState(contextValue)}> + <MessageProvider item={buildItem(isEncrypted)}> + <Reply attachment={attachment as IAttachment} getCustomEmoji={getCustomEmoji} msg={msg} /> + </MessageProvider> + </MessageRoomProvider> + </Provider> + ); +}; + +describe('Reply', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockFormatAttachmentUrl.mockImplementation((url: string) => `formatted:${url}`); + }); + + describe('null gate', () => { + it('renders nothing when there is no attachment', () => { + const { toJSON } = renderReply({ attachment: undefined }); + expect(toJSON()).toBeNull(); + }); + + it('renders nothing when the message is encrypted', () => { + const { toJSON } = renderReply({ attachment: { author_name: 'Alice', text: 'Hello' }, isEncrypted: true }); + expect(toJSON()).toBeNull(); + }); + }); + + it('renders the reply with an author/text testID', () => { + const { getByTestId } = renderReply({ attachment: { author_name: 'Alice', text: 'Hello' } }); + expect(getByTestId('reply-Alice-Hello')).toBeTruthy(); + }); + + describe('onPress', () => { + it('does nothing when there is neither a title_link nor an author_link', () => { + const { getByTestId } = renderReply({ attachment: { author_name: 'Alice', text: 'Hello' } }); + fireEvent.press(getByTestId('reply-Alice-Hello')); + expect(mockOpenLink).not.toHaveBeenCalled(); + expect(mockFileDownloadAndPreview).not.toHaveBeenCalled(); + }); + + it('downloads and previews file attachments that have a title_link', async () => { + const attachment: IAttachment = { type: 'file', title_link: '/file-upload/doc', text: 'doc', author_name: 'Alice' }; + const { getByTestId } = renderReply({ attachment }); + fireEvent.press(getByTestId('reply-Alice-doc')); + + await waitFor(() => expect(mockFileDownloadAndPreview).toHaveBeenCalled()); + expect(mockFormatAttachmentUrl).toHaveBeenCalledWith('/file-upload/doc', 'user-1', 'token', 'https://open.rocket.chat'); + expect(mockFileDownloadAndPreview).toHaveBeenCalledWith('formatted:/file-upload/doc', attachment, 'msg-1'); + expect(mockOpenLink).not.toHaveBeenCalled(); + }); + + it('opens the link for non-file attachments using the title_link', () => { + const { getByTestId } = renderReply({ + attachment: { title_link: 'https://rocket.chat', text: 'link', author_name: 'Alice' } + }); + fireEvent.press(getByTestId('reply-Alice-link')); + expect(mockOpenLink).toHaveBeenCalledWith('https://rocket.chat', 'light'); + expect(mockFileDownloadAndPreview).not.toHaveBeenCalled(); + }); + + it('falls back to the author_link when there is no title_link', () => { + const { getByTestId } = renderReply({ + attachment: { author_link: 'https://author.example', text: 'link', author_name: 'Alice' } + }); + fireEvent.press(getByTestId('reply-Alice-link')); + expect(mockOpenLink).toHaveBeenCalledWith('https://author.example', 'light'); + }); + }); + + it('disables the touchable when the attachment is a message_link', () => { + const { getByTestId } = renderReply({ + attachment: { message_link: '/msg', author_name: 'Alice', text: 'Hello' } + }); + expect(getByTestId('reply-Alice-Hello').props.accessibilityState.disabled).toBe(true); + }); + + describe('Description', () => { + it('uses MarkdownPreview for file attachments whose title is a filename', () => { + const { getByTestId } = renderReply({ attachment: { type: 'file', title: 'script.py', author_name: 'Alice' } }); + const preview = getByTestId('reply-markdown-preview'); + expect(preview).toBeTruthy(); + expect(preview.props.children).toBe('script.py'); + }); + + it('uses Markdown when the attachment has explicit text', () => { + const { getByTestId } = renderReply({ attachment: { type: 'file', text: '**bold**', author_name: 'Alice' } }); + expect(getByTestId('reply-markdown').props.children).toBe('**bold**'); + }); + }); + + describe('Title', () => { + it('shows the formatted time only for message_link attachments with a timestamp', () => { + const { getByText } = renderReply({ + attachment: { message_link: '/msg', ts: '2021-01-01T13:45:00.000Z', author_name: 'Alice', text: 'Hi' } + }); + expect(getByText('13:45')).toBeTruthy(); + }); + + it('does not show a time without a message_link', () => { + const { queryByText } = renderReply({ + attachment: { ts: '2021-01-01T13:45:00.000Z', author_name: 'Alice', text: 'Hi' } + }); + expect(queryByText('13:45')).toBeNull(); + }); + }); + + describe('UrlImage', () => { + it('renders the thumbnail image when thumb_url is present', () => { + const { getByTestId } = renderReply({ + attachment: { thumb_url: 'https://open.rocket.chat/thumb.png', author_name: 'Alice', text: 'Hi' } + }); + expect(getByTestId('reply-url-image')).toBeTruthy(); + }); + + it('does not render an image without thumb_url', () => { + const { queryByTestId } = renderReply({ attachment: { author_name: 'Alice', text: 'Hi' } }); + expect(queryByTestId('reply-url-image')).toBeNull(); + }); + }); + + describe('Fields', () => { + it('renders attachment fields when present', () => { + const { getByText } = renderReply({ + attachment: { + author_name: 'Alice', + text: 'Hi', + fields: [{ title: 'Status', value: 'Online', short: true }] + } + }); + expect(getByText('Status')).toBeTruthy(); + }); + }); +}); diff --git a/app/containers/message/hooks/__tests__/useFile.test.ts b/app/containers/message/hooks/__tests__/useFile.test.ts new file mode 100644 index 00000000000..8f72264a398 --- /dev/null +++ b/app/containers/message/hooks/__tests__/useFile.test.ts @@ -0,0 +1,84 @@ +import { act, renderHook } from '@testing-library/react-native'; + +import { useFile } from '../useFile'; +import { type IAttachment } from '../../../../definitions'; +import { getMessageById } from '../../../../lib/database/services/Message'; +import { getThreadMessageById } from '../../../../lib/database/services/ThreadMessage'; + +jest.mock('../../../../lib/database/services/Message', () => ({ + getMessageById: jest.fn() +})); + +jest.mock('../../../../lib/database/services/ThreadMessage', () => ({ + getThreadMessageById: jest.fn() +})); + +const mockGetMessageById = getMessageById as jest.Mock; +const mockGetThreadMessageById = getThreadMessageById as jest.Mock; + +const file = { title: 'original.png', title_link: '/original' } as IAttachment; + +// Flushes the async checkMessage effect (two awaited DB reads) inside act so any +// setIsMessagePersisted update settles before assertions run. +const flushEffect = () => + act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + +describe('useFile', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockGetThreadMessageById.mockResolvedValue(undefined); + mockGetMessageById.mockResolvedValue(undefined); + }); + + it('returns the file prop and treats forwarded merges as a no-op while the thread message persists', async () => { + mockGetThreadMessageById.mockResolvedValue({ id: 'thread-message' }); + const { result } = renderHook(() => useFile(file, 'msg-id')); + + await flushEffect(); + expect(mockGetThreadMessageById).toHaveBeenCalledWith('msg-id'); + expect(mockGetMessageById).not.toHaveBeenCalled(); + expect(result.current[0]).toBe(file); + + act(() => result.current[1]({ title_link: '/forwarded' })); + expect(result.current[0]).toBe(file); + }); + + it('stays persisted when no thread message exists but the message is found', async () => { + mockGetThreadMessageById.mockResolvedValue(undefined); + mockGetMessageById.mockResolvedValue({ id: 'message' }); + const { result } = renderHook(() => useFile(file, 'msg-id')); + + await flushEffect(); + expect(mockGetMessageById).toHaveBeenCalledWith('msg-id'); + expect(result.current[0]).toBe(file); + + act(() => result.current[1]({ title_link: '/forwarded' })); + expect(result.current[0]).toBe(file); + }); + + it('becomes not-persisted and returns the merged localFile when neither message is found', async () => { + const { result } = renderHook(() => useFile(file, 'msg-id')); + + await flushEffect(); + expect(mockGetMessageById).toHaveBeenCalledWith('msg-id'); + + act(() => result.current[1]({ title_link: '/forwarded' })); + expect(result.current[0].title_link).toBe('/forwarded'); + expect(result.current[0]).not.toBe(file); + expect(result.current[0].title).toBe('original.png'); + }); + + it('starts not-persisted when messageId is empty and merges forwarded files into localFile', async () => { + const { result } = renderHook(() => useFile(file, '')); + + await flushEffect(); + expect(result.current[0]).toBe(file); + + act(() => result.current[1]({ title_link: '/forwarded' })); + expect(result.current[0]).not.toBe(file); + expect(result.current[0].title_link).toBe('/forwarded'); + }); +}); diff --git a/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx b/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx new file mode 100644 index 00000000000..8295ec2b3fb --- /dev/null +++ b/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx @@ -0,0 +1,268 @@ +import { type ReactNode } from 'react'; +import { Provider } from 'react-redux'; +import { act, renderHook, waitFor } from '@testing-library/react-native'; + +import { useMediaAutoDownload } from '../useMediaAutoDownload'; +import { MessageProvider } from '../../stores/MessageStore'; +import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { mockedStore } from '../../../../reducers/mockedStore'; +import { type IAttachment, type IUserMessage, type TAnyMessageModel } from '../../../../definitions'; +import { cancelDownload, downloadMediaFile, getMediaCache, isDownloadActive } from '../../../../lib/methods/handleMediaDownload'; +import { fetchAutoDownloadEnabled } from '../../../../lib/methods/autoDownloadPreference'; +import { formatAttachmentUrl } from '../../../../lib/methods/helpers/formatAttachmentUrl'; +import { isImageBase64 } from '../../../../lib/methods/isImageBase64'; +import { emitter } from '../../../../lib/methods/helpers/emitter'; + +jest.mock('../../../../lib/methods/handleMediaDownload', () => ({ + downloadMediaFile: jest.fn(), + getMediaCache: jest.fn(), + isDownloadActive: jest.fn(), + cancelDownload: jest.fn() +})); + +jest.mock('../../../../lib/methods/autoDownloadPreference', () => ({ + fetchAutoDownloadEnabled: jest.fn() +})); + +jest.mock('../../../../lib/methods/helpers/formatAttachmentUrl', () => ({ + formatAttachmentUrl: jest.fn() +})); + +jest.mock('../../../../lib/methods/isImageBase64', () => ({ + isImageBase64: jest.fn() +})); + +// Real mitt behind spies so resumeDownload registrations can be observed and the +// download listener can be driven with emitter.emit. +jest.mock('../../../../lib/methods/helpers/emitter', () => { + const mittModule = require('mitt'); + const mitt = mittModule.default || mittModule; + const instance = mitt(); + return { + emitter: { + on: jest.fn((type: string, handler: (uri: string) => void) => instance.on(type, handler)), + off: jest.fn((type: string, handler: (uri: string) => void) => instance.off(type, handler)), + emit: (type: string, event: string) => instance.emit(type, event) + } + }; +}); + +// The persisted/localFile logic is covered by useFile.test; here useFile is a plain +// state cell so setCurrentFile merges are observable through the returned currentFile. +jest.mock('../useFile', () => { + const { useState } = require('react'); + return { + useFile: (file: IAttachment) => { + const [current, setCurrent] = useState(file); + const manage = (partial: Partial<IAttachment>) => setCurrent((prev: IAttachment) => ({ ...prev, ...partial })); + return [current, manage]; + } + }; +}); + +const mockDownloadMediaFile = downloadMediaFile as jest.Mock; +const mockGetMediaCache = getMediaCache as jest.Mock; +const mockIsDownloadActive = isDownloadActive as jest.Mock; +const mockCancelDownload = cancelDownload as jest.Mock; +const mockFetchAutoDownloadEnabled = fetchAutoDownloadEnabled as jest.Mock; +const mockFormatAttachmentUrl = formatAttachmentUrl as jest.Mock; +const mockIsImageBase64 = isImageBase64 as jest.Mock; +const mockEmitterOn = emitter.on as jest.Mock; + +const URL = 'https://open.rocket.chat/file.png'; +const USER = { id: 'user-1', username: 'john', token: 'token' }; + +// Flushes the mount effect's async cache/auto-download chain inside act, used when the +// final status equals the initial 'to-download' so there is no change for waitFor to await. +const flushMount = () => + act(async () => { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + }); + +const renderMediaHook = ({ + file, + author, + showAttachment, + ctx = {} +}: { + file: IAttachment; + author?: IUserMessage; + showAttachment?: Function; + ctx?: Record<string, any>; +}) => { + const contextValue = { baseUrl: 'https://open.rocket.chat', user: USER, ...ctx }; + const item = { id: 'msg-1' } as unknown as TAnyMessageModel; + const wrapper = ({ children }: { children: ReactNode }) => ( + <Provider store={mockedStore}> + <MessageRoomProvider {...pickMessageRoomState(contextValue)}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> + ); + return renderHook(() => useMediaAutoDownload({ file, author, showAttachment }), { wrapper }); +}; + +describe('useMediaAutoDownload', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockFormatAttachmentUrl.mockReturnValue(URL); + mockIsImageBase64.mockReturnValue(false); + mockGetMediaCache.mockResolvedValue({ exists: false }); + mockIsDownloadActive.mockReturnValue(false); + mockFetchAutoDownloadEnabled.mockReturnValue(false); + mockDownloadMediaFile.mockResolvedValue('file://downloaded'); + }); + + describe('getFileType selection', () => { + it('resolves an image file to the image auto-download preference', async () => { + const { result } = renderMediaHook({ file: { image_url: '/img' } }); + await flushMount(); + expect(mockFetchAutoDownloadEnabled).toHaveBeenCalledWith('imagePreferenceDownload'); + expect(result.current.status).toBe('to-download'); + }); + + it('resolves a video file to the video auto-download preference', async () => { + renderMediaHook({ file: { video_url: '/video' } }); + await flushMount(); + expect(mockFetchAutoDownloadEnabled).toHaveBeenCalledWith('videoPreferenceDownload'); + }); + + it('resolves an audio file to the audio auto-download preference', async () => { + renderMediaHook({ file: { audio_url: '/audio' } }); + await flushMount(); + expect(mockFetchAutoDownloadEnabled).toHaveBeenCalledWith('audioPreferenceDownload'); + }); + + it('defaults to the image preference when the file has no media url', async () => { + renderMediaHook({ file: {} }); + await flushMount(); + expect(mockFetchAutoDownloadEnabled).toHaveBeenCalledWith('imagePreferenceDownload'); + }); + }); + + describe('isEncrypted', () => { + it('is true when the current file e2e is pending', async () => { + const { result } = renderMediaHook({ file: { image_url: '/img', e2e: 'pending' } }); + await flushMount(); + expect(result.current.isEncrypted).toBe(true); + }); + + it('is false when the current file e2e is not pending', async () => { + const { result } = renderMediaHook({ file: { image_url: '/img', e2e: 'done' } }); + await flushMount(); + expect(mockGetMediaCache).toHaveBeenCalled(); + expect(result.current.isEncrypted).toBe(false); + }); + }); + + describe('mount effect', () => { + it('marks a base64 image as downloaded without hitting the cache', async () => { + mockIsImageBase64.mockReturnValue(true); + const { result } = renderMediaHook({ file: { image_url: '/img' } }); + await waitFor(() => expect(result.current.status).toBe('downloaded')); + expect(mockGetMediaCache).not.toHaveBeenCalled(); + }); + + it('marks the file downloaded and updates title_link when a non-encrypted cache exists', async () => { + mockGetMediaCache.mockResolvedValue({ exists: true, uri: 'file://cache' }); + const { result } = renderMediaHook({ file: { image_url: '/img' } }); + await waitFor(() => expect(result.current.status).toBe('downloaded')); + expect(result.current.currentFile.title_link).toBe('file://cache'); + }); + + it('resumes an active download by registering the download listener and showing loading', async () => { + mockIsDownloadActive.mockReturnValue(true); + const { result } = renderMediaHook({ file: { image_url: '/img' } }); + await waitFor(() => expect(result.current.status).toBe('loading')); + expect(mockEmitterOn).toHaveBeenCalledWith(`downloadMedia${URL}`, expect.any(Function)); + }); + }); + + describe('tryAutoDownload', () => { + it('downloads when the auto-download preference is enabled', async () => { + mockFetchAutoDownloadEnabled.mockReturnValue(true); + const { result } = renderMediaHook({ file: { image_url: '/img' } }); + await waitFor(() => expect(mockDownloadMediaFile).toHaveBeenCalled()); + await waitFor(() => expect(result.current.status).toBe('downloaded')); + expect(result.current.currentFile.title_link).toBe('file://downloaded'); + }); + + it('downloads when the author is the current user even if auto-download is disabled', async () => { + renderMediaHook({ file: { image_url: '/img' }, author: { _id: 'user-1' } as IUserMessage }); + await waitFor(() => expect(mockDownloadMediaFile).toHaveBeenCalled()); + }); + + it('stays in to-download when auto-download is disabled and the author is someone else', async () => { + const { result } = renderMediaHook({ file: { image_url: '/img' }, author: { _id: 'other' } as IUserMessage }); + await flushMount(); + expect(mockFetchAutoDownloadEnabled).toHaveBeenCalled(); + expect(mockDownloadMediaFile).not.toHaveBeenCalled(); + expect(result.current.status).toBe('to-download'); + }); + + it('returns to to-download when the download fails', async () => { + mockFetchAutoDownloadEnabled.mockReturnValue(true); + mockDownloadMediaFile.mockRejectedValue(new Error('boom')); + const { result } = renderMediaHook({ file: { image_url: '/img' } }); + await waitFor(() => expect(mockDownloadMediaFile).toHaveBeenCalled()); + await waitFor(() => expect(result.current.status).toBe('to-download')); + }); + }); + + describe('onPress', () => { + it('cancels the download and returns to to-download while loading', async () => { + mockIsDownloadActive.mockReturnValue(true); + const { result } = renderMediaHook({ file: { image_url: '/img' } }); + await waitFor(() => expect(result.current.status).toBe('loading')); + + act(() => result.current.onPress()); + expect(mockCancelDownload).toHaveBeenCalledWith(URL); + expect(result.current.status).toBe('to-download'); + }); + + it('starts a download when pressed in the to-download state', async () => { + const { result } = renderMediaHook({ file: { image_url: '/img' } }); + await flushMount(); + expect(result.current.status).toBe('to-download'); + expect(mockDownloadMediaFile).not.toHaveBeenCalled(); + + await act(async () => result.current.onPress()); + expect(mockDownloadMediaFile).toHaveBeenCalledTimes(1); + }); + + it('opens the attachment when downloaded, unlocked and openable', async () => { + mockGetMediaCache.mockResolvedValue({ exists: true, uri: 'file://cache' }); + const showAttachment = jest.fn(); + const { result } = renderMediaHook({ file: { image_url: '/img' }, showAttachment }); + await waitFor(() => expect(result.current.status).toBe('downloaded')); + + act(() => result.current.onPress()); + expect(showAttachment).toHaveBeenCalledWith(expect.objectContaining({ title_link: 'file://cache' })); + }); + + it('does not open the attachment when there is no showAttachment handler', async () => { + mockGetMediaCache.mockResolvedValue({ exists: true, uri: 'file://cache' }); + const { result } = renderMediaHook({ file: { image_url: '/img' } }); + await waitFor(() => expect(result.current.status).toBe('downloaded')); + + act(() => result.current.onPress()); + expect(result.current.status).toBe('downloaded'); + }); + + it('does not open the attachment while it is still encrypted', async () => { + mockIsDownloadActive.mockReturnValue(true); + const showAttachment = jest.fn(); + const { result } = renderMediaHook({ file: { image_url: '/img', e2e: 'pending' }, showAttachment }); + await waitFor(() => expect(result.current.status).toBe('loading')); + + act(() => emitter.emit(`downloadMedia${URL}`, 'file://encrypted')); + await waitFor(() => expect(result.current.status).toBe('downloaded')); + expect(result.current.isEncrypted).toBe(true); + + act(() => result.current.onPress()); + expect(showAttachment).not.toHaveBeenCalled(); + }); + }); +}); From 7a058b1f18547a42297b356c277a494fcc55e683 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 16:10:59 -0300 Subject: [PATCH 065/144] refactor: tighten message module TypeScript types Replace Function/any with concrete signatures, drop @ts-ignore/as Function casts now that underlying types align, and use indexed-access types for TInfoMessage. --- app/containers/ThreadDetails.tsx | 4 ++-- app/containers/message/components/Content.tsx | 1 - app/containers/message/components/Thread.tsx | 2 +- .../__tests__/useMediaAutoDownload.test.tsx | 2 +- .../message/hooks/useMediaAutoDownload.tsx | 2 +- .../hooks/useMessageAccessibilityLabel.ts | 1 - .../message/stores/MessageRoomStore.tsx | 12 ++++++------ app/containers/message/stores/MessageStore.tsx | 2 -- app/containers/message/testHelpers.tsx | 2 +- app/containers/message/utils.ts | 16 ++++++++-------- 10 files changed, 20 insertions(+), 24 deletions(-) diff --git a/app/containers/ThreadDetails.tsx b/app/containers/ThreadDetails.tsx index 08c6ce78005..8eaa1e3a322 100644 --- a/app/containers/ThreadDetails.tsx +++ b/app/containers/ThreadDetails.tsx @@ -46,7 +46,7 @@ interface IThreadDetails { id: string; }; badgeColor?: string; - toggleFollowThread: Function; + toggleFollowThread?: (isFollowingThread: boolean, tmid: string) => Promise<void> | void; style: ViewStyle; } @@ -62,7 +62,7 @@ const ThreadDetails = ({ item, user, badgeColor, toggleFollowThread, style }: IT replies = '+999'; } - const isFollowing = item.replies?.find((u: string) => u === user?.id); + const isFollowing = !!item.replies?.find((u: string) => u === user?.id); return ( <View style={[styles.container, style]}> diff --git a/app/containers/message/components/Content.tsx b/app/containers/message/components/Content.tsx index 56fabaf1087..db9ae9a2e21 100644 --- a/app/containers/message/components/Content.tsx +++ b/app/containers/message/components/Content.tsx @@ -50,7 +50,6 @@ const Content = () => { const isIgnored = useMessageIgnored(); if (isInfo) { - // @ts-ignore const infoMessage = getInfoMessage({ type, role, msg: messageText, author, comment }); const renderMessageContent = ( diff --git a/app/containers/message/components/Thread.tsx b/app/containers/message/components/Thread.tsx index ac7da945808..cf1740f1ae9 100644 --- a/app/containers/message/components/Thread.tsx +++ b/app/containers/message/components/Thread.tsx @@ -47,7 +47,7 @@ const Thread = () => { }} user={{ id: user?.id ?? '' }} badgeColor={threadBadgeColor} - toggleFollowThread={toggleFollowThread as Function} + toggleFollowThread={toggleFollowThread} style={styles.threadDetails} /> </View> diff --git a/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx b/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx index 8295ec2b3fb..496a6f588d1 100644 --- a/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx +++ b/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx @@ -89,7 +89,7 @@ const renderMediaHook = ({ }: { file: IAttachment; author?: IUserMessage; - showAttachment?: Function; + showAttachment?: (file: IAttachment) => void; ctx?: Record<string, any>; }) => { const contextValue = { baseUrl: 'https://open.rocket.chat', user: USER, ...ctx }; diff --git a/app/containers/message/hooks/useMediaAutoDownload.tsx b/app/containers/message/hooks/useMediaAutoDownload.tsx index 0b0ce4771da..88e98eedbfa 100644 --- a/app/containers/message/hooks/useMediaAutoDownload.tsx +++ b/app/containers/message/hooks/useMediaAutoDownload.tsx @@ -56,7 +56,7 @@ export const useMediaAutoDownload = ({ }: { file: IAttachment; author?: IUserMessage; - showAttachment?: Function; + showAttachment?: (file: IAttachment) => void; }) => { 'use memo'; diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.ts b/app/containers/message/hooks/useMessageAccessibilityLabel.ts index b29ba47183e..3730ee4d1f8 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.ts +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.ts @@ -58,7 +58,6 @@ export const useMessageAccessibilityLabel = (): string => { label = i18n.t('Encrypted_message'); } if (isInfo) { - // @ts-ignore label = getInfoMessage({ type, role, msg, author, comment }); } label = stripMentions(label, mentions, channels); diff --git a/app/containers/message/stores/MessageRoomStore.tsx b/app/containers/message/stores/MessageRoomStore.tsx index 5c0df28b4c8..3bf1ab931ec 100644 --- a/app/containers/message/stores/MessageRoomStore.tsx +++ b/app/containers/message/stores/MessageRoomStore.tsx @@ -17,7 +17,7 @@ export type MessageRoomState = { fetchThreadName?: (tmid: string, id: string) => Promise<string | undefined>; toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise<void>; jumpToMessage?: (link: string) => void; - closeEmojiAndAction?: (action?: Function, params?: any) => void; + closeEmojiAndAction?: (action?: (params?: unknown) => void, params?: unknown) => void; // row action handlers onReactionPress?: (emoji: string, id: string) => void; onReactionLongPress?: (item: TAnyMessageModel) => void; @@ -26,7 +26,7 @@ export type MessageRoomState = { onThreadPress?: (item: TAnyMessageModel) => void; replyBroadcast?: (item: TAnyMessageModel) => void; errorActionsShow?: (item: TAnyMessageModel) => void; - onAnswerButtonPress?: Function; + onAnswerButtonPress?: (message: string, tshow?: boolean) => void; onEncryptedPress?: () => void; archived?: boolean; isReadReceiptEnabled?: boolean; @@ -78,14 +78,14 @@ const ROOM_STATE_KEYS: (keyof MessageRoomState)[] = [ 'autoTranslateLanguage' ]; -export const pickMessageRoomState = (source: Record<string, any> = {}): Partial<MessageRoomState> => { - const state: Partial<MessageRoomState> = {}; +export const pickMessageRoomState = (source: Record<string, unknown> = {}): Partial<MessageRoomState> => { + const state: Record<string, unknown> = {}; ROOM_STATE_KEYS.forEach(key => { if (source[key] !== undefined) { - (state as Record<string, unknown>)[key] = source[key]; + state[key] = source[key]; } }); - return state; + return state as Partial<MessageRoomState>; }; export const createMessageRoomStore = (initial: MessageRoomState) => createStore<MessageRoomState>(() => ({ ...initial })); diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index 15c6ec9d1af..4edee399d76 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -66,9 +66,7 @@ export const useMessageField = <T,>(selector: (item: TAnyMessageModel) => T): T // Plain REST objects (no experimentalSubscribe) never emit again after the initial render. const subscribeModel = (m: TAnyMessageModel, store: MessageStore) => { - // @ts-ignore: experimentalSubscribe is not yet in WatermelonDB's TS types if (typeof m.experimentalSubscribe !== 'function') return undefined; - // @ts-ignore: experimentalSubscribe is not yet in WatermelonDB's TS types return m.experimentalSubscribe(() => store.setState(s => ({ tick: s.tick + 1 }))); }; diff --git a/app/containers/message/testHelpers.tsx b/app/containers/message/testHelpers.tsx index d34ecd40dd9..0da033e768f 100644 --- a/app/containers/message/testHelpers.tsx +++ b/app/containers/message/testHelpers.tsx @@ -10,7 +10,7 @@ import { MessageRoomProvider, pickMessageRoomState, type MessageRoomState } from interface IMessageProvidersOptions { item?: TAnyMessageModel; previousItem?: TAnyMessageModel; - context?: Record<string, any>; + context?: Record<string, unknown>; room?: Partial<MessageRoomState>; withRedux?: boolean; } diff --git a/app/containers/message/utils.ts b/app/containers/message/utils.ts index 08dceb44947..f01367d54de 100644 --- a/app/containers/message/utils.ts +++ b/app/containers/message/utils.ts @@ -1,6 +1,6 @@ /* eslint-disable complexity */ -import { type IAttachment } from '../../definitions'; -import { type MessageTypesValues, type TMessageModel } from '../../definitions/IMessage'; +import { type IAttachment, type IMessageTranslations } from '../../definitions'; +import { type MessageTypesValues, type TAnyMessageModel, type TMessageModel } from '../../definitions/IMessage'; import I18n from '../../i18n'; import { DISCUSSION } from './constants'; @@ -82,11 +82,11 @@ const messagesWithAuthorName: MessageTypesValues[] = [ export const messageHaveAuthorName = (type: MessageTypesValues): boolean => messagesWithAuthorName.includes(type); type TInfoMessage = { - type: MessageTypesValues; - role: string; - msg: string; - author: { username: string }; - comment?: string; + type: TAnyMessageModel['t']; + role: TAnyMessageModel['role']; + msg: TAnyMessageModel['msg']; + author: TAnyMessageModel['u']; + comment?: TAnyMessageModel['comment']; }; export const getInfoMessage = ({ type, role, msg, author, comment }: TInfoMessage): string => { @@ -198,7 +198,7 @@ export const getMessageTranslation = ( } const { translations } = message; if (translations) { - const translation = translations.find((trans: any) => trans.language === autoTranslateLanguage); + const translation = translations.find((trans: IMessageTranslations) => trans.language === autoTranslateLanguage); return translation?.value || null; } return null; From 00e9bbe809526459464d6c121309cdff28306580 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 18:47:43 -0300 Subject: [PATCH 066/144] refactor: replace useMessagePress hand-rolled debounce with useDebounce --- .../message/stores/MessageStore.tsx | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index 4edee399d76..ba77fe3ba2e 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useEffect, useRef, useState, type ReactElement, type ReactNode } from 'react'; +import { createContext, useContext, useEffect, useState, type ReactElement, type ReactNode } from 'react'; import { createStore, useStore } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; import { Keyboard } from 'react-native'; @@ -7,7 +7,7 @@ import { type IAttachment, type TAnyMessageModel } from '../../../definitions'; import { getMessageTranslation } from '../utils'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../../lib/constants/keys'; import { messagesStatus } from '../../../lib/constants/messagesStatus'; -import { debounce } from '../../../lib/methods/helpers'; +import { useDebounce } from '../../../lib/methods/helpers/debounce'; import openLink from '../../../lib/methods/helpers/openLink'; import { useTheme } from '../../../theme'; import { @@ -359,11 +359,8 @@ export const useMessagePress = (): (() => void) => { const isIgnored = useMessageIgnored(); const revealIgnored = useRevealIgnored(); - // pressHandlerRef holds the latest press logic; updated after every render via - // useEffect so the render body stays free of ref writes. - const pressHandlerRef = useRef<() => void>(() => {}); - useEffect(() => { - pressHandlerRef.current = () => { + const handlePress = useDebounce( + () => { if (isIgnored) { return revealIgnored(); } @@ -377,20 +374,15 @@ export const useMessagePress = (): (() => void) => { if (item.dlm && onDiscussionPress) { onDiscussionPress(item); } - }; - }); - // onPressRef holds the single debounced instance for the component lifetime. - // Initialised to a noop; replaced with the real debounced fn in the first effect - // so that pressHandlerRef.current is never read during render. - const onPressRef = useRef(debounce(() => {}, 300, true)); - useEffect(() => { - onPressRef.current = debounce(() => pressHandlerRef.current?.(), 300, true); - }, []); + }, + 300, + { leading: true, trailing: false } + ); return () => { if (closeEmojiAndAction) { - return closeEmojiAndAction(onPressRef.current); + return closeEmojiAndAction(handlePress); } - return onPressRef.current(); + return handlePress(); }; }; From 2a821133b41472c9c045a95ce49a749994121b63 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 18:51:14 -0300 Subject: [PATCH 067/144] refactor: mechanical convention cleanup in message module --- .../message/components/Reactions.tsx | 34 ++++++++----------- app/containers/message/components/Urls.tsx | 24 ++++++------- app/containers/message/components/User.tsx | 4 +-- .../hooks/useMessageAccessibilityLabel.ts | 4 +-- .../message/stores/MessageStore.tsx | 18 +++------- .../stores/__tests__/MessageStore.test.tsx | 6 ++-- 6 files changed, 37 insertions(+), 53 deletions(-) diff --git a/app/containers/message/components/Reactions.tsx b/app/containers/message/components/Reactions.tsx index 727ed60faa8..104edc3a9ec 100644 --- a/app/containers/message/components/Reactions.tsx +++ b/app/containers/message/components/Reactions.tsx @@ -6,8 +6,7 @@ import { CustomIcon } from '../../CustomIcon'; import styles from '../styles'; import Emoji from './Emoji'; import { BUTTON_HIT_SLOP } from '../utils'; -import { themes } from '../../../lib/constants/colors'; -import { type TSupportedThemes, useTheme } from '../../../theme'; +import { useTheme } from '../../../theme'; import { useMessageCtx, useMessageId, useReactions } from '../stores/MessageStore'; import { useGetCustomEmoji, @@ -25,12 +24,12 @@ interface IReaction { interface IMessageReaction { reaction: IReaction; - theme: TSupportedThemes; } -const AddReaction = ({ theme }: { theme: TSupportedThemes }) => { +const AddReaction = () => { 'use memo'; + const { colors } = useTheme(); const reactionInit = useReactionInit(); const id = useMessageId(); const { fontScale } = useWindowDimensions(); @@ -42,19 +41,20 @@ const AddReaction = ({ theme }: { theme: TSupportedThemes }) => { testID='message-add-reaction' accessibilityRole='button' accessibilityLabel={I18n.t('Add_reaction')} - style={[styles.reactionButton, { backgroundColor: themes[theme].surfaceRoom }]} + style={[styles.reactionButton, { backgroundColor: colors.surfaceRoom }]} hitSlop={BUTTON_HIT_SLOP} - android_ripple={{ color: themes[theme].strokeLight }}> - <View style={[styles.reactionContainer, { borderColor: themes[theme].strokeLight, height }]}> - <CustomIcon name='reaction-add' size={20} color={themes[theme].badgeBackgroundLevel2} /> + android_ripple={{ color: colors.strokeLight }}> + <View style={[styles.reactionContainer, { borderColor: colors.strokeLight, height }]}> + <CustomIcon name='reaction-add' size={20} color={colors.badgeBackgroundLevel2} /> </View> </Touchable> ); }; -const Reaction = ({ reaction, theme }: IMessageReaction) => { +const Reaction = ({ reaction }: IMessageReaction) => { 'use memo'; + const { colors } = useTheme(); const { item } = useMessageCtx(); const id = useMessageId(); const onReactionPress = useOnReactionPress(); @@ -73,21 +73,18 @@ const Reaction = ({ reaction, theme }: IMessageReaction) => { accessibilityRole='button' accessibilityLabel={`${reaction.emoji}, ${reaction.usernames.length}`} accessibilityState={{ selected: reacted }} - style={[styles.reactionButton, { backgroundColor: reacted ? themes[theme].surfaceNeutral : themes[theme].surfaceRoom }]} + style={[styles.reactionButton, { backgroundColor: reacted ? colors.surfaceNeutral : colors.surfaceRoom }]} hitSlop={BUTTON_HIT_SLOP} - android_ripple={{ color: themes[theme].strokeLight }}> + android_ripple={{ color: colors.strokeLight }}> <View - style={[ - styles.reactionContainer, - { borderColor: reacted ? themes[theme].badgeBackgroundLevel2 : themes[theme].strokeLight, height } - ]}> + style={[styles.reactionContainer, { borderColor: reacted ? colors.badgeBackgroundLevel2 : colors.strokeLight, height }]}> <Emoji content={reaction.emoji} standardEmojiStyle={styles.reactionEmoji} customEmojiStyle={styles.reactionCustomEmoji} getCustomEmoji={getCustomEmoji ?? (() => null)} /> - <Text style={[styles.reactionCount, { color: themes[theme].badgeBackgroundLevel2 }]}>{reaction.usernames.length}</Text> + <Text style={[styles.reactionCount, { color: colors.badgeBackgroundLevel2 }]}>{reaction.usernames.length}</Text> </View> </Touchable> ); @@ -96,7 +93,6 @@ const Reaction = ({ reaction, theme }: IMessageReaction) => { const Reactions = () => { 'use memo'; - const { theme } = useTheme(); const reactions = useReactions(); if (!Array.isArray(reactions) || reactions.length === 0) { @@ -105,9 +101,9 @@ const Reactions = () => { return ( <View style={styles.reactionsContainer}> {reactions.map(reaction => ( - <Reaction key={reaction.emoji} reaction={reaction} theme={theme} /> + <Reaction key={reaction.emoji} reaction={reaction} /> ))} - <AddReaction theme={theme} /> + <AddReaction /> </View> ); }; diff --git a/app/containers/message/components/Urls.tsx b/app/containers/message/components/Urls.tsx index 1d178e88910..6afbe1c80bd 100644 --- a/app/containers/message/components/Urls.tsx +++ b/app/containers/message/components/Urls.tsx @@ -1,10 +1,9 @@ -import { useCallback, useContext, useEffect, useLayoutEffect, useState, type ReactElement } from 'react'; +import { useContext, useEffect, useLayoutEffect, useState, type ReactElement } from 'react'; import { StyleSheet, Text, View, type ViewStyle } from 'react-native'; import Clipboard from '@react-native-clipboard/clipboard'; import { Image } from 'expo-image'; import axios from 'axios'; -import { useAppSelector } from '../../../lib/hooks/useAppSelector'; import Touchable from './Touchable'; import openLink from '../../../lib/methods/helpers/openLink'; import sharedStyles from '../../../views/Styles'; @@ -16,6 +15,7 @@ import { type IUrl } from '../../../definitions'; import { WidthAwareContext } from './WidthAwareView'; import { useUrls } from '../stores/MessageStore'; import { useBaseUrl, useMessageUser } from '../stores/MessageRoomStore'; +import { useSetting } from '../../../lib/hooks/useSetting'; const styles = StyleSheet.create({ container: { @@ -123,22 +123,18 @@ const Url = ({ url }: { url: IUrl }) => { const { colors, theme } = useTheme(); const baseUrl = useBaseUrl(); const user = useMessageUser(); - const API_Embed = useAppSelector(state => state.settings.API_Embed); + const API_Embed = useSetting('API_Embed') as boolean; const [imageUrl, setImageUrl] = useState<string | null>(null); - const getImageUrl = useCallback(() => { - const _imageUrl = url.image || url.url; - - if (!_imageUrl) return null; - if (_imageUrl.startsWith('http')) return _imageUrl; - return `${baseUrl}/${_imageUrl}?rc_uid=${user?.id ?? ''}&rc_token=${user?.token ?? ''}`; - }, [url.image, url.url, baseUrl, user?.id, user?.token]); - useEffect(() => { const verifyUrlIsImage = async () => { try { - const _imageUrl = getImageUrl(); - if (!_imageUrl || !API_Embed) return; + const rawImageUrl = url.image || url.url; + if (!rawImageUrl || !API_Embed) return; + + const _imageUrl = rawImageUrl.startsWith('http') + ? rawImageUrl + : `${baseUrl}/${rawImageUrl}?rc_uid=${user?.id ?? ''}&rc_token=${user?.token ?? ''}`; const response = await axios.head(_imageUrl); const contentType = response.headers['content-type']; @@ -150,7 +146,7 @@ const Url = ({ url }: { url: IUrl }) => { } }; verifyUrlIsImage(); - }, [url.image, url.url, API_Embed, getImageUrl]); + }, [url.image, url.url, API_Embed, baseUrl, user?.id, user?.token]); const onPress = () => openLink(url.url, theme); diff --git a/app/containers/message/components/User.tsx b/app/containers/message/components/User.tsx index 04bd97edab7..681965ce4f6 100644 --- a/app/containers/message/components/User.tsx +++ b/app/containers/message/components/User.tsx @@ -8,7 +8,7 @@ import { messageHaveAuthorName } from '../utils'; import MessageTime from './Time'; import { useResponsiveLayout } from '../../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; import { useSetting } from '../../../lib/hooks/useSetting'; -import { useMessageAuthor, useMessageGrouping, useMessageMeta } from '../stores/MessageStore'; +import { useMessageAuthor, useMessageGrouping, useMessageHeaderMeta } from '../stores/MessageStore'; import { useMessageUser, useNavToRoomInfo } from '../stores/MessageRoomStore'; const styles = StyleSheet.create({ @@ -50,7 +50,7 @@ const User = () => { const { isLargeFontScale } = useResponsiveLayout(); const isHeader = useMessageGrouping(); const { u: author, alias } = useMessageAuthor(); - const { t: type } = useMessageMeta(); + const { t: type } = useMessageHeaderMeta(); if (isHeader) { const username = (useRealName && author?.name) || author?.username; diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.ts b/app/containers/message/hooks/useMessageAccessibilityLabel.ts index 3730ee4d1f8..c9874d37b73 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.ts +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.ts @@ -8,7 +8,7 @@ import { useIsEncrypted, useIsInfo, useMessageAuthor, - useMessageMeta, + useMessageHeaderMeta, useMessageText, useThreadData, useThreadPosition @@ -41,7 +41,7 @@ export const useMessageAccessibilityLabel = (): string => { const { isThreadReply, isThreadSequential } = useThreadPosition(); const isInfo = useIsInfo(); const isEncrypted = useIsEncrypted(); - const { ts, unread } = useMessageMeta(); + const { ts, unread } = useMessageHeaderMeta(); const isReadReceiptEnabled = useIsReadReceiptEnabled(); const imageDescriptionLabel = useImageDescriptionLabel(attachments, messageText); diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index ba77fe3ba2e..33ce38c2270 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -178,7 +178,7 @@ export const useContentData = (): Pick<TAnyMessageModel, 'md' | 'mentions' | 'ch ); }; -export const useMessageMeta = (): Pick<TAnyMessageModel, 'ts' | 'unread' | 'pinned' | 't'> => { +export const useMessageHeaderMeta = (): Pick<TAnyMessageModel, 'ts' | 'unread' | 'pinned' | 't'> => { const { store, item } = useMessageCtx(); return useStore( store, @@ -299,14 +299,6 @@ export const useMessageText = (): { messageText: TAnyMessageModel['msg']; isTran ); }; -export const useOnPressRaw = (): MessageStoreState['onPress'] => { - const { store } = useMessageCtx(); - return useStore(store, s => s.onPress); -}; -export const useOnLongPressRaw = (): MessageStoreState['onLongPress'] => { - const { store } = useMessageCtx(); - return useStore(store, s => s.onLongPress); -}; export const useThreadBadgeColor = (): string | undefined => { const { store } = useMessageCtx(); return useStore(store, s => s.threadBadgeColor); @@ -322,12 +314,12 @@ export const useRevealIgnored = (): (() => void) => { }; export const useMessageLongPress = (): (() => void) => { - const { item } = useMessageCtx(); + const { item, store } = useMessageCtx(); const isInfo = useIsInfo(); const { hasError } = useMessageStatus(); const isEncrypted = useIsEncrypted(); const archived = useArchived(); - const onLongPress = useOnLongPressRaw(); + const onLongPress = useStore(store, s => s.onLongPress); return () => { if (isInfo || hasError || isEncrypted || archived) { return; @@ -350,9 +342,9 @@ export const useOnLinkPress = (): ((link: string) => void) => { }; export const useMessagePress = (): (() => void) => { - const { item } = useMessageCtx(); + const { item, store } = useMessageCtx(); const isThreadRoom = useIsThreadRoom(); - const onPress = useOnPressRaw(); + const onPress = useStore(store, s => s.onPress); const onThreadPress = useOnThreadPress(); const onDiscussionPress = useOnDiscussionPress(); const closeEmojiAndAction = useCloseEmojiAndAction(); diff --git a/app/containers/message/stores/__tests__/MessageStore.test.tsx b/app/containers/message/stores/__tests__/MessageStore.test.tsx index a3610d6aaeb..9793c266786 100644 --- a/app/containers/message/stores/__tests__/MessageStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageStore.test.tsx @@ -21,7 +21,7 @@ import { useMessageField, useMessageGrouping, useMessageIgnored, - useMessageMeta, + useMessageHeaderMeta, useMessageStatus, useMessageText, useReactions, @@ -539,10 +539,10 @@ describe('MessageStore', () => { expect(latest()).toEqual({ messageText: 'Olá mundo', isTranslated: true }); }); - it('useMessageMeta returns ts, unread, pinned and t', () => { + it('useMessageHeaderMeta returns ts, unread, pinned and t', () => { const ts = new Date('2024-01-01T10:00:00Z'); const model = buildFakeModel({ ts, unread: true, pinned: true, t: 'room_changed_topic' }); - const { latest } = renderDerived(model, useMessageMeta); + const { latest } = renderDerived(model, useMessageHeaderMeta); expect(latest()).toEqual({ ts: model.ts, unread: model.unread, pinned: model.pinned, t: model.t }); }); }); From ec77896f5657c12c8007768ee78c44d971734426 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 19:09:02 -0300 Subject: [PATCH 068/144] refactor: decompose message Content into per-branch leaves --- app/containers/message/components/Content.tsx | 123 ++++-------------- .../components/Content/ContentWrapper.tsx | 22 ++++ .../components/Content/EncryptedContent.tsx | 28 ++++ .../components/Content/IgnoredContent.tsx | 27 ++++ .../components/Content/InfoContent.tsx | 40 ++++++ .../components/Content/MarkdownContent.tsx | 38 ++++++ .../components/Content/PreviewContent.tsx | 30 +++++ .../message/stores/MessageStore.tsx | 18 +++ 8 files changed, 226 insertions(+), 100 deletions(-) create mode 100644 app/containers/message/components/Content/ContentWrapper.tsx create mode 100644 app/containers/message/components/Content/EncryptedContent.tsx create mode 100644 app/containers/message/components/Content/IgnoredContent.tsx create mode 100644 app/containers/message/components/Content/InfoContent.tsx create mode 100644 app/containers/message/components/Content/MarkdownContent.tsx create mode 100644 app/containers/message/components/Content/PreviewContent.tsx diff --git a/app/containers/message/components/Content.tsx b/app/containers/message/components/Content.tsx index db9ae9a2e21..b89a133ca84 100644 --- a/app/containers/message/components/Content.tsx +++ b/app/containers/message/components/Content.tsx @@ -1,119 +1,42 @@ -import { Text, View } from 'react-native'; - -import I18n from '../../../i18n'; -import styles from '../styles'; -import Markdown, { MarkdownPreview } from '../../markdown'; -import User from './User'; -import { messageHaveAuthorName, getInfoMessage, getPreviewMessageFromAttachment } from '../utils'; -import { useTheme } from '../../../theme'; -import { themes } from '../../../lib/constants/colors'; -import { useSetting } from '../../../lib/hooks/useSetting'; -import { type MessageTypesValues } from '../../../definitions'; -import { - useContentData, - useIsEncrypted, - useIsInfo, - useMessageAuthor, - useMessageIgnored, - useMessageStatus, - useMessageText, - useOnLinkPress, - useThreadData -} from '../stores/MessageStore'; -import { - useAutoTranslate, - useGetCustomEmoji, - useIsThreadRoom, - useMessageUser, - useNavToRoomInfo -} from '../stores/MessageRoomStore'; +import EncryptedContent from './Content/EncryptedContent'; +import IgnoredContent from './Content/IgnoredContent'; +import InfoContent from './Content/InfoContent'; +import MarkdownContent from './Content/MarkdownContent'; +import PreviewContent from './Content/PreviewContent'; +import { useIsEncrypted, useIsInfo, useMessageIgnored, useMessageText, useThreadData } from '../stores/MessageStore'; +import { useIsThreadRoom } from '../stores/MessageRoomStore'; const Content = () => { 'use memo'; - const { theme } = useTheme(); - const useRealName = useSetting('UI_Use_Real_Name') as boolean; - const user = useMessageUser(); - const onLinkPress = useOnLinkPress(); - const getCustomEmoji = useGetCustomEmoji() ?? (() => null); - const navToRoomInfo = useNavToRoomInfo(); - const isThreadRoom = useIsThreadRoom(); - const { tmid } = useThreadData(); - const { autoTranslateLanguage } = useAutoTranslate(); - const isInfo = useIsInfo(); - const isEncrypted = useIsEncrypted(); - const { md, mentions, channels, comment, attachments, t: type } = useContentData(); - const { isTemp } = useMessageStatus(); - const { messageText, isTranslated } = useMessageText(); - const { u: author, role } = useMessageAuthor(); const isIgnored = useMessageIgnored(); + const isEncrypted = useIsEncrypted(); + const { tmid } = useThreadData(); + const isThreadRoom = useIsThreadRoom(); + const { messageText } = useMessageText(); if (isInfo) { - const infoMessage = getInfoMessage({ type, role, msg: messageText, author, comment }); - - const renderMessageContent = ( - <Text style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} accessibilityLabel={infoMessage}> - {infoMessage} - </Text> - ); - if (messageHaveAuthorName(type as MessageTypesValues)) { - return ( - <Text> - <User /> {renderMessageContent} - </Text> - ); - } - - return renderMessageContent; + return <InfoContent />; } - const isPreview = tmid && !isThreadRoom; - let content = null; + if (isIgnored) { + return <IgnoredContent />; + } if (isEncrypted) { - content = ( - <Text - style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} - accessibilityLabel={I18n.t('Encrypted_message')} - testID='message-encrypted'> - {I18n.t('Encrypted_message')} - </Text> - ); - } else if (isPreview) { - const previewMsg = - messageText || (attachments?.length ? getPreviewMessageFromAttachment(attachments[0], autoTranslateLanguage) : undefined); - content = previewMsg ? <MarkdownPreview testID={`message-preview-${previewMsg}`} msg={previewMsg} /> : null; - } else if (messageText) { - content = ( - <Markdown - msg={messageText} - md={type !== 'e2e' ? md : undefined} - getCustomEmoji={getCustomEmoji} - username={user?.username ?? ''} - channels={channels} - mentions={mentions} - navToRoomInfo={navToRoomInfo} - useRealName={useRealName} - onLinkPress={onLinkPress} - isTranslated={isTranslated} - /> - ); + return <EncryptedContent />; } - if (isIgnored) { - content = ( - <Text style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} testID={`message-ignored-${messageText}`}> - {I18n.t('Message_Ignored')} - </Text> - ); + if (tmid && !isThreadRoom) { + return <PreviewContent />; + } + + if (messageText) { + return <MarkdownContent />; } - return content ? ( - <View style={isTemp && styles.temp} testID={`message-content-${messageText || ''}`}> - {content} - </View> - ) : null; + return null; }; Content.displayName = 'MessageContent'; diff --git a/app/containers/message/components/Content/ContentWrapper.tsx b/app/containers/message/components/Content/ContentWrapper.tsx new file mode 100644 index 00000000000..0527b1d77ac --- /dev/null +++ b/app/containers/message/components/Content/ContentWrapper.tsx @@ -0,0 +1,22 @@ +import { type ReactNode } from 'react'; +import { View } from 'react-native'; + +import styles from '../../styles'; +import { useMessageStatus, useMessageText } from '../../stores/MessageStore'; + +const ContentWrapper = ({ children }: { children: ReactNode }) => { + 'use memo'; + + const { isTemp } = useMessageStatus(); + const { messageText } = useMessageText(); + + return ( + <View style={isTemp && styles.temp} testID={`message-content-${messageText || ''}`}> + {children} + </View> + ); +}; + +ContentWrapper.displayName = 'MessageContentWrapper'; + +export default ContentWrapper; diff --git a/app/containers/message/components/Content/EncryptedContent.tsx b/app/containers/message/components/Content/EncryptedContent.tsx new file mode 100644 index 00000000000..c42fd7bf300 --- /dev/null +++ b/app/containers/message/components/Content/EncryptedContent.tsx @@ -0,0 +1,28 @@ +import { Text } from 'react-native'; + +import I18n from '../../../../i18n'; +import styles from '../../styles'; +import { useTheme } from '../../../../theme'; +import { themes } from '../../../../lib/constants/colors'; +import ContentWrapper from './ContentWrapper'; + +const EncryptedContent = () => { + 'use memo'; + + const { theme } = useTheme(); + + return ( + <ContentWrapper> + <Text + style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} + accessibilityLabel={I18n.t('Encrypted_message')} + testID='message-encrypted'> + {I18n.t('Encrypted_message')} + </Text> + </ContentWrapper> + ); +}; + +EncryptedContent.displayName = 'MessageEncryptedContent'; + +export default EncryptedContent; diff --git a/app/containers/message/components/Content/IgnoredContent.tsx b/app/containers/message/components/Content/IgnoredContent.tsx new file mode 100644 index 00000000000..56ea81efe33 --- /dev/null +++ b/app/containers/message/components/Content/IgnoredContent.tsx @@ -0,0 +1,27 @@ +import { Text } from 'react-native'; + +import I18n from '../../../../i18n'; +import styles from '../../styles'; +import { useTheme } from '../../../../theme'; +import { themes } from '../../../../lib/constants/colors'; +import { useMessageText } from '../../stores/MessageStore'; +import ContentWrapper from './ContentWrapper'; + +const IgnoredContent = () => { + 'use memo'; + + const { theme } = useTheme(); + const { messageText } = useMessageText(); + + return ( + <ContentWrapper> + <Text style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} testID={`message-ignored-${messageText}`}> + {I18n.t('Message_Ignored')} + </Text> + </ContentWrapper> + ); +}; + +IgnoredContent.displayName = 'MessageIgnoredContent'; + +export default IgnoredContent; diff --git a/app/containers/message/components/Content/InfoContent.tsx b/app/containers/message/components/Content/InfoContent.tsx new file mode 100644 index 00000000000..b5ee2e88c07 --- /dev/null +++ b/app/containers/message/components/Content/InfoContent.tsx @@ -0,0 +1,40 @@ +import { Text } from 'react-native'; + +import styles from '../../styles'; +import { getInfoMessage, messageHaveAuthorName } from '../../utils'; +import { useTheme } from '../../../../theme'; +import { themes } from '../../../../lib/constants/colors'; +import { type MessageTypesValues } from '../../../../definitions'; +import { useInfoData, useMessageAuthor, useMessageText } from '../../stores/MessageStore'; +import User from '../User'; + +const InfoContent = () => { + 'use memo'; + + const { theme } = useTheme(); + const { t: type, comment } = useInfoData(); + const { u: author, role } = useMessageAuthor(); + const { messageText } = useMessageText(); + + const infoMessage = getInfoMessage({ type, role, msg: messageText, author, comment }); + + const renderMessageContent = ( + <Text style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} accessibilityLabel={infoMessage}> + {infoMessage} + </Text> + ); + + if (messageHaveAuthorName(type as MessageTypesValues)) { + return ( + <Text> + <User /> {renderMessageContent} + </Text> + ); + } + + return renderMessageContent; +}; + +InfoContent.displayName = 'MessageInfoContent'; + +export default InfoContent; diff --git a/app/containers/message/components/Content/MarkdownContent.tsx b/app/containers/message/components/Content/MarkdownContent.tsx new file mode 100644 index 00000000000..14cdcb4a4e3 --- /dev/null +++ b/app/containers/message/components/Content/MarkdownContent.tsx @@ -0,0 +1,38 @@ +import Markdown from '../../../markdown'; +import { useSetting } from '../../../../lib/hooks/useSetting'; +import { useMarkdownData, useMessageText, useOnLinkPress } from '../../stores/MessageStore'; +import { useGetCustomEmoji, useMessageUser, useNavToRoomInfo } from '../../stores/MessageRoomStore'; +import ContentWrapper from './ContentWrapper'; + +const MarkdownContent = () => { + 'use memo'; + + const useRealName = useSetting('UI_Use_Real_Name') as boolean; + const user = useMessageUser(); + const onLinkPress = useOnLinkPress(); + const getCustomEmoji = useGetCustomEmoji() ?? (() => null); + const navToRoomInfo = useNavToRoomInfo(); + const { md, mentions, channels, t: type } = useMarkdownData(); + const { messageText, isTranslated } = useMessageText(); + + return ( + <ContentWrapper> + <Markdown + msg={messageText} + md={type !== 'e2e' ? md : undefined} + getCustomEmoji={getCustomEmoji} + username={user?.username ?? ''} + channels={channels} + mentions={mentions} + navToRoomInfo={navToRoomInfo} + useRealName={useRealName} + onLinkPress={onLinkPress} + isTranslated={isTranslated} + /> + </ContentWrapper> + ); +}; + +MarkdownContent.displayName = 'MessageMarkdownContent'; + +export default MarkdownContent; diff --git a/app/containers/message/components/Content/PreviewContent.tsx b/app/containers/message/components/Content/PreviewContent.tsx new file mode 100644 index 00000000000..8541f9cb318 --- /dev/null +++ b/app/containers/message/components/Content/PreviewContent.tsx @@ -0,0 +1,30 @@ +import { MarkdownPreview } from '../../../markdown'; +import { getPreviewMessageFromAttachment } from '../../utils'; +import { useAttachments, useMessageText } from '../../stores/MessageStore'; +import { useAutoTranslate } from '../../stores/MessageRoomStore'; +import ContentWrapper from './ContentWrapper'; + +const PreviewContent = () => { + 'use memo'; + + const { messageText } = useMessageText(); + const attachments = useAttachments(); + const { autoTranslateLanguage } = useAutoTranslate(); + + const previewMsg = + messageText || (attachments?.length ? getPreviewMessageFromAttachment(attachments[0], autoTranslateLanguage) : undefined); + + if (!previewMsg) { + return null; + } + + return ( + <ContentWrapper> + <MarkdownPreview testID={`message-preview-${previewMsg}`} msg={previewMsg} /> + </ContentWrapper> + ); +}; + +PreviewContent.displayName = 'MessagePreviewContent'; + +export default PreviewContent; diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index 33ce38c2270..82ceb3f4987 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -178,6 +178,24 @@ export const useContentData = (): Pick<TAnyMessageModel, 'md' | 'mentions' | 'ch ); }; +export const useInfoData = (): Pick<TAnyMessageModel, 't' | 'comment'> => { + const { store, item } = useMessageCtx(); + return useStore( + store, + useShallow(() => ({ t: item.t, comment: item.comment })) + ); +}; + +export const useMarkdownData = (): Pick<TAnyMessageModel, 'md' | 'mentions' | 'channels' | 't'> => { + const { store, item } = useMessageCtx(); + return useStore( + store, + useShallow(() => ({ md: item.md, mentions: item.mentions, channels: item.channels, t: item.t })) + ); +}; + +export const useAttachments = (): TAnyMessageModel['attachments'] => useMessageField(item => item.attachments); + export const useMessageHeaderMeta = (): Pick<TAnyMessageModel, 'ts' | 'unread' | 'pinned' | 't'> => { const { store, item } = useMessageCtx(); return useStore( From 3aa109267bb1ab1d19b9b1b18d9081ddb2f1ace5 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 19:26:48 -0300 Subject: [PATCH 069/144] refactor: decompose message Message.tsx into per-branch renderers --- app/containers/message/components/Message.tsx | 170 +++--------------- .../components/Message/BlocksBranch.tsx | 23 +++ .../components/Message/CompactMessage.tsx | 60 +++++++ .../components/Message/DefaultBranch.tsx | 39 ++++ .../components/Message/DiscussionBranch.tsx | 19 ++ .../components/Message/JitsiBranch.tsx | 21 +++ .../components/Message/NormalMessage.tsx | 40 +++++ .../components/Message/PreviewBranch.tsx | 29 +++ .../message/components/MessageAvatar.tsx | 4 +- 9 files changed, 258 insertions(+), 147 deletions(-) create mode 100644 app/containers/message/components/Message/BlocksBranch.tsx create mode 100644 app/containers/message/components/Message/CompactMessage.tsx create mode 100644 app/containers/message/components/Message/DefaultBranch.tsx create mode 100644 app/containers/message/components/Message/DiscussionBranch.tsx create mode 100644 app/containers/message/components/Message/JitsiBranch.tsx create mode 100644 app/containers/message/components/Message/NormalMessage.tsx create mode 100644 app/containers/message/components/Message/PreviewBranch.tsx diff --git a/app/containers/message/components/Message.tsx b/app/containers/message/components/Message.tsx index c9791bca7f6..f8adbb3b3ae 100644 --- a/app/containers/message/components/Message.tsx +++ b/app/containers/message/components/Message.tsx @@ -1,44 +1,31 @@ -import { View, type ViewStyle, type AccessibilityActionEvent, type AccessibilityActionInfo } from 'react-native'; import { A11y } from 'react-native-a11y-order'; -import User from './User'; -import styles from '../styles'; -import RepliedThread from './RepliedThread'; -import MessageAvatar from './MessageAvatar'; -import Attachments from './Attachments'; -import Urls from './Urls'; -import Thread from './Thread'; -import Blocks from './Blocks'; -import Reactions from './Reactions'; -import Broadcast from './Broadcast'; -import Discussion from './Discussion'; -import Content from './Content'; -import CallButton from './CallButton'; import { useTheme } from '../../../theme'; -import RightIcons from './RightIcons'; import { WidthAwareView } from './WidthAwareView'; -import MessageTime from './Time'; import { useResponsiveLayout } from '../../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; -import Quote from './Attachments/Quote'; import Touch from './Touch'; +import BlocksBranch from './Message/BlocksBranch'; +import JitsiBranch from './Message/JitsiBranch'; +import DiscussionBranch from './Message/DiscussionBranch'; +import PreviewBranch from './Message/PreviewBranch'; +import DefaultBranch from './Message/DefaultBranch'; +import CompactMessage from './Message/CompactMessage'; +import NormalMessage from './Message/NormalMessage'; import { useLastFocusedMessageRef } from '../../../lib/a11y/useLastFocusedMessageRef'; import { useMessageAccessibilityLabel } from '../hooks/useMessageAccessibilityLabel'; import { useMessageAccessibilityActions } from '../hooks/useMessageAccessibilityActions'; import { useMessageAccessibilityHint } from '../hooks/useMessageAccessibilityHint'; import { useIsBeingEdited } from '../../../views/RoomView/InteractionStore'; -import { useArchived, useAutoTranslate } from '../stores/MessageRoomStore'; +import { useArchived } from '../stores/MessageRoomStore'; import { useBlocks, - useContentData, useIsInfo, - useMessageAuthor, useMessageField, useMessageGrouping, useMessageIgnored, useMessageLongPress, useMessagePress, useMessageStatus, - useMessageText, useThreadPosition } from '../stores/MessageStore'; @@ -47,151 +34,44 @@ type TMessageProps = { highlighted?: boolean; }; -interface IMessageA11y { - accessibilityActions?: AccessibilityActionInfo[]; - onAccessibilityAction?: (event: AccessibilityActionEvent) => void; - handleLongPress?: () => void; -} - -const MessageInner = (props: TMessageProps) => { +export const MessageInner = (props: TMessageProps) => { 'use memo'; const { isLargeFontScale } = useResponsiveLayout(); const isHeader = useMessageGrouping(); - const { t: type, attachments } = useContentData(); + const type = useMessageField(item => item.t); const { blocks } = useBlocks(); - const { u: author } = useMessageAuthor(); const showTimeLarge = isLargeFontScale && isHeader; - let content; - if (props.isPreview) { - content = ( - <> - <User /> - {showTimeLarge ? <MessageTime /> : null} - <> - <Quote attachments={attachments} /> - <Content /> - <Attachments attachments={attachments} author={author} /> - </> - <Urls /> - </> - ); - } - - if (type === 'discussion-created') { - content = ( - <> - <User /> - {showTimeLarge ? <MessageTime /> : null} - <Discussion /> - </> - ); - } - - if (type === 'jitsi_call_started') { - content = ( - <> - <User /> - <Content /> - <CallButton /> - {showTimeLarge ? <MessageTime /> : null} - </> - ); - } - + let branch; if (blocks && blocks.length) { - content = ( - <> - <User /> - <Blocks /> - <Thread /> - <Reactions /> - {showTimeLarge ? <MessageTime /> : null} - </> - ); - } - - if (!content) { - content = ( - <> - <User /> - {showTimeLarge ? <MessageTime /> : null} - <View style={{ gap: 4 }}> - <Quote attachments={attachments} /> - <Content /> - <Attachments attachments={attachments} author={author} /> - <Urls /> - <Thread /> - <Reactions /> - <Broadcast /> - </View> - </> - ); + branch = <BlocksBranch showTimeLarge={showTimeLarge} />; + } else if (type === 'jitsi_call_started') { + branch = <JitsiBranch showTimeLarge={showTimeLarge} />; + } else if (type === 'discussion-created') { + branch = <DiscussionBranch showTimeLarge={showTimeLarge} />; + } else if (props.isPreview) { + branch = <PreviewBranch showTimeLarge={showTimeLarge} />; + } else { + branch = <DefaultBranch showTimeLarge={showTimeLarge} />; } - return <WidthAwareView>{content}</WidthAwareView>; + return <WidthAwareView>{branch}</WidthAwareView>; }; MessageInner.displayName = 'MessageInner'; -const Message = (props: TMessageProps & IMessageA11y) => { +const Message = (props: TMessageProps) => { 'use memo'; - const isHeader = useMessageGrouping(); const { isThreadReply, isThreadSequential } = useThreadPosition(); const isInfo = useIsInfo(); - const { messageText, isTranslated } = useMessageText(); - const { t: type, attachments } = useContentData(); - const { u: author } = useMessageAuthor(); - const id = useMessageField(item => item.id); - const { autoTranslateLanguage } = useAutoTranslate(); const isIgnored = useMessageIgnored(); if (isThreadReply || isThreadSequential || isInfo || isIgnored) { - const thread = isThreadReply ? <RepliedThread isHeader={isHeader} /> : null; - const infoStyle: ViewStyle = isInfo ? { alignItems: 'center' } : {}; - return ( - <View style={[styles.container, { marginTop: 4 }]}> - {thread} - <View style={[styles.flex, infoStyle]}> - <MessageAvatar small /> - <A11y.Index - accessible={isTranslated} - accessibilityLabel={messageText || ''} - accessibilityLanguage={autoTranslateLanguage} - index={2} - style={{ flex: 1 }}> - <View style={styles.messageContent}> - <Content /> - {isInfo && type === 'message_pinned' ? ( - <View pointerEvents='none'> - <Attachments attachments={attachments} author={author} /> - </View> - ) : null} - </View> - </A11y.Index> - </View> - </View> - ); + return <CompactMessage />; } - return ( - <View testID={`message-${id}`} style={styles.container}> - <A11y.Index - accessible={isTranslated} - accessibilityLabel={messageText || ''} - accessibilityLanguage={autoTranslateLanguage} - index={2}> - <View style={styles.flex}> - <MessageAvatar /> - <View style={styles.messageContent}> - <MessageInner isPreview={props.isPreview} /> - </View> - {!isHeader ? <RightIcons /> : null} - </View> - </A11y.Index> - </View> - ); + return <NormalMessage isPreview={props.isPreview} />; }; Message.displayName = 'Message'; @@ -253,7 +133,7 @@ const MessageTouchable = (props: TMessageProps) => { onAccessibilityAction={e => { if (e.nativeEvent.actionName === 'showActions') handleLongPress(); }}> - <Message isPreview={props.isPreview} handleLongPress={!isDisabled ? handleLongPress : undefined} /> + <Message isPreview={props.isPreview} /> </Touch> </A11y.Index> </A11y.Order> diff --git a/app/containers/message/components/Message/BlocksBranch.tsx b/app/containers/message/components/Message/BlocksBranch.tsx new file mode 100644 index 00000000000..1cd5527624d --- /dev/null +++ b/app/containers/message/components/Message/BlocksBranch.tsx @@ -0,0 +1,23 @@ +import User from '../User'; +import Blocks from '../Blocks'; +import Thread from '../Thread'; +import Reactions from '../Reactions'; +import MessageTime from '../Time'; + +const BlocksBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { + 'use memo'; + + return ( + <> + <User /> + <Blocks /> + <Thread /> + <Reactions /> + {showTimeLarge ? <MessageTime /> : null} + </> + ); +}; + +BlocksBranch.displayName = 'MessageBlocksBranch'; + +export default BlocksBranch; diff --git a/app/containers/message/components/Message/CompactMessage.tsx b/app/containers/message/components/Message/CompactMessage.tsx new file mode 100644 index 00000000000..76bc93f93b6 --- /dev/null +++ b/app/containers/message/components/Message/CompactMessage.tsx @@ -0,0 +1,60 @@ +import { View, type ViewStyle } from 'react-native'; +import { A11y } from 'react-native-a11y-order'; + +import styles from '../../styles'; +import RepliedThread from '../RepliedThread'; +import MessageAvatar from '../MessageAvatar'; +import Content from '../Content'; +import Attachments from '../Attachments'; +import { + useContentData, + useIsInfo, + useMessageAuthor, + useMessageGrouping, + useMessageText, + useThreadPosition +} from '../../stores/MessageStore'; +import { useAutoTranslate } from '../../stores/MessageRoomStore'; + +const CompactMessage = () => { + 'use memo'; + + const isHeader = useMessageGrouping(); + const { isThreadReply } = useThreadPosition(); + const isInfo = useIsInfo(); + const { messageText, isTranslated } = useMessageText(); + const { t: type, attachments } = useContentData(); + const { u: author } = useMessageAuthor(); + const { autoTranslateLanguage } = useAutoTranslate(); + + const thread = isThreadReply ? <RepliedThread isHeader={isHeader} /> : null; + const infoStyle: ViewStyle = isInfo ? { alignItems: 'center' } : {}; + + return ( + <View style={[styles.container, { marginTop: 4 }]}> + {thread} + <View style={[styles.flex, infoStyle]}> + <MessageAvatar small /> + <A11y.Index + accessible={isTranslated} + accessibilityLabel={messageText || ''} + accessibilityLanguage={autoTranslateLanguage} + index={2} + style={{ flex: 1 }}> + <View style={styles.messageContent}> + <Content /> + {isInfo && type === 'message_pinned' ? ( + <View pointerEvents='none'> + <Attachments attachments={attachments} author={author} /> + </View> + ) : null} + </View> + </A11y.Index> + </View> + </View> + ); +}; + +CompactMessage.displayName = 'MessageCompact'; + +export default CompactMessage; diff --git a/app/containers/message/components/Message/DefaultBranch.tsx b/app/containers/message/components/Message/DefaultBranch.tsx new file mode 100644 index 00000000000..5df1ceaf317 --- /dev/null +++ b/app/containers/message/components/Message/DefaultBranch.tsx @@ -0,0 +1,39 @@ +import { View } from 'react-native'; + +import User from '../User'; +import Content from '../Content'; +import Attachments from '../Attachments'; +import Quote from '../Attachments/Quote'; +import Urls from '../Urls'; +import Thread from '../Thread'; +import Reactions from '../Reactions'; +import Broadcast from '../Broadcast'; +import MessageTime from '../Time'; +import { useAttachments, useMessageAuthor } from '../../stores/MessageStore'; + +const DefaultBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { + 'use memo'; + + const attachments = useAttachments(); + const { u: author } = useMessageAuthor(); + + return ( + <> + <User /> + {showTimeLarge ? <MessageTime /> : null} + <View style={{ gap: 4 }}> + <Quote attachments={attachments} /> + <Content /> + <Attachments attachments={attachments} author={author} /> + <Urls /> + <Thread /> + <Reactions /> + <Broadcast /> + </View> + </> + ); +}; + +DefaultBranch.displayName = 'MessageDefaultBranch'; + +export default DefaultBranch; diff --git a/app/containers/message/components/Message/DiscussionBranch.tsx b/app/containers/message/components/Message/DiscussionBranch.tsx new file mode 100644 index 00000000000..21f12f86a6d --- /dev/null +++ b/app/containers/message/components/Message/DiscussionBranch.tsx @@ -0,0 +1,19 @@ +import User from '../User'; +import Discussion from '../Discussion'; +import MessageTime from '../Time'; + +const DiscussionBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { + 'use memo'; + + return ( + <> + <User /> + {showTimeLarge ? <MessageTime /> : null} + <Discussion /> + </> + ); +}; + +DiscussionBranch.displayName = 'MessageDiscussionBranch'; + +export default DiscussionBranch; diff --git a/app/containers/message/components/Message/JitsiBranch.tsx b/app/containers/message/components/Message/JitsiBranch.tsx new file mode 100644 index 00000000000..395ec857a3e --- /dev/null +++ b/app/containers/message/components/Message/JitsiBranch.tsx @@ -0,0 +1,21 @@ +import User from '../User'; +import Content from '../Content'; +import CallButton from '../CallButton'; +import MessageTime from '../Time'; + +const JitsiBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { + 'use memo'; + + return ( + <> + <User /> + <Content /> + <CallButton /> + {showTimeLarge ? <MessageTime /> : null} + </> + ); +}; + +JitsiBranch.displayName = 'MessageJitsiBranch'; + +export default JitsiBranch; diff --git a/app/containers/message/components/Message/NormalMessage.tsx b/app/containers/message/components/Message/NormalMessage.tsx new file mode 100644 index 00000000000..c2ac394c362 --- /dev/null +++ b/app/containers/message/components/Message/NormalMessage.tsx @@ -0,0 +1,40 @@ +import { View } from 'react-native'; +import { A11y } from 'react-native-a11y-order'; + +import styles from '../../styles'; +import MessageAvatar from '../MessageAvatar'; +import RightIcons from '../RightIcons'; +import { MessageInner } from '../Message'; +import { useMessageField, useMessageGrouping, useMessageText } from '../../stores/MessageStore'; +import { useAutoTranslate } from '../../stores/MessageRoomStore'; + +const NormalMessage = ({ isPreview }: { isPreview?: boolean }) => { + 'use memo'; + + const isHeader = useMessageGrouping(); + const { messageText, isTranslated } = useMessageText(); + const { autoTranslateLanguage } = useAutoTranslate(); + const id = useMessageField(item => item.id); + + return ( + <View testID={`message-${id}`} style={styles.container}> + <A11y.Index + accessible={isTranslated} + accessibilityLabel={messageText || ''} + accessibilityLanguage={autoTranslateLanguage} + index={2}> + <View style={styles.flex}> + <MessageAvatar /> + <View style={styles.messageContent}> + <MessageInner isPreview={isPreview} /> + </View> + {!isHeader ? <RightIcons /> : null} + </View> + </A11y.Index> + </View> + ); +}; + +NormalMessage.displayName = 'MessageNormal'; + +export default NormalMessage; diff --git a/app/containers/message/components/Message/PreviewBranch.tsx b/app/containers/message/components/Message/PreviewBranch.tsx new file mode 100644 index 00000000000..5f230a62951 --- /dev/null +++ b/app/containers/message/components/Message/PreviewBranch.tsx @@ -0,0 +1,29 @@ +import User from '../User'; +import Content from '../Content'; +import Attachments from '../Attachments'; +import Quote from '../Attachments/Quote'; +import Urls from '../Urls'; +import MessageTime from '../Time'; +import { useAttachments, useMessageAuthor } from '../../stores/MessageStore'; + +const PreviewBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { + 'use memo'; + + const attachments = useAttachments(); + const { u: author } = useMessageAuthor(); + + return ( + <> + <User /> + {showTimeLarge ? <MessageTime /> : null} + <Quote attachments={attachments} /> + <Content /> + <Attachments attachments={attachments} author={author} /> + <Urls /> + </> + ); +}; + +PreviewBranch.displayName = 'MessagePreviewBranch'; + +export default PreviewBranch; diff --git a/app/containers/message/components/MessageAvatar.tsx b/app/containers/message/components/MessageAvatar.tsx index 3658e5ff67c..0c6ee0f24e6 100644 --- a/app/containers/message/components/MessageAvatar.tsx +++ b/app/containers/message/components/MessageAvatar.tsx @@ -6,7 +6,7 @@ import styles from '../styles'; import { type IMessageAvatar } from '../interfaces'; import { SubscriptionType } from '../../../definitions'; import { useResponsiveLayout } from '../../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; -import { useAvatar, useMessageAuthor, useMessageGrouping } from '../stores/MessageStore'; +import { useAvatar, useMessageField, useMessageGrouping } from '../stores/MessageStore'; import { useGetCustomEmoji, useMessageUser, useNavToRoomInfo } from '../stores/MessageRoomStore'; const AVATAR_BASE_SIZE = 36; @@ -25,7 +25,7 @@ const MessageAvatar = ({ small }: IMessageAvatar) => { const getCustomEmoji = useGetCustomEmoji() ?? (() => null); const { fontScaleLimited } = useResponsiveLayout(); const { avatar, emoji } = useAvatar(); - const { u: author } = useMessageAuthor(); + const author = useMessageField(item => item.u); const isHeader = useMessageGrouping(); const smallSize = 20 * fontScaleLimited; const normalSize = AVATAR_BASE_SIZE * fontScaleLimited; From dbef733a9634cb0cd96c88da1568e1c7fa63825a Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 19:32:13 -0300 Subject: [PATCH 070/144] feat: add useCustomEmoji hook and migrate Preview to it --- app/containers/message/components/Preview.tsx | 13 ++++----- app/definitions/IEmoji.ts | 2 ++ app/lib/hooks/useCustomEmoji.test.ts | 29 +++++++++++++++++++ app/lib/hooks/useCustomEmoji.ts | 9 ++++++ 4 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 app/lib/hooks/useCustomEmoji.test.ts create mode 100644 app/lib/hooks/useCustomEmoji.ts diff --git a/app/containers/message/components/Preview.tsx b/app/containers/message/components/Preview.tsx index 1917a880f49..7f395b96414 100644 --- a/app/containers/message/components/Preview.tsx +++ b/app/containers/message/components/Preview.tsx @@ -1,22 +1,19 @@ import Message from '../index'; import { MessageRoomProvider } from '../stores/MessageRoomStore'; import { useAppSelector } from '../../../lib/hooks/useAppSelector'; +import { useCustomEmoji } from '../../../lib/hooks/useCustomEmoji'; import { getUserSelector } from '../../../selectors/login'; -import { type TAnyMessageModel, type TGetCustomEmoji } from '../../../definitions'; +import { type TAnyMessageModel } from '../../../definitions'; const MessagePreview = ({ message }: { message: TAnyMessageModel }) => { 'use memo'; - const { user, baseUrl, customEmojis } = useAppSelector(state => ({ + const { user, baseUrl } = useAppSelector(state => ({ user: getUserSelector(state), - baseUrl: state.server.server, - customEmojis: state.customEmojis + baseUrl: state.server.server })); + const getCustomEmoji = useCustomEmoji(); - const getCustomEmoji: TGetCustomEmoji = name => { - const emoji = customEmojis[name]; - return emoji ?? null; - }; return ( <MessageRoomProvider user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} rid={message.rid}> <Message item={message} isPreview /> diff --git a/app/definitions/IEmoji.ts b/app/definitions/IEmoji.ts index 4b025979fe6..9adda58a5ab 100644 --- a/app/definitions/IEmoji.ts +++ b/app/definitions/IEmoji.ts @@ -22,6 +22,8 @@ export interface ICustomEmojis { export type TGetCustomEmoji = (name: string) => ICustomEmoji | null; +export const resolveCustomEmoji = (customEmojis: ICustomEmojis, name: string): ICustomEmoji | null => customEmojis[name] ?? null; + export type TFrequentlyUsedEmojiModel = IFrequentlyUsedEmoji & Model; export interface ICustomEmojiModel { diff --git a/app/lib/hooks/useCustomEmoji.test.ts b/app/lib/hooks/useCustomEmoji.test.ts new file mode 100644 index 00000000000..70562350e5d --- /dev/null +++ b/app/lib/hooks/useCustomEmoji.test.ts @@ -0,0 +1,29 @@ +import { renderHook } from '@testing-library/react-native'; + +import { useCustomEmoji } from './useCustomEmoji'; +import { resolveCustomEmoji } from '../../definitions'; + +const customEmojis = { nyan_rocket: { name: 'nyan_rocket', extension: 'png' } }; + +jest.mock('./useAppSelector', () => ({ + useAppSelector: (selector: (state: any) => any) => selector({ customEmojis }) +})); + +describe('resolveCustomEmoji', () => { + it('returns the emoji when the name is registered', () => { + expect(resolveCustomEmoji(customEmojis, 'nyan_rocket')).toBe(customEmojis.nyan_rocket); + }); + + it('returns null when the name is not registered', () => { + expect(resolveCustomEmoji(customEmojis, 'unknown')).toBeNull(); + }); +}); + +describe('useCustomEmoji', () => { + it('returns a getter resolving against state.customEmojis', () => { + const { result } = renderHook(() => useCustomEmoji()); + + expect(result.current('nyan_rocket')).toBe(customEmojis.nyan_rocket); + expect(result.current('unknown')).toBeNull(); + }); +}); diff --git a/app/lib/hooks/useCustomEmoji.ts b/app/lib/hooks/useCustomEmoji.ts new file mode 100644 index 00000000000..6cc19cdd81f --- /dev/null +++ b/app/lib/hooks/useCustomEmoji.ts @@ -0,0 +1,9 @@ +import { type TGetCustomEmoji, resolveCustomEmoji } from '../../definitions'; +import { useAppSelector } from './useAppSelector'; + +export const useCustomEmoji = (): TGetCustomEmoji => { + 'use memo'; + + const customEmojis = useAppSelector(state => state.customEmojis); + return name => resolveCustomEmoji(customEmojis, name); +}; From cfad92a93a63c51148d351684680167511fb7911 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 19:38:29 -0300 Subject: [PATCH 071/144] refactor: break Message/MessageInner import cycle and narrow author reads --- app/containers/message/components/Message.tsx | 35 ------------------- .../components/Message/CompactMessage.tsx | 4 +-- .../components/Message/DefaultBranch.tsx | 4 +-- .../components/Message/MessageInner.tsx | 34 ++++++++++++++++++ .../components/Message/NormalMessage.tsx | 2 +- .../components/Message/PreviewBranch.tsx | 4 +-- 6 files changed, 41 insertions(+), 42 deletions(-) create mode 100644 app/containers/message/components/Message/MessageInner.tsx diff --git a/app/containers/message/components/Message.tsx b/app/containers/message/components/Message.tsx index f8adbb3b3ae..9d03e74d1c2 100644 --- a/app/containers/message/components/Message.tsx +++ b/app/containers/message/components/Message.tsx @@ -1,14 +1,7 @@ import { A11y } from 'react-native-a11y-order'; import { useTheme } from '../../../theme'; -import { WidthAwareView } from './WidthAwareView'; -import { useResponsiveLayout } from '../../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; import Touch from './Touch'; -import BlocksBranch from './Message/BlocksBranch'; -import JitsiBranch from './Message/JitsiBranch'; -import DiscussionBranch from './Message/DiscussionBranch'; -import PreviewBranch from './Message/PreviewBranch'; -import DefaultBranch from './Message/DefaultBranch'; import CompactMessage from './Message/CompactMessage'; import NormalMessage from './Message/NormalMessage'; import { useLastFocusedMessageRef } from '../../../lib/a11y/useLastFocusedMessageRef'; @@ -18,10 +11,8 @@ import { useMessageAccessibilityHint } from '../hooks/useMessageAccessibilityHin import { useIsBeingEdited } from '../../../views/RoomView/InteractionStore'; import { useArchived } from '../stores/MessageRoomStore'; import { - useBlocks, useIsInfo, useMessageField, - useMessageGrouping, useMessageIgnored, useMessageLongPress, useMessagePress, @@ -34,32 +25,6 @@ type TMessageProps = { highlighted?: boolean; }; -export const MessageInner = (props: TMessageProps) => { - 'use memo'; - - const { isLargeFontScale } = useResponsiveLayout(); - const isHeader = useMessageGrouping(); - const type = useMessageField(item => item.t); - const { blocks } = useBlocks(); - const showTimeLarge = isLargeFontScale && isHeader; - - let branch; - if (blocks && blocks.length) { - branch = <BlocksBranch showTimeLarge={showTimeLarge} />; - } else if (type === 'jitsi_call_started') { - branch = <JitsiBranch showTimeLarge={showTimeLarge} />; - } else if (type === 'discussion-created') { - branch = <DiscussionBranch showTimeLarge={showTimeLarge} />; - } else if (props.isPreview) { - branch = <PreviewBranch showTimeLarge={showTimeLarge} />; - } else { - branch = <DefaultBranch showTimeLarge={showTimeLarge} />; - } - - return <WidthAwareView>{branch}</WidthAwareView>; -}; -MessageInner.displayName = 'MessageInner'; - const Message = (props: TMessageProps) => { 'use memo'; diff --git a/app/containers/message/components/Message/CompactMessage.tsx b/app/containers/message/components/Message/CompactMessage.tsx index 76bc93f93b6..e6911df77f0 100644 --- a/app/containers/message/components/Message/CompactMessage.tsx +++ b/app/containers/message/components/Message/CompactMessage.tsx @@ -9,7 +9,7 @@ import Attachments from '../Attachments'; import { useContentData, useIsInfo, - useMessageAuthor, + useMessageField, useMessageGrouping, useMessageText, useThreadPosition @@ -24,7 +24,7 @@ const CompactMessage = () => { const isInfo = useIsInfo(); const { messageText, isTranslated } = useMessageText(); const { t: type, attachments } = useContentData(); - const { u: author } = useMessageAuthor(); + const author = useMessageField(item => item.u); const { autoTranslateLanguage } = useAutoTranslate(); const thread = isThreadReply ? <RepliedThread isHeader={isHeader} /> : null; diff --git a/app/containers/message/components/Message/DefaultBranch.tsx b/app/containers/message/components/Message/DefaultBranch.tsx index 5df1ceaf317..d1caebe701a 100644 --- a/app/containers/message/components/Message/DefaultBranch.tsx +++ b/app/containers/message/components/Message/DefaultBranch.tsx @@ -9,13 +9,13 @@ import Thread from '../Thread'; import Reactions from '../Reactions'; import Broadcast from '../Broadcast'; import MessageTime from '../Time'; -import { useAttachments, useMessageAuthor } from '../../stores/MessageStore'; +import { useAttachments, useMessageField } from '../../stores/MessageStore'; const DefaultBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { 'use memo'; const attachments = useAttachments(); - const { u: author } = useMessageAuthor(); + const author = useMessageField(item => item.u); return ( <> diff --git a/app/containers/message/components/Message/MessageInner.tsx b/app/containers/message/components/Message/MessageInner.tsx new file mode 100644 index 00000000000..af60924285a --- /dev/null +++ b/app/containers/message/components/Message/MessageInner.tsx @@ -0,0 +1,34 @@ +import { WidthAwareView } from '../WidthAwareView'; +import { useResponsiveLayout } from '../../../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; +import BlocksBranch from './BlocksBranch'; +import JitsiBranch from './JitsiBranch'; +import DiscussionBranch from './DiscussionBranch'; +import PreviewBranch from './PreviewBranch'; +import DefaultBranch from './DefaultBranch'; +import { useBlocks, useMessageField, useMessageGrouping } from '../../stores/MessageStore'; + +export const MessageInner = ({ isPreview }: { isPreview?: boolean }) => { + 'use memo'; + + const { isLargeFontScale } = useResponsiveLayout(); + const isHeader = useMessageGrouping(); + const type = useMessageField(item => item.t); + const { blocks } = useBlocks(); + const showTimeLarge = isLargeFontScale && isHeader; + + let branch; + if (blocks && blocks.length) { + branch = <BlocksBranch showTimeLarge={showTimeLarge} />; + } else if (type === 'jitsi_call_started') { + branch = <JitsiBranch showTimeLarge={showTimeLarge} />; + } else if (type === 'discussion-created') { + branch = <DiscussionBranch showTimeLarge={showTimeLarge} />; + } else if (isPreview) { + branch = <PreviewBranch showTimeLarge={showTimeLarge} />; + } else { + branch = <DefaultBranch showTimeLarge={showTimeLarge} />; + } + + return <WidthAwareView>{branch}</WidthAwareView>; +}; +MessageInner.displayName = 'MessageInner'; diff --git a/app/containers/message/components/Message/NormalMessage.tsx b/app/containers/message/components/Message/NormalMessage.tsx index c2ac394c362..2bf17415eab 100644 --- a/app/containers/message/components/Message/NormalMessage.tsx +++ b/app/containers/message/components/Message/NormalMessage.tsx @@ -4,7 +4,7 @@ import { A11y } from 'react-native-a11y-order'; import styles from '../../styles'; import MessageAvatar from '../MessageAvatar'; import RightIcons from '../RightIcons'; -import { MessageInner } from '../Message'; +import { MessageInner } from './MessageInner'; import { useMessageField, useMessageGrouping, useMessageText } from '../../stores/MessageStore'; import { useAutoTranslate } from '../../stores/MessageRoomStore'; diff --git a/app/containers/message/components/Message/PreviewBranch.tsx b/app/containers/message/components/Message/PreviewBranch.tsx index 5f230a62951..6ad2cc7ace7 100644 --- a/app/containers/message/components/Message/PreviewBranch.tsx +++ b/app/containers/message/components/Message/PreviewBranch.tsx @@ -4,13 +4,13 @@ import Attachments from '../Attachments'; import Quote from '../Attachments/Quote'; import Urls from '../Urls'; import MessageTime from '../Time'; -import { useAttachments, useMessageAuthor } from '../../stores/MessageStore'; +import { useAttachments, useMessageField } from '../../stores/MessageStore'; const PreviewBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { 'use memo'; const attachments = useAttachments(); - const { u: author } = useMessageAuthor(); + const author = useMessageField(item => item.u); return ( <> From 6be3190639514bb523a228ed371e1baae9108c38 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 20:06:37 -0300 Subject: [PATCH 072/144] refactor: retire ROOM_STATE_KEYS and pickMessageRoomState test picker --- .../message/__tests__/index.test.tsx | 10 +++-- .../Attachments/Attachments.stories.tsx | 9 ++-- .../CollapsibleQuote.stories.tsx | 4 +- .../CollapsibleQuote.test.tsx | 7 ++- .../Attachments/Image/Container.test.tsx | 14 +++--- .../components/Attachments/Reply.test.tsx | 8 ++-- .../components/__tests__/Content.test.tsx | 7 ++- .../components/__tests__/Reactions.test.tsx | 2 +- .../components/__tests__/Thread.test.tsx | 10 ++--- .../components/stories/Broadcast.stories.tsx | 6 +-- .../components/stories/CallButton.stories.tsx | 6 +-- .../components/stories/Content.stories.tsx | 6 +-- .../components/stories/Discussion.stories.tsx | 6 +-- .../components/stories/Message.stories.tsx | 25 ++++++----- .../stories/MessageAvatar.stories.tsx | 6 +-- .../components/stories/Reactions.stories.tsx | 6 +-- .../stories/RepliedThread.stories.tsx | 4 +- .../stories/RightIcons/Edited.stories.tsx | 4 +- .../stories/RightIcons/Encrypted.stories.tsx | 6 +-- .../RightIcons/MessageError.stories.tsx | 6 +-- .../stories/RightIcons/Pinned.stories.tsx | 4 +- .../RightIcons/ReadReceipt.stories.tsx | 14 ++++-- .../stories/RightIcons/Translated.stories.tsx | 14 ++++-- .../components/stories/Thread.stories.tsx | 6 +-- .../components/stories/Time.stories.tsx | 6 +-- .../components/stories/Urls.stories.tsx | 6 +-- .../components/stories/User.stories.tsx | 6 +-- .../__tests__/useMediaAutoDownload.test.tsx | 8 ++-- .../useMessageAccessibilityHint.test.tsx | 6 +-- .../useMessageAccessibilityLabel.test.tsx | 6 +-- .../message/stores/MessageRoomStore.tsx | 45 ------------------- .../__tests__/MessageRoomStore.test.tsx | 4 +- .../stores/__tests__/MessageStore.test.tsx | 6 +-- app/containers/message/testHelpers.tsx | 6 +-- 34 files changed, 128 insertions(+), 161 deletions(-) diff --git a/app/containers/message/__tests__/index.test.tsx b/app/containers/message/__tests__/index.test.tsx index a767c0712a5..a46111384d7 100644 --- a/app/containers/message/__tests__/index.test.tsx +++ b/app/containers/message/__tests__/index.test.tsx @@ -5,7 +5,7 @@ import MessageContainer from '../index'; import { type TAnyMessageModel } from '../../../definitions'; import { createInteractionStore, InteractionStoreContext } from '../../../views/RoomView/InteractionStore'; import { renderWithMessageProviders } from '../testHelpers'; -import { pickMessageRoomState } from '../stores/MessageRoomStore'; +import { type MessageRoomState } from '../stores/MessageRoomStore'; jest.mock('../components/Touch', () => { const { forwardRef } = require('react'); @@ -60,10 +60,12 @@ const baseProps = { rid: 'room-1' }; +const room: Partial<MessageRoomState> = { rid: baseProps.rid }; + const renderContainer = (itemOverrides: Record<string, any> = {}, propOverrides: Record<string, any> = {}) => { const props = { ...baseProps, ...propOverrides }; return renderWithMessageProviders(<MessageContainer item={createMockMessage(itemOverrides)} {...props} />, { - room: pickMessageRoomState(props) + room }); }; @@ -99,7 +101,7 @@ describe('edit highlight reacts to InteractionStore', () => { <InteractionStoreContext.Provider value={store}> <MessageContainer item={item} {...baseProps} /> </InteractionStoreContext.Provider>, - { room: pickMessageRoomState(baseProps) } + { room } ); expect(getByTestId('message-editing-msg-1')).toBeTruthy(); }); @@ -111,7 +113,7 @@ describe('edit highlight reacts to InteractionStore', () => { <InteractionStoreContext.Provider value={store}> <MessageContainer item={item} {...baseProps} /> </InteractionStoreContext.Provider>, - { room: pickMessageRoomState(baseProps) } + { room } ); await act(() => { store.getState().actions.reset(); diff --git a/app/containers/message/components/Attachments/Attachments.stories.tsx b/app/containers/message/components/Attachments/Attachments.stories.tsx index a128045a555..d75cd768137 100644 --- a/app/containers/message/components/Attachments/Attachments.stories.tsx +++ b/app/containers/message/components/Attachments/Attachments.stories.tsx @@ -5,16 +5,13 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { selectServerSuccess } from '../../../../actions/server'; import { type TAnyMessageModel } from '../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import Attachments from './Attachments'; -const mockMessageContext = { - id: 'msg-id', +const mockMessageContext: Partial<MessageRoomState> = { baseUrl: 'https://open.rocket.chat', user: { id: 'user-id', username: 'rocket.cat', token: 'token' }, - onLongPress: () => {}, - translateLanguage: undefined, getCustomEmoji: () => null, showAttachment: undefined }; @@ -23,7 +20,7 @@ const mockItem = { id: 'msg-id', msg: '', u: { username: 'rocket.cat' }, autoTra const StoryWrapper = ({ store, children }: { store: any; children: ReactNode }) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState(mockMessageContext)}> + <MessageRoomProvider {...mockMessageContext}> <MessageProvider item={mockItem}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx b/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx index 608f23fe19b..4037b3e9749 100644 --- a/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx +++ b/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx @@ -1,6 +1,6 @@ import { View } from 'react-native'; -import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; +import { MessageRoomProvider } from '../../../stores/MessageRoomStore'; import CollapsibleQuote from '.'; const testAttachment = { @@ -23,7 +23,7 @@ export default { export const Item = () => ( <View style={{ padding: 10 }}> - <MessageRoomProvider {...pickMessageRoomState({ onLongPress: () => {}, user: { username: 'Marcos' } })}> + <MessageRoomProvider user={{ username: 'Marcos' }}> <CollapsibleQuote attachment={testAttachment} getCustomEmoji={() => null} /> </MessageRoomProvider> </View> diff --git a/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx b/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx index 6bab716906c..ecdd49d3ba3 100644 --- a/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx +++ b/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx @@ -5,7 +5,7 @@ import { setUser } from '../../../../../actions/login'; import { mockedStore } from '../../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../../definitions'; import { MessageProvider } from '../../../stores/MessageStore'; -import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../../stores/MessageRoomStore'; import CollapsibleQuote from '.'; const testAttachment = { @@ -39,8 +39,7 @@ const initialMockedStoreState = () => { initialMockedStoreState(); -const contextValue = { - onLongPress: () => {}, +const contextValue: Partial<MessageRoomState> = { user: { username: 'Marcos' } }; @@ -48,7 +47,7 @@ const item = { id: 'collapsible-quote-msg' } as unknown as TAnyMessageModel; const Render = () => ( <Provider store={mockedStore}> - <MessageRoomProvider {...pickMessageRoomState(contextValue)}> + <MessageRoomProvider {...contextValue}> <MessageProvider item={item}> <CollapsibleQuote attachment={testAttachment} getCustomEmoji={mockFn} /> </MessageProvider> diff --git a/app/containers/message/components/Attachments/Image/Container.test.tsx b/app/containers/message/components/Attachments/Image/Container.test.tsx index d99f49a7a11..4eb0217ecd5 100644 --- a/app/containers/message/components/Attachments/Image/Container.test.tsx +++ b/app/containers/message/components/Attachments/Image/Container.test.tsx @@ -5,7 +5,7 @@ import { Provider } from 'react-redux'; import { type TAnyMessageModel } from '../../../../../definitions'; import { MessageProvider } from '../../../stores/MessageStore'; -import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../../stores/MessageRoomStore'; import { mockedStore } from '../../../../../reducers/mockedStore'; import ImageContainer from './Container'; @@ -35,18 +35,16 @@ jest.mock('../../../../../lib/hooks/useAltTextSupported', () => ({ })); const renderImageContainer = (props?: Partial<ComponentProps<typeof ImageContainer>>) => { - const contextValue = { - id: 'message-id', + const id = 'message-id'; + const contextValue: Partial<MessageRoomState> = { baseUrl: 'https://open.rocket.chat', - user: { id: 'user-id', username: 'rocket.cat', token: 'token' }, - onLongPress: jest.fn(), - translateLanguage: undefined + user: { id: 'user-id', username: 'rocket.cat', token: 'token' } }; - const item = { id: contextValue.id } as unknown as TAnyMessageModel; + const item = { id } as unknown as TAnyMessageModel; return render( <Provider store={mockedStore}> <A11y.Order> - <MessageRoomProvider {...pickMessageRoomState(contextValue)}> + <MessageRoomProvider {...contextValue}> <MessageProvider item={item}> <ImageContainer file={{ image_url: 'https://open.rocket.chat/image.png', image_type: 'image/png' }} {...props} /> </MessageProvider> diff --git a/app/containers/message/components/Attachments/Reply.test.tsx b/app/containers/message/components/Attachments/Reply.test.tsx index 320d5b79087..7b5d04350bc 100644 --- a/app/containers/message/components/Attachments/Reply.test.tsx +++ b/app/containers/message/components/Attachments/Reply.test.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import Reply from './Reply'; import { MessageProvider } from '../../stores/MessageStore'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { mockedStore } from '../../../../reducers/mockedStore'; import { type IAttachment, type TAnyMessageModel } from '../../../../definitions'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../../../lib/constants/keys'; @@ -63,9 +63,9 @@ const renderReply = ({ attachment?: IAttachment; msg?: string; isEncrypted?: boolean; - ctx?: Record<string, any>; + ctx?: Partial<MessageRoomState>; }) => { - const contextValue = { + const contextValue: Partial<MessageRoomState> = { user: { id: 'user-1', username: 'john', token: 'token' }, baseUrl: 'https://open.rocket.chat', timeFormat: 'HH:mm', @@ -73,7 +73,7 @@ const renderReply = ({ }; return render( <Provider store={mockedStore}> - <MessageRoomProvider {...pickMessageRoomState(contextValue)}> + <MessageRoomProvider {...contextValue}> <MessageProvider item={buildItem(isEncrypted)}> <Reply attachment={attachment as IAttachment} getCustomEmoji={getCustomEmoji} msg={msg} /> </MessageProvider> diff --git a/app/containers/message/components/__tests__/Content.test.tsx b/app/containers/message/components/__tests__/Content.test.tsx index fa3d891a521..b15a93786bc 100644 --- a/app/containers/message/components/__tests__/Content.test.tsx +++ b/app/containers/message/components/__tests__/Content.test.tsx @@ -3,7 +3,7 @@ import { render } from '@testing-library/react-native'; import Content from '../Content'; import { MessageProvider } from '../../stores/MessageStore'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { setUser } from '../../../../actions/login'; import { mockedStore } from '../../../../reducers/mockedStore'; import { type IAttachment, type TAnyMessageModel } from '../../../../definitions'; @@ -52,16 +52,15 @@ const buildItem = ( const tree = (overrides: TOverrides) => { const { msg, attachments, isEncrypted, autoTranslateLanguage, tmid, isIgnored } = overrides; - const contextValue = { + const contextValue: Partial<MessageRoomState> = { user: { username: 'john' }, - onLinkPress: jest.fn(), getCustomEmoji: jest.fn(), navToRoomInfo: jest.fn(), autoTranslateLanguage }; return ( <Provider store={mockedStore}> - <MessageRoomProvider {...pickMessageRoomState(contextValue)}> + <MessageRoomProvider {...contextValue}> <MessageProvider item={buildItem(msg, attachments, isEncrypted, tmid)} isIgnored={isIgnored}> <Content /> </MessageProvider> diff --git a/app/containers/message/components/__tests__/Reactions.test.tsx b/app/containers/message/components/__tests__/Reactions.test.tsx index 6f052e3b332..ecf5fa8d2cd 100644 --- a/app/containers/message/components/__tests__/Reactions.test.tsx +++ b/app/containers/message/components/__tests__/Reactions.test.tsx @@ -54,7 +54,7 @@ const buildOnReactionPress = (item: FakeItem) => (emoji: string) => { const TestWrapper = ({ item, onReactionPress }: { item: FakeItem; onReactionPress: (emoji: string) => void }) => ( <MessageProviders item={item} - context={{ + room={{ user: { username: 'john' }, reactionInit: jest.fn(), onReactionPress, diff --git a/app/containers/message/components/__tests__/Thread.test.tsx b/app/containers/message/components/__tests__/Thread.test.tsx index 33d45065b6a..806b86f3e02 100644 --- a/app/containers/message/components/__tests__/Thread.test.tsx +++ b/app/containers/message/components/__tests__/Thread.test.tsx @@ -3,15 +3,13 @@ import { Provider } from 'react-redux'; import Thread from '../Thread'; import { MessageProvider } from '../../stores/MessageStore'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { type TAnyMessageModel } from '../../../../definitions'; import { mockedStore } from '../../../../reducers/mockedStore'; -const baseContextValue = { - threadBadgeColor: undefined, +const baseContextValue: Partial<MessageRoomState> = { toggleFollowThread: jest.fn(), user: { id: 'user1', username: 'user1' }, - replies: [], onThreadPress: jest.fn() }; @@ -21,7 +19,7 @@ const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel const renderThread = (item: TAnyMessageModel) => render( <Provider store={mockedStore}> - <MessageRoomProvider {...pickMessageRoomState(baseContextValue)}> + <MessageRoomProvider {...baseContextValue}> <MessageProvider item={item}> <Thread /> </MessageProvider> @@ -39,7 +37,7 @@ describe('Thread — tlm-only update regression', () => { const updatedItem = buildItem({ tlm: new Date('2024-01-01T00:00:00Z') }); rerender( <Provider store={mockedStore}> - <MessageRoomProvider {...pickMessageRoomState(baseContextValue)}> + <MessageRoomProvider {...baseContextValue}> <MessageProvider item={updatedItem}> <Thread /> </MessageProvider> diff --git a/app/containers/message/components/stories/Broadcast.stories.tsx b/app/containers/message/components/stories/Broadcast.stories.tsx index 1a9d898275a..c0c53a45251 100644 --- a/app/containers/message/components/stories/Broadcast.stories.tsx +++ b/app/containers/message/components/stories/Broadcast.stories.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import BroadcastLeaf from '../Broadcast'; @@ -23,7 +23,7 @@ const ownAuthorItem = { autoTranslate: false } as unknown as TAnyMessageModel; -const room = { +const room: Partial<MessageRoomState> = { broadcast: true, user: { id: 'reader-id', username: 'rocket.cat' }, replyBroadcast: () => {} @@ -31,7 +31,7 @@ const room = { const StoryWrapper = ({ item, children }: { item: TAnyMessageModel; children: ReactNode }) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageRoomProvider {...room}> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/stories/CallButton.stories.tsx b/app/containers/message/components/stories/CallButton.stories.tsx index 724a3f98039..0488be2988d 100644 --- a/app/containers/message/components/stories/CallButton.stories.tsx +++ b/app/containers/message/components/stories/CallButton.stories.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import CallButtonLeaf from '../CallButton'; @@ -16,13 +16,13 @@ const item = { autoTranslate: false } as unknown as TAnyMessageModel; -const room = { +const room: Partial<MessageRoomState> = { handleEnterCall: () => {} }; const StoryWrapper = ({ children }: { children: ReactNode }) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageRoomProvider {...room}> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/stories/Content.stories.tsx b/app/containers/message/components/stories/Content.stories.tsx index 6b9daaba139..aa2ae8adc35 100644 --- a/app/containers/message/components/stories/Content.stories.tsx +++ b/app/containers/message/components/stories/Content.stories.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import ContentLeaf from '../Content'; @@ -17,14 +17,14 @@ const item = { autoTranslate: false } as unknown as TAnyMessageModel; -const room = { +const room: Partial<MessageRoomState> = { user: { id: 'reader-id', username: 'reader' }, getCustomEmoji: () => null }; const StoryWrapper = ({ children }: { children: ReactNode }) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageRoomProvider {...room}> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/stories/Discussion.stories.tsx b/app/containers/message/components/stories/Discussion.stories.tsx index 84d4a5d4095..8a96a8463f1 100644 --- a/app/containers/message/components/stories/Discussion.stories.tsx +++ b/app/containers/message/components/stories/Discussion.stories.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import DiscussionLeaf from '../Discussion'; @@ -18,14 +18,14 @@ const item = { autoTranslate: false } as unknown as TAnyMessageModel; -const room = { +const room: Partial<MessageRoomState> = { user: { id: 'reader-id', username: 'reader' }, onDiscussionPress: () => {} }; const StoryWrapper = ({ children }: { children: ReactNode }) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageRoomProvider {...room}> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/stories/Message.stories.tsx b/app/containers/message/components/stories/Message.stories.tsx index 114e7629445..27e91acfc3d 100644 --- a/app/containers/message/components/stories/Message.stories.tsx +++ b/app/containers/message/components/stories/Message.stories.tsx @@ -17,7 +17,7 @@ import { import { mockedStore as store } from '../../../../reducers/mockedStore'; import { updateSettings } from '../../../../actions/settings'; import { createInteractionStore, InteractionStoreContext } from '../../../../views/RoomView/InteractionStore'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; const _theme = 'light'; @@ -75,23 +75,24 @@ const getCustomEmoji = (content: string): ICustomEmoji | null => { return customEmoji ?? null; }; -const storyContextValue = { - user, - baseUrl, +const containerHandlers = { onPress: () => {}, onLongPress: () => {}, + threadBadgeColor: themes[_theme].badgeBackgroundLevel1 +}; + +const roomHandlers: Partial<MessageRoomState> = { + user, + baseUrl, reactionInit: () => {}, - onErrorPress: () => {}, replyBroadcast: () => {}, onReactionPress: () => {}, onEncryptedPress: () => {}, onDiscussionPress: () => {}, onThreadPress: () => {}, onReactionLongPress: () => {}, - onLinkPress: () => {}, onAnswerButtonPress: () => {}, jumpToMessage: () => {}, - threadBadgeColor: themes[_theme].badgeBackgroundLevel1, getCustomEmoji, navToRoomInfo: () => {}, showAttachment: undefined, @@ -231,21 +232,25 @@ const renderMessageStory = (fontScale: number, props: any) => { const containerProps = { item, previousItem, + isIgnored, + ...containerHandlers + }; + + const room: Partial<MessageRoomState> = { rid, Message_GroupingPeriod: 300, timeFormat, broadcast, archived, - isIgnored, isReadReceiptEnabled, autoTranslateRoom: autoTranslateRoom ?? (isTranslated ? true : undefined), autoTranslateLanguage: autoTranslateLanguage ?? (isTranslated ? 'en' : undefined), - ...storyContextValue + ...roomHandlers }; return ( <ResponsiveLayoutContext.Provider value={responsiveLayoutProviderLargeFontValue(fontScale)}> - <MessageRoomProvider {...pickMessageRoomState(containerProps)}> + <MessageRoomProvider {...room}> <MessageContainer {...containerProps} /> </MessageRoomProvider> </ResponsiveLayoutContext.Provider> diff --git a/app/containers/message/components/stories/MessageAvatar.stories.tsx b/app/containers/message/components/stories/MessageAvatar.stories.tsx index 08d217559b3..aa09b1835d0 100644 --- a/app/containers/message/components/stories/MessageAvatar.stories.tsx +++ b/app/containers/message/components/stories/MessageAvatar.stories.tsx @@ -4,7 +4,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { selectServerRequest } from '../../../../actions/server'; import { type TAnyMessageModel } from '../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import MessageAvatarLeaf from '../MessageAvatar'; @@ -18,14 +18,14 @@ const item = { autoTranslate: false } as unknown as TAnyMessageModel; -const room = { +const room: Partial<MessageRoomState> = { user: { id: 'reader-id', username: 'reader' }, getCustomEmoji: () => null }; const StoryWrapper = ({ children }: { children: ReactNode }) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageRoomProvider {...room}> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/stories/Reactions.stories.tsx b/app/containers/message/components/stories/Reactions.stories.tsx index 56f17b8a500..02d697a20dd 100644 --- a/app/containers/message/components/stories/Reactions.stories.tsx +++ b/app/containers/message/components/stories/Reactions.stories.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import ReactionsLeaf from '../Reactions'; @@ -20,7 +20,7 @@ const item = { ] } as unknown as TAnyMessageModel; -const room = { +const room: Partial<MessageRoomState> = { user: { id: 'reader-id', username: 'rocket.cat' }, getCustomEmoji: () => null, onReactionPress: () => {}, @@ -30,7 +30,7 @@ const room = { const StoryWrapper = ({ children }: { children: ReactNode }) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageRoomProvider {...room}> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/stories/RepliedThread.stories.tsx b/app/containers/message/components/stories/RepliedThread.stories.tsx index 01a9a02f6a3..8a199969d0f 100644 --- a/app/containers/message/components/stories/RepliedThread.stories.tsx +++ b/app/containers/message/components/stories/RepliedThread.stories.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import RepliedThreadLeaf from '../RepliedThread'; @@ -20,7 +20,7 @@ const item = { const StoryWrapper = ({ children }: { children: ReactNode }) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState({})}> + <MessageRoomProvider> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/stories/RightIcons/Edited.stories.tsx b/app/containers/message/components/stories/RightIcons/Edited.stories.tsx index 746ada6e2bf..2cf180fc3fc 100644 --- a/app/containers/message/components/stories/RightIcons/Edited.stories.tsx +++ b/app/containers/message/components/stories/RightIcons/Edited.stories.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; +import { MessageRoomProvider } from '../../../stores/MessageRoomStore'; import { MessageProvider } from '../../../stores/MessageStore'; import EditedLeaf from '../../RightIcons/Edited'; @@ -26,7 +26,7 @@ const plainItem = { const StoryWrapper = ({ item, children }: { item: TAnyMessageModel; children: ReactNode }) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState({})}> + <MessageRoomProvider> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/stories/RightIcons/Encrypted.stories.tsx b/app/containers/message/components/stories/RightIcons/Encrypted.stories.tsx index d55ba8763dd..6a7005128bb 100644 --- a/app/containers/message/components/stories/RightIcons/Encrypted.stories.tsx +++ b/app/containers/message/components/stories/RightIcons/Encrypted.stories.tsx @@ -4,7 +4,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../../definitions'; import { E2E_MESSAGE_TYPE } from '../../../../../lib/constants/keys'; -import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../../stores/MessageRoomStore'; import { MessageProvider } from '../../../stores/MessageStore'; import EncryptedLeaf from '../../RightIcons/Encrypted'; @@ -26,13 +26,13 @@ const plainItem = { autoTranslate: false } as unknown as TAnyMessageModel; -const room = { +const room: Partial<MessageRoomState> = { onEncryptedPress: () => {} }; const StoryWrapper = ({ item, children }: { item: TAnyMessageModel; children: ReactNode }) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageRoomProvider {...room}> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/stories/RightIcons/MessageError.stories.tsx b/app/containers/message/components/stories/RightIcons/MessageError.stories.tsx index 8b8ff91b5a4..a79b5be6508 100644 --- a/app/containers/message/components/stories/RightIcons/MessageError.stories.tsx +++ b/app/containers/message/components/stories/RightIcons/MessageError.stories.tsx @@ -4,7 +4,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../../definitions'; import { messagesStatus } from '../../../../../lib/constants/messagesStatus'; -import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../../stores/MessageRoomStore'; import { MessageProvider } from '../../../stores/MessageStore'; import MessageErrorLeaf from '../../RightIcons/MessageError'; @@ -26,13 +26,13 @@ const okItem = { autoTranslate: false } as unknown as TAnyMessageModel; -const room = { +const room: Partial<MessageRoomState> = { errorActionsShow: () => {} }; const StoryWrapper = ({ item, children }: { item: TAnyMessageModel; children: ReactNode }) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageRoomProvider {...room}> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/stories/RightIcons/Pinned.stories.tsx b/app/containers/message/components/stories/RightIcons/Pinned.stories.tsx index b0e3463131c..2f67013ee71 100644 --- a/app/containers/message/components/stories/RightIcons/Pinned.stories.tsx +++ b/app/containers/message/components/stories/RightIcons/Pinned.stories.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; +import { MessageRoomProvider } from '../../../stores/MessageRoomStore'; import { MessageProvider } from '../../../stores/MessageStore'; import PinnedLeaf from '../../RightIcons/Pinned'; @@ -27,7 +27,7 @@ const unpinnedItem = { const StoryWrapper = ({ item, children }: { item: TAnyMessageModel; children: ReactNode }) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState({})}> + <MessageRoomProvider> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/stories/RightIcons/ReadReceipt.stories.tsx b/app/containers/message/components/stories/RightIcons/ReadReceipt.stories.tsx index f4d7f0b2745..31e81407f28 100644 --- a/app/containers/message/components/stories/RightIcons/ReadReceipt.stories.tsx +++ b/app/containers/message/components/stories/RightIcons/ReadReceipt.stories.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../../stores/MessageRoomStore'; import { MessageProvider } from '../../../stores/MessageStore'; import ReadReceiptLeaf from '../../RightIcons/ReadReceipt'; @@ -25,9 +25,17 @@ const unreadItem = { autoTranslate: false } as unknown as TAnyMessageModel; -const StoryWrapper = ({ item, room, children }: { item: TAnyMessageModel; room: Record<string, any>; children: ReactNode }) => ( +const StoryWrapper = ({ + item, + room, + children +}: { + item: TAnyMessageModel; + room: Partial<MessageRoomState>; + children: ReactNode; +}) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageRoomProvider {...room}> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/stories/RightIcons/Translated.stories.tsx b/app/containers/message/components/stories/RightIcons/Translated.stories.tsx index 8e8a2d37564..969bf65ad0b 100644 --- a/app/containers/message/components/stories/RightIcons/Translated.stories.tsx +++ b/app/containers/message/components/stories/RightIcons/Translated.stories.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../../stores/MessageRoomStore'; import { MessageProvider } from '../../../stores/MessageStore'; import TranslatedLeaf from '../../RightIcons/Translated'; @@ -24,9 +24,17 @@ const plainItem = { u: { _id: 'author-id', username: 'other.user' } } as unknown as TAnyMessageModel; -const StoryWrapper = ({ item, room, children }: { item: TAnyMessageModel; room: Record<string, any>; children: ReactNode }) => ( +const StoryWrapper = ({ + item, + room, + children +}: { + item: TAnyMessageModel; + room: Partial<MessageRoomState>; + children: ReactNode; +}) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageRoomProvider {...room}> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/stories/Thread.stories.tsx b/app/containers/message/components/stories/Thread.stories.tsx index 23bc287e29c..96f659d6a67 100644 --- a/app/containers/message/components/stories/Thread.stories.tsx +++ b/app/containers/message/components/stories/Thread.stories.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import ThreadLeaf from '../Thread'; @@ -19,7 +19,7 @@ const item = { autoTranslate: false } as unknown as TAnyMessageModel; -const room = { +const room: Partial<MessageRoomState> = { user: { id: 'reader-id', username: 'reader' }, onThreadPress: () => {}, isThreadRoom: false @@ -27,7 +27,7 @@ const room = { const StoryWrapper = ({ children }: { children: ReactNode }) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageRoomProvider {...room}> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/stories/Time.stories.tsx b/app/containers/message/components/stories/Time.stories.tsx index edce66113a3..2b06ced338f 100644 --- a/app/containers/message/components/stories/Time.stories.tsx +++ b/app/containers/message/components/stories/Time.stories.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import MessageTimeLeaf from '../Time'; @@ -17,13 +17,13 @@ const item = { autoTranslate: false } as unknown as TAnyMessageModel; -const room = { +const room: Partial<MessageRoomState> = { timeFormat: 'LT' }; const StoryWrapper = ({ children }: { children: ReactNode }) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageRoomProvider {...room}> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/stories/Urls.stories.tsx b/app/containers/message/components/stories/Urls.stories.tsx index b3ffba66f29..790eafba882 100644 --- a/app/containers/message/components/stories/Urls.stories.tsx +++ b/app/containers/message/components/stories/Urls.stories.tsx @@ -4,7 +4,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { updateSettings } from '../../../../actions/settings'; import { type TAnyMessageModel } from '../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import UrlsLeaf from '../Urls'; @@ -26,14 +26,14 @@ const item = { ] } as unknown as TAnyMessageModel; -const room = { +const room: Partial<MessageRoomState> = { user: { id: 'reader-id', username: 'reader', token: 'token' }, baseUrl: 'https://open.rocket.chat' }; const StoryWrapper = ({ children }: { children: ReactNode }) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageRoomProvider {...room}> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/components/stories/User.stories.tsx b/app/containers/message/components/stories/User.stories.tsx index 410ebe820af..8166af09f59 100644 --- a/app/containers/message/components/stories/User.stories.tsx +++ b/app/containers/message/components/stories/User.stories.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../definitions'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import UserLeaf from '../User'; @@ -20,14 +20,14 @@ const item = { autoTranslate: false } as unknown as TAnyMessageModel; -const room = { +const room: Partial<MessageRoomState> = { user: { id: 'reader-id', username: 'reader' }, timeFormat: 'LT' }; const StoryWrapper = ({ children }: { children: ReactNode }) => ( <Provider store={store}> - <MessageRoomProvider {...pickMessageRoomState(room)}> + <MessageRoomProvider {...room}> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx b/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx index 496a6f588d1..9a7b0b964bb 100644 --- a/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx +++ b/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx @@ -4,7 +4,7 @@ import { act, renderHook, waitFor } from '@testing-library/react-native'; import { useMediaAutoDownload } from '../useMediaAutoDownload'; import { MessageProvider } from '../../stores/MessageStore'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { mockedStore } from '../../../../reducers/mockedStore'; import { type IAttachment, type IUserMessage, type TAnyMessageModel } from '../../../../definitions'; import { cancelDownload, downloadMediaFile, getMediaCache, isDownloadActive } from '../../../../lib/methods/handleMediaDownload'; @@ -90,13 +90,13 @@ const renderMediaHook = ({ file: IAttachment; author?: IUserMessage; showAttachment?: (file: IAttachment) => void; - ctx?: Record<string, any>; + ctx?: Partial<MessageRoomState>; }) => { - const contextValue = { baseUrl: 'https://open.rocket.chat', user: USER, ...ctx }; + const contextValue: Partial<MessageRoomState> = { baseUrl: 'https://open.rocket.chat', user: USER, ...ctx }; const item = { id: 'msg-1' } as unknown as TAnyMessageModel; const wrapper = ({ children }: { children: ReactNode }) => ( <Provider store={mockedStore}> - <MessageRoomProvider {...pickMessageRoomState(contextValue)}> + <MessageRoomProvider {...contextValue}> <MessageProvider item={item}>{children}</MessageProvider> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/hooks/__tests__/useMessageAccessibilityHint.test.tsx b/app/containers/message/hooks/__tests__/useMessageAccessibilityHint.test.tsx index 7ddd31303bc..16ae3bcc066 100644 --- a/app/containers/message/hooks/__tests__/useMessageAccessibilityHint.test.tsx +++ b/app/containers/message/hooks/__tests__/useMessageAccessibilityHint.test.tsx @@ -2,7 +2,7 @@ import { render } from '@testing-library/react-native'; import { Provider } from 'react-redux'; import { useMessageAccessibilityHint } from '../useMessageAccessibilityHint'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import { type TAnyMessageModel } from '../../../../definitions'; import { mockedStore } from '../../../../reducers/mockedStore'; @@ -10,7 +10,7 @@ import { mockedStore } from '../../../../reducers/mockedStore'; const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel => ({ id: 'msg-1', ...overrides } as TAnyMessageModel); -const renderHint = (item: TAnyMessageModel, config: Record<string, any> = {}) => { +const renderHint = (item: TAnyMessageModel, config: Partial<MessageRoomState> = {}) => { const spy = jest.fn(); const Probe = () => { spy(useMessageAccessibilityHint()); @@ -18,7 +18,7 @@ const renderHint = (item: TAnyMessageModel, config: Record<string, any> = {}) => }; render( <Provider store={mockedStore}> - <MessageRoomProvider {...pickMessageRoomState(config)}> + <MessageRoomProvider {...config}> <MessageProvider item={item}> <Probe /> </MessageProvider> diff --git a/app/containers/message/hooks/__tests__/useMessageAccessibilityLabel.test.tsx b/app/containers/message/hooks/__tests__/useMessageAccessibilityLabel.test.tsx index a273bd5f328..f4352341876 100644 --- a/app/containers/message/hooks/__tests__/useMessageAccessibilityLabel.test.tsx +++ b/app/containers/message/hooks/__tests__/useMessageAccessibilityLabel.test.tsx @@ -3,7 +3,7 @@ import { render } from '@testing-library/react-native'; import { useMessageAccessibilityLabel } from '../useMessageAccessibilityLabel'; import { MessageProvider } from '../../stores/MessageStore'; -import { MessageRoomProvider, pickMessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { updateSettings } from '../../../../actions/settings'; import { mockedStore } from '../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../definitions'; @@ -26,7 +26,7 @@ const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel ...overrides } as TAnyMessageModel); -const renderLabel = (item: TAnyMessageModel, config: Record<string, any> = {}, previousItem?: TAnyMessageModel) => { +const renderLabel = (item: TAnyMessageModel, config: Partial<MessageRoomState> = {}, previousItem?: TAnyMessageModel) => { const spy = jest.fn(); const Probe = () => { spy(useMessageAccessibilityLabel()); @@ -34,7 +34,7 @@ const renderLabel = (item: TAnyMessageModel, config: Record<string, any> = {}, p }; render( <Provider store={mockedStore}> - <MessageRoomProvider {...pickMessageRoomState(config)}> + <MessageRoomProvider {...config}> <MessageProvider item={item} previousItem={previousItem}> <Probe /> </MessageProvider> diff --git a/app/containers/message/stores/MessageRoomStore.tsx b/app/containers/message/stores/MessageRoomStore.tsx index 3bf1ab931ec..26edd56d8e2 100644 --- a/app/containers/message/stores/MessageRoomStore.tsx +++ b/app/containers/message/stores/MessageRoomStore.tsx @@ -43,51 +43,6 @@ export type MessageRoomState = { autoTranslateLanguage?: string; }; -// The room-scoped fields this store owns. Tests and stories use pickMessageRoomState to -// derive MessageRoomProvider props from a single fixture object. Add a field to -// MessageRoomState → add its key here. -const ROOM_STATE_KEYS: (keyof MessageRoomState)[] = [ - 'getCustomEmoji', - 'navToRoomInfo', - 'showAttachment', - 'blockAction', - 'handleEnterCall', - 'fetchThreadName', - 'toggleFollowThread', - 'jumpToMessage', - 'closeEmojiAndAction', - 'onReactionPress', - 'onReactionLongPress', - 'reactionInit', - 'onDiscussionPress', - 'onThreadPress', - 'replyBroadcast', - 'errorActionsShow', - 'onAnswerButtonPress', - 'onEncryptedPress', - 'archived', - 'isReadReceiptEnabled', - 'rid', - 'user', - 'baseUrl', - 'broadcast', - 'isThreadRoom', - 'Message_GroupingPeriod', - 'timeFormat', - 'autoTranslateRoom', - 'autoTranslateLanguage' -]; - -export const pickMessageRoomState = (source: Record<string, unknown> = {}): Partial<MessageRoomState> => { - const state: Record<string, unknown> = {}; - ROOM_STATE_KEYS.forEach(key => { - if (source[key] !== undefined) { - state[key] = source[key]; - } - }); - return state as Partial<MessageRoomState>; -}; - export const createMessageRoomStore = (initial: MessageRoomState) => createStore<MessageRoomState>(() => ({ ...initial })); export type MessageRoomStore = ReturnType<typeof createMessageRoomStore>; diff --git a/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx b/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx index ac3fd4af5b5..ac6055f9179 100644 --- a/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx @@ -1,7 +1,7 @@ import { act, render } from '@testing-library/react-native'; import { Provider } from 'react-redux'; -import { MessageRoomProvider, pickMessageRoomState, useTimeFormat } from '../MessageRoomStore'; +import { MessageRoomProvider, useTimeFormat } from '../MessageRoomStore'; import { mockedStore } from '../../../../reducers/mockedStore'; describe('MessageRoomStore', () => { @@ -13,7 +13,7 @@ describe('MessageRoomStore', () => { }; const wrap = (timeFormat: string) => ( <Provider store={mockedStore}> - <MessageRoomProvider {...pickMessageRoomState({ timeFormat })}> + <MessageRoomProvider timeFormat={timeFormat}> <Probe /> </MessageRoomProvider> </Provider> diff --git a/app/containers/message/stores/__tests__/MessageStore.test.tsx b/app/containers/message/stores/__tests__/MessageStore.test.tsx index 9793c266786..0ab9966cf2a 100644 --- a/app/containers/message/stores/__tests__/MessageStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageStore.test.tsx @@ -6,7 +6,7 @@ import { type TAnyMessageModel } from '../../../../definitions'; import { mockedStore } from '../../../../reducers/mockedStore'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../../../lib/constants/keys'; import { messagesStatus } from '../../../../lib/constants/messagesStatus'; -import { MessageRoomProvider, pickMessageRoomState, type MessageRoomState } from '../MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from '../MessageRoomStore'; import { MessageProvider, useAvatar, @@ -133,7 +133,7 @@ const renderDerived = ( }; render( <Provider store={mockedStore}> - <MessageRoomProvider {...pickMessageRoomState(config ?? {})}> + <MessageRoomProvider {...(config ?? {})}> <MessageProvider item={item} previousItem={previousItem}> <Probe /> </MessageProvider> @@ -497,7 +497,7 @@ describe('MessageStore', () => { }; const wrap = (previousItem: TAnyMessageModel) => ( <Provider store={mockedStore}> - <MessageRoomProvider {...pickMessageRoomState({ broadcast: false, Message_GroupingPeriod: 300 })}> + <MessageRoomProvider broadcast={false} Message_GroupingPeriod={300}> <MessageProvider item={item} previousItem={previousItem}> <Probe /> </MessageProvider> diff --git a/app/containers/message/testHelpers.tsx b/app/containers/message/testHelpers.tsx index 0da033e768f..33036518ae6 100644 --- a/app/containers/message/testHelpers.tsx +++ b/app/containers/message/testHelpers.tsx @@ -5,12 +5,11 @@ import { render } from '@testing-library/react-native'; import { type TAnyMessageModel } from '../../definitions'; import { mockedStore } from '../../reducers/mockedStore'; import { MessageProvider } from './stores/MessageStore'; -import { MessageRoomProvider, pickMessageRoomState, type MessageRoomState } from './stores/MessageRoomStore'; +import { MessageRoomProvider, type MessageRoomState } from './stores/MessageRoomStore'; interface IMessageProvidersOptions { item?: TAnyMessageModel; previousItem?: TAnyMessageModel; - context?: Record<string, unknown>; room?: Partial<MessageRoomState>; withRedux?: boolean; } @@ -18,12 +17,11 @@ interface IMessageProvidersOptions { export const MessageProviders = ({ item, previousItem, - context, room, withRedux = true, children }: IMessageProvidersOptions & { children: ReactNode }): ReactElement => { - const roomState = room ?? pickMessageRoomState(context ?? {}); + const roomState = room ?? {}; let tree: ReactNode = children; if (item) { From a01f1f5cc530dad08f28c35d3a95b3e093ef08c8 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 20:11:32 -0300 Subject: [PATCH 073/144] refactor: retire getCustomEmoji room-store field for useCustomEmoji hook --- .../Attachments/Attachments.stories.tsx | 1 - .../components/Attachments/Attachments.tsx | 5 +++-- .../message/components/Attachments/Quote.tsx | 4 ++-- .../components/Content/MarkdownContent.tsx | 5 +++-- .../message/components/MessageAvatar.tsx | 5 +++-- app/containers/message/components/Preview.tsx | 4 +--- .../message/components/Reactions.tsx | 11 +++-------- .../components/__tests__/Content.test.tsx | 1 - .../components/__tests__/Reactions.test.tsx | 5 +---- .../components/stories/Content.stories.tsx | 3 +-- .../components/stories/Message.stories.tsx | 19 ++++++++----------- .../stories/MessageAvatar.stories.tsx | 3 +-- .../components/stories/Reactions.stories.tsx | 1 - .../message/stores/MessageRoomStore.tsx | 3 --- app/views/MessagesView/index.tsx | 1 - app/views/RoomView/index.tsx | 1 - app/views/SearchMessagesView/index.tsx | 1 - 17 files changed, 26 insertions(+), 47 deletions(-) diff --git a/app/containers/message/components/Attachments/Attachments.stories.tsx b/app/containers/message/components/Attachments/Attachments.stories.tsx index d75cd768137..62691a24bf5 100644 --- a/app/containers/message/components/Attachments/Attachments.stories.tsx +++ b/app/containers/message/components/Attachments/Attachments.stories.tsx @@ -12,7 +12,6 @@ import Attachments from './Attachments'; const mockMessageContext: Partial<MessageRoomState> = { baseUrl: 'https://open.rocket.chat', user: { id: 'user-id', username: 'rocket.cat', token: 'token' }, - getCustomEmoji: () => null, showAttachment: undefined }; diff --git a/app/containers/message/components/Attachments/Attachments.tsx b/app/containers/message/components/Attachments/Attachments.tsx index ca17ec8a2a2..c836eb29a3b 100644 --- a/app/containers/message/components/Attachments/Attachments.tsx +++ b/app/containers/message/components/Attachments/Attachments.tsx @@ -7,18 +7,19 @@ import Video from './Video'; import CollapsibleQuote from './CollapsibleQuote'; import AttachedActions from './AttachedActions'; import Reply from './Reply'; -import { useShowAttachment, useGetCustomEmoji } from '../../stores/MessageRoomStore'; +import { useShowAttachment } from '../../stores/MessageRoomStore'; import { useTranslateLanguage } from '../../stores/MessageStore'; import { type IMessageAttachments } from '../../interfaces'; import { getMessageFromAttachment } from '../../utils'; import { isContentAttachment } from './utils'; +import { useCustomEmoji } from '../../../../lib/hooks/useCustomEmoji'; const Attachments: FC<IMessageAttachments> = ({ attachments, author }: IMessageAttachments) => { 'use memo'; const translateLanguage = useTranslateLanguage(); const showAttachment = useShowAttachment(); - const getCustomEmoji = useGetCustomEmoji() ?? (() => null); + const getCustomEmoji = useCustomEmoji(); const nonQuoteAttachments = attachments?.filter(isContentAttachment); diff --git a/app/containers/message/components/Attachments/Quote.tsx b/app/containers/message/components/Attachments/Quote.tsx index 273aa5a395a..de1a70639c6 100644 --- a/app/containers/message/components/Attachments/Quote.tsx +++ b/app/containers/message/components/Attachments/Quote.tsx @@ -6,14 +6,14 @@ import { type IMessageAttachments } from '../../interfaces'; import { type IAttachment } from '../../../../definitions'; import { getMessageFromAttachment } from '../../utils'; import { isQuoteAttachment } from './utils'; -import { useGetCustomEmoji } from '../../stores/MessageRoomStore'; import { useTranslateLanguage } from '../../stores/MessageStore'; +import { useCustomEmoji } from '../../../../lib/hooks/useCustomEmoji'; const Quote: FC<IMessageAttachments> = ({ attachments }: IMessageAttachments) => { 'use memo'; const translateLanguage = useTranslateLanguage(); - const getCustomEmoji = useGetCustomEmoji() ?? (() => null); + const getCustomEmoji = useCustomEmoji(); const quotes = attachments?.filter(isQuoteAttachment); diff --git a/app/containers/message/components/Content/MarkdownContent.tsx b/app/containers/message/components/Content/MarkdownContent.tsx index 14cdcb4a4e3..f7277e4d8c6 100644 --- a/app/containers/message/components/Content/MarkdownContent.tsx +++ b/app/containers/message/components/Content/MarkdownContent.tsx @@ -1,7 +1,8 @@ import Markdown from '../../../markdown'; import { useSetting } from '../../../../lib/hooks/useSetting'; +import { useCustomEmoji } from '../../../../lib/hooks/useCustomEmoji'; import { useMarkdownData, useMessageText, useOnLinkPress } from '../../stores/MessageStore'; -import { useGetCustomEmoji, useMessageUser, useNavToRoomInfo } from '../../stores/MessageRoomStore'; +import { useMessageUser, useNavToRoomInfo } from '../../stores/MessageRoomStore'; import ContentWrapper from './ContentWrapper'; const MarkdownContent = () => { @@ -10,7 +11,7 @@ const MarkdownContent = () => { const useRealName = useSetting('UI_Use_Real_Name') as boolean; const user = useMessageUser(); const onLinkPress = useOnLinkPress(); - const getCustomEmoji = useGetCustomEmoji() ?? (() => null); + const getCustomEmoji = useCustomEmoji(); const navToRoomInfo = useNavToRoomInfo(); const { md, mentions, channels, t: type } = useMarkdownData(); const { messageText, isTranslated } = useMessageText(); diff --git a/app/containers/message/components/MessageAvatar.tsx b/app/containers/message/components/MessageAvatar.tsx index 0c6ee0f24e6..18f246851a5 100644 --- a/app/containers/message/components/MessageAvatar.tsx +++ b/app/containers/message/components/MessageAvatar.tsx @@ -6,8 +6,9 @@ import styles from '../styles'; import { type IMessageAvatar } from '../interfaces'; import { SubscriptionType } from '../../../definitions'; import { useResponsiveLayout } from '../../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; +import { useCustomEmoji } from '../../../lib/hooks/useCustomEmoji'; import { useAvatar, useMessageField, useMessageGrouping } from '../stores/MessageStore'; -import { useGetCustomEmoji, useMessageUser, useNavToRoomInfo } from '../stores/MessageRoomStore'; +import { useMessageUser, useNavToRoomInfo } from '../stores/MessageRoomStore'; const AVATAR_BASE_SIZE = 36; @@ -22,7 +23,7 @@ const MessageAvatar = ({ small }: IMessageAvatar) => { const user = useMessageUser(); const navToRoomInfo = useNavToRoomInfo(); - const getCustomEmoji = useGetCustomEmoji() ?? (() => null); + const getCustomEmoji = useCustomEmoji(); const { fontScaleLimited } = useResponsiveLayout(); const { avatar, emoji } = useAvatar(); const author = useMessageField(item => item.u); diff --git a/app/containers/message/components/Preview.tsx b/app/containers/message/components/Preview.tsx index 7f395b96414..a260e5d1715 100644 --- a/app/containers/message/components/Preview.tsx +++ b/app/containers/message/components/Preview.tsx @@ -1,7 +1,6 @@ import Message from '../index'; import { MessageRoomProvider } from '../stores/MessageRoomStore'; import { useAppSelector } from '../../../lib/hooks/useAppSelector'; -import { useCustomEmoji } from '../../../lib/hooks/useCustomEmoji'; import { getUserSelector } from '../../../selectors/login'; import { type TAnyMessageModel } from '../../../definitions'; @@ -12,10 +11,9 @@ const MessagePreview = ({ message }: { message: TAnyMessageModel }) => { user: getUserSelector(state), baseUrl: state.server.server })); - const getCustomEmoji = useCustomEmoji(); return ( - <MessageRoomProvider user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} rid={message.rid}> + <MessageRoomProvider user={user} baseUrl={baseUrl} rid={message.rid}> <Message item={message} isPreview /> </MessageRoomProvider> ); diff --git a/app/containers/message/components/Reactions.tsx b/app/containers/message/components/Reactions.tsx index 104edc3a9ec..59577a721b0 100644 --- a/app/containers/message/components/Reactions.tsx +++ b/app/containers/message/components/Reactions.tsx @@ -7,14 +7,9 @@ import styles from '../styles'; import Emoji from './Emoji'; import { BUTTON_HIT_SLOP } from '../utils'; import { useTheme } from '../../../theme'; +import { useCustomEmoji } from '../../../lib/hooks/useCustomEmoji'; import { useMessageCtx, useMessageId, useReactions } from '../stores/MessageStore'; -import { - useGetCustomEmoji, - useMessageUser, - useOnReactionLongPress, - useOnReactionPress, - useReactionInit -} from '../stores/MessageRoomStore'; +import { useMessageUser, useOnReactionLongPress, useOnReactionPress, useReactionInit } from '../stores/MessageRoomStore'; interface IReaction { _id: string; @@ -60,7 +55,7 @@ const Reaction = ({ reaction }: IMessageReaction) => { const onReactionPress = useOnReactionPress(); const onReactionLongPress = useOnReactionLongPress(); const user = useMessageUser(); - const getCustomEmoji = useGetCustomEmoji() ?? (() => null); + const getCustomEmoji = useCustomEmoji(); const { fontScale } = useWindowDimensions(); const height = 28 * fontScale; const reacted = reaction.usernames.findIndex((item: string) => item === user?.username) !== -1; diff --git a/app/containers/message/components/__tests__/Content.test.tsx b/app/containers/message/components/__tests__/Content.test.tsx index b15a93786bc..6289edc307c 100644 --- a/app/containers/message/components/__tests__/Content.test.tsx +++ b/app/containers/message/components/__tests__/Content.test.tsx @@ -54,7 +54,6 @@ const tree = (overrides: TOverrides) => { const { msg, attachments, isEncrypted, autoTranslateLanguage, tmid, isIgnored } = overrides; const contextValue: Partial<MessageRoomState> = { user: { username: 'john' }, - getCustomEmoji: jest.fn(), navToRoomInfo: jest.fn(), autoTranslateLanguage }; diff --git a/app/containers/message/components/__tests__/Reactions.test.tsx b/app/containers/message/components/__tests__/Reactions.test.tsx index ecf5fa8d2cd..08014f8c463 100644 --- a/app/containers/message/components/__tests__/Reactions.test.tsx +++ b/app/containers/message/components/__tests__/Reactions.test.tsx @@ -20,8 +20,6 @@ const initialMockedStoreState = () => { initialMockedStoreState(); -const getCustomEmoji = jest.fn(); - type FakeItem = TAnyMessageModel & { reactions: IReaction[]; _emit: () => void }; // Mirrors the MessageStore.test.tsx fake-model pattern: a stable item mutated in place, @@ -58,8 +56,7 @@ const TestWrapper = ({ item, onReactionPress }: { item: FakeItem; onReactionPres user: { username: 'john' }, reactionInit: jest.fn(), onReactionPress, - onReactionLongPress: jest.fn(), - getCustomEmoji + onReactionLongPress: jest.fn() }}> <Reactions /> </MessageProviders> diff --git a/app/containers/message/components/stories/Content.stories.tsx b/app/containers/message/components/stories/Content.stories.tsx index aa2ae8adc35..87df5e33651 100644 --- a/app/containers/message/components/stories/Content.stories.tsx +++ b/app/containers/message/components/stories/Content.stories.tsx @@ -18,8 +18,7 @@ const item = { } as unknown as TAnyMessageModel; const room: Partial<MessageRoomState> = { - user: { id: 'reader-id', username: 'reader' }, - getCustomEmoji: () => null + user: { id: 'reader-id', username: 'reader' } }; const StoryWrapper = ({ children }: { children: ReactNode }) => ( diff --git a/app/containers/message/components/stories/Message.stories.tsx b/app/containers/message/components/stories/Message.stories.tsx index 27e91acfc3d..5ef3dd261e9 100644 --- a/app/containers/message/components/stories/Message.stories.tsx +++ b/app/containers/message/components/stories/Message.stories.tsx @@ -2,7 +2,6 @@ import { ScrollView } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; import MessageContainer from '../../index'; -import { type ICustomEmoji } from '../../../../definitions/IEmoji'; import { type TAnyMessageModel } from '../../../../definitions'; import { E2E_MESSAGE_TYPE } from '../../../../lib/constants/keys'; import { messagesStatus } from '../../../../lib/constants/messagesStatus'; @@ -16,6 +15,7 @@ import { } from '../../../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; import { mockedStore as store } from '../../../../reducers/mockedStore'; import { updateSettings } from '../../../../actions/settings'; +import { setCustomEmojis } from '../../../../actions/customEmojis'; import { createInteractionStore, InteractionStoreContext } from '../../../../views/RoomView/InteractionStore'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; @@ -55,6 +55,13 @@ const longText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'; store.dispatch(updateSettings('API_Embed', true)); +store.dispatch( + setCustomEmojis({ + marioparty: { name: 'marioparty', extension: 'gif' }, + react_rocket: { name: 'react_rocket', extension: 'png' }, + nyan_rocket: { name: 'nyan_rocket', extension: 'png' } + }) +); const responsiveLayoutProviderLargeFontValue = (fontScale: number) => ({ fontScale, @@ -66,15 +73,6 @@ const responsiveLayoutProviderLargeFontValue = (fontScale: number) => ({ height: 800 }); -const getCustomEmoji = (content: string): ICustomEmoji | null => { - const customEmoji = { - marioparty: { name: content, extension: 'gif' }, - react_rocket: { name: content, extension: 'png' }, - nyan_rocket: { name: content, extension: 'png' } - }[content]; - return customEmoji ?? null; -}; - const containerHandlers = { onPress: () => {}, onLongPress: () => {}, @@ -93,7 +91,6 @@ const roomHandlers: Partial<MessageRoomState> = { onReactionLongPress: () => {}, onAnswerButtonPress: () => {}, jumpToMessage: () => {}, - getCustomEmoji, navToRoomInfo: () => {}, showAttachment: undefined, blockAction: undefined, diff --git a/app/containers/message/components/stories/MessageAvatar.stories.tsx b/app/containers/message/components/stories/MessageAvatar.stories.tsx index aa09b1835d0..59ab1850732 100644 --- a/app/containers/message/components/stories/MessageAvatar.stories.tsx +++ b/app/containers/message/components/stories/MessageAvatar.stories.tsx @@ -19,8 +19,7 @@ const item = { } as unknown as TAnyMessageModel; const room: Partial<MessageRoomState> = { - user: { id: 'reader-id', username: 'reader' }, - getCustomEmoji: () => null + user: { id: 'reader-id', username: 'reader' } }; const StoryWrapper = ({ children }: { children: ReactNode }) => ( diff --git a/app/containers/message/components/stories/Reactions.stories.tsx b/app/containers/message/components/stories/Reactions.stories.tsx index 02d697a20dd..77af06be29c 100644 --- a/app/containers/message/components/stories/Reactions.stories.tsx +++ b/app/containers/message/components/stories/Reactions.stories.tsx @@ -22,7 +22,6 @@ const item = { const room: Partial<MessageRoomState> = { user: { id: 'reader-id', username: 'rocket.cat' }, - getCustomEmoji: () => null, onReactionPress: () => {}, onReactionLongPress: () => {}, reactionInit: () => {} diff --git a/app/containers/message/stores/MessageRoomStore.tsx b/app/containers/message/stores/MessageRoomStore.tsx index 26edd56d8e2..97e47d2f4de 100644 --- a/app/containers/message/stores/MessageRoomStore.tsx +++ b/app/containers/message/stores/MessageRoomStore.tsx @@ -2,14 +2,12 @@ import { createContext, useContext, useEffect, useState, type ReactElement, type import { createStore, useStore } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; -import { type TGetCustomEmoji } from '../../../definitions/IEmoji'; import { type IAttachment, type TAnyMessageModel } from '../../../definitions'; import { type IRoomInfoParam } from '../../../views/SearchMessagesView'; import { useSetting } from '../../../lib/hooks/useSetting'; export type MessageRoomState = { // stable handlers - getCustomEmoji?: TGetCustomEmoji; navToRoomInfo?: (navParam: IRoomInfoParam) => void; showAttachment?: (file: IAttachment) => void; blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void; @@ -74,7 +72,6 @@ export const MessageRoomProvider = ({ children, ...state }: { children: ReactNod return <MessageRoomStoreContext.Provider value={store}>{children}</MessageRoomStoreContext.Provider>; }; -export const useGetCustomEmoji = (): TGetCustomEmoji | undefined => useMessageRoomStore(s => s.getCustomEmoji); export const useNavToRoomInfo = (): ((navParam: IRoomInfoParam) => void) | undefined => useMessageRoomStore(s => s.navToRoomInfo); export const useShowAttachment = (): ((file: IAttachment) => void) | undefined => useMessageRoomStore(s => s.showAttachment); export const useBlockAction = (): MessageRoomState['blockAction'] => useMessageRoomStore(s => s.blockAction); diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index 001eafb7ed2..979df8dac1b 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -353,7 +353,6 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { return ( <SafeAreaView style={{ backgroundColor: themes[theme].surfaceRoom }} testID={this.content.testID}> <MessageRoomProvider - getCustomEmoji={this.getCustomEmoji} navToRoomInfo={this.navToRoomInfo} showAttachment={this.showAttachment} user={user} diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index a3f18a637eb..0bed8dc4f05 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1661,7 +1661,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { /> ) : null} <MessageRoomProvider - getCustomEmoji={this.getCustomEmoji} navToRoomInfo={this.navToRoomInfo} showAttachment={this.showAttachment} blockAction={this.blockAction} diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index 505731f837a..d2e2186464a 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -318,7 +318,6 @@ class SearchMessagesView extends Component<ISearchMessagesViewProps, ISearchMess return ( <MessageRoomProvider - getCustomEmoji={this.getCustomEmoji} navToRoomInfo={this.navToRoomInfo} showAttachment={this.showAttachment} jumpToMessage={this.jumpToMessageByUrl} From 4af3c807e4d711cc042e22c3de7d1fd1d986d7ff Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 20:14:06 -0300 Subject: [PATCH 074/144] refactor: remove dead getCustomEmoji methods from message list views --- app/views/MessagesView/index.tsx | 10 ---------- app/views/SearchMessagesView/index.tsx | 10 ---------- 2 files changed, 20 deletions(-) diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index 979df8dac1b..5c2ddedab73 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -29,7 +29,6 @@ import { type IMessage, type TAnyMessageModel, type IUrl, - type TGetCustomEmoji, type ICustomEmoji } from '../../definitions'; import { getFiles, getMessages, getPinnedMessages, togglePinMessage, toggleStarMessage } from '../../lib/services/restApi'; @@ -287,15 +286,6 @@ class MessagesView extends Component<IMessagesViewProps, IMessagesViewState> { } }; - getCustomEmoji: TGetCustomEmoji = name => { - const { customEmojis } = this.props; - const emoji = customEmojis[name]; - if (emoji) { - return emoji; - } - return null; - }; - showAttachment = (attachment: IAttachment) => { const { navigation } = this.props; navigation.navigate('AttachmentView', { attachment }); diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index d2e2186464a..3fc09cbcb32 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -37,7 +37,6 @@ import { type ISubscription, SubscriptionType, type TSubscriptionModel, - type TGetCustomEmoji, type ICustomEmoji } from '../../definitions'; import { searchMessages } from '../../lib/services/restApi'; @@ -208,15 +207,6 @@ class SearchMessagesView extends Component<ISearchMessagesViewProps, ISearchMess await this.getMessages(searchText, true); }, textInputDebounceTime); - getCustomEmoji: TGetCustomEmoji = name => { - const { customEmojis } = this.props; - const emoji = customEmojis[name]; - if (emoji) { - return emoji; - } - return null; - }; - showAttachment = (attachment: IAttachment) => { const { navigation } = this.props; navigation.navigate('AttachmentView', { attachment }); From 49ecae7d5f29d700b75c18651640be1a22b15594 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 3 Jul 2026 20:22:54 -0300 Subject: [PATCH 075/144] refactor: add explicit deps to MessageStore mirror effect, document MessageRoomStore's --- app/containers/message/stores/MessageRoomStore.tsx | 8 +++++--- app/containers/message/stores/MessageStore.tsx | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/containers/message/stores/MessageRoomStore.tsx b/app/containers/message/stores/MessageRoomStore.tsx index 97e47d2f4de..889101cd95d 100644 --- a/app/containers/message/stores/MessageRoomStore.tsx +++ b/app/containers/message/stores/MessageRoomStore.tsx @@ -62,9 +62,11 @@ export const MessageRoomProvider = ({ children, ...state }: { children: ReactNod const resolvedState = { ...state, timeFormat: state.timeFormat ?? Message_TimeFormat }; const [store] = useState(() => createMessageRoomStore(resolvedState)); - // No dependency array: the store must mirror the provider's props every render. - // setState notifies every listener on each render, but handlers/constants are stable - // refs, so each selector's snapshot stays Object.is-equal and no consumer re-renders. + // Runs every render by design: resolvedState is rebuilt from props each render, so it can never + // be a stable dependency (state is a rest-spread, fresh each render — useMemo can't stabilize it). + // Per-field Object.is bail in each selector means this mirror only re-renders a consumer whose own + // field changed. A field-level deps array would reintroduce the hand-maintained key list the zustand + // migration deliberately removed. useEffect(() => { store.setState(resolvedState); }); diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index 82ceb3f4987..ee83c0e0da9 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -100,7 +100,7 @@ export const MessageProvider = ({ // Mirror per-message row handlers so field-level selectors subscribe without churning the context value. useEffect(() => { store.setState({ onPress, onLongPress, threadBadgeColor }); - }); + }, [onPress, onLongPress, threadBadgeColor, store]); const revealIgnored = () => store.setState({ manualUnignored: true }); From e84f43695629826affe1928d53ecf42194d6e330 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 6 Jul 2026 11:13:30 -0300 Subject: [PATCH 076/144] perf: memoize returned closures from useMessagePress/useMessageLongPress Add 'use memo' so React Compiler (annotation mode) returns a stable closure when inputs are unchanged, instead of a fresh arrow each call. --- app/containers/message/stores/MessageStore.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index ee83c0e0da9..3a6a052d90b 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -332,6 +332,8 @@ export const useRevealIgnored = (): (() => void) => { }; export const useMessageLongPress = (): (() => void) => { + 'use memo'; + const { item, store } = useMessageCtx(); const isInfo = useIsInfo(); const { hasError } = useMessageStatus(); @@ -360,6 +362,8 @@ export const useOnLinkPress = (): ((link: string) => void) => { }; export const useMessagePress = (): (() => void) => { + 'use memo'; + const { item, store } = useMessageCtx(); const isThreadRoom = useIsThreadRoom(); const onPress = useStore(store, s => s.onPress); From 541e85b44b2a99e58a2bc328b68a099c0b8bff37 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 6 Jul 2026 12:18:30 -0300 Subject: [PATCH 077/144] perf: de-churn RoomView on incoming messages - hideSystemMessages returns stable refs (model field / redux prop / shared empty) instead of a fresh [] each render, so the message-list WatermelonDB query stops re-subscribing on every RoomView render - RoomView.shouldComponentUpdate ignores lastMessage for non-livechat rooms (livechat still uses it for on-hold header updates), so a new message no longer forces a full RoomView re-render --- app/views/RoomView/index.tsx | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 0bed8dc4f05..336f8251d8e 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -117,6 +117,8 @@ import { InvitedRoom } from './components/InvitedRoom'; import { getInvitationData } from '../../lib/methods/getInvitationData'; import { isInviteSubscription } from '../../lib/methods/isInviteSubscription'; +const EMPTY_HIDE_SYSTEM_MESSAGES: string[] = []; + class RoomView extends Component<IRoomViewProps, IRoomViewState> { private rid?: string; private t?: string; @@ -299,7 +301,12 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { if (!dequal(nextProps.route?.params, route?.params)) { return true; } - return roomAttrsUpdate.some(key => !dequal(nextState.roomUpdate[key], roomUpdate[key])); + return roomAttrsUpdate.some(key => { + if (key === 'lastMessage' && this.t !== 'l') { + return false; + } + return !dequal(nextState.roomUpdate[key], roomUpdate[key]); + }); } componentDidUpdate(prevProps: IRoomViewProps, prevState: IRoomViewState) { @@ -447,11 +454,15 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { const { Hide_System_Messages } = this.props; // FIXME: handle servers with version < 3.0.0 - let hideSystemMessages = Array.isArray(sysMes) ? sysMes : Hide_System_Messages; - if (!Array.isArray(hideSystemMessages)) { - hideSystemMessages = []; + // Return stable refs (model field / redux prop / shared empty) — a fresh [] here re-subscribes + // the message-list WatermelonDB query on every RoomView render (fetchMessages dep). + if (Array.isArray(sysMes)) { + return sysMes; + } + if (Array.isArray(Hide_System_Messages)) { + return Hide_System_Messages; } - return hideSystemMessages ?? []; + return EMPTY_HIDE_SYSTEM_MESSAGES; } setHeader = () => { From a3e289813a9996811e8df71c0c739dc572ca6097 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 6 Jul 2026 13:35:37 -0300 Subject: [PATCH 078/144] fix: re-hide a revealed ignored message when its ignore state changes A manually revealed ignored message stayed revealed forever because the manual-reveal flag was never reset. It now resets when the isIgnored prop transitions, so the message re-hides; a benign re-render with the same value keeps the reveal. This rides on converting MessageStore to a store-only context, matching the MessageRoomStore and InteractionStore siblings: item, previousItem and isIgnored move into the zustand store state, the context carries only the store ref, field/domain/derived hooks read state inside their selectors via a private useMessageStore helper, reveal is a stable action built in the store initializer, and useMessageCtx is retired in favour of useMessageItem. --- .../message/components/Broadcast.tsx | 4 +- .../message/components/Discussion.tsx | 4 +- .../message/components/Reactions.tsx | 4 +- .../components/RightIcons/MessageError.tsx | 4 +- app/containers/message/components/Thread.tsx | 4 +- .../message/stores/MessageStore.tsx | 244 +++++++----------- .../stores/__tests__/MessageStore.test.tsx | 53 +++- 7 files changed, 154 insertions(+), 163 deletions(-) diff --git a/app/containers/message/components/Broadcast.tsx b/app/containers/message/components/Broadcast.tsx index 4bd5e28f3fe..5ef7aed1491 100644 --- a/app/containers/message/components/Broadcast.tsx +++ b/app/containers/message/components/Broadcast.tsx @@ -7,13 +7,13 @@ import { BUTTON_HIT_SLOP } from '../utils'; import I18n from '../../../i18n'; import { themes } from '../../../lib/constants/colors'; import { useTheme } from '../../../theme'; -import { useMessageAuthor, useMessageCtx } from '../stores/MessageStore'; +import { useMessageAuthor, useMessageItem } from '../stores/MessageStore'; import { useBroadcast, useMessageUser, useReplyBroadcast } from '../stores/MessageRoomStore'; const Broadcast = () => { 'use memo'; - const { item } = useMessageCtx(); + const item = useMessageItem(); const user = useMessageUser(); const replyBroadcast = useReplyBroadcast(); const broadcast = useBroadcast(); diff --git a/app/containers/message/components/Discussion.tsx b/app/containers/message/components/Discussion.tsx index 3c2b6631305..c1eab94c1ab 100644 --- a/app/containers/message/components/Discussion.tsx +++ b/app/containers/message/components/Discussion.tsx @@ -8,14 +8,14 @@ import { CustomIcon } from '../../CustomIcon'; import { DISCUSSION } from '../constants'; import { formatDateThreads } from '../../../lib/methods/helpers/room'; import { useTheme } from '../../../theme'; -import { useDiscussion, useMessageCtx, useMessageText } from '../stores/MessageStore'; +import { useDiscussion, useMessageItem, useMessageText } from '../stores/MessageStore'; import { useOnDiscussionPress } from '../stores/MessageRoomStore'; const Discussion = () => { 'use memo'; const { colors } = useTheme(); - const { item } = useMessageCtx(); + const item = useMessageItem(); const { dcount, dlm } = useDiscussion(); const { messageText } = useMessageText(); let time; diff --git a/app/containers/message/components/Reactions.tsx b/app/containers/message/components/Reactions.tsx index 59577a721b0..0605c20a309 100644 --- a/app/containers/message/components/Reactions.tsx +++ b/app/containers/message/components/Reactions.tsx @@ -8,7 +8,7 @@ import Emoji from './Emoji'; import { BUTTON_HIT_SLOP } from '../utils'; import { useTheme } from '../../../theme'; import { useCustomEmoji } from '../../../lib/hooks/useCustomEmoji'; -import { useMessageCtx, useMessageId, useReactions } from '../stores/MessageStore'; +import { useMessageId, useMessageItem, useReactions } from '../stores/MessageStore'; import { useMessageUser, useOnReactionLongPress, useOnReactionPress, useReactionInit } from '../stores/MessageRoomStore'; interface IReaction { @@ -50,7 +50,7 @@ const Reaction = ({ reaction }: IMessageReaction) => { 'use memo'; const { colors } = useTheme(); - const { item } = useMessageCtx(); + const item = useMessageItem(); const id = useMessageId(); const onReactionPress = useOnReactionPress(); const onReactionLongPress = useOnReactionLongPress(); diff --git a/app/containers/message/components/RightIcons/MessageError.tsx b/app/containers/message/components/RightIcons/MessageError.tsx index 8a07d06bb88..35071ed55f4 100644 --- a/app/containers/message/components/RightIcons/MessageError.tsx +++ b/app/containers/message/components/RightIcons/MessageError.tsx @@ -4,14 +4,14 @@ import styles from '../../styles'; import { BUTTON_HIT_SLOP } from '../../utils'; import { themes } from '../../../../lib/constants/colors'; import { useTheme } from '../../../../theme'; -import { useMessageCtx, useMessageStatus } from '../../stores/MessageStore'; +import { useMessageItem, useMessageStatus } from '../../stores/MessageStore'; import { useErrorActionsShow } from '../../stores/MessageRoomStore'; const MessageError = () => { 'use memo'; const { theme } = useTheme(); - const { item } = useMessageCtx(); + const item = useMessageItem(); const errorActionsShow = useErrorActionsShow(); const { hasError } = useMessageStatus(); diff --git a/app/containers/message/components/Thread.tsx b/app/containers/message/components/Thread.tsx index cf1740f1ae9..6bc29e4e313 100644 --- a/app/containers/message/components/Thread.tsx +++ b/app/containers/message/components/Thread.tsx @@ -5,14 +5,14 @@ import ThreadDetails from '../../ThreadDetails'; import I18n from '../../../i18n'; import { useTheme } from '../../../theme'; import Touchable from './Touchable'; -import { useMessageCtx, useMessageText, useReplies, useThreadBadgeColor, useThreadData } from '../stores/MessageStore'; +import { useMessageItem, useMessageText, useReplies, useThreadBadgeColor, useThreadData } from '../stores/MessageStore'; import { useIsThreadRoom, useMessageUser, useOnThreadPress, useToggleFollowThread } from '../stores/MessageRoomStore'; const Thread = () => { 'use memo'; const { theme, colors } = useTheme(); - const { item } = useMessageCtx(); + const item = useMessageItem(); const isThreadRoom = useIsThreadRoom(); const threadBadgeColor = useThreadBadgeColor(); const toggleFollowThread = useToggleFollowThread(); diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index 3a6a052d90b..e7e8c6729e8 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -25,44 +25,46 @@ import { type MessageStoreState = { tick: number; + item: TAnyMessageModel; + previousItem?: TAnyMessageModel; + isIgnored: boolean; manualUnignored: boolean; + reveal: () => void; onPress?: () => void; onLongPress?: (item: TAnyMessageModel) => void; threadBadgeColor?: string; }; -const createMessageStore = (initial: Partial<MessageStoreState>) => - createStore<MessageStoreState>(() => ({ tick: 0, manualUnignored: false, ...initial })); +const createMessageStore = ( + initial: Pick<MessageStoreState, 'item' | 'previousItem' | 'isIgnored' | 'onPress' | 'onLongPress' | 'threadBadgeColor'> +) => + createStore<MessageStoreState>(set => ({ + tick: 0, + manualUnignored: false, + reveal: () => set({ manualUnignored: true }), + ...initial + })); type MessageStore = ReturnType<typeof createMessageStore>; -type MessageCtxValue = { - store: MessageStore; - item: TAnyMessageModel; - previousItem?: TAnyMessageModel; - revealIgnored: () => void; - isIgnored: boolean; -}; - -const MessageStoreContext = createContext<MessageCtxValue | null>(null); +const MessageStoreContext = createContext<MessageStore | null>(null); -export const useMessageCtx = (): MessageCtxValue => { - const ctx = useContext(MessageStoreContext); - if (!ctx) { +const useMessageStore = <T,>(selector: (state: MessageStoreState) => T): T => { + const store = useContext(MessageStoreContext); + if (!store) { throw new Error('Message hooks must be used within a MessageProvider'); } - return ctx; + return useStore(store, selector); }; +export const useMessageItem = (): TAnyMessageModel => useMessageStore(s => s.item); + // The selector must return a referentially stable value for an unchanged field (Object.is bail); // for multiple fields use a focused domain hook with useShallow, never call this hook repeatedly. // Stability of JSON fields depends on the model's @json(..., { memo: true }) decorators // (app/lib/database/model/Message.js); a field that drops memo:true returns a fresh ref every // access and would defeat both the Object.is and useShallow bail. -export const useMessageField = <T,>(selector: (item: TAnyMessageModel) => T): T => { - const { store, item } = useMessageCtx(); - return useStore(store, () => selector(item)); -}; +export const useMessageField = <T,>(selector: (item: TAnyMessageModel) => T): T => useMessageStore(s => selector(s.item)); // Plain REST objects (no experimentalSubscribe) never emit again after the initial render. const subscribeModel = (m: TAnyMessageModel, store: MessageStore) => { @@ -89,7 +91,9 @@ export const MessageProvider = ({ }): ReactElement => { 'use memo'; - const [store] = useState(() => createMessageStore({ onPress, onLongPress, threadBadgeColor })); + const [store] = useState(() => + createMessageStore({ item, previousItem, isIgnored: isIgnored ?? false, onPress, onLongPress, threadBadgeColor }) + ); // Header grouping and thread position depend on the previous record too, so each effect // subscribes one record; both feed the same tick. Keeping them separate means changing @@ -102,107 +106,63 @@ export const MessageProvider = ({ store.setState({ onPress, onLongPress, threadBadgeColor }); }, [onPress, onLongPress, threadBadgeColor, store]); - const revealIgnored = () => store.setState({ manualUnignored: true }); + // Push item/previousItem prop changes into the store so field selectors re-read (temp→server swap, neighbour changes). + useEffect(() => { + store.setState({ item, previousItem }); + }, [item, previousItem, store]); - return ( - <MessageStoreContext.Provider value={{ store, item, previousItem, revealIgnored, isIgnored: isIgnored ?? false }}> - {children} - </MessageStoreContext.Provider> - ); + // Reset the manual reveal only when the ignore state actually transitions. + useEffect(() => { + store.setState({ isIgnored: isIgnored ?? false, manualUnignored: false }); + }, [isIgnored, store]); + + return <MessageStoreContext.Provider value={store}>{children}</MessageStoreContext.Provider>; }; export const useReactions = (): TAnyMessageModel['reactions'] => useMessageField(item => item.reactions); export const useUrls = (): TAnyMessageModel['urls'] => useMessageField(item => item.urls); -export const useBlocks = (): Pick<TAnyMessageModel, 'blocks' | 'id'> => { - const { store, item } = useMessageCtx(); - return useStore( - store, - useShallow(() => ({ blocks: item.blocks, id: item.id })) - ); -}; +export const useBlocks = (): Pick<TAnyMessageModel, 'blocks' | 'id'> => + useMessageStore(useShallow(s => ({ blocks: s.item.blocks, id: s.item.id }))); -export const useDiscussion = (): Pick<TAnyMessageModel, 'dcount' | 'dlm'> => { - const { store, item } = useMessageCtx(); - return useStore( - store, - useShallow(() => ({ dcount: item.dcount, dlm: item.dlm })) - ); -}; +export const useDiscussion = (): Pick<TAnyMessageModel, 'dcount' | 'dlm'> => + useMessageStore(useShallow(s => ({ dcount: s.item.dcount, dlm: s.item.dlm }))); -export const useThreadData = (): Pick<TAnyMessageModel, 'tcount' | 'tlm' | 'tmid' | 'id'> => { - const { store, item } = useMessageCtx(); - return useStore( - store, - useShallow(() => ({ tcount: item.tcount, tlm: item.tlm, tmid: item.tmid, id: item.id })) - ); -}; +export const useThreadData = (): Pick<TAnyMessageModel, 'tcount' | 'tlm' | 'tmid' | 'id'> => + useMessageStore(useShallow(s => ({ tcount: s.item.tcount, tlm: s.item.tlm, tmid: s.item.tmid, id: s.item.id }))); -export const useRepliedThreadData = (): Pick<TAnyMessageModel, 'tmid' | 'tmsg' | 'id'> => { - const { store, item } = useMessageCtx(); - return useStore( - store, - useShallow(() => ({ tmid: item.tmid, tmsg: item.tmsg, id: item.id })) - ); -}; +export const useRepliedThreadData = (): Pick<TAnyMessageModel, 'tmid' | 'tmsg' | 'id'> => + useMessageStore(useShallow(s => ({ tmid: s.item.tmid, tmsg: s.item.tmsg, id: s.item.id }))); -export const useMessageAuthor = (): Pick<TAnyMessageModel, 'u' | 'alias' | 'role'> => { - const { store, item } = useMessageCtx(); - return useStore( - store, - useShallow(() => ({ u: item.u, alias: item.alias, role: item.role })) - ); -}; +export const useMessageAuthor = (): Pick<TAnyMessageModel, 'u' | 'alias' | 'role'> => + useMessageStore(useShallow(s => ({ u: s.item.u, alias: s.item.alias, role: s.item.role }))); -export const useAvatar = (): Pick<TAnyMessageModel, 'avatar' | 'emoji'> => { - const { store, item } = useMessageCtx(); - return useStore( - store, - useShallow(() => ({ avatar: item.avatar, emoji: item.emoji })) - ); -}; +export const useAvatar = (): Pick<TAnyMessageModel, 'avatar' | 'emoji'> => + useMessageStore(useShallow(s => ({ avatar: s.item.avatar, emoji: s.item.emoji }))); -export const useContentData = (): Pick<TAnyMessageModel, 'md' | 'mentions' | 'channels' | 'comment' | 'attachments' | 't'> => { - const { store, item } = useMessageCtx(); - return useStore( - store, - useShallow(() => ({ - md: item.md, - mentions: item.mentions, - channels: item.channels, - comment: item.comment, - attachments: item.attachments, - t: item.t +export const useContentData = (): Pick<TAnyMessageModel, 'md' | 'mentions' | 'channels' | 'comment' | 'attachments' | 't'> => + useMessageStore( + useShallow(s => ({ + md: s.item.md, + mentions: s.item.mentions, + channels: s.item.channels, + comment: s.item.comment, + attachments: s.item.attachments, + t: s.item.t })) ); -}; -export const useInfoData = (): Pick<TAnyMessageModel, 't' | 'comment'> => { - const { store, item } = useMessageCtx(); - return useStore( - store, - useShallow(() => ({ t: item.t, comment: item.comment })) - ); -}; +export const useInfoData = (): Pick<TAnyMessageModel, 't' | 'comment'> => + useMessageStore(useShallow(s => ({ t: s.item.t, comment: s.item.comment }))); -export const useMarkdownData = (): Pick<TAnyMessageModel, 'md' | 'mentions' | 'channels' | 't'> => { - const { store, item } = useMessageCtx(); - return useStore( - store, - useShallow(() => ({ md: item.md, mentions: item.mentions, channels: item.channels, t: item.t })) - ); -}; +export const useMarkdownData = (): Pick<TAnyMessageModel, 'md' | 'mentions' | 'channels' | 't'> => + useMessageStore(useShallow(s => ({ md: s.item.md, mentions: s.item.mentions, channels: s.item.channels, t: s.item.t }))); export const useAttachments = (): TAnyMessageModel['attachments'] => useMessageField(item => item.attachments); -export const useMessageHeaderMeta = (): Pick<TAnyMessageModel, 'ts' | 'unread' | 'pinned' | 't'> => { - const { store, item } = useMessageCtx(); - return useStore( - store, - useShallow(() => ({ ts: item.ts, unread: item.unread, pinned: item.pinned, t: item.t })) - ); -}; +export const useMessageHeaderMeta = (): Pick<TAnyMessageModel, 'ts' | 'unread' | 'pinned' | 't'> => + useMessageStore(useShallow(s => ({ ts: s.item.ts, unread: s.item.unread, pinned: s.item.pinned, t: s.item.t }))); const computeIsHeader = ( prev: TAnyMessageModel | undefined, @@ -236,39 +196,38 @@ const computeIsHeader = ( }; export const useMessageGrouping = (): boolean => { - const { store, item, previousItem } = useMessageCtx(); const broadcast = useBroadcast(); const Message_GroupingPeriod = useMessageGroupingPeriod(); - return useStore(store, () => - computeIsHeader(previousItem, item, !!broadcast, Message_GroupingPeriod, item.status === messagesStatus.ERROR) + return useMessageStore(s => + computeIsHeader(s.previousItem, s.item, !!broadcast, Message_GroupingPeriod, s.item.status === messagesStatus.ERROR) ); }; export const useThreadPosition = (): { isThreadReply: boolean; isThreadSequential: boolean } => { - const { store, item, previousItem } = useMessageCtx(); const isThreadRoom = useIsThreadRoom(); - return useStore( - store, - useShallow(() => { + return useMessageStore( + useShallow(s => { if (isThreadRoom) { return { isThreadReply: false, isThreadSequential: false }; } - const isThreadReply = !!(previousItem && item.tmid && previousItem.tmid !== item.tmid && previousItem.id !== item.tmid); - return { isThreadReply, isThreadSequential: !!item.tmid }; + const isThreadReply = !!( + s.previousItem && + s.item.tmid && + s.previousItem.tmid !== s.item.tmid && + s.previousItem.id !== s.item.tmid + ); + return { isThreadReply, isThreadSequential: !!s.item.tmid }; }) ); }; -export const useMessageStatus = (): { hasError: boolean; isTemp: boolean } => { - const { store, item } = useMessageCtx(); - return useStore( - store, - useShallow(() => ({ - hasError: item.status === messagesStatus.ERROR, - isTemp: item.status === messagesStatus.TEMP || item.status === messagesStatus.ERROR +export const useMessageStatus = (): { hasError: boolean; isTemp: boolean } => + useMessageStore( + useShallow(s => ({ + hasError: s.item.status === messagesStatus.ERROR, + isTemp: s.item.status === messagesStatus.TEMP || s.item.status === messagesStatus.ERROR })) ); -}; export const useIsEncrypted = (): boolean => useMessageField(item => item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE); @@ -287,28 +246,25 @@ export const useMessageId = (): TAnyMessageModel['id'] => useMessageField(item = export const useReplies = (): TAnyMessageModel['replies'] => useMessageField(item => item.replies); export const useTranslateLanguage = (): string | undefined => { - const { store, item } = useMessageCtx(); const { autoTranslateRoom, autoTranslateLanguage } = useAutoTranslate(); const user = useMessageUser(); - return useStore(store, () => { - const otherUserMessage = item.u?.username !== user?.username; - const canTranslate = autoTranslateRoom && autoTranslateLanguage && item.autoTranslate !== false && otherUserMessage; + return useMessageStore(s => { + const otherUserMessage = s.item.u?.username !== user?.username; + const canTranslate = autoTranslateRoom && autoTranslateLanguage && s.item.autoTranslate !== false && otherUserMessage; return canTranslate ? autoTranslateLanguage : undefined; }); }; export const useMessageText = (): { messageText: TAnyMessageModel['msg']; isTranslated: boolean } => { - const { store, item } = useMessageCtx(); const user = useMessageUser(); const { autoTranslateRoom, autoTranslateLanguage } = useAutoTranslate(); - return useStore( - store, - useShallow(() => { - let messageText = item.msg; + return useMessageStore( + useShallow(s => { + let messageText = s.item.msg; let isTranslated = false; - const otherUserMessage = item.u?.username !== user?.username; - if (autoTranslateRoom && item.autoTranslate && autoTranslateLanguage && otherUserMessage) { - const translated = getMessageTranslation(item, autoTranslateLanguage); + const otherUserMessage = s.item.u?.username !== user?.username; + if (autoTranslateRoom && s.item.autoTranslate && autoTranslateLanguage && otherUserMessage) { + const translated = getMessageTranslation(s.item, autoTranslateLanguage); isTranslated = !!translated; messageText = translated || messageText; } @@ -317,29 +273,21 @@ export const useMessageText = (): { messageText: TAnyMessageModel['msg']; isTran ); }; -export const useThreadBadgeColor = (): string | undefined => { - const { store } = useMessageCtx(); - return useStore(store, s => s.threadBadgeColor); -}; -export const useMessageIgnored = (): boolean => { - const { store, isIgnored } = useMessageCtx(); - const manualUnignored = useStore(store, s => s.manualUnignored); - return manualUnignored ? false : isIgnored; -}; -export const useRevealIgnored = (): (() => void) => { - const { revealIgnored } = useMessageCtx(); - return revealIgnored; -}; +export const useThreadBadgeColor = (): string | undefined => useMessageStore(s => s.threadBadgeColor); + +export const useMessageIgnored = (): boolean => useMessageStore(s => (s.manualUnignored ? false : s.isIgnored)); + +export const useRevealIgnored = (): (() => void) => useMessageStore(s => s.reveal); export const useMessageLongPress = (): (() => void) => { 'use memo'; - const { item, store } = useMessageCtx(); + const item = useMessageItem(); const isInfo = useIsInfo(); const { hasError } = useMessageStatus(); const isEncrypted = useIsEncrypted(); const archived = useArchived(); - const onLongPress = useStore(store, s => s.onLongPress); + const onLongPress = useMessageStore(s => s.onLongPress); return () => { if (isInfo || hasError || isEncrypted || archived) { return; @@ -349,7 +297,7 @@ export const useMessageLongPress = (): (() => void) => { }; export const useOnLinkPress = (): ((link: string) => void) => { - const { item } = useMessageCtx(); + const item = useMessageItem(); const jumpToMessage = useJumpToMessage(); const { theme } = useTheme(); return (link: string) => { @@ -364,9 +312,9 @@ export const useOnLinkPress = (): ((link: string) => void) => { export const useMessagePress = (): (() => void) => { 'use memo'; - const { item, store } = useMessageCtx(); + const item = useMessageItem(); const isThreadRoom = useIsThreadRoom(); - const onPress = useStore(store, s => s.onPress); + const onPress = useMessageStore(s => s.onPress); const onThreadPress = useOnThreadPress(); const onDiscussionPress = useOnDiscussionPress(); const closeEmojiAndAction = useCloseEmojiAndAction(); diff --git a/app/containers/message/stores/__tests__/MessageStore.test.tsx b/app/containers/message/stores/__tests__/MessageStore.test.tsx index 0ab9966cf2a..1affac51e8f 100644 --- a/app/containers/message/stores/__tests__/MessageStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageStore.test.tsx @@ -17,11 +17,11 @@ import { useIsEncrypted, useIsInfo, useMessageAuthor, - useMessageCtx, useMessageField, useMessageGrouping, useMessageIgnored, useMessageHeaderMeta, + useMessageItem, useMessageStatus, useMessageText, useReactions, @@ -180,7 +180,7 @@ describe('MessageStore', () => { expect(() => renderMessage(plainItem, { reactions: useReactions })).not.toThrow(); }); - it('reflects an item identity change synchronously', () => { + it('reflects an item identity change', () => { const modelA = buildFakeModel({ id: 'msg-a', msg: 'Model A' }); const modelB = buildFakeModel({ id: 'msg-b', msg: 'Model B' }); const spy = jest.fn(); @@ -192,6 +192,22 @@ describe('MessageStore', () => { expect(spy).toHaveBeenLastCalledWith('Model B'); }); + + it('useMessageItem returns a stable ref across a tick bump (in-place mutation)', () => { + const model = buildFakeModel(); + const { renderCount, latestValue } = renderMessage(model, { item: useMessageItem }); + + expect(latestValue('item')).toBe(model); + const baseline = renderCount('item'); + + act(() => { + (model as any).urls = [{ url: 'x' }]; + model._emit(); + }); + + expect(renderCount('item')).toBe(baseline); + expect(latestValue('item')).toBe(model); + }); }); describe('domain hooks', () => { @@ -558,9 +574,9 @@ describe('MessageStore', () => { consoleErrorSpy.mockRestore(); }); - it('useMessageCtx throws', () => { + it('useMessageItem throws', () => { const Probe = () => { - useMessageCtx(); + useMessageItem(); return null; }; expect(() => render(<Probe />)).toThrow('Message hooks must be used within a MessageProvider'); @@ -624,9 +640,36 @@ describe('MessageStore', () => { fireEvent.press(getByTestId('reveal')); expect(spy).toHaveBeenLastCalledWith(false); - // manualUnignored is never reset, so re-rendering with the same isIgnored seed keeps it revealed. + // The reset effect is keyed on isIgnored; a re-render with the same seed does not re-run it, so the reveal sticks. act(() => rerender(wrap())); expect(spy).toHaveBeenLastCalledWith(false); }); + + it('re-hides after the ignore state transitions away and back', () => { + const model = buildFakeModel(); + const spy = jest.fn(); + const Probe = () => { + spy(useMessageIgnored()); + const reveal = useRevealIgnored(); + return <Text testID='reveal' onPress={reveal} />; + }; + const wrap = (ignored: boolean) => ( + <MessageProvider item={model} isIgnored={ignored}> + <Probe /> + </MessageProvider> + ); + + const { rerender, getByTestId } = render(wrap(true)); + expect(spy).toHaveBeenLastCalledWith(true); + + fireEvent.press(getByTestId('reveal')); + expect(spy).toHaveBeenLastCalledWith(false); + + act(() => rerender(wrap(false))); + expect(spy).toHaveBeenLastCalledWith(false); + + act(() => rerender(wrap(true))); + expect(spy).toHaveBeenLastCalledWith(true); + }); }); }); From cfc0e5ab0cadf3c50163cb8beeeaad958c75b84d Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 6 Jul 2026 14:13:33 -0300 Subject: [PATCH 079/144] fix: key Quote reply list by attachment identity, not array index Reordering or editing quoted attachments reused the wrong Reply instance by raw index, carrying a stale loading state onto a different attachment. Key by the stable-identity expression the sibling Attachments.tsx already uses (title_link || message_link, index only as fallback). --- .../components/Attachments/Quote.test.tsx | 97 +++++++++++++++++++ .../message/components/Attachments/Quote.tsx | 9 +- 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 app/containers/message/components/Attachments/Quote.test.tsx diff --git a/app/containers/message/components/Attachments/Quote.test.tsx b/app/containers/message/components/Attachments/Quote.test.tsx new file mode 100644 index 00000000000..7f4176f1973 --- /dev/null +++ b/app/containers/message/components/Attachments/Quote.test.tsx @@ -0,0 +1,97 @@ +import { fireEvent, render, waitFor } from '@testing-library/react-native'; +import { Provider } from 'react-redux'; + +import Quote from './Quote'; +import { MessageProvider } from '../../stores/MessageStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; +import { mockedStore } from '../../../../reducers/mockedStore'; +import { type IAttachment, type TAnyMessageModel } from '../../../../definitions'; +import { fileDownloadAndPreview } from '../../../../lib/methods/helpers'; + +jest.mock('../../../markdown', () => { + const React = require('react'); + const { Text } = require('react-native'); + return { + __esModule: true, + default: ({ msg }: { msg?: string }) => <Text testID='reply-markdown'>{msg}</Text>, + MarkdownPreview: ({ msg }: { msg?: string }) => <Text testID='reply-markdown-preview'>{msg}</Text> + }; +}); + +// Never resolves, so a click leaves the reply's loading state stuck at `true`. +jest.mock('../../../../lib/methods/helpers', () => ({ + fileDownloadAndPreview: jest.fn(() => new Promise(() => {})) +})); + +jest.mock('../../../../lib/methods/helpers/openLink', () => ({ + __esModule: true, + default: jest.fn() +})); + +jest.mock('../../../../lib/methods/helpers/formatAttachmentUrl', () => ({ + formatAttachmentUrl: jest.fn((url: string) => `formatted:${url}`) +})); + +jest.mock('expo-image', () => { + const { View } = require('react-native'); + const Image = () => <View testID='reply-url-image' />; + Image.loadAsync = jest.fn(); + return { Image }; +}); + +const mockFileDownloadAndPreview = fileDownloadAndPreview as jest.Mock; + +const buildItem = () => ({ id: 'msg-1' } as unknown as TAnyMessageModel); + +const renderQuote = (attachments: IAttachment[], ctx: Partial<MessageRoomState> = {}) => { + const contextValue: Partial<MessageRoomState> = { + user: { id: 'user-1', username: 'john', token: 'token' }, + baseUrl: 'https://open.rocket.chat', + timeFormat: 'HH:mm', + ...ctx + }; + return render( + <Provider store={mockedStore}> + <MessageRoomProvider {...contextValue}> + <MessageProvider item={buildItem()}> + <Quote attachments={attachments} /> + </MessageProvider> + </MessageRoomProvider> + </Provider> + ); +}; + +describe('Quote', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('keeps per-attachment loading state pinned to its own reply when the quotes are reordered', async () => { + const attachmentA: IAttachment = { type: 'file', title_link: 'file-a', text: 'A', author_name: 'Author' }; + const attachmentB: IAttachment = { type: 'file', title_link: 'file-b', text: 'B', author_name: 'Author' }; + + const { getByTestId, rerender } = renderQuote([attachmentA, attachmentB]); + + fireEvent.press(getByTestId('reply-Author-A')); + await waitFor(() => expect(mockFileDownloadAndPreview).toHaveBeenCalled()); + expect(getByTestId('reply-Author-A').props.accessibilityState.disabled).toBe(true); + expect(getByTestId('reply-Author-B').props.accessibilityState.disabled).toBe(false); + + rerender( + <Provider store={mockedStore}> + <MessageRoomProvider + user={{ id: 'user-1', username: 'john', token: 'token' }} + baseUrl='https://open.rocket.chat' + timeFormat='HH:mm'> + <MessageProvider item={buildItem()}> + <Quote attachments={[attachmentB, attachmentA]} /> + </MessageProvider> + </MessageRoomProvider> + </Provider> + ); + + // Reordering must not transfer attachment A's stuck loading state onto attachment B. + expect(getByTestId('reply-Author-B').props.accessibilityState.disabled).toBe(false); + expect(getByTestId('reply-Author-A').props.accessibilityState.disabled).toBe(true); + }); +}); diff --git a/app/containers/message/components/Attachments/Quote.tsx b/app/containers/message/components/Attachments/Quote.tsx index de1a70639c6..92e17441732 100644 --- a/app/containers/message/components/Attachments/Quote.tsx +++ b/app/containers/message/components/Attachments/Quote.tsx @@ -24,7 +24,14 @@ const Quote: FC<IMessageAttachments> = ({ attachments }: IMessageAttachments) => const quotesElements = quotes.map((file: IAttachment, index: number) => { const msg = getMessageFromAttachment(file, translateLanguage); - return <Reply key={index} attachment={file} getCustomEmoji={getCustomEmoji} msg={msg} />; + return ( + <Reply + key={file.title_link || file.message_link || `reply-${index}`} + attachment={file} + getCustomEmoji={getCustomEmoji} + msg={msg} + /> + ); }); return <View style={{ gap: 4 }}>{quotesElements}</View>; From 233b1315f84c2d36cb5adf1ef2c0638d53c6bb10 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 6 Jul 2026 14:18:10 -0300 Subject: [PATCH 080/144] fix: derive RepliedThread display in render, fetch on real inputs The display msg was snapshotted from isEncrypted/tmsg into useState at mount and a []-deps effect overwrote it, so it went stale when the model updated and missed later tmid/id changes (the captured fetch closed over the initial ids). Derive the display value during render, keep state only for the fetched thread name, and depend the fetch effect on tmid/id/ displayMsg so it re-runs on change. --- .../message/components/RepliedThread.tsx | 19 +++-- .../__tests__/RepliedThread.test.tsx | 84 +++++++++++++++++++ 2 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 app/containers/message/components/__tests__/RepliedThread.test.tsx diff --git a/app/containers/message/components/RepliedThread.tsx b/app/containers/message/components/RepliedThread.tsx index dc344c18831..efa30a457a0 100644 --- a/app/containers/message/components/RepliedThread.tsx +++ b/app/containers/message/components/RepliedThread.tsx @@ -18,22 +18,25 @@ const RepliedThread = ({ isHeader }: IMessageRepliedThread) => { const fetchThreadName = useFetchThreadName(); const { tmid, tmsg, id } = useRepliedThreadData(); const isEncrypted = useIsEncrypted(); - const [msg, setMsg] = useState(isEncrypted ? I18n.t('Encrypted_message') : tmsg); + const displayMsg = isEncrypted ? I18n.t('Encrypted_message') : tmsg; + const [fetchedName, setFetchedName] = useState<string | undefined>(); useEffect(() => { - if (!msg) { - fetch(); + if (displayMsg) { + return; } - }, []); + const fetch = async () => { + const threadName = fetchThreadName ? await fetchThreadName(tmid, id) : ''; + setFetchedName(threadName); + }; + fetch(); + }, [tmid, id, displayMsg, fetchThreadName]); if (!tmid || !isHeader) { return null; } - const fetch = async () => { - const threadName = fetchThreadName ? await fetchThreadName(tmid, id) : ''; - setMsg(threadName); - }; + const msg = displayMsg || fetchedName; if (!msg) { return null; diff --git a/app/containers/message/components/__tests__/RepliedThread.test.tsx b/app/containers/message/components/__tests__/RepliedThread.test.tsx new file mode 100644 index 00000000000..ec30e1cbb1d --- /dev/null +++ b/app/containers/message/components/__tests__/RepliedThread.test.tsx @@ -0,0 +1,84 @@ +import { render, waitFor } from '@testing-library/react-native'; +import { Provider } from 'react-redux'; + +import RepliedThread from '../RepliedThread'; +import { MessageProvider } from '../../stores/MessageStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { mockedStore } from '../../../../reducers/mockedStore'; +import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../../../lib/constants/keys'; + +const buildItem = (overrides: Partial<TAnyMessageModel> = {}): TAnyMessageModel => + ({ id: 'msg1', tmid: 'thread1', tmsg: 'original reply', ...overrides } as unknown as TAnyMessageModel); + +const renderRepliedThread = (item: TAnyMessageModel, roomState: Partial<MessageRoomState> = {}) => + render( + <Provider store={mockedStore}> + <MessageRoomProvider {...roomState}> + <MessageProvider item={item}> + <RepliedThread isHeader /> + </MessageProvider> + </MessageRoomProvider> + </Provider> + ); + +describe('RepliedThread', () => { + test('renders the encrypted placeholder when the message is encrypted, ignoring tmsg', () => { + const item = buildItem({ t: E2E_MESSAGE_TYPE, e2e: E2E_STATUS.PENDING, tmsg: 'plaintext should not show' }); + const { getByTestId, queryByTestId } = renderRepliedThread(item); + + expect(getByTestId('message-thread-replied-on-Encrypted message')).toBeTruthy(); + expect(queryByTestId('message-thread-replied-on-plaintext should not show')).toBeNull(); + }); + + test('fetches and renders the thread name when tmsg is missing', async () => { + const fetchThreadName = jest.fn().mockResolvedValue('fetched thread name'); + const item = buildItem({ tmsg: undefined }); + const { getByTestId } = renderRepliedThread(item, { fetchThreadName }); + + await waitFor(() => expect(getByTestId('message-thread-replied-on-fetched thread name')).toBeTruthy()); + expect(fetchThreadName).toHaveBeenCalledWith('thread1', 'msg1'); + }); + + test('re-fetches when tmid/id change without remounting', async () => { + const fetchThreadName = jest.fn().mockResolvedValueOnce('first thread name').mockResolvedValueOnce('second thread name'); + const item = buildItem({ tmid: 'thread1', id: 'msg1', tmsg: undefined }); + const { getByTestId, rerender } = renderRepliedThread(item, { fetchThreadName }); + + await waitFor(() => expect(getByTestId('message-thread-replied-on-first thread name')).toBeTruthy()); + + const updatedItem = buildItem({ tmid: 'thread2', id: 'msg2', tmsg: undefined }); + rerender( + <Provider store={mockedStore}> + <MessageRoomProvider fetchThreadName={fetchThreadName}> + <MessageProvider item={updatedItem}> + <RepliedThread isHeader /> + </MessageProvider> + </MessageRoomProvider> + </Provider> + ); + + await waitFor(() => expect(getByTestId('message-thread-replied-on-second thread name')).toBeTruthy()); + expect(fetchThreadName).toHaveBeenLastCalledWith('thread2', 'msg2'); + }); + + test('updates the display when tmsg/isEncrypted change without remounting', () => { + const item = buildItem({ tmsg: 'first reply' }); + const { getByTestId, rerender } = renderRepliedThread(item); + + expect(getByTestId('message-thread-replied-on-first reply')).toBeTruthy(); + + const updatedItem = buildItem({ tmsg: 'second reply' }); + rerender( + <Provider store={mockedStore}> + <MessageRoomProvider> + <MessageProvider item={updatedItem}> + <RepliedThread isHeader /> + </MessageProvider> + </MessageRoomProvider> + </Provider> + ); + + expect(getByTestId('message-thread-replied-on-second reply')).toBeTruthy(); + }); +}); From 9e112592fc0aadbf3c3f573fe750d47ac6b38763 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 6 Jul 2026 14:23:56 -0300 Subject: [PATCH 081/144] refactor: drive useMediaAutoDownload status via an event reducer Status was written through scattered setStatus literals across five handlers and onPress re-branched on the status string, duplicating the transition table. Extract a downloadStatusReducer over named events (download_started/succeeded/failed/canceled, cache_hit); handlers dispatch events and onPress derives its action from state. Behavior unchanged. --- .../__tests__/downloadStatusReducer.test.ts | 23 ++++++++++++ .../__tests__/useMediaAutoDownload.test.tsx | 12 +++++++ .../message/hooks/useMediaAutoDownload.tsx | 35 +++++++++++++------ 3 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 app/containers/message/hooks/__tests__/downloadStatusReducer.test.ts diff --git a/app/containers/message/hooks/__tests__/downloadStatusReducer.test.ts b/app/containers/message/hooks/__tests__/downloadStatusReducer.test.ts new file mode 100644 index 00000000000..e3d1ee04e1b --- /dev/null +++ b/app/containers/message/hooks/__tests__/downloadStatusReducer.test.ts @@ -0,0 +1,23 @@ +import { downloadStatusReducer, type TDownloadEvent } from '../useMediaAutoDownload'; +import { type TDownloadState } from '../../../../lib/methods/handleMediaDownload'; + +describe('downloadStatusReducer', () => { + const cases: Array<[TDownloadState, TDownloadEvent, TDownloadState]> = [ + ['to-download', 'download_started', 'loading'], + ['loading', 'download_succeeded', 'downloaded'], + ['to-download', 'download_succeeded', 'downloaded'], + ['to-download', 'cache_hit', 'downloaded'], + ['loading', 'cache_hit', 'downloaded'], + ['loading', 'download_failed', 'to-download'], + ['loading', 'download_canceled', 'to-download'], + ['downloaded', 'download_started', 'loading'] + ]; + + it.each(cases)('from %s on %s moves to %s', (from, event, to) => { + expect(downloadStatusReducer(from, event)).toBe(to); + }); + + it('ignores unknown events and keeps the current state', () => { + expect(downloadStatusReducer('to-download', 'unknown_event' as TDownloadEvent)).toBe('to-download'); + }); +}); diff --git a/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx b/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx index 9a7b0b964bb..9fde89439df 100644 --- a/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx +++ b/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx @@ -209,6 +209,18 @@ describe('useMediaAutoDownload', () => { await waitFor(() => expect(mockDownloadMediaFile).toHaveBeenCalled()); await waitFor(() => expect(result.current.status).toBe('to-download')); }); + + it('retries and succeeds after a previous download failure', async () => { + mockFetchAutoDownloadEnabled.mockReturnValue(true); + mockDownloadMediaFile.mockRejectedValueOnce(new Error('boom')); + const { result } = renderMediaHook({ file: { image_url: '/img' } }); + await waitFor(() => expect(mockDownloadMediaFile).toHaveBeenCalledTimes(1)); + await waitFor(() => expect(result.current.status).toBe('to-download')); + + await act(async () => result.current.onPress()); + await waitFor(() => expect(mockDownloadMediaFile).toHaveBeenCalledTimes(2)); + await waitFor(() => expect(result.current.status).toBe('downloaded')); + }); }); describe('onPress', () => { diff --git a/app/containers/message/hooks/useMediaAutoDownload.tsx b/app/containers/message/hooks/useMediaAutoDownload.tsx index 88e98eedbfa..c3deec8da0f 100644 --- a/app/containers/message/hooks/useMediaAutoDownload.tsx +++ b/app/containers/message/hooks/useMediaAutoDownload.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useReducer } from 'react'; import { type IAttachment, type IUserMessage } from '../../../definitions'; import { isImageBase64 } from '../../../lib/methods/isImageBase64'; @@ -49,6 +49,23 @@ const getOriginalURL = (file: IAttachment): string | null => { return null; }; +export type TDownloadEvent = 'download_started' | 'download_succeeded' | 'download_failed' | 'download_canceled' | 'cache_hit'; + +export const downloadStatusReducer = (state: TDownloadState, event: TDownloadEvent): TDownloadState => { + switch (event) { + case 'download_started': + return 'loading'; + case 'download_succeeded': + case 'cache_hit': + return 'downloaded'; + case 'download_failed': + case 'download_canceled': + return 'to-download'; + default: + return state; + } +}; + export const useMediaAutoDownload = ({ file, author, @@ -64,7 +81,7 @@ export const useMediaAutoDownload = ({ const id = useMessageId(); const baseUrl = useBaseUrl(); const user = useMessageUser(); - const [status, setStatus] = useState<TDownloadState>('to-download'); + const [status, dispatchDownloadEvent] = useReducer(downloadStatusReducer, 'to-download'); const [currentFile, setCurrentFile] = useFile(file, id ?? ''); const originalUrl = getOriginalURL(file); const url = formatAttachmentUrl( @@ -91,7 +108,7 @@ export const useMediaAutoDownload = ({ } }; if (fileType === 'image' && isImageBase64(url)) { - setStatus('downloaded'); + dispatchDownloadEvent('cache_hit'); } else { handleCache(); } @@ -106,7 +123,7 @@ export const useMediaAutoDownload = ({ }, []); const resumeDownload = () => { - setStatus('loading'); + dispatchDownloadEvent('download_started'); emitter.on(`downloadMedia${url}`, downloadMediaListener); }; @@ -115,14 +132,12 @@ export const useMediaAutoDownload = ({ const isAutoDownloadEnabled = fetchAutoDownloadEnabled(`${fileType}PreferenceDownload`); if (isAutoDownloadEnabled || isCurrentUserAuthor) { await download(); - } else { - setStatus('to-download'); } }; const download = async () => { try { - setStatus('loading'); + dispatchDownloadEvent('download_started'); const uri = await downloadMediaFile({ messageId: id ?? '', downloadUrl: url, @@ -134,7 +149,7 @@ export const useMediaAutoDownload = ({ setDecrypted(); updateCurrentFile(uri); } catch (e) { - setStatus('to-download'); + dispatchDownloadEvent('download_failed'); } }; @@ -142,7 +157,7 @@ export const useMediaAutoDownload = ({ setCurrentFile({ title_link: uri }); - setStatus('downloaded'); + dispatchDownloadEvent('download_succeeded'); }; const setDecrypted = () => { @@ -168,7 +183,7 @@ export const useMediaAutoDownload = ({ const onPress = () => { if (status === 'loading') { cancelDownload(url); - setStatus('to-download'); + dispatchDownloadEvent('download_canceled'); return; } if (status === 'to-download') { From 529e69b9544ce01713180885fe2ff592c6a02fe9 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 6 Jul 2026 14:30:45 -0300 Subject: [PATCH 082/144] refactor: split MessageRoomProvider and diff-guard the store mirror - Split into a base MessageRoomStoreProvider (no useSetting) and a WithSetting variant, dispatched on whether a timeFormat prop is passed. Callers that pass timeFormat (MessagesView, SearchMessagesView) no longer subscribe to or re-render on Message_TimeFormat changes. Public MessageRoomProvider name and prop contract are unchanged. - Guard the props->store mirror effect with a shallow diff against the store's current state (keys derived from the state object, no hand-maintained key list), so setState fires only on a real change instead of every render. Reactive props still propagate. --- .../message/stores/MessageRoomStore.tsx | 49 ++++++++--- .../__tests__/MessageRoomStore.test.tsx | 87 ++++++++++++++++++- 2 files changed, 124 insertions(+), 12 deletions(-) diff --git a/app/containers/message/stores/MessageRoomStore.tsx b/app/containers/message/stores/MessageRoomStore.tsx index 889101cd95d..e2ca5da2379 100644 --- a/app/containers/message/stores/MessageRoomStore.tsx +++ b/app/containers/message/stores/MessageRoomStore.tsx @@ -55,25 +55,52 @@ const useMessageRoomStore = <T,>(selector: (state: MessageRoomState) => T): T => return useStore(store, selector); }; -export const MessageRoomProvider = ({ children, ...state }: { children: ReactNode } & MessageRoomState): ReactElement => { +const MessageRoomStoreProvider = ({ children, ...state }: { children: ReactNode } & MessageRoomState): ReactElement => { 'use memo'; - const Message_TimeFormat = useSetting('Message_TimeFormat') as string; - const resolvedState = { ...state, timeFormat: state.timeFormat ?? Message_TimeFormat }; - const [store] = useState(() => createMessageRoomStore(resolvedState)); - - // Runs every render by design: resolvedState is rebuilt from props each render, so it can never - // be a stable dependency (state is a rest-spread, fresh each render — useMemo can't stabilize it). - // Per-field Object.is bail in each selector means this mirror only re-renders a consumer whose own - // field changed. A field-level deps array would reintroduce the hand-maintained key list the zustand - // migration deliberately removed. + const [store] = useState(() => createMessageRoomStore(state)); + + // state is a rest-spread rebuilt from props each render, so it can never be a stable + // dependency (useMemo can't stabilize it, and a field-level deps array would reintroduce the + // hand-maintained key list the zustand migration deliberately removed). Diffing against the + // store's current state before calling setState keeps this from forcing a zustand + // notification (and consumer re-render) on every render; per-field Object.is bail in each + // selector still means only a consumer whose own field changed re-renders. useEffect(() => { - store.setState(resolvedState); + const current = store.getState(); + const changed = Object.keys(state).some( + key => !Object.is(state[key as keyof MessageRoomState], current[key as keyof MessageRoomState]) + ); + if (changed) { + store.setState(state); + } }); return <MessageRoomStoreContext.Provider value={store}>{children}</MessageRoomStoreContext.Provider>; }; +const MessageRoomProviderWithSetting = ({ children, ...state }: { children: ReactNode } & MessageRoomState): ReactElement => { + 'use memo'; + + const Message_TimeFormat = useSetting('Message_TimeFormat') as string; + + return ( + <MessageRoomStoreProvider {...state} timeFormat={Message_TimeFormat}> + {children} + </MessageRoomStoreProvider> + ); +}; + +export const MessageRoomProvider = ({ children, ...state }: { children: ReactNode } & MessageRoomState): ReactElement => { + 'use memo'; + + return state.timeFormat != null ? ( + <MessageRoomStoreProvider {...state}>{children}</MessageRoomStoreProvider> + ) : ( + <MessageRoomProviderWithSetting {...state}>{children}</MessageRoomProviderWithSetting> + ); +}; + export const useNavToRoomInfo = (): ((navParam: IRoomInfoParam) => void) | undefined => useMessageRoomStore(s => s.navToRoomInfo); export const useShowAttachment = (): ((file: IAttachment) => void) | undefined => useMessageRoomStore(s => s.showAttachment); export const useBlockAction = (): MessageRoomState['blockAction'] => useMessageRoomStore(s => s.blockAction); diff --git a/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx b/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx index ac6055f9179..0ed07eae901 100644 --- a/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx @@ -1,8 +1,10 @@ +import { memo } from 'react'; import { act, render } from '@testing-library/react-native'; import { Provider } from 'react-redux'; -import { MessageRoomProvider, useTimeFormat } from '../MessageRoomStore'; +import { MessageRoomProvider, useAutoTranslate, useTimeFormat } from '../MessageRoomStore'; import { mockedStore } from '../../../../reducers/mockedStore'; +import { updateSettings } from '../../../../actions/settings'; describe('MessageRoomStore', () => { it('mirrors updated provider props into the store after mount', () => { @@ -25,4 +27,87 @@ describe('MessageRoomStore', () => { act(() => rerender(wrap('h:mm a'))); expect(spy).toHaveBeenLastCalledWith('h:mm a'); }); + + it('does not subscribe to Message_TimeFormat when a timeFormat prop is passed', () => { + const spy = jest.fn(); + const Probe = () => { + spy(useTimeFormat()); + return null; + }; + + render( + <Provider store={mockedStore}> + <MessageRoomProvider timeFormat='literal-format'> + <Probe /> + </MessageRoomProvider> + </Provider> + ); + expect(spy).toHaveBeenLastCalledWith('literal-format'); + const callsBefore = spy.mock.calls.length; + + act(() => { + mockedStore.dispatch(updateSettings('Message_TimeFormat', 'h:mm a')); + }); + + expect(spy.mock.calls.length).toBe(callsBefore); + expect(spy).toHaveBeenLastCalledWith('literal-format'); + }); + + it('uses the Message_TimeFormat setting when no timeFormat prop is passed', () => { + mockedStore.dispatch(updateSettings('Message_TimeFormat', 'MMM Do YYYY')); + + const spy = jest.fn(); + const Probe = () => { + spy(useTimeFormat()); + return null; + }; + + render( + <Provider store={mockedStore}> + <MessageRoomProvider> + <Probe /> + </MessageRoomProvider> + </Provider> + ); + expect(spy).toHaveBeenLastCalledWith('MMM Do YYYY'); + + act(() => { + mockedStore.dispatch(updateSettings('Message_TimeFormat', 'h:mm a')); + }); + + expect(spy).toHaveBeenLastCalledWith('h:mm a'); + }); + + it('propagates a reactive prop change without re-rendering an unrelated consumer', () => { + const timeFormatSpy = jest.fn(); + const autoTranslateSpy = jest.fn(); + // memoized so re-renders only come from the store notifying its own subscription, + // not from the parent tree re-rendering with a new element identity + const TimeFormatProbe = memo(() => { + timeFormatSpy(useTimeFormat()); + return null; + }); + const AutoTranslateProbe = memo(() => { + autoTranslateSpy(useAutoTranslate()); + return null; + }); + + const wrap = (autoTranslateRoom: boolean) => ( + <Provider store={mockedStore}> + <MessageRoomProvider timeFormat='fixed-format' autoTranslateRoom={autoTranslateRoom}> + <TimeFormatProbe /> + <AutoTranslateProbe /> + </MessageRoomProvider> + </Provider> + ); + + const { rerender } = render(wrap(false)); + expect(autoTranslateSpy).toHaveBeenLastCalledWith({ autoTranslateRoom: false, autoTranslateLanguage: undefined }); + const timeFormatCallsBefore = timeFormatSpy.mock.calls.length; + + act(() => rerender(wrap(true))); + + expect(autoTranslateSpy).toHaveBeenLastCalledWith({ autoTranslateRoom: true, autoTranslateLanguage: undefined }); + expect(timeFormatSpy.mock.calls.length).toBe(timeFormatCallsBefore); + }); }); From 061658ca03fa9bb7735468f51425a0ef2718a2d0 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 6 Jul 2026 14:54:07 -0300 Subject: [PATCH 083/144] fix: narrow tmid before fetchThreadName in RepliedThread effect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 233b1315f rework moved the fetchThreadName call into an effect above the !tmid early return, so tmid (string | undefined) was no longer narrowed to string at the call site, breaking the type check. Guard the effect on tmid as well — the render path already returns null when tmid is absent, so fetching in that case was a no-op. --- app/containers/message/components/RepliedThread.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/containers/message/components/RepliedThread.tsx b/app/containers/message/components/RepliedThread.tsx index efa30a457a0..5bc0ce49345 100644 --- a/app/containers/message/components/RepliedThread.tsx +++ b/app/containers/message/components/RepliedThread.tsx @@ -22,7 +22,7 @@ const RepliedThread = ({ isHeader }: IMessageRepliedThread) => { const [fetchedName, setFetchedName] = useState<string | undefined>(); useEffect(() => { - if (displayMsg) { + if (displayMsg || !tmid) { return; } const fetch = async () => { From 3eec1c1c827e0cd54b246f68332ba28e25b871c9 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 6 Jul 2026 14:54:16 -0300 Subject: [PATCH 084/144] refactor: model RoomView interaction state as a discriminated union MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InteractionStore held interaction as two flat fields (action + selectedMessages), so invalid combos (edit with many selected ids, action out of sync with the selection) were representable and only prevented by call-site discipline. Replace them with a single discriminated union — edit/quote/react/null — and event-style action creators, making invalid states unrepresentable at the type level. Public selectors (useMessageAction, useSelectedMessages, useIsBeingEdited) are preserved by deriving from the union (useSelectedMessages via useShallow, since edit/react derive a fresh single-element array), so the composer consumers need no changes. RoomView getState() reads and createInteractionStore initializers updated to the union. ShareView drops its duplicate action/selectedMessages state; send(), onRemoveQuoteMessage and componentWillUnmount now read the interaction from the store instead of this.state. --- .../MessageComposer/MessageComposer.test.tsx | 20 ++--- .../message/__tests__/index.test.tsx | 4 +- .../components/stories/Message.stories.tsx | 2 +- app/views/RoomView/InteractionStore.test.tsx | 86 ++++++++++++++++++- app/views/RoomView/InteractionStore.tsx | 57 ++++++++---- app/views/RoomView/index.tsx | 40 ++++----- app/views/ShareView/ShareView.test.tsx | 36 +++++++- app/views/ShareView/index.tsx | 44 ++++++---- 8 files changed, 211 insertions(+), 78 deletions(-) diff --git a/app/containers/MessageComposer/MessageComposer.test.tsx b/app/containers/MessageComposer/MessageComposer.test.tsx index e511f287dc2..06972d48354 100644 --- a/app/containers/MessageComposer/MessageComposer.test.tsx +++ b/app/containers/MessageComposer/MessageComposer.test.tsx @@ -12,8 +12,8 @@ import { mockedStore } from '../../reducers/mockedStore'; import { type IPermissionsState } from '../../reducers/permissions'; import { type IMessage, type IShareAttachment } from '../../definitions'; import { colors } from '../../lib/constants/colors'; -import { type IRoomContext, RoomContext, type TMessageAction } from '../../views/RoomView/context'; -import { InteractionProvider } from '../../views/RoomView/InteractionStore'; +import { type IRoomContext, RoomContext } from '../../views/RoomView/context'; +import { InteractionProvider, type TInteraction } from '../../views/RoomView/InteractionStore'; import * as EmojiKeyboardHook from './hooks/useEmojiKeyboard'; import { initStore } from '../../lib/store/auxStore'; import { search } from '../../lib/methods/search'; @@ -122,7 +122,7 @@ const Render = ({ children }: { context?: Partial<IRoomContext>; - interactionState?: { action?: TMessageAction; selectedMessages?: string[] }; + interactionState?: TInteraction; children?: ReactElement; }) => ( <Provider store={mockedStore}> @@ -649,7 +649,7 @@ describe('MessageComposer', () => { return renderAndFlush( <Render context={{ rid: 'rid', onSendMessage, editCancel, editRequest }} - interactionState={{ action: 'edit', selectedMessages: [id] }} + interactionState={{ kind: 'edit', messageId: id }} /> ); }); @@ -682,9 +682,7 @@ describe('MessageComposer', () => { const editRequest = jest.fn(); const id = 'image'; test('edit image', async () => { - await renderAndFlush( - <Render context={{ rid: 'rid', editRequest }} interactionState={{ action: 'edit', selectedMessages: [id] }} /> - ); + await renderAndFlush(<Render context={{ rid: 'rid', editRequest }} interactionState={{ kind: 'edit', messageId: id }} />); await screen.findByTestId('message-composer'); await user.press(screen.getByTestId('message-composer-send')); expect(editRequest).toHaveBeenCalledWith({ id, msg: `Attachment description for ${id}`, rid: 'rid' }); @@ -716,14 +714,14 @@ describe('MessageComposer', () => { describe('Quote', () => { test('Add quote `abc`', async () => { - render(<Render interactionState={{ action: 'quote', selectedMessages: ['abc'] }} />); + render(<Render interactionState={{ kind: 'quote', messageIds: ['abc'] }} />); await screen.findByTestId('composer-quote-abc'); expect(screen.queryByTestId('composer-quote-abc')).toBeOnTheScreen(); expect(screen.toJSON()).toMatchSnapshot(); }); test('Add quote `def`', async () => { - render(<Render interactionState={{ action: 'quote', selectedMessages: ['abc', 'def'] }} />); + render(<Render interactionState={{ kind: 'quote', messageIds: ['abc', 'def'] }} />); await screen.findByTestId('composer-quote-abc'); expect(screen.queryByTestId('composer-quote-abc')).toBeOnTheScreen(); expect(screen.queryByTestId('composer-quote-def')).toBeOnTheScreen(); @@ -732,9 +730,7 @@ describe('MessageComposer', () => { test('Remove a quote', async () => { const onRemoveQuoteMessage = jest.fn(); - render( - <Render context={{ onRemoveQuoteMessage }} interactionState={{ action: 'quote', selectedMessages: ['abc', 'def'] }} /> - ); + render(<Render context={{ onRemoveQuoteMessage }} interactionState={{ kind: 'quote', messageIds: ['abc', 'def'] }} />); await screen.findByTestId('composer-quote-def'); await user.press(screen.getByTestId('composer-quote-remove-def')); expect(onRemoveQuoteMessage).toHaveBeenCalledTimes(1); diff --git a/app/containers/message/__tests__/index.test.tsx b/app/containers/message/__tests__/index.test.tsx index a46111384d7..af2da17f326 100644 --- a/app/containers/message/__tests__/index.test.tsx +++ b/app/containers/message/__tests__/index.test.tsx @@ -95,7 +95,7 @@ it('fires the onLongPress callback when long-pressed', () => { describe('edit highlight reacts to InteractionStore', () => { it('shows editing testID when the store marks this message as being edited', () => { - const store = createInteractionStore({ action: 'edit', selectedMessages: ['msg-1'] }); + const store = createInteractionStore({ kind: 'edit', messageId: 'msg-1' }); const item = createMockMessage({ id: 'msg-1' }); const { getByTestId } = renderWithMessageProviders( <InteractionStoreContext.Provider value={store}> @@ -107,7 +107,7 @@ describe('edit highlight reacts to InteractionStore', () => { }); it('removes editing testID when the store resets', async () => { - const store = createInteractionStore({ action: 'edit', selectedMessages: ['msg-1'] }); + const store = createInteractionStore({ kind: 'edit', messageId: 'msg-1' }); const item = createMockMessage({ id: 'msg-1' }); const { queryByTestId } = renderWithMessageProviders( <InteractionStoreContext.Provider value={store}> diff --git a/app/containers/message/components/stories/Message.stories.tsx b/app/containers/message/components/stories/Message.stories.tsx index 5ef3dd261e9..b287995fea9 100644 --- a/app/containers/message/components/stories/Message.stories.tsx +++ b/app/containers/message/components/stories/Message.stories.tsx @@ -2401,7 +2401,7 @@ export const Temp = () => <Message msg='Temp message' status={messagesStatus.TEM export const TempLargeFont = () => <MessageLargeFont msg='Temp message' status={messagesStatus.TEMP} isTemp />; -const editingStore = createInteractionStore({ action: 'edit', selectedMessages: ['editing-message'] }); +const editingStore = createInteractionStore({ kind: 'edit', messageId: 'editing-message' }); export const Editing = () => ( <InteractionStoreContext.Provider value={editingStore}> diff --git a/app/views/RoomView/InteractionStore.test.tsx b/app/views/RoomView/InteractionStore.test.tsx index 7199f757e47..96d24cdf715 100644 --- a/app/views/RoomView/InteractionStore.test.tsx +++ b/app/views/RoomView/InteractionStore.test.tsx @@ -1,6 +1,13 @@ import { render } from '@testing-library/react-native'; -import { InteractionProvider, useIsBeingEdited, useMessageAction, useSelectedMessages } from './InteractionStore'; +import { + createInteractionStore, + InteractionProvider, + type TInteraction, + useIsBeingEdited, + useMessageAction, + useSelectedMessages +} from './InteractionStore'; describe('InteractionStore', () => { describe('useIsBeingEdited', () => { @@ -25,7 +32,7 @@ describe('InteractionStore', () => { }; render( - <InteractionProvider initialState={{ action: 'edit', selectedMessages: ['msg-1'] }}> + <InteractionProvider initialState={{ kind: 'edit', messageId: 'msg-1' }}> <Probe /> </InteractionProvider> ); @@ -62,4 +69,79 @@ describe('InteractionStore', () => { expect(() => render(<Probe />)).toThrow('Interaction hooks must be used within an InteractionProvider'); }); }); + + describe('action creators (union transitions)', () => { + it('setEditing sets an edit interaction with the given messageId', () => { + const store = createInteractionStore(); + store.getState().actions.setEditing('msg-1'); + expect(store.getState().interaction).toEqual({ kind: 'edit', messageId: 'msg-1' }); + }); + + it('initQuote starts a quote interaction with a single messageId', () => { + const store = createInteractionStore(); + store.getState().actions.initQuote('msg-1'); + expect(store.getState().interaction).toEqual({ kind: 'quote', messageIds: ['msg-1'] }); + }); + + it('appendQuote appends to an existing quote interaction', () => { + const store = createInteractionStore(); + store.getState().actions.initQuote('msg-1'); + store.getState().actions.appendQuote('msg-2'); + expect(store.getState().interaction).toEqual({ kind: 'quote', messageIds: ['msg-1', 'msg-2'] }); + }); + + it('appendQuote starts a new quote interaction when there is none', () => { + const store = createInteractionStore(); + store.getState().actions.appendQuote('msg-1'); + expect(store.getState().interaction).toEqual({ kind: 'quote', messageIds: ['msg-1'] }); + }); + + it('removeQuote drops a messageId and keeps the quote interaction if others remain', () => { + const store = createInteractionStore({ kind: 'quote', messageIds: ['msg-1', 'msg-2'] }); + store.getState().actions.removeQuote('msg-1'); + expect(store.getState().interaction).toEqual({ kind: 'quote', messageIds: ['msg-2'] }); + }); + + it('removeQuote clears the interaction when the last quoted message is removed', () => { + const store = createInteractionStore({ kind: 'quote', messageIds: ['msg-1'] }); + store.getState().actions.removeQuote('msg-1'); + expect(store.getState().interaction).toBeNull(); + }); + + it('removeQuote is a no-op when the current interaction is not a quote', () => { + const store = createInteractionStore({ kind: 'edit', messageId: 'msg-1' }); + store.getState().actions.removeQuote('msg-1'); + expect(store.getState().interaction).toEqual({ kind: 'edit', messageId: 'msg-1' }); + }); + + it('setReacting sets a react interaction with the given messageId', () => { + const store = createInteractionStore(); + store.getState().actions.setReacting('msg-1'); + expect(store.getState().interaction).toEqual({ kind: 'react', messageId: 'msg-1' }); + }); + + it('setQuotes sets a quote interaction with the given ids, or clears it when empty', () => { + const store = createInteractionStore(); + store.getState().actions.setQuotes(['msg-1', 'msg-2']); + expect(store.getState().interaction).toEqual({ kind: 'quote', messageIds: ['msg-1', 'msg-2'] }); + + store.getState().actions.setQuotes([]); + expect(store.getState().interaction).toBeNull(); + }); + + it('reset clears any interaction', () => { + const store = createInteractionStore({ kind: 'react', messageId: 'msg-1' }); + store.getState().actions.reset(); + expect(store.getState().interaction).toBeNull(); + }); + + it('does not allow representing invalid combinations at the type level', () => { + // @ts-expect-error edit requires a single messageId, not messageIds + const invalidEdit: TInteraction = { kind: 'edit', messageIds: ['msg-1'] }; + // @ts-expect-error quote requires messageIds, not messageId + const invalidQuote: TInteraction = { kind: 'quote', messageId: 'msg-1' }; + expect(invalidEdit).toBeDefined(); + expect(invalidQuote).toBeDefined(); + }); + }); }); diff --git a/app/views/RoomView/InteractionStore.tsx b/app/views/RoomView/InteractionStore.tsx index 1680f7470da..eee4ed73f08 100644 --- a/app/views/RoomView/InteractionStore.tsx +++ b/app/views/RoomView/InteractionStore.tsx @@ -1,8 +1,15 @@ import { createContext, type ReactElement, useContext, useState } from 'react'; import { createStore, useStore } from 'zustand'; +import { useShallow } from 'zustand/react/shallow'; import { type TMessageAction } from './context'; +export type TInteraction = + | { kind: 'edit'; messageId: string } + | { kind: 'quote'; messageIds: string[] } + | { kind: 'react'; messageId: string } + | null; + type TInteractionActions = { setEditing(messageId: string): void; initQuote(messageId: string): void; @@ -14,28 +21,34 @@ type TInteractionActions = { }; type InteractionState = { - action: TMessageAction; - selectedMessages: string[]; + interaction: TInteraction; // Built once inside the store initializer — stable ref for consumers. actions: TInteractionActions; }; -export const createInteractionStore = (initialState?: { action?: TMessageAction; selectedMessages?: string[] }) => +export const createInteractionStore = (initialInteraction?: TInteraction) => createStore<InteractionState>()(set => ({ - action: initialState?.action ?? null, - selectedMessages: initialState?.selectedMessages ?? [], + interaction: initialInteraction ?? null, actions: { - setEditing: messageId => set({ action: 'edit', selectedMessages: [messageId] }), - initQuote: messageId => set({ action: 'quote', selectedMessages: [messageId] }), - appendQuote: messageId => set(state => ({ selectedMessages: [...state.selectedMessages, messageId] })), + setEditing: messageId => set({ interaction: { kind: 'edit', messageId } }), + initQuote: messageId => set({ interaction: { kind: 'quote', messageIds: [messageId] } }), + appendQuote: messageId => + set(state => + state.interaction?.kind === 'quote' + ? { interaction: { kind: 'quote', messageIds: [...state.interaction.messageIds, messageId] } } + : { interaction: { kind: 'quote', messageIds: [messageId] } } + ), removeQuote: messageId => set(state => { - const msgs = state.selectedMessages.filter(m => m !== messageId); - return { selectedMessages: msgs, action: msgs.length ? 'quote' : null }; + if (state.interaction?.kind !== 'quote') { + return {}; + } + const messageIds = state.interaction.messageIds.filter(m => m !== messageId); + return { interaction: messageIds.length ? { kind: 'quote', messageIds } : null }; }), - setReacting: messageId => set({ action: 'react', selectedMessages: [messageId] }), - setQuotes: messageIds => set({ action: messageIds.length ? 'quote' : null, selectedMessages: messageIds }), - reset: () => set({ action: null, selectedMessages: [] }) + setReacting: messageId => set({ interaction: { kind: 'react', messageId } }), + setQuotes: messageIds => set({ interaction: messageIds.length ? { kind: 'quote', messageIds } : null }), + reset: () => set({ interaction: null }) } })); @@ -55,19 +68,29 @@ const useInteractionStore = <T,>(selector: (state: InteractionState) => T): T => return useStore(store, selector); }; -export const useMessageAction = (): TMessageAction => useInteractionStore(s => s.action); -export const useSelectedMessages = (): string[] => useInteractionStore(s => s.selectedMessages); +export const useMessageAction = (): TMessageAction => useInteractionStore(s => s.interaction?.kind ?? null); + +export const useSelectedMessages = (): string[] => + useInteractionStore( + useShallow(s => { + const { interaction } = s; + if (!interaction) { + return []; + } + return interaction.kind === 'quote' ? interaction.messageIds : [interaction.messageId]; + }) + ); export const useIsBeingEdited = (messageId: string): boolean => { const store = useContext(InteractionStoreContext) ?? fallbackStore; - return useStore(store, s => s.action === 'edit' && s.selectedMessages[0] === messageId); + return useStore(store, s => s.interaction?.kind === 'edit' && s.interaction.messageId === messageId); }; export const InteractionProvider = ({ initialState, children }: { - initialState?: { action?: TMessageAction; selectedMessages?: string[] }; + initialState?: TInteraction; children: ReactElement; }): ReactElement => { 'use memo'; diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 336f8251d8e..3313e534122 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -171,11 +171,8 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { this.jumpToMessageId = props.route.params?.jumpToMessageId; this.jumpToThreadId = props.route.params?.jumpToThreadId; const roomUserId = props.route.params?.roomUserId ?? getUidDirectMessage(room); - const selectedMessages = props.route.params?.messageId ? [props.route.params.messageId] : []; - this.interactionStore = createInteractionStore({ - selectedMessages, - action: selectedMessages.length ? 'quote' : null - }); + const { messageId } = props.route.params ?? {}; + this.interactionStore = createInteractionStore(messageId ? { kind: 'quote', messageIds: [messageId] } : null); this.state = { joined: true, room, @@ -226,7 +223,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { componentDidMount() { const { navigation, dispatch } = this.props; - const { selectedMessages } = this.interactionStore.getState(); + const { interaction } = this.interactionStore.getState(); dispatch(clearInAppFeedback()); this.mounted = true; this.didMountInteraction = InteractionManager.runAfterInteractions(() => { @@ -256,8 +253,8 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { if (isIOS && this.rid) { this.updateUnreadCount(); } - if (selectedMessages.length === 1) { - this.onQuoteInit(selectedMessages[0]); + if (interaction?.kind === 'quote' && interaction.messageIds.length === 1) { + this.onQuoteInit(interaction.messageIds[0]); } }); EventEmitter.addEventListener('ROOM_REMOVED', this.handleRoomRemoved); @@ -798,8 +795,8 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { }; onEditInit = (messageId: string) => { - const { action, actions } = this.interactionStore.getState(); - if (action) { + const { interaction, actions } = this.interactionStore.getState(); + if (interaction) { return; } actions.setEditing(messageId); @@ -844,14 +841,14 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { }; onQuoteInit = (messageId: string) => { - const { action, selectedMessages, actions } = this.interactionStore.getState(); - if (action === 'quote') { - if (!selectedMessages.includes(messageId)) { + const { interaction, actions } = this.interactionStore.getState(); + if (interaction?.kind === 'quote') { + if (!interaction.messageIds.includes(messageId)) { actions.appendQuote(messageId); } return; } - if (action) { + if (interaction) { return; } actions.initQuote(messageId); @@ -867,15 +864,12 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { showReactionPicker = () => { const { showActionSheet } = this.props; - const { selectedMessages } = this.interactionStore.getState(); + const { interaction } = this.interactionStore.getState(); + const messageId = interaction?.kind === 'react' ? interaction.messageId : undefined; setTimeout(() => { showActionSheet({ children: ( - <ReactionPicker - messageId={selectedMessages[0]} - onEmojiSelected={this.onReactionPress} - reactionClose={this.onReactionClose} - /> + <ReactionPicker messageId={messageId} onEmojiSelected={this.onReactionPress} reactionClose={this.onReactionClose} /> ), snaps: ['50%'], enableContentPanningGesture: false, @@ -886,7 +880,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { }; onReactionInit = (messageId: string) => { - if (this.interactionStore.getState().action) { + if (this.interactionStore.getState().interaction) { return; } this.handleCloseEmoji(() => { @@ -902,8 +896,8 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { }; onMessageLongPress = (message: TAnyMessageModel) => { - const { action } = this.interactionStore.getState(); - if (action && action !== 'quote') { + const { interaction } = this.interactionStore.getState(); + if (interaction && interaction.kind !== 'quote') { return; } // if it's a thread message on main room, we disable the long press diff --git a/app/views/ShareView/ShareView.test.tsx b/app/views/ShareView/ShareView.test.tsx index 278278cc877..8e8a6561df4 100644 --- a/app/views/ShareView/ShareView.test.tsx +++ b/app/views/ShareView/ShareView.test.tsx @@ -103,9 +103,7 @@ const makeInstance = ({ room: { rid: 'room-id', t: 'c' } as any, thread: '', maxFileSize: undefined, - mediaAllowList: undefined, - selectedMessages: [], - action: null + mediaAllowList: undefined }; if (serverInfoVersion) { @@ -178,4 +176,36 @@ describe('ShareView', () => { spy.mockRestore(); }); + + it('send() builds msg from prepareQuoteMessage using the interaction store quote ids', async () => { + const shareView = makeInstance({ + mime: 'image/jpeg', + serverVersion: '8.3.0', + serverInfoVersion: '8.3.0', + isShareExtension: true + }); + shareView.state.attachments[0].canUpload = true; + shareView.state = { + ...shareView.state, + selected: shareView.state.attachments[0] + }; + shareView.saveSelectedDescription = jest.fn() as any; + + shareView.interactionStore.getState().actions.setQuotes(['msg-1']); + + const prepareQuoteMessageMod = require('../../containers/MessageComposer/helpers/prepareQuoteMessage'); + const prepareSpy = jest.spyOn(prepareQuoteMessageMod, 'prepareQuoteMessage').mockResolvedValue('quoted-text'); + + const sendFileMessageMod = require('../../lib/methods/sendFileMessage'); + const spy = jest.spyOn(sendFileMessageMod, 'sendFileMessage').mockResolvedValue(undefined); + + await shareView.send(); + + expect(prepareSpy).toHaveBeenCalledWith('', ['msg-1']); + const fileArg = spy.mock.calls[0]?.[1] as { msg?: string } | undefined; + expect(fileArg?.msg).toBe('quoted-text'); + + spy.mockRestore(); + prepareSpy.mockRestore(); + }); }); diff --git a/app/views/ShareView/index.tsx b/app/views/ShareView/index.tsx index 39c561d7a3d..00bc23be3ec 100644 --- a/app/views/ShareView/index.tsx +++ b/app/views/ShareView/index.tsx @@ -39,7 +39,12 @@ import { sendAttachments } from '../../lib/methods/sendFileMessage/sendAttachmen import { sendMessage } from '../../lib/methods/sendMessage'; import { hasPermission, isAndroid, canUploadFile, isReadOnly, isBlocked } from '../../lib/methods/helpers'; import { RoomContext } from '../RoomView/context'; -import { createInteractionStore, InteractionStoreContext, type InteractionStore } from '../RoomView/InteractionStore'; +import { + createInteractionStore, + InteractionStoreContext, + type InteractionStore, + type TInteraction +} from '../RoomView/InteractionStore'; import { appStart } from '../../actions/app'; interface IShareViewState { @@ -52,8 +57,6 @@ interface IShareViewState { thread: TThreadModel | string; maxFileSize?: number; mediaAllowList?: string; - selectedMessages: string[]; - action: TMessageAction; } interface IShareViewProps { @@ -74,6 +77,11 @@ interface IShareViewProps { type TShareServerInfo = Partial<Pick<IServer, 'version' | 'FileUpload_MaxFileSize' | 'FileUpload_MediaTypeWhiteList'>>; +// ShareView only supports the quote flow (messages are filled later via startShareView -> setQuotes); +// edit/react/reply carry no messageId here, so they can't be represented by the union. +const mapActionToInteraction = (action: TMessageAction): TInteraction => + action === 'quote' ? { kind: 'quote', messageIds: [] } : null; + class ShareView extends Component<IShareViewProps, IShareViewState> { private messageComposerRef: RefObject<IMessageComposerRef | null>; private files: any[]; @@ -91,7 +99,7 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { this.serverInfo = props.route.params?.serverInfo ?? {}; this.finishShareView = props.route.params?.finishShareView; this.sentMessage = false; - this.interactionStore = createInteractionStore({ action: props.route.params?.action, selectedMessages: [] }); + this.interactionStore = createInteractionStore(mapActionToInteraction(props.route.params?.action)); this.state = { selected: {} as IShareAttachment, @@ -102,11 +110,7 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { room: props.route.params?.room ?? {}, thread: props.route.params?.thread ?? {}, maxFileSize: this.isShareExtension ? this.serverInfo?.FileUpload_MaxFileSize : props.FileUpload_MaxFileSize, - mediaAllowList: this.isShareExtension - ? this.serverInfo?.FileUpload_MediaTypeWhiteList - : props.FileUpload_MediaTypeWhiteList, - selectedMessages: [], - action: props.route.params?.action + mediaAllowList: this.isShareExtension ? this.serverInfo?.FileUpload_MediaTypeWhiteList : props.FileUpload_MediaTypeWhiteList }; this.getServerInfo(); } @@ -122,10 +126,16 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { console.countReset(`${this.constructor.name}.render calls`); if (this.finishShareView && !this.sentMessage) { const text = this.messageComposerRef.current?.getText(); - this.finishShareView(text, this.state.selectedMessages); + this.finishShareView(text, this.getSelectedMessageIds()); } }; + // ShareView's interaction store only ever holds the quote flow. + getSelectedMessageIds = (): string[] => { + const { interaction } = this.interactionStore.getState(); + return interaction?.kind === 'quote' ? interaction.messageIds : []; + }; + getThreadId = (thread: TThreadModel | string | undefined) => { let threadId = undefined; if (typeof thread === 'object') { @@ -239,7 +249,6 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { // Synchronization needed for Fabric to work await new Promise(resolve => setTimeout(resolve, 100)); this.messageComposerRef.current?.setInput(text); - this.setState({ selectedMessages }); this.interactionStore.getState().actions.setQuotes(selectedMessages); } }; @@ -249,7 +258,7 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { Keyboard.dismiss(); - const { attachments, room, text, thread, action, selectedMessages } = this.state; + const { attachments, room, text, thread } = this.state; const { navigation, server, user, dispatch } = this.props; // flush the composer caption into the selected attachment before sending this.saveSelectedDescription(); @@ -267,8 +276,9 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { } let msg: string | undefined; - if (action === 'quote') { - msg = await prepareQuoteMessage('', selectedMessages); + const { interaction } = this.interactionStore.getState(); + if (interaction?.kind === 'quote') { + msg = await prepareQuoteMessage('', interaction.messageIds); } const { isAltTextSupported } = this; @@ -297,7 +307,7 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { } catch { if (!this.isShareExtension) { const text = this.messageComposerRef.current?.getText(); - this.finishShareView(text, this.state.selectedMessages); + this.finishShareView(text, this.getSelectedMessageIds()); } } @@ -383,9 +393,7 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { } onRemoveQuoteMessage = (messageId: string) => { - const { selectedMessages } = this.state; - const newSelectedMessages = selectedMessages.filter(item => item !== messageId); - this.setState({ selectedMessages: newSelectedMessages, action: newSelectedMessages.length ? 'quote' : null }); + const newSelectedMessages = this.getSelectedMessageIds().filter(item => item !== messageId); this.interactionStore.getState().actions.setQuotes(newSelectedMessages); }; From 722f0d4eb376d82deb1dd73cd5ed8ff2b89873f7 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 6 Jul 2026 15:10:52 -0300 Subject: [PATCH 085/144] fix: guard RepliedThread fetch against stale writes The 233b1315f change made the thread-name fetch effect re-runnable (deps went from [] to tmid/id/displayMsg/fetchThreadName). Add an ignore flag cleared on cleanup so a slow in-flight fetch can no longer overwrite the name after the inputs change. --- app/containers/message/components/RepliedThread.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/containers/message/components/RepliedThread.tsx b/app/containers/message/components/RepliedThread.tsx index 5bc0ce49345..9e416bf81bd 100644 --- a/app/containers/message/components/RepliedThread.tsx +++ b/app/containers/message/components/RepliedThread.tsx @@ -25,11 +25,17 @@ const RepliedThread = ({ isHeader }: IMessageRepliedThread) => { if (displayMsg || !tmid) { return; } + let ignore = false; const fetch = async () => { const threadName = fetchThreadName ? await fetchThreadName(tmid, id) : ''; - setFetchedName(threadName); + if (!ignore) { + setFetchedName(threadName); + } }; fetch(); + return () => { + ignore = true; + }; }, [tmid, id, displayMsg, fetchThreadName]); if (!tmid || !isHeader) { From dbac499fd6274b21344a1e79fff5e8744d09afe1 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 6 Jul 2026 16:14:01 -0300 Subject: [PATCH 086/144] refactor: move Attachments unit tests into components/__tests__ (NATIVE-22) --- .../Attachments.utils.test.ts} | 2 +- .../CollapsibleQuote.test.tsx | 12 ++++++------ .../Image => __tests__}/Image.test.tsx | 6 +++--- .../ImageContainer.test.tsx} | 18 +++++++++--------- .../{Attachments => __tests__}/Quote.test.tsx | 2 +- .../{Attachments => __tests__}/Reply.test.tsx | 2 +- 6 files changed, 21 insertions(+), 21 deletions(-) rename app/containers/message/components/{Attachments/utils.test.ts => __tests__/Attachments.utils.test.ts} (97%) rename app/containers/message/components/{Attachments/CollapsibleQuote => __tests__}/CollapsibleQuote.test.tsx (92%) rename app/containers/message/components/{Attachments/Image => __tests__}/Image.test.tsx (81%) rename app/containers/message/components/{Attachments/Image/Container.test.tsx => __tests__/ImageContainer.test.tsx} (84%) rename app/containers/message/components/{Attachments => __tests__}/Quote.test.tsx (98%) rename app/containers/message/components/{Attachments => __tests__}/Reply.test.tsx (99%) diff --git a/app/containers/message/components/Attachments/utils.test.ts b/app/containers/message/components/__tests__/Attachments.utils.test.ts similarity index 97% rename from app/containers/message/components/Attachments/utils.test.ts rename to app/containers/message/components/__tests__/Attachments.utils.test.ts index d6b042dcdca..8f29a4627f0 100644 --- a/app/containers/message/components/Attachments/utils.test.ts +++ b/app/containers/message/components/__tests__/Attachments.utils.test.ts @@ -1,5 +1,5 @@ import { type IAttachment } from '../../../../definitions'; -import { isContentAttachment, isQuoteAttachment } from './utils'; +import { isContentAttachment, isQuoteAttachment } from '../Attachments/utils'; describe('Attachments predicates', () => { const quote: IAttachment = { author_name: 'rocket.cat', ts: new Date(), text: 'quoted message' }; diff --git a/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx b/app/containers/message/components/__tests__/CollapsibleQuote.test.tsx similarity index 92% rename from app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx rename to app/containers/message/components/__tests__/CollapsibleQuote.test.tsx index ecdd49d3ba3..a57a8ce173e 100644 --- a/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.test.tsx +++ b/app/containers/message/components/__tests__/CollapsibleQuote.test.tsx @@ -1,12 +1,12 @@ import { fireEvent, render, within } from '@testing-library/react-native'; import { Provider } from 'react-redux'; -import { setUser } from '../../../../../actions/login'; -import { mockedStore } from '../../../../../reducers/mockedStore'; -import { type TAnyMessageModel } from '../../../../../definitions'; -import { MessageProvider } from '../../../stores/MessageStore'; -import { MessageRoomProvider, type MessageRoomState } from '../../../stores/MessageRoomStore'; -import CollapsibleQuote from '.'; +import { setUser } from '../../../../actions/login'; +import { mockedStore } from '../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { MessageProvider } from '../../stores/MessageStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; +import CollapsibleQuote from '../Attachments/CollapsibleQuote'; const testAttachment = { ts: '1970-01-01T00:00:00.000Z', diff --git a/app/containers/message/components/Attachments/Image/Image.test.tsx b/app/containers/message/components/__tests__/Image.test.tsx similarity index 81% rename from app/containers/message/components/Attachments/Image/Image.test.tsx rename to app/containers/message/components/__tests__/Image.test.tsx index d23d6b3166b..41b29cd85ce 100644 --- a/app/containers/message/components/Attachments/Image/Image.test.tsx +++ b/app/containers/message/components/__tests__/Image.test.tsx @@ -1,10 +1,10 @@ import { render, waitFor } from '@testing-library/react-native'; import { Image as ExpoImage } from 'expo-image'; -import { WidthAwareContext } from '../../WidthAwareView'; -import { MessageImage } from './Image'; +import { WidthAwareContext } from '../WidthAwareView'; +import { MessageImage } from '../Attachments/Image/Image'; -jest.mock('../../../../../lib/methods/userPreferences', () => ({ +jest.mock('../../../../lib/methods/userPreferences', () => ({ useUserPreferences: jest.fn(() => [true, jest.fn()]) })); diff --git a/app/containers/message/components/Attachments/Image/Container.test.tsx b/app/containers/message/components/__tests__/ImageContainer.test.tsx similarity index 84% rename from app/containers/message/components/Attachments/Image/Container.test.tsx rename to app/containers/message/components/__tests__/ImageContainer.test.tsx index 4eb0217ecd5..0483324a1c5 100644 --- a/app/containers/message/components/Attachments/Image/Container.test.tsx +++ b/app/containers/message/components/__tests__/ImageContainer.test.tsx @@ -3,20 +3,20 @@ import { type ComponentProps } from 'react'; import { A11y } from 'react-native-a11y-order'; import { Provider } from 'react-redux'; -import { type TAnyMessageModel } from '../../../../../definitions'; -import { MessageProvider } from '../../../stores/MessageStore'; -import { MessageRoomProvider, type MessageRoomState } from '../../../stores/MessageRoomStore'; -import { mockedStore } from '../../../../../reducers/mockedStore'; -import ImageContainer from './Container'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { MessageProvider } from '../../stores/MessageStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; +import { mockedStore } from '../../../../reducers/mockedStore'; +import ImageContainer from '../Attachments/Image/Container'; -jest.mock('../../../../markdown', () => { +jest.mock('../../../markdown', () => { const React = require('react'); const { Text } = require('react-native'); return ({ msg }: { msg?: string }) => <Text>{msg}</Text>; }); -jest.mock('../../../hooks/useMediaAutoDownload', () => ({ +jest.mock('../../hooks/useMediaAutoDownload', () => ({ useMediaAutoDownload: jest.fn(() => ({ status: 'downloaded', onPress: jest.fn(), @@ -25,12 +25,12 @@ jest.mock('../../../hooks/useMediaAutoDownload', () => ({ })) })); -jest.mock('./Image', () => ({ +jest.mock('../Attachments/Image/Image', () => ({ MessageImage: () => null })); const mockUseAltTextSupported = jest.fn(); -jest.mock('../../../../../lib/hooks/useAltTextSupported', () => ({ +jest.mock('../../../../lib/hooks/useAltTextSupported', () => ({ useAltTextSupported: () => mockUseAltTextSupported() })); diff --git a/app/containers/message/components/Attachments/Quote.test.tsx b/app/containers/message/components/__tests__/Quote.test.tsx similarity index 98% rename from app/containers/message/components/Attachments/Quote.test.tsx rename to app/containers/message/components/__tests__/Quote.test.tsx index 7f4176f1973..d565fcfc5d6 100644 --- a/app/containers/message/components/Attachments/Quote.test.tsx +++ b/app/containers/message/components/__tests__/Quote.test.tsx @@ -1,7 +1,7 @@ import { fireEvent, render, waitFor } from '@testing-library/react-native'; import { Provider } from 'react-redux'; -import Quote from './Quote'; +import Quote from '../Attachments/Quote'; import { MessageProvider } from '../../stores/MessageStore'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { mockedStore } from '../../../../reducers/mockedStore'; diff --git a/app/containers/message/components/Attachments/Reply.test.tsx b/app/containers/message/components/__tests__/Reply.test.tsx similarity index 99% rename from app/containers/message/components/Attachments/Reply.test.tsx rename to app/containers/message/components/__tests__/Reply.test.tsx index 7b5d04350bc..f3ac9b32efd 100644 --- a/app/containers/message/components/Attachments/Reply.test.tsx +++ b/app/containers/message/components/__tests__/Reply.test.tsx @@ -1,7 +1,7 @@ import { fireEvent, render, waitFor } from '@testing-library/react-native'; import { Provider } from 'react-redux'; -import Reply from './Reply'; +import Reply from '../Attachments/Reply'; import { MessageProvider } from '../../stores/MessageStore'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { mockedStore } from '../../../../reducers/mockedStore'; From ca23750369f24c032fca69dfaa8cd593f3d44812 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 6 Jul 2026 16:21:22 -0300 Subject: [PATCH 087/144] refactor: move Attachments stories into components/stories and register snapshots (NATIVE-22) --- .../CollapsibleQuote.stories.tsx | 30 - .../Attachments.stories.tsx | 2 +- .../stories/CollapsibleQuote.stories.tsx | 52 + .../Image => stories}/Image.stories.tsx | 4 +- .../__snapshots__/leaves.test.tsx.snap | 4716 +++++++++++++++-- .../stories/__tests__/leaves.test.tsx | 6 + 6 files changed, 4195 insertions(+), 615 deletions(-) delete mode 100644 app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx rename app/containers/message/components/{Attachments => stories}/Attachments.stories.tsx (98%) create mode 100644 app/containers/message/components/stories/CollapsibleQuote.stories.tsx rename app/containers/message/components/{Attachments/Image => stories}/Image.stories.tsx (92%) diff --git a/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx b/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx deleted file mode 100644 index 4037b3e9749..00000000000 --- a/app/containers/message/components/Attachments/CollapsibleQuote/CollapsibleQuote.stories.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { View } from 'react-native'; - -import { MessageRoomProvider } from '../../../stores/MessageRoomStore'; -import CollapsibleQuote from '.'; - -const testAttachment = { - ts: '1970-01-01T00:00:00.000Z', - title: 'Engineering (9 today)', - fields: [ - { - title: 'Out Today:\n', - value: - 'Ricardo Mellu, 1 day, until Fri Mar 11\nLoma, 1 day, until Fri Mar 11\nAnitta, 3 hours\nDiego Carlitos, 19 days, until Fri Mar 11\nGabriel Vasconcelos, 5 days, until Fri Mar 11\nJorge Leite, 1 day, until Fri Mar 11\nKevin Aleman, 1 day, until Fri Mar 11\nPierre, 1 day, until Fri Mar 11\nTiago Evangelista Pinto, 1 day, until Fri Mar 11' - } - ], - attachments: [], - collapsed: true -}; - -export default { - title: 'CollapsibleQuote' -}; - -export const Item = () => ( - <View style={{ padding: 10 }}> - <MessageRoomProvider user={{ username: 'Marcos' }}> - <CollapsibleQuote attachment={testAttachment} getCustomEmoji={() => null} /> - </MessageRoomProvider> - </View> -); diff --git a/app/containers/message/components/Attachments/Attachments.stories.tsx b/app/containers/message/components/stories/Attachments.stories.tsx similarity index 98% rename from app/containers/message/components/Attachments/Attachments.stories.tsx rename to app/containers/message/components/stories/Attachments.stories.tsx index 62691a24bf5..3ffdee25d91 100644 --- a/app/containers/message/components/Attachments/Attachments.stories.tsx +++ b/app/containers/message/components/stories/Attachments.stories.tsx @@ -7,7 +7,7 @@ import { selectServerSuccess } from '../../../../actions/server'; import { type TAnyMessageModel } from '../../../../definitions'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; -import Attachments from './Attachments'; +import Attachments from '../Attachments/Attachments'; const mockMessageContext: Partial<MessageRoomState> = { baseUrl: 'https://open.rocket.chat', diff --git a/app/containers/message/components/stories/CollapsibleQuote.stories.tsx b/app/containers/message/components/stories/CollapsibleQuote.stories.tsx new file mode 100644 index 00000000000..12a68c9b352 --- /dev/null +++ b/app/containers/message/components/stories/CollapsibleQuote.stories.tsx @@ -0,0 +1,52 @@ +import { type ReactNode } from 'react'; +import { View } from 'react-native'; +import { Provider } from 'react-redux'; + +import { createMockedStore } from '../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../definitions'; +import { MessageRoomProvider } from '../../stores/MessageRoomStore'; +import { MessageProvider } from '../../stores/MessageStore'; +import CollapsibleQuote from '../Attachments/CollapsibleQuote'; + +const store = createMockedStore(); + +const item = { + id: 'msg-id', + msg: '', + u: { _id: 'author-id', username: 'rocket.cat' }, + autoTranslate: false +} as unknown as TAnyMessageModel; + +const StoryWrapper = ({ children }: { children: ReactNode }) => ( + <Provider store={store}> + <MessageRoomProvider user={{ username: 'Marcos' }}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> +); + +const testAttachment = { + ts: '1970-01-01T00:00:00.000Z', + title: 'Engineering (9 today)', + fields: [ + { + title: 'Out Today:\n', + value: + 'Ricardo Mellu, 1 day, until Fri Mar 11\nLoma, 1 day, until Fri Mar 11\nAnitta, 3 hours\nDiego Carlitos, 19 days, until Fri Mar 11\nGabriel Vasconcelos, 5 days, until Fri Mar 11\nJorge Leite, 1 day, until Fri Mar 11\nKevin Aleman, 1 day, until Fri Mar 11\nPierre, 1 day, until Fri Mar 11\nTiago Evangelista Pinto, 1 day, until Fri Mar 11' + } + ], + attachments: [], + collapsed: true +}; + +export default { + title: 'CollapsibleQuote' +}; + +export const Item = () => ( + <StoryWrapper> + <View style={{ padding: 10 }}> + <CollapsibleQuote attachment={testAttachment} getCustomEmoji={() => null} /> + </View> + </StoryWrapper> +); diff --git a/app/containers/message/components/Attachments/Image/Image.stories.tsx b/app/containers/message/components/stories/Image.stories.tsx similarity index 92% rename from app/containers/message/components/Attachments/Image/Image.stories.tsx rename to app/containers/message/components/stories/Image.stories.tsx index 78018ab8500..e762e7a75f2 100644 --- a/app/containers/message/components/Attachments/Image/Image.stories.tsx +++ b/app/containers/message/components/stories/Image.stories.tsx @@ -1,7 +1,7 @@ import { View } from 'react-native'; -import { WidthAwareContext } from '../../WidthAwareView'; -import { MessageImage } from './Image'; +import { WidthAwareContext } from '../WidthAwareView'; +import { MessageImage } from '../Attachments/Image/Image'; const MOCK_URI = 'https://picsum.photos/400/300'; diff --git a/app/containers/message/components/stories/__tests__/__snapshots__/leaves.test.tsx.snap b/app/containers/message/components/stories/__tests__/__snapshots__/leaves.test.tsx.snap index cb670e675f0..d56ec17f9da 100644 --- a/app/containers/message/components/stories/__tests__/__snapshots__/leaves.test.tsx.snap +++ b/app/containers/message/components/stories/__tests__/__snapshots__/leaves.test.tsx.snap @@ -310,6 +310,116 @@ exports[`Story Snapshots: Content should match snapshot 1`] = ` </View> `; +exports[`Story Snapshots: Default should match snapshot 1`] = ` +<View + style={ + { + "padding": 10, + } + } +> + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <ActivityIndicator + color="#6C727A" + size={54} + style={ + [ + { + "flex": 1, + "padding": 16, + }, + undefined, + ] + } + /> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> +</View> +`; + exports[`Story Snapshots: Discussion should match snapshot 1`] = ` <View style={ @@ -666,287 +776,164 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` exports[`Story Snapshots: EncryptedHidden should match snapshot 1`] = `null`; -exports[`Story Snapshots: MessageAvatar should match snapshot 1`] = ` +exports[`Story Snapshots: GIF should match snapshot 1`] = ` <View style={ { - "alignItems": "flex-end", - "width": 36, + "padding": 10, } } > <View - accessible={true} style={ [ { "borderRadius": 4, - "height": 36, - "width": 36, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", }, { - "marginTop": 4, + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", }, ] } - testID="avatar" > - <ExternalKeyboardView - canBeFocused={true} - enableA11yFocus={false} - enableContextMenu={false} - group={false} - haloEffect={true} - hasKeyDownPress={false} - hasKeyUpPress={true} - hasOnFocusChanged={true} - lockFocus={0} - onContextMenuPress={[Function]} - onFocusChange={[Function]} - onKeyUpPress={[Function]} - orderIndex={-1} - screenAutoA11yFocusDelay={300} + <ActivityIndicator + color="#6C727A" + size={54} style={ [ + { + "flex": 1, + "padding": 16, + }, undefined, - undefined, + ] + } + /> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + > + <View + style={ + [ + { + "borderRadius": 4, + "padding": 4, + }, + { + "backgroundColor": "#E4E7EA", + }, ] } > - <RNGestureHandlerButton - activeOpacity={1} - collapsable={false} - delayLongPress={600} - enabled={true} - handlerTag={-1} - handlerType="NativeViewGestureHandler" - innerRef={null} - onActiveStateChange={[Function]} - onGestureHandlerEvent={[Function]} - onGestureHandlerStateChange={[Function]} - onPress={[Function]} - rippleColor="#E4E7EA" + <Text style={ [ { - "backgroundColor": undefined, - "borderRadius": undefined, - "margin": undefined, - "marginBottom": undefined, - "marginEnd": undefined, - "marginHorizontal": undefined, - "marginLeft": undefined, - "marginRight": undefined, - "marginStart": undefined, - "marginTop": undefined, - "marginVertical": undefined, + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontWeight": "700", + "textAlign": "left", }, { - "cursor": undefined, + "color": "#1F2329", }, ] } - underlayColor="#E4E7EA" > - <View - collapsable={false} - style={ - { - "backgroundColor": "#E4E7EA", - "borderBottomLeftRadius": undefined, - "borderBottomRightRadius": undefined, - "borderRadius": undefined, - "borderTopLeftRadius": undefined, - "borderTopRightRadius": undefined, - "bottom": 0, - "left": 0, - "opacity": 0, - "position": "absolute", - "right": 0, - "top": 0, - } - } - /> - <View - accessibilityLabel="rocket.cat's avatar" - accessible={true} - style={{}} - > - <ViewManagerAdapter_ExpoImage - borderRadius={4} - containerViewRef={{"current":"Object"}} - contentFit="cover" - contentPosition={ - { - "left": "50%", - "top": "50%", - } - } - height={36} - nativeViewRef={{"current":"NativeComponent"}} - onError={[Function]} - onLoad={[Function]} - onLoadStart={[Function]} - onProgress={[Function]} - placeholder={[]} - priority="high" - source={ - [ - { - "headers": { - "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", - }, - "uri": "https://open.rocket.chat/avatar/rocket.cat?format=png&size=72", - }, - ] - } - style={ - { - "borderRadius": 4, - "height": 36, - "width": 36, - } - } - transition={null} - width={36} - /> - </View> - </RNGestureHandlerButton> - </ExternalKeyboardView> + GIF + </Text> + </View> </View> </View> `; -exports[`Story Snapshots: MessageError should match snapshot 1`] = ` -<View - accessibilityState={ - { - "busy": undefined, - "checked": undefined, - "disabled": undefined, - "expanded": undefined, - "selected": undefined, - } - } - accessibilityValue={ - { - "max": undefined, - "min": undefined, - "now": undefined, - "text": undefined, - } - } - accessible={true} - collapsable={false} - focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - onResponderGrant={[Function]} - onResponderMove={[Function]} - onResponderRelease={[Function]} - onResponderTerminate={[Function]} - onResponderTerminationRequest={[Function]} - onStartShouldSetResponder={[Function]} - style={ - { - "paddingLeft": 5, - } - } -> - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#EC0D2A", - "fontSize": 16, - }, - [ - { - "lineHeight": 16, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> -</View> -`; - -exports[`Story Snapshots: MessageErrorHidden should match snapshot 1`] = `null`; - -exports[`Story Snapshots: Pinned should match snapshot 1`] = ` -<Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#2F343D", - "fontSize": 16, - }, - [ - { - "lineHeight": 16, - }, - { - "paddingLeft": 5, - }, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - testID="Pinned message-pinned" -> -  -</Text> -`; - -exports[`Story Snapshots: PinnedHidden should match snapshot 1`] = `null`; - -exports[`Story Snapshots: Reactions should match snapshot 1`] = ` +exports[`Story Snapshots: Item should match snapshot 1`] = ` <View style={ { - "flexDirection": "row", - "flexWrap": "wrap", - "gap": 4, + "padding": 10, } } > <View - accessibilityLabel=":joy:, 2" - accessibilityRole="button" accessibilityState={ { "busy": undefined, "checked": undefined, "disabled": undefined, "expanded": undefined, - "selected": true, + "selected": undefined, } } accessibilityValue={ @@ -980,456 +967,3801 @@ exports[`Story Snapshots: Reactions should match snapshot 1`] = ` style={ [ { + "alignItems": "center", "borderRadius": 4, + "borderWidth": 1, + "flexDirection": "row", + "minHeight": 40, }, + undefined, { - "backgroundColor": "#E4E7EA", + "backgroundColor": "#F7F8FA", + "borderBottomColor": "#EBECEF", + "borderLeftColor": "#CBCED1", + "borderLeftWidth": 2, + "borderRightColor": "#EBECEF", + "borderTopColor": "#EBECEF", }, ] } - testID="message-reaction-:joy:" + testID="collapsibleQuoteTouchable-Engineering (9 today)" > <View style={ - [ - { - "alignItems": "center", - "borderRadius": 4, - "borderWidth": 1, - "flexDirection": "row", - "gap": 4, - "justifyContent": "center", - "minWidth": 46.3, - "paddingHorizontal": 4, - }, - { - "borderColor": "#1D74F5", - "height": 28, - }, - ] + { + "flexDirection": "row", + } } > - <Text + <View style={ { - "color": "#ffffff", - "fontSize": 13, + "borderRadius": 4, + "flex": 1, + "padding": 8, } } > - 😂 - </Text> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 14, - "fontWeight": "600", - "lineHeight": 18, - "textAlign": "left", - }, + <View + style={ { - "color": "#1D74F5", - }, - ] + "flexDirection": "row", + } + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "500", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + > + Engineering (9 today) + </Text> + </View> + </View> + <View + style={ + { + "alignItems": "center", + "height": 20, + "justifyContent": "center", + "right": 8, + "top": 8, + "width": 20, + } } > - 2 - </Text> + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#9EA2A8", + "fontSize": 22, + }, + [ + { + "lineHeight": 22, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> </View> </View> - <View - accessibilityLabel=":heart:, 1" - accessibilityRole="button" - accessibilityState={ - { - "busy": undefined, - "checked": undefined, - "disabled": undefined, - "expanded": undefined, - "selected": false, - } +</View> +`; + +exports[`Story Snapshots: Loading should match snapshot 1`] = ` +<View + style={ + { + "padding": 10, } - accessibilityValue={ - { - "max": undefined, - "min": undefined, - "now": undefined, - "text": undefined, - } + } +> + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] } - accessible={true} - collapsable={false} - focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - onResponderGrant={[Function]} - onResponderMove={[Function]} - onResponderRelease={[Function]} - onResponderTerminate={[Function]} - onResponderTerminationRequest={[Function]} - onStartShouldSetResponder={[Function]} + /> + <View style={ [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], { - "borderRadius": 4, + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, }, { - "backgroundColor": "#FFFFFF", + "backgroundColor": "#E4E7EA", }, ] } - testID="message-reaction-:heart:" - > - <View - style={ + /> + <View + style={ + [ [ { - "alignItems": "center", "borderRadius": 4, - "borderWidth": 1, - "flexDirection": "row", - "gap": 4, - "justifyContent": "center", - "minWidth": 46.3, - "paddingHorizontal": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", }, { "borderColor": "#CBCED1", - "height": 28, + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <ActivityIndicator + color="#6C727A" + size={54} + style={ + [ + { + "flex": 1, + "padding": 16, }, + undefined, ] } - > - <Text - style={ - { - "color": "#ffffff", - "fontSize": 13, - } - } - > - ❤️ - </Text> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 14, - "fontWeight": "600", - "lineHeight": 18, - "textAlign": "left", - }, - { - "color": "#1D74F5", - }, - ] - } - > - 1 - </Text> - </View> + /> </View> <View - accessibilityLabel="Add reaction" - accessibilityRole="button" - accessibilityState={ + style={ { - "busy": undefined, - "checked": undefined, - "disabled": undefined, - "expanded": undefined, - "selected": undefined, + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, } } - accessibilityValue={ - { - "max": undefined, - "min": undefined, - "now": undefined, - "text": undefined, - } + /> +</View> +`; + +exports[`Story Snapshots: MessageAvatar should match snapshot 1`] = ` +<View + style={ + { + "alignItems": "flex-end", + "width": 36, } + } +> + <View accessible={true} - collapsable={false} - focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } - onBlur={[Function]} - onClick={[Function]} - onFocus={[Function]} - onResponderGrant={[Function]} - onResponderMove={[Function]} - onResponderRelease={[Function]} - onResponderTerminate={[Function]} - onResponderTerminationRequest={[Function]} - onStartShouldSetResponder={[Function]} style={ [ { "borderRadius": 4, + "height": 36, + "width": 36, }, { - "backgroundColor": "#FFFFFF", + "marginTop": 4, }, ] } - testID="message-add-reaction" + testID="avatar" > - <View + <ExternalKeyboardView + canBeFocused={true} + enableA11yFocus={false} + enableContextMenu={false} + group={false} + haloEffect={true} + hasKeyDownPress={false} + hasKeyUpPress={true} + hasOnFocusChanged={true} + lockFocus={0} + onContextMenuPress={[Function]} + onFocusChange={[Function]} + onKeyUpPress={[Function]} + orderIndex={-1} + screenAutoA11yFocusDelay={300} style={ [ - { - "alignItems": "center", - "borderRadius": 4, - "borderWidth": 1, - "flexDirection": "row", - "gap": 4, - "justifyContent": "center", - "minWidth": 46.3, - "paddingHorizontal": 4, - }, - { - "borderColor": "#CBCED1", - "height": 28, - }, + undefined, + undefined, ] } > - <Text - allowFontScaling={false} - selectable={false} + <RNGestureHandlerButton + activeOpacity={1} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={-1} + handlerType="NativeViewGestureHandler" + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[Function]} + rippleColor="#E4E7EA" style={ [ { - "color": "#1D74F5", - "fontSize": 20, + "backgroundColor": undefined, + "borderRadius": undefined, + "margin": undefined, + "marginBottom": undefined, + "marginEnd": undefined, + "marginHorizontal": undefined, + "marginLeft": undefined, + "marginRight": undefined, + "marginStart": undefined, + "marginTop": undefined, + "marginVertical": undefined, }, - [ - { - "lineHeight": 20, - }, - undefined, - ], { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", + "cursor": undefined, }, - {}, ] } + underlayColor="#E4E7EA" > -  - </Text> - </View> - </View> -</View> -`; - -exports[`Story Snapshots: ReadReceipt should match snapshot 1`] = ` -<Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#095AD2", - "fontSize": 25, - }, - [ - { - "lineHeight": 25, - }, - { - "marginTop": -5, - "paddingLeft": 5, - }, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - testID="read-receipt-read" -> -  -</Text> -`; - -exports[`Story Snapshots: ReadReceiptHidden should match snapshot 1`] = `null`; - -exports[`Story Snapshots: ReadReceiptUnread should match snapshot 1`] = ` -<Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#9EA2A8", - "fontSize": 25, - }, - [ - { - "lineHeight": 25, - }, - { - "marginTop": -5, - "paddingLeft": 5, - }, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - testID="read-receipt-unread" -> -  -</Text> + <View + collapsable={false} + style={ + { + "backgroundColor": "#E4E7EA", + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "bottom": 0, + "left": 0, + "opacity": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + /> + <View + accessibilityLabel="rocket.cat's avatar" + accessible={true} + style={{}} + > + <ViewManagerAdapter_ExpoImage + borderRadius={4} + containerViewRef={{"current":"Object"}} + contentFit="cover" + contentPosition={ + { + "left": "50%", + "top": "50%", + } + } + height={36} + nativeViewRef={{"current":"NativeComponent"}} + onError={[Function]} + onLoad={[Function]} + onLoadStart={[Function]} + onProgress={[Function]} + placeholder={[]} + priority="high" + source={ + [ + { + "headers": { + "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", + }, + "uri": "https://open.rocket.chat/avatar/rocket.cat?format=png&size=72", + }, + ] + } + style={ + { + "borderRadius": 4, + "height": 36, + "width": 36, + } + } + transition={null} + width={36} + /> + </View> + </RNGestureHandlerButton> + </ExternalKeyboardView> + </View> +</View> `; -exports[`Story Snapshots: RepliedThread should match snapshot 1`] = ` +exports[`Story Snapshots: MessageError should match snapshot 1`] = ` <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} style={ { - "alignItems": "center", - "flexDirection": "row", - "gap": 10, + "paddingLeft": 5, } } - testID="message-thread-replied-on-Original thread message" > - <View - style={ - { - "alignItems": "flex-end", - "width": 36, - } - } - > - <Text - allowFontScaling={false} - selectable={false} - style={ - [ - { - "color": "#095AD2", - "fontSize": 20, - }, - [ - { - "lineHeight": 20, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> - </View> <Text - accessibilityLabel="Original thread message" - numberOfLines={1} + allowFontScaling={false} + selectable={false} style={ [ { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - "lineHeight": undefined, - }, - { - "backgroundColor": "transparent", - "flex": 1, - "fontFamily": "Inter", + "color": "#EC0D2A", "fontSize": 16, - "fontWeight": "400", - "textAlign": "left", }, + [ + { + "lineHeight": 16, + }, + undefined, + ], { - "color": "#095AD2", + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", }, + {}, ] } - testID="markdown-preview-Original thread message" > - Original thread message +  </Text> +</View> +`; + +exports[`Story Snapshots: MessageErrorHidden should match snapshot 1`] = `null`; + +exports[`Story Snapshots: MultipleImagesNewServer should match snapshot 1`] = ` +<View + style={ + { + "padding": 10, + "width": 350, + } + } +> <View style={ { - "alignItems": "center", - "justifyContent": "center", - "marginLeft": 4, - "marginRight": 4, + "gap": 4, } } > - <Text - allowFontScaling={false} - selectable={false} + <View style={ - [ + { + "gap": 4, + } + } + > + <View + accessibilityLabel="Image 1" + accessibilityRole="imagebutton" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "borderRadius": 4, + "flexDirection": "column", + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 54, + }, + [ + { + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + </View> + </View> + <View + accessibilityLabel="Alternative text" + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignSelf": "flex-start", + "borderRadius": 4, + "bottom": 12, + "height": 20, + "justifyContent": "center", + "paddingHorizontal": 4, + "position": "absolute", + "right": 12, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "700", + "lineHeight": 20, + "textAlign": "left", + }, + { + "color": "#1F2329", + }, + ] + } + > + ALT + </Text> + </View> + </View> + <View + style={ + { + "gap": 4, + } + } + > + <View + accessibilityLabel="Image 2" + accessibilityRole="imagebutton" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "borderRadius": 4, + "flexDirection": "column", + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 54, + }, + [ + { + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + </View> + </View> + <View + accessibilityLabel="Alternative text" + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignSelf": "flex-start", + "borderRadius": 4, + "bottom": 12, + "height": 20, + "justifyContent": "center", + "paddingHorizontal": 4, + "position": "absolute", + "right": 12, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "700", + "lineHeight": 20, + "textAlign": "left", + }, + { + "color": "#1F2329", + }, + ] + } + > + ALT + </Text> + </View> + </View> + <View + style={ + { + "gap": 4, + } + } + > + <View + accessibilityLabel="Image 3" + accessibilityRole="imagebutton" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "borderRadius": 4, + "flexDirection": "column", + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 54, + }, + [ + { + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + </View> + </View> + <View + accessibilityLabel="Alternative text" + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignSelf": "flex-start", + "borderRadius": 4, + "bottom": 12, + "height": 20, + "justifyContent": "center", + "paddingHorizontal": 4, + "position": "absolute", + "right": 12, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "700", + "lineHeight": 20, + "textAlign": "left", + }, + { + "color": "#1F2329", + }, + ] + } + > + ALT + </Text> + </View> + </View> + <View + style={ + { + "gap": 4, + } + } + > + <View + accessibilityLabel="Image 4" + accessibilityRole="imagebutton" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "borderRadius": 4, + "flexDirection": "column", + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 54, + }, + [ + { + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + </View> + </View> + <View + accessibilityLabel="Alternative text" + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignSelf": "flex-start", + "borderRadius": 4, + "bottom": 12, + "height": 20, + "justifyContent": "center", + "paddingHorizontal": 4, + "position": "absolute", + "right": 12, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "700", + "lineHeight": 20, + "textAlign": "left", + }, + { + "color": "#1F2329", + }, + ] + } + > + ALT + </Text> + </View> + </View> + <View + style={ + { + "gap": 4, + } + } + > + <View + accessibilityLabel="Image 5" + accessibilityRole="imagebutton" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "borderRadius": 4, + "flexDirection": "column", + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 54, + }, + [ + { + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + </View> + </View> + <View + accessibilityLabel="Alternative text" + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignSelf": "flex-start", + "borderRadius": 4, + "bottom": 12, + "height": 20, + "justifyContent": "center", + "paddingHorizontal": 4, + "position": "absolute", + "right": 12, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "700", + "lineHeight": 20, + "textAlign": "left", + }, + { + "color": "#1F2329", + }, + ] + } + > + ALT + </Text> + </View> + </View> + </View> +</View> +`; + +exports[`Story Snapshots: MultipleImagesOldServer should match snapshot 1`] = ` +<View + style={ + { + "padding": 10, + "width": 350, + } + } +> + <View + style={ + { + "gap": 4, + } + } + > + <View + style={ + { + "gap": 4, + } + } + > + <View + style={ + { + "gap": 2, + } + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + <Text + style={ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + } + } + > + <Text + accessibilityLabel="Image 1" + style={ + [ + { + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + ] + } + > + Image 1 + </Text> + </Text> + </Text> + </View> + <View + accessibilityLabel="Image without description" + accessibilityRole="imagebutton" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "borderRadius": 4, + "flexDirection": "column", + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 54, + }, + [ + { + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + </View> + </View> + </View> + <View + style={ + { + "gap": 4, + } + } + > + <View + style={ + { + "gap": 2, + } + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + <Text + style={ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + } + } + > + <Text + accessibilityLabel="Image 2" + style={ + [ + { + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + ] + } + > + Image 2 + </Text> + </Text> + </Text> + </View> + <View + accessibilityLabel="Image without description" + accessibilityRole="imagebutton" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "borderRadius": 4, + "flexDirection": "column", + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 54, + }, + [ + { + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + </View> + </View> + </View> + <View + style={ + { + "gap": 4, + } + } + > + <View + style={ + { + "gap": 2, + } + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + <Text + style={ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + } + } + > + <Text + accessibilityLabel="Image 3" + style={ + [ + { + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + ] + } + > + Image 3 + </Text> + </Text> + </Text> + </View> + <View + accessibilityLabel="Image without description" + accessibilityRole="imagebutton" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "borderRadius": 4, + "flexDirection": "column", + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 54, + }, + [ + { + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + </View> + </View> + </View> + <View + style={ + { + "gap": 4, + } + } + > + <View + style={ + { + "gap": 2, + } + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + <Text + style={ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + } + } + > + <Text + accessibilityLabel="Image 4" + style={ + [ + { + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + ] + } + > + Image 4 + </Text> + </Text> + </Text> + </View> + <View + accessibilityLabel="Image without description" + accessibilityRole="imagebutton" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "borderRadius": 4, + "flexDirection": "column", + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 54, + }, + [ + { + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + </View> + </View> + </View> + </View> +</View> +`; + +exports[`Story Snapshots: Pinned should match snapshot 1`] = ` +<Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 16, + }, + [ + { + "lineHeight": 16, + }, + { + "paddingLeft": 5, + }, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + testID="Pinned message-pinned" +> +  +</Text> +`; + +exports[`Story Snapshots: PinnedHidden should match snapshot 1`] = `null`; + +exports[`Story Snapshots: Reactions should match snapshot 1`] = ` +<View + style={ + { + "flexDirection": "row", + "flexWrap": "wrap", + "gap": 4, + } + } +> + <View + accessibilityLabel=":joy:, 2" + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": true, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "borderRadius": 4, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + testID="message-reaction-:joy:" + > + <View + style={ + [ + { + "alignItems": "center", + "borderRadius": 4, + "borderWidth": 1, + "flexDirection": "row", + "gap": 4, + "justifyContent": "center", + "minWidth": 46.3, + "paddingHorizontal": 4, + }, + { + "borderColor": "#1D74F5", + "height": 28, + }, + ] + } + > + <Text + style={ + { + "color": "#ffffff", + "fontSize": 13, + } + } + > + 😂 + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#1D74F5", + }, + ] + } + > + 2 + </Text> + </View> + </View> + <View + accessibilityLabel=":heart:, 1" + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": false, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "borderRadius": 4, + }, + { + "backgroundColor": "#FFFFFF", + }, + ] + } + testID="message-reaction-:heart:" + > + <View + style={ + [ + { + "alignItems": "center", + "borderRadius": 4, + "borderWidth": 1, + "flexDirection": "row", + "gap": 4, + "justifyContent": "center", + "minWidth": 46.3, + "paddingHorizontal": 4, + }, + { + "borderColor": "#CBCED1", + "height": 28, + }, + ] + } + > + <Text + style={ + { + "color": "#ffffff", + "fontSize": 13, + } + } + > + ❤️ + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "600", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#1D74F5", + }, + ] + } + > + 1 + </Text> + </View> + </View> + <View + accessibilityLabel="Add reaction" + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "borderRadius": 4, + }, + { + "backgroundColor": "#FFFFFF", + }, + ] + } + testID="message-add-reaction" + > + <View + style={ + [ + { + "alignItems": "center", + "borderRadius": 4, + "borderWidth": 1, + "flexDirection": "row", + "gap": 4, + "justifyContent": "center", + "minWidth": 46.3, + "paddingHorizontal": 4, + }, + { + "borderColor": "#CBCED1", + "height": 28, + }, + ] + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#1D74F5", + "fontSize": 20, + }, + [ + { + "lineHeight": 20, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + </View> +</View> +`; + +exports[`Story Snapshots: ReadReceipt should match snapshot 1`] = ` +<Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#095AD2", + "fontSize": 25, + }, + [ + { + "lineHeight": 25, + }, + { + "marginTop": -5, + "paddingLeft": 5, + }, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + testID="read-receipt-read" +> +  +</Text> +`; + +exports[`Story Snapshots: ReadReceiptHidden should match snapshot 1`] = `null`; + +exports[`Story Snapshots: ReadReceiptUnread should match snapshot 1`] = ` +<Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#9EA2A8", + "fontSize": 25, + }, + [ + { + "lineHeight": 25, + }, + { + "marginTop": -5, + "paddingLeft": 5, + }, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + testID="read-receipt-unread" +> +  +</Text> +`; + +exports[`Story Snapshots: RepliedThread should match snapshot 1`] = ` +<View + style={ + { + "alignItems": "center", + "flexDirection": "row", + "gap": 10, + } + } + testID="message-thread-replied-on-Original thread message" +> + <View + style={ + { + "alignItems": "flex-end", + "width": 36, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#095AD2", + "fontSize": 20, + }, + [ + { + "lineHeight": 20, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <Text + accessibilityLabel="Original thread message" + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#2F343D", + "lineHeight": undefined, + }, + { + "backgroundColor": "transparent", + "flex": 1, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#095AD2", + }, + ] + } + testID="markdown-preview-Original thread message" + > + Original thread message + </Text> + <View + style={ + { + "alignItems": "center", + "justifyContent": "center", + "marginLeft": 4, + "marginRight": 4, + } + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#6C727A", + "fontSize": 20, + }, + [ + { + "lineHeight": 20, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> +</View> +`; + +exports[`Story Snapshots: SingleImageNewServer should match snapshot 1`] = ` +<View + style={ + { + "padding": 10, + "width": 350, + } + } +> + <View + style={ + { + "gap": 4, + } + } + > + <View + style={ + { + "gap": 4, + } + } + > + <View + accessibilityLabel="A wavy orange and black pattern, designed to be used as a wallpaper" + accessibilityRole="imagebutton" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "borderRadius": 4, + "flexDirection": "column", + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 54, + }, + [ + { + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + </View> + </View> + <View + accessibilityLabel="Alternative text" + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignSelf": "flex-start", + "borderRadius": 4, + "bottom": 12, + "height": 20, + "justifyContent": "center", + "paddingHorizontal": 4, + "position": "absolute", + "right": 12, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 14, + "fontWeight": "700", + "lineHeight": 20, + "textAlign": "left", + }, + { + "color": "#1F2329", + }, + ] + } + > + ALT + </Text> + </View> + </View> + </View> +</View> +`; + +exports[`Story Snapshots: SingleImageNoAlt should match snapshot 1`] = ` +<View + style={ + { + "padding": 10, + "width": 350, + } + } +> + <View + style={ + { + "gap": 4, + } + } + > + <View + accessibilityLabel="Image without description" + accessibilityRole="imagebutton" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "borderRadius": 4, + "flexDirection": "column", + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 54, + }, + [ + { + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + </View> + </View> + </View> +</View> +`; + +exports[`Story Snapshots: SingleImageOldServer should match snapshot 1`] = ` +<View + style={ + { + "padding": 10, + "width": 350, + } + } +> + <View + style={ + { + "gap": 4, + } + } + > + <View + style={ + { + "gap": 4, + } + } + > + <View + style={ + { + "gap": 2, + } + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + <Text + style={ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + } + } + > + <Text + accessibilityLabel="A wavy orange and black pattern, designed to be used as a wallpaper" + style={ + [ + { + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + ] + } + > + A wavy orange and black pattern, designed to be used as a wallpaper + </Text> + </Text> + </Text> + </View> + <View + accessibilityLabel="Image without description" + accessibilityRole="imagebutton" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ { - "color": "#6C727A", - "fontSize": 20, - }, - [ + "borderRadius": 4, + "flexDirection": "column", + } + } + > + <View + onLayout={[Function]} + style={ { - "lineHeight": 20, - }, - undefined, - ], - { - "fontFamily": "custom", - "fontStyle": "normal", - "fontWeight": "normal", - }, - {}, - ] - } - > -  - </Text> + "flex": 1, + } + } + > + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#2F343D", + "fontSize": 54, + }, + [ + { + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + </View> + </View> + </View> </View> </View> `; @@ -2034,3 +5366,223 @@ exports[`Story Snapshots: User should match snapshot 1`] = ` /> </View> `; + +exports[`Story Snapshots: WithAltTextNewServer should match snapshot 1`] = ` +<View + style={ + { + "padding": 10, + } + } +> + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <ActivityIndicator + color="#6C727A" + size={54} + style={ + [ + { + "flex": 1, + "padding": 16, + }, + undefined, + ] + } + /> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> +</View> +`; + +exports[`Story Snapshots: WithAltTextOldServer should match snapshot 1`] = ` +<View + style={ + { + "padding": 10, + } + } +> + <View + style={ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "borderWidth": 0, + "bottom": 0, + "left": 0, + "position": "absolute", + "right": 0, + "top": 0, + }, + { + "backgroundColor": "#E4E7EA", + }, + ] + } + /> + <View + style={ + [ + [ + { + "borderRadius": 4, + "minHeight": 200, + "overflow": "hidden", + "width": "100%", + }, + { + "borderColor": "#CBCED1", + "borderRadius": 4, + "borderWidth": 1, + "overflow": "hidden", + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + <ActivityIndicator + color="#6C727A" + size={54} + style={ + [ + { + "flex": 1, + "padding": 16, + }, + undefined, + ] + } + /> + </View> + <View + style={ + { + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> +</View> +`; diff --git a/app/containers/message/components/stories/__tests__/leaves.test.tsx b/app/containers/message/components/stories/__tests__/leaves.test.tsx index e341085742d..5120fa78c3d 100644 --- a/app/containers/message/components/stories/__tests__/leaves.test.tsx +++ b/app/containers/message/components/stories/__tests__/leaves.test.tsx @@ -1,9 +1,12 @@ import { generateSnapshots } from '../../../../../../.rnstorybook/generateSnapshots'; +import * as attachments from '../Attachments.stories'; import * as broadcast from '../Broadcast.stories'; import * as callButton from '../CallButton.stories'; +import * as collapsibleQuote from '../CollapsibleQuote.stories'; import * as content from '../Content.stories'; import * as discussion from '../Discussion.stories'; import * as emoji from '../Emoji.stories'; +import * as image from '../Image.stories'; import * as messageAvatar from '../MessageAvatar.stories'; import * as reactions from '../Reactions.stories'; import * as repliedThread from '../RepliedThread.stories'; @@ -18,11 +21,14 @@ import * as pinned from '../RightIcons/Pinned.stories'; import * as readReceipt from '../RightIcons/ReadReceipt.stories'; import * as translated from '../RightIcons/Translated.stories'; +generateSnapshots(attachments); generateSnapshots(broadcast); generateSnapshots(callButton); +generateSnapshots(collapsibleQuote); generateSnapshots(content); generateSnapshots(discussion); generateSnapshots(emoji); +generateSnapshots(image); generateSnapshots(messageAvatar); generateSnapshots(reactions); generateSnapshots(repliedThread); From 06e966b3821069c9486cde762c1f33d227dc7a3e Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 6 Jul 2026 16:43:35 -0300 Subject: [PATCH 088/144] refactor: flatten CollapsibleQuote and OverlayComponent to single-file leaves (NATIVE-22) --- .../index.tsx => CollapsibleQuote.tsx} | 20 +++++++++---------- .../index.tsx => OverlayComponent.tsx} | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) rename app/containers/message/components/Attachments/{CollapsibleQuote/index.tsx => CollapsibleQuote.tsx} (87%) rename app/containers/message/components/{OverlayComponent/index.tsx => OverlayComponent.tsx} (75%) diff --git a/app/containers/message/components/Attachments/CollapsibleQuote/index.tsx b/app/containers/message/components/Attachments/CollapsibleQuote.tsx similarity index 87% rename from app/containers/message/components/Attachments/CollapsibleQuote/index.tsx rename to app/containers/message/components/Attachments/CollapsibleQuote.tsx index da948197c80..186bf88139b 100644 --- a/app/containers/message/components/Attachments/CollapsibleQuote/index.tsx +++ b/app/containers/message/components/Attachments/CollapsibleQuote.tsx @@ -2,16 +2,16 @@ import { transparentize } from 'color2k'; import { useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; -import { themes } from '../../../../../lib/constants/colors'; -import { type IAttachment } from '../../../../../definitions/IAttachment'; -import { type TGetCustomEmoji } from '../../../../../definitions/IEmoji'; -import { CustomIcon } from '../../../../CustomIcon'; -import { useTheme } from '../../../../../theme'; -import sharedStyles from '../../../../../views/Styles'; -import Markdown from '../../../../markdown'; -import { useMessageUser } from '../../../stores/MessageRoomStore'; -import Touchable from '../../Touchable'; -import { BUTTON_HIT_SLOP } from '../../../utils'; +import { themes } from '../../../../lib/constants/colors'; +import { type IAttachment } from '../../../../definitions/IAttachment'; +import { type TGetCustomEmoji } from '../../../../definitions/IEmoji'; +import { CustomIcon } from '../../../CustomIcon'; +import { useTheme } from '../../../../theme'; +import sharedStyles from '../../../../views/Styles'; +import Markdown from '../../../markdown'; +import { useMessageUser } from '../../stores/MessageRoomStore'; +import Touchable from '../Touchable'; +import { BUTTON_HIT_SLOP } from '../../utils'; const styles = StyleSheet.create({ button: { diff --git a/app/containers/message/components/OverlayComponent/index.tsx b/app/containers/message/components/OverlayComponent.tsx similarity index 75% rename from app/containers/message/components/OverlayComponent/index.tsx rename to app/containers/message/components/OverlayComponent.tsx index 9d06de4001b..938bd0ecfca 100644 --- a/app/containers/message/components/OverlayComponent/index.tsx +++ b/app/containers/message/components/OverlayComponent.tsx @@ -1,9 +1,9 @@ import { type StyleProp, View, type ViewStyle } from 'react-native'; -import styles from '../../styles'; -import { useTheme } from '../../../../theme'; -import RCActivityIndicator from '../../../ActivityIndicator'; -import { CustomIcon, type TIconsName } from '../../../CustomIcon'; +import styles from '../styles'; +import { useTheme } from '../../../theme'; +import RCActivityIndicator from '../../ActivityIndicator'; +import { CustomIcon, type TIconsName } from '../../CustomIcon'; const OverlayComponent = ({ loading = false, From 00027bc90f169886676fbe6da66007b9abc742e5 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 10:51:54 -0300 Subject: [PATCH 089/144] refactor: retire RoomView getCustomEmoji in favor of useCustomEmoji in ReactionsList (NATIVE-22) --- app/containers/ReactionsList/AllTab.tsx | 11 +++++----- .../ReactionsList/ReactionsList.stories.tsx | 20 +++++++++---------- .../ReactionsList/ReactionsList.test.tsx | 19 ++++++++---------- app/containers/ReactionsList/index.tsx | 8 ++++---- .../message/components/MessageAvatar.tsx | 2 +- .../message/components/Reactions.tsx | 2 +- app/views/RoomView/definitions.ts | 9 +-------- app/views/RoomView/index.tsx | 13 +----------- 8 files changed, 31 insertions(+), 53 deletions(-) diff --git a/app/containers/ReactionsList/AllTab.tsx b/app/containers/ReactionsList/AllTab.tsx index 9e927b1b148..1cfa875911f 100644 --- a/app/containers/ReactionsList/AllTab.tsx +++ b/app/containers/ReactionsList/AllTab.tsx @@ -4,23 +4,22 @@ import { Text, View, FlatList } from 'react-native'; import Emoji from '../message/components/Emoji'; import { useTheme } from '../../theme'; import { type IReaction } from '../../definitions'; -import { type TGetCustomEmoji } from '../../definitions/IEmoji'; import I18n from '../../i18n'; import styles from './styles'; import { useAppSelector } from '../../lib/hooks/useAppSelector'; +import { useCustomEmoji } from '../../lib/hooks/useCustomEmoji'; interface IAllReactionsListItemProps { - getCustomEmoji: TGetCustomEmoji; item: IReaction; } interface IAllTabProps { - getCustomEmoji: TGetCustomEmoji; reactions?: IReaction[]; } -const AllReactionsListItem = ({ item, getCustomEmoji }: IAllReactionsListItemProps) => { +const AllReactionsListItem = ({ item }: IAllReactionsListItemProps) => { const { colors } = useTheme(); + const getCustomEmoji = useCustomEmoji(); const useRealName = useAppSelector(state => state.settings.UI_Use_Real_Name); const username = useAppSelector(state => state.login.user.username); const count = item.usernames.length; @@ -60,12 +59,12 @@ const AllReactionsListItem = ({ item, getCustomEmoji }: IAllReactionsListItemPro ); }; -const AllTab = ({ reactions, getCustomEmoji }: IAllTabProps): ReactElement => ( +const AllTab = ({ reactions }: IAllTabProps): ReactElement => ( <View style={styles.allTabContainer} testID='reactionsListAllTab'> <FlatList data={reactions} contentContainerStyle={styles.listContainer} - renderItem={({ item }) => <AllReactionsListItem item={item} getCustomEmoji={getCustomEmoji} />} + renderItem={({ item }) => <AllReactionsListItem item={item} />} keyExtractor={item => item.emoji} /> </View> diff --git a/app/containers/ReactionsList/ReactionsList.stories.tsx b/app/containers/ReactionsList/ReactionsList.stories.tsx index 01f907525ed..d88d86fb6ad 100644 --- a/app/containers/ReactionsList/ReactionsList.stories.tsx +++ b/app/containers/ReactionsList/ReactionsList.stories.tsx @@ -1,17 +1,15 @@ import { View } from 'react-native'; -import { type TGetCustomEmoji, type ICustomEmoji } from '../../definitions'; +import { type ICustomEmojis } from '../../definitions'; import ReactionsList from '.'; import { mockedStore as store } from '../../reducers/mockedStore'; import { updateSettings } from '../../actions/settings'; +import { setCustomEmojis } from '../../actions/customEmojis'; -const getCustomEmoji: TGetCustomEmoji = content => { - const customEmoji = { - marioparty: { name: content, extension: 'gif' }, - react_rocket: { name: content, extension: 'png' }, - nyan_rocket: { name: content, extension: 'png' } - }[content] as ICustomEmoji; - return customEmoji; +const customEmojis: ICustomEmojis = { + marioparty: { name: 'marioparty', extension: 'gif' }, + react_rocket: { name: 'react_rocket', extension: 'png' }, + nyan_rocket: { name: 'nyan_rocket', extension: 'png' } }; const reactions = [ @@ -49,18 +47,20 @@ const reactions = [ export const ReactionsListStory = () => { store.dispatch(updateSettings('UI_Use_Real_Name', false)); + store.dispatch(setCustomEmojis(customEmojis)); return ( <View style={{ paddingVertical: 10, flex: 1 }}> - <ReactionsList getCustomEmoji={getCustomEmoji} reactions={reactions} /> + <ReactionsList reactions={reactions} /> </View> ); }; export const ReactionsListFullName = () => { store.dispatch(updateSettings('UI_Use_Real_Name', true)); + store.dispatch(setCustomEmojis(customEmojis)); return ( <View style={{ paddingVertical: 10, flex: 1 }}> - <ReactionsList getCustomEmoji={getCustomEmoji} reactions={reactions} /> + <ReactionsList reactions={reactions} /> </View> ); }; diff --git a/app/containers/ReactionsList/ReactionsList.test.tsx b/app/containers/ReactionsList/ReactionsList.test.tsx index dc431e8b2bc..4db20297f19 100644 --- a/app/containers/ReactionsList/ReactionsList.test.tsx +++ b/app/containers/ReactionsList/ReactionsList.test.tsx @@ -105,16 +105,12 @@ const mockStore = createStore( server: { server: 'https://open.rocket.chat', version: '6.0.0' - } + }, + customEmojis: {} } ) => state ); -const mockGetCustomEmoji = (emoji: string) => ({ - name: emoji, - extension: 'png' -}); - const mockReactions: IReaction[] = [ { _id: 'reaction1', @@ -138,13 +134,13 @@ describe('ReactionsList Integration Tests', () => { }); it('renders empty ReactionsList when no reactions', () => { - renderWithRedux(<ReactionsList getCustomEmoji={mockGetCustomEmoji} reactions={[]} />); + renderWithRedux(<ReactionsList reactions={[]} />); expect(screen.getByTestId('reactionsList')).toBeOnTheScreen(); expect(screen.getByTestId('reactionsListAllTab')).toBeOnTheScreen(); }); it('renders ReactionsList with reactions and allows navigation between tabs', () => { - renderWithRedux(<ReactionsList getCustomEmoji={mockGetCustomEmoji} reactions={mockReactions} />); + renderWithRedux(<ReactionsList reactions={mockReactions} />); // Verify All tab content expect(screen.getByText('4 people reacted')).toBeOnTheScreen(); @@ -185,14 +181,15 @@ describe('ReactionsList Integration Tests', () => { server: { server: 'https://open.rocket.chat', version: '6.0.0' - } + }, + customEmojis: {} } ) => state ); render( <Provider store={storeWithoutRealNames}> - <ReactionsList getCustomEmoji={mockGetCustomEmoji} reactions={mockReactions} /> + <ReactionsList reactions={mockReactions} /> </Provider> ); @@ -227,7 +224,7 @@ describe('ReactionsList Integration Tests', () => { } ]; - renderWithRedux(<ReactionsList getCustomEmoji={mockGetCustomEmoji} reactions={customReactions} />); + renderWithRedux(<ReactionsList reactions={customReactions} />); // Verify custom emoji is rendered expect(screen.getByTestId('tab-:custom_emoji:')).toBeOnTheScreen(); diff --git a/app/containers/ReactionsList/index.tsx b/app/containers/ReactionsList/index.tsx index d1beac082e9..11e96924b27 100644 --- a/app/containers/ReactionsList/index.tsx +++ b/app/containers/ReactionsList/index.tsx @@ -1,6 +1,5 @@ import { Text, View } from 'react-native'; -import { type TGetCustomEmoji } from '../../definitions/IEmoji'; import { type IReaction } from '../../definitions'; import I18n from '../../i18n'; import styles from './styles'; @@ -8,9 +7,9 @@ import AllTab from './AllTab'; import UsersList from './UsersList'; import { TabView } from '../TabView'; import Emoji from '../message/components/Emoji'; +import { useCustomEmoji } from '../../lib/hooks/useCustomEmoji'; interface IReactionsListProps { - getCustomEmoji: TGetCustomEmoji; reactions?: IReaction[]; } @@ -50,12 +49,13 @@ const useRoutes = (reactions: IReaction[] | undefined) => { }; }; -const ReactionsList = ({ reactions, getCustomEmoji }: IReactionsListProps) => { +const ReactionsList = ({ reactions }: IReactionsListProps) => { + const getCustomEmoji = useCustomEmoji(); const { routes, sortedReactions } = useRoutes(reactions); const renderScene = ({ route }: { route: IRoute }) => { if (route.key === 'all') { - return <AllTab reactions={sortedReactions} getCustomEmoji={getCustomEmoji} />; + return <AllTab reactions={sortedReactions} />; } if (route.emoji && route.usernames && route.names) { return <UsersList emoji={route.emoji} usernames={route.usernames} names={route.names} />; diff --git a/app/containers/message/components/MessageAvatar.tsx b/app/containers/message/components/MessageAvatar.tsx index 18f246851a5..5ca505d4f72 100644 --- a/app/containers/message/components/MessageAvatar.tsx +++ b/app/containers/message/components/MessageAvatar.tsx @@ -48,7 +48,7 @@ const MessageAvatar = ({ small }: IMessageAvatar) => { size={size} borderRadius={4} onPress={onPress} - getCustomEmoji={getCustomEmoji ?? (() => null)} + getCustomEmoji={getCustomEmoji} avatar={avatar} emoji={emoji} /> diff --git a/app/containers/message/components/Reactions.tsx b/app/containers/message/components/Reactions.tsx index 0605c20a309..113a99be18c 100644 --- a/app/containers/message/components/Reactions.tsx +++ b/app/containers/message/components/Reactions.tsx @@ -77,7 +77,7 @@ const Reaction = ({ reaction }: IMessageReaction) => { content={reaction.emoji} standardEmojiStyle={styles.reactionEmoji} customEmojiStyle={styles.reactionCustomEmoji} - getCustomEmoji={getCustomEmoji ?? (() => null)} + getCustomEmoji={getCustomEmoji} /> <Text style={[styles.reactionCount, { color: colors.badgeBackgroundLevel2 }]}>{reaction.usernames.length}</Text> </View> diff --git a/app/views/RoomView/definitions.ts b/app/views/RoomView/definitions.ts index 6aabaf7dd0e..dca60c4ba5b 100644 --- a/app/views/RoomView/definitions.ts +++ b/app/views/RoomView/definitions.ts @@ -1,13 +1,7 @@ import { type EdgeInsets } from 'react-native-safe-area-context'; import { type ChatsStackParamList } from '../../stacks/types'; -import { - type IBaseScreen, - type ILastMessage, - type ILoggedUser, - type TSubscriptionModel, - type ICustomEmojis -} from '../../definitions'; +import { type IBaseScreen, type ILastMessage, type ILoggedUser, type TSubscriptionModel } from '../../definitions'; import { type IActionSheetProvider } from '../../containers/ActionSheet'; export interface IRoomViewProps extends IActionSheetProvider, IBaseScreen<ChatsStackParamList, 'RoomView'> { @@ -18,7 +12,6 @@ export interface IRoomViewProps extends IActionSheetProvider, IBaseScreen<ChatsS Hide_System_Messages?: string[]; baseUrl: string; serverVersion: string | null; - customEmojis: ICustomEmojis; isMasterDetail: boolean; replyBroadcast: Function; width: number; diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 3313e534122..f62432c9e94 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -70,7 +70,6 @@ import { type TAnyMessageModel, type TSubscriptionModel, type IEmoji, - type TGetCustomEmoji, type RoomType } from '../../definitions'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/constants/keys'; @@ -932,7 +931,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { onReactionLongPress = (message: TAnyMessageModel) => { const { showActionSheet } = this.props; this.handleCloseEmoji(showActionSheet, { - children: <ReactionsList reactions={message?.reactions} getCustomEmoji={this.getCustomEmoji} />, + children: <ReactionsList reactions={message?.reactions} />, snaps: ['50%'], enableContentPanningGesture: false, fullContainer: true @@ -1129,15 +1128,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { this.resetAction(); }; - getCustomEmoji: TGetCustomEmoji = name => { - const { customEmojis } = this.props; - const emoji = customEmojis[name]; - if (emoji) { - return emoji; - } - return null; - }; - setLastOpen = (lastOpen: Date | null) => this.setState({ lastOpen }); onJoin = () => { @@ -1720,7 +1710,6 @@ const mapStateToProps = (state: IApplicationState) => ({ user: getUserSelector(state), isAuthenticated: state.login.isAuthenticated, Message_GroupingPeriod: state.settings.Message_GroupingPeriod as number, - customEmojis: state.customEmojis, baseUrl: state.server.server, serverVersion: state.server.version, Message_Read_Receipt_Enabled: state.settings.Message_Read_Receipt_Enabled as boolean, From 0a3d98fceebc825875561869d3c21cd8ac6a78a6 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 10:51:54 -0300 Subject: [PATCH 090/144] refactor: dedupe TMessageAction into definitions (NATIVE-22) --- .../MessageComposer/components/MessageComposerContent.tsx | 2 +- app/views/RoomView/InteractionStore.tsx | 2 +- app/views/RoomView/context.ts | 2 -- app/views/ShareView/index.tsx | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/containers/MessageComposer/components/MessageComposerContent.tsx b/app/containers/MessageComposer/components/MessageComposerContent.tsx index 7c729c54c7a..d7d3498bc3c 100644 --- a/app/containers/MessageComposer/components/MessageComposerContent.tsx +++ b/app/containers/MessageComposer/components/MessageComposerContent.tsx @@ -1,7 +1,7 @@ import { memo, type ReactElement, type RefObject } from 'react'; import { type LayoutChangeEvent, StyleSheet, View } from 'react-native'; -import { type TMessageAction } from '../../../views/RoomView/context'; +import { type TMessageAction } from '../../../definitions'; import { type IComposerInput } from '../interfaces'; import { useTheme } from '../../../theme'; import { RecordAudio } from './RecordAudio'; diff --git a/app/views/RoomView/InteractionStore.tsx b/app/views/RoomView/InteractionStore.tsx index eee4ed73f08..8bd3e68741e 100644 --- a/app/views/RoomView/InteractionStore.tsx +++ b/app/views/RoomView/InteractionStore.tsx @@ -2,7 +2,7 @@ import { createContext, type ReactElement, useContext, useState } from 'react'; import { createStore, useStore } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; -import { type TMessageAction } from './context'; +import { type TMessageAction } from '../../definitions'; export type TInteraction = | { kind: 'edit'; messageId: string } diff --git a/app/views/RoomView/context.ts b/app/views/RoomView/context.ts index 16c04d8e11c..b6cedd28790 100644 --- a/app/views/RoomView/context.ts +++ b/app/views/RoomView/context.ts @@ -1,7 +1,5 @@ import { createContext, useContext } from 'react'; -export type TMessageAction = 'reply' | 'quote' | 'edit' | 'react' | null; - export interface IRoomContext { rid?: string; t?: string; diff --git a/app/views/ShareView/index.tsx b/app/views/ShareView/index.tsx index 00bc23be3ec..bd3fd8b10a6 100644 --- a/app/views/ShareView/index.tsx +++ b/app/views/ShareView/index.tsx @@ -78,7 +78,7 @@ interface IShareViewProps { type TShareServerInfo = Partial<Pick<IServer, 'version' | 'FileUpload_MaxFileSize' | 'FileUpload_MediaTypeWhiteList'>>; // ShareView only supports the quote flow (messages are filled later via startShareView -> setQuotes); -// edit/react/reply carry no messageId here, so they can't be represented by the union. +// edit/react carry no messageId here, so they can't be represented by the union. const mapActionToInteraction = (action: TMessageAction): TInteraction => action === 'quote' ? { kind: 'quote', messageIds: [] } : null; From ce3d8ee6aa6f048d15a1024683c947b145ddd8c1 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 10:51:54 -0300 Subject: [PATCH 091/144] refactor: rename useIsInfo to useIsInfoMessage returning boolean (NATIVE-22) --- app/containers/message/components/Content.tsx | 4 +- app/containers/message/components/Message.tsx | 6 +- .../components/Message/CompactMessage.tsx | 4 +- .../hooks/useMessageAccessibilityLabel.ts | 4 +- .../message/stores/MessageStore.tsx | 8 +- .../stores/__tests__/MessageStore.test.tsx | 118 ++++++++++++++++-- 6 files changed, 121 insertions(+), 23 deletions(-) diff --git a/app/containers/message/components/Content.tsx b/app/containers/message/components/Content.tsx index b89a133ca84..4fa72707a87 100644 --- a/app/containers/message/components/Content.tsx +++ b/app/containers/message/components/Content.tsx @@ -3,13 +3,13 @@ import IgnoredContent from './Content/IgnoredContent'; import InfoContent from './Content/InfoContent'; import MarkdownContent from './Content/MarkdownContent'; import PreviewContent from './Content/PreviewContent'; -import { useIsEncrypted, useIsInfo, useMessageIgnored, useMessageText, useThreadData } from '../stores/MessageStore'; +import { useIsEncrypted, useIsInfoMessage, useMessageIgnored, useMessageText, useThreadData } from '../stores/MessageStore'; import { useIsThreadRoom } from '../stores/MessageRoomStore'; const Content = () => { 'use memo'; - const isInfo = useIsInfo(); + const isInfo = useIsInfoMessage(); const isIgnored = useMessageIgnored(); const isEncrypted = useIsEncrypted(); const { tmid } = useThreadData(); diff --git a/app/containers/message/components/Message.tsx b/app/containers/message/components/Message.tsx index 9d03e74d1c2..cd82c3961dd 100644 --- a/app/containers/message/components/Message.tsx +++ b/app/containers/message/components/Message.tsx @@ -11,7 +11,7 @@ import { useMessageAccessibilityHint } from '../hooks/useMessageAccessibilityHin import { useIsBeingEdited } from '../../../views/RoomView/InteractionStore'; import { useArchived } from '../stores/MessageRoomStore'; import { - useIsInfo, + useIsInfoMessage, useMessageField, useMessageIgnored, useMessageLongPress, @@ -29,7 +29,7 @@ const Message = (props: TMessageProps) => { 'use memo'; const { isThreadReply, isThreadSequential } = useThreadPosition(); - const isInfo = useIsInfo(); + const isInfo = useIsInfoMessage(); const isIgnored = useMessageIgnored(); if (isThreadReply || isThreadSequential || isInfo || isIgnored) { @@ -46,7 +46,7 @@ const MessageTouchable = (props: TMessageProps) => { const { colors } = useTheme(); const { ref: touchRef, markAsLastFocused } = useLastFocusedMessageRef(); const { isThreadReply } = useThreadPosition(); - const isInfo = useIsInfo(); + const isInfo = useIsInfoMessage(); const archived = useArchived(); const { hasError, isTemp } = useMessageStatus(); const type = useMessageField(item => item.t); diff --git a/app/containers/message/components/Message/CompactMessage.tsx b/app/containers/message/components/Message/CompactMessage.tsx index e6911df77f0..1abf13ceee7 100644 --- a/app/containers/message/components/Message/CompactMessage.tsx +++ b/app/containers/message/components/Message/CompactMessage.tsx @@ -8,7 +8,7 @@ import Content from '../Content'; import Attachments from '../Attachments'; import { useContentData, - useIsInfo, + useIsInfoMessage, useMessageField, useMessageGrouping, useMessageText, @@ -21,7 +21,7 @@ const CompactMessage = () => { const isHeader = useMessageGrouping(); const { isThreadReply } = useThreadPosition(); - const isInfo = useIsInfo(); + const isInfo = useIsInfoMessage(); const { messageText, isTranslated } = useMessageText(); const { t: type, attachments } = useContentData(); const author = useMessageField(item => item.u); diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.ts b/app/containers/message/hooks/useMessageAccessibilityLabel.ts index c9874d37b73..685cd266bdd 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.ts +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.ts @@ -6,7 +6,7 @@ import { type IUserChannel, type IUserMention } from '../../../definitions'; import { useContentData, useIsEncrypted, - useIsInfo, + useIsInfoMessage, useMessageAuthor, useMessageHeaderMeta, useMessageText, @@ -39,7 +39,7 @@ export const useMessageAccessibilityLabel = (): string => { const { messageText, isTranslated } = useMessageText(); const { tmid } = useThreadData(); const { isThreadReply, isThreadSequential } = useThreadPosition(); - const isInfo = useIsInfo(); + const isInfo = useIsInfoMessage(); const isEncrypted = useIsEncrypted(); const { ts, unread } = useMessageHeaderMeta(); const isReadReceiptEnabled = useIsReadReceiptEnabled(); diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index e7e8c6729e8..53ad2bb0fbd 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -231,12 +231,12 @@ export const useMessageStatus = (): { hasError: boolean; isTemp: boolean } => export const useIsEncrypted = (): boolean => useMessageField(item => item.t === E2E_MESSAGE_TYPE && item.e2e !== E2E_STATUS.DONE); -export const useIsInfo = (): string | boolean => +export const useIsInfoMessage = (): boolean => useMessageField(item => { if (['e2e', 'discussion-created', 'jitsi_call_started', 'videoconf'].includes(item.t as string)) { return false; } - return item.t; + return !!item.t; }); export const useIsEdited = (): boolean => useMessageField(item => (item.editedBy && !!item.editedBy.username) ?? false); @@ -283,7 +283,7 @@ export const useMessageLongPress = (): (() => void) => { 'use memo'; const item = useMessageItem(); - const isInfo = useIsInfo(); + const isInfo = useIsInfoMessage(); const { hasError } = useMessageStatus(); const isEncrypted = useIsEncrypted(); const archived = useArchived(); @@ -297,6 +297,8 @@ export const useMessageLongPress = (): (() => void) => { }; export const useOnLinkPress = (): ((link: string) => void) => { + 'use memo'; + const item = useMessageItem(); const jumpToMessage = useJumpToMessage(); const { theme } = useTheme(); diff --git a/app/containers/message/stores/__tests__/MessageStore.test.tsx b/app/containers/message/stores/__tests__/MessageStore.test.tsx index 1affac51e8f..649331adb8b 100644 --- a/app/containers/message/stores/__tests__/MessageStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageStore.test.tsx @@ -1,11 +1,13 @@ -import { act, fireEvent, render } from '@testing-library/react-native'; +import { act, fireEvent, render, renderHook } from '@testing-library/react-native'; +import { type ReactNode } from 'react'; import { Text } from 'react-native'; import { Provider } from 'react-redux'; -import { type TAnyMessageModel } from '../../../../definitions'; +import { type IAttachment, type TAnyMessageModel } from '../../../../definitions'; import { mockedStore } from '../../../../reducers/mockedStore'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../../../lib/constants/keys'; import { messagesStatus } from '../../../../lib/constants/messagesStatus'; +import openLink from '../../../../lib/methods/helpers/openLink'; import { MessageRoomProvider, type MessageRoomState } from '../MessageRoomStore'; import { MessageProvider, @@ -15,7 +17,7 @@ import { useDiscussion, useIsEdited, useIsEncrypted, - useIsInfo, + useIsInfoMessage, useMessageAuthor, useMessageField, useMessageGrouping, @@ -24,14 +26,23 @@ import { useMessageItem, useMessageStatus, useMessageText, + useOnLinkPress, useReactions, useRepliedThreadData, useRevealIgnored, useThreadData, useThreadPosition, + useTranslateLanguage, useUrls } from '../MessageStore'; +jest.mock('../../../../lib/methods/helpers/openLink', () => ({ + __esModule: true, + default: jest.fn() +})); + +const mockOpenLink = openLink as jest.Mock; + type Subscriber = () => void; type FakeModel = TAnyMessageModel & { _emit: () => void; experimentalSubscribe: (cb: Subscriber) => () => void }; @@ -147,6 +158,17 @@ const renderDerived = ( return { latest, spy }; }; +const wrapWithProviders = + (item: TAnyMessageModel, config?: Partial<MessageRoomState>) => + ({ children }: { children: ReactNode }) => + ( + <Provider store={mockedStore}> + <MessageRoomProvider {...(config ?? {})}> + <MessageProvider item={item}>{children}</MessageProvider> + </MessageRoomProvider> + </Provider> + ); + describe('MessageStore', () => { describe('field hooks', () => { it('return the current field values', () => { @@ -359,22 +381,22 @@ describe('MessageStore', () => { expect(latest()).toBe(false); }); - it('useIsInfo returns false for a discussion-created message', () => { + it('useIsInfoMessage returns false for a discussion-created message', () => { const model = buildFakeModel({ t: 'discussion-created' }); - const { latest } = renderDerived(model, useIsInfo); + const { latest } = renderDerived(model, useIsInfoMessage); expect(latest()).toBe(false); }); - it('useIsInfo returns the type for an info message', () => { + it('useIsInfoMessage returns true for an info message', () => { const model = buildFakeModel({ t: 'room_changed_topic' }); - const { latest } = renderDerived(model, useIsInfo); - expect(latest()).toBe('room_changed_topic'); + const { latest } = renderDerived(model, useIsInfoMessage); + expect(latest()).toBe(true); }); - it('useIsInfo returns undefined for a default text message', () => { + it('useIsInfoMessage returns false for a default text message', () => { const model = buildFakeModel(); - const { latest } = renderDerived(model, useIsInfo); - expect(latest()).toBeUndefined(); + const { latest } = renderDerived(model, useIsInfoMessage); + expect(latest()).toBe(false); }); it('useIsEdited returns false for a default model', () => { @@ -672,4 +694,78 @@ describe('MessageStore', () => { expect(spy).toHaveBeenLastCalledWith(true); }); }); + + describe('useOnLinkPress', () => { + beforeEach(() => { + mockOpenLink.mockClear(); + }); + + it('jumps to the message when the link matches an attachment message_link', () => { + const jumpToMessage = jest.fn(); + const link = 'https://example.com/msg-link'; + const model = buildFakeModel({ attachments: [{ message_link: link } as IAttachment] }); + const { result } = renderHook(() => useOnLinkPress(), { wrapper: wrapWithProviders(model, { jumpToMessage }) }); + + result.current(link); + + expect(jumpToMessage).toHaveBeenCalledWith(link); + expect(mockOpenLink).not.toHaveBeenCalled(); + }); + + it('opens the link when it does not match any attachment message_link', () => { + const jumpToMessage = jest.fn(); + const link = 'https://example.com/other'; + const model = buildFakeModel({ attachments: [{ message_link: 'https://example.com/msg-link' } as IAttachment] }); + const { result } = renderHook(() => useOnLinkPress(), { wrapper: wrapWithProviders(model, { jumpToMessage }) }); + + result.current(link); + + expect(jumpToMessage).not.toHaveBeenCalled(); + expect(mockOpenLink).toHaveBeenCalledWith(link, 'light'); + }); + + it('does not jump when attachments is undefined', () => { + const jumpToMessage = jest.fn(); + const link = 'https://example.com/msg-link'; + const model = buildFakeModel({ attachments: undefined }); + const { result } = renderHook(() => useOnLinkPress(), { wrapper: wrapWithProviders(model, { jumpToMessage }) }); + + result.current(link); + + expect(jumpToMessage).not.toHaveBeenCalled(); + expect(mockOpenLink).toHaveBeenCalledWith(link, 'light'); + }); + }); + + describe('translation boundary on item.autoTranslate', () => { + const translationConfig = { autoTranslateRoom: true, autoTranslateLanguage: 'pt-BR', user: { username: 'alice' } }; + + it('useTranslateLanguage returns the language when autoTranslate is undefined', () => { + const model = buildFakeModel({ autoTranslate: undefined, u: { _id: 'u2', username: 'bob' } }); + const { latest } = renderDerived(model, useTranslateLanguage, { config: translationConfig }); + expect(latest()).toBe('pt-BR'); + }); + + it('useTranslateLanguage returns the language when autoTranslate is true', () => { + const model = buildFakeModel({ autoTranslate: true, u: { _id: 'u2', username: 'bob' } }); + const { latest } = renderDerived(model, useTranslateLanguage, { config: translationConfig }); + expect(latest()).toBe('pt-BR'); + }); + + it('useTranslateLanguage returns undefined when autoTranslate is false', () => { + const model = buildFakeModel({ autoTranslate: false, u: { _id: 'u2', username: 'bob' } }); + const { latest } = renderDerived(model, useTranslateLanguage, { config: translationConfig }); + expect(latest()).toBeUndefined(); + }); + + it('useMessageText does not translate when autoTranslate is undefined, even in an auto-translating room', () => { + const model = buildFakeModel({ + autoTranslate: undefined, + translations: [{ _id: 't1', language: 'pt-BR', value: 'Olá mundo' }], + u: { _id: 'u2', username: 'bob' } + }); + const { latest } = renderDerived(model, useMessageText, { config: translationConfig }); + expect(latest()).toEqual({ messageText: model.msg, isTranslated: false }); + }); + }); }); From 4c906ea78ec72b209a28af3ae5b326898d99dd7e Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 10:51:54 -0300 Subject: [PATCH 092/144] refactor: pass isHeader into MessageInner instead of re-reading grouping (NATIVE-22) --- app/containers/message/components/Message/MessageInner.tsx | 5 ++--- app/containers/message/components/Message/NormalMessage.tsx | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/containers/message/components/Message/MessageInner.tsx b/app/containers/message/components/Message/MessageInner.tsx index af60924285a..c42e4884748 100644 --- a/app/containers/message/components/Message/MessageInner.tsx +++ b/app/containers/message/components/Message/MessageInner.tsx @@ -5,13 +5,12 @@ import JitsiBranch from './JitsiBranch'; import DiscussionBranch from './DiscussionBranch'; import PreviewBranch from './PreviewBranch'; import DefaultBranch from './DefaultBranch'; -import { useBlocks, useMessageField, useMessageGrouping } from '../../stores/MessageStore'; +import { useBlocks, useMessageField } from '../../stores/MessageStore'; -export const MessageInner = ({ isPreview }: { isPreview?: boolean }) => { +export const MessageInner = ({ isPreview, isHeader }: { isPreview?: boolean; isHeader: boolean }) => { 'use memo'; const { isLargeFontScale } = useResponsiveLayout(); - const isHeader = useMessageGrouping(); const type = useMessageField(item => item.t); const { blocks } = useBlocks(); const showTimeLarge = isLargeFontScale && isHeader; diff --git a/app/containers/message/components/Message/NormalMessage.tsx b/app/containers/message/components/Message/NormalMessage.tsx index 2bf17415eab..e2f29828aeb 100644 --- a/app/containers/message/components/Message/NormalMessage.tsx +++ b/app/containers/message/components/Message/NormalMessage.tsx @@ -26,7 +26,7 @@ const NormalMessage = ({ isPreview }: { isPreview?: boolean }) => { <View style={styles.flex}> <MessageAvatar /> <View style={styles.messageContent}> - <MessageInner isPreview={isPreview} /> + <MessageInner isPreview={isPreview} isHeader={isHeader} /> </View> {!isHeader ? <RightIcons /> : null} </View> From 81760b8c6f6b2a4cab3013cb67b602780f3babfa Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 10:51:54 -0300 Subject: [PATCH 093/144] fix: remove self-assignment in CollapsibleQuote color override (NATIVE-22) --- .../message/components/Attachments/CollapsibleQuote.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/containers/message/components/Attachments/CollapsibleQuote.tsx b/app/containers/message/components/Attachments/CollapsibleQuote.tsx index 186bf88139b..ac2905783db 100644 --- a/app/containers/message/components/Attachments/CollapsibleQuote.tsx +++ b/app/containers/message/components/Attachments/CollapsibleQuote.tsx @@ -123,7 +123,8 @@ const CollapsibleQuote = ({ attachment, getCustomEmoji }: IMessageReply) => { setCollapsed(!collapsed); }; - let { strokeExtraLight, surfaceTint: backgroundColor, strokeLight, strokeMedium, fontSecondaryInfo } = themes[theme]; + const { fontSecondaryInfo } = themes[theme]; + let { strokeExtraLight, surfaceTint: backgroundColor, strokeLight, strokeMedium } = themes[theme]; try { if (attachment.color) { @@ -131,7 +132,6 @@ const CollapsibleQuote = ({ attachment, getCustomEmoji }: IMessageReply) => { strokeExtraLight = attachment.color; strokeLight = attachment.color; strokeMedium = attachment.color; - fontSecondaryInfo = fontSecondaryInfo; } } catch (e) { // fallback to default From 827dc4d418c885b276059238acfe0f70b5497fc1 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 14:02:50 -0300 Subject: [PATCH 094/144] refactor: use colors from useTheme instead of themes[theme] in Message components (NATIVE-22) --- .../Attachments/CollapsibleQuote.tsx | 12 +++---- .../message/components/Attachments/Reply.tsx | 35 ++++++++----------- .../message/components/Broadcast.tsx | 9 +++-- .../message/components/CallButton.tsx | 9 +++-- .../components/Content/EncryptedContent.tsx | 5 ++- .../components/Content/IgnoredContent.tsx | 5 ++- .../components/Content/InfoContent.tsx | 5 ++- .../components/RightIcons/MessageError.tsx | 5 ++- 8 files changed, 35 insertions(+), 50 deletions(-) diff --git a/app/containers/message/components/Attachments/CollapsibleQuote.tsx b/app/containers/message/components/Attachments/CollapsibleQuote.tsx index ac2905783db..1a7d952055d 100644 --- a/app/containers/message/components/Attachments/CollapsibleQuote.tsx +++ b/app/containers/message/components/Attachments/CollapsibleQuote.tsx @@ -2,7 +2,6 @@ import { transparentize } from 'color2k'; import { useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; -import { themes } from '../../../../lib/constants/colors'; import { type IAttachment } from '../../../../definitions/IAttachment'; import { type TGetCustomEmoji } from '../../../../definitions/IEmoji'; import { CustomIcon } from '../../../CustomIcon'; @@ -88,7 +87,7 @@ const AttText = ({ text, getCustomEmoji }: IMessageAttText) => { const Fields = ({ attachment, getCustomEmoji }: IMessageFields) => { 'use memo'; - const { theme } = useTheme(); + const { colors } = useTheme(); const user = useMessageUser(); if (!attachment.fields) { @@ -99,7 +98,7 @@ const Fields = ({ attachment, getCustomEmoji }: IMessageFields) => { <> {attachment.fields.map(field => ( <View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}> - <Text testID='collapsibleQuoteTouchableFieldTitle' style={[styles.fieldTitle, { color: themes[theme].fontDefault }]}> + <Text testID='collapsibleQuoteTouchableFieldTitle' style={[styles.fieldTitle, { color: colors.fontDefault }]}> {field.title} </Text> <Markdown msg={field?.value || ''} username={user?.username} getCustomEmoji={getCustomEmoji} /> @@ -112,7 +111,7 @@ const Fields = ({ attachment, getCustomEmoji }: IMessageFields) => { const CollapsibleQuote = ({ attachment, getCustomEmoji }: IMessageReply) => { 'use memo'; - const { theme } = useTheme(); + const { colors } = useTheme(); const [collapsed, setCollapsed] = useState(attachment?.collapsed); if (!attachment) { @@ -123,8 +122,7 @@ const CollapsibleQuote = ({ attachment, getCustomEmoji }: IMessageReply) => { setCollapsed(!collapsed); }; - const { fontSecondaryInfo } = themes[theme]; - let { strokeExtraLight, surfaceTint: backgroundColor, strokeLight, strokeMedium } = themes[theme]; + let { strokeExtraLight, surfaceTint: backgroundColor, strokeLight, strokeMedium } = colors; try { if (attachment.color) { @@ -158,7 +156,7 @@ const CollapsibleQuote = ({ attachment, getCustomEmoji }: IMessageReply) => { <View style={styles.touchableContainer}> <View style={styles.attachmentContainer}> <View style={styles.authorContainer}> - <Text style={[styles.title, { color: fontSecondaryInfo }]}>{attachment.title}</Text> + <Text style={[styles.title, { color: colors.fontSecondaryInfo }]}>{attachment.title}</Text> </View> {!collapsed && <AttText text={attachment.text} getCustomEmoji={getCustomEmoji} />} {!collapsed && <Fields attachment={attachment} getCustomEmoji={getCustomEmoji} />} diff --git a/app/containers/message/components/Attachments/Reply.tsx b/app/containers/message/components/Attachments/Reply.tsx index 770c2f2ae5f..94160e9d1c8 100644 --- a/app/containers/message/components/Attachments/Reply.tsx +++ b/app/containers/message/components/Attachments/Reply.tsx @@ -3,11 +3,10 @@ import { StyleSheet, Text, View } from 'react-native'; import { Image } from 'expo-image'; import { type IAttachment, type TGetCustomEmoji } from '../../../../definitions'; -import { themes } from '../../../../lib/constants/colors'; import { fileDownloadAndPreview } from '../../../../lib/methods/helpers'; import { formatAttachmentUrl } from '../../../../lib/methods/helpers/formatAttachmentUrl'; import openLink from '../../../../lib/methods/helpers/openLink'; -import { type TSupportedThemes, useTheme } from '../../../../theme'; +import { useTheme } from '../../../../theme'; import sharedStyles from '../../../../views/Styles'; import RCActivityIndicator from '../../../ActivityIndicator'; import Markdown, { MarkdownPreview } from '../../../markdown'; @@ -88,20 +87,21 @@ interface IMessageReply { msg?: string; } -const Title = ({ attachment, theme }: { attachment: IAttachment; theme: TSupportedThemes }) => { +const Title = ({ attachment }: { attachment: IAttachment }) => { 'use memo'; + const { colors } = useTheme(); const timeFormat = useTimeFormat(); const time = attachment.message_link && attachment.ts ? dayjs(attachment.ts).format(timeFormat) : null; return ( <View style={styles.authorContainer}> {attachment.author_name ? ( - <Text numberOfLines={1} style={[styles.author, { color: themes[theme].fontHint }]}> + <Text numberOfLines={1} style={[styles.author, { color: colors.fontHint }]}> {attachment.author_name} </Text> ) : null} - {time ? <Text style={[messageStyles.time, { color: themes[theme].fontSecondaryInfo }]}>{time}</Text> : null} - {attachment.title ? <Text style={[styles.title, { color: themes[theme].fontDefault }]}>{attachment.title}</Text> : null} + {time ? <Text style={[messageStyles.time, { color: colors.fontSecondaryInfo }]}>{time}</Text> : null} + {attachment.title ? <Text style={[styles.title, { color: colors.fontDefault }]}>{attachment.title}</Text> : null} </View> ); }; @@ -143,17 +143,10 @@ const UrlImage = ({ image }: { image?: string }) => { return <Image source={{ uri: image }} style={styles.image} contentFit='cover' />; }; -const Fields = ({ - attachment, - theme, - getCustomEmoji -}: { - attachment: IAttachment; - theme: TSupportedThemes; - getCustomEmoji: TGetCustomEmoji; -}) => { +const Fields = ({ attachment, getCustomEmoji }: { attachment: IAttachment; getCustomEmoji: TGetCustomEmoji }) => { 'use memo'; + const { colors } = useTheme(); const user = useMessageUser(); if (!attachment.fields) { @@ -164,7 +157,7 @@ const Fields = ({ <View style={styles.fieldsContainer}> {attachment.fields.map(field => ( <View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}> - <Text style={[styles.fieldTitle, { color: themes[theme].fontDefault }]}>{field.title}</Text> + <Text style={[styles.fieldTitle, { color: colors.fontDefault }]}>{field.title}</Text> <Markdown msg={field?.value || ''} username={user?.username} getCustomEmoji={getCustomEmoji} /> </View> ))} @@ -176,7 +169,7 @@ const Reply = ({ attachment, getCustomEmoji, msg }: IMessageReply) => { 'use memo'; const [loading, setLoading] = useState(false); - const { theme } = useTheme(); + const { colors, theme } = useTheme(); const baseUrl = useBaseUrl(); const user = useMessageUser(); const id = useMessageId(); @@ -201,7 +194,7 @@ const Reply = ({ attachment, getCustomEmoji, msg }: IMessageReply) => { openLink(url, theme); }; - let { strokeLight } = themes[theme]; + let { strokeLight } = colors; if (attachment.color) { strokeLight = attachment.color; } @@ -220,17 +213,17 @@ const Reply = ({ attachment, getCustomEmoji, msg }: IMessageReply) => { disabled={!!(loading || attachment.message_link)}> <View style={styles.attachmentContainer}> <View style={styles.titleAndDescriptionContainer}> - <Title attachment={attachment} theme={theme} /> + <Title attachment={attachment} /> <Description attachment={attachment} getCustomEmoji={getCustomEmoji} /> <Quote attachments={attachment.attachments} /> <Attachments attachments={attachment.attachments} /> - <Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} /> + <Fields attachment={attachment} getCustomEmoji={getCustomEmoji} /> {loading ? ( <View style={styles.backdrop}> <View style={[ styles.backdrop, - { backgroundColor: themes[theme].surfaceNeutral, opacity: themes[theme].attachmentLoadingOpacity } + { backgroundColor: colors.surfaceNeutral, opacity: colors.attachmentLoadingOpacity } ]}></View> <RCActivityIndicator /> </View> diff --git a/app/containers/message/components/Broadcast.tsx b/app/containers/message/components/Broadcast.tsx index 5ef7aed1491..39b85e28659 100644 --- a/app/containers/message/components/Broadcast.tsx +++ b/app/containers/message/components/Broadcast.tsx @@ -5,7 +5,6 @@ import { CustomIcon } from '../../CustomIcon'; import styles from '../styles'; import { BUTTON_HIT_SLOP } from '../utils'; import I18n from '../../../i18n'; -import { themes } from '../../../lib/constants/colors'; import { useTheme } from '../../../theme'; import { useMessageAuthor, useMessageItem } from '../stores/MessageStore'; import { useBroadcast, useMessageUser, useReplyBroadcast } from '../stores/MessageRoomStore'; @@ -17,7 +16,7 @@ const Broadcast = () => { const user = useMessageUser(); const replyBroadcast = useReplyBroadcast(); const broadcast = useBroadcast(); - const { theme } = useTheme(); + const { colors } = useTheme(); const { u: author } = useMessageAuthor(); const isOwn = author?._id === user?.id; @@ -26,12 +25,12 @@ const Broadcast = () => { <View style={styles.buttonContainer}> <Touchable onPress={() => replyBroadcast?.(item)} - style={[styles.button, { backgroundColor: themes[theme].badgeBackgroundLevel2 }]} + style={[styles.button, { backgroundColor: colors.badgeBackgroundLevel2 }]} hitSlop={BUTTON_HIT_SLOP} testID='message-broadcast-reply'> <View style={styles.buttonInnerContainer}> - <CustomIcon name='arrow-back' size={20} color={themes[theme].fontWhite} /> - <Text style={[styles.buttonText, { color: themes[theme].fontWhite }]}>{I18n.t('Reply')}</Text> + <CustomIcon name='arrow-back' size={20} color={colors.fontWhite} /> + <Text style={[styles.buttonText, { color: colors.fontWhite }]}>{I18n.t('Reply')}</Text> </View> </Touchable> </View> diff --git a/app/containers/message/components/CallButton.tsx b/app/containers/message/components/CallButton.tsx index 53937e884ea..8e0cb90b684 100644 --- a/app/containers/message/components/CallButton.tsx +++ b/app/containers/message/components/CallButton.tsx @@ -5,7 +5,6 @@ import { BUTTON_HIT_SLOP } from '../utils'; import styles from '../styles'; import I18n from '../../../i18n'; import { CustomIcon } from '../../CustomIcon'; -import { themes } from '../../../lib/constants/colors'; import { useTheme } from '../../../theme'; import { useHandleEnterCall } from '../stores/MessageRoomStore'; @@ -13,16 +12,16 @@ const CallButton = () => { 'use memo'; const handleEnterCall = useHandleEnterCall(); - const { theme } = useTheme(); + const { colors } = useTheme(); return ( <View style={styles.buttonContainer}> <Touchable onPress={handleEnterCall} - style={[styles.button, { backgroundColor: themes[theme].badgeBackgroundLevel2 }]} + style={[styles.button, { backgroundColor: colors.badgeBackgroundLevel2 }]} hitSlop={BUTTON_HIT_SLOP}> <View style={styles.buttonInnerContainer}> - <CustomIcon name='video' size={16} color={themes[theme].fontWhite} /> - <Text style={[styles.buttonText, { color: themes[theme].fontWhite }]}>{I18n.t('Click_to_join')}</Text> + <CustomIcon name='video' size={16} color={colors.fontWhite} /> + <Text style={[styles.buttonText, { color: colors.fontWhite }]}>{I18n.t('Click_to_join')}</Text> </View> </Touchable> </View> diff --git a/app/containers/message/components/Content/EncryptedContent.tsx b/app/containers/message/components/Content/EncryptedContent.tsx index c42fd7bf300..2b14006a27a 100644 --- a/app/containers/message/components/Content/EncryptedContent.tsx +++ b/app/containers/message/components/Content/EncryptedContent.tsx @@ -3,18 +3,17 @@ import { Text } from 'react-native'; import I18n from '../../../../i18n'; import styles from '../../styles'; import { useTheme } from '../../../../theme'; -import { themes } from '../../../../lib/constants/colors'; import ContentWrapper from './ContentWrapper'; const EncryptedContent = () => { 'use memo'; - const { theme } = useTheme(); + const { colors } = useTheme(); return ( <ContentWrapper> <Text - style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} + style={[styles.textInfo, { color: colors.fontSecondaryInfo }]} accessibilityLabel={I18n.t('Encrypted_message')} testID='message-encrypted'> {I18n.t('Encrypted_message')} diff --git a/app/containers/message/components/Content/IgnoredContent.tsx b/app/containers/message/components/Content/IgnoredContent.tsx index 56ea81efe33..4354a08eb1b 100644 --- a/app/containers/message/components/Content/IgnoredContent.tsx +++ b/app/containers/message/components/Content/IgnoredContent.tsx @@ -3,19 +3,18 @@ import { Text } from 'react-native'; import I18n from '../../../../i18n'; import styles from '../../styles'; import { useTheme } from '../../../../theme'; -import { themes } from '../../../../lib/constants/colors'; import { useMessageText } from '../../stores/MessageStore'; import ContentWrapper from './ContentWrapper'; const IgnoredContent = () => { 'use memo'; - const { theme } = useTheme(); + const { colors } = useTheme(); const { messageText } = useMessageText(); return ( <ContentWrapper> - <Text style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} testID={`message-ignored-${messageText}`}> + <Text style={[styles.textInfo, { color: colors.fontSecondaryInfo }]} testID={`message-ignored-${messageText}`}> {I18n.t('Message_Ignored')} </Text> </ContentWrapper> diff --git a/app/containers/message/components/Content/InfoContent.tsx b/app/containers/message/components/Content/InfoContent.tsx index b5ee2e88c07..b209bdd61d4 100644 --- a/app/containers/message/components/Content/InfoContent.tsx +++ b/app/containers/message/components/Content/InfoContent.tsx @@ -3,7 +3,6 @@ import { Text } from 'react-native'; import styles from '../../styles'; import { getInfoMessage, messageHaveAuthorName } from '../../utils'; import { useTheme } from '../../../../theme'; -import { themes } from '../../../../lib/constants/colors'; import { type MessageTypesValues } from '../../../../definitions'; import { useInfoData, useMessageAuthor, useMessageText } from '../../stores/MessageStore'; import User from '../User'; @@ -11,7 +10,7 @@ import User from '../User'; const InfoContent = () => { 'use memo'; - const { theme } = useTheme(); + const { colors } = useTheme(); const { t: type, comment } = useInfoData(); const { u: author, role } = useMessageAuthor(); const { messageText } = useMessageText(); @@ -19,7 +18,7 @@ const InfoContent = () => { const infoMessage = getInfoMessage({ type, role, msg: messageText, author, comment }); const renderMessageContent = ( - <Text style={[styles.textInfo, { color: themes[theme].fontSecondaryInfo }]} accessibilityLabel={infoMessage}> + <Text style={[styles.textInfo, { color: colors.fontSecondaryInfo }]} accessibilityLabel={infoMessage}> {infoMessage} </Text> ); diff --git a/app/containers/message/components/RightIcons/MessageError.tsx b/app/containers/message/components/RightIcons/MessageError.tsx index 35071ed55f4..c980ea0bedf 100644 --- a/app/containers/message/components/RightIcons/MessageError.tsx +++ b/app/containers/message/components/RightIcons/MessageError.tsx @@ -2,7 +2,6 @@ import Touchable from '../Touchable'; import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; import { BUTTON_HIT_SLOP } from '../../utils'; -import { themes } from '../../../../lib/constants/colors'; import { useTheme } from '../../../../theme'; import { useMessageItem, useMessageStatus } from '../../stores/MessageStore'; import { useErrorActionsShow } from '../../stores/MessageRoomStore'; @@ -10,7 +9,7 @@ import { useErrorActionsShow } from '../../stores/MessageRoomStore'; const MessageError = () => { 'use memo'; - const { theme } = useTheme(); + const { colors } = useTheme(); const item = useMessageItem(); const errorActionsShow = useErrorActionsShow(); const { hasError } = useMessageStatus(); @@ -21,7 +20,7 @@ const MessageError = () => { return ( <Touchable onPress={() => errorActionsShow?.(item)} style={styles.rightIcons} hitSlop={BUTTON_HIT_SLOP}> - <CustomIcon name='warning' color={themes[theme].buttonBackgroundDangerDefault} size={16} /> + <CustomIcon name='warning' color={colors.buttonBackgroundDangerDefault} size={16} /> </Touchable> ); }; From 14e5726b28a06c940c8924b5b8e8b363451039dd Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 14:10:20 -0300 Subject: [PATCH 095/144] docs: refine CONTEXT.md message glossary (NATIVE-22) --- CONTEXT.md | 104 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 83 insertions(+), 21 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index b6f02d280ae..f32fd6377bc 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -11,20 +11,47 @@ | **Direct Message** | A 1-on-1 private Room (type `'d'`) between two users | DM, PM, private message | | **Thread** | A branched conversation spawned from a single Message, identified by `tmid` (thread message id) | Reply chain | | **Discussion** | A separate Room spawned from a parent Room, identified by `prid` (parent room id) — unlike Threads, Discussions are full Rooms | Sub-room, sub-channel | -| **Team** | An organizational container that groups multiple Channels and users under a single entity | Workspace (ambiguous) | +| **Team** | An organizational container that groups multiple Channels and users under a single entity | Workspace (ambiguous) | +| **Broadcast Room** | A Room where only authorized users can send Messages; other users can only Reply Broadcast to existing Messages | Broadcast channel | +| **Reply Broadcast**| The action of replying to a Message in a Broadcast Room when the current user cannot send regular Messages | Broadcast reply | ## Messages -| Term | Definition | Aliases to avoid | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ------------------- | -| **Message** | A unit of communication within a Room, identified by `_id` with content in `msg` and parsed markdown in `md` | Chat message, text | -| **Thread Message** | A Message that belongs to a Thread, identified by presence of `tmid` | Reply, thread reply | -| **System Message** | A Message generated by the server to record events (user joined, room archived, role changed) — identified by `t` (type) field | Event, notification | -| **Attachment** | Rich media or structured data embedded in a Message (image, video, audio, file, or action buttons) | File, media | -| **Reaction** | An emoji response to a Message, tracking which usernames reacted | Emoji reaction | -| **Mention** | An `@username` reference within a Message that triggers notifications | Tag, ping | -| **Draft Message** | A user's unsent composition stored on a Subscription or Thread (`draftMessage` field) | Unsent message | -| **Snippet** | A saved excerpt from a Message | — | +### Core + +| Term | Definition | Aliases to avoid | +| ------------------ | ---------------------------------------------------------------------------------------------------------- | ------------------------- | +| **Message** | A unit of communication within a Room, identified by `_id` with content in `msg` and parsed markdown in `md` | Chat message, text | +| **Thread Message** | A Message that belongs to a Thread, identified by presence of `tmid` | Reply, thread reply | +| **Thread Parent** | The Message a Thread is spawned from; the target of its Thread Messages' `tmid` | Thread root, parent message | +| **Attachment** | Rich media or structured data embedded in a Message (image, video, audio, file, or action buttons) | File, media | +| **Reaction** | An emoji response to a Message, tracking which usernames reacted | Emoji reaction | +| **Mention** | An `@username` reference within a Message that triggers notifications | Tag, ping | +| **Draft Message** | A user's unsent composition stored on a Subscription or Thread (`draftMessage` field) | Unsent message | +| **Snippet** | A saved excerpt from a Message | — | + +### System Messages + +A **System Message** is any server-generated Message (carrying a `t` type field) rather than a user-typed one. The code draws a hard boundary between room-event system messages and typed-event system messages — keep them apart. + +| Term | Definition | Aliases to avoid | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------- | +| **System Message** | A server-generated Message identified by a `t` type field; umbrella term covering Info Messages and the typed events below | Event, notification | +| **Info Message** | A System Message that records a room event (user joined, room archived, role changed, muted) — rendered compact and non-interactive. Excludes `e2e`, `discussion-created`, `jitsi_call_started`, and `videoconf` | System event, event message | +| **Discussion-Created Message** | A System Message (`t = 'discussion-created'`) recording that a Discussion was spawned from this Message; rendered with a link to the new Room | — | +| **Call Message** | A System Message (`t = 'jitsi_call_started'` or `t = 'videoconf'`) recording a Video Conference and offering a join affordance | Video call message | +| **Encrypted Message** | A System Message (`t = 'e2e'`) whose content is pending E2E decryption (`e2e !== 'done'`); shown as an "Encrypted message" placeholder until decrypted | Pending E2E message | + +### Content & Visibility States + +| Term | Definition | Aliases to avoid | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | +| **Ignored Message** | A Message whose author is an Ignored User in this Room; shown as a "Message ignored" placeholder until tapped to reveal | Muted message | +| **Ignored User** | A User the current user has hidden in a specific Room (`room.ignored` list); their Messages render as Ignored Messages | Muted user | +| **Auto-Translate** | A per-Room setting that replaces other users' Message bodies with translations in the user's chosen language | Live translate | +| **Translated Message** | A Message whose body is shown via its auto-translated text, because Auto-Translate is on for the Room and the author is another user | Auto-translated message | +| **Blocks Message** | A Message whose body is structured Blocks from a Rocket.Chat App, rendered instead of markdown | App message | +| **Message Preview** | A Message rendered outside its Room context (search, pinned, share extension, notifications); no interactions, reactions, or thread context | Preview row | ## Message Grouping @@ -37,13 +64,29 @@ ## Message Status -| Term | Definition | Aliases to avoid | -| ----------- | -------------------------------------------------------------------- | ---------------- | -| **Sent** | Message successfully delivered to server (status `0`) | Delivered | -| **Temp** | Message created locally but not yet confirmed by server (status `1`) | Pending, sending | -| **Error** | Message that failed to send (status `2`) | Failed | -| **Pinned** | Message flagged as important and pinned to the Room by a user | Bookmarked | -| **Starred** | Message bookmarked by the current user for personal reference | Saved | +The delivery lifecycle of a Message (`status` field). Exactly one status at a time; not to be confused with Message Flags. + +| Term | Definition | Aliases to avoid | +| --------- | --------------------------------------------------------------------- | ---------------- | +| **Sent** | Message successfully delivered to server (status `0`) | Delivered | +| **Temp** | Message created locally but not yet confirmed by server (status `1`) | Pending, sending | +| **Error** | Message that failed to send (status `2`) | Failed | + +## Message Flags + +Independent boolean markers on a Message, orthogonal to its Status — a Message can be both Pinned and Starred, and either regardless of delivery state. + +| Term | Definition | Aliases to avoid | +| ----------- | --------------------------------------------------------------------------------------------- | ---------------- | +| **Pinned** | A Message flagged as important for the whole Room; visible to all Members via the pinned list | Bookmarked | +| **Starred** | A Message bookmarked by the current user for personal reference; visible only to that user | Saved | + +## Message Separators + +| Term | Definition | Aliases to avoid | +| -------------------- | --------------------------------------------------------------------------------------------------- | ---------------- | +| **Date Separator** | A divider rendered between two Messages on different days, showing the date of the newer Message | Date divider | +| **Unread Separator** | A divider rendered between the last read Message and the first unread Message in a Room | Unread divider | ## Message Loading @@ -67,9 +110,19 @@ Two distinct kinds of transient per-Room state drive how the Room view renders M | Term | Definition | Owner | Scope | | --------------------- | --------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------- | -| **Interaction state** | Which Message is selected and the current Message action — reply, quote, edit, or react | A per-Room interaction store | Migrated to the per-Room store as part of the Message row work | +| **Interaction state** | Which Message is selected and the current Message Action (Quote, Edit, or React) | A per-Room interaction store | Migrated to the per-Room store as part of the Message row work | | **Positional state** | Which Message is highlighted and the jump or scroll position | The Room view scroll machinery | Stays with the Room view scroll machinery and is migrated separately | +### Message Actions + +A **Message Action** is the current interaction mode on a Message in the Room view. The three actions are **Quote**, **Edit**, and **React** — there is no "reply" action; replying in a Thread is a separate navigation, not an interaction mode. + +| Term | Definition | Aliases to avoid | +| ---------- | ---------------------------------------------------------------------------------------- | ---------------- | +| **Quote** | A Message Action where one or more Messages are selected to be quoted into the composer | Multi-quote | +| **Edit** | A Message Action where a single Message is being edited by the current user | Editing | +| **React** | A Message Action where a single Message is the target of a reaction picker | Reacting | + ## Users & Roles | Term | Definition | Aliases to avoid | @@ -158,12 +211,15 @@ Two distinct kinds of transient per-Room state drive how the Room view renders M - A **Room** can be of type **Channel**, **Group**, **Direct Message**, or **Omnichannel Room** - A **Subscription** belongs to exactly one **Room** and one **User** - A **Message** belongs to exactly one **Room** (via `rid`) -- A **Thread** is spawned from exactly one **Message** and contains one or more **Thread Messages** -- A **Discussion** creates a new **Room** linked to a parent **Room** (via `prid`) +- A **Thread** is spawned from exactly one **Thread Parent** and contains one or more **Thread Messages** that reference it via `tmid` +- A **Discussion** creates a new **Room** linked to a parent **Room** (via `prid`); a **Discussion-Created Message** in the parent records the spawning - A **Team** has exactly one main **Room** and can contain multiple **Channels** - An **Omnichannel Room** connects exactly one **Visitor** with zero or one **Agents** (via **Served By**) - An **Agent** belongs to one or more **Departments** - An **Inquiry** becomes an **Omnichannel Room** when picked up by an **Agent** +- A **Broadcast Room** restricts sending to authorized users; others interact via **Reply Broadcast** +- An **Ignored Message** is a **Message** whose author is an **Ignored User** in the current **Room**; revealing it is per-Message and ephemeral +- A **Call Message** records a **Video Conference** and offers a join affordance - A **Room** view shows a **Live Window** by default; a **Jump to Message** replaces it with an **Anchored Window** - A **Gap** is bracketed by **Loader Rows**; resolving a Loader Row fetches a **Chunk** and may shrink or close the Gap - **Jump to Message** fetches a **Chunk** centered on the target (`loadSurroundingMessages`), bracketed by an **Older Loader** and a **Newer Loader** when more Messages exist on either side @@ -187,3 +243,9 @@ Two distinct kinds of transient per-Room state drive how the Room view renders M - **"History"** is overloaded: **Server History** is the recent-Servers reconnection list; **Room History** is older Messages fetched on demand. The action `roomHistoryRequest` and saga `ROOM.HISTORY_REQUEST` refer to **Room History**. - **"Window"** is used metaphorically in the Subscriptions dialogue ("a Subscription is the user's window into it"); a **Message Window** is the concrete observed Message range in the Room view. Disambiguate when both could be meant. - **"Load more"** is directional: older Messages are an **Older Loader** (`MORE`/`PREVIOUS_CHUNK`), newer Messages are a **Newer Loader** (`NEXT_CHUNK`). Avoid bare "load more". +- **"System message" vs "Info message"** — **System Message** is the umbrella (any `t`-bearing server Message); **Info Message** is the narrower set of room-event System Messages. The typed events `e2e`, `discussion-created`, `jitsi_call_started`, and `videoconf` are System Messages but NOT Info Messages — each gets its own rendering branch. +- **"Thread reply"** is overloaded. The glossary's **Thread Message** is the data concept (any Message with `tmid`); the code's `isThreadReply` is a *rendering position* — the first Thread Message in a run shown in the parent Room, which gets the "in reply to" header. Do not use "thread reply" for the data concept. +- **"Preview"** is overloaded. **Message Preview** (`isPreview`) is a Message rendered outside its Room (search, pinned, share, notifications). `PreviewContent` is a different concept: the compact body of a Thread Message shown in the parent Room. Disambiguate when either could be meant. +- **"Muted"** is overloaded: a User can be muted in a Room (a moderator action that removes send permission, recorded by `user-muted`/`mute_unmute` System Messages) OR be an **Ignored User** (a per-viewer filter that hides their Messages behind an Ignored Message placeholder, stored in `room.ignored`). Muting is a room permission; ignoring is a personal filter. Different concepts — keep them apart. +- **"Reply"** is overloaded: **Reply Broadcast** is the action available to non-authorized users in a Broadcast Room; replying in a **Thread** is navigation into the Thread view. Neither is a **Message Action** — there is no "reply" Message Action. +- **"Status" vs "flags"** — a Message has exactly one delivery **Status** (Sent, Temp, Error). **Pinned** and **Starred** are independent **Message Flags**, not statuses; do not group them with delivery states. From 5dd2032fbf8e201c9546e8c9257fd4646a54cd80 Mon Sep 17 00:00:00 2001 From: diegolmello <diegolmello@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:12:54 +0000 Subject: [PATCH 096/144] chore: format code and fix lint issues --- CONTEXT.md | 90 +++++++++++++++++++++++++++--------------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index f32fd6377bc..7127eec850b 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -2,45 +2,45 @@ ## Rooms & Conversations -| Term | Definition | Aliases to avoid | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------- | -| **Room** | A server-side conversation container with shared state (name, type, settings) | Chat, conversation | -| **Subscription** | A user's personal relationship to a Room, holding per-user state (unread count, favorite, muted, open) | Membership, room entry | -| **Channel** | A public Room (type `'c'`) visible to all server users | Public room | -| **Group** | A private Room (type `'p'`) visible only to invited members | Private room, private channel | -| **Direct Message** | A 1-on-1 private Room (type `'d'`) between two users | DM, PM, private message | -| **Thread** | A branched conversation spawned from a single Message, identified by `tmid` (thread message id) | Reply chain | -| **Discussion** | A separate Room spawned from a parent Room, identified by `prid` (parent room id) — unlike Threads, Discussions are full Rooms | Sub-room, sub-channel | -| **Team** | An organizational container that groups multiple Channels and users under a single entity | Workspace (ambiguous) | -| **Broadcast Room** | A Room where only authorized users can send Messages; other users can only Reply Broadcast to existing Messages | Broadcast channel | -| **Reply Broadcast**| The action of replying to a Message in a Broadcast Room when the current user cannot send regular Messages | Broadcast reply | +| Term | Definition | Aliases to avoid | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------- | +| **Room** | A server-side conversation container with shared state (name, type, settings) | Chat, conversation | +| **Subscription** | A user's personal relationship to a Room, holding per-user state (unread count, favorite, muted, open) | Membership, room entry | +| **Channel** | A public Room (type `'c'`) visible to all server users | Public room | +| **Group** | A private Room (type `'p'`) visible only to invited members | Private room, private channel | +| **Direct Message** | A 1-on-1 private Room (type `'d'`) between two users | DM, PM, private message | +| **Thread** | A branched conversation spawned from a single Message, identified by `tmid` (thread message id) | Reply chain | +| **Discussion** | A separate Room spawned from a parent Room, identified by `prid` (parent room id) — unlike Threads, Discussions are full Rooms | Sub-room, sub-channel | +| **Team** | An organizational container that groups multiple Channels and users under a single entity | Workspace (ambiguous) | +| **Broadcast Room** | A Room where only authorized users can send Messages; other users can only Reply Broadcast to existing Messages | Broadcast channel | +| **Reply Broadcast** | The action of replying to a Message in a Broadcast Room when the current user cannot send regular Messages | Broadcast reply | ## Messages ### Core -| Term | Definition | Aliases to avoid | -| ------------------ | ---------------------------------------------------------------------------------------------------------- | ------------------------- | -| **Message** | A unit of communication within a Room, identified by `_id` with content in `msg` and parsed markdown in `md` | Chat message, text | -| **Thread Message** | A Message that belongs to a Thread, identified by presence of `tmid` | Reply, thread reply | -| **Thread Parent** | The Message a Thread is spawned from; the target of its Thread Messages' `tmid` | Thread root, parent message | -| **Attachment** | Rich media or structured data embedded in a Message (image, video, audio, file, or action buttons) | File, media | -| **Reaction** | An emoji response to a Message, tracking which usernames reacted | Emoji reaction | -| **Mention** | An `@username` reference within a Message that triggers notifications | Tag, ping | -| **Draft Message** | A user's unsent composition stored on a Subscription or Thread (`draftMessage` field) | Unsent message | -| **Snippet** | A saved excerpt from a Message | — | +| Term | Definition | Aliases to avoid | +| ------------------ | ------------------------------------------------------------------------------------------------------------ | --------------------------- | +| **Message** | A unit of communication within a Room, identified by `_id` with content in `msg` and parsed markdown in `md` | Chat message, text | +| **Thread Message** | A Message that belongs to a Thread, identified by presence of `tmid` | Reply, thread reply | +| **Thread Parent** | The Message a Thread is spawned from; the target of its Thread Messages' `tmid` | Thread root, parent message | +| **Attachment** | Rich media or structured data embedded in a Message (image, video, audio, file, or action buttons) | File, media | +| **Reaction** | An emoji response to a Message, tracking which usernames reacted | Emoji reaction | +| **Mention** | An `@username` reference within a Message that triggers notifications | Tag, ping | +| **Draft Message** | A user's unsent composition stored on a Subscription or Thread (`draftMessage` field) | Unsent message | +| **Snippet** | A saved excerpt from a Message | — | ### System Messages A **System Message** is any server-generated Message (carrying a `t` type field) rather than a user-typed one. The code draws a hard boundary between room-event system messages and typed-event system messages — keep them apart. -| Term | Definition | Aliases to avoid | -| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------- | -| **System Message** | A server-generated Message identified by a `t` type field; umbrella term covering Info Messages and the typed events below | Event, notification | -| **Info Message** | A System Message that records a room event (user joined, room archived, role changed, muted) — rendered compact and non-interactive. Excludes `e2e`, `discussion-created`, `jitsi_call_started`, and `videoconf` | System event, event message | -| **Discussion-Created Message** | A System Message (`t = 'discussion-created'`) recording that a Discussion was spawned from this Message; rendered with a link to the new Room | — | -| **Call Message** | A System Message (`t = 'jitsi_call_started'` or `t = 'videoconf'`) recording a Video Conference and offering a join affordance | Video call message | -| **Encrypted Message** | A System Message (`t = 'e2e'`) whose content is pending E2E decryption (`e2e !== 'done'`); shown as an "Encrypted message" placeholder until decrypted | Pending E2E message | +| Term | Definition | Aliases to avoid | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | +| **System Message** | A server-generated Message identified by a `t` type field; umbrella term covering Info Messages and the typed events below | Event, notification | +| **Info Message** | A System Message that records a room event (user joined, room archived, role changed, muted) — rendered compact and non-interactive. Excludes `e2e`, `discussion-created`, `jitsi_call_started`, and `videoconf` | System event, event message | +| **Discussion-Created Message** | A System Message (`t = 'discussion-created'`) recording that a Discussion was spawned from this Message; rendered with a link to the new Room | — | +| **Call Message** | A System Message (`t = 'jitsi_call_started'` or `t = 'videoconf'`) recording a Video Conference and offering a join affordance | Video call message | +| **Encrypted Message** | A System Message (`t = 'e2e'`) whose content is pending E2E decryption (`e2e !== 'done'`); shown as an "Encrypted message" placeholder until decrypted | Pending E2E message | ### Content & Visibility States @@ -67,7 +67,7 @@ A **System Message** is any server-generated Message (carrying a `t` type field) The delivery lifecycle of a Message (`status` field). Exactly one status at a time; not to be confused with Message Flags. | Term | Definition | Aliases to avoid | -| --------- | --------------------------------------------------------------------- | ---------------- | +| --------- | -------------------------------------------------------------------- | ---------------- | | **Sent** | Message successfully delivered to server (status `0`) | Delivered | | **Temp** | Message created locally but not yet confirmed by server (status `1`) | Pending, sending | | **Error** | Message that failed to send (status `2`) | Failed | @@ -76,17 +76,17 @@ The delivery lifecycle of a Message (`status` field). Exactly one status at a ti Independent boolean markers on a Message, orthogonal to its Status — a Message can be both Pinned and Starred, and either regardless of delivery state. -| Term | Definition | Aliases to avoid | +| Term | Definition | Aliases to avoid | | ----------- | --------------------------------------------------------------------------------------------- | ---------------- | | **Pinned** | A Message flagged as important for the whole Room; visible to all Members via the pinned list | Bookmarked | | **Starred** | A Message bookmarked by the current user for personal reference; visible only to that user | Saved | ## Message Separators -| Term | Definition | Aliases to avoid | -| -------------------- | --------------------------------------------------------------------------------------------------- | ---------------- | -| **Date Separator** | A divider rendered between two Messages on different days, showing the date of the newer Message | Date divider | -| **Unread Separator** | A divider rendered between the last read Message and the first unread Message in a Room | Unread divider | +| Term | Definition | Aliases to avoid | +| -------------------- | ------------------------------------------------------------------------------------------------ | ---------------- | +| **Date Separator** | A divider rendered between two Messages on different days, showing the date of the newer Message | Date divider | +| **Unread Separator** | A divider rendered between the last read Message and the first unread Message in a Room | Unread divider | ## Message Loading @@ -108,20 +108,20 @@ Independent boolean markers on a Message, orthogonal to its Status — a Message Two distinct kinds of transient per-Room state drive how the Room view renders Messages. They have different owners and are migrated independently, so keep them apart. -| Term | Definition | Owner | Scope | -| --------------------- | --------------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------- | -| **Interaction state** | Which Message is selected and the current Message Action (Quote, Edit, or React) | A per-Room interaction store | Migrated to the per-Room store as part of the Message row work | -| **Positional state** | Which Message is highlighted and the jump or scroll position | The Room view scroll machinery | Stays with the Room view scroll machinery and is migrated separately | +| Term | Definition | Owner | Scope | +| --------------------- | -------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------- | +| **Interaction state** | Which Message is selected and the current Message Action (Quote, Edit, or React) | A per-Room interaction store | Migrated to the per-Room store as part of the Message row work | +| **Positional state** | Which Message is highlighted and the jump or scroll position | The Room view scroll machinery | Stays with the Room view scroll machinery and is migrated separately | ### Message Actions A **Message Action** is the current interaction mode on a Message in the Room view. The three actions are **Quote**, **Edit**, and **React** — there is no "reply" action; replying in a Thread is a separate navigation, not an interaction mode. -| Term | Definition | Aliases to avoid | -| ---------- | ---------------------------------------------------------------------------------------- | ---------------- | -| **Quote** | A Message Action where one or more Messages are selected to be quoted into the composer | Multi-quote | -| **Edit** | A Message Action where a single Message is being edited by the current user | Editing | -| **React** | A Message Action where a single Message is the target of a reaction picker | Reacting | +| Term | Definition | Aliases to avoid | +| --------- | --------------------------------------------------------------------------------------- | ---------------- | +| **Quote** | A Message Action where one or more Messages are selected to be quoted into the composer | Multi-quote | +| **Edit** | A Message Action where a single Message is being edited by the current user | Editing | +| **React** | A Message Action where a single Message is the target of a reaction picker | Reacting | ## Users & Roles @@ -244,7 +244,7 @@ A **Message Action** is the current interaction mode on a Message in the Room vi - **"Window"** is used metaphorically in the Subscriptions dialogue ("a Subscription is the user's window into it"); a **Message Window** is the concrete observed Message range in the Room view. Disambiguate when both could be meant. - **"Load more"** is directional: older Messages are an **Older Loader** (`MORE`/`PREVIOUS_CHUNK`), newer Messages are a **Newer Loader** (`NEXT_CHUNK`). Avoid bare "load more". - **"System message" vs "Info message"** — **System Message** is the umbrella (any `t`-bearing server Message); **Info Message** is the narrower set of room-event System Messages. The typed events `e2e`, `discussion-created`, `jitsi_call_started`, and `videoconf` are System Messages but NOT Info Messages — each gets its own rendering branch. -- **"Thread reply"** is overloaded. The glossary's **Thread Message** is the data concept (any Message with `tmid`); the code's `isThreadReply` is a *rendering position* — the first Thread Message in a run shown in the parent Room, which gets the "in reply to" header. Do not use "thread reply" for the data concept. +- **"Thread reply"** is overloaded. The glossary's **Thread Message** is the data concept (any Message with `tmid`); the code's `isThreadReply` is a _rendering position_ — the first Thread Message in a run shown in the parent Room, which gets the "in reply to" header. Do not use "thread reply" for the data concept. - **"Preview"** is overloaded. **Message Preview** (`isPreview`) is a Message rendered outside its Room (search, pinned, share, notifications). `PreviewContent` is a different concept: the compact body of a Thread Message shown in the parent Room. Disambiguate when either could be meant. - **"Muted"** is overloaded: a User can be muted in a Room (a moderator action that removes send permission, recorded by `user-muted`/`mute_unmute` System Messages) OR be an **Ignored User** (a per-viewer filter that hides their Messages behind an Ignored Message placeholder, stored in `room.ignored`). Muting is a room permission; ignoring is a personal filter. Different concepts — keep them apart. - **"Reply"** is overloaded: **Reply Broadcast** is the action available to non-authorized users in a Broadcast Room; replying in a **Thread** is navigation into the Thread view. Neither is a **Message Action** — there is no "reply" Message Action. From 112cdb93a9b395d31d24f32e0a5a13ef5b41e262 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 14:33:08 -0300 Subject: [PATCH 097/144] refactor: guard appendQuote against duplicate ids and rename useArchived to useIsArchived (NATIVE-22) --- app/containers/message/components/Message.tsx | 4 ++-- app/containers/message/stores/MessageRoomStore.tsx | 2 +- app/containers/message/stores/MessageStore.tsx | 4 ++-- app/views/RoomView/InteractionStore.tsx | 14 +++++++++----- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/app/containers/message/components/Message.tsx b/app/containers/message/components/Message.tsx index cd82c3961dd..bc9a3d5cb55 100644 --- a/app/containers/message/components/Message.tsx +++ b/app/containers/message/components/Message.tsx @@ -9,7 +9,7 @@ import { useMessageAccessibilityLabel } from '../hooks/useMessageAccessibilityLa import { useMessageAccessibilityActions } from '../hooks/useMessageAccessibilityActions'; import { useMessageAccessibilityHint } from '../hooks/useMessageAccessibilityHint'; import { useIsBeingEdited } from '../../../views/RoomView/InteractionStore'; -import { useArchived } from '../stores/MessageRoomStore'; +import { useIsArchived } from '../stores/MessageRoomStore'; import { useIsInfoMessage, useMessageField, @@ -47,7 +47,7 @@ const MessageTouchable = (props: TMessageProps) => { const { ref: touchRef, markAsLastFocused } = useLastFocusedMessageRef(); const { isThreadReply } = useThreadPosition(); const isInfo = useIsInfoMessage(); - const archived = useArchived(); + const archived = useIsArchived(); const { hasError, isTemp } = useMessageStatus(); const type = useMessageField(item => item.t); const id = useMessageField(item => item.id); diff --git a/app/containers/message/stores/MessageRoomStore.tsx b/app/containers/message/stores/MessageRoomStore.tsx index e2ca5da2379..2f57e188fa6 100644 --- a/app/containers/message/stores/MessageRoomStore.tsx +++ b/app/containers/message/stores/MessageRoomStore.tsx @@ -122,7 +122,7 @@ export const useErrorActionsShow = (): MessageRoomState['errorActionsShow'] => u export const useOnAnswerButtonPress = (): MessageRoomState['onAnswerButtonPress'] => useMessageRoomStore(s => s.onAnswerButtonPress); export const useOnEncryptedPress = (): MessageRoomState['onEncryptedPress'] => useMessageRoomStore(s => s.onEncryptedPress); -export const useArchived = (): boolean | undefined => useMessageRoomStore(s => s.archived); +export const useIsArchived = (): boolean | undefined => useMessageRoomStore(s => s.archived); export const useIsReadReceiptEnabled = (): boolean | undefined => useMessageRoomStore(s => s.isReadReceiptEnabled); export const useRid = (): string | undefined => useMessageRoomStore(s => s.rid); diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index 53ad2bb0fbd..214f2303e44 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -11,7 +11,7 @@ import { useDebounce } from '../../../lib/methods/helpers/debounce'; import openLink from '../../../lib/methods/helpers/openLink'; import { useTheme } from '../../../theme'; import { - useArchived, + useIsArchived, useAutoTranslate, useBroadcast, useCloseEmojiAndAction, @@ -286,7 +286,7 @@ export const useMessageLongPress = (): (() => void) => { const isInfo = useIsInfoMessage(); const { hasError } = useMessageStatus(); const isEncrypted = useIsEncrypted(); - const archived = useArchived(); + const archived = useIsArchived(); const onLongPress = useMessageStore(s => s.onLongPress); return () => { if (isInfo || hasError || isEncrypted || archived) { diff --git a/app/views/RoomView/InteractionStore.tsx b/app/views/RoomView/InteractionStore.tsx index 8bd3e68741e..15b82d83955 100644 --- a/app/views/RoomView/InteractionStore.tsx +++ b/app/views/RoomView/InteractionStore.tsx @@ -33,11 +33,15 @@ export const createInteractionStore = (initialInteraction?: TInteraction) => setEditing: messageId => set({ interaction: { kind: 'edit', messageId } }), initQuote: messageId => set({ interaction: { kind: 'quote', messageIds: [messageId] } }), appendQuote: messageId => - set(state => - state.interaction?.kind === 'quote' - ? { interaction: { kind: 'quote', messageIds: [...state.interaction.messageIds, messageId] } } - : { interaction: { kind: 'quote', messageIds: [messageId] } } - ), + set(state => { + if (state.interaction?.kind !== 'quote') { + return { interaction: { kind: 'quote', messageIds: [messageId] } }; + } + if (state.interaction.messageIds.includes(messageId)) { + return {}; + } + return { interaction: { kind: 'quote', messageIds: [...state.interaction.messageIds, messageId] } }; + }), removeQuote: messageId => set(state => { if (state.interaction?.kind !== 'quote') { From 5dd895a79332738949526534217f07bdc5ea718a Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 14:54:47 -0300 Subject: [PATCH 098/144] refactor: make REST string-ts path explicit in computeIsHeader, drop ts-expect-error and try/catch (NATIVE-22) --- .../hooks/useMessageAccessibilityLabel.ts | 2 +- .../message/stores/MessageStore.tsx | 31 +++++++++---------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/app/containers/message/hooks/useMessageAccessibilityLabel.ts b/app/containers/message/hooks/useMessageAccessibilityLabel.ts index 685cd266bdd..aa1f3c0ee7c 100644 --- a/app/containers/message/hooks/useMessageAccessibilityLabel.ts +++ b/app/containers/message/hooks/useMessageAccessibilityLabel.ts @@ -62,7 +62,7 @@ export const useMessageAccessibilityLabel = (): string => { } label = stripMentions(label, mentions, channels); - const hour = ts ? new Date(ts as Date).toLocaleTimeString() : ''; + const hour = ts ? new Date(ts).toLocaleTimeString() : ''; const user = useRealName ? author?.name : author?.username || ''; const readOrUnreadLabel = !unread && unread !== null ? i18n.t('Message_was_read') : i18n.t('Message_was_not_read'); const readReceipt = isReadReceiptEnabled && !isInfo ? readOrUnreadLabel : ''; diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index 214f2303e44..3b24e0f33ff 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -174,25 +174,22 @@ const computeIsHeader = ( if (hasError || (prev && prev.status === messagesStatus.ERROR)) { return true; } - try { - if ( - prev && - // @ts-expect-error IMessage types ts as Date, IMessageFromServer as string; the date op is valid at runtime - prev.ts.toDateString() === item.ts.toDateString() && - prev.u?.username === item.u?.username && - !(prev.groupable === false || item.groupable === false || broadcast === true) && - // @ts-expect-error IMessage types ts as Date, IMessageFromServer as string; the date op is valid at runtime - item.ts - prev.ts < Message_GroupingPeriod * 1000 && - prev.tmid === item.tmid && - item.t !== 'rm' && - prev.t !== 'rm' - ) { - return false; - } - return true; - } catch { + // Grouping only applies to DB models with a Date ts; REST-sourced messages carry a string ts and are always headers. + if (!prev || Message_GroupingPeriod == null || !(prev.ts instanceof Date) || !(item.ts instanceof Date)) { return true; } + if ( + prev.ts.toDateString() === item.ts.toDateString() && + prev.u?.username === item.u?.username && + !(prev.groupable === false || item.groupable === false || broadcast === true) && + item.ts.getTime() - prev.ts.getTime() < Message_GroupingPeriod * 1000 && + prev.tmid === item.tmid && + item.t !== 'rm' && + prev.t !== 'rm' + ) { + return false; + } + return true; }; export const useMessageGrouping = (): boolean => { From c6b44d5633f114444f6886e2d3d18bd06a627dec Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 15:43:10 -0300 Subject: [PATCH 099/144] refactor: extract memoized RoomProviders FC, stabilize RoomView RoomContext value (NATIVE-22) --- app/views/RoomView/RoomProviders.test.tsx | 53 ++++++++ app/views/RoomView/RoomProviders.tsx | 52 ++++++++ app/views/RoomView/index.tsx | 145 +++++++++++----------- 3 files changed, 176 insertions(+), 74 deletions(-) create mode 100644 app/views/RoomView/RoomProviders.test.tsx create mode 100644 app/views/RoomView/RoomProviders.tsx diff --git a/app/views/RoomView/RoomProviders.test.tsx b/app/views/RoomView/RoomProviders.test.tsx new file mode 100644 index 00000000000..c2f714fbd0d --- /dev/null +++ b/app/views/RoomView/RoomProviders.test.tsx @@ -0,0 +1,53 @@ +import { render } from '@testing-library/react-native'; + +import { RoomProviders } from './RoomProviders'; +import { useRoomContext, type IRoomContext } from './context'; +import { createInteractionStore } from './InteractionStore'; + +describe('RoomProviders', () => { + it('keeps the same RoomContext value reference across re-renders with unchanged props', () => { + const store = createInteractionStore(); + const room = { rid: 'rid-1' }; + const values: IRoomContext[] = []; + + const Probe = () => { + values.push(useRoomContext()); + return null; + }; + + const Parent = () => ( + <RoomProviders store={store} rid='rid-1' t='c' room={room}> + <Probe /> + </RoomProviders> + ); + + const { rerender } = render(<Parent />); + rerender(<Parent />); + + expect(values).toHaveLength(2); + expect(values[1]).toBe(values[0]); + }); + + it('produces a new RoomContext value reference when a prop changes', () => { + const store = createInteractionStore(); + const values: IRoomContext[] = []; + + const Probe = () => { + values.push(useRoomContext()); + return null; + }; + + const rooms = [{ rid: 'rid-1' }, { rid: 'rid-2' }]; + const Parent = ({ roomIndex }: { roomIndex: number }) => ( + <RoomProviders store={store} rid='rid-1' t='c' room={rooms[roomIndex]}> + <Probe /> + </RoomProviders> + ); + + const { rerender } = render(<Parent roomIndex={0} />); + rerender(<Parent roomIndex={1} />); + + expect(values).toHaveLength(2); + expect(values[1]).not.toBe(values[0]); + }); +}); diff --git a/app/views/RoomView/RoomProviders.tsx b/app/views/RoomView/RoomProviders.tsx new file mode 100644 index 00000000000..e51d16eec04 --- /dev/null +++ b/app/views/RoomView/RoomProviders.tsx @@ -0,0 +1,52 @@ +import { type ReactElement } from 'react'; + +import { RoomContext, type IRoomContext } from './context'; +import { type InteractionStore, InteractionStoreContext } from './InteractionStore'; + +type IRoomProvidersProps = IRoomContext & { + store: InteractionStore; + children: ReactElement; +}; + +export const RoomProviders = ({ + store, + children, + rid, + t, + tmid, + room, + sharing, + isAutocompleteVisible, + editCancel, + editRequest, + onRemoveQuoteMessage, + onSendMessage, + setQuotesAndText, + getText, + updateAutocompleteVisible +}: IRoomProvidersProps): ReactElement => { + 'use memo'; + + return ( + <InteractionStoreContext.Provider value={store}> + <RoomContext.Provider + value={{ + rid, + t, + tmid, + room, + sharing, + isAutocompleteVisible, + editCancel, + editRequest, + onRemoveQuoteMessage, + onSendMessage, + setQuotesAndText, + getText, + updateAutocompleteVisible + }}> + {children} + </RoomContext.Provider> + </InteractionStoreContext.Provider> + ); +}; diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index f62432c9e94..0f56c83a895 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -96,8 +96,8 @@ import { import { withActionSheet } from '../../containers/ActionSheet'; import { goRoom, type TGoRoomItem } from '../../lib/methods/helpers/goRoom'; import { ComposerAttachments, type IMessageComposerRef, MessageComposerContainer } from '../../containers/MessageComposer'; -import { RoomContext } from './context'; -import { createInteractionStore, InteractionStoreContext, type InteractionStore } from './InteractionStore'; +import { createInteractionStore, type InteractionStore } from './InteractionStore'; +import { RoomProviders } from './RoomProviders'; import { MessageRoomProvider } from '../../containers/message/stores/MessageRoomStore'; import AudioManager from '../../lib/methods/AudioManager'; import { type IListContainerRef, type TListRef } from './List/definitions'; @@ -1629,79 +1629,76 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { const federated = 'id' in room && isRoomFederated(room); return ( - <InteractionStoreContext.Provider value={this.interactionStore}> - <RoomContext.Provider - value={{ - rid, - t, - room, - tmid: this.tmid, - sharing: false, - updateAutocompleteVisible: this.updateAutocompleteVisible, - isAutocompleteVisible, - onRemoveQuoteMessage: this.onRemoveQuoteMessage, - editCancel: this.onEditCancel, - editRequest: this.onEditRequest, - onSendMessage: this.handleSendMessage, - setQuotesAndText: this.setQuotesAndText, - getText: this.getText - }}> - <SafeAreaView style={{ backgroundColor: themes[theme].surfaceRoom }} testID='room-view'> - {!this.tmid ? ( - <Banner - title={I18n.t('Announcement')} - text={announcement} - bannerClosed={bannerClosed} - closeBanner={this.closeBanner} - /> - ) : null} - <MessageRoomProvider - navToRoomInfo={this.navToRoomInfo} - showAttachment={this.showAttachment} - blockAction={this.blockAction} - handleEnterCall={this.handleEnterCall} - fetchThreadName={this.fetchThreadName} - toggleFollowThread={this.toggleFollowThread} - jumpToMessage={this.jumpToMessageByUrl} - closeEmojiAndAction={this.handleCloseEmoji} - onReactionPress={this.onReactionPress} - onReactionLongPress={this.onReactionLongPress} - reactionInit={this.onReactionInit} - onDiscussionPress={this.onDiscussionPress} - onThreadPress={this.onThreadPress} - replyBroadcast={this.replyBroadcast} - errorActionsShow={this.errorActionsShow} - onAnswerButtonPress={this.handleSendMessage} - onEncryptedPress={this.onEncryptedPress} - archived={'id' in room && room.archived} - isReadReceiptEnabled={Message_Read_Receipt_Enabled && !federated} + <RoomProviders + store={this.interactionStore} + rid={rid} + t={t} + room={room} + tmid={this.tmid} + sharing={false} + isAutocompleteVisible={isAutocompleteVisible} + updateAutocompleteVisible={this.updateAutocompleteVisible} + onRemoveQuoteMessage={this.onRemoveQuoteMessage} + editCancel={this.onEditCancel} + editRequest={this.onEditRequest} + onSendMessage={this.handleSendMessage} + setQuotesAndText={this.setQuotesAndText} + getText={this.getText}> + <SafeAreaView style={{ backgroundColor: themes[theme].surfaceRoom }} testID='room-view'> + {!this.tmid ? ( + <Banner + title={I18n.t('Announcement')} + text={announcement} + bannerClosed={bannerClosed} + closeBanner={this.closeBanner} + /> + ) : null} + <MessageRoomProvider + navToRoomInfo={this.navToRoomInfo} + showAttachment={this.showAttachment} + blockAction={this.blockAction} + handleEnterCall={this.handleEnterCall} + fetchThreadName={this.fetchThreadName} + toggleFollowThread={this.toggleFollowThread} + jumpToMessage={this.jumpToMessageByUrl} + closeEmojiAndAction={this.handleCloseEmoji} + onReactionPress={this.onReactionPress} + onReactionLongPress={this.onReactionLongPress} + reactionInit={this.onReactionInit} + onDiscussionPress={this.onDiscussionPress} + onThreadPress={this.onThreadPress} + replyBroadcast={this.replyBroadcast} + errorActionsShow={this.errorActionsShow} + onAnswerButtonPress={this.handleSendMessage} + onEncryptedPress={this.onEncryptedPress} + archived={'id' in room && room.archived} + isReadReceiptEnabled={Message_Read_Receipt_Enabled && !federated} + rid={rid} + user={user as any} + baseUrl={baseUrl} + broadcast={'id' in room && room.broadcast} + isThreadRoom={!!this.tmid} + Message_GroupingPeriod={Message_GroupingPeriod} + autoTranslateRoom={canAutoTranslate && 'id' in room && room.autoTranslate} + autoTranslateLanguage={'id' in room ? room.autoTranslateLanguage : undefined}> + <List + ref={this.list} + listRef={this.flatList} rid={rid} - user={user as any} - baseUrl={baseUrl} - broadcast={'id' in room && room.broadcast} - isThreadRoom={!!this.tmid} - Message_GroupingPeriod={Message_GroupingPeriod} - autoTranslateRoom={canAutoTranslate && 'id' in room && room.autoTranslate} - autoTranslateLanguage={'id' in room ? room.autoTranslateLanguage : undefined}> - <List - ref={this.list} - listRef={this.flatList} - rid={rid} - t={t as RoomType} - tmid={this.tmid} - renderRow={this.renderItem} - hideSystemMessages={this.hideSystemMessages} - showMessageInMainThread={user.showMessageInMainThread ?? false} - serverVersion={serverVersion} - /> - </MessageRoomProvider> - {this.renderFooter()} - {this.renderActions()} - <UploadProgress rid={rid} user={user} baseUrl={baseUrl} width={width} /> - <JoinCode ref={this.joinCode} onJoin={this.onJoin} rid={rid} t={t} theme={theme} /> - </SafeAreaView> - </RoomContext.Provider> - </InteractionStoreContext.Provider> + t={t as RoomType} + tmid={this.tmid} + renderRow={this.renderItem} + hideSystemMessages={this.hideSystemMessages} + showMessageInMainThread={user.showMessageInMainThread ?? false} + serverVersion={serverVersion} + /> + </MessageRoomProvider> + {this.renderFooter()} + {this.renderActions()} + <UploadProgress rid={rid} user={user} baseUrl={baseUrl} width={width} /> + <JoinCode ref={this.joinCode} onJoin={this.onJoin} rid={rid} t={t} theme={theme} /> + </SafeAreaView> + </RoomProviders> ); } } From d316bda8022957bdc710ea38a19329fe63842fa5 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 15:49:31 -0300 Subject: [PATCH 100/144] refactor: wire ShareView to shared RoomProviders FC (NATIVE-22) --- app/views/ShareView/index.tsx | 56 +++++++++++++++-------------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/app/views/ShareView/index.tsx b/app/views/ShareView/index.tsx index bd3fd8b10a6..46ea681edd7 100644 --- a/app/views/ShareView/index.tsx +++ b/app/views/ShareView/index.tsx @@ -38,13 +38,8 @@ import { import { sendAttachments } from '../../lib/methods/sendFileMessage/sendAttachments'; import { sendMessage } from '../../lib/methods/sendMessage'; import { hasPermission, isAndroid, canUploadFile, isReadOnly, isBlocked } from '../../lib/methods/helpers'; -import { RoomContext } from '../RoomView/context'; -import { - createInteractionStore, - InteractionStoreContext, - type InteractionStore, - type TInteraction -} from '../RoomView/InteractionStore'; +import { RoomProviders } from '../RoomView/RoomProviders'; +import { createInteractionStore, type InteractionStore, type TInteraction } from '../RoomView/InteractionStore'; import { appStart } from '../../actions/app'; interface IShareViewState { @@ -403,31 +398,28 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { if (attachments.length) { return ( - <InteractionStoreContext.Provider value={this.interactionStore}> - <RoomContext.Provider - value={{ - rid: room.rid, - t: room.t, - room, - tmid: this.getThreadId(thread), - sharing: true, - onSendMessage: this.send, - onRemoveQuoteMessage: this.onRemoveQuoteMessage - }}> - <View style={styles.container}> - <Preview - // using key just to reset zoom/move after change selected - key={selected?.path} - item={selected} - length={attachments.length} - theme={theme} - /> - <MessageComposerContainer ref={this.messageComposerRef}> - <Thumbs attachments={attachments} onPress={this.selectFile} onRemove={this.removeFile} /> - </MessageComposerContainer> - </View> - </RoomContext.Provider> - </InteractionStoreContext.Provider> + <RoomProviders + store={this.interactionStore} + rid={room.rid} + t={room.t} + room={room} + tmid={this.getThreadId(thread)} + sharing + onSendMessage={this.send} + onRemoveQuoteMessage={this.onRemoveQuoteMessage}> + <View style={styles.container}> + <Preview + // using key just to reset zoom/move after change selected + key={selected?.path} + item={selected} + length={attachments.length} + theme={theme} + /> + <MessageComposerContainer ref={this.messageComposerRef}> + <Thumbs attachments={attachments} onPress={this.selectFile} onRemove={this.removeFile} /> + </MessageComposerContainer> + </View> + </RoomProviders> ); } From 7278a6225246b2cf79d62e9659c3fd59d09a71ec Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 16:38:46 -0300 Subject: [PATCH 101/144] chore: remove console.count render log (NATIVE-22) --- app/views/RoomView/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 0f56c83a895..c51dbcdce42 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1593,7 +1593,6 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { }; render() { - console.count(`${this.constructor.name}.render calls`); const { room, isAutocompleteVisible, showMissingE2EEKey, showE2EEDisabledRoom, canAutoTranslate } = this.state; const { user, baseUrl, theme, width, serverVersion, navigation, Message_GroupingPeriod, Message_Read_Receipt_Enabled } = this.props; From d2b29fc290a08663e8d493d73ec6c985edd4f53a Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 16:40:09 -0300 Subject: [PATCH 102/144] test: pin autoTranslate boundary for translate hooks (NATIVE-22) --- .../stores/__tests__/MessageStore.test.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/containers/message/stores/__tests__/MessageStore.test.tsx b/app/containers/message/stores/__tests__/MessageStore.test.tsx index 649331adb8b..8813ab4e55d 100644 --- a/app/containers/message/stores/__tests__/MessageStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageStore.test.tsx @@ -767,5 +767,25 @@ describe('MessageStore', () => { const { latest } = renderDerived(model, useMessageText, { config: translationConfig }); expect(latest()).toEqual({ messageText: model.msg, isTranslated: false }); }); + + it('useMessageText translates when autoTranslate is true', () => { + const model = buildFakeModel({ + autoTranslate: true, + translations: [{ _id: 't1', language: 'pt-BR', value: 'Olá mundo' }], + u: { _id: 'u2', username: 'bob' } + }); + const { latest } = renderDerived(model, useMessageText, { config: translationConfig }); + expect(latest()).toEqual({ messageText: 'Olá mundo', isTranslated: true }); + }); + + it('useMessageText does not translate when autoTranslate is false', () => { + const model = buildFakeModel({ + autoTranslate: false, + translations: [{ _id: 't1', language: 'pt-BR', value: 'Olá mundo' }], + u: { _id: 'u2', username: 'bob' } + }); + const { latest } = renderDerived(model, useMessageText, { config: translationConfig }); + expect(latest()).toEqual({ messageText: model.msg, isTranslated: false }); + }); }); }); From f41a8baa4a2b8def01e287383f08e60385a8db4e Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 16:41:19 -0300 Subject: [PATCH 103/144] chore: co-locate message testHelpers (NATIVE-22) --- app/containers/message/__tests__/index.test.tsx | 2 +- app/containers/message/components/__tests__/Reactions.test.tsx | 2 +- .../message/{testHelpers.tsx => index.testHelpers.tsx} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename app/containers/message/{testHelpers.tsx => index.testHelpers.tsx} (100%) diff --git a/app/containers/message/__tests__/index.test.tsx b/app/containers/message/__tests__/index.test.tsx index af2da17f326..f4d9664f016 100644 --- a/app/containers/message/__tests__/index.test.tsx +++ b/app/containers/message/__tests__/index.test.tsx @@ -4,7 +4,7 @@ import { fireEvent } from '@testing-library/react-native'; import MessageContainer from '../index'; import { type TAnyMessageModel } from '../../../definitions'; import { createInteractionStore, InteractionStoreContext } from '../../../views/RoomView/InteractionStore'; -import { renderWithMessageProviders } from '../testHelpers'; +import { renderWithMessageProviders } from '../index.testHelpers'; import { type MessageRoomState } from '../stores/MessageRoomStore'; jest.mock('../components/Touch', () => { diff --git a/app/containers/message/components/__tests__/Reactions.test.tsx b/app/containers/message/components/__tests__/Reactions.test.tsx index 08014f8c463..ff43b26aeb5 100644 --- a/app/containers/message/components/__tests__/Reactions.test.tsx +++ b/app/containers/message/components/__tests__/Reactions.test.tsx @@ -1,7 +1,7 @@ import { render, fireEvent } from '@testing-library/react-native'; import Reactions from '../Reactions'; -import { MessageProviders } from '../../testHelpers'; +import { MessageProviders } from '../../index.testHelpers'; import { setUser } from '../../../../actions/login'; import { mockedStore } from '../../../../reducers/mockedStore'; import { type IReaction, type TAnyMessageModel } from '../../../../definitions'; diff --git a/app/containers/message/testHelpers.tsx b/app/containers/message/index.testHelpers.tsx similarity index 100% rename from app/containers/message/testHelpers.tsx rename to app/containers/message/index.testHelpers.tsx From 954b18fcd7f298d5e8e48a8b3f8ec06b08fd0591 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 16:43:48 -0300 Subject: [PATCH 104/144] refactor: narrow Discussion item subscription (NATIVE-22) --- app/containers/message/components/Discussion.tsx | 6 +++--- app/containers/message/stores/MessageRoomStore.tsx | 2 +- app/containers/message/stores/MessageStore.tsx | 2 +- app/views/RoomView/index.tsx | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/containers/message/components/Discussion.tsx b/app/containers/message/components/Discussion.tsx index c1eab94c1ab..0d725039443 100644 --- a/app/containers/message/components/Discussion.tsx +++ b/app/containers/message/components/Discussion.tsx @@ -8,14 +8,14 @@ import { CustomIcon } from '../../CustomIcon'; import { DISCUSSION } from '../constants'; import { formatDateThreads } from '../../../lib/methods/helpers/room'; import { useTheme } from '../../../theme'; -import { useDiscussion, useMessageItem, useMessageText } from '../stores/MessageStore'; +import { useDiscussion, useMessageField, useMessageText } from '../stores/MessageStore'; import { useOnDiscussionPress } from '../stores/MessageRoomStore'; const Discussion = () => { 'use memo'; const { colors } = useTheme(); - const item = useMessageItem(); + const drid = useMessageField(item => item.drid); const { dcount, dlm } = useDiscussion(); const { messageText } = useMessageText(); let time; @@ -30,7 +30,7 @@ const Discussion = () => { <Text style={[styles.discussionText, { color: colors.fontDefault }]}>{messageText}</Text> <View style={[styles.buttonContainer, { gap: 8 }]}> <Touchable - onPress={() => onDiscussionPress?.(item)} + onPress={() => onDiscussionPress?.(drid)} style={[styles.button, { backgroundColor: colors.badgeBackgroundLevel2 }]} hitSlop={BUTTON_HIT_SLOP}> <View style={styles.buttonInnerContainer}> diff --git a/app/containers/message/stores/MessageRoomStore.tsx b/app/containers/message/stores/MessageRoomStore.tsx index 2f57e188fa6..7f890781ca6 100644 --- a/app/containers/message/stores/MessageRoomStore.tsx +++ b/app/containers/message/stores/MessageRoomStore.tsx @@ -20,7 +20,7 @@ export type MessageRoomState = { onReactionPress?: (emoji: string, id: string) => void; onReactionLongPress?: (item: TAnyMessageModel) => void; reactionInit?: (messageId: string) => void; - onDiscussionPress?: (item: TAnyMessageModel) => void; + onDiscussionPress?: (drid: TAnyMessageModel['drid']) => void; onThreadPress?: (item: TAnyMessageModel) => void; replyBroadcast?: (item: TAnyMessageModel) => void; errorActionsShow?: (item: TAnyMessageModel) => void; diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index 3b24e0f33ff..6a14c8abaee 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -333,7 +333,7 @@ export const useMessagePress = (): (() => void) => { onThreadPress?.(item); } if (item.dlm && onDiscussionPress) { - onDiscussionPress(item); + onDiscussionPress(item.drid); } }, 300, diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index c51dbcdce42..745357f216e 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -953,10 +953,10 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { }; onDiscussionPress = debounce( - async (item: TAnyMessageModel) => { + async (drid: TAnyMessageModel['drid']) => { const { isMasterDetail } = this.props; - if (!item.drid) return; - const sub = await getRoomInfo(item.drid); + if (!drid) return; + const sub = await getRoomInfo(drid); if (sub) { goRoom({ item: sub as TGoRoomItem, From e83d3f35b92db023f3feb5e9dbf60a2fab5aa0e3 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 17:32:29 -0300 Subject: [PATCH 105/144] fix: unify autoTranslate boundary to truthy (NATIVE-22) --- app/containers/message/stores/MessageStore.tsx | 2 +- app/containers/message/stores/__tests__/MessageStore.test.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index 6a14c8abaee..52dd146e964 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -247,7 +247,7 @@ export const useTranslateLanguage = (): string | undefined => { const user = useMessageUser(); return useMessageStore(s => { const otherUserMessage = s.item.u?.username !== user?.username; - const canTranslate = autoTranslateRoom && autoTranslateLanguage && s.item.autoTranslate !== false && otherUserMessage; + const canTranslate = autoTranslateRoom && autoTranslateLanguage && s.item.autoTranslate && otherUserMessage; return canTranslate ? autoTranslateLanguage : undefined; }); }; diff --git a/app/containers/message/stores/__tests__/MessageStore.test.tsx b/app/containers/message/stores/__tests__/MessageStore.test.tsx index 8813ab4e55d..9fe8b2f5a0a 100644 --- a/app/containers/message/stores/__tests__/MessageStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageStore.test.tsx @@ -740,10 +740,10 @@ describe('MessageStore', () => { describe('translation boundary on item.autoTranslate', () => { const translationConfig = { autoTranslateRoom: true, autoTranslateLanguage: 'pt-BR', user: { username: 'alice' } }; - it('useTranslateLanguage returns the language when autoTranslate is undefined', () => { + it('useTranslateLanguage returns undefined when autoTranslate is undefined', () => { const model = buildFakeModel({ autoTranslate: undefined, u: { _id: 'u2', username: 'bob' } }); const { latest } = renderDerived(model, useTranslateLanguage, { config: translationConfig }); - expect(latest()).toBe('pt-BR'); + expect(latest()).toBeUndefined(); }); it('useTranslateLanguage returns the language when autoTranslate is true', () => { From b8c9a904f52fcf7ae9d454367b373ace0c8e885f Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 17:32:57 -0300 Subject: [PATCH 106/144] refactor: collapse item/previousItem effects to one (NATIVE-22) --- .../message/stores/MessageStore.tsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index 52dd146e964..9b878a8710f 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -95,22 +95,23 @@ export const MessageProvider = ({ createMessageStore({ item, previousItem, isIgnored: isIgnored ?? false, onPress, onLongPress, threadBadgeColor }) ); - // Header grouping and thread position depend on the previous record too, so each effect - // subscribes one record; both feed the same tick. Keeping them separate means changing - // previousItem does not tear down and recreate item's subscription. - useEffect(() => subscribeModel(item, store), [item, store]); - useEffect(() => (previousItem ? subscribeModel(previousItem, store) : undefined), [previousItem, store]); + // Push item/previousItem into the store and (re)subscribe both records to the same tick. + // Resubscribing on either change is a single sync commit, so there's no observable churn. + useEffect(() => { + store.setState({ item, previousItem }); + const unsubscribeItem = subscribeModel(item, store); + const unsubscribePrevious = previousItem ? subscribeModel(previousItem, store) : undefined; + return () => { + unsubscribeItem?.(); + unsubscribePrevious?.(); + }; + }, [item, previousItem, store]); // Mirror per-message row handlers so field-level selectors subscribe without churning the context value. useEffect(() => { store.setState({ onPress, onLongPress, threadBadgeColor }); }, [onPress, onLongPress, threadBadgeColor, store]); - // Push item/previousItem prop changes into the store so field selectors re-read (temp→server swap, neighbour changes). - useEffect(() => { - store.setState({ item, previousItem }); - }, [item, previousItem, store]); - // Reset the manual reveal only when the ignore state actually transitions. useEffect(() => { store.setState({ isIgnored: isIgnored ?? false, manualUnignored: false }); From f98690cb817d68ac133d6cdeab2762dce458d7a5 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 17:34:17 -0300 Subject: [PATCH 107/144] refactor: drop bespoke action->interaction mapping in ShareView (NATIVE-22) --- app/views/ShareView/index.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/app/views/ShareView/index.tsx b/app/views/ShareView/index.tsx index 46ea681edd7..b3bb50dc709 100644 --- a/app/views/ShareView/index.tsx +++ b/app/views/ShareView/index.tsx @@ -31,7 +31,6 @@ import { type IShareAttachment, type IUser, RootEnum, - type TMessageAction, type TSubscriptionModel, type TThreadModel } from '../../definitions'; @@ -39,7 +38,7 @@ import { sendAttachments } from '../../lib/methods/sendFileMessage/sendAttachmen import { sendMessage } from '../../lib/methods/sendMessage'; import { hasPermission, isAndroid, canUploadFile, isReadOnly, isBlocked } from '../../lib/methods/helpers'; import { RoomProviders } from '../RoomView/RoomProviders'; -import { createInteractionStore, type InteractionStore, type TInteraction } from '../RoomView/InteractionStore'; +import { createInteractionStore, type InteractionStore } from '../RoomView/InteractionStore'; import { appStart } from '../../actions/app'; interface IShareViewState { @@ -72,11 +71,6 @@ interface IShareViewProps { type TShareServerInfo = Partial<Pick<IServer, 'version' | 'FileUpload_MaxFileSize' | 'FileUpload_MediaTypeWhiteList'>>; -// ShareView only supports the quote flow (messages are filled later via startShareView -> setQuotes); -// edit/react carry no messageId here, so they can't be represented by the union. -const mapActionToInteraction = (action: TMessageAction): TInteraction => - action === 'quote' ? { kind: 'quote', messageIds: [] } : null; - class ShareView extends Component<IShareViewProps, IShareViewState> { private messageComposerRef: RefObject<IMessageComposerRef | null>; private files: any[]; @@ -94,7 +88,8 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { this.serverInfo = props.route.params?.serverInfo ?? {}; this.finishShareView = props.route.params?.finishShareView; this.sentMessage = false; - this.interactionStore = createInteractionStore(mapActionToInteraction(props.route.params?.action)); + // ShareView only ever uses the quote flow; real ids arrive later via startShareView -> setQuotes. + this.interactionStore = createInteractionStore(); this.state = { selected: {} as IShareAttachment, From 10521f4322c8ede96e1e0d14510d67738984c618 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 17:34:50 -0300 Subject: [PATCH 108/144] refactor: extract useMessageTouchable pressability hook (NATIVE-22) --- app/containers/message/components/Message.tsx | 13 ++++----- .../message/stores/MessageStore.tsx | 29 +++++++++++++++---- 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/app/containers/message/components/Message.tsx b/app/containers/message/components/Message.tsx index bc9a3d5cb55..226ac19dd65 100644 --- a/app/containers/message/components/Message.tsx +++ b/app/containers/message/components/Message.tsx @@ -9,7 +9,6 @@ import { useMessageAccessibilityLabel } from '../hooks/useMessageAccessibilityLa import { useMessageAccessibilityActions } from '../hooks/useMessageAccessibilityActions'; import { useMessageAccessibilityHint } from '../hooks/useMessageAccessibilityHint'; import { useIsBeingEdited } from '../../../views/RoomView/InteractionStore'; -import { useIsArchived } from '../stores/MessageRoomStore'; import { useIsInfoMessage, useMessageField, @@ -17,6 +16,7 @@ import { useMessageLongPress, useMessagePress, useMessageStatus, + useMessageTouchable, useThreadPosition } from '../stores/MessageStore'; @@ -45,18 +45,15 @@ const MessageTouchable = (props: TMessageProps) => { const { colors } = useTheme(); const { ref: touchRef, markAsLastFocused } = useLastFocusedMessageRef(); - const { isThreadReply } = useThreadPosition(); const isInfo = useIsInfoMessage(); - const archived = useIsArchived(); - const { hasError, isTemp } = useMessageStatus(); - const type = useMessageField(item => item.t); + const { hasError } = useMessageStatus(); + const { tappable } = useMessageTouchable(); const id = useMessageField(item => item.id); const isBeingEdited = useIsBeingEdited(id); const onPressAction = useMessagePress(); const onLongPress = useMessageLongPress(); const accessibilityLabelValue = useMessageAccessibilityLabel(); - const isDisabled = (isInfo && !isThreadReply) || archived || isTemp || type === 'jitsi_call_started'; - const accessibilityActions = useMessageAccessibilityActions(isDisabled); + const accessibilityActions = useMessageAccessibilityActions(!tappable); const accessibilityHint = useMessageAccessibilityHint(); let backgroundColor = undefined; @@ -87,7 +84,7 @@ const MessageTouchable = (props: TMessageProps) => { componentRef={touchRef} onLongPress={handleLongPress} onPress={onPressAction} - disabled={isDisabled} + disabled={!tappable} style={{ backgroundColor }} testID={isBeingEdited ? `message-editing-${id}` : undefined} accessible diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index 9b878a8710f..dc0dc921146 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -277,17 +277,34 @@ export const useMessageIgnored = (): boolean => useMessageStore(s => (s.manualUn export const useRevealIgnored = (): (() => void) => useMessageStore(s => s.reveal); -export const useMessageLongPress = (): (() => void) => { +// Single source of truth for pressability, shared by the Touch gate, long-press guard and +// press guard. longPressable drops encrypted messages (tap can still open a thread; the action +// sheet is suppressed); revealsIgnored is tappable ∧ isIgnored (a tap reveals instead of pressing). +export const useMessageTouchable = (): { tappable: boolean; longPressable: boolean; revealsIgnored: boolean } => { 'use memo'; - const item = useMessageItem(); const isInfo = useIsInfoMessage(); - const { hasError } = useMessageStatus(); + const { hasError, isTemp } = useMessageStatus(); const isEncrypted = useIsEncrypted(); const archived = useIsArchived(); + const isIgnored = useMessageIgnored(); + const type = useMessageField(item => item.t); + + const tappable = !(isInfo || hasError || archived || isTemp || type === 'jitsi_call_started'); + const longPressable = tappable && !isEncrypted; + const revealsIgnored = tappable && isIgnored; + + return { tappable, longPressable, revealsIgnored }; +}; + +export const useMessageLongPress = (): (() => void) => { + 'use memo'; + + const item = useMessageItem(); + const { longPressable } = useMessageTouchable(); const onLongPress = useMessageStore(s => s.onLongPress); return () => { - if (isInfo || hasError || isEncrypted || archived) { + if (!longPressable) { return; } onLongPress?.(item); @@ -318,12 +335,12 @@ export const useMessagePress = (): (() => void) => { const onThreadPress = useOnThreadPress(); const onDiscussionPress = useOnDiscussionPress(); const closeEmojiAndAction = useCloseEmojiAndAction(); - const isIgnored = useMessageIgnored(); + const { revealsIgnored } = useMessageTouchable(); const revealIgnored = useRevealIgnored(); const handlePress = useDebounce( () => { - if (isIgnored) { + if (revealsIgnored) { return revealIgnored(); } if (onPress) { From da66df3e27a7819903e03d2fa1c22537065cefc8 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 17:35:33 -0300 Subject: [PATCH 109/144] perf: sync only reactive fields in MessageRoomStoreProvider (NATIVE-22) --- .../message/stores/MessageRoomStore.tsx | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/app/containers/message/stores/MessageRoomStore.tsx b/app/containers/message/stores/MessageRoomStore.tsx index 7f890781ca6..c05e1e81d39 100644 --- a/app/containers/message/stores/MessageRoomStore.tsx +++ b/app/containers/message/stores/MessageRoomStore.tsx @@ -60,21 +60,16 @@ const MessageRoomStoreProvider = ({ children, ...state }: { children: ReactNode const [store] = useState(() => createMessageRoomStore(state)); - // state is a rest-spread rebuilt from props each render, so it can never be a stable - // dependency (useMemo can't stabilize it, and a field-level deps array would reintroduce the - // hand-maintained key list the zustand migration deliberately removed). Diffing against the - // store's current state before calling setState keeps this from forcing a zustand - // notification (and consumer re-render) on every render; per-field Object.is bail in each - // selector still means only a consumer whose own field changed re-renders. + // Only timeFormat/autoTranslateRoom/autoTranslateLanguage (the reactive tail) change after + // mount; the rest are constants/handlers captured once above. Keying on just those 3 fields + // lets React's own dependency diff replace the old full 28-key Object.is scan. useEffect(() => { - const current = store.getState(); - const changed = Object.keys(state).some( - key => !Object.is(state[key as keyof MessageRoomState], current[key as keyof MessageRoomState]) - ); - if (changed) { - store.setState(state); - } - }); + store.setState({ + timeFormat: state.timeFormat, + autoTranslateRoom: state.autoTranslateRoom, + autoTranslateLanguage: state.autoTranslateLanguage + }); + }, [state.timeFormat, state.autoTranslateRoom, state.autoTranslateLanguage, store]); return <MessageRoomStoreContext.Provider value={store}>{children}</MessageRoomStoreContext.Provider>; }; From f27d8aacfa5d99a92b407480892c4bddea9bd982 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 17:39:22 -0300 Subject: [PATCH 110/144] refactor: extract shared BranchAttachmentContent for message branches (NATIVE-22) --- .../Message/BranchAttachmentContent.tsx | 22 +++++++++++++++++++ .../components/Message/DefaultBranch.tsx | 14 ++---------- .../components/Message/PreviewBranch.tsx | 14 ++---------- 3 files changed, 26 insertions(+), 24 deletions(-) create mode 100644 app/containers/message/components/Message/BranchAttachmentContent.tsx diff --git a/app/containers/message/components/Message/BranchAttachmentContent.tsx b/app/containers/message/components/Message/BranchAttachmentContent.tsx new file mode 100644 index 00000000000..8f86a115bff --- /dev/null +++ b/app/containers/message/components/Message/BranchAttachmentContent.tsx @@ -0,0 +1,22 @@ +import Content from '../Content'; +import Attachments from '../Attachments'; +import Quote from '../Attachments/Quote'; +import Urls from '../Urls'; +import { useAttachments, useMessageField } from '../../stores/MessageStore'; + +// Shared leaf composition for DefaultBranch and PreviewBranch +export const BranchAttachmentContent = () => { + 'use memo'; + + const attachments = useAttachments(); + const author = useMessageField(item => item.u); + + return ( + <> + <Quote attachments={attachments} /> + <Content /> + <Attachments attachments={attachments} author={author} /> + <Urls /> + </> + ); +}; diff --git a/app/containers/message/components/Message/DefaultBranch.tsx b/app/containers/message/components/Message/DefaultBranch.tsx index d1caebe701a..701c6f4d44c 100644 --- a/app/containers/message/components/Message/DefaultBranch.tsx +++ b/app/containers/message/components/Message/DefaultBranch.tsx @@ -1,31 +1,21 @@ import { View } from 'react-native'; import User from '../User'; -import Content from '../Content'; -import Attachments from '../Attachments'; -import Quote from '../Attachments/Quote'; -import Urls from '../Urls'; import Thread from '../Thread'; import Reactions from '../Reactions'; import Broadcast from '../Broadcast'; import MessageTime from '../Time'; -import { useAttachments, useMessageField } from '../../stores/MessageStore'; +import { BranchAttachmentContent } from './BranchAttachmentContent'; const DefaultBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { 'use memo'; - const attachments = useAttachments(); - const author = useMessageField(item => item.u); - return ( <> <User /> {showTimeLarge ? <MessageTime /> : null} <View style={{ gap: 4 }}> - <Quote attachments={attachments} /> - <Content /> - <Attachments attachments={attachments} author={author} /> - <Urls /> + <BranchAttachmentContent /> <Thread /> <Reactions /> <Broadcast /> diff --git a/app/containers/message/components/Message/PreviewBranch.tsx b/app/containers/message/components/Message/PreviewBranch.tsx index 6ad2cc7ace7..432a38fef8e 100644 --- a/app/containers/message/components/Message/PreviewBranch.tsx +++ b/app/containers/message/components/Message/PreviewBranch.tsx @@ -1,25 +1,15 @@ import User from '../User'; -import Content from '../Content'; -import Attachments from '../Attachments'; -import Quote from '../Attachments/Quote'; -import Urls from '../Urls'; import MessageTime from '../Time'; -import { useAttachments, useMessageField } from '../../stores/MessageStore'; +import { BranchAttachmentContent } from './BranchAttachmentContent'; const PreviewBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { 'use memo'; - const attachments = useAttachments(); - const author = useMessageField(item => item.u); - return ( <> <User /> {showTimeLarge ? <MessageTime /> : null} - <Quote attachments={attachments} /> - <Content /> - <Attachments attachments={attachments} author={author} /> - <Urls /> + <BranchAttachmentContent /> </> ); }; From e81c177d094373339d8b1314c14a4067927b8973 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 17:41:23 -0300 Subject: [PATCH 111/144] refactor: rename Touchable to MessageActionTouchable (NATIVE-22) --- .../components/Attachments/CollapsibleQuote.tsx | 6 +++--- .../message/components/Attachments/Image/Button.tsx | 6 +++--- .../message/components/Attachments/Reply.tsx | 6 +++--- .../message/components/Attachments/Video.tsx | 6 +++--- app/containers/message/components/Broadcast.tsx | 6 +++--- app/containers/message/components/CallButton.tsx | 6 +++--- app/containers/message/components/Discussion.tsx | 6 +++--- .../{Touchable.tsx => MessageActionTouchable.tsx} | 4 ++-- app/containers/message/components/Reactions.tsx | 10 +++++----- .../message/components/RightIcons/Encrypted.tsx | 6 +++--- .../message/components/RightIcons/MessageError.tsx | 6 +++--- app/containers/message/components/Thread.tsx | 6 +++--- app/containers/message/components/Urls.tsx | 6 +++--- 13 files changed, 40 insertions(+), 40 deletions(-) rename app/containers/message/components/{Touchable.tsx => MessageActionTouchable.tsx} (79%) diff --git a/app/containers/message/components/Attachments/CollapsibleQuote.tsx b/app/containers/message/components/Attachments/CollapsibleQuote.tsx index 1a7d952055d..a4a1d1a7427 100644 --- a/app/containers/message/components/Attachments/CollapsibleQuote.tsx +++ b/app/containers/message/components/Attachments/CollapsibleQuote.tsx @@ -9,7 +9,7 @@ import { useTheme } from '../../../../theme'; import sharedStyles from '../../../../views/Styles'; import Markdown from '../../../markdown'; import { useMessageUser } from '../../stores/MessageRoomStore'; -import Touchable from '../Touchable'; +import MessageActionTouchable from '../MessageActionTouchable'; import { BUTTON_HIT_SLOP } from '../../utils'; const styles = StyleSheet.create({ @@ -137,7 +137,7 @@ const CollapsibleQuote = ({ attachment, getCustomEmoji }: IMessageReply) => { return ( <> - <Touchable + <MessageActionTouchable testID={`collapsibleQuoteTouchable-${attachment.title}`} onPress={onPress} style={[ @@ -165,7 +165,7 @@ const CollapsibleQuote = ({ attachment, getCustomEmoji }: IMessageReply) => { <CustomIcon name={!collapsed ? 'chevron-up' : 'chevron-down'} size={22} color={strokeMedium} /> </View> </View> - </Touchable> + </MessageActionTouchable> </> ); }; diff --git a/app/containers/message/components/Attachments/Image/Button.tsx b/app/containers/message/components/Attachments/Image/Button.tsx index f69396a6ec3..eb98ec00da8 100644 --- a/app/containers/message/components/Attachments/Image/Button.tsx +++ b/app/containers/message/components/Attachments/Image/Button.tsx @@ -1,7 +1,7 @@ import { type ReactElement } from 'react'; import { type PressableProps } from 'react-native'; -import Touchable from '../../Touchable'; +import MessageActionTouchable from '../../MessageActionTouchable'; import styles from '../../../styles'; interface IMessageButton { @@ -22,13 +22,13 @@ export const Button = ({ 'use memo'; return ( - <Touchable + <MessageActionTouchable accessibilityLabel={accessibilityLabel} accessibilityRole={accessibilityRole} disabled={disabled} onPress={onPress} style={styles.imageContainer}> {children} - </Touchable> + </MessageActionTouchable> ); }; diff --git a/app/containers/message/components/Attachments/Reply.tsx b/app/containers/message/components/Attachments/Reply.tsx index 94160e9d1c8..90cc9515721 100644 --- a/app/containers/message/components/Attachments/Reply.tsx +++ b/app/containers/message/components/Attachments/Reply.tsx @@ -14,7 +14,7 @@ import { Attachments } from './components'; import Quote from './Quote'; import { useBaseUrl, useMessageUser, useTimeFormat } from '../../stores/MessageRoomStore'; import { useIsEncrypted, useMessageId } from '../../stores/MessageStore'; -import Touchable from '../Touchable'; +import MessageActionTouchable from '../MessageActionTouchable'; import messageStyles from '../../styles'; import dayjs from '../../../../lib/dayjs'; @@ -201,7 +201,7 @@ const Reply = ({ attachment, getCustomEmoji, msg }: IMessageReply) => { return ( <View style={{ gap: 4 }}> - <Touchable + <MessageActionTouchable testID={`reply-${attachment?.author_name}-${attachment?.text}`} onPress={onPress} style={[ @@ -231,7 +231,7 @@ const Reply = ({ attachment, getCustomEmoji, msg }: IMessageReply) => { </View> <UrlImage image={attachment.thumb_url} /> </View> - </Touchable> + </MessageActionTouchable> {msg ? <Markdown msg={msg} username={user?.username} getCustomEmoji={getCustomEmoji} /> : null} </View> ); diff --git a/app/containers/message/components/Attachments/Video.tsx b/app/containers/message/components/Attachments/Video.tsx index e48f2fbb006..e7798be9f8c 100644 --- a/app/containers/message/components/Attachments/Video.tsx +++ b/app/containers/message/components/Attachments/Video.tsx @@ -12,7 +12,7 @@ import sharedStyles from '../../../../views/Styles'; import { type TIconsName } from '../../../CustomIcon'; import { LISTENER } from '../../../Toast'; import Markdown from '../../../markdown'; -import Touchable from '../Touchable'; +import MessageActionTouchable from '../MessageActionTouchable'; import { useMediaAutoDownload } from '../../hooks/useMediaAutoDownload'; import { useMessageUser } from '../../stores/MessageRoomStore'; import BlurComponent from '../OverlayComponent'; @@ -102,9 +102,9 @@ const Video = ({ file, showAttachment, getCustomEmoji, author, msg }: IMessageVi return ( <View style={{ gap: 4 }}> {msg ? <Markdown msg={msg} username={user?.username} getCustomEmoji={getCustomEmoji} /> : null} - <Touchable onPress={_onPress} style={messageStyles.image}> + <MessageActionTouchable onPress={_onPress} style={messageStyles.image}> <Thumbnail status={status} encrypted={isEncrypted} /> - </Touchable> + </MessageActionTouchable> </View> ); }; diff --git a/app/containers/message/components/Broadcast.tsx b/app/containers/message/components/Broadcast.tsx index 39b85e28659..89a96999d4f 100644 --- a/app/containers/message/components/Broadcast.tsx +++ b/app/containers/message/components/Broadcast.tsx @@ -1,6 +1,6 @@ import { Text, View } from 'react-native'; -import Touchable from './Touchable'; +import MessageActionTouchable from './MessageActionTouchable'; import { CustomIcon } from '../../CustomIcon'; import styles from '../styles'; import { BUTTON_HIT_SLOP } from '../utils'; @@ -23,7 +23,7 @@ const Broadcast = () => { if (broadcast && !isOwn) { return ( <View style={styles.buttonContainer}> - <Touchable + <MessageActionTouchable onPress={() => replyBroadcast?.(item)} style={[styles.button, { backgroundColor: colors.badgeBackgroundLevel2 }]} hitSlop={BUTTON_HIT_SLOP} @@ -32,7 +32,7 @@ const Broadcast = () => { <CustomIcon name='arrow-back' size={20} color={colors.fontWhite} /> <Text style={[styles.buttonText, { color: colors.fontWhite }]}>{I18n.t('Reply')}</Text> </View> - </Touchable> + </MessageActionTouchable> </View> ); } diff --git a/app/containers/message/components/CallButton.tsx b/app/containers/message/components/CallButton.tsx index 8e0cb90b684..0adc986d563 100644 --- a/app/containers/message/components/CallButton.tsx +++ b/app/containers/message/components/CallButton.tsx @@ -1,6 +1,6 @@ import { Text, View } from 'react-native'; -import Touchable from './Touchable'; +import MessageActionTouchable from './MessageActionTouchable'; import { BUTTON_HIT_SLOP } from '../utils'; import styles from '../styles'; import I18n from '../../../i18n'; @@ -15,7 +15,7 @@ const CallButton = () => { const { colors } = useTheme(); return ( <View style={styles.buttonContainer}> - <Touchable + <MessageActionTouchable onPress={handleEnterCall} style={[styles.button, { backgroundColor: colors.badgeBackgroundLevel2 }]} hitSlop={BUTTON_HIT_SLOP}> @@ -23,7 +23,7 @@ const CallButton = () => { <CustomIcon name='video' size={16} color={colors.fontWhite} /> <Text style={[styles.buttonText, { color: colors.fontWhite }]}>{I18n.t('Click_to_join')}</Text> </View> - </Touchable> + </MessageActionTouchable> </View> ); }; diff --git a/app/containers/message/components/Discussion.tsx b/app/containers/message/components/Discussion.tsx index 0d725039443..dd52a38fb2f 100644 --- a/app/containers/message/components/Discussion.tsx +++ b/app/containers/message/components/Discussion.tsx @@ -1,6 +1,6 @@ import { Text, View } from 'react-native'; -import Touchable from './Touchable'; +import MessageActionTouchable from './MessageActionTouchable'; import { BUTTON_HIT_SLOP, formatMessageCount } from '../utils'; import styles from '../styles'; import I18n from '../../../i18n'; @@ -29,7 +29,7 @@ const Discussion = () => { <Text style={[styles.startedDiscussion, { color: colors.fontSecondaryInfo }]}>{I18n.t('Started_discussion')}</Text> <Text style={[styles.discussionText, { color: colors.fontDefault }]}>{messageText}</Text> <View style={[styles.buttonContainer, { gap: 8 }]}> - <Touchable + <MessageActionTouchable onPress={() => onDiscussionPress?.(drid)} style={[styles.button, { backgroundColor: colors.badgeBackgroundLevel2 }]} hitSlop={BUTTON_HIT_SLOP}> @@ -37,7 +37,7 @@ const Discussion = () => { <CustomIcon name='discussions' size={16} color={colors.fontWhite} /> <Text style={[styles.buttonText, { color: colors.fontWhite }]}>{buttonText}</Text> </View> - </Touchable> + </MessageActionTouchable> <Text style={[styles.time, { color: colors.fontSecondaryInfo }]}>{time}</Text> </View> </View> diff --git a/app/containers/message/components/Touchable.tsx b/app/containers/message/components/MessageActionTouchable.tsx similarity index 79% rename from app/containers/message/components/Touchable.tsx rename to app/containers/message/components/MessageActionTouchable.tsx index 1524dbfc341..220561af2a1 100644 --- a/app/containers/message/components/Touchable.tsx +++ b/app/containers/message/components/MessageActionTouchable.tsx @@ -8,7 +8,7 @@ interface IProps extends PressableProps { onLongPress?: () => void; } -const RCTouchable: FC<IProps> = ({ children, ...props }) => { +const MessageActionTouchable: FC<IProps> = ({ children, ...props }) => { 'use memo'; const onLongPress = useMessageLongPress(); @@ -20,4 +20,4 @@ const RCTouchable: FC<IProps> = ({ children, ...props }) => { ); }; -export default RCTouchable; +export default MessageActionTouchable; diff --git a/app/containers/message/components/Reactions.tsx b/app/containers/message/components/Reactions.tsx index 113a99be18c..c4da75f1f62 100644 --- a/app/containers/message/components/Reactions.tsx +++ b/app/containers/message/components/Reactions.tsx @@ -1,7 +1,7 @@ import { Text, useWindowDimensions, View } from 'react-native'; import I18n from '../../../i18n'; -import Touchable from './Touchable'; +import MessageActionTouchable from './MessageActionTouchable'; import { CustomIcon } from '../../CustomIcon'; import styles from '../styles'; import Emoji from './Emoji'; @@ -30,7 +30,7 @@ const AddReaction = () => { const { fontScale } = useWindowDimensions(); const height = 28 * fontScale; return ( - <Touchable + <MessageActionTouchable onPress={() => reactionInit?.(id)} key='message-add-reaction' testID='message-add-reaction' @@ -42,7 +42,7 @@ const AddReaction = () => { <View style={[styles.reactionContainer, { borderColor: colors.strokeLight, height }]}> <CustomIcon name='reaction-add' size={20} color={colors.badgeBackgroundLevel2} /> </View> - </Touchable> + </MessageActionTouchable> ); }; @@ -60,7 +60,7 @@ const Reaction = ({ reaction }: IMessageReaction) => { const height = 28 * fontScale; const reacted = reaction.usernames.findIndex((item: string) => item === user?.username) !== -1; return ( - <Touchable + <MessageActionTouchable onPress={() => onReactionPress?.(reaction.emoji, id)} onLongPress={() => onReactionLongPress?.(item)} key={reaction.emoji} @@ -81,7 +81,7 @@ const Reaction = ({ reaction }: IMessageReaction) => { /> <Text style={[styles.reactionCount, { color: colors.badgeBackgroundLevel2 }]}>{reaction.usernames.length}</Text> </View> - </Touchable> + </MessageActionTouchable> ); }; diff --git a/app/containers/message/components/RightIcons/Encrypted.tsx b/app/containers/message/components/RightIcons/Encrypted.tsx index 79f1ceb9d43..8e930a17ecc 100644 --- a/app/containers/message/components/RightIcons/Encrypted.tsx +++ b/app/containers/message/components/RightIcons/Encrypted.tsx @@ -1,4 +1,4 @@ -import Touchable from '../Touchable'; +import MessageActionTouchable from '../MessageActionTouchable'; import { CustomIcon } from '../../../CustomIcon'; import { BUTTON_HIT_SLOP } from '../../utils'; import styles from '../../styles'; @@ -17,9 +17,9 @@ const Encrypted = () => { } return ( - <Touchable onPress={onEncryptedPress} style={styles.rightIcons} hitSlop={BUTTON_HIT_SLOP}> + <MessageActionTouchable onPress={onEncryptedPress} style={styles.rightIcons} hitSlop={BUTTON_HIT_SLOP}> <CustomIcon name='encrypted' size={16} /> - </Touchable> + </MessageActionTouchable> ); }; diff --git a/app/containers/message/components/RightIcons/MessageError.tsx b/app/containers/message/components/RightIcons/MessageError.tsx index c980ea0bedf..9c4de4ed8d1 100644 --- a/app/containers/message/components/RightIcons/MessageError.tsx +++ b/app/containers/message/components/RightIcons/MessageError.tsx @@ -1,4 +1,4 @@ -import Touchable from '../Touchable'; +import MessageActionTouchable from '../MessageActionTouchable'; import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; import { BUTTON_HIT_SLOP } from '../../utils'; @@ -19,9 +19,9 @@ const MessageError = () => { } return ( - <Touchable onPress={() => errorActionsShow?.(item)} style={styles.rightIcons} hitSlop={BUTTON_HIT_SLOP}> + <MessageActionTouchable onPress={() => errorActionsShow?.(item)} style={styles.rightIcons} hitSlop={BUTTON_HIT_SLOP}> <CustomIcon name='warning' color={colors.buttonBackgroundDangerDefault} size={16} /> - </Touchable> + </MessageActionTouchable> ); }; diff --git a/app/containers/message/components/Thread.tsx b/app/containers/message/components/Thread.tsx index 6bc29e4e313..d0868611a9c 100644 --- a/app/containers/message/components/Thread.tsx +++ b/app/containers/message/components/Thread.tsx @@ -4,7 +4,7 @@ import styles from '../styles'; import ThreadDetails from '../../ThreadDetails'; import I18n from '../../../i18n'; import { useTheme } from '../../../theme'; -import Touchable from './Touchable'; +import MessageActionTouchable from './MessageActionTouchable'; import { useMessageItem, useMessageText, useReplies, useThreadBadgeColor, useThreadData } from '../stores/MessageStore'; import { useIsThreadRoom, useMessageUser, useOnThreadPress, useToggleFollowThread } from '../stores/MessageRoomStore'; @@ -31,14 +31,14 @@ const Thread = () => { return ( <View style={styles.buttonContainer}> - <Touchable + <MessageActionTouchable onPress={() => onThreadPress?.(item)} accessibilityRole='button' accessibilityLabel={I18n.t('View_Thread')} style={[styles.button, { backgroundColor }]} testID={`message-thread-button-${messageText}`}> <Text style={[styles.buttonText, { color: textColor }]}>{I18n.t('View_Thread')}</Text> - </Touchable> + </MessageActionTouchable> <ThreadDetails item={{ tcount, diff --git a/app/containers/message/components/Urls.tsx b/app/containers/message/components/Urls.tsx index 6afbe1c80bd..172f9fd1c5f 100644 --- a/app/containers/message/components/Urls.tsx +++ b/app/containers/message/components/Urls.tsx @@ -4,7 +4,7 @@ import Clipboard from '@react-native-clipboard/clipboard'; import { Image } from 'expo-image'; import axios from 'axios'; -import Touchable from './Touchable'; +import MessageActionTouchable from './MessageActionTouchable'; import openLink from '../../../lib/methods/helpers/openLink'; import sharedStyles from '../../../views/Styles'; import { useTheme } from '../../../theme'; @@ -162,7 +162,7 @@ const Url = ({ url }: { url: IUrl }) => { } return ( - <Touchable + <MessageActionTouchable onPress={onPress} onLongPress={onLongPress} style={[ @@ -179,7 +179,7 @@ const Url = ({ url }: { url: IUrl }) => { {imageUrl ? <UrlImage image={imageUrl} hasContent={hasContent} /> : null} {hasContent ? <UrlContent title={url.title} description={url.description} /> : null} </> - </Touchable> + </MessageActionTouchable> ); }; const Urls = (): ReactElement[] | null => { From 4afdf9f27a76a005be064d7f4f573cc67c01675a Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 17:41:40 -0300 Subject: [PATCH 112/144] chore: route ModalBlockView's empty-message fixture through unknown (NATIVE-22) --- app/views/ModalBlockView.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/ModalBlockView.tsx b/app/views/ModalBlockView.tsx index 3360c62098d..ae39435bf46 100644 --- a/app/views/ModalBlockView.tsx +++ b/app/views/ModalBlockView.tsx @@ -97,8 +97,9 @@ const LoadingIndicator = ({ loading }: { loading: boolean }) => { // A UIKit modal is not a message, but the shared media components it can render // (ImageContainer -> Button -> Touchable) assume a per-message context. Provide an // empty one: no long-press target, no id-scoped cache. Images still load via the -// room provider's user/baseUrl. -const EMPTY_MESSAGE = {} as TAnyMessageModel; +// room provider's user/baseUrl. Routed through `unknown` (the repo's convention for +// fake TAnyMessageModel fixtures) so the cast reads as intentional, not a real message. +const EMPTY_MESSAGE = {} as unknown as TAnyMessageModel; class ModalBlockView extends Component<IModalBlockViewProps, IModalBlockViewState> { private submitting: boolean; From 35121152fb43bf0b7b4dc7cab210f579cc5b9396 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Tue, 7 Jul 2026 17:47:01 -0300 Subject: [PATCH 113/144] refactor: strip redundant displayName from message components (NATIVE-22) --- .../message/components/Attachments/Attachments.tsx | 2 -- .../message/components/Attachments/CollapsibleQuote.tsx | 1 - .../message/components/Attachments/Image/Container.tsx | 2 -- .../message/components/Attachments/Image/Image.tsx | 2 -- app/containers/message/components/Attachments/Quote.tsx | 2 -- app/containers/message/components/Attachments/Reply.tsx | 3 --- app/containers/message/components/Blocks.ts | 2 -- app/containers/message/components/Broadcast.tsx | 2 -- app/containers/message/components/CallButton.tsx | 2 -- app/containers/message/components/Content.tsx | 2 -- app/containers/message/components/Content/ContentWrapper.tsx | 2 -- .../message/components/Content/EncryptedContent.tsx | 2 -- app/containers/message/components/Content/IgnoredContent.tsx | 2 -- app/containers/message/components/Content/InfoContent.tsx | 2 -- .../message/components/Content/MarkdownContent.tsx | 2 -- app/containers/message/components/Content/PreviewContent.tsx | 2 -- app/containers/message/components/Discussion.tsx | 2 -- app/containers/message/components/Emoji.tsx | 2 -- app/containers/message/components/Message.tsx | 3 --- app/containers/message/components/Message/BlocksBranch.tsx | 2 -- app/containers/message/components/Message/CompactMessage.tsx | 2 -- app/containers/message/components/Message/DefaultBranch.tsx | 2 -- .../message/components/Message/DiscussionBranch.tsx | 2 -- app/containers/message/components/Message/JitsiBranch.tsx | 2 -- app/containers/message/components/Message/MessageInner.tsx | 1 - app/containers/message/components/Message/NormalMessage.tsx | 2 -- app/containers/message/components/Message/PreviewBranch.tsx | 2 -- app/containers/message/components/MessageAvatar.tsx | 2 -- app/containers/message/components/Reactions.tsx | 4 ---- app/containers/message/components/RepliedThread.tsx | 2 -- .../message/components/RightIcons/MessageError.tsx | 2 -- app/containers/message/components/RightIcons/ReadReceipt.tsx | 1 - app/containers/message/components/Thread.tsx | 2 -- app/containers/message/components/Urls.tsx | 5 ----- app/containers/message/components/User.tsx | 2 -- 35 files changed, 74 deletions(-) diff --git a/app/containers/message/components/Attachments/Attachments.tsx b/app/containers/message/components/Attachments/Attachments.tsx index c836eb29a3b..e846e2735d3 100644 --- a/app/containers/message/components/Attachments/Attachments.tsx +++ b/app/containers/message/components/Attachments/Attachments.tsx @@ -98,6 +98,4 @@ const Attachments: FC<IMessageAttachments> = ({ attachments, author }: IMessageA return <View style={{ gap: 4 }}>{attachmentsElements}</View>; }; -Attachments.displayName = 'MessageAttachments'; - export default Attachments; diff --git a/app/containers/message/components/Attachments/CollapsibleQuote.tsx b/app/containers/message/components/Attachments/CollapsibleQuote.tsx index a4a1d1a7427..209e024511e 100644 --- a/app/containers/message/components/Attachments/CollapsibleQuote.tsx +++ b/app/containers/message/components/Attachments/CollapsibleQuote.tsx @@ -170,7 +170,6 @@ const CollapsibleQuote = ({ attachment, getCustomEmoji }: IMessageReply) => { ); }; -CollapsibleQuote.displayName = 'CollapsibleQuote'; Fields.displayName = 'CollapsibleQuoteFields'; export default CollapsibleQuote; diff --git a/app/containers/message/components/Attachments/Image/Container.tsx b/app/containers/message/components/Attachments/Image/Container.tsx index af00157c4f0..aafacb9cc6e 100644 --- a/app/containers/message/components/Attachments/Image/Container.tsx +++ b/app/containers/message/components/Attachments/Image/Container.tsx @@ -61,6 +61,4 @@ const ImageContainer = ({ return image; }; -ImageContainer.displayName = 'MessageImageContainer'; - export default ImageContainer; diff --git a/app/containers/message/components/Attachments/Image/Image.tsx b/app/containers/message/components/Attachments/Image/Image.tsx index 0156276037f..906ebe13b4d 100644 --- a/app/containers/message/components/Attachments/Image/Image.tsx +++ b/app/containers/message/components/Attachments/Image/Image.tsx @@ -98,5 +98,3 @@ export const MessageImage = ({ uri, status, encrypted = false, imagePreview, ima </> ); }; - -MessageImage.displayName = 'MessageImage'; diff --git a/app/containers/message/components/Attachments/Quote.tsx b/app/containers/message/components/Attachments/Quote.tsx index 92e17441732..5ab3ac912e0 100644 --- a/app/containers/message/components/Attachments/Quote.tsx +++ b/app/containers/message/components/Attachments/Quote.tsx @@ -37,6 +37,4 @@ const Quote: FC<IMessageAttachments> = ({ attachments }: IMessageAttachments) => return <View style={{ gap: 4 }}>{quotesElements}</View>; }; -Quote.displayName = 'MessageQuote'; - export default Quote; diff --git a/app/containers/message/components/Attachments/Reply.tsx b/app/containers/message/components/Attachments/Reply.tsx index 90cc9515721..e26c1783b69 100644 --- a/app/containers/message/components/Attachments/Reply.tsx +++ b/app/containers/message/components/Attachments/Reply.tsx @@ -237,9 +237,6 @@ const Reply = ({ attachment, getCustomEmoji, msg }: IMessageReply) => { ); }; -Reply.displayName = 'MessageReply'; -Title.displayName = 'MessageReplyTitle'; -Description.displayName = 'MessageReplyDescription'; Fields.displayName = 'MessageReplyFields'; export default Reply; diff --git a/app/containers/message/components/Blocks.ts b/app/containers/message/components/Blocks.ts index 109491070d8..f8897a04a56 100644 --- a/app/containers/message/components/Blocks.ts +++ b/app/containers/message/components/Blocks.ts @@ -36,6 +36,4 @@ const Blocks = () => { return null; }; -Blocks.displayName = 'MessageBlocks'; - export default Blocks; diff --git a/app/containers/message/components/Broadcast.tsx b/app/containers/message/components/Broadcast.tsx index 89a96999d4f..3beb5f500ee 100644 --- a/app/containers/message/components/Broadcast.tsx +++ b/app/containers/message/components/Broadcast.tsx @@ -39,6 +39,4 @@ const Broadcast = () => { return null; }; -Broadcast.displayName = 'MessageBroadcast'; - export default Broadcast; diff --git a/app/containers/message/components/CallButton.tsx b/app/containers/message/components/CallButton.tsx index 0adc986d563..381d648ef36 100644 --- a/app/containers/message/components/CallButton.tsx +++ b/app/containers/message/components/CallButton.tsx @@ -28,6 +28,4 @@ const CallButton = () => { ); }; -CallButton.displayName = 'CallButton'; - export default CallButton; diff --git a/app/containers/message/components/Content.tsx b/app/containers/message/components/Content.tsx index 4fa72707a87..2d2c44ab25a 100644 --- a/app/containers/message/components/Content.tsx +++ b/app/containers/message/components/Content.tsx @@ -39,6 +39,4 @@ const Content = () => { return null; }; -Content.displayName = 'MessageContent'; - export default Content; diff --git a/app/containers/message/components/Content/ContentWrapper.tsx b/app/containers/message/components/Content/ContentWrapper.tsx index 0527b1d77ac..6c749e917ed 100644 --- a/app/containers/message/components/Content/ContentWrapper.tsx +++ b/app/containers/message/components/Content/ContentWrapper.tsx @@ -17,6 +17,4 @@ const ContentWrapper = ({ children }: { children: ReactNode }) => { ); }; -ContentWrapper.displayName = 'MessageContentWrapper'; - export default ContentWrapper; diff --git a/app/containers/message/components/Content/EncryptedContent.tsx b/app/containers/message/components/Content/EncryptedContent.tsx index 2b14006a27a..9233e621daf 100644 --- a/app/containers/message/components/Content/EncryptedContent.tsx +++ b/app/containers/message/components/Content/EncryptedContent.tsx @@ -22,6 +22,4 @@ const EncryptedContent = () => { ); }; -EncryptedContent.displayName = 'MessageEncryptedContent'; - export default EncryptedContent; diff --git a/app/containers/message/components/Content/IgnoredContent.tsx b/app/containers/message/components/Content/IgnoredContent.tsx index 4354a08eb1b..2dbd7c3bbb9 100644 --- a/app/containers/message/components/Content/IgnoredContent.tsx +++ b/app/containers/message/components/Content/IgnoredContent.tsx @@ -21,6 +21,4 @@ const IgnoredContent = () => { ); }; -IgnoredContent.displayName = 'MessageIgnoredContent'; - export default IgnoredContent; diff --git a/app/containers/message/components/Content/InfoContent.tsx b/app/containers/message/components/Content/InfoContent.tsx index b209bdd61d4..777bbcc64d6 100644 --- a/app/containers/message/components/Content/InfoContent.tsx +++ b/app/containers/message/components/Content/InfoContent.tsx @@ -34,6 +34,4 @@ const InfoContent = () => { return renderMessageContent; }; -InfoContent.displayName = 'MessageInfoContent'; - export default InfoContent; diff --git a/app/containers/message/components/Content/MarkdownContent.tsx b/app/containers/message/components/Content/MarkdownContent.tsx index f7277e4d8c6..8d7fbcb4580 100644 --- a/app/containers/message/components/Content/MarkdownContent.tsx +++ b/app/containers/message/components/Content/MarkdownContent.tsx @@ -34,6 +34,4 @@ const MarkdownContent = () => { ); }; -MarkdownContent.displayName = 'MessageMarkdownContent'; - export default MarkdownContent; diff --git a/app/containers/message/components/Content/PreviewContent.tsx b/app/containers/message/components/Content/PreviewContent.tsx index 8541f9cb318..11fc99fea25 100644 --- a/app/containers/message/components/Content/PreviewContent.tsx +++ b/app/containers/message/components/Content/PreviewContent.tsx @@ -25,6 +25,4 @@ const PreviewContent = () => { ); }; -PreviewContent.displayName = 'MessagePreviewContent'; - export default PreviewContent; diff --git a/app/containers/message/components/Discussion.tsx b/app/containers/message/components/Discussion.tsx index dd52a38fb2f..0e5e06cd213 100644 --- a/app/containers/message/components/Discussion.tsx +++ b/app/containers/message/components/Discussion.tsx @@ -44,6 +44,4 @@ const Discussion = () => { ); }; -Discussion.displayName = 'MessageDiscussion'; - export default Discussion; diff --git a/app/containers/message/components/Emoji.tsx b/app/containers/message/components/Emoji.tsx index 4c426e99162..5f0efab42ad 100644 --- a/app/containers/message/components/Emoji.tsx +++ b/app/containers/message/components/Emoji.tsx @@ -16,6 +16,4 @@ const Emoji = ({ content, standardEmojiStyle, customEmojiStyle, getCustomEmoji } return <Text style={standardEmojiStyle}>{formatShortnameToUnicode(content)}</Text>; }; -Emoji.displayName = 'MessageEmoji'; - export default Emoji; diff --git a/app/containers/message/components/Message.tsx b/app/containers/message/components/Message.tsx index 226ac19dd65..3e32931068f 100644 --- a/app/containers/message/components/Message.tsx +++ b/app/containers/message/components/Message.tsx @@ -38,7 +38,6 @@ const Message = (props: TMessageProps) => { return <NormalMessage isPreview={props.isPreview} />; }; -Message.displayName = 'Message'; const MessageTouchable = (props: TMessageProps) => { 'use memo'; @@ -102,6 +101,4 @@ const MessageTouchable = (props: TMessageProps) => { ); }; -MessageTouchable.displayName = 'MessageTouchable'; - export default MessageTouchable; diff --git a/app/containers/message/components/Message/BlocksBranch.tsx b/app/containers/message/components/Message/BlocksBranch.tsx index 1cd5527624d..f2f491c9655 100644 --- a/app/containers/message/components/Message/BlocksBranch.tsx +++ b/app/containers/message/components/Message/BlocksBranch.tsx @@ -18,6 +18,4 @@ const BlocksBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { ); }; -BlocksBranch.displayName = 'MessageBlocksBranch'; - export default BlocksBranch; diff --git a/app/containers/message/components/Message/CompactMessage.tsx b/app/containers/message/components/Message/CompactMessage.tsx index 1abf13ceee7..d4eb6c705fb 100644 --- a/app/containers/message/components/Message/CompactMessage.tsx +++ b/app/containers/message/components/Message/CompactMessage.tsx @@ -55,6 +55,4 @@ const CompactMessage = () => { ); }; -CompactMessage.displayName = 'MessageCompact'; - export default CompactMessage; diff --git a/app/containers/message/components/Message/DefaultBranch.tsx b/app/containers/message/components/Message/DefaultBranch.tsx index 701c6f4d44c..3c8241af8d2 100644 --- a/app/containers/message/components/Message/DefaultBranch.tsx +++ b/app/containers/message/components/Message/DefaultBranch.tsx @@ -24,6 +24,4 @@ const DefaultBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { ); }; -DefaultBranch.displayName = 'MessageDefaultBranch'; - export default DefaultBranch; diff --git a/app/containers/message/components/Message/DiscussionBranch.tsx b/app/containers/message/components/Message/DiscussionBranch.tsx index 21f12f86a6d..f08741b99cb 100644 --- a/app/containers/message/components/Message/DiscussionBranch.tsx +++ b/app/containers/message/components/Message/DiscussionBranch.tsx @@ -14,6 +14,4 @@ const DiscussionBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { ); }; -DiscussionBranch.displayName = 'MessageDiscussionBranch'; - export default DiscussionBranch; diff --git a/app/containers/message/components/Message/JitsiBranch.tsx b/app/containers/message/components/Message/JitsiBranch.tsx index 395ec857a3e..e9163855c30 100644 --- a/app/containers/message/components/Message/JitsiBranch.tsx +++ b/app/containers/message/components/Message/JitsiBranch.tsx @@ -16,6 +16,4 @@ const JitsiBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { ); }; -JitsiBranch.displayName = 'MessageJitsiBranch'; - export default JitsiBranch; diff --git a/app/containers/message/components/Message/MessageInner.tsx b/app/containers/message/components/Message/MessageInner.tsx index c42e4884748..ef7e2b2654f 100644 --- a/app/containers/message/components/Message/MessageInner.tsx +++ b/app/containers/message/components/Message/MessageInner.tsx @@ -30,4 +30,3 @@ export const MessageInner = ({ isPreview, isHeader }: { isPreview?: boolean; isH return <WidthAwareView>{branch}</WidthAwareView>; }; -MessageInner.displayName = 'MessageInner'; diff --git a/app/containers/message/components/Message/NormalMessage.tsx b/app/containers/message/components/Message/NormalMessage.tsx index e2f29828aeb..2b8b747320a 100644 --- a/app/containers/message/components/Message/NormalMessage.tsx +++ b/app/containers/message/components/Message/NormalMessage.tsx @@ -35,6 +35,4 @@ const NormalMessage = ({ isPreview }: { isPreview?: boolean }) => { ); }; -NormalMessage.displayName = 'MessageNormal'; - export default NormalMessage; diff --git a/app/containers/message/components/Message/PreviewBranch.tsx b/app/containers/message/components/Message/PreviewBranch.tsx index 432a38fef8e..7d8aa4ca453 100644 --- a/app/containers/message/components/Message/PreviewBranch.tsx +++ b/app/containers/message/components/Message/PreviewBranch.tsx @@ -14,6 +14,4 @@ const PreviewBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { ); }; -PreviewBranch.displayName = 'MessagePreviewBranch'; - export default PreviewBranch; diff --git a/app/containers/message/components/MessageAvatar.tsx b/app/containers/message/components/MessageAvatar.tsx index 5ca505d4f72..e9fa6536b04 100644 --- a/app/containers/message/components/MessageAvatar.tsx +++ b/app/containers/message/components/MessageAvatar.tsx @@ -58,6 +58,4 @@ const MessageAvatar = ({ small }: IMessageAvatar) => { return <AvatarContainer />; }; -MessageAvatar.displayName = 'MessageAvatar'; - export default MessageAvatar; diff --git a/app/containers/message/components/Reactions.tsx b/app/containers/message/components/Reactions.tsx index c4da75f1f62..7982328b248 100644 --- a/app/containers/message/components/Reactions.tsx +++ b/app/containers/message/components/Reactions.tsx @@ -103,8 +103,4 @@ const Reactions = () => { ); }; -Reaction.displayName = 'MessageReaction'; -Reactions.displayName = 'MessageReactions'; -AddReaction.displayName = 'MessageAddReaction'; - export default Reactions; diff --git a/app/containers/message/components/RepliedThread.tsx b/app/containers/message/components/RepliedThread.tsx index 9e416bf81bd..900a502be18 100644 --- a/app/containers/message/components/RepliedThread.tsx +++ b/app/containers/message/components/RepliedThread.tsx @@ -61,6 +61,4 @@ const RepliedThread = ({ isHeader }: IMessageRepliedThread) => { ); }; -RepliedThread.displayName = 'MessageRepliedThread'; - export default RepliedThread; diff --git a/app/containers/message/components/RightIcons/MessageError.tsx b/app/containers/message/components/RightIcons/MessageError.tsx index 9c4de4ed8d1..e9306781bd0 100644 --- a/app/containers/message/components/RightIcons/MessageError.tsx +++ b/app/containers/message/components/RightIcons/MessageError.tsx @@ -25,6 +25,4 @@ const MessageError = () => { ); }; -MessageError.displayName = 'MessageError'; - export default MessageError; diff --git a/app/containers/message/components/RightIcons/ReadReceipt.tsx b/app/containers/message/components/RightIcons/ReadReceipt.tsx index 83421d79bf0..f96e30c4b1d 100644 --- a/app/containers/message/components/RightIcons/ReadReceipt.tsx +++ b/app/containers/message/components/RightIcons/ReadReceipt.tsx @@ -28,6 +28,5 @@ const ReadReceipt = () => { } return null; }; -ReadReceipt.displayName = 'MessageReadReceipt'; export default ReadReceipt; diff --git a/app/containers/message/components/Thread.tsx b/app/containers/message/components/Thread.tsx index d0868611a9c..08bb09a4f63 100644 --- a/app/containers/message/components/Thread.tsx +++ b/app/containers/message/components/Thread.tsx @@ -54,6 +54,4 @@ const Thread = () => { ); }; -Thread.displayName = 'MessageThread'; - export default Thread; diff --git a/app/containers/message/components/Urls.tsx b/app/containers/message/components/Urls.tsx index 172f9fd1c5f..4b4a9920312 100644 --- a/app/containers/message/components/Urls.tsx +++ b/app/containers/message/components/Urls.tsx @@ -194,9 +194,4 @@ const Urls = (): ReactElement[] | null => { return urls.map((url: IUrl) => <Url url={url} key={url.url} />); }; -UrlContent.displayName = 'MessageUrlContent'; -UrlImage.displayName = 'MessageUrlImage'; -Url.displayName = 'MessageUrl'; -Urls.displayName = 'MessageUrls'; - export default Urls; diff --git a/app/containers/message/components/User.tsx b/app/containers/message/components/User.tsx index 681965ce4f6..eabae487efe 100644 --- a/app/containers/message/components/User.tsx +++ b/app/containers/message/components/User.tsx @@ -95,6 +95,4 @@ const User = () => { return null; }; -User.displayName = 'MessageUser'; - export default User; From f0c61cc054bac15c64fb76bca20b5f8fd13418e4 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 10:14:43 -0300 Subject: [PATCH 114/144] chore: refresh time-relative Timestamp story snapshot The Timestamp story renders a fixed unix time with the relative (`R`) format, so its snapshot drifts with wall-clock time (`2 years ago` -> `a year ago`). --- app/containers/markdown/__snapshots__/Markdown.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap index 2517ef4eb26..009e5615a36 100644 --- a/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap +++ b/app/containers/markdown/__snapshots__/Markdown.test.tsx.snap @@ -5882,7 +5882,7 @@ exports[`Story Snapshots: Timestamp should match snapshot 1`] = ` ] } > - 2 years ago + a year ago </Text> </Text> </Text> From b074fee5773c7b3b9619599d4e370614df620181 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 11:35:57 -0300 Subject: [PATCH 115/144] refactor: rename InteractionStore to MessageActionStore, normalize action verbs (NATIVE-22) --- .../MessageComposer/MessageComposer.test.tsx | 23 ++- .../MessageComposer/MessageComposer.tsx | 2 +- .../MessageComposer/components/CancelEdit.tsx | 2 +- .../components/ComposerInput.tsx | 2 +- .../components/Quotes/Quotes.tsx | 2 +- .../MessageComposer/hooks/useAutoSaveDraft.ts | 2 +- .../hooks/useChooseMedia.test.tsx | 6 +- .../MessageComposer/hooks/useChooseMedia.ts | 2 +- .../message/__tests__/index.test.tsx | 20 +-- app/containers/message/components/Message.tsx | 2 +- .../components/stories/Message.stories.tsx | 12 +- app/definitions/IMessage.ts | 3 +- app/views/RoomView/InteractionStore.test.tsx | 147 ------------------ app/views/RoomView/InteractionStore.tsx | 104 ------------- .../RoomView/MessageActionStore.test.tsx | 137 ++++++++++++++++ app/views/RoomView/MessageActionStore.tsx | 103 ++++++++++++ app/views/RoomView/RoomProviders.test.tsx | 6 +- app/views/RoomView/RoomProviders.tsx | 8 +- app/views/RoomView/index.tsx | 50 +++--- app/views/ShareView/ShareView.test.tsx | 4 +- app/views/ShareView/index.tsx | 26 ++-- 21 files changed, 325 insertions(+), 338 deletions(-) delete mode 100644 app/views/RoomView/InteractionStore.test.tsx delete mode 100644 app/views/RoomView/InteractionStore.tsx create mode 100644 app/views/RoomView/MessageActionStore.test.tsx create mode 100644 app/views/RoomView/MessageActionStore.tsx diff --git a/app/containers/MessageComposer/MessageComposer.test.tsx b/app/containers/MessageComposer/MessageComposer.test.tsx index d4208d30d1b..4e6b423dc0e 100644 --- a/app/containers/MessageComposer/MessageComposer.test.tsx +++ b/app/containers/MessageComposer/MessageComposer.test.tsx @@ -13,7 +13,7 @@ import { type IPermissionsState } from '../../reducers/permissions'; import { type IMessage, type IShareAttachment } from '../../definitions'; import { colors } from '../../lib/constants/colors'; import { type IRoomContext, RoomContext } from '../../views/RoomView/context'; -import { InteractionProvider, type TInteraction } from '../../views/RoomView/InteractionStore'; +import { MessageActionProvider, type TMessageActionState } from '../../views/RoomView/MessageActionStore'; import * as EmojiKeyboardHook from './hooks/useEmojiKeyboard'; import { initStore } from '../../lib/store/auxStore'; import { searchRemote } from '../../lib/methods/search'; @@ -119,15 +119,15 @@ const initialContext = { const Render = ({ context, - interactionState, + action, children }: { context?: Partial<IRoomContext>; - interactionState?: TInteraction; + action?: TMessageActionState; children?: ReactElement; }) => ( <Provider store={mockedStore}> - <InteractionProvider initialState={interactionState}> + <MessageActionProvider initialAction={action}> <RoomContext.Provider value={{ ...initialContext, ...context }}> <MessageComposerContainer> <> @@ -136,7 +136,7 @@ const Render = ({ </> </MessageComposerContainer> </RoomContext.Provider> - </InteractionProvider> + </MessageActionProvider> </Provider> ); @@ -648,10 +648,7 @@ describe('MessageComposer', () => { const id = 'messageId'; beforeEach(() => { return renderAndFlush( - <Render - context={{ rid: 'rid', onSendMessage, editCancel, editRequest }} - interactionState={{ kind: 'edit', messageId: id }} - /> + <Render context={{ rid: 'rid', onSendMessage, editCancel, editRequest }} action={{ kind: 'edit', messageId: id }} /> ); }); test('init', async () => { @@ -683,7 +680,7 @@ describe('MessageComposer', () => { const editRequest = jest.fn(); const id = 'image'; test('edit image', async () => { - await renderAndFlush(<Render context={{ rid: 'rid', editRequest }} interactionState={{ kind: 'edit', messageId: id }} />); + await renderAndFlush(<Render context={{ rid: 'rid', editRequest }} action={{ kind: 'edit', messageId: id }} />); await screen.findByTestId('message-composer'); await user.press(screen.getByTestId('message-composer-send')); expect(editRequest).toHaveBeenCalledWith({ id, msg: `Attachment description for ${id}`, rid: 'rid' }); @@ -715,14 +712,14 @@ describe('MessageComposer', () => { describe('Quote', () => { test('Add quote `abc`', async () => { - render(<Render interactionState={{ kind: 'quote', messageIds: ['abc'] }} />); + render(<Render action={{ kind: 'quote', messageIds: ['abc'] }} />); await screen.findByTestId('composer-quote-abc'); expect(screen.queryByTestId('composer-quote-abc')).toBeOnTheScreen(); expect(screen.toJSON()).toMatchSnapshot(); }); test('Add quote `def`', async () => { - render(<Render interactionState={{ kind: 'quote', messageIds: ['abc', 'def'] }} />); + render(<Render action={{ kind: 'quote', messageIds: ['abc', 'def'] }} />); await screen.findByTestId('composer-quote-abc'); expect(screen.queryByTestId('composer-quote-abc')).toBeOnTheScreen(); expect(screen.queryByTestId('composer-quote-def')).toBeOnTheScreen(); @@ -731,7 +728,7 @@ describe('MessageComposer', () => { test('Remove a quote', async () => { const onRemoveQuoteMessage = jest.fn(); - render(<Render context={{ onRemoveQuoteMessage }} interactionState={{ kind: 'quote', messageIds: ['abc', 'def'] }} />); + render(<Render context={{ onRemoveQuoteMessage }} action={{ kind: 'quote', messageIds: ['abc', 'def'] }} />); await screen.findByTestId('composer-quote-def'); await user.press(screen.getByTestId('composer-quote-remove-def')); expect(onRemoveQuoteMessage).toHaveBeenCalledTimes(1); diff --git a/app/containers/MessageComposer/MessageComposer.tsx b/app/containers/MessageComposer/MessageComposer.tsx index 1fda1b7954b..171b980244c 100644 --- a/app/containers/MessageComposer/MessageComposer.tsx +++ b/app/containers/MessageComposer/MessageComposer.tsx @@ -5,7 +5,7 @@ import { Q } from '@nozbe/watermelondb'; import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; import { useRoomContext } from '../../views/RoomView/context'; -import { useMessageAction, useSelectedMessages } from '../../views/RoomView/InteractionStore'; +import { useMessageAction, useSelectedMessages } from '../../views/RoomView/MessageActionStore'; import { Autocomplete } from './components'; import { MIN_HEIGHT } from './constants'; import { diff --git a/app/containers/MessageComposer/components/CancelEdit.tsx b/app/containers/MessageComposer/components/CancelEdit.tsx index 8c65d7bbc3e..a643096da99 100644 --- a/app/containers/MessageComposer/components/CancelEdit.tsx +++ b/app/containers/MessageComposer/components/CancelEdit.tsx @@ -1,6 +1,6 @@ import { BaseButton } from './Buttons'; import { useRoomContext } from '../../../views/RoomView/context'; -import { useMessageAction } from '../../../views/RoomView/InteractionStore'; +import { useMessageAction } from '../../../views/RoomView/MessageActionStore'; import { Gap } from './Gap'; export const CancelEdit = () => { diff --git a/app/containers/MessageComposer/components/ComposerInput.tsx b/app/containers/MessageComposer/components/ComposerInput.tsx index 90e2c5df4b4..85431d80b85 100644 --- a/app/containers/MessageComposer/components/ComposerInput.tsx +++ b/app/containers/MessageComposer/components/ComposerInput.tsx @@ -33,7 +33,7 @@ import database from '../../../lib/database'; import Navigation from '../../../lib/navigation/appNavigation'; import { emitter } from '../../../lib/methods/helpers/emitter'; import { useRoomContext } from '../../../views/RoomView/context'; -import { useMessageAction, useSelectedMessages } from '../../../views/RoomView/InteractionStore'; +import { useMessageAction, useSelectedMessages } from '../../../views/RoomView/MessageActionStore'; import { getMessageById } from '../../../lib/database/services/Message'; import { generateTriggerId } from '../../../lib/methods/actions'; import { executeCommandPreview } from '../../../lib/services/restApi'; diff --git a/app/containers/MessageComposer/components/Quotes/Quotes.tsx b/app/containers/MessageComposer/components/Quotes/Quotes.tsx index 377d07f6d5e..3bde7fcdd9e 100644 --- a/app/containers/MessageComposer/components/Quotes/Quotes.tsx +++ b/app/containers/MessageComposer/components/Quotes/Quotes.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, type ReactElement } from 'react'; import { FlatList } from 'react-native'; import { Quote } from './Quote'; -import { useMessageAction, useSelectedMessages } from '../../../../views/RoomView/InteractionStore'; +import { useMessageAction, useSelectedMessages } from '../../../../views/RoomView/MessageActionStore'; export const Quotes = (): ReactElement | null => { 'use memo'; diff --git a/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts b/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts index 2f7c2c0c5f2..31ed36b7903 100644 --- a/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts +++ b/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts @@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef } from 'react'; import { saveDraftMessage } from '../../../lib/methods/draftMessage'; import { useRoomContext } from '../../../views/RoomView/context'; -import { useMessageAction, useSelectedMessages } from '../../../views/RoomView/InteractionStore'; +import { useMessageAction, useSelectedMessages } from '../../../views/RoomView/MessageActionStore'; import { useFocused } from '../context'; export const useAutoSaveDraft = (text = '') => { diff --git a/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx b/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx index cbd4be40d0a..4b821fe6c7b 100644 --- a/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx +++ b/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx @@ -18,7 +18,7 @@ jest.mock('../../../views/RoomView/context', () => ({ useRoomContext: jest.fn() })); -jest.mock('../../../views/RoomView/InteractionStore', () => ({ +jest.mock('../../../views/RoomView/MessageActionStore', () => ({ useMessageAction: jest.fn(), useSelectedMessages: jest.fn() })); @@ -51,8 +51,8 @@ const mockGetDocumentAsync = require('expo-document-picker').getDocumentAsync as const mockUseAppSelector = require('../../../lib/hooks/useAppSelector').useAppSelector as jest.Mock; const mockUseMessageComposerApi = require('../context').useMessageComposerApi as jest.Mock; const mockUseRoomContext = require('../../../views/RoomView/context').useRoomContext as jest.Mock; -const mockUseMessageAction = require('../../../views/RoomView/InteractionStore').useMessageAction as jest.Mock; -const mockUseSelectedMessages = require('../../../views/RoomView/InteractionStore').useSelectedMessages as jest.Mock; +const mockUseMessageAction = require('../../../views/RoomView/MessageActionStore').useMessageAction as jest.Mock; +const mockUseSelectedMessages = require('../../../views/RoomView/MessageActionStore').useSelectedMessages as jest.Mock; const mockUseAltTextSupported = require('../../../lib/hooks/useAltTextSupported').useAltTextSupported as jest.Mock; const mockGetSubscriptionByRoomId = require('../../../lib/database/services/Subscription').getSubscriptionByRoomId as jest.Mock; const mockGetThreadById = require('../../../lib/database/services/Thread').getThreadById as jest.Mock; diff --git a/app/containers/MessageComposer/hooks/useChooseMedia.ts b/app/containers/MessageComposer/hooks/useChooseMedia.ts index 068b3e0581e..e8349973222 100644 --- a/app/containers/MessageComposer/hooks/useChooseMedia.ts +++ b/app/containers/MessageComposer/hooks/useChooseMedia.ts @@ -10,7 +10,7 @@ import { getThreadById } from '../../../lib/database/services/Thread'; import Navigation from '../../../lib/navigation/appNavigation'; import { useAppSelector } from '../../../lib/hooks/useAppSelector'; import { useRoomContext } from '../../../views/RoomView/context'; -import { useMessageAction, useSelectedMessages } from '../../../views/RoomView/InteractionStore'; +import { useMessageAction, useSelectedMessages } from '../../../views/RoomView/MessageActionStore'; import { type IShareAttachment } from '../../../definitions'; import ImagePicker, { type ImageOrVideo } from '../../../lib/methods/helpers/ImagePicker/ImagePicker'; import { useMessageComposerApi } from '../context'; diff --git a/app/containers/message/__tests__/index.test.tsx b/app/containers/message/__tests__/index.test.tsx index f4d9664f016..06971fac9e7 100644 --- a/app/containers/message/__tests__/index.test.tsx +++ b/app/containers/message/__tests__/index.test.tsx @@ -3,7 +3,7 @@ import { fireEvent } from '@testing-library/react-native'; import MessageContainer from '../index'; import { type TAnyMessageModel } from '../../../definitions'; -import { createInteractionStore, InteractionStoreContext } from '../../../views/RoomView/InteractionStore'; +import { createMessageActionStore, MessageActionStoreContext } from '../../../views/RoomView/MessageActionStore'; import { renderWithMessageProviders } from '../index.testHelpers'; import { type MessageRoomState } from '../stores/MessageRoomStore'; @@ -93,30 +93,30 @@ it('fires the onLongPress callback when long-pressed', () => { expect(onLongPress).toHaveBeenCalledTimes(1); }); -describe('edit highlight reacts to InteractionStore', () => { +describe('edit highlight reacts to MessageActionStore', () => { it('shows editing testID when the store marks this message as being edited', () => { - const store = createInteractionStore({ kind: 'edit', messageId: 'msg-1' }); + const store = createMessageActionStore({ kind: 'edit', messageId: 'msg-1' }); const item = createMockMessage({ id: 'msg-1' }); const { getByTestId } = renderWithMessageProviders( - <InteractionStoreContext.Provider value={store}> + <MessageActionStoreContext.Provider value={store}> <MessageContainer item={item} {...baseProps} /> - </InteractionStoreContext.Provider>, + </MessageActionStoreContext.Provider>, { room } ); expect(getByTestId('message-editing-msg-1')).toBeTruthy(); }); - it('removes editing testID when the store resets', async () => { - const store = createInteractionStore({ kind: 'edit', messageId: 'msg-1' }); + it('removes editing testID when the store clears', async () => { + const store = createMessageActionStore({ kind: 'edit', messageId: 'msg-1' }); const item = createMockMessage({ id: 'msg-1' }); const { queryByTestId } = renderWithMessageProviders( - <InteractionStoreContext.Provider value={store}> + <MessageActionStoreContext.Provider value={store}> <MessageContainer item={item} {...baseProps} /> - </InteractionStoreContext.Provider>, + </MessageActionStoreContext.Provider>, { room } ); await act(() => { - store.getState().actions.reset(); + store.getState().actions.clear(); }); expect(queryByTestId('message-editing-msg-1')).toBeNull(); }); diff --git a/app/containers/message/components/Message.tsx b/app/containers/message/components/Message.tsx index 3e32931068f..395c62fcd73 100644 --- a/app/containers/message/components/Message.tsx +++ b/app/containers/message/components/Message.tsx @@ -8,7 +8,7 @@ import { useLastFocusedMessageRef } from '../../../lib/a11y/useLastFocusedMessag import { useMessageAccessibilityLabel } from '../hooks/useMessageAccessibilityLabel'; import { useMessageAccessibilityActions } from '../hooks/useMessageAccessibilityActions'; import { useMessageAccessibilityHint } from '../hooks/useMessageAccessibilityHint'; -import { useIsBeingEdited } from '../../../views/RoomView/InteractionStore'; +import { useIsBeingEdited } from '../../../views/RoomView/MessageActionStore'; import { useIsInfoMessage, useMessageField, diff --git a/app/containers/message/components/stories/Message.stories.tsx b/app/containers/message/components/stories/Message.stories.tsx index b287995fea9..8d6a12dd600 100644 --- a/app/containers/message/components/stories/Message.stories.tsx +++ b/app/containers/message/components/stories/Message.stories.tsx @@ -16,7 +16,7 @@ import { import { mockedStore as store } from '../../../../reducers/mockedStore'; import { updateSettings } from '../../../../actions/settings'; import { setCustomEmojis } from '../../../../actions/customEmojis'; -import { createInteractionStore, InteractionStoreContext } from '../../../../views/RoomView/InteractionStore'; +import { createMessageActionStore, MessageActionStoreContext } from '../../../../views/RoomView/MessageActionStore'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; const _theme = 'light'; @@ -2401,18 +2401,18 @@ export const Temp = () => <Message msg='Temp message' status={messagesStatus.TEM export const TempLargeFont = () => <MessageLargeFont msg='Temp message' status={messagesStatus.TEMP} isTemp />; -const editingStore = createInteractionStore({ kind: 'edit', messageId: 'editing-message' }); +const editingStore = createMessageActionStore({ kind: 'edit', messageId: 'editing-message' }); export const Editing = () => ( - <InteractionStoreContext.Provider value={editingStore}> + <MessageActionStoreContext.Provider value={editingStore}> <Message id='editing-message' msg='Message being edited' /> - </InteractionStoreContext.Provider> + </MessageActionStoreContext.Provider> ); export const EditingLargeFont = () => ( - <InteractionStoreContext.Provider value={editingStore}> + <MessageActionStoreContext.Provider value={editingStore}> <MessageLargeFont id='editing-message' msg='Message being edited' /> - </InteractionStoreContext.Provider> + </MessageActionStoreContext.Provider> ); export const SystemMessages = () => ( diff --git a/app/definitions/IMessage.ts b/app/definitions/IMessage.ts index 756228addec..1a6c6749f1e 100644 --- a/app/definitions/IMessage.ts +++ b/app/definitions/IMessage.ts @@ -2,13 +2,14 @@ import type Model from '@nozbe/watermelondb/Model'; import { type Root } from '@rocket.chat/message-parser'; import { type MessageTypeLoad } from '../lib/constants/messageTypeLoad'; +import { type TMessageActionState } from '../views/RoomView/MessageActionStore'; import { type IAttachment } from './IAttachment'; import { type IReaction } from './IReaction'; import { type TThreadMessageModel } from './IThreadMessage'; import { type TThreadModel } from './IThread'; import { type IUrl, type IUrlFromServer } from './IUrl'; -export type TMessageAction = 'quote' | 'edit' | 'react' | null; +export type TMessageAction = NonNullable<TMessageActionState>['kind'] | null; export type MessageType = | 'jitsi_call_started' diff --git a/app/views/RoomView/InteractionStore.test.tsx b/app/views/RoomView/InteractionStore.test.tsx deleted file mode 100644 index 96d24cdf715..00000000000 --- a/app/views/RoomView/InteractionStore.test.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { render } from '@testing-library/react-native'; - -import { - createInteractionStore, - InteractionProvider, - type TInteraction, - useIsBeingEdited, - useMessageAction, - useSelectedMessages -} from './InteractionStore'; - -describe('InteractionStore', () => { - describe('useIsBeingEdited', () => { - it('returns false with no provider (module fallback store) and does not throw', () => { - const spy = jest.fn(); - const Probe = () => { - spy(useIsBeingEdited('msg-1')); - return null; - }; - - expect(() => render(<Probe />)).not.toThrow(); - expect(spy).toHaveBeenLastCalledWith(false); - }); - - it('returns true only for the message being edited under a provider', () => { - const editedSpy = jest.fn(); - const otherSpy = jest.fn(); - const Probe = () => { - editedSpy(useIsBeingEdited('msg-1')); - otherSpy(useIsBeingEdited('msg-2')); - return null; - }; - - render( - <InteractionProvider initialState={{ kind: 'edit', messageId: 'msg-1' }}> - <Probe /> - </InteractionProvider> - ); - - expect(editedSpy).toHaveBeenLastCalledWith(true); - expect(otherSpy).toHaveBeenLastCalledWith(false); - }); - }); - - describe('action hooks without a provider', () => { - let consoleErrorSpy: jest.SpyInstance; - - beforeEach(() => { - consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { - consoleErrorSpy.mockRestore(); - }); - - it('useMessageAction throws', () => { - const Probe = () => { - useMessageAction(); - return null; - }; - expect(() => render(<Probe />)).toThrow('Interaction hooks must be used within an InteractionProvider'); - }); - - it('useSelectedMessages throws', () => { - const Probe = () => { - useSelectedMessages(); - return null; - }; - expect(() => render(<Probe />)).toThrow('Interaction hooks must be used within an InteractionProvider'); - }); - }); - - describe('action creators (union transitions)', () => { - it('setEditing sets an edit interaction with the given messageId', () => { - const store = createInteractionStore(); - store.getState().actions.setEditing('msg-1'); - expect(store.getState().interaction).toEqual({ kind: 'edit', messageId: 'msg-1' }); - }); - - it('initQuote starts a quote interaction with a single messageId', () => { - const store = createInteractionStore(); - store.getState().actions.initQuote('msg-1'); - expect(store.getState().interaction).toEqual({ kind: 'quote', messageIds: ['msg-1'] }); - }); - - it('appendQuote appends to an existing quote interaction', () => { - const store = createInteractionStore(); - store.getState().actions.initQuote('msg-1'); - store.getState().actions.appendQuote('msg-2'); - expect(store.getState().interaction).toEqual({ kind: 'quote', messageIds: ['msg-1', 'msg-2'] }); - }); - - it('appendQuote starts a new quote interaction when there is none', () => { - const store = createInteractionStore(); - store.getState().actions.appendQuote('msg-1'); - expect(store.getState().interaction).toEqual({ kind: 'quote', messageIds: ['msg-1'] }); - }); - - it('removeQuote drops a messageId and keeps the quote interaction if others remain', () => { - const store = createInteractionStore({ kind: 'quote', messageIds: ['msg-1', 'msg-2'] }); - store.getState().actions.removeQuote('msg-1'); - expect(store.getState().interaction).toEqual({ kind: 'quote', messageIds: ['msg-2'] }); - }); - - it('removeQuote clears the interaction when the last quoted message is removed', () => { - const store = createInteractionStore({ kind: 'quote', messageIds: ['msg-1'] }); - store.getState().actions.removeQuote('msg-1'); - expect(store.getState().interaction).toBeNull(); - }); - - it('removeQuote is a no-op when the current interaction is not a quote', () => { - const store = createInteractionStore({ kind: 'edit', messageId: 'msg-1' }); - store.getState().actions.removeQuote('msg-1'); - expect(store.getState().interaction).toEqual({ kind: 'edit', messageId: 'msg-1' }); - }); - - it('setReacting sets a react interaction with the given messageId', () => { - const store = createInteractionStore(); - store.getState().actions.setReacting('msg-1'); - expect(store.getState().interaction).toEqual({ kind: 'react', messageId: 'msg-1' }); - }); - - it('setQuotes sets a quote interaction with the given ids, or clears it when empty', () => { - const store = createInteractionStore(); - store.getState().actions.setQuotes(['msg-1', 'msg-2']); - expect(store.getState().interaction).toEqual({ kind: 'quote', messageIds: ['msg-1', 'msg-2'] }); - - store.getState().actions.setQuotes([]); - expect(store.getState().interaction).toBeNull(); - }); - - it('reset clears any interaction', () => { - const store = createInteractionStore({ kind: 'react', messageId: 'msg-1' }); - store.getState().actions.reset(); - expect(store.getState().interaction).toBeNull(); - }); - - it('does not allow representing invalid combinations at the type level', () => { - // @ts-expect-error edit requires a single messageId, not messageIds - const invalidEdit: TInteraction = { kind: 'edit', messageIds: ['msg-1'] }; - // @ts-expect-error quote requires messageIds, not messageId - const invalidQuote: TInteraction = { kind: 'quote', messageId: 'msg-1' }; - expect(invalidEdit).toBeDefined(); - expect(invalidQuote).toBeDefined(); - }); - }); -}); diff --git a/app/views/RoomView/InteractionStore.tsx b/app/views/RoomView/InteractionStore.tsx deleted file mode 100644 index 15b82d83955..00000000000 --- a/app/views/RoomView/InteractionStore.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { createContext, type ReactElement, useContext, useState } from 'react'; -import { createStore, useStore } from 'zustand'; -import { useShallow } from 'zustand/react/shallow'; - -import { type TMessageAction } from '../../definitions'; - -export type TInteraction = - | { kind: 'edit'; messageId: string } - | { kind: 'quote'; messageIds: string[] } - | { kind: 'react'; messageId: string } - | null; - -type TInteractionActions = { - setEditing(messageId: string): void; - initQuote(messageId: string): void; - appendQuote(messageId: string): void; - removeQuote(messageId: string): void; - setReacting(messageId: string): void; - setQuotes(messageIds: string[]): void; - reset(): void; -}; - -type InteractionState = { - interaction: TInteraction; - // Built once inside the store initializer — stable ref for consumers. - actions: TInteractionActions; -}; - -export const createInteractionStore = (initialInteraction?: TInteraction) => - createStore<InteractionState>()(set => ({ - interaction: initialInteraction ?? null, - actions: { - setEditing: messageId => set({ interaction: { kind: 'edit', messageId } }), - initQuote: messageId => set({ interaction: { kind: 'quote', messageIds: [messageId] } }), - appendQuote: messageId => - set(state => { - if (state.interaction?.kind !== 'quote') { - return { interaction: { kind: 'quote', messageIds: [messageId] } }; - } - if (state.interaction.messageIds.includes(messageId)) { - return {}; - } - return { interaction: { kind: 'quote', messageIds: [...state.interaction.messageIds, messageId] } }; - }), - removeQuote: messageId => - set(state => { - if (state.interaction?.kind !== 'quote') { - return {}; - } - const messageIds = state.interaction.messageIds.filter(m => m !== messageId); - return { interaction: messageIds.length ? { kind: 'quote', messageIds } : null }; - }), - setReacting: messageId => set({ interaction: { kind: 'react', messageId } }), - setQuotes: messageIds => set({ interaction: messageIds.length ? { kind: 'quote', messageIds } : null }), - reset: () => set({ interaction: null }) - } - })); - -export type InteractionStore = ReturnType<typeof createInteractionStore>; - -export const InteractionStoreContext = createContext<InteractionStore | null>(null); - -// Rows rendered outside a RoomView (search, pinned) can never be in edit mode. -// Using a module-level fallback avoids a conditional hook call in useIsBeingEdited. -const fallbackStore = createInteractionStore(); - -const useInteractionStore = <T,>(selector: (state: InteractionState) => T): T => { - const store = useContext(InteractionStoreContext); - if (!store) { - throw new Error('Interaction hooks must be used within an InteractionProvider'); - } - return useStore(store, selector); -}; - -export const useMessageAction = (): TMessageAction => useInteractionStore(s => s.interaction?.kind ?? null); - -export const useSelectedMessages = (): string[] => - useInteractionStore( - useShallow(s => { - const { interaction } = s; - if (!interaction) { - return []; - } - return interaction.kind === 'quote' ? interaction.messageIds : [interaction.messageId]; - }) - ); - -export const useIsBeingEdited = (messageId: string): boolean => { - const store = useContext(InteractionStoreContext) ?? fallbackStore; - return useStore(store, s => s.interaction?.kind === 'edit' && s.interaction.messageId === messageId); -}; - -export const InteractionProvider = ({ - initialState, - children -}: { - initialState?: TInteraction; - children: ReactElement; -}): ReactElement => { - 'use memo'; - - const [store] = useState(() => createInteractionStore(initialState)); - return <InteractionStoreContext.Provider value={store}>{children}</InteractionStoreContext.Provider>; -}; diff --git a/app/views/RoomView/MessageActionStore.test.tsx b/app/views/RoomView/MessageActionStore.test.tsx new file mode 100644 index 00000000000..328fbb8d76d --- /dev/null +++ b/app/views/RoomView/MessageActionStore.test.tsx @@ -0,0 +1,137 @@ +import { render } from '@testing-library/react-native'; + +import { + createMessageActionStore, + MessageActionProvider, + useIsBeingEdited, + useMessageAction, + useSelectedMessages +} from './MessageActionStore'; + +describe('MessageActionStore', () => { + describe('useIsBeingEdited', () => { + it('returns false with no provider (module fallback store) and does not throw', () => { + const spy = jest.fn(); + const Probe = () => { + spy(useIsBeingEdited('msg-1')); + return null; + }; + + expect(() => render(<Probe />)).not.toThrow(); + expect(spy).toHaveBeenLastCalledWith(false); + }); + + it('returns true only for the message being edited under a provider', () => { + const editedSpy = jest.fn(); + const otherSpy = jest.fn(); + const Probe = () => { + editedSpy(useIsBeingEdited('msg-1')); + otherSpy(useIsBeingEdited('msg-2')); + return null; + }; + + render( + <MessageActionProvider initialAction={{ kind: 'edit', messageId: 'msg-1' }}> + <Probe /> + </MessageActionProvider> + ); + + expect(editedSpy).toHaveBeenLastCalledWith(true); + expect(otherSpy).toHaveBeenLastCalledWith(false); + }); + }); + + describe('action hooks without a provider', () => { + let consoleErrorSpy: jest.SpyInstance; + + beforeEach(() => { + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + }); + + it('useMessageAction throws', () => { + const Probe = () => { + useMessageAction(); + return null; + }; + expect(() => render(<Probe />)).toThrow('Interaction hooks must be used within a MessageActionProvider'); + }); + + it('useSelectedMessages throws', () => { + const Probe = () => { + useSelectedMessages(); + return null; + }; + expect(() => render(<Probe />)).toThrow('Interaction hooks must be used within a MessageActionProvider'); + }); + }); + + describe('action creators (union transitions)', () => { + it('startEditing sets an edit action with the given messageId', () => { + const store = createMessageActionStore(); + store.getState().actions.startEditing('msg-1'); + expect(store.getState().action).toEqual({ kind: 'edit', messageId: 'msg-1' }); + }); + + it('startQuote starts a quote action with a single messageId', () => { + const store = createMessageActionStore(); + store.getState().actions.startQuote('msg-1'); + expect(store.getState().action).toEqual({ kind: 'quote', messageIds: ['msg-1'] }); + }); + + it('addQuote appends to an existing quote action', () => { + const store = createMessageActionStore(); + store.getState().actions.startQuote('msg-1'); + store.getState().actions.addQuote('msg-2'); + expect(store.getState().action).toEqual({ kind: 'quote', messageIds: ['msg-1', 'msg-2'] }); + }); + + it('addQuote starts a new quote action when there is none', () => { + const store = createMessageActionStore(); + store.getState().actions.addQuote('msg-1'); + expect(store.getState().action).toEqual({ kind: 'quote', messageIds: ['msg-1'] }); + }); + + it('removeQuote drops a messageId and keeps the quote action if others remain', () => { + const store = createMessageActionStore({ kind: 'quote', messageIds: ['msg-1', 'msg-2'] }); + store.getState().actions.removeQuote('msg-1'); + expect(store.getState().action).toEqual({ kind: 'quote', messageIds: ['msg-2'] }); + }); + + it('removeQuote clears the action when the last quoted message is removed', () => { + const store = createMessageActionStore({ kind: 'quote', messageIds: ['msg-1'] }); + store.getState().actions.removeQuote('msg-1'); + expect(store.getState().action).toBeNull(); + }); + + it('removeQuote is a no-op when the current action is not a quote', () => { + const store = createMessageActionStore({ kind: 'edit', messageId: 'msg-1' }); + store.getState().actions.removeQuote('msg-1'); + expect(store.getState().action).toEqual({ kind: 'edit', messageId: 'msg-1' }); + }); + + it('startReacting sets a react action with the given messageId', () => { + const store = createMessageActionStore(); + store.getState().actions.startReacting('msg-1'); + expect(store.getState().action).toEqual({ kind: 'react', messageId: 'msg-1' }); + }); + + it('setQuoteMessageIds sets a quote action with the given ids, or clears it when empty', () => { + const store = createMessageActionStore(); + store.getState().actions.setQuoteMessageIds(['msg-1', 'msg-2']); + expect(store.getState().action).toEqual({ kind: 'quote', messageIds: ['msg-1', 'msg-2'] }); + + store.getState().actions.setQuoteMessageIds([]); + expect(store.getState().action).toBeNull(); + }); + + it('clear resets any action', () => { + const store = createMessageActionStore({ kind: 'react', messageId: 'msg-1' }); + store.getState().actions.clear(); + expect(store.getState().action).toBeNull(); + }); + }); +}); diff --git a/app/views/RoomView/MessageActionStore.tsx b/app/views/RoomView/MessageActionStore.tsx new file mode 100644 index 00000000000..b47209e52b0 --- /dev/null +++ b/app/views/RoomView/MessageActionStore.tsx @@ -0,0 +1,103 @@ +import { createContext, type ReactNode, useContext, useState } from 'react'; +import { createStore, useStore } from 'zustand'; +import { useShallow } from 'zustand/react/shallow'; + +export type TMessageActionState = + | { kind: 'edit'; messageId: string } + | { kind: 'quote'; messageIds: string[] } + | { kind: 'react'; messageId: string } + | null; + +type TMessageActionActions = { + startEditing(messageId: string): void; + startQuote(messageId: string): void; + addQuote(messageId: string): void; + removeQuote(messageId: string): void; + startReacting(messageId: string): void; + setQuoteMessageIds(messageIds: string[]): void; + clear(): void; +}; + +type MessageActionState = { + action: TMessageActionState; + // Built once inside the store initializer — stable ref for consumers. + actions: TMessageActionActions; +}; + +export const createMessageActionStore = (initialAction?: TMessageActionState) => + createStore<MessageActionState>()(set => ({ + action: initialAction ?? null, + actions: { + startEditing: messageId => set({ action: { kind: 'edit', messageId } }), + startQuote: messageId => set({ action: { kind: 'quote', messageIds: [messageId] } }), + addQuote: messageId => + set(state => { + if (state.action?.kind !== 'quote') { + return { action: { kind: 'quote', messageIds: [messageId] } }; + } + if (state.action.messageIds.includes(messageId)) { + return {}; + } + return { action: { kind: 'quote', messageIds: [...state.action.messageIds, messageId] } }; + }), + removeQuote: messageId => + set(state => { + if (state.action?.kind !== 'quote') { + return {}; + } + const messageIds = state.action.messageIds.filter(m => m !== messageId); + return { action: messageIds.length ? { kind: 'quote', messageIds } : null }; + }), + startReacting: messageId => set({ action: { kind: 'react', messageId } }), + setQuoteMessageIds: messageIds => set({ action: messageIds.length ? { kind: 'quote', messageIds } : null }), + clear: () => set({ action: null }) + } + })); + +export type TMessageActionStore = ReturnType<typeof createMessageActionStore>; + +export const MessageActionStoreContext = createContext<TMessageActionStore | null>(null); + +// Rows rendered outside a RoomView (search, pinned) can never be in edit mode. +// Using a module-level fallback avoids a conditional hook call in useIsBeingEdited. +const fallbackStore = createMessageActionStore(); + +const useMessageActionStore = <T,>(selector: (state: MessageActionState) => T): T => { + const store = useContext(MessageActionStoreContext); + if (!store) { + throw new Error('Interaction hooks must be used within a MessageActionProvider'); + } + return useStore(store, selector); +}; + +export const useMessageAction = (): NonNullable<TMessageActionState>['kind'] | null => + useMessageActionStore(s => s.action?.kind ?? null); + +export const useSelectedMessages = (): string[] => + useMessageActionStore( + useShallow(s => { + const { action } = s; + if (!action) { + return []; + } + return action.kind === 'quote' ? action.messageIds : [action.messageId]; + }) + ); + +export const useIsBeingEdited = (messageId: string): boolean => { + const store = useContext(MessageActionStoreContext) ?? fallbackStore; + return useStore(store, s => s.action?.kind === 'edit' && s.action.messageId === messageId); +}; + +export const MessageActionProvider = ({ + initialAction, + children +}: { + initialAction?: TMessageActionState; + children: ReactNode; +}): ReactNode => { + 'use memo'; + + const [store] = useState(() => createMessageActionStore(initialAction)); + return <MessageActionStoreContext.Provider value={store}>{children}</MessageActionStoreContext.Provider>; +}; diff --git a/app/views/RoomView/RoomProviders.test.tsx b/app/views/RoomView/RoomProviders.test.tsx index c2f714fbd0d..a0be5dad78a 100644 --- a/app/views/RoomView/RoomProviders.test.tsx +++ b/app/views/RoomView/RoomProviders.test.tsx @@ -2,11 +2,11 @@ import { render } from '@testing-library/react-native'; import { RoomProviders } from './RoomProviders'; import { useRoomContext, type IRoomContext } from './context'; -import { createInteractionStore } from './InteractionStore'; +import { createMessageActionStore } from './MessageActionStore'; describe('RoomProviders', () => { it('keeps the same RoomContext value reference across re-renders with unchanged props', () => { - const store = createInteractionStore(); + const store = createMessageActionStore(); const room = { rid: 'rid-1' }; const values: IRoomContext[] = []; @@ -29,7 +29,7 @@ describe('RoomProviders', () => { }); it('produces a new RoomContext value reference when a prop changes', () => { - const store = createInteractionStore(); + const store = createMessageActionStore(); const values: IRoomContext[] = []; const Probe = () => { diff --git a/app/views/RoomView/RoomProviders.tsx b/app/views/RoomView/RoomProviders.tsx index e51d16eec04..9e284b6a89a 100644 --- a/app/views/RoomView/RoomProviders.tsx +++ b/app/views/RoomView/RoomProviders.tsx @@ -1,10 +1,10 @@ import { type ReactElement } from 'react'; import { RoomContext, type IRoomContext } from './context'; -import { type InteractionStore, InteractionStoreContext } from './InteractionStore'; +import { type TMessageActionStore, MessageActionStoreContext } from './MessageActionStore'; type IRoomProvidersProps = IRoomContext & { - store: InteractionStore; + store: TMessageActionStore; children: ReactElement; }; @@ -28,7 +28,7 @@ export const RoomProviders = ({ 'use memo'; return ( - <InteractionStoreContext.Provider value={store}> + <MessageActionStoreContext.Provider value={store}> <RoomContext.Provider value={{ rid, @@ -47,6 +47,6 @@ export const RoomProviders = ({ }}> {children} </RoomContext.Provider> - </InteractionStoreContext.Provider> + </MessageActionStoreContext.Provider> ); }; diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 74d391cd254..4a133b176a3 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -96,7 +96,7 @@ import { import { withActionSheet } from '../../containers/ActionSheet'; import { goRoom, type TGoRoomItem } from '../../lib/methods/helpers/goRoom'; import { ComposerAttachments, type IMessageComposerRef, MessageComposerContainer } from '../../containers/MessageComposer'; -import { createInteractionStore, type InteractionStore } from './InteractionStore'; +import { createMessageActionStore, type TMessageActionStore } from './MessageActionStore'; import { RoomProviders } from './RoomProviders'; import { MessageRoomProvider } from '../../containers/message/stores/MessageRoomStore'; import AudioManager from '../../lib/methods/AudioManager'; @@ -145,7 +145,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { }; private sub?: RoomClass; private unsubscribeBlur?: () => void; - private interactionStore: InteractionStore; + private messageActionStore: TMessageActionStore; constructor(props: IRoomViewProps) { super(props); @@ -171,7 +171,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { this.jumpToThreadId = props.route.params?.jumpToThreadId; const roomUserId = props.route.params?.roomUserId ?? getUidDirectMessage(room); const { messageId } = props.route.params ?? {}; - this.interactionStore = createInteractionStore(messageId ? { kind: 'quote', messageIds: [messageId] } : null); + this.messageActionStore = createMessageActionStore(messageId ? { kind: 'quote', messageIds: [messageId] } : null); this.state = { joined: true, room, @@ -222,7 +222,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { componentDidMount() { const { navigation, dispatch } = this.props; - const { interaction } = this.interactionStore.getState(); + const { action } = this.messageActionStore.getState(); dispatch(clearInAppFeedback()); this.mounted = true; this.didMountInteraction = InteractionManager.runAfterInteractions(() => { @@ -252,8 +252,8 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { if (isIOS && this.rid) { this.updateUnreadCount(); } - if (interaction?.kind === 'quote' && interaction.messageIds.length === 1) { - this.onQuoteInit(interaction.messageIds[0]); + if (action?.kind === 'quote' && action.messageIds.length === 1) { + this.onQuoteInit(action.messageIds[0]); } }); EventEmitter.addEventListener('ROOM_REMOVED', this.handleRoomRemoved); @@ -794,11 +794,11 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { }; onEditInit = (messageId: string) => { - const { interaction, actions } = this.interactionStore.getState(); - if (interaction) { + const { action, actions } = this.messageActionStore.getState(); + if (action) { return; } - actions.setEditing(messageId); + actions.startEditing(messageId); }; onEditCancel = () => { @@ -840,31 +840,31 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { }; onQuoteInit = (messageId: string) => { - const { interaction, actions } = this.interactionStore.getState(); - if (interaction?.kind === 'quote') { - if (!interaction.messageIds.includes(messageId)) { - actions.appendQuote(messageId); + const { action, actions } = this.messageActionStore.getState(); + if (action?.kind === 'quote') { + if (!action.messageIds.includes(messageId)) { + actions.addQuote(messageId); } return; } - if (interaction) { + if (action) { return; } - actions.initQuote(messageId); + actions.startQuote(messageId); }; onRemoveQuoteMessage = (messageId: string) => { - this.interactionStore.getState().actions.removeQuote(messageId); + this.messageActionStore.getState().actions.removeQuote(messageId); }; resetAction = () => { - this.interactionStore.getState().actions.reset(); + this.messageActionStore.getState().actions.clear(); }; showReactionPicker = () => { const { showActionSheet } = this.props; - const { interaction } = this.interactionStore.getState(); - const messageId = interaction?.kind === 'react' ? interaction.messageId : undefined; + const { action } = this.messageActionStore.getState(); + const messageId = action?.kind === 'react' ? action.messageId : undefined; setTimeout(() => { showActionSheet({ children: ( @@ -879,11 +879,11 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { }; onReactionInit = (messageId: string) => { - if (this.interactionStore.getState().interaction) { + if (this.messageActionStore.getState().action) { return; } this.handleCloseEmoji(() => { - this.interactionStore.getState().actions.setReacting(messageId); + this.messageActionStore.getState().actions.startReacting(messageId); this.showReactionPicker(); }); }; @@ -895,8 +895,8 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { }; onMessageLongPress = (message: TAnyMessageModel) => { - const { interaction } = this.interactionStore.getState(); - if (interaction && interaction.kind !== 'quote') { + const { action } = this.messageActionStore.getState(); + if (action && action.kind !== 'quote') { return; } // if it's a thread message on main room, we disable the long press @@ -1379,7 +1379,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { }; setQuotesAndText = (text: string, quotes: string[]) => { - this.interactionStore.getState().actions.setQuotes(quotes); + this.messageActionStore.getState().actions.setQuoteMessageIds(quotes); this.messageComposerRef.current?.setInput(text || ''); }; @@ -1630,7 +1630,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { return ( <RoomProviders - store={this.interactionStore} + store={this.messageActionStore} rid={rid} t={t} room={room} diff --git a/app/views/ShareView/ShareView.test.tsx b/app/views/ShareView/ShareView.test.tsx index 8e8a6561df4..ece9cf482cd 100644 --- a/app/views/ShareView/ShareView.test.tsx +++ b/app/views/ShareView/ShareView.test.tsx @@ -177,7 +177,7 @@ describe('ShareView', () => { spy.mockRestore(); }); - it('send() builds msg from prepareQuoteMessage using the interaction store quote ids', async () => { + it('send() builds msg from prepareQuoteMessage using the message action store quote ids', async () => { const shareView = makeInstance({ mime: 'image/jpeg', serverVersion: '8.3.0', @@ -191,7 +191,7 @@ describe('ShareView', () => { }; shareView.saveSelectedDescription = jest.fn() as any; - shareView.interactionStore.getState().actions.setQuotes(['msg-1']); + shareView.messageActionStore.getState().actions.setQuoteMessageIds(['msg-1']); const prepareQuoteMessageMod = require('../../containers/MessageComposer/helpers/prepareQuoteMessage'); const prepareSpy = jest.spyOn(prepareQuoteMessageMod, 'prepareQuoteMessage').mockResolvedValue('quoted-text'); diff --git a/app/views/ShareView/index.tsx b/app/views/ShareView/index.tsx index b3bb50dc709..0c92b3ee7d0 100644 --- a/app/views/ShareView/index.tsx +++ b/app/views/ShareView/index.tsx @@ -38,7 +38,7 @@ import { sendAttachments } from '../../lib/methods/sendFileMessage/sendAttachmen import { sendMessage } from '../../lib/methods/sendMessage'; import { hasPermission, isAndroid, canUploadFile, isReadOnly, isBlocked } from '../../lib/methods/helpers'; import { RoomProviders } from '../RoomView/RoomProviders'; -import { createInteractionStore, type InteractionStore } from '../RoomView/InteractionStore'; +import { createMessageActionStore, type TMessageActionStore } from '../RoomView/MessageActionStore'; import { appStart } from '../../actions/app'; interface IShareViewState { @@ -78,7 +78,7 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { private serverInfo: TShareServerInfo; private finishShareView: (text?: string, selectedMessages?: string[]) => void; private sentMessage: boolean; - private interactionStore: InteractionStore; + private messageActionStore: TMessageActionStore; constructor(props: IShareViewProps) { super(props); @@ -88,8 +88,8 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { this.serverInfo = props.route.params?.serverInfo ?? {}; this.finishShareView = props.route.params?.finishShareView; this.sentMessage = false; - // ShareView only ever uses the quote flow; real ids arrive later via startShareView -> setQuotes. - this.interactionStore = createInteractionStore(); + // ShareView only ever uses the quote flow; real ids arrive later via startShareView -> setQuoteMessageIds. + this.messageActionStore = createMessageActionStore(); this.state = { selected: {} as IShareAttachment, @@ -120,10 +120,10 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { } }; - // ShareView's interaction store only ever holds the quote flow. + // ShareView's message action store only ever holds the quote flow. getSelectedMessageIds = (): string[] => { - const { interaction } = this.interactionStore.getState(); - return interaction?.kind === 'quote' ? interaction.messageIds : []; + const { action } = this.messageActionStore.getState(); + return action?.kind === 'quote' ? action.messageIds : []; }; getThreadId = (thread: TThreadModel | string | undefined) => { @@ -239,7 +239,7 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { // Synchronization needed for Fabric to work await new Promise(resolve => setTimeout(resolve, 100)); this.messageComposerRef.current?.setInput(text); - this.interactionStore.getState().actions.setQuotes(selectedMessages); + this.messageActionStore.getState().actions.setQuoteMessageIds(selectedMessages); } }; @@ -266,9 +266,9 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { } let msg: string | undefined; - const { interaction } = this.interactionStore.getState(); - if (interaction?.kind === 'quote') { - msg = await prepareQuoteMessage('', interaction.messageIds); + const { action } = this.messageActionStore.getState(); + if (action?.kind === 'quote') { + msg = await prepareQuoteMessage('', action.messageIds); } const { isAltTextSupported } = this; @@ -384,7 +384,7 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { onRemoveQuoteMessage = (messageId: string) => { const newSelectedMessages = this.getSelectedMessageIds().filter(item => item !== messageId); - this.interactionStore.getState().actions.setQuotes(newSelectedMessages); + this.messageActionStore.getState().actions.setQuoteMessageIds(newSelectedMessages); }; renderContent = () => { @@ -394,7 +394,7 @@ class ShareView extends Component<IShareViewProps, IShareViewState> { if (attachments.length) { return ( <RoomProviders - store={this.interactionStore} + store={this.messageActionStore} rid={room.rid} t={room.t} room={room} From 01ad242da2012f71719b6daf9ce55c02c9c3c1af Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 13:03:40 -0300 Subject: [PATCH 116/144] refactor: collapse message-action hooks into one useMessageAction() (NATIVE-22) --- .../MessageComposer/MessageComposer.tsx | 17 +++-- .../MessageComposer/components/CancelEdit.tsx | 2 +- .../components/ComposerInput.tsx | 21 +++--- .../components/Quotes/Quotes.tsx | 6 +- .../MessageComposer/hooks/useAutoSaveDraft.ts | 13 ++-- .../hooks/useChooseMedia.test.tsx | 5 +- .../MessageComposer/hooks/useChooseMedia.ts | 11 ++- .../RoomView/MessageActionStore.test.tsx | 72 ++++++++++++++++--- app/views/RoomView/MessageActionStore.tsx | 16 +---- 9 files changed, 102 insertions(+), 61 deletions(-) diff --git a/app/containers/MessageComposer/MessageComposer.tsx b/app/containers/MessageComposer/MessageComposer.tsx index 171b980244c..bc91c0b6489 100644 --- a/app/containers/MessageComposer/MessageComposer.tsx +++ b/app/containers/MessageComposer/MessageComposer.tsx @@ -5,7 +5,7 @@ import { Q } from '@nozbe/watermelondb'; import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; import { useRoomContext } from '../../views/RoomView/context'; -import { useMessageAction, useSelectedMessages } from '../../views/RoomView/MessageActionStore'; +import { useMessageAction } from '../../views/RoomView/MessageActionStore'; import { Autocomplete } from './components'; import { MIN_HEIGHT } from './constants'; import { @@ -57,7 +57,6 @@ export const MessageComposer = ({ useCloseKeyboardWhenOrientationChanges(); const { rid, tmid, sharing, editRequest, onSendMessage, setQuotesAndText } = useRoomContext(); const action = useMessageAction(); - const selectedMessages = useSelectedMessages(); const alsoSendThreadToChannel = useAlsoSendThreadToChannel(); const { showEmojiKeyboard, showEmojiSearchbar, openEmojiSearchbar, resetKeyboard, keyboardHeight } = useEmojiKeyboard(); const { setAlsoSendThreadToChannel, setAutocompleteParams, clearAttachments } = useMessageComposerApi(); @@ -111,13 +110,13 @@ export const MessageComposer = ({ const textFromInput = composerInputComponentRef.current.getTextAndClear(); - if (action === 'edit') { + if (action?.kind === 'edit') { const updatedAttachments = attachments.length ? attachments.map(({ description, altText, fileId, filename }) => altTextSupported ? { description: altText || '', fileId, filename } : { description: description || '' } ) : undefined; - editRequest?.({ id: selectedMessages[0], msg: textFromInput, rid, attachments: updatedAttachments }); + editRequest?.({ id: action.messageId, msg: textFromInput, rid, attachments: updatedAttachments }); clearAttachments(); return; } @@ -125,8 +124,8 @@ export const MessageComposer = ({ if (attachments.length) { let quotedMessage: string | undefined; - if (action === 'quote') { - quotedMessage = await prepareQuoteMessage(textFromInput, selectedMessages); + if (action?.kind === 'quote') { + quotedMessage = await prepareQuoteMessage(textFromInput, action.messageIds); } try { @@ -149,8 +148,8 @@ export const MessageComposer = ({ } } - if (action === 'quote') { - const quoteMessage = await prepareQuoteMessage(textFromInput, selectedMessages); + if (action?.kind === 'quote') { + const quoteMessage = await prepareQuoteMessage(textFromInput, action.messageIds); onSendMessage?.(quoteMessage); return; } @@ -248,7 +247,7 @@ export const MessageComposer = ({ }}> <MessageComposerContent recordingAudio={recordingAudio} - action={action} + action={action?.kind ?? null} showEmojiSearchbar={showEmojiSearchbar} composerInputComponentRef={composerInputComponentRef} composerInputRef={composerInputRef} diff --git a/app/containers/MessageComposer/components/CancelEdit.tsx b/app/containers/MessageComposer/components/CancelEdit.tsx index a643096da99..4cbf75b312d 100644 --- a/app/containers/MessageComposer/components/CancelEdit.tsx +++ b/app/containers/MessageComposer/components/CancelEdit.tsx @@ -9,7 +9,7 @@ export const CancelEdit = () => { const { editCancel } = useRoomContext(); const action = useMessageAction(); - if (action !== 'edit') { + if (action?.kind !== 'edit') { return null; } return ( diff --git a/app/containers/MessageComposer/components/ComposerInput.tsx b/app/containers/MessageComposer/components/ComposerInput.tsx index 85431d80b85..cf8279794c0 100644 --- a/app/containers/MessageComposer/components/ComposerInput.tsx +++ b/app/containers/MessageComposer/components/ComposerInput.tsx @@ -33,7 +33,7 @@ import database from '../../../lib/database'; import Navigation from '../../../lib/navigation/appNavigation'; import { emitter } from '../../../lib/methods/helpers/emitter'; import { useRoomContext } from '../../../views/RoomView/context'; -import { useMessageAction, useSelectedMessages } from '../../../views/RoomView/MessageActionStore'; +import { useMessageAction } from '../../../views/RoomView/MessageActionStore'; import { getMessageById } from '../../../lib/database/services/Message'; import { generateTriggerId } from '../../../lib/methods/actions'; import { executeCommandPreview } from '../../../lib/services/restApi'; @@ -53,7 +53,6 @@ export const ComposerInput = memo( const { colors, theme } = useTheme(); const { rid, tmid, sharing, setQuotesAndText, room } = useRoomContext(); const action = useMessageAction(); - const selectedMessages = useSelectedMessages(); const focused = useFocused(); const { setFocused, setMicOrSend, setAutocompleteParams } = useMessageComposerApi(); const autocompleteType = useAutocompleteParams()?.type; @@ -95,18 +94,18 @@ export const ComposerInput = memo( } }; - if (action !== 'edit' && firstRender.current) { + if (action?.kind !== 'edit' && firstRender.current) { firstRender.current = false; setDraftMessage(); } if (sharing) return; if (usedCannedResponse) setInput(usedCannedResponse); - }, [action, rid, tmid, usedCannedResponse]); + }, [action?.kind, rid, tmid, usedCannedResponse]); // Edit/quote useEffect(() => { - const fetchMessageAndSetInput = async () => { - const message = await getMessageById(selectedMessages[0]); + const fetchMessageAndSetInput = async (messageId: string) => { + const message = await getMessageById(messageId); if (message) { setInput(message?.msg || (altTextSupported ? '' : message?.attachments?.[0]?.description || '')); } @@ -114,19 +113,19 @@ export const ComposerInput = memo( if (sharing) return; - if (prevAction === 'edit' && action !== 'edit') { + if (prevAction?.kind === 'edit' && action?.kind !== 'edit') { setInput(''); return; } - if (action === 'edit' && selectedMessages[0]) { + if (action?.kind === 'edit') { focus(); - fetchMessageAndSetInput(); + fetchMessageAndSetInput(action.messageId); return; } - if (action === 'quote' && selectedMessages.length) { + if (action?.kind === 'quote' && action.messageIds.length) { focus(); } - }, [action, selectedMessages]); + }, [action]); useFocusEffect( useCallback(() => { diff --git a/app/containers/MessageComposer/components/Quotes/Quotes.tsx b/app/containers/MessageComposer/components/Quotes/Quotes.tsx index 3bde7fcdd9e..1011b46e54d 100644 --- a/app/containers/MessageComposer/components/Quotes/Quotes.tsx +++ b/app/containers/MessageComposer/components/Quotes/Quotes.tsx @@ -2,13 +2,13 @@ import { useEffect, useRef, type ReactElement } from 'react'; import { FlatList } from 'react-native'; import { Quote } from './Quote'; -import { useMessageAction, useSelectedMessages } from '../../../../views/RoomView/MessageActionStore'; +import { useMessageAction } from '../../../../views/RoomView/MessageActionStore'; export const Quotes = (): ReactElement | null => { 'use memo'; - const selectedMessages = useSelectedMessages(); const action = useMessageAction(); + const selectedMessages = action?.kind === 'quote' ? action.messageIds : []; const nQuotesRef = useRef(0); const listRef = useRef<FlatList>(null); @@ -21,7 +21,7 @@ export const Quotes = (): ReactElement | null => { nQuotesRef.current = selectedMessages.length; }, [selectedMessages.length]); - if (action !== 'quote') { + if (action?.kind !== 'quote') { return null; } diff --git a/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts b/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts index 31ed36b7903..6e0399e2497 100644 --- a/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts +++ b/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts @@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef } from 'react'; import { saveDraftMessage } from '../../../lib/methods/draftMessage'; import { useRoomContext } from '../../../views/RoomView/context'; -import { useMessageAction, useSelectedMessages } from '../../../views/RoomView/MessageActionStore'; +import { useMessageAction } from '../../../views/RoomView/MessageActionStore'; import { useFocused } from '../context'; export const useAutoSaveDraft = (text = '') => { @@ -12,7 +12,6 @@ export const useAutoSaveDraft = (text = '') => { const route = useRoute(); const { rid, tmid } = useRoomContext(); const action = useMessageAction(); - const selectedMessages = useSelectedMessages(); const focused = useFocused(); const oldText = useRef(''); const intervalRef = useRef<number | null>(null); @@ -22,11 +21,13 @@ export const useAutoSaveDraft = (text = '') => { const saveMessageDraft = useCallback( (m?: string) => { if (route.name === 'ShareView') return; - if (action === 'edit') return; + if (action?.kind === 'edit') return; let draftMessage = ''; - if (selectedMessages?.length) { - draftMessage = JSON.stringify({ quotes: selectedMessages, msg: text }); + if (action?.kind === 'quote') { + draftMessage = JSON.stringify({ quotes: action.messageIds, msg: text }); + } else if (action?.kind === 'react') { + draftMessage = JSON.stringify({ quotes: [action.messageId], msg: text }); } else { draftMessage = m ?? text; } @@ -35,7 +36,7 @@ export const useAutoSaveDraft = (text = '') => { saveDraftMessage({ rid, tmid, draftMessage }); } }, - [action, rid, tmid, text, selectedMessages, route.name] + [action, rid, tmid, text, route.name] ); // if focused on composer input, saves every N seconds diff --git a/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx b/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx index 4b821fe6c7b..e1e0f18fe2e 100644 --- a/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx +++ b/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx @@ -19,8 +19,7 @@ jest.mock('../../../views/RoomView/context', () => ({ })); jest.mock('../../../views/RoomView/MessageActionStore', () => ({ - useMessageAction: jest.fn(), - useSelectedMessages: jest.fn() + useMessageAction: jest.fn() })); jest.mock('../../../lib/hooks/useAltTextSupported', () => ({ @@ -52,7 +51,6 @@ const mockUseAppSelector = require('../../../lib/hooks/useAppSelector').useAppSe const mockUseMessageComposerApi = require('../context').useMessageComposerApi as jest.Mock; const mockUseRoomContext = require('../../../views/RoomView/context').useRoomContext as jest.Mock; const mockUseMessageAction = require('../../../views/RoomView/MessageActionStore').useMessageAction as jest.Mock; -const mockUseSelectedMessages = require('../../../views/RoomView/MessageActionStore').useSelectedMessages as jest.Mock; const mockUseAltTextSupported = require('../../../lib/hooks/useAltTextSupported').useAltTextSupported as jest.Mock; const mockGetSubscriptionByRoomId = require('../../../lib/database/services/Subscription').getSubscriptionByRoomId as jest.Mock; const mockGetThreadById = require('../../../lib/database/services/Thread').getThreadById as jest.Mock; @@ -78,7 +76,6 @@ describe('useChooseMedia', () => { getText: jest.fn(() => 'draft') }); mockUseMessageAction.mockReturnValue(null); - mockUseSelectedMessages.mockReturnValue([]); mockGetSubscriptionByRoomId.mockResolvedValue({ rid: 'room-id', t: 'c' }); mockGetThreadById.mockResolvedValue({ id: 'thread-id' }); }); diff --git a/app/containers/MessageComposer/hooks/useChooseMedia.ts b/app/containers/MessageComposer/hooks/useChooseMedia.ts index e8349973222..350fd9d79d9 100644 --- a/app/containers/MessageComposer/hooks/useChooseMedia.ts +++ b/app/containers/MessageComposer/hooks/useChooseMedia.ts @@ -10,7 +10,7 @@ import { getThreadById } from '../../../lib/database/services/Thread'; import Navigation from '../../../lib/navigation/appNavigation'; import { useAppSelector } from '../../../lib/hooks/useAppSelector'; import { useRoomContext } from '../../../views/RoomView/context'; -import { useMessageAction, useSelectedMessages } from '../../../views/RoomView/MessageActionStore'; +import { useMessageAction } from '../../../views/RoomView/MessageActionStore'; import { type IShareAttachment } from '../../../definitions'; import ImagePicker, { type ImageOrVideo } from '../../../lib/methods/helpers/ImagePicker/ImagePicker'; import { useMessageComposerApi } from '../context'; @@ -34,7 +34,6 @@ export const useChooseMedia = ({ const { addAttachments } = useMessageComposerApi(); const { setQuotesAndText, getText } = useRoomContext(); const action = useMessageAction(); - const selectedMessages = useSelectedMessages(); const altTextSupported = useAltTextSupported(); const allowList = FileUpload_MediaTypeWhiteList as string; const maxFileSize = FileUpload_MaxFileSize as number; @@ -97,6 +96,12 @@ export const useChooseMedia = ({ const startShareView = () => { const text = getText?.() || ''; + let selectedMessages: string[] = []; + if (action?.kind === 'quote') { + selectedMessages = action.messageIds; + } else if (action) { + selectedMessages = [action.messageId]; + } return { selectedMessages, text @@ -118,7 +123,7 @@ export const useChooseMedia = ({ room, thread: thread || tmid, attachments, - action, + action: action?.kind ?? null, finishShareView, startShareView }); diff --git a/app/views/RoomView/MessageActionStore.test.tsx b/app/views/RoomView/MessageActionStore.test.tsx index 328fbb8d76d..3f95a1c6ff7 100644 --- a/app/views/RoomView/MessageActionStore.test.tsx +++ b/app/views/RoomView/MessageActionStore.test.tsx @@ -1,12 +1,6 @@ import { render } from '@testing-library/react-native'; -import { - createMessageActionStore, - MessageActionProvider, - useIsBeingEdited, - useMessageAction, - useSelectedMessages -} from './MessageActionStore'; +import { createMessageActionStore, MessageActionProvider, useIsBeingEdited, useMessageAction } from './MessageActionStore'; describe('MessageActionStore', () => { describe('useIsBeingEdited', () => { @@ -59,13 +53,71 @@ describe('MessageActionStore', () => { }; expect(() => render(<Probe />)).toThrow('Interaction hooks must be used within a MessageActionProvider'); }); + }); - it('useSelectedMessages throws', () => { + describe('useMessageAction under a provider', () => { + it('returns null when there is no action', () => { + const spy = jest.fn(); const Probe = () => { - useSelectedMessages(); + spy(useMessageAction()); return null; }; - expect(() => render(<Probe />)).toThrow('Interaction hooks must be used within a MessageActionProvider'); + + render( + <MessageActionProvider> + <Probe /> + </MessageActionProvider> + ); + + expect(spy).toHaveBeenLastCalledWith(null); + }); + + it('returns the edit action', () => { + const spy = jest.fn(); + const Probe = () => { + spy(useMessageAction()); + return null; + }; + + render( + <MessageActionProvider initialAction={{ kind: 'edit', messageId: 'msg-1' }}> + <Probe /> + </MessageActionProvider> + ); + + expect(spy).toHaveBeenLastCalledWith({ kind: 'edit', messageId: 'msg-1' }); + }); + + it('returns the quote action', () => { + const spy = jest.fn(); + const Probe = () => { + spy(useMessageAction()); + return null; + }; + + render( + <MessageActionProvider initialAction={{ kind: 'quote', messageIds: ['msg-1', 'msg-2'] }}> + <Probe /> + </MessageActionProvider> + ); + + expect(spy).toHaveBeenLastCalledWith({ kind: 'quote', messageIds: ['msg-1', 'msg-2'] }); + }); + + it('returns the react action', () => { + const spy = jest.fn(); + const Probe = () => { + spy(useMessageAction()); + return null; + }; + + render( + <MessageActionProvider initialAction={{ kind: 'react', messageId: 'msg-1' }}> + <Probe /> + </MessageActionProvider> + ); + + expect(spy).toHaveBeenLastCalledWith({ kind: 'react', messageId: 'msg-1' }); }); }); diff --git a/app/views/RoomView/MessageActionStore.tsx b/app/views/RoomView/MessageActionStore.tsx index b47209e52b0..e39bb0ab9bb 100644 --- a/app/views/RoomView/MessageActionStore.tsx +++ b/app/views/RoomView/MessageActionStore.tsx @@ -1,6 +1,5 @@ import { createContext, type ReactNode, useContext, useState } from 'react'; import { createStore, useStore } from 'zustand'; -import { useShallow } from 'zustand/react/shallow'; export type TMessageActionState = | { kind: 'edit'; messageId: string } @@ -70,19 +69,8 @@ const useMessageActionStore = <T,>(selector: (state: MessageActionState) => T): return useStore(store, selector); }; -export const useMessageAction = (): NonNullable<TMessageActionState>['kind'] | null => - useMessageActionStore(s => s.action?.kind ?? null); - -export const useSelectedMessages = (): string[] => - useMessageActionStore( - useShallow(s => { - const { action } = s; - if (!action) { - return []; - } - return action.kind === 'quote' ? action.messageIds : [action.messageId]; - }) - ); +// `action` is a single ref replaced wholesale on every `set` — no useShallow needed. +export const useMessageAction = (): TMessageActionState => useMessageActionStore(s => s.action); export const useIsBeingEdited = (messageId: string): boolean => { const store = useContext(MessageActionStoreContext) ?? fallbackStore; From b0b35168f85913641be1b1dbe9b2e12f706ebc9c Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 13:28:52 -0300 Subject: [PATCH 117/144] refactor: harden no-provider fallback with an inert MessageActionStore (NATIVE-22) Replace the writable module-level fallbackStore with a frozen inertStore whose actions throw on call, closing the latent cross-room write vector for no-provider consumers (search/pinned rows). Document useIsBeingEdited's graceful-degradation contract via JSDoc, and expand the test suite: no-provider isolation, useIsBeingEdited across all kinds, addQuote dedup guard, clear() from each kind. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- .../RoomView/MessageActionStore.test.tsx | 85 ++++++++++++++++++- app/views/RoomView/MessageActionStore.tsx | 24 +++++- 2 files changed, 103 insertions(+), 6 deletions(-) diff --git a/app/views/RoomView/MessageActionStore.test.tsx b/app/views/RoomView/MessageActionStore.test.tsx index 3f95a1c6ff7..ce579701452 100644 --- a/app/views/RoomView/MessageActionStore.test.tsx +++ b/app/views/RoomView/MessageActionStore.test.tsx @@ -1,10 +1,16 @@ import { render } from '@testing-library/react-native'; -import { createMessageActionStore, MessageActionProvider, useIsBeingEdited, useMessageAction } from './MessageActionStore'; +import { + createMessageActionStore, + inertStore, + MessageActionProvider, + useIsBeingEdited, + useMessageAction +} from './MessageActionStore'; describe('MessageActionStore', () => { describe('useIsBeingEdited', () => { - it('returns false with no provider (module fallback store) and does not throw', () => { + it('returns false with no provider (inert store) and does not throw', () => { const spy = jest.fn(); const Probe = () => { spy(useIsBeingEdited('msg-1')); @@ -15,6 +21,28 @@ describe('MessageActionStore', () => { expect(spy).toHaveBeenLastCalledWith(false); }); + it('does not share mutable state across separate no-provider renders, and its actions throw', () => { + const spyA = jest.fn(); + const spyB = jest.fn(); + const ProbeA = () => { + spyA(useIsBeingEdited('msg-1')); + return null; + }; + const ProbeB = () => { + spyB(useIsBeingEdited('msg-1')); + return null; + }; + + render(<ProbeA />); + render(<ProbeB />); + + expect(spyA).toHaveBeenLastCalledWith(false); + expect(spyB).toHaveBeenLastCalledWith(false); + expect(() => inertStore.getState().actions.startEditing('x')).toThrow( + 'MessageActionStore: no provider — actions unavailable' + ); + }); + it('returns true only for the message being edited under a provider', () => { const editedSpy = jest.fn(); const otherSpy = jest.fn(); @@ -33,6 +61,38 @@ describe('MessageActionStore', () => { expect(editedSpy).toHaveBeenLastCalledWith(true); expect(otherSpy).toHaveBeenLastCalledWith(false); }); + + it('returns false for a quote action, even for a matching messageId', () => { + const spy = jest.fn(); + const Probe = () => { + spy(useIsBeingEdited('msg-1')); + return null; + }; + + render( + <MessageActionProvider initialAction={{ kind: 'quote', messageIds: ['msg-1'] }}> + <Probe /> + </MessageActionProvider> + ); + + expect(spy).toHaveBeenLastCalledWith(false); + }); + + it('returns false for a react action, even for a matching messageId', () => { + const spy = jest.fn(); + const Probe = () => { + spy(useIsBeingEdited('msg-1')); + return null; + }; + + render( + <MessageActionProvider initialAction={{ kind: 'react', messageId: 'msg-1' }}> + <Probe /> + </MessageActionProvider> + ); + + expect(spy).toHaveBeenLastCalledWith(false); + }); }); describe('action hooks without a provider', () => { @@ -147,6 +207,13 @@ describe('MessageActionStore', () => { expect(store.getState().action).toEqual({ kind: 'quote', messageIds: ['msg-1'] }); }); + it('addQuote does not append a duplicate messageId', () => { + const store = createMessageActionStore(); + store.getState().actions.startQuote('msg-1'); + store.getState().actions.addQuote('msg-1'); + expect(store.getState().action).toEqual({ kind: 'quote', messageIds: ['msg-1'] }); + }); + it('removeQuote drops a messageId and keeps the quote action if others remain', () => { const store = createMessageActionStore({ kind: 'quote', messageIds: ['msg-1', 'msg-2'] }); store.getState().actions.removeQuote('msg-1'); @@ -180,10 +247,22 @@ describe('MessageActionStore', () => { expect(store.getState().action).toBeNull(); }); - it('clear resets any action', () => { + it('clear resets a react action', () => { const store = createMessageActionStore({ kind: 'react', messageId: 'msg-1' }); store.getState().actions.clear(); expect(store.getState().action).toBeNull(); }); + + it('clear resets an edit action', () => { + const store = createMessageActionStore({ kind: 'edit', messageId: 'msg-1' }); + store.getState().actions.clear(); + expect(store.getState().action).toBeNull(); + }); + + it('clear resets a quote action', () => { + const store = createMessageActionStore({ kind: 'quote', messageIds: ['msg-1', 'msg-2'] }); + store.getState().actions.clear(); + expect(store.getState().action).toBeNull(); + }); }); }); diff --git a/app/views/RoomView/MessageActionStore.tsx b/app/views/RoomView/MessageActionStore.tsx index e39bb0ab9bb..d3244d9d4d5 100644 --- a/app/views/RoomView/MessageActionStore.tsx +++ b/app/views/RoomView/MessageActionStore.tsx @@ -57,9 +57,23 @@ export type TMessageActionStore = ReturnType<typeof createMessageActionStore>; export const MessageActionStoreContext = createContext<TMessageActionStore | null>(null); +const noOpAction = (): never => { + throw new Error('MessageActionStore: no provider — actions unavailable'); +}; + +const NO_OP_ACTIONS: TMessageActionActions = { + startEditing: noOpAction, + startQuote: noOpAction, + addQuote: noOpAction, + removeQuote: noOpAction, + startReacting: noOpAction, + setQuoteMessageIds: noOpAction, + clear: noOpAction +}; + // Rows rendered outside a RoomView (search, pinned) can never be in edit mode. -// Using a module-level fallback avoids a conditional hook call in useIsBeingEdited. -const fallbackStore = createMessageActionStore(); +// `action` is fixed at null (no `set` param) and its actions always throw — a safe, inert fallback. +export const inertStore = createStore<MessageActionState>()(() => ({ action: null, actions: NO_OP_ACTIONS })); const useMessageActionStore = <T,>(selector: (state: MessageActionState) => T): T => { const store = useContext(MessageActionStoreContext); @@ -72,8 +86,12 @@ const useMessageActionStore = <T,>(selector: (state: MessageActionState) => T): // `action` is a single ref replaced wholesale on every `set` — no useShallow needed. export const useMessageAction = (): TMessageActionState => useMessageActionStore(s => s.action); +/** + * Unlike `useMessageAction`, which throws without a provider, this hook degrades to `false` via + * an inert store — search/pinned message rows render outside a `MessageActionProvider` and can never be editing. + */ export const useIsBeingEdited = (messageId: string): boolean => { - const store = useContext(MessageActionStoreContext) ?? fallbackStore; + const store = useContext(MessageActionStoreContext) ?? inertStore; return useStore(store, s => s.action?.kind === 'edit' && s.action.messageId === messageId); }; From 5b9bc597e06c78912a6104d557bbdbdf02e41172 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 14:38:24 -0300 Subject: [PATCH 118/144] fix: resync room-reactive fields in MessageRoomStore (NATIVE-22) --- .../message/stores/MessageRoomStore.tsx | 22 ++++++++++++++---- .../__tests__/MessageRoomStore.test.tsx | 23 ++++++++++++++++++- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/app/containers/message/stores/MessageRoomStore.tsx b/app/containers/message/stores/MessageRoomStore.tsx index c05e1e81d39..80109505469 100644 --- a/app/containers/message/stores/MessageRoomStore.tsx +++ b/app/containers/message/stores/MessageRoomStore.tsx @@ -60,16 +60,28 @@ const MessageRoomStoreProvider = ({ children, ...state }: { children: ReactNode const [store] = useState(() => createMessageRoomStore(state)); - // Only timeFormat/autoTranslateRoom/autoTranslateLanguage (the reactive tail) change after - // mount; the rest are constants/handlers captured once above. Keying on just those 3 fields - // lets React's own dependency diff replace the old full 28-key Object.is scan. + // These fields can change mid-session (e.g. an open room gets archived), unlike the + // constants/handlers captured once above. The dep array keeps store writes on-change only. useEffect(() => { store.setState({ timeFormat: state.timeFormat, autoTranslateRoom: state.autoTranslateRoom, - autoTranslateLanguage: state.autoTranslateLanguage + autoTranslateLanguage: state.autoTranslateLanguage, + archived: state.archived, + broadcast: state.broadcast, + isReadReceiptEnabled: state.isReadReceiptEnabled, + Message_GroupingPeriod: state.Message_GroupingPeriod }); - }, [state.timeFormat, state.autoTranslateRoom, state.autoTranslateLanguage, store]); + }, [ + state.timeFormat, + state.autoTranslateRoom, + state.autoTranslateLanguage, + state.archived, + state.broadcast, + state.isReadReceiptEnabled, + state.Message_GroupingPeriod, + store + ]); return <MessageRoomStoreContext.Provider value={store}>{children}</MessageRoomStoreContext.Provider>; }; diff --git a/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx b/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx index 0ed07eae901..b686ba31385 100644 --- a/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx @@ -2,7 +2,7 @@ import { memo } from 'react'; import { act, render } from '@testing-library/react-native'; import { Provider } from 'react-redux'; -import { MessageRoomProvider, useAutoTranslate, useTimeFormat } from '../MessageRoomStore'; +import { MessageRoomProvider, useAutoTranslate, useIsArchived, useTimeFormat } from '../MessageRoomStore'; import { mockedStore } from '../../../../reducers/mockedStore'; import { updateSettings } from '../../../../actions/settings'; @@ -110,4 +110,25 @@ describe('MessageRoomStore', () => { expect(autoTranslateSpy).toHaveBeenLastCalledWith({ autoTranslateRoom: true, autoTranslateLanguage: undefined }); expect(timeFormatSpy.mock.calls.length).toBe(timeFormatCallsBefore); }); + + it('resyncs archived when a room gets archived mid-session', () => { + const spy = jest.fn(); + const Probe = () => { + spy(useIsArchived()); + return null; + }; + const wrap = (archived: boolean) => ( + <Provider store={mockedStore}> + <MessageRoomProvider timeFormat='fixed-format' archived={archived}> + <Probe /> + </MessageRoomProvider> + </Provider> + ); + + const { rerender } = render(wrap(false)); + expect(spy).toHaveBeenLastCalledWith(false); + + act(() => rerender(wrap(true))); + expect(spy).toHaveBeenLastCalledWith(true); + }); }); From 052ba9b8bee7cbca92a7c9e40584b11913f8785a Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 14:48:59 -0300 Subject: [PATCH 119/144] refactor: move MessageActionStore into containers/message/stores (NATIVE-22) Relocates MessageActionStore from views/RoomView into containers/message/stores so reusable layers no longer import upward from a view, and moves the TMessageActionState discriminated union into app/definitions (where the derived TMessageAction already lives), inverting the definitions -> views arrow. RoomView/ShareView keep creating and providing the store. --- app/containers/MessageComposer/MessageComposer.test.tsx | 4 ++-- app/containers/MessageComposer/MessageComposer.tsx | 2 +- app/containers/MessageComposer/components/CancelEdit.tsx | 2 +- .../MessageComposer/components/ComposerInput.tsx | 2 +- .../MessageComposer/components/Quotes/Quotes.tsx | 2 +- app/containers/MessageComposer/hooks/useAutoSaveDraft.ts | 2 +- .../MessageComposer/hooks/useChooseMedia.test.tsx | 4 ++-- app/containers/MessageComposer/hooks/useChooseMedia.ts | 2 +- app/containers/message/__tests__/index.test.tsx | 2 +- app/containers/message/components/Message.tsx | 2 +- .../message/components/stories/Message.stories.tsx | 2 +- .../message/stores}/MessageActionStore.tsx | 6 +----- .../message/stores/__tests__}/MessageActionStore.test.tsx | 2 +- app/definitions/IMessage.ts | 7 ++++++- app/views/RoomView/RoomProviders.test.tsx | 2 +- app/views/RoomView/RoomProviders.tsx | 2 +- app/views/RoomView/index.tsx | 2 +- app/views/ShareView/index.tsx | 2 +- 18 files changed, 25 insertions(+), 24 deletions(-) rename app/{views/RoomView => containers/message/stores}/MessageActionStore.tsx (95%) rename app/{views/RoomView => containers/message/stores/__tests__}/MessageActionStore.test.tsx (99%) diff --git a/app/containers/MessageComposer/MessageComposer.test.tsx b/app/containers/MessageComposer/MessageComposer.test.tsx index 4e6b423dc0e..fdcf1a973e0 100644 --- a/app/containers/MessageComposer/MessageComposer.test.tsx +++ b/app/containers/MessageComposer/MessageComposer.test.tsx @@ -10,10 +10,10 @@ import { selectServerRequest } from '../../actions/server'; import { setUser } from '../../actions/login'; import { mockedStore } from '../../reducers/mockedStore'; import { type IPermissionsState } from '../../reducers/permissions'; -import { type IMessage, type IShareAttachment } from '../../definitions'; +import { type IMessage, type IShareAttachment, type TMessageActionState } from '../../definitions'; import { colors } from '../../lib/constants/colors'; import { type IRoomContext, RoomContext } from '../../views/RoomView/context'; -import { MessageActionProvider, type TMessageActionState } from '../../views/RoomView/MessageActionStore'; +import { MessageActionProvider } from '../message/stores/MessageActionStore'; import * as EmojiKeyboardHook from './hooks/useEmojiKeyboard'; import { initStore } from '../../lib/store/auxStore'; import { searchRemote } from '../../lib/methods/search'; diff --git a/app/containers/MessageComposer/MessageComposer.tsx b/app/containers/MessageComposer/MessageComposer.tsx index bc91c0b6489..40ee12556aa 100644 --- a/app/containers/MessageComposer/MessageComposer.tsx +++ b/app/containers/MessageComposer/MessageComposer.tsx @@ -5,7 +5,7 @@ import { Q } from '@nozbe/watermelondb'; import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; import { useRoomContext } from '../../views/RoomView/context'; -import { useMessageAction } from '../../views/RoomView/MessageActionStore'; +import { useMessageAction } from '../message/stores/MessageActionStore'; import { Autocomplete } from './components'; import { MIN_HEIGHT } from './constants'; import { diff --git a/app/containers/MessageComposer/components/CancelEdit.tsx b/app/containers/MessageComposer/components/CancelEdit.tsx index 4cbf75b312d..4b776a86fc9 100644 --- a/app/containers/MessageComposer/components/CancelEdit.tsx +++ b/app/containers/MessageComposer/components/CancelEdit.tsx @@ -1,6 +1,6 @@ import { BaseButton } from './Buttons'; import { useRoomContext } from '../../../views/RoomView/context'; -import { useMessageAction } from '../../../views/RoomView/MessageActionStore'; +import { useMessageAction } from '../../message/stores/MessageActionStore'; import { Gap } from './Gap'; export const CancelEdit = () => { diff --git a/app/containers/MessageComposer/components/ComposerInput.tsx b/app/containers/MessageComposer/components/ComposerInput.tsx index cf8279794c0..a0547bab0a6 100644 --- a/app/containers/MessageComposer/components/ComposerInput.tsx +++ b/app/containers/MessageComposer/components/ComposerInput.tsx @@ -33,7 +33,7 @@ import database from '../../../lib/database'; import Navigation from '../../../lib/navigation/appNavigation'; import { emitter } from '../../../lib/methods/helpers/emitter'; import { useRoomContext } from '../../../views/RoomView/context'; -import { useMessageAction } from '../../../views/RoomView/MessageActionStore'; +import { useMessageAction } from '../../message/stores/MessageActionStore'; import { getMessageById } from '../../../lib/database/services/Message'; import { generateTriggerId } from '../../../lib/methods/actions'; import { executeCommandPreview } from '../../../lib/services/restApi'; diff --git a/app/containers/MessageComposer/components/Quotes/Quotes.tsx b/app/containers/MessageComposer/components/Quotes/Quotes.tsx index 1011b46e54d..e8a54af866d 100644 --- a/app/containers/MessageComposer/components/Quotes/Quotes.tsx +++ b/app/containers/MessageComposer/components/Quotes/Quotes.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, type ReactElement } from 'react'; import { FlatList } from 'react-native'; import { Quote } from './Quote'; -import { useMessageAction } from '../../../../views/RoomView/MessageActionStore'; +import { useMessageAction } from '../../../message/stores/MessageActionStore'; export const Quotes = (): ReactElement | null => { 'use memo'; diff --git a/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts b/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts index 6e0399e2497..4c3d538c704 100644 --- a/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts +++ b/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts @@ -3,7 +3,7 @@ import { useCallback, useEffect, useRef } from 'react'; import { saveDraftMessage } from '../../../lib/methods/draftMessage'; import { useRoomContext } from '../../../views/RoomView/context'; -import { useMessageAction } from '../../../views/RoomView/MessageActionStore'; +import { useMessageAction } from '../../message/stores/MessageActionStore'; import { useFocused } from '../context'; export const useAutoSaveDraft = (text = '') => { diff --git a/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx b/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx index e1e0f18fe2e..6233013945e 100644 --- a/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx +++ b/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx @@ -18,7 +18,7 @@ jest.mock('../../../views/RoomView/context', () => ({ useRoomContext: jest.fn() })); -jest.mock('../../../views/RoomView/MessageActionStore', () => ({ +jest.mock('../../message/stores/MessageActionStore', () => ({ useMessageAction: jest.fn() })); @@ -50,7 +50,7 @@ const mockGetDocumentAsync = require('expo-document-picker').getDocumentAsync as const mockUseAppSelector = require('../../../lib/hooks/useAppSelector').useAppSelector as jest.Mock; const mockUseMessageComposerApi = require('../context').useMessageComposerApi as jest.Mock; const mockUseRoomContext = require('../../../views/RoomView/context').useRoomContext as jest.Mock; -const mockUseMessageAction = require('../../../views/RoomView/MessageActionStore').useMessageAction as jest.Mock; +const mockUseMessageAction = require('../../message/stores/MessageActionStore').useMessageAction as jest.Mock; const mockUseAltTextSupported = require('../../../lib/hooks/useAltTextSupported').useAltTextSupported as jest.Mock; const mockGetSubscriptionByRoomId = require('../../../lib/database/services/Subscription').getSubscriptionByRoomId as jest.Mock; const mockGetThreadById = require('../../../lib/database/services/Thread').getThreadById as jest.Mock; diff --git a/app/containers/MessageComposer/hooks/useChooseMedia.ts b/app/containers/MessageComposer/hooks/useChooseMedia.ts index 350fd9d79d9..34f2d06bddb 100644 --- a/app/containers/MessageComposer/hooks/useChooseMedia.ts +++ b/app/containers/MessageComposer/hooks/useChooseMedia.ts @@ -10,7 +10,7 @@ import { getThreadById } from '../../../lib/database/services/Thread'; import Navigation from '../../../lib/navigation/appNavigation'; import { useAppSelector } from '../../../lib/hooks/useAppSelector'; import { useRoomContext } from '../../../views/RoomView/context'; -import { useMessageAction } from '../../../views/RoomView/MessageActionStore'; +import { useMessageAction } from '../../message/stores/MessageActionStore'; import { type IShareAttachment } from '../../../definitions'; import ImagePicker, { type ImageOrVideo } from '../../../lib/methods/helpers/ImagePicker/ImagePicker'; import { useMessageComposerApi } from '../context'; diff --git a/app/containers/message/__tests__/index.test.tsx b/app/containers/message/__tests__/index.test.tsx index 06971fac9e7..51996cbb186 100644 --- a/app/containers/message/__tests__/index.test.tsx +++ b/app/containers/message/__tests__/index.test.tsx @@ -3,7 +3,7 @@ import { fireEvent } from '@testing-library/react-native'; import MessageContainer from '../index'; import { type TAnyMessageModel } from '../../../definitions'; -import { createMessageActionStore, MessageActionStoreContext } from '../../../views/RoomView/MessageActionStore'; +import { createMessageActionStore, MessageActionStoreContext } from '../stores/MessageActionStore'; import { renderWithMessageProviders } from '../index.testHelpers'; import { type MessageRoomState } from '../stores/MessageRoomStore'; diff --git a/app/containers/message/components/Message.tsx b/app/containers/message/components/Message.tsx index 395c62fcd73..196ac279c5f 100644 --- a/app/containers/message/components/Message.tsx +++ b/app/containers/message/components/Message.tsx @@ -8,7 +8,7 @@ import { useLastFocusedMessageRef } from '../../../lib/a11y/useLastFocusedMessag import { useMessageAccessibilityLabel } from '../hooks/useMessageAccessibilityLabel'; import { useMessageAccessibilityActions } from '../hooks/useMessageAccessibilityActions'; import { useMessageAccessibilityHint } from '../hooks/useMessageAccessibilityHint'; -import { useIsBeingEdited } from '../../../views/RoomView/MessageActionStore'; +import { useIsBeingEdited } from '../stores/MessageActionStore'; import { useIsInfoMessage, useMessageField, diff --git a/app/containers/message/components/stories/Message.stories.tsx b/app/containers/message/components/stories/Message.stories.tsx index 8d6a12dd600..2114006c5da 100644 --- a/app/containers/message/components/stories/Message.stories.tsx +++ b/app/containers/message/components/stories/Message.stories.tsx @@ -16,7 +16,7 @@ import { import { mockedStore as store } from '../../../../reducers/mockedStore'; import { updateSettings } from '../../../../actions/settings'; import { setCustomEmojis } from '../../../../actions/customEmojis'; -import { createMessageActionStore, MessageActionStoreContext } from '../../../../views/RoomView/MessageActionStore'; +import { createMessageActionStore, MessageActionStoreContext } from '../../stores/MessageActionStore'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; const _theme = 'light'; diff --git a/app/views/RoomView/MessageActionStore.tsx b/app/containers/message/stores/MessageActionStore.tsx similarity index 95% rename from app/views/RoomView/MessageActionStore.tsx rename to app/containers/message/stores/MessageActionStore.tsx index d3244d9d4d5..b563e2da727 100644 --- a/app/views/RoomView/MessageActionStore.tsx +++ b/app/containers/message/stores/MessageActionStore.tsx @@ -1,11 +1,7 @@ import { createContext, type ReactNode, useContext, useState } from 'react'; import { createStore, useStore } from 'zustand'; -export type TMessageActionState = - | { kind: 'edit'; messageId: string } - | { kind: 'quote'; messageIds: string[] } - | { kind: 'react'; messageId: string } - | null; +import { type TMessageActionState } from '../../../definitions'; type TMessageActionActions = { startEditing(messageId: string): void; diff --git a/app/views/RoomView/MessageActionStore.test.tsx b/app/containers/message/stores/__tests__/MessageActionStore.test.tsx similarity index 99% rename from app/views/RoomView/MessageActionStore.test.tsx rename to app/containers/message/stores/__tests__/MessageActionStore.test.tsx index ce579701452..f9fb187c6a2 100644 --- a/app/views/RoomView/MessageActionStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageActionStore.test.tsx @@ -6,7 +6,7 @@ import { MessageActionProvider, useIsBeingEdited, useMessageAction -} from './MessageActionStore'; +} from '../MessageActionStore'; describe('MessageActionStore', () => { describe('useIsBeingEdited', () => { diff --git a/app/definitions/IMessage.ts b/app/definitions/IMessage.ts index 1a6c6749f1e..4e211f9fb21 100644 --- a/app/definitions/IMessage.ts +++ b/app/definitions/IMessage.ts @@ -2,13 +2,18 @@ import type Model from '@nozbe/watermelondb/Model'; import { type Root } from '@rocket.chat/message-parser'; import { type MessageTypeLoad } from '../lib/constants/messageTypeLoad'; -import { type TMessageActionState } from '../views/RoomView/MessageActionStore'; import { type IAttachment } from './IAttachment'; import { type IReaction } from './IReaction'; import { type TThreadMessageModel } from './IThreadMessage'; import { type TThreadModel } from './IThread'; import { type IUrl, type IUrlFromServer } from './IUrl'; +export type TMessageActionState = + | { kind: 'edit'; messageId: string } + | { kind: 'quote'; messageIds: string[] } + | { kind: 'react'; messageId: string } + | null; + export type TMessageAction = NonNullable<TMessageActionState>['kind'] | null; export type MessageType = diff --git a/app/views/RoomView/RoomProviders.test.tsx b/app/views/RoomView/RoomProviders.test.tsx index a0be5dad78a..3d7545c8202 100644 --- a/app/views/RoomView/RoomProviders.test.tsx +++ b/app/views/RoomView/RoomProviders.test.tsx @@ -2,7 +2,7 @@ import { render } from '@testing-library/react-native'; import { RoomProviders } from './RoomProviders'; import { useRoomContext, type IRoomContext } from './context'; -import { createMessageActionStore } from './MessageActionStore'; +import { createMessageActionStore } from '../../containers/message/stores/MessageActionStore'; describe('RoomProviders', () => { it('keeps the same RoomContext value reference across re-renders with unchanged props', () => { diff --git a/app/views/RoomView/RoomProviders.tsx b/app/views/RoomView/RoomProviders.tsx index 9e284b6a89a..b87211b86ed 100644 --- a/app/views/RoomView/RoomProviders.tsx +++ b/app/views/RoomView/RoomProviders.tsx @@ -1,7 +1,7 @@ import { type ReactElement } from 'react'; import { RoomContext, type IRoomContext } from './context'; -import { type TMessageActionStore, MessageActionStoreContext } from './MessageActionStore'; +import { type TMessageActionStore, MessageActionStoreContext } from '../../containers/message/stores/MessageActionStore'; type IRoomProvidersProps = IRoomContext & { store: TMessageActionStore; diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 4a133b176a3..25762af7df3 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -96,7 +96,7 @@ import { import { withActionSheet } from '../../containers/ActionSheet'; import { goRoom, type TGoRoomItem } from '../../lib/methods/helpers/goRoom'; import { ComposerAttachments, type IMessageComposerRef, MessageComposerContainer } from '../../containers/MessageComposer'; -import { createMessageActionStore, type TMessageActionStore } from './MessageActionStore'; +import { createMessageActionStore, type TMessageActionStore } from '../../containers/message/stores/MessageActionStore'; import { RoomProviders } from './RoomProviders'; import { MessageRoomProvider } from '../../containers/message/stores/MessageRoomStore'; import AudioManager from '../../lib/methods/AudioManager'; diff --git a/app/views/ShareView/index.tsx b/app/views/ShareView/index.tsx index 0c92b3ee7d0..3f8ef84aae8 100644 --- a/app/views/ShareView/index.tsx +++ b/app/views/ShareView/index.tsx @@ -38,7 +38,7 @@ import { sendAttachments } from '../../lib/methods/sendFileMessage/sendAttachmen import { sendMessage } from '../../lib/methods/sendMessage'; import { hasPermission, isAndroid, canUploadFile, isReadOnly, isBlocked } from '../../lib/methods/helpers'; import { RoomProviders } from '../RoomView/RoomProviders'; -import { createMessageActionStore, type TMessageActionStore } from '../RoomView/MessageActionStore'; +import { createMessageActionStore, type TMessageActionStore } from '../../containers/message/stores/MessageActionStore'; import { appStart } from '../../actions/app'; interface IShareViewState { From 9e1d3f8fe5f9d61a62f51c078ef759e33f427e89 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 15:01:14 -0300 Subject: [PATCH 120/144] fix: restore Jitsi started-a-call line for jitsi_call_started messages (NATIVE-22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JitsiBranch rendered a bare <Content /> after the refactor, and useIsInfoMessage() explicitly excludes jitsi_call_started (it isn't a compact/non-touchable info row), so InfoContent never rendered the localized "started a call" line — only the join button remained. Give Content a local isInfo override so JitsiBranch alone can force InfoContent, matching the pre-refactor output (User + Started_call text + join button) without touching useIsInfoMessage or any other branch. Add a jitsi_call_started story to cover it going forward. --- app/containers/message/components/Content.tsx | 7 +- .../components/Message/JitsiBranch.tsx | 2 +- .../components/__tests__/JitsiBranch.test.tsx | 37 + .../__snapshots__/Message.test.tsx.snap | 4462 ++++++++++------- .../components/stories/Message.stories.tsx | 4 + 5 files changed, 2811 insertions(+), 1701 deletions(-) create mode 100644 app/containers/message/components/__tests__/JitsiBranch.test.tsx diff --git a/app/containers/message/components/Content.tsx b/app/containers/message/components/Content.tsx index 2d2c44ab25a..cd03df9a7fd 100644 --- a/app/containers/message/components/Content.tsx +++ b/app/containers/message/components/Content.tsx @@ -6,10 +6,13 @@ import PreviewContent from './Content/PreviewContent'; import { useIsEncrypted, useIsInfoMessage, useMessageIgnored, useMessageText, useThreadData } from '../stores/MessageStore'; import { useIsThreadRoom } from '../stores/MessageRoomStore'; -const Content = () => { +// isInfo forces InfoContent regardless of useIsInfoMessage — jitsi_call_started is deliberately +// excluded there (it isn't a compact/non-touchable info row) but still needs the info-style body. +const Content = ({ isInfo: isInfoProp }: { isInfo?: boolean } = {}) => { 'use memo'; - const isInfo = useIsInfoMessage(); + const isInfoMessage = useIsInfoMessage(); + const isInfo = isInfoProp ?? isInfoMessage; const isIgnored = useMessageIgnored(); const isEncrypted = useIsEncrypted(); const { tmid } = useThreadData(); diff --git a/app/containers/message/components/Message/JitsiBranch.tsx b/app/containers/message/components/Message/JitsiBranch.tsx index e9163855c30..945daf74f87 100644 --- a/app/containers/message/components/Message/JitsiBranch.tsx +++ b/app/containers/message/components/Message/JitsiBranch.tsx @@ -9,7 +9,7 @@ const JitsiBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { return ( <> <User /> - <Content /> + <Content isInfo /> <CallButton /> {showTimeLarge ? <MessageTime /> : null} </> diff --git a/app/containers/message/components/__tests__/JitsiBranch.test.tsx b/app/containers/message/components/__tests__/JitsiBranch.test.tsx new file mode 100644 index 00000000000..6dab34edff3 --- /dev/null +++ b/app/containers/message/components/__tests__/JitsiBranch.test.tsx @@ -0,0 +1,37 @@ +import { Provider } from 'react-redux'; +import { render } from '@testing-library/react-native'; + +import JitsiBranch from '../Message/JitsiBranch'; +import { MessageProvider } from '../../stores/MessageStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; +import { mockedStore } from '../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../definitions'; +import I18n from '../../../../i18n'; + +const item = { + id: 'msg-1', + msg: '', + t: 'jitsi_call_started', + u: { _id: 'author-id', username: 'diego.mello' } +} as unknown as TAnyMessageModel; + +const renderJitsiBranch = () => { + const room: Partial<MessageRoomState> = { handleEnterCall: jest.fn() }; + return render( + <Provider store={mockedStore}> + <MessageRoomProvider {...room}> + <MessageProvider item={item}> + <JitsiBranch showTimeLarge={false} /> + </MessageProvider> + </MessageRoomProvider> + </Provider> + ); +}; + +describe('JitsiBranch', () => { + test('renders the localized "started a call" line AND the join button for a jitsi_call_started message', () => { + const { getByText } = renderJitsiBranch(); + expect(getByText(I18n.t('Started_call', { userBy: 'diego.mello' }))).toBeTruthy(); + expect(getByText(I18n.t('Click_to_join'))).toBeTruthy(); + }); +}); diff --git a/app/containers/message/components/__tests__/__snapshots__/Message.test.tsx.snap b/app/containers/message/components/__tests__/__snapshots__/Message.test.tsx.snap index 4bf8543433b..4b0bda15861 100644 --- a/app/containers/message/components/__tests__/__snapshots__/Message.test.tsx.snap +++ b/app/containers/message/components/__tests__/__snapshots__/Message.test.tsx.snap @@ -63053,7 +63053,7 @@ exports[`Story Snapshots: InlineKatexLargeFont should match snapshot 1`] = ` </RCTScrollView> `; -exports[`Story Snapshots: Katex should match snapshot 1`] = ` +exports[`Story Snapshots: JitsiCall should match snapshot 1`] = ` <RCTScrollView style={ { @@ -63094,15 +63094,7 @@ exports[`Story Snapshots: Katex should match snapshot 1`] = ` } > <View - accessibilityActions={ - [ - { - "label": "Show message actions", - "name": "showActions", - }, - ] - } - accessibilityLabel="diego.mello 10:00:00 AM \\[ \\color{black} \\colorbox{white}{ \\boxed{ \\begin{matrix} \\;\\;\\;\\; \\overlinesegment {\\underlinesegment{ \\Huge Test\\; Test \\; in \\; Test} } & \\\\ \\;\\;\\textit{develop}\\end{matrix} } }\\]." + accessibilityLabel="diego.mello 10:00:00 AM." accessibilityRole="button" accessibilityState={ { @@ -63165,7 +63157,7 @@ exports[`Story Snapshots: Katex should match snapshot 1`] = ` testID="message-story-message-137" > <A11yIndexView - accessibilityLabel="\\[ \\color{black} \\colorbox{white}{ \\boxed{ \\begin{matrix} \\;\\;\\;\\; \\overlinesegment {\\underlinesegment{ \\Huge Test\\; Test \\; in \\; Test} } & \\\\ \\;\\;\\textit{develop}\\end{matrix} } }\\]" + accessibilityLabel="" accessible={false} importantForAccessibility="yes" orderFocusType={0} @@ -63442,25 +63434,141 @@ exports[`Story Snapshots: Katex should match snapshot 1`] = ` } /> </View> + <Text + accessibilityLabel="Call started by diego.mello" + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + > + Call started by diego.mello + </Text> <View style={ { - "gap": 4, + "alignItems": "center", + "flexDirection": "row", } } > <View - style={false} - testID="message-content-\\[ \\color{black} \\colorbox{white}{ \\boxed{ \\begin{matrix} \\;\\;\\;\\; \\overlinesegment {\\underlinesegment{ \\Huge Test\\; Test \\; in \\; Test} } & \\\\ \\;\\;\\textit{develop}\\end{matrix} } }\\]" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignItems": "center", + "borderRadius": 4, + "flexDirection": "row", + "justifyContent": "center", + "paddingHorizontal": 12, + "paddingVertical": 8, + }, + { + "backgroundColor": "#1D74F5", + }, + ] + } > <View style={ { - "gap": 2, + "alignItems": "center", + "flexDirection": "row", + "gap": 8, } } > - <View /> + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#FFFFFF", + "fontSize": 16, + }, + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600", + "lineHeight": 16, + "textAlign": "left", + }, + { + "color": "#FFFFFF", + }, + ] + } + > + Click to join! + </Text> </View> </View> </View> @@ -63478,7 +63586,7 @@ exports[`Story Snapshots: Katex should match snapshot 1`] = ` </RCTScrollView> `; -exports[`Story Snapshots: KatexArray should match snapshot 1`] = ` +exports[`Story Snapshots: JitsiCallLargeFont should match snapshot 1`] = ` <RCTScrollView style={ { @@ -63519,15 +63627,7 @@ exports[`Story Snapshots: KatexArray should match snapshot 1`] = ` } > <View - accessibilityActions={ - [ - { - "label": "Show message actions", - "name": "showActions", - }, - ] - } - accessibilityLabel="diego.mello 10:00:00 AM \\begin{array}{|c|c|c|c|c|c|} \\hline \\text{...} & \\text{...} \\\\ \\hline \\end{array}." + accessibilityLabel="diego.mello 10:00:00 AM." accessibilityRole="button" accessibilityState={ { @@ -63590,7 +63690,7 @@ exports[`Story Snapshots: KatexArray should match snapshot 1`] = ` testID="message-story-message-138" > <A11yIndexView - accessibilityLabel="\\begin{array}{|c|c|c|c|c|c|} \\hline \\text{...} & \\text{...} \\\\ \\hline \\end{array}" + accessibilityLabel="" accessible={false} importantForAccessibility="yes" orderFocusType={0} @@ -63608,7 +63708,7 @@ exports[`Story Snapshots: KatexArray should match snapshot 1`] = ` style={ { "alignItems": "flex-end", - "width": 36, + "width": 46.800000000000004, } } > @@ -63618,8 +63718,8 @@ exports[`Story Snapshots: KatexArray should match snapshot 1`] = ` [ { "borderRadius": 4, - "height": 36, - "width": 36, + "height": 46.800000000000004, + "width": 46.800000000000004, }, { "marginTop": 4, @@ -63719,7 +63819,7 @@ exports[`Story Snapshots: KatexArray should match snapshot 1`] = ` "top": "50%", } } - height={36} + height={46.800000000000004} nativeViewRef={{"current":"NativeComponent"}} onError={[Function]} onLoad={[Function]} @@ -63733,19 +63833,19 @@ exports[`Story Snapshots: KatexArray should match snapshot 1`] = ` "headers": { "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", }, - "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=72", + "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=93.60000000000001", }, ] } style={ { "borderRadius": 4, - "height": 36, - "width": 36, + "height": 46.800000000000004, + "width": 46.800000000000004, } } transition={null} - width={36} + width={46.800000000000004} /> </View> </RNGestureHandlerButton> @@ -63839,25 +63939,6 @@ exports[`Story Snapshots: KatexArray should match snapshot 1`] = ` > diego.mello </Text> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 13, - "fontWeight": "400", - "lineHeight": 18, - "textAlign": "left", - }, - { - "color": "#6C727A", - }, - ] - } - > - 10:00 AM - </Text> </View> <View style={ @@ -63867,28 +63948,163 @@ exports[`Story Snapshots: KatexArray should match snapshot 1`] = ` } /> </View> + <Text + accessibilityLabel="Call started by diego.mello" + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + > + Call started by diego.mello + </Text> <View style={ { - "gap": 4, + "alignItems": "center", + "flexDirection": "row", } } > <View - style={false} - testID="message-content-\\begin{array}{|c|c|c|c|c|c|} \\hline \\text{...} & \\text{...} \\\\ \\hline \\end{array}" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + hitSlop={ + { + "bottom": 4, + "left": 4, + "right": 4, + "top": 4, + } + } + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignItems": "center", + "borderRadius": 4, + "flexDirection": "row", + "justifyContent": "center", + "paddingHorizontal": 12, + "paddingVertical": 8, + }, + { + "backgroundColor": "#1D74F5", + }, + ] + } > <View style={ { - "gap": 2, + "alignItems": "center", + "flexDirection": "row", + "gap": 8, } } > - <View /> + <Text + allowFontScaling={false} + selectable={false} + style={ + [ + { + "color": "#FFFFFF", + "fontSize": 20.8, + }, + [ + { + "lineHeight": 20.8, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 12, + "fontWeight": "600", + "lineHeight": 16, + "textAlign": "left", + }, + { + "color": "#FFFFFF", + }, + ] + } + > + Click to join! + </Text> </View> </View> </View> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + > + 10:00 AM + </Text> </View> </View> </View> @@ -63903,7 +64119,7 @@ exports[`Story Snapshots: KatexArray should match snapshot 1`] = ` </RCTScrollView> `; -exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` +exports[`Story Snapshots: Katex should match snapshot 1`] = ` <RCTScrollView style={ { @@ -63952,7 +64168,7 @@ exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM \\begin{array}{|c|c|c|c|c|c|} \\hline \\text{...} & \\text{...} \\\\ \\hline \\end{array}." + accessibilityLabel="diego.mello 10:00:00 AM \\[ \\color{black} \\colorbox{white}{ \\boxed{ \\begin{matrix} \\;\\;\\;\\; \\overlinesegment {\\underlinesegment{ \\Huge Test\\; Test \\; in \\; Test} } & \\\\ \\;\\;\\textit{develop}\\end{matrix} } }\\]." accessibilityRole="button" accessibilityState={ { @@ -64015,7 +64231,7 @@ exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` testID="message-story-message-139" > <A11yIndexView - accessibilityLabel="\\begin{array}{|c|c|c|c|c|c|} \\hline \\text{...} & \\text{...} \\\\ \\hline \\end{array}" + accessibilityLabel="\\[ \\color{black} \\colorbox{white}{ \\boxed{ \\begin{matrix} \\;\\;\\;\\; \\overlinesegment {\\underlinesegment{ \\Huge Test\\; Test \\; in \\; Test} } & \\\\ \\;\\;\\textit{develop}\\end{matrix} } }\\]" accessible={false} importantForAccessibility="yes" orderFocusType={0} @@ -64033,7 +64249,7 @@ exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` style={ { "alignItems": "flex-end", - "width": 46.800000000000004, + "width": 36, } } > @@ -64043,8 +64259,8 @@ exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` [ { "borderRadius": 4, - "height": 46.800000000000004, - "width": 46.800000000000004, + "height": 36, + "width": 36, }, { "marginTop": 4, @@ -64144,7 +64360,7 @@ exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` "top": "50%", } } - height={46.800000000000004} + height={36} nativeViewRef={{"current":"NativeComponent"}} onError={[Function]} onLoad={[Function]} @@ -64158,19 +64374,19 @@ exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` "headers": { "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", }, - "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=93.60000000000001", + "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=72", }, ] } style={ { "borderRadius": 4, - "height": 46.800000000000004, - "width": 46.800000000000004, + "height": 36, + "width": 36, } } transition={null} - width={46.800000000000004} + width={36} /> </View> </RNGestureHandlerButton> @@ -64264,6 +64480,25 @@ exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` > diego.mello </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + > + 10:00 AM + </Text> </View> <View style={ @@ -64273,25 +64508,6 @@ exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` } /> </View> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 13, - "fontWeight": "400", - "lineHeight": 18, - "textAlign": "left", - }, - { - "color": "#6C727A", - }, - ] - } - > - 10:00 AM - </Text> <View style={ { @@ -64301,7 +64517,7 @@ exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` > <View style={false} - testID="message-content-\\begin{array}{|c|c|c|c|c|c|} \\hline \\text{...} & \\text{...} \\\\ \\hline \\end{array}" + testID="message-content-\\[ \\color{black} \\colorbox{white}{ \\boxed{ \\begin{matrix} \\;\\;\\;\\; \\overlinesegment {\\underlinesegment{ \\Huge Test\\; Test \\; in \\; Test} } & \\\\ \\;\\;\\textit{develop}\\end{matrix} } }\\]" > <View style={ @@ -64328,7 +64544,7 @@ exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` </RCTScrollView> `; -exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` +exports[`Story Snapshots: KatexArray should match snapshot 1`] = ` <RCTScrollView style={ { @@ -64377,7 +64593,7 @@ exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM \\[ \\color{black} \\colorbox{white}{ \\boxed{ \\begin{matrix} \\;\\;\\;\\; \\overlinesegment {\\underlinesegment{ \\Huge Test\\; Test \\; in \\; Test} } & \\\\ \\;\\;\\textit{develop}\\end{matrix} } }\\]." + accessibilityLabel="diego.mello 10:00:00 AM \\begin{array}{|c|c|c|c|c|c|} \\hline \\text{...} & \\text{...} \\\\ \\hline \\end{array}." accessibilityRole="button" accessibilityState={ { @@ -64440,7 +64656,7 @@ exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` testID="message-story-message-140" > <A11yIndexView - accessibilityLabel="\\[ \\color{black} \\colorbox{white}{ \\boxed{ \\begin{matrix} \\;\\;\\;\\; \\overlinesegment {\\underlinesegment{ \\Huge Test\\; Test \\; in \\; Test} } & \\\\ \\;\\;\\textit{develop}\\end{matrix} } }\\]" + accessibilityLabel="\\begin{array}{|c|c|c|c|c|c|} \\hline \\text{...} & \\text{...} \\\\ \\hline \\end{array}" accessible={false} importantForAccessibility="yes" orderFocusType={0} @@ -64458,7 +64674,7 @@ exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` style={ { "alignItems": "flex-end", - "width": 46.800000000000004, + "width": 36, } } > @@ -64468,8 +64684,8 @@ exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` [ { "borderRadius": 4, - "height": 46.800000000000004, - "width": 46.800000000000004, + "height": 36, + "width": 36, }, { "marginTop": 4, @@ -64569,7 +64785,7 @@ exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` "top": "50%", } } - height={46.800000000000004} + height={36} nativeViewRef={{"current":"NativeComponent"}} onError={[Function]} onLoad={[Function]} @@ -64583,19 +64799,19 @@ exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` "headers": { "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", }, - "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=93.60000000000001", + "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=72", }, ] } style={ { "borderRadius": 4, - "height": 46.800000000000004, - "width": 46.800000000000004, + "height": 36, + "width": 36, } } transition={null} - width={46.800000000000004} + width={36} /> </View> </RNGestureHandlerButton> @@ -64689,6 +64905,25 @@ exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` > diego.mello </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + > + 10:00 AM + </Text> </View> <View style={ @@ -64698,25 +64933,6 @@ exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` } /> </View> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 13, - "fontWeight": "400", - "lineHeight": 18, - "textAlign": "left", - }, - { - "color": "#6C727A", - }, - ] - } - > - 10:00 AM - </Text> <View style={ { @@ -64726,7 +64942,7 @@ exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` > <View style={false} - testID="message-content-\\[ \\color{black} \\colorbox{white}{ \\boxed{ \\begin{matrix} \\;\\;\\;\\; \\overlinesegment {\\underlinesegment{ \\Huge Test\\; Test \\; in \\; Test} } & \\\\ \\;\\;\\textit{develop}\\end{matrix} } }\\]" + testID="message-content-\\begin{array}{|c|c|c|c|c|c|} \\hline \\text{...} & \\text{...} \\\\ \\hline \\end{array}" > <View style={ @@ -64753,7 +64969,7 @@ exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` </RCTScrollView> `; -exports[`Story Snapshots: Lists should match snapshot 1`] = ` +exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` <RCTScrollView style={ { @@ -64802,9 +65018,7 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` }, ] } - accessibilityLabel="diego.mello 10:00:00 AM * Dogs -* cats -- cats." + accessibilityLabel="diego.mello 10:00:00 AM \\begin{array}{|c|c|c|c|c|c|} \\hline \\text{...} & \\text{...} \\\\ \\hline \\end{array}." accessibilityRole="button" accessibilityState={ { @@ -64867,9 +65081,7 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` testID="message-story-message-141" > <A11yIndexView - accessibilityLabel="* Dogs -* cats -- cats" + accessibilityLabel="\\begin{array}{|c|c|c|c|c|c|} \\hline \\text{...} & \\text{...} \\\\ \\hline \\end{array}" accessible={false} importantForAccessibility="yes" orderFocusType={0} @@ -64887,7 +65099,7 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` style={ { "alignItems": "flex-end", - "width": 36, + "width": 46.800000000000004, } } > @@ -64897,8 +65109,8 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` [ { "borderRadius": 4, - "height": 36, - "width": 36, + "height": 46.800000000000004, + "width": 46.800000000000004, }, { "marginTop": 4, @@ -64998,7 +65210,7 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` "top": "50%", } } - height={36} + height={46.800000000000004} nativeViewRef={{"current":"NativeComponent"}} onError={[Function]} onLoad={[Function]} @@ -65012,19 +65224,19 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` "headers": { "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", }, - "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=72", + "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=93.60000000000001", }, ] } style={ { "borderRadius": 4, - "height": 36, - "width": 36, + "height": 46.800000000000004, + "width": 46.800000000000004, } } transition={null} - width={36} + width={46.800000000000004} /> </View> </RNGestureHandlerButton> @@ -65118,25 +65330,6 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` > diego.mello </Text> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 13, - "fontWeight": "400", - "lineHeight": 18, - "textAlign": "left", - }, - { - "color": "#6C727A", - }, - ] - } - > - 10:00 AM - </Text> </View> <View style={ @@ -65146,6 +65339,25 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` } /> </View> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + > + 10:00 AM + </Text> <View style={ { @@ -65155,9 +65367,7 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` > <View style={false} - testID="message-content-* Dogs -* cats -- cats" + testID="message-content-\\begin{array}{|c|c|c|c|c|c|} \\hline \\text{...} & \\text{...} \\\\ \\hline \\end{array}" > <View style={ @@ -65166,229 +65376,7 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` } } > - <View> - <View - style={ - { - "flexDirection": "row", - } - } - > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - • - </Text> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Dogs" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Dogs - </Text> - </Text> - </Text> - </View> - <View - style={ - { - "flexDirection": "row", - } - } - > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - • - </Text> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="cats" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - cats - </Text> - </Text> - </Text> - </View> - </View> - <View> - <View - style={ - { - "flexDirection": "row", - } - } - > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - • - </Text> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="cats" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - cats - </Text> - </Text> - </Text> - </View> - </View> + <View /> </View> </View> </View> @@ -65402,6 +65390,19 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` </ExternalKeyboardView> </A11yIndexView> </A11yOrderView> + </View> +</RCTScrollView> +`; + +exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` +<RCTScrollView + style={ + { + "backgroundColor": "#FFFFFF", + } + } +> + <View> <A11yOrderView orderKey="«r3r»" > @@ -65410,6 +65411,1071 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` orderFocusType={0} orderIndex={1} orderKey="«r3r»" + > + <ExternalKeyboardView + canBeFocused={true} + enableA11yFocus={false} + enableContextMenu={true} + group={false} + haloEffect={true} + hasKeyDownPress={false} + hasKeyUpPress={true} + hasOnFocusChanged={true} + lockFocus={0} + onContextMenuPress={[Function]} + onFocusChange={[Function]} + onKeyUpPress={[Function]} + orderIndex={-1} + screenAutoA11yFocusDelay={300} + style={ + [ + undefined, + undefined, + ] + } + > + <View + accessibilityActions={ + [ + { + "label": "Show message actions", + "name": "showActions", + }, + ] + } + accessibilityLabel="diego.mello 10:00:00 AM \\[ \\color{black} \\colorbox{white}{ \\boxed{ \\begin{matrix} \\;\\;\\;\\; \\overlinesegment {\\underlinesegment{ \\Huge Test\\; Test \\; in \\; Test} } & \\\\ \\;\\;\\textit{develop}\\end{matrix} } }\\]." + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": false, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onAccessibilityAction={[Function]} + onClick={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "backgroundColor": undefined, + "borderRadius": undefined, + "margin": undefined, + "marginBottom": undefined, + "marginEnd": undefined, + "marginHorizontal": undefined, + "marginLeft": undefined, + "marginRight": undefined, + "marginStart": undefined, + "marginTop": undefined, + "marginVertical": undefined, + "opacity": 1, + } + } + > + <View + style={{}} + > + <View + style={ + { + "flexDirection": "column", + "gap": 8, + "paddingHorizontal": 12, + "paddingVertical": 4, + "width": "100%", + } + } + testID="message-story-message-142" + > + <A11yIndexView + accessibilityLabel="\\[ \\color{black} \\colorbox{white}{ \\boxed{ \\begin{matrix} \\;\\;\\;\\; \\overlinesegment {\\underlinesegment{ \\Huge Test\\; Test \\; in \\; Test} } & \\\\ \\;\\;\\textit{develop}\\end{matrix} } }\\]" + accessible={false} + importantForAccessibility="yes" + orderFocusType={0} + orderIndex={2} + orderKey="«r3r»" + > + <View + style={ + { + "flexDirection": "row", + } + } + > + <View + style={ + { + "alignItems": "flex-end", + "width": 46.800000000000004, + } + } + > + <View + accessible={true} + style={ + [ + { + "borderRadius": 4, + "height": 46.800000000000004, + "width": 46.800000000000004, + }, + { + "marginTop": 4, + }, + ] + } + testID="avatar" + > + <ExternalKeyboardView + canBeFocused={true} + enableA11yFocus={false} + enableContextMenu={false} + group={false} + haloEffect={true} + hasKeyDownPress={false} + hasKeyUpPress={true} + hasOnFocusChanged={true} + lockFocus={0} + onContextMenuPress={[Function]} + onFocusChange={[Function]} + onKeyUpPress={[Function]} + orderIndex={-1} + screenAutoA11yFocusDelay={300} + style={ + [ + undefined, + undefined, + ] + } + > + <RNGestureHandlerButton + activeOpacity={1} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={-1} + handlerType="NativeViewGestureHandler" + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[Function]} + rippleColor="#E4E7EA" + style={ + [ + { + "backgroundColor": undefined, + "borderRadius": undefined, + "margin": undefined, + "marginBottom": undefined, + "marginEnd": undefined, + "marginHorizontal": undefined, + "marginLeft": undefined, + "marginRight": undefined, + "marginStart": undefined, + "marginTop": undefined, + "marginVertical": undefined, + }, + { + "cursor": undefined, + }, + ] + } + underlayColor="#E4E7EA" + > + <View + collapsable={false} + style={ + { + "backgroundColor": "#E4E7EA", + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "bottom": 0, + "left": 0, + "opacity": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + /> + <View + accessibilityLabel="diego.mello's avatar" + accessible={true} + style={{}} + > + <ViewManagerAdapter_ExpoImage + borderRadius={4} + containerViewRef={{"current":"Object"}} + contentFit="cover" + contentPosition={ + { + "left": "50%", + "top": "50%", + } + } + height={46.800000000000004} + nativeViewRef={{"current":"NativeComponent"}} + onError={[Function]} + onLoad={[Function]} + onLoadStart={[Function]} + onProgress={[Function]} + placeholder={[]} + priority="high" + source={ + [ + { + "headers": { + "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", + }, + "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=93.60000000000001", + }, + ] + } + style={ + { + "borderRadius": 4, + "height": 46.800000000000004, + "width": 46.800000000000004, + } + } + transition={null} + width={46.800000000000004} + /> + </View> + </RNGestureHandlerButton> + </ExternalKeyboardView> + </View> + </View> + <View + style={ + { + "flex": 1, + "marginLeft": 10, + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + "justifyContent": "space-between", + } + } + > + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, + } + } + testID="username-header-diego.mello" + > + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#1F2329", + }, + ] + } + > + diego.mello + </Text> + </View> + <View + style={ + { + "flexDirection": "row", + } + } + /> + </View> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + > + 10:00 AM + </Text> + <View + style={ + { + "gap": 4, + } + } + > + <View + style={false} + testID="message-content-\\[ \\color{black} \\colorbox{white}{ \\boxed{ \\begin{matrix} \\;\\;\\;\\; \\overlinesegment {\\underlinesegment{ \\Huge Test\\; Test \\; in \\; Test} } & \\\\ \\;\\;\\textit{develop}\\end{matrix} } }\\]" + > + <View + style={ + { + "gap": 2, + } + } + > + <View /> + </View> + </View> + </View> + </View> + </View> + </View> + </A11yIndexView> + </View> + </View> + </View> + </ExternalKeyboardView> + </A11yIndexView> + </A11yOrderView> + </View> +</RCTScrollView> +`; + +exports[`Story Snapshots: Lists should match snapshot 1`] = ` +<RCTScrollView + style={ + { + "backgroundColor": "#FFFFFF", + } + } +> + <View> + <A11yOrderView + orderKey="«r3s»" + > + <A11yIndexView + importantForAccessibility="yes" + orderFocusType={0} + orderIndex={1} + orderKey="«r3s»" + > + <ExternalKeyboardView + canBeFocused={true} + enableA11yFocus={false} + enableContextMenu={true} + group={false} + haloEffect={true} + hasKeyDownPress={false} + hasKeyUpPress={true} + hasOnFocusChanged={true} + lockFocus={0} + onContextMenuPress={[Function]} + onFocusChange={[Function]} + onKeyUpPress={[Function]} + orderIndex={-1} + screenAutoA11yFocusDelay={300} + style={ + [ + undefined, + undefined, + ] + } + > + <View + accessibilityActions={ + [ + { + "label": "Show message actions", + "name": "showActions", + }, + ] + } + accessibilityLabel="diego.mello 10:00:00 AM * Dogs +* cats +- cats." + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": false, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onAccessibilityAction={[Function]} + onClick={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "backgroundColor": undefined, + "borderRadius": undefined, + "margin": undefined, + "marginBottom": undefined, + "marginEnd": undefined, + "marginHorizontal": undefined, + "marginLeft": undefined, + "marginRight": undefined, + "marginStart": undefined, + "marginTop": undefined, + "marginVertical": undefined, + "opacity": 1, + } + } + > + <View + style={{}} + > + <View + style={ + { + "flexDirection": "column", + "gap": 8, + "paddingHorizontal": 12, + "paddingVertical": 4, + "width": "100%", + } + } + testID="message-story-message-143" + > + <A11yIndexView + accessibilityLabel="* Dogs +* cats +- cats" + accessible={false} + importantForAccessibility="yes" + orderFocusType={0} + orderIndex={2} + orderKey="«r3s»" + > + <View + style={ + { + "flexDirection": "row", + } + } + > + <View + style={ + { + "alignItems": "flex-end", + "width": 36, + } + } + > + <View + accessible={true} + style={ + [ + { + "borderRadius": 4, + "height": 36, + "width": 36, + }, + { + "marginTop": 4, + }, + ] + } + testID="avatar" + > + <ExternalKeyboardView + canBeFocused={true} + enableA11yFocus={false} + enableContextMenu={false} + group={false} + haloEffect={true} + hasKeyDownPress={false} + hasKeyUpPress={true} + hasOnFocusChanged={true} + lockFocus={0} + onContextMenuPress={[Function]} + onFocusChange={[Function]} + onKeyUpPress={[Function]} + orderIndex={-1} + screenAutoA11yFocusDelay={300} + style={ + [ + undefined, + undefined, + ] + } + > + <RNGestureHandlerButton + activeOpacity={1} + collapsable={false} + delayLongPress={600} + enabled={true} + handlerTag={-1} + handlerType="NativeViewGestureHandler" + innerRef={null} + onActiveStateChange={[Function]} + onGestureHandlerEvent={[Function]} + onGestureHandlerStateChange={[Function]} + onPress={[Function]} + rippleColor="#E4E7EA" + style={ + [ + { + "backgroundColor": undefined, + "borderRadius": undefined, + "margin": undefined, + "marginBottom": undefined, + "marginEnd": undefined, + "marginHorizontal": undefined, + "marginLeft": undefined, + "marginRight": undefined, + "marginStart": undefined, + "marginTop": undefined, + "marginVertical": undefined, + }, + { + "cursor": undefined, + }, + ] + } + underlayColor="#E4E7EA" + > + <View + collapsable={false} + style={ + { + "backgroundColor": "#E4E7EA", + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "bottom": 0, + "left": 0, + "opacity": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + /> + <View + accessibilityLabel="diego.mello's avatar" + accessible={true} + style={{}} + > + <ViewManagerAdapter_ExpoImage + borderRadius={4} + containerViewRef={{"current":"Object"}} + contentFit="cover" + contentPosition={ + { + "left": "50%", + "top": "50%", + } + } + height={36} + nativeViewRef={{"current":"NativeComponent"}} + onError={[Function]} + onLoad={[Function]} + onLoadStart={[Function]} + onProgress={[Function]} + placeholder={[]} + priority="high" + source={ + [ + { + "headers": { + "User-Agent": "RC Mobile; ios unknown; vunknown (unknown)", + }, + "uri": "https://open.rocket.chat/avatar/diego.mello?format=png&size=72", + }, + ] + } + style={ + { + "borderRadius": 4, + "height": 36, + "width": 36, + } + } + transition={null} + width={36} + /> + </View> + </RNGestureHandlerButton> + </ExternalKeyboardView> + </View> + </View> + <View + style={ + { + "flex": 1, + "marginLeft": 10, + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + { + "alignItems": "center", + "flex": 1, + "flexDirection": "row", + "justifyContent": "space-between", + } + } + > + <View + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": undefined, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, + } + } + testID="username-header-diego.mello" + > + <Text + numberOfLines={1} + style={ + [ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "600", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#1F2329", + }, + ] + } + > + diego.mello + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 13, + "fontWeight": "400", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + > + 10:00 AM + </Text> + </View> + <View + style={ + { + "flexDirection": "row", + } + } + /> + </View> + <View + style={ + { + "gap": 4, + } + } + > + <View + style={false} + testID="message-content-* Dogs +* cats +- cats" + > + <View + style={ + { + "gap": 2, + } + } + > + <View> + <View + style={ + { + "flexDirection": "row", + } + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + • + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + <Text + style={ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + } + } + > + <Text + accessibilityLabel="Dogs" + style={ + [ + { + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + ] + } + > + Dogs + </Text> + </Text> + </Text> + </View> + <View + style={ + { + "flexDirection": "row", + } + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + • + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + <Text + style={ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + } + } + > + <Text + accessibilityLabel="cats" + style={ + [ + { + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + ] + } + > + cats + </Text> + </Text> + </Text> + </View> + </View> + <View> + <View + style={ + { + "flexDirection": "row", + } + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + • + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + <Text + style={ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + } + } + > + <Text + accessibilityLabel="cats" + style={ + [ + { + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + ] + } + > + cats + </Text> + </Text> + </Text> + </View> + </View> + </View> + </View> + </View> + </View> + </View> + </View> + </A11yIndexView> + </View> + </View> + </View> + </ExternalKeyboardView> + </A11yIndexView> + </A11yOrderView> + <A11yOrderView + orderKey="«r3t»" + > + <A11yIndexView + importantForAccessibility="yes" + orderFocusType={0} + orderIndex={1} + orderKey="«r3t»" > <ExternalKeyboardView canBeFocused={true} @@ -65503,7 +66569,7 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-142" + testID="message-story-message-144" > <A11yIndexView accessibilityLabel="1. Dogs @@ -65512,7 +66578,7 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r3r»" + orderKey="«r3t»" > <View style={ @@ -65993,13 +67059,13 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r3s»" + orderKey="«r3u»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r3s»" + orderKey="«r3u»" > <ExternalKeyboardView canBeFocused={true} @@ -66092,7 +67158,7 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-143" + testID="message-story-message-145" > <A11yIndexView accessibilityLabel="1. Dogs" @@ -66100,7 +67166,7 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r3s»" + orderKey="«r3u»" > <View style={ @@ -66493,294 +67559,294 @@ exports[`Story Snapshots: Lists should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r3t»" + orderKey="«r3v»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r3t»" - > - <ExternalKeyboardView - canBeFocused={true} - enableA11yFocus={false} - enableContextMenu={true} - group={false} - haloEffect={true} - hasKeyDownPress={false} - hasKeyUpPress={true} - hasOnFocusChanged={true} - lockFocus={0} - onContextMenuPress={[Function]} - onFocusChange={[Function]} - onKeyUpPress={[Function]} - orderIndex={-1} - screenAutoA11yFocusDelay={300} - style={ - [ - undefined, - undefined, - ] - } - > - <View - accessibilityActions={ - [ - { - "label": "Show message actions", - "name": "showActions", - }, - ] - } - accessibilityLabel="diego.mello 10:00:00 AM 2. Cats." - accessibilityRole="button" - accessibilityState={ - { - "busy": undefined, - "checked": undefined, - "disabled": false, - "expanded": undefined, - "selected": undefined, - } - } - accessibilityValue={ - { - "max": undefined, - "min": undefined, - "now": undefined, - "text": undefined, - } - } - accessible={true} - collapsable={false} - focusable={true} - onAccessibilityAction={[Function]} - onClick={[Function]} - onResponderGrant={[Function]} - onResponderMove={[Function]} - onResponderRelease={[Function]} - onResponderTerminate={[Function]} - onResponderTerminationRequest={[Function]} - onStartShouldSetResponder={[Function]} - style={ - { - "backgroundColor": undefined, - "borderRadius": undefined, - "margin": undefined, - "marginBottom": undefined, - "marginEnd": undefined, - "marginHorizontal": undefined, - "marginLeft": undefined, - "marginRight": undefined, - "marginStart": undefined, - "marginTop": undefined, - "marginVertical": undefined, - "opacity": 1, - } - } - > - <View - style={{}} - > - <View - style={ - { - "flexDirection": "column", - "gap": 8, - "paddingHorizontal": 12, - "paddingVertical": 4, - "width": "100%", - } - } - testID="message-story-message-144" - > - <A11yIndexView - accessibilityLabel="2. Cats" - accessible={false} - importantForAccessibility="yes" - orderFocusType={0} - orderIndex={2} - orderKey="«r3t»" - > - <View - style={ - { - "flexDirection": "row", - } - } - > - <View - style={ - { - "alignItems": "flex-end", - "width": 36, - } - } - /> - <View - style={ - { - "flex": 1, - "marginLeft": 10, - } - } - > - <View - onLayout={[Function]} - style={ - { - "flex": 1, - } - } - > - <View - style={ - { - "gap": 4, - } - } - > - <View - style={false} - testID="message-content-2. Cats" - > - <View - style={ - { - "gap": 2, - } - } - > - <View> - <View - style={ - { - "flexDirection": "row", - } - } - > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "fontVariant": [ - "tabular-nums", - ], - }, - { - "color": "#2F343D", - }, - ] - } - > - 2 - . - </Text> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Cats" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Cats - </Text> - </Text> - </Text> - </View> - </View> - </View> - </View> - </View> - </View> - </View> - <View - style={ - { - "flexDirection": "row", - } - } - /> - </View> - </A11yIndexView> - </View> - </View> - </View> - </ExternalKeyboardView> - </A11yIndexView> - </A11yOrderView> - </View> -</RCTScrollView> -`; - -exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` -<RCTScrollView - style={ - { - "backgroundColor": "#FFFFFF", - } - } -> - <View> - <A11yOrderView - orderKey="«r3u»" - > - <A11yIndexView - importantForAccessibility="yes" - orderFocusType={0} - orderIndex={1} - orderKey="«r3u»" + orderKey="«r3v»" + > + <ExternalKeyboardView + canBeFocused={true} + enableA11yFocus={false} + enableContextMenu={true} + group={false} + haloEffect={true} + hasKeyDownPress={false} + hasKeyUpPress={true} + hasOnFocusChanged={true} + lockFocus={0} + onContextMenuPress={[Function]} + onFocusChange={[Function]} + onKeyUpPress={[Function]} + orderIndex={-1} + screenAutoA11yFocusDelay={300} + style={ + [ + undefined, + undefined, + ] + } + > + <View + accessibilityActions={ + [ + { + "label": "Show message actions", + "name": "showActions", + }, + ] + } + accessibilityLabel="diego.mello 10:00:00 AM 2. Cats." + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": false, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onAccessibilityAction={[Function]} + onClick={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "backgroundColor": undefined, + "borderRadius": undefined, + "margin": undefined, + "marginBottom": undefined, + "marginEnd": undefined, + "marginHorizontal": undefined, + "marginLeft": undefined, + "marginRight": undefined, + "marginStart": undefined, + "marginTop": undefined, + "marginVertical": undefined, + "opacity": 1, + } + } + > + <View + style={{}} + > + <View + style={ + { + "flexDirection": "column", + "gap": 8, + "paddingHorizontal": 12, + "paddingVertical": 4, + "width": "100%", + } + } + testID="message-story-message-146" + > + <A11yIndexView + accessibilityLabel="2. Cats" + accessible={false} + importantForAccessibility="yes" + orderFocusType={0} + orderIndex={2} + orderKey="«r3v»" + > + <View + style={ + { + "flexDirection": "row", + } + } + > + <View + style={ + { + "alignItems": "flex-end", + "width": 36, + } + } + /> + <View + style={ + { + "flex": 1, + "marginLeft": 10, + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + { + "gap": 4, + } + } + > + <View + style={false} + testID="message-content-2. Cats" + > + <View + style={ + { + "gap": 2, + } + } + > + <View> + <View + style={ + { + "flexDirection": "row", + } + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "fontVariant": [ + "tabular-nums", + ], + }, + { + "color": "#2F343D", + }, + ] + } + > + 2 + . + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + <Text + style={ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + } + } + > + <Text + accessibilityLabel="Cats" + style={ + [ + { + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + ] + } + > + Cats + </Text> + </Text> + </Text> + </View> + </View> + </View> + </View> + </View> + </View> + </View> + <View + style={ + { + "flexDirection": "row", + } + } + /> + </View> + </A11yIndexView> + </View> + </View> + </View> + </ExternalKeyboardView> + </A11yIndexView> + </A11yOrderView> + </View> +</RCTScrollView> +`; + +exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` +<RCTScrollView + style={ + { + "backgroundColor": "#FFFFFF", + } + } +> + <View> + <A11yOrderView + orderKey="«r40»" + > + <A11yIndexView + importantForAccessibility="yes" + orderFocusType={0} + orderIndex={1} + orderKey="«r40»" > <ExternalKeyboardView canBeFocused={true} @@ -66875,7 +67941,7 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-146" + testID="message-story-message-148" > <A11yIndexView accessibilityLabel="* Dogs @@ -66885,7 +67951,7 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r3u»" + orderKey="«r40»" > <View style={ @@ -67414,13 +68480,13 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r3v»" + orderKey="«r41»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r3v»" + orderKey="«r41»" > <ExternalKeyboardView canBeFocused={true} @@ -67514,7 +68580,7 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-147" + testID="message-story-message-149" > <A11yIndexView accessibilityLabel="1. Dogs @@ -67523,7 +68589,7 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r3v»" + orderKey="«r41»" > <View style={ @@ -68004,13 +69070,13 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r40»" + orderKey="«r42»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r40»" + orderKey="«r42»" > <ExternalKeyboardView canBeFocused={true} @@ -68103,7 +69169,7 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-148" + testID="message-story-message-150" > <A11yIndexView accessibilityLabel="1. Dogs" @@ -68111,7 +69177,7 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r40»" + orderKey="«r42»" > <View style={ @@ -68504,13 +69570,13 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r41»" + orderKey="«r43»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r41»" + orderKey="«r43»" > <ExternalKeyboardView canBeFocused={true} @@ -68589,287 +69655,6 @@ exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` "opacity": 1, } } - > - <View - style={{}} - > - <View - style={ - { - "flexDirection": "column", - "gap": 8, - "paddingHorizontal": 12, - "paddingVertical": 4, - "width": "100%", - } - } - testID="message-story-message-149" - > - <A11yIndexView - accessibilityLabel="2. Cats" - accessible={false} - importantForAccessibility="yes" - orderFocusType={0} - orderIndex={2} - orderKey="«r41»" - > - <View - style={ - { - "flexDirection": "row", - } - } - > - <View - style={ - { - "alignItems": "flex-end", - "width": 46.800000000000004, - } - } - /> - <View - style={ - { - "flex": 1, - "marginLeft": 10, - } - } - > - <View - onLayout={[Function]} - style={ - { - "flex": 1, - } - } - > - <View - style={ - { - "gap": 4, - } - } - > - <View - style={false} - testID="message-content-2. Cats" - > - <View - style={ - { - "gap": 2, - } - } - > - <View> - <View - style={ - { - "flexDirection": "row", - } - } - > - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "fontVariant": [ - "tabular-nums", - ], - }, - { - "color": "#2F343D", - }, - ] - } - > - 2 - . - </Text> - <Text - style={ - [ - { - "backgroundColor": "transparent", - "fontFamily": "Inter", - "fontSize": 16, - "fontWeight": "400", - "lineHeight": 22, - "textAlign": "left", - }, - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - }, - { - "color": "#2F343D", - }, - ] - } - > - <Text - style={ - { - "backgroundColor": "transparent", - "flexShrink": 1, - "fontFamily": "Inter", - "fontWeight": "400", - "textAlign": "left", - } - } - > - <Text - accessibilityLabel="Cats" - style={ - [ - { - "flexShrink": 1, - "fontSize": 16, - "lineHeight": 22, - }, - { - "color": "#2F343D", - }, - ] - } - > - Cats - </Text> - </Text> - </Text> - </View> - </View> - </View> - </View> - </View> - </View> - </View> - <View - style={ - { - "flexDirection": "row", - } - } - /> - </View> - </A11yIndexView> - </View> - </View> - </View> - </ExternalKeyboardView> - </A11yIndexView> - </A11yOrderView> - </View> -</RCTScrollView> -`; - -exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` -<RCTScrollView - style={ - { - "backgroundColor": "#FFFFFF", - } - } -> - <View> - <A11yOrderView - orderKey="«r42»" - > - <A11yIndexView - importantForAccessibility="yes" - orderFocusType={0} - orderIndex={1} - orderKey="«r42»" - > - <ExternalKeyboardView - canBeFocused={true} - enableA11yFocus={false} - enableContextMenu={true} - group={false} - haloEffect={true} - hasKeyDownPress={false} - hasKeyUpPress={true} - hasOnFocusChanged={true} - lockFocus={0} - onContextMenuPress={[Function]} - onFocusChange={[Function]} - onKeyUpPress={[Function]} - orderIndex={-1} - screenAutoA11yFocusDelay={300} - style={ - [ - undefined, - undefined, - ] - } - > - <View - accessibilityActions={ - [ - { - "label": "Show message actions", - "name": "showActions", - }, - ] - } - accessibilityLabel="johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername 10:00:00 AM this is a normal message." - accessibilityRole="button" - accessibilityState={ - { - "busy": undefined, - "checked": undefined, - "disabled": false, - "expanded": undefined, - "selected": undefined, - } - } - accessibilityValue={ - { - "max": undefined, - "min": undefined, - "now": undefined, - "text": undefined, - } - } - accessible={true} - collapsable={false} - focusable={true} - onAccessibilityAction={[Function]} - onClick={[Function]} - onResponderGrant={[Function]} - onResponderMove={[Function]} - onResponderRelease={[Function]} - onResponderTerminate={[Function]} - onResponderTerminationRequest={[Function]} - onStartShouldSetResponder={[Function]} - style={ - { - "backgroundColor": undefined, - "borderRadius": undefined, - "margin": undefined, - "marginBottom": undefined, - "marginEnd": undefined, - "marginHorizontal": undefined, - "marginLeft": undefined, - "marginRight": undefined, - "marginStart": undefined, - "marginTop": undefined, - "marginVertical": undefined, - "opacity": 1, - } - } > <View style={{}} @@ -68885,6 +69670,287 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` } } testID="message-story-message-151" + > + <A11yIndexView + accessibilityLabel="2. Cats" + accessible={false} + importantForAccessibility="yes" + orderFocusType={0} + orderIndex={2} + orderKey="«r43»" + > + <View + style={ + { + "flexDirection": "row", + } + } + > + <View + style={ + { + "alignItems": "flex-end", + "width": 46.800000000000004, + } + } + /> + <View + style={ + { + "flex": 1, + "marginLeft": 10, + } + } + > + <View + onLayout={[Function]} + style={ + { + "flex": 1, + } + } + > + <View + style={ + { + "gap": 4, + } + } + > + <View + style={false} + testID="message-content-2. Cats" + > + <View + style={ + { + "gap": 2, + } + } + > + <View> + <View + style={ + { + "flexDirection": "row", + } + } + > + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "fontVariant": [ + "tabular-nums", + ], + }, + { + "color": "#2F343D", + }, + ] + } + > + 2 + . + </Text> + <Text + style={ + [ + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontWeight": "400", + "lineHeight": 22, + "textAlign": "left", + }, + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + <Text + style={ + { + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontWeight": "400", + "textAlign": "left", + } + } + > + <Text + accessibilityLabel="Cats" + style={ + [ + { + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + ] + } + > + Cats + </Text> + </Text> + </Text> + </View> + </View> + </View> + </View> + </View> + </View> + </View> + <View + style={ + { + "flexDirection": "row", + } + } + /> + </View> + </A11yIndexView> + </View> + </View> + </View> + </ExternalKeyboardView> + </A11yIndexView> + </A11yOrderView> + </View> +</RCTScrollView> +`; + +exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` +<RCTScrollView + style={ + { + "backgroundColor": "#FFFFFF", + } + } +> + <View> + <A11yOrderView + orderKey="«r44»" + > + <A11yIndexView + importantForAccessibility="yes" + orderFocusType={0} + orderIndex={1} + orderKey="«r44»" + > + <ExternalKeyboardView + canBeFocused={true} + enableA11yFocus={false} + enableContextMenu={true} + group={false} + haloEffect={true} + hasKeyDownPress={false} + hasKeyUpPress={true} + hasOnFocusChanged={true} + lockFocus={0} + onContextMenuPress={[Function]} + onFocusChange={[Function]} + onKeyUpPress={[Function]} + orderIndex={-1} + screenAutoA11yFocusDelay={300} + style={ + [ + undefined, + undefined, + ] + } + > + <View + accessibilityActions={ + [ + { + "label": "Show message actions", + "name": "showActions", + }, + ] + } + accessibilityLabel="johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername 10:00:00 AM this is a normal message." + accessibilityRole="button" + accessibilityState={ + { + "busy": undefined, + "checked": undefined, + "disabled": false, + "expanded": undefined, + "selected": undefined, + } + } + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onAccessibilityAction={[Function]} + onClick={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "backgroundColor": undefined, + "borderRadius": undefined, + "margin": undefined, + "marginBottom": undefined, + "marginEnd": undefined, + "marginHorizontal": undefined, + "marginLeft": undefined, + "marginRight": undefined, + "marginStart": undefined, + "marginTop": undefined, + "marginVertical": undefined, + "opacity": 1, + } + } + > + <View + style={{}} + > + <View + style={ + { + "flexDirection": "column", + "gap": 8, + "paddingHorizontal": 12, + "paddingVertical": 4, + "width": "100%", + } + } + testID="message-story-message-153" > <A11yIndexView accessibilityLabel="this is a normal message" @@ -68892,7 +69958,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r42»" + orderKey="«r44»" > <View style={ @@ -69243,13 +70309,13 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r43»" + orderKey="«r45»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r43»" + orderKey="«r45»" > <ExternalKeyboardView canBeFocused={true} @@ -69342,7 +70408,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-152" + testID="message-story-message-154" > <A11yIndexView accessibilityLabel="Edited message" @@ -69350,7 +70416,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r43»" + orderKey="«r45»" > <View style={ @@ -69737,13 +70803,13 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r44»" + orderKey="«r46»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r44»" + orderKey="«r46»" > <ExternalKeyboardView canBeFocused={true} @@ -69836,7 +70902,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-153" + testID="message-story-message-155" > <A11yIndexView accessibilityLabel="Translated message" @@ -69845,7 +70911,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r44»" + orderKey="«r46»" > <View style={ @@ -70231,13 +71297,13 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r45»" + orderKey="«r47»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r45»" + orderKey="«r47»" > <ExternalKeyboardView canBeFocused={true} @@ -70330,7 +71396,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-154" + testID="message-story-message-156" > <A11yIndexView accessibilityLabel="Encrypted message" @@ -70338,7 +71404,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r45»" + orderKey="«r47»" > <View style={ @@ -70726,7 +71792,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r46»" + orderKey="«r48»" > <View style={ @@ -70738,7 +71804,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-155" + testID="message-story-message-157" > <A11yIndexView accessibilityLabel="Error message" @@ -70746,7 +71812,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r46»" + orderKey="«r48»" > <View style={ @@ -71169,13 +72235,13 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r47»" + orderKey="«r49»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r47»" + orderKey="«r49»" > <ExternalKeyboardView canBeFocused={true} @@ -71268,7 +72334,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-156" + testID="message-story-message-158" > <A11yIndexView accessibilityLabel="Message with read receipt" @@ -71276,7 +72342,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r47»" + orderKey="«r49»" > <View style={ @@ -71658,13 +72724,13 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r48»" + orderKey="«r4a»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r48»" + orderKey="«r4a»" > <ExternalKeyboardView canBeFocused={true} @@ -71757,7 +72823,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-157" + testID="message-story-message-159" > <A11yIndexView accessibilityLabel="Message with read receipt" @@ -71765,7 +72831,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r48»" + orderKey="«r4a»" > <View style={ @@ -72183,7 +73249,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r49»" + orderKey="«r4b»" > <View style={ @@ -72195,7 +73261,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-158" + testID="message-story-message-160" > <A11yIndexView accessibilityLabel="Show all icons " @@ -72204,7 +73270,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r49»" + orderKey="«r4b»" > <View style={ @@ -72762,7 +73828,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r4a»" + orderKey="«r4c»" > <View style={ @@ -72774,7 +73840,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-159" + testID="message-story-message-161" > <A11yIndexView accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." @@ -72783,7 +73849,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4a»" + orderKey="«r4c»" > <View style={ @@ -73341,7 +74407,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r4b»" + orderKey="«r4d»" > <View style={ @@ -73353,7 +74419,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-161" + testID="message-story-message-163" > <A11yIndexView accessibilityLabel="small message" @@ -73362,7 +74428,7 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4b»" + orderKey="«r4d»" > <View style={ @@ -73933,13 +74999,13 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r4c»" + orderKey="«r4e»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r4c»" + orderKey="«r4e»" > <ExternalKeyboardView canBeFocused={true} @@ -74032,7 +75098,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-163" + testID="message-story-message-165" > <A11yIndexView accessibilityLabel="this is a normal message" @@ -74040,7 +75106,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4c»" + orderKey="«r4e»" > <View style={ @@ -74391,13 +75457,13 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r4d»" + orderKey="«r4f»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r4d»" + orderKey="«r4f»" > <ExternalKeyboardView canBeFocused={true} @@ -74490,7 +75556,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-164" + testID="message-story-message-166" > <A11yIndexView accessibilityLabel="Edited message" @@ -74498,7 +75564,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4d»" + orderKey="«r4f»" > <View style={ @@ -74885,13 +75951,13 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r4e»" + orderKey="«r4g»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r4e»" + orderKey="«r4g»" > <ExternalKeyboardView canBeFocused={true} @@ -74984,7 +76050,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-165" + testID="message-story-message-167" > <A11yIndexView accessibilityLabel="Translated message" @@ -74993,7 +76059,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4e»" + orderKey="«r4g»" > <View style={ @@ -75379,13 +76445,13 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r4f»" + orderKey="«r4h»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r4f»" + orderKey="«r4h»" > <ExternalKeyboardView canBeFocused={true} @@ -75478,7 +76544,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-166" + testID="message-story-message-168" > <A11yIndexView accessibilityLabel="Encrypted message" @@ -75486,7 +76552,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4f»" + orderKey="«r4h»" > <View style={ @@ -75874,7 +76940,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r4g»" + orderKey="«r4i»" > <View style={ @@ -75886,7 +76952,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-167" + testID="message-story-message-169" > <A11yIndexView accessibilityLabel="Error message" @@ -75894,7 +76960,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4g»" + orderKey="«r4i»" > <View style={ @@ -76317,13 +77383,13 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r4h»" + orderKey="«r4j»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r4h»" + orderKey="«r4j»" > <ExternalKeyboardView canBeFocused={true} @@ -76416,7 +77482,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-168" + testID="message-story-message-170" > <A11yIndexView accessibilityLabel="Message with read receipt" @@ -76424,7 +77490,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4h»" + orderKey="«r4j»" > <View style={ @@ -76806,13 +77872,13 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r4i»" + orderKey="«r4k»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r4i»" + orderKey="«r4k»" > <ExternalKeyboardView canBeFocused={true} @@ -76905,7 +77971,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-169" + testID="message-story-message-171" > <A11yIndexView accessibilityLabel="Message with read receipt" @@ -76913,7 +77979,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4i»" + orderKey="«r4k»" > <View style={ @@ -77331,7 +78397,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r4j»" + orderKey="«r4l»" > <View style={ @@ -77343,7 +78409,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-170" + testID="message-story-message-172" > <A11yIndexView accessibilityLabel="Show all icons " @@ -77352,7 +78418,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4j»" + orderKey="«r4l»" > <View style={ @@ -77910,7 +78976,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r4k»" + orderKey="«r4m»" > <View style={ @@ -77922,7 +78988,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-171" + testID="message-story-message-173" > <A11yIndexView accessibilityLabel="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." @@ -77931,7 +78997,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4k»" + orderKey="«r4m»" > <View style={ @@ -78489,7 +79555,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r4l»" + orderKey="«r4n»" > <View style={ @@ -78501,7 +79567,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-173" + testID="message-story-message-175" > <A11yIndexView accessibilityLabel="small message" @@ -78510,7 +79576,7 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4l»" + orderKey="«r4n»" > <View style={ @@ -79081,13 +80147,13 @@ exports[`Story Snapshots: Mentions should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r4m»" + orderKey="«r4o»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r4m»" + orderKey="«r4o»" > <ExternalKeyboardView canBeFocused={true} @@ -79180,7 +80246,7 @@ exports[`Story Snapshots: Mentions should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-175" + testID="message-story-message-177" > <A11yIndexView accessibilityLabel="@rocket.cat @diego.mello @all @here #general @team" @@ -79188,7 +80254,7 @@ exports[`Story Snapshots: Mentions should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4m»" + orderKey="«r4o»" > <View style={ @@ -79718,13 +80784,13 @@ exports[`Story Snapshots: Mentions should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r4n»" + orderKey="«r4p»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r4n»" + orderKey="«r4p»" > <ExternalKeyboardView canBeFocused={true} @@ -79817,7 +80883,7 @@ exports[`Story Snapshots: Mentions should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-176" + testID="message-story-message-178" > <A11yIndexView accessibilityLabel="@rocket.cat Lorem ipsum dolor @diego.mello sit amet, @all consectetur adipiscing @here elit, sed do eiusmod tempor #general incididunt ut labore et dolore magna aliqua." @@ -79825,7 +80891,7 @@ exports[`Story Snapshots: Mentions should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4n»" + orderKey="«r4p»" > <View style={ @@ -80350,13 +81416,13 @@ exports[`Story Snapshots: MentionsLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r4o»" + orderKey="«r4q»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r4o»" + orderKey="«r4q»" > <ExternalKeyboardView canBeFocused={true} @@ -80449,7 +81515,7 @@ exports[`Story Snapshots: MentionsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-177" + testID="message-story-message-179" > <A11yIndexView accessibilityLabel="@rocket.cat @diego.mello @all @here #general @team" @@ -80457,7 +81523,7 @@ exports[`Story Snapshots: MentionsLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4o»" + orderKey="«r4q»" > <View style={ @@ -80987,13 +82053,13 @@ exports[`Story Snapshots: MentionsLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r4p»" + orderKey="«r4r»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r4p»" + orderKey="«r4r»" > <ExternalKeyboardView canBeFocused={true} @@ -81086,7 +82152,7 @@ exports[`Story Snapshots: MentionsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-178" + testID="message-story-message-180" > <A11yIndexView accessibilityLabel="@rocket.cat Lorem ipsum dolor @diego.mello sit amet, @all consectetur adipiscing @here elit, sed do eiusmod tempor #general incididunt ut labore et dolore magna aliqua." @@ -81094,7 +82160,7 @@ exports[`Story Snapshots: MentionsLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4p»" + orderKey="«r4r»" > <View style={ @@ -81619,13 +82685,13 @@ exports[`Story Snapshots: Message should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r4q»" + orderKey="«r4s»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r4q»" + orderKey="«r4s»" > <ExternalKeyboardView canBeFocused={true} @@ -81718,7 +82784,7 @@ exports[`Story Snapshots: Message should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-179" + testID="message-story-message-181" > <A11yIndexView accessibilityLabel="" @@ -81726,7 +82792,7 @@ exports[`Story Snapshots: Message should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4q»" + orderKey="«r4s»" > <View style={ @@ -82029,13 +83095,13 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` > <View> <A11yOrderView - orderKey="«r4r»" + orderKey="«r4t»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r4r»" + orderKey="«r4t»" > <ExternalKeyboardView canBeFocused={true} @@ -82128,7 +83194,7 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` "width": "100%", } } - testID="message-story-message-180" + testID="message-story-message-182" > <A11yIndexView accessibilityLabel="Forwarded message with file inside" @@ -82136,7 +83202,7 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4r»" + orderKey="«r4t»" > <View style={ @@ -82782,13 +83848,13 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r4s»" + orderKey="«r4u»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r4s»" + orderKey="«r4u»" > <ExternalKeyboardView canBeFocused={true} @@ -82881,7 +83947,7 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` "width": "100%", } } - testID="message-story-message-181" + testID="message-story-message-183" > <A11yIndexView accessibilityLabel="Forwarded message with nested image" @@ -82889,7 +83955,7 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4s»" + orderKey="«r4u»" > <View style={ @@ -83589,13 +84655,13 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r4t»" + orderKey="«r4v»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r4t»" + orderKey="«r4v»" > <ExternalKeyboardView canBeFocused={true} @@ -83688,7 +84754,7 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` "width": "100%", } } - testID="message-story-message-182" + testID="message-story-message-184" > <A11yIndexView accessibilityLabel="Forwarded message with nested audio" @@ -83696,7 +84762,7 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4t»" + orderKey="«r4v»" > <View style={ @@ -84408,13 +85474,13 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r4u»" + orderKey="«r50»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r4u»" + orderKey="«r50»" > <ExternalKeyboardView canBeFocused={true} @@ -84507,7 +85573,7 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` "width": "100%", } } - testID="message-story-message-183" + testID="message-story-message-185" > <A11yIndexView accessibilityLabel="Forwarded message with nested video" @@ -84515,7 +85581,7 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4u»" + orderKey="«r50»" > <View style={ @@ -85118,13 +86184,13 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r4v»" + orderKey="«r51»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r4v»" + orderKey="«r51»" > <ExternalKeyboardView canBeFocused={true} @@ -85217,7 +86283,7 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` "width": "100%", } } - testID="message-story-message-184" + testID="message-story-message-186" > <A11yIndexView accessibilityLabel="Message forwarded twice with file inside" @@ -85225,7 +86291,7 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r4v»" + orderKey="«r51»" > <View style={ @@ -86020,13 +87086,13 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r50»" + orderKey="«r52»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r50»" + orderKey="«r52»" > <ExternalKeyboardView canBeFocused={true} @@ -86119,7 +87185,7 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-185" + testID="message-story-message-187" > <A11yIndexView accessibilityLabel="I'm fine!" @@ -86127,7 +87193,7 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r50»" + orderKey="«r52»" > <View style={ @@ -86509,13 +87575,13 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r51»" + orderKey="«r53»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r51»" + orderKey="«r53»" > <ExternalKeyboardView canBeFocused={true} @@ -86608,7 +87674,7 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-186" + testID="message-story-message-188" > <A11yIndexView accessibilityLabel="I'm fine!" @@ -86616,7 +87682,7 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r51»" + orderKey="«r53»" > <View style={ @@ -86766,13 +87832,13 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r52»" + orderKey="«r54»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r52»" + orderKey="«r54»" > <ExternalKeyboardView canBeFocused={true} @@ -86865,7 +87931,7 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-188" + testID="message-story-message-190" > <A11yIndexView accessibilityLabel="I'm fine!" @@ -86873,7 +87939,7 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r52»" + orderKey="«r54»" > <View style={ @@ -87255,13 +88321,13 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r53»" + orderKey="«r55»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r53»" + orderKey="«r55»" > <ExternalKeyboardView canBeFocused={true} @@ -87354,7 +88420,7 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-189" + testID="message-story-message-191" > <A11yIndexView accessibilityLabel="I'm fine!" @@ -87362,7 +88428,7 @@ exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r53»" + orderKey="«r55»" > <View style={ @@ -87525,13 +88591,13 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot > <View> <A11yOrderView - orderKey="«r54»" + orderKey="«r56»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r54»" + orderKey="«r56»" > <ExternalKeyboardView canBeFocused={true} @@ -87624,7 +88690,7 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot "width": "100%", } } - testID="message-story-message-191" + testID="message-story-message-193" > <A11yIndexView accessibilityLabel="I'm fine!" @@ -87632,7 +88698,7 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r54»" + orderKey="«r56»" > <View style={ @@ -88014,13 +89080,13 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r55»" + orderKey="«r57»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r55»" + orderKey="«r57»" > <ExternalKeyboardView canBeFocused={true} @@ -88113,7 +89179,7 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot "width": "100%", } } - testID="message-story-message-192" + testID="message-story-message-194" > <A11yIndexView accessibilityLabel="I'm fine!" @@ -88121,7 +89187,7 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r55»" + orderKey="«r57»" > <View style={ @@ -88271,13 +89337,13 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r56»" + orderKey="«r58»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r56»" + orderKey="«r58»" > <ExternalKeyboardView canBeFocused={true} @@ -88370,7 +89436,7 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot "width": "100%", } } - testID="message-story-message-194" + testID="message-story-message-196" > <A11yIndexView accessibilityLabel="I'm fine!" @@ -88378,7 +89444,7 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r56»" + orderKey="«r58»" > <View style={ @@ -88760,13 +89826,13 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r57»" + orderKey="«r59»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r57»" + orderKey="«r59»" > <ExternalKeyboardView canBeFocused={true} @@ -88859,7 +89925,7 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot "width": "100%", } } - testID="message-story-message-195" + testID="message-story-message-197" > <A11yIndexView accessibilityLabel="I'm fine!" @@ -88867,7 +89933,7 @@ exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r57»" + orderKey="«r59»" > <View style={ @@ -89030,13 +90096,13 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r58»" + orderKey="«r5a»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r58»" + orderKey="«r5a»" > <ExternalKeyboardView canBeFocused={true} @@ -89129,7 +90195,7 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-197" + testID="message-story-message-199" > <A11yIndexView accessibilityLabel="I'm fine!" @@ -89137,7 +90203,7 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r58»" + orderKey="«r5a»" > <View style={ @@ -89660,13 +90726,13 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r59»" + orderKey="«r5b»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r59»" + orderKey="«r5b»" > <ExternalKeyboardView canBeFocused={true} @@ -89759,7 +90825,7 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-198" + testID="message-story-message-200" > <A11yIndexView accessibilityLabel="I'm fine!" @@ -89767,7 +90833,7 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r59»" + orderKey="«r5b»" > <View style={ @@ -90597,13 +91663,13 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5a»" + orderKey="«r5c»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5a»" + orderKey="«r5c»" > <ExternalKeyboardView canBeFocused={true} @@ -90696,7 +91762,7 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-199" + testID="message-story-message-201" > <A11yIndexView accessibilityLabel="Looks cool!" @@ -90704,7 +91770,7 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5a»" + orderKey="«r5c»" > <View style={ @@ -91404,13 +92470,13 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5b»" + orderKey="«r5d»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5b»" + orderKey="«r5d»" > <ExternalKeyboardView canBeFocused={true} @@ -91503,7 +92569,7 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-200" + testID="message-story-message-202" > <A11yIndexView accessibilityLabel="Yes, I am" @@ -91511,7 +92577,7 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5b»" + orderKey="«r5d»" > <View style={ @@ -92285,13 +93351,13 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r5c»" + orderKey="«r5e»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5c»" + orderKey="«r5e»" > <ExternalKeyboardView canBeFocused={true} @@ -92384,7 +93450,7 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-201" + testID="message-story-message-203" > <A11yIndexView accessibilityLabel="Here is the file" @@ -92392,7 +93458,7 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5c»" + orderKey="«r5e»" > <View style={ @@ -92902,13 +93968,13 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5d»" + orderKey="«r5f»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5d»" + orderKey="«r5f»" > <ExternalKeyboardView canBeFocused={true} @@ -93001,7 +94067,7 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-202" + testID="message-story-message-204" > <A11yIndexView accessibilityLabel="Got it!" @@ -93009,7 +94075,7 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5d»" + orderKey="«r5f»" > <View style={ @@ -93596,13 +94662,13 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5e»" + orderKey="«r5g»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5e»" + orderKey="«r5g»" > <ExternalKeyboardView canBeFocused={true} @@ -93695,7 +94761,7 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-203" + testID="message-story-message-205" > <A11yIndexView accessibilityLabel="Multiple files" @@ -93703,7 +94769,7 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5e»" + orderKey="«r5g»" > <View style={ @@ -94377,13 +95443,13 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot > <View> <A11yOrderView - orderKey="«r5f»" + orderKey="«r5h»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5f»" + orderKey="«r5h»" > <ExternalKeyboardView canBeFocused={true} @@ -94476,7 +95542,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot "width": "100%", } } - testID="message-story-message-204" + testID="message-story-message-206" > <A11yIndexView accessibilityLabel="Here is the file" @@ -94484,7 +95550,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5f»" + orderKey="«r5h»" > <View style={ @@ -94994,13 +96060,13 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5g»" + orderKey="«r5i»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5g»" + orderKey="«r5i»" > <ExternalKeyboardView canBeFocused={true} @@ -95093,7 +96159,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot "width": "100%", } } - testID="message-story-message-205" + testID="message-story-message-207" > <A11yIndexView accessibilityLabel="Got it!" @@ -95101,7 +96167,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5g»" + orderKey="«r5i»" > <View style={ @@ -95688,13 +96754,13 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5h»" + orderKey="«r5j»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5h»" + orderKey="«r5j»" > <ExternalKeyboardView canBeFocused={true} @@ -95787,7 +96853,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot "width": "100%", } } - testID="message-story-message-206" + testID="message-story-message-208" > <A11yIndexView accessibilityLabel="Multiple files" @@ -95795,7 +96861,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5h»" + orderKey="«r5j»" > <View style={ @@ -96456,13 +97522,13 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5i»" + orderKey="«r5k»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5i»" + orderKey="«r5k»" > <ExternalKeyboardView canBeFocused={true} @@ -96555,7 +97621,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot "width": "100%", } } - testID="message-story-message-207" + testID="message-story-message-209" > <A11yIndexView accessibilityLabel="Forwarded message with nested audio" @@ -96563,7 +97629,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5i»" + orderKey="«r5k»" > <View style={ @@ -97275,13 +98341,13 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5j»" + orderKey="«r5l»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5j»" + orderKey="«r5l»" > <ExternalKeyboardView canBeFocused={true} @@ -97374,7 +98440,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot "width": "100%", } } - testID="message-story-message-208" + testID="message-story-message-210" > <A11yIndexView accessibilityLabel="Forwarded message with nested video" @@ -97382,7 +98448,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5j»" + orderKey="«r5l»" > <View style={ @@ -97985,13 +99051,13 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5k»" + orderKey="«r5m»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5k»" + orderKey="«r5m»" > <ExternalKeyboardView canBeFocused={true} @@ -98084,7 +99150,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot "width": "100%", } } - testID="message-story-message-209" + testID="message-story-message-211" > <A11yIndexView accessibilityLabel="Message forwarded twice with file inside" @@ -98092,7 +99158,7 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5k»" + orderKey="«r5m»" > <View style={ @@ -98887,13 +99953,13 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = > <View> <A11yOrderView - orderKey="«r5l»" + orderKey="«r5n»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5l»" + orderKey="«r5n»" > <ExternalKeyboardView canBeFocused={true} @@ -98986,7 +100052,7 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = "width": "100%", } } - testID="message-story-message-210" + testID="message-story-message-212" > <A11yIndexView accessibilityLabel="I'm fine!" @@ -98994,7 +100060,7 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5l»" + orderKey="«r5n»" > <View style={ @@ -99517,13 +100583,13 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5m»" + orderKey="«r5o»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5m»" + orderKey="«r5o»" > <ExternalKeyboardView canBeFocused={true} @@ -99616,7 +100682,7 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = "width": "100%", } } - testID="message-story-message-211" + testID="message-story-message-213" > <A11yIndexView accessibilityLabel="I'm fine!" @@ -99624,7 +100690,7 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5m»" + orderKey="«r5o»" > <View style={ @@ -100191,13 +101257,13 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5n»" + orderKey="«r5p»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5n»" + orderKey="«r5p»" > <ExternalKeyboardView canBeFocused={true} @@ -100290,7 +101356,7 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = "width": "100%", } } - testID="message-story-message-212" + testID="message-story-message-214" > <A11yIndexView accessibilityLabel="Looks cool!" @@ -100298,7 +101364,7 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5n»" + orderKey="«r5p»" > <View style={ @@ -100998,13 +102064,13 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5o»" + orderKey="«r5q»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5o»" + orderKey="«r5q»" > <ExternalKeyboardView canBeFocused={true} @@ -101097,7 +102163,7 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = "width": "100%", } } - testID="message-story-message-213" + testID="message-story-message-215" > <A11yIndexView accessibilityLabel="Yes, I am" @@ -101105,7 +102171,7 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5o»" + orderKey="«r5q»" > <View style={ @@ -101879,13 +102945,13 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r5p»" + orderKey="«r5r»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5p»" + orderKey="«r5r»" > <ExternalKeyboardView canBeFocused={true} @@ -101979,7 +103045,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-214" + testID="message-story-message-216" > <A11yIndexView accessibilityLabel="How are you?" @@ -101987,7 +103053,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5p»" + orderKey="«r5r»" > <View style={ @@ -102687,13 +103753,13 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5q»" + orderKey="«r5s»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5q»" + orderKey="«r5s»" > <ExternalKeyboardView canBeFocused={true} @@ -103071,7 +104137,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5q»" + orderKey="«r5s»" style={ { "flex": 1, @@ -103124,13 +104190,13 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5r»" + orderKey="«r5t»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5r»" + orderKey="«r5t»" > <ExternalKeyboardView canBeFocused={true} @@ -103508,7 +104574,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5r»" + orderKey="«r5t»" style={ { "flex": 1, @@ -103561,13 +104627,13 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5s»" + orderKey="«r5u»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5s»" + orderKey="«r5u»" > <ExternalKeyboardView canBeFocused={true} @@ -103945,7 +105011,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5s»" + orderKey="«r5u»" style={ { "flex": 1, @@ -103998,13 +105064,13 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5t»" + orderKey="«r5v»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5t»" + orderKey="«r5v»" > <ExternalKeyboardView canBeFocused={true} @@ -104382,7 +105448,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5t»" + orderKey="«r5v»" style={ { "flex": 1, @@ -104435,13 +105501,13 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5u»" + orderKey="«r60»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5u»" + orderKey="«r60»" > <ExternalKeyboardView canBeFocused={true} @@ -104819,7 +105885,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5u»" + orderKey="«r60»" style={ { "flex": 1, @@ -104872,13 +105938,13 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r5v»" + orderKey="«r61»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r5v»" + orderKey="«r61»" > <ExternalKeyboardView canBeFocused={true} @@ -105256,7 +106322,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r5v»" + orderKey="«r61»" style={ { "flex": 1, @@ -105309,13 +106375,13 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r60»" + orderKey="«r62»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r60»" + orderKey="«r62»" > <ExternalKeyboardView canBeFocused={true} @@ -105693,7 +106759,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r60»" + orderKey="«r62»" style={ { "flex": 1, @@ -105746,13 +106812,13 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r61»" + orderKey="«r63»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r61»" + orderKey="«r63»" > <ExternalKeyboardView canBeFocused={true} @@ -106130,7 +107196,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r61»" + orderKey="«r63»" style={ { "flex": 1, @@ -106183,13 +107249,13 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r62»" + orderKey="«r64»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r62»" + orderKey="«r64»" > <ExternalKeyboardView canBeFocused={true} @@ -106567,7 +107633,7 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r62»" + orderKey="«r64»" style={ { "flex": 1, @@ -106633,13 +107699,13 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = > <View> <A11yOrderView - orderKey="«r63»" + orderKey="«r65»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r63»" + orderKey="«r65»" > <ExternalKeyboardView canBeFocused={true} @@ -106733,7 +107799,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = "width": "100%", } } - testID="message-story-message-233" + testID="message-story-message-235" > <A11yIndexView accessibilityLabel="How are you?" @@ -106741,7 +107807,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r63»" + orderKey="«r65»" > <View style={ @@ -107441,13 +108507,13 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r64»" + orderKey="«r66»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r64»" + orderKey="«r66»" > <ExternalKeyboardView canBeFocused={true} @@ -107825,7 +108891,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r64»" + orderKey="«r66»" style={ { "flex": 1, @@ -107878,13 +108944,13 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r65»" + orderKey="«r67»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r65»" + orderKey="«r67»" > <ExternalKeyboardView canBeFocused={true} @@ -108262,7 +109328,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r65»" + orderKey="«r67»" style={ { "flex": 1, @@ -108315,13 +109381,13 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r66»" + orderKey="«r68»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r66»" + orderKey="«r68»" > <ExternalKeyboardView canBeFocused={true} @@ -108699,7 +109765,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r66»" + orderKey="«r68»" style={ { "flex": 1, @@ -108752,13 +109818,13 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r67»" + orderKey="«r69»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r67»" + orderKey="«r69»" > <ExternalKeyboardView canBeFocused={true} @@ -109136,7 +110202,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r67»" + orderKey="«r69»" style={ { "flex": 1, @@ -109189,13 +110255,13 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r68»" + orderKey="«r6a»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r68»" + orderKey="«r6a»" > <ExternalKeyboardView canBeFocused={true} @@ -109573,7 +110639,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r68»" + orderKey="«r6a»" style={ { "flex": 1, @@ -109626,13 +110692,13 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r69»" + orderKey="«r6b»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r69»" + orderKey="«r6b»" > <ExternalKeyboardView canBeFocused={true} @@ -110010,7 +111076,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r69»" + orderKey="«r6b»" style={ { "flex": 1, @@ -110063,13 +111129,13 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r6a»" + orderKey="«r6c»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6a»" + orderKey="«r6c»" > <ExternalKeyboardView canBeFocused={true} @@ -110447,7 +111513,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6a»" + orderKey="«r6c»" style={ { "flex": 1, @@ -110500,13 +111566,13 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r6b»" + orderKey="«r6d»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6b»" + orderKey="«r6d»" > <ExternalKeyboardView canBeFocused={true} @@ -110884,7 +111950,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6b»" + orderKey="«r6d»" style={ { "flex": 1, @@ -110937,13 +112003,13 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r6c»" + orderKey="«r6e»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6c»" + orderKey="«r6e»" > <ExternalKeyboardView canBeFocused={true} @@ -111321,7 +112387,7 @@ exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6c»" + orderKey="«r6e»" style={ { "flex": 1, @@ -111387,13 +112453,13 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r6d»" + orderKey="«r6f»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6d»" + orderKey="«r6f»" > <ExternalKeyboardView canBeFocused={true} @@ -111486,7 +112552,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-252" + testID="message-story-message-254" > <A11yIndexView accessibilityLabel="Small name (2 chars)" @@ -111494,7 +112560,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6d»" + orderKey="«r6f»" > <View style={ @@ -111845,7 +112911,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r6e»" + orderKey="«r6g»" > <View style={ @@ -111857,7 +112923,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-253" + testID="message-story-message-255" > <A11yIndexView accessibilityLabel="Small name with icons" @@ -111866,7 +112932,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6e»" + orderKey="«r6g»" > <View style={ @@ -112453,13 +113519,13 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r6f»" + orderKey="«r6h»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6f»" + orderKey="«r6h»" > <ExternalKeyboardView canBeFocused={true} @@ -112552,7 +113618,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-254" + testID="message-story-message-256" > <A11yIndexView accessibilityLabel="Medium name (8 chars)" @@ -112560,7 +113626,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6f»" + orderKey="«r6h»" > <View style={ @@ -112911,7 +113977,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r6g»" + orderKey="«r6i»" > <View style={ @@ -112923,7 +113989,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-255" + testID="message-story-message-257" > <A11yIndexView accessibilityLabel="Medium name with icons" @@ -112932,7 +113998,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6g»" + orderKey="«r6i»" > <View style={ @@ -113519,13 +114585,13 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r6h»" + orderKey="«r6j»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6h»" + orderKey="«r6j»" > <ExternalKeyboardView canBeFocused={true} @@ -113618,7 +114684,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-256" + testID="message-story-message-258" > <A11yIndexView accessibilityLabel="Long name (22 chars) - should truncate" @@ -113626,7 +114692,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6h»" + orderKey="«r6j»" > <View style={ @@ -113977,7 +115043,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r6i»" + orderKey="«r6k»" > <View style={ @@ -113989,7 +115055,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-257" + testID="message-story-message-259" > <A11yIndexView accessibilityLabel="Long name with icons - timestamp visible" @@ -113998,7 +115064,7 @@ exports[`Story Snapshots: Names should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6i»" + orderKey="«r6k»" > <View style={ @@ -114598,13 +115664,13 @@ exports[`Story Snapshots: Pinned should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r6j»" + orderKey="«r6l»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6j»" + orderKey="«r6l»" > <ExternalKeyboardView canBeFocused={true} @@ -114697,7 +115763,7 @@ exports[`Story Snapshots: Pinned should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-258" + testID="message-story-message-260" > <A11yIndexView accessibilityLabel="Message header" @@ -114705,7 +115771,7 @@ exports[`Story Snapshots: Pinned should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6j»" + orderKey="«r6l»" > <View style={ @@ -115086,13 +116152,13 @@ exports[`Story Snapshots: Pinned should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r6k»" + orderKey="«r6m»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6k»" + orderKey="«r6m»" > <ExternalKeyboardView canBeFocused={true} @@ -115185,7 +116251,7 @@ exports[`Story Snapshots: Pinned should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-259" + testID="message-story-message-261" > <A11yIndexView accessibilityLabel="Message without header" @@ -115193,7 +116259,7 @@ exports[`Story Snapshots: Pinned should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6k»" + orderKey="«r6m»" > <View style={ @@ -115355,13 +116421,13 @@ exports[`Story Snapshots: PinnedLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r6l»" + orderKey="«r6n»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6l»" + orderKey="«r6n»" > <ExternalKeyboardView canBeFocused={true} @@ -115454,7 +116520,7 @@ exports[`Story Snapshots: PinnedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-261" + testID="message-story-message-263" > <A11yIndexView accessibilityLabel="Message header" @@ -115462,7 +116528,7 @@ exports[`Story Snapshots: PinnedLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6l»" + orderKey="«r6n»" > <View style={ @@ -115843,13 +116909,13 @@ exports[`Story Snapshots: PinnedLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r6m»" + orderKey="«r6o»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6m»" + orderKey="«r6o»" > <ExternalKeyboardView canBeFocused={true} @@ -115942,7 +117008,7 @@ exports[`Story Snapshots: PinnedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-262" + testID="message-story-message-264" > <A11yIndexView accessibilityLabel="Message without header" @@ -115950,7 +117016,7 @@ exports[`Story Snapshots: PinnedLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6m»" + orderKey="«r6o»" > <View style={ @@ -116112,13 +117178,13 @@ exports[`Story Snapshots: Reactions should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r6n»" + orderKey="«r6p»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6n»" + orderKey="«r6p»" > <ExternalKeyboardView canBeFocused={true} @@ -116211,7 +117277,7 @@ exports[`Story Snapshots: Reactions should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-264" + testID="message-story-message-266" > <A11yIndexView accessibilityLabel="Reactions" @@ -116219,7 +117285,7 @@ exports[`Story Snapshots: Reactions should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6n»" + orderKey="«r6p»" > <View style={ @@ -117114,13 +118180,13 @@ exports[`Story Snapshots: Reactions should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r6o»" + orderKey="«r6q»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6o»" + orderKey="«r6q»" > <ExternalKeyboardView canBeFocused={true} @@ -117213,7 +118279,7 @@ exports[`Story Snapshots: Reactions should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-265" + testID="message-story-message-267" > <A11yIndexView accessibilityLabel="Multiple Reactions" @@ -117221,7 +118287,7 @@ exports[`Story Snapshots: Reactions should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6o»" + orderKey="«r6q»" > <View style={ @@ -118585,13 +119651,13 @@ exports[`Story Snapshots: ReactionsLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r6p»" + orderKey="«r6r»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6p»" + orderKey="«r6r»" > <ExternalKeyboardView canBeFocused={true} @@ -118684,7 +119750,7 @@ exports[`Story Snapshots: ReactionsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-266" + testID="message-story-message-268" > <A11yIndexView accessibilityLabel="Reactions" @@ -118692,7 +119758,7 @@ exports[`Story Snapshots: ReactionsLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6p»" + orderKey="«r6r»" > <View style={ @@ -119587,13 +120653,13 @@ exports[`Story Snapshots: ReactionsLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r6q»" + orderKey="«r6s»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6q»" + orderKey="«r6s»" > <ExternalKeyboardView canBeFocused={true} @@ -119686,7 +120752,7 @@ exports[`Story Snapshots: ReactionsLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-267" + testID="message-story-message-269" > <A11yIndexView accessibilityLabel="Multiple Reactions" @@ -119694,7 +120760,7 @@ exports[`Story Snapshots: ReactionsLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6q»" + orderKey="«r6s»" > <View style={ @@ -121058,13 +122124,13 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m > <View> <A11yOrderView - orderKey="«r6r»" + orderKey="«r6t»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6r»" + orderKey="«r6t»" > <ExternalKeyboardView canBeFocused={true} @@ -121158,7 +122224,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m "width": "100%", } } - testID="message-story-message-268" + testID="message-story-message-270" > <A11yIndexView accessibilityLabel="How are you?" @@ -121166,7 +122232,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6r»" + orderKey="«r6t»" > <View style={ @@ -121866,13 +122932,13 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r6s»" + orderKey="«r6u»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6s»" + orderKey="«r6u»" > <ExternalKeyboardView canBeFocused={true} @@ -122133,7 +123199,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6s»" + orderKey="«r6u»" style={ { "flex": 1, @@ -122186,13 +123252,13 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r6t»" + orderKey="«r6v»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6t»" + orderKey="«r6v»" > <ExternalKeyboardView canBeFocused={true} @@ -122453,7 +123519,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6t»" + orderKey="«r6v»" style={ { "flex": 1, @@ -122506,13 +123572,13 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r6u»" + orderKey="«r70»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6u»" + orderKey="«r70»" > <ExternalKeyboardView canBeFocused={true} @@ -122773,7 +123839,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6u»" + orderKey="«r70»" style={ { "flex": 1, @@ -122839,13 +123905,13 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont > <View> <A11yOrderView - orderKey="«r6v»" + orderKey="«r71»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r6v»" + orderKey="«r71»" > <ExternalKeyboardView canBeFocused={true} @@ -122939,7 +124005,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont "width": "100%", } } - testID="message-story-message-272" + testID="message-story-message-274" > <A11yIndexView accessibilityLabel="How are you?" @@ -122947,7 +124013,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r6v»" + orderKey="«r71»" > <View style={ @@ -123647,13 +124713,13 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r70»" + orderKey="«r72»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r70»" + orderKey="«r72»" > <ExternalKeyboardView canBeFocused={true} @@ -123914,7 +124980,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r70»" + orderKey="«r72»" style={ { "flex": 1, @@ -123967,13 +125033,13 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r71»" + orderKey="«r73»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r71»" + orderKey="«r73»" > <ExternalKeyboardView canBeFocused={true} @@ -124234,7 +125300,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r71»" + orderKey="«r73»" style={ { "flex": 1, @@ -124287,13 +125353,13 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r72»" + orderKey="«r74»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r72»" + orderKey="«r74»" > <ExternalKeyboardView canBeFocused={true} @@ -124554,7 +125620,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r72»" + orderKey="«r74»" style={ { "flex": 1, @@ -124620,13 +125686,13 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma > <View> <A11yOrderView - orderKey="«r73»" + orderKey="«r75»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r73»" + orderKey="«r75»" > <ExternalKeyboardView canBeFocused={true} @@ -125004,7 +126070,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r73»" + orderKey="«r75»" style={ { "flex": 1, @@ -125057,13 +126123,13 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r74»" + orderKey="«r76»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r74»" + orderKey="«r76»" > <ExternalKeyboardView canBeFocused={true} @@ -125324,7 +126390,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r74»" + orderKey="«r76»" style={ { "flex": 1, @@ -125377,13 +126443,13 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r75»" + orderKey="«r77»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r75»" + orderKey="«r77»" > <ExternalKeyboardView canBeFocused={true} @@ -125644,7 +126710,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r75»" + orderKey="«r77»" style={ { "flex": 1, @@ -125697,13 +126763,13 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r76»" + orderKey="«r78»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r76»" + orderKey="«r78»" > <ExternalKeyboardView canBeFocused={true} @@ -125964,7 +127030,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should ma importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r76»" + orderKey="«r78»" style={ { "flex": 1, @@ -126030,13 +127096,13 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont > <View> <A11yOrderView - orderKey="«r77»" + orderKey="«r79»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r77»" + orderKey="«r79»" > <ExternalKeyboardView canBeFocused={true} @@ -126414,7 +127480,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r77»" + orderKey="«r79»" style={ { "flex": 1, @@ -126467,13 +127533,13 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r78»" + orderKey="«r7a»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r78»" + orderKey="«r7a»" > <ExternalKeyboardView canBeFocused={true} @@ -126734,7 +127800,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r78»" + orderKey="«r7a»" style={ { "flex": 1, @@ -126787,13 +127853,13 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r79»" + orderKey="«r7b»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r79»" + orderKey="«r7b»" > <ExternalKeyboardView canBeFocused={true} @@ -127054,7 +128120,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r79»" + orderKey="«r7b»" style={ { "flex": 1, @@ -127107,13 +128173,13 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r7a»" + orderKey="«r7c»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r7a»" + orderKey="«r7c»" > <ExternalKeyboardView canBeFocused={true} @@ -127374,7 +128440,7 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7a»" + orderKey="«r7c»" style={ { "flex": 1, @@ -127440,13 +128506,13 @@ exports[`Story Snapshots: ShowButtonAsAttachment should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r7b»" + orderKey="«r7d»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r7b»" + orderKey="«r7d»" > <ExternalKeyboardView canBeFocused={true} @@ -127539,7 +128605,7 @@ exports[`Story Snapshots: ShowButtonAsAttachment should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-286" + testID="message-story-message-288" > <A11yIndexView accessibilityLabel="" @@ -127547,7 +128613,7 @@ exports[`Story Snapshots: ShowButtonAsAttachment should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7b»" + orderKey="«r7d»" > <View style={ @@ -127971,13 +129037,13 @@ exports[`Story Snapshots: ShowButtonAsAttachment should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r7c»" + orderKey="«r7e»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r7c»" + orderKey="«r7e»" > <ExternalKeyboardView canBeFocused={true} @@ -128070,7 +129136,7 @@ exports[`Story Snapshots: ShowButtonAsAttachment should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-287" + testID="message-story-message-289" > <A11yIndexView accessibilityLabel="" @@ -128078,7 +129144,7 @@ exports[`Story Snapshots: ShowButtonAsAttachment should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7c»" + orderKey="«r7e»" > <View style={ @@ -128680,13 +129746,13 @@ exports[`Story Snapshots: ShowButtonAsAttachmentLargeFont should match snapshot > <View> <A11yOrderView - orderKey="«r7d»" + orderKey="«r7f»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r7d»" + orderKey="«r7f»" > <ExternalKeyboardView canBeFocused={true} @@ -128779,7 +129845,7 @@ exports[`Story Snapshots: ShowButtonAsAttachmentLargeFont should match snapshot "width": "100%", } } - testID="message-story-message-288" + testID="message-story-message-290" > <A11yIndexView accessibilityLabel="" @@ -128787,7 +129853,7 @@ exports[`Story Snapshots: ShowButtonAsAttachmentLargeFont should match snapshot importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7d»" + orderKey="«r7f»" > <View style={ @@ -129211,13 +130277,13 @@ exports[`Story Snapshots: ShowButtonAsAttachmentLargeFont should match snapshot </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r7e»" + orderKey="«r7g»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r7e»" + orderKey="«r7g»" > <ExternalKeyboardView canBeFocused={true} @@ -129310,7 +130376,7 @@ exports[`Story Snapshots: ShowButtonAsAttachmentLargeFont should match snapshot "width": "100%", } } - testID="message-story-message-289" + testID="message-story-message-291" > <A11yIndexView accessibilityLabel="" @@ -129318,7 +130384,7 @@ exports[`Story Snapshots: ShowButtonAsAttachmentLargeFont should match snapshot importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7e»" + orderKey="«r7g»" > <View style={ @@ -129920,13 +130986,13 @@ exports[`Story Snapshots: StaticAvatar should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r7f»" + orderKey="«r7h»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r7f»" + orderKey="«r7h»" > <ExternalKeyboardView canBeFocused={true} @@ -130019,7 +131085,7 @@ exports[`Story Snapshots: StaticAvatar should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-290" + testID="message-story-message-292" > <A11yIndexView accessibilityLabel="Message" @@ -130027,7 +131093,7 @@ exports[`Story Snapshots: StaticAvatar should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7f»" + orderKey="«r7h»" > <View style={ @@ -130391,13 +131457,13 @@ exports[`Story Snapshots: StaticAvatarLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r7g»" + orderKey="«r7i»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r7g»" + orderKey="«r7i»" > <ExternalKeyboardView canBeFocused={true} @@ -130490,7 +131556,7 @@ exports[`Story Snapshots: StaticAvatarLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-291" + testID="message-story-message-293" > <A11yIndexView accessibilityLabel="Message" @@ -130498,7 +131564,7 @@ exports[`Story Snapshots: StaticAvatarLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7g»" + orderKey="«r7i»" > <View style={ @@ -130862,7 +131928,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r7h»" + orderKey="«r7j»" > <View style={ @@ -131044,7 +132110,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7h»" + orderKey="«r7j»" style={ { "flex": 1, @@ -131106,7 +132172,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r7i»" + orderKey="«r7k»" > <View style={ @@ -131288,7 +132354,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7i»" + orderKey="«r7k»" style={ { "flex": 1, @@ -131350,7 +132416,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r7j»" + orderKey="«r7l»" > <View style={ @@ -131532,7 +132598,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7j»" + orderKey="«r7l»" style={ { "flex": 1, @@ -131597,7 +132663,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r7k»" + orderKey="«r7m»" > <View style={ @@ -131779,7 +132845,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7k»" + orderKey="«r7m»" style={ { "flex": 1, @@ -131841,7 +132907,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r7l»" + orderKey="«r7n»" > <View style={ @@ -132023,7 +133089,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7l»" + orderKey="«r7n»" style={ { "flex": 1, @@ -132085,7 +133151,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r7m»" + orderKey="«r7o»" > <View style={ @@ -132267,7 +133333,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7m»" + orderKey="«r7o»" style={ { "flex": 1, @@ -132329,7 +133395,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r7n»" + orderKey="«r7p»" > <View style={ @@ -132511,7 +133577,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7n»" + orderKey="«r7p»" style={ { "flex": 1, @@ -132573,7 +133639,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r7o»" + orderKey="«r7q»" > <View style={ @@ -132755,7 +133821,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7o»" + orderKey="«r7q»" style={ { "flex": 1, @@ -132817,7 +133883,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r7p»" + orderKey="«r7r»" > <View style={ @@ -132999,7 +134065,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7p»" + orderKey="«r7r»" style={ { "flex": 1, @@ -133061,7 +134127,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r7q»" + orderKey="«r7s»" > <View style={ @@ -133243,7 +134309,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7q»" + orderKey="«r7s»" style={ { "flex": 1, @@ -133305,7 +134371,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r7r»" + orderKey="«r7t»" > <View style={ @@ -133487,7 +134553,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7r»" + orderKey="«r7t»" style={ { "flex": 1, @@ -133549,7 +134615,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r7s»" + orderKey="«r7u»" > <View style={ @@ -133731,7 +134797,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7s»" + orderKey="«r7u»" style={ { "flex": 1, @@ -133793,7 +134859,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r7t»" + orderKey="«r7v»" > <View style={ @@ -133975,7 +135041,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7t»" + orderKey="«r7v»" style={ { "flex": 1, @@ -134037,7 +135103,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r7u»" + orderKey="«r80»" > <View style={ @@ -134219,7 +135285,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7u»" + orderKey="«r80»" style={ { "flex": 1, @@ -134281,7 +135347,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r7v»" + orderKey="«r81»" > <View style={ @@ -134463,7 +135529,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r7v»" + orderKey="«r81»" style={ { "flex": 1, @@ -134525,7 +135591,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r80»" + orderKey="«r82»" > <View style={ @@ -134707,7 +135773,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r80»" + orderKey="«r82»" style={ { "flex": 1, @@ -134769,7 +135835,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r81»" + orderKey="«r83»" > <View style={ @@ -134951,7 +136017,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r81»" + orderKey="«r83»" style={ { "flex": 1, @@ -135013,7 +136079,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r82»" + orderKey="«r84»" > <View style={ @@ -135195,7 +136261,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r82»" + orderKey="«r84»" style={ { "flex": 1, @@ -135257,7 +136323,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r83»" + orderKey="«r85»" > <View style={ @@ -135439,7 +136505,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r83»" + orderKey="«r85»" style={ { "flex": 1, @@ -135501,7 +136567,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r84»" + orderKey="«r86»" > <View style={ @@ -135683,7 +136749,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r84»" + orderKey="«r86»" style={ { "flex": 1, @@ -135745,7 +136811,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r85»" + orderKey="«r87»" > <View style={ @@ -135927,7 +136993,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r85»" + orderKey="«r87»" style={ { "flex": 1, @@ -135989,7 +137055,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r86»" + orderKey="«r88»" > <View style={ @@ -136171,7 +137237,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r86»" + orderKey="«r88»" style={ { "flex": 1, @@ -136233,7 +137299,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r87»" + orderKey="«r89»" > <View style={ @@ -136415,7 +137481,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r87»" + orderKey="«r89»" style={ { "flex": 1, @@ -136477,7 +137543,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r88»" + orderKey="«r8a»" > <View style={ @@ -136659,7 +137725,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r88»" + orderKey="«r8a»" style={ { "flex": 1, @@ -136721,7 +137787,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r89»" + orderKey="«r8b»" > <View style={ @@ -136903,7 +137969,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r89»" + orderKey="«r8b»" style={ { "flex": 1, @@ -136965,7 +138031,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8a»" + orderKey="«r8c»" > <View style={ @@ -137147,7 +138213,7 @@ exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8a»" + orderKey="«r8c»" style={ { "flex": 1, @@ -137200,7 +138266,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r8b»" + orderKey="«r8d»" > <View style={ @@ -137382,7 +138448,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8b»" + orderKey="«r8d»" style={ { "flex": 1, @@ -137444,7 +138510,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8c»" + orderKey="«r8e»" > <View style={ @@ -137626,7 +138692,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8c»" + orderKey="«r8e»" style={ { "flex": 1, @@ -137688,7 +138754,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8d»" + orderKey="«r8f»" > <View style={ @@ -137870,7 +138936,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8d»" + orderKey="«r8f»" style={ { "flex": 1, @@ -137935,7 +139001,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8e»" + orderKey="«r8g»" > <View style={ @@ -138117,7 +139183,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8e»" + orderKey="«r8g»" style={ { "flex": 1, @@ -138179,7 +139245,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8f»" + orderKey="«r8h»" > <View style={ @@ -138361,7 +139427,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8f»" + orderKey="«r8h»" style={ { "flex": 1, @@ -138423,7 +139489,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8g»" + orderKey="«r8i»" > <View style={ @@ -138605,7 +139671,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8g»" + orderKey="«r8i»" style={ { "flex": 1, @@ -138667,7 +139733,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8h»" + orderKey="«r8j»" > <View style={ @@ -138849,7 +139915,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8h»" + orderKey="«r8j»" style={ { "flex": 1, @@ -138911,7 +139977,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8i»" + orderKey="«r8k»" > <View style={ @@ -139093,7 +140159,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8i»" + orderKey="«r8k»" style={ { "flex": 1, @@ -139155,7 +140221,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8j»" + orderKey="«r8l»" > <View style={ @@ -139337,7 +140403,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8j»" + orderKey="«r8l»" style={ { "flex": 1, @@ -139399,7 +140465,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8k»" + orderKey="«r8m»" > <View style={ @@ -139581,7 +140647,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8k»" + orderKey="«r8m»" style={ { "flex": 1, @@ -139643,7 +140709,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8l»" + orderKey="«r8n»" > <View style={ @@ -139825,7 +140891,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8l»" + orderKey="«r8n»" style={ { "flex": 1, @@ -139887,7 +140953,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8m»" + orderKey="«r8o»" > <View style={ @@ -140069,7 +141135,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8m»" + orderKey="«r8o»" style={ { "flex": 1, @@ -140131,7 +141197,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8n»" + orderKey="«r8p»" > <View style={ @@ -140313,7 +141379,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8n»" + orderKey="«r8p»" style={ { "flex": 1, @@ -140375,7 +141441,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8o»" + orderKey="«r8q»" > <View style={ @@ -140557,7 +141623,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8o»" + orderKey="«r8q»" style={ { "flex": 1, @@ -140619,7 +141685,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8p»" + orderKey="«r8r»" > <View style={ @@ -140801,7 +141867,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8p»" + orderKey="«r8r»" style={ { "flex": 1, @@ -140863,7 +141929,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8q»" + orderKey="«r8s»" > <View style={ @@ -141045,7 +142111,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8q»" + orderKey="«r8s»" style={ { "flex": 1, @@ -141107,7 +142173,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8r»" + orderKey="«r8t»" > <View style={ @@ -141289,7 +142355,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8r»" + orderKey="«r8t»" style={ { "flex": 1, @@ -141351,7 +142417,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8s»" + orderKey="«r8u»" > <View style={ @@ -141533,7 +142599,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8s»" + orderKey="«r8u»" style={ { "flex": 1, @@ -141595,7 +142661,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8t»" + orderKey="«r8v»" > <View style={ @@ -141777,7 +142843,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8t»" + orderKey="«r8v»" style={ { "flex": 1, @@ -141839,7 +142905,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8u»" + orderKey="«r90»" > <View style={ @@ -142021,7 +143087,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8u»" + orderKey="«r90»" style={ { "flex": 1, @@ -142083,7 +143149,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r8v»" + orderKey="«r91»" > <View style={ @@ -142265,7 +143331,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r8v»" + orderKey="«r91»" style={ { "flex": 1, @@ -142327,7 +143393,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r90»" + orderKey="«r92»" > <View style={ @@ -142509,7 +143575,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r90»" + orderKey="«r92»" style={ { "flex": 1, @@ -142571,7 +143637,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r91»" + orderKey="«r93»" > <View style={ @@ -142753,7 +143819,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r91»" + orderKey="«r93»" style={ { "flex": 1, @@ -142815,7 +143881,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r92»" + orderKey="«r94»" > <View style={ @@ -142997,7 +144063,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r92»" + orderKey="«r94»" style={ { "flex": 1, @@ -143059,7 +144125,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r93»" + orderKey="«r95»" > <View style={ @@ -143241,7 +144307,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r93»" + orderKey="«r95»" style={ { "flex": 1, @@ -143303,7 +144369,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r94»" + orderKey="«r96»" > <View style={ @@ -143485,7 +144551,7 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r94»" + orderKey="«r96»" style={ { "flex": 1, @@ -143538,13 +144604,13 @@ exports[`Story Snapshots: Temp should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r95»" + orderKey="«r97»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r95»" + orderKey="«r97»" > <ExternalKeyboardView canBeFocused={true} @@ -143629,7 +144695,7 @@ exports[`Story Snapshots: Temp should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-344" + testID="message-story-message-346" > <A11yIndexView accessibilityLabel="Temp message" @@ -143637,7 +144703,7 @@ exports[`Story Snapshots: Temp should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r95»" + orderKey="«r97»" > <View style={ @@ -144005,13 +145071,13 @@ exports[`Story Snapshots: TempLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r96»" + orderKey="«r98»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r96»" + orderKey="«r98»" > <ExternalKeyboardView canBeFocused={true} @@ -144096,7 +145162,7 @@ exports[`Story Snapshots: TempLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-345" + testID="message-story-message-347" > <A11yIndexView accessibilityLabel="Temp message" @@ -144104,7 +145170,7 @@ exports[`Story Snapshots: TempLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r96»" + orderKey="«r98»" > <View style={ @@ -144472,13 +145538,13 @@ exports[`Story Snapshots: ThumbnailFromServer should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r97»" + orderKey="«r99»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r97»" + orderKey="«r99»" > <ExternalKeyboardView canBeFocused={true} @@ -144571,7 +145637,7 @@ exports[`Story Snapshots: ThumbnailFromServer should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-346" + testID="message-story-message-348" > <A11yIndexView accessibilityLabel="this is a thumbnail" @@ -144579,7 +145645,7 @@ exports[`Story Snapshots: ThumbnailFromServer should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r97»" + orderKey="«r99»" > <View style={ @@ -145154,13 +146220,13 @@ exports[`Story Snapshots: ThumbnailFromServerLargeFont should match snapshot 1`] > <View> <A11yOrderView - orderKey="«r98»" + orderKey="«r9a»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r98»" + orderKey="«r9a»" > <ExternalKeyboardView canBeFocused={true} @@ -145253,7 +146319,7 @@ exports[`Story Snapshots: ThumbnailFromServerLargeFont should match snapshot 1`] "width": "100%", } } - testID="message-story-message-347" + testID="message-story-message-349" > <A11yIndexView accessibilityLabel="this is a thumbnail" @@ -145261,7 +146327,7 @@ exports[`Story Snapshots: ThumbnailFromServerLargeFont should match snapshot 1`] importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r98»" + orderKey="«r9a»" > <View style={ @@ -145836,13 +146902,13 @@ exports[`Story Snapshots: TimeFormat should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r99»" + orderKey="«r9b»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r99»" + orderKey="«r9b»" > <ExternalKeyboardView canBeFocused={true} @@ -145935,7 +147001,7 @@ exports[`Story Snapshots: TimeFormat should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-348" + testID="message-story-message-350" > <A11yIndexView accessibilityLabel="Testing" @@ -145943,7 +147009,7 @@ exports[`Story Snapshots: TimeFormat should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r99»" + orderKey="«r9b»" > <View style={ @@ -146307,13 +147373,13 @@ exports[`Story Snapshots: TimeFormatLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r9a»" + orderKey="«r9c»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9a»" + orderKey="«r9c»" > <ExternalKeyboardView canBeFocused={true} @@ -146406,7 +147472,7 @@ exports[`Story Snapshots: TimeFormatLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-349" + testID="message-story-message-351" > <A11yIndexView accessibilityLabel="Testing" @@ -146414,7 +147480,7 @@ exports[`Story Snapshots: TimeFormatLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9a»" + orderKey="«r9c»" > <View style={ @@ -146778,13 +147844,13 @@ exports[`Story Snapshots: Translated should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r9b»" + orderKey="«r9d»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9b»" + orderKey="«r9d»" > <ExternalKeyboardView canBeFocused={true} @@ -146877,7 +147943,7 @@ exports[`Story Snapshots: Translated should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-350" + testID="message-story-message-352" > <A11yIndexView accessibilityLabel="Message translated" @@ -146886,7 +147952,7 @@ exports[`Story Snapshots: Translated should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9b»" + orderKey="«r9d»" > <View style={ @@ -147285,13 +148351,13 @@ exports[`Story Snapshots: TranslatedLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r9c»" + orderKey="«r9e»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9c»" + orderKey="«r9e»" > <ExternalKeyboardView canBeFocused={true} @@ -147384,7 +148450,7 @@ exports[`Story Snapshots: TranslatedLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-351" + testID="message-story-message-353" > <A11yIndexView accessibilityLabel="Message translated" @@ -147393,7 +148459,7 @@ exports[`Story Snapshots: TranslatedLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9c»" + orderKey="«r9e»" > <View style={ @@ -147792,13 +148858,13 @@ exports[`Story Snapshots: TwoShortCustomFieldsWithMarkdown should match snapshot > <View> <A11yOrderView - orderKey="«r9d»" + orderKey="«r9f»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9d»" + orderKey="«r9f»" > <ExternalKeyboardView canBeFocused={true} @@ -147891,7 +148957,7 @@ exports[`Story Snapshots: TwoShortCustomFieldsWithMarkdown should match snapshot "width": "100%", } } - testID="message-story-message-352" + testID="message-story-message-354" > <A11yIndexView accessibilityLabel="Message" @@ -147899,7 +148965,7 @@ exports[`Story Snapshots: TwoShortCustomFieldsWithMarkdown should match snapshot importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9d»" + orderKey="«r9f»" > <View style={ @@ -148979,13 +150045,13 @@ exports[`Story Snapshots: TwoShortCustomFieldsWithMarkdownLargeFont should match > <View> <A11yOrderView - orderKey="«r9e»" + orderKey="«r9g»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9e»" + orderKey="«r9g»" > <ExternalKeyboardView canBeFocused={true} @@ -149078,7 +150144,7 @@ exports[`Story Snapshots: TwoShortCustomFieldsWithMarkdownLargeFont should match "width": "100%", } } - testID="message-story-message-353" + testID="message-story-message-355" > <A11yIndexView accessibilityLabel="Message" @@ -149086,7 +150152,7 @@ exports[`Story Snapshots: TwoShortCustomFieldsWithMarkdownLargeFont should match importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9e»" + orderKey="«r9g»" > <View style={ @@ -150166,13 +151232,13 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r9f»" + orderKey="«r9h»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9f»" + orderKey="«r9h»" > <ExternalKeyboardView canBeFocused={true} @@ -150265,7 +151331,7 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-354" + testID="message-story-message-356" > <A11yIndexView accessibilityLabel="" @@ -150273,7 +151339,7 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9f»" + orderKey="«r9h»" > <View style={ @@ -150760,13 +151826,13 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r9g»" + orderKey="«r9i»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9g»" + orderKey="«r9i»" > <ExternalKeyboardView canBeFocused={true} @@ -150859,7 +151925,7 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-355" + testID="message-story-message-357" > <A11yIndexView accessibilityLabel="Message :nyan_rocket:" @@ -150867,7 +151933,7 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9g»" + orderKey="«r9i»" > <View style={ @@ -151360,13 +152426,13 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r9h»" + orderKey="«r9j»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9h»" + orderKey="«r9j»" > <ExternalKeyboardView canBeFocused={true} @@ -151459,7 +152525,7 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-356" + testID="message-story-message-358" > <A11yIndexView accessibilityLabel="" @@ -151467,7 +152533,7 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9h»" + orderKey="«r9j»" > <View style={ @@ -151637,13 +152703,13 @@ exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r9i»" + orderKey="«r9k»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9i»" + orderKey="«r9k»" > <ExternalKeyboardView canBeFocused={true} @@ -151736,7 +152802,7 @@ exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-358" + testID="message-story-message-360" > <A11yIndexView accessibilityLabel="" @@ -151744,7 +152810,7 @@ exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9i»" + orderKey="«r9k»" > <View style={ @@ -152133,13 +153199,13 @@ exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r9j»" + orderKey="«r9l»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9j»" + orderKey="«r9l»" > <ExternalKeyboardView canBeFocused={true} @@ -152232,7 +153298,7 @@ exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-359" + testID="message-story-message-361" > <A11yIndexView accessibilityLabel="" @@ -152240,7 +153306,7 @@ exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9j»" + orderKey="«r9l»" > <View style={ @@ -152585,13 +153651,13 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r9k»" + orderKey="«r9m»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9k»" + orderKey="«r9m»" > <ExternalKeyboardView canBeFocused={true} @@ -152684,7 +153750,7 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-360" + testID="message-story-message-362" > <A11yIndexView accessibilityLabel="" @@ -152692,7 +153758,7 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9k»" + orderKey="«r9m»" > <View style={ @@ -153081,13 +154147,13 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r9l»" + orderKey="«r9n»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9l»" + orderKey="«r9n»" > <ExternalKeyboardView canBeFocused={true} @@ -153180,7 +154246,7 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-361" + testID="message-story-message-363" > <A11yIndexView accessibilityLabel="" @@ -153188,7 +154254,7 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9l»" + orderKey="«r9n»" > <View style={ @@ -153533,13 +154599,13 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r9m»" + orderKey="«r9o»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9m»" + orderKey="«r9o»" > <ExternalKeyboardView canBeFocused={true} @@ -153632,7 +154698,7 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-362" + testID="message-story-message-364" > <A11yIndexView accessibilityLabel="" @@ -153640,7 +154706,7 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9m»" + orderKey="«r9o»" > <View style={ @@ -154127,13 +155193,13 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r9n»" + orderKey="«r9p»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9n»" + orderKey="«r9p»" > <ExternalKeyboardView canBeFocused={true} @@ -154226,7 +155292,7 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-363" + testID="message-story-message-365" > <A11yIndexView accessibilityLabel="Message :nyan_rocket:" @@ -154234,7 +155300,7 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9n»" + orderKey="«r9p»" > <View style={ @@ -154727,13 +155793,13 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r9o»" + orderKey="«r9q»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9o»" + orderKey="«r9q»" > <ExternalKeyboardView canBeFocused={true} @@ -154826,7 +155892,7 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-364" + testID="message-story-message-366" > <A11yIndexView accessibilityLabel="" @@ -154834,7 +155900,7 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9o»" + orderKey="«r9q»" > <View style={ @@ -155004,13 +156070,13 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r9p»" + orderKey="«r9r»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9p»" + orderKey="«r9r»" > <ExternalKeyboardView canBeFocused={true} @@ -155103,7 +156169,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-366" + testID="message-story-message-368" > <A11yIndexView accessibilityLabel="Small username (2 chars)" @@ -155111,7 +156177,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9p»" + orderKey="«r9r»" > <View style={ @@ -155462,7 +156528,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r9q»" + orderKey="«r9s»" > <View style={ @@ -155474,7 +156540,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-367" + testID="message-story-message-369" > <A11yIndexView accessibilityLabel="Small username with icons" @@ -155483,7 +156549,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9q»" + orderKey="«r9s»" > <View style={ @@ -156070,13 +157136,13 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r9r»" + orderKey="«r9t»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9r»" + orderKey="«r9t»" > <ExternalKeyboardView canBeFocused={true} @@ -156169,7 +157235,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-368" + testID="message-story-message-370" > <A11yIndexView accessibilityLabel="Medium username (8 chars)" @@ -156177,7 +157243,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9r»" + orderKey="«r9t»" > <View style={ @@ -156528,7 +157594,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r9s»" + orderKey="«r9u»" > <View style={ @@ -156540,7 +157606,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-369" + testID="message-story-message-371" > <A11yIndexView accessibilityLabel="Medium username with icons" @@ -156549,7 +157615,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9s»" + orderKey="«r9u»" > <View style={ @@ -157136,13 +158202,13 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` </View> </A11yOrderView> <A11yOrderView - orderKey="«r9t»" + orderKey="«r9v»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9t»" + orderKey="«r9v»" > <ExternalKeyboardView canBeFocused={true} @@ -157235,7 +158301,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-370" + testID="message-story-message-372" > <A11yIndexView accessibilityLabel="Long username (22 chars) - should truncate" @@ -157243,7 +158309,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9t»" + orderKey="«r9v»" > <View style={ @@ -157594,7 +158660,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«r9u»" + orderKey="«ra0»" > <View style={ @@ -157606,7 +158672,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-371" + testID="message-story-message-373" > <A11yIndexView accessibilityLabel="Long username with icons - timestamp visible" @@ -157615,7 +158681,7 @@ exports[`Story Snapshots: Usernames should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9u»" + orderKey="«ra0»" > <View style={ @@ -158215,13 +159281,13 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«r9v»" + orderKey="«ra1»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«r9v»" + orderKey="«ra1»" > <ExternalKeyboardView canBeFocused={true} @@ -158314,7 +159380,7 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-372" + testID="message-story-message-374" > <A11yIndexView accessibilityLabel="Message" @@ -158322,7 +159388,7 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«r9v»" + orderKey="«ra1»" > <View style={ @@ -158692,13 +159758,13 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«ra0»" + orderKey="«ra2»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«ra0»" + orderKey="«ra2»" > <ExternalKeyboardView canBeFocused={true} @@ -158791,7 +159857,7 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-373" + testID="message-story-message-375" > <A11yIndexView accessibilityLabel="Message" @@ -158799,7 +159865,7 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«ra0»" + orderKey="«ra2»" > <View style={ @@ -159182,13 +160248,13 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«ra1»" + orderKey="«ra3»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«ra1»" + orderKey="«ra3»" > <ExternalKeyboardView canBeFocused={true} @@ -159281,7 +160347,7 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-374" + testID="message-story-message-376" > <A11yIndexView accessibilityLabel="Message" @@ -159289,7 +160355,7 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«ra1»" + orderKey="«ra3»" > <View style={ @@ -159659,13 +160725,13 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«ra2»" + orderKey="«ra4»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«ra2»" + orderKey="«ra4»" > <ExternalKeyboardView canBeFocused={true} @@ -159758,7 +160824,7 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-375" + testID="message-story-message-377" > <A11yIndexView accessibilityLabel="Message" @@ -159766,7 +160832,7 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«ra2»" + orderKey="«ra4»" > <View style={ @@ -160149,13 +161215,13 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«ra3»" + orderKey="«ra5»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«ra3»" + orderKey="«ra5»" > <ExternalKeyboardView canBeFocused={true} @@ -160248,7 +161314,7 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-376" + testID="message-story-message-378" > <A11yIndexView accessibilityLabel="" @@ -160256,7 +161322,7 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«ra3»" + orderKey="«ra5»" > <View style={ @@ -160890,13 +161956,13 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«ra4»" + orderKey="«ra6»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«ra4»" + orderKey="«ra6»" > <ExternalKeyboardView canBeFocused={true} @@ -160989,7 +162055,7 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-377" + testID="message-story-message-379" > <A11yIndexView accessibilityLabel="" @@ -160997,7 +162063,7 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«ra4»" + orderKey="«ra6»" > <View style={ @@ -161355,13 +162421,13 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«ra5»" + orderKey="«ra7»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«ra5»" + orderKey="«ra7»" > <ExternalKeyboardView canBeFocused={true} @@ -161454,7 +162520,7 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-379" + testID="message-story-message-381" > <A11yIndexView accessibilityLabel="multi file" @@ -161462,7 +162528,7 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«ra5»" + orderKey="«ra7»" > <View style={ @@ -162061,13 +163127,13 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«ra6»" + orderKey="«ra8»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«ra6»" + orderKey="«ra8»" > <ExternalKeyboardView canBeFocused={true} @@ -162160,7 +163226,7 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-381" + testID="message-story-message-383" > <A11yIndexView accessibilityLabel="" @@ -162168,7 +163234,7 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«ra6»" + orderKey="«ra8»" > <View style={ @@ -163055,13 +164121,13 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«ra7»" + orderKey="«ra9»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«ra7»" + orderKey="«ra9»" > <ExternalKeyboardView canBeFocused={true} @@ -163154,7 +164220,7 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-383" + testID="message-story-message-385" > <A11yIndexView accessibilityLabel="" @@ -163162,7 +164228,7 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«ra7»" + orderKey="«ra9»" > <View style={ @@ -163796,13 +164862,13 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«ra8»" + orderKey="«raa»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«ra8»" + orderKey="«raa»" > <ExternalKeyboardView canBeFocused={true} @@ -163895,7 +164961,7 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-384" + testID="message-story-message-386" > <A11yIndexView accessibilityLabel="First message" @@ -163903,7 +164969,7 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«ra8»" + orderKey="«raa»" > <View style={ @@ -164022,13 +165088,13 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«ra9»" + orderKey="«rab»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«ra9»" + orderKey="«rab»" > <ExternalKeyboardView canBeFocused={true} @@ -164121,7 +165187,7 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-386" + testID="message-story-message-388" > <A11yIndexView accessibilityLabel="" @@ -164129,7 +165195,7 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«ra9»" + orderKey="«rab»" > <View style={ @@ -164487,13 +165553,13 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«raa»" + orderKey="«rac»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«raa»" + orderKey="«rac»" > <ExternalKeyboardView canBeFocused={true} @@ -164586,7 +165652,7 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-388" + testID="message-story-message-390" > <A11yIndexView accessibilityLabel="" @@ -164594,7 +165660,7 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«raa»" + orderKey="«rac»" > <View style={ @@ -164897,13 +165963,13 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«rab»" + orderKey="«rad»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«rab»" + orderKey="«rad»" > <ExternalKeyboardView canBeFocused={true} @@ -164996,7 +166062,7 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-390" + testID="message-story-message-392" > <A11yIndexView accessibilityLabel="multi file" @@ -165004,7 +166070,7 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«rab»" + orderKey="«rad»" > <View style={ @@ -165951,13 +167017,13 @@ exports[`Story Snapshots: WithFile should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«rac»" + orderKey="«rae»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«rac»" + orderKey="«rae»" > <ExternalKeyboardView canBeFocused={true} @@ -166050,7 +167116,7 @@ exports[`Story Snapshots: WithFile should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-392" + testID="message-story-message-394" > <A11yIndexView accessibilityLabel="" @@ -166058,7 +167124,7 @@ exports[`Story Snapshots: WithFile should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«rac»" + orderKey="«rae»" > <View style={ @@ -166599,13 +167665,13 @@ exports[`Story Snapshots: WithFile should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«rad»" + orderKey="«raf»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«rad»" + orderKey="«raf»" > <ExternalKeyboardView canBeFocused={true} @@ -166698,7 +167764,7 @@ exports[`Story Snapshots: WithFile should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-393" + testID="message-story-message-395" > <A11yIndexView accessibilityLabel="" @@ -166706,7 +167772,7 @@ exports[`Story Snapshots: WithFile should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«rad»" + orderKey="«raf»" > <View style={ @@ -167270,13 +168336,13 @@ exports[`Story Snapshots: WithFileLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«rae»" + orderKey="«rag»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«rae»" + orderKey="«rag»" > <ExternalKeyboardView canBeFocused={true} @@ -167369,7 +168435,7 @@ exports[`Story Snapshots: WithFileLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-395" + testID="message-story-message-397" > <A11yIndexView accessibilityLabel="" @@ -167377,7 +168443,7 @@ exports[`Story Snapshots: WithFileLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«rae»" + orderKey="«rag»" > <View style={ @@ -167918,13 +168984,13 @@ exports[`Story Snapshots: WithFileLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«raf»" + orderKey="«rah»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«raf»" + orderKey="«rah»" > <ExternalKeyboardView canBeFocused={true} @@ -168017,7 +169083,7 @@ exports[`Story Snapshots: WithFileLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-396" + testID="message-story-message-398" > <A11yIndexView accessibilityLabel="" @@ -168025,7 +169091,7 @@ exports[`Story Snapshots: WithFileLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«raf»" + orderKey="«rah»" > <View style={ @@ -168347,13 +169413,13 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«rag»" + orderKey="«rai»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«rag»" + orderKey="«rai»" > <ExternalKeyboardView canBeFocused={true} @@ -168446,7 +169512,7 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-398" + testID="message-story-message-400" > <A11yIndexView accessibilityLabel="" @@ -168454,7 +169520,7 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«rag»" + orderKey="«rai»" > <View style={ @@ -169021,13 +170087,13 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«rah»" + orderKey="«raj»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«rah»" + orderKey="«raj»" > <ExternalKeyboardView canBeFocused={true} @@ -169120,7 +170186,7 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-399" + testID="message-story-message-401" > <A11yIndexView accessibilityLabel="Message, not description" @@ -169128,7 +170194,7 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«rah»" + orderKey="«raj»" > <View style={ @@ -170184,13 +171250,13 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«rai»" + orderKey="«rak»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«rai»" + orderKey="«rak»" > <ExternalKeyboardView canBeFocused={true} @@ -170283,7 +171349,7 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-400" + testID="message-story-message-402" > <A11yIndexView accessibilityLabel="multi file" @@ -170291,7 +171357,7 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«rai»" + orderKey="«rak»" > <View style={ @@ -170972,13 +172038,13 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«raj»" + orderKey="«ral»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«raj»" + orderKey="«ral»" > <ExternalKeyboardView canBeFocused={true} @@ -171071,7 +172137,7 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-401" + testID="message-story-message-403" > <A11yIndexView accessibilityLabel="" @@ -171079,7 +172145,7 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«raj»" + orderKey="«ral»" > <View style={ @@ -171646,13 +172712,13 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«rak»" + orderKey="«ram»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«rak»" + orderKey="«ram»" > <ExternalKeyboardView canBeFocused={true} @@ -171745,7 +172811,7 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-402" + testID="message-story-message-404" > <A11yIndexView accessibilityLabel="" @@ -171753,7 +172819,7 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«rak»" + orderKey="«ram»" > <View style={ @@ -172276,13 +173342,13 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«ral»" + orderKey="«ran»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«ral»" + orderKey="«ran»" > <ExternalKeyboardView canBeFocused={true} @@ -172375,7 +173441,7 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-403" + testID="message-story-message-405" > <A11yIndexView accessibilityLabel="" @@ -172383,7 +173449,7 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«ral»" + orderKey="«ran»" > <View style={ @@ -172674,13 +173740,13 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«ram»" + orderKey="«rao»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«ram»" + orderKey="«rao»" > <ExternalKeyboardView canBeFocused={true} @@ -172773,7 +173839,7 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-405" + testID="message-story-message-407" > <A11yIndexView accessibilityLabel="multi file" @@ -172781,7 +173847,7 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«ram»" + orderKey="«rao»" > <View style={ @@ -173462,13 +174528,13 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«ran»" + orderKey="«rap»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«ran»" + orderKey="«rap»" > <ExternalKeyboardView canBeFocused={true} @@ -173561,7 +174627,7 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-406" + testID="message-story-message-408" > <A11yIndexView accessibilityLabel="" @@ -173569,7 +174635,7 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«ran»" + orderKey="«rap»" > <View style={ @@ -174136,13 +175202,13 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«rao»" + orderKey="«raq»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«rao»" + orderKey="«raq»" > <ExternalKeyboardView canBeFocused={true} @@ -174235,7 +175301,7 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-407" + testID="message-story-message-409" > <A11yIndexView accessibilityLabel="" @@ -174243,7 +175309,7 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«rao»" + orderKey="«raq»" > <View style={ @@ -174768,13 +175834,13 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«rap»" + orderKey="«rar»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«rap»" + orderKey="«rar»" > <ExternalKeyboardView canBeFocused={true} @@ -174867,7 +175933,7 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-408" + testID="message-story-message-410" > <A11yIndexView accessibilityLabel="" @@ -174875,7 +175941,7 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«rap»" + orderKey="«rar»" > <View style={ @@ -175124,13 +176190,13 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«raq»" + orderKey="«ras»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«raq»" + orderKey="«ras»" > <ExternalKeyboardView canBeFocused={true} @@ -175223,7 +176289,7 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-410" + testID="message-story-message-412" > <A11yIndexView accessibilityLabel="multi file" @@ -175231,7 +176297,7 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«raq»" + orderKey="«ras»" > <View style={ @@ -175857,13 +176923,13 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«rar»" + orderKey="«rat»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«rar»" + orderKey="«rat»" > <ExternalKeyboardView canBeFocused={true} @@ -175956,7 +177022,7 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-411" + testID="message-story-message-413" > <A11yIndexView accessibilityLabel="" @@ -175964,7 +177030,7 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«rar»" + orderKey="«rat»" > <View style={ @@ -176489,13 +177555,13 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«ras»" + orderKey="«rau»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«ras»" + orderKey="«rau»" > <ExternalKeyboardView canBeFocused={true} @@ -176588,7 +177654,7 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-412" + testID="message-story-message-414" > <A11yIndexView accessibilityLabel="" @@ -176596,7 +177662,7 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«ras»" + orderKey="«rau»" > <View style={ @@ -177022,13 +178088,13 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` </A11yIndexView> </A11yOrderView> <A11yOrderView - orderKey="«rat»" + orderKey="«rav»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«rat»" + orderKey="«rav»" > <ExternalKeyboardView canBeFocused={true} @@ -177121,7 +178187,7 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-413" + testID="message-story-message-415" > <A11yIndexView accessibilityLabel="multi file" @@ -177129,7 +178195,7 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«rat»" + orderKey="«rav»" > <View style={ @@ -177755,13 +178821,13 @@ exports[`Story Snapshots: WithoutHeader should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«rau»" + orderKey="«rb0»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«rau»" + orderKey="«rb0»" > <ExternalKeyboardView canBeFocused={true} @@ -177854,7 +178920,7 @@ exports[`Story Snapshots: WithoutHeader should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-414" + testID="message-story-message-416" > <A11yIndexView accessibilityLabel="Message" @@ -177862,7 +178928,7 @@ exports[`Story Snapshots: WithoutHeader should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«rau»" + orderKey="«rb0»" > <View style={ @@ -177994,13 +179060,13 @@ exports[`Story Snapshots: WithoutHeaderLargeFont should match snapshot 1`] = ` > <View> <A11yOrderView - orderKey="«rav»" + orderKey="«rb1»" > <A11yIndexView importantForAccessibility="yes" orderFocusType={0} orderIndex={1} - orderKey="«rav»" + orderKey="«rb1»" > <ExternalKeyboardView canBeFocused={true} @@ -178093,7 +179159,7 @@ exports[`Story Snapshots: WithoutHeaderLargeFont should match snapshot 1`] = ` "width": "100%", } } - testID="message-story-message-416" + testID="message-story-message-418" > <A11yIndexView accessibilityLabel="Message" @@ -178101,7 +179167,7 @@ exports[`Story Snapshots: WithoutHeaderLargeFont should match snapshot 1`] = ` importantForAccessibility="yes" orderFocusType={0} orderIndex={2} - orderKey="«rav»" + orderKey="«rb1»" > <View style={ diff --git a/app/containers/message/components/stories/Message.stories.tsx b/app/containers/message/components/stories/Message.stories.tsx index 2114006c5da..5885a85bdb5 100644 --- a/app/containers/message/components/stories/Message.stories.tsx +++ b/app/containers/message/components/stories/Message.stories.tsx @@ -2843,3 +2843,7 @@ export const InlineKatex = () => ( </> ); export const InlineKatexLargeFont = () => <MessageLargeFont {...inlineKatex} />; + +export const JitsiCall = () => <Message type='jitsi_call_started' />; + +export const JitsiCallLargeFont = () => <MessageLargeFont type='jitsi_call_started' />; From 82637b9c61a37c66bec3adc911f44c7f414e638f Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 15:08:10 -0300 Subject: [PATCH 121/144] test: document useFile's stuck isMessagePersisted contract (NATIVE-22) Ticket 004: confirmed the stuck-false transition bug at the unit level, but the only caller (useMediaAutoDownload) keys its message list by the same id, so a real messageId change always remounts the hook with a fresh seed. Ruled the behavior fix out of scope; kept the test as a documented contract. --- .../message/hooks/__tests__/useFile.test.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/containers/message/hooks/__tests__/useFile.test.ts b/app/containers/message/hooks/__tests__/useFile.test.ts index 8f72264a398..373fffe3315 100644 --- a/app/containers/message/hooks/__tests__/useFile.test.ts +++ b/app/containers/message/hooks/__tests__/useFile.test.ts @@ -81,4 +81,27 @@ describe('useFile', () => { expect(result.current[0]).not.toBe(file); expect(result.current[0].title_link).toBe('/forwarded'); }); + + // Documents the current contract: isMessagePersisted is seeded once and the effect only ever + // flips it to false, so it can't recover to true on a later messageId change. Not fixed: the + // only caller keys its list by that same id, so a real id change always remounts the hook fresh. + it('stays stuck not-persisted after rerendering with a real persisted messageId (documents current contract)', async () => { + mockGetThreadMessageById.mockResolvedValue(undefined); + mockGetMessageById.mockResolvedValue(undefined); + const { result, rerender } = renderHook(({ messageId }: { messageId: string }) => useFile(file, messageId), { + initialProps: { messageId: '' } + }); + + await flushEffect(); + expect(result.current[0]).toBe(file); + + mockGetMessageById.mockResolvedValue({ id: 'msg-id' }); + rerender({ messageId: 'msg-id' }); + await flushEffect(); + + expect(mockGetMessageById).toHaveBeenCalledWith('msg-id'); + act(() => result.current[1]({ title_link: '/forwarded' })); + expect(result.current[0]).not.toBe(file); + expect(result.current[0].title_link).toBe('/forwarded'); + }); }); From cf2b383eb7141705110c0044aae2d6ca5784c5aa Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 15:16:32 -0300 Subject: [PATCH 122/144] test: cover useMessageTouchable/useMessageLongPress/useMessagePress branches (NATIVE-22) --- .../stores/__tests__/MessageStore.test.tsx | 264 +++++++++++++++++- 1 file changed, 263 insertions(+), 1 deletion(-) diff --git a/app/containers/message/stores/__tests__/MessageStore.test.tsx b/app/containers/message/stores/__tests__/MessageStore.test.tsx index 9fe8b2f5a0a..1e117561668 100644 --- a/app/containers/message/stores/__tests__/MessageStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageStore.test.tsx @@ -1,6 +1,6 @@ import { act, fireEvent, render, renderHook } from '@testing-library/react-native'; import { type ReactNode } from 'react'; -import { Text } from 'react-native'; +import { Keyboard, Text } from 'react-native'; import { Provider } from 'react-redux'; import { type IAttachment, type TAnyMessageModel } from '../../../../definitions'; @@ -24,8 +24,11 @@ import { useMessageIgnored, useMessageHeaderMeta, useMessageItem, + useMessageLongPress, + useMessagePress, useMessageStatus, useMessageText, + useMessageTouchable, useOnLinkPress, useReactions, useRepliedThreadData, @@ -169,6 +172,30 @@ const wrapWithProviders = </Provider> ); +// Like wrapWithProviders, but also forwards the row-level MessageProvider props (onPress, +// onLongPress, isIgnored) needed by the touch/press/long-press hooks. +const renderMessageHook = <T,>( + useHook: () => T, + item: TAnyMessageModel, + options: { + isIgnored?: boolean; + onPress?: () => void; + onLongPress?: (msg: TAnyMessageModel) => void; + config?: Partial<MessageRoomState>; + } = {} +) => + renderHook(() => useHook(), { + wrapper: ({ children }: { children: ReactNode }) => ( + <Provider store={mockedStore}> + <MessageRoomProvider {...(options.config ?? {})}> + <MessageProvider item={item} isIgnored={options.isIgnored} onPress={options.onPress} onLongPress={options.onLongPress}> + {children} + </MessageProvider> + </MessageRoomProvider> + </Provider> + ) + }); + describe('MessageStore', () => { describe('field hooks', () => { it('return the current field values', () => { @@ -737,6 +764,241 @@ describe('MessageStore', () => { }); }); + describe('useMessageTouchable', () => { + it('is fully tappable and long-pressable for a plain text message', () => { + const model = buildFakeModel(); + const { result } = renderMessageHook(useMessageTouchable, model); + expect(result.current).toEqual({ tappable: true, longPressable: true, revealsIgnored: false }); + }); + + it('is untouchable for an info message', () => { + const model = buildFakeModel({ t: 'room_changed_topic' }); + const { result } = renderMessageHook(useMessageTouchable, model); + expect(result.current).toEqual({ tappable: false, longPressable: false, revealsIgnored: false }); + }); + + it('is untouchable when the message has an error status', () => { + const model = buildFakeModel({ status: messagesStatus.ERROR }); + const { result } = renderMessageHook(useMessageTouchable, model); + expect(result.current).toEqual({ tappable: false, longPressable: false, revealsIgnored: false }); + }); + + it('is untouchable when the room is archived', () => { + const model = buildFakeModel(); + const { result } = renderMessageHook(useMessageTouchable, model, { config: { archived: true } }); + expect(result.current).toEqual({ tappable: false, longPressable: false, revealsIgnored: false }); + }); + + // Post-refactor: tappable now also excludes isTemp, so a still-sending message loses both + // tap and long-press here — the old long-press guard only checked isInfo/hasError/isEncrypted/archived. + it('is untouchable while the message is temp (sending)', () => { + const model = buildFakeModel({ status: messagesStatus.TEMP }); + const { result } = renderMessageHook(useMessageTouchable, model); + expect(result.current).toEqual({ tappable: false, longPressable: false, revealsIgnored: false }); + }); + + // Post-refactor: tappable now also excludes jitsi_call_started, which the old long-press guard did not. + it('is untouchable for a jitsi_call_started message', () => { + const model = buildFakeModel({ t: 'jitsi_call_started' }); + const { result } = renderMessageHook(useMessageTouchable, model); + expect(result.current).toEqual({ tappable: false, longPressable: false, revealsIgnored: false }); + }); + + it('is tappable but not long-pressable for a pending encrypted message', () => { + const model = buildFakeModel({ t: E2E_MESSAGE_TYPE, e2e: 'pending' }); + const { result } = renderMessageHook(useMessageTouchable, model); + expect(result.current).toEqual({ tappable: true, longPressable: false, revealsIgnored: false }); + }); + + it('reveals ignored on an otherwise-tappable ignored message', () => { + const model = buildFakeModel(); + const { result } = renderMessageHook(useMessageTouchable, model, { isIgnored: true }); + expect(result.current).toEqual({ tappable: true, longPressable: true, revealsIgnored: true }); + }); + + it('does not reveal ignored when the message is not tappable', () => { + const model = buildFakeModel(); + const { result } = renderMessageHook(useMessageTouchable, model, { isIgnored: true, config: { archived: true } }); + expect(result.current).toEqual({ tappable: false, longPressable: false, revealsIgnored: false }); + }); + }); + + describe('useMessageLongPress', () => { + it('fires onLongPress with the item when longPressable', () => { + const model = buildFakeModel(); + const onLongPress = jest.fn(); + const { result } = renderMessageHook(useMessageLongPress, model, { onLongPress }); + + act(() => result.current()); + + expect(onLongPress).toHaveBeenCalledWith(model); + }); + + it('does not fire onLongPress for an info message', () => { + const model = buildFakeModel({ t: 'room_changed_topic' }); + const onLongPress = jest.fn(); + const { result } = renderMessageHook(useMessageLongPress, model, { onLongPress }); + + act(() => result.current()); + + expect(onLongPress).not.toHaveBeenCalled(); + }); + + it('does not fire onLongPress for an encrypted message', () => { + const model = buildFakeModel({ t: E2E_MESSAGE_TYPE, e2e: 'pending' }); + const onLongPress = jest.fn(); + const { result } = renderMessageHook(useMessageLongPress, model, { onLongPress }); + + act(() => result.current()); + + expect(onLongPress).not.toHaveBeenCalled(); + }); + + // See useMessageTouchable above: this suppression is new vs the pre-refactor guard. + it('does not fire onLongPress on a temp (sending) message', () => { + const model = buildFakeModel({ status: messagesStatus.TEMP }); + const onLongPress = jest.fn(); + const { result } = renderMessageHook(useMessageLongPress, model, { onLongPress }); + + act(() => result.current()); + + expect(onLongPress).not.toHaveBeenCalled(); + }); + + it('does not fire onLongPress on a jitsi_call_started message', () => { + const model = buildFakeModel({ t: 'jitsi_call_started' }); + const onLongPress = jest.fn(); + const { result } = renderMessageHook(useMessageLongPress, model, { onLongPress }); + + act(() => result.current()); + + expect(onLongPress).not.toHaveBeenCalled(); + }); + }); + + describe('useMessagePress', () => { + let dismissSpy: jest.SpyInstance; + + beforeEach(() => { + jest.useFakeTimers(); + dismissSpy = jest.spyOn(Keyboard, 'dismiss').mockImplementation(() => {}); + }); + + afterEach(() => { + dismissSpy.mockRestore(); + jest.useRealTimers(); + }); + + it('reveals an ignored message instead of pressing', () => { + const model = buildFakeModel({ tmid: 't1' }); + const onThreadPress = jest.fn(); + const { result } = renderMessageHook(useMessagePress, model, { isIgnored: true, config: { onThreadPress } }); + + act(() => result.current()); + + expect(dismissSpy).not.toHaveBeenCalled(); + expect(onThreadPress).not.toHaveBeenCalled(); + }); + + it('calls the row onPress handler and skips keyboard dismiss and thread/discussion routing', () => { + const model = buildFakeModel({ tmid: 't1', dlm: new Date(), drid: 'drid-1' }); + const onPress = jest.fn(); + const onThreadPress = jest.fn(); + const onDiscussionPress = jest.fn(); + const { result } = renderMessageHook(useMessagePress, model, { onPress, config: { onThreadPress, onDiscussionPress } }); + + act(() => result.current()); + + expect(onPress).toHaveBeenCalledTimes(1); + expect(dismissSpy).not.toHaveBeenCalled(); + expect(onThreadPress).not.toHaveBeenCalled(); + expect(onDiscussionPress).not.toHaveBeenCalled(); + }); + + it('dismisses the keyboard and routes to thread press for a threaded reply', () => { + const model = buildFakeModel({ tmid: 't1' }); + const onThreadPress = jest.fn(); + const { result } = renderMessageHook(useMessagePress, model, { config: { onThreadPress } }); + + act(() => result.current()); + + expect(dismissSpy).toHaveBeenCalledTimes(1); + expect(onThreadPress).toHaveBeenCalledWith(model); + }); + + it('routes to thread press for a root message that has replies (tlm)', () => { + const model = buildFakeModel({ tlm: new Date() }); + const onThreadPress = jest.fn(); + const { result } = renderMessageHook(useMessagePress, model, { config: { onThreadPress } }); + + act(() => result.current()); + + expect(onThreadPress).toHaveBeenCalledWith(model); + }); + + it('does not route to thread press while already inside the thread room', () => { + const model = buildFakeModel({ tmid: 't1' }); + const onThreadPress = jest.fn(); + const { result } = renderMessageHook(useMessagePress, model, { config: { onThreadPress, isThreadRoom: true } }); + + act(() => result.current()); + + expect(onThreadPress).not.toHaveBeenCalled(); + }); + + it('routes to discussion press when the message has a discussion', () => { + const model = buildFakeModel({ dlm: new Date(), drid: 'drid-1' }); + const onDiscussionPress = jest.fn(); + const { result } = renderMessageHook(useMessagePress, model, { config: { onDiscussionPress } }); + + act(() => result.current()); + + expect(onDiscussionPress).toHaveBeenCalledWith('drid-1'); + }); + + it('debounces on the leading edge, suppressing an immediate repeat until the window elapses', () => { + const model = buildFakeModel({ tmid: 't1' }); + const onThreadPress = jest.fn(); + const { result } = renderMessageHook(useMessagePress, model, { config: { onThreadPress } }); + + act(() => result.current()); + expect(onThreadPress).toHaveBeenCalledTimes(1); + + act(() => result.current()); + expect(onThreadPress).toHaveBeenCalledTimes(1); + + act(() => jest.advanceTimersByTime(300)); + + act(() => result.current()); + expect(onThreadPress).toHaveBeenCalledTimes(2); + }); + + it('routes the debounced press through closeEmojiAndAction instead of running it directly', () => { + const model = buildFakeModel({ tmid: 't1' }); + const onThreadPress = jest.fn(); + const closeEmojiAndAction = jest.fn(); + const { result } = renderMessageHook(useMessagePress, model, { config: { onThreadPress, closeEmojiAndAction } }); + + act(() => result.current()); + + expect(closeEmojiAndAction).toHaveBeenCalledWith(expect.any(Function)); + expect(dismissSpy).not.toHaveBeenCalled(); + expect(onThreadPress).not.toHaveBeenCalled(); + }); + + it('runs the gated press once closeEmojiAndAction invokes the handler it was given', () => { + const model = buildFakeModel({ tmid: 't1' }); + const onThreadPress = jest.fn(); + const closeEmojiAndAction = jest.fn((action?: (params?: unknown) => void) => action?.()); + const { result } = renderMessageHook(useMessagePress, model, { config: { onThreadPress, closeEmojiAndAction } }); + + act(() => result.current()); + + expect(dismissSpy).toHaveBeenCalledTimes(1); + expect(onThreadPress).toHaveBeenCalledWith(model); + }); + }); + describe('translation boundary on item.autoTranslate', () => { const translationConfig = { autoTranslateRoom: true, autoTranslateLanguage: 'pt-BR', user: { username: 'alice' } }; From f7680342559161545b156effa6fcb5ef99eed567 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 15:23:17 -0300 Subject: [PATCH 123/144] refactor: narrow IMessage.blocks and e2eMentions off any (NATIVE-22) blocks now types to @rocket.chat/ui-kit's LayoutBlock union (the type already rendered by UiKitMessage), and e2eMentions matches getE2EEMentions's real return shape. Closes the any hole at the useBlocks() hook boundary. --- .../message/stores/__tests__/MessageStore.test.tsx | 2 +- app/definitions/IMessage.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/containers/message/stores/__tests__/MessageStore.test.tsx b/app/containers/message/stores/__tests__/MessageStore.test.tsx index 1e117561668..d500527a81b 100644 --- a/app/containers/message/stores/__tests__/MessageStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageStore.test.tsx @@ -261,7 +261,7 @@ describe('MessageStore', () => { describe('domain hooks', () => { it('useBlocks selects blocks and id', () => { - const model = buildFakeModel({ blocks: [{ appId: 'app-1' }] }); + const model = buildFakeModel({ blocks: [{ type: 'divider', appId: 'app-1' }] }); const { latestValue } = renderMessage(model, { blocks: useBlocks }); expect(latestValue('blocks')).toEqual({ blocks: model.blocks, id: model.id }); }); diff --git a/app/definitions/IMessage.ts b/app/definitions/IMessage.ts index 4e211f9fb21..c348b22d48e 100644 --- a/app/definitions/IMessage.ts +++ b/app/definitions/IMessage.ts @@ -1,5 +1,6 @@ import type Model from '@nozbe/watermelondb/Model'; import { type Root } from '@rocket.chat/message-parser'; +import { type LayoutBlock } from '@rocket.chat/ui-kit'; import { type MessageTypeLoad } from '../lib/constants/messageTypeLoad'; import { type IAttachment } from './IAttachment'; @@ -57,6 +58,11 @@ export interface IMessageTranslations { export type E2EType = 'pending' | 'done'; +export interface IE2EEMentions { + e2eUserMentions: string[]; + e2eChannelMentions: string[]; +} + export interface ILastMessage { _id?: string; rid?: string; @@ -173,14 +179,14 @@ export interface IMessage extends IMessageFromServer { autoTranslate?: boolean; translations?: IMessageTranslations[]; tmsg?: string; - blocks?: any; + blocks?: LayoutBlock[] | null; e2e?: E2EType; tshow?: boolean; comment?: string; subscription?: { id: string }; user?: string; editedAt?: string | Date; - e2eMentions?: any; + e2eMentions?: IE2EEMentions; } export type TMessageModel = IMessage & From 49ceb59820ffd0a6b48982f6f4260b12bf3a1264 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 17:28:48 -0300 Subject: [PATCH 124/144] test: guard {memo:true} @json invariant (NATIVE-22) --- .../message/stores/MessageStore.tsx | 1 + .../model/__tests__/Message.memo.test.ts | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 app/lib/database/model/__tests__/Message.memo.test.ts diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index dc0dc921146..cb9068f7a9b 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -64,6 +64,7 @@ export const useMessageItem = (): TAnyMessageModel => useMessageStore(s => s.ite // Stability of JSON fields depends on the model's @json(..., { memo: true }) decorators // (app/lib/database/model/Message.js); a field that drops memo:true returns a fresh ref every // access and would defeat both the Object.is and useShallow bail. +// Guarded by app/lib/database/model/__tests__/Message.memo.test.ts. export const useMessageField = <T,>(selector: (item: TAnyMessageModel) => T): T => useMessageStore(s => selector(s.item)); // Plain REST objects (no experimentalSubscribe) never emit again after the initial render. diff --git a/app/lib/database/model/__tests__/Message.memo.test.ts b/app/lib/database/model/__tests__/Message.memo.test.ts new file mode 100644 index 00000000000..b0c9c091ea6 --- /dev/null +++ b/app/lib/database/model/__tests__/Message.memo.test.ts @@ -0,0 +1,25 @@ +import fs from 'fs'; +import path from 'path'; + +// MessageStore's Object.is/useShallow bail-outs depend on every @json field returning a stable +// reference for an unchanged record. That stability comes only from WatermelonDB's `{ memo: true }` +// decorator option (app/containers/message/stores/MessageStore.tsx:62-67). This is a source +// characterization test: it can't observe the runtime accessor without an LDB mock, so it guards +// the textual invariant instead. +const messageSource = fs.readFileSync(path.join(__dirname, '../Message.js'), 'utf8'); + +describe('Message.js @json memo invariant', () => { + const jsonDeclarations = messageSource.match(/@json\([^)]*\)/g) ?? []; + + // Positive control: a regex that matches nothing would let every assertion below pass vacuously. + it('finds the expected number of @json fields', () => { + expect(jsonDeclarations.length).toBe(13); + }); + + it('declares { memo: true } on every @json field', () => { + expect(jsonDeclarations.length).toBeGreaterThan(0); + jsonDeclarations.forEach(declaration => { + expect(declaration).toContain('{ memo: true }'); + }); + }); +}); From ffb81e9815a72a35bfc298f27e59d1e782c9d08f Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 17:40:04 -0300 Subject: [PATCH 125/144] feat: warn on capture-once store footguns (NATIVE-22) --- .../message/stores/MessageRoomStore.tsx | 53 ++++++++++++++++++- .../message/stores/MessageStore.tsx | 47 +++++++++++++++- .../__tests__/MessageRoomStore.test.tsx | 46 ++++++++++++++++ .../stores/__tests__/MessageStore.test.tsx | 39 ++++++++++++++ 4 files changed, 182 insertions(+), 3 deletions(-) diff --git a/app/containers/message/stores/MessageRoomStore.tsx b/app/containers/message/stores/MessageRoomStore.tsx index 80109505469..8e6162e4917 100644 --- a/app/containers/message/stores/MessageRoomStore.tsx +++ b/app/containers/message/stores/MessageRoomStore.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useEffect, useState, type ReactElement, type ReactNode } from 'react'; +import { createContext, useContext, useEffect, useRef, useState, type ReactElement, type ReactNode } from 'react'; import { createStore, useStore } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; @@ -55,10 +55,61 @@ const useMessageRoomStore = <T,>(selector: (state: MessageRoomState) => T): T => return useStore(store, selector); }; +// Handlers and room constants captured once at mount by MessageRoomStoreProvider below (everything +// outside the reactive-tail resync effect). Callers MUST pass referentially stable values for these. +const FROZEN_KEYS = [ + 'navToRoomInfo', + 'showAttachment', + 'blockAction', + 'handleEnterCall', + 'fetchThreadName', + 'toggleFollowThread', + 'jumpToMessage', + 'closeEmojiAndAction', + 'onReactionPress', + 'onReactionLongPress', + 'reactionInit', + 'onDiscussionPress', + 'onThreadPress', + 'replyBroadcast', + 'errorActionsShow', + 'onAnswerButtonPress', + 'onEncryptedPress', + 'rid', + 'user', + 'baseUrl', + 'isThreadRoom' +] as const satisfies readonly (keyof MessageRoomState)[]; + +const useFrozenHandlersGuardProd = (_state: MessageRoomState): void => {}; + +// Warns once (after mount) if a frozen handler/constant's identity changes, since the provider +// only captures the initial value and never re-syncs it (see FROZEN_KEYS above). +const useFrozenHandlersGuardDev = (state: MessageRoomState): void => { + const initialRef = useRef(state); + const warnedRef = useRef(false); + useEffect(() => { + if (warnedRef.current) return; + const changed = FROZEN_KEYS.filter(key => !Object.is(initialRef.current[key], state[key])); + if (changed.length > 0) { + warnedRef.current = true; + console.warn( + `[MessageRoomStore] handler/constant identity changed after mount for: ${changed.join(', ')}. ` + + 'MessageRoomProvider captures these once; pass referentially stable values.' + ); + } + }); +}; + +const useFrozenHandlersGuard: (state: MessageRoomState) => void = __DEV__ + ? useFrozenHandlersGuardDev + : useFrozenHandlersGuardProd; + const MessageRoomStoreProvider = ({ children, ...state }: { children: ReactNode } & MessageRoomState): ReactElement => { 'use memo'; const [store] = useState(() => createMessageRoomStore(state)); + useFrozenHandlersGuard(state); // These fields can change mid-session (e.g. an open room gets archived), unlike the // constants/handlers captured once above. The dep array keeps store writes on-change only. diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index cb9068f7a9b..b809de5c07d 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useEffect, useState, type ReactElement, type ReactNode } from 'react'; +import { createContext, useContext, useEffect, useRef, useState, type ReactElement, type ReactNode } from 'react'; import { createStore, useStore } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; import { Keyboard } from 'react-native'; @@ -65,7 +65,50 @@ export const useMessageItem = (): TAnyMessageModel => useMessageStore(s => s.ite // (app/lib/database/model/Message.js); a field that drops memo:true returns a fresh ref every // access and would defeat both the Object.is and useShallow bail. // Guarded by app/lib/database/model/__tests__/Message.memo.test.ts. -export const useMessageField = <T,>(selector: (item: TAnyMessageModel) => T): T => useMessageStore(s => selector(s.item)); +const isRecordOrArray = (value: unknown): value is Record<string, unknown> | unknown[] => + typeof value === 'object' && value !== null; + +const shallowEqual = (a: Record<string, unknown> | unknown[], b: Record<string, unknown> | unknown[]): boolean => { + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + if (aKeys.length !== bKeys.length) return false; + return aKeys.every(key => Object.is((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key])); +}; + +const useMessageFieldProd = <T,>(selector: (item: TAnyMessageModel) => T): T => useMessageStore(s => selector(s.item)); + +// Detects a selector that returns a fresh object/array every call (e.g. an inline `i => ({ a: i.a })`): +// on an unchanged tick, a memoized field selector keeps returning the same reference (Object.is bail +// holds), while a naive selector produces a new-but-shallow-equal value every time. Warns once. +const useMessageFieldDev = <T,>(selector: (item: TAnyMessageModel) => T): T => { + const prevRef = useRef<{ tick: number; result: T } | undefined>(undefined); + const warnedRef = useRef(false); + return useMessageStore(s => { + const result = selector(s.item); + const prev = prevRef.current; + if ( + !warnedRef.current && + prev && + prev.tick === s.tick && + !Object.is(prev.result, result) && + isRecordOrArray(prev.result) && + isRecordOrArray(result) && + shallowEqual(prev.result, result) + ) { + warnedRef.current = true; + console.warn( + '[MessageStore] useMessageField selector returned a new reference for unchanged data. ' + + 'Use a useShallow domain hook instead of an inline object/array selector.' + ); + } + prevRef.current = { tick: s.tick, result }; + return result; + }); +}; + +export const useMessageField: <T>(selector: (item: TAnyMessageModel) => T) => T = __DEV__ + ? useMessageFieldDev + : useMessageFieldProd; // Plain REST objects (no experimentalSubscribe) never emit again after the initial render. const subscribeModel = (m: TAnyMessageModel, store: MessageStore) => { diff --git a/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx b/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx index b686ba31385..d2428c83cec 100644 --- a/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx @@ -1,5 +1,6 @@ import { memo } from 'react'; import { act, render } from '@testing-library/react-native'; +import { Text } from 'react-native'; import { Provider } from 'react-redux'; import { MessageRoomProvider, useAutoTranslate, useIsArchived, useTimeFormat } from '../MessageRoomStore'; @@ -131,4 +132,49 @@ describe('MessageRoomStore', () => { act(() => rerender(wrap(true))); expect(spy).toHaveBeenLastCalledWith(true); }); + + describe('frozen handler guard (dev)', () => { + let warnSpy: jest.SpyInstance; + + beforeEach(() => { + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + warnSpy.mockRestore(); + }); + + it('warns once when a frozen handler identity changes after mount', () => { + const wrap = (navToRoomInfo: () => void) => ( + <Provider store={mockedStore}> + <MessageRoomProvider timeFormat='fixed-format' navToRoomInfo={navToRoomInfo}> + <Text>probe</Text> + </MessageRoomProvider> + </Provider> + ); + + const { rerender } = render(wrap(() => {})); + expect(warnSpy).not.toHaveBeenCalled(); + + act(() => rerender(wrap(() => {}))); + + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('navToRoomInfo')); + }); + + it('does not warn when the same handler reference is passed across re-renders', () => { + const navToRoomInfo = () => {}; + const wrap = () => ( + <Provider store={mockedStore}> + <MessageRoomProvider timeFormat='fixed-format' navToRoomInfo={navToRoomInfo}> + <Text>probe</Text> + </MessageRoomProvider> + </Provider> + ); + + const { rerender } = render(wrap()); + act(() => rerender(wrap())); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + }); }); diff --git a/app/containers/message/stores/__tests__/MessageStore.test.tsx b/app/containers/message/stores/__tests__/MessageStore.test.tsx index d500527a81b..fd8a8a08dee 100644 --- a/app/containers/message/stores/__tests__/MessageStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageStore.test.tsx @@ -1050,4 +1050,43 @@ describe('MessageStore', () => { expect(latest()).toEqual({ messageText: model.msg, isTranslated: false }); }); }); + + describe('useMessageField misuse guard (dev)', () => { + let warnSpy: jest.SpyInstance; + + beforeEach(() => { + warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + }); + + afterEach(() => { + warnSpy.mockRestore(); + }); + + // A selector that returns a fresh object on every call never lets React's useSyncExternalStore + // snapshot settle, so this misuse also trips React's own update-depth guard. The dev warning + // still fires (once) before that crash, which is what this test asserts. + it('warns when the selector returns a new object every call for unchanged data', () => { + const model = buildFakeModel(); + + expect(() => { + renderMessageHook(() => useMessageField(item => ({ id: item.id })), model); + }).toThrow('Maximum update depth exceeded'); + + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('[MessageStore] useMessageField selector')); + }); + + it('does not warn for a primitive field selector', () => { + const model = buildFakeModel(); + renderMessageHook(() => useMessageField(item => item.id), model); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + + it('does not warn for a memoized object field selector (stable reference across calls)', () => { + const model = buildFakeModel(); + renderMessageHook(() => useMessageField(item => item.reactions), model); + + expect(warnSpy).not.toHaveBeenCalled(); + }); + }); }); From 5827c9ca75ddaa3043119c0c49ca1b1cd0b595f6 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 17:41:10 -0300 Subject: [PATCH 126/144] refactor: drop dead editing/rightButtonsWidth state and close the any hole (NATIVE-22) --- app/views/RoomView/constants.ts | 4 +--- app/views/RoomView/definitions.ts | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/views/RoomView/constants.ts b/app/views/RoomView/constants.ts index 4f790e05832..448cf161420 100644 --- a/app/views/RoomView/constants.ts +++ b/app/views/RoomView/constants.ts @@ -5,13 +5,11 @@ export const stateAttrsUpdate = [ 'lastOpen', 'canAutoTranslate', 'loading', - 'editing', 'readOnly', 'member', 'canForwardGuest', 'canReturnQueue', - 'canViewCannedResponse', - 'rightButtonsWidth' + 'canViewCannedResponse' ] as TStateAttrsUpdate[]; export const roomAttrsUpdate = [ diff --git a/app/views/RoomView/definitions.ts b/app/views/RoomView/definitions.ts index dca60c4ba5b..812e4f3ce03 100644 --- a/app/views/RoomView/definitions.ts +++ b/app/views/RoomView/definitions.ts @@ -30,7 +30,6 @@ export type TStateAttrsUpdate = keyof IRoomViewState; export type TRoomUpdate = keyof TSubscriptionModel; export interface IRoomViewState { - [key: string]: any; joined: boolean; room: | TSubscriptionModel @@ -59,4 +58,8 @@ export interface IRoomViewState { isAutocompleteVisible: boolean; showMissingE2EEKey: boolean; showE2EEDisabledRoom: boolean; + canForwardGuest: boolean; + canReturnQueue: boolean; + canViewCannedResponse: boolean; + canPlaceLivechatOnHold: boolean; } From 43494d2dc1db9560cae4b4826f0dcc6d90e3442b Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 17:44:47 -0300 Subject: [PATCH 127/144] test: close Blocks/RoomProviders/MessageRoomStore coverage gaps (NATIVE-22) --- .../components/__tests__/Blocks.test.tsx | 80 +++++++++++++++++++ .../__tests__/MessageRoomStore.test.tsx | 20 +++++ app/views/RoomView/RoomProviders.test.tsx | 30 ++++++- 3 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 app/containers/message/components/__tests__/Blocks.test.tsx diff --git a/app/containers/message/components/__tests__/Blocks.test.tsx b/app/containers/message/components/__tests__/Blocks.test.tsx new file mode 100644 index 00000000000..4c1b7595635 --- /dev/null +++ b/app/containers/message/components/__tests__/Blocks.test.tsx @@ -0,0 +1,80 @@ +import { Provider } from 'react-redux'; +import { render } from '@testing-library/react-native'; + +import Blocks from '../Blocks'; +import { MessageProvider } from '../../stores/MessageStore'; +import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; +import { mockedStore } from '../../../../reducers/mockedStore'; +import { type TAnyMessageModel } from '../../../../definitions'; + +jest.mock('../../../UIKit/MessageBlock', () => ({ + messageBlockWithContext: jest.fn(() => () => null) +})); + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { messageBlockWithContext } = jest.requireMock('../../../UIKit/MessageBlock'); + +const buildItem = (blocks: TAnyMessageModel['blocks']) => ({ id: 'msg-1', blocks } as unknown as TAnyMessageModel); + +const renderBlocks = (blocks: TAnyMessageModel['blocks'], config: Partial<MessageRoomState> = {}) => + render( + <Provider store={mockedStore}> + <MessageRoomProvider timeFormat='fixed-format' {...config}> + <MessageProvider item={buildItem(blocks)}> + <Blocks /> + </MessageProvider> + </MessageRoomProvider> + </Provider> + ); + +describe('Blocks', () => { + beforeEach(() => { + messageBlockWithContext.mockClear(); + }); + + it('renders null and skips messageBlockWithContext when blocks is null', () => { + const { toJSON } = renderBlocks(null); + expect(toJSON()).toBeNull(); + expect(messageBlockWithContext).not.toHaveBeenCalled(); + }); + + it('renders null and skips messageBlockWithContext when blocks is empty', () => { + const { toJSON } = renderBlocks([]); + expect(toJSON()).toBeNull(); + expect(messageBlockWithContext).not.toHaveBeenCalled(); + }); + + it('wires appId from the first block and forwards rid', () => { + renderBlocks([{ appId: 'app-1' }] as TAnyMessageModel['blocks'], { rid: 'room-1' }); + expect(messageBlockWithContext).toHaveBeenCalledWith(expect.objectContaining({ appId: 'app-1', rid: 'room-1' })); + }); + + it('falls back appId to an empty string when the first block has none', () => { + renderBlocks([{}] as TAnyMessageModel['blocks']); + expect(messageBlockWithContext).toHaveBeenCalledWith(expect.objectContaining({ appId: '' })); + }); + + it("calls blockAction with the wired params and rid defaulted to '' when absent", async () => { + const blockAction = jest.fn(); + renderBlocks([{ appId: 'app-1' }] as TAnyMessageModel['blocks'], { blockAction }); + + const { action } = messageBlockWithContext.mock.calls[0][0]; + await action({ actionId: 'submit', value: 'v', blockId: 'block-1' }); + + expect(blockAction).toHaveBeenCalledWith({ + actionId: 'submit', + appId: 'app-1', + value: 'v', + blockId: 'block-1', + rid: '', + mid: 'msg-1' + }); + }); + + it('no-ops without throwing when blockAction is undefined', async () => { + renderBlocks([{ appId: 'app-1' }] as TAnyMessageModel['blocks']); + + const { action } = messageBlockWithContext.mock.calls[0][0]; + await expect(action({ actionId: 'submit', value: 'v', blockId: 'block-1' })).resolves.toBeUndefined(); + }); +}); diff --git a/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx b/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx index d2428c83cec..9c5ee9304ef 100644 --- a/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx @@ -8,6 +8,26 @@ import { mockedStore } from '../../../../reducers/mockedStore'; import { updateSettings } from '../../../../actions/settings'; describe('MessageRoomStore', () => { + describe('outside a MessageRoomProvider', () => { + let consoleErrorSpy: jest.SpyInstance; + + beforeEach(() => { + consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + }); + + it('useIsArchived throws', () => { + const Probe = () => { + useIsArchived(); + return null; + }; + expect(() => render(<Probe />)).toThrow('Message room hooks must be used within a MessageRoomProvider'); + }); + }); + it('mirrors updated provider props into the store after mount', () => { const spy = jest.fn(); const Probe = () => { diff --git a/app/views/RoomView/RoomProviders.test.tsx b/app/views/RoomView/RoomProviders.test.tsx index 3d7545c8202..1160b04be76 100644 --- a/app/views/RoomView/RoomProviders.test.tsx +++ b/app/views/RoomView/RoomProviders.test.tsx @@ -1,8 +1,8 @@ -import { render } from '@testing-library/react-native'; +import { act, render } from '@testing-library/react-native'; import { RoomProviders } from './RoomProviders'; import { useRoomContext, type IRoomContext } from './context'; -import { createMessageActionStore } from '../../containers/message/stores/MessageActionStore'; +import { createMessageActionStore, useIsBeingEdited, useMessageAction } from '../../containers/message/stores/MessageActionStore'; describe('RoomProviders', () => { it('keeps the same RoomContext value reference across re-renders with unchanged props', () => { @@ -50,4 +50,30 @@ describe('RoomProviders', () => { expect(values).toHaveLength(2); expect(values[1]).not.toBe(values[0]); }); + + it('resolves useMessageAction/useIsBeingEdited to the store passed in props, not some other store', () => { + const store = createMessageActionStore(); + const actionSpy = jest.fn(); + const isBeingEditedSpy = jest.fn(); + + const Probe = () => { + actionSpy(useMessageAction()); + isBeingEditedSpy(useIsBeingEdited('msg-1')); + return null; + }; + + render( + <RoomProviders store={store} rid='rid-1' t='c' room={{ rid: 'rid-1' }}> + <Probe /> + </RoomProviders> + ); + + expect(actionSpy).toHaveBeenLastCalledWith(null); + expect(isBeingEditedSpy).toHaveBeenLastCalledWith(false); + + act(() => store.getState().actions.startEditing('msg-1')); + + expect(actionSpy).toHaveBeenLastCalledWith({ kind: 'edit', messageId: 'msg-1' }); + expect(isBeingEditedSpy).toHaveBeenLastCalledWith(true); + }); }); From d3283db43132729d3e20c5e4d2fefe9a0d20bd9f Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 17:54:28 -0300 Subject: [PATCH 128/144] refactor: remove any from message story fixtures (NATIVE-22) --- .../stories/Attachments.stories.tsx | 2 +- .../components/stories/Message.stories.tsx | 29 ++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/app/containers/message/components/stories/Attachments.stories.tsx b/app/containers/message/components/stories/Attachments.stories.tsx index 3ffdee25d91..28ca40d4b68 100644 --- a/app/containers/message/components/stories/Attachments.stories.tsx +++ b/app/containers/message/components/stories/Attachments.stories.tsx @@ -17,7 +17,7 @@ const mockMessageContext: Partial<MessageRoomState> = { const mockItem = { id: 'msg-id', msg: '', u: { username: 'rocket.cat' }, autoTranslate: false } as unknown as TAnyMessageModel; -const StoryWrapper = ({ store, children }: { store: any; children: ReactNode }) => ( +const StoryWrapper = ({ store, children }: { store: ReturnType<typeof createMockedStore>; children: ReactNode }) => ( <Provider store={store}> <MessageRoomProvider {...mockMessageContext}> <MessageProvider item={mockItem}>{children}</MessageProvider> diff --git a/app/containers/message/components/stories/Message.stories.tsx b/app/containers/message/components/stories/Message.stories.tsx index 5885a85bdb5..d8b2c37816e 100644 --- a/app/containers/message/components/stories/Message.stories.tsx +++ b/app/containers/message/components/stories/Message.stories.tsx @@ -1,3 +1,4 @@ +import { type ComponentType } from 'react'; import { ScrollView } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; @@ -101,7 +102,7 @@ const roomHandlers: Partial<MessageRoomState> = { export default { title: 'Message', decorators: [ - (Story: any) => ( + (Story: ComponentType) => ( <NavigationContainer> <ScrollView style={{ backgroundColor: themes[_theme].surfaceRoom }}> <Story /> @@ -119,7 +120,7 @@ const editedByFixture = { _id: 'edited-by', username: 'diego.mello' }; let storyMessageIdCounter = 0; -const buildMessage = (overrides: Record<string, any> = {}): TAnyMessageModel => { +const buildMessage = (overrides: Record<string, unknown> = {}): TAnyMessageModel => { const definedOverrides = Object.fromEntries(Object.entries(overrides).filter(([, value]) => value !== undefined)); return { id: `story-message-${++storyMessageIdCounter}`, @@ -162,7 +163,7 @@ const buildMessage = (overrides: Record<string, any> = {}): TAnyMessageModel => // Builds the message model for a single <Message>/<MessageLargeFont> call, pulling the // scalar display flags (author/type/hasError/status/isEdited/isTranslated) out of the // props that used to be passed straight to the leaf component. -const buildItem = (props: any): TAnyMessageModel => { +const buildItem = (props: Record<string, unknown>): TAnyMessageModel => { const { author: authorOverride, type, hasError, status, isEdited, isTranslated, ...itemProps } = props; const resolvedAuthor = authorOverride ?? (isTranslated ? otherUser : author); return buildMessage({ @@ -205,7 +206,7 @@ const resolvePreviousItem = (item: TAnyMessageModel, isHeader: boolean | undefin return undefined; }; -const renderMessageStory = (fontScale: number, props: any) => { +const renderMessageStory = (fontScale: number, props: Record<string, unknown>) => { const { isHeader, isThreadReply, @@ -220,7 +221,21 @@ const renderMessageStory = (fontScale: number, props: any) => { autoTranslateRoom, autoTranslateLanguage, ...itemProps - } = props; + } = props as { + isHeader?: boolean; + isThreadReply?: boolean; + previousItem?: TAnyMessageModel; + broadcast?: boolean; + archived?: boolean; + timeFormat?: string; + useRealName?: boolean; + isReadReceiptEnabled?: boolean; + isIgnored?: boolean; + isTranslated?: boolean; + autoTranslateRoom?: boolean; + autoTranslateLanguage?: string; + [key: string]: unknown; + }; const item = buildItem({ ...itemProps, isTranslated }); const previousItem = previousItemOverride ?? resolvePreviousItem(item, isHeader, isThreadReply); @@ -254,10 +269,10 @@ const renderMessageStory = (fontScale: number, props: any) => { ); }; -export const Message = (props: any) => renderMessageStory(1, props); +export const Message = (props: Record<string, unknown>) => renderMessageStory(1, props); // The large font components are not perfect because the text's font scale increases only with the device's font size setting. -const MessageLargeFont = (props: any) => renderMessageStory(FONT_SCALE_LIMIT, props); +const MessageLargeFont = (props: Record<string, unknown>) => renderMessageStory(FONT_SCALE_LIMIT, props); export const Basic = () => ( <> From 0a12d2f48582dbb274eefeae1ef1bc08ffdb0711 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 18:01:34 -0300 Subject: [PATCH 129/144] refactor: split message components into Message/ (density) + Layout/ (type) (NATIVE-22) --- .../AttachmentLayout.tsx} | 4 +-- .../BlocksLayout.tsx} | 4 +-- .../DiscussionLayout.tsx} | 4 +-- .../JitsiLayout.tsx} | 4 +-- .../components/Layout/PreviewLayout.tsx | 17 ++++++++++ .../StandardLayout.tsx} | 8 ++--- .../message/components/Layout/index.tsx | 32 +++++++++++++++++++ .../{NormalMessage.tsx => FullMessage.tsx} | 8 ++--- .../components/Message/MessageInner.tsx | 32 ------------------- .../components/Message/PreviewBranch.tsx | 17 ---------- .../{Message.tsx => Message/index.tsx} | 22 ++++++------- ...siBranch.test.tsx => JitsiLayout.test.tsx} | 10 +++--- 12 files changed, 81 insertions(+), 81 deletions(-) rename app/containers/message/components/{Message/BranchAttachmentContent.tsx => Layout/AttachmentLayout.tsx} (81%) rename app/containers/message/components/{Message/BlocksBranch.tsx => Layout/BlocksLayout.tsx} (77%) rename app/containers/message/components/{Message/DiscussionBranch.tsx => Layout/DiscussionLayout.tsx} (70%) rename app/containers/message/components/{Message/JitsiBranch.tsx => Layout/JitsiLayout.tsx} (75%) create mode 100644 app/containers/message/components/Layout/PreviewLayout.tsx rename app/containers/message/components/{Message/DefaultBranch.tsx => Layout/StandardLayout.tsx} (66%) create mode 100644 app/containers/message/components/Layout/index.tsx rename app/containers/message/components/Message/{NormalMessage.tsx => FullMessage.tsx} (82%) delete mode 100644 app/containers/message/components/Message/MessageInner.tsx delete mode 100644 app/containers/message/components/Message/PreviewBranch.tsx rename app/containers/message/components/{Message.tsx => Message/index.tsx} (77%) rename app/containers/message/components/__tests__/{JitsiBranch.test.tsx => JitsiLayout.test.tsx} (84%) diff --git a/app/containers/message/components/Message/BranchAttachmentContent.tsx b/app/containers/message/components/Layout/AttachmentLayout.tsx similarity index 81% rename from app/containers/message/components/Message/BranchAttachmentContent.tsx rename to app/containers/message/components/Layout/AttachmentLayout.tsx index 8f86a115bff..48ba74f20e4 100644 --- a/app/containers/message/components/Message/BranchAttachmentContent.tsx +++ b/app/containers/message/components/Layout/AttachmentLayout.tsx @@ -4,8 +4,8 @@ import Quote from '../Attachments/Quote'; import Urls from '../Urls'; import { useAttachments, useMessageField } from '../../stores/MessageStore'; -// Shared leaf composition for DefaultBranch and PreviewBranch -export const BranchAttachmentContent = () => { +// Shared leaf composition for StandardLayout and PreviewLayout +export const AttachmentLayout = () => { 'use memo'; const attachments = useAttachments(); diff --git a/app/containers/message/components/Message/BlocksBranch.tsx b/app/containers/message/components/Layout/BlocksLayout.tsx similarity index 77% rename from app/containers/message/components/Message/BlocksBranch.tsx rename to app/containers/message/components/Layout/BlocksLayout.tsx index f2f491c9655..31d3c0eadfb 100644 --- a/app/containers/message/components/Message/BlocksBranch.tsx +++ b/app/containers/message/components/Layout/BlocksLayout.tsx @@ -4,7 +4,7 @@ import Thread from '../Thread'; import Reactions from '../Reactions'; import MessageTime from '../Time'; -const BlocksBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { +const BlocksLayout = ({ showTimeLarge }: { showTimeLarge: boolean }) => { 'use memo'; return ( @@ -18,4 +18,4 @@ const BlocksBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { ); }; -export default BlocksBranch; +export default BlocksLayout; diff --git a/app/containers/message/components/Message/DiscussionBranch.tsx b/app/containers/message/components/Layout/DiscussionLayout.tsx similarity index 70% rename from app/containers/message/components/Message/DiscussionBranch.tsx rename to app/containers/message/components/Layout/DiscussionLayout.tsx index f08741b99cb..11ea3071be9 100644 --- a/app/containers/message/components/Message/DiscussionBranch.tsx +++ b/app/containers/message/components/Layout/DiscussionLayout.tsx @@ -2,7 +2,7 @@ import User from '../User'; import Discussion from '../Discussion'; import MessageTime from '../Time'; -const DiscussionBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { +const DiscussionLayout = ({ showTimeLarge }: { showTimeLarge: boolean }) => { 'use memo'; return ( @@ -14,4 +14,4 @@ const DiscussionBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { ); }; -export default DiscussionBranch; +export default DiscussionLayout; diff --git a/app/containers/message/components/Message/JitsiBranch.tsx b/app/containers/message/components/Layout/JitsiLayout.tsx similarity index 75% rename from app/containers/message/components/Message/JitsiBranch.tsx rename to app/containers/message/components/Layout/JitsiLayout.tsx index 945daf74f87..314edcc7e46 100644 --- a/app/containers/message/components/Message/JitsiBranch.tsx +++ b/app/containers/message/components/Layout/JitsiLayout.tsx @@ -3,7 +3,7 @@ import Content from '../Content'; import CallButton from '../CallButton'; import MessageTime from '../Time'; -const JitsiBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { +const JitsiLayout = ({ showTimeLarge }: { showTimeLarge: boolean }) => { 'use memo'; return ( @@ -16,4 +16,4 @@ const JitsiBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { ); }; -export default JitsiBranch; +export default JitsiLayout; diff --git a/app/containers/message/components/Layout/PreviewLayout.tsx b/app/containers/message/components/Layout/PreviewLayout.tsx new file mode 100644 index 00000000000..8664388870a --- /dev/null +++ b/app/containers/message/components/Layout/PreviewLayout.tsx @@ -0,0 +1,17 @@ +import User from '../User'; +import MessageTime from '../Time'; +import { AttachmentLayout } from './AttachmentLayout'; + +const PreviewLayout = ({ showTimeLarge }: { showTimeLarge: boolean }) => { + 'use memo'; + + return ( + <> + <User /> + {showTimeLarge ? <MessageTime /> : null} + <AttachmentLayout /> + </> + ); +}; + +export default PreviewLayout; diff --git a/app/containers/message/components/Message/DefaultBranch.tsx b/app/containers/message/components/Layout/StandardLayout.tsx similarity index 66% rename from app/containers/message/components/Message/DefaultBranch.tsx rename to app/containers/message/components/Layout/StandardLayout.tsx index 3c8241af8d2..c379dd80c4b 100644 --- a/app/containers/message/components/Message/DefaultBranch.tsx +++ b/app/containers/message/components/Layout/StandardLayout.tsx @@ -5,9 +5,9 @@ import Thread from '../Thread'; import Reactions from '../Reactions'; import Broadcast from '../Broadcast'; import MessageTime from '../Time'; -import { BranchAttachmentContent } from './BranchAttachmentContent'; +import { AttachmentLayout } from './AttachmentLayout'; -const DefaultBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { +const StandardLayout = ({ showTimeLarge }: { showTimeLarge: boolean }) => { 'use memo'; return ( @@ -15,7 +15,7 @@ const DefaultBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { <User /> {showTimeLarge ? <MessageTime /> : null} <View style={{ gap: 4 }}> - <BranchAttachmentContent /> + <AttachmentLayout /> <Thread /> <Reactions /> <Broadcast /> @@ -24,4 +24,4 @@ const DefaultBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { ); }; -export default DefaultBranch; +export default StandardLayout; diff --git a/app/containers/message/components/Layout/index.tsx b/app/containers/message/components/Layout/index.tsx new file mode 100644 index 00000000000..aff8b95f069 --- /dev/null +++ b/app/containers/message/components/Layout/index.tsx @@ -0,0 +1,32 @@ +import { WidthAwareView } from '../WidthAwareView'; +import { useResponsiveLayout } from '../../../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; +import BlocksLayout from './BlocksLayout'; +import JitsiLayout from './JitsiLayout'; +import DiscussionLayout from './DiscussionLayout'; +import PreviewLayout from './PreviewLayout'; +import StandardLayout from './StandardLayout'; +import { useBlocks, useMessageField } from '../../stores/MessageStore'; + +export const Layout = ({ isPreview, isHeader }: { isPreview?: boolean; isHeader: boolean }) => { + 'use memo'; + + const { isLargeFontScale } = useResponsiveLayout(); + const type = useMessageField(item => item.t); + const { blocks } = useBlocks(); + const showTimeLarge = isLargeFontScale && isHeader; + + let layout; + if (blocks && blocks.length) { + layout = <BlocksLayout showTimeLarge={showTimeLarge} />; + } else if (type === 'jitsi_call_started') { + layout = <JitsiLayout showTimeLarge={showTimeLarge} />; + } else if (type === 'discussion-created') { + layout = <DiscussionLayout showTimeLarge={showTimeLarge} />; + } else if (isPreview) { + layout = <PreviewLayout showTimeLarge={showTimeLarge} />; + } else { + layout = <StandardLayout showTimeLarge={showTimeLarge} />; + } + + return <WidthAwareView>{layout}</WidthAwareView>; +}; diff --git a/app/containers/message/components/Message/NormalMessage.tsx b/app/containers/message/components/Message/FullMessage.tsx similarity index 82% rename from app/containers/message/components/Message/NormalMessage.tsx rename to app/containers/message/components/Message/FullMessage.tsx index 2b8b747320a..34eb6040d74 100644 --- a/app/containers/message/components/Message/NormalMessage.tsx +++ b/app/containers/message/components/Message/FullMessage.tsx @@ -4,11 +4,11 @@ import { A11y } from 'react-native-a11y-order'; import styles from '../../styles'; import MessageAvatar from '../MessageAvatar'; import RightIcons from '../RightIcons'; -import { MessageInner } from './MessageInner'; +import { Layout } from '../Layout'; import { useMessageField, useMessageGrouping, useMessageText } from '../../stores/MessageStore'; import { useAutoTranslate } from '../../stores/MessageRoomStore'; -const NormalMessage = ({ isPreview }: { isPreview?: boolean }) => { +const FullMessage = ({ isPreview }: { isPreview?: boolean }) => { 'use memo'; const isHeader = useMessageGrouping(); @@ -26,7 +26,7 @@ const NormalMessage = ({ isPreview }: { isPreview?: boolean }) => { <View style={styles.flex}> <MessageAvatar /> <View style={styles.messageContent}> - <MessageInner isPreview={isPreview} isHeader={isHeader} /> + <Layout isPreview={isPreview} isHeader={isHeader} /> </View> {!isHeader ? <RightIcons /> : null} </View> @@ -35,4 +35,4 @@ const NormalMessage = ({ isPreview }: { isPreview?: boolean }) => { ); }; -export default NormalMessage; +export default FullMessage; diff --git a/app/containers/message/components/Message/MessageInner.tsx b/app/containers/message/components/Message/MessageInner.tsx deleted file mode 100644 index ef7e2b2654f..00000000000 --- a/app/containers/message/components/Message/MessageInner.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { WidthAwareView } from '../WidthAwareView'; -import { useResponsiveLayout } from '../../../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; -import BlocksBranch from './BlocksBranch'; -import JitsiBranch from './JitsiBranch'; -import DiscussionBranch from './DiscussionBranch'; -import PreviewBranch from './PreviewBranch'; -import DefaultBranch from './DefaultBranch'; -import { useBlocks, useMessageField } from '../../stores/MessageStore'; - -export const MessageInner = ({ isPreview, isHeader }: { isPreview?: boolean; isHeader: boolean }) => { - 'use memo'; - - const { isLargeFontScale } = useResponsiveLayout(); - const type = useMessageField(item => item.t); - const { blocks } = useBlocks(); - const showTimeLarge = isLargeFontScale && isHeader; - - let branch; - if (blocks && blocks.length) { - branch = <BlocksBranch showTimeLarge={showTimeLarge} />; - } else if (type === 'jitsi_call_started') { - branch = <JitsiBranch showTimeLarge={showTimeLarge} />; - } else if (type === 'discussion-created') { - branch = <DiscussionBranch showTimeLarge={showTimeLarge} />; - } else if (isPreview) { - branch = <PreviewBranch showTimeLarge={showTimeLarge} />; - } else { - branch = <DefaultBranch showTimeLarge={showTimeLarge} />; - } - - return <WidthAwareView>{branch}</WidthAwareView>; -}; diff --git a/app/containers/message/components/Message/PreviewBranch.tsx b/app/containers/message/components/Message/PreviewBranch.tsx deleted file mode 100644 index 7d8aa4ca453..00000000000 --- a/app/containers/message/components/Message/PreviewBranch.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import User from '../User'; -import MessageTime from '../Time'; -import { BranchAttachmentContent } from './BranchAttachmentContent'; - -const PreviewBranch = ({ showTimeLarge }: { showTimeLarge: boolean }) => { - 'use memo'; - - return ( - <> - <User /> - {showTimeLarge ? <MessageTime /> : null} - <BranchAttachmentContent /> - </> - ); -}; - -export default PreviewBranch; diff --git a/app/containers/message/components/Message.tsx b/app/containers/message/components/Message/index.tsx similarity index 77% rename from app/containers/message/components/Message.tsx rename to app/containers/message/components/Message/index.tsx index 196ac279c5f..e9615e49111 100644 --- a/app/containers/message/components/Message.tsx +++ b/app/containers/message/components/Message/index.tsx @@ -1,14 +1,14 @@ import { A11y } from 'react-native-a11y-order'; -import { useTheme } from '../../../theme'; -import Touch from './Touch'; -import CompactMessage from './Message/CompactMessage'; -import NormalMessage from './Message/NormalMessage'; -import { useLastFocusedMessageRef } from '../../../lib/a11y/useLastFocusedMessageRef'; -import { useMessageAccessibilityLabel } from '../hooks/useMessageAccessibilityLabel'; -import { useMessageAccessibilityActions } from '../hooks/useMessageAccessibilityActions'; -import { useMessageAccessibilityHint } from '../hooks/useMessageAccessibilityHint'; -import { useIsBeingEdited } from '../stores/MessageActionStore'; +import { useTheme } from '../../../../theme'; +import Touch from '../Touch'; +import CompactMessage from './CompactMessage'; +import FullMessage from './FullMessage'; +import { useLastFocusedMessageRef } from '../../../../lib/a11y/useLastFocusedMessageRef'; +import { useMessageAccessibilityLabel } from '../../hooks/useMessageAccessibilityLabel'; +import { useMessageAccessibilityActions } from '../../hooks/useMessageAccessibilityActions'; +import { useMessageAccessibilityHint } from '../../hooks/useMessageAccessibilityHint'; +import { useIsBeingEdited } from '../../stores/MessageActionStore'; import { useIsInfoMessage, useMessageField, @@ -18,7 +18,7 @@ import { useMessageStatus, useMessageTouchable, useThreadPosition -} from '../stores/MessageStore'; +} from '../../stores/MessageStore'; type TMessageProps = { isPreview?: boolean; @@ -36,7 +36,7 @@ const Message = (props: TMessageProps) => { return <CompactMessage />; } - return <NormalMessage isPreview={props.isPreview} />; + return <FullMessage isPreview={props.isPreview} />; }; const MessageTouchable = (props: TMessageProps) => { diff --git a/app/containers/message/components/__tests__/JitsiBranch.test.tsx b/app/containers/message/components/__tests__/JitsiLayout.test.tsx similarity index 84% rename from app/containers/message/components/__tests__/JitsiBranch.test.tsx rename to app/containers/message/components/__tests__/JitsiLayout.test.tsx index 6dab34edff3..bfaae1594a8 100644 --- a/app/containers/message/components/__tests__/JitsiBranch.test.tsx +++ b/app/containers/message/components/__tests__/JitsiLayout.test.tsx @@ -1,7 +1,7 @@ import { Provider } from 'react-redux'; import { render } from '@testing-library/react-native'; -import JitsiBranch from '../Message/JitsiBranch'; +import JitsiLayout from '../Layout/JitsiLayout'; import { MessageProvider } from '../../stores/MessageStore'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { mockedStore } from '../../../../reducers/mockedStore'; @@ -15,22 +15,22 @@ const item = { u: { _id: 'author-id', username: 'diego.mello' } } as unknown as TAnyMessageModel; -const renderJitsiBranch = () => { +const renderJitsiLayout = () => { const room: Partial<MessageRoomState> = { handleEnterCall: jest.fn() }; return render( <Provider store={mockedStore}> <MessageRoomProvider {...room}> <MessageProvider item={item}> - <JitsiBranch showTimeLarge={false} /> + <JitsiLayout showTimeLarge={false} /> </MessageProvider> </MessageRoomProvider> </Provider> ); }; -describe('JitsiBranch', () => { +describe('JitsiLayout', () => { test('renders the localized "started a call" line AND the join button for a jitsi_call_started message', () => { - const { getByText } = renderJitsiBranch(); + const { getByText } = renderJitsiLayout(); expect(getByText(I18n.t('Started_call', { userBy: 'diego.mello' }))).toBeTruthy(); expect(getByText(I18n.t('Click_to_join'))).toBeTruthy(); }); From 780456c639a7c4de060045f64db4624a583e9099 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 18:05:16 -0300 Subject: [PATCH 130/144] refactor: colocate Content orchestrator into Content/index.tsx (NATIVE-22) --- .../components/{Content.tsx => Content/index.tsx} | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) rename app/containers/message/components/{Content.tsx => Content/index.tsx} (70%) diff --git a/app/containers/message/components/Content.tsx b/app/containers/message/components/Content/index.tsx similarity index 70% rename from app/containers/message/components/Content.tsx rename to app/containers/message/components/Content/index.tsx index cd03df9a7fd..b38ed94725e 100644 --- a/app/containers/message/components/Content.tsx +++ b/app/containers/message/components/Content/index.tsx @@ -1,10 +1,10 @@ -import EncryptedContent from './Content/EncryptedContent'; -import IgnoredContent from './Content/IgnoredContent'; -import InfoContent from './Content/InfoContent'; -import MarkdownContent from './Content/MarkdownContent'; -import PreviewContent from './Content/PreviewContent'; -import { useIsEncrypted, useIsInfoMessage, useMessageIgnored, useMessageText, useThreadData } from '../stores/MessageStore'; -import { useIsThreadRoom } from '../stores/MessageRoomStore'; +import EncryptedContent from './EncryptedContent'; +import IgnoredContent from './IgnoredContent'; +import InfoContent from './InfoContent'; +import MarkdownContent from './MarkdownContent'; +import PreviewContent from './PreviewContent'; +import { useIsEncrypted, useIsInfoMessage, useMessageIgnored, useMessageText, useThreadData } from '../../stores/MessageStore'; +import { useIsThreadRoom } from '../../stores/MessageRoomStore'; // isInfo forces InfoContent regardless of useIsInfoMessage — jitsi_call_started is deliberately // excluded there (it isn't a compact/non-touchable info row) but still needs the info-style body. From 5e560b785bf92555036a1efe25d30f221a27d7bf Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Wed, 8 Jul 2026 18:16:36 -0300 Subject: [PATCH 131/144] chore: annotate Touch.tsx with 'use memo' compiler directive (NATIVE-22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Touch.tsx was the only PR-touched message-render-path leaf missing the directive under compilationMode 'annotation'. No react-hooks lint rule (use-memo, preserve-manual-memoization) actually detects a missing directive — they validate useMemo() usage and compiler-preservation of existing manual memoization, both already enforced via the extended plugin:react-hooks/recommended config at their default severities (0 violations). Enabling them explicitly would only duplicate or, if downgraded to 'warn' per convention, weaken already-clean 'error' defaults, so .eslintrc.js is left unchanged. --- app/containers/message/components/Touch.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/containers/message/components/Touch.tsx b/app/containers/message/components/Touch.tsx index be7239ea2a9..710126cb539 100644 --- a/app/containers/message/components/Touch.tsx +++ b/app/containers/message/components/Touch.tsx @@ -51,6 +51,8 @@ const Touch = forwardRef<View, ITouchProps>( }, ref ) => { + 'use memo'; + const { colors } = useTheme(); // The background color must be applied to the RectButton, not the View. // If set on the View, the touch opacity animation won't work properly. From f2a428fed01f03a506964943cdb033a8b5697633 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 9 Jul 2026 10:41:07 -0300 Subject: [PATCH 132/144] =?UTF-8?q?refactor:=20rename=20AttachmentLayout?= =?UTF-8?q?=20=E2=86=92=20ContentLayout=20(NATIVE-22)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Layout/{AttachmentLayout.tsx => ContentLayout.tsx} | 4 ++-- app/containers/message/components/Layout/PreviewLayout.tsx | 4 ++-- app/containers/message/components/Layout/StandardLayout.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename app/containers/message/components/Layout/{AttachmentLayout.tsx => ContentLayout.tsx} (81%) diff --git a/app/containers/message/components/Layout/AttachmentLayout.tsx b/app/containers/message/components/Layout/ContentLayout.tsx similarity index 81% rename from app/containers/message/components/Layout/AttachmentLayout.tsx rename to app/containers/message/components/Layout/ContentLayout.tsx index 48ba74f20e4..d794c657fc9 100644 --- a/app/containers/message/components/Layout/AttachmentLayout.tsx +++ b/app/containers/message/components/Layout/ContentLayout.tsx @@ -4,8 +4,8 @@ import Quote from '../Attachments/Quote'; import Urls from '../Urls'; import { useAttachments, useMessageField } from '../../stores/MessageStore'; -// Shared leaf composition for StandardLayout and PreviewLayout -export const AttachmentLayout = () => { +// Main-path Content orchestration for StandardLayout and PreviewLayout +export const ContentLayout = () => { 'use memo'; const attachments = useAttachments(); diff --git a/app/containers/message/components/Layout/PreviewLayout.tsx b/app/containers/message/components/Layout/PreviewLayout.tsx index 8664388870a..ad9703af3f6 100644 --- a/app/containers/message/components/Layout/PreviewLayout.tsx +++ b/app/containers/message/components/Layout/PreviewLayout.tsx @@ -1,6 +1,6 @@ import User from '../User'; import MessageTime from '../Time'; -import { AttachmentLayout } from './AttachmentLayout'; +import { ContentLayout } from './ContentLayout'; const PreviewLayout = ({ showTimeLarge }: { showTimeLarge: boolean }) => { 'use memo'; @@ -9,7 +9,7 @@ const PreviewLayout = ({ showTimeLarge }: { showTimeLarge: boolean }) => { <> <User /> {showTimeLarge ? <MessageTime /> : null} - <AttachmentLayout /> + <ContentLayout /> </> ); }; diff --git a/app/containers/message/components/Layout/StandardLayout.tsx b/app/containers/message/components/Layout/StandardLayout.tsx index c379dd80c4b..083e388fd7b 100644 --- a/app/containers/message/components/Layout/StandardLayout.tsx +++ b/app/containers/message/components/Layout/StandardLayout.tsx @@ -5,7 +5,7 @@ import Thread from '../Thread'; import Reactions from '../Reactions'; import Broadcast from '../Broadcast'; import MessageTime from '../Time'; -import { AttachmentLayout } from './AttachmentLayout'; +import { ContentLayout } from './ContentLayout'; const StandardLayout = ({ showTimeLarge }: { showTimeLarge: boolean }) => { 'use memo'; @@ -15,7 +15,7 @@ const StandardLayout = ({ showTimeLarge }: { showTimeLarge: boolean }) => { <User /> {showTimeLarge ? <MessageTime /> : null} <View style={{ gap: 4 }}> - <AttachmentLayout /> + <ContentLayout /> <Thread /> <Reactions /> <Broadcast /> From b907bc88a09bd12ff0041d13b971a8474518de0e Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 9 Jul 2026 10:45:41 -0300 Subject: [PATCH 133/144] refactor: split Message and MessageTouchable into separate files (NATIVE-22) --- .../message/components/Message/Message.tsx | 24 +++++++++++++++++ .../message/components/Message/index.tsx | 26 ++----------------- 2 files changed, 26 insertions(+), 24 deletions(-) create mode 100644 app/containers/message/components/Message/Message.tsx diff --git a/app/containers/message/components/Message/Message.tsx b/app/containers/message/components/Message/Message.tsx new file mode 100644 index 00000000000..860d5b2d4c3 --- /dev/null +++ b/app/containers/message/components/Message/Message.tsx @@ -0,0 +1,24 @@ +import CompactMessage from './CompactMessage'; +import FullMessage from './FullMessage'; +import { useIsInfoMessage, useMessageIgnored, useThreadPosition } from '../../stores/MessageStore'; + +export type TMessageProps = { + isPreview?: boolean; + highlighted?: boolean; +}; + +const Message = (props: TMessageProps) => { + 'use memo'; + + const { isThreadReply, isThreadSequential } = useThreadPosition(); + const isInfo = useIsInfoMessage(); + const isIgnored = useMessageIgnored(); + + if (isThreadReply || isThreadSequential || isInfo || isIgnored) { + return <CompactMessage />; + } + + return <FullMessage isPreview={props.isPreview} />; +}; + +export default Message; diff --git a/app/containers/message/components/Message/index.tsx b/app/containers/message/components/Message/index.tsx index e9615e49111..d08418714a6 100644 --- a/app/containers/message/components/Message/index.tsx +++ b/app/containers/message/components/Message/index.tsx @@ -2,8 +2,7 @@ import { A11y } from 'react-native-a11y-order'; import { useTheme } from '../../../../theme'; import Touch from '../Touch'; -import CompactMessage from './CompactMessage'; -import FullMessage from './FullMessage'; +import Message, { type TMessageProps } from './Message'; import { useLastFocusedMessageRef } from '../../../../lib/a11y/useLastFocusedMessageRef'; import { useMessageAccessibilityLabel } from '../../hooks/useMessageAccessibilityLabel'; import { useMessageAccessibilityActions } from '../../hooks/useMessageAccessibilityActions'; @@ -12,33 +11,12 @@ import { useIsBeingEdited } from '../../stores/MessageActionStore'; import { useIsInfoMessage, useMessageField, - useMessageIgnored, useMessageLongPress, useMessagePress, useMessageStatus, - useMessageTouchable, - useThreadPosition + useMessageTouchable } from '../../stores/MessageStore'; -type TMessageProps = { - isPreview?: boolean; - highlighted?: boolean; -}; - -const Message = (props: TMessageProps) => { - 'use memo'; - - const { isThreadReply, isThreadSequential } = useThreadPosition(); - const isInfo = useIsInfoMessage(); - const isIgnored = useMessageIgnored(); - - if (isThreadReply || isThreadSequential || isInfo || isIgnored) { - return <CompactMessage />; - } - - return <FullMessage isPreview={props.isPreview} />; -}; - const MessageTouchable = (props: TMessageProps) => { 'use memo'; From 387fd4a3138a07fe671f3c7b7ef0b77b9ed12db3 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 9 Jul 2026 13:50:51 -0300 Subject: [PATCH 134/144] fix: correct stale InteractionStore error message in MessageActionStore (NATIVE-22) --- app/containers/message/stores/MessageActionStore.tsx | 2 +- .../message/stores/__tests__/MessageActionStore.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/containers/message/stores/MessageActionStore.tsx b/app/containers/message/stores/MessageActionStore.tsx index b563e2da727..433ea93fb0c 100644 --- a/app/containers/message/stores/MessageActionStore.tsx +++ b/app/containers/message/stores/MessageActionStore.tsx @@ -74,7 +74,7 @@ export const inertStore = createStore<MessageActionState>()(() => ({ action: nul const useMessageActionStore = <T,>(selector: (state: MessageActionState) => T): T => { const store = useContext(MessageActionStoreContext); if (!store) { - throw new Error('Interaction hooks must be used within a MessageActionProvider'); + throw new Error('Message action hooks must be used within a MessageActionProvider'); } return useStore(store, selector); }; diff --git a/app/containers/message/stores/__tests__/MessageActionStore.test.tsx b/app/containers/message/stores/__tests__/MessageActionStore.test.tsx index f9fb187c6a2..a342618f4a4 100644 --- a/app/containers/message/stores/__tests__/MessageActionStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageActionStore.test.tsx @@ -111,7 +111,7 @@ describe('MessageActionStore', () => { useMessageAction(); return null; }; - expect(() => render(<Probe />)).toThrow('Interaction hooks must be used within a MessageActionProvider'); + expect(() => render(<Probe />)).toThrow('Message action hooks must be used within a MessageActionProvider'); }); }); From de784df35f42d8d937d7f08a7d622a03c772d456 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 9 Jul 2026 14:25:28 -0300 Subject: [PATCH 135/144] refactor: use MessageActionProvider in prod, relocate test helper (NATIVE-22) RoomProviders now mounts MessageActionProvider (external-store param) instead of the raw context, so no message provider is prod-unused. Move index.testHelpers into __tests__/testHelpers with an ignore pattern so Jest does not treat it as a suite. --- app/containers/message/__tests__/index.test.tsx | 2 +- .../{index.testHelpers.tsx => __tests__/testHelpers.tsx} | 8 ++++---- .../message/components/__tests__/Reactions.test.tsx | 2 +- app/containers/message/stores/MessageActionStore.tsx | 4 +++- app/views/RoomView/RoomProviders.tsx | 6 +++--- jest.config.js | 2 +- 6 files changed, 13 insertions(+), 11 deletions(-) rename app/containers/message/{index.testHelpers.tsx => __tests__/testHelpers.tsx} (79%) diff --git a/app/containers/message/__tests__/index.test.tsx b/app/containers/message/__tests__/index.test.tsx index 51996cbb186..2e42f4c088e 100644 --- a/app/containers/message/__tests__/index.test.tsx +++ b/app/containers/message/__tests__/index.test.tsx @@ -4,7 +4,7 @@ import { fireEvent } from '@testing-library/react-native'; import MessageContainer from '../index'; import { type TAnyMessageModel } from '../../../definitions'; import { createMessageActionStore, MessageActionStoreContext } from '../stores/MessageActionStore'; -import { renderWithMessageProviders } from '../index.testHelpers'; +import { renderWithMessageProviders } from './testHelpers'; import { type MessageRoomState } from '../stores/MessageRoomStore'; jest.mock('../components/Touch', () => { diff --git a/app/containers/message/index.testHelpers.tsx b/app/containers/message/__tests__/testHelpers.tsx similarity index 79% rename from app/containers/message/index.testHelpers.tsx rename to app/containers/message/__tests__/testHelpers.tsx index 33036518ae6..6775a41b771 100644 --- a/app/containers/message/index.testHelpers.tsx +++ b/app/containers/message/__tests__/testHelpers.tsx @@ -2,10 +2,10 @@ import { type ReactElement, type ReactNode } from 'react'; import { Provider } from 'react-redux'; import { render } from '@testing-library/react-native'; -import { type TAnyMessageModel } from '../../definitions'; -import { mockedStore } from '../../reducers/mockedStore'; -import { MessageProvider } from './stores/MessageStore'; -import { MessageRoomProvider, type MessageRoomState } from './stores/MessageRoomStore'; +import { type TAnyMessageModel } from '../../../definitions'; +import { mockedStore } from '../../../reducers/mockedStore'; +import { MessageProvider } from '../stores/MessageStore'; +import { MessageRoomProvider, type MessageRoomState } from '../stores/MessageRoomStore'; interface IMessageProvidersOptions { item?: TAnyMessageModel; diff --git a/app/containers/message/components/__tests__/Reactions.test.tsx b/app/containers/message/components/__tests__/Reactions.test.tsx index ff43b26aeb5..33ad050df72 100644 --- a/app/containers/message/components/__tests__/Reactions.test.tsx +++ b/app/containers/message/components/__tests__/Reactions.test.tsx @@ -1,7 +1,7 @@ import { render, fireEvent } from '@testing-library/react-native'; import Reactions from '../Reactions'; -import { MessageProviders } from '../../index.testHelpers'; +import { MessageProviders } from '../../__tests__/testHelpers'; import { setUser } from '../../../../actions/login'; import { mockedStore } from '../../../../reducers/mockedStore'; import { type IReaction, type TAnyMessageModel } from '../../../../definitions'; diff --git a/app/containers/message/stores/MessageActionStore.tsx b/app/containers/message/stores/MessageActionStore.tsx index 433ea93fb0c..a5b77f8c6a8 100644 --- a/app/containers/message/stores/MessageActionStore.tsx +++ b/app/containers/message/stores/MessageActionStore.tsx @@ -92,14 +92,16 @@ export const useIsBeingEdited = (messageId: string): boolean => { }; export const MessageActionProvider = ({ + store: externalStore, initialAction, children }: { + store?: TMessageActionStore; initialAction?: TMessageActionState; children: ReactNode; }): ReactNode => { 'use memo'; - const [store] = useState(() => createMessageActionStore(initialAction)); + const [store] = useState(() => externalStore ?? createMessageActionStore(initialAction)); return <MessageActionStoreContext.Provider value={store}>{children}</MessageActionStoreContext.Provider>; }; diff --git a/app/views/RoomView/RoomProviders.tsx b/app/views/RoomView/RoomProviders.tsx index b87211b86ed..a9e66a994c1 100644 --- a/app/views/RoomView/RoomProviders.tsx +++ b/app/views/RoomView/RoomProviders.tsx @@ -1,7 +1,7 @@ import { type ReactElement } from 'react'; import { RoomContext, type IRoomContext } from './context'; -import { type TMessageActionStore, MessageActionStoreContext } from '../../containers/message/stores/MessageActionStore'; +import { type TMessageActionStore, MessageActionProvider } from '../../containers/message/stores/MessageActionStore'; type IRoomProvidersProps = IRoomContext & { store: TMessageActionStore; @@ -28,7 +28,7 @@ export const RoomProviders = ({ 'use memo'; return ( - <MessageActionStoreContext.Provider value={store}> + <MessageActionProvider store={store}> <RoomContext.Provider value={{ rid, @@ -47,6 +47,6 @@ export const RoomProviders = ({ }}> {children} </RoomContext.Provider> - </MessageActionStoreContext.Provider> + </MessageActionProvider> ); }; diff --git a/jest.config.js b/jest.config.js index b2b30897d93..aa5e3a720c6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,6 @@ module.exports = { modulePathIgnorePatterns: ['<rootDir>/.*worktrees/'], - testPathIgnorePatterns: ['e2e', 'node_modules', '<rootDir>/.*worktrees/'], + testPathIgnorePatterns: ['e2e', 'node_modules', '<rootDir>/.*worktrees/', '/__tests__/testHelpers\\.tsx$'], transformIgnorePatterns: [ 'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg|@rocket.chat/ui-kit|@rocket.chat/sdk|tiny-events)' ], From 149d7e1f9605382db12cf9f31d14cfe6d1971c73 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 9 Jul 2026 14:34:57 -0300 Subject: [PATCH 136/144] chore: remove dead displayName and redundant comment (NATIVE-22) Drop inert Fields.displayName assignments (plain FCs, not React.memo) in Reply and CollapsibleQuote, and a redundant ContentLayout comment. Addresses self-review nits on PR #7455. --- .../message/components/Attachments/CollapsibleQuote.tsx | 2 -- app/containers/message/components/Attachments/Reply.tsx | 2 -- app/containers/message/components/Layout/ContentLayout.tsx | 1 - 3 files changed, 5 deletions(-) diff --git a/app/containers/message/components/Attachments/CollapsibleQuote.tsx b/app/containers/message/components/Attachments/CollapsibleQuote.tsx index 717aaeb97b7..2a4cdd2d5f0 100644 --- a/app/containers/message/components/Attachments/CollapsibleQuote.tsx +++ b/app/containers/message/components/Attachments/CollapsibleQuote.tsx @@ -166,6 +166,4 @@ const CollapsibleQuote = ({ attachment }: IMessageReply) => { ); }; -Fields.displayName = 'CollapsibleQuoteFields'; - export default CollapsibleQuote; diff --git a/app/containers/message/components/Attachments/Reply.tsx b/app/containers/message/components/Attachments/Reply.tsx index 4d4a297d450..32e5d48b6ad 100644 --- a/app/containers/message/components/Attachments/Reply.tsx +++ b/app/containers/message/components/Attachments/Reply.tsx @@ -236,6 +236,4 @@ const Reply = ({ attachment, msg }: IMessageReply) => { ); }; -Fields.displayName = 'MessageReplyFields'; - export default Reply; diff --git a/app/containers/message/components/Layout/ContentLayout.tsx b/app/containers/message/components/Layout/ContentLayout.tsx index d794c657fc9..aeb31c34a62 100644 --- a/app/containers/message/components/Layout/ContentLayout.tsx +++ b/app/containers/message/components/Layout/ContentLayout.tsx @@ -4,7 +4,6 @@ import Quote from '../Attachments/Quote'; import Urls from '../Urls'; import { useAttachments, useMessageField } from '../../stores/MessageStore'; -// Main-path Content orchestration for StandardLayout and PreviewLayout export const ContentLayout = () => { 'use memo'; From 0a958081b67342dfea36d7947586b3148e4fcb1a Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 9 Jul 2026 15:03:11 -0300 Subject: [PATCH 137/144] refactor: group message touch-surfaces into Touchable/ (NATIVE-22) Move Touch, MessageActionTouchable, and the misnamed Message/index.tsx (actually MessageTouchable) into a single components/Touchable/ folder. Pure move + rename + import path fixups, no behavior change. --- app/containers/message/__tests__/index.test.tsx | 2 +- .../message/components/Attachments/CollapsibleQuote.tsx | 2 +- .../message/components/Attachments/Image/Button.tsx | 2 +- app/containers/message/components/Attachments/Reply.tsx | 2 +- app/containers/message/components/Attachments/Video.tsx | 2 +- app/containers/message/components/Broadcast.tsx | 2 +- app/containers/message/components/CallButton.tsx | 2 +- app/containers/message/components/Discussion.tsx | 2 +- app/containers/message/components/Reactions.tsx | 2 +- app/containers/message/components/RightIcons/Encrypted.tsx | 2 +- app/containers/message/components/RightIcons/MessageError.tsx | 2 +- app/containers/message/components/Thread.tsx | 2 +- .../components/{ => Touchable}/MessageActionTouchable.tsx | 2 +- .../{Message/index.tsx => Touchable/MessageTouchable.tsx} | 4 ++-- app/containers/message/components/{ => Touchable}/Touch.tsx | 4 ++-- app/containers/message/components/Urls.tsx | 2 +- app/containers/message/components/__tests__/Preview.test.tsx | 2 +- app/containers/message/index.tsx | 4 ++-- 18 files changed, 21 insertions(+), 21 deletions(-) rename app/containers/message/components/{ => Touchable}/MessageActionTouchable.tsx (88%) rename app/containers/message/components/{Message/index.tsx => Touchable/MessageTouchable.tsx} (96%) rename app/containers/message/components/{ => Touchable}/Touch.tsx (96%) diff --git a/app/containers/message/__tests__/index.test.tsx b/app/containers/message/__tests__/index.test.tsx index 2e42f4c088e..e46bab4f5be 100644 --- a/app/containers/message/__tests__/index.test.tsx +++ b/app/containers/message/__tests__/index.test.tsx @@ -7,7 +7,7 @@ import { createMessageActionStore, MessageActionStoreContext } from '../stores/M import { renderWithMessageProviders } from './testHelpers'; import { type MessageRoomState } from '../stores/MessageRoomStore'; -jest.mock('../components/Touch', () => { +jest.mock('../components/Touchable/Touch', () => { const { forwardRef } = require('react'); const { TouchableOpacity } = require('react-native'); return forwardRef(({ children, onPress, onLongPress, ...props }: any, ref: any) => ( diff --git a/app/containers/message/components/Attachments/CollapsibleQuote.tsx b/app/containers/message/components/Attachments/CollapsibleQuote.tsx index 2a4cdd2d5f0..761a0a22920 100644 --- a/app/containers/message/components/Attachments/CollapsibleQuote.tsx +++ b/app/containers/message/components/Attachments/CollapsibleQuote.tsx @@ -8,7 +8,7 @@ import { useTheme } from '../../../../theme'; import sharedStyles from '../../../../views/Styles'; import Markdown from '../../../markdown'; import { useMessageUser } from '../../stores/MessageRoomStore'; -import MessageActionTouchable from '../MessageActionTouchable'; +import MessageActionTouchable from '../Touchable/MessageActionTouchable'; import { BUTTON_HIT_SLOP } from '../../utils'; const styles = StyleSheet.create({ diff --git a/app/containers/message/components/Attachments/Image/Button.tsx b/app/containers/message/components/Attachments/Image/Button.tsx index eb98ec00da8..9304df94156 100644 --- a/app/containers/message/components/Attachments/Image/Button.tsx +++ b/app/containers/message/components/Attachments/Image/Button.tsx @@ -1,7 +1,7 @@ import { type ReactElement } from 'react'; import { type PressableProps } from 'react-native'; -import MessageActionTouchable from '../../MessageActionTouchable'; +import MessageActionTouchable from '../../Touchable/MessageActionTouchable'; import styles from '../../../styles'; interface IMessageButton { diff --git a/app/containers/message/components/Attachments/Reply.tsx b/app/containers/message/components/Attachments/Reply.tsx index 32e5d48b6ad..e35ff7ca980 100644 --- a/app/containers/message/components/Attachments/Reply.tsx +++ b/app/containers/message/components/Attachments/Reply.tsx @@ -14,7 +14,7 @@ import { Attachments } from './components'; import Quote from './Quote'; import { useBaseUrl, useMessageUser, useTimeFormat } from '../../stores/MessageRoomStore'; import { useIsEncrypted, useMessageId } from '../../stores/MessageStore'; -import MessageActionTouchable from '../MessageActionTouchable'; +import MessageActionTouchable from '../Touchable/MessageActionTouchable'; import messageStyles from '../../styles'; import dayjs from '../../../../lib/dayjs'; diff --git a/app/containers/message/components/Attachments/Video.tsx b/app/containers/message/components/Attachments/Video.tsx index 7c33b740f5d..4894b758997 100644 --- a/app/containers/message/components/Attachments/Video.tsx +++ b/app/containers/message/components/Attachments/Video.tsx @@ -11,7 +11,7 @@ import sharedStyles from '../../../../views/Styles'; import { type TIconsName } from '../../../CustomIcon'; import { LISTENER } from '../../../Toast'; import Markdown from '../../../markdown'; -import MessageActionTouchable from '../MessageActionTouchable'; +import MessageActionTouchable from '../Touchable/MessageActionTouchable'; import { useMediaAutoDownload } from '../../hooks/useMediaAutoDownload'; import { useMessageUser } from '../../stores/MessageRoomStore'; import BlurComponent from '../OverlayComponent'; diff --git a/app/containers/message/components/Broadcast.tsx b/app/containers/message/components/Broadcast.tsx index 3beb5f500ee..111e312ae09 100644 --- a/app/containers/message/components/Broadcast.tsx +++ b/app/containers/message/components/Broadcast.tsx @@ -1,6 +1,6 @@ import { Text, View } from 'react-native'; -import MessageActionTouchable from './MessageActionTouchable'; +import MessageActionTouchable from './Touchable/MessageActionTouchable'; import { CustomIcon } from '../../CustomIcon'; import styles from '../styles'; import { BUTTON_HIT_SLOP } from '../utils'; diff --git a/app/containers/message/components/CallButton.tsx b/app/containers/message/components/CallButton.tsx index 381d648ef36..6202c8422ae 100644 --- a/app/containers/message/components/CallButton.tsx +++ b/app/containers/message/components/CallButton.tsx @@ -1,6 +1,6 @@ import { Text, View } from 'react-native'; -import MessageActionTouchable from './MessageActionTouchable'; +import MessageActionTouchable from './Touchable/MessageActionTouchable'; import { BUTTON_HIT_SLOP } from '../utils'; import styles from '../styles'; import I18n from '../../../i18n'; diff --git a/app/containers/message/components/Discussion.tsx b/app/containers/message/components/Discussion.tsx index 0e5e06cd213..13e80c630eb 100644 --- a/app/containers/message/components/Discussion.tsx +++ b/app/containers/message/components/Discussion.tsx @@ -1,6 +1,6 @@ import { Text, View } from 'react-native'; -import MessageActionTouchable from './MessageActionTouchable'; +import MessageActionTouchable from './Touchable/MessageActionTouchable'; import { BUTTON_HIT_SLOP, formatMessageCount } from '../utils'; import styles from '../styles'; import I18n from '../../../i18n'; diff --git a/app/containers/message/components/Reactions.tsx b/app/containers/message/components/Reactions.tsx index 2641a65f94e..9129bc17e17 100644 --- a/app/containers/message/components/Reactions.tsx +++ b/app/containers/message/components/Reactions.tsx @@ -1,7 +1,7 @@ import { Text, useWindowDimensions, View } from 'react-native'; import I18n from '../../../i18n'; -import MessageActionTouchable from './MessageActionTouchable'; +import MessageActionTouchable from './Touchable/MessageActionTouchable'; import { CustomIcon } from '../../CustomIcon'; import styles from '../styles'; import Emoji from './Emoji'; diff --git a/app/containers/message/components/RightIcons/Encrypted.tsx b/app/containers/message/components/RightIcons/Encrypted.tsx index 8e930a17ecc..65f2ee8412f 100644 --- a/app/containers/message/components/RightIcons/Encrypted.tsx +++ b/app/containers/message/components/RightIcons/Encrypted.tsx @@ -1,4 +1,4 @@ -import MessageActionTouchable from '../MessageActionTouchable'; +import MessageActionTouchable from '../Touchable/MessageActionTouchable'; import { CustomIcon } from '../../../CustomIcon'; import { BUTTON_HIT_SLOP } from '../../utils'; import styles from '../../styles'; diff --git a/app/containers/message/components/RightIcons/MessageError.tsx b/app/containers/message/components/RightIcons/MessageError.tsx index e9306781bd0..ecb71ae6db6 100644 --- a/app/containers/message/components/RightIcons/MessageError.tsx +++ b/app/containers/message/components/RightIcons/MessageError.tsx @@ -1,4 +1,4 @@ -import MessageActionTouchable from '../MessageActionTouchable'; +import MessageActionTouchable from '../Touchable/MessageActionTouchable'; import { CustomIcon } from '../../../CustomIcon'; import styles from '../../styles'; import { BUTTON_HIT_SLOP } from '../../utils'; diff --git a/app/containers/message/components/Thread.tsx b/app/containers/message/components/Thread.tsx index 08bb09a4f63..fdfbd0d8360 100644 --- a/app/containers/message/components/Thread.tsx +++ b/app/containers/message/components/Thread.tsx @@ -4,7 +4,7 @@ import styles from '../styles'; import ThreadDetails from '../../ThreadDetails'; import I18n from '../../../i18n'; import { useTheme } from '../../../theme'; -import MessageActionTouchable from './MessageActionTouchable'; +import MessageActionTouchable from './Touchable/MessageActionTouchable'; import { useMessageItem, useMessageText, useReplies, useThreadBadgeColor, useThreadData } from '../stores/MessageStore'; import { useIsThreadRoom, useMessageUser, useOnThreadPress, useToggleFollowThread } from '../stores/MessageRoomStore'; diff --git a/app/containers/message/components/MessageActionTouchable.tsx b/app/containers/message/components/Touchable/MessageActionTouchable.tsx similarity index 88% rename from app/containers/message/components/MessageActionTouchable.tsx rename to app/containers/message/components/Touchable/MessageActionTouchable.tsx index 220561af2a1..3a03fdbf802 100644 --- a/app/containers/message/components/MessageActionTouchable.tsx +++ b/app/containers/message/components/Touchable/MessageActionTouchable.tsx @@ -1,7 +1,7 @@ import { type ReactNode, type FC } from 'react'; import { Pressable, type PressableProps } from 'react-native'; -import { useMessageLongPress } from '../stores/MessageStore'; +import { useMessageLongPress } from '../../stores/MessageStore'; interface IProps extends PressableProps { children: ReactNode; diff --git a/app/containers/message/components/Message/index.tsx b/app/containers/message/components/Touchable/MessageTouchable.tsx similarity index 96% rename from app/containers/message/components/Message/index.tsx rename to app/containers/message/components/Touchable/MessageTouchable.tsx index d08418714a6..e4e39c43855 100644 --- a/app/containers/message/components/Message/index.tsx +++ b/app/containers/message/components/Touchable/MessageTouchable.tsx @@ -1,8 +1,8 @@ import { A11y } from 'react-native-a11y-order'; import { useTheme } from '../../../../theme'; -import Touch from '../Touch'; -import Message, { type TMessageProps } from './Message'; +import Touch from './Touch'; +import Message, { type TMessageProps } from '../Message/Message'; import { useLastFocusedMessageRef } from '../../../../lib/a11y/useLastFocusedMessageRef'; import { useMessageAccessibilityLabel } from '../../hooks/useMessageAccessibilityLabel'; import { useMessageAccessibilityActions } from '../../hooks/useMessageAccessibilityActions'; diff --git a/app/containers/message/components/Touch.tsx b/app/containers/message/components/Touchable/Touch.tsx similarity index 96% rename from app/containers/message/components/Touch.tsx rename to app/containers/message/components/Touchable/Touch.tsx index 710126cb539..e405384a8f0 100644 --- a/app/containers/message/components/Touch.tsx +++ b/app/containers/message/components/Touchable/Touch.tsx @@ -12,8 +12,8 @@ import { } from 'react-native'; import { withKeyboardFocus } from 'react-native-external-keyboard'; -import { useTheme } from '../../../theme'; -import { isIOS } from '../../../lib/methods/helpers'; +import { useTheme } from '../../../../theme'; +import { isIOS } from '../../../../lib/methods/helpers'; export interface ITouchProps extends TouchableWithoutFeedbackProps { children: ReactNode; diff --git a/app/containers/message/components/Urls.tsx b/app/containers/message/components/Urls.tsx index 4b4a9920312..ec42868cfa7 100644 --- a/app/containers/message/components/Urls.tsx +++ b/app/containers/message/components/Urls.tsx @@ -4,7 +4,7 @@ import Clipboard from '@react-native-clipboard/clipboard'; import { Image } from 'expo-image'; import axios from 'axios'; -import MessageActionTouchable from './MessageActionTouchable'; +import MessageActionTouchable from './Touchable/MessageActionTouchable'; import openLink from '../../../lib/methods/helpers/openLink'; import sharedStyles from '../../../views/Styles'; import { useTheme } from '../../../theme'; diff --git a/app/containers/message/components/__tests__/Preview.test.tsx b/app/containers/message/components/__tests__/Preview.test.tsx index 28ec84ba56e..e6a55125c9b 100644 --- a/app/containers/message/components/__tests__/Preview.test.tsx +++ b/app/containers/message/components/__tests__/Preview.test.tsx @@ -5,7 +5,7 @@ import MessagePreview from '../Preview'; import { mockedStore } from '../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../definitions'; -jest.mock('../Touch', () => { +jest.mock('../Touchable/Touch', () => { const { forwardRef } = require('react'); const { TouchableOpacity } = require('react-native'); return forwardRef(({ children, onPress, onLongPress, ...props }: any, ref: any) => ( diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index e583c8b5abf..37fff0f8caf 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -1,6 +1,6 @@ import { memo } from 'react'; -import Message from './components/Message'; +import MessageTouchable from './components/Touchable/MessageTouchable'; import { type TAnyMessageModel } from '../../definitions'; import MessageSeparator from '../MessageSeparator'; import { MessageProvider } from './stores/MessageStore'; @@ -30,7 +30,7 @@ const MessageContainer = (props: IMessageContainerProps) => { onLongPress={props.onLongPress} threadBadgeColor={props.threadBadgeColor} isIgnored={props.isIgnored}> - <Message isPreview={props.isPreview} highlighted={props.highlighted} /> + <MessageTouchable isPreview={props.isPreview} highlighted={props.highlighted} /> <MessageSeparator ts={dateSeparator} unread={showUnreadSeparator} /> </MessageProvider> ); From 555f941226fd761648ab609ea98e2288b2f9a7f0 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 9 Jul 2026 15:15:28 -0300 Subject: [PATCH 138/144] docs: retire 'interaction' from glossary, fold into Message Action State (NATIVE-22) InteractionStore was renamed to MessageActionStore; the ubiquitous-language glossary followed. Selection now lives inside the Message Action union, so the separate 'Interaction state' term collapses into 'Message Action State'. --- CONTEXT.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 7127eec850b..1399e8e8b1d 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -104,18 +104,18 @@ Independent boolean markers on a Message, orthogonal to its Status — a Message | **Room History** | Older Messages of a Room fetched on demand from the server (distinct from **Server History**) | Message history | | **Jump to Message** | Re-position the Room view onto a target Message that may be far from the Live Tail or not yet synced — fetches a surrounding Chunk | Scroll to message | -## Message Interaction & Position State +## Message Action & Position State -Two distinct kinds of transient per-Room state drive how the Room view renders Messages. They have different owners and are migrated independently, so keep them apart. +Two distinct kinds of transient per-Room state drive how the Room view renders Messages. Keep them apart. -| Term | Definition | Owner | Scope | -| --------------------- | -------------------------------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------- | -| **Interaction state** | Which Message is selected and the current Message Action (Quote, Edit, or React) | A per-Room interaction store | Migrated to the per-Room store as part of the Message row work | -| **Positional state** | Which Message is highlighted and the jump or scroll position | The Room view scroll machinery | Stays with the Room view scroll machinery and is migrated separately | +| Term | Definition | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------- | +| **Message Action State** | The active Message Action in the Room (Quote, Edit, or React) and its target Message(s); null when none is active | +| **Positional State** | Which Message is highlighted and the jump or scroll position | ### Message Actions -A **Message Action** is the current interaction mode on a Message in the Room view. The three actions are **Quote**, **Edit**, and **React** — there is no "reply" action; replying in a Thread is a separate navigation, not an interaction mode. +A **Message Action** is the active mode on a Message in the Room view. The three actions are **Quote**, **Edit**, and **React** — there is no "reply" action; replying in a Thread is a separate navigation, not a Message Action. | Term | Definition | Aliases to avoid | | --------- | --------------------------------------------------------------------------------------- | ---------------- | @@ -249,3 +249,4 @@ A **Message Action** is the current interaction mode on a Message in the Room vi - **"Muted"** is overloaded: a User can be muted in a Room (a moderator action that removes send permission, recorded by `user-muted`/`mute_unmute` System Messages) OR be an **Ignored User** (a per-viewer filter that hides their Messages behind an Ignored Message placeholder, stored in `room.ignored`). Muting is a room permission; ignoring is a personal filter. Different concepts — keep them apart. - **"Reply"** is overloaded: **Reply Broadcast** is the action available to non-authorized users in a Broadcast Room; replying in a **Thread** is navigation into the Thread view. Neither is a **Message Action** — there is no "reply" Message Action. - **"Status" vs "flags"** — a Message has exactly one delivery **Status** (Sent, Temp, Error). **Pinned** and **Starred** are independent **Message Flags**, not statuses; do not group them with delivery states. +- **"Interaction" retired** — the selection-plus-action state was once an "interaction" concept; the canonical term is now **Message Action State**. Use **Message Action**, not "interaction", for which Message is selected and how. (Selection is not separate — it lives inside the active Message Action.) From 514c18d9fb7ccf2cc635569223aa7797c864eed8 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 9 Jul 2026 15:34:16 -0300 Subject: [PATCH 139/144] refactor: dedup message component helpers (NATIVE-22) - add useIsOwnMessage selector; drop repeated author/user equality in User and Broadcast - add getAttachmentKey; collapse the four copy-pasted attachment key fallbacks - extract MessageAccessibleIndex; share the a11y index between Compact/FullMessage - add shallowEqual to MessagePreview selector to stop re-renders on unrelated dispatches --- .../components/Attachments/Attachments.tsx | 8 +++--- .../message/components/Attachments/Quote.tsx | 4 +-- .../message/components/Attachments/utils.ts | 4 +++ .../message/components/Broadcast.tsx | 8 +++--- .../components/Message/CompactMessage.tsx | 15 +++-------- .../components/Message/FullMessage.tsx | 15 +++-------- .../components/MessageAccessibleIndex.tsx | 26 +++++++++++++++++++ app/containers/message/components/Preview.tsx | 13 +++++++--- app/containers/message/components/User.tsx | 7 +++-- .../message/stores/MessageStore.tsx | 6 +++++ 10 files changed, 64 insertions(+), 42 deletions(-) create mode 100644 app/containers/message/components/MessageAccessibleIndex.tsx diff --git a/app/containers/message/components/Attachments/Attachments.tsx b/app/containers/message/components/Attachments/Attachments.tsx index f60e28e8325..ee8e1042b8b 100644 --- a/app/containers/message/components/Attachments/Attachments.tsx +++ b/app/containers/message/components/Attachments/Attachments.tsx @@ -11,7 +11,7 @@ import { useShowAttachment } from '../../stores/MessageRoomStore'; import { useTranslateLanguage } from '../../stores/MessageStore'; import { type IMessageAttachments } from '../../interfaces'; import { getMessageFromAttachment } from '../../utils'; -import { isContentAttachment } from './utils'; +import { getAttachmentKey, isContentAttachment } from './utils'; const Attachments: FC<IMessageAttachments> = ({ attachments, author }: IMessageAttachments) => { 'use memo'; @@ -51,14 +51,14 @@ const Attachments: FC<IMessageAttachments> = ({ attachments, author }: IMessageA } if (file.actions && file.actions.length > 0) { - return <AttachedActions key={file.title_link || file.message_link || `actions-${index}`} attachment={file} />; + return <AttachedActions key={getAttachmentKey(file, 'actions', index)} attachment={file} />; } if (typeof file.collapsed === 'boolean') { - return <CollapsibleQuote key={file.title_link || file.message_link || `collapsible-${index}`} attachment={file} />; + return <CollapsibleQuote key={getAttachmentKey(file, 'collapsible', index)} attachment={file} />; } if (file.attachments?.length) { - return <Reply key={file.title_link || file.message_link || `reply-${index}`} attachment={file} msg={msg} />; + return <Reply key={getAttachmentKey(file, 'reply', index)} attachment={file} msg={msg} />; } return null; diff --git a/app/containers/message/components/Attachments/Quote.tsx b/app/containers/message/components/Attachments/Quote.tsx index 256b1de6169..65e3d2df941 100644 --- a/app/containers/message/components/Attachments/Quote.tsx +++ b/app/containers/message/components/Attachments/Quote.tsx @@ -5,7 +5,7 @@ import { Reply } from './components'; import { type IMessageAttachments } from '../../interfaces'; import { type IAttachment } from '../../../../definitions'; import { getMessageFromAttachment } from '../../utils'; -import { isQuoteAttachment } from './utils'; +import { getAttachmentKey, isQuoteAttachment } from './utils'; import { useTranslateLanguage } from '../../stores/MessageStore'; const Quote: FC<IMessageAttachments> = ({ attachments }: IMessageAttachments) => { @@ -22,7 +22,7 @@ const Quote: FC<IMessageAttachments> = ({ attachments }: IMessageAttachments) => const quotesElements = quotes.map((file: IAttachment, index: number) => { const msg = getMessageFromAttachment(file, translateLanguage); - return <Reply key={file.title_link || file.message_link || `reply-${index}`} attachment={file} msg={msg} />; + return <Reply key={getAttachmentKey(file, 'reply', index)} attachment={file} msg={msg} />; }); return <View style={{ gap: 4 }}>{quotesElements}</View>; diff --git a/app/containers/message/components/Attachments/utils.ts b/app/containers/message/components/Attachments/utils.ts index 294b89d1007..7ae916d758c 100644 --- a/app/containers/message/components/Attachments/utils.ts +++ b/app/containers/message/components/Attachments/utils.ts @@ -1,5 +1,9 @@ import { type IAttachment } from '../../../../definitions'; +// Prefer a stable server-provided link as the React key; fall back to a prefixed index. +export const getAttachmentKey = (file: IAttachment, prefix: string, index: number): string => + file.title_link || file.message_link || `${prefix}-${index}`; + // Webhook/integration attachments can carry their own media alongside text/color (e.g. embeds). const hasOwnMedia = (file: IAttachment): boolean => !!(file.image_url || file.audio_url || file.video_url); diff --git a/app/containers/message/components/Broadcast.tsx b/app/containers/message/components/Broadcast.tsx index 111e312ae09..0fc5d9d2e83 100644 --- a/app/containers/message/components/Broadcast.tsx +++ b/app/containers/message/components/Broadcast.tsx @@ -6,19 +6,17 @@ import styles from '../styles'; import { BUTTON_HIT_SLOP } from '../utils'; import I18n from '../../../i18n'; import { useTheme } from '../../../theme'; -import { useMessageAuthor, useMessageItem } from '../stores/MessageStore'; -import { useBroadcast, useMessageUser, useReplyBroadcast } from '../stores/MessageRoomStore'; +import { useIsOwnMessage, useMessageItem } from '../stores/MessageStore'; +import { useBroadcast, useReplyBroadcast } from '../stores/MessageRoomStore'; const Broadcast = () => { 'use memo'; const item = useMessageItem(); - const user = useMessageUser(); const replyBroadcast = useReplyBroadcast(); const broadcast = useBroadcast(); const { colors } = useTheme(); - const { u: author } = useMessageAuthor(); - const isOwn = author?._id === user?.id; + const isOwn = useIsOwnMessage(); if (broadcast && !isOwn) { return ( diff --git a/app/containers/message/components/Message/CompactMessage.tsx b/app/containers/message/components/Message/CompactMessage.tsx index d4eb6c705fb..946bd8b2f2b 100644 --- a/app/containers/message/components/Message/CompactMessage.tsx +++ b/app/containers/message/components/Message/CompactMessage.tsx @@ -1,9 +1,9 @@ import { View, type ViewStyle } from 'react-native'; -import { A11y } from 'react-native-a11y-order'; import styles from '../../styles'; import RepliedThread from '../RepliedThread'; import MessageAvatar from '../MessageAvatar'; +import MessageAccessibleIndex from '../MessageAccessibleIndex'; import Content from '../Content'; import Attachments from '../Attachments'; import { @@ -11,10 +11,8 @@ import { useIsInfoMessage, useMessageField, useMessageGrouping, - useMessageText, useThreadPosition } from '../../stores/MessageStore'; -import { useAutoTranslate } from '../../stores/MessageRoomStore'; const CompactMessage = () => { 'use memo'; @@ -22,10 +20,8 @@ const CompactMessage = () => { const isHeader = useMessageGrouping(); const { isThreadReply } = useThreadPosition(); const isInfo = useIsInfoMessage(); - const { messageText, isTranslated } = useMessageText(); const { t: type, attachments } = useContentData(); const author = useMessageField(item => item.u); - const { autoTranslateLanguage } = useAutoTranslate(); const thread = isThreadReply ? <RepliedThread isHeader={isHeader} /> : null; const infoStyle: ViewStyle = isInfo ? { alignItems: 'center' } : {}; @@ -35,12 +31,7 @@ const CompactMessage = () => { {thread} <View style={[styles.flex, infoStyle]}> <MessageAvatar small /> - <A11y.Index - accessible={isTranslated} - accessibilityLabel={messageText || ''} - accessibilityLanguage={autoTranslateLanguage} - index={2} - style={{ flex: 1 }}> + <MessageAccessibleIndex style={{ flex: 1 }}> <View style={styles.messageContent}> <Content /> {isInfo && type === 'message_pinned' ? ( @@ -49,7 +40,7 @@ const CompactMessage = () => { </View> ) : null} </View> - </A11y.Index> + </MessageAccessibleIndex> </View> </View> ); diff --git a/app/containers/message/components/Message/FullMessage.tsx b/app/containers/message/components/Message/FullMessage.tsx index 34eb6040d74..61fb17837b3 100644 --- a/app/containers/message/components/Message/FullMessage.tsx +++ b/app/containers/message/components/Message/FullMessage.tsx @@ -1,28 +1,21 @@ import { View } from 'react-native'; -import { A11y } from 'react-native-a11y-order'; import styles from '../../styles'; import MessageAvatar from '../MessageAvatar'; +import MessageAccessibleIndex from '../MessageAccessibleIndex'; import RightIcons from '../RightIcons'; import { Layout } from '../Layout'; -import { useMessageField, useMessageGrouping, useMessageText } from '../../stores/MessageStore'; -import { useAutoTranslate } from '../../stores/MessageRoomStore'; +import { useMessageField, useMessageGrouping } from '../../stores/MessageStore'; const FullMessage = ({ isPreview }: { isPreview?: boolean }) => { 'use memo'; const isHeader = useMessageGrouping(); - const { messageText, isTranslated } = useMessageText(); - const { autoTranslateLanguage } = useAutoTranslate(); const id = useMessageField(item => item.id); return ( <View testID={`message-${id}`} style={styles.container}> - <A11y.Index - accessible={isTranslated} - accessibilityLabel={messageText || ''} - accessibilityLanguage={autoTranslateLanguage} - index={2}> + <MessageAccessibleIndex> <View style={styles.flex}> <MessageAvatar /> <View style={styles.messageContent}> @@ -30,7 +23,7 @@ const FullMessage = ({ isPreview }: { isPreview?: boolean }) => { </View> {!isHeader ? <RightIcons /> : null} </View> - </A11y.Index> + </MessageAccessibleIndex> </View> ); }; diff --git a/app/containers/message/components/MessageAccessibleIndex.tsx b/app/containers/message/components/MessageAccessibleIndex.tsx new file mode 100644 index 00000000000..cc1fd812da0 --- /dev/null +++ b/app/containers/message/components/MessageAccessibleIndex.tsx @@ -0,0 +1,26 @@ +import { type ReactNode } from 'react'; +import { type StyleProp, type ViewStyle } from 'react-native'; +import { A11y } from 'react-native-a11y-order'; + +import { useMessageText } from '../stores/MessageStore'; +import { useAutoTranslate } from '../stores/MessageRoomStore'; + +const MessageAccessibleIndex = ({ style, children }: { style?: StyleProp<ViewStyle>; children: ReactNode }) => { + 'use memo'; + + const { messageText, isTranslated } = useMessageText(); + const { autoTranslateLanguage } = useAutoTranslate(); + + return ( + <A11y.Index + accessible={isTranslated} + accessibilityLabel={messageText || ''} + accessibilityLanguage={autoTranslateLanguage} + index={2} + style={style}> + {children} + </A11y.Index> + ); +}; + +export default MessageAccessibleIndex; diff --git a/app/containers/message/components/Preview.tsx b/app/containers/message/components/Preview.tsx index a260e5d1715..7e3c22910be 100644 --- a/app/containers/message/components/Preview.tsx +++ b/app/containers/message/components/Preview.tsx @@ -1,3 +1,5 @@ +import { shallowEqual } from 'react-redux'; + import Message from '../index'; import { MessageRoomProvider } from '../stores/MessageRoomStore'; import { useAppSelector } from '../../../lib/hooks/useAppSelector'; @@ -7,10 +9,13 @@ import { type TAnyMessageModel } from '../../../definitions'; const MessagePreview = ({ message }: { message: TAnyMessageModel }) => { 'use memo'; - const { user, baseUrl } = useAppSelector(state => ({ - user: getUserSelector(state), - baseUrl: state.server.server - })); + const { user, baseUrl } = useAppSelector( + state => ({ + user: getUserSelector(state), + baseUrl: state.server.server + }), + shallowEqual + ); return ( <MessageRoomProvider user={user} baseUrl={baseUrl} rid={message.rid}> diff --git a/app/containers/message/components/User.tsx b/app/containers/message/components/User.tsx index eabae487efe..ad6b214b02a 100644 --- a/app/containers/message/components/User.tsx +++ b/app/containers/message/components/User.tsx @@ -8,8 +8,8 @@ import { messageHaveAuthorName } from '../utils'; import MessageTime from './Time'; import { useResponsiveLayout } from '../../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; import { useSetting } from '../../../lib/hooks/useSetting'; -import { useMessageAuthor, useMessageGrouping, useMessageHeaderMeta } from '../stores/MessageStore'; -import { useMessageUser, useNavToRoomInfo } from '../stores/MessageRoomStore'; +import { useIsOwnMessage, useMessageAuthor, useMessageGrouping, useMessageHeaderMeta } from '../stores/MessageStore'; +import { useNavToRoomInfo } from '../stores/MessageRoomStore'; const styles = StyleSheet.create({ container: { @@ -44,18 +44,17 @@ const User = () => { 'use memo'; const useRealName = useSetting('UI_Use_Real_Name') as boolean; - const user = useMessageUser(); const navToRoomInfo = useNavToRoomInfo(); const { colors } = useTheme(); const { isLargeFontScale } = useResponsiveLayout(); const isHeader = useMessageGrouping(); const { u: author, alias } = useMessageAuthor(); const { t: type } = useMessageHeaderMeta(); + const itsMe = useIsOwnMessage(); if (isHeader) { const username = (useRealName && author?.name) || author?.username; const aliasUsername = alias ? <Text style={[styles.alias, { color: colors.fontSecondaryInfo }]}> @{username}</Text> : null; - const itsMe = author?._id === user?.id; const onUserPress = () => { navToRoomInfo?.({ diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index b809de5c07d..e12f6a368fd 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -183,6 +183,12 @@ export const useRepliedThreadData = (): Pick<TAnyMessageModel, 'tmid' | 'tmsg' | export const useMessageAuthor = (): Pick<TAnyMessageModel, 'u' | 'alias' | 'role'> => useMessageStore(useShallow(s => ({ u: s.item.u, alias: s.item.alias, role: s.item.role }))); +export const useIsOwnMessage = (): boolean => { + const author = useMessageStore(s => s.item.u); + const user = useMessageUser(); + return author?._id === user?.id; +}; + export const useAvatar = (): Pick<TAnyMessageModel, 'avatar' | 'emoji'> => useMessageStore(useShallow(s => ({ avatar: s.item.avatar, emoji: s.item.emoji }))); From 73004e88d3fee087812e2ce1f1f90010ae81c897 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Thu, 9 Jul 2026 15:39:41 -0300 Subject: [PATCH 140/144] docs: add Message component architecture reference (NATIVE-22) --- app/containers/message/docs/ARCHITECTURE.md | 104 ++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 app/containers/message/docs/ARCHITECTURE.md diff --git a/app/containers/message/docs/ARCHITECTURE.md b/app/containers/message/docs/ARCHITECTURE.md new file mode 100644 index 00000000000..c6dfac99acd --- /dev/null +++ b/app/containers/message/docs/ARCHITECTURE.md @@ -0,0 +1,104 @@ +# Message Component Architecture + +Load-bearing reference for `app/containers/message/`: how one Message row is provided, composed, and rendered. Domain terms (**Message Action**, **Message Action State**, **Positional State**, **Message Preview**) are defined in the repo glossary `CONTEXT.md` — this document uses them as defined there. Post-refactor (PR #7455): the module moved from a single class-component prop chain to three scoped Zustand stores plus a function-component render tree. + +--- + +## Purpose & entry point + +The module renders **one Message row**. The public entry point is the default export of `app/containers/message/index.tsx`: + +```tsx +<MessageProvider item previousItem onPress onLongPress threadBadgeColor isIgnored> + <MessageTouchable isPreview highlighted /> + <MessageSeparator ts unread /> +</MessageProvider> +``` + +`MessageProvider` seeds the per-row `MessageStore` (see below); `MessageTouchable` is the pressable row itself; `MessageSeparator` (a sibling, not part of this module) renders the date/unread divider above the row. + +This default export is consumed as `renderRow` from three list contexts — `RoomView` (the main Message Window), `MessagesView` (pinned/starred lists), `SearchMessagesView` (search results) — plus one non-list context: `components/Preview.tsx` wraps a single Message with its own `MessageRoomProvider` for a **Message Preview** rendered outside a Room (forward-message target picker, quoted-message preview in markdown). A `highlighted` boolean and `isPreview` flag are the only pieces of **Positional State** and preview-mode state this module receives from its caller — the Message Window itself (jump target, scroll position) is owned by `RoomView`, not here. + +--- + +## The three stores + +| Store | File | Scope | Provider mounted at | Lifetime | +| ---------------------- | ------------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ------------------------- | +| **MessageStore** | `stores/MessageStore.tsx` | One Message record (`item`) + its previous neighbor | `index.tsx`, once per row | Row mount → unmount | +| **MessageRoomStore** | `stores/MessageRoomStore.tsx` | Room-scoped context: handlers, `rid`, `baseUrl`, user, settings | `RoomView`, `MessagesView`, `SearchMessagesView`, `ModalBlockView`, `Preview.tsx` — wraps the whole list/row | Room/view mount → unmount | +| **MessageActionStore** | `stores/MessageActionStore.tsx` | The active **Message Action** (Quote/Edit/React) and its target(s) | `RoomView` only, via `RoomProviders` — one per Room, wraps List **and** the composer | Room mount → unmount | + +### Why three, not one + +Each store changes at a different frequency and has a different audience, and collapsing them would force every row to re-render on changes it doesn't care about: + +- **MessageStore** changes per-row, on that row's own WatermelonDB record emitting (`experimentalSubscribe`) — a `tick` counter bumps and only that row's selectors re-run. `previousItem` doubles as the neighbor lookup used by grouping/thread-position logic. +- **MessageRoomStore** is mostly static for the life of a Room: navigation callbacks, `rid`, `baseUrl`, and the logged user are captured once (see `FROZEN_KEYS`) and never resynced, because they're expected to be referentially stable for the Room's lifetime. A second, explicitly reactive tail (`timeFormat`, `autoTranslateRoom`, `autoTranslateLanguage`, `archived`, `broadcast`, `isReadReceiptEnabled`, `Message_GroupingPeriod`) is pushed on every render via a dependency-gated `useEffect`, because those _can_ change mid-session (e.g. a Room gets archived). +- **MessageActionStore** is shared Room-wide state that both the Message list (to highlight the row being edited) and `MessageComposer` (to know what's being quoted/edited) read — it has to live above both, not per-row. + +### Granular selectors + `useShallow` + `'use memo'` + +Every store exposes narrow, single-purpose hooks (`useReactions`, `useBlocks`, `useMessageAuthor`, `useIsEncrypted`, …) instead of one broad "give me the message" hook — each leaf component subscribes to only the fields it renders, so a field it doesn't use changing elsewhere on the record never re-renders it. Hooks returning more than one field use Zustand's `useShallow` so a same-value re-tick doesn't produce a new object reference (`useBlocks`, `useThreadData`, `useMessageAuthor`, `useContentData`, etc.). Components use the `'use memo'` directive (React Compiler, `babel-plugin-react-compiler`) rather than hand-written `useMemo`/`useCallback` for their own memoization. + +Two dev-only guards protect this pattern from silent regressions (both no-op in production builds): + +- `useMessageFieldDev` (MessageStore) warns once if a `useMessageField` selector returns a fresh-but-shallow-equal object/array on an unchanged tick — a sign someone wrote an inline `item => ({ a: item.a })` instead of a `useShallow` domain hook. Field stability itself depends on the model's `@json(..., { memo: true })` decorators (`app/lib/database/model/Message.js`), guarded separately by `Message.memo.test.ts`. +- `useFrozenHandlersGuardDev` (MessageRoomStore) warns once if any `FROZEN_KEYS` value's identity changes after mount, since the provider captures those once and never re-syncs them. + +### Inert-store fallback vs. throwing + +Rows can render outside a full provider stack (a **Message Preview** has no `MessageActionProvider`; some tests mount only `MessageStore`/`MessageRoomStore`). The three stores disagree deliberately on how to fail: + +- **MessageStore** and **MessageRoomStore** hooks throw (`'Message hooks must be used within a MessageProvider'` / `'...MessageRoomProvider'`) if no provider is present — every row and every Room-scoped view is expected to always supply both. +- **MessageActionStore** distinguishes by hook: `useMessageAction()` throws without a provider, but `useIsBeingEdited(messageId)` falls back to a shared `inertStore` (`action: null`, and every action method throws `NO_OP_ACTIONS`) — a Message Preview rendered without a `MessageActionProvider` can ask "am I being edited?" and safely get `false` instead of crashing, since it can never actually enter an edit session. + +--- + +## Component composition + +```mermaid +flowchart TD + A["MessageContainer (index.tsx)"] --> B["MessageProvider (MessageStore)"] + B --> C["MessageTouchable"] + C -->|"hasError / isInfo"| D["Message (bare, no Touch wrapper)"] + C -->|otherwise| E["Touch (press/long-press gate + a11y)"] + E --> D + D -->|"isThreadReply / isThreadSequential / isInfo / isIgnored"| F["CompactMessage"] + D -->|otherwise| G["FullMessage"] + F --> H["MessageAvatar (small) + Content [+ Attachments if pinned info]"] + G --> I["MessageAvatar + Layout + RightIcons (hidden when grouped)"] + I --> J["Layout dispatch"] + J -->|blocks| K["BlocksLayout"] + J -->|jitsi_call_started| L["JitsiLayout"] + J -->|discussion-created| M["DiscussionLayout"] + J -->|isPreview| N["PreviewLayout"] + J -->|default| O["StandardLayout"] + O --> P["User + Time + ContentLayout + Thread + Reactions + Broadcast"] + P --> Q["Content dispatch"] + Q -->|isInfo| R["InfoContent"] + Q -->|isIgnored| S["IgnoredContent"] + Q -->|isEncrypted| T["EncryptedContent"] + Q -->|"tmid, not thread room"| U["PreviewContent"] + Q -->|has text| V["MarkdownContent"] + O --> W["Attachments (Image/Audio/Video/AttachedActions/CollapsibleQuote/Reply)"] +``` + +- **MessageTouchable** (`components/Touchable/MessageTouchable.tsx`) is the press/long-press gate and the a11y surface for the row: it reads `useMessageTouchable()`'s derived `tappable`/`longPressable`/`revealsIgnored` flags plus `useIsBeingEdited` (for the edit-mode highlight color) and builds the accessibility label/hint/actions. Error and Info Messages skip the `Touch` wrapper entirely — the whole row isn't pressable, though sub-elements (links, attachments) still are through their own handlers. +- **`Touch`** (`components/Touchable/Touch.tsx`) is the actual platform touchable (`TouchableOpacity` on iOS, `TouchableHighlight` on Android) wrapped for `react-native-external-keyboard` D-pad focus; it carries no Message-specific logic. +- **`Message`** (`components/Message/Message.tsx`) is the density switch: `CompactMessage` for a **Thread Message**'s in-parent-room preview (`isThreadReply`/`isThreadSequential`), an **Info Message**, or an Ignored Message; `FullMessage` otherwise. +- **`FullMessage`** renders the avatar, dispatches to `Layout`, and appends `RightIcons` only when the row shows its own **Message Header** (`!isHeader` from `useMessageGrouping`) — a **Grouped Message** has no right-icon row. +- **`CompactMessage`** renders a small avatar and `Content` directly (no `Layout` dispatch), with an extra read-only `Attachments` render for a pinned Info Message (`t === 'message_pinned'`) and an optional `RepliedThread` header when it's the first Thread Message in a run. +- **`Layout`** (`components/Layout/index.tsx`) picks among `BlocksLayout` (a Blocks Message from an App), `JitsiLayout`/`DiscussionLayout` (their respective System Message types), `PreviewLayout` (a **Message Preview**), and `StandardLayout` (everything else), all wrapped in `WidthAwareView` for width-dependent children (images, markdown wrapping). +- **`Content`** (`components/Content/index.tsx`) is a second, narrower dispatch inside `StandardLayout`/`PreviewLayout`/`CompactMessage` for the body itself: Info → Ignored → Encrypted → thread-preview (`PreviewContent`, only outside a Thread room) → markdown, in that priority order. +- **`Attachments`** (`components/Attachments/Attachments.tsx`) filters to non-quote attachments and maps each to `Image`/`Audio`/`Video`/`AttachedActions`/`CollapsibleQuote`/`Reply` by shape (`image_url`, `audio_url`, `video_url`, `actions`, `collapsed`, nested `attachments`). +- **`RightIcons`** (`components/RightIcons/index.tsx`) is a flat row of independent flag icons — `Pinned`, `Encrypted`, `Edited`, `MessageError`, `Translated`, `ReadReceipt` — each reading its own MessageStore/MessageRoomStore slice. +- **`MessageActionTouchable`** (`components/Touchable/MessageActionTouchable.tsx`) is a smaller building block (a `Pressable` wired to `useMessageLongPress`) reused by several leaf components (Reactions, Thread, Discussion, Broadcast, attachment sub-views) that need the row's long-press behavior without being the row's own top-level touchable. + +--- + +## Vocabulary alignment + +`TMessageActionState` (`definitions/IMessage.ts`) is `{ kind: 'edit' | 'quote' | 'react', ... } | null` — this matches CONTEXT.md's **Message Action** definition exactly (Quote, Edit, React; no "reply" action). `MessageActionStore`'s `action` field is the **Message Action State** as defined in the glossary; the retired term "interaction" does not appear anywhere in this module's stores or hooks. **Positional State** (highlight, jump/scroll position) is not owned here — it arrives as the plain `highlighted` prop threaded down from `RoomView`'s list. + +**Known follow-up:** `MessageRoomState`'s handler/constant fields are all individually optional (`?`), which is looser than the shape the values actually have once mounted (all frozen fields are supplied together). A future pass may flatten this to a non-optional shape post-mount; as of this refactor it remains all-optional. From be9f9ebc186a9f2c294aaef65ba163eea9652e52 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 10 Jul 2026 09:02:20 -0300 Subject: [PATCH 141/144] fix: never quote edit/react message id in media ShareView (NATIVE-22) --- .../hooks/useChooseMedia.test.tsx | 36 +++++++++++++++++++ .../MessageComposer/hooks/useChooseMedia.ts | 7 +--- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx b/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx index 6233013945e..da1ddb62bae 100644 --- a/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx +++ b/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx @@ -131,4 +131,40 @@ describe('useChooseMedia', () => { }); expect(mockNavigate).not.toHaveBeenCalled(); }); + + it('forwards quoted message ids to ShareView as selectedMessages', async () => { + mockUseAltTextSupported.mockReturnValue(false); + mockUseMessageAction.mockReturnValue({ kind: 'quote', messageIds: ['msg-1', 'msg-2'] }); + mockGetDocumentAsync.mockResolvedValue({ + canceled: false, + assets: [{ name: 'legacy.pdf', size: 12, mimeType: 'application/pdf', uri: 'file:///tmp/legacy.pdf' }] + }); + + const { result } = renderHook(() => useChooseMedia({ rid: 'room-id', tmid: 'thread-id', permissionToUpload: true })); + + await result.current.chooseFile(); + + await waitFor(() => expect(mockNavigate).toHaveBeenCalled()); + const { action, startShareView } = mockNavigate.mock.calls[0][1]; + expect(action).toBe('quote'); + expect(startShareView().selectedMessages).toEqual(['msg-1', 'msg-2']); + }); + + it('does not quote the message when the action is edit', async () => { + mockUseAltTextSupported.mockReturnValue(false); + mockUseMessageAction.mockReturnValue({ kind: 'edit', messageId: 'msg-1' }); + mockGetDocumentAsync.mockResolvedValue({ + canceled: false, + assets: [{ name: 'legacy.pdf', size: 12, mimeType: 'application/pdf', uri: 'file:///tmp/legacy.pdf' }] + }); + + const { result } = renderHook(() => useChooseMedia({ rid: 'room-id', tmid: 'thread-id', permissionToUpload: true })); + + await result.current.chooseFile(); + + await waitFor(() => expect(mockNavigate).toHaveBeenCalled()); + const { action, startShareView } = mockNavigate.mock.calls[0][1]; + expect(action).toBe('edit'); + expect(startShareView().selectedMessages).toEqual([]); + }); }); diff --git a/app/containers/MessageComposer/hooks/useChooseMedia.ts b/app/containers/MessageComposer/hooks/useChooseMedia.ts index 34f2d06bddb..626737fd53f 100644 --- a/app/containers/MessageComposer/hooks/useChooseMedia.ts +++ b/app/containers/MessageComposer/hooks/useChooseMedia.ts @@ -96,12 +96,7 @@ export const useChooseMedia = ({ const startShareView = () => { const text = getText?.() || ''; - let selectedMessages: string[] = []; - if (action?.kind === 'quote') { - selectedMessages = action.messageIds; - } else if (action) { - selectedMessages = [action.messageId]; - } + const selectedMessages = action?.kind === 'quote' ? action.messageIds : []; return { selectedMessages, text From abe0f0ca8511ae2122d1d77c42c2f85da1f80c3d Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Fri, 10 Jul 2026 09:16:31 -0300 Subject: [PATCH 142/144] refactor: move IRoomInfoParam to definitions (NATIVE-22) --- app/containers/message/stores/MessageRoomStore.tsx | 3 +-- app/definitions/IRoom.ts | 11 ++++++++++- app/views/MessagesView/index.tsx | 2 +- app/views/SearchMessagesView/index.tsx | 11 +---------- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/containers/message/stores/MessageRoomStore.tsx b/app/containers/message/stores/MessageRoomStore.tsx index 8e6162e4917..d397c438ed4 100644 --- a/app/containers/message/stores/MessageRoomStore.tsx +++ b/app/containers/message/stores/MessageRoomStore.tsx @@ -2,8 +2,7 @@ import { createContext, useContext, useEffect, useRef, useState, type ReactEleme import { createStore, useStore } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; -import { type IAttachment, type TAnyMessageModel } from '../../../definitions'; -import { type IRoomInfoParam } from '../../../views/SearchMessagesView'; +import { type IAttachment, type IRoomInfoParam, type TAnyMessageModel } from '../../../definitions'; import { useSetting } from '../../../lib/hooks/useSetting'; export type MessageRoomState = { diff --git a/app/definitions/IRoom.ts b/app/definitions/IRoom.ts index e77221f13bc..2fd78ce4138 100644 --- a/app/definitions/IRoom.ts +++ b/app/definitions/IRoom.ts @@ -3,7 +3,7 @@ import type Model from '@nozbe/watermelondb/Model'; import { type IMessage } from './IMessage'; import { type IRocketChatRecord } from './IRocketChatRecord'; import { type IServedBy } from './IServedBy'; -import { type IVisitor, type SubscriptionType } from './ISubscription'; +import { type ISubscription, type IVisitor, type SubscriptionType } from './ISubscription'; import { type IUser, type TNotifications } from './IUser'; interface IRequestTranscript { @@ -15,6 +15,15 @@ interface IRequestTranscript { export type TUserWaitingForE2EKeys = { userId: string; ts: Date }; +export interface IRoomInfoParam { + room?: ISubscription; + member?: any; + rid: string; + t: SubscriptionType; + joined?: boolean; + itsMe?: boolean; +} + export interface IRoom { fname?: string; _id: string; diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index 89bf620eda5..4175ab9141a 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -20,9 +20,9 @@ import SafeAreaView from '../../containers/SafeAreaView'; import getThreadName from '../../lib/methods/getThreadName'; import styles from './styles'; import { type ChatsStackParamList } from '../../stacks/types'; -import { type IRoomInfoParam } from '../SearchMessagesView'; import { type IApplicationState, + type IRoomInfoParam, type TMessageModel, type ISubscription, SubscriptionType, diff --git a/app/views/SearchMessagesView/index.tsx b/app/views/SearchMessagesView/index.tsx index 12dda4dcb89..c642a40e78e 100644 --- a/app/views/SearchMessagesView/index.tsx +++ b/app/views/SearchMessagesView/index.tsx @@ -36,7 +36,7 @@ import { type TMessageModel, type IUrl, type IAttachment, - type ISubscription, + type IRoomInfoParam, SubscriptionType, type TSubscriptionModel } from '../../definitions'; @@ -53,15 +53,6 @@ interface ISearchMessagesViewState { searchText: string; } -export interface IRoomInfoParam { - room?: ISubscription; - member?: any; - rid: string; - t: SubscriptionType; - joined?: boolean; - itsMe?: boolean; -} - interface INavigationOption { navigation: CompositeNavigationProp< NativeStackNavigationProp<ChatsStackParamList, 'SearchMessagesView'>, From 7a1f61b6136190778daaac2f121e96e8b89e1467 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 13 Jul 2026 13:24:28 -0300 Subject: [PATCH 143/144] refactor: source author inside Attachments, drop CollapsibleQuote fragment (NATIVE-22) --- .../components/Attachments/Attachments.tsx | 5 +- .../Attachments/CollapsibleQuote.tsx | 56 +++++++++---------- .../components/Layout/ContentLayout.tsx | 5 +- .../components/Message/CompactMessage.tsx | 11 +--- app/containers/message/interfaces.ts | 3 +- 5 files changed, 35 insertions(+), 45 deletions(-) diff --git a/app/containers/message/components/Attachments/Attachments.tsx b/app/containers/message/components/Attachments/Attachments.tsx index ee8e1042b8b..0eb95137c5d 100644 --- a/app/containers/message/components/Attachments/Attachments.tsx +++ b/app/containers/message/components/Attachments/Attachments.tsx @@ -8,16 +8,17 @@ import CollapsibleQuote from './CollapsibleQuote'; import AttachedActions from './AttachedActions'; import Reply from './Reply'; import { useShowAttachment } from '../../stores/MessageRoomStore'; -import { useTranslateLanguage } from '../../stores/MessageStore'; +import { useMessageField, useTranslateLanguage } from '../../stores/MessageStore'; import { type IMessageAttachments } from '../../interfaces'; import { getMessageFromAttachment } from '../../utils'; import { getAttachmentKey, isContentAttachment } from './utils'; -const Attachments: FC<IMessageAttachments> = ({ attachments, author }: IMessageAttachments) => { +const Attachments: FC<IMessageAttachments> = ({ attachments }: IMessageAttachments) => { 'use memo'; const translateLanguage = useTranslateLanguage(); const showAttachment = useShowAttachment(); + const author = useMessageField(item => item.u); const nonQuoteAttachments = attachments?.filter(isContentAttachment); diff --git a/app/containers/message/components/Attachments/CollapsibleQuote.tsx b/app/containers/message/components/Attachments/CollapsibleQuote.tsx index 761a0a22920..3bc665222c2 100644 --- a/app/containers/message/components/Attachments/CollapsibleQuote.tsx +++ b/app/containers/message/components/Attachments/CollapsibleQuote.tsx @@ -132,37 +132,35 @@ const CollapsibleQuote = ({ attachment }: IMessageReply) => { } return ( - <> - <MessageActionTouchable - testID={`collapsibleQuoteTouchable-${attachment.title}`} - onPress={onPress} - style={[ - styles.button, - attachment.description && styles.marginBottom, - { - backgroundColor, - borderLeftColor: strokeLight, - borderTopColor: strokeExtraLight, - borderRightColor: strokeExtraLight, - borderBottomColor: strokeExtraLight, - borderLeftWidth: 2 - } - ]} - hitSlop={BUTTON_HIT_SLOP}> - <View style={styles.touchableContainer}> - <View style={styles.attachmentContainer}> - <View style={styles.authorContainer}> - <Text style={[styles.title, { color: colors.fontSecondaryInfo }]}>{attachment.title}</Text> - </View> - {!collapsed && <AttText text={attachment.text} />} - {!collapsed && <Fields attachment={attachment} />} - </View> - <View style={styles.iconContainer}> - <CustomIcon name={!collapsed ? 'chevron-up' : 'chevron-down'} size={22} color={strokeMedium} /> + <MessageActionTouchable + testID={`collapsibleQuoteTouchable-${attachment.title}`} + onPress={onPress} + style={[ + styles.button, + attachment.description && styles.marginBottom, + { + backgroundColor, + borderLeftColor: strokeLight, + borderTopColor: strokeExtraLight, + borderRightColor: strokeExtraLight, + borderBottomColor: strokeExtraLight, + borderLeftWidth: 2 + } + ]} + hitSlop={BUTTON_HIT_SLOP}> + <View style={styles.touchableContainer}> + <View style={styles.attachmentContainer}> + <View style={styles.authorContainer}> + <Text style={[styles.title, { color: colors.fontSecondaryInfo }]}>{attachment.title}</Text> </View> + {!collapsed && <AttText text={attachment.text} />} + {!collapsed && <Fields attachment={attachment} />} </View> - </MessageActionTouchable> - </> + <View style={styles.iconContainer}> + <CustomIcon name={!collapsed ? 'chevron-up' : 'chevron-down'} size={22} color={strokeMedium} /> + </View> + </View> + </MessageActionTouchable> ); }; diff --git a/app/containers/message/components/Layout/ContentLayout.tsx b/app/containers/message/components/Layout/ContentLayout.tsx index aeb31c34a62..0cabe7aad85 100644 --- a/app/containers/message/components/Layout/ContentLayout.tsx +++ b/app/containers/message/components/Layout/ContentLayout.tsx @@ -2,19 +2,18 @@ import Content from '../Content'; import Attachments from '../Attachments'; import Quote from '../Attachments/Quote'; import Urls from '../Urls'; -import { useAttachments, useMessageField } from '../../stores/MessageStore'; +import { useAttachments } from '../../stores/MessageStore'; export const ContentLayout = () => { 'use memo'; const attachments = useAttachments(); - const author = useMessageField(item => item.u); return ( <> <Quote attachments={attachments} /> <Content /> - <Attachments attachments={attachments} author={author} /> + <Attachments attachments={attachments} /> <Urls /> </> ); diff --git a/app/containers/message/components/Message/CompactMessage.tsx b/app/containers/message/components/Message/CompactMessage.tsx index 946bd8b2f2b..89b18a83605 100644 --- a/app/containers/message/components/Message/CompactMessage.tsx +++ b/app/containers/message/components/Message/CompactMessage.tsx @@ -6,13 +6,7 @@ import MessageAvatar from '../MessageAvatar'; import MessageAccessibleIndex from '../MessageAccessibleIndex'; import Content from '../Content'; import Attachments from '../Attachments'; -import { - useContentData, - useIsInfoMessage, - useMessageField, - useMessageGrouping, - useThreadPosition -} from '../../stores/MessageStore'; +import { useContentData, useIsInfoMessage, useMessageGrouping, useThreadPosition } from '../../stores/MessageStore'; const CompactMessage = () => { 'use memo'; @@ -21,7 +15,6 @@ const CompactMessage = () => { const { isThreadReply } = useThreadPosition(); const isInfo = useIsInfoMessage(); const { t: type, attachments } = useContentData(); - const author = useMessageField(item => item.u); const thread = isThreadReply ? <RepliedThread isHeader={isHeader} /> : null; const infoStyle: ViewStyle = isInfo ? { alignItems: 'center' } : {}; @@ -36,7 +29,7 @@ const CompactMessage = () => { <Content /> {isInfo && type === 'message_pinned' ? ( <View pointerEvents='none'> - <Attachments attachments={attachments} author={author} /> + <Attachments attachments={attachments} /> </View> ) : null} </View> diff --git a/app/containers/message/interfaces.ts b/app/containers/message/interfaces.ts index b669c84656f..fa73f9e4c94 100644 --- a/app/containers/message/interfaces.ts +++ b/app/containers/message/interfaces.ts @@ -1,11 +1,10 @@ import { type StyleProp } from 'react-native'; import { type ImageStyle } from 'expo-image'; -import { type IAttachment, type IUserMessage } from '../../definitions'; +import { type IAttachment } from '../../definitions'; export interface IMessageAttachments { attachments?: IAttachment[]; - author?: IUserMessage; } export interface IMessageAvatar { From cd5ea75652bc2b093108466e0b2a931f7d9c10d1 Mon Sep 17 00:00:00 2001 From: Diego Mello <diegolmello@gmail.com> Date: Mon, 13 Jul 2026 13:29:22 -0300 Subject: [PATCH 144/144] refactor: drop unnecessary user cast in RoomView MessageRoomProvider (NATIVE-22) --- app/views/RoomView/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 25762af7df3..edffea7126a 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -1674,7 +1674,7 @@ class RoomView extends Component<IRoomViewProps, IRoomViewState> { archived={'id' in room && room.archived} isReadReceiptEnabled={Message_Read_Receipt_Enabled && !federated} rid={rid} - user={user as any} + user={user} baseUrl={baseUrl} broadcast={'id' in room && room.broadcast} isThreadRoom={!!this.tmid}