diff --git a/package.json b/package.json index fc34d9d81542..0cf05689bfae 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "dependencies": { "@formatjs/fast-memoize": "1.2.6", "@indutny/frameless-titlebar": "2.3.5", - "@indutny/sneequals": "3.2.0", + "@indutny/sneequals": "4.0.0", "@popperjs/core": "2.11.6", "@react-spring/web": "9.5.5", "@signalapp/better-sqlite3": "8.1.1", diff --git a/ts/hooks/useProxySelector.ts b/ts/hooks/useProxySelector.ts new file mode 100644 index 000000000000..5359070e4587 --- /dev/null +++ b/ts/hooks/useProxySelector.ts @@ -0,0 +1,23 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { useCallback, useMemo } from 'react'; +import { useSelector } from 'react-redux'; +import { memoize } from '@indutny/sneequals'; + +import type { StateType } from '../state/reducer'; + +export function useProxySelector, Result>( + selector: (state: StateType, ...params: Params) => Result, + ...params: Params +): Result { + const memoized = useMemo(() => memoize(selector), [selector]); + + return useSelector( + useCallback( + (state: StateType) => memoized(state, ...params), + // eslint-disable-next-line react-hooks/exhaustive-deps + [memoized, ...params] + ) + ); +} diff --git a/ts/state/ducks/calling.ts b/ts/state/ducks/calling.ts index bd9967435dff..a9f3d4e73b69 100644 --- a/ts/state/ducks/calling.ts +++ b/ts/state/ducks/calling.ts @@ -55,6 +55,8 @@ import { ToastType } from '../../types/Toast'; import type { ShowToastActionType } from './toast'; import { singleProtoJobQueue } from '../../jobs/singleProtoJobQueue'; import MessageSender from '../../textsecure/SendMessage'; +import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; +import { useBoundActions } from '../../hooks/useBoundActions'; // State @@ -1566,6 +1568,10 @@ export const actions = { toggleSpeakerView, }; +export const useCallingActions = (): BoundActionCreatorsMapObject< + typeof actions +> => useBoundActions(actions); + export type ActionsType = ReadonlyDeep; // Reducer diff --git a/ts/state/selectors/message.ts b/ts/state/selectors/message.ts index 5c77ce6dceb8..1fe13e7040c2 100644 --- a/ts/state/selectors/message.ts +++ b/ts/state/selectors/message.ts @@ -7,7 +7,6 @@ import filesize from 'filesize'; import getDirection from 'direction'; import emojiRegex from 'emoji-regex'; import LinkifyIt from 'linkify-it'; -import { memoize } from '@indutny/sneequals'; import type { StateType } from '../reducer'; import type { @@ -269,62 +268,58 @@ export function getConversation( // Message -export const getAttachmentsForMessage = memoize( - ({ - sticker, - attachments = [], - }: MessageWithUIFieldsType): Array => { - if (sticker && sticker.data) { - const { data } = sticker; +export const getAttachmentsForMessage = ({ + sticker, + attachments = [], +}: MessageWithUIFieldsType): Array => { + if (sticker && sticker.data) { + const { data } = sticker; - // We don't show anything if we don't have the sticker or the blurhash... - if (!data.blurHash && (data.pending || !data.path)) { - return []; - } - - return [ - { - ...data, - // We want to show the blurhash for stickers, not the spinner - pending: false, - url: data.path - ? window.Signal.Migrations.getAbsoluteAttachmentPath(data.path) - : undefined, - }, - ]; + // We don't show anything if we don't have the sticker or the blurhash... + if (!data.blurHash && (data.pending || !data.path)) { + return []; } - return attachments - .filter(attachment => !attachment.error || canBeDownloaded(attachment)) - .map(attachment => getPropsForAttachment(attachment)) - .filter(isNotNil); + return [ + { + ...data, + // We want to show the blurhash for stickers, not the spinner + pending: false, + url: data.path + ? window.Signal.Migrations.getAbsoluteAttachmentPath(data.path) + : undefined, + }, + ]; } -); -export const processBodyRanges = memoize( - ( - { bodyRanges }: Pick, - options: { conversationSelector: GetConversationByIdType } - ): HydratedBodyRangesType | undefined => { - if (!bodyRanges) { - return undefined; - } + return attachments + .filter(attachment => !attachment.error || canBeDownloaded(attachment)) + .map(attachment => getPropsForAttachment(attachment)) + .filter(isNotNil); +}; - return bodyRanges - .filter(range => range.mentionUuid) - .map(range => { - const { conversationSelector } = options; - const conversation = conversationSelector(range.mentionUuid); - - return { - ...range, - conversationID: conversation.id, - replacementText: conversation.title, - }; - }) - .sort((a, b) => b.start - a.start); +export const processBodyRanges = ( + { bodyRanges }: Pick, + options: { conversationSelector: GetConversationByIdType } +): HydratedBodyRangesType | undefined => { + if (!bodyRanges) { + return undefined; } -); + + return bodyRanges + .filter(range => range.mentionUuid) + .map(range => { + const { conversationSelector } = options; + const conversation = conversationSelector(range.mentionUuid); + + return { + ...range, + conversationID: conversation.id, + replacementText: conversation.title, + }; + }) + .sort((a, b) => b.start - a.start); +}; const getAuthorForMessage = ( message: MessageWithUIFieldsType, @@ -482,74 +477,72 @@ const getPropsForStoryReplyContext = ( }; }; -export const getPropsForQuote = memoize( - ( - message: Pick< - MessageWithUIFieldsType, - 'conversationId' | 'quote' | 'payment' - >, - { - conversationSelector, - ourConversationId, - }: { - conversationSelector: GetConversationByIdType; - ourConversationId?: string; - } - ): PropsData['quote'] => { - const { quote } = message; - if (!quote) { - return undefined; - } - - const { - author, - authorUuid, - id: sentAt, - isViewOnce, - isGiftBadge: isTargetGiftBadge, - referencedMessageNotFound, - payment, - text = '', - } = quote; - - const contact = conversationSelector(authorUuid || author); - - const authorId = contact.id; - const authorName = contact.name; - const authorPhoneNumber = contact.phoneNumber; - const authorProfileName = contact.profileName; - const authorTitle = contact.title; - const isFromMe = authorId === ourConversationId; - - const firstAttachment = quote.attachments && quote.attachments[0]; - const conversation = getConversation(message, conversationSelector); - - const { conversationColor, customColor } = - getConversationColorAttributes(conversation); - - return { - authorId, - authorName, - authorPhoneNumber, - authorProfileName, - authorTitle, - bodyRanges: processBodyRanges(quote, { conversationSelector }), - conversationColor, - conversationTitle: conversation.title, - customColor, - isFromMe, - rawAttachment: firstAttachment - ? processQuoteAttachment(firstAttachment) - : undefined, - payment, - isGiftBadge: Boolean(isTargetGiftBadge), - isViewOnce, - referencedMessageNotFound, - sentAt: Number(sentAt), - text, - }; +export const getPropsForQuote = ( + message: Pick< + MessageWithUIFieldsType, + 'conversationId' | 'quote' | 'payment' + >, + { + conversationSelector, + ourConversationId, + }: { + conversationSelector: GetConversationByIdType; + ourConversationId?: string; } -); +): PropsData['quote'] => { + const { quote } = message; + if (!quote) { + return undefined; + } + + const { + author, + authorUuid, + id: sentAt, + isViewOnce, + isGiftBadge: isTargetGiftBadge, + referencedMessageNotFound, + payment, + text = '', + } = quote; + + const contact = conversationSelector(authorUuid || author); + + const authorId = contact.id; + const authorName = contact.name; + const authorPhoneNumber = contact.phoneNumber; + const authorProfileName = contact.profileName; + const authorTitle = contact.title; + const isFromMe = authorId === ourConversationId; + + const firstAttachment = quote.attachments && quote.attachments[0]; + const conversation = getConversation(message, conversationSelector); + + const { conversationColor, customColor } = + getConversationColorAttributes(conversation); + + return { + authorId, + authorName, + authorPhoneNumber, + authorProfileName, + authorTitle, + bodyRanges: processBodyRanges(quote, { conversationSelector }), + conversationColor, + conversationTitle: conversation.title, + customColor, + isFromMe, + rawAttachment: firstAttachment + ? processQuoteAttachment(firstAttachment) + : undefined, + payment, + isGiftBadge: Boolean(isTargetGiftBadge), + isViewOnce, + referencedMessageNotFound, + sentAt: Number(sentAt), + text, + }; +}; export type GetPropsForMessageOptions = Pick< GetPropsForBubbleOptions, @@ -632,113 +625,110 @@ function getTextDirection(body?: string): TextDirection { } } -export const getPropsForMessage = memoize( - ( - message: MessageWithUIFieldsType, - options: GetPropsForMessageOptions - ): Omit => { - const attachments = getAttachmentsForMessage(message); - const bodyRanges = processBodyRanges(message, options); - const author = getAuthorForMessage(message, options); - const previews = getPreviewsForMessage(message); - const reactions = getReactionsForMessage(message, options); - const quote = getPropsForQuote(message, options); - const storyReplyContext = getPropsForStoryReplyContext(message, options); - const textAttachment = getTextAttachment(message); - const payment = getPayment(message); +export const getPropsForMessage = ( + message: MessageWithUIFieldsType, + options: GetPropsForMessageOptions +): Omit => { + const attachments = getAttachmentsForMessage(message); + const bodyRanges = processBodyRanges(message, options); + const author = getAuthorForMessage(message, options); + const previews = getPreviewsForMessage(message); + const reactions = getReactionsForMessage(message, options); + const quote = getPropsForQuote(message, options); + const storyReplyContext = getPropsForStoryReplyContext(message, options); + const textAttachment = getTextAttachment(message); + const payment = getPayment(message); - const { - accountSelector, - conversationSelector, - ourConversationId, - ourNumber, - ourACI, - regionCode, - selectedMessageId, - selectedMessageCounter, - contactNameColorSelector, - } = options; + const { + accountSelector, + conversationSelector, + ourConversationId, + ourNumber, + ourACI, + regionCode, + selectedMessageId, + selectedMessageCounter, + contactNameColorSelector, + } = options; - const { expireTimer, expirationStartTimestamp, conversationId } = message; - const expirationLength = expireTimer - ? DurationInSeconds.toMillis(expireTimer) - : undefined; + const { expireTimer, expirationStartTimestamp, conversationId } = message; + const expirationLength = expireTimer + ? DurationInSeconds.toMillis(expireTimer) + : undefined; - const conversation = getConversation(message, conversationSelector); - const isGroup = conversation.type === 'group'; - const { sticker } = message; + const conversation = getConversation(message, conversationSelector); + const isGroup = conversation.type === 'group'; + const { sticker } = message; - const isMessageTapToView = isTapToView(message); + const isMessageTapToView = isTapToView(message); - const isSelected = message.id === selectedMessageId; + const isSelected = message.id === selectedMessageId; - const selectedReaction = ( - (message.reactions || []).find(re => re.fromId === ourConversationId) || - {} - ).emoji; + const selectedReaction = ( + (message.reactions || []).find(re => re.fromId === ourConversationId) || {} + ).emoji; - const authorId = getContactId(message, { - conversationSelector, - ourConversationId, - ourNumber, - ourACI, - }); - const contactNameColor = contactNameColorSelector(conversationId, authorId); + const authorId = getContactId(message, { + conversationSelector, + ourConversationId, + ourNumber, + ourACI, + }); + const contactNameColor = contactNameColorSelector(conversationId, authorId); - const { conversationColor, customColor } = - getConversationColorAttributes(conversation); + const { conversationColor, customColor } = + getConversationColorAttributes(conversation); - return { - attachments, - author, - bodyRanges, - previews, - quote, - reactions, - storyReplyContext, - textAttachment, - payment, - canDeleteForEveryone: canDeleteForEveryone(message), - canDownload: canDownload(message, conversationSelector), - canReact: canReact(message, ourConversationId, conversationSelector), - canReply: canReply(message, ourConversationId, conversationSelector), - canRetry: hasErrors(message), - canRetryDeleteForEveryone: canRetryDeleteForEveryone(message), - contact: getPropsForEmbeddedContact(message, regionCode, accountSelector), - contactNameColor, - conversationColor, - conversationId, - conversationTitle: conversation.title, - conversationType: isGroup ? 'group' : 'direct', - customColor, - deletedForEveryone: message.deletedForEveryone || false, - direction: isIncoming(message) ? 'incoming' : 'outgoing', - displayLimit: message.displayLimit, - expirationLength, - expirationTimestamp: calculateExpirationTimestamp({ - expireTimer, - expirationStartTimestamp, - }), - giftBadge: message.giftBadge, - id: message.id, - isBlocked: conversation.isBlocked || false, - isMessageRequestAccepted: conversation?.acceptedMessageRequest ?? true, - isSelected, - isSelectedCounter: isSelected ? selectedMessageCounter : undefined, - isSticker: Boolean(sticker), - isTapToView: isMessageTapToView, - isTapToViewError: - isMessageTapToView && isIncoming(message) && message.isTapToViewInvalid, - isTapToViewExpired: isMessageTapToView && message.isErased, - readStatus: message.readStatus ?? ReadStatus.Read, - selectedReaction, - status: getMessagePropStatus(message, ourConversationId), - text: message.body, - textDirection: getTextDirection(message.body), - timestamp: message.sent_at, - }; - } -); + return { + attachments, + author, + bodyRanges, + previews, + quote, + reactions, + storyReplyContext, + textAttachment, + payment, + canDeleteForEveryone: canDeleteForEveryone(message), + canDownload: canDownload(message, conversationSelector), + canReact: canReact(message, ourConversationId, conversationSelector), + canReply: canReply(message, ourConversationId, conversationSelector), + canRetry: hasErrors(message), + canRetryDeleteForEveryone: canRetryDeleteForEveryone(message), + contact: getPropsForEmbeddedContact(message, regionCode, accountSelector), + contactNameColor, + conversationColor, + conversationId, + conversationTitle: conversation.title, + conversationType: isGroup ? 'group' : 'direct', + customColor, + deletedForEveryone: message.deletedForEveryone || false, + direction: isIncoming(message) ? 'incoming' : 'outgoing', + displayLimit: message.displayLimit, + expirationLength, + expirationTimestamp: calculateExpirationTimestamp({ + expireTimer, + expirationStartTimestamp, + }), + giftBadge: message.giftBadge, + id: message.id, + isBlocked: conversation.isBlocked || false, + isMessageRequestAccepted: conversation?.acceptedMessageRequest ?? true, + isSelected, + isSelectedCounter: isSelected ? selectedMessageCounter : undefined, + isSticker: Boolean(sticker), + isTapToView: isMessageTapToView, + isTapToViewError: + isMessageTapToView && isIncoming(message) && message.isTapToViewInvalid, + isTapToViewExpired: isMessageTapToView && message.isErased, + readStatus: message.readStatus ?? ReadStatus.Read, + selectedReaction, + status: getMessagePropStatus(message, ourConversationId), + text: message.body, + textDirection: getTextDirection(message.body), + timestamp: message.sent_at, + }; +}; // This is getPropsForMessage but wrapped in reselect's createSelector so that // we can derive all of the selector dependencies that getPropsForMessage @@ -1067,7 +1057,7 @@ function getPropsForTimerNotification( const { expireTimer, fromSync, source, sourceUuid } = timerUpdate; const disabled = !expireTimer; const sourceId = sourceUuid || source; - const formattedContact = conversationSelector(sourceId); + const { id: formattedContactId, title } = conversationSelector(sourceId); // Pacify typescript type MaybeExpireTimerType = @@ -1087,7 +1077,7 @@ function getPropsForTimerNotification( }; const basicProps = { - ...formattedContact, + title, ...maybeExpireTimer, type: 'fromOther' as const, }; @@ -1098,7 +1088,7 @@ function getPropsForTimerNotification( type: 'fromSync' as const, }; } - if (formattedContact.id === ourConversationId) { + if (formattedContactId === ourConversationId) { return { ...basicProps, type: 'fromMe' as const, diff --git a/ts/state/selectors/timeline.ts b/ts/state/selectors/timeline.ts index 66c2c2fa1514..056e259f76dd 100644 --- a/ts/state/selectors/timeline.ts +++ b/ts/state/selectors/timeline.ts @@ -1,11 +1,9 @@ // Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { memoize } from '@indutny/sneequals'; import type { TimelineItemType } from '../../components/conversation/TimelineItem'; import type { StateType } from '../reducer'; -import type { MessageWithUIFieldsType } from '../ducks/conversations'; import { getContactNameColorSelector, getConversationSelector, @@ -23,41 +21,14 @@ import { import { getActiveCall, getCallSelector } from './calling'; import { getPropsForBubble } from './message'; -const getTimelineItemInner = memoize( - (message: MessageWithUIFieldsType, state: StateType): TimelineItemType => { - const selectedMessage = getSelectedMessage(state); - const conversationSelector = getConversationSelector(state); - const regionCode = getRegionCode(state); - const ourNumber = getUserNumber(state); - const ourACI = getUserACI(state); - const ourPNI = getUserPNI(state); - const ourConversationId = getUserConversationId(state); - const callSelector = getCallSelector(state); - const activeCall = getActiveCall(state); - const accountSelector = getAccountSelector(state); - const contactNameColorSelector = getContactNameColorSelector(state); - - return getPropsForBubble(message, { - conversationSelector, - ourConversationId, - ourNumber, - ourACI, - ourPNI, - regionCode, - selectedMessageId: selectedMessage?.id, - selectedMessageCounter: selectedMessage?.counter, - contactNameColorSelector, - callSelector, - activeCall, - accountSelector, - }); - } -); - export const getTimelineItem = ( state: StateType, - id: string + id?: string ): TimelineItemType | undefined => { + if (id === undefined) { + return undefined; + } + const messageLookup = getMessages(state); const message = messageLookup[id]; @@ -65,5 +36,30 @@ export const getTimelineItem = ( return undefined; } - return getTimelineItemInner(message, state); + const selectedMessage = getSelectedMessage(state); + const conversationSelector = getConversationSelector(state); + const regionCode = getRegionCode(state); + const ourNumber = getUserNumber(state); + const ourACI = getUserACI(state); + const ourPNI = getUserPNI(state); + const ourConversationId = getUserConversationId(state); + const callSelector = getCallSelector(state); + const activeCall = getActiveCall(state); + const accountSelector = getAccountSelector(state); + const contactNameColorSelector = getContactNameColorSelector(state); + + return getPropsForBubble(message, { + conversationSelector, + ourConversationId, + ourNumber, + ourACI, + ourPNI, + regionCode, + selectedMessageId: selectedMessage?.id, + selectedMessageCounter: selectedMessage?.counter, + contactNameColorSelector, + callSelector, + activeCall, + accountSelector, + }); }; diff --git a/ts/state/smart/Timeline.tsx b/ts/state/smart/Timeline.tsx index a2aef61e8d5b..f16bcc18a6da 100644 --- a/ts/state/smart/Timeline.tsx +++ b/ts/state/smart/Timeline.tsx @@ -32,9 +32,6 @@ import { SmartContactSpoofingReviewDialog } from './ContactSpoofingReviewDialog' import type { PropsType as SmartContactSpoofingReviewDialogPropsType } from './ContactSpoofingReviewDialog'; import { SmartTypingBubble } from './TypingBubble'; import { SmartHeroRow } from './HeroRow'; -import { renderAudioAttachment } from './renderAudioAttachment'; -import { renderEmojiPicker } from './renderEmojiPicker'; -import { renderReactionPicker } from './renderReactionPicker'; import { getOwn } from '../../util/getOwn'; import { assertDev } from '../../util/assert'; @@ -82,9 +79,6 @@ function renderItem({ messageId={messageId} previousMessageId={previousMessageId} nextMessageId={nextMessageId} - renderEmojiPicker={renderEmojiPicker} - renderReactionPicker={renderReactionPicker} - renderAudioAttachment={renderAudioAttachment} unreadIndicatorPlacement={unreadIndicatorPlacement} /> ); diff --git a/ts/state/smart/TimelineItem.tsx b/ts/state/smart/TimelineItem.tsx index c96e2aa3115d..0c67c09e4324 100644 --- a/ts/state/smart/TimelineItem.tsx +++ b/ts/state/smart/TimelineItem.tsx @@ -3,18 +3,21 @@ import type { RefObject } from 'react'; import React from 'react'; -import { connect } from 'react-redux'; - -import { mapDispatchToProps } from '../actions'; -import type { StateType } from '../reducer'; +import { useSelector } from 'react-redux'; import { TimelineItem } from '../../components/conversation/TimelineItem'; +import type { WidthBreakpoint } from '../../components/_util'; +import { useProxySelector } from '../../hooks/useProxySelector'; +import { useConversationsActions } from '../ducks/conversations'; +import { useComposerActions } from '../ducks/composer'; +import { useGlobalModalActions } from '../ducks/globalModals'; +import { useAccountsActions } from '../ducks/accounts'; +import { useLightboxActions } from '../ducks/lightbox'; +import { useStoriesActions } from '../ducks/stories'; +import { useCallingActions } from '../ducks/calling'; import { getPreferredBadgeSelector } from '../selectors/badges'; import { getIntl, getInteractionMode, getTheme } from '../selectors/user'; -import { - getConversationSelector, - getSelectedMessage, -} from '../selectors/conversations'; +import { getSelectedMessage } from '../selectors/conversations'; import { getTimelineItem } from '../selectors/timeline'; import { areMessagesInSameGroup, @@ -25,9 +28,13 @@ import { import { SmartContactName } from './ContactName'; import { SmartUniversalTimerNotification } from './UniversalTimerNotification'; import { isSameDay } from '../../util/timestamp'; +import { renderAudioAttachment } from './renderAudioAttachment'; +import { renderEmojiPicker } from './renderEmojiPicker'; +import { renderReactionPicker } from './renderReactionPicker'; type ExternalProps = { containerElementRef: RefObject; + containerWidthBreakpoint: WidthBreakpoint; conversationId: string; isOldestTimelineItem: boolean; messageId: string; @@ -44,9 +51,10 @@ function renderUniversalTimerNotification(): JSX.Element { return ; } -const mapStateToProps = (state: StateType, props: ExternalProps) => { +export function SmartTimelineItem(props: ExternalProps): JSX.Element { const { containerElementRef, + containerWidthBreakpoint, conversationId, isOldestTimelineItem, messageId, @@ -55,21 +63,19 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => { unreadIndicatorPlacement, } = props; - const item = getTimelineItem(state, messageId); - const previousItem = previousMessageId - ? getTimelineItem(state, previousMessageId) - : undefined; - const nextItem = nextMessageId - ? getTimelineItem(state, nextMessageId) - : undefined; + const i18n = useSelector(getIntl); + const getPreferredBadge = useSelector(getPreferredBadgeSelector); + const interactionMode = useSelector(getInteractionMode); + const theme = useSelector(getTheme); + const item = useProxySelector(getTimelineItem, messageId); + const previousItem = useProxySelector(getTimelineItem, previousMessageId); + const nextItem = useProxySelector(getTimelineItem, nextMessageId); - const selectedMessage = getSelectedMessage(state); + const selectedMessage = useSelector(getSelectedMessage); const isSelected = Boolean( selectedMessage && messageId === selectedMessage.id ); - const conversation = getConversationSelector(state)(conversationId); - const isNextItemCallingNotification = nextItem?.type === 'callHistory'; const shouldCollapseAbove = areMessagesInSameGroup( @@ -97,28 +103,96 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => { !isSameDay(previousItem.timestamp, item.timestamp) ); - return { - item, - id: messageId, - containerElementRef, - conversationId, - conversationColor: conversation.conversationColor, - customColor: conversation.customColor, - getPreferredBadge: getPreferredBadgeSelector(state), - isNextItemCallingNotification, - isSelected, - renderContact, - renderUniversalTimerNotification, - shouldCollapseAbove, - shouldCollapseBelow, - shouldHideMetadata, - shouldRenderDateHeader, - i18n: getIntl(state), - interactionMode: getInteractionMode(state), - theme: getTheme(state), - }; -}; + const { + blockGroupLinkRequests, + clearSelectedMessage, + deleteMessage, + deleteMessageForEveryone, + doubleCheckMissingQuoteReference, + kickOffAttachmentDownload, + markAttachmentAsCorrupted, + messageExpanded, + openGiftBadge, + pushPanelForConversation, + retryDeleteForEveryone, + retryMessageSend, + saveAttachment, + selectMessage, + showConversation, + showExpiredIncomingTapToViewToast, + showExpiredOutgoingTapToViewToast, + startConversation, + } = useConversationsActions(); -const smart = connect(mapStateToProps, mapDispatchToProps); + const { reactToMessage, scrollToQuotedMessage, setQuoteByMessageId } = + useComposerActions(); -export const SmartTimelineItem = smart(TimelineItem); + const { + showContactModal, + toggleForwardMessageModal, + toggleSafetyNumberModal, + } = useGlobalModalActions(); + + const { checkForAccount } = useAccountsActions(); + + const { showLightbox, showLightboxForViewOnceMedia } = useLightboxActions(); + + const { viewStory } = useStoriesActions(); + + const { returnToActiveCall, startCallingLobby } = useCallingActions(); + + return ( + + ); +} diff --git a/ts/test-mock/benchmarks/group_send_bench.ts b/ts/test-mock/benchmarks/group_send_bench.ts index 42ade93e3cdc..443c8af2e0f7 100644 --- a/ts/test-mock/benchmarks/group_send_bench.ts +++ b/ts/test-mock/benchmarks/group_send_bench.ts @@ -123,7 +123,10 @@ void (async () => { const leftPane = window.locator('.left-pane-wrapper'); const item = leftPane - .locator('.module-conversation-list__item--contact-or-conversation') + .locator( + '.module-conversation-list__item--contact-or-conversation' + + `>> text=${LAST_MESSAGE}` + ) .first(); await item.click(); } diff --git a/ts/test-mock/benchmarks/send_bench.ts b/ts/test-mock/benchmarks/send_bench.ts index ec98add4897b..b84ea0cf9da3 100644 --- a/ts/test-mock/benchmarks/send_bench.ts +++ b/ts/test-mock/benchmarks/send_bench.ts @@ -75,7 +75,7 @@ void (async () => { { const leftPane = window.locator('.left-pane-wrapper'); const item = leftPane.locator( - `[data-testid="${first.toContact().uuid}"]` + `[data-testid="${first.toContact().uuid}"] >> text=${LAST_MESSAGE}` ); await item.click(); } diff --git a/ts/util/getOwn.ts b/ts/util/getOwn.ts index 5d5e12ca616c..d88119e23d14 100644 --- a/ts/util/getOwn.ts +++ b/ts/util/getOwn.ts @@ -1,11 +1,9 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { has } from 'lodash'; - export function getOwn( obj: TObject, key: TKey ): TObject[TKey] | undefined { - return has(obj, key) ? obj[key] : undefined; + return Object.hasOwn(obj, key) ? obj[key] : undefined; } diff --git a/yarn.lock b/yarn.lock index 89621cf10eb4..46ef9c5c8979 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1705,10 +1705,10 @@ classnames "^2.2.6" deepmerge "^4.2.2" -"@indutny/sneequals@3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@indutny/sneequals/-/sneequals-3.2.0.tgz#dd73d097fca6c8a89b1766bb87cc78f32017165d" - integrity sha512-dnL/SCNA2BceqJ4J/CR8R+dUCBfHnDBgPFBUv5w7Sa7QRoi2pNplJoundP9b8L8FnVrh4VAVPqM5q3H0f+n6Dg== +"@indutny/sneequals@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@indutny/sneequals/-/sneequals-4.0.0.tgz#94f74e577019759c5d12818e7c7ff1b9300653a4" + integrity sha512-kQUBQtcm4aVqJil+KRfA7SycJqcWlFEa7MJTYyl4XAahHOPXnzgqvlzUPQOw1tRFlvnzxRpXNUpJxej2fdAPjg== "@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": version "0.1.3"