diff --git a/.maestro/tests/room/threads.yaml b/.maestro/tests/room/threads.yaml index 20261bdc51c..f45f17151b4 100644 --- a/.maestro/tests/room/threads.yaml +++ b/.maestro/tests/room/threads.yaml @@ -294,6 +294,14 @@ tags: timeout: 60000 - tapOn: id: 'header-back' +- extendedWaitUntil: + notVisible: + id: 'room-view-title-thread' + timeout: 60000 +- extendedWaitUntil: + visible: + id: 'room-view-title-${output.room.name}' + timeout: 60000 # should create thread delete the message and the thread show the correct number of messages - runFlow: diff --git a/CONTEXT.md b/CONTEXT.md index 20bd94c0532..3224eedfa44 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -2,18 +2,21 @@ ## 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) — never a **DDP Subscription** | Membership, room entry | -| **Channel** | A public Room (type `'c'`) visible to all Workspace 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 (a different thing) | -| **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), never a **DDP Subscription** | Membership, room entry | +| **Subscribed Room** | A Room the user has joined, backed by a persisted Subscription model. Detected by the presence of a Subscription record. | Joined room | +| **Preview Mode** | Viewing a Room without joining: no Subscription record exists and the Room data comes from navigation params or a REST lookup. Persists until the user joins. Detected by the absence of a Subscription. | — | +| **Invited** | A Room where a Subscription exists but its status is `invited` and not yet accepted. Detected by a Subscription whose status is `invited`. | — | +| **Channel** | A public Room (type `'c'`) visible to all Workspace 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 (a different thing) | +| **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 @@ -125,10 +128,12 @@ A **Last Open** and a **Last Seen** are not interchangeable — conflating them Two distinct kinds of transient per-Room state drive how the Room view renders Messages. Keep them apart. -| 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 | +| Term | Definition | Owner | Scope | +| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ----------------------------- | ------------------------------------ | +| **Message Action State** | The active Message Action in the Room (Quote, Edit, or React) and its target Message(s); null when none is active | A per-Room MessageActionStore | One Room's Message rows and composer | +| **Positional State** | Umbrella for which Message is highlighted and the jump or scroll position. Splits across two owners (below). | — | — | +| **Jump orchestration** | Deciding to jump to a Message, resolving its anchor, and requesting the jump | RoomView (`useJumpToMessage`) | The Room screen | +| **Scroll and highlight execution** | Performing the scroll onto the target Message and rendering its highlight | The List component | The rendered Message list | ### Message Actions @@ -162,17 +167,19 @@ A **Message Action** is the active mode on a Message in the Room view. The three ## Omnichannel / Livechat -| Term | Definition | Aliases to avoid | -| ---------------------- | ---------------------------------------------------------------------------------------------------- | ---------------------------- | -| **Omnichannel Room** | A customer-service Room (type `'l'`) connecting a Visitor to an Agent | Livechat room, support chat | -| **Visitor** | An external customer who initiates an Omnichannel conversation, identified by a unique token | Client, customer, end-user | -| **Agent** | A User designated to handle Omnichannel conversations, with `statusLivechat` (available/unavailable) | Support agent, operator, rep | -| **Inquiry** | A queued Omnichannel request waiting to be picked up or routed to an Agent | Queue item, ticket | -| **Department** | An organizational unit that groups Agents for Omnichannel routing | Team (ambiguous), group | -| **Omnichannel Source** | How an Omnichannel conversation was initiated (widget, email, sms, app, api) | Channel origin | -| **Served By** | The Agent currently assigned to handle an Omnichannel Room | Assigned agent, handler | -| **On Hold** | An Omnichannel Room temporarily paused by the Agent | Paused, suspended | -| **Transfer** | Moving an Omnichannel Room to a different Agent or Department | Forward, reassign, handoff | +| Term | Definition | Aliases to avoid | +| ---------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| **Omnichannel Room** | A customer-service Room (type `'l'`) connecting a Visitor to an Agent | Livechat room, support chat | +| **Visitor** | An external customer who initiates an Omnichannel conversation, identified by a unique token | Client, customer, end-user | +| **Agent** | A User designated to handle Omnichannel conversations, with `statusLivechat` (available/unavailable) | Support agent, operator, rep | +| **Inquiry** | A queued Omnichannel request waiting to be picked up or routed to an Agent | Queue item, ticket | +| **Department** | An organizational unit that groups Agents for Omnichannel routing | Team (ambiguous), group | +| **Omnichannel Source** | How an Omnichannel conversation was initiated (widget, email, sms, app, api) | Channel origin | +| **Served By** | The Agent currently assigned to handle an Omnichannel Room | Assigned agent, handler | +| **On Hold** | An Omnichannel Room temporarily paused by the Agent | Paused, suspended | +| **Transfer** | Moving an Omnichannel Room to a different Agent or Department | Forward, reassign, handoff | +| **Routing Config** | Per-server Omnichannel routing settings that say whether Agents may Return to Queue and see the Inquiry queue | Livechat config, routing settings | +| **Return to Queue** | An Agent handing an Omnichannel Room back so it becomes an Inquiry again | Return inquiry, release chat | ## Encryption diff --git a/app/actions/actionsTypes.ts b/app/actions/actionsTypes.ts index 06445d9600e..1eae46f6e78 100644 --- a/app/actions/actionsTypes.ts +++ b/app/actions/actionsTypes.ts @@ -34,6 +34,7 @@ export const INQUIRY = createRequestTypes('INQUIRY', [ 'QUEUE_UPDATE', 'QUEUE_REMOVE' ]); +export const ROUTING_CONFIG = createRequestTypes('ROUTING_CONFIG'); export const APP = createRequestTypes('APP', [ 'START', 'READY', diff --git a/app/containers/MessageComposer/MessageComposer.test.tsx b/app/containers/MessageComposer/MessageComposer.test.tsx index 8a15b8a2472..b92bb035192 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, type TMessageActionState } from '../../definitions'; import { colors } from '../../lib/constants/colors'; -import { type IRoomContext, RoomContext } from '../../views/RoomView/context'; +import { type ComposerState } from '../../views/RoomView/definitions'; +import { ComposerProvider } from '../../views/RoomView/stores/ComposerStore'; import { MessageActionProvider } from '../message/stores/MessageActionStore'; import * as EmojiKeyboardHook from './hooks/useEmojiKeyboard'; import { initStore } from '../../lib/store/auxStore'; @@ -122,20 +123,20 @@ const Render = ({ action, children }: { - context?: Partial; + context?: Partial; action?: TMessageActionState; children?: ReactElement; }) => ( - + <> {children} - + ); diff --git a/app/containers/MessageComposer/MessageComposer.tsx b/app/containers/MessageComposer/MessageComposer.tsx index bcbe3e3a947..6805a659507 100644 --- a/app/containers/MessageComposer/MessageComposer.tsx +++ b/app/containers/MessageComposer/MessageComposer.tsx @@ -4,8 +4,13 @@ import { useBackHandler } from '@react-native-community/hooks'; import { Q } from '@nozbe/watermelondb'; import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'; -import { useRoomContext } from '../../views/RoomView/context'; -import { useMessageAction } from '../message/stores/MessageActionStore'; +import { + useComposerRid, + useComposerSharing, + useComposerStoreApi, + useComposerTmid +} from '../../views/RoomView/stores/ComposerStore'; +import { useMessageActionKind, useMessageActionStoreApi } from '../message/stores/MessageActionStore'; import { Autocomplete } from './components'; import { MIN_HEIGHT } from './constants'; import { @@ -53,8 +58,12 @@ export const MessageComposer = ({ }); const contentHeight = useSharedValue(MIN_HEIGHT); useCloseKeyboardWhenOrientationChanges(); - const { rid, tmid, sharing, editRequest, onSendMessage, setQuotesAndText } = useRoomContext(); - const action = useMessageAction(); + const rid = useComposerRid(); + const tmid = useComposerTmid(); + const sharing = useComposerSharing(); + const composerStore = useComposerStoreApi(); + const messageActionStore = useMessageActionStoreApi(); + const actionKind = useMessageActionKind(); const alsoSendThreadToChannel = useAlsoSendThreadToChannel(); const { showEmojiKeyboard, showEmojiSearchbar, openEmojiSearchbar, resetKeyboard, keyboardHeight } = useEmojiKeyboard(); const { setAlsoSendThreadToChannel, setAutocompleteParams, clearAttachments } = useMessageComposerApi(); @@ -74,9 +83,9 @@ export const MessageComposer = ({ return false; }); - const closeEmojiKeyboardAndAction = (action?: Function, params?: any) => { + const closeEmojiKeyboardAndAction = (onClosed?: Function, params?: any) => { resetKeyboard(); - action && action(params); + onClosed && onClosed(params); }; useImperativeHandle(forwardedRef, () => ({ @@ -94,6 +103,11 @@ export const MessageComposer = ({ const handleSendMessage = async () => { if (!rid) return; + const { editRequest, onSendMessage, setQuotesAndText } = composerStore.getState(); + const { action } = messageActionStore.getState(); + const editingMessageId = action?.kind === 'edit' ? action.messageId : undefined; + const quotedMessageIds = action?.kind === 'quote' ? action.messageIds : []; + if (alsoSendThreadToChannel) { setAlsoSendThreadToChannel(false); } @@ -108,13 +122,13 @@ export const MessageComposer = ({ const textFromInput = composerInputComponentRef.current.getTextAndClear(); - if (action?.kind === 'edit') { + if (editingMessageId) { const updatedAttachments = attachments.length ? attachments.map(({ description, altText, fileId, filename }) => altTextSupported ? { description: altText || '', fileId, filename } : { description: description || '' } ) : undefined; - editRequest?.({ id: action.messageId, msg: textFromInput, rid, attachments: updatedAttachments }); + editRequest?.({ id: editingMessageId, msg: textFromInput, rid, attachments: updatedAttachments }); clearAttachments(); return; } @@ -122,8 +136,8 @@ export const MessageComposer = ({ if (attachments.length) { let quotedMessage: string | undefined; - if (action?.kind === 'quote') { - quotedMessage = await prepareQuoteMessage(textFromInput, action.messageIds, tmid); + if (quotedMessageIds.length) { + quotedMessage = await prepareQuoteMessage(textFromInput, quotedMessageIds, tmid); } try { @@ -146,8 +160,8 @@ export const MessageComposer = ({ } } - if (action?.kind === 'quote') { - const quoteMessage = await prepareQuoteMessage(textFromInput, action.messageIds, tmid); + if (quotedMessageIds.length) { + const quoteMessage = await prepareQuoteMessage(textFromInput, quotedMessageIds, tmid); onSendMessage?.(quoteMessage); return; } @@ -245,7 +259,7 @@ export const MessageComposer = ({ }}> ( - ({ children }, ref): ReactElement => { + ({ children = }, ref): ReactElement => { return ( diff --git a/app/containers/MessageComposer/__snapshots__/MessageComposer.test.tsx.snap b/app/containers/MessageComposer/__snapshots__/MessageComposer.test.tsx.snap index 80bd5075c9e..544ac12a650 100644 --- a/app/containers/MessageComposer/__snapshots__/MessageComposer.test.tsx.snap +++ b/app/containers/MessageComposer/__snapshots__/MessageComposer.test.tsx.snap @@ -9599,7 +9599,7 @@ exports[`MessageComposer Toolbar tap emoji 1`] = ` borderless={true} collapsable={false} delayLongPress={600} - handlerTag={11} + handlerTag={-1} handlerType="NativeViewGestureHandler" hitSlop={ { diff --git a/app/containers/MessageComposer/components/Autocomplete/Autocomplete.tsx b/app/containers/MessageComposer/components/Autocomplete/Autocomplete.tsx index f8db6d74439..7f66d0167d9 100644 --- a/app/containers/MessageComposer/components/Autocomplete/Autocomplete.tsx +++ b/app/containers/MessageComposer/components/Autocomplete/Autocomplete.tsx @@ -8,7 +8,8 @@ import { AutocompleteItem } from './AutocompleteItem'; import { useAutocomplete } from '../../hooks'; import { type IAutocompleteItemProps } from '../../interfaces'; import { AutocompletePreview } from './AutocompletePreview'; -import { useRoomContext } from '../../../../views/RoomView/context'; +import { useComposerRid, useUpdateAutocompleteVisible } from '../../../../views/RoomView/stores/ComposerStore'; +import { useAutocompleteA11yAnnounce } from './useAutocompleteA11yAnnounce'; import { useStyle } from './styles'; export const Autocomplete = ({ @@ -20,7 +21,10 @@ export const Autocomplete = ({ style: AnimatedStyle; accessibilityFocusOnInput: () => void; }): ReactElement | null => { - const { rid, updateAutocompleteVisible } = useRoomContext(); + useAutocompleteA11yAnnounce(); + + const rid = useComposerRid(); + const updateAutocompleteVisible = useUpdateAutocompleteVisible(); const { text, type, params } = useAutocompleteParams(); const items = useAutocomplete({ rid, diff --git a/app/containers/MessageComposer/components/Autocomplete/useAutocompleteA11yAnnounce.test.tsx b/app/containers/MessageComposer/components/Autocomplete/useAutocompleteA11yAnnounce.test.tsx new file mode 100644 index 00000000000..4cd3e646db2 --- /dev/null +++ b/app/containers/MessageComposer/components/Autocomplete/useAutocompleteA11yAnnounce.test.tsx @@ -0,0 +1,75 @@ +import { act, renderHook } from '@testing-library/react-native'; +import { AccessibilityInfo } from 'react-native'; +import { type ReactNode } from 'react'; + +import { useAutocompleteA11yAnnounce } from './useAutocompleteA11yAnnounce'; +import { createComposerStore, ComposerStoreContext } from '../../../../views/RoomView/stores/ComposerStore'; +import { type ComposerStore } from '../../../../views/RoomView/definitions'; + +const externalState = { + rid: 'rid-1', + t: 'c', + tmid: undefined, + room: { rid: 'rid-1', t: 'c' }, + sharing: false, + editCancel: jest.fn(), + editRequest: jest.fn(() => Promise.resolve()), + onRemoveQuoteMessage: jest.fn(), + onSendMessage: jest.fn(), + setQuotesAndText: jest.fn(), + getText: jest.fn(() => '') +}; + +const setup = () => { + const store: ComposerStore = createComposerStore(externalState); + const wrapper = ({ children }: { children: ReactNode }) => ( + {children} + ); + const view = renderHook(() => useAutocompleteA11yAnnounce(), { wrapper }); + return { store, ...view }; +}; + +describe('useAutocompleteA11yAnnounce', () => { + let announceSpy: jest.SpyInstance; + + beforeEach(() => { + jest.useFakeTimers(); + announceSpy = jest.spyOn(AccessibilityInfo, 'announceForAccessibility').mockImplementation(() => {}); + }); + + afterEach(() => { + announceSpy.mockRestore(); + jest.useRealTimers(); + }); + + it('announces 800ms after a false→true transition', () => { + const { store } = setup(); + + act(() => store.getState().updateAutocompleteVisible(true)); + expect(announceSpy).not.toHaveBeenCalled(); + + act(() => jest.advanceTimersByTime(800)); + expect(announceSpy).toHaveBeenCalledTimes(1); + }); + + it('clears the pending timeout when visibility flips back before 800ms', () => { + const { store } = setup(); + + act(() => store.getState().updateAutocompleteVisible(true)); + act(() => jest.advanceTimersByTime(400)); + act(() => store.getState().updateAutocompleteVisible(false)); + + act(() => jest.advanceTimersByTime(800)); + expect(announceSpy).not.toHaveBeenCalled(); + }); + + it('clears the pending timeout on unmount', () => { + const { store, unmount } = setup(); + + act(() => store.getState().updateAutocompleteVisible(true)); + unmount(); + + act(() => jest.advanceTimersByTime(800)); + expect(announceSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/app/containers/MessageComposer/components/Autocomplete/useAutocompleteA11yAnnounce.ts b/app/containers/MessageComposer/components/Autocomplete/useAutocompleteA11yAnnounce.ts new file mode 100644 index 00000000000..30dadfa9d1f --- /dev/null +++ b/app/containers/MessageComposer/components/Autocomplete/useAutocompleteA11yAnnounce.ts @@ -0,0 +1,22 @@ +import { useEffect } from 'react'; +import { AccessibilityInfo } from 'react-native'; + +import I18n from '../../../../i18n'; +import { useIsAutocompleteVisible } from '../../../../views/RoomView/stores/ComposerStore'; + +export const useAutocompleteA11yAnnounce = (): void => { + const isAutocompleteVisible = useIsAutocompleteVisible(); + + useEffect(() => { + if (!isAutocompleteVisible) { + return; + } + + // timeout to prevent conflict with default keyboard announcement. + const timeout = setTimeout(() => { + AccessibilityInfo.announceForAccessibility(I18n.t('The_autocomplete_options_are_available_above_the_input_composer')); + }, 800); + + return () => clearTimeout(timeout); + }, [isAutocompleteVisible]); +}; diff --git a/app/containers/MessageComposer/components/Buttons/ActionsButton.tsx b/app/containers/MessageComposer/components/Buttons/ActionsButton.tsx index 3ef381074fd..6c4833049f1 100644 --- a/app/containers/MessageComposer/components/Buttons/ActionsButton.tsx +++ b/app/containers/MessageComposer/components/Buttons/ActionsButton.tsx @@ -9,10 +9,12 @@ import Navigation from '../../../../lib/navigation/appNavigation'; import { useMasterDetail } from '../../../../lib/hooks/useMasterDetail'; import { usePermissions } from '../../../../lib/hooks/usePermissions'; import { useCanUploadFile, useChooseMedia } from '../../hooks'; -import { useRoomContext } from '../../../../views/RoomView/context'; +import { useComposerRid, useComposerTmid, useComposerType } from '../../../../views/RoomView/stores/ComposerStore'; export const ActionsButton = () => { - const { rid, tmid, t } = useRoomContext(); + const rid = useComposerRid(); + const tmid = useComposerTmid(); + const t = useComposerType(); const { closeEmojiKeyboardAndAction } = useContext(MessageInnerContext); const permissionToUpload = useCanUploadFile(rid); const [permissionToViewCannedResponses] = usePermissions(['view-canned-responses'], rid); diff --git a/app/containers/MessageComposer/components/Buttons/MicOrSendButton.tsx b/app/containers/MessageComposer/components/Buttons/MicOrSendButton.tsx index 7dde28e7e34..d1d22c8c08a 100644 --- a/app/containers/MessageComposer/components/Buttons/MicOrSendButton.tsx +++ b/app/containers/MessageComposer/components/Buttons/MicOrSendButton.tsx @@ -7,13 +7,14 @@ import i18n from '../../../../i18n'; import { useAppSelector } from '../../../../lib/hooks/useAppSelector'; import { openAppSettings } from '../../../../lib/methods/helpers/openAppSettings'; import { useTheme } from '../../../../theme'; -import { useRoomContext } from '../../../../views/RoomView/context'; +import { useComposerRid, useComposerSharing } from '../../../../views/RoomView/stores/ComposerStore'; import { MessageInnerContext, useComposerAttachments, useMessageComposerApi, useMicOrSend } from '../../context'; import { useCanUploadFile } from '../../hooks'; import { BaseButton } from './BaseButton'; export const MicOrSendButton = (): ReactElement | null => { - const { rid, sharing } = useRoomContext(); + const rid = useComposerRid(); + const sharing = useComposerSharing(); const micOrSend = useMicOrSend(); const attachments = useComposerAttachments(); const { sendMessage } = useContext(MessageInnerContext); diff --git a/app/containers/MessageComposer/components/CancelEdit.tsx b/app/containers/MessageComposer/components/CancelEdit.tsx index 86c6fb1679a..f99ce062124 100644 --- a/app/containers/MessageComposer/components/CancelEdit.tsx +++ b/app/containers/MessageComposer/components/CancelEdit.tsx @@ -1,10 +1,10 @@ import { BaseButton } from './Buttons'; -import { useRoomContext } from '../../../views/RoomView/context'; +import { useEditCancel } from '../../../views/RoomView/stores/ComposerStore'; import { useMessageAction } from '../../message/stores/MessageActionStore'; import { Gap } from './Gap'; export const CancelEdit = () => { - const { editCancel } = useRoomContext(); + const editCancel = useEditCancel(); const action = useMessageAction(); if (action?.kind !== 'edit') { diff --git a/app/containers/MessageComposer/components/ComposerInput.tsx b/app/containers/MessageComposer/components/ComposerInput.tsx index a0547bab0a6..4df191d42e6 100644 --- a/app/containers/MessageComposer/components/ComposerInput.tsx +++ b/app/containers/MessageComposer/components/ComposerInput.tsx @@ -1,4 +1,4 @@ -import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef } from 'react'; +import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; import { TextInput, StyleSheet, type TextInputProps, InteractionManager } from 'react-native'; import { useDebouncedCallback } from 'use-debounce'; import { useDispatch } from 'react-redux'; @@ -32,7 +32,13 @@ import { 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 { + useComposerRid, + useComposerRoom, + useComposerSharing, + useComposerTmid, + useSetQuotesAndText +} from '../../../views/RoomView/stores/ComposerStore'; import { useMessageAction } from '../../message/stores/MessageActionStore'; import { getMessageById } from '../../../lib/database/services/Message'; import { generateTriggerId } from '../../../lib/methods/actions'; @@ -48,352 +54,352 @@ import { isExternalKeyboardConnected } from '../../../lib/methods/helpers/extern const defaultSelection: IInputSelection = { start: 0, end: 0 }; -export const ComposerInput = memo( - forwardRef(({ inputRef }, ref) => { - const { colors, theme } = useTheme(); - const { rid, tmid, sharing, setQuotesAndText, room } = useRoomContext(); - const action = useMessageAction(); - const focused = useFocused(); - const { setFocused, setMicOrSend, setAutocompleteParams } = useMessageComposerApi(); - const autocompleteType = useAutocompleteParams()?.type; - const textRef = useRef(''); - const firstRender = useRef(true); - const selectionRef = useRef(defaultSelection); - const dispatch = useDispatch(); - const isMasterDetail = useMasterDetail(); - const altTextSupported = useAltTextSupported(); - let placeholder = tmid ? I18n.t('Add_thread_reply') : ''; - if (room && !tmid) { - placeholder = I18n.t('Message_roomname', { roomName: (room.t === 'd' ? '@' : '#') + getRoomTitle(room) }); - if (!isTablet && placeholder.length > COMPOSER_INPUT_PLACEHOLDER_MAX_LENGTH) { - placeholder = `${placeholder.slice(0, COMPOSER_INPUT_PLACEHOLDER_MAX_LENGTH)}...`; - } +export const ComposerInput = forwardRef(({ inputRef }, ref) => { + const { colors, theme } = useTheme(); + const rid = useComposerRid(); + const tmid = useComposerTmid(); + const sharing = useComposerSharing(); + const setQuotesAndText = useSetQuotesAndText(); + const room = useComposerRoom(); + const action = useMessageAction(); + const focused = useFocused(); + const { setFocused, setMicOrSend, setAutocompleteParams } = useMessageComposerApi(); + const autocompleteType = useAutocompleteParams()?.type; + const textRef = useRef(''); + const firstRender = useRef(true); + const selectionRef = useRef(defaultSelection); + const dispatch = useDispatch(); + const isMasterDetail = useMasterDetail(); + const altTextSupported = useAltTextSupported(); + let placeholder = tmid ? I18n.t('Add_thread_reply') : ''; + if (room && !tmid) { + placeholder = I18n.t('Message_roomname', { roomName: (room.t === 'd' ? '@' : '#') + getRoomTitle(room) }); + if (!isTablet && placeholder.length > COMPOSER_INPUT_PLACEHOLDER_MAX_LENGTH) { + placeholder = `${placeholder.slice(0, COMPOSER_INPUT_PLACEHOLDER_MAX_LENGTH)}...`; } - const route = useRoute>(); - const usedCannedResponse = route.params?.usedCannedResponse; - const prevAction = usePrevious(action); - - // subscribe to changes on mic state to update draft after a message is sent - useMicOrSend(); - const { saveMessageDraft } = useAutoSaveDraft(textRef.current); - - // workaround to handle issues with iOS back swipe navigation - const { iOSBackSwipe } = useIOSBackSwipeHandler(); - - // Draft/Canned Responses - useEffect(() => { - const setDraftMessage = async () => { - const draftMessage = await loadDraftMessage({ rid, tmid }); - if (draftMessage) { - const parsedDraft = parseJson(draftMessage); - if (parsedDraft?.msg || parsedDraft?.quotes) { - setQuotesAndText?.(parsedDraft.msg, parsedDraft.quotes); - } else { - setInput(draftMessage); - } + } + const route = useRoute>(); + const usedCannedResponse = route.params?.usedCannedResponse; + const prevAction = usePrevious(action); + + // subscribe to changes on mic state to update draft after a message is sent + useMicOrSend(); + const { saveMessageDraft } = useAutoSaveDraft(textRef.current); + + // workaround to handle issues with iOS back swipe navigation + const { iOSBackSwipe } = useIOSBackSwipeHandler(); + + // Draft/Canned Responses + useEffect(() => { + const setDraftMessage = async () => { + const draftMessage = await loadDraftMessage({ rid, tmid }); + if (draftMessage) { + const parsedDraft = parseJson(draftMessage); + if (parsedDraft?.msg || parsedDraft?.quotes) { + setQuotesAndText?.(parsedDraft.msg, parsedDraft.quotes); + } else { + setInput(draftMessage); } - }; - - if (action?.kind !== 'edit' && firstRender.current) { - firstRender.current = false; - setDraftMessage(); } - if (sharing) return; - if (usedCannedResponse) setInput(usedCannedResponse); - }, [action?.kind, rid, tmid, usedCannedResponse]); - - // Edit/quote - useEffect(() => { - const fetchMessageAndSetInput = async (messageId: string) => { - const message = await getMessageById(messageId); - if (message) { - setInput(message?.msg || (altTextSupported ? '' : message?.attachments?.[0]?.description || '')); - } - }; - - if (sharing) return; + }; - if (prevAction?.kind === 'edit' && action?.kind !== 'edit') { - setInput(''); - return; - } - if (action?.kind === 'edit') { - focus(); - fetchMessageAndSetInput(action.messageId); - return; - } - if (action?.kind === 'quote' && action.messageIds.length) { - focus(); - } - }, [action]); - - useFocusEffect( - useCallback(() => { - const task = InteractionManager.runAfterInteractions(() => { - emitter.on('addMarkdown', ({ style }) => { - const { start, end } = selectionRef.current; - const text = textRef.current; - const markdown = MARKDOWN_STYLES[style]; - const newText = `${text.substr(0, start)}${markdown}${text.substr(start, end - start)}${markdown}${text.substr(end)}`; - setInput(newText, { - start: start + markdown.length, - end: start === end ? start + markdown.length : end + markdown.length - }); - }); - emitter.on('toolbarMention', () => { - if (autocompleteType) { - return; - } - const { start, end } = selectionRef.current; - const text = textRef.current; - const newText = `${text.substr(0, start)}@${text.substr(start, end - start)}${text.substr(end)}`; - setInput(newText, { start: start + 1, end: start === end ? start + 1 : end + 1 }); - setAutocompleteParams({ text: '', type: '@' }); - }); - }); - return () => { - emitter.off('addMarkdown'); - emitter.off('toolbarMention'); - task?.cancel(); - }; - }, [rid, tmid, autocompleteType]) - ); - - useImperativeHandle(ref, () => ({ - getTextAndClear: () => { - const text = textRef.current; - setInput('', undefined, true); - return text; - }, - getText: () => textRef.current, - getSelection: () => selectionRef.current, - setInput, - onAutocompleteItemSelected, - focus - })); - - const setInput: TSetInput = (text, selection, forceUpdateDraftMessage) => { - const message = text.trim(); - textRef.current = message; - - if (forceUpdateDraftMessage) { - saveMessageDraft(''); + if (action?.kind !== 'edit' && firstRender.current) { + firstRender.current = false; + setDraftMessage(); + } + if (sharing) return; + if (usedCannedResponse) setInput(usedCannedResponse); + }, [action?.kind, rid, tmid, usedCannedResponse]); + + // Edit/quote + useEffect(() => { + const fetchMessageAndSetInput = async (messageId: string) => { + const message = await getMessageById(messageId); + if (message) { + setInput(message?.msg || (altTextSupported ? '' : message?.attachments?.[0]?.description || '')); } + }; - inputRef.current?.setNativeProps?.({ text }); + if (sharing) return; - if (selection) { - // setSelection won't trigger onSelectionChange, so we need it to be ran after new text is set - setTimeout(() => { - inputRef.current?.setSelection?.(selection.start, selection.end); - selectionRef.current = selection; - }, 50); - } - setMicOrSend(message.length === 0 ? 'mic' : 'send'); - }; + if (prevAction?.kind === 'edit' && action?.kind !== 'edit') { + setInput(''); + return; + } + if (action?.kind === 'edit') { + focus(); + fetchMessageAndSetInput(action.messageId); + return; + } + if (action?.kind === 'quote' && action.messageIds.length) { + focus(); + } + }, [action]); - const focus = () => { - setTimeout(() => { - if (inputRef.current) { - inputRef.current.focus(); + useFocusEffect(() => { + const task = InteractionManager.runAfterInteractions(() => { + emitter.on('addMarkdown', ({ style }) => { + const { start, end } = selectionRef.current; + const text = textRef.current; + const markdown = MARKDOWN_STYLES[style]; + const newText = `${text.substr(0, start)}${markdown}${text.substr(start, end - start)}${markdown}${text.substr(end)}`; + setInput(newText, { + start: start + markdown.length, + end: start === end ? start + markdown.length : end + markdown.length + }); + }); + emitter.on('toolbarMention', () => { + if (autocompleteType) { + return; } - }, 300); - }; - - const onChangeText: TextInputProps['onChangeText'] = text => { - textRef.current = text; - debouncedOnChangeText(text); - setInput(text); + const { start, end } = selectionRef.current; + const text = textRef.current; + const newText = `${text.substr(0, start)}@${text.substr(start, end - start)}${text.substr(end)}`; + setInput(newText, { start: start + 1, end: start === end ? start + 1 : end + 1 }); + setAutocompleteParams({ text: '', type: '@' }); + }); + }); + return () => { + emitter.off('addMarkdown'); + emitter.off('toolbarMention'); + task?.cancel(); }; + }); - const onSelectionChange: TextInputProps['onSelectionChange'] = e => { - selectionRef.current = e.nativeEvent.selection; - }; + useImperativeHandle(ref, () => ({ + getTextAndClear: () => { + const text = textRef.current; + setInput('', undefined, true); + return text; + }, + getText: () => textRef.current, + getSelection: () => selectionRef.current, + setInput, + onAutocompleteItemSelected, + focus + })); + + const setInput: TSetInput = (text, selection, forceUpdateDraftMessage) => { + const message = text.trim(); + textRef.current = message; + + if (forceUpdateDraftMessage) { + saveMessageDraft(''); + } - const onFocus: TextInputProps['onFocus'] = () => { - setFocused(true); - }; + inputRef.current?.setNativeProps?.({ text }); - const onTouchStart: TextInputProps['onTouchStart'] = () => { - setFocused(true); - }; + if (selection) { + // setSelection won't trigger onSelectionChange, so we need it to be ran after new text is set + setTimeout(() => { + inputRef.current?.setSelection?.(selection.start, selection.end); + selectionRef.current = selection; + }, 50); + } + setMicOrSend(message.length === 0 ? 'mic' : 'send'); + }; - const onBlur: TextInputProps['onBlur'] = () => { - if (!iOSBackSwipe.current && !isExternalKeyboardConnected()) { - setFocused(false); - stopAutocomplete(); + const focus = () => { + setTimeout(() => { + if (inputRef.current) { + inputRef.current.focus(); } - }; + }, 300); + }; + + const onChangeText: TextInputProps['onChangeText'] = text => { + textRef.current = text; + debouncedOnChangeText(text); + setInput(text); + }; + + const onSelectionChange: TextInputProps['onSelectionChange'] = e => { + selectionRef.current = e.nativeEvent.selection; + }; + + const onFocus: TextInputProps['onFocus'] = () => { + setFocused(true); + }; + + const onTouchStart: TextInputProps['onTouchStart'] = () => { + setFocused(true); + }; + + const onBlur: TextInputProps['onBlur'] = () => { + if (!iOSBackSwipe.current && !isExternalKeyboardConnected()) { + setFocused(false); + stopAutocomplete(); + } + }; - const onAutocompleteItemSelected: IAutocompleteItemProps['onPress'] = async item => { - if (item.type === 'loading') { - return null; - } + const onAutocompleteItemSelected: IAutocompleteItemProps['onPress'] = async item => { + if (item.type === 'loading') { + return null; + } - // If it's slash command preview, we need to execute the command - if (item.type === '/preview') { - try { - if (!rid) return; - const db = database.active; - const commandsCollection = db.get('slash_commands'); - const commandRecord = await commandsCollection.find(item.text); - const { appId } = commandRecord; - const triggerId = generateTriggerId(appId); - executeCommandPreview(item.text, item.params, rid, item.preview, triggerId, tmid); - } catch (e) { - log(e); - } - requestAnimationFrame(() => { - stopAutocomplete(); - setInput('', { start: 0, end: 0 }); - }); - return; + // If it's slash command preview, we need to execute the command + if (item.type === '/preview') { + try { + if (!rid) return; + const db = database.active; + const commandsCollection = db.get('slash_commands'); + const commandRecord = await commandsCollection.find(item.text); + const { appId } = commandRecord; + const triggerId = generateTriggerId(appId); + executeCommandPreview(item.text, item.params, rid, item.preview, triggerId, tmid); + } catch (e) { + log(e); } - - // If it's canned response, but there's no canned responses, we open the canned responses view - if (item.type === '!' && item.id === NO_CANNED_RESPONSES) { - const params = { rid }; - if (isMasterDetail) { - Navigation.navigate('ModalStackNavigator', { screen: 'CannedResponsesListView', params }); - } else { - Navigation.navigate('CannedResponsesListView', params); - } + requestAnimationFrame(() => { stopAutocomplete(); - return; - } + setInput('', { start: 0, end: 0 }); + }); + return; + } - const text = textRef.current; - const { start, end } = selectionRef.current; - const cursor = Math.max(start, end); - const regexp = getMentionRegexp(); - let result = text.substr(0, cursor).replace(regexp, ''); - // Remove the ! after select the canned response - if (item.type === '!') { - const lastIndexOfExclamation = text.lastIndexOf('!', cursor); - result = text.substr(0, lastIndexOfExclamation).replace(regexp, ''); + // If it's canned response, but there's no canned responses, we open the canned responses view + if (item.type === '!' && item.id === NO_CANNED_RESPONSES) { + const params = { rid }; + if (isMasterDetail) { + Navigation.navigate('ModalStackNavigator', { screen: 'CannedResponsesListView', params }); + } else { + Navigation.navigate('CannedResponsesListView', params); } - let mention = ''; - switch (item.type) { - case '@': - mention = fetchIsAllOrHere(item) ? item.title : item.subtitle || item.title; - break; - case '#': - mention = item.subtitle ? item.subtitle : ''; - break; - case ':': - mention = `${typeof item.emoji === 'string' ? item.emoji : item.emoji.name}:`; - break; - case '/': - mention = item.title; - break; - case '!': - mention = item.subtitle ? item.subtitle : ''; - break; - default: - mention = ''; - } - const newText = `${result}${mention} ${text.slice(cursor)}`; + stopAutocomplete(); + return; + } - const newCursor = result.length + mention.length + 1; - setInput(newText, { start: newCursor, end: newCursor }); - focus(); - requestAnimationFrame(() => { - stopAutocomplete(); - }); - }; + const text = textRef.current; + const { start, end } = selectionRef.current; + const cursor = Math.max(start, end); + const regexp = getMentionRegexp(); + let textBeforeMention = text.substr(0, cursor).replace(regexp, ''); + // Remove the ! after select the canned response + if (item.type === '!') { + const lastIndexOfExclamation = text.lastIndexOf('!', cursor); + textBeforeMention = text.substr(0, lastIndexOfExclamation).replace(regexp, ''); + } + let mention = ''; + switch (item.type) { + case '@': + mention = fetchIsAllOrHere(item) ? item.title : item.subtitle || item.title; + break; + case '#': + mention = item.subtitle ? item.subtitle : ''; + break; + case ':': + mention = `${typeof item.emoji === 'string' ? item.emoji : item.emoji.name}:`; + break; + case '/': + mention = item.title; + break; + case '!': + mention = item.subtitle ? item.subtitle : ''; + break; + default: + mention = ''; + } + const newText = `${textBeforeMention}${mention} ${text.slice(cursor)}`; - const stopAutocomplete = () => { - setAutocompleteParams({ text: '', type: null, params: '' }); - }; + const newCursor = textBeforeMention.length + mention.length + 1; + setInput(newText, { start: newCursor, end: newCursor }); + focus(); + requestAnimationFrame(() => { + stopAutocomplete(); + }); + }; - const debouncedOnChangeText = useDebouncedCallback(async (text: string) => { - const isTextEmpty = text.length === 0; - handleTyping(!isTextEmpty); - if (isTextEmpty || !focused) { - stopAutocomplete(); - return; - } - const { start, end } = selectionRef.current; - const cursor = Math.max(start, end); - const whiteSpaceOrBreakLineRegex = /[\s\n]+/; - const txt = - cursor < text.length ? text.substr(0, cursor).split(whiteSpaceOrBreakLineRegex) : text.split(whiteSpaceOrBreakLineRegex); - const lastWord = txt[txt.length - 1]; - const autocompleteText = lastWord.substring(1); - - if (!lastWord) { - stopAutocomplete(); - return; - } - if (!sharing && text.match(/^\//)) { - const commandParameter = text.match(/^\/([a-z0-9._-]+) (.+)/im); - if (commandParameter) { - const db = database.active; - const [, command, params] = commandParameter; - const commandsCollection = db.get('slash_commands'); - try { - const commandRecord = await commandsCollection.find(command); - if (commandRecord.providesPreview) { - setAutocompleteParams({ params, text: command, type: '/preview' }); - } - return; - } catch (e) { - // do nothing + const stopAutocomplete = () => { + setAutocompleteParams({ text: '', type: null, params: '' }); + }; + + const debouncedOnChangeText = useDebouncedCallback(async (text: string) => { + const isTextEmpty = text.length === 0; + handleTyping(!isTextEmpty); + if (isTextEmpty || !focused) { + stopAutocomplete(); + return; + } + const { start, end } = selectionRef.current; + const cursor = Math.max(start, end); + const whiteSpaceOrBreakLineRegex = /[\s\n]+/; + const txt = + cursor < text.length ? text.substr(0, cursor).split(whiteSpaceOrBreakLineRegex) : text.split(whiteSpaceOrBreakLineRegex); + const lastWord = txt[txt.length - 1]; + const autocompleteText = lastWord.substring(1); + + if (!lastWord) { + stopAutocomplete(); + return; + } + if (!sharing && text.match(/^\//)) { + const commandParameter = text.match(/^\/([a-z0-9._-]+) (.+)/im); + if (commandParameter) { + const db = database.active; + const [, command, params] = commandParameter; + const commandsCollection = db.get('slash_commands'); + try { + const commandRecord = await commandsCollection.find(command); + if (commandRecord.providesPreview) { + setAutocompleteParams({ params, text: command, type: '/preview' }); } + return; + } catch (e) { + // do nothing } - setAutocompleteParams({ text: autocompleteText, type: '/' }); - return; - } - if (lastWord.match(/^#/)) { - setAutocompleteParams({ text: autocompleteText, type: '#' }); - return; - } - if (lastWord.match(/^@/)) { - setAutocompleteParams({ text: autocompleteText, type: '@' }); - return; - } - if (lastWord.match(/^:/)) { - setAutocompleteParams({ text: autocompleteText, type: ':' }); - return; } - if (lastWord.match(/^!/) && room?.t === 'l') { - setAutocompleteParams({ text: autocompleteText, type: '!' }); - return; - } - - stopAutocomplete(); - }, textInputDebounceTime); - - const handleTyping = (isTyping: boolean) => { - if (sharing || !rid) return; - dispatch(userTyping(rid, isTyping, tmid ? { tmid } : {})); - }; + setAutocompleteParams({ text: autocompleteText, type: '/' }); + return; + } + if (lastWord.match(/^#/)) { + setAutocompleteParams({ text: autocompleteText, type: '#' }); + return; + } + if (lastWord.match(/^@/)) { + setAutocompleteParams({ text: autocompleteText, type: '@' }); + return; + } + if (lastWord.match(/^:/)) { + setAutocompleteParams({ text: autocompleteText, type: ':' }); + return; + } + if (lastWord.match(/^!/) && room?.t === 'l') { + setAutocompleteParams({ text: autocompleteText, type: '!' }); + return; + } - return ( - { - inputRef.current = component; - }} - blurOnSubmit={false} - onChangeText={onChangeText} - onTouchStart={onTouchStart} - onSelectionChange={onSelectionChange} - onFocus={onFocus} - onBlur={onBlur} - underlineColorAndroid='transparent' - defaultValue='' - multiline - {...(autocompleteType ? { autoComplete: 'off', autoCorrect: false, autoCapitalize: 'none' } : {})} - keyboardAppearance={theme === 'light' ? 'light' : 'dark'} - // eslint-disable-next-line no-nested-ternary - testID={`message-composer-input${tmid ? '-thread' : sharing ? '-share' : ''}`} - /> - ); - }) -); + stopAutocomplete(); + }, textInputDebounceTime); + + const handleTyping = (isTyping: boolean) => { + if (sharing || !rid) return; + dispatch(userTyping(rid, isTyping, tmid ? { tmid } : {})); + }; + + return ( + { + inputRef.current = component; + }} + blurOnSubmit={false} + onChangeText={onChangeText} + onTouchStart={onTouchStart} + onSelectionChange={onSelectionChange} + onFocus={onFocus} + onBlur={onBlur} + underlineColorAndroid='transparent' + defaultValue='' + multiline + {...(autocompleteType ? { autoComplete: 'off', autoCorrect: false, autoCapitalize: 'none' } : {})} + keyboardAppearance={theme === 'light' ? 'light' : 'dark'} + // eslint-disable-next-line no-nested-ternary + testID={`message-composer-input${tmid ? '-thread' : sharing ? '-share' : ''}`} + /> + ); +}); const styles = StyleSheet.create({ textInput: { diff --git a/app/containers/MessageComposer/components/Quotes/Quote.tsx b/app/containers/MessageComposer/components/Quotes/Quote.tsx index 80e20613cb7..498fd433a5f 100644 --- a/app/containers/MessageComposer/components/Quotes/Quote.tsx +++ b/app/containers/MessageComposer/components/Quotes/Quote.tsx @@ -3,7 +3,7 @@ import { View, Text } from 'react-native'; import dayjs from '../../../../lib/dayjs'; import { useTheme } from '../../../../theme'; import sharedStyles from '../../../../views/Styles'; -import { useRoomContext } from '../../../../views/RoomView/context'; +import { useComposerTmid, useOnRemoveQuoteMessage } from '../../../../views/RoomView/stores/ComposerStore'; import { BaseButton } from '../Buttons'; import { useMessage } from '../../hooks'; import { useAppSelector } from '../../../../lib/hooks/useAppSelector'; @@ -11,9 +11,10 @@ import { MarkdownPreview } from '../../../markdown'; export const Quote = ({ messageId }: { messageId: string }) => { const [styles, colors] = useStyle(); - const { tmid, onRemoveQuoteMessage } = useRoomContext(); + const tmid = useComposerTmid(); const message = useMessage(messageId, tmid); const useRealName = useAppSelector(({ settings }) => settings.UI_Use_Real_Name); + const onRemoveQuoteMessage = useOnRemoveQuoteMessage(); let username = ''; let msg = ''; diff --git a/app/containers/MessageComposer/components/Quotes/Quotes.tsx b/app/containers/MessageComposer/components/Quotes/Quotes.tsx index 7d648315a36..ae1fb0a5ce0 100644 --- a/app/containers/MessageComposer/components/Quotes/Quotes.tsx +++ b/app/containers/MessageComposer/components/Quotes/Quotes.tsx @@ -2,11 +2,10 @@ import { useEffect, useRef, type ReactElement } from 'react'; import { FlatList } from 'react-native'; import { Quote } from './Quote'; -import { useMessageAction } from '../../../message/stores/MessageActionStore'; +import { useQuotedMessageIds } from '../../../message/stores/MessageActionStore'; export const Quotes = (): ReactElement | null => { - const action = useMessageAction(); - const selectedMessages = action?.kind === 'quote' ? action.messageIds : []; + const selectedMessages = useQuotedMessageIds(); const nQuotesRef = useRef(0); const listRef = useRef(null); @@ -19,7 +18,7 @@ export const Quotes = (): ReactElement | null => { nQuotesRef.current = selectedMessages.length; }, [selectedMessages.length]); - if (action?.kind !== 'quote') { + if (!selectedMessages.length) { return null; } diff --git a/app/containers/MessageComposer/components/RecordAudio/RecordAudio.tsx b/app/containers/MessageComposer/components/RecordAudio/RecordAudio.tsx index 296505efd85..0e602c5f0e3 100644 --- a/app/containers/MessageComposer/components/RecordAudio/RecordAudio.tsx +++ b/app/containers/MessageComposer/components/RecordAudio/RecordAudio.tsx @@ -16,7 +16,7 @@ import { RECORDING_EXTENSION, RECORDING_MODE, RECORDING_SETTINGS } from '../../. import { useAppSelector } from '../../../../lib/hooks/useAppSelector'; import log from '../../../../lib/methods/helpers/log'; import { type IUpload } from '../../../../definitions'; -import { useRoomContext } from '../../../../views/RoomView/context'; +import { useComposerRid, useComposerTmid } from '../../../../views/RoomView/stores/ComposerStore'; import { useCanUploadFile } from '../../hooks'; import { Duration, type IDurationRef } from './Duration'; import AudioPlayer from '../../../AudioPlayer'; @@ -30,7 +30,8 @@ export const RecordAudio = (): ReactElement | null => { const numberOfTriesRef = useRef(0); const [status, setStatus] = useState<'recording' | 'reviewing'>('recording'); const { setRecordingAudio } = useMessageComposerApi(); - const { rid, tmid } = useRoomContext(); + const rid = useComposerRid(); + const tmid = useComposerTmid(); const server = useAppSelector(state => state.server.server); const user = useAppSelector(state => ({ id: state.login.user.id, token: state.login.user.token }), shallowEqual); const permissionToUpload = useCanUploadFile(rid); diff --git a/app/containers/MessageComposer/components/SendThreadToChannel.tsx b/app/containers/MessageComposer/components/SendThreadToChannel.tsx index c59a9f3c949..2605efbc22e 100644 --- a/app/containers/MessageComposer/components/SendThreadToChannel.tsx +++ b/app/containers/MessageComposer/components/SendThreadToChannel.tsx @@ -4,7 +4,7 @@ import { useEffect, useRef, type ReactElement } from 'react'; import { type Subscription } from 'rxjs'; import { Q } from '@nozbe/watermelondb'; -import { useRoomContext } from '../../../views/RoomView/context'; +import { useComposerTmid } from '../../../views/RoomView/stores/ComposerStore'; import { useAlsoSendThreadToChannel, useMessageComposerApi } from '../context'; import { CustomIcon } from '../../CustomIcon'; import { useTheme } from '../../../theme'; @@ -19,7 +19,7 @@ export const SendThreadToChannel = (): ReactElement | null => { const alsoSendThreadToChannel = useAlsoSendThreadToChannel(); const { setAlsoSendThreadToChannel } = useMessageComposerApi(); const { showEmojiSearchbar } = useEmojiKeyboard(); - const { tmid } = useRoomContext(); + const tmid = useComposerTmid(); const { colors } = useTheme(); const subscription = useRef(null); const alsoSendThreadToChannelUserPref = useAppSelector(state => state.login.user.alsoSendThreadToChannel); diff --git a/app/containers/MessageComposer/components/Toolbar/Default.tsx b/app/containers/MessageComposer/components/Toolbar/Default.tsx index 52e41ea3611..db92991bdf4 100644 --- a/app/containers/MessageComposer/components/Toolbar/Default.tsx +++ b/app/containers/MessageComposer/components/Toolbar/Default.tsx @@ -5,11 +5,11 @@ import { ActionsButton, BaseButton } from '../Buttons'; import { useMessageComposerApi } from '../../context'; import { Gap } from '../Gap'; import { emitter } from '../../../../lib/methods/helpers/emitter'; -import { useRoomContext } from '../../../../views/RoomView/context'; +import { useComposerSharing } from '../../../../views/RoomView/stores/ComposerStore'; import { useEmojiKeyboard } from '../../hooks/useEmojiKeyboard'; export const Default = (): ReactElement | null => { - const { sharing } = useRoomContext(); + const sharing = useComposerSharing(); const { setMarkdownToolbar } = useMessageComposerApi(); const { openEmojiKeyboard } = useEmojiKeyboard(); diff --git a/app/containers/MessageComposer/components/Unfocused/Left.tsx b/app/containers/MessageComposer/components/Unfocused/Left.tsx index 74863711b66..f684ae63ea1 100644 --- a/app/containers/MessageComposer/components/Unfocused/Left.tsx +++ b/app/containers/MessageComposer/components/Unfocused/Left.tsx @@ -5,10 +5,10 @@ import { useFocused } from '../../context'; import { useEmojiKeyboard } from '../../hooks/useEmojiKeyboard'; import { ActionsButton } from '../Buttons'; import { MIN_HEIGHT } from '../../constants'; -import { useRoomContext } from '../../../../views/RoomView/context'; +import { useComposerSharing } from '../../../../views/RoomView/stores/ComposerStore'; export const Left = (): ReactElement | null => { - const { sharing } = useRoomContext(); + const sharing = useComposerSharing(); const focused = useFocused(); const { showEmojiKeyboard, showEmojiSearchbar } = useEmojiKeyboard(); if (focused || showEmojiKeyboard || showEmojiSearchbar || sharing) { diff --git a/app/containers/MessageComposer/context.tsx b/app/containers/MessageComposer/context.tsx index b623c7df7a5..a51d03d5214 100644 --- a/app/containers/MessageComposer/context.tsx +++ b/app/containers/MessageComposer/context.tsx @@ -84,8 +84,8 @@ export const useComposerAttachments = (): State['attachments'] => useComposerSto type TMessageInnerContext = { sendMessage(): void; onEmojiSelected(emoji: IEmoji): void; - // TODO: action should be required - closeEmojiKeyboardAndAction(action?: Function, params?: any): void; + // TODO: onClosed should be required + closeEmojiKeyboardAndAction(onClosed?: Function, params?: any): void; focus(): void; }; diff --git a/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts b/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts index b90850c6438..528c03c15ad 100644 --- a/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts +++ b/app/containers/MessageComposer/hooks/useAutoSaveDraft.ts @@ -2,13 +2,14 @@ import { useRoute } from '@react-navigation/native'; import { useCallback, useEffect, useRef } from 'react'; import { saveDraftMessage } from '../../../lib/methods/draftMessage'; -import { useRoomContext } from '../../../views/RoomView/context'; +import { useComposerRid, useComposerTmid } from '../../../views/RoomView/stores/ComposerStore'; import { useMessageAction } from '../../message/stores/MessageActionStore'; import { useFocused } from '../context'; export const useAutoSaveDraft = (text = '') => { const route = useRoute(); - const { rid, tmid } = useRoomContext(); + const rid = useComposerRid(); + const tmid = useComposerTmid(); const action = useMessageAction(); const focused = useFocused(); const oldText = useRef(''); diff --git a/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx b/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx index da1ddb62bae..a827fc792b0 100644 --- a/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx +++ b/app/containers/MessageComposer/hooks/useChooseMedia.test.tsx @@ -14,12 +14,14 @@ jest.mock('../context', () => ({ useMessageComposerApi: jest.fn() })); -jest.mock('../../../views/RoomView/context', () => ({ - useRoomContext: jest.fn() +jest.mock('../../../views/RoomView/stores/ComposerStore', () => ({ + useSetQuotesAndText: jest.fn(), + useGetText: jest.fn() })); jest.mock('../../message/stores/MessageActionStore', () => ({ - useMessageAction: jest.fn() + useMessageAction: jest.fn(), + useQuotedMessageIds: jest.fn(() => []) })); jest.mock('../../../lib/hooks/useAltTextSupported', () => ({ @@ -49,8 +51,10 @@ jest.mock('../../../lib/methods/helpers/ImagePicker/ImagePicker', () => ({ const mockGetDocumentAsync = require('expo-document-picker').getDocumentAsync as jest.Mock; 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 mockUseSetQuotesAndText = require('../../../views/RoomView/stores/ComposerStore').useSetQuotesAndText as jest.Mock; +const mockUseGetText = require('../../../views/RoomView/stores/ComposerStore').useGetText as jest.Mock; const mockUseMessageAction = require('../../message/stores/MessageActionStore').useMessageAction as jest.Mock; +const mockUseQuotedMessageIds = require('../../message/stores/MessageActionStore').useQuotedMessageIds 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; @@ -71,10 +75,8 @@ describe('useChooseMedia', () => { }) ); mockUseMessageComposerApi.mockReturnValue({ addAttachments }); - mockUseRoomContext.mockReturnValue({ - setQuotesAndText: jest.fn(), - getText: jest.fn(() => 'draft') - }); + mockUseSetQuotesAndText.mockReturnValue(jest.fn()); + mockUseGetText.mockReturnValue(jest.fn(() => 'draft')); mockUseMessageAction.mockReturnValue(null); mockGetSubscriptionByRoomId.mockResolvedValue({ rid: 'room-id', t: 'c' }); mockGetThreadById.mockResolvedValue({ id: 'thread-id' }); @@ -135,6 +137,7 @@ describe('useChooseMedia', () => { it('forwards quoted message ids to ShareView as selectedMessages', async () => { mockUseAltTextSupported.mockReturnValue(false); mockUseMessageAction.mockReturnValue({ kind: 'quote', messageIds: ['msg-1', 'msg-2'] }); + mockUseQuotedMessageIds.mockReturnValue(['msg-1', 'msg-2']); mockGetDocumentAsync.mockResolvedValue({ canceled: false, assets: [{ name: 'legacy.pdf', size: 12, mimeType: 'application/pdf', uri: 'file:///tmp/legacy.pdf' }] @@ -153,6 +156,7 @@ describe('useChooseMedia', () => { it('does not quote the message when the action is edit', async () => { mockUseAltTextSupported.mockReturnValue(false); mockUseMessageAction.mockReturnValue({ kind: 'edit', messageId: 'msg-1' }); + mockUseQuotedMessageIds.mockReturnValue([]); mockGetDocumentAsync.mockResolvedValue({ canceled: false, assets: [{ name: 'legacy.pdf', size: 12, mimeType: 'application/pdf', uri: 'file:///tmp/legacy.pdf' }] diff --git a/app/containers/MessageComposer/hooks/useChooseMedia.ts b/app/containers/MessageComposer/hooks/useChooseMedia.ts index 6b4078e9c28..0f723e86421 100644 --- a/app/containers/MessageComposer/hooks/useChooseMedia.ts +++ b/app/containers/MessageComposer/hooks/useChooseMedia.ts @@ -9,8 +9,8 @@ import { getSubscriptionByRoomId } from '../../../lib/database/services/Subscrip 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 '../../message/stores/MessageActionStore'; +import { useGetText, useSetQuotesAndText } from '../../../views/RoomView/stores/ComposerStore'; +import { useMessageAction, useQuotedMessageIds } from '../../message/stores/MessageActionStore'; import { type IShareAttachment } from '../../../definitions'; import ImagePicker, { type ImageOrVideo } from '../../../lib/methods/helpers/ImagePicker/ImagePicker'; import { useMessageComposerApi } from '../context'; @@ -30,8 +30,10 @@ export const useChooseMedia = ({ }) => { const { FileUpload_MediaTypeWhiteList, FileUpload_MaxFileSize } = useAppSelector(state => state.settings); const { addAttachments } = useMessageComposerApi(); - const { setQuotesAndText, getText } = useRoomContext(); + const setQuotesAndText = useSetQuotesAndText(); + const getText = useGetText(); const action = useMessageAction(); + const quotedMessageIds = useQuotedMessageIds(); const altTextSupported = useAltTextSupported(); const allowList = FileUpload_MediaTypeWhiteList as string; const maxFileSize = FileUpload_MaxFileSize as number; @@ -94,9 +96,8 @@ export const useChooseMedia = ({ const startShareView = () => { const text = getText?.() || ''; - const selectedMessages = action?.kind === 'quote' ? action.messageIds : []; return { - selectedMessages, + selectedMessages: quotedMessageIds, text }; }; diff --git a/app/containers/MessageComposer/index.tsx b/app/containers/MessageComposer/index.tsx index 08eb216d93f..e4096f60c34 100644 --- a/app/containers/MessageComposer/index.tsx +++ b/app/containers/MessageComposer/index.tsx @@ -1,3 +1,2 @@ export * from './interfaces'; export * from './MessageComposerContainer'; -export { ComposerAttachments } from './components/Attachments/ComposerAttachments'; diff --git a/app/containers/UIKit/UiKitModal.stories.tsx b/app/containers/UIKit/UiKitModal.stories.tsx index adee3d2d256..74b60d5a771 100644 --- a/app/containers/UIKit/UiKitModal.stories.tsx +++ b/app/containers/UIKit/UiKitModal.stories.tsx @@ -11,7 +11,7 @@ export default { title: 'UIKit/UiKitModal', decorators: [ (Story: any) => ( - + diff --git a/app/containers/message/components/Blocks.ts b/app/containers/message/components/Blocks.ts index 2cd4cde05e2..023ff2fd002 100644 --- a/app/containers/message/components/Blocks.ts +++ b/app/containers/message/components/Blocks.ts @@ -1,7 +1,7 @@ import { createElement } from 'react'; import { messageBlockWithContext } from '../../UIKit/MessageBlock'; -import { useBlockAction, useRid } from '../stores/MessageRoomStore'; +import { useRid, useBlockAction } from '../stores/MessageRoomStore'; import { useBlocks } from '../stores/MessageStore'; const Blocks = () => { diff --git a/app/containers/message/components/MessageA11yIndex.tsx b/app/containers/message/components/MessageA11yIndex.tsx new file mode 100644 index 00000000000..ceea5979676 --- /dev/null +++ b/app/containers/message/components/MessageA11yIndex.tsx @@ -0,0 +1,42 @@ +import { type ReactElement, type ReactNode } from 'react'; +import { View, type StyleProp, type ViewStyle } from 'react-native'; +import { A11y } from 'react-native-a11y-order'; + +import { useA11yGate } from '../stores/A11yGate'; + +interface IMessageA11yIndexProps { + index: number; + accessible?: boolean; + accessibilityLabel?: string; + accessibilityLanguage?: string; + style?: StyleProp; + children: ReactNode; +} + +const MessageA11yIndex = ({ + index, + accessible, + accessibilityLabel, + accessibilityLanguage, + style, + children +}: IMessageA11yIndexProps): ReactElement => { + const enabled = useA11yGate(); + + if (!enabled) { + return style ? {children} : <>{children}; + } + + return ( + + {children} + + ); +}; + +export default MessageA11yIndex; diff --git a/app/containers/message/components/MessageA11yOrder.tsx b/app/containers/message/components/MessageA11yOrder.tsx new file mode 100644 index 00000000000..3ddfda0d60b --- /dev/null +++ b/app/containers/message/components/MessageA11yOrder.tsx @@ -0,0 +1,14 @@ +import { type ReactElement, type ReactNode } from 'react'; +import { A11y } from 'react-native-a11y-order'; + +import { useA11yGate } from '../stores/A11yGate'; + +const MessageA11yOrder = ({ children }: { children: ReactNode }): ReactElement => { + const enabled = useA11yGate(); + + if (!enabled) return <>{children}; + + return {children}; +}; + +export default MessageA11yOrder; diff --git a/app/containers/message/components/MessageAccessibleIndex.tsx b/app/containers/message/components/MessageAccessibleIndex.tsx index 497378683e8..39e4a49146b 100644 --- a/app/containers/message/components/MessageAccessibleIndex.tsx +++ b/app/containers/message/components/MessageAccessibleIndex.tsx @@ -1,23 +1,23 @@ 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'; +import MessageA11yIndex from './MessageA11yIndex'; const MessageAccessibleIndex = ({ style, children }: { style?: StyleProp; children: ReactNode }) => { const { messageText, isTranslated } = useMessageText(); const { autoTranslateLanguage } = useAutoTranslate(); return ( - {children} - + ); }; diff --git a/app/containers/message/components/Preview.tsx b/app/containers/message/components/Preview.tsx index d1707b488a6..ffa67879af0 100644 --- a/app/containers/message/components/Preview.tsx +++ b/app/containers/message/components/Preview.tsx @@ -1,24 +1,15 @@ -import { shallowEqual } from 'react-redux'; - import Message from '../index'; import { MessageRoomProvider } from '../stores/MessageRoomStore'; -import { useAppSelector } from '../../../lib/hooks/useAppSelector'; -import { getUserSelector } from '../../../selectors/login'; +import { A11yGateProvider } from '../stores/A11yGate'; import { type TAnyMessageModel } from '../../../definitions'; const MessagePreview = ({ message }: { message: TAnyMessageModel }) => { - const { user, baseUrl } = useAppSelector( - state => ({ - user: getUserSelector(state), - baseUrl: state.server.server - }), - shallowEqual - ); - return ( - - - + + + + + ); }; diff --git a/app/containers/message/components/Reactions.tsx b/app/containers/message/components/Reactions.tsx index 2e456f261fd..e18aeabfd09 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 { useMessageId, useMessageItem, useReactions } from '../stores/MessageStore'; -import { useMessageUser, useOnReactionLongPress, useOnReactionPress, useReactionInit } from '../stores/MessageRoomStore'; +import { useMessageUser, useReactionInit, useOnReactionPress, useOnReactionLongPress } from '../stores/MessageRoomStore'; interface IReaction { _id: string; diff --git a/app/containers/message/components/Thread.tsx b/app/containers/message/components/Thread.tsx index 5634ec68f13..e072112db72 100644 --- a/app/containers/message/components/Thread.tsx +++ b/app/containers/message/components/Thread.tsx @@ -13,10 +13,10 @@ const Thread = () => { const item = useMessageItem(); const isThreadRoom = useIsThreadRoom(); const threadBadgeColor = useThreadBadgeColor(); + const onThreadPress = useOnThreadPress(); const toggleFollowThread = useToggleFollowThread(); const user = useMessageUser(); const replies = useReplies(); - const onThreadPress = useOnThreadPress(); const { tcount, tlm, id } = useThreadData(); const { messageText } = useMessageText(); diff --git a/app/containers/message/components/Touchable/MessageTouchable.tsx b/app/containers/message/components/Touchable/MessageTouchable.tsx index 6af0cd90ee1..2f9fc52fee7 100644 --- a/app/containers/message/components/Touchable/MessageTouchable.tsx +++ b/app/containers/message/components/Touchable/MessageTouchable.tsx @@ -1,6 +1,6 @@ -import { A11y } from 'react-native-a11y-order'; - import { useTheme } from '../../../../theme'; +import MessageA11yOrder from '../MessageA11yOrder'; +import MessageA11yIndex from '../MessageA11yIndex'; import Touch from './Touch'; import Message, { type TMessageProps } from '../Message/Message'; import { useLastFocusedMessageRef } from '../../../../lib/a11y/useLastFocusedMessageRef'; @@ -10,7 +10,7 @@ import { useMessageAccessibilityHint } from '../../hooks/useMessageAccessibility import { useIsBeingEdited } from '../../stores/MessageActionStore'; import { useIsInfoMessage, - useMessageField, + useMessageId, useMessageLongPress, useMessagePress, useMessageStatus, @@ -23,7 +23,7 @@ const MessageTouchable = (props: TMessageProps) => { const isInfo = useIsInfoMessage(); const { hasError } = useMessageStatus(); const { tappable } = useMessageTouchable(); - const id = useMessageField(item => item.id); + const id = useMessageId(); const isBeingEdited = useIsBeingEdited(id); const onPressAction = useMessagePress(); const onLongPress = useMessageLongPress(); @@ -41,9 +41,9 @@ const MessageTouchable = (props: TMessageProps) => { if (hasError || isInfo) { return ( - + - + ); } @@ -53,8 +53,8 @@ const MessageTouchable = (props: TMessageProps) => { }; return ( - - + + { }}> - - + + ); }; diff --git a/app/containers/message/components/__tests__/Blocks.test.tsx b/app/containers/message/components/__tests__/Blocks.test.tsx index d4d82da3338..3d225f72785 100644 --- a/app/containers/message/components/__tests__/Blocks.test.tsx +++ b/app/containers/message/components/__tests__/Blocks.test.tsx @@ -56,7 +56,7 @@ describe('Blocks', () => { 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 }); + renderBlocks([{ appId: 'app-1' }] as TAnyMessageModel['blocks'], { handlers: { blockAction } }); const { action } = messageBlockWithContext.mock.calls[0][0]; await action({ actionId: 'submit', value: 'v', blockId: 'block-1' }); diff --git a/app/containers/message/components/__tests__/CollapsibleQuote.test.tsx b/app/containers/message/components/__tests__/CollapsibleQuote.test.tsx index c2f1a9ba02a..79549c29ffb 100644 --- a/app/containers/message/components/__tests__/CollapsibleQuote.test.tsx +++ b/app/containers/message/components/__tests__/CollapsibleQuote.test.tsx @@ -38,7 +38,6 @@ const initialMockedStoreState = () => { initialMockedStoreState(); const contextValue: Partial = { - user: { username: 'Marcos' }, timeFormat: 'LT' }; diff --git a/app/containers/message/components/__tests__/Content.test.tsx b/app/containers/message/components/__tests__/Content.test.tsx index 44f7f01f8b7..0bc07e0fead 100644 --- a/app/containers/message/components/__tests__/Content.test.tsx +++ b/app/containers/message/components/__tests__/Content.test.tsx @@ -53,8 +53,7 @@ const buildItem = ( const tree = (overrides: TOverrides) => { const { msg, attachments, isEncrypted, autoTranslateLanguage, tmid, isIgnored } = overrides; const contextValue: Partial = { - user: { username: 'john' }, - navToRoomInfo: jest.fn(), + handlers: { navToRoomInfo: jest.fn() }, autoTranslateLanguage }; return ( diff --git a/app/containers/message/components/__tests__/ImageContainer.test.tsx b/app/containers/message/components/__tests__/ImageContainer.test.tsx index 0483324a1c5..1a4ee89d7b9 100644 --- a/app/containers/message/components/__tests__/ImageContainer.test.tsx +++ b/app/containers/message/components/__tests__/ImageContainer.test.tsx @@ -36,10 +36,7 @@ jest.mock('../../../../lib/hooks/useAltTextSupported', () => ({ const renderImageContainer = (props?: Partial>) => { const id = 'message-id'; - const contextValue: Partial = { - baseUrl: 'https://open.rocket.chat', - user: { id: 'user-id', username: 'rocket.cat', token: 'token' } - }; + const contextValue: Partial = {}; const item = { id } as unknown as TAnyMessageModel; return render( diff --git a/app/containers/message/components/__tests__/JitsiLayout.test.tsx b/app/containers/message/components/__tests__/JitsiLayout.test.tsx index bfaae1594a8..7021d3ae203 100644 --- a/app/containers/message/components/__tests__/JitsiLayout.test.tsx +++ b/app/containers/message/components/__tests__/JitsiLayout.test.tsx @@ -3,7 +3,7 @@ import { render } from '@testing-library/react-native'; import JitsiLayout from '../Layout/JitsiLayout'; import { MessageProvider } from '../../stores/MessageStore'; -import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider } from '../../stores/MessageRoomStore'; import { mockedStore } from '../../../../reducers/mockedStore'; import { type TAnyMessageModel } from '../../../../definitions'; import I18n from '../../../../i18n'; @@ -15,18 +15,16 @@ const item = { u: { _id: 'author-id', username: 'diego.mello' } } as unknown as TAnyMessageModel; -const renderJitsiLayout = () => { - const room: Partial = { handleEnterCall: jest.fn() }; - return render( +const renderJitsiLayout = () => + render( - + ); -}; describe('JitsiLayout', () => { test('renders the localized "started a call" line AND the join button for a jitsi_call_started message', () => { diff --git a/app/containers/message/components/__tests__/MessageA11yGate.test.tsx b/app/containers/message/components/__tests__/MessageA11yGate.test.tsx new file mode 100644 index 00000000000..dd35d442cb9 --- /dev/null +++ b/app/containers/message/components/__tests__/MessageA11yGate.test.tsx @@ -0,0 +1,125 @@ +import { Text } from 'react-native'; +import { render } from '@testing-library/react-native'; + +import { type TAnyMessageModel } from '../../../../definitions'; +import { A11yGateContext } from '../../stores/A11yGate'; +import { type MessageRoomState } from '../../stores/MessageRoomStore'; +import MessageTouchable from '../Touchable/MessageTouchable'; +import MessageAccessibleIndex from '../MessageAccessibleIndex'; +import { MessageProviders } from '../../__tests__/testHelpers'; + +// Render the react-native-a11y-order Fabric views as plain testID-bearing Views so the gating +// seam is observable from the tree. This is the only thing mocked; the gate value is driven purely +// through A11yGateContext, never by mocking useIsAccessibilityNavigationEnabled. +jest.mock('react-native-a11y-order', () => { + const { View: RNView } = require('react-native'); + return { + A11y: { + Order: ({ children, ...props }: any) => ( + + {children} + + ), + Index: ({ children, index, ...props }: any) => ( + + {children} + + ) + } + }; +}); + +// Touch pulls in gesture-handler internals irrelevant to the a11y seam; render a plain wrapper instead. +jest.mock('../Touchable/Touch', () => { + const { forwardRef } = require('react'); + const { View: RNView } = require('react-native'); + return forwardRef(({ children, ...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' }, + groupable: true, + experimentalSubscribe: jest.fn(() => jest.fn()), + ...overrides + }) as unknown as TAnyMessageModel; + +const room: Partial = { rid: 'room-1' }; + +const renderTouchable = (gate: boolean, itemOverrides: Record = {}) => + render( + + + + + + ); + +describe('per-row a11y wrapper gating', () => { + describe('MessageTouchable — normal message', () => { + it('gate false: no A11y.Order / A11y.Index, message still renders', () => { + const { queryByTestId } = renderTouchable(false); + expect(queryByTestId('a11y-order')).toBeNull(); + expect(queryByTestId('a11y-index-1')).toBeNull(); + expect(queryByTestId('message-msg-1')).toBeTruthy(); + }); + + it('gate true: wraps in A11y.Order and A11y.Index index=1', () => { + const { getByTestId } = renderTouchable(true); + expect(getByTestId('a11y-order')).toBeTruthy(); + expect(getByTestId('a11y-index-1')).toBeTruthy(); + expect(getByTestId('message-msg-1')).toBeTruthy(); + }); + }); + + describe('MessageTouchable — info message (Order-only branch)', () => { + it('gate true: A11y.Order present, no A11y.Index', () => { + const { getByTestId, queryByTestId } = renderTouchable(true, { t: 'uj' }); + expect(getByTestId('a11y-order')).toBeTruthy(); + expect(queryByTestId('a11y-index-1')).toBeNull(); + }); + }); + + describe('MessageAccessibleIndex — index=2', () => { + const renderIndex = (gate: boolean, style?: object) => + render( + + + + child + + + + ); + + it('gate false with style: plain View keeps the style, no A11y.Index', () => { + const { queryByTestId, getByTestId, toJSON } = renderIndex(false, { flex: 1 }); + expect(queryByTestId('a11y-index-2')).toBeNull(); + expect(getByTestId('index-child')).toBeTruthy(); + expect(toJSON()).toMatchObject({ type: 'View', props: { style: { flex: 1 } } }); + }); + + it('gate false unstyled: child renders directly, no wrapper', () => { + const { queryByTestId, getByTestId } = renderIndex(false); + expect(queryByTestId('a11y-index-2')).toBeNull(); + expect(getByTestId('index-child')).toBeTruthy(); + }); + + it('gate true: A11y.Index index=2 forwards translation-aware props and style', () => { + const { getByTestId } = renderIndex(true, { flex: 1 }); + const index = getByTestId('a11y-index-2'); + expect(index.props.accessibilityLanguage).toBe('pt-BR'); + expect(index.props.style).toEqual({ flex: 1 }); + }); + }); +}); diff --git a/app/containers/message/components/__tests__/Quote.test.tsx b/app/containers/message/components/__tests__/Quote.test.tsx index fae5f9cbd29..36b1f5b8364 100644 --- a/app/containers/message/components/__tests__/Quote.test.tsx +++ b/app/containers/message/components/__tests__/Quote.test.tsx @@ -45,8 +45,6 @@ const buildItem = () => ({ id: 'msg-1' }) as unknown as TAnyMessageModel; const renderQuote = (attachments: IAttachment[], ctx: Partial = {}) => { const contextValue: Partial = { - user: { id: 'user-1', username: 'john', token: 'token' }, - baseUrl: 'https://open.rocket.chat', timeFormat: 'HH:mm', ...ctx }; @@ -79,10 +77,7 @@ describe('Quote', () => { rerender( - + diff --git a/app/containers/message/components/__tests__/Reactions.test.tsx b/app/containers/message/components/__tests__/Reactions.test.tsx index 33ad050df72..3fecfb0252d 100644 --- a/app/containers/message/components/__tests__/Reactions.test.tsx +++ b/app/containers/message/components/__tests__/Reactions.test.tsx @@ -4,7 +4,7 @@ import Reactions from '../Reactions'; import { MessageProviders } from '../../__tests__/testHelpers'; import { setUser } from '../../../../actions/login'; import { mockedStore } from '../../../../reducers/mockedStore'; -import { type IReaction, type TAnyMessageModel } from '../../../../definitions'; +import { type IReaction, type IUseRoomMessageHandlersResult, type TAnyMessageModel } from '../../../../definitions'; const initialMockedStoreState = () => { mockedStore.dispatch( @@ -53,10 +53,11 @@ const TestWrapper = ({ item, onReactionPress }: { item: FakeItem; onReactionPres diff --git a/app/containers/message/components/__tests__/RepliedThread.test.tsx b/app/containers/message/components/__tests__/RepliedThread.test.tsx index eb98f724ab4..e462abe34f0 100644 --- a/app/containers/message/components/__tests__/RepliedThread.test.tsx +++ b/app/containers/message/components/__tests__/RepliedThread.test.tsx @@ -3,7 +3,7 @@ import { Provider } from 'react-redux'; import RepliedThread from '../RepliedThread'; import { MessageProvider } from '../../stores/MessageStore'; -import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; +import { MessageRoomProvider } from '../../stores/MessageRoomStore'; import { type TAnyMessageModel } from '../../../../definitions'; import { mockedStore } from '../../../../reducers/mockedStore'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../../../lib/constants/keys'; @@ -11,10 +11,10 @@ import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../../../lib/constants/keys'; const buildItem = (overrides: Partial = {}): TAnyMessageModel => ({ id: 'msg1', tmid: 'thread1', tmsg: 'original reply', ...overrides }) as unknown as TAnyMessageModel; -const renderRepliedThread = (item: TAnyMessageModel, roomState: Partial = {}) => +const renderRepliedThread = (item: TAnyMessageModel, fetchThreadName?: jest.Mock) => render( - + @@ -34,7 +34,7 @@ describe('RepliedThread', () => { 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 }); + const { getByTestId } = renderRepliedThread(item, fetchThreadName); await waitFor(() => expect(getByTestId('message-thread-replied-on-fetched thread name')).toBeTruthy()); expect(fetchThreadName).toHaveBeenCalledWith('thread1', 'msg1'); @@ -43,14 +43,14 @@ describe('RepliedThread', () => { 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 }); + 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( - + diff --git a/app/containers/message/components/__tests__/Reply.test.tsx b/app/containers/message/components/__tests__/Reply.test.tsx index a6081a0a96e..c60aa0eda76 100644 --- a/app/containers/message/components/__tests__/Reply.test.tsx +++ b/app/containers/message/components/__tests__/Reply.test.tsx @@ -5,6 +5,8 @@ import Reply from '../Attachments/Reply'; import { MessageProvider } from '../../stores/MessageStore'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { mockedStore } from '../../../../reducers/mockedStore'; +import { setUser } from '../../../../actions/login'; +import { selectServerSuccess } from '../../../../actions/server'; import { type IAttachment, type TAnyMessageModel } from '../../../../definitions'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../../../lib/constants/keys'; import { fileDownloadAndPreview } from '../../../../lib/methods/helpers/fileDownload'; @@ -62,8 +64,6 @@ const renderReply = ({ ctx?: Partial; }) => { const contextValue: Partial = { - user: { id: 'user-1', username: 'john', token: 'token' }, - baseUrl: 'https://open.rocket.chat', timeFormat: 'HH:mm', ...ctx }; @@ -78,6 +78,9 @@ const renderReply = ({ ); }; +mockedStore.dispatch(setUser({ id: 'user-1', username: 'john', token: 'token' })); +mockedStore.dispatch(selectServerSuccess({ server: 'https://open.rocket.chat', version: '', name: '' })); + describe('Reply', () => { beforeEach(() => { jest.clearAllMocks(); diff --git a/app/containers/message/components/__tests__/Thread.test.tsx b/app/containers/message/components/__tests__/Thread.test.tsx index 21160e3b49a..5259a942397 100644 --- a/app/containers/message/components/__tests__/Thread.test.tsx +++ b/app/containers/message/components/__tests__/Thread.test.tsx @@ -8,9 +8,7 @@ import { type TAnyMessageModel } from '../../../../definitions'; import { mockedStore } from '../../../../reducers/mockedStore'; const baseContextValue: Partial = { - toggleFollowThread: jest.fn(), - user: { id: 'user1', username: 'user1' }, - onThreadPress: jest.fn() + handlers: { toggleFollowThread: jest.fn(), onThreadPress: jest.fn() } }; const buildItem = (overrides: Partial = {}): TAnyMessageModel => 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 b10d8347923..a3436816837 100644 --- a/app/containers/message/components/__tests__/__snapshots__/Message.test.tsx.snap +++ b/app/containers/message/components/__tests__/__snapshots__/Message.test.tsx.snap @@ -9,468 +9,448 @@ exports[`Story Snapshots: Archived should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello - - - 10:00 AM + This message is inside an archived room - - - - - - - - - - This message is inside an archived room - - - - - - + + - + - - - + + + `; @@ -484,468 +464,448 @@ exports[`Story Snapshots: ArchivedLargeFont should match snapshot 1`] = ` } > - - - + - - + - - + + - - - - - - - + } + transition={null} + width={46.800000000000004} + /> + + + + + + + + + + diego.mello + + + + 10:00 AM + + + - - + - diego.mello + This message is inside an archived room - - - - - 10:00 AM + - - - - - - - This message is inside an archived room - - - - - - - + - - - + + + `; @@ -959,7366 +919,6665 @@ exports[`Story Snapshots: AttachmentWithTextAndLink should match snapshot 1`] = } > - - - + + + + + + + + + + + + + - - - + diego.mello + + + 10:00 AM + + + + + + + - + + - - + + Rocket.Chat + + + + > + + + + + + Rocket.Chat + + + + + , the best open source chat + + + + - - + + + - + - diego.mello - + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > - 10:00 AM +  - + + + + + + + + + + + +`; + +exports[`Story Snapshots: AttachmentWithTextAndLinkLargeFont should match snapshot 1`] = ` + + + + + + + + + + + + + + - - - - - - - - Rocket.Chat - - - - - - - - - Rocket.Chat - - - - - , the best open source chat - - - - - - - - - - - - - - - - -  - - - - - - - + transition={null} + width={46.800000000000004} + /> - - - + + + - - - - - - - -`; - -exports[`Story Snapshots: AttachmentWithTextAndLinkLargeFont should match snapshot 1`] = ` - - - - - - - - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - + - - diego.mello - - - - - - 10:00 AM - - - - + + Rocket.Chat + + + - - - - - Rocket.Chat - - - - - - - Rocket.Chat - - - - - , the best open source chat - + Rocket.Chat - - - - - - - - - - - - - + -  + { + "color": "#2F343D", + }, + ] + } + > + , the best open source chat + - - + - - - - - - - - - - -`; - -exports[`Story Snapshots: Avatar should match snapshot 1`] = ` - - - - - - - - - - - + - - - - + - - - - - - - - + /> - - no.avatar - + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > - 10:00 AM +  - - - - - - - This message has no avatar. - - - - - - - + - - - - + + + + +`; + +exports[`Story Snapshots: Avatar should match snapshot 1`] = ` + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + no.avatar + + + 10:00 AM + + + + - - + - diego.mello - - - 10:00 AM + This message has no avatar. - - - - - - - - - - This message uses the user's profile picture as the avatar. - - - - - - + + - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - 😄 - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello - - - 10:00 AM + This message uses the user's profile picture as the avatar. - - - - - - - - - - This message uses a Unicode emoji as the avatar. - - - - - - + + - + - - - - + + + - - + + + + + + + + 😄 + + + + + + + - - - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello - - - 10:00 AM + This message uses a Unicode emoji as the avatar. - - - - - - - - - - This message uses a custom animated emoji as the avatar. - - - - - - + + - + - - - - + + + - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello - - - 10:00 AM + This message uses a custom animated emoji as the avatar. - - - - - - - - - - This message uses a custom static emoji as the avatar. - - - - - - + + - + - - - - - -`; - -exports[`Story Snapshots: Basic should match snapshot 1`] = ` - - - + + + - - + - - + - - + + - + - - - - - - + } + transition={null} + width={36} + /> + + + + + + + + + + + diego.mello + + + 10:00 AM + + + + - - + - diego.mello - - - 10:00 AM + This message uses a custom static emoji as the avatar. - - - - - - - - - - Message - - - - - - + + - + - - - - + + + + +`; + +exports[`Story Snapshots: Basic should match snapshot 1`] = ` + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello - - - 10:00 AM + Message - - - - - - - - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - - - - - - + + - + - - - - - -`; - -exports[`Story Snapshots: BasicLargeFont should match snapshot 1`] = ` - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - - - - - 10:00 AM + - - - - - - - Message - - - - - - - + - - - - + + + + +`; + +exports[`Story Snapshots: BasicLargeFont should match snapshot 1`] = ` + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - + - diego.mello + Message - - - - - 10:00 AM + - - - - - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - - - - - - - + - - - - - -`; - -exports[`Story Snapshots: BlockQuote should match snapshot 1`] = ` - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - + - diego.mello - - - 10:00 AM + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - - - - - - - - - - - - - Testing block quote - - - - - - - - + + - + - - - - + + + + +`; + +exports[`Story Snapshots: BlockQuote should match snapshot 1`] = ` + + + - - + - + + + + + + + + + + + + - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + + - diego.mello - - - 10:00 AM - - - - - - - - - - - - - - Testing block quote - - - - - - - - - Testing block quote - + Testing block quote - + - + - - - - - -`; - -exports[`Story Snapshots: BlockQuoteLargeFont should match snapshot 1`] = ` - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + + - diego.mello + + + Testing block quote + + - - 10:00 AM - - - - - - - - - - - Testing block quote - - - - - - - - + Testing block quote + + + - + - - - - + + + + +`; + +exports[`Story Snapshots: BlockQuoteLargeFont should match snapshot 1`] = ` + + + - - + - - + - - + + - - - - - - - + } + transition={null} + width={46.800000000000004} + /> + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + - diego.mello - - - - - - 10:00 AM - - - - - - - - - - - Testing block quote - - - - - - - - - Testing block quote - + Testing block quote - + - + - - - - - -`; - -exports[`Story Snapshots: Broadcast should match snapshot 1`] = ` - - - + + + - - + - - + - - + + - - - - - - - + } + transition={null} + width={46.800000000000004} + /> + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + - diego.mello - - - 10:00 AM - - - - - - - - - - Broadcasted message - + Testing block quote - + - + + - - - -  - - - Reply - - - - - + Testing block quote + + + - + - - - + + + `; -exports[`Story Snapshots: BroadcastLargeFont should match snapshot 1`] = ` +exports[`Story Snapshots: Broadcast should match snapshot 1`] = ` - - - + - - + - - + + - - - - - - - + } + transition={null} + width={36} + /> + + + + + + + + + + diego.mello + + + 10:00 AM + + + + - - - - diego.mello - - - - - 10:00 AM - - - - - - - - Broadcasted message - - - - - - - - - -  - - - Reply - - - - - + Broadcasted message + + + - - - - - - - - - - -`; - -exports[`Story Snapshots: CollapsedAttachments should match snapshot 1`] = ` - - - - - - - - - - - + - - + - - - - - - - - - - - - - diego.mello - - - 10:00 AM - - - - - - - - - - - Message - - - - - - - - - - - - Title collapsed - - - - - -  - - - - - + Reply + - + - - - - + + + + +`; + +exports[`Story Snapshots: BroadcastLargeFont should match snapshot 1`] = ` + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - + - diego.mello + Broadcasted message - + + + + + + + - 10:00 AM - - - +  + + + > + Reply + - + + + + + + + + + + + +`; + +exports[`Story Snapshots: CollapsedAttachments should match snapshot 1`] = ` + + + + + + + + + + + + + + - + + + + + + + + + + + diego.mello + + + 10:00 AM + + + + + + + + - - + + - - - - - Title collapsed - - - - - - - Title collapsed - - - - - - - - Field 1 - - - - - - Value 1 - - - - - - - - Field 2 - - - - - - Value 2 - - - - - - - - - - - + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + ] + } + > + Message + + + + + + + + @@ -11659,1302 +10399,2161 @@ exports[`Story Snapshots: CollapsedAttachments should match snapshot 1`] = ` "fontVariant": [ "tabular-nums", ], - "fontWeight": "400", - "lineHeight": 22, + "fontWeight": "500", "textAlign": "left", }, { - "color": "#2F343D", + "color": "#6C727A", }, ] } > - - - Message - - + Title collapsed + + +  + + - + - - - - - -`; - -exports[`Story Snapshots: CollapsedAttachmentsLargeFont should match snapshot 1`] = ` - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - - diego.mello - - - - - - 10:00 AM - - - - - Message - + Title collapsed - - - - - + - - Title collapsed - - + + + + -  + Field 1 - - - - - - - - - - - - - - - - - - - + + + + Value 1 + + + + + + + + Field 2 + + + + + + Value 2 + + + + + + + + + + + + + + + + + Message + + + + + + + + + + + + + + + +`; + +exports[`Story Snapshots: CollapsedAttachmentsLargeFont should match snapshot 1`] = ` + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - - diego.mello + Message + + + + + + + + + + + Title collapsed + + + > + +  + + - + + + + + + + + + + + + + + + + + + + + + - 10:00 AM - - + + + + + + + + + + + diego.mello + + + + + + 10:00 AM + + + + + + + Title collapsed + + + - - - - - Title collapsed - - - + + + + + + + Field 1 + + - - - - Title collapsed - - - - - + - - Field 1 + Value 1 - - - - - Value 1 - - - - - - + + + + + + Field 2 + + + + - Field 2 + Value 2 - - - - - Value 2 - - - - - - + + - + + + + + + - - - - - Message - - - - - - + Message + + + - + - - - + + + `; @@ -13217,407 +12755,537 @@ exports[`Story Snapshots: CollapsibleAttachmentWithText should match snapshot 1` } > - - - + - - + - + + + + + + + + + + + + - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello - - - 10:00 AM + This is the main message body. - - - + + + + + + @@ -13631,821 +13299,712 @@ exports[`Story Snapshots: CollapsibleAttachmentWithText should match snapshot 1` "fontVariant": [ "tabular-nums", ], - "fontWeight": "400", - "lineHeight": 22, + "fontWeight": "500", "textAlign": "left", }, { - "color": "#2F343D", + "color": "#6C727A", }, ] } > - - - This is the main message body. - - + Collapsed attachment block - - - - - - Collapsed attachment block - - - - - -  - - - - +  + - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - - diego.mello - - - 10:00 AM - - - - - + + Collapsed attachment block + + + + + + + This attachment text should NOT appear as plain text above the message or duplicate before the block. + + + + + + + Field 1 + - - Collapsed attachment block - - - - - - - This attachment text should NOT appear as plain text above the message or duplicate before the block. - - - - - - - Field 1 + Value 1 - - - - - Value 1 - - - - - - + + + + + + Field 2 + + + + - - Field 2 - - - - - - Value 2 - - - - - - - Long field + Value 2 - - - - - This field value could also contribute to duplicate text when expanded. - - - - - - + + - - - + Long field + + - Attachment description that might also leak as duplicate plain text. + + + This field value could also contribute to duplicate text when expanded. + + - - + + - + + - - - - This is the main message body. - - + Attachment description that might also leak as duplicate plain text. - - + + + + + + + + This is the main message body. + + + + + - + - - - + + + `; @@ -14864,407 +14362,537 @@ exports[`Story Snapshots: CollapsibleAttachmentWithTextLargeFont should match sn } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - + - diego.mello + This is the main message body. - - - - - 10:00 AM + + + + + @@ -15278,916 +14906,712 @@ exports[`Story Snapshots: CollapsibleAttachmentWithTextLargeFont should match sn "fontVariant": [ "tabular-nums", ], - "fontWeight": "400", - "lineHeight": 22, + "fontWeight": "500", "textAlign": "left", }, { - "color": "#2F343D", + "color": "#6C727A", }, ] } > - - - This is the main message body. - - + Collapsed attachment block - - - - - - Collapsed attachment block - - - - - -  - - - - +  + - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - - diego.mello - - - - - - 10:00 AM - - + + Collapsed attachment block + + + - - - - - Collapsed attachment block - - - + + + + + + + Field 1 + + - - - - This attachment text should NOT appear as plain text above the message or duplicate before the block. - - - - - + - - - Field 1 - - - - - - Value 1 - - - - - - - Field 2 + Value 1 - - - - - Value 2 - - - - - - + + + + + + Field 2 + + + + - Long field + Value 2 - - - - - This field value could also contribute to duplicate text when expanded. - - - - - - + + - - - + Long field + + - Attachment description that might also leak as duplicate plain text. - - - - + + + This field value could also contribute to duplicate text when expanded. + + + + + + - + + - - - - This is the main message body. - - + Attachment description that might also leak as duplicate plain text. - - + + + + + + + + This is the main message body. + + + + + - + - - - + + + `; @@ -16511,524 +15969,565 @@ exports[`Story Snapshots: ColoredAttachments should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - + - - diego.mello - - - 10:00 AM - - - - - - - - + + + Field 1 + - - - - Field 1 + Value 1 - - - - - Value 1 - - - - - - + + + + + + Field 2 + + + + - Field 2 + Value 2 - - - - - Value 2 - - - - - - + + + + + + + + + + + Field 1 + - - - - Field 1 + Value 1 - - - - - Value 1 - - - - - - + + + + + + Field 2 + + + + - Field 2 + Value 2 - - - - - Value 2 - - - - - - + + + + + + + + + + + Field 1 + - - - - Field 1 + Value 1 - - - - - Value 1 - - - - - - + + + + + + Field 2 + + + + - Field 2 + Value 2 - - - - - Value 2 - - - - - - + + @@ -17783,13 +17221,13 @@ exports[`Story Snapshots: ColoredAttachments should match snapshot 1`] = ` - + - - - + + + `; @@ -17803,524 +17241,565 @@ exports[`Story Snapshots: ColoredAttachmentsLargeFont should match snapshot 1`] } > - - - + - - + + + + + + + + + + + + + - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - - diego.mello - - - - - - 10:00 AM - - + + + Field 1 + - - - - Field 1 + Value 1 - + + + + + + Field 2 + + + + + - - - - Value 1 - - - - - - - - Field 2 + Value 2 - - - - - Value 2 - - - - - - + + + + + + + + + + + Field 1 + - - - - Field 1 + Value 1 - - - - - Value 1 - - - - - - + + + + + + Field 2 + + + + - Field 2 + Value 2 - - - - - Value 2 - - - - - - + + + + + + + + + - - + Field 1 + + - + - - Field 1 + Value 1 - - - - - Value 1 - - - - - - + + + + + + Field 2 + + + + - Field 2 + Value 2 - - - - - Value 2 - - - - - - + + @@ -19075,13 +18493,13 @@ exports[`Story Snapshots: ColoredAttachmentsLargeFont should match snapshot 1`] - + - - - + + + `; @@ -19095,609 +18513,745 @@ exports[`Story Snapshots: CustomFields should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - + diego.mello + + - + 10:00 AM + + + + + + + + - - - + - - - - - - - - - - - diego.mello - - - 10:00 AM - - - - - - - + > + rocket.cat + + + + + + Custom fields + + + + + + + Field 1 + - - - rocket.cat - - - - - Custom fields - + Value 1 - - + + + + + Field 2 + + + - - Field 1 + Value 2 - - - - - Value 1 - - - - - - - - Field 2 - - - - - - Value 2 - - - - - - + + - + + + + + + - - - - - Message - - - - - - + Message + + + - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - + - - diego.mello - - - 10:00 AM - - - - - - - + + rocket.cat + + + + + + + Custom fields + + + + + + + Field 1 + - - - rocket.cat - - - - - Custom fields - + Value 1 - - + + + + + Field 2 + + + - - Field 1 + Value 2 - - - - - Value 1 - - - - - - + + + + + + Field 3 + + + + - Field 2 + Value 3 - - - - - Value 2 - - - - - - + + + + + + Field 4 + + + + - - Field 3 - - - - - - Value 3 - - - - - - - Field 4 + Value 4 - - - - - Value 4 - - - - - - + + + + + + Field 5 + + + + - Field 5 + Value 5 - - - - - Value 5 - - - - - - + + - + + + + + + - - - - - Message - - - - - - + Message + + + - + - - - + + + `; @@ -21097,609 +20475,650 @@ exports[`Story Snapshots: CustomFieldsLargeFont should match snapshot 1`] = ` } > - - - - + + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - - diego.mello - - - - - - 10:00 AM - - + + rocket.cat + + + + + + + Custom fields + + + + + + + Field 1 + - - rocket.cat - - - - - - - Custom fields - - - - - - - Field 1 + Value 1 - - - - - Value 1 - - - - - - + + + + + + Field 2 + + + + - Field 2 + Value 2 - - - - - Value 2 - - - - - - + + - + + + + + + - - - - - Message - - - - - - + Message + + + - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - - diego.mello - - - - - - 10:00 AM - - + + rocket.cat + + + + + + + Custom fields + + + + + + + Field 1 + - - - rocket.cat - - - - - - Custom fields - - - - - - - Field 1 + Value 1 - - - - - Value 1 - - - - - - + + + + + + Field 2 + + + + - Field 2 + Value 2 - - - - - Value 2 - - - - - - + + + + + + Field 3 + + + + - Field 3 + Value 3 - - - - - Value 3 - - - - - - + + + + + + Field 4 + + + + - Field 4 + Value 4 - - - - - Value 4 - - - - - - + + + + + + Field 5 + + + + - Field 5 + Value 5 - - - - - Value 5 - - - - - - + + - + + + + + + - - - - - Message - - - - - - + Message + + + - + - - - + + + `; @@ -23099,476 +22437,456 @@ exports[`Story Snapshots: DateAndUnreadSeparators should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + rocket.cat + + + 10:00 AM + + + + - - + - rocket.cat - - - 10:00 AM + Fourth message - - - - - - - - - - Fourth message - - - - - - + + - + - - - + + + - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello - - - 10:00 AM + Third message - - - - - - - - - - Third message - - - - - - + + - + - - - + + + - - - + + - - - - - - - - - Second message - - - - - - + Second message + + + - - + + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + rocket.cat + + + 10:00 AM + + + + - - + - rocket.cat - - - 10:00 AM + Second message - - - - - - - - - - Second message - - - - - - + + - + - - - + + + - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello - - - 10:00 AM + First message - - - - - - - - - - First message - - - - - - + + - + - - - + + + `; @@ -25399,476 +24637,456 @@ exports[`Story Snapshots: DateAndUnreadSeparatorsLargeFont should match snapshot } > - - - + - - + - + + + + + + + + + + + + - - - - - - - - - + > + rocket.cat + + + + 10:00 AM + + + - - + - rocket.cat + Fourth message - - - - - 10:00 AM + - - - - - - - Fourth message - - - - - - - + - - - + + + - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - + - diego.mello + Third message - - - - - 10:00 AM + - - - - - - - Third message - - - - - - - + - - - + + + - - - + + - - - - - - - - - Second message - - - - - - + Second message + + + - - + + - - - - + + + - - + - - + - - + + - - - - - - - + } + transition={null} + width={46.800000000000004} + /> + + + + + + + + + + rocket.cat + + + + 10:00 AM + + + - - + - rocket.cat + Second message - - - - - 10:00 AM + - - - - - - - Second message - - - - - - - + - - - + + + - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - + - diego.mello + First message - - - - - 10:00 AM + - - - - - - - First message - - - - - - - + - - - + + + `; @@ -27699,421 +26837,528 @@ exports[`Story Snapshots: Discussion should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + - + + + - - - - diego.mello - - - 10:00 AM - - - - + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontStyle": "italic", + "fontVariant": [ + "tabular-nums", + ], + "fontWeight": "400", + "lineHeight": 20, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + > + Started a discussion: + + + This is a discussion + + + - Started a discussion: +  - This is a discussion + No messages yet - - - - -  - - - No messages yet - - - - - + - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + + Started a discussion: + + + This is a discussion + + - - - - diego.mello - - - 10:00 AM - - - - + "justifyContent": "center", + "paddingHorizontal": 12, + "paddingVertical": 8, + }, + { + "backgroundColor": "#1D74F5", + }, + ] + } + > - Started a discussion: +  - This is a discussion + 1 message - - - - -  - - - 1 message - - - - - November 10, 2017 - - + + November 10, 2017 + - + - - - - + + + - - + - - + - - + + - - - - - - - + } + transition={null} + width={36} + /> + + + + + + + + + + diego.mello + + + 10:00 AM + + + + + Started a discussion: + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + - - - - diego.mello - - - 10:00 AM - - - - + "justifyContent": "center", + "paddingHorizontal": 12, + "paddingVertical": 8, + }, + { + "backgroundColor": "#1D74F5", + }, + ] + } + > - Started a discussion: +  - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + 10 messages - - - - -  - - - 10 messages - - - - - November 10, 2017 - - + + November 10, 2017 + - + - - - - + + + - - + - + + + + + + + + + + + + - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + + Started a discussion: + + + This is a discussion + + - - - - diego.mello - - - 10:00 AM - - - - + "justifyContent": "center", + "paddingHorizontal": 12, + "paddingVertical": 8, + }, + { + "backgroundColor": "#1D74F5", + }, + ] + } + > - Started a discussion: +  - This is a discussion + +999 messages - - - - -  - - - +999 messages - - - - - November 10, 2017 - - + + November 10, 2017 + - + - - - + + + `; @@ -30102,421 +29160,528 @@ exports[`Story Snapshots: DiscussionLargeFont should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + + Started a discussion: + + + This is a discussion + + - - - - diego.mello - - - - - - 10:00 AM - + "justifyContent": "center", + "paddingHorizontal": 12, + "paddingVertical": 8, + }, + { + "backgroundColor": "#1D74F5", + }, + ] + } + > - Started a discussion: +  - This is a discussion + No messages yet - - - - -  - - - No messages yet - - - - - + - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + + Started a discussion: + + + This is a discussion + + - - - diego.mello - - - - - - 10:00 AM - - - - Started a discussion: +  - This is a discussion + 1 message - - - - -  - - - 1 message - - - - - November 10, 2017 - - + + November 10, 2017 + - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + + Started a discussion: + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + - - - diego.mello - - - - - - 10:00 AM - - - Started a discussion: +  - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + 10 messages - - - - -  - - - 10 messages - - - - - November 10, 2017 - - + + November 10, 2017 + - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - + - - - - diego.mello - - - - - + This is a discussion + + + - 10:00 AM - + } + 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", + }, + ] + } + > - Started a discussion: +  - This is a discussion + +999 messages - - - - -  - - - +999 messages - - - - - November 10, 2017 - - + + November 10, 2017 + - + - - - + + + `; @@ -32505,780 +31483,740 @@ exports[`Story Snapshots: Edited should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - + diego.mello + + - + 10:00 AM + + + + + - - - - - - - + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + - + + - - + - diego.mello - - - 10:00 AM + Message header - - - - -  - - - - - - - - - - - Message header - - - - - - + + - + - - - - + + + - - + + - - - - - - - - - - Message without header - - - - - - - - - -  + + + Message without header + + - + + + + + +  + + - - - + + + `; @@ -33292,780 +32230,740 @@ exports[`Story Snapshots: EditedLargeFont should match snapshot 1`] = ` } > - - - - + + + + + + + + + + + + + + - - - + diego.mello + + + + + - - - - - - - + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + - + + 10:00 AM + + + - - + - diego.mello + Message header - - - - -  - - - - - - 10:00 AM + - - - - - - - Message header - - - - - - - + - - - - + + + - - + + - - - - - - - - - - Message without header - - - - - - - - - -  + + + Message without header + + - + + + + + +  + + - - - + + + `; @@ -34079,477 +32977,457 @@ exports[`Story Snapshots: Editing should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello + Message being edited - - 10:00 AM - - - - - - - - - - - Message being edited - - - - - - + + - + - - - + + + `; @@ -34563,477 +33441,457 @@ exports[`Story Snapshots: EditingLargeFont should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - + - diego.mello + Message being edited - - - - - 10:00 AM + - - - - - - - Message being edited - - - - - - - + - - - + + + `; @@ -35047,2975 +33905,2855 @@ exports[`Story Snapshots: Emojis should match snapshot 1`] = ` } > - - - - + + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - - diego.mello - - - 10:00 AM - - - + - - - - - - - 👊 - - - 🤙👏 - - - - + 🤙👏 + - + - - - - + + + - - + - - + - - + + - - - - - - - + } + transition={null} + width={36} + /> + + + + + + + + + + diego.mello + + + 10:00 AM + + + + - - - diego.mello - - - 10:00 AM - - - - - - - - - - 👏 - - - - + 👏 + - + - - - - + + + - - + + + + + + + + + + + + + - - + diego.mello + + - + 10:00 AM + + + + + + + + - - - - - - - - - - - - - + + - - diego.mello - - - 10:00 AM - + transition={null} + width={30} + /> - - - - - - - - - - - - - - - - + } + transition={null} + width={30} + /> - + - - - - + + + - - + + + + + + + + + + + + + - - + diego.mello + + - + 10:00 AM + + + + + + + + - - - - - - - - - - - - - - - diego.mello - - - 10:00 AM - - - - - - - - - - - - - + transition={null} + width={30} + /> - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - + 🤙 + + - - diego.mello - - - 10:00 AM - - - - - - - - - - 🤙 - - - - - - + transition={null} + width={30} + /> - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + + 🤙 + + - diego.mello + :react_rocket: - 10:00 AM + 🤙 - - - - - - - - - - 🤙 - - - :react_rocket: - - - 🤙 - - - 🤙 - - - - - - + 🤙 + + + - + - - - + + + `; @@ -38029,2975 +36767,2855 @@ exports[`Story Snapshots: EmojisLargeFont should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - - diego.mello - - - + - - - 10:00 AM - - - - - - - 👊 - - - 🤙👏 - - - - + 🤙👏 + - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - - diego.mello - - - - - - 10:00 AM - - - - - - - 👏 - - - - + 👏 + - + - - - - + + + - - + - - + - - + + - - - - - - - + } + transition={null} + width={46.800000000000004} + /> + + + + + + + + + + diego.mello + + + + 10:00 AM + + + + + + - - diego.mello - + style={ + { + "height": 30, + "width": 30, + } + } + transition={null} + width={30} + /> - - - 10:00 AM - - - - - - - - - - - - - - - - + } + transition={null} + width={30} + /> - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - diego.mello - - - - - - 10:00 AM - - - - - - - - - - + transition={null} + width={30} + /> - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + + + 🤙 + - - diego.mello - - - - - - 10:00 AM - - - - - - - 🤙 - - - - - - + transition={null} + width={30} + /> - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - + + 🤙 + + - diego.mello + :react_rocket: - - - - - 10:00 AM - - - - + 🤙 + + - - - - 🤙 - - - :react_rocket: - - - 🤙 - - - 🤙 - - - - - - + 🤙 + + + - + - - - + + + `; @@ -41011,688 +39629,1096 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + - - - - diego.mello - - - 10:00 AM - - - - - -  - - - - - - - - Encrypted message - - - +  + - + + + + Encrypted message + + + + - - - - + + + - - + + + + + + + + + Encrypted message + + + + + + + + +  + + + + + + + + + + + + + + + + + + + + + + + + - - - - + - - - Encrypted message - - - - + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 13, + "fontVariant": [ + "tabular-nums", + ], + "fontWeight": "400", + "lineHeight": 18, + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + > + 10:00 AM + - - - - - - - - - - - - - - - + Encrypted message + + + + - + - + 😂 + + - - - - - - + 1 + + - - - + + 1 + + + + + - - diego.mello - - - 10:00 AM - - - + - + - -  - - - + "color": "#1D74F5", + }, + ] + } + > + 1 + + + - - - Encrypted message - - - - - - - 😂 - - - 1 - - - - - - - - 1 - - - - - - - 🤔 - - - 1 - - - - - - -  - - - - +  + - + - - - - + + + - - + + +  + + + - + + - + + + + + + - -  - - - + + + + + + + + + - Encrypted message - + } + > -  + Encrypted message - + + + + + + + + + + + - + + - - + - + + - - - - - - - - - + + + + + + + + + + + diego.mello + + + 10:00 AM + + - Encrypted message +  - + + + + + Encrypted message + + + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + - - - - diego.mello - - - 10:00 AM - - - - - -  - - - - - - - - Encrypted message - - - +  + - + + + + Encrypted message + + + + - - - - + + + - - + + + + + + + + + + + + + + + diego.mello + + + 10:00 AM + + - - - + + + - - - - - - - - - - - - - - - - - diego.mello - - - 10:00 AM - - - - - -  - - - - - - - - Encrypted message - - - - - - - + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + + + + + + + Encrypted message + - - - - + + + - - + - + - - - + - - - + + + + + + - - - - diego.mello - - - 10:00 AM - - - @@ -44501,14 +43453,6 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` accessible={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -44520,95 +43464,156 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` onStartShouldSetResponder={[Function]} style={ { - "paddingLeft": 5, + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, } } + testID="username-header-diego.mello" > + diego.mello + + -  + 10:00 AM + + 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, + } + } + > + +  + + +  + + + + + + -  + Encrypted message + + + + + + + + + + + + + - + + Encrypted message + + + + + + + + - Encrypted message - - + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + +  + - + - - + + +`; + +exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` + + + - - + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - diego.mello - - - 10:00 AM - - - - - -  - - - -  - - - - - - - Encrypted message - - - - - - - - - - - - - - - - - - - - - - - - - - - - Encrypted message - - - - + > + diego.mello + + + + + 10:00 AM + + + -  + Encrypted message - + - - - - - -`; - -exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` - - - + + + - - + + - - - - - - - - - - - + testID="message-encrypted" + > + Encrypted message + - + + + + + +  + + + + + + + + + + + + + + + + + - - - - diego.mello - - - - - -  - - - - - + + - 10:00 AM - - - - - Encrypted message - - - + transition={null} + width={46.800000000000004} + /> - - - + + + - - - - - - - - - - - - - - - - - Encrypted message - - - - + diego.mello + - - - - - - - - - - - - - - + + 10:00 AM + - - + Encrypted message + + + + - - - - - - - - - - - - + 😂 + + + 1 + + + + + + - - diego.mello - - - - + - -  - - - + "color": "#1D74F5", + }, + ] + } + > + 1 + - + + - 10:00 AM - - - - - Encrypted message - - - - + + 1 + + + + + + - - - 😂 - - - 1 - - - - - - - - 1 - - - - - - - 🤔 - - - 1 - - - - - - -  - - - - + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + - + - - - - + + + - - + + +  + + + - + + - + + + + + + - -  - - - + + + + + + + + + - Encrypted message - + } + > -  + Encrypted message + + + + + + + + + + + + - + - - + - + + - - - - - - - - - + + + + + + + + + + + diego.mello + + - Encrypted message +  - + + + 10:00 AM + + + + + Encrypted message + + + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + - - - - diego.mello - - - - - -  - - - - + 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, + } + } + > - 10:00 AM +  - - - - Encrypted message - - - - + + 10:00 AM + + + + + Encrypted message + + + + - - - - + + + - - + + + + + + + + + + + + + + + diego.mello + + - - - - - - - - - - - - - - - + + + - - - - - diego.mello - - - - - -  - - - - - - 10:00 AM - - - - - Encrypted message - - - - - - - + "lineHeight": 20.8, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + + + + + 10:00 AM + + + + + Encrypted message + - - - - + + + - - + - + - - - + - - - + + + + + + - - - - diego.mello - - - @@ -48981,14 +47785,6 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` accessible={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -49000,95 +47796,134 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` onStartShouldSetResponder={[Function]} style={ { - "paddingLeft": 5, + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, } } + testID="username-header-diego.mello" > -  + diego.mello + + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + { + "paddingLeft": 5, + } + } + > + +  + + -  +  - - + 10:00 AM + + + + + Encrypted message + + + + + + + + + + + + + + + + - 10:00 AM - + } + /> + - + + Encrypted message + + + + + + + + - Encrypted message - - + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + +  + - + - - + + +`; + +exports[`Story Snapshots: Error should match snapshot 1`] = ` + + + - - + + + + + + + + + + + + + + + diego.mello + + + 10:00 AM + + - - - + + + + + + + + - - - - - - - - - - - - - - - - - diego.mello - - - - - -  - - - -  - - - - - 10:00 AM - - - - - Encrypted message - - - - - - - + This message has error + + + + - - - - + + + - - - - - - + + - - - - - - - Encrypted message - - - - - - - - -  - - - -  - - - - - - + transition={null} + width={36} + /> + + + - - - - - -`; - -exports[`Story Snapshots: Error should match snapshot 1`] = ` - - - - + - - + - + + 10:00 AM + + + + + - - - - - - +  + + - - - diego.mello - - - 10:00 AM - - - - - -  - - - - - - - - - - This message has error - - + This message has error too - - + + - + - - + + +`; + +exports[`Story Snapshots: ErrorLargeFont should match snapshot 1`] = ` + + + - + + + + + + + + + + + + - + - + + + + - - - - - - +  + + + + 10:00 AM + - - diego.mello - - - 10:00 AM - - - - -  + This message has error - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diego.mello + + + - - + + + + + 10:00 AM + + + + + + - - - This message has error too - - + This message has error too - - + + - + - + `; -exports[`Story Snapshots: ErrorLargeFont should match snapshot 1`] = ` +exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] = ` - - - + - + - - - + - - - + + + + + + - - - - diego.mello - - - @@ -51235,14 +50382,6 @@ exports[`Story Snapshots: ErrorLargeFont should match snapshot 1`] = ` accessible={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -51254,418 +50393,566 @@ exports[`Story Snapshots: ErrorLargeFont should match snapshot 1`] = ` onStartShouldSetResponder={[Function]} style={ { - "paddingLeft": 5, + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, } } + testID="username-header-diego.mello" > + diego.mello + + -  + 10:00 AM + - - - 10:00 AM - - - + + + + + + test.py + + + + test.py + + + + + + + + - This message has error + + Check out this Python file + - + - + - - + - - + - + - - - + - - - + + + + + + - - - - diego.mello - - - @@ -51690,14 +50977,6 @@ exports[`Story Snapshots: ErrorLargeFont should match snapshot 1`] = ` accessible={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -51709,708 +50988,193 @@ exports[`Story Snapshots: ErrorLargeFont should match snapshot 1`] = ` onStartShouldSetResponder={[Function]} style={ { - "paddingLeft": 5, + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, } } + testID="username-header-diego.mello" > -  + diego.mello - - - - - 10:00 AM - - - - - - - This message has error too - - + 10:00 AM + - - - - - - - - - -`; - -exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - - diego.mello - - - 10:00 AM - - - - - - - - - - test.py - - - - test.py - - - + "fontFamily": "Inter", + "fontSize": 16, + "fontVariant": [ + "tabular-nums", + ], + "fontWeight": "500", + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + Component.tsx + - - - - - - - Check out this Python file - - + Component.tsx + + + + + + TypeScript component + + + + + - + - - - - + + + - - + - - + - - + + - - - - - - - + } + transition={null} + width={36} + /> + + + + + + + + + + diego.mello + + + 10:00 AM + + + + - - - diego.mello - - - 10:00 AM - - - - - - - - - - Component.tsx - - - - Component.tsx - - - + "fontFamily": "Inter", + "fontSize": 16, + "fontVariant": [ + "tabular-nums", + ], + "fontWeight": "500", + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + config.json + - - - - - - - TypeScript component - - + config.json - - - - - - - - - - - - - - - - - + - - - - - - - + "color": "#2F343D", + }, + ] + } + > + Configuration file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diego.mello + + + 10:00 AM + + + + - - - diego.mello - - - 10:00 AM - - - - - + + main.go + + + - - - + This is the + + - config.json - - - + + main + + + + - config.json + entry point for the application - - + + - + + + + + + - - - - - Configuration file - - - - - - + File with description + + + - + - - - - + + + - - + + - + - - - - + + document.pdf + + + + testID="markdown-preview-document.pdf" + > + document.pdf + - - + + - - - - - diego.mello - - - 10:00 AM - - - - - + + image.png + + + - - - - - main.go - - - - - - - This is the - - - - - main - - - - - entry point for the application - - - - - - - - - - - - - - - File with description - - + image.png @@ -54421,1024 +52988,1716 @@ exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] - + + - - - - + + + + +`; + +exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match snapshot 1`] = ` + + + - - + + + + + + + + + + + + + - + + + diego.mello + + + + + 10:00 AM + + + + test.py + + + - - - - - document.pdf - - - - document.pdf - - - - - - - - - - - - image.png - - - - image.png - - - - + test.py + + + > + + + + Check out this Python file + + + + + - + - - - - - -`; - -exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match snapshot 1`] = ` - - - + + + - - + + + + + + + + + + + + + - - - + diego.mello + + + + + + 10:00 AM + + + + + - - - + + Component.tsx + + + + testID="markdown-preview-Component.tsx" + > + Component.tsx + - - + + - - - diego.mello + TypeScript component - - + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + diego.mello + + + + + + 10:00 AM + + + + + - 10:00 AM - - - - - - - test.py - - - - test.py - - - + "fontFamily": "Inter", + "fontSize": 16, + "fontVariant": [ + "tabular-nums", + ], + "fontWeight": "500", + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + config.json + - - - - - - - Check out this Python file - - + config.json - - - - - - - - - - - - - - - - - + - - - - - - - + "color": "#2F343D", + }, + ] + } + > + Configuration file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diego.mello + + + + 10:00 AM + + + - - - diego.mello - - - - - - 10:00 AM - - + + main.go + + + - - - - - Component.tsx - - - Component.tsx + This is the - - + + + + main + + + + + entry point for the application + + + - + + + + + + - - - - - TypeScript component - - - - - - + File with description + + + - + - - - - + + + - - + + - + - - - - + + document.pdf + + + + testID="markdown-preview-document.pdf" + > + document.pdf + - - + + - - - - - diego.mello - - - - - - 10:00 AM - - + + image.png + + + - - - - - config.json - - - - config.json - - - - - - - - - - - - Configuration file - - + image.png @@ -56727,1964 +55916,1707 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna - + + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - - diego.mello - - - - - - 10:00 AM - - + - - - - - main.go - - - - - - - This is the - - - - - main - - - - - entry point for the application - - - - - - + File.pdf + + + + + + + - + - File with description + + File.pdf + - + + + + + + + multi file + + + + + - + - - - - + + + + +`; + +exports[`Story Snapshots: FullName should match snapshot 1`] = ` + + + - - + + + + + + + + + + + + + - - - - + + 10:00 AM + + + + + + + + - - - - - - - - document.pdf - - - - document.pdf - - - - - - - - - - - - image.png - - - - image.png - - - - - - - + Message + + + - - + - - - - + + + + +`; + +exports[`Story Snapshots: FullNameLargeFont should match snapshot 1`] = ` + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + Diego Mello + + /> + + + 10:00 AM + + + - - - - diego.mello - - - - - 10:00 AM - - - - - - - - - - - - - File.pdf - - - - - - - - - - - - - - - - - - File.pdf - - - - - - - - - - - - - - - multi file - - - - - - + Message + + + - + - - - + + + `; -exports[`Story Snapshots: FullName should match snapshot 1`] = ` +exports[`Story Snapshots: GroupedMessages should match snapshot 1`] = ` - - - + - - + - - + + - - - - - - - + } + transition={null} + width={36} + /> + + + + + + + + + + diego.mello + + + 10:00 AM + + + + - - + - Diego Mello - - - 10:00 AM + ... - - - - - - - - - - Message - - - - - - + + - + - - - - - -`; - -exports[`Story Snapshots: FullNameLargeFont should match snapshot 1`] = ` - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + 10:00 AM + + + + - - + - Diego Mello + Different user - - - + + + + + + + + + + + + + + + + + + + + + + + - 10:00 AM - - - - - - - - Message - - - - - - + This is the third message + + + - + + - - - - - -`; - -exports[`Story Snapshots: GroupedMessages should match snapshot 1`] = ` - - - + + + - - + + - - + - - - - - - - - - - - - - - - diego.mello - - - 10:00 AM + This is the second message - - - - - - - - - - ... - - - - - - + + - + + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - - - 10:00 AM + This is the first message - - - - - - - - - - Different user - - - - - - + + - + - - - - + + + + +`; + +exports[`Story Snapshots: GroupedMessagesLargeFont should match snapshot 1`] = ` + + + - - + - - + - - - + + - - - - - - This is the third message - - - - - - + transition={null} + width={46.800000000000004} + /> - - - - + + + - - - - - - - - - - - - - - - - - - - - This is the second message - - - - - - - + diego.mello + - - - - - - - - - - - - - - + + 10:00 AM + - + - - - - - - - - - - - - - - diego.mello - - - 10:00 AM + ... - - - - - - - - - - This is the first message - - - - - - + + - + - - - - - -`; - -exports[`Story Snapshots: GroupedMessagesLargeFont should match snapshot 1`] = ` - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + + 10:00 AM + + + - - - diego.mello + Different user - - - - - 10:00 AM + - - - - - - - ... - - - - - - - + - - - - + + + - - + + - - - - - - - - - - - - - - - + - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + This is the third message - - - - - 10:00 AM + - - - - - - - Different user - - - - - - - + + - - - - + + + - - + + - - - - + - - - - - This is the third message - - - - - - + This is the second message + + + - - + + - - - - + + + - - + - - + - - - + + - - - - - - This is the second message - - - - - - + transition={null} + width={46.800000000000004} + /> - - - - + + + - - - - - - - - - - - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - + - diego.mello + This is the first message - - - - - 10:00 AM + - - - - - - - This is the first message - - - - - - - + - - - + + + `; @@ -63433,325 +61199,308 @@ exports[`Story Snapshots: Ignored should match snapshot 1`] = ` } > - - - + - + - - + - + + - - - - - - - - - + + + + + + + + - - - - Message ignored. Tap to display it. - - - - + Message ignored. Tap to display it. + + - - - + + + `; @@ -63765,325 +61514,308 @@ exports[`Story Snapshots: IgnoredLargeFont should match snapshot 1`] = ` } > - - - + - + - - + - + + - - - - - - - - - + + + + + + + + - - - - Message ignored. Tap to display it. - - - - + Message ignored. Tap to display it. + + - - - + + + `; @@ -64097,462 +61829,442 @@ exports[`Story Snapshots: InlineKatex should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - - diego.mello - - - 10:00 AM + + xˆ2 + yˆ2 - zˆ2 - - - - - - - - - - xˆ2 + yˆ2 - zˆ2 - - - - - - + + - + - - - + + + `; @@ -64566,462 +62278,442 @@ exports[`Story Snapshots: InlineKatexLargeFont should match snapshot 1`] = ` } > - - - + - - + - - + + - - - - - - - + } + transition={null} + width={46.800000000000004} + /> + + + + + + + + + + diego.mello + + + + 10:00 AM + + + - - - - diego.mello - - - - - 10:00 AM - - - - - - - - xˆ2 + yˆ2 - zˆ2 - - - - - - + + xˆ2 + yˆ2 - zˆ2 + + + - + - - - + + + `; @@ -65035,538 +62727,518 @@ exports[`Story Snapshots: JitsiCall should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + Call started by diego.mello + + + - - - - diego.mello - - - 10:00 AM - - - - + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + - Call started by diego.mello + Click to join! - - - - -  - - - Click to join! - - - - - + - - - + + + `; @@ -65580,538 +63252,518 @@ exports[`Story Snapshots: JitsiCallLargeFont should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + Call started by diego.mello + + + - - - - diego.mello - - - - - Call started by diego.mello +  - - - - -  - - - Click to join! - - - - - 10:00 AM + Click to join! - + + 10:00 AM + + - - - + + + `; @@ -66125,424 +63777,404 @@ exports[`Story Snapshots: Katex should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - - - diego.mello - - - 10:00 AM - - - - - - - - - - - + - + - - - + + + `; @@ -66556,860 +64188,409 @@ exports[`Story Snapshots: KatexArray should match snapshot 1`] = ` } > - - - + - - + - - - - - - - - - - - - - - - - - diego.mello - - - 10:00 AM - - - + + - - - - - - - - + transition={null} + width={36} + /> - - - + + + - - - - - - - -`; - -exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` - - - - - - - - - + - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - - - diego.mello - - - - - - 10:00 AM - - - - - - - - + - + - - - + + + `; -exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` +exports[`Story Snapshots: KatexArrayLargeFont should match snapshot 1`] = ` - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - - - diego.mello - - - - - - 10:00 AM - - - - - - - - + - + - - - + + + `; -exports[`Story Snapshots: Lists should match snapshot 1`] = ` +exports[`Story Snapshots: KatexLargeFont should match snapshot 1`] = ` - - - + - + + + + + + + + + + + + - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - + + + + + + + + + + + + +`; + +exports[`Story Snapshots: Lists should match snapshot 1`] = ` + + + + + + + + + + + + + + - - - diego.mello - - - 10:00 AM - - - - - - - - - - - • - - - - - Dogs - - - - - - - • - - - - - cats - - - - - - - - - • - - - - - cats - - - - - - - - + transition={null} + width={36} + /> - - - + + + - - - - - - - - - - - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - + - diego.mello + • - 10:00 AM - - - - - - - - - - - 1 - . - - - - - Dogs - - - - - - - 2 - . - - - - - Cats - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - + > + Dogs + + + + - diego.mello + • - 10:00 AM + + + cats + + + + - - - - - - + + + - - 1 - . - - - - - Dogs - - - - - - + cats + + + - + - - - - + + + - - + - - + - - - + + - - - - - - 2 - . - - - - - Cats - - - - - - - - + transition={null} + width={36} + /> - - - - + + + - - - - - - - -`; - -exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` - - - - - - - - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - + - diego.mello + 1 + . - - - - - 10:00 AM - - - - - - - - • - - - - - Dogs - - - - - + - - • - - - - - cats - - - - - - - + + + + + + 2 + . + + + + - - • - - - - - cats - - - - - - + Cats + + + - + - - - - + + + - - + - - + - - + + - - - - - - - + } + transition={null} + width={36} + /> + + + + + + + + + + diego.mello + + + 10:00 AM + + + + - + - diego.mello + 1 + . - - - - - 10:00 AM - - - - - - - - 1 - . - - - - - Dogs - - - - - + - - 2 - . - - - - - Cats - - - - - - + Dogs + + + - + - - - - + + + - - + + - - - - - - - - - - - - - - + - diego.mello + 2 + . - - - - - 10:00 AM - - - - - - + - - 1 - . - - - - - Dogs - - - - - - + Cats + + + - + + - - - - + + + + +`; + +exports[`Story Snapshots: ListsLargeFont should match snapshot 1`] = ` + + + - - + + + + + + + + + + + + + - + testID="username-header-diego.mello" + > + + diego.mello + + + + + 10:00 AM + + + - + - - - - - 2 - . - - - - - Cats - - - - - - - - - - - - - - - - - - - - - -`; - -exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` - - - - - - - - - - - - - - - - - + + - - - - - - - - + > + + Dogs + + + + - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + • - 10:00 AM + + + cats + + + + - - - - + • + + - - this is a normal message - + cats - + - + - - - - + + + - - - + + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - + - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + 1 + . - 10:00 AM + + + Dogs + + - - -  - - - - - - - + - - Edited message - + Cats - + - + - - - - + + + - - + - + + + + + + + + + + + + - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - + - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + 1 + . - 10:00 AM - - - - -  - - - - - - - - - - Translated message - + Dogs - + - + - - - - + + + - - + + - - - + + - - - - - - - - - - - - - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + 2 + . - 10:00 AM - - - - -  - - - - - - - - Encrypted message + } + } + > + + Cats + + - + + - - - - + + + + +`; + +exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` + + + - - + - + - - - + - - - + + + + + + - - - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - 10:00 AM - - - @@ -74362,14 +69758,6 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` accessible={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -74381,1454 +69769,907 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` onStartShouldSetResponder={[Function]} style={ { - "paddingLeft": 5, + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, } } + testID="username-header-johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername" > -  + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - - - - + 10:00 AM + + + + + + + - Error message + + this is a normal message + - + - + - - + - - - + + + + + + + + + + + + + + - - - - - - - - - - - + > + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + + + 10:00 AM + - - + + + + + + + + - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - 10:00 AM - - - - -  + Edited message - - - - - - - - - Message with read receipt - - - - - - + + - + - - - - + + + - - + - - + - - - - - - - - - - - - - - - - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - 10:00 AM - - - + + - - -  - - - -  - - - - - - - Encrypted message - - - + transition={null} + width={36} + /> - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - 10:00 AM - - - @@ -75853,14 +70694,6 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` accessible={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -75872,551 +70705,456 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` onStartShouldSetResponder={[Function]} style={ { - "paddingLeft": 5, + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, } } + testID="username-header-johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername" > -  + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - -  + 10:00 AM - -  - - - - -  - + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + - -  - - - - - Encrypted message - + + + + + Translated message + + + + + - + - - + - - + - + - - - + - - - + + + + + + - - - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - 10:00 AM - - - @@ -76441,14 +71179,6 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` accessible={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -76460,551 +71190,897 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` onStartShouldSetResponder={[Function]} style={ { - "paddingLeft": 5, + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, } } + testID="username-header-johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername" > -  + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - -  + 10:00 AM + - + -  - + [ + { + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + - + + -  + Encrypted message - -  - - - - - - - Encrypted message - - + - - + - + + + + + + + + + + + + - + + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + + - + + + + - + + + + + + + + - - + - - - + > + Error message + + + + + + + + + + + + - + - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - 10:00 AM - - + "backgroundColor": "#E4E7EA", + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "bottom": 0, + "left": 0, + "opacity": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + /> + + + + + + + + + @@ -77029,14 +72105,6 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` accessible={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -77048,44 +72116,67 @@ exports[`Story Snapshots: LongNameUser should match snapshot 1`] = ` onStartShouldSetResponder={[Function]} style={ { - "paddingLeft": 5, + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, } } + testID="username-header-johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername" > + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + + -  + 10:00 AM -  +  + + + + + + + + Message with read receipt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + + -  + 10:00 AM + + +  + + -  +  - -  - - - - - Encrypted message - + + Encrypted message + + - + - - - -`; - -exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` - - - + - - - - - - - - - - - - - - - - - - - + + - - - - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - - - - 10:00 AM - - - - - - - - this is a normal message - - - - - - - - - - - - + } + transition={null} + width={36} + /> + + + - - - - - - + - - + + 10:00 AM + + + + - + - - - - - - - - - - - - - +  + + + + - - - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - - - -  - - - - - - 10:00 AM - - - - - - - - Edited message - - - - - - - - - - + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + + + +  + + + + +  + + + +  + + + + + + + Encrypted message + - - - - + + + - - + + + + + + + + + + + + + + + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + + + 10:00 AM + + - - - + + + - - - - - - - - - - - - - +  + + + + - - - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - - - -  - - - - - - 10:00 AM - - - - - - - - Translated message - - - - - - - - - - + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + + + +  + + + +  + + + + + + + Encrypted message + - - - - + + + - - + + + + + + + + + + + + - - + - - - - - - - - - - - - - + 10:00 AM + + + + + - - - - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - - - -  - - - - - - 10:00 AM - - - - - Encrypted message - - - - - - - + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + + + +  + + + + +  + + + + +  + + + +  + + + + + + + Encrypted message + - - - - + + + + +`; + +exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` + + + - - + - + - - - + - - - + + + + + + - - - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - @@ -79593,14 +74835,6 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` accessible={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -79612,1454 +74846,1392 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` onStartShouldSetResponder={[Function]} style={ { - "paddingLeft": 5, + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, } } + testID="username-header-johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername" > -  + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + - - - 10:00 AM - - + > + 10:00 AM + - - Error message + + this is a normal message + - + - + - - + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + - - - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - - -  - - - + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + + + + + 10:00 AM + + + + - 10:00 AM - - - - - - - - Message with read receipt - - - - - - + Edited message + + + - + - - - - + + + - - + + + + + + + + + + + + + - - - + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + + + + + - - - - - - - + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + - + + 10:00 AM + + + - - - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - - - -  - - - -  - - - - - 10:00 AM - - - - Encrypted message + Translated message - - + + - + - - - - + + + - - + - + - - - + - - - + + + + + + - - - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - @@ -81084,14 +76256,6 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` accessible={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -81103,551 +76267,897 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` onStartShouldSetResponder={[Function]} style={ { - "paddingLeft": 5, + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, } } + testID="username-header-johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername" > -  + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - + -  - + [ + { + "lineHeight": 20.8, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + - + + 10:00 AM + + + -  + Encrypted message - - -  - - - + + + + + + + + + + + + + + + + -  - - + } + transition={null} + width={46.800000000000004} + /> + + + + + + + + - 10:00 AM + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + + - +  + + + + + + 10:00 AM + + + + + - Encrypted message + + Error message + - + - + - - + - - + - + - - - + - - - + + + + + + - - - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - @@ -81672,14 +77182,6 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` accessible={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -81691,44 +77193,45 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` onStartShouldSetResponder={[Function]} style={ { - "paddingLeft": 5, + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, } } + testID="username-header-johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername" > -  + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername -  +  - + + 10:00 AM + + + + - + -  - - - - -  - + ] + } + > + + + Message with read receipt + + + + - -  - - - - - 10:00 AM - - - - - Encrypted message - - + - - + - - + - + - - - + - - - + + + + + + - - - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - @@ -82260,14 +77663,6 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` accessible={true} collapsable={false} focusable={true} - hitSlop={ - { - "bottom": 4, - "left": 4, - "right": 4, - "top": 4, - } - } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -82279,59 +77674,134 @@ exports[`Story Snapshots: LongNameUserLargeFont should match snapshot 1`] = ` onStartShouldSetResponder={[Function]} style={ { - "paddingLeft": 5, + "alignItems": "center", + "flexDirection": "row", + "flexShrink": 1, + "gap": 4, } } + testID="username-header-johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername" > -  + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + + +  + + -  +  - + + 10:00 AM + + + -  - - - - -  + Encrypted message - + + + + + + + + + + + + + + + + -  - - + } + transition={null} + width={46.800000000000004} + /> + + + + + + + + - 10:00 AM + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + + - - + + + +  + + + + +  + + + + - Encrypted message - - + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + +  + + + + + 10:00 AM + + + + + Encrypted message + - + - - - -`; - -exports[`Story Snapshots: Mentions should match snapshot 1`] = ` - - - + - - + + + + + + + + + + + + - - + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + + + + - + - - - - - - - - - - - - - - - - - diego.mello - - - 10:00 AM - - - - - - - - - - - rocket.cat - - - - - - diego.mello - - - - - - all - - - - - - here - - - - - - general - - - - - - team - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diego.mello - - - 10:00 AM - - - - - - - - - - - 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. - - - - - - - - - - - - - - - - - - -`; - -exports[`Story Snapshots: MentionsLargeFont should match snapshot 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - diego.mello - - - - - - 10:00 AM - - - - - - - - rocket.cat - - - - - - diego.mello - - - - - - all - - - - - - here - - - - - - general - - - - - - team - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diego.mello - - - - - - 10:00 AM - - - - - - - - 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. - - - - - - - - - - - - - - - - - - -`; - -exports[`Story Snapshots: Message should match snapshot 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - diego.mello - - - 10:00 AM - - - - - - - - - - - - - - - - - -`; - -exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1`] = ` - - - - - - - - - - - - - - - - - - - - - - - - - - - - diego.mello - - - 10:00 AM - - - - - - - - - - - Forwarded message with file inside - - - - - - - - - - - - - rocket.cat - - - 10:00 AM - - - - - - - - - - user - - - document.pdf - - - - document.pdf - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diego.mello - - - 10:00 AM - - - - - - - - - - - Forwarded message with nested image - - - - - - - - - - - - - rocket.cat - - - - - - - - - Nested image from forwarded message - - - - - - - - - - -  - - - - - - - - - - - - - - - - - + "color": "#2F343D", + "fontSize": 20.8, + }, + [ + { + "lineHeight": 20.8, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + + + +  + + + + +  + + + + +  + + + +  + + + + + 10:00 AM + + + + + Encrypted message + - - - - + + + - - + + + + + + + + + + + + + + + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + + + + + +  + + + + +  + + + + +  + + + + +  + + + +  + + + + + 10:00 AM + + - - - - - - - - - - - - - - - - - - - - diego.mello - - - 10:00 AM - - - - - - - - - - - Forwarded message with nested audio - - - - - - - - - - - - - rocket.cat - - - - - - - -  - - - - - - - - - - - - - - - - - - - - - - - - + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontVariant": [ + "tabular-nums", + ], + "fontWeight": "400", + "textAlign": "left", + }, + { + "color": "#6C727A", + }, + ] + } + testID="message-encrypted" + > + Encrypted message + - - - - + + + + +`; + +exports[`Story Snapshots: Mentions should match snapshot 1`] = ` + + + - - + - - + - - - - - - - - - - - - - - - - - diego.mello - - - 10:00 AM - - - + + - - - - - - - - Forwarded message with nested video - - - - - - - - - - - - - rocket.cat - - - - - - - - -  - - - - - - - - - - - + transition={null} + width={36} + /> - - - + + + - - - - - - - - - - - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello + rocket.cat - 10:00 AM + - - - - - - - - - - Message forwarded twice with file inside - - - - - - - - - - - - - rocket.cat - - - 10:00 AM - - - - - - - - - - rocket.cat - - - 10:00 AM - - - - - - - - - - user - - - document.pdf - - - - document.pdf - - - - - - - - - - - - - - - - - + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontSize": 16, + "fontVariant": [ + "tabular-nums", + ], + "fontWeight": "600", + "textAlign": "left", + }, + { + "color": "#9B1325", + }, + ] + } + > + diego.mello + + + + + + all + + + + + + here + + + + + + general + + + + + + team + + + - + - - - - - -`; - -exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + + rocket.cat + + + Lorem ipsum dolor + + + > + diego.mello + + sit amet, + + - diego.mello + all + + + consectetur adipiscing - 10:00 AM + here - - + > + elit, sed do eiusmod tempor + -  + general - - - - - - - - - I'm fine! - - - - - - + incididunt ut labore et dolore magna aliqua. + + + - + - - - - + + + + +`; + +exports[`Story Snapshots: MentionsLargeFont should match snapshot 1`] = ` + + + - - + - - + - - - + + - - - - - - I'm fine! - - - - - - + transition={null} + width={46.800000000000004} + /> - - - -  - - - - + + + - - - - - - - - - - - - - - - - - - - - - + } + 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" + > + + diego.mello + + + + 10:00 AM + + + - - + + rocket.cat + + + + + diego.mello + + + - 10:00 AM + all - - + > + + -  + here - - - - - - - - - I'm fine! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - I'm fine! - - - - - - - - - - -  - + general + + + + + + team + + + + - + - - - - - -`; - -exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot 1`] = ` - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - + + rocket.cat + + + Lorem ipsum dolor + + diego.mello - - + > + sit amet, + -  + all - - - - 10:00 AM - - - - + consectetur adipiscing + + + here + + + elit, sed do eiusmod tempor + + + general + + - - - - I'm fine! - - - - - - + incididunt ut labore et dolore magna aliqua. + + + - + - - - - + + + + +`; + +exports[`Story Snapshots: Message should match snapshot 1`] = ` + + + - - + - - + - - - + + - - - - - - I'm fine! - - - - - - + transition={null} + width={36} + /> - + + + + + + + -  + diego.mello + + + 10:00 AM + - + + - - - - + + + + +`; + +exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1`] = ` + + + - - + + + + + + + + + + + + + - + - + diego.mello + + - - - - - - - - - + > + 10:00 AM + + + + - - + - diego.mello - - - - -  + Forwarded message with file inside - - - - 10:00 AM + - - - - - - - I'm fine! - - - - - - - - - - - - - - - - - - - - - - - - + rocket.cat + + + 10:00 AM + + + + - - I'm fine! - - - + + + + + user + + + document.pdf + + + + document.pdf + + + + + + - - -  - - - + - - - - - -`; - -exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello - - - 10:00 AM + Forwarded message with nested image - - - + + + + + + + + rocket.cat + + + + + + + Nested image from forwarded message + + + + + - - I'm a very long long title and I'll break - - - - + + - - How are you? - + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  - + + - - - - - - I'm fine! - - - - - - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello - - - 10:00 AM + Forwarded message with nested audio - - - + + + + + + + + rocket.cat + + + - rocket.cat +  - - - - How are you? - - - - - - - - - - - - - - - - - - rocket.cat - - - - + - - - How are you? - - + + - - - - + } + style={ + [ + { + "borderRadius": 6, + "height": 12, + "position": "absolute", + "width": 12, + }, + { + "backgroundColor": "#D1EBFE", + }, + [Function], + ] + } + /> + - - - - - Reply attachment can have a description - - - - - - - - - - - - I'm fine! - - - - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello - - - 10:00 AM + Forwarded message with nested video - - - + + + + + + - - - - - - Looks cool! - - - - - + + rocket.cat + + + + - rocket.cat +  - - - - - - - What you think about this one? - - - - - - - - - - -  - - - - - - - @@ -95260,882 +85665,895 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello - - - 10:00 AM - - - - + > + Message forwarded twice with file inside + + + + + + + - - - - - - Yes, I am - - - - - + + rocket.cat + + + 10:00 AM + + + - - - rocket.cat - - + }, + { + "borderColor": "#CBCED1", + }, + ] + } + testID="reply-rocket.cat-" + > - + - - Are you seeing this mario - - - - - - ? - - + }, + { + "color": "#6C727A", + }, + ] + } + > + 10:00 AM - - - -  - + + + + user + + + document.pdf + + + + document.pdf + + + - @@ -96149,18 +86567,18 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` - + - - - + + + `; -exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` +exports[`Story Snapshots: MessageWithReadReceipt should match snapshot 1`] = ` - - - + + + + + + + + + + + + + - + + + diego.mello + + + 10:00 AM + + - - - - - - - - - - + testID="read-receipt-unread" + > +  + - + + - - + - diego.mello - - - 10:00 AM + I'm fine! - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - rocket.cat - - - script.sh - - - - script.sh - - - - - - - - - - - - Here is the file - - - - - - + + + I'm fine! + + + - + + + + +  + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + +  + + + + + - - + - diego.mello - - - 10:00 AM + I'm fine! - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - rocket.cat - - - config.yaml - - - - - - - This is a configuration file with - - - - - important - - - - - settings - - - - - - - - - - - - - - - Got it! - - - - - - + I'm fine! + + + - + + + + +  + - - - - + + + + +`; + +exports[`Story Snapshots: MessageWithReadReceiptLargeFont should match snapshot 1`] = ` + + + - - + + + + + + + + + + + + + - + + + diego.mello + + - - - - - - - - - - + testID="read-receipt-unread" + > +  + - + + 10:00 AM + + + - - - - diego.mello - - - 10:00 AM - - - - - - - - - - - - - rocket.cat - - - index.ts - - - - index.ts - - - - - - - - - - - - rocket.cat - - - styles.css - - - - styles.css - - - - - - - - - - - - Multiple files - - - - - - + "backgroundColor": "transparent", + "flexShrink": 1, + "fontFamily": "Inter", + "fontVariant": [ + "tabular-nums", + ], + "fontWeight": "400", + "textAlign": "left", + } + } + > + + I'm fine! + + + - + - - - - - -`; - -exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot 1`] = ` - - - + + + - - + + - - - - - - - - - - - - - - - - - diego.mello - - - - - 10:00 AM - - - - - - - - - - rocket.cat - - - script.sh - - - - script.sh - - - - - - - - - - - - Here is the file - - - - - - + I'm fine! + + + - + + + + +  + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + - - - - - diego.mello - - - - - - 10:00 AM - - - - - - - - - - rocket.cat - - - config.yaml - - - - - - - This is a configuration file with - - - - - important - - - - - settings - - - - - - - - - - +  + + + + + 10:00 AM + + + + + + - - - - - Got it! - - - - - - + I'm fine! + + + - + - - - - + + + - - + + - - + - - - - - - - + "color": "#2F343D", + }, + ] + } + > + I'm fine! + + + + + + + + + + +  + + + + + + + + + +`; + +exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + diego.mello + + + 10:00 AM + + + + - - - diego.mello - - - - - - 10:00 AM - - - - - - - rocket.cat - - - index.ts - - - - index.ts - - - + I'm a very long long title and I'll break + - - - - - - - rocket.cat - - - styles.css - - - styles.css + How are you? - - + + - + + + + + + - - - - - Multiple files - - - - - - + I'm fine! + + + - + - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - - diego.mello - - - - - - 10:00 AM - - - + rocket.cat + + + + - Forwarded message with nested audio + + How are you? + + + + - + + + + + + + rocket.cat + + + - - + + How are you? + - - rocket.cat - - - - - - - -  - - - - - - - - - - - - - + transition={null} + width={15} + /> - - + + + + + + + Reply attachment can have a description + + + + + + + + + + + + I'm fine! + + + - + - - - - + + + - - + + + + + + + + + + + + + - + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello + Looks cool! - - + + + + + + - - - 10:00 AM - - - - - - - - Forwarded message with nested video - - - - - + + rocket.cat + + + - - rocket.cat + + What you think about this one? + - + + + - - - + + + - -  - - - + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + @@ -102096,918 +91894,865 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello + Yes, I am - - + + + + + + - - - 10:00 AM - - - - - - - - Message forwarded twice with file inside - - - - - + + rocket.cat + + + + + + + Are you seeing this mario + + + + + + ? + + + + + - - rocket.cat - - + - 10:00 AM - - - + /> - - - - - - rocket.cat - - - 10:00 AM - - - - - - - - - - user - - - document.pdf - - - - document.pdf - - - - - - - - - +  + + @@ -103018,18 +92763,18 @@ exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot - + - - - + + + `; -exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = ` +exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - - diego.mello - - - - - - 10:00 AM - - - - + - - - I'm a very long long title and I'll break - - - - - - - How are you? - - - - - - + "fontFamily": "Inter", + "fontSize": 16, + "fontVariant": [ + "tabular-nums", + ], + "fontWeight": "500", + "textAlign": "left", + }, + { + "color": "#2F343D", + }, + ] + } + > + script.sh + - - - - - - - I'm fine! - - + script.sh + + + + + + Here is the file + + + + + - + - - - - + + + - - + + + + + + + + + + + + + - + - + diego.mello + + - - - - - - - - - + > + 10:00 AM + + + + - - - diego.mello - - - - - - 10:00 AM - - + + rocket.cat + + + config.yaml + + + - - - - - rocket.cat - - - + - - How are you? - - - - + "fontWeight": "700", + "textAlign": "left", + }, + ] + } + > + important - - - - - - - - - - - - I'm fine! + + + settings + - + + + + + + + Got it! + + + + + - + - - - - + + + - - + + + + + + + + + + + + + - - + diego.mello + + - + 10:00 AM + + + + + + + + - - - + + rocket.cat + + + index.ts + + + + testID="markdown-preview-index.ts" + > + index.ts + - - + + - - - - - diego.mello - - - - - - 10:00 AM - - + + + rocket.cat + + + styles.css + + - - - Looks cool! - - + styles.css - + + + + + + - - - - - - - rocket.cat - - - - - - - - - What you think about this one? - - - - - - - - - - -  - - - - - - - - - - - - - + "color": "#2F343D", + }, + ] + } + > + Multiple files + + + - + - - - - + + + + +`; + +exports[`Story Snapshots: MessageWithReplyAndFileLargeFont should match snapshot 1`] = ` + + + - - + + + + + + + + + + + + + - + + + diego.mello + + + + + + 10:00 AM + + - - - - - - - - - - - - - - - diego.mello - - - - - - 10:00 AM - - + + + rocket.cat + + + script.sh + + - - - Yes, I am - - + script.sh - - - - - - - - rocket.cat - - - - - - - - - Are you seeing this mario - - - - - - ? - - - - - - - - - - -  - - - - - - - - - - - - + + + + + + Here is the file + + + + + - + - - - - - -`; - -exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - - diego.mello - - - 10:00 AM - - - - - - - - - - - How are you? - - - - - - - - View thread - - + } + > - - -  - - - 1 - - - + - -  - - - 0 - - + config.yaml + - - - - + This is a configuration file with + + - -  + + important + - - - + + + settings + + + + + + + + + Got it! + + + + + - + - - - - + + + - - + - + - -  - - - + + + + + + + + + + + + + diego.mello + + + + + - How are you? + 10:00 AM - -  - - - - - - + - - - + + + rocket.cat + + + index.ts + + + + index.ts + + + + + + + + + + > + + + rocket.cat + + + styles.css + + + + styles.css + + - - + + - - - I'm fine! + + + Multiple files + + - + - - - - + + + - - + + + + + + + + + + + + + - + + diego.mello + + + -  - + /> - Thread with emoji :) 😂 + 10:00 AM - -  - - - - - - + - - - - - - - + > + Forwarded message with nested audio + + + + - - - - I'm fine! - + + + + + rocket.cat + + + + + + + +  + + + + + + + + + + + + + + + + + - + - - - - + + + - - + + + + + + + + + + + + + - + + diego.mello + + + -  - + /> - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + 10:00 AM - -  - - - - - - + - - - - - - - + > + Forwarded message with nested video + + + + - - - - I'm fine! - + + + + + rocket.cat + + + + + + + + +  + + + + + + + + - + - - - - + + + - - + + + + + + + + + + + + + - + + diego.mello + + + -  - + /> - How are you? + 10:00 AM - -  - - - - - + + + + + Message forwarded twice with file inside + + + + + - + - - - + + > + + rocket.cat + + + 10:00 AM + + + + + + + + + + rocket.cat + + + 10:00 AM + + + + + + + + + + user + + + document.pdf + + + + document.pdf + + + + + + + + + + + + - - + + - + + + + + + + + +`; + +exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = ` + + + + + + + + + + - - + + - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - + style={ + { + "borderRadius": 4, + "height": 46.800000000000004, + "width": 46.800000000000004, + } + } + transition={null} + width={46.800000000000004} + /> - - + + - - - - - - - - - - - + + diego.mello + + + -  - + /> - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + 10:00 AM - -  - - - - - - + - - - + + > + + I'm a very long long title and I'll break + + + + + + + How are you? + + + + + - - + + - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + I'm fine! + + - + - - - - + + + - - + + + + + + + + + + + + + - + + diego.mello + + + -  - + /> - Thread with attachment + 10:00 AM - -  - - - - - - + - - - + + > + + rocket.cat + + + + + + + How are you? + + + + + + + + - - + + - - - This is a description :nyan_rocket: + + + I'm fine! + + - + - - - - + + + - - + + + + + + + + + + + + + - + + diego.mello + + + -  - + /> - Thread with image attachment + 10:00 AM - -  - - - - - + + + + + Looks cool! + + + + + - + - - - + + > + + rocket.cat + + + + + + + + + What you think about this one? + + + + + + + + + + +  + + + + + + + + - - + + - + + + + + + + + + + + + + + - - + + - example.png - + style={ + { + "borderRadius": 4, + "height": 46.800000000000004, + "width": 46.800000000000004, + } + } + transition={null} + width={46.800000000000004} + /> - - + + - - - - - - - - - - - + + diego.mello + + + -  - + /> - Thread with file attachment + 10:00 AM - -  - - - - - + + + + + Yes, I am + + + + + - + - - - + + > + + rocket.cat + + + + + + + + + Are you seeing this mario + + + + + + ? + + + + + + + + + + +  + + + + + + + + - - - - - - - - - presentation.pptx - + - + - - - - + + + + +`; + +exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` + + + - - + - - -  - - - + - Thread with multiple attachments - - - -  - - + + + + + + + + - + - - - - - - - + diego.mello + + + 10:00 AM + + - - first-file.pdf + + + How are you? + + - - - - - - - - - - -`; - -exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = ` - - - - - - - - - - - + - - - - - - - + View thread + - - + +  + - diego.mello + 1 - - - 10:00 AM - - - - - - - - How are you? - - - - - - - +  + + - - View thread - - - + + + + + + - - -  - - - 1 - - - - -  - - - 0 - - - - - - - - - - -  - - - - + "backgroundColor": "#E4E7EA", + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "bottom": 0, + "left": 0, + "opacity": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + /> + + +  + - - + + - + - - - - + + + - - + + +  + + + - + + - + + + + + + - -  - - - + + + + + + + + + - How are you? - + } + > -  + I'm fine! - + + + + + + + + + + + + - + + + Thread with emoji :) 😂 + + + +  + + + + + + + - - - - - - - - - - - - - - + + - I'm fine! - + style={ + { + "borderRadius": 4, + "height": 20, + "width": 20, + } + } + transition={null} + width={20} + /> - - + + + + + + + + + I'm fine! + + - - - - + + + - - + + +  + + + - + + - + + + + + + - -  - - - + + + + + + + + + - Thread with emoji :) 😂 - + } + > -  + I'm fine! - + + + + + + + + + + + + - + + + How are you? + + + - - - - - - - - - - - - +  + + + + + + + - - + + - I'm fine! - + style={ + { + "borderRadius": 4, + "height": 20, + "width": 20, + } + } + transition={null} + width={20} + /> - - + + - - - - - - - - - - -  + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - + + + + + + + + + + + + + +  + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - - - -  - - - + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + + + + - + - - + - + + - - - - - - - - - + + + + + + + + - - - - I'm fine! - - - - + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + - - - - + + + - - + + +  + + + - + - + - -  - - - - How are you? - - - -  - - - + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + + + + - + - - + - + + - - - - - - - - - + + + + + + + + - - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - - - - + This is a description :nyan_rocket: + + - - - - + + + - - + + +  + + + - + + - + + + + + + - -  - - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - - + + + + + + + + + + -  + example.png - + + + + + + + + + + + + - + + + Thread with file attachment + + + +  + + + + + + + - - + - + + - - - - - - - - - + + + + + + + + - - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - - - - + presentation.pptx + + - - - - + + + - - + + +  + + + - + + - + + + + + + - -  - - - + + + + + + + + + - Thread with attachment - + } + > -  + first-file.pdf + + + + + + + + +`; + +exports[`Story Snapshots: MessageWithThreadLargeFont should match snapshot 1`] = ` + + + + + + + + - + - - + - - - - - - - - - - - + /> - + - This is a description :nyan_rocket: - + style={ + { + "borderRadius": 4, + "height": 46.800000000000004, + "width": 46.800000000000004, + } + } + transition={null} + width={46.800000000000004} + /> - - + + - - - - - - - - - - - + + diego.mello + + + -  - + /> - Thread with image attachment + 10:00 AM - -  - - - - - - + - - - - - - - + > + How are you? + + + + - - - example.png + View thread + + + + +  + + + 1 + + + + +  + + + 0 + + + + + + + + + + +  + + + + + + - + - - - - + + + - - + + +  + + + - + - + - -  - - - - Thread with file attachment - - - -  - - - - - - - - - - - - - - - - - + + + + + + - - + + - presentation.pptx - + style={ + { + "borderRadius": 4, + "height": 26, + "width": 26, + } + } + transition={null} + width={26} + /> - - + + + + + + + + + I'm fine! + + - - - - + + + - - + + +  + + + - + + - + + + + + + - -  - - - + + + + + + + + + - Thread with multiple attachments - + } + > -  + I'm fine! - + + + + + + + + + + + + - - - - - - - - - - - - - + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + +  + + + + + + + - - + + - first-file.pdf - + style={ + { + "borderRadius": 4, + "height": 26, + "width": 26, + } + } + transition={null} + width={26} + /> - - + + + + + + + + + I'm fine! + + - - - - - -`; - -exports[`Story Snapshots: Names should match snapshot 1`] = ` - - - + + + - - + - - - - - - - - - - - - - - - +  + + + + How are you? + + + - - - - - JD - - - 10:00 AM - - - - - - - - - - - Small name (2 chars) - - - - - - - - - - + "lineHeight": 26, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + - - - - - - - - - + - + - - - + - - - + + + + + + - - - JD - - - 10:00 AM + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - + + + + + + + + + + + + + - -  - - - -  - - - - -  - - - - -  - - - +  + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + - -  - - - +  + + + + + + + + -  - - + + + + + + + + - Encrypted message + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - + - - + - - + - - + + + Thread with attachment + + + +  + + + + + + + - - - - - - - - - - - - - - - - - John Doe - - - 10:00 AM - - - + + - - - - - - - - Medium name (8 chars) - - - - - - + transition={null} + width={26} + /> - + + + + + + + + + This is a description :nyan_rocket: + - + - - - - + + + - + +  + + + - + Thread with image attachment + + + - + + + + + + - - - + - - - + + + + + + - - - John Doe - - - 10:00 AM + example.png - + + + + + + + + + + + + + - -  - - - -  - - - - -  - - - - -  - - - - -  - - - +  + + + + Thread with file attachment + + + +  + + + + + + + + -  - - + + + + + + + + - Encrypted message + presentation.pptx - + - - + - - + - - - - - - - - - - - - - - - +  + + + + Thread with multiple attachments + + + - - - - - John Doe With An Extremely Long Display Name That Should Definitely Be Truncated In Any Reasonable User Interface Layout - - - 10:00 AM - - - - - - - - - - - Long name (22 chars) - should truncate - - - - - - - - - - + "lineHeight": 26, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + - - - - - - - - - + - + - - - + - - - + + + + + + - - - John Doe With An Extremely Long Display Name That Should Definitely Be Truncated In Any Reasonable User Interface Layout - - - 10:00 AM + first-file.pdf - + + + + + + + + +`; + +exports[`Story Snapshots: Names should match snapshot 1`] = ` + + + + + + + + + + - -  - - - -  - - - - + + -  - - + "borderRadius": 4, + "height": 36, + "width": 36, + } + } + transition={null} + width={36} + /> + + + + + + + + - -  - - - -  - - - -  - - - - - - - Encrypted message - - - - - - - - - - - -`; - -exports[`Story Snapshots: Pinned should match snapshot 1`] = ` - - - - - - - - - - - - + JD + + - - - - - - - - - + > + 10:00 AM + + + + - - + - diego.mello - - - 10:00 AM - - - - -  + Small name (2 chars) - - - - - - - - - Message header - - - - - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + JD + + + 10:00 AM + + + + +  + + + +  + + + + +  + + + + +  + + + + +  + + + +  + + + + + + + Encrypted message + - - - - + + + - - + - - + - - - + + - - - - - - Message without header - - - - - - + transition={null} + width={36} + /> - - - -  - - - - + + + - - - - - - - -`; - -exports[`Story Snapshots: PinnedLargeFont should match snapshot 1`] = ` - - - - - - - - - - - - - - - - - - + > + John Doe + + + 10:00 AM + + + + - - + - diego.mello - - - - -  + Medium name (8 chars) - - - - 10:00 AM + - - - - - - - Message header - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + John Doe + + + 10:00 AM + + + + +  + + + +  + + + + +  + + + + +  + + + + +  + + + +  + + + + + + + Encrypted message + - - - - + + + - - + - - + - - - + + - - - - - - Message without header - - - - - - + transition={null} + width={36} + /> - - - -  - - - - + + + - - - - - - - -`; - -exports[`Story Snapshots: Reactions should match snapshot 1`] = ` - - - - - - - - - - - - - - - - - - + > + John Doe With An Extremely Long Display Name That Should Definitely Be Truncated In Any Reasonable User Interface Layout + + + 10:00 AM + + + + - - - - diego.mello - - - 10:00 AM - - - - - - - - - - - Reactions - - - - - - - - - - 😂 - - - 1 - - - - - - - - 99 - - - - - - - 🤔 - - - 999 - - - - - - - 🤔 - - - 9999 - - - - - - -  - - - - - + Long name (22 chars) - should truncate + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + John Doe With An Extremely Long Display Name That Should Definitely Be Truncated In Any Reasonable User Interface Layout + + + 10:00 AM + + + + +  + + + +  + + + + +  + + + + +  + + + + +  + + + +  + + + + + + + Encrypted message + - - - - + + + + +`; + +exports[`Story Snapshots: Pinned should match snapshot 1`] = ` + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + +  + + + + + - - - - diego.mello - - - 10:00 AM - - - - - - - - - - - Multiple Reactions - - - - - - - - - - - 1 - - - - - - - - 1 - - - - - - - - 1 - - - - - - - ❤️ - - - 1 - - - - + + - - - 🐶 - - - 1 - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + Message without header + + + + + + + + + + +  + + + + + + + + + +`; + +exports[`Story Snapshots: PinnedLargeFont should match snapshot 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + diego.mello + + + + +  + + + + + 10:00 AM + + + + + + + - - - 😀 - - - 1 - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - 😬 - - - 1 - - - - + + + + + + + + + +  + + + + + + + + + +`; + +exports[`Story Snapshots: Reactions should match snapshot 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + diego.mello + + + 10:00 AM + + + + + + + + + + - - - 😁 - - - 1 - - - - + + + + + + + + + 😂 + + + 1 + + + + + + + + 99 + + + + + + + 🤔 + + + 999 + + + + + + - - -  - - - - + } + > + 🤔 + + + 9999 + + + + + + +  + - + - - - - - -`; - -exports[`Story Snapshots: ReactionsLargeFont should match snapshot 1`] = ` - - - + + + - - + + + + + + + + + + + + + - - + diego.mello + + - + 10:00 AM + + + + + + + + - - - - - - - + "color": "#2F343D", + }, + ] + } + > + Multiple Reactions + + + - + + 1 + + + + + + - - diego.mello - - - + + 1 + - + + - 10:00 AM - - - - - - - - Reactions - - - - - - - - - - 😂 - - - 1 - - - - - - - - 99 - - - - - - - 🤔 - - - 999 - - - - + - - - 🤔 - - - 9999 - - - - + 1 + + + + + + + ❤️ + + - - -  - - - - + "color": "#1D74F5", + }, + ] + } + > + 1 + - - - - - - - - - - - - - - - - - - - + - + 🐶 + + - - - - - - + 1 + + - - - + 😀 + + + 1 + + + + + + + 😬 + + + 1 + + + + + + - - diego.mello - - - + + > + 1 + - + + - 10:00 AM - - - - - - - - Multiple Reactions - - - - - - - - - - - 1 - - - - - - - - 1 - - - - - - - - 1 - - - - - - - ❤️ - - - 1 - - - - - - - 🐶 - - - 1 - - - - +  + + + + + + + + + + + + + + +`; + +exports[`Story Snapshots: ReactionsLargeFont should match snapshot 1`] = ` + + + + + + + + + + + + + + + + + + + + + + + + + diego.mello + + + + + + 10:00 AM + + + + + + + - - - 😀 - - - 1 - - - - + + + + + + + + + 😂 + + + 1 + + + + + + + - - - 😬 - - - 1 - - - - + 99 + + + + + + + 🤔 + + + 999 + + + + + + - - - 😁 - - - 1 - - - - + 🤔 + + + 9999 + + + + + + - - -  - - - - + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + - + - - - - - -`; - -exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should match snapshot 1`] = ` - - - + + + - - + + + + + + + + + + + + + - + + + diego.mello + + + + + + 10:00 AM + + + + + + + + Multiple Reactions + + + + + + + + + + 1 + + + + + + + + 1 + + + + + + + + 1 + + + + - - + - + ❤️ + + - - - - - - + 1 + + - - - - - - diego.mello - - - 10:00 AM - - - - - - - - - - - How are you? - - - - - - - - - View thread - - - - - - -  - - - 1 - - - - -  - - - 0 - - - - - - - - - - -  - - - - - - - + } + 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-:dog:" + > + + + 🐶 + + + 1 + - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - + 1 + + + + + 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-:grimacing:" + > + + + 😬 + + + 1 + + + - - I'm fine! - + + 😁 + + + 1 + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + - + - - - - + + + + +`; + +exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should match snapshot 1`] = ` + + + - - + - + - - + - - - - - - - - - - - + /> - + - This is a description - + style={ + { + "borderRadius": 4, + "height": 36, + "width": 36, + } + } + transition={null} + width={36} + /> - - + + - - - - - - - -`; - -exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont should match snapshot 1`] = ` - - - - - - - - - + diego.mello + + - + 10:00 AM + + + + + + + + - - - - - - - + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + ] + } + > + How are you? + + + - + View thread + + + + + +  + - diego.mello + 1 - - - 10:00 AM - - - - - - - - How are you? - - - - - - - +  + + - - View thread - - - + + + + + + - - -  - - - 1 - - - - -  - - - 0 - - - + /> - - - - - - -  - - - - +  + - - + + - + - - - - + + + - - + - + - - + - - - - - - - - - - - + /> - + - I'm fine! - + style={ + { + "borderRadius": 4, + "height": 20, + "width": 20, + } + } + transition={null} + width={20} + /> - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - - - - + I'm fine! + + - - - - + + + - - + - + - - + - + + - - - - - - - - - + + + + + + + + - - - - This is a description - - - - + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + - - - - - -`; - -exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should match snapshot 1`] = ` - - - + + + - - + - + - -  - - - + + + + + + + + + - How are you? - + } + > -  + This is a description + + + + + + + + +`; + +exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButtonLargeFont should match snapshot 1`] = ` + + + + + + + + - + - - + - + + - - - - - - - - - + + + + + + + + - - - I'm fine! - - + diego.mello + - - - - - - - - - - - - - - - + + + + 10:00 AM + - + - - - - - - - + > + How are you? + + + + - - - Cool! + View thread - - - - - - - - - - - - - - - - - - - - - - +  + + + 1 + + + + +  + + + > + 0 + - - - - - - - - + - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - + + + + + + +  + + + + + - + - - - - + + + - - + - + - - + - + + - - - - - - - - - + + + + + + + + - - - - This is a description - - - - + I'm fine! + + - - - - - -`; - -exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont should match snapshot 1`] = ` - - - + + + - - + - + - -  - - - + + + + + + + + + - How are you? - + } + > -  + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + + + + + + + + + + - + - - + - - - - - - - - - - - + /> - + - I'm fine! - + style={ + { + "borderRadius": 4, + "height": 26, + "width": 26, + } + } + transition={null} + width={26} + /> - - + + + + + + + + + This is a description + + - - - - + + + + +`; + +exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReply should match snapshot 1`] = ` + + + - - + + +  + + + - + + - + + + + + + - - + - + + - - - - - - - - - + + + + + + + + - - - - Cool! - - - - + I'm fine! + + - - - - + + + - - + - + - - + - + + - - - - - - - - - + + + + + + + + - - - - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - - - - + Cool! + + - - - - + + + - - + - + - - + - + + - - - - - - - - - + + + + + + + + - - - - This is a description - - - - + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + - - - - - -`; - -exports[`Story Snapshots: ShowButtonAsAttachment should match snapshot 1`] = ` - - - + + + - - + - - + - - - - - - - - - - - - - - - - - diego.mello - - - 10:00 AM - - - + + - - - - - - - - Test Button - - - - - - - - Text button - - - - + transition={null} + width={20} + /> - + + + + + + + + + This is a description + - + - - - - + + + + +`; + +exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadReplyLargeFont should match snapshot 1`] = ` + + + - - + - - + + + How are you? + + + +  + + + + + + + - - + + - - - - - - - + } + transition={null} + width={26} + /> - - + + + + + + + + I'm fine! + + + + + + + + + + + + + + + + + + - - - - diego.mello - - - 10:00 AM - - - + + - - - - - - - - 🥑 - - - - - - - - Message with markdown - - - - - - - - - - - - Some text - - - - - - - - - - This is a test - - - - - - - - Text button - - - - + transition={null} + width={26} + /> + + + + + + + + + + Cool! + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This is a description + - + - - - + + + `; -exports[`Story Snapshots: ShowButtonAsAttachmentLargeFont should match snapshot 1`] = ` +exports[`Story Snapshots: ShowButtonAsAttachment should match snapshot 1`] = ` - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + - diego.mello + Test Button - - + + + + - + } + /> - 10:00 AM + Text button - - - - - - - Test Button - - - - - - - - Text button - - - - - + - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + + + 🥑 + + - diego.mello + - - + + + Message with markdown + + + + + + - + } + /> - 10:00 AM - - - - - - - 🥑 - - - - - - - - Message with markdown - - - - - - - - - - - Some text - - - - - - - - - - This is a test - + Some text - - - - - Text button - - - - + + + + + + + + This is a test + + + + + + + Text button + + - + - - - + + + `; -exports[`Story Snapshots: StaticAvatar should match snapshot 1`] = ` +exports[`Story Snapshots: ShowButtonAsAttachmentLargeFont should match snapshot 1`] = ` - - - + + + + + + + + + + + + + + + + + + + + diego.mello + + + + + + 10:00 AM + + + + + + + + Test Button + + + + + + + + Text button + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - + - diego.mello + 🥑 - 10:00 AM + - - - - - - - - Message - + Message with markdown - - - - - - - - - - - - - - - -`; - -exports[`Story Snapshots: StaticAvatarLargeFont should match snapshot 1`] = ` - - - - - - - - - - - - + + + - + - - - - + - - - - - - - + > + Some text + + + + + - + + - diego.mello + This is a test - - + + + + - + } + /> - 10:00 AM + Text button - - - - - - - Message - - - - - - - + - + - - - + + + `; -exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` +exports[`Story Snapshots: StaticAvatar should match snapshot 1`] = ` - - - - + - - + + - - - - - - - - - - diego.mello - - - - message removed - - - - - - - - - - - - - + + + + + + + + - - - + + + diego.mello + + + 10:00 AM + + + - - + + + + + + + Message + + + + + + + + - - - - - diego.mello - - - - joined the channel - - - - - - + + +`; + +exports[`Story Snapshots: StaticAvatarLargeFont should match snapshot 1`] = ` + + + - - - + - - + + - - - - - - - + + + + + + + + - - + - diego.mello - - - + + + diego.mello + + + + + + 10:00 AM + + - Pinned a message: - - - + "gap": 4, + } + } + > + + + + + + Message + + + + + + + + - + - - + + +`; + +exports[`Story Snapshots: SystemMessages should match snapshot 1`] = ` + + + - - - - + + - - - - - - + + + - + + - - - - diego.mello - - - - left the channel - + + + diego.mello - - + + + message removed + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - removed rocket.cat - + > + diego.mello - - + + + joined the channel + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - added rocket.cat - + > + diego.mello - - + + + Pinned a message: + + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - muted rocket.cat - + > + diego.mello - - + + + left the channel + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - unmuted rocket.cat - + > + diego.mello - - + + + removed rocket.cat + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - defined rocket.cat as admin - + > + diego.mello - - + + + added rocket.cat + + + - - + - - - - + + - - - - - - + + + - + + - + + diego.mello + + + + muted rocket.cat + + + + + + + + + + + - - - diego.mello - - - + + + - removed rocket.cat as admin - + "borderRadius": 4, + "height": 20, + "width": 20, + } + } + transition={null} + width={20} + /> + + + + + + + + + + diego.mello - - + + + unmuted rocket.cat + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - changed room name to: New name - + > + diego.mello - - + + + defined rocket.cat as admin + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - changed room description to: new description - + > + diego.mello - - + + + removed rocket.cat as admin + + + - - + - - - - + + - - - - - - + + + - + + - - - - diego.mello - - - - changed room announcement to: new announcement - + + + diego.mello - - + + + changed room name to: New name + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - changed room topic to: new topic - + > + diego.mello - - + + + changed room description to: new description + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - changed room to public - + > + diego.mello - - + + + changed room announcement to: new announcement + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - disabled E2E encryption for this room - + > + diego.mello - - + + + changed room topic to: new topic + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - enabled E2E encryption for this room - + > + diego.mello - - + + + changed room to public + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - removed @rocket.cat from this team - + > + diego.mello - - + + + disabled E2E encryption for this room + + + - - + - - - - + + - - - - - - + + + - + + - + + diego.mello + + + + enabled E2E encryption for this room + + + + + + + + + + + - - - diego.mello - - - + + + - added @rocket.cat to this team - + "borderRadius": 4, + "height": 20, + "width": 20, + } + } + transition={null} + width={20} + /> + + + + + + + + + + diego.mello - - + + + removed @rocket.cat from this team + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - added #channel-name to this team - + > + diego.mello - - + + + added @rocket.cat to this team + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - converted #channel-name to a team - + > + diego.mello - - + + + added #channel-name to this team + + + - - + - - - - + + - - - - - - + + + - + + - - - - diego.mello - - - - converted #channel-name to channel - + + + diego.mello - - + + + converted #channel-name to a team + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - deleted #channel-name - + > + diego.mello - - + + + converted #channel-name to channel + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - removed #channel-name from this team - + > + diego.mello - - + + + deleted #channel-name + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - was removed by ABAC - + > + diego.mello - - + + + removed #channel-name from this team + + + - - + - - - - + + - - - - - - + + + - + + - + + > + diego.mello + + - Unsupported system message + was removed by ABAC - - + + + + + + + + + + + + + + + + + + + + + + + Unsupported system message + + - + `; @@ -142311,6489 +137399,6688 @@ exports[`Story Snapshots: SystemMessagesLargeFont should match snapshot 1`] = ` } > - - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - message removed - + > + diego.mello - - + + + message removed + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - joined the channel - + > + diego.mello - - + + + joined the channel + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - Pinned a message: - + > + diego.mello - - - + + + Pinned a message: + + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - left the channel - + > + diego.mello - - + + + left the channel + + + - - + - - - - + + - - - - - - + + + - + + - + + diego.mello + + + + removed rocket.cat + + + + + + + + + + + - - - diego.mello - - - + + + - removed rocket.cat - + "borderRadius": 4, + "height": 26, + "width": 26, + } + } + transition={null} + width={26} + /> + + + + + + + + + + diego.mello - - + + + added rocket.cat + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - added rocket.cat - + > + diego.mello - - + + + muted rocket.cat + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - muted rocket.cat - + > + diego.mello - - + + + unmuted rocket.cat + + + - - + - - - - + + - - - - - - + + + - + + - - - - diego.mello - - - - unmuted rocket.cat - + > + + + diego.mello - - + + + defined rocket.cat as admin + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - defined rocket.cat as admin - + > + diego.mello - - + + + removed rocket.cat as admin + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - removed rocket.cat as admin - + > + diego.mello - - + + + changed room name to: New name + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - changed room name to: New name - + > + diego.mello - - + + + changed room description to: new description + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - changed room description to: new description - + > + diego.mello - - + + + changed room announcement to: new announcement + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - changed room announcement to: new announcement - + > + diego.mello - - + + + changed room topic to: new topic + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - changed room topic to: new topic - + > + diego.mello - - + + + changed room to public + + + - - + - - - - + + + - - - - - - + + + - + + - + - - - diego.mello - - - - changed room to public - + > + diego.mello - - + + + disabled E2E encryption for this room + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - disabled E2E encryption for this room - + > + diego.mello - - + + + enabled E2E encryption for this room + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - enabled E2E encryption for this room - + > + diego.mello - - + + + removed @rocket.cat from this team + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - removed @rocket.cat from this team - + > + diego.mello - - + + + added @rocket.cat to this team + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - added @rocket.cat to this team - + > + diego.mello - - + + + added #channel-name to this team + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - added #channel-name to this team - + > + diego.mello - - + + + converted #channel-name to a team + + + - - + - - - - - - + + - - - - - - - - - - diego.mello - - - - converted #channel-name to a team - + "borderRadius": 4, + "height": 26, + "width": 26, + } + } + transition={null} + width={26} + /> + + + + + + + + + + diego.mello - - + + + converted #channel-name to channel + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - converted #channel-name to channel - + > + diego.mello - - + + + deleted #channel-name + + + - - + - - - - + + - - - - - - + + + - + + - + - - - diego.mello - - - - deleted #channel-name - + > + diego.mello - - + + + removed #channel-name from this team + + + - - + - - - - + + - - - - - - + + + - + + - - - - diego.mello - - - - removed #channel-name from this team - + + + diego.mello - - + + + was removed by ABAC + + + - - + - - - - + + - - - - - - + + + - + + - - - - diego.mello - - - - was removed by ABAC - - - - + Unsupported system message + + - - + + +`; + +exports[`Story Snapshots: Temp should match snapshot 1`] = ` + + + - - - + - - + + + + + + + + + + + + + + + diego.mello + + + 10:00 AM + + + - - + + + + + + + Temp message + + + + + + + + - - - - Unsupported system message - - - - + `; -exports[`Story Snapshots: Temp should match snapshot 1`] = ` +exports[`Story Snapshots: TempLargeFont should match snapshot 1`] = ` - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - + - diego.mello - - - 10:00 AM + Temp message - - - - - - - - - - Temp message - - - - - - + + - + - - - + + + `; -exports[`Story Snapshots: TempLargeFont should match snapshot 1`] = ` +exports[`Story Snapshots: ThumbnailFromServer should match snapshot 1`] = ` - - - + + + + + + + + + + + + + - + + + diego.mello + + + 10:00 AM + + + + + + - - - - + + Title + + + + > + + + + Image text + + + + - - + + + - - - - diego.mello - - - - - 10:00 AM - - - - - - - - Temp message - - - - - - + this is a thumbnail + + + - + - - - + + + `; -exports[`Story Snapshots: ThumbnailFromServer should match snapshot 1`] = ` +exports[`Story Snapshots: ThumbnailFromServerLargeFont should match snapshot 1`] = ` - - - + + + + + + + + + + + + + - + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - - diego.mello - - - 10:00 AM - - - - - + + Title + + + - - - - - Title - - - - - - - Image text - - - - - - - + Image text + + + - - - - - - - this is a thumbnail - - - - + } + transition={null} + width={80} + /> + + + + + + this is a thumbnail + + + + + - + - - - + + + `; -exports[`Story Snapshots: ThumbnailFromServerLargeFont should match snapshot 1`] = ` +exports[`Story Snapshots: TimeFormat should match snapshot 1`] = ` - - - + - - + - - - - - - - - - - - - - - - - - diego.mello - - - - - + + - 10:00 AM - - - - - - - - - - Title - - - - - - - Image text - - - - - - - - - - - - - - - - this is a thumbnail - - - - - - + "borderRadius": 4, + "height": 36, + "width": 36, + } + } + transition={null} + width={36} + /> - - - + + + - - - - - - - -`; - -exports[`Story Snapshots: TimeFormat should match snapshot 1`] = ` - - - - - - - - - - - - - - - - - - + > + diego.mello + + + 10 November 2017 + + + + - - + - diego.mello - - - 10 November 2017 + Testing - - - - - - - - - - Testing - - - - - - + + - + - - - + + + `; @@ -151649,476 +146377,456 @@ exports[`Story Snapshots: TimeFormatLargeFont should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10 November 2017 + + + - - + - diego.mello + Testing - - - - - 10 November 2017 + - - - - - - - Testing - - - - - - - + - - - + + + `; @@ -152132,512 +146840,491 @@ exports[`Story Snapshots: Translated should match snapshot 1`] = ` } > - - - + - + + + + + + + + + + + + - + other.user + + - + 10:00 AM + + + + + - - - - - - - + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + - + + - - + - other.user - - - 10:00 AM + Message translated - - - - -  - - - - - - - - - - - Message translated - - - - - - + + - + - - - + + + `; @@ -152651,512 +147338,491 @@ exports[`Story Snapshots: TranslatedLargeFont should match snapshot 1`] = ` } > - - - + - + + + + + + + + + + + + - - - - - - - - - - + > + other.user + - - - - other.user - - - - - -  - - - - + "lineHeight": 20.8, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + + + + + 10:00 AM + + + + - 10:00 AM - - - - - - - - Message translated - - - - - - + Message translated + + + - + - - - + + + `; @@ -153170,1310 +147836,1290 @@ exports[`Story Snapshots: TwoShortCustomFieldsWithMarkdown should match snapshot } > - - - + - - + - - + + - - - - - - - + } + transition={null} + width={36} + /> - + + + + + + + - + diego.mello + + + 10:00 AM + + + + + + + - - - diego.mello - - - 10:00 AM - - - - - + + rocket.cat + + + + + + + Custom fields + + + + + + + Field 1 + - - - rocket.cat - - - - - Custom fields - + Value 1 - - + + + + + Field 2 + + + - - Field 1 - - - - - Value 1 - - - - - - - - Field 2 - - - - - - - Value 2 - - - + [ + { + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + { + "color": "#095AD2", + }, + ] + } + > + Value 2 - - - + + + + + + + + + + + rocket.cat + + + + + + + Custom fields 2 + + + + + + + Field 1 + - - rocket.cat + + Value 1 + - - + + + + + Field 2 + + + - Custom fields 2 - - - - - - - - Field 1 - - - - - - Value 1 - - - - - - - - Field 2 - - - - - - - Value 2 - - - + [ + { + "flexShrink": 1, + "fontSize": 16, + "lineHeight": 22, + }, + { + "color": "#2F343D", + }, + { + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontVariant": [ + "tabular-nums", + ], + "fontWeight": "700", + "textAlign": "left", + }, + ] + } + > + Value 2 - - - + + + - + + + + + + - - - - - Message - - - - - - + Message + + + - + - - - + + + `; @@ -154487,704 +149133,650 @@ exports[`Story Snapshots: TwoShortCustomFieldsWithMarkdownLargeFont should match } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - - diego.mello - - - - - - 10:00 AM - - + + rocket.cat + + + + + + + Custom fields + + + + + + + Field 1 + - - - rocket.cat - - - - - - Custom fields - - - - - - - - Field 1 - - - - - - Value 1 - - - - - - - Field 2 + Value 1 - - - - - - - Value 2 - - - - - - - - + + - - - - - - - - rocket.cat - - - - - - - Custom fields 2 - - - - - + Field 2 + + + - - Field 1 - - - - - Value 1 - - - - - - - - Field 2 - - - - - - - Value 2 - - - + Value 2 - - - + + + + + + + - + rocket.cat + + + + + + + Custom fields 2 + + + + + + - Message - - - - - - - - - - - - - - - - - - -`; - -exports[`Story Snapshots: URL should match snapshot 1`] = ` - - - - - - - - - - - - - - - - - + Field 1 + + + + + + Value 1 + + + + + + + > + + Field 2 + + + + + + + + Value 2 + + + + + + + + - - + + - - + - diego.mello - - - 10:00 AM + Message - - - - - - - - Rocket.Chat - Free, Open Source, Enterprise Team Chat - - - Rocket.Chat is the leading open source team chat software solution. Free, unlimited and completely customizable with on-premises and SaaS cloud hosting. - - - - - - - Google - - - Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. - - - - + + - + - - - - - - + + + + +`; + +exports[`Story Snapshots: URL should match snapshot 1`] = ` + + + + + - - + - - + + - - - - - - - + } + transition={null} + width={36} + /> - + + + + + + + - - + - - - diego.mello - - - 10:00 AM - - - - - - - - - - - Message - - - - - - - - - - - - Google - - - Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. - - - - - + "color": "#6C727A", + }, + ] + } + > + 10:00 AM + + - - - - - - - - - - - - - - - - - + - - - Google - - - Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. - - - - + "color": "#6C727A", + }, + ] + } + > + Rocket.Chat is the leading open source team chat software solution. Free, unlimited and completely customizable with on-premises and SaaS cloud hosting. + + 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", + }, + ] + } + > + + + Google + + + Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. + + + - + - - - - - -`; - -exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` - - - + + + - - + + + + + + + + + + + + + - - + diego.mello + + - + 10:00 AM + + + + + + + + - + + Message + - - - + + - - + + Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. + + + + + + + + + + + + + + + + + + + + + + + - - diego.mello - - - 10:00 AM - - - - - - + - - - Google - - - Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. - - - - + "color": "#6C727A", + }, + ] + } + > + Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. + - + + - - - - + + + + +`; + +exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - + Google + + - - diego.mello - - - 10:00 AM - - - - - - + + + + + + + + + + + + + + + + + + + + + + - + } + transition={null} + width={36} + /> + + + + + + + + + + diego.mello + + + 10:00 AM + + + + + - + - - - + + + `; @@ -158283,959 +152809,919 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + - + + + 10:00 AM + + + - - - - diego.mello - - - - + } + 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", + }, + ] + } + > + - 10:00 AM + Google - - - - - Google - - - Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. - - - - + "color": "#6C727A", + }, + ] + } + > + Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. + - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + - - - - - diego.mello - - - - - - 10:00 AM - - - - - - + /> - + + 10:00 AM + + + + + - - - + + + `; @@ -159249,2423 +153735,1733 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` } > - - - + - - + - - - - - - - - - - - - - - - - - - diego.mello - - - - - - 10:00 AM - - - - - - Rocket.Chat - Free, Open Source, Enterprise Team Chat - - - Rocket.Chat is the leading open source team chat software solution. Free, unlimited and completely customizable with on-premises and SaaS cloud hosting. - - - - - - - Google - - - Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. - - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + > + diego.mello + - - - - - diego.mello - - - - - - 10:00 AM - - - - - - - - Message - - - - - - - - - - - - Google - - - Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. - - - - - - + /> - - - - - - - - - - - - - - + + 10:00 AM + - - - + - - - Google - - - Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. - - - - + "color": "#6C727A", + }, + ] + } + > + Rocket.Chat is the leading open source team chat software solution. Free, unlimited and completely customizable with on-premises and SaaS cloud hosting. + + 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", + }, + ] + } + > + + + Google + + + Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. + + + - + - - - - - -`; - -exports[`Story Snapshots: Usernames should match snapshot 1`] = ` - - - + + + - - + + + + + + + + + + + + + - - - + diego.mello + + + + + + 10:00 AM + + + + + - + + Message + - - - + + - - + Google + + - - jd - - - 10:00 AM - - - - - - - - - - - Small username (2 chars) - - - - - - + Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. + - + - - - - + + + - - - - - - - - - - - - + /> - - jd - - - 10:00 AM - + + + Google + + + Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. + + + - + + + + + + + + + +`; + +exports[`Story Snapshots: Usernames should match snapshot 1`] = ` + + + + + + + + + + - -  - - - -  - - - - + + -  - - + "borderRadius": 4, + "height": 36, + "width": 36, + } + } + transition={null} + width={36} + /> + + + + + + + + -  + jd - - -  + 10:00 AM - -  - - - - - - - Encrypted message - + /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - john.doe - - - 10:00 AM - - - - - - - - - - - Medium username (8 chars) - - - - - - + Small username (2 chars) + + + - + - - - - + + + - - - - + - + + - - - - - - - - + } + transition={null} + width={36} + /> + + + + + + + - - + + 10:00 AM + + + + +  + + - + +  + + + + - john.doe - - +  + + + + +  + + + + - 10:00 AM - - - +  + + + +  + + + + + + + Encrypted message + + + + + + + + + + + + + + + - -  - - - -  - - - - + + -  - - + "borderRadius": 4, + "height": 36, + "width": 36, + } + } + transition={null} + width={36} + /> + + + + + + + + -  + john.doe - - -  + 10:00 AM - -  - - - - - - - Encrypted message - + /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - 10:00 AM - - - - - - - - - - - Long username (22 chars) - should truncate - - - - - - + Medium username (8 chars) + + + - + - - - - + + + - + + + + + + + + + + + + - + + john.doe + + + 10:00 AM + + + + +  + + + +  + + + - - + + + - - - - - + "lineHeight": 16, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + + + +  + + + +  + - - + + + + + + + + + + + + + + - - johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - - 10:00 AM - - + "backgroundColor": "#E4E7EA", + "borderBottomLeftRadius": undefined, + "borderBottomRightRadius": undefined, + "borderRadius": undefined, + "borderTopLeftRadius": undefined, + "borderTopRightRadius": undefined, + "bottom": 0, + "left": 0, + "opacity": 0, + "position": "absolute", + "right": 0, + "top": 0, + } + } + /> + + + + + + + + + - -  - -  + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername - - -  + 10:00 AM + + + - -  - - - - -  - + { + "color": "#2F343D", + }, + ] + } + > + + + Long username (22 chars) - should truncate + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + johndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusernamejohndoeverylongusername + + + 10:00 AM + + + + +  + + + +  + + + + +  + + + + -  - - + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + - - - Encrypted message - - + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + +  + + + + + + + Encrypted message + - + - + `; @@ -164036,990 +158360,950 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - + Diego Mello + - - - - - - - - + @ + diego.mello + + + + 10:00 AM + + + + - - + - - Diego Mello - - @ - diego.mello - - - - 10:00 AM - - - - - - - - - - - Message - - - - - - + Message + + + - + - - - - + + + - - + + + + + + + + + + + + + - - - + Diego Mello + - - - - - - - - + @ + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + + 10:00 AM + + + + - - - - Diego Mello - - @ - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - - - - 10:00 AM - - - - - - - - - - - Message - - - - - - + } + > + + + + Message + + + - + - - - + + + `; @@ -165033,990 +159317,950 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - + Diego Mello + - - - - - - - - + @ + diego.mello + + + + + 10:00 AM + + + - - + - Diego Mello - - @ - diego.mello - + Message - - - - - 10:00 AM + - - - - - - - Message - - - - - - - + - - - - + + + - - + + + + + + + + + + + + + - - - + Diego Mello + - - - - - - - - + @ + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + + + + + 10:00 AM + + + - - + - Diego Mello - - @ - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. - + Message - - - - - 10:00 AM + - - - - - - - Message - - - - - - - + - - - + + + `; @@ -166030,303 +160274,526 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - + diego.mello + + - + 10:00 AM + + + + + + + + - - - - + This is a description + + - - - - - - - + > + + + + + - diego.mello +  - + + - 10:00 AM - - - - - - + /> - - - - - This is a description - - - - - - - - -  - - - - - - - - - - - + "borderRadius": 2, + "height": 4, + "width": "100%", + }, + [Function], + { + "backgroundColor": "#156FF5", + }, + ] + } + /> + - + - - - - + + + - - + + - - + + + + Header false + + + + + + +  + + + + - - - - Header false - - - + /> - - -  - - - - - - - - - - - - + /> - - + + - - - - + + + - - + + - + + > + + + + multi file + + + + + - - - - - multi file - - - - + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + @@ -167486,235 +161843,235 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` style={ [ { - "alignItems": "center", - "borderRadius": 4, - "borderWidth": 1, - "flex": 1, - "flexDirection": "row", - "height": 56, + "borderRadius": 2, + "height": 4, + "width": "100%", }, { - "backgroundColor": "#FFFFFF", - "borderColor": "#EBECEF", + "backgroundColor": "#CBCED1", }, ] } > - -  - - - + + - - - - - - - - - + "backgroundColor": "#D1EBFE", + }, + [Function], + ] + } + /> + + + + + + + +  + + + + @@ -167722,409 +162079,730 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` style={ [ { - "alignItems": "center", - "borderRadius": 4, - "borderWidth": 1, - "flex": 1, - "flexDirection": "row", - "height": 56, + "borderRadius": 2, + "height": 4, + "width": "100%", }, { - "backgroundColor": "#FFFFFF", - "borderColor": "#EBECEF", + "backgroundColor": "#CBCED1", }, ] } > - -  - - - + + - - - - - - - - - + "backgroundColor": "#D1EBFE", + }, + [Function], + ] + } + /> - - + + - - - - + + + - - + + - - + + + +  + + + + + + + + + + + + + + + + + + This is a description + + + + + + + + + +  + + + + @@ -168132,576 +162810,235 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` style={ [ { - "alignItems": "center", - "borderRadius": 4, - "borderWidth": 1, - "flex": 1, - "flexDirection": "row", - "height": 56, + "borderRadius": 2, + "height": 4, + "width": "100%", }, { - "backgroundColor": "#FFFFFF", - "borderColor": "#EBECEF", + "backgroundColor": "#CBCED1", }, ] } > - -  - - - - - - - - - - - - - - - - - - - This is a description - - - - - - + /> - - -  - - - - - - - - - - - - + /> + + + + + + + +  + + + + @@ -168709,250 +163046,77 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` style={ [ { - "alignItems": "center", - "borderRadius": 4, - "borderWidth": 1, - "flex": 1, - "flexDirection": "row", - "height": 56, + "borderRadius": 2, + "height": 4, + "width": "100%", }, { - "backgroundColor": "#FFFFFF", - "borderColor": "#EBECEF", + "backgroundColor": "#CBCED1", }, ] } > - -  - - - + + - - - - - - - - - + "backgroundColor": "#D1EBFE", + }, + [Function], + ] + } + /> - - + + - - - + + + `; @@ -168966,303 +163130,526 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - + diego.mello + + + + + + 10:00 AM + + + + + - - - - + This is a description + + + - - - - - - - + transition={null} + width={15} + /> + + + + - diego.mello +  - - - 10:00 AM - - - + - - - - This is a description - - - - - - + /> - - -  - - - - - - - - - - - - + /> - + + + + + + + + + + + + + + + + + + + + + + First message + + + + + + + + - - - - + + + - - + + - - - - - - - First message - - + This is a description - - + + - - - - - - - - - - - - - - - - - - - - - - - +  + + + + - + - - - - This is a description - - - - + } + > - - -  - - - - - - - - - - - + > + + - - + + - - - - + + + - - + + - - + +  + + + + @@ -170588,475 +164845,455 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` style={ [ { - "alignItems": "center", - "borderRadius": 4, - "borderWidth": 1, - "flex": 1, - "flexDirection": "row", - "height": 56, + "borderRadius": 2, + "height": 4, + "width": "100%", }, { - "backgroundColor": "#FFFFFF", - "borderColor": "#EBECEF", + "backgroundColor": "#CBCED1", }, ] } > - -  - - - + + - - - - - - - - - + "backgroundColor": "#D1EBFE", + }, + [Function], + ] + } + /> - - + + - - - - + + + - - + + - + + > + + + + multi file + + + + + + - - - - - - multi file - - - - + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + @@ -171064,576 +165301,576 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` style={ [ { - "alignItems": "center", - "borderRadius": 4, - "borderWidth": 1, - "flex": 1, - "flexDirection": "row", - "height": 56, + "borderRadius": 2, + "height": 4, + "width": "100%", }, { - "backgroundColor": "#FFFFFF", - "borderColor": "#EBECEF", + "backgroundColor": "#CBCED1", }, ] } > - -  - - - + + - - - - - - - - - + "backgroundColor": "#D1EBFE", + }, + [Function], + ] + } + /> - + + + + + + + + This is a description + - - - - This is a description - - - - - - + } + transition={null} + width={15} + /> + + + + + + +  + + + + + - -  - - - + + - - - - - - - - - + "backgroundColor": "#D1EBFE", + }, + [Function], + ] + } + /> + + + + + + + +  + + + + @@ -171641,250 +165878,77 @@ exports[`Story Snapshots: WithAudioLargeFont should match snapshot 1`] = ` style={ [ { - "alignItems": "center", - "borderRadius": 4, - "borderWidth": 1, - "flex": 1, - "flexDirection": "row", - "height": 56, + "borderRadius": 2, + "height": 4, + "width": "100%", }, { - "backgroundColor": "#FFFFFF", - "borderColor": "#EBECEF", + "backgroundColor": "#CBCED1", }, ] } > - -  - - - + + - - - - - - - - - + "backgroundColor": "#D1EBFE", + }, + [Function], + ] + } + /> - - + + - - - + + + `; @@ -171898,975 +165962,874 @@ exports[`Story Snapshots: WithFile should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - - diego.mello - - - 10:00 AM - - - - - + - - - - - - - - File.pdf - - - - - - + File.pdf + + + + + + + + + + + This is a description + - - - - This is a description - - - - - - + } + transition={null} + width={15} + /> - - + + - + - - - - + + + - - + + - - - - - - - - - - File.pdf - - - - - - - + /> - This is a description + File.pdf - - - - + + + + - - + + - - - - - - - File.pdf - - - - - - + "uri": "https://open.rocket.chat/emoji-custom/nyan_rocket.png", + }, + ] + } + style={ + { + "height": 15, + "width": 15, + } + } + transition={null} + width={15} + /> + + + + + + + + + - This is a description + File.pdf - - - + + + + + This is a description + + + + + + + - - + + - - - + + + `; @@ -173259,554 +167283,473 @@ exports[`Story Snapshots: WithFileLargeFont should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + - - - + + + 10:00 AM + + + + + - - diego.mello - - - - - - 10:00 AM - - - - - - - - - - File.pdf - - - - - - - + /> - This is a description + File.pdf - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - File.pdf - - - - - - + } + transition={null} + width={15} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + - This is a description + File.pdf - - - + + + + + This is a description + + + + + + + - - + + - - - + + + `; @@ -174366,1109 +168350,1177 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - + diego.mello + + - + 10:00 AM + + + + + + + + - - - - + This is a description + + - - - - - - - + > + + + + + - - diego.mello - - + - 10:00 AM - - - - - - + /> - - - - - This is a description - - - - - - - - - - - - - -  - - - - - +  + + - + - - - - + + + - - + + + + + + + + + + + + + - + - + diego.mello + + - - - - - - - - - + > + 10:00 AM + + + + - - + - diego.mello - - - 10:00 AM + Message, not description - - - + + + + + + - - - - - Message, not description - - + This is a description - - - - - - - - This is a description - - - - - - - - + + + + + + + + + + - - - - - -  - - - - - + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + + + + + + + + + This is a description + - - - - This is a description - - - - - - - - + + + + + + + - + - - - - -  - - - + + - - + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + + + + + + - + - - - - -  - - - + + - - + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + - + - - - - + + + - - + + + + + + + + + + + + + - - + diego.mello + + - + 10:00 AM + + + + + + + + - - - - - - - + "color": "#2F343D", + }, + ] + } + > + multi file + + + + - + - diego.mello - + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > - 10:00 AM +  + + - - - - - - multi file - - - - - - - + - - - - -  - - - - - - + - + - - - - -  - - - - - + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + - + - - - - + + + - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - diego.mello - - - 10:00 AM - - - - - - - - - + - - - This is a description for Base64 Attachment - - - - - - - - + - - - - - - - - + transition={null} + width={15} + /> + + + + + + + + + + - + - - - + + + `; @@ -177725,2525 +171629,2445 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - diego.mello + + This is a description + - - + - - 10:00 AM - + + - - - - - This is a description - - - - - - - - - - -  - - - - - +  + + - + - - - - + + + - - + + - - - - - - - - - Header false - - - - - + + + + + + + + - + - - - - -  - - - - - + "lineHeight": 70.2, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + - - + + - - - - + + + - - + + + + + + + + + + + + + - - + diego.mello + + - + 10:00 AM + + + + + + + + - - - - - - - + "color": "#2F343D", + }, + ] + } + > + multi file + + + + - + - diego.mello - + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > - 10:00 AM +  + + - - - - - - multi file - - - - - - - + - - - - -  - - - - - - + - + - - - - -  - - - - - + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + + - + - - - - + + + - - - + + + + + + + + + + + + + + - - - + diego.mello + + + + + + 10:00 AM + + + + + - - - - + This is a description for Base64 Attachment + + - - - - - - - + > + + + + + - - diego.mello - - - - - - 10:00 AM - - - + /> - - - - - This is a description for Base64 Attachment - - - - - - - - - + + - - - - - - - - + "padding": 16, + }, + undefined, + ] + } + /> + - + - - - + + + `; @@ -180257,1744 +174081,1684 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + 10:00 AM + + + + - - - diego.mello - - 10:00 AM - - - - - - - + > + This is a description + - - - - This is a description - - - - - - + } + transition={null} + width={15} + /> - + + + + + - - - -  - - - - + }, + { + "borderColor": "#CBCED1", + "borderWidth": 1, + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + +  + - + - - - - + + + - - + + - - - - - - - - - Header false - - - - - + + + + + + + - - - -  - - - - + "color": "#2F343D", + "fontSize": 54, + }, + [ + { + "lineHeight": 54, + }, + undefined, + ], + { + "fontFamily": "custom", + "fontStyle": "normal", + "fontWeight": "normal", + }, + {}, + ] + } + > +  + - - + + - - - - + + + - - + + + + + + + + + + + + + - - + diego.mello + + - + 10:00 AM + + + + + + + + - - - - - - - + "color": "#2F343D", + }, + ] + } + > + multi file + + + - + - diego.mello - + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > - 10:00 AM - - - + > +  + + + + - - - - - - multi file - - - - - - - - - - -  - - - - - - + - - - -  - - - - + }, + { + "borderColor": "#CBCED1", + "borderWidth": 1, + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + +  + - + - - - + + + `; @@ -182008,1921 +175772,1861 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` } > - - - + + + + + + + + + + + + + - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - diego.mello - - - - - - 10:00 AM - - - - + + This is a description + - - - - This is a description - - - - - - + } + transition={null} + width={15} + /> - + + + + + - - - -  - - - - + }, + { + "borderColor": "#CBCED1", + "borderWidth": 1, + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + +  + - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + + - diego.mello +  - - + + + + + + + + + + + + + + + + + + + + + - 10:00 AM - - - - - - - - -  - - - - - - + "borderRadius": 4, + "height": 46.800000000000004, + "width": 46.800000000000004, + } + } + transition={null} + width={46.800000000000004} + /> - - - + + + - - - - - - - - - - - - - - - - - - - - - + > + diego.mello + + + + 10:00 AM + + + - - + - diego.mello + multi file - - - - - 10:00 AM + + + + + - - - - - - multi file - - - - - - - + - - - -  - - - - - + - + + + + + + + - - - -  - - - - + }, + { + "borderColor": "#CBCED1", + "borderWidth": 1, + }, + ], + { + "alignItems": "center", + "justifyContent": "center", + "position": "absolute", + }, + ] + } + > + +  + - + - - - + + + `; @@ -183936,238 +177640,218 @@ exports[`Story Snapshots: WithoutHeader should match snapshot 1`] = ` } > - - - + + - - - - - - - - - Message - - - - - - + Message + + + - - + + - - - + + + `; @@ -184181,238 +177865,218 @@ exports[`Story Snapshots: WithoutHeaderLargeFont should match snapshot 1`] = ` } > - - - + + - - - - - - - - - Message - - - - - - + Message + + + - - + + - - - + + + `; diff --git a/app/containers/message/components/stories/Attachments.stories.tsx b/app/containers/message/components/stories/Attachments.stories.tsx index 28ca40d4b68..378ed253719 100644 --- a/app/containers/message/components/stories/Attachments.stories.tsx +++ b/app/containers/message/components/stories/Attachments.stories.tsx @@ -4,15 +4,14 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { selectServerSuccess } from '../../../../actions/server'; +import { setUser } from '../../../../actions/login'; import { type TAnyMessageModel } from '../../../../definitions'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import Attachments from '../Attachments/Attachments'; const mockMessageContext: Partial = { - baseUrl: 'https://open.rocket.chat', - user: { id: 'user-id', username: 'rocket.cat', token: 'token' }, - showAttachment: undefined + handlers: { showAttachment: undefined } }; const mockItem = { id: 'msg-id', msg: '', u: { username: 'rocket.cat' }, autoTranslate: false } as unknown as TAnyMessageModel; @@ -50,6 +49,7 @@ const MOCK_MULTIPLE_IMAGES = [ const createServerStore = (version: string) => { const store = createMockedStore(); store.dispatch(selectServerSuccess({ server: 'https://open.rocket.chat', version, name: 'Test' })); + store.dispatch(setUser({ id: 'user-id', username: 'rocket.cat', token: 'token' })); return store; }; diff --git a/app/containers/message/components/stories/Broadcast.stories.tsx b/app/containers/message/components/stories/Broadcast.stories.tsx index c0c53a45251..9da56279b1c 100644 --- a/app/containers/message/components/stories/Broadcast.stories.tsx +++ b/app/containers/message/components/stories/Broadcast.stories.tsx @@ -2,12 +2,14 @@ import { type ReactNode } from 'react'; import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; +import { setUser } from '../../../../actions/login'; import { type TAnyMessageModel } from '../../../../definitions'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import BroadcastLeaf from '../Broadcast'; const store = createMockedStore(); +store.dispatch(setUser({ id: 'reader-id', username: 'rocket.cat' })); const otherAuthorItem = { id: 'msg-id', @@ -24,9 +26,7 @@ const ownAuthorItem = { } as unknown as TAnyMessageModel; const room: Partial = { - broadcast: true, - user: { id: 'reader-id', username: 'rocket.cat' }, - replyBroadcast: () => {} + broadcast: true }; const StoryWrapper = ({ item, children }: { item: TAnyMessageModel; children: ReactNode }) => ( diff --git a/app/containers/message/components/stories/CallButton.stories.tsx b/app/containers/message/components/stories/CallButton.stories.tsx index 0488be2988d..ff92c6c4088 100644 --- a/app/containers/message/components/stories/CallButton.stories.tsx +++ b/app/containers/message/components/stories/CallButton.stories.tsx @@ -16,9 +16,7 @@ const item = { autoTranslate: false } as unknown as TAnyMessageModel; -const room: Partial = { - handleEnterCall: () => {} -}; +const room: Partial = {}; const StoryWrapper = ({ children }: { children: ReactNode }) => ( diff --git a/app/containers/message/components/stories/CollapsibleQuote.stories.tsx b/app/containers/message/components/stories/CollapsibleQuote.stories.tsx index e1a5c57b0bd..a3ebd40049a 100644 --- a/app/containers/message/components/stories/CollapsibleQuote.stories.tsx +++ b/app/containers/message/components/stories/CollapsibleQuote.stories.tsx @@ -19,7 +19,7 @@ const item = { const StoryWrapper = ({ children }: { children: ReactNode }) => ( - + {children} diff --git a/app/containers/message/components/stories/Content.stories.tsx b/app/containers/message/components/stories/Content.stories.tsx index 87df5e33651..b70fff785d3 100644 --- a/app/containers/message/components/stories/Content.stories.tsx +++ b/app/containers/message/components/stories/Content.stories.tsx @@ -2,12 +2,14 @@ import { type ReactNode } from 'react'; import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; +import { setUser } from '../../../../actions/login'; import { type TAnyMessageModel } from '../../../../definitions'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import ContentLeaf from '../Content'; const store = createMockedStore(); +store.dispatch(setUser({ id: 'reader-id', username: 'reader' })); const item = { id: 'msg-id', @@ -17,9 +19,7 @@ const item = { autoTranslate: false } as unknown as TAnyMessageModel; -const room: Partial = { - user: { id: 'reader-id', username: 'reader' } -}; +const room: Partial = {}; const StoryWrapper = ({ children }: { children: ReactNode }) => ( diff --git a/app/containers/message/components/stories/Discussion.stories.tsx b/app/containers/message/components/stories/Discussion.stories.tsx index 8a96a8463f1..310d6466455 100644 --- a/app/containers/message/components/stories/Discussion.stories.tsx +++ b/app/containers/message/components/stories/Discussion.stories.tsx @@ -2,12 +2,14 @@ import { type ReactNode } from 'react'; import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; +import { setUser } from '../../../../actions/login'; import { type TAnyMessageModel } from '../../../../definitions'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import DiscussionLeaf from '../Discussion'; const store = createMockedStore(); +store.dispatch(setUser({ id: 'reader-id', username: 'reader' })); const item = { id: 'msg-id', @@ -18,10 +20,7 @@ const item = { autoTranslate: false } as unknown as TAnyMessageModel; -const room: Partial = { - user: { id: 'reader-id', username: 'reader' }, - onDiscussionPress: () => {} -}; +const room: Partial = {}; 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 d8b2c37816e..c2a78ef0872 100644 --- a/app/containers/message/components/stories/Message.stories.tsx +++ b/app/containers/message/components/stories/Message.stories.tsx @@ -17,6 +17,8 @@ import { import { mockedStore as store } from '../../../../reducers/mockedStore'; import { updateSettings } from '../../../../actions/settings'; import { setCustomEmojis } from '../../../../actions/customEmojis'; +import { setUser } from '../../../../actions/login'; +import { selectServerSuccess } from '../../../../actions/server'; import { createMessageActionStore, MessageActionStoreContext } from '../../stores/MessageActionStore'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; @@ -55,6 +57,8 @@ const date = new Date(2017, 10, 10, 10); const longText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'; +store.dispatch(setUser(user)); +store.dispatch(selectServerSuccess({ server: baseUrl, version: '', name: '' })); store.dispatch(updateSettings('API_Embed', true)); store.dispatch( setCustomEmojis({ @@ -81,22 +85,9 @@ const containerHandlers = { }; const roomHandlers: Partial = { - user, - baseUrl, reactionInit: () => {}, - replyBroadcast: () => {}, - onReactionPress: () => {}, - onEncryptedPress: () => {}, - onDiscussionPress: () => {}, - onThreadPress: () => {}, - onReactionLongPress: () => {}, - onAnswerButtonPress: () => {}, jumpToMessage: () => {}, - navToRoomInfo: () => {}, - showAttachment: undefined, - blockAction: undefined, - handleEnterCall: undefined, - fetchThreadName: undefined + handlers: { navToRoomInfo: () => {}, showAttachment: undefined } }; export default { diff --git a/app/containers/message/components/stories/MessageAvatar.stories.tsx b/app/containers/message/components/stories/MessageAvatar.stories.tsx index 59ab1850732..9efcd2061dc 100644 --- a/app/containers/message/components/stories/MessageAvatar.stories.tsx +++ b/app/containers/message/components/stories/MessageAvatar.stories.tsx @@ -3,6 +3,7 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { selectServerRequest } from '../../../../actions/server'; +import { setUser } from '../../../../actions/login'; import { type TAnyMessageModel } from '../../../../definitions'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; @@ -10,6 +11,7 @@ import MessageAvatarLeaf from '../MessageAvatar'; const store = createMockedStore(); store.dispatch(selectServerRequest('https://open.rocket.chat', '7.0.0')); +store.dispatch(setUser({ id: 'reader-id', username: 'reader' })); const item = { id: 'msg-id', @@ -18,9 +20,7 @@ const item = { autoTranslate: false } as unknown as TAnyMessageModel; -const room: Partial = { - user: { id: 'reader-id', username: 'reader' } -}; +const room: Partial = {}; 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 77af06be29c..27187e99c63 100644 --- a/app/containers/message/components/stories/Reactions.stories.tsx +++ b/app/containers/message/components/stories/Reactions.stories.tsx @@ -2,12 +2,14 @@ import { type ReactNode } from 'react'; import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; +import { setUser } from '../../../../actions/login'; import { type TAnyMessageModel } from '../../../../definitions'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import ReactionsLeaf from '../Reactions'; const store = createMockedStore(); +store.dispatch(setUser({ id: 'reader-id', username: 'rocket.cat' })); const item = { id: 'msg-id', @@ -21,9 +23,6 @@ const item = { } as unknown as TAnyMessageModel; const room: Partial = { - user: { id: 'reader-id', username: 'rocket.cat' }, - onReactionPress: () => {}, - onReactionLongPress: () => {}, reactionInit: () => {} }; diff --git a/app/containers/message/components/stories/RightIcons/Encrypted.stories.tsx b/app/containers/message/components/stories/RightIcons/Encrypted.stories.tsx index 6a7005128bb..d25ec66291b 100644 --- a/app/containers/message/components/stories/RightIcons/Encrypted.stories.tsx +++ b/app/containers/message/components/stories/RightIcons/Encrypted.stories.tsx @@ -26,9 +26,7 @@ const plainItem = { autoTranslate: false } as unknown as TAnyMessageModel; -const room: Partial = { - onEncryptedPress: () => {} -}; +const room: Partial = {}; const StoryWrapper = ({ item, children }: { item: TAnyMessageModel; children: ReactNode }) => ( diff --git a/app/containers/message/components/stories/RightIcons/Translated.stories.tsx b/app/containers/message/components/stories/RightIcons/Translated.stories.tsx index 969bf65ad0b..65ae0ca37eb 100644 --- a/app/containers/message/components/stories/RightIcons/Translated.stories.tsx +++ b/app/containers/message/components/stories/RightIcons/Translated.stories.tsx @@ -46,16 +46,14 @@ export default { // An auto-translated message from another user renders the language icon. export const Translated = () => ( - + ); // A message that is not translated renders nothing. export const TranslatedHidden = () => ( - + ); diff --git a/app/containers/message/components/stories/Thread.stories.tsx b/app/containers/message/components/stories/Thread.stories.tsx index 96f659d6a67..a00662fa05a 100644 --- a/app/containers/message/components/stories/Thread.stories.tsx +++ b/app/containers/message/components/stories/Thread.stories.tsx @@ -2,12 +2,14 @@ import { type ReactNode } from 'react'; import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; +import { setUser } from '../../../../actions/login'; import { type TAnyMessageModel } from '../../../../definitions'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import ThreadLeaf from '../Thread'; const store = createMockedStore(); +store.dispatch(setUser({ id: 'reader-id', username: 'reader' })); const item = { id: 'msg-id', @@ -20,8 +22,6 @@ const item = { } as unknown as TAnyMessageModel; const room: Partial = { - user: { id: 'reader-id', username: 'reader' }, - onThreadPress: () => {}, isThreadRoom: false }; diff --git a/app/containers/message/components/stories/Urls.stories.tsx b/app/containers/message/components/stories/Urls.stories.tsx index 790eafba882..3fa8dcf0580 100644 --- a/app/containers/message/components/stories/Urls.stories.tsx +++ b/app/containers/message/components/stories/Urls.stories.tsx @@ -3,6 +3,8 @@ import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; import { updateSettings } from '../../../../actions/settings'; +import { setUser } from '../../../../actions/login'; +import { selectServerSuccess } from '../../../../actions/server'; import { type TAnyMessageModel } from '../../../../definitions'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; @@ -10,6 +12,8 @@ import UrlsLeaf from '../Urls'; const store = createMockedStore(); store.dispatch(updateSettings('API_Embed', true)); +store.dispatch(setUser({ id: 'reader-id', username: 'reader', token: 'token' })); +store.dispatch(selectServerSuccess({ server: 'https://open.rocket.chat', version: '', name: '' })); const item = { id: 'msg-id', @@ -26,10 +30,7 @@ const item = { ] } as unknown as TAnyMessageModel; -const room: Partial = { - user: { id: 'reader-id', username: 'reader', token: 'token' }, - baseUrl: 'https://open.rocket.chat' -}; +const room: Partial = {}; const StoryWrapper = ({ children }: { children: ReactNode }) => ( diff --git a/app/containers/message/components/stories/User.stories.tsx b/app/containers/message/components/stories/User.stories.tsx index 8166af09f59..3e7f105d773 100644 --- a/app/containers/message/components/stories/User.stories.tsx +++ b/app/containers/message/components/stories/User.stories.tsx @@ -2,12 +2,14 @@ import { type ReactNode } from 'react'; import { Provider } from 'react-redux'; import { createMockedStore } from '../../../../reducers/mockedStore'; +import { setUser } from '../../../../actions/login'; import { type TAnyMessageModel } from '../../../../definitions'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { MessageProvider } from '../../stores/MessageStore'; import UserLeaf from '../User'; const store = createMockedStore(); +store.dispatch(setUser({ id: 'reader-id', username: 'reader' })); const item = { id: 'msg-id', @@ -21,7 +23,6 @@ const item = { } as unknown as TAnyMessageModel; const room: Partial = { - user: { id: 'reader-id', username: 'reader' }, timeFormat: 'LT' }; diff --git a/app/containers/message/docs/ARCHITECTURE.md b/app/containers/message/docs/ARCHITECTURE.md deleted file mode 100644 index d888b33ac6b..00000000000 --- a/app/containers/message/docs/ARCHITECTURE.md +++ /dev/null @@ -1,104 +0,0 @@ -# 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` 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` + React Compiler - -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.). Memoization comes from React Compiler (`babel-plugin-react-compiler`, `compilationMode: 'infer'`) rather than hand-written `useMemo`/`useCallback`. - -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 by `variant` — `content` (the default) keeps 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`), `quote` keeps quote attachments and renders each as a `Reply`. It owns the nested-quote recursion: `Reply` takes the nested lists as `children`, so the recursion stays inside this one module. Callers pick the variant directly; there is no separate `Quote` component. -- **`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. diff --git a/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx b/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx index 679005cf479..69c2a2d9c55 100644 --- a/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx +++ b/app/containers/message/hooks/__tests__/useMediaAutoDownload.test.tsx @@ -6,6 +6,8 @@ import { useMediaAutoDownload } from '../useMediaAutoDownload'; import { MessageProvider } from '../../stores/MessageStore'; import { MessageRoomProvider, type MessageRoomState } from '../../stores/MessageRoomStore'; import { mockedStore } from '../../../../reducers/mockedStore'; +import { setUser } from '../../../../actions/login'; +import { selectServerSuccess } from '../../../../actions/server'; import { type IAttachment, type IUserMessage, type TAnyMessageModel } from '../../../../definitions'; import { cancelDownload, downloadMediaFile, getMediaCache, isDownloadActive } from '../../../../lib/methods/handleMediaDownload'; import { fetchAutoDownloadEnabled } from '../../../../lib/methods/autoDownloadPreference'; @@ -59,6 +61,9 @@ const mockEmitterOn = emitter.on as jest.Mock; const URL = 'https://open.rocket.chat/file.png'; const USER = { id: 'user-1', username: 'john', token: 'token' }; +mockedStore.dispatch(setUser(USER)); +mockedStore.dispatch(selectServerSuccess({ server: 'https://open.rocket.chat', version: '', name: '' })); + // 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 = () => @@ -79,7 +84,7 @@ const renderMediaHook = ({ showAttachment?: (file: IAttachment) => void; ctx?: Partial; }) => { - const contextValue: Partial = { baseUrl: 'https://open.rocket.chat', user: USER, ...ctx }; + const contextValue: Partial = { ...ctx }; const item = { id: 'msg-1' } as unknown as TAnyMessageModel; const wrapper = ({ children }: { children: ReactNode }) => ( diff --git a/app/containers/message/hooks/__tests__/useMessageAccessibilityLabel.test.tsx b/app/containers/message/hooks/__tests__/useMessageAccessibilityLabel.test.tsx index 3b78be202a2..dd443af5353 100644 --- a/app/containers/message/hooks/__tests__/useMessageAccessibilityLabel.test.tsx +++ b/app/containers/message/hooks/__tests__/useMessageAccessibilityLabel.test.tsx @@ -123,8 +123,7 @@ describe('useMessageAccessibilityLabel', () => { expect( renderLabel(buildItem({ autoTranslate: true, translations: [{ _id: 't1', language: 'en', value: 'translated text' }] }), { autoTranslateRoom: true, - autoTranslateLanguage: 'en', - user: { username: 'bob' } + autoTranslateLanguage: 'en' }) ).toBe(`alice ${HOUR} Message translated into English.`); }); @@ -139,7 +138,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' } ) ).toBe(`alice ${HOUR} Message translated into English. Image description: A wavy pattern Message was not read`); }); diff --git a/app/containers/message/hooks/useMediaAutoDownload.tsx b/app/containers/message/hooks/useMediaAutoDownload.tsx index 8b9200f3fc2..44f9bc79082 100644 --- a/app/containers/message/hooks/useMediaAutoDownload.tsx +++ b/app/containers/message/hooks/useMediaAutoDownload.tsx @@ -79,15 +79,14 @@ export const useMediaAutoDownload = ({ const baseUrl = useBaseUrl(); const user = useMessageUser(); const [status, dispatchDownloadEvent] = useReducer(downloadStatusReducer, 'to-download'); - // Local overrides (downloaded uri, decrypted state) win over the file prop, which may still carry the remote url const [fileOverrides, setFileOverrides] = useState | null>(null); const currentFile = fileOverrides ? { ...file, ...fileOverrides } : file; 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'; @@ -127,7 +126,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(); @@ -138,7 +137,7 @@ export const useMediaAutoDownload = ({ try { dispatchDownloadEvent('download_started'); const uri = await downloadMediaFile({ - messageId: id ?? '', + messageId: id, downloadUrl: url, type: fileType, mimeType: getFileProperty(currentFile, fileType, 'type'), diff --git a/app/containers/message/stores/A11yGate.tsx b/app/containers/message/stores/A11yGate.tsx new file mode 100644 index 00000000000..10a47a5db9a --- /dev/null +++ b/app/containers/message/stores/A11yGate.tsx @@ -0,0 +1,17 @@ +import { createContext, use, type ReactElement, type ReactNode } from 'react'; + +import { useIsAccessibilityNavigationEnabled } from '../../../lib/hooks/useIsAccessibilityNavigationEnabled'; + +// One boolean per room: is accessibility navigation (screen reader OR external keyboard) active? +// Message rows read it to skip the react-native-a11y-order wrappers when nobody needs them. +// Defaults false so message leaves rendered outside a provider (stories, isolated tests) render +// gated-off — identical to how they render for a touch user with no assistive tech. +export const A11yGateContext = createContext(false); + +export const useA11yGate = (): boolean => use(A11yGateContext); + +export const A11yGateProvider = ({ children }: { children: ReactNode }): ReactElement => { + const enabled = useIsAccessibilityNavigationEnabled(); + + return {children}; +}; diff --git a/app/containers/message/stores/MessageActionStore.tsx b/app/containers/message/stores/MessageActionStore.tsx index b96641d335a..fd415da8b00 100644 --- a/app/containers/message/stores/MessageActionStore.tsx +++ b/app/containers/message/stores/MessageActionStore.tsx @@ -53,44 +53,46 @@ export type TMessageActionStore = ReturnType; export const MessageActionStoreContext = createContext(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 -}; +const inertStore = createStore>()(() => ({ action: null })); -// Rows rendered outside a RoomView (search, pinned) can never be in edit mode. -// `action` is fixed at null (no `set` param) and its actions always throw — a safe, inert fallback. -export const inertStore = createStore()(() => ({ action: null, actions: NO_OP_ACTIONS })); - -const useMessageActionStore = (selector: (state: MessageActionState) => T): T => { +export const useMessageActionStoreApi = (): TMessageActionStore => { const store = useContext(MessageActionStoreContext); if (!store) { throw new Error('Message action hooks must be used within a MessageActionProvider'); } - return useStore(store, selector); + return store; }; +const useMessageActionStore = (selector: (state: MessageActionState) => T): T => + useStore(useMessageActionStoreApi(), selector); + // `action` is a single ref replaced wholesale on every `set` — no useShallow needed. export const useMessageAction = (): TMessageActionState => useMessageActionStore(s => s.action); +export const useMessageActionKind = (): NonNullable['kind'] | null => + useMessageActionStore(s => s.action?.kind ?? null); + /** * 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) ?? inertStore; - return useStore(store, s => s.action?.kind === 'edit' && s.action.messageId === messageId); + return useStore( + store, + (s: Pick) => s.action?.kind === 'edit' && s.action.messageId === messageId + ); }; +// Stable ref so the selector doesn't emit a fresh array when not quoting. +const EMPTY_MESSAGE_IDS: string[] = []; + +export const useQuotedMessageIds = (): string[] => + useMessageActionStore(s => (s.action?.kind === 'quote' ? s.action.messageIds : EMPTY_MESSAGE_IDS)); + +export const useEditingMessageId = (): string | undefined => + useMessageActionStore(s => (s.action?.kind === 'edit' ? s.action.messageId : undefined)); + export const MessageActionProvider = ({ store: externalStore, initialAction, diff --git a/app/containers/message/stores/MessageRoomStore.tsx b/app/containers/message/stores/MessageRoomStore.tsx index 8f6914c6c07..8c204c29a45 100644 --- a/app/containers/message/stores/MessageRoomStore.tsx +++ b/app/containers/message/stores/MessageRoomStore.tsx @@ -1,45 +1,38 @@ -import { createContext, useContext, useEffect, useRef, useState, type ReactElement, type ReactNode } from 'react'; +import { createContext, useContext, useEffect, useMemo, useState, type ReactElement, type ReactNode } from 'react'; import { createStore, useStore } from 'zustand'; import { useShallow } from 'zustand/react/shallow'; -import { type IAttachment, type IRoomInfoParam, type TAnyMessageModel } from '../../../definitions'; +import { type IUseRoomMessageHandlersResult, type IUser, type TAnyMessageModel } from '../../../definitions'; +import { useAppSelector } from '../../../lib/hooks/useAppSelector'; +import { useLiveRef } from '../../../lib/hooks/useLiveRef'; import { useSetting } from '../../../lib/hooks/useSetting'; +import { getUserSelector } from '../../../selectors/login'; -export type MessageRoomState = { - // stable handlers - 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; - toggleFollowThread?: (isFollowingThread: boolean, tmid?: string) => Promise; +type LiveCallbacks = { jumpToMessage?: (link: string) => void; closeEmojiAndAction?: (action?: (params?: unknown) => void, params?: unknown) => void; - // row action handlers - onReactionPress?: (emoji: string, id: string) => void; - onReactionLongPress?: (item: TAnyMessageModel) => void; reactionInit?: (messageId: string) => void; - onDiscussionPress?: (drid: TAnyMessageModel['drid']) => void; - onThreadPress?: (item: TAnyMessageModel) => void; - replyBroadcast?: (item: TAnyMessageModel) => void; errorActionsShow?: (item: TAnyMessageModel) => void; - onAnswerButtonPress?: (message: string, tshow?: boolean) => void; - onEncryptedPress?: () => void; - archived?: boolean; - isReadReceiptEnabled?: boolean; - // room constants +}; + +export type MessageRoomState = LiveCallbacks & { + handlers?: Partial; rid?: string; - user?: { id?: string; username?: string; token?: string }; - baseUrl?: string; - broadcast?: boolean; isThreadRoom?: boolean; + tmid?: string; + archived?: boolean; + broadcast?: boolean; + isReadReceiptEnabled?: boolean; Message_GroupingPeriod?: number; timeFormat?: string; - // reactive tail (provider keeps current) autoTranslateRoom?: boolean; autoTranslateLanguage?: string; }; +type MessageRoomSnapshot = { [K in keyof Required]: MessageRoomState[K] }; + +type LiveCallbacksSnapshot = { [K in keyof Required]: LiveCallbacks[K] }; + export const createMessageRoomStore = (initial: MessageRoomState) => createStore(() => ({ ...initial })); export type MessageRoomStore = ReturnType; @@ -54,74 +47,60 @@ const useMessageRoomStore = (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 useLiveCallbacks = (callbacks: LiveCallbacks): LiveCallbacksSnapshot => { + const latest = useLiveRef(callbacks); + const [wrappers] = useState>(() => ({ + jumpToMessage: (...args) => latest.current.jumpToMessage?.(...args), + closeEmojiAndAction: (...args) => latest.current.closeEmojiAndAction?.(...args), + reactionInit: (...args) => latest.current.reactionInit?.(...args), + errorActionsShow: (...args) => latest.current.errorActionsShow?.(...args) + })); + + const hasJumpToMessage = !!callbacks.jumpToMessage; + const hasCloseEmojiAndAction = !!callbacks.closeEmojiAndAction; + const hasReactionInit = !!callbacks.reactionInit; + const hasErrorActionsShow = !!callbacks.errorActionsShow; + + return useMemo( + () => ({ + jumpToMessage: hasJumpToMessage ? wrappers.jumpToMessage : undefined, + closeEmojiAndAction: hasCloseEmojiAndAction ? wrappers.closeEmojiAndAction : undefined, + reactionInit: hasReactionInit ? wrappers.reactionInit : undefined, + errorActionsShow: hasErrorActionsShow ? wrappers.errorActionsShow : undefined + }), + [wrappers, hasJumpToMessage, hasCloseEmojiAndAction, hasReactionInit, hasErrorActionsShow] + ); }; -const useFrozenHandlersGuard: (state: MessageRoomState) => void = __DEV__ - ? useFrozenHandlersGuardDev - : useFrozenHandlersGuardProd; - -const MessageRoomStoreProvider = ({ children, ...state }: { children: ReactNode } & MessageRoomState): ReactElement => { - const [store] = useState(() => createMessageRoomStore(state)); - useFrozenHandlersGuard(state); +export const MessageRoomProvider = ({ children, ...state }: { children: ReactNode } & MessageRoomState): ReactElement => { + const timeFormatSetting = useSetting('Message_TimeFormat') as string; + const timeFormat = state.timeFormat ?? timeFormatSetting; + const callbacks = useLiveCallbacks(state); + const [store] = useState(() => createMessageRoomStore({ ...state, ...callbacks, timeFormat })); - // 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, + const snapshot: MessageRoomSnapshot = { + ...callbacks, + handlers: state.handlers, + rid: state.rid, + isThreadRoom: state.isThreadRoom, + tmid: state.tmid, + timeFormat, autoTranslateRoom: state.autoTranslateRoom, autoTranslateLanguage: state.autoTranslateLanguage, archived: state.archived, broadcast: state.broadcast, isReadReceiptEnabled: state.isReadReceiptEnabled, Message_GroupingPeriod: state.Message_GroupingPeriod - }); + }; + store.setState(snapshot); }, [ - state.timeFormat, + callbacks, + state.handlers, + state.rid, + state.isThreadRoom, + state.tmid, + timeFormat, state.autoTranslateRoom, state.autoTranslateLanguage, state.archived, @@ -134,51 +113,44 @@ const MessageRoomStoreProvider = ({ children, ...state }: { children: ReactNode return {children}; }; -const MessageRoomProviderWithSetting = ({ children, ...state }: { children: ReactNode } & MessageRoomState): ReactElement => { - const Message_TimeFormat = useSetting('Message_TimeFormat') as string; - - return ( - - {children} - - ); -}; - -export const MessageRoomProvider = ({ children, ...state }: { children: ReactNode } & MessageRoomState): ReactElement => { - return state.timeFormat != null ? ( - {children} - ) : ( - {children} - ); -}; - -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 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 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 useNavToRoomInfo = (): IUseRoomMessageHandlersResult['navToRoomInfo'] | undefined => + useMessageRoomStore(s => s.handlers?.navToRoomInfo); +export const useShowAttachment = (): IUseRoomMessageHandlersResult['showAttachment'] | undefined => + useMessageRoomStore(s => s.handlers?.showAttachment); +export const useBlockAction = (): IUseRoomMessageHandlersResult['blockAction'] | undefined => + useMessageRoomStore(s => s.handlers?.blockAction); +export const useHandleEnterCall = (): IUseRoomMessageHandlersResult['handleEnterCall'] | undefined => + useMessageRoomStore(s => s.handlers?.handleEnterCall); +export const useOnDiscussionPress = (): IUseRoomMessageHandlersResult['onDiscussionPress'] | undefined => + useMessageRoomStore(s => s.handlers?.onDiscussionPress); +export const useOnThreadPress = (): IUseRoomMessageHandlersResult['onThreadPress'] | undefined => + useMessageRoomStore(s => s.handlers?.onThreadPress); +export const useOnEncryptedPress = (): IUseRoomMessageHandlersResult['onEncryptedPress'] | undefined => + useMessageRoomStore(s => s.handlers?.onEncryptedPress); +export const useOnReactionPress = (): IUseRoomMessageHandlersResult['onReactionPress'] | undefined => + useMessageRoomStore(s => s.handlers?.onReactionPress); +export const useOnReactionLongPress = (): IUseRoomMessageHandlersResult['onReactionLongPress'] | undefined => + useMessageRoomStore(s => s.handlers?.onReactionLongPress); +export const useReplyBroadcast = (): IUseRoomMessageHandlersResult['replyBroadcast'] | undefined => + useMessageRoomStore(s => s.handlers?.replyBroadcast); +export const useFetchThreadName = (): IUseRoomMessageHandlersResult['fetchThreadName'] | undefined => + useMessageRoomStore(s => s.handlers?.fetchThreadName); +export const useToggleFollowThread = (): IUseRoomMessageHandlersResult['toggleFollowThread'] | undefined => + useMessageRoomStore(s => s.handlers?.toggleFollowThread); +export const useOnAnswerButtonPress = (): IUseRoomMessageHandlersResult['onAnswerButtonPress'] | undefined => + useMessageRoomStore(s => s.handlers?.onAnswerButtonPress); 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); -export const useMessageUser = (): MessageRoomState['user'] => useMessageRoomStore(s => s.user); -export const useBaseUrl = (): string | undefined => useMessageRoomStore(s => s.baseUrl); +export const useMessageUser = (): IUser => useAppSelector(getUserSelector); +export const useBaseUrl = (): string => useAppSelector(state => state.server.server); 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); diff --git a/app/containers/message/stores/MessageStore.tsx b/app/containers/message/stores/MessageStore.tsx index 09d6f0b81ea..0d8c493009b 100644 --- a/app/containers/message/stores/MessageStore.tsx +++ b/app/containers/message/stores/MessageStore.tsx @@ -19,8 +19,8 @@ import { useJumpToMessage, useMessageGroupingPeriod, useMessageUser, - useOnDiscussionPress, - useOnThreadPress + useOnThreadPress, + useOnDiscussionPress } from './MessageRoomStore'; type MessageStoreState = { @@ -291,14 +291,20 @@ export const useMessageId = (): TAnyMessageModel['id'] => useMessageField(item = export const useReplies = (): TAnyMessageModel['replies'] => useMessageField(item => item.replies); +const autoTranslateLanguageFor = ( + item: TAnyMessageModel, + username: string | undefined, + autoTranslateRoom: boolean | undefined, + autoTranslateLanguage: string | undefined +): string | undefined => + autoTranslateRoom && autoTranslateLanguage && item.autoTranslate && item.u?.username !== username + ? autoTranslateLanguage + : undefined; + export const useTranslateLanguage = (): string | undefined => { const { autoTranslateRoom, autoTranslateLanguage } = useAutoTranslate(); const user = useMessageUser(); - return useMessageStore(s => { - const otherUserMessage = s.item.u?.username !== user?.username; - const canTranslate = autoTranslateRoom && autoTranslateLanguage && s.item.autoTranslate && otherUserMessage; - return canTranslate ? autoTranslateLanguage : undefined; - }); + return useMessageStore(s => autoTranslateLanguageFor(s.item, user?.username, autoTranslateRoom, autoTranslateLanguage)); }; export const useMessageText = (): { messageText: TAnyMessageModel['msg']; isTranslated: boolean } => { @@ -308,9 +314,9 @@ export const useMessageText = (): { messageText: TAnyMessageModel['msg']; isTran useShallow(s => { let messageText = s.item.msg; let isTranslated = false; - const otherUserMessage = s.item.u?.username !== user?.username; - if (autoTranslateRoom && s.item.autoTranslate && autoTranslateLanguage && otherUserMessage) { - const translated = getMessageTranslation(s.item, autoTranslateLanguage); + const language = autoTranslateLanguageFor(s.item, user?.username, autoTranslateRoom, autoTranslateLanguage); + if (language) { + const translated = getMessageTranslation(s.item, language); isTranslated = !!translated; messageText = translated || messageText; } diff --git a/app/containers/message/stores/__tests__/MessageActionStore.test.tsx b/app/containers/message/stores/__tests__/MessageActionStore.test.tsx index a342618f4a4..7746921db11 100644 --- a/app/containers/message/stores/__tests__/MessageActionStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageActionStore.test.tsx @@ -2,10 +2,11 @@ import { render } from '@testing-library/react-native'; import { createMessageActionStore, - inertStore, MessageActionProvider, + useEditingMessageId, useIsBeingEdited, - useMessageAction + useMessageAction, + useQuotedMessageIds } from '../MessageActionStore'; describe('MessageActionStore', () => { @@ -21,7 +22,7 @@ describe('MessageActionStore', () => { expect(spy).toHaveBeenLastCalledWith(false); }); - it('does not share mutable state across separate no-provider renders, and its actions throw', () => { + it('does not share mutable state across separate no-provider renders', () => { const spyA = jest.fn(); const spyB = jest.fn(); const ProbeA = () => { @@ -38,9 +39,6 @@ describe('MessageActionStore', () => { 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', () => { @@ -95,6 +93,74 @@ describe('MessageActionStore', () => { }); }); + describe('useQuotedMessageIds', () => { + it('returns the quoted messageIds under a quote action', () => { + const spy = jest.fn(); + const Probe = () => { + spy(useQuotedMessageIds()); + return null; + }; + + render( + + + + ); + + expect(spy).toHaveBeenLastCalledWith(['msg-1', 'msg-2']); + }); + + it('returns an empty array for non-quote actions', () => { + const spy = jest.fn(); + const Probe = () => { + spy(useQuotedMessageIds()); + return null; + }; + + render( + + + + ); + + expect(spy).toHaveBeenLastCalledWith([]); + }); + }); + + describe('useEditingMessageId', () => { + it('returns the messageId under an edit action', () => { + const spy = jest.fn(); + const Probe = () => { + spy(useEditingMessageId()); + return null; + }; + + render( + + + + ); + + expect(spy).toHaveBeenLastCalledWith('msg-1'); + }); + + it('returns undefined for non-edit actions', () => { + const spy = jest.fn(); + const Probe = () => { + spy(useEditingMessageId()); + return null; + }; + + render( + + + + ); + + expect(spy).toHaveBeenLastCalledWith(undefined); + }); + }); + describe('action hooks without a provider', () => { let consoleErrorSpy: jest.SpyInstance; diff --git a/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx b/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx index 9c5ee9304ef..b284a1faac7 100644 --- a/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageRoomStore.test.tsx @@ -1,9 +1,19 @@ import { memo } from 'react'; -import { act, render } from '@testing-library/react-native'; +import { act, fireEvent, render, screen } from '@testing-library/react-native'; import { Text } from 'react-native'; import { Provider } from 'react-redux'; -import { MessageRoomProvider, useAutoTranslate, useIsArchived, useTimeFormat } from '../MessageRoomStore'; +import { + MessageRoomProvider, + type MessageRoomState, + useAutoTranslate, + useBlockAction, + useIsArchived, + useNavToRoomInfo, + useReactionInit, + useShowAttachment, + useTimeFormat +} from '../MessageRoomStore'; import { mockedStore } from '../../../../reducers/mockedStore'; import { updateSettings } from '../../../../actions/settings'; @@ -20,24 +30,24 @@ describe('MessageRoomStore', () => { }); it('useIsArchived throws', () => { - const Probe = () => { + const Consumer = () => { useIsArchived(); return null; }; - expect(() => render()).toThrow('Message room hooks must be used within a MessageRoomProvider'); + expect(() => render()).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 = () => { + const Consumer = () => { spy(useTimeFormat()); return null; }; const wrap = (timeFormat: string) => ( - + ); @@ -51,7 +61,7 @@ describe('MessageRoomStore', () => { it('does not subscribe to Message_TimeFormat when a timeFormat prop is passed', () => { const spy = jest.fn(); - const Probe = () => { + const Consumer = () => { spy(useTimeFormat()); return null; }; @@ -59,7 +69,7 @@ describe('MessageRoomStore', () => { render( - + ); @@ -78,7 +88,7 @@ describe('MessageRoomStore', () => { mockedStore.dispatch(updateSettings('Message_TimeFormat', 'MMM Do YYYY')); const spy = jest.fn(); - const Probe = () => { + const Consumer = () => { spy(useTimeFormat()); return null; }; @@ -86,7 +96,7 @@ describe('MessageRoomStore', () => { render( - + ); @@ -104,11 +114,11 @@ describe('MessageRoomStore', () => { 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(() => { + const TimeFormatConsumer = memo(() => { timeFormatSpy(useTimeFormat()); return null; }); - const AutoTranslateProbe = memo(() => { + const AutoTranslateConsumer = memo(() => { autoTranslateSpy(useAutoTranslate()); return null; }); @@ -116,8 +126,8 @@ describe('MessageRoomStore', () => { const wrap = (autoTranslateRoom: boolean) => ( - - + + ); @@ -134,14 +144,14 @@ describe('MessageRoomStore', () => { it('resyncs archived when a room gets archived mid-session', () => { const spy = jest.fn(); - const Probe = () => { + const Consumer = () => { spy(useIsArchived()); return null; }; const wrap = (archived: boolean) => ( - + ); @@ -153,48 +163,130 @@ describe('MessageRoomStore', () => { expect(spy).toHaveBeenLastCalledWith(true); }); - describe('frozen handler guard (dev)', () => { - let warnSpy: jest.SpyInstance; + describe('handlers bag', () => { + it('reads a handler through the fine-grained selector on the first render', () => { + const blockAction = jest.fn(); + const spy = jest.fn(); + const Consumer = () => { + spy(useBlockAction()); + return null; + }; - beforeEach(() => { - warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); - }); + render( + + + + + + ); - afterEach(() => { - warnSpy.mockRestore(); + expect(spy).toHaveBeenNthCalledWith(1, blockAction); }); - it('warns once when a frozen handler identity changes after mount', () => { - const wrap = (navToRoomInfo: () => void) => ( + it('exposes navToRoomInfo/showAttachment from the handler bag', () => { + const bagNav = jest.fn(); + const bagShow = jest.fn(); + const navSpy = jest.fn(); + const showSpy = jest.fn(); + const Consumer = () => { + navSpy(useNavToRoomInfo()); + showSpy(useShowAttachment()); + return null; + }; + + render( - - probe + + ); - const { rerender } = render(wrap(() => {})); - expect(warnSpy).not.toHaveBeenCalled(); - - act(() => rerender(wrap(() => {}))); - - expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('navToRoomInfo')); + expect(navSpy).toHaveBeenLastCalledWith(bagNav); + expect(showSpy).toHaveBeenLastCalledWith(bagShow); }); - it('does not warn when the same handler reference is passed across re-renders', () => { - const navToRoomInfo = () => {}; - const wrap = () => ( + it('resyncs the bag when the provider receives a new handlers prop', () => { + const first = jest.fn(); + const second = jest.fn(); + const spy = jest.fn(); + const Consumer = () => { + spy(useBlockAction()); + return null; + }; + const tree = (blockAction: jest.Mock) => ( - - probe + + ); - const { rerender } = render(wrap()); - act(() => rerender(wrap())); + const { rerender } = render(tree(first)); + + expect(spy).toHaveBeenLastCalledWith(first); + + rerender(tree(second)); + + expect(spy).toHaveBeenLastCalledWith(second); + }); + }); + + describe('live callbacks', () => { + const wrap = (config: Partial) => ( + + + + + + ); + + const renderSpy = jest.fn(); + const ReactionInitConsumer = memo(() => { + const reactionInit = useReactionInit(); + renderSpy(reactionInit); + return reactionInit?.('message-id')}>reaction; + }); + + beforeEach(() => renderSpy.mockClear()); + + it('invokes the latest callback after a rerender', () => { + const first = jest.fn(); + const second = jest.fn(); + + const { rerender } = render(wrap({ reactionInit: first })); + act(() => rerender(wrap({ reactionInit: second }))); + + fireEvent.press(screen.getByText('reaction')); + + expect(second).toHaveBeenCalledWith('message-id'); + expect(first).not.toHaveBeenCalled(); + }); + + it('keeps the callback identity stable across rerenders', () => { + const { rerender } = render(wrap({ reactionInit: jest.fn() })); + const renderCallsBefore = renderSpy.mock.calls.length; + + act(() => rerender(wrap({ reactionInit: jest.fn() }))); + + expect(renderSpy.mock.calls.length).toBe(renderCallsBefore); + }); + + it('stays undefined when the provider does not supply the callback', () => { + render(wrap({})); + + expect(renderSpy).toHaveBeenLastCalledWith(undefined); + }); + + it('becomes defined when the provider starts supplying the callback', () => { + const reactionInit = jest.fn(); + const { rerender } = render(wrap({})); + expect(renderSpy).toHaveBeenLastCalledWith(undefined); + + act(() => rerender(wrap({ reactionInit }))); + fireEvent.press(screen.getByText('reaction')); - expect(warnSpy).not.toHaveBeenCalled(); + expect(reactionInit).toHaveBeenCalledWith('message-id'); }); }); }); diff --git a/app/containers/message/stores/__tests__/MessageStore.test.tsx b/app/containers/message/stores/__tests__/MessageStore.test.tsx index e867a51f41b..fe3880fcd4f 100644 --- a/app/containers/message/stores/__tests__/MessageStore.test.tsx +++ b/app/containers/message/stores/__tests__/MessageStore.test.tsx @@ -598,7 +598,7 @@ describe('MessageStore', () => { u: { _id: 'u2', username: 'bob' } }); const { latest } = renderDerived(model, useMessageText, { - config: { autoTranslateRoom: true, autoTranslateLanguage: 'pt-BR', user: { username: 'alice' } } + config: { autoTranslateRoom: true, autoTranslateLanguage: 'pt-BR' } }); expect(latest()).toEqual({ messageText: 'Olá mundo', isTranslated: true }); }); @@ -891,7 +891,7 @@ describe('MessageStore', () => { 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 } }); + const { result } = renderMessageHook(useMessagePress, model, { isIgnored: true, config: { handlers: { onThreadPress } } }); act(() => result.current()); @@ -904,7 +904,10 @@ describe('MessageStore', () => { const onPress = jest.fn(); const onThreadPress = jest.fn(); const onDiscussionPress = jest.fn(); - const { result } = renderMessageHook(useMessagePress, model, { onPress, config: { onThreadPress, onDiscussionPress } }); + const { result } = renderMessageHook(useMessagePress, model, { + onPress, + config: { handlers: { onThreadPress, onDiscussionPress } } + }); act(() => result.current()); @@ -917,7 +920,7 @@ describe('MessageStore', () => { 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 } }); + const { result } = renderMessageHook(useMessagePress, model, { config: { handlers: { onThreadPress } } }); act(() => result.current()); @@ -928,7 +931,7 @@ describe('MessageStore', () => { 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 } }); + const { result } = renderMessageHook(useMessagePress, model, { config: { handlers: { onThreadPress } } }); act(() => result.current()); @@ -938,7 +941,9 @@ describe('MessageStore', () => { 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 } }); + const { result } = renderMessageHook(useMessagePress, model, { + config: { isThreadRoom: true, handlers: { onThreadPress } } + }); act(() => result.current()); @@ -948,7 +953,7 @@ describe('MessageStore', () => { 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 } }); + const { result } = renderMessageHook(useMessagePress, model, { config: { handlers: { onDiscussionPress } } }); act(() => result.current()); @@ -958,7 +963,7 @@ describe('MessageStore', () => { 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 } }); + const { result } = renderMessageHook(useMessagePress, model, { config: { handlers: { onThreadPress } } }); act(() => result.current()); expect(onThreadPress).toHaveBeenCalledTimes(1); @@ -976,7 +981,9 @@ describe('MessageStore', () => { const model = buildFakeModel({ tmid: 't1' }); const onThreadPress = jest.fn(); const closeEmojiAndAction = jest.fn(); - const { result } = renderMessageHook(useMessagePress, model, { config: { onThreadPress, closeEmojiAndAction } }); + const { result } = renderMessageHook(useMessagePress, model, { + config: { closeEmojiAndAction, handlers: { onThreadPress } } + }); act(() => result.current()); @@ -989,7 +996,9 @@ describe('MessageStore', () => { 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 } }); + const { result } = renderMessageHook(useMessagePress, model, { + config: { closeEmojiAndAction, handlers: { onThreadPress } } + }); act(() => result.current()); @@ -999,7 +1008,7 @@ describe('MessageStore', () => { }); describe('translation boundary on item.autoTranslate', () => { - const translationConfig = { autoTranslateRoom: true, autoTranslateLanguage: 'pt-BR', user: { username: 'alice' } }; + const translationConfig = { autoTranslateRoom: true, autoTranslateLanguage: 'pt-BR' }; it('useTranslateLanguage returns undefined when autoTranslate is undefined', () => { const model = buildFakeModel({ autoTranslate: undefined, u: { _id: 'u2', username: 'bob' } }); diff --git a/app/definitions/IRoomMessageHandlers.ts b/app/definitions/IRoomMessageHandlers.ts new file mode 100644 index 00000000000..7237594244c --- /dev/null +++ b/app/definitions/IRoomMessageHandlers.ts @@ -0,0 +1,27 @@ +import { type IAttachment } from './IAttachment'; +import { type IEmoji } from './IEmoji'; +import { type IMessage, type TAnyMessageModel } from './IMessage'; +import { type IRoomInfoParam } from './IRoom'; + +export interface IUseRoomMessageHandlersResult { + blockAction: (params: { + actionId: string; + appId: string; + value: any; + blockId: string; + rid: string; + mid: string; + }) => Promise; + navToRoomInfo: (navParam: IRoomInfoParam) => void; + handleEnterCall: () => void; + onDiscussionPress: (drid: TAnyMessageModel['drid']) => void; + onThreadPress: (item: TAnyMessageModel) => void; + onEncryptedPress: () => void; + showAttachment: (attachment: IAttachment) => void; + onReactionPress: (emoji: IEmoji, messageId: string) => Promise; + onReactionLongPress: (message: TAnyMessageModel) => void; + replyBroadcast: (message: IMessage) => void; + fetchThreadName: (threadId: string, messageId: string) => Promise; + toggleFollowThread: (isFollowingThread: boolean, threadId?: string) => Promise; + onAnswerButtonPress: (message?: string, tshow?: boolean) => void; +} diff --git a/app/definitions/index.ts b/app/definitions/index.ts index 45d4bdc2dd6..8e19366cca9 100644 --- a/app/definitions/index.ts +++ b/app/definitions/index.ts @@ -21,6 +21,7 @@ export * from './IPreferences'; export * from './IProfile'; export * from './IReaction'; export * from './IRole'; +export * from './IRoomMessageHandlers'; export * from './IRoom'; export * from './ISearch'; export * from './IServer'; diff --git a/app/definitions/redux/index.ts b/app/definitions/redux/index.ts index f32d7041e69..c20408dea7f 100644 --- a/app/definitions/redux/index.ts +++ b/app/definitions/redux/index.ts @@ -1,5 +1,6 @@ // ACTIONS import { type TActionInquiry } from '../../ee/omnichannel/actions/inquiry'; +import { type TActionRoutingConfig } from '../../ee/omnichannel/actions/routingConfig'; import { type TActionActiveUsers } from '../../actions/activeUsers'; import { type TActionApp } from '../../actions/app'; import { type TActionCreateChannel } from '../../actions/createChannel'; @@ -35,6 +36,7 @@ import { type IServer } from '../../reducers/server'; import { type TSettingsState } from '../../reducers/settings'; import { type IShare } from '../../reducers/share'; import { type IInquiry } from '../../ee/omnichannel/reducers/inquiry'; +import { type IRoutingConfig } from '../../ee/omnichannel/reducers/routingConfig'; import { type IPermissionsState } from '../../reducers/permissions'; import { type IEnterpriseModules } from '../../reducers/enterpriseModules'; import { type IVideoConf } from '../../reducers/videoConf'; @@ -67,6 +69,7 @@ export interface IApplicationState { inviteLinks: IInviteLinks; createDiscussion: ICreateDiscussion; inquiry: IInquiry; + routingConfig: IRoutingConfig; enterpriseModules: IEnterpriseModules; encryption: IEncryption; permissions: IPermissionsState; @@ -93,6 +96,7 @@ export type TApplicationActions = TActionActiveUsers & TActionServer & TActionApp & TActionInquiry & + TActionRoutingConfig & TActionPermissions & TActionEnterpriseModules & TActionVideoConf & diff --git a/app/ee/omnichannel/actions/routingConfig.ts b/app/ee/omnichannel/actions/routingConfig.ts new file mode 100644 index 00000000000..f1701f52ab9 --- /dev/null +++ b/app/ee/omnichannel/actions/routingConfig.ts @@ -0,0 +1,25 @@ +import { type Action } from 'redux'; + +import { ROUTING_CONFIG } from '../../../actions/actionsTypes'; + +interface IRoutingConfigSuccess extends Action { + returnQueue: boolean; +} + +interface IRoutingConfigFailure extends Action { + error: unknown; +} + +export type TActionRoutingConfig = Action & Partial; + +export function routingConfigRequest(): Action { + return { type: ROUTING_CONFIG.REQUEST }; +} + +export function routingConfigSuccess(returnQueue: boolean): IRoutingConfigSuccess { + return { type: ROUTING_CONFIG.SUCCESS, returnQueue }; +} + +export function routingConfigFailure(error: unknown): IRoutingConfigFailure { + return { type: ROUTING_CONFIG.FAILURE, error }; +} diff --git a/app/ee/omnichannel/hooks/useCanReturnQueue.test.tsx b/app/ee/omnichannel/hooks/useCanReturnQueue.test.tsx new file mode 100644 index 00000000000..2fc974f8da1 --- /dev/null +++ b/app/ee/omnichannel/hooks/useCanReturnQueue.test.tsx @@ -0,0 +1,42 @@ +import { renderHook, waitFor } from '@testing-library/react-native'; +import { Provider } from 'react-redux'; + +jest.mock('../../../lib/services/restApi', () => ({ getRoutingConfig: jest.fn() })); + +import { getRoutingConfig } from '../../../lib/services/restApi'; +import routingConfigSaga from '../sagas/routingConfig'; +import { createRecordingStore, cancelSagaTasks } from '../../../lib/testUtils/sagaStore'; +import { useCanReturnQueue } from './useCanReturnQueue'; +import type { ReactNode } from 'react'; + +const mockGetRoutingConfig = getRoutingConfig as jest.MockedFunction; + +afterEach(() => { + cancelSagaTasks(); + jest.clearAllMocks(); +}); + +const createWrapper = () => { + const recording = createRecordingStore(routingConfigSaga); + const Wrapper = ({ children }: { children: ReactNode }) => {children}; + return { ...recording, Wrapper }; +}; + +it('does not request when disabled and stays false while unloaded', () => { + const { Wrapper } = createWrapper(); + const { result } = renderHook(() => useCanReturnQueue(false), { wrapper: Wrapper }); + + expect(result.current).toBe(false); + expect(mockGetRoutingConfig).not.toHaveBeenCalled(); +}); + +it('requests once when two consumers mount together', async () => { + mockGetRoutingConfig.mockResolvedValue({ returnQueue: true } as never); + const { Wrapper } = createWrapper(); + const first = renderHook(() => useCanReturnQueue(true), { wrapper: Wrapper }); + const second = renderHook(() => useCanReturnQueue(true), { wrapper: Wrapper }); + + await waitFor(() => expect(first.result.current).toBe(true)); + expect(second.result.current).toBe(true); + expect(mockGetRoutingConfig).toHaveBeenCalledTimes(1); +}); diff --git a/app/ee/omnichannel/hooks/useCanReturnQueue.ts b/app/ee/omnichannel/hooks/useCanReturnQueue.ts new file mode 100644 index 00000000000..0536d767358 --- /dev/null +++ b/app/ee/omnichannel/hooks/useCanReturnQueue.ts @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; + +import { routingConfigRequest } from '../actions/routingConfig'; +import { useAppSelector } from '../../../lib/hooks/useAppSelector'; + +export function useCanReturnQueue(enabled: boolean): boolean { + const dispatch = useDispatch(); + const returnQueue = useAppSelector(state => state.routingConfig.returnQueue); + + useEffect(() => { + if (enabled && returnQueue === null) { + dispatch(routingConfigRequest()); + } + }, [dispatch, enabled, returnQueue]); + + return enabled && returnQueue === true; +} diff --git a/app/ee/omnichannel/lib/__tests__/takeInquiry.test.ts b/app/ee/omnichannel/lib/__tests__/takeInquiry.test.ts new file mode 100644 index 00000000000..5ab52a63152 --- /dev/null +++ b/app/ee/omnichannel/lib/__tests__/takeInquiry.test.ts @@ -0,0 +1,38 @@ +import sdk from '../../../../lib/services/sdk'; +import { store as reduxStore } from '../../../../lib/store/auxStore'; +import { takeInquiry } from '..'; + +jest.mock('../../../../lib/services/sdk', () => ({ + __esModule: true, + default: { post: jest.fn(() => Promise.resolve()), get: jest.fn(), methodCallWrapper: jest.fn(() => Promise.resolve()) } +})); +jest.mock('../../../../lib/store/auxStore', () => ({ store: { getState: jest.fn() } })); +jest.mock('../subscriptions/inquiry', () => ({ __esModule: true, default: jest.fn() })); +jest.mock('../../../../lib/methods/helpers/events', () => ({ + __esModule: true, + default: { addEventListener: jest.fn(), removeListener: jest.fn() } +})); + +const mockGetState = reduxStore.getState as jest.Mock; + +describe('takeInquiry', () => { + beforeEach(() => jest.clearAllMocks()); + + it('uses the REST endpoint on a modern Workspace', async () => { + mockGetState.mockReturnValue({ server: { version: '7.11.0' } }); + + await takeInquiry('inquiry-1'); + + expect(sdk.post).toHaveBeenCalledWith('livechat/inquiries.take', { inquiryId: 'inquiry-1' }); + expect(sdk.methodCallWrapper).not.toHaveBeenCalled(); + }); + + it('falls back to the DDP method on an older Workspace', async () => { + mockGetState.mockReturnValue({ server: { version: '7.10.0' } }); + + await takeInquiry('inquiry-1'); + + expect(sdk.methodCallWrapper).toHaveBeenCalledWith('livechat:takeInquiry', 'inquiry-1'); + expect(sdk.post).not.toHaveBeenCalled(); + }); +}); diff --git a/app/ee/omnichannel/lib/index.ts b/app/ee/omnichannel/lib/index.ts index 60308b3cb43..a3afef65b9e 100644 --- a/app/ee/omnichannel/lib/index.ts +++ b/app/ee/omnichannel/lib/index.ts @@ -28,7 +28,8 @@ export const getInquiriesQueued = (serverVersion: string) => { // this inquiry is added to the db by the subscriptions stream // and will be removed by the queue stream // RC 2.4.0 -export const takeInquiry = (inquiryId: string, serverVersion: string) => { +export const takeInquiry = (inquiryId: string) => { + const serverVersion = reduxStore.getState().server.version; if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '7.11.0')) { return sdk.post('livechat/inquiries.take', { inquiryId }); } diff --git a/app/ee/omnichannel/reducers/routingConfig.test.ts b/app/ee/omnichannel/reducers/routingConfig.test.ts new file mode 100644 index 00000000000..236afa1eb46 --- /dev/null +++ b/app/ee/omnichannel/reducers/routingConfig.test.ts @@ -0,0 +1,18 @@ +import { inquiryReset } from '../actions/inquiry'; +import { routingConfigSuccess } from '../actions/routingConfig'; +import { selectServerRequest } from '../../../actions/server'; +import routingConfig, { initialState } from './routingConfig'; + +describe('routingConfig reducer', () => { + it('stores the server value', () => { + expect(routingConfig(initialState, routingConfigSuccess(true))).toEqual({ returnQueue: true }); + }); + + it('clears the value when a server selection starts', () => { + expect(routingConfig({ returnQueue: true }, selectServerRequest('https://new.example', '7.0.0'))).toEqual(initialState); + }); + + it('does not change when the inquiry queue resets', () => { + expect(routingConfig({ returnQueue: true }, inquiryReset())).toEqual({ returnQueue: true }); + }); +}); diff --git a/app/ee/omnichannel/reducers/routingConfig.ts b/app/ee/omnichannel/reducers/routingConfig.ts new file mode 100644 index 00000000000..5cd03a5bbc7 --- /dev/null +++ b/app/ee/omnichannel/reducers/routingConfig.ts @@ -0,0 +1,22 @@ +import { type AnyAction } from 'redux'; +import { ROUTING_CONFIG } from '../../../actions/actionsTypes'; +import { SERVER } from '../../../actions/actionsTypes'; + +export interface IRoutingConfig { + returnQueue: boolean | null; +} + +export const initialState: IRoutingConfig = { + returnQueue: null +}; + +export default function routingConfig(state = initialState, action: AnyAction): IRoutingConfig { + switch (action.type) { + case ROUTING_CONFIG.SUCCESS: + return { returnQueue: action.returnQueue }; + case SERVER.SELECT_REQUEST: + return initialState; + default: + return state; + } +} diff --git a/app/ee/omnichannel/sagas/routingConfig.test.ts b/app/ee/omnichannel/sagas/routingConfig.test.ts new file mode 100644 index 00000000000..02db5508026 --- /dev/null +++ b/app/ee/omnichannel/sagas/routingConfig.test.ts @@ -0,0 +1,39 @@ +jest.mock('../../../lib/services/restApi', () => ({ getRoutingConfig: jest.fn() })); + +import { getRoutingConfig } from '../../../lib/services/restApi'; +import { routingConfigRequest } from '../actions/routingConfig'; +import routingConfig from './routingConfig'; +import { cancelSagaTasks, createRecordingStore, flushSagaMicrotasks } from '../../../lib/testUtils/sagaStore'; + +const mockGetRoutingConfig = getRoutingConfig as jest.MockedFunction; + +afterEach(() => { + cancelSagaTasks(); + jest.clearAllMocks(); +}); + +it('loads once and retries after a failed request', async () => { + mockGetRoutingConfig.mockRejectedValueOnce(new Error('offline')).mockResolvedValueOnce({ returnQueue: true } as never); + const { store } = createRecordingStore(routingConfig); + + store.dispatch(routingConfigRequest()); + await flushSagaMicrotasks(); + expect(store.getState().routingConfig.returnQueue).toBeNull(); + + store.dispatch(routingConfigRequest()); + await flushSagaMicrotasks(); + expect(store.getState().routingConfig.returnQueue).toBe(true); + expect(mockGetRoutingConfig).toHaveBeenCalledTimes(2); +}); + +it('ignores repeated requests after loading', async () => { + mockGetRoutingConfig.mockResolvedValue({ returnQueue: false } as never); + const { store } = createRecordingStore(routingConfig); + + store.dispatch(routingConfigRequest()); + store.dispatch(routingConfigRequest()); + await flushSagaMicrotasks(); + + expect(store.getState().routingConfig.returnQueue).toBe(false); + expect(mockGetRoutingConfig).toHaveBeenCalledTimes(1); +}); diff --git a/app/ee/omnichannel/sagas/routingConfig.ts b/app/ee/omnichannel/sagas/routingConfig.ts new file mode 100644 index 00000000000..1348cd131dd --- /dev/null +++ b/app/ee/omnichannel/sagas/routingConfig.ts @@ -0,0 +1,23 @@ +import { put, select, takeLeading } from 'redux-saga/effects'; + +import { ROUTING_CONFIG } from '../../../actions/actionsTypes'; +import { getRoutingConfig } from '../../../lib/services/restApi'; +import { routingConfigFailure, routingConfigSuccess } from '../actions/routingConfig'; + +const handleRequest = function* handleRequest(): Generator { + const returnQueue = yield select(state => state.routingConfig.returnQueue); + if (returnQueue !== null) { + return; + } + + try { + const routingConfig = yield getRoutingConfig(); + yield put(routingConfigSuccess(routingConfig.returnQueue)); + } catch (error) { + yield put(routingConfigFailure(error)); + } +}; + +export default function* routingConfig() { + yield takeLeading(ROUTING_CONFIG.REQUEST, handleRequest); +} diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 8f9606fe55d..7099e12f1fb 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -784,6 +784,7 @@ "room_changed_topic_to": "changed room topic to: {{topic}}", "room_changed_type": "changed room to {{type}}", "room_disallowed_reactions": "disallowed reactions", + "Room_failed_to_load": "This room could not be loaded. Check your connection and try again.", "Room_Info": "Room info", "Room_Info_Edit": "Room info edit", "Room_name_changed_to": "changed room name to: {{name}}", diff --git a/app/lib/hooks/__tests__/useObservable.test.ts b/app/lib/hooks/__tests__/useObservable.test.ts new file mode 100644 index 00000000000..a4e5b4a8b66 --- /dev/null +++ b/app/lib/hooks/__tests__/useObservable.test.ts @@ -0,0 +1,47 @@ +import { act, renderHook } from '@testing-library/react-native'; +import { Observable, Subject } from 'rxjs'; + +import { useObservable } from '../useObservable'; + +describe('useObservable', () => { + it('returns undefined until the observable emits, then the latest value', () => { + const subject = new Subject(); + const { result } = renderHook(() => useObservable(subject)); + + expect(result.current).toBeUndefined(); + act(() => subject.next(1)); + expect(result.current).toBe(1); + act(() => subject.next(2)); + expect(result.current).toBe(2); + }); + + it('drops the previous value when the observable changes', () => { + const first = new Subject(); + const second = new Subject(); + const { result, rerender } = renderHook(({ source }: { source: Subject }) => useObservable(source), { + initialProps: { source: first } + }); + + act(() => first.next('first')); + expect(result.current).toBe('first'); + + rerender({ source: second }); + expect(result.current).toBeUndefined(); + act(() => second.next('second')); + expect(result.current).toBe('second'); + }); + + it('unsubscribes on unmount', () => { + const teardown = jest.fn(); + const observable = new Observable(() => teardown); + const { unmount } = renderHook(() => useObservable(observable)); + expect(teardown).not.toHaveBeenCalled(); + unmount(); + expect(teardown).toHaveBeenCalledTimes(1); + }); + + it('returns undefined without an observable', () => { + const { result } = renderHook(() => useObservable(undefined)); + expect(result.current).toBeUndefined(); + }); +}); diff --git a/app/lib/hooks/__tests__/usePermissions.test.ts b/app/lib/hooks/__tests__/usePermissions.test.ts new file mode 100644 index 00000000000..6da1374c463 --- /dev/null +++ b/app/lib/hooks/__tests__/usePermissions.test.ts @@ -0,0 +1,84 @@ +import { act, renderHook } from '@testing-library/react-native'; + +import database from '../../database'; +import { store as reduxStore } from '../../store/auxStore'; +import { getSubscriptionByRoomId } from '../../database/services/Subscription'; +import { usePermissions } from '../usePermissions'; + +let mockState: any; +jest.mock('../useAppSelector', () => ({ + useAppSelector: (selector: (state: any) => unknown) => selector(mockState) +})); + +jest.mock('../../database', () => ({ __esModule: true, default: { active: { get: jest.fn() } } })); +jest.mock('../../store/auxStore', () => ({ store: { getState: jest.fn() } })); +jest.mock('../../database/services/Subscription', () => ({ getSubscriptionByRoomId: jest.fn() })); + +const mockGet = database.active.get as jest.Mock; +const mockGetState = reduxStore.getState as jest.Mock; +const mockGetSubscriptionByRoomId = getSubscriptionByRoomId as jest.Mock; + +const PERMISSION = 'toggle-room-e2e-encryption'; + +const configureRoles = ({ + userRoles, + subRoles, + permissionRoles +}: { + userRoles: string[]; + subRoles: string[]; + permissionRoles: string[]; +}) => { + mockState = { + login: { user: { roles: userRoles } }, + permissions: { [PERMISSION]: permissionRoles } + }; + mockGetState.mockReturnValue({ login: { user: { roles: userRoles } } }); + + const subRecord = { + roles: subRoles, + observe: () => ({ + subscribe: (cb: (sub: any) => void) => { + cb({ roles: subRoles }); + return { unsubscribe: jest.fn() }; + } + }) + }; + mockGetSubscriptionByRoomId.mockResolvedValue(subRecord); + mockGet.mockImplementation(() => ({ find: jest.fn(() => Promise.resolve(subRecord)) })); +}; + +const flush = () => act(() => Promise.resolve()); + +describe('usePermissions', () => { + beforeEach(() => jest.clearAllMocks()); + + const scenarios = [ + { name: 'user role grants the permission', userRoles: ['admin'], subRoles: [], permissionRoles: ['admin'], expected: true }, + { + name: 'subscription role grants the permission', + userRoles: [], + subRoles: ['owner'], + permissionRoles: ['owner'], + expected: true + }, + { + name: "the user's roles lack the permission", + userRoles: ['user'], + subRoles: ['moderator'], + permissionRoles: ['owner'], + expected: false + } + ]; + + scenarios.forEach(scenario => { + it(`grants ${scenario.expected} when ${scenario.name}`, async () => { + configureRoles(scenario); + + const { result } = renderHook(() => usePermissions([PERMISSION], 'rid-1')); + await flush(); + + expect(result.current[0]).toBe(scenario.expected); + }); + }); +}); diff --git a/app/lib/hooks/useLiveRef.test.ts b/app/lib/hooks/useLiveRef.test.ts new file mode 100644 index 00000000000..23ba4d57524 --- /dev/null +++ b/app/lib/hooks/useLiveRef.test.ts @@ -0,0 +1,30 @@ +import { renderHook } from '@testing-library/react-native'; + +import { useLiveRef } from './useLiveRef'; + +describe('useLiveRef', () => { + it('exposes the initial value on first render', () => { + const { result } = renderHook(() => useLiveRef('first')); + + expect(result.current.current).toBe('first'); + }); + + it('refreshes ref.current to the latest value on rerender', () => { + const { result, rerender } = renderHook(({ value }: { value: number }) => useLiveRef(value), { initialProps: { value: 1 } }); + + rerender({ value: 2 }); + + expect(result.current.current).toBe(2); + }); + + it('keeps a stable ref object identity across rerenders', () => { + const { result, rerender } = renderHook(({ value }: { value: string }) => useLiveRef(value), { + initialProps: { value: 'a' } + }); + const firstRef = result.current; + + rerender({ value: 'b' }); + + expect(result.current).toBe(firstRef); + }); +}); diff --git a/app/lib/hooks/useLiveRef.ts b/app/lib/hooks/useLiveRef.ts new file mode 100644 index 00000000000..e99597db6cc --- /dev/null +++ b/app/lib/hooks/useLiveRef.ts @@ -0,0 +1,12 @@ +import { type RefObject, useEffect, useRef } from 'react'; + +// Ref refreshed every render (no deps array), so a narrowly-keyed effect can read the latest closure +// without listing it as a dependency. Blessed escape hatch for the intentional deps-less mirror. +// Stand-in for useEffectEvent (stable in React 19.2); migrate 1:1 once RN ships React >= 19.2. +export function useLiveRef(value: T): RefObject { + const ref = useRef(value); + useEffect(() => { + ref.current = value; + }); + return ref; +} diff --git a/app/lib/hooks/useObservable.ts b/app/lib/hooks/useObservable.ts new file mode 100644 index 00000000000..8f587c446a0 --- /dev/null +++ b/app/lib/hooks/useObservable.ts @@ -0,0 +1,27 @@ +import { useCallback, useRef, useSyncExternalStore } from 'react'; +import { type Observable } from 'rxjs'; + +type Emission = { source: Observable; value: T }; + +export function useObservable(observable: Observable | undefined): T | undefined { + const latestEmission = useRef | undefined>(undefined); + + const subscribe = useCallback( + (onChange: () => void) => { + if (!observable) { + return () => {}; + } + const subscription = observable.subscribe(value => { + latestEmission.current = { source: observable, value }; + onChange(); + }); + return () => subscription.unsubscribe(); + }, + [observable] + ); + + return useSyncExternalStore(subscribe, () => { + const emission = latestEmission.current; + return emission && emission.source === observable ? emission.value : undefined; + }); +} diff --git a/app/lib/hooks/useShortnameToUnicode/index.tsx b/app/lib/hooks/useShortnameToUnicode/index.tsx index d6f6d78a75c..890c8a71f1b 100644 --- a/app/lib/hooks/useShortnameToUnicode/index.tsx +++ b/app/lib/hooks/useShortnameToUnicode/index.tsx @@ -35,7 +35,7 @@ const useShortnameToUnicode = (isEmojiPicker?: boolean) => { const replaceShortnameWithUnicode = (shortname: string) => { const name = shortname.replace(/:/g, ''); - + // a custom emoji sharing a built-in shortcode/alias must win if (customEmojis(name)) { return shortname; diff --git a/app/lib/methods/emojis.ts b/app/lib/methods/emojis.ts index 63f4d885a9c..58ed685db6c 100644 --- a/app/lib/methods/emojis.ts +++ b/app/lib/methods/emojis.ts @@ -10,7 +10,7 @@ const FREQUENTLY_USED_TABLE = 'frequently_used_emojis'; // Looked up by content, never used as the record id: emoji content / custom names can be // non-ASCII and corrupt across the native SQLite bridge when used as WatermelonDB ids. -const getEmojiContent = (emoji: IEmoji) => (typeof emoji === 'string' ? emoji : emoji.name); +export const getEmojiContent = (emoji: IEmoji) => (typeof emoji === 'string' ? emoji : emoji.name); export const addFrequentlyUsed = async (emoji: IEmoji) => { const db = database.active; diff --git a/app/lib/methods/helpers/debounce.ts b/app/lib/methods/helpers/debounce.ts index f792d965d34..1acf491e8e7 100644 --- a/app/lib/methods/helpers/debounce.ts +++ b/app/lib/methods/helpers/debounce.ts @@ -1,4 +1,4 @@ -import { useDebouncedCallback, type Options } from 'use-debounce'; +import { useDebouncedCallback, type DebouncedState, type Options } from 'use-debounce'; export function debounce(func: Function, wait?: number, immediate?: boolean) { let timeout: ReturnType | null; @@ -24,6 +24,6 @@ export function debounce(func: Function, wait?: number, immediate?: boolean) { return _debounce; } -export function useDebounce(func: (...args: any) => any, wait?: number, options?: Options): (...args: any[]) => void { +export function useDebounce any>(func: T, wait?: number, options?: Options): DebouncedState { return useDebouncedCallback(func, wait || 1000, options); } diff --git a/app/lib/methods/helpers/goRoom.test.ts b/app/lib/methods/helpers/goRoom.test.ts new file mode 100644 index 00000000000..b8e44a735d3 --- /dev/null +++ b/app/lib/methods/helpers/goRoom.test.ts @@ -0,0 +1,104 @@ +import { InteractionManager } from 'react-native'; + +import { goRoom } from './goRoom'; +import { peekOrCreateRoomStore, acquireRoomStore, releaseRoomStore } from '../../../views/RoomView/stores/RoomStore'; + +jest.mock('../../navigation/appNavigation', () => ({ + __esModule: true, + default: { + getCurrentRoute: jest.fn(() => ({ name: 'RoomsListView' })), + setParams: jest.fn(), + popTo: jest.fn(), + dispatch: jest.fn() + } +})); + +jest.mock('../../database/services/Subscription', () => ({ + getSubscriptionByRoomId: jest.fn(() => Promise.resolve(null)) +})); + +// RoomStore transitive imports pull native/ESM modules (mobile-crypto via readMessages); +// stub them so importing goRoom -> RoomStore stays runnable under jest. +jest.mock('../readMessages', () => ({ readMessages: jest.fn(() => Promise.resolve()) })); +jest.mock('../loadThreadMessages', () => ({ loadThreadMessages: jest.fn(() => Promise.resolve()) })); +jest.mock('../../services/restApi', () => ({ getUserInfo: jest.fn() })); +jest.mock('../isInviteSubscription', () => ({ isInviteSubscription: jest.fn(() => false) })); +jest.mock('../../../views/RoomView/services/getMessages', () => ({ + __esModule: true, + default: jest.fn(() => Promise.resolve()) +})); +jest.mock('../../store/auxStore', () => ({ + store: { + getState: () => ({ + settings: {}, + login: { user: { id: 'u1', username: 'user' } } + }) + } +})); +jest.mock('../../database', () => ({ + __esModule: true, + default: { + active: { + get: () => ({ + query: () => ({ observeWithColumns: () => ({ subscribe: () => ({ unsubscribe: jest.fn() }) }) }) + }) + } + } +})); + +let graceCb: (() => void) | undefined; + +describe('goRoom RoomStore warm-up', () => { + beforeEach(() => { + graceCb = undefined; + jest.spyOn(InteractionManager, 'runAfterInteractions').mockImplementation(((cb: () => void) => { + graceCb = cb; + return { then: () => {} }; + }) as unknown as typeof InteractionManager.runAfterInteractions); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('tears down the warmed store via its grace sweep when navigation is effectively cancelled (no mount)', async () => { + await goRoom({ item: { rid: 'r1', t: 'c' as any }, isMasterDetail: false } as any); + // warm-up left the entry at refCount 0 and scheduled its own grace sweep + + expect(graceCb).toBeDefined(); + + // Peek the warmed entry to capture its instance (peek does not touch refCount). + const warmed = peekOrCreateRoomStore({ rid: 'r1', initialRoom: {} as any }); + + graceCb!(); // grace sweep: refCount 0 -> entry torn down + + const fresh = peekOrCreateRoomStore({ rid: 'r1', initialRoom: {} as any }); // brand-new instance + expect(fresh).not.toBe(warmed); + + releaseRoomStore('r1'); + }); + + it('keeps the store alive when RoomView mounts before the grace sweep fires', async () => { + await goRoom({ item: { rid: 'r1', t: 'c' as any }, isMasterDetail: false } as any); + + expect(graceCb).toBeDefined(); + + // Simulate RoomView mounting against the warmed entry and acquiring it: refCount 0 -> 1. + const mounted = peekOrCreateRoomStore({ rid: 'r1', initialRoom: {} as any }); + acquireRoomStore({ rid: 'r1' }, mounted); + + // Grace sweep fires after the transition: refCount is 1, so the entry stays alive. + graceCb!(); + + const stillAlive = peekOrCreateRoomStore({ rid: 'r1', initialRoom: {} as any }); + expect(stillAlive).toBe(mounted); + + releaseRoomStore('r1'); + }); + + it('does not warm up the store when there is no rid', async () => { + await goRoom({ item: { t: 'c' as any }, isMasterDetail: false } as any); + + expect(graceCb).toBeUndefined(); + }); +}); diff --git a/app/lib/methods/helpers/goRoom.ts b/app/lib/methods/helpers/goRoom.ts index 6433a6c08bb..3d7d808f526 100644 --- a/app/lib/methods/helpers/goRoom.ts +++ b/app/lib/methods/helpers/goRoom.ts @@ -12,6 +12,7 @@ import { import { getRoomTitle, getUidDirectMessage } from './helpers'; import { createDirectMessage } from '../createDirectMessage'; import { emitErrorCreateDirectMessage } from './emitErrorCreateDirectMessage'; +import type { peekOrCreateRoomStore as TPeekOrCreateRoomStore } from '../../../views/RoomView/stores/RoomStore'; interface IGoRoomItem { search?: boolean; // comes from spotlight @@ -21,18 +22,29 @@ interface IGoRoomItem { name?: string; prid?: string; visitor?: IVisitor; + joinCodeRequired?: boolean; } export type TGoRoomItem = IGoRoomItem | TSubscriptionModel | ISubscription | IOmnichannelRoomVisitor; +interface RoomRouteParams { + rid?: string; + name: string; + t?: SubscriptionType; + prid?: string; + visitor?: IVisitor; + joinCodeRequired?: boolean; + roomUserId?: string; +} + const navigate = ({ item, isMasterDetail, ...props }: { item: TGoRoomItem; isMasterDetail: boolean }) => { - const routeParams = { + const routeParams: RoomRouteParams = { rid: item.rid, name: getRoomTitle(item), t: item.t, prid: item.prid, - room: item, visitor: item.visitor, + joinCodeRequired: item.joinCodeRequired, roomUserId: getUidDirectMessage(item), ...props }; @@ -43,6 +55,31 @@ const navigate = ({ item, isMasterDetail, ...props }: { item: TGoRoomItem; isMas return; } + // Warm the RoomStore at press time so its DB observer runs during the nav transition and + // RoomView mounts against a hydrated store. peekOrCreate leaves the entry at refCount 0 and + // schedules its own grace sweep: if RoomView acquires it, it stays alive; otherwise (cancelled/ + // failed navigation) the sweep reclaims it after the transition. No explicit release needed. + if (routeParams.rid) { + // Lazy require: goRoom is a low-level helper imported across the app, RoomStore lives in the + // view layer and pulls the encryption/native graph. Loading it only when a warm-up actually + // runs keeps that graph out of every goRoom importer. + const { peekOrCreateRoomStore } = require('../../../views/RoomView/stores/RoomStore') as { + peekOrCreateRoomStore: typeof TPeekOrCreateRoomStore; + }; + peekOrCreateRoomStore({ + rid: routeParams.rid, + initialRoom: { + rid: routeParams.rid, + t: routeParams.t as string, + name: routeParams.name, + prid: routeParams.prid, + visitor: routeParams.visitor, + joinCodeRequired: routeParams.joinCodeRequired + }, + roomUserId: routeParams.roomUserId + }); + } + Navigation.popTo('DrawerNavigator'); if (isMasterDetail) { return Navigation.dispatch((state: any) => { @@ -113,11 +150,7 @@ export const goRoom = async ({ } } - /** - * Fetches subscription from database to use goRoom with a more complete room object. - * We might want to review this logic in the future, since react-navigation complains about non-serializable data - * https://reactnavigation.org/docs/troubleshooting/#i-get-the-warning-non-serializable-values-were-found-in-the-navigation-state - */ + // Re-fetch the subscription so scalar route params (name, prid, visitor, roomUserId) are as complete as possible. let _item = item; if (item.rid) { const sub = await getSubscriptionByRoomId(item.rid); diff --git a/app/lib/methods/helpers/isReadOnly.test.ts b/app/lib/methods/helpers/isReadOnly.test.ts new file mode 100644 index 00000000000..e048d6268f2 --- /dev/null +++ b/app/lib/methods/helpers/isReadOnly.test.ts @@ -0,0 +1,97 @@ +import { isReadOnly, isReadOnlySync } from './isReadOnly'; +import { hasPermission } from './helpers'; +import { store as reduxStore } from '../../store/auxStore'; + +jest.mock('./helpers', () => ({ + hasPermission: jest.fn() +})); + +jest.mock('../../store/auxStore', () => ({ + store: { getState: jest.fn() } +})); + +const mockedHasPermission = hasPermission as jest.Mock; +const mockedGetState = (reduxStore as unknown as { getState: jest.Mock }).getState; + +const username = 'user1'; +const postReadOnlyPermission = ['admin']; + +beforeEach(() => { + jest.clearAllMocks(); + mockedGetState.mockReturnValue({ permissions: { 'post-readonly': postReadOnlyPermission } }); + mockedHasPermission.mockResolvedValue([false]); +}); + +describe('isReadOnly (async)', () => { + test('archived room is read only', async () => { + expect(await isReadOnly({ archived: true }, username)).toBe(true); + expect(mockedHasPermission).not.toHaveBeenCalled(); + }); + + test('muted user is read only', async () => { + expect(await isReadOnly({ muted: [username] }, username)).toBe(true); + expect(mockedHasPermission).not.toHaveBeenCalled(); + }); + + test('non-ro room is not read only', async () => { + expect(await isReadOnly({}, username)).toBe(false); + expect(mockedHasPermission).not.toHaveBeenCalled(); + }); + + test('ro + archived returns true without fetching permission', async () => { + expect(await isReadOnly({ ro: true, rid: 'rid1', archived: true }, username)).toBe(true); + expect(mockedHasPermission).not.toHaveBeenCalled(); + }); + + test('ro + muted returns true without fetching permission', async () => { + expect(await isReadOnly({ ro: true, rid: 'rid1', muted: [username] }, username)).toBe(true); + expect(mockedHasPermission).not.toHaveBeenCalled(); + }); + + test('ro room without post permission is read only', async () => { + mockedHasPermission.mockResolvedValue([false]); + expect(await isReadOnly({ ro: true, rid: 'rid1' }, username)).toBe(true); + }); + + test('ro room with post permission is not read only', async () => { + mockedHasPermission.mockResolvedValue([true]); + expect(await isReadOnly({ ro: true, rid: 'rid1' }, username)).toBe(false); + }); + + test('ro room with unmuted user is not read only', async () => { + mockedHasPermission.mockResolvedValue([false]); + expect(await isReadOnly({ ro: true, rid: 'rid1', unmuted: [username] }, username)).toBe(false); + }); +}); + +describe('isReadOnlySync', () => { + test('archived room is read only', () => { + expect(isReadOnlySync({ archived: true }, username, postReadOnlyPermission, [])).toBe(true); + }); + + test('muted user is read only', () => { + expect(isReadOnlySync({ muted: [username] }, username, postReadOnlyPermission, [])).toBe(true); + }); + + test('non-ro room is not read only', () => { + expect(isReadOnlySync({}, username, postReadOnlyPermission, [])).toBe(false); + }); + + test('ro room without matching role is read only', () => { + expect(isReadOnlySync({ ro: true, roles: ['user'] }, username, postReadOnlyPermission, ['user'])).toBe(true); + }); + + test('ro room with matching room role is not read only', () => { + expect(isReadOnlySync({ ro: true, roles: ['admin'] }, username, postReadOnlyPermission, [])).toBe(false); + }); + + test('ro room with matching user role is not read only', () => { + expect(isReadOnlySync({ ro: true, roles: [] }, username, postReadOnlyPermission, ['admin'])).toBe(false); + }); + + test('ro room with unmuted user is not read only', () => { + expect(isReadOnlySync({ ro: true, roles: ['user'], unmuted: [username] }, username, postReadOnlyPermission, ['user'])).toBe( + false + ); + }); +}); diff --git a/app/lib/methods/helpers/isReadOnly.ts b/app/lib/methods/helpers/isReadOnly.ts index 345f51ecbeb..1dca9bb578b 100644 --- a/app/lib/methods/helpers/isReadOnly.ts +++ b/app/lib/methods/helpers/isReadOnly.ts @@ -14,7 +14,7 @@ const canPostReadOnly = async (room: Partial, username?: string) const isMuted = (room: Partial, username?: string) => room && room.muted && room.muted.find && !!room.muted.find(m => m === username); -export const isReadOnly = async (room: Partial, username?: string): Promise => { +const evaluateReadOnly = (room: Partial, username: string | undefined, allowPost: boolean): boolean => { if (room.archived) { return true; } @@ -22,11 +22,30 @@ export const isReadOnly = async (room: Partial, username?: string return true; } if (room?.ro) { - const allowPost = await canPostReadOnly(room, username); - if (allowPost) { - return false; - } - return true; + return !allowPost; } return false; }; + +export const isReadOnly = async (room: Partial, username?: string): Promise => { + if (room.archived || isMuted(room, username)) { + return true; + } + const allowPost = room?.ro ? await canPostReadOnly(room, username) : false; + return evaluateReadOnly(room, username, allowPost); +}; + +export const isReadOnlySync = ( + room: Partial, + username: string, + postReadOnlyPermission: string[] | undefined, + userRoles: string[] +): boolean => { + let allowPost = false; + if (room?.ro) { + const isUnmuted = !!room?.unmuted?.find(m => m === username); + const mergedRoles = [...new Set([...(room.roles || []), ...userRoles])]; + allowPost = !!postReadOnlyPermission?.some(r => mergedRoles.includes(r)) || isUnmuted; + } + return evaluateReadOnly(room, username, allowPost); +}; diff --git a/app/lib/methods/toggleFollowThread.ts b/app/lib/methods/toggleFollowThread.ts new file mode 100644 index 00000000000..0e72e031d24 --- /dev/null +++ b/app/lib/methods/toggleFollowThread.ts @@ -0,0 +1,14 @@ +import I18n from '../../i18n'; +import EventEmitter from './helpers/events'; +import log from './helpers/log'; +import { toggleFollowMessage } from '../services/restApi'; +import { LISTENER } from '../../containers/Toast'; + +export const toggleFollowThread = async (threadMessageId: string, isFollowingThread: boolean): Promise => { + try { + await toggleFollowMessage(threadMessageId, !isFollowingThread); + EventEmitter.emit(LISTENER, { message: isFollowingThread ? I18n.t('Unfollowed_thread') : I18n.t('Following_thread') }); + } catch (e) { + log(e); + } +}; diff --git a/app/lib/navigation/appNavigation.test.ts b/app/lib/navigation/appNavigation.test.ts new file mode 100644 index 00000000000..3e8380d652a --- /dev/null +++ b/app/lib/navigation/appNavigation.test.ts @@ -0,0 +1,28 @@ +import Navigation from './appNavigation'; + +const mockDispatch = jest.fn(); + +describe('popToRoom', () => { + beforeEach(() => { + jest.clearAllMocks(); + Navigation.navigationRef.current = { dispatch: mockDispatch } as any; + }); + + it('merges params when popping back to a retained RoomView', () => { + Navigation.popToRoom(false); + + expect(mockDispatch).toHaveBeenCalledWith({ + type: 'POP_TO', + payload: { name: 'RoomView', params: undefined, merge: true } + }); + }); + + it('pops to DrawerNavigator on master detail', () => { + Navigation.popToRoom(true); + + expect(mockDispatch).toHaveBeenCalledWith({ + type: 'POP_TO', + payload: { name: 'DrawerNavigator', params: undefined, merge: undefined } + }); + }); +}); diff --git a/app/lib/navigation/appNavigation.ts b/app/lib/navigation/appNavigation.ts index 58ae4966448..b74fdc00687 100644 --- a/app/lib/navigation/appNavigation.ts +++ b/app/lib/navigation/appNavigation.ts @@ -24,8 +24,8 @@ function replace(name: string, params: any) { } // Pops to the first occurrence of the given route name, usually RoomView -function popTo(name: string) { - navigationRef.current?.dispatch(StackActions.popTo(name)); +function popTo(name: string, params?: any, options?: { merge?: boolean }) { + navigationRef.current?.dispatch(StackActions.popTo(name, params, options)); } // Removes RoomView from the stack and leaves only RoomsListView open @@ -47,7 +47,8 @@ function popToRoom(isMasterDetail: boolean) { if (isMasterDetail) { popTo('DrawerNavigator'); } else { - popTo('RoomView'); + // merge keeps the retained RoomView's existing params instead of replacing them with undefined + popTo('RoomView', undefined, { merge: true }); } } diff --git a/app/reducers/index.js b/app/reducers/index.js index 4258685f94f..ae7824146c8 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -1,6 +1,7 @@ import { combineReducers } from 'redux'; import inquiry from '../ee/omnichannel/reducers/inquiry'; +import routingConfig from '../ee/omnichannel/reducers/routingConfig'; import settings from './settings'; import login from './login'; import meteor from './connect'; @@ -45,6 +46,7 @@ export default combineReducers({ inviteLinks, createDiscussion, inquiry, + routingConfig, enterpriseModules, encryption, permissions, diff --git a/app/sagas/index.js b/app/sagas/index.js index b60cc211837..d5b65d01919 100644 --- a/app/sagas/index.js +++ b/app/sagas/index.js @@ -1,6 +1,7 @@ import { all } from 'redux-saga/effects'; import inquiry from '../ee/omnichannel/sagas/inquiry'; +import routingConfig from '../ee/omnichannel/sagas/routingConfig'; import login from './login'; import rooms from './rooms'; import room from './room'; @@ -30,6 +31,7 @@ const root = function* root() { inviteLinks(), createDiscussion(), inquiry(), + routingConfig(), encryption(), videoConf(), troubleshootingNotification() diff --git a/app/stacks/MasterDetailStack/types.ts b/app/stacks/MasterDetailStack/types.ts index fb3e3619344..03e6b649200 100644 --- a/app/stacks/MasterDetailStack/types.ts +++ b/app/stacks/MasterDetailStack/types.ts @@ -42,11 +42,13 @@ export type ModalStackParamList = { }; }; RoomInfoView: { - room: ISubscription; - member: any; + room?: ISubscription; + member?: any; rid: string; t: SubscriptionType; showCloseModal?: boolean; + fromRid?: string; + itsMe?: boolean; }; SelectListView: { data: any; diff --git a/app/stacks/types.ts b/app/stacks/types.ts index 8c1b6608281..734985edd49 100644 --- a/app/stacks/types.ts +++ b/app/stacks/types.ts @@ -14,7 +14,8 @@ import { type TDataSelect, type TMessageAction, type TSubscriptionModel, - type TThreadModel + type TThreadModel, + type IVisitor } from '../definitions'; import { type ModalStackParamList } from './MasterDetailStack/types'; import { type TNavigation } from './stackType'; @@ -38,7 +39,8 @@ export type ChatsStackParamList = { name?: string; fname?: string; prid?: string; - room?: TSubscriptionModel | { rid: string; t: string; name?: string; fname?: string; prid?: string }; + visitor?: IVisitor; + joinCodeRequired?: boolean; jumpToMessageId?: string; jumpToThreadId?: string; roomUserId?: string | null; diff --git a/app/views/MessagesView/index.tsx b/app/views/MessagesView/index.tsx index 4175ab9141a..94702c2c7a3 100644 --- a/app/views/MessagesView/index.tsx +++ b/app/views/MessagesView/index.tsx @@ -9,6 +9,7 @@ import { type EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-cont import { type MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import Message from '../../containers/message'; import { MessageRoomProvider } from '../../containers/message/stores/MessageRoomStore'; +import { A11yGateProvider } from '../../containers/message/stores/A11yGate'; import ActivityIndicator from '../../containers/ActivityIndicator'; import I18n from '../../i18n'; import getFileUrlAndTypeFromMessage from './getFileUrlAndTypeFromMessage'; @@ -24,7 +25,6 @@ import { type IApplicationState, type IRoomInfoParam, type TMessageModel, - type ISubscription, SubscriptionType, type IAttachment, type IMessage, @@ -44,7 +44,6 @@ interface IMessagesViewProps { username: string; token: string; }; - baseUrl: string; navigation: CompositeNavigationProp< NativeStackNavigationProp, NativeStackNavigationProp @@ -72,7 +71,6 @@ interface IParams { name?: string; fname?: string; prid?: string; - room?: ISubscription; jumpToMessageId?: string; jumpToThreadId?: string; roomUserId?: string; @@ -82,7 +80,6 @@ class MessagesView extends Component { private rid: string; private t: SubscriptionType; private content: any; - private room?: ISubscription; constructor(props: IMessagesViewProps) { super(props); @@ -141,8 +138,7 @@ class MessagesView extends Component { let params: IParams = { rid: this.rid, jumpToMessageId: item._id, - t: this.t, - room: this.room + t: this.t }; if (item.tmid) { Navigation.popToRoom(isMasterDetail); @@ -291,6 +287,8 @@ class MessagesView extends Component { navigation.navigate('AttachmentView', { attachment }); }; + messageHandlers = { navToRoomInfo: this.navToRoomInfo, showAttachment: this.showAttachment }; + onLongPress = (message: IMessage) => { this.setState({ message }, this.showActionSheet); }; @@ -334,7 +332,7 @@ class MessagesView extends Component { render() { const { messages, loading } = this.state; - const { theme, user, baseUrl, insets } = this.props; + const { theme, insets } = this.props; if (!loading && messages.length === 0) { return this.renderEmpty(); @@ -342,31 +340,25 @@ class MessagesView extends Component { return ( - - item._id} - onEndReached={this.load} - contentContainerStyle={{ paddingBottom: insets.bottom }} - ListFooterComponent={loading ? : null} - /> - + + + item._id} + onEndReached={this.load} + contentContainerStyle={{ paddingBottom: insets.bottom }} + ListFooterComponent={loading ? : null} + /> + + ); } } const mapStateToProps = (state: IApplicationState) => ({ - baseUrl: state.server.server, user: getUserSelector(state) }); diff --git a/app/views/ModalBlockView.tsx b/app/views/ModalBlockView.tsx index d9eb9802d67..290977e8dff 100644 --- a/app/views/ModalBlockView.tsx +++ b/app/views/ModalBlockView.tsx @@ -17,8 +17,8 @@ import { triggerBlockAction, triggerCancel, triggerSubmitView } from '../lib/met import { type IApplicationState, type TAnyMessageModel } from '../definitions'; import KeyboardView from '../containers/KeyboardView'; import { MessageRoomProvider } from '../containers/message/stores/MessageRoomStore'; +import { A11yGateProvider } from '../containers/message/stores/A11yGate'; import { MessageProvider } from '../containers/message/stores/MessageStore'; -import { getUserSelector } from '../selectors/login'; const styles = StyleSheet.create({ content: { @@ -54,8 +54,6 @@ interface IModalBlockViewProps { route: RouteProp; theme: TSupportedThemes; language: string; - user: ReturnType; - baseUrl: string; } // eslint-disable-next-line no-sequences @@ -97,7 +95,7 @@ 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. Routed through `unknown` (the repo's convention for +// user/baseUrl the media hooks now read from redux. 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; @@ -260,7 +258,7 @@ class ModalBlockView extends Component - - - - - + + + + + + + @@ -295,9 +295,7 @@ class ModalBlockView extends Component ({ - language: state.login.user.language as string, - baseUrl: state.server.server, - user: getUserSelector(state) + language: state.login.user.language as string }); export default connect(mapStateToProps)(ModalBlockView); diff --git a/app/views/RoomView/JoinCode.tsx b/app/views/RoomView/JoinCode.tsx deleted file mode 100644 index e4aaee45346..00000000000 --- a/app/views/RoomView/JoinCode.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import { forwardRef, memo, useImperativeHandle, useState } from 'react'; -import { InteractionManager, StyleSheet, Text, View } from 'react-native'; -import Modal from 'react-native-modal'; -import { connect } from 'react-redux'; -import { GestureHandlerRootView } from 'react-native-gesture-handler'; - -import I18n from '../../i18n'; -import Button from '../../containers/Button'; -import { FormTextInput } from '../../containers/TextInput'; -import sharedStyles from '../Styles'; -import { themes } from '../../lib/constants/colors'; -import { joinRoom } from '../../lib/services/restApi'; -import { type TSupportedThemes } from '../../theme'; -import { withMasterDetail } from '../../lib/hooks/useMasterDetail'; - -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center' - }, - content: { - padding: 16, - width: '100%', - borderRadius: 4 - }, - title: { - fontSize: 16, - paddingBottom: 8, - ...sharedStyles.textBold, - ...sharedStyles.textAlignCenter - }, - button: { - minWidth: 96, - marginBottom: 0 - }, - buttonContainer: { - flexDirection: 'row', - justifyContent: 'space-between' - }, - tablet: { - height: undefined - } -}); - -export interface IJoinCodeProps { - rid: string; - t: string; - onJoin: Function; - isMasterDetail: boolean; - theme: TSupportedThemes; -} - -export interface IJoinCode { - show: () => void; -} - -const JoinCode = memo( - forwardRef(({ rid, t, onJoin, isMasterDetail, theme }, ref) => { - const [visible, setVisible] = useState(false); - const [error, setError] = useState(false); - const [code, setCode] = useState(''); - - const show = () => setVisible(true); - - const hide = () => setVisible(false); - - const handleJoinRoom = async () => { - try { - await joinRoom(rid, code, t as any); - onJoin(); - hide(); - } catch (e) { - setError(true); - } - }; - - useImperativeHandle(ref, () => ({ show })); - - return ( - - - - {I18n.t('Insert_Join_Code')} - { - if (e) { - InteractionManager.runAfterInteractions(() => { - e.focus(); - }); - } - }} - returnKeyType='send' - autoCapitalize='none' - onChangeText={setCode} - onSubmitEditing={handleJoinRoom} - placeholder={I18n.t('Join_Code')} - secureTextEntry - error={error ? { error: 'error-code-invalid', reason: I18n.t('Code_or_password_invalid') } : undefined} - testID='join-code-input' - /> - -