diff --git a/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.stories.tsx b/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.stories.tsx index ee87b9da24..f596e06653 100644 --- a/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.stories.tsx +++ b/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.stories.tsx @@ -25,6 +25,8 @@ const PIN_1: Pin = { }, message: { id: 'message-1', + sentAtTimestamp: 1, + receivedAtCounter: 1, poll: { question: 'What should we get for lunch?', }, @@ -40,6 +42,8 @@ const PIN_2: Pin = { }, message: { id: 'message-2', + sentAtTimestamp: 2, + receivedAtCounter: 2, text: { body: 'We found a cute pottery store close to Inokashira Park that we’re going to check out on Saturday. Anyone want to meet at the south exit at Kichijoji station at 1pm? Too early?', bodyRanges: [ @@ -59,6 +63,8 @@ const PIN_3: Pin = { }, message: { id: 'message-3', + sentAtTimestamp: 3, + receivedAtCounter: 3, text: { body: 'Photo', bodyRanges: [], @@ -107,7 +113,10 @@ export function Default(): React.JSX.Element { ); } -function Variant(props: { title: string; message: Omit }) { +function Variant(props: { + title: string; + message: Omit; +}) { const pin: Pin = { id: 1 as PinnedMessageId, sender: { @@ -117,6 +126,8 @@ function Variant(props: { title: string; message: Omit }) { }, message: { id: 'message-1', + sentAtTimestamp: 1, + receivedAtCounter: 1, ...props.message, }, }; diff --git a/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.tsx b/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.tsx index 4118908797..dbec9afe77 100644 --- a/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.tsx +++ b/ts/components/conversation/pinned-messages/PinnedMessagesBar.dom.tsx @@ -43,6 +43,8 @@ export type PinMessagePoll = Readonly<{ export type PinMessage = Readonly<{ id: string; + sentAtTimestamp: number; + receivedAtCounter: number; text?: PinMessageText | null; attachment?: PinMessageAttachment | null; contact?: PinMessageContact | null; @@ -186,7 +188,7 @@ function TabsList(props: { return ( - {props.pins.map((pin, pinIndex) => { + {props.pins.toReversed().map((pin, pinIndex) => { return ( ; + pinnedMessages: ReadonlyArray; canPinMessages: boolean; onPinnedMessageRemoveAll: () => void; renderTimelineItem: (props: SmartTimelineItemProps) => React.JSX.Element; @@ -71,7 +71,7 @@ export const PinnedMessagesPanel = memo(function PinnedMessagesPanel( const next = props.pinnedMessages[pinnedMessageIndex + 1]; const prev = props.pinnedMessages[pinnedMessageIndex - 1]; return ( - + {props.renderTimelineItem({ containerElementRef, containerWidthBreakpoint, @@ -80,9 +80,9 @@ export const PinnedMessagesPanel = memo(function PinnedMessagesPanel( isBlocked: props.conversation.isBlocked ?? false, isGroup: props.conversation.type === 'group', isOldestTimelineItem: pinnedMessageIndex === 0, - messageId: pinnedMessage.message.id, - nextMessageId: next?.message.id, - previousMessageId: prev?.message.id, + messageId: pinnedMessage.messageId, + nextMessageId: next?.messageId, + previousMessageId: prev?.messageId, unreadIndicatorPlacement: undefined, })} diff --git a/ts/messageModifiers/PinnedMessages.preload.ts b/ts/messageModifiers/PinnedMessages.preload.ts index 3f6ea66be7..63917434c0 100644 --- a/ts/messageModifiers/PinnedMessages.preload.ts +++ b/ts/messageModifiers/PinnedMessages.preload.ts @@ -121,7 +121,7 @@ export async function onPinnedMessageAdd( }); } - window.reduxActions.pinnedMessages.onPinnedMessagesChanged( + window.reduxActions.conversations.onPinnedMessagesChanged( targetConversation.id ); } @@ -167,7 +167,7 @@ export async function onPinnedMessageRemove( ); drop(pinnedMessagesCleanupService.trigger('onPinnedMessageRemove')); - window.reduxActions.pinnedMessages.onPinnedMessagesChanged( + window.reduxActions.conversations.onPinnedMessagesChanged( targetConversationId ); } diff --git a/ts/models/conversations.preload.ts b/ts/models/conversations.preload.ts index 4879fc430a..c64b53ff7b 100644 --- a/ts/models/conversations.preload.ts +++ b/ts/models/conversations.preload.ts @@ -275,7 +275,7 @@ const { getMostRecentAddressableMessages, getMostRecentAddressableNondisappearingMessages, getNewerMessagesByConversation, - getPinnedMessagesForConversation, + getPinnedMessagesPreloadDataForConversation, } = DataReader; const { addStickerPackReference } = DataWriter; @@ -1698,13 +1698,14 @@ export class ConversationModel { `latest timestamp=${cleaned.at(-1)?.sent_at}` ); - const pinnedMessages = await getPinnedMessagesForConversation(this.id); + const pinnedMessagesPreloadData = + await getPinnedMessagesPreloadDataForConversation(this.id); addPreloadData({ conversationId: this.id, messages: cleaned, metrics, - pinnedMessages, + pinnedMessagesPreloadData, unboundedFetch, }); } finally { @@ -1816,8 +1817,10 @@ export class ConversationModel { `latest timestamp=${cleaned.at(-1)?.sent_at}` ); - const pinnedMessages = - await DataReader.getPinnedMessagesForConversation(conversationId); + const pinnedMessagesPreloadData = + await DataReader.getPinnedMessagesPreloadDataForConversation( + conversationId + ); // Because our `getOlderMessages` fetch above didn't specify a receivedAt, we got // the most recent N messages in the conversation. If it has a conflict with @@ -1829,7 +1832,7 @@ export class ConversationModel { conversationId, messages: cleaned, metrics, - pinnedMessages, + pinnedMessagesPreloadData, scrollToMessageId, unboundedFetch, }); @@ -1994,14 +1997,14 @@ export class ConversationModel { const scrollToMessageId = options && options.disableScroll ? undefined : messageId; - const pinnedMessages = - await DataReader.getPinnedMessagesForConversation(conversationId); + const pinnedMessagesPreloadData = + await getPinnedMessagesPreloadDataForConversation(conversationId); messagesReset({ conversationId, messages: cleaned, metrics, - pinnedMessages, + pinnedMessagesPreloadData, scrollToMessageId, }); } catch (error) { diff --git a/ts/sql/Interface.std.ts b/ts/sql/Interface.std.ts index faf99ded61..725c18a116 100644 --- a/ts/sql/Interface.std.ts +++ b/ts/sql/Interface.std.ts @@ -69,7 +69,7 @@ import type { PinnedMessage, PinnedMessageId, PinnedMessageParams, - PinnedMessageRenderData, + PinnedMessagePreloadData, } from '../types/PinnedMessage.std.js'; import type { AppendPinnedMessageResult } from './server/pinnedMessages.std.js'; import type { @@ -986,9 +986,9 @@ type ReadableInterface = { getAllMegaphones: () => ReadonlyArray; hasMegaphone: (megaphoneId: RemoteMegaphoneId) => boolean; - getPinnedMessagesForConversation: ( + getPinnedMessagesPreloadDataForConversation: ( conversationId: string - ) => ReadonlyArray; + ) => ReadonlyArray; getNextExpiringPinnedMessageAcrossConversations: () => PinnedMessage | null; getMessagesNeedingUpgrade: ( diff --git a/ts/sql/Server.node.ts b/ts/sql/Server.node.ts index a940904bf9..cb9545e2c0 100644 --- a/ts/sql/Server.node.ts +++ b/ts/sql/Server.node.ts @@ -259,7 +259,7 @@ import { deleteExpiredChatFolders, } from './server/chatFolders.std.js'; import { - getPinnedMessagesForConversation, + getPinnedMessagesPreloadDataForConversation, getNextExpiringPinnedMessageAcrossConversations, appendPinnedMessage, deletePinnedMessageByMessageId, @@ -493,7 +493,7 @@ export const DataReader: ServerReadableInterface = { getAllMegaphones, hasMegaphone, - getPinnedMessagesForConversation, + getPinnedMessagesPreloadDataForConversation, getNextExpiringPinnedMessageAcrossConversations, callLinkExists, diff --git a/ts/sql/server/pinnedMessages.std.ts b/ts/sql/server/pinnedMessages.std.ts index cb1cdeaa8b..a36fe48116 100644 --- a/ts/sql/server/pinnedMessages.std.ts +++ b/ts/sql/server/pinnedMessages.std.ts @@ -5,7 +5,7 @@ import type { PinnedMessage, PinnedMessageId, PinnedMessageParams, - PinnedMessageRenderData, + PinnedMessagePreloadData, } from '../../types/PinnedMessage.std.js'; import { strictAssert } from '../../util/assert.std.js'; import { hydrateMessage } from '../hydration.std.js'; @@ -34,10 +34,10 @@ function _getMessageById( return hydrateMessage(db, row); } -function _getPinnedMessageRenderData( +function _getPinnedMessagePreloadData( db: ReadableDB, pinnedMessage: PinnedMessage -): PinnedMessageRenderData { +): PinnedMessagePreloadData { const message = _getMessageById(db, pinnedMessage.messageId); strictAssert( message != null, @@ -46,10 +46,10 @@ function _getPinnedMessageRenderData( return { pinnedMessage, message }; } -export function getPinnedMessagesForConversation( +export function getPinnedMessagesPreloadDataForConversation( db: ReadableDB, conversationId: string -): ReadonlyArray { +): ReadonlyArray { return db.transaction(() => { const [query, params] = sql` SELECT * FROM pinnedMessages @@ -61,7 +61,7 @@ export function getPinnedMessagesForConversation( .prepare(query) .all(params) .map(pinnedMessage => { - return _getPinnedMessageRenderData(db, pinnedMessage); + return _getPinnedMessagePreloadData(db, pinnedMessage); }); })(); } diff --git a/ts/state/actions.preload.ts b/ts/state/actions.preload.ts index 3c862e69d5..8beb3715eb 100644 --- a/ts/state/actions.preload.ts +++ b/ts/state/actions.preload.ts @@ -28,7 +28,6 @@ import { actions as megaphones } from './ducks/megaphones.preload.js'; import { actions as nav } from './ducks/nav.std.js'; import { actions as network } from './ducks/network.dom.js'; import { actions as notificationProfiles } from './ducks/notificationProfiles.preload.js'; -import { actions as pinnedMessages } from './ducks/pinnedMessages.preload.js'; import { actions as safetyNumber } from './ducks/safetyNumber.preload.js'; import { actions as search } from './ducks/search.preload.js'; import { actions as stickers } from './ducks/stickers.preload.js'; @@ -68,7 +67,6 @@ export const actionCreators: ReduxActions = { nav, network, notificationProfiles, - pinnedMessages, safetyNumber, search, stickers, diff --git a/ts/state/ducks/conversations.preload.ts b/ts/state/ducks/conversations.preload.ts index 532944449c..37b20b19f2 100644 --- a/ts/state/ducks/conversations.preload.ts +++ b/ts/state/ducks/conversations.preload.ts @@ -61,6 +61,7 @@ import type { ConversationAttributesType, DraftEditMessageType, LastMessageStatus, + MessageAttributesType, ReadonlyMessageAttributesType, } from '../../model-types.d.ts'; import type { @@ -96,6 +97,7 @@ import { getPendingAvatarDownloadSelector, getAllConversations, getActivePanel, + getSelectedConversationId, } from '../selectors/conversations.dom.js'; import { getIntl } from '../selectors/user.std.js'; import type { @@ -243,7 +245,14 @@ import { CurrentChatFolders } from '../../types/CurrentChatFolders.std.js'; import { itemStorage } from '../../textsecure/Storage.preload.js'; import { enqueuePollVoteForSend as enqueuePollVoteForSendHelper } from '../../polls/enqueuePollVoteForSend.preload.js'; import { updateChatFolderStateOnTargetConversationChanged } from './chatFolders.preload.js'; -import type { PinnedMessageRenderData } from '../../types/PinnedMessage.std.js'; +import type { + PinnedMessage, + PinnedMessagePreloadData, +} from '../../types/PinnedMessage.std.js'; +import type { StateThunk } from '../types.std.js'; +import { getPinnedMessagesLimit } from '../../util/pinnedMessages.dom.js'; +import { getPinnedMessageExpiresAt } from '../../util/pinnedMessages.std.js'; +import { pinnedMessagesCleanupService } from '../../services/expiring/pinnedMessagesCleanupService.preload.js'; const { chunk, @@ -496,6 +505,7 @@ export type ConversationMessageType = ReadonlyDeep<{ isNearBottom?: boolean; messageChangeCounter: number; messageIds: ReadonlyArray; + pinnedMessages: ReadonlyArray; messageLoadingState?: undefined | TimelineMessageLoadingState; metrics: MessageMetricsType; scrollToMessageId?: string; @@ -504,7 +514,7 @@ export type ConversationMessageType = ReadonlyDeep<{ export type ConversationPreloadDataType = ReadonlyDeep<{ conversationId: string; messages: ReadonlyArray; - pinnedMessages: ReadonlyArray; + pinnedMessagesPreloadData: ReadonlyArray; metrics: MessageMetricsType; unboundedFetch: boolean; }>; @@ -714,6 +724,7 @@ export const SET_PROFILE_UPDATE_ERROR = export const ADD_PRELOAD_DATA = 'conversations/ADD_PRELOAD_DATA'; export const CONSUME_PRELOAD_DATA = 'conversations/CONSUME_PRELOAD_DATA'; export const MESSAGES_RESET = 'conversations/MESSAGES_RESET'; +const PINNED_MESSAGES_REPLACE = 'conversations/PINNED_MESSAGES_REPLACE'; export type CancelVerificationDataByConversationActionType = ReadonlyDeep<{ type: typeof CANCEL_CONVERSATION_PENDING_VERIFICATION; @@ -1087,6 +1098,14 @@ type PanelAnimationStartedActionType = ReadonlyDeep<{ payload: null; }>; +type PinnedMessagesReplace = ReadonlyDeep<{ + type: typeof PINNED_MESSAGES_REPLACE; + payload: { + conversationId: string; + pinnedMessagesPreloadData: ReadonlyArray; + }; +}>; + type ReplaceAvatarsActionType = ReadonlyDeep<{ type: typeof REPLACE_AVATARS; payload: { @@ -1145,6 +1164,7 @@ export type ConversationActionType = | PanelAnimationDoneActionType | PopPanelActionType | PushPanelActionType + | PinnedMessagesReplace | RemoveAllConversationsActionType | RepairNewestMessageActionType | RepairOldestMessageActionType @@ -1245,6 +1265,9 @@ export const actions = { onMarkUnread, onMoveToInbox, onUndoArchive, + onPinnedMessagesChanged, + onPinnedMessageAdd, + onPinnedMessageRemove, openGiftBadge, popPanelForConversation, pushPanelForConversation, @@ -3381,7 +3404,7 @@ function messagesReset({ conversationId, messages, metrics, - pinnedMessages, + pinnedMessagesPreloadData, scrollToMessageId, unboundedFetch, }: MessagesResetOptionsType): MessagesResetActionType { @@ -3400,7 +3423,7 @@ function messagesReset({ conversationId, messages, metrics, - pinnedMessages, + pinnedMessagesPreloadData, scrollToMessageId, }, }; @@ -5103,6 +5126,120 @@ function startAvatarDownload( }; } +function getMessageAuthorAci( + message: ReadonlyMessageAttributesType +): AciString { + if (isIncoming(message)) { + strictAssert( + isAciString(message.sourceServiceId), + 'Message sourceServiceId must be an ACI' + ); + return message.sourceServiceId; + } + return itemStorage.user.getCheckedAci(); +} + +type PinnedMessageTarget = ReadonlyDeep<{ + conversationId: string; + targetMessageId: string; + targetAuthorAci: AciString; + targetSentTimestamp: number; +}>; + +async function getPinnedMessageTarget( + targetMessageId: string +): Promise { + const message = await DataReader.getMessageById(targetMessageId); + if (message == null) { + throw new Error('getPinnedMessageTarget: Target message not found'); + } + return { + conversationId: message.conversationId, + targetMessageId: message.id, + targetAuthorAci: getMessageAuthorAci(message), + targetSentTimestamp: message.sent_at, + }; +} + +function onPinnedMessagesChanged( + conversationId: string +): StateThunk { + return async (dispatch, getState) => { + const selectedConversationId = getSelectedConversationId(getState()); + if ( + selectedConversationId == null || + selectedConversationId !== conversationId + ) { + return; + } + + const pinnedMessagesPreloadData = + await DataReader.getPinnedMessagesPreloadDataForConversation( + conversationId + ); + + dispatch({ + type: PINNED_MESSAGES_REPLACE, + payload: { + conversationId, + pinnedMessagesPreloadData, + }, + }); + }; +} + +function onPinnedMessageAdd( + targetMessageId: string, + pinDurationSeconds: DurationInSeconds | null +): StateThunk { + return async dispatch => { + const target = await getPinnedMessageTarget(targetMessageId); + const targetConversation = window.ConversationController.get( + target.conversationId + ); + strictAssert(targetConversation != null, 'Missing target conversation'); + + await conversationJobQueue.add({ + type: conversationQueueJobEnum.enum.PinMessage, + ...target, + pinDurationSeconds, + }); + + const pinnedMessagesLimit = getPinnedMessagesLimit(); + + const pinnedAt = Date.now(); + const expiresAt = getPinnedMessageExpiresAt(pinnedAt, pinDurationSeconds); + + await DataWriter.appendPinnedMessage(pinnedMessagesLimit, { + conversationId: target.conversationId, + messageId: target.targetMessageId, + expiresAt, + pinnedAt, + }); + drop(pinnedMessagesCleanupService.trigger('onPinnedMessageAdd')); + + await targetConversation.addNotification('pinned-message-notification', { + pinnedMessageId: targetMessageId, + sourceServiceId: itemStorage.user.getCheckedAci(), + }); + + dispatch(onPinnedMessagesChanged(target.conversationId)); + }; +} + +function onPinnedMessageRemove(targetMessageId: string): StateThunk { + return async dispatch => { + const target = await getPinnedMessageTarget(targetMessageId); + await conversationJobQueue.add({ + type: conversationQueueJobEnum.enum.UnpinMessage, + ...target, + }); + await DataWriter.deletePinnedMessageByMessageId(targetMessageId); + drop(pinnedMessagesCleanupService.trigger('onPinnedMessageRemove')); + dispatch(onPinnedMessagesChanged(target.conversationId)); + }; +} + // Reducer export function getEmptyState(): ConversationsStateType { @@ -5524,6 +5661,7 @@ function updateMessageLookup( metrics, scrollToMessageId, unboundedFetch, + pinnedMessagesPreloadData, }: MessagesResetDataType ): ConversationsStateType { const { messagesByConversation, messagesLookup } = state; @@ -5556,6 +5694,14 @@ function updateMessageLookup( const messageIds = sorted.map(message => message.id); + const extraMessagesLookup: Record = {}; + const pinnedMessages: Array = []; + + for (const { pinnedMessage, message } of pinnedMessagesPreloadData) { + extraMessagesLookup[message.id] = message; + pinnedMessages.push(pinnedMessage); + } + return { ...state, preloadData: undefined, @@ -5568,6 +5714,7 @@ function updateMessageLookup( : {}), messagesLookup: { ...messagesLookup, + ...extraMessagesLookup, ...lookup, }, messagesByConversation: { @@ -5579,6 +5726,7 @@ function updateMessageLookup( ? existingConversation.scrollToMessageCounter + 1 : 0, messageIds, + pinnedMessages, metrics: { ...metrics, newest, @@ -5589,6 +5737,63 @@ function updateMessageLookup( }; } +function maybeDropMessageIdsFromMessagesLookup( + messagesLookup: MessageLookupType, + messageIdsToMaybeRemove: ReadonlyArray, + pinnedMessages: ReadonlyArray +): MessageLookupType { + const pinnedMessagesMessageIds = new Set(); + for (const pinnedMessage of pinnedMessages) { + pinnedMessagesMessageIds.add(pinnedMessage.messageId); + } + + const messageIdsToRemove = new Set(); + for (const messageIdToMaybeRemove of messageIdsToMaybeRemove) { + if (!pinnedMessagesMessageIds.has(messageIdToMaybeRemove)) { + messageIdsToRemove.add(messageIdToMaybeRemove); + } + } + + if (messageIdsToRemove.size === 0) { + return messagesLookup; + } + + const updatedMessagesLookup: Record = {}; + for (const messageId of Object.keys(messagesLookup)) { + if (!messageIdsToRemove.has(messageId)) { + updatedMessagesLookup[messageId] = messagesLookup[messageId]; + } + } + + return updatedMessagesLookup; +} + +function maybeDropMessageIdFromPinnedMessages( + messagesByConversation: MessagesByConversationType, + conversationId: string, + messageId: string +): MessagesByConversationType { + const prevConversationMessages = messagesByConversation[conversationId]; + if ( + prevConversationMessages == null || + prevConversationMessages.pinnedMessages.length === 0 + ) { + return messagesByConversation; + } + + return { + ...messagesByConversation, + [conversationId]: { + ...prevConversationMessages, + pinnedMessages: prevConversationMessages.pinnedMessages.filter( + pinnedMessage => { + return pinnedMessage.messageId !== messageId; + } + ), + }, + }; +} + function dropPreloadData( state: ConversationsStateType ): ConversationsStateType { @@ -5736,7 +5941,11 @@ export function reducer( return { ...state, - messagesLookup: omit(state.messagesLookup, messageIdsToRemove), + messagesLookup: maybeDropMessageIdsFromMessagesLookup( + state.messagesLookup, + messageIdsToRemove, + conversationMessages?.pinnedMessages ?? [] + ), messagesByConversation: { ...state.messagesByConversation, [conversationId]: { @@ -5767,7 +5976,11 @@ export function reducer( return { ...state, - messagesLookup: omit(state.messagesLookup, messageIdsToRemove), + messagesLookup: maybeDropMessageIdsFromMessagesLookup( + state.messagesLookup, + messageIdsToRemove, + conversationMessages.pinnedMessages + ), messagesByConversation: { ...state.messagesByConversation, [conversationId]: { @@ -5877,7 +6090,11 @@ export function reducer( stack: [], watermark: -1, }, - messagesLookup: omit(state.messagesLookup, [...messageIds]), + messagesLookup: maybeDropMessageIdsFromMessagesLookup( + state.messagesLookup, + [...messageIds], + [] + ), messagesByConversation: omit(state.messagesByConversation, [ conversationId, ]), @@ -6232,6 +6449,8 @@ export function reducer( : existingMessage.isSpoilerExpanded, }; + const wasDeletedForEveryone = updatedMessage.deletedForEveryone; + return { ...maybeUpdateSelectedMessageForDetails( { @@ -6245,6 +6464,13 @@ export function reducer( ...state.messagesLookup, [id]: updatedMessage, }, + messagesByConversation: !wasDeletedForEveryone + ? state.messagesByConversation + : maybeDropMessageIdFromPinnedMessages( + state.messagesByConversation, + conversationId, + id + ), }; } @@ -6497,18 +6723,29 @@ export function reducer( }; } + const pinnedMessages = existingConversation.pinnedMessages.filter( + pinnedMesage => { + return pinnedMesage.messageId !== id; + } + ); + return { ...maybeUpdateSelectedMessageForDetails( { messageId: id, targetedMessageForDetails: undefined }, state ), preloadData: undefined, - messagesLookup: omit(messagesLookup, id), + messagesLookup: maybeDropMessageIdsFromMessagesLookup( + messagesLookup, + [id], + pinnedMessages + ), messagesByConversation: { [conversationId]: { ...existingConversation, messageIds, metrics, + pinnedMessages, }, }, }; @@ -7512,5 +7749,39 @@ export function reducer( }; } + if (action.type === PINNED_MESSAGES_REPLACE) { + const { conversationId, pinnedMessagesPreloadData } = action.payload; + + const extraMessagesLookup: Record = {}; + const pinnedMessages: Array = []; + + for (const pinnedMessagePreloadData of pinnedMessagesPreloadData) { + const { message, pinnedMessage } = pinnedMessagePreloadData; + extraMessagesLookup[message.id] = message; + pinnedMessages.push(pinnedMessage); + } + + return { + ...state, + messagesLookup: + state.selectedConversationId !== conversationId + ? state.messagesLookup + : { + ...state.messagesLookup, + ...extraMessagesLookup, + }, + messagesByConversation: + state.messagesByConversation[conversationId] == null + ? state.messagesByConversation + : { + ...state.messagesByConversation, + [conversationId]: { + ...state.messagesByConversation[conversationId], + pinnedMessages, + }, + }, + }; + } + return state; } diff --git a/ts/state/ducks/pinnedMessages.preload.ts b/ts/state/ducks/pinnedMessages.preload.ts deleted file mode 100644 index ee5221c3fa..0000000000 --- a/ts/state/ducks/pinnedMessages.preload.ts +++ /dev/null @@ -1,312 +0,0 @@ -// Copyright 2025 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import type { ReadonlyDeep } from 'type-fest'; -import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.std.js'; -import { useBoundActions } from '../../hooks/useBoundActions.std.js'; -import { DataReader, DataWriter } from '../../sql/Client.preload.js'; -import { strictAssert } from '../../util/assert.std.js'; -import type { PinnedMessageRenderData } from '../../types/PinnedMessage.std.js'; -import type { ReadonlyMessageAttributesType } from '../../model-types.js'; -import type { AciString } from '../../types/ServiceId.std.js'; -import { isIncoming } from '../selectors/message.preload.js'; -import { isAciString } from '../../util/isAciString.std.js'; -import { itemStorage } from '../../textsecure/Storage.preload.js'; -import type { StateThunk } from '../types.std.js'; -import type { DurationInSeconds } from '../../util/durations/duration-in-seconds.std.js'; -import { - conversationJobQueue, - conversationQueueJobEnum, -} from '../../jobs/conversationJobQueue.preload.js'; -import { getPinnedMessagesLimit } from '../../util/pinnedMessages.dom.js'; -import { getPinnedMessageExpiresAt } from '../../util/pinnedMessages.std.js'; -import { getSelectedConversationId } from '../selectors/conversations.dom.js'; -import type { - AddPreloadDataActionType, - ConsumePreloadDataActionType, - ConversationUnloadedActionType, - MessageChangedActionType, - MessagesResetActionType, - TargetedConversationChangedActionType, -} from './conversations.preload.js'; -import { - ADD_PRELOAD_DATA, - CONSUME_PRELOAD_DATA, - CONVERSATION_UNLOADED, - MESSAGE_CHANGED, - TARGETED_CONVERSATION_CHANGED, -} from './conversations.preload.js'; -import { pinnedMessagesCleanupService } from '../../services/expiring/pinnedMessagesCleanupService.preload.js'; -import { drop } from '../../util/drop.std.js'; - -type PreloadData = ReadonlyDeep<{ - conversationId: string; - pinnedMessages: ReadonlyArray; -}>; - -export type PinnedMessagesState = ReadonlyDeep<{ - preloadData: PreloadData | null; - conversationId: string | null; - pinnedMessages: ReadonlyArray | null; -}>; - -const PINNED_MESSAGES_REPLACE = 'pinnedMessages/PINNED_MESSAGES_REPLACE'; - -export type PinnedMessagesReplace = ReadonlyDeep<{ - type: typeof PINNED_MESSAGES_REPLACE; - payload: { - conversationId: string; - pinnedMessages: ReadonlyArray; - }; -}>; - -export type PinnedMessagesAction = ReadonlyDeep; - -export function getEmptyState(): PinnedMessagesState { - return { - preloadData: null, - conversationId: null, - pinnedMessages: null, - }; -} - -function getMessageAuthorAci( - message: ReadonlyMessageAttributesType -): AciString { - if (isIncoming(message)) { - strictAssert( - isAciString(message.sourceServiceId), - 'Message sourceServiceId must be an ACI' - ); - return message.sourceServiceId; - } - return itemStorage.user.getCheckedAci(); -} - -type PinnedMessageTarget = ReadonlyDeep<{ - conversationId: string; - targetMessageId: string; - targetAuthorAci: AciString; - targetSentTimestamp: number; -}>; - -async function getPinnedMessageTarget( - targetMessageId: string -): Promise { - const message = await DataReader.getMessageById(targetMessageId); - if (message == null) { - throw new Error('getPinnedMessageTarget: Target message not found'); - } - return { - conversationId: message.conversationId, - targetMessageId: message.id, - targetAuthorAci: getMessageAuthorAci(message), - targetSentTimestamp: message.sent_at, - }; -} - -function onPinnedMessagesChanged( - conversationId: string -): StateThunk { - return async (dispatch, getState) => { - const selectedConversationId = getSelectedConversationId(getState()); - if ( - selectedConversationId == null || - selectedConversationId !== conversationId - ) { - return; - } - - const pinnedMessages = - await DataReader.getPinnedMessagesForConversation(conversationId); - - dispatch({ - type: PINNED_MESSAGES_REPLACE, - payload: { - conversationId, - pinnedMessages, - }, - }); - }; -} - -function onPinnedMessageAdd( - targetMessageId: string, - pinDurationSeconds: DurationInSeconds | null -): StateThunk { - return async dispatch => { - const target = await getPinnedMessageTarget(targetMessageId); - const targetConversation = window.ConversationController.get( - target.conversationId - ); - strictAssert(targetConversation != null, 'Missing target conversation'); - - await conversationJobQueue.add({ - type: conversationQueueJobEnum.enum.PinMessage, - ...target, - pinDurationSeconds, - }); - - const pinnedMessagesLimit = getPinnedMessagesLimit(); - - const pinnedAt = Date.now(); - const expiresAt = getPinnedMessageExpiresAt(pinnedAt, pinDurationSeconds); - - await DataWriter.appendPinnedMessage(pinnedMessagesLimit, { - conversationId: target.conversationId, - messageId: target.targetMessageId, - expiresAt, - pinnedAt, - }); - drop(pinnedMessagesCleanupService.trigger('onPinnedMessageAdd')); - - await targetConversation.addNotification('pinned-message-notification', { - pinnedMessageId: targetMessageId, - sourceServiceId: itemStorage.user.getCheckedAci(), - }); - - dispatch(onPinnedMessagesChanged(target.conversationId)); - }; -} - -function onPinnedMessageRemove(targetMessageId: string): StateThunk { - return async dispatch => { - const target = await getPinnedMessageTarget(targetMessageId); - await conversationJobQueue.add({ - type: conversationQueueJobEnum.enum.UnpinMessage, - ...target, - }); - await DataWriter.deletePinnedMessageByMessageId(targetMessageId); - drop(pinnedMessagesCleanupService.trigger('onPinnedMessageRemove')); - dispatch(onPinnedMessagesChanged(target.conversationId)); - }; -} - -export const actions = { - onPinnedMessagesChanged, - onPinnedMessageAdd, - onPinnedMessageRemove, -}; - -export const usePinnedMessagesActions = (): BoundActionCreatorsMapObject< - typeof actions -> => useBoundActions(actions); - -function updateMessageInPinnedMessages( - pinnedMessages: ReadonlyArray, - message: ReadonlyMessageAttributesType -): ReadonlyArray { - return pinnedMessages.map(pinnedMessage => { - if (pinnedMessage.message.id === message.id) { - return { ...pinnedMessage, message }; - } - return pinnedMessage; - }); -} - -export function reducer( - state: PinnedMessagesState = getEmptyState(), - action: - | PinnedMessagesAction - | MessageChangedActionType - | TargetedConversationChangedActionType - | ConversationUnloadedActionType - | AddPreloadDataActionType - | ConsumePreloadDataActionType - | MessagesResetActionType -): PinnedMessagesState { - switch (action.type) { - case PINNED_MESSAGES_REPLACE: - if (state.conversationId !== action.payload.conversationId) { - return state; - } - - return { - ...state, - pinnedMessages: action.payload.pinnedMessages, - }; - case TARGETED_CONVERSATION_CHANGED: { - const conversationId = action.payload.conversationId ?? null; - return { - ...state, - preloadData: - conversationId != null && - state.preloadData != null && - state.preloadData.conversationId === conversationId - ? state.preloadData - : null, - conversationId, - pinnedMessages: null, - }; - } - case CONVERSATION_UNLOADED: - if (state.conversationId !== action.payload.conversationId) { - return state; - } - return { - ...state, - conversationId: null, - pinnedMessages: null, - }; - case ADD_PRELOAD_DATA: - return { - ...state, - preloadData: { - conversationId: action.payload.conversationId, - pinnedMessages: action.payload.pinnedMessages, - }, - }; - case CONSUME_PRELOAD_DATA: - if (state.preloadData == null) { - return state; - } - if (state.preloadData.conversationId === action.payload.conversationId) { - return { - ...state, - preloadData: null, - conversationId: state.preloadData.conversationId, - pinnedMessages: state.preloadData.pinnedMessages, - }; - } - return { - ...state, - preloadData: null, - }; - case MESSAGE_CHANGED: { - let nextState = state; - - if ( - nextState.conversationId === action.payload.conversationId && - nextState.pinnedMessages != null - ) { - nextState = { - ...nextState, - pinnedMessages: updateMessageInPinnedMessages( - nextState.pinnedMessages, - action.payload.data - ), - }; - } - - if ( - nextState.preloadData != null && - nextState.preloadData.conversationId === action.payload.id - ) { - nextState = { - ...nextState, - preloadData: { - ...nextState.preloadData, - pinnedMessages: updateMessageInPinnedMessages( - nextState.preloadData.pinnedMessages, - action.payload.data - ), - }, - }; - } - - return nextState; - } - default: - return state; - } -} diff --git a/ts/state/getInitialState.preload.ts b/ts/state/getInitialState.preload.ts index 901526f142..05f5e69769 100644 --- a/ts/state/getInitialState.preload.ts +++ b/ts/state/getInitialState.preload.ts @@ -31,7 +31,6 @@ import { getEmptyState as megaphonesEmptyState } from './ducks/megaphones.preloa import { getEmptyState as navEmptyState } from './ducks/nav.std.js'; import { getEmptyState as networkEmptyState } from './ducks/network.dom.js'; import { getEmptyState as notificationProfilesEmptyState } from './ducks/notificationProfiles.preload.js'; -import { getEmptyState as pinnedMessagesEmptyState } from './ducks/pinnedMessages.preload.js'; import { getEmptyState as preferredReactionsEmptyState } from './ducks/preferredReactions.preload.js'; import { getEmptyState as safetyNumberEmptyState } from './ducks/safetyNumber.preload.js'; import { getEmptyState as searchEmptyState } from './ducks/search.preload.js'; @@ -176,7 +175,6 @@ function getEmptyState(): StateType { nav: navEmptyState(), network: networkEmptyState(), notificationProfiles: notificationProfilesEmptyState(), - pinnedMessages: pinnedMessagesEmptyState(), preferredReactions: preferredReactionsEmptyState(), safetyNumber: safetyNumberEmptyState(), search: searchEmptyState(), diff --git a/ts/state/initializeRedux.preload.ts b/ts/state/initializeRedux.preload.ts index a89d4c012e..45060b86a9 100644 --- a/ts/state/initializeRedux.preload.ts +++ b/ts/state/initializeRedux.preload.ts @@ -94,10 +94,6 @@ export function initializeRedux(data: ReduxInitData): void { megaphones: bindActionCreators(actionCreators.megaphones, store.dispatch), nav: bindActionCreators(actionCreators.nav, store.dispatch), network: bindActionCreators(actionCreators.network, store.dispatch), - pinnedMessages: bindActionCreators( - actionCreators.pinnedMessages, - store.dispatch - ), notificationProfiles: bindActionCreators( actionCreators.notificationProfiles, store.dispatch diff --git a/ts/state/reducer.preload.ts b/ts/state/reducer.preload.ts index ca8ae1877c..5dbc553182 100644 --- a/ts/state/reducer.preload.ts +++ b/ts/state/reducer.preload.ts @@ -30,7 +30,6 @@ import { reducer as megaphones } from './ducks/megaphones.preload.js'; import { reducer as nav } from './ducks/nav.std.js'; import { reducer as network } from './ducks/network.dom.js'; import { reducer as notificationProfiles } from './ducks/notificationProfiles.preload.js'; -import { reducer as pinnedMessages } from './ducks/pinnedMessages.preload.js'; import { reducer as preferredReactions } from './ducks/preferredReactions.preload.js'; import { reducer as safetyNumber } from './ducks/safetyNumber.preload.js'; import { reducer as search } from './ducks/search.preload.js'; @@ -70,7 +69,6 @@ export const reducer = combineReducers({ nav, network, notificationProfiles, - pinnedMessages, preferredReactions, safetyNumber, search, diff --git a/ts/state/selectors/conversations.dom.ts b/ts/state/selectors/conversations.dom.ts index 939475a0c0..3815afd901 100644 --- a/ts/state/selectors/conversations.dom.ts +++ b/ts/state/selectors/conversations.dom.ts @@ -89,6 +89,7 @@ import { import type { AllChatFoldersMutedStats } from '../../util/countMutedStats.std.js'; import { countAllChatFoldersMutedStats } from '../../util/countMutedStats.std.js'; import { getActiveProfile } from './notificationProfiles.dom.js'; +import type { PinnedMessage } from '../../types/PinnedMessage.std.js'; const { isNumber, pick } = lodash; @@ -318,6 +319,18 @@ export const getConversationMessages = createSelector( } ); +export const getPinnedMessages: StateSelector> = + createSelector(getConversationMessages, conversationMessages => { + return conversationMessages?.pinnedMessages ?? []; + }); + +export const getPinnedMessagesMessageIds: StateSelector> = + createSelector(getPinnedMessages, pinnedMessages => { + return pinnedMessages.map(pinnedMessage => { + return pinnedMessage.messageId; + }); + }); + const collator = new Intl.Collator(); // Note: we will probably want to put i18n and regionCode back when we are formatting diff --git a/ts/state/selectors/message.preload.ts b/ts/state/selectors/message.preload.ts index ee6b5a52b3..a454d1dd38 100644 --- a/ts/state/selectors/message.preload.ts +++ b/ts/state/selectors/message.preload.ts @@ -103,6 +103,7 @@ import { getMessages, getCachedConversationMemberColorsSelector, getContactNameColor, + getPinnedMessagesMessageIds, } from './conversations.dom.js'; import { getIntl, @@ -168,7 +169,6 @@ import { LONG_MESSAGE } from '../../types/MIME.std.js'; import type { MessageRequestResponseNotificationData } from '../../components/conversation/MessageRequestResponseNotification.dom.js'; import type { PinnedMessageNotificationData } from '../../components/conversation/pinned-messages/PinnedMessageNotification.dom.js'; import { canEditGroupInfo } from '../../util/canEditGroupInfo.preload.js'; -import { getPinnedMessagesMessageIds } from './pinnedMessages.dom.js'; import { isPinnedMessagesSendEnabled } from '../../util/isPinnedMessagesEnabled.dom.js'; import { getPinnedMessagesLimit } from '../../util/pinnedMessages.dom.js'; diff --git a/ts/state/selectors/pinnedMessages.dom.ts b/ts/state/selectors/pinnedMessages.dom.ts deleted file mode 100644 index 5d6bc17f5f..0000000000 --- a/ts/state/selectors/pinnedMessages.dom.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2025 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { createSelector } from 'reselect'; -import type { PinnedMessageRenderData } from '../../types/PinnedMessage.std.js'; -import type { PinnedMessagesState } from '../ducks/pinnedMessages.preload.js'; -import type { StateType } from '../reducer.preload.js'; -import type { StateSelector } from '../types.std.js'; -import { getSelectedConversationId } from './conversations.dom.js'; -import { softAssert } from '../../util/assert.std.js'; - -export function getPinnedMessagesState(state: StateType): PinnedMessagesState { - return state.pinnedMessages; -} - -export const getPinnedMessages: StateSelector< - ReadonlyArray -> = createSelector( - getPinnedMessagesState, - getSelectedConversationId, - (state, selectedConversationId) => { - const expectedConversationId = selectedConversationId ?? null; - if (expectedConversationId !== state.conversationId) { - softAssert( - false, - 'getPinnedMessages: State is not in sync with the selected conversation' - ); - return []; - } - return state.pinnedMessages ?? []; - } -); - -export const getPinnedMessagesMessageIds: StateSelector> = - createSelector(getPinnedMessages, pinnedMessages => { - return pinnedMessages.map(pinnedMessage => { - return pinnedMessage.message.id; - }); - }); diff --git a/ts/state/selectors/timeline.preload.ts b/ts/state/selectors/timeline.preload.ts index dfc1f1f839..e692cf6f7a 100644 --- a/ts/state/selectors/timeline.preload.ts +++ b/ts/state/selectors/timeline.preload.ts @@ -11,6 +11,7 @@ import { getSelectedMessageIds, getMessages, getCachedConversationMemberColorsSelector, + getPinnedMessagesMessageIds, } from './conversations.dom.js'; import { getAccountSelector } from './accounts.std.js'; import { @@ -25,7 +26,6 @@ import { getActiveCall, getCallSelector } from './calling.std.js'; import { getPropsForBubble } from './message.preload.js'; import { getCallHistorySelector } from './callHistory.std.js'; import { useProxySelector } from '../../hooks/useProxySelector.std.js'; -import { getPinnedMessagesMessageIds } from './pinnedMessages.dom.js'; const getTimelineItem = ( state: StateType, diff --git a/ts/state/smart/PinnedMessagesBar.preload.tsx b/ts/state/smart/PinnedMessagesBar.preload.tsx index 221219114b..b9effd3016 100644 --- a/ts/state/smart/PinnedMessagesBar.preload.tsx +++ b/ts/state/smart/PinnedMessagesBar.preload.tsx @@ -15,6 +15,8 @@ import { getIntl } from '../selectors/user.std.js'; import { getConversationSelector, getSelectedConversationId, + getPinnedMessages, + getMessages, } from '../selectors/conversations.dom.js'; import { strictAssert } from '../../util/assert.std.js'; import { useConversationsActions } from '../ducks/conversations.preload.js'; @@ -39,8 +41,6 @@ import * as Attachment from '../../util/Attachment.std.js'; import * as MIME from '../../types/MIME.std.js'; import * as EmbeddedContact from '../../types/EmbeddedContact.std.js'; import type { StateSelector } from '../types.std.js'; -import { usePinnedMessagesActions } from '../ducks/pinnedMessages.preload.js'; -import { getPinnedMessages } from '../selectors/pinnedMessages.dom.js'; function getPinMessageAttachment( props: MessagePropsType @@ -101,9 +101,15 @@ function isPinMessageSticker(props: MessagePropsType): boolean { return props.isSticker ?? false; } -function getPinMessage(props: MessagePropsType): PinMessage { +function getPinMessage( + props: MessagePropsType, + sentAtTimestamp: number, + receivedAtCounter: number +): PinMessage { return { id: props.id, + sentAtTimestamp, + receivedAtCounter, text: getPinMessageText(props), attachment: getPinMessageAttachment(props), contact: getPinMessageContact(props), @@ -153,23 +159,22 @@ function getNextPinId( const selectPins: StateSelector> = createSelector( getPinnedMessages, + getMessages, getMessagePropsSelector, - (pinnedMessages, messagePropsSelector) => { - const sorted = orderBy( - pinnedMessages, - ['message.received_at', 'message.sent_at'], - ['ASC', 'ASC'] - ); - - return sorted.map((pinnedMessageRenderData): Pin => { - const { pinnedMessage, message } = pinnedMessageRenderData; - + (pinnedMessages, messagesLookup, messagePropsSelector) => { + return pinnedMessages.map((pinnedMessage): Pin => { + const message = messagesLookup[pinnedMessage.messageId]; + strictAssert(message != null, 'Missing pinned message'); const messageProps = messagePropsSelector(message); return { id: pinnedMessage.id, sender: getPinSender(messageProps), - message: getPinMessage(messageProps), + message: getPinMessage( + messageProps, + message.sent_at, + message.received_at + ), }; }); } @@ -187,7 +192,7 @@ function getNodeDataMessageId(node: Node): string | null { } function useTimelineIntersectionObserver( - pins: ReadonlyArray, + unsortedPins: ReadonlyArray, onCurrentChange: (current: PinnedMessageId) => void ) { const onCurrentChangeRef = useRef(onCurrentChange); @@ -197,10 +202,19 @@ function useTimelineIntersectionObserver( useEffect(() => { // We only need to track anything if there are multiple pins - if (pins.length <= 1) { + if (unsortedPins.length <= 1) { return; } + const pins = orderBy( + unsortedPins, + [ + pin => pin.message.receivedAtCounter, + pin => pin.message.sentAtTimestamp, + ], + ['ASC', 'ASC'] + ); + const scroller = document.querySelector( '.module-timeline__messages__container' ); @@ -334,7 +348,7 @@ function useTimelineIntersectionObserver( mutationObserver.disconnect(); intersectionObserver.disconnect(); }; - }, [pins]); + }, [unsortedPins]); } export const SmartPinnedMessagesBar = memo(function SmartPinnedMessagesBar() { @@ -352,12 +366,11 @@ export const SmartPinnedMessagesBar = memo(function SmartPinnedMessagesBar() { const pins = useSelector(selectPins); const canPinMessages = getCanPinMessages(conversation); - const { pushPanelForConversation, scrollToMessage } = + const { onPinnedMessageRemove, pushPanelForConversation, scrollToMessage } = useConversationsActions(); - const { onPinnedMessageRemove } = usePinnedMessagesActions(); const [current, setCurrent] = useState(() => { - return pins.at(-1)?.id ?? null; + return pins.at(0)?.id ?? null; }); const isCurrentOutOfDate = useMemo(() => { @@ -376,7 +389,7 @@ export const SmartPinnedMessagesBar = memo(function SmartPinnedMessagesBar() { }, [current, pins]); if (isCurrentOutOfDate) { - setCurrent(pins.at(-1)?.id ?? null); + setCurrent(pins.at(0)?.id ?? null); } const handleCurrentChange = useCallback( diff --git a/ts/state/smart/PinnedMessagesPanel.preload.tsx b/ts/state/smart/PinnedMessagesPanel.preload.tsx index b8d0d6a141..577e38c729 100644 --- a/ts/state/smart/PinnedMessagesPanel.preload.tsx +++ b/ts/state/smart/PinnedMessagesPanel.preload.tsx @@ -4,14 +4,15 @@ import React, { memo, useCallback } from 'react'; import { useSelector } from 'react-redux'; import { getIntl } from '../selectors/user.std.js'; -import { getConversationByIdSelector } from '../selectors/conversations.dom.js'; +import { + getConversationByIdSelector, + getPinnedMessages, +} from '../selectors/conversations.dom.js'; import { strictAssert } from '../../util/assert.std.js'; import { PinnedMessagesPanel } from '../../components/conversation/pinned-messages/PinnedMessagesPanel.dom.js'; import type { SmartTimelineItemProps } from './TimelineItem.preload.js'; import { SmartTimelineItem } from './TimelineItem.preload.js'; -import { getPinnedMessages } from '../selectors/pinnedMessages.dom.js'; import { canPinMessages as getCanPinMessages } from '../selectors/message.preload.js'; -import { usePinnedMessagesActions } from '../ducks/pinnedMessages.preload.js'; import { useConversationsActions } from '../ducks/conversations.preload.js'; export type SmartPinnedMessagesPanelProps = Readonly<{ @@ -28,7 +29,8 @@ export const SmartPinnedMessagesPanel = memo(function SmartPinnedMessagesPanel( const i18n = useSelector(getIntl); const conversationSelector = useSelector(getConversationByIdSelector); const conversation = conversationSelector(props.conversationId); - const { popPanelForConversation } = useConversationsActions(); + const { onPinnedMessageRemove, popPanelForConversation } = + useConversationsActions(); strictAssert( conversation, @@ -38,11 +40,9 @@ export const SmartPinnedMessagesPanel = memo(function SmartPinnedMessagesPanel( const pinnedMessages = useSelector(getPinnedMessages); const canPinMessages = getCanPinMessages(conversation); - const { onPinnedMessageRemove } = usePinnedMessagesActions(); - const handlePinnedMessageRemoveAll = useCallback(() => { popPanelForConversation(); - for (const { pinnedMessage } of pinnedMessages) { + for (const pinnedMessage of pinnedMessages) { onPinnedMessageRemove(pinnedMessage.messageId); } }, [popPanelForConversation, pinnedMessages, onPinnedMessageRemove]); diff --git a/ts/state/smart/Timeline.preload.tsx b/ts/state/smart/Timeline.preload.tsx index f102177263..7efb1ebd4a 100644 --- a/ts/state/smart/Timeline.preload.tsx +++ b/ts/state/smart/Timeline.preload.tsx @@ -32,6 +32,7 @@ import { getSafeConversationWithSameTitle, getSelectedConversationId, getTargetedMessage, + getPinnedMessages, } from '../selectors/conversations.dom.js'; import { getIntl, getTheme } from '../selectors/user.std.js'; import type { PropsType as SmartCollidingAvatarsPropsType } from './CollidingAvatars.dom.js'; @@ -48,7 +49,6 @@ import { SmartTypingBubble } from './TypingBubble.preload.js'; import { AttachmentDownloadManager } from '../../jobs/AttachmentDownloadManager.preload.js'; import { isInFullScreenCall as getIsInFullScreenCall } from '../selectors/calling.std.js'; import { SmartPinnedMessagesBar } from './PinnedMessagesBar.preload.js'; -import { getPinnedMessages } from '../selectors/pinnedMessages.dom.js'; const { isEmpty } = lodash; diff --git a/ts/state/smart/TimelineItem.preload.tsx b/ts/state/smart/TimelineItem.preload.tsx index 0b292c3311..ded3878b03 100644 --- a/ts/state/smart/TimelineItem.preload.tsx +++ b/ts/state/smart/TimelineItem.preload.tsx @@ -40,7 +40,6 @@ import { renderReactionPicker } from './renderReactionPicker.dom.js'; import type { MessageRequestState } from '../../components/conversation/MessageRequestActionsConfirmation.dom.js'; import { TargetedMessageSource } from '../ducks/conversationsEnums.std.js'; import type { MessageInteractivity } from '../../components/conversation/Message.dom.js'; -import { usePinnedMessagesActions } from '../ducks/pinnedMessages.preload.js'; export type SmartTimelineItemProps = { containerElementRef: RefObject; @@ -132,6 +131,8 @@ export const SmartTimelineItem = memo(function SmartTimelineItem( kickOffAttachmentDownload, markAttachmentAsCorrupted, messageExpanded, + onPinnedMessageAdd, + onPinnedMessageRemove, openGiftBadge, pushPanelForConversation, retryDeleteForEveryone, @@ -151,9 +152,6 @@ export const SmartTimelineItem = memo(function SmartTimelineItem( toggleSelectMessage, } = useConversationsActions(); - const { onPinnedMessageAdd, onPinnedMessageRemove } = - usePinnedMessagesActions(); - const { endPoll, reactToMessage, diff --git a/ts/state/types.std.ts b/ts/state/types.std.ts index 389021b421..1ac20b8c84 100644 --- a/ts/state/types.std.ts +++ b/ts/state/types.std.ts @@ -32,7 +32,6 @@ import type { actions as megaphones } from './ducks/megaphones.preload.js'; import type { actions as nav } from './ducks/nav.std.js'; import type { actions as network } from './ducks/network.dom.js'; import type { actions as notificationProfiles } from './ducks/notificationProfiles.preload.js'; -import type { actions as pinnedMessages } from './ducks/pinnedMessages.preload.js'; import type { actions as safetyNumber } from './ducks/safetyNumber.preload.js'; import type { actions as search } from './ducks/search.preload.js'; import type { actions as stickers } from './ducks/stickers.preload.js'; @@ -71,7 +70,6 @@ export type ReduxActions = { nav: typeof nav; network: typeof network; notificationProfiles: typeof notificationProfiles; - pinnedMessages: typeof pinnedMessages; safetyNumber: typeof safetyNumber; search: typeof search; stickers: typeof stickers; diff --git a/ts/test-electron/state/ducks/conversations_test.preload.ts b/ts/test-electron/state/ducks/conversations_test.preload.ts index 2405078b5b..49beda0dbe 100644 --- a/ts/test-electron/state/ducks/conversations_test.preload.ts +++ b/ts/test-electron/state/ducks/conversations_test.preload.ts @@ -467,6 +467,7 @@ describe('both/state/ducks/conversations', () => { return { messageChangeCounter: 0, messageIds: [], + pinnedMessages: [], metrics: { totalUnseen: 0, }, @@ -1548,6 +1549,7 @@ describe('both/state/ducks/conversations', () => { }, scrollToMessageCounter: 0, messageIds: [messageId, messageIdTwo, messageIdThree], + pinnedMessages: [], }, }, }; @@ -1628,6 +1630,7 @@ describe('both/state/ducks/conversations', () => { [conversationId]: { messageChangeCounter: 0, messageIds: [messageId, messageIdTwo, messageIdThree], + pinnedMessages: [], metrics: { totalUnseen: 0, }, diff --git a/ts/types/PinnedMessage.std.ts b/ts/types/PinnedMessage.std.ts index 9c6e7cc22e..b3fd6d2d8f 100644 --- a/ts/types/PinnedMessage.std.ts +++ b/ts/types/PinnedMessage.std.ts @@ -16,7 +16,7 @@ export type PinnedMessage = Readonly<{ export type PinnedMessageParams = Omit; -export type PinnedMessageRenderData = Readonly<{ +export type PinnedMessagePreloadData = Readonly<{ pinnedMessage: PinnedMessage; message: ReadonlyMessageAttributesType; }>;