Fix pinned messages state syncing

This commit is contained in:
Jamie
2025-12-29 20:36:07 +01:00
committed by GitHub
parent b405e3d83d
commit 963c94a2a2
25 changed files with 388 additions and 437 deletions
@@ -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 were 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<PinMessage, 'id'> }) {
function Variant(props: {
title: string;
message: Omit<PinMessage, 'id' | 'sentAtTimestamp' | 'receivedAtCounter'>;
}) {
const pin: Pin = {
id: 1 as PinnedMessageId,
sender: {
@@ -117,6 +126,8 @@ function Variant(props: { title: string; message: Omit<PinMessage, 'id'> }) {
},
message: {
id: 'message-1',
sentAtTimestamp: 1,
receivedAtCounter: 1,
...props.message,
},
};
@@ -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 (
<AriaClickable.SubWidget>
<Tabs.List className={tw('flex h-full flex-col')}>
{props.pins.map((pin, pinIndex) => {
{props.pins.toReversed().map((pin, pinIndex) => {
return (
<TabTrigger
key={pin.id}
@@ -13,7 +13,7 @@ import React, {
import { useLayoutEffect } from '@react-aria/utils';
import type { LocalizerType } from '../../../types/I18N.std.js';
import type { ConversationType } from '../../../state/ducks/conversations.preload.js';
import type { PinnedMessageRenderData } from '../../../types/PinnedMessage.std.js';
import type { PinnedMessage } from '../../../types/PinnedMessage.std.js';
import type { SmartTimelineItemProps } from '../../../state/smart/TimelineItem.preload.js';
import { WidthBreakpoint } from '../../_util.std.js';
import { AxoScrollArea } from '../../../axo/AxoScrollArea.dom.js';
@@ -32,7 +32,7 @@ import { AxoAlertDialog } from '../../../axo/AxoAlertDialog.dom.js';
export type PinnedMessagesPanelProps = Readonly<{
i18n: LocalizerType;
conversation: ConversationType;
pinnedMessages: ReadonlyArray<PinnedMessageRenderData>;
pinnedMessages: ReadonlyArray<PinnedMessage>;
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 (
<Fragment key={pinnedMessage.pinnedMessage.id}>
<Fragment key={pinnedMessage.id}>
{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,
})}
</Fragment>
@@ -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
);
}
+12 -9
View File
@@ -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) {
+3 -3
View File
@@ -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<RemoteMegaphoneType>;
hasMegaphone: (megaphoneId: RemoteMegaphoneId) => boolean;
getPinnedMessagesForConversation: (
getPinnedMessagesPreloadDataForConversation: (
conversationId: string
) => ReadonlyArray<PinnedMessageRenderData>;
) => ReadonlyArray<PinnedMessagePreloadData>;
getNextExpiringPinnedMessageAcrossConversations: () => PinnedMessage | null;
getMessagesNeedingUpgrade: (
+2 -2
View File
@@ -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,
+6 -6
View File
@@ -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<PinnedMessageRenderData> {
): ReadonlyArray<PinnedMessagePreloadData> {
return db.transaction(() => {
const [query, params] = sql`
SELECT * FROM pinnedMessages
@@ -61,7 +61,7 @@ export function getPinnedMessagesForConversation(
.prepare(query)
.all<PinnedMessage>(params)
.map(pinnedMessage => {
return _getPinnedMessageRenderData(db, pinnedMessage);
return _getPinnedMessagePreloadData(db, pinnedMessage);
});
})();
}
-2
View File
@@ -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,
+279 -8
View File
@@ -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<string>;
pinnedMessages: ReadonlyArray<PinnedMessage>;
messageLoadingState?: undefined | TimelineMessageLoadingState;
metrics: MessageMetricsType;
scrollToMessageId?: string;
@@ -504,7 +514,7 @@ export type ConversationMessageType = ReadonlyDeep<{
export type ConversationPreloadDataType = ReadonlyDeep<{
conversationId: string;
messages: ReadonlyArray<ReadonlyMessageAttributesType>;
pinnedMessages: ReadonlyArray<PinnedMessageRenderData>;
pinnedMessagesPreloadData: ReadonlyArray<PinnedMessagePreloadData>;
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<PinnedMessagePreloadData>;
};
}>;
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<PinnedMessageTarget> {
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<PinnedMessagesReplace> {
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<string, MessageType> = {};
const pinnedMessages: Array<PinnedMessage> = [];
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<string>,
pinnedMessages: ReadonlyArray<PinnedMessage>
): MessageLookupType {
const pinnedMessagesMessageIds = new Set<string>();
for (const pinnedMessage of pinnedMessages) {
pinnedMessagesMessageIds.add(pinnedMessage.messageId);
}
const messageIdsToRemove = new Set<string>();
for (const messageIdToMaybeRemove of messageIdsToMaybeRemove) {
if (!pinnedMessagesMessageIds.has(messageIdToMaybeRemove)) {
messageIdsToRemove.add(messageIdToMaybeRemove);
}
}
if (messageIdsToRemove.size === 0) {
return messagesLookup;
}
const updatedMessagesLookup: Record<string, MessageWithUIFieldsType> = {};
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<string, MessageAttributesType> = {};
const pinnedMessages: Array<PinnedMessage> = [];
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;
}
-312
View File
@@ -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<PinnedMessageRenderData>;
}>;
export type PinnedMessagesState = ReadonlyDeep<{
preloadData: PreloadData | null;
conversationId: string | null;
pinnedMessages: ReadonlyArray<PinnedMessageRenderData> | null;
}>;
const PINNED_MESSAGES_REPLACE = 'pinnedMessages/PINNED_MESSAGES_REPLACE';
export type PinnedMessagesReplace = ReadonlyDeep<{
type: typeof PINNED_MESSAGES_REPLACE;
payload: {
conversationId: string;
pinnedMessages: ReadonlyArray<PinnedMessageRenderData>;
};
}>;
export type PinnedMessagesAction = ReadonlyDeep<PinnedMessagesReplace>;
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<PinnedMessageTarget> {
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<PinnedMessagesReplace> {
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<PinnedMessageRenderData>,
message: ReadonlyMessageAttributesType
): ReadonlyArray<PinnedMessageRenderData> {
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;
}
}
-2
View File
@@ -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(),
-4
View File
@@ -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
-2
View File
@@ -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,
+13
View File
@@ -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<ReadonlyArray<PinnedMessage>> =
createSelector(getConversationMessages, conversationMessages => {
return conversationMessages?.pinnedMessages ?? [];
});
export const getPinnedMessagesMessageIds: StateSelector<ReadonlyArray<string>> =
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
+1 -1
View File
@@ -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';
-39
View File
@@ -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<PinnedMessageRenderData>
> = 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<ReadonlyArray<string>> =
createSelector(getPinnedMessages, pinnedMessages => {
return pinnedMessages.map(pinnedMessage => {
return pinnedMessage.message.id;
});
});
+1 -1
View File
@@ -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,
+34 -21
View File
@@ -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<ReadonlyArray<Pin>> = 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<Pin>,
unsortedPins: ReadonlyArray<Pin>,
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<Pin>(
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(
@@ -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]);
+1 -1
View File
@@ -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;
+2 -4
View File
@@ -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<HTMLElement>;
@@ -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,
-2
View File
@@ -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;
@@ -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,
},
+1 -1
View File
@@ -16,7 +16,7 @@ export type PinnedMessage = Readonly<{
export type PinnedMessageParams = Omit<PinnedMessage, 'id'>;
export type PinnedMessageRenderData = Readonly<{
export type PinnedMessagePreloadData = Readonly<{
pinnedMessage: PinnedMessage;
message: ReadonlyMessageAttributesType;
}>;