mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-08 08:58:38 +01:00
Setup pin/unpin actions and mark messages pinned in timeline
This commit is contained in:
@@ -1470,6 +1470,10 @@
|
|||||||
"messageformat": "Pin message",
|
"messageformat": "Pin message",
|
||||||
"description": "Shown on the drop-down menu for an individual message, pins the current message"
|
"description": "Shown on the drop-down menu for an individual message, pins the current message"
|
||||||
},
|
},
|
||||||
|
"icu:MessageContextMenu__UnpinMessage": {
|
||||||
|
"messageformat": "Unpin message",
|
||||||
|
"description": "Shown on the drop-down menu for an individual message, unpins the current message"
|
||||||
|
},
|
||||||
"icu:Poll__end-poll": {
|
"icu:Poll__end-poll": {
|
||||||
"messageformat": "End poll",
|
"messageformat": "End poll",
|
||||||
"description": "Label for button/menu item to end a poll. Shown in the poll votes modal and in the message context menu"
|
"description": "Label for button/menu item to end a poll. Shown in the poll votes modal and in the message context menu"
|
||||||
@@ -9266,6 +9270,10 @@
|
|||||||
"messageformat": "Edited",
|
"messageformat": "Edited",
|
||||||
"description": "label for an edited message"
|
"description": "label for an edited message"
|
||||||
},
|
},
|
||||||
|
"icu:MessageMetadata__pinned": {
|
||||||
|
"messageformat": "Pinned",
|
||||||
|
"description": "label for an pinned message"
|
||||||
|
},
|
||||||
"icu:DraftGifMessageSendModal__Title": {
|
"icu:DraftGifMessageSendModal__Title": {
|
||||||
"messageformat": "Add a message",
|
"messageformat": "Add a message",
|
||||||
"description": "Draft GIF Message Send Modal > Title"
|
"description": "Draft GIF Message Send Modal > Title"
|
||||||
|
|||||||
+36
-139
@@ -1193,12 +1193,34 @@ $message-padding-horizontal: 12px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.module-message__metadata {
|
.module-message__metadata {
|
||||||
|
@include mixins.font-caption;
|
||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
|
user-select: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
color: light-dark(variables.$color-gray-60, variables.$color-gray-25);
|
||||||
|
|
||||||
|
&--outgoing {
|
||||||
|
color: variables.$color-white-alpha-80;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--with-image-no-caption {
|
||||||
|
color: light-dark(variables.$color-white, variables.$color-white-alpha-80);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--outline-only-bubble {
|
||||||
|
color: light-dark(variables.$color-gray-60, variables.$color-gray-25);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--sticker {
|
||||||
|
color: variables.$color-gray-60;
|
||||||
|
}
|
||||||
|
|
||||||
&--inline {
|
&--inline {
|
||||||
float: inline-end;
|
float: inline-end;
|
||||||
@@ -1211,43 +1233,14 @@ $message-padding-horizontal: 12px;
|
|||||||
.module-message__metadata__edited {
|
.module-message__metadata__edited {
|
||||||
@include mixins.button-reset;
|
@include mixins.button-reset;
|
||||||
& {
|
& {
|
||||||
@include mixins.font-caption;
|
|
||||||
color: variables.$color-gray-60;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-inline-end: 6px;
|
margin-inline-end: 6px;
|
||||||
z-index: variables.$z-index-base;
|
z-index: variables.$z-index-base;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include mixins.dark-theme {
|
|
||||||
color: variables.$color-gray-25;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message__metadata__sms {
|
.module-message__metadata__sms {
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
display: inline-block;
|
|
||||||
margin-inline-start: 6px;
|
margin-inline-start: 6px;
|
||||||
// High margin to leave space for the increase when we go to two checks
|
|
||||||
margin-bottom: 2px;
|
|
||||||
|
|
||||||
@include mixins.color-svg(
|
|
||||||
'../images/icons/v2/lock-unlock-outline-12.svg',
|
|
||||||
variables.$color-white
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-message__metadata__sms--incoming {
|
|
||||||
@include mixins.light-theme {
|
|
||||||
background-color: variables.$color-gray-60;
|
|
||||||
}
|
|
||||||
@include mixins.dark-theme {
|
|
||||||
background-color: variables.$color-gray-25;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-message__container--outgoing .module-message__metadata__edited {
|
|
||||||
color: variables.$color-white-alpha-80;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// With an image and no caption, this section needs to be on top of the image overlay
|
// With an image and no caption, this section needs to be on top of the image overlay
|
||||||
@@ -1265,62 +1258,9 @@ $message-padding-horizontal: 12px;
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message__metadata--outline-only-bubble {
|
|
||||||
@include mixins.light-theme {
|
|
||||||
color: variables.$color-gray-60;
|
|
||||||
}
|
|
||||||
@include mixins.dark-theme {
|
|
||||||
color: variables.$color-gray-25;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-message__metadata__date {
|
|
||||||
@include mixins.font-caption;
|
|
||||||
user-select: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
@include mixins.light-theme {
|
|
||||||
color: variables.$color-white-alpha-80;
|
|
||||||
}
|
|
||||||
@include mixins.dark-theme {
|
|
||||||
color: variables.$color-white-alpha-80;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.module-message__metadata__tapable {
|
.module-message__metadata__tapable {
|
||||||
@include mixins.button-reset;
|
@include mixins.button-reset;
|
||||||
}
|
}
|
||||||
.module-message__metadata__date--incoming {
|
|
||||||
color: variables.$color-white-alpha-80;
|
|
||||||
|
|
||||||
@include mixins.light-theme {
|
|
||||||
color: variables.$color-gray-60;
|
|
||||||
}
|
|
||||||
@include mixins.dark-theme {
|
|
||||||
color: variables.$color-gray-25;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.module-message__metadata__date--with-image-no-caption {
|
|
||||||
@include mixins.light-theme {
|
|
||||||
color: variables.$color-white;
|
|
||||||
}
|
|
||||||
@include mixins.dark-theme {
|
|
||||||
color: variables.$color-white-alpha-80;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.module-message__metadata__date--outline-only-bubble {
|
|
||||||
@include mixins.light-theme {
|
|
||||||
color: variables.$color-gray-60;
|
|
||||||
}
|
|
||||||
@include mixins.dark-theme {
|
|
||||||
color: variables.$color-gray-25;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-message__metadata__date--with-sticker {
|
|
||||||
@include mixins.light-theme {
|
|
||||||
color: variables.$color-gray-60;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-message__metadata__status-icon {
|
.module-message__metadata__status-icon {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
@@ -1338,41 +1278,25 @@ $message-padding-horizontal: 12px;
|
|||||||
}
|
}
|
||||||
@include mixins.color-svg(
|
@include mixins.color-svg(
|
||||||
'../images/icons/v3/message_status/messagestatus-sending.svg',
|
'../images/icons/v3/message_status/messagestatus-sending.svg',
|
||||||
variables.$color-white
|
currentColor
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message__metadata__status-icon--sent {
|
.module-message__metadata__status-icon--sent {
|
||||||
@include mixins.light-theme {
|
@include mixins.color-svg(
|
||||||
@include mixins.color-svg(
|
'../images/icons/v3/message_status/messagestatus-sent.svg',
|
||||||
'../images/icons/v3/message_status/messagestatus-sent.svg',
|
currentColor
|
||||||
variables.$color-white-alpha-80
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
@include mixins.dark-theme {
|
|
||||||
@include mixins.color-svg(
|
|
||||||
'../images/icons/v3/message_status/messagestatus-sent.svg',
|
|
||||||
variables.$color-white-alpha-80
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.module-message__metadata__status-icon--delivered {
|
.module-message__metadata__status-icon--delivered {
|
||||||
// We reduce the margin size to keep the overall width the same
|
// We reduce the margin size to keep the overall width the same
|
||||||
margin-inline-end: 0px;
|
margin-inline-end: 0px;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
|
|
||||||
@include mixins.light-theme {
|
@include mixins.color-svg(
|
||||||
@include mixins.color-svg(
|
'../images/icons/v3/message_status/messagestatus-delivered.svg',
|
||||||
'../images/icons/v3/message_status/messagestatus-delivered.svg',
|
currentColor
|
||||||
variables.$color-white-alpha-80
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
@include mixins.dark-theme {
|
|
||||||
@include mixins.color-svg(
|
|
||||||
'../images/icons/v3/message_status/messagestatus-delivered.svg',
|
|
||||||
variables.$color-white-alpha-80
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.module-message__metadata__status-icon--read,
|
.module-message__metadata__status-icon--read,
|
||||||
.module-message__metadata__status-icon--viewed {
|
.module-message__metadata__status-icon--viewed {
|
||||||
@@ -1380,42 +1304,15 @@ $message-padding-horizontal: 12px;
|
|||||||
margin-inline-end: 0px;
|
margin-inline-end: 0px;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
|
|
||||||
@include mixins.light-theme {
|
@include mixins.color-svg(
|
||||||
@include mixins.color-svg(
|
'../images/icons/v3/message_status/messagestatus-read.svg',
|
||||||
'../images/icons/v3/message_status/messagestatus-read.svg',
|
currentColor
|
||||||
variables.$color-white-alpha-80
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
@include mixins.dark-theme {
|
|
||||||
@include mixins.color-svg(
|
|
||||||
'../images/icons/v3/message_status/messagestatus-read.svg',
|
|
||||||
variables.$color-white-alpha-80
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// When status indicators are overlaid on top of an image, they use different colors
|
// When status indicators are overlaid on top of an image, they use different colors
|
||||||
.module-message__metadata__status-icon--with-image-no-caption {
|
.module-message__metadata__status-icon--with-image-no-caption {
|
||||||
@include mixins.dark-theme {
|
color: light-dark(variables.$color-white, variables.$color-gray-02);
|
||||||
background-color: variables.$color-gray-02;
|
|
||||||
}
|
|
||||||
@include mixins.light-theme {
|
|
||||||
background-color: variables.$color-white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.module-message__metadata__status-icon--with-sticker {
|
|
||||||
@include mixins.light-theme {
|
|
||||||
background-color: variables.$color-gray-60;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.module-message__metadata__status-icon--outline-only-bubble {
|
|
||||||
@include mixins.light-theme {
|
|
||||||
background-color: variables.$color-gray-60;
|
|
||||||
}
|
|
||||||
@include mixins.dark-theme {
|
|
||||||
background-color: variables.$color-gray-25;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message__metadata__spinner-container {
|
.module-message__metadata__spinner-container {
|
||||||
|
|||||||
@@ -266,7 +266,7 @@
|
|||||||
--type-text-body-large: 0.875rem /* 14px */;
|
--type-text-body-large: 0.875rem /* 14px */;
|
||||||
--type-text-body-medium: 0.8125rem /* 13px */;
|
--type-text-body-medium: 0.8125rem /* 13px */;
|
||||||
--type-text-body-small: 0.75rem /* 12px */;
|
--type-text-body-small: 0.75rem /* 12px */;
|
||||||
--type-text-caption: 0.6825rem /* 11px */;
|
--type-text-caption: 0.6875rem /* 11px */;
|
||||||
/* font-weight */
|
/* font-weight */
|
||||||
--font-weight-*: initial; /* reset defaults */
|
--font-weight-*: initial; /* reset defaults */
|
||||||
--font-weight-semibold: 600;
|
--font-weight-semibold: 600;
|
||||||
|
|||||||
@@ -73,12 +73,24 @@ export namespace AxoDialog {
|
|||||||
export type ContentProps = Readonly<{
|
export type ContentProps = Readonly<{
|
||||||
size: ContentSize;
|
size: ContentSize;
|
||||||
escape: ContentEscape;
|
escape: ContentEscape;
|
||||||
|
disableMissingAriaDescriptionWarning?: boolean;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export const Content: FC<ContentProps> = memo(props => {
|
export const Content: FC<ContentProps> = memo(props => {
|
||||||
const sizeConfig = ContentSizes[props.size];
|
const sizeConfig = ContentSizes[props.size];
|
||||||
const handleContentEscapeEvent = useContentEscapeBehavior(props.escape);
|
const handleContentEscapeEvent = useContentEscapeBehavior(props.escape);
|
||||||
|
|
||||||
|
const descriptionProps = useMemo((): Dialog.DialogContentProps => {
|
||||||
|
if (props.disableMissingAriaDescriptionWarning) {
|
||||||
|
// Generally you should just add a description with `AxoDialog.Description`
|
||||||
|
// and use `sr-only` to hide it if you don't want it to be visible
|
||||||
|
// https://www.radix-ui.com/primitives/docs/components/dialog#description
|
||||||
|
return { 'aria-describedby': undefined };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}, [props.disableMissingAriaDescriptionWarning]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Overlay className={AxoBaseDialog.overlayStyles}>
|
<Dialog.Overlay className={AxoBaseDialog.overlayStyles}>
|
||||||
@@ -90,6 +102,7 @@ export namespace AxoDialog {
|
|||||||
width: sizeConfig.width,
|
width: sizeConfig.width,
|
||||||
minWidth: 320,
|
minWidth: 320,
|
||||||
}}
|
}}
|
||||||
|
{...descriptionProps}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</Dialog.Content>
|
</Dialog.Content>
|
||||||
|
|||||||
@@ -2985,6 +2985,11 @@ export async function startApp(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.message.pinMessage != null) {
|
if (data.message.pinMessage != null) {
|
||||||
|
if (!isPinnedMessagesReceiveEnabled()) {
|
||||||
|
log.warn('Dropping PinMessage because the flag is disabled');
|
||||||
|
confirm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
strictAssert(data.timestamp != null, 'Missing sent timestamp');
|
strictAssert(data.timestamp != null, 'Missing sent timestamp');
|
||||||
await PinnedMessages.onPinnedMessageAdd({
|
await PinnedMessages.onPinnedMessageAdd({
|
||||||
targetSentTimestamp: data.message.pinMessage.targetSentTimestamp,
|
targetSentTimestamp: data.message.pinMessage.targetSentTimestamp,
|
||||||
@@ -3129,6 +3134,11 @@ export async function startApp(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.message.unpinMessage != null) {
|
if (data.message.unpinMessage != null) {
|
||||||
|
if (!isPinnedMessagesReceiveEnabled()) {
|
||||||
|
log.warn('Dropping UnpinMessage because the flag is disabled');
|
||||||
|
confirm();
|
||||||
|
return;
|
||||||
|
}
|
||||||
await PinnedMessages.onPinnedMessageRemove({
|
await PinnedMessages.onPinnedMessageRemove({
|
||||||
targetSentTimestamp: data.message.unpinMessage.targetSentTimestamp,
|
targetSentTimestamp: data.message.unpinMessage.targetSentTimestamp,
|
||||||
targetAuthorAci: data.message.unpinMessage.targetAuthorAci,
|
targetAuthorAci: data.message.unpinMessage.targetAuthorAci,
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ const MESSAGE_DEFAULT_PROPS = {
|
|||||||
doubleCheckMissingQuoteReference: shouldNeverBeCalled,
|
doubleCheckMissingQuoteReference: shouldNeverBeCalled,
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
isMessageRequestAccepted: true,
|
isMessageRequestAccepted: true,
|
||||||
|
isPinned: false,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
isSelectMode: false,
|
isSelectMode: false,
|
||||||
isSMS: false,
|
isSMS: false,
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export const CallingNotification: React.FC<PropsType> = React.memo(
|
|||||||
onForward={null}
|
onForward={null}
|
||||||
onMoreInfo={null}
|
onMoreInfo={null}
|
||||||
onPinMessage={null}
|
onPinMessage={null}
|
||||||
|
onUnpinMessage={null}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
// @ts-expect-error -- React/TS doesn't know about inert
|
// @ts-expect-error -- React/TS doesn't know about inert
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ import {
|
|||||||
} from '../fun/data/emojis.std.js';
|
} from '../fun/data/emojis.std.js';
|
||||||
import { useGroupedAndOrderedReactions } from '../../util/groupAndOrderReactions.dom.js';
|
import { useGroupedAndOrderedReactions } from '../../util/groupAndOrderReactions.dom.js';
|
||||||
import type { AxoMenuBuilder } from '../../axo/AxoMenuBuilder.dom.js';
|
import type { AxoMenuBuilder } from '../../axo/AxoMenuBuilder.dom.js';
|
||||||
|
import type { RenderAudioAttachmentProps } from '../../state/smart/renderAudioAttachment.preload.js';
|
||||||
|
|
||||||
const { drop, take, unescape } = lodash;
|
const { drop, take, unescape } = lodash;
|
||||||
|
|
||||||
@@ -326,6 +327,8 @@ export type PropsData = {
|
|||||||
isTapToViewExpired?: boolean;
|
isTapToViewExpired?: boolean;
|
||||||
isTapToViewError?: boolean;
|
isTapToViewError?: boolean;
|
||||||
|
|
||||||
|
isPinned: boolean;
|
||||||
|
|
||||||
readStatus?: ReadStatus;
|
readStatus?: ReadStatus;
|
||||||
|
|
||||||
expirationLength?: number;
|
expirationLength?: number;
|
||||||
@@ -361,7 +364,7 @@ export type PropsHousekeeping = {
|
|||||||
interactivity: MessageInteractivity;
|
interactivity: MessageInteractivity;
|
||||||
interactionMode: InteractionModeType;
|
interactionMode: InteractionModeType;
|
||||||
platform: string;
|
platform: string;
|
||||||
renderAudioAttachment: (props: AudioAttachmentProps) => JSX.Element;
|
renderAudioAttachment: (props: RenderAudioAttachmentProps) => JSX.Element;
|
||||||
shouldCollapseAbove: boolean;
|
shouldCollapseAbove: boolean;
|
||||||
shouldCollapseBelow: boolean;
|
shouldCollapseBelow: boolean;
|
||||||
shouldHideMetadata: boolean;
|
shouldHideMetadata: boolean;
|
||||||
@@ -826,6 +829,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||||||
expirationTimestamp,
|
expirationTimestamp,
|
||||||
giftBadge,
|
giftBadge,
|
||||||
i18n,
|
i18n,
|
||||||
|
isPinned,
|
||||||
isTapToView,
|
isTapToView,
|
||||||
isTapToViewError,
|
isTapToViewError,
|
||||||
isTapToViewExpired,
|
isTapToViewExpired,
|
||||||
@@ -840,6 +844,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||||||
if (
|
if (
|
||||||
!expirationLength &&
|
!expirationLength &&
|
||||||
!expirationTimestamp &&
|
!expirationTimestamp &&
|
||||||
|
!isPinned &&
|
||||||
(!status || SENT_STATUSES.has(status)) &&
|
(!status || SENT_STATUSES.has(status)) &&
|
||||||
shouldHideMetadata
|
shouldHideMetadata
|
||||||
) {
|
) {
|
||||||
@@ -1084,6 +1089,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
isEditedMessage,
|
isEditedMessage,
|
||||||
|
isPinned,
|
||||||
isSMS,
|
isSMS,
|
||||||
isSticker,
|
isSticker,
|
||||||
retryMessageSend,
|
retryMessageSend,
|
||||||
@@ -1107,6 +1113,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id={id}
|
id={id}
|
||||||
isEditedMessage={isEditedMessage}
|
isEditedMessage={isEditedMessage}
|
||||||
|
isPinned={isPinned}
|
||||||
isSMS={isSMS}
|
isSMS={isSMS}
|
||||||
isInline={isInline}
|
isInline={isInline}
|
||||||
isOutlineOnlyBubble={
|
isOutlineOnlyBubble={
|
||||||
@@ -1153,12 +1160,12 @@ export class Message extends React.PureComponent<Props, State> {
|
|||||||
attachmentDroppedDueToSize,
|
attachmentDroppedDueToSize,
|
||||||
attachments,
|
attachments,
|
||||||
cancelAttachmentDownload,
|
cancelAttachmentDownload,
|
||||||
conversationId,
|
|
||||||
direction,
|
direction,
|
||||||
expirationLength,
|
expirationLength,
|
||||||
expirationTimestamp,
|
expirationTimestamp,
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
|
isPinned,
|
||||||
isSticker,
|
isSticker,
|
||||||
isVoiceMessagePlayed,
|
isVoiceMessagePlayed,
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
@@ -1301,7 +1308,6 @@ export class Message extends React.PureComponent<Props, State> {
|
|||||||
i18n,
|
i18n,
|
||||||
buttonRef: this.audioButtonRef,
|
buttonRef: this.audioButtonRef,
|
||||||
renderingContext,
|
renderingContext,
|
||||||
theme,
|
|
||||||
attachment: firstAttachment,
|
attachment: firstAttachment,
|
||||||
collapseMetadata,
|
collapseMetadata,
|
||||||
withContentAbove,
|
withContentAbove,
|
||||||
@@ -1311,9 +1317,8 @@ export class Message extends React.PureComponent<Props, State> {
|
|||||||
expirationLength,
|
expirationLength,
|
||||||
expirationTimestamp,
|
expirationTimestamp,
|
||||||
id,
|
id,
|
||||||
conversationId,
|
isPinned,
|
||||||
played: isVoiceMessagePlayed,
|
played: isVoiceMessagePlayed,
|
||||||
pushPanelForConversation,
|
|
||||||
status,
|
status,
|
||||||
textPending: textAttachment?.pending,
|
textPending: textAttachment?.pending,
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -1336,7 +1341,10 @@ export class Message extends React.PureComponent<Props, State> {
|
|||||||
const isIncoming = direction === 'incoming';
|
const isIncoming = direction === 'incoming';
|
||||||
|
|
||||||
const willShowMetadata =
|
const willShowMetadata =
|
||||||
expirationLength || expirationTimestamp || !shouldHideMetadata;
|
expirationLength ||
|
||||||
|
expirationTimestamp ||
|
||||||
|
isPinned ||
|
||||||
|
!shouldHideMetadata;
|
||||||
|
|
||||||
// Note: this has to be interactive for the case where text comes along with the
|
// Note: this has to be interactive for the case where text comes along with the
|
||||||
// attachment. But we don't want the user to tab here unless that text exists.
|
// attachment. But we don't want the user to tab here unless that text exists.
|
||||||
@@ -1426,6 +1434,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id={id}
|
id={id}
|
||||||
isEditedMessage={false}
|
isEditedMessage={false}
|
||||||
|
isPinned={isPinned}
|
||||||
isSMS={false}
|
isSMS={false}
|
||||||
isInline={false}
|
isInline={false}
|
||||||
isOutlineOnlyBubble={false}
|
isOutlineOnlyBubble={false}
|
||||||
@@ -2699,6 +2708,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||||||
expirationTimestamp,
|
expirationTimestamp,
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
|
isPinned,
|
||||||
isTapToViewError,
|
isTapToViewError,
|
||||||
isTapToViewExpired,
|
isTapToViewExpired,
|
||||||
pushPanelForConversation,
|
pushPanelForConversation,
|
||||||
@@ -2763,6 +2773,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id={id}
|
id={id}
|
||||||
isEditedMessage={false}
|
isEditedMessage={false}
|
||||||
|
isPinned={isPinned}
|
||||||
isSMS={false}
|
isSMS={false}
|
||||||
isInline={false}
|
isInline={false}
|
||||||
isOutlineOnlyBubble={false}
|
isOutlineOnlyBubble={false}
|
||||||
@@ -2804,6 +2815,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id={id}
|
id={id}
|
||||||
isEditedMessage={false}
|
isEditedMessage={false}
|
||||||
|
isPinned={isPinned}
|
||||||
isSMS={false}
|
isSMS={false}
|
||||||
isInline={false}
|
isInline={false}
|
||||||
isOutlineOnlyBubble={false}
|
isOutlineOnlyBubble={false}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export type OwnProps = Readonly<{
|
|||||||
expirationLength?: number;
|
expirationLength?: number;
|
||||||
expirationTimestamp?: number;
|
expirationTimestamp?: number;
|
||||||
id: string;
|
id: string;
|
||||||
|
isPinned: boolean;
|
||||||
played: boolean;
|
played: boolean;
|
||||||
status?: MessageStatusType;
|
status?: MessageStatusType;
|
||||||
textPending?: boolean;
|
textPending?: boolean;
|
||||||
@@ -156,6 +157,7 @@ export function MessageAudio(props: Props): JSX.Element {
|
|||||||
expirationLength,
|
expirationLength,
|
||||||
expirationTimestamp,
|
expirationTimestamp,
|
||||||
id,
|
id,
|
||||||
|
isPinned,
|
||||||
played,
|
played,
|
||||||
status,
|
status,
|
||||||
textPending,
|
textPending,
|
||||||
@@ -381,6 +383,7 @@ export function MessageAudio(props: Props): JSX.Element {
|
|||||||
hasText={withContentBelow}
|
hasText={withContentBelow}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
id={id}
|
id={id}
|
||||||
|
isPinned={isPinned}
|
||||||
isShowingImage={false}
|
isShowingImage={false}
|
||||||
isSticker={false}
|
isSticker={false}
|
||||||
pushPanelForConversation={pushPanelForConversation}
|
pushPanelForConversation={pushPanelForConversation}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type MessageContextMenuProps = Readonly<{
|
|||||||
onForward: (() => void) | null;
|
onForward: (() => void) | null;
|
||||||
onDeleteMessage: (() => void) | null;
|
onDeleteMessage: (() => void) | null;
|
||||||
onPinMessage: (() => void) | null;
|
onPinMessage: (() => void) | null;
|
||||||
|
onUnpinMessage: (() => void) | null;
|
||||||
onMoreInfo: (() => void) | null;
|
onMoreInfo: (() => void) | null;
|
||||||
onSelect: (() => void) | null;
|
onSelect: (() => void) | null;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -47,6 +48,7 @@ export function MessageContextMenu({
|
|||||||
onForward,
|
onForward,
|
||||||
onDeleteMessage,
|
onDeleteMessage,
|
||||||
onPinMessage,
|
onPinMessage,
|
||||||
|
onUnpinMessage,
|
||||||
children,
|
children,
|
||||||
}: MessageContextMenuProps): JSX.Element {
|
}: MessageContextMenuProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
@@ -104,6 +106,11 @@ export function MessageContextMenu({
|
|||||||
{i18n('icu:MessageContextMenu__PinMessage')}
|
{i18n('icu:MessageContextMenu__PinMessage')}
|
||||||
</AxoMenuBuilder.Item>
|
</AxoMenuBuilder.Item>
|
||||||
)}
|
)}
|
||||||
|
{isPinnedMessagesReceiveEnabled() && onUnpinMessage && (
|
||||||
|
<AxoMenuBuilder.Item symbol="pin-slash" onSelect={onUnpinMessage}>
|
||||||
|
{i18n('icu:MessageContextMenu__UnpinMessage')}
|
||||||
|
</AxoMenuBuilder.Item>
|
||||||
|
)}
|
||||||
{onMoreInfo && (
|
{onMoreInfo && (
|
||||||
<AxoMenuBuilder.Item symbol="info" onSelect={onMoreInfo}>
|
<AxoMenuBuilder.Item symbol="info" onSelect={onMoreInfo}>
|
||||||
{i18n('icu:MessageContextMenu__info')}
|
{i18n('icu:MessageContextMenu__info')}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const defaultMessage: MessageDataPropsType = {
|
|||||||
renderMenu: undefined,
|
renderMenu: undefined,
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
isMessageRequestAccepted: true,
|
isMessageRequestAccepted: true,
|
||||||
|
isPinned: false,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
isSelectMode: false,
|
isSelectMode: false,
|
||||||
isSMS: false,
|
isSMS: false,
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import { ConfirmationDialog } from '../ConfirmationDialog.dom.js';
|
|||||||
import { refMerger } from '../../util/refMerger.std.js';
|
import { refMerger } from '../../util/refMerger.std.js';
|
||||||
import type { Size } from '../../hooks/useSizeObserver.dom.js';
|
import type { Size } from '../../hooks/useSizeObserver.dom.js';
|
||||||
import { SizeObserver } from '../../hooks/useSizeObserver.dom.js';
|
import { SizeObserver } from '../../hooks/useSizeObserver.dom.js';
|
||||||
|
import { AxoSymbol } from '../../axo/AxoSymbol.dom.js';
|
||||||
|
import { tw } from '../../axo/tw.dom.js';
|
||||||
|
|
||||||
type PropsType = {
|
type PropsType = {
|
||||||
deletedForEveryone?: boolean;
|
deletedForEveryone?: boolean;
|
||||||
@@ -28,6 +30,7 @@ type PropsType = {
|
|||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
id: string;
|
id: string;
|
||||||
isEditedMessage?: boolean;
|
isEditedMessage?: boolean;
|
||||||
|
isPinned: boolean;
|
||||||
isSMS?: boolean;
|
isSMS?: boolean;
|
||||||
isInline?: boolean;
|
isInline?: boolean;
|
||||||
isOutlineOnlyBubble?: boolean;
|
isOutlineOnlyBubble?: boolean;
|
||||||
@@ -57,6 +60,7 @@ export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
|
|||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
isEditedMessage,
|
isEditedMessage,
|
||||||
|
isPinned,
|
||||||
isSMS,
|
isSMS,
|
||||||
isOutlineOnlyBubble,
|
isOutlineOnlyBubble,
|
||||||
isInline,
|
isInline,
|
||||||
@@ -135,19 +139,7 @@ export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
timestampNode = (
|
timestampNode = (
|
||||||
<span
|
<span className="module-message__metadata__date">{statusInfo}</span>
|
||||||
className={classNames({
|
|
||||||
'module-message__metadata__date': true,
|
|
||||||
'module-message__metadata__date--with-sticker': isSticker,
|
|
||||||
'module-message__metadata__date--outline-only-bubble':
|
|
||||||
isOutlineOnlyBubble,
|
|
||||||
[`module-message__metadata__date--${direction}`]: !isSticker,
|
|
||||||
'module-message__metadata__date--with-image-no-caption':
|
|
||||||
withImageNoCaption,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{statusInfo}
|
|
||||||
</span>
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
timestampNode = (
|
timestampNode = (
|
||||||
@@ -195,12 +187,22 @@ export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
|
|||||||
|
|
||||||
const className = classNames(
|
const className = classNames(
|
||||||
'module-message__metadata',
|
'module-message__metadata',
|
||||||
|
direction === 'outgoing' && 'module-message__metadata--outgoing',
|
||||||
isInline && 'module-message__metadata--inline',
|
isInline && 'module-message__metadata--inline',
|
||||||
withImageNoCaption && 'module-message__metadata--with-image-no-caption',
|
withImageNoCaption && 'module-message__metadata--with-image-no-caption',
|
||||||
isOutlineOnlyBubble && 'module-message__metadata--outline-only-bubble'
|
isOutlineOnlyBubble && 'module-message__metadata--outline-only-bubble',
|
||||||
|
isSticker && 'module-message__metadata--sticker'
|
||||||
);
|
);
|
||||||
const children = (
|
const children = (
|
||||||
<>
|
<>
|
||||||
|
{isPinned && (
|
||||||
|
<span className={tw('me-1')}>
|
||||||
|
<AxoSymbol.InlineGlyph
|
||||||
|
symbol="pin-fill"
|
||||||
|
label={i18n('icu:MessageMetadata__pinned')}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{isEditedMessage && showEditHistoryModal && (
|
{isEditedMessage && showEditHistoryModal && (
|
||||||
<button
|
<button
|
||||||
className="module-message__metadata__edited"
|
className="module-message__metadata__edited"
|
||||||
@@ -212,9 +214,9 @@ export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
|
|||||||
)}
|
)}
|
||||||
{timestampNode}
|
{timestampNode}
|
||||||
{isSMS ? (
|
{isSMS ? (
|
||||||
<div
|
<div className="module-message__metadata__sms">
|
||||||
className={`module-message__metadata__sms module-message__metadata__sms--${direction}`}
|
<AxoSymbol.InlineGlyph symbol="lock-open" label={null} />
|
||||||
/>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{expirationLength ? (
|
{expirationLength ? (
|
||||||
<ExpireTimer
|
<ExpireTimer
|
||||||
@@ -239,16 +241,7 @@ export const MessageMetadata = forwardRef<HTMLDivElement, Readonly<PropsType>>(
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message__metadata__status-icon',
|
'module-message__metadata__status-icon',
|
||||||
`module-message__metadata__status-icon--${status}`,
|
`module-message__metadata__status-icon--${status}`
|
||||||
isSticker
|
|
||||||
? 'module-message__metadata__status-icon--with-sticker'
|
|
||||||
: null,
|
|
||||||
withImageNoCaption
|
|
||||||
? 'module-message__metadata__status-icon--with-image-no-caption'
|
|
||||||
: null,
|
|
||||||
isOutlineOnlyBubble
|
|
||||||
? 'module-message__metadata__status-icon--outline-only-bubble'
|
|
||||||
: null
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const defaultMessageProps: TimelineMessagesProps = {
|
|||||||
canEditMessage: true,
|
canEditMessage: true,
|
||||||
canEndPoll: false,
|
canEndPoll: false,
|
||||||
canForward: true,
|
canForward: true,
|
||||||
|
canPinMessages: true,
|
||||||
canReact: true,
|
canReact: true,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
canRetry: true,
|
canRetry: true,
|
||||||
@@ -105,6 +106,7 @@ const defaultMessageProps: TimelineMessagesProps = {
|
|||||||
interactionMode: 'keyboard',
|
interactionMode: 'keyboard',
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
isMessageRequestAccepted: true,
|
isMessageRequestAccepted: true,
|
||||||
|
isPinned: false,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
isSelectMode: false,
|
isSelectMode: false,
|
||||||
isSMS: false,
|
isSMS: false,
|
||||||
@@ -137,6 +139,8 @@ const defaultMessageProps: TimelineMessagesProps = {
|
|||||||
shouldCollapseBelow: false,
|
shouldCollapseBelow: false,
|
||||||
shouldHideMetadata: false,
|
shouldHideMetadata: false,
|
||||||
showSpoiler: action('showSpoiler'),
|
showSpoiler: action('showSpoiler'),
|
||||||
|
onPinnedMessageAdd: action('onPinnedMessageAdd'),
|
||||||
|
onPinnedMessageRemove: action('onPinnedMessageRemove'),
|
||||||
pushPanelForConversation: action('default--pushPanelForConversation'),
|
pushPanelForConversation: action('default--pushPanelForConversation'),
|
||||||
showContactModal: action('default--showContactModal'),
|
showContactModal: action('default--showContactModal'),
|
||||||
showAttachmentDownloadStillInProgressToast: action(
|
showAttachmentDownloadStillInProgressToast: action(
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ function mockMessageTimelineItem(
|
|||||||
canEditMessage: true,
|
canEditMessage: true,
|
||||||
canEndPoll: false,
|
canEndPoll: false,
|
||||||
canForward: true,
|
canForward: true,
|
||||||
|
canPinMessages: true,
|
||||||
canReact: true,
|
canReact: true,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
canRetry: true,
|
canRetry: true,
|
||||||
@@ -67,6 +68,7 @@ function mockMessageTimelineItem(
|
|||||||
text: 'Hello there from the new world!',
|
text: 'Hello there from the new world!',
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
isMessageRequestAccepted: true,
|
isMessageRequestAccepted: true,
|
||||||
|
isPinned: false,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
isSelectMode: false,
|
isSelectMode: false,
|
||||||
isSMS: false,
|
isSMS: false,
|
||||||
@@ -312,6 +314,8 @@ const actions = () => ({
|
|||||||
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),
|
||||||
|
|
||||||
openGiftBadge: action('openGiftBadge'),
|
openGiftBadge: action('openGiftBadge'),
|
||||||
|
onPinnedMessageAdd: action('onPinnedMessageAdd'),
|
||||||
|
onPinnedMessageRemove: action('onPinnedMessageRemove'),
|
||||||
scrollToPinnedMessage: action('scrollToPinnedMessage'),
|
scrollToPinnedMessage: action('scrollToPinnedMessage'),
|
||||||
scrollToPollMessage: action('scrollToPollMessage'),
|
scrollToPollMessage: action('scrollToPollMessage'),
|
||||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const getDefaultProps = () => ({
|
|||||||
getPreferredBadge: () => undefined,
|
getPreferredBadge: () => undefined,
|
||||||
id: 'asdf',
|
id: 'asdf',
|
||||||
isNextItemCallingNotification: false,
|
isNextItemCallingNotification: false,
|
||||||
|
isPinned: false,
|
||||||
isTargeted: false,
|
isTargeted: false,
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
isGroup: false,
|
isGroup: false,
|
||||||
@@ -69,6 +70,8 @@ const getDefaultProps = () => ({
|
|||||||
openGiftBadge: action('openGiftBadge'),
|
openGiftBadge: action('openGiftBadge'),
|
||||||
saveAttachment: action('saveAttachment'),
|
saveAttachment: action('saveAttachment'),
|
||||||
saveAttachments: action('saveAttachments'),
|
saveAttachments: action('saveAttachments'),
|
||||||
|
onPinnedMessageAdd: action('onPinnedMessageAdd'),
|
||||||
|
onPinnedMessageRemove: action('onPinnedMessageRemove'),
|
||||||
onOpenEditNicknameAndNoteModal: action('onOpenEditNicknameAndNoteModal'),
|
onOpenEditNicknameAndNoteModal: action('onOpenEditNicknameAndNoteModal'),
|
||||||
onOutgoingAudioCallInConversation: action(
|
onOutgoingAudioCallInConversation: action(
|
||||||
'onOutgoingAudioCallInConversation'
|
'onOutgoingAudioCallInConversation'
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import type { Meta, StoryFn } from '@storybook/react';
|
|||||||
|
|
||||||
import { SignalService } from '../../protobuf/index.std.js';
|
import { SignalService } from '../../protobuf/index.std.js';
|
||||||
import { ConversationColors } from '../../types/Colors.std.js';
|
import { ConversationColors } from '../../types/Colors.std.js';
|
||||||
import type { AudioAttachmentProps } from './Message.dom.js';
|
|
||||||
import type { Props } from './TimelineMessage.dom.js';
|
import type { Props } from './TimelineMessage.dom.js';
|
||||||
import { TimelineMessage } from './TimelineMessage.dom.js';
|
import { TimelineMessage } from './TimelineMessage.dom.js';
|
||||||
import { MessageInteractivity, TextDirection } from './Message.dom.js';
|
import { MessageInteractivity, TextDirection } from './Message.dom.js';
|
||||||
@@ -43,6 +42,7 @@ import { getFakeBadge } from '../../test-helpers/getFakeBadge.std.js';
|
|||||||
import { ThemeType } from '../../types/Util.std.js';
|
import { ThemeType } from '../../types/Util.std.js';
|
||||||
import { BadgeCategory } from '../../badges/BadgeCategory.std.js';
|
import { BadgeCategory } from '../../badges/BadgeCategory.std.js';
|
||||||
import { PaymentEventKind } from '../../types/Payment.std.js';
|
import { PaymentEventKind } from '../../types/Payment.std.js';
|
||||||
|
import type { RenderAudioAttachmentProps } from '../../state/smart/renderAudioAttachment.preload.js';
|
||||||
|
|
||||||
const { isBoolean, noop } = lodash;
|
const { isBoolean, noop } = lodash;
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ const renderReactionPicker: Props['renderReactionPicker'] = () => <div />;
|
|||||||
function MessageAudioContainer({
|
function MessageAudioContainer({
|
||||||
played,
|
played,
|
||||||
...props
|
...props
|
||||||
}: AudioAttachmentProps): JSX.Element {
|
}: RenderAudioAttachmentProps): JSX.Element {
|
||||||
const [isActive, setIsActive] = React.useState<boolean>(false);
|
const [isActive, setIsActive] = React.useState<boolean>(false);
|
||||||
const [currentTime, setCurrentTime] = React.useState<number>(0);
|
const [currentTime, setCurrentTime] = React.useState<number>(0);
|
||||||
const [playbackRate, setPlaybackRate] = React.useState<number>(1);
|
const [playbackRate, setPlaybackRate] = React.useState<number>(1);
|
||||||
@@ -214,6 +214,7 @@ function MessageAudioContainer({
|
|||||||
{...props}
|
{...props}
|
||||||
active={active}
|
active={active}
|
||||||
computePeaks={computePeaks}
|
computePeaks={computePeaks}
|
||||||
|
isPinned={false}
|
||||||
onPlayMessage={handlePlayMessage}
|
onPlayMessage={handlePlayMessage}
|
||||||
played={_played}
|
played={_played}
|
||||||
pushPanelForConversation={action('pushPanelForConversation')}
|
pushPanelForConversation={action('pushPanelForConversation')}
|
||||||
@@ -236,6 +237,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||||||
canCopy: true,
|
canCopy: true,
|
||||||
canEditMessage: true,
|
canEditMessage: true,
|
||||||
canEndPoll: overrideProps.direction === 'outgoing',
|
canEndPoll: overrideProps.direction === 'outgoing',
|
||||||
|
canPinMessages: overrideProps.canPinMessages ?? true,
|
||||||
canReact: true,
|
canReact: true,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
@@ -276,6 +278,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||||||
isMessageRequestAccepted: isBoolean(overrideProps.isMessageRequestAccepted)
|
isMessageRequestAccepted: isBoolean(overrideProps.isMessageRequestAccepted)
|
||||||
? overrideProps.isMessageRequestAccepted
|
? overrideProps.isMessageRequestAccepted
|
||||||
: true,
|
: true,
|
||||||
|
isPinned: isBoolean(overrideProps.isPinned) ? overrideProps.isPinned : false,
|
||||||
isSelected: isBoolean(overrideProps.isSelected)
|
isSelected: isBoolean(overrideProps.isSelected)
|
||||||
? overrideProps.isSelected
|
? overrideProps.isSelected
|
||||||
: false,
|
: false,
|
||||||
@@ -294,6 +297,8 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||||||
messageExpanded: action('messageExpanded'),
|
messageExpanded: action('messageExpanded'),
|
||||||
showConversation: action('showConversation'),
|
showConversation: action('showConversation'),
|
||||||
openGiftBadge: action('openGiftBadge'),
|
openGiftBadge: action('openGiftBadge'),
|
||||||
|
onPinnedMessageAdd: action('onPinnedMessageAdd'),
|
||||||
|
onPinnedMessageRemove: action('onPinnedMessageRemove'),
|
||||||
previews: overrideProps.previews || [],
|
previews: overrideProps.previews || [],
|
||||||
quote: overrideProps.quote || undefined,
|
quote: overrideProps.quote || undefined,
|
||||||
reactions: overrideProps.reactions,
|
reactions: overrideProps.reactions,
|
||||||
@@ -3288,3 +3293,9 @@ AttachmentWithError.args = {
|
|||||||
],
|
],
|
||||||
status: 'sent',
|
status: 'sent',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PinnedMessages = Template.bind({});
|
||||||
|
PinnedMessages.args = {
|
||||||
|
text: 'I am pinned',
|
||||||
|
isPinned: true,
|
||||||
|
};
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { isNotNil } from '../../util/isNotNil.std.js';
|
|||||||
import type { AxoMenuBuilder } from '../../axo/AxoMenuBuilder.dom.js';
|
import type { AxoMenuBuilder } from '../../axo/AxoMenuBuilder.dom.js';
|
||||||
import { AxoContextMenu } from '../../axo/AxoContextMenu.dom.js';
|
import { AxoContextMenu } from '../../axo/AxoContextMenu.dom.js';
|
||||||
import { PinMessageDialog } from './pinned-messages/PinMessageDialog.dom.js';
|
import { PinMessageDialog } from './pinned-messages/PinMessageDialog.dom.js';
|
||||||
|
import type { DurationInSeconds } from '../../util/durations/duration-in-seconds.std.js';
|
||||||
import { useDocumentKeyDown } from '../../hooks/useDocumentKeyDown.dom.js';
|
import { useDocumentKeyDown } from '../../hooks/useDocumentKeyDown.dom.js';
|
||||||
|
|
||||||
const { useAxoContextMenuOutsideKeyboardTrigger } = AxoContextMenu;
|
const { useAxoContextMenuOutsideKeyboardTrigger } = AxoContextMenu;
|
||||||
@@ -53,11 +54,17 @@ export type PropsData = {
|
|||||||
canRetryDeleteForEveryone: boolean;
|
canRetryDeleteForEveryone: boolean;
|
||||||
canReact: boolean;
|
canReact: boolean;
|
||||||
canReply: boolean;
|
canReply: boolean;
|
||||||
|
canPinMessages: boolean;
|
||||||
selectedReaction?: string;
|
selectedReaction?: string;
|
||||||
isTargeted?: boolean;
|
isTargeted?: boolean;
|
||||||
} & Omit<MessagePropsData, 'renderingContext' | 'menu'>;
|
} & Omit<MessagePropsData, 'renderingContext' | 'menu'>;
|
||||||
|
|
||||||
export type PropsActions = {
|
export type PropsActions = {
|
||||||
|
onPinnedMessageAdd: (
|
||||||
|
messageId: string,
|
||||||
|
duration: DurationInSeconds | null
|
||||||
|
) => void;
|
||||||
|
onPinnedMessageRemove: (messageId: string) => void;
|
||||||
pushPanelForConversation: PushPanelForConversationActionType;
|
pushPanelForConversation: PushPanelForConversationActionType;
|
||||||
toggleDeleteMessagesModal: (props: DeleteMessagesPropsType) => void;
|
toggleDeleteMessagesModal: (props: DeleteMessagesPropsType) => void;
|
||||||
toggleForwardMessagesModal: (payload: ForwardMessagesPayload) => void;
|
toggleForwardMessagesModal: (payload: ForwardMessagesPayload) => void;
|
||||||
@@ -106,6 +113,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||||||
canReply,
|
canReply,
|
||||||
canRetry,
|
canRetry,
|
||||||
canRetryDeleteForEveryone,
|
canRetryDeleteForEveryone,
|
||||||
|
canPinMessages,
|
||||||
containerElementRef,
|
containerElementRef,
|
||||||
containerWidthBreakpoint,
|
containerWidthBreakpoint,
|
||||||
conversationId,
|
conversationId,
|
||||||
@@ -113,10 +121,13 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
interactivity,
|
interactivity,
|
||||||
|
isPinned,
|
||||||
isTargeted,
|
isTargeted,
|
||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
copyMessageText,
|
copyMessageText,
|
||||||
endPoll,
|
endPoll,
|
||||||
|
onPinnedMessageAdd,
|
||||||
|
onPinnedMessageRemove,
|
||||||
pushPanelForConversation,
|
pushPanelForConversation,
|
||||||
reactToMessage,
|
reactToMessage,
|
||||||
renderReactionPicker,
|
renderReactionPicker,
|
||||||
@@ -277,6 +288,18 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||||||
setPinMessageDialogOpen(true);
|
setPinMessageDialogOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handlePinnedMessageAdd = useCallback(
|
||||||
|
(messageId: string, duration: DurationInSeconds | null) => {
|
||||||
|
onPinnedMessageAdd(messageId, duration);
|
||||||
|
setPinMessageDialogOpen(false);
|
||||||
|
},
|
||||||
|
[onPinnedMessageAdd]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUnpinMessage = useCallback(() => {
|
||||||
|
onPinnedMessageRemove(id);
|
||||||
|
}, [onPinnedMessageRemove, id]);
|
||||||
|
|
||||||
const toggleReactionPickerKeyboard = useToggleReactionPicker(
|
const toggleReactionPickerKeyboard = useToggleReactionPicker(
|
||||||
handleReact || noop
|
handleReact || noop
|
||||||
);
|
);
|
||||||
@@ -339,7 +362,12 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||||||
messageIds: [id],
|
messageIds: [id],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onPinMessage={handleOpenPinMessageDialog}
|
onPinMessage={
|
||||||
|
canPinMessages && !isPinned ? handleOpenPinMessageDialog : null
|
||||||
|
}
|
||||||
|
onUnpinMessage={
|
||||||
|
canPinMessages && isPinned ? handleUnpinMessage : null
|
||||||
|
}
|
||||||
onMoreInfo={() =>
|
onMoreInfo={() =>
|
||||||
pushPanelForConversation({
|
pushPanelForConversation({
|
||||||
type: PanelType.MessageDetails,
|
type: PanelType.MessageDetails,
|
||||||
@@ -355,6 +383,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||||||
canCopy,
|
canCopy,
|
||||||
canEditMessage,
|
canEditMessage,
|
||||||
canForward,
|
canForward,
|
||||||
|
canPinMessages,
|
||||||
canRetry,
|
canRetry,
|
||||||
canSelect,
|
canSelect,
|
||||||
canEndPoll,
|
canEndPoll,
|
||||||
@@ -364,10 +393,12 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||||||
handleDownload,
|
handleDownload,
|
||||||
handleReact,
|
handleReact,
|
||||||
handleOpenPinMessageDialog,
|
handleOpenPinMessageDialog,
|
||||||
|
handleUnpinMessage,
|
||||||
endPoll,
|
endPoll,
|
||||||
handleReplyToMessage,
|
handleReplyToMessage,
|
||||||
i18n,
|
i18n,
|
||||||
id,
|
id,
|
||||||
|
isPinned,
|
||||||
pushPanelForConversation,
|
pushPanelForConversation,
|
||||||
retryDeleteForEveryone,
|
retryDeleteForEveryone,
|
||||||
retryMessageSend,
|
retryMessageSend,
|
||||||
@@ -461,10 +492,7 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||||||
messageId={id}
|
messageId={id}
|
||||||
open={pinMessageDialogOpen}
|
open={pinMessageDialogOpen}
|
||||||
onOpenChange={setPinMessageDialogOpen}
|
onOpenChange={setPinMessageDialogOpen}
|
||||||
onPinMessage={() => {
|
onPinnedMessageAdd={handlePinnedMessageAdd}
|
||||||
// TODO
|
|
||||||
setPinMessageDialogOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function Default(): JSX.Element {
|
|||||||
open={open}
|
open={open}
|
||||||
onOpenChange={setOpen}
|
onOpenChange={setOpen}
|
||||||
messageId="42"
|
messageId="42"
|
||||||
onPinMessage={action('onPinMessage')}
|
onPinnedMessageAdd={action('onPinnedMessageAdd')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,21 +7,18 @@ import { AxoRadioGroup } from '../../../axo/AxoRadioGroup.dom.js';
|
|||||||
import { DurationInSeconds } from '../../../util/durations/duration-in-seconds.std.js';
|
import { DurationInSeconds } from '../../../util/durations/duration-in-seconds.std.js';
|
||||||
import { strictAssert } from '../../../util/assert.std.js';
|
import { strictAssert } from '../../../util/assert.std.js';
|
||||||
|
|
||||||
export enum DurationOption {
|
enum DurationOption {
|
||||||
TIME_24_HOURS = 'TIME_24_HOURS',
|
TIME_24_HOURS = 'TIME_24_HOURS',
|
||||||
TIME_7_DAYS = 'TIME_7_DAYS',
|
TIME_7_DAYS = 'TIME_7_DAYS',
|
||||||
TIME_30_DAYS = 'TIME_30_DAYS',
|
TIME_30_DAYS = 'TIME_30_DAYS',
|
||||||
FOREVER = 'FOREVER',
|
FOREVER = 'FOREVER',
|
||||||
}
|
}
|
||||||
export type DurationValue =
|
|
||||||
| { seconds: number; forever?: never }
|
|
||||||
| { seconds?: never; forever: true };
|
|
||||||
|
|
||||||
const DURATION_OPTIONS: Record<DurationOption, DurationValue> = {
|
const DURATION_OPTIONS: Record<DurationOption, DurationInSeconds | null> = {
|
||||||
[DurationOption.TIME_24_HOURS]: { seconds: DurationInSeconds.fromHours(24) },
|
[DurationOption.TIME_24_HOURS]: DurationInSeconds.fromHours(24),
|
||||||
[DurationOption.TIME_7_DAYS]: { seconds: DurationInSeconds.fromDays(7) },
|
[DurationOption.TIME_7_DAYS]: DurationInSeconds.fromDays(7),
|
||||||
[DurationOption.TIME_30_DAYS]: { seconds: DurationInSeconds.fromDays(30) },
|
[DurationOption.TIME_30_DAYS]: DurationInSeconds.fromDays(30),
|
||||||
[DurationOption.FOREVER]: { forever: true },
|
[DurationOption.FOREVER]: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function isValidDurationOption(value: string): value is DurationOption {
|
function isValidDurationOption(value: string): value is DurationOption {
|
||||||
@@ -33,13 +30,16 @@ export type PinMessageDialogProps = Readonly<{
|
|||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
onPinMessage: (messageId: string, duration: DurationValue) => void;
|
onPinnedMessageAdd: (
|
||||||
|
messageId: string,
|
||||||
|
duration: DurationInSeconds | null
|
||||||
|
) => void;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export const PinMessageDialog = memo(function PinMessageDialog(
|
export const PinMessageDialog = memo(function PinMessageDialog(
|
||||||
props: PinMessageDialogProps
|
props: PinMessageDialogProps
|
||||||
) {
|
) {
|
||||||
const { i18n, messageId, onPinMessage, onOpenChange } = props;
|
const { i18n, messageId, onPinnedMessageAdd, onOpenChange } = props;
|
||||||
const [duration, setDuration] = useState(DurationOption.TIME_7_DAYS);
|
const [duration, setDuration] = useState(DurationOption.TIME_7_DAYS);
|
||||||
|
|
||||||
const handleValueChange = useCallback((value: string) => {
|
const handleValueChange = useCallback((value: string) => {
|
||||||
@@ -51,14 +51,18 @@ export const PinMessageDialog = memo(function PinMessageDialog(
|
|||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
}, [onOpenChange]);
|
}, [onOpenChange]);
|
||||||
|
|
||||||
const handlePinMessage = useCallback(() => {
|
const handlePinnedMessageAdd = useCallback(() => {
|
||||||
const durationValue = DURATION_OPTIONS[duration];
|
const durationValue = DURATION_OPTIONS[duration];
|
||||||
onPinMessage(messageId, durationValue);
|
onPinnedMessageAdd(messageId, durationValue);
|
||||||
}, [duration, onPinMessage, messageId]);
|
}, [duration, onPinnedMessageAdd, messageId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AxoDialog.Root open={props.open} onOpenChange={onOpenChange}>
|
<AxoDialog.Root open={props.open} onOpenChange={onOpenChange}>
|
||||||
<AxoDialog.Content size="sm" escape="cancel-is-noop">
|
<AxoDialog.Content
|
||||||
|
size="sm"
|
||||||
|
escape="cancel-is-noop"
|
||||||
|
disableMissingAriaDescriptionWarning
|
||||||
|
>
|
||||||
<AxoDialog.Header>
|
<AxoDialog.Header>
|
||||||
<AxoDialog.Title>
|
<AxoDialog.Title>
|
||||||
{i18n('icu:PinMessageDialog__Title')}
|
{i18n('icu:PinMessageDialog__Title')}
|
||||||
@@ -101,7 +105,10 @@ export const PinMessageDialog = memo(function PinMessageDialog(
|
|||||||
<AxoDialog.Action variant="secondary" onClick={handleCancel}>
|
<AxoDialog.Action variant="secondary" onClick={handleCancel}>
|
||||||
{i18n('icu:PinMessageDialog__Cancel')}
|
{i18n('icu:PinMessageDialog__Cancel')}
|
||||||
</AxoDialog.Action>
|
</AxoDialog.Action>
|
||||||
<AxoDialog.Action variant="primary" onClick={handlePinMessage}>
|
<AxoDialog.Action
|
||||||
|
variant="primary"
|
||||||
|
onClick={handlePinnedMessageAdd}
|
||||||
|
>
|
||||||
{i18n('icu:PinMessageDialog__Pin')}
|
{i18n('icu:PinMessageDialog__Pin')}
|
||||||
</AxoDialog.Action>
|
</AxoDialog.Action>
|
||||||
</AxoDialog.Actions>
|
</AxoDialog.Actions>
|
||||||
|
|||||||
@@ -42,8 +42,11 @@ export class JobLogger implements LoggerType {
|
|||||||
this.logger.trace(this.#prefix(), ...args);
|
this.logger.trace(this.#prefix(), ...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
child(): LoggerType {
|
child(name: string): JobLogger {
|
||||||
throw new Error('Not supported');
|
return new JobLogger(
|
||||||
|
{ id: this.#id, queueType: this.#queueType },
|
||||||
|
this.logger.child(name)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#prefix(): string {
|
#prefix(): string {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
handleMultipleSendErrors,
|
handleMultipleSendErrors,
|
||||||
maybeExpandErrors,
|
maybeExpandErrors,
|
||||||
} from './handleMultipleSendErrors.std.js';
|
} from './handleMultipleSendErrors.std.js';
|
||||||
|
import { itemStorage } from '../../textsecure/Storage.preload.js';
|
||||||
|
|
||||||
export type SendMessageJobOptions<Data> = Readonly<{
|
export type SendMessageJobOptions<Data> = Readonly<{
|
||||||
sendName: string; // ex: 'sendExampleMessage'
|
sendName: string; // ex: 'sendExampleMessage'
|
||||||
@@ -49,15 +50,12 @@ export function createSendMessageJob<Data>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { recipientServiceIdsWithoutMe, untrustedServiceIds } =
|
||||||
allRecipientServiceIds,
|
getSendRecipientLists({
|
||||||
recipientServiceIdsWithoutMe,
|
log,
|
||||||
untrustedServiceIds,
|
conversation,
|
||||||
} = getSendRecipientLists({
|
conversationIds: Array.from(conversation.getMemberConversationIds()),
|
||||||
log,
|
});
|
||||||
conversation,
|
|
||||||
conversationIds: conversation.getRecipients(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (untrustedServiceIds.length > 0) {
|
if (untrustedServiceIds.length > 0) {
|
||||||
window.reduxActions.conversations.conversationStoppedByMissingVerification(
|
window.reduxActions.conversations.conversationStoppedByMissingVerification(
|
||||||
@@ -84,10 +82,11 @@ export function createSendMessageJob<Data>(
|
|||||||
await conversation.queueJob(
|
await conversation.queueJob(
|
||||||
`conversationQueue/${sendName}/sync`,
|
`conversationQueue/${sendName}/sync`,
|
||||||
async () => {
|
async () => {
|
||||||
|
const ourAci = itemStorage.user.getCheckedAci();
|
||||||
const encodedDataMessage = await job.messaging.getDataOrEditMessage(
|
const encodedDataMessage = await job.messaging.getDataOrEditMessage(
|
||||||
{
|
{
|
||||||
...messageOptions,
|
...messageOptions,
|
||||||
recipients: allRecipientServiceIds,
|
recipients: [ourAci],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { DataWriter } from '../sql/Client.preload.js';
|
import { DataWriter } from '../sql/Client.preload.js';
|
||||||
import type { AciString } from '../types/ServiceId.std.js';
|
import type { AciString } from '../types/ServiceId.std.js';
|
||||||
import { DurationInSeconds } from '../util/durations/duration-in-seconds.std.js';
|
import type { DurationInSeconds } from '../util/durations/duration-in-seconds.std.js';
|
||||||
import * as RemoteConfig from '../RemoteConfig.dom.js';
|
|
||||||
import { parseIntWithFallback } from '../util/parseIntWithFallback.std.js';
|
|
||||||
import { createLogger } from '../logging/log.std.js';
|
import { createLogger } from '../logging/log.std.js';
|
||||||
import type { MessageModifierTarget } from './helpers/findMessageModifierTarget.preload.js';
|
import type { MessageModifierTarget } from './helpers/findMessageModifierTarget.preload.js';
|
||||||
import { findMessageModifierTarget } from './helpers/findMessageModifierTarget.preload.js';
|
import { findMessageModifierTarget } from './helpers/findMessageModifierTarget.preload.js';
|
||||||
@@ -12,6 +10,8 @@ import { isValidSenderAciForConversation } from './helpers/isValidSenderAciForCo
|
|||||||
import { isGroupV2 } from '../util/whatTypeOfConversation.dom.js';
|
import { isGroupV2 } from '../util/whatTypeOfConversation.dom.js';
|
||||||
import { SignalService as Proto } from '../protobuf/index.std.js';
|
import { SignalService as Proto } from '../protobuf/index.std.js';
|
||||||
import type { ConversationModel } from '../models/conversations.preload.js';
|
import type { ConversationModel } from '../models/conversations.preload.js';
|
||||||
|
import { getPinnedMessagesLimit } from '../util/pinnedMessages.dom.js';
|
||||||
|
import { getPinnedMessageExpiresAt } from '../util/pinnedMessages.std.js';
|
||||||
|
|
||||||
const { AccessRequired } = Proto.AccessControl;
|
const { AccessRequired } = Proto.AccessControl;
|
||||||
const { Role } = Proto.Member;
|
const { Role } = Proto.Member;
|
||||||
@@ -92,6 +92,10 @@ export async function onPinnedMessageAdd(
|
|||||||
log.info(`Truncated older pinned message ${pinnedMessageId}`);
|
log.info(`Truncated older pinned message ${pinnedMessageId}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.reduxActions.conversations.onPinnedMessagesChanged(
|
||||||
|
targetConversation.id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function onPinnedMessageRemove(
|
export async function onPinnedMessageRemove(
|
||||||
@@ -120,6 +124,7 @@ export async function onPinnedMessageRemove(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const targetMessageId = target.targetMessage.id;
|
const targetMessageId = target.targetMessage.id;
|
||||||
|
const targetConversationId = target.targetConversation.id;
|
||||||
|
|
||||||
const deletedPinnedMessageId =
|
const deletedPinnedMessageId =
|
||||||
await DataWriter.deletePinnedMessageByMessageId(targetMessageId);
|
await DataWriter.deletePinnedMessageByMessageId(targetMessageId);
|
||||||
@@ -132,6 +137,10 @@ export async function onPinnedMessageRemove(
|
|||||||
log.info(
|
log.info(
|
||||||
`Deleted pinned message ${deletedPinnedMessageId} for messageId ${targetMessageId}`
|
`Deleted pinned message ${deletedPinnedMessageId} for messageId ${targetMessageId}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
window.reduxActions.conversations.onPinnedMessagesChanged(
|
||||||
|
targetConversationId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function canSenderEditGroupAttributes(
|
function canSenderEditGroupAttributes(
|
||||||
@@ -175,19 +184,3 @@ function validatePinnedMessageTarget(
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPinnedMessageExpiresAt(
|
|
||||||
receivedAtTimestamp: number,
|
|
||||||
pinDuration: DurationInSeconds | null
|
|
||||||
): number | null {
|
|
||||||
if (pinDuration == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const pinDurationMs = DurationInSeconds.toMillis(pinDuration);
|
|
||||||
return receivedAtTimestamp + pinDurationMs;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPinnedMessagesLimit(): number {
|
|
||||||
const remoteValue = RemoteConfig.getValue('global.pinned_message_limit');
|
|
||||||
return parseIntWithFallback(remoteValue, 3);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -274,6 +274,7 @@ const {
|
|||||||
getMostRecentAddressableMessages,
|
getMostRecentAddressableMessages,
|
||||||
getMostRecentAddressableNondisappearingMessages,
|
getMostRecentAddressableNondisappearingMessages,
|
||||||
getNewerMessagesByConversation,
|
getNewerMessagesByConversation,
|
||||||
|
getPinnedMessagesForConversation,
|
||||||
} = DataReader;
|
} = DataReader;
|
||||||
const { addStickerPackReference } = DataWriter;
|
const { addStickerPackReference } = DataWriter;
|
||||||
|
|
||||||
@@ -1688,10 +1689,13 @@ export class ConversationModel {
|
|||||||
`latest timestamp=${cleaned.at(-1)?.sent_at}`
|
`latest timestamp=${cleaned.at(-1)?.sent_at}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const pinnedMessages = await getPinnedMessagesForConversation(this.id);
|
||||||
|
|
||||||
addPreloadData({
|
addPreloadData({
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
messages: cleaned,
|
messages: cleaned,
|
||||||
metrics,
|
metrics,
|
||||||
|
pinnedMessages,
|
||||||
unboundedFetch,
|
unboundedFetch,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1803,6 +1807,9 @@ export class ConversationModel {
|
|||||||
`latest timestamp=${cleaned.at(-1)?.sent_at}`
|
`latest timestamp=${cleaned.at(-1)?.sent_at}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const pinnedMessages =
|
||||||
|
await DataReader.getPinnedMessagesForConversation(conversationId);
|
||||||
|
|
||||||
// Because our `getOlderMessages` fetch above didn't specify a receivedAt, we got
|
// 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
|
// the most recent N messages in the conversation. If it has a conflict with
|
||||||
// metrics, fetched a bit before, that's likely a race condition. So we tell our
|
// metrics, fetched a bit before, that's likely a race condition. So we tell our
|
||||||
@@ -1813,6 +1820,7 @@ export class ConversationModel {
|
|||||||
conversationId,
|
conversationId,
|
||||||
messages: cleaned,
|
messages: cleaned,
|
||||||
metrics,
|
metrics,
|
||||||
|
pinnedMessages,
|
||||||
scrollToMessageId,
|
scrollToMessageId,
|
||||||
unboundedFetch,
|
unboundedFetch,
|
||||||
});
|
});
|
||||||
@@ -1977,10 +1985,14 @@ export class ConversationModel {
|
|||||||
const scrollToMessageId =
|
const scrollToMessageId =
|
||||||
options && options.disableScroll ? undefined : messageId;
|
options && options.disableScroll ? undefined : messageId;
|
||||||
|
|
||||||
|
const pinnedMessages =
|
||||||
|
await DataReader.getPinnedMessagesForConversation(conversationId);
|
||||||
|
|
||||||
messagesReset({
|
messagesReset({
|
||||||
conversationId,
|
conversationId,
|
||||||
messages: cleaned,
|
messages: cleaned,
|
||||||
metrics,
|
metrics,
|
||||||
|
pinnedMessages,
|
||||||
scrollToMessageId,
|
scrollToMessageId,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import lodash from 'lodash';
|
|||||||
import { type PhoneNumber } from 'google-libphonenumber';
|
import { type PhoneNumber } from 'google-libphonenumber';
|
||||||
|
|
||||||
import { clipboard, ipcRenderer } from 'electron';
|
import { clipboard, ipcRenderer } from 'electron';
|
||||||
import type { ReadonlyDeep } from 'type-fest';
|
import type { ReadonlyDeep, SetOptional } from 'type-fest';
|
||||||
import { DataReader, DataWriter } from '../../sql/Client.preload.js';
|
import { DataReader, DataWriter } from '../../sql/Client.preload.js';
|
||||||
import type { AttachmentType } from '../../types/Attachment.std.js';
|
import type { AttachmentType } from '../../types/Attachment.std.js';
|
||||||
import type { StateType as RootStateType } from '../reducer.preload.js';
|
import type { StateType as RootStateType } from '../reducer.preload.js';
|
||||||
@@ -96,6 +96,7 @@ import {
|
|||||||
getPendingAvatarDownloadSelector,
|
getPendingAvatarDownloadSelector,
|
||||||
getAllConversations,
|
getAllConversations,
|
||||||
getActivePanel,
|
getActivePanel,
|
||||||
|
getSelectedConversationId,
|
||||||
} from '../selectors/conversations.dom.js';
|
} from '../selectors/conversations.dom.js';
|
||||||
import { getIntl } from '../selectors/user.std.js';
|
import { getIntl } from '../selectors/user.std.js';
|
||||||
import type {
|
import type {
|
||||||
@@ -243,6 +244,10 @@ import { CurrentChatFolders } from '../../types/CurrentChatFolders.std.js';
|
|||||||
import { itemStorage } from '../../textsecure/Storage.preload.js';
|
import { itemStorage } from '../../textsecure/Storage.preload.js';
|
||||||
import { enqueuePollVoteForSend as enqueuePollVoteForSendHelper } from '../../polls/enqueuePollVoteForSend.preload.js';
|
import { enqueuePollVoteForSend as enqueuePollVoteForSendHelper } from '../../polls/enqueuePollVoteForSend.preload.js';
|
||||||
import { updateChatFolderStateOnTargetConversationChanged } from './chatFolders.preload.js';
|
import { updateChatFolderStateOnTargetConversationChanged } from './chatFolders.preload.js';
|
||||||
|
import type { PinnedMessage } 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';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
chunk,
|
chunk,
|
||||||
@@ -503,10 +508,22 @@ export type ConversationMessageType = ReadonlyDeep<{
|
|||||||
export type ConversationPreloadDataType = ReadonlyDeep<{
|
export type ConversationPreloadDataType = ReadonlyDeep<{
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
messages: ReadonlyArray<ReadonlyMessageAttributesType>;
|
messages: ReadonlyArray<ReadonlyMessageAttributesType>;
|
||||||
|
pinnedMessages: ReadonlyArray<PinnedMessage>;
|
||||||
metrics: MessageMetricsType;
|
metrics: MessageMetricsType;
|
||||||
unboundedFetch: boolean;
|
unboundedFetch: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type MessagesResetDataType = ReadonlyDeep<
|
||||||
|
ConversationPreloadDataType & {
|
||||||
|
scrollToMessageId?: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type MessagesResetOptionsType = SetOptional<
|
||||||
|
MessagesResetDataType,
|
||||||
|
'unboundedFetch'
|
||||||
|
>;
|
||||||
|
|
||||||
export type MessagesByConversationType = ReadonlyDeep<{
|
export type MessagesByConversationType = ReadonlyDeep<{
|
||||||
[key: string]: ConversationMessageType | undefined;
|
[key: string]: ConversationMessageType | undefined;
|
||||||
}>;
|
}>;
|
||||||
@@ -625,6 +642,7 @@ export type ConversationsStateType = ReadonlyDeep<{
|
|||||||
// Note: it's very important that both of these locations are always kept up to date
|
// Note: it's very important that both of these locations are always kept up to date
|
||||||
messagesLookup: MessageLookupType;
|
messagesLookup: MessageLookupType;
|
||||||
messagesByConversation: MessagesByConversationType;
|
messagesByConversation: MessagesByConversationType;
|
||||||
|
pinnedMessages: ReadonlyArray<PinnedMessage>;
|
||||||
|
|
||||||
lastCenterMessageByConversation: LastCenterMessageByConversationType;
|
lastCenterMessageByConversation: LastCenterMessageByConversationType;
|
||||||
|
|
||||||
@@ -698,6 +716,7 @@ export const SET_PENDING_REQUESTED_AVATAR_DOWNLOAD =
|
|||||||
'conversations/SET_PENDING_REQUESTED_AVATAR_DOWNLOAD';
|
'conversations/SET_PENDING_REQUESTED_AVATAR_DOWNLOAD';
|
||||||
export const SET_PROFILE_UPDATE_ERROR =
|
export const SET_PROFILE_UPDATE_ERROR =
|
||||||
'conversations/SET_PROFILE_UPDATE_ERROR';
|
'conversations/SET_PROFILE_UPDATE_ERROR';
|
||||||
|
const REPLACE_PINNED_MESSAGES = 'conversations/REPLACE_PINNED_MESSAGES';
|
||||||
|
|
||||||
export type CancelVerificationDataByConversationActionType = ReadonlyDeep<{
|
export type CancelVerificationDataByConversationActionType = ReadonlyDeep<{
|
||||||
type: typeof CANCEL_CONVERSATION_PENDING_VERIFICATION;
|
type: typeof CANCEL_CONVERSATION_PENDING_VERIFICATION;
|
||||||
@@ -934,15 +953,7 @@ export type RepairOldestMessageActionType = ReadonlyDeep<{
|
|||||||
}>;
|
}>;
|
||||||
export type MessagesResetActionType = ReadonlyDeep<{
|
export type MessagesResetActionType = ReadonlyDeep<{
|
||||||
type: 'MESSAGES_RESET';
|
type: 'MESSAGES_RESET';
|
||||||
payload: {
|
payload: MessagesResetDataType;
|
||||||
conversationId: string;
|
|
||||||
messages: ReadonlyArray<ReadonlyMessageAttributesType>;
|
|
||||||
metrics: MessageMetricsType;
|
|
||||||
scrollToMessageId?: string;
|
|
||||||
// The set of provided messages should be trusted, even if it conflicts with metrics,
|
|
||||||
// because we weren't looking for a specific time window of messages with our query.
|
|
||||||
unboundedFetch: boolean;
|
|
||||||
};
|
|
||||||
}>;
|
}>;
|
||||||
export type SetMessageLoadingStateActionType = ReadonlyDeep<{
|
export type SetMessageLoadingStateActionType = ReadonlyDeep<{
|
||||||
type: 'SET_MESSAGE_LOADING_STATE';
|
type: 'SET_MESSAGE_LOADING_STATE';
|
||||||
@@ -1086,6 +1097,13 @@ type ReplaceAvatarsActionType = ReadonlyDeep<{
|
|||||||
avatars: ReadonlyArray<AvatarDataType>;
|
avatars: ReadonlyArray<AvatarDataType>;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
type ReplacePinnedMessagesActionType = ReadonlyDeep<{
|
||||||
|
type: typeof REPLACE_PINNED_MESSAGES;
|
||||||
|
payload: {
|
||||||
|
conversationId: string;
|
||||||
|
pinnedMessages: ReadonlyArray<PinnedMessage>;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
export type AddPreloadDataActionType = ReadonlyDeep<{
|
export type AddPreloadDataActionType = ReadonlyDeep<{
|
||||||
type: 'ADD_PRELOAD_DATA';
|
type: 'ADD_PRELOAD_DATA';
|
||||||
payload: ConversationPreloadDataType;
|
payload: ConversationPreloadDataType;
|
||||||
@@ -1141,6 +1159,7 @@ export type ConversationActionType =
|
|||||||
| RepairNewestMessageActionType
|
| RepairNewestMessageActionType
|
||||||
| RepairOldestMessageActionType
|
| RepairOldestMessageActionType
|
||||||
| ReplaceAvatarsActionType
|
| ReplaceAvatarsActionType
|
||||||
|
| ReplacePinnedMessagesActionType
|
||||||
| ReviewConversationNameCollisionActionType
|
| ReviewConversationNameCollisionActionType
|
||||||
| ScrollToMessageActionType
|
| ScrollToMessageActionType
|
||||||
| SetPendingRequestedAvatarDownloadActionType
|
| SetPendingRequestedAvatarDownloadActionType
|
||||||
@@ -1236,6 +1255,9 @@ export const actions = {
|
|||||||
onArchive,
|
onArchive,
|
||||||
onMarkUnread,
|
onMarkUnread,
|
||||||
onMoveToInbox,
|
onMoveToInbox,
|
||||||
|
onPinnedMessagesChanged,
|
||||||
|
onPinnedMessageAdd,
|
||||||
|
onPinnedMessageRemove,
|
||||||
onUndoArchive,
|
onUndoArchive,
|
||||||
openGiftBadge,
|
openGiftBadge,
|
||||||
popPanelForConversation,
|
popPanelForConversation,
|
||||||
@@ -1329,7 +1351,7 @@ function onArchive(
|
|||||||
unknown,
|
unknown,
|
||||||
ConversationUnloadedActionType | ShowToastActionType
|
ConversationUnloadedActionType | ShowToastActionType
|
||||||
> {
|
> {
|
||||||
return (dispatch, getState) => {
|
return dispatch => {
|
||||||
const conversation = window.ConversationController.get(conversationId);
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
throw new Error('onArchive: Conversation not found!');
|
throw new Error('onArchive: Conversation not found!');
|
||||||
@@ -1338,11 +1360,7 @@ function onArchive(
|
|||||||
const wasPinned = conversation.attributes.isPinned ?? false;
|
const wasPinned = conversation.attributes.isPinned ?? false;
|
||||||
conversation.setArchived(true);
|
conversation.setArchived(true);
|
||||||
|
|
||||||
onConversationClosed(conversationId, 'archive')(
|
dispatch(onConversationClosed(conversationId, 'archive'));
|
||||||
dispatch,
|
|
||||||
getState,
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SHOW_TOAST,
|
type: SHOW_TOAST,
|
||||||
@@ -1365,7 +1383,7 @@ function onUndoArchive(
|
|||||||
unknown,
|
unknown,
|
||||||
TargetedConversationChangedActionType
|
TargetedConversationChangedActionType
|
||||||
> {
|
> {
|
||||||
return (dispatch, getState) => {
|
return dispatch => {
|
||||||
const conversation = window.ConversationController.get(conversationId);
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
throw new Error('onUndoArchive: Conversation not found!');
|
throw new Error('onUndoArchive: Conversation not found!');
|
||||||
@@ -1375,9 +1393,11 @@ function onUndoArchive(
|
|||||||
if (options.wasPinned) {
|
if (options.wasPinned) {
|
||||||
conversation.pin();
|
conversation.pin();
|
||||||
}
|
}
|
||||||
showConversation({
|
dispatch(
|
||||||
conversationId,
|
showConversation({
|
||||||
})(dispatch, getState, null);
|
conversationId,
|
||||||
|
})
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1915,7 +1935,7 @@ function deleteMessages({
|
|||||||
messageIds: ReadonlyArray<string>;
|
messageIds: ReadonlyArray<string>;
|
||||||
lastSelectedMessage?: MessageTimestamps;
|
lastSelectedMessage?: MessageTimestamps;
|
||||||
}): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
}): ThunkAction<void, RootStateType, unknown, NoopActionType> {
|
||||||
return async (dispatch, getState) => {
|
return async dispatch => {
|
||||||
if (!messageIds || messageIds.length === 0) {
|
if (!messageIds || messageIds.length === 0) {
|
||||||
log.warn('deleteMessages: No message ids provided');
|
log.warn('deleteMessages: No message ids provided');
|
||||||
return;
|
return;
|
||||||
@@ -1968,7 +1988,7 @@ function deleteMessages({
|
|||||||
cleanupMessages,
|
cleanupMessages,
|
||||||
});
|
});
|
||||||
|
|
||||||
popPanelForConversation()(dispatch, getState, undefined);
|
dispatch(popPanelForConversation());
|
||||||
|
|
||||||
if (nearbyMessageId != null) {
|
if (nearbyMessageId != null) {
|
||||||
dispatch(scrollToMessage(conversationId, nearbyMessageId));
|
dispatch(scrollToMessage(conversationId, nearbyMessageId));
|
||||||
@@ -2019,11 +2039,7 @@ function destroyMessages(
|
|||||||
name: 'destroymessages',
|
name: 'destroymessages',
|
||||||
idForLogging: conversation.idForLogging(),
|
idForLogging: conversation.idForLogging(),
|
||||||
task: async () => {
|
task: async () => {
|
||||||
onConversationClosed(conversationId, 'delete messages')(
|
dispatch(onConversationClosed(conversationId, 'delete messages'));
|
||||||
dispatch,
|
|
||||||
getState,
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
await conversation.destroyMessages({ source: 'local-delete' });
|
await conversation.destroyMessages({ source: 'local-delete' });
|
||||||
|
|
||||||
@@ -2110,11 +2126,7 @@ function setMessageToEdit(
|
|||||||
dispatch(scrollToMessage(conversationId, messageId));
|
dispatch(scrollToMessage(conversationId, messageId));
|
||||||
}
|
}
|
||||||
|
|
||||||
setQuoteByMessageId(conversationId, undefined)(
|
dispatch(setQuoteByMessageId(conversationId, undefined));
|
||||||
dispatch,
|
|
||||||
getState,
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
let attachmentThumbnail: string | undefined;
|
let attachmentThumbnail: string | undefined;
|
||||||
if (message.attachments) {
|
if (message.attachments) {
|
||||||
@@ -3379,21 +3391,14 @@ function reviewConversationNameCollision(): ReviewConversationNameCollisionActio
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessageResetOptionsType = ReadonlyDeep<{
|
|
||||||
conversationId: string;
|
|
||||||
messages: ReadonlyArray<ReadonlyMessageAttributesType>;
|
|
||||||
metrics: MessageMetricsType;
|
|
||||||
scrollToMessageId?: string;
|
|
||||||
unboundedFetch?: boolean;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
function messagesReset({
|
function messagesReset({
|
||||||
conversationId,
|
conversationId,
|
||||||
messages,
|
messages,
|
||||||
metrics,
|
metrics,
|
||||||
|
pinnedMessages,
|
||||||
scrollToMessageId,
|
scrollToMessageId,
|
||||||
unboundedFetch,
|
unboundedFetch,
|
||||||
}: MessageResetOptionsType): MessagesResetActionType {
|
}: MessagesResetOptionsType): MessagesResetActionType {
|
||||||
for (const message of messages) {
|
for (const message of messages) {
|
||||||
strictAssert(
|
strictAssert(
|
||||||
message.conversationId === conversationId,
|
message.conversationId === conversationId,
|
||||||
@@ -3405,10 +3410,11 @@ function messagesReset({
|
|||||||
return {
|
return {
|
||||||
type: 'MESSAGES_RESET',
|
type: 'MESSAGES_RESET',
|
||||||
payload: {
|
payload: {
|
||||||
unboundedFetch: Boolean(unboundedFetch),
|
unboundedFetch: unboundedFetch ?? false,
|
||||||
conversationId,
|
conversationId,
|
||||||
messages,
|
messages,
|
||||||
metrics,
|
metrics,
|
||||||
|
pinnedMessages,
|
||||||
scrollToMessageId,
|
scrollToMessageId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -4336,7 +4342,7 @@ export function saveAttachmentFromMessage(
|
|||||||
messageId: string,
|
messageId: string,
|
||||||
providedAttachment?: AttachmentType
|
providedAttachment?: AttachmentType
|
||||||
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
|
): ThunkAction<void, RootStateType, unknown, ShowToastActionType> {
|
||||||
return async (dispatch, getState) => {
|
return async dispatch => {
|
||||||
const message = await getMessageById(messageId);
|
const message = await getMessageById(messageId);
|
||||||
if (!message) {
|
if (!message) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -4354,7 +4360,7 @@ export function saveAttachmentFromMessage(
|
|||||||
? providedAttachment
|
? providedAttachment
|
||||||
: attachments[0];
|
: attachments[0];
|
||||||
|
|
||||||
saveAttachment(attachment, timestamp)(dispatch, getState, null);
|
dispatch(saveAttachment(attachment, timestamp));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4840,7 +4846,7 @@ function showConversation({
|
|||||||
|
|
||||||
// notify composer in case we need to stop recording a voice note
|
// notify composer in case we need to stop recording a voice note
|
||||||
if (conversations.selectedConversationId) {
|
if (conversations.selectedConversationId) {
|
||||||
saveDraftRecordingIfNeeded()(dispatch, getState, undefined);
|
dispatch(saveDraftRecordingIfNeeded());
|
||||||
dispatch(
|
dispatch(
|
||||||
onConversationClosed(
|
onConversationClosed(
|
||||||
conversations.selectedConversationId,
|
conversations.selectedConversationId,
|
||||||
@@ -4874,7 +4880,7 @@ function onConversationOpened(
|
|||||||
| SetFocusActionType
|
| SetFocusActionType
|
||||||
| SetQuotedMessageActionType
|
| SetQuotedMessageActionType
|
||||||
> {
|
> {
|
||||||
return async (dispatch, getState) => {
|
return async dispatch => {
|
||||||
const promises: Array<Promise<void>> = [];
|
const promises: Array<Promise<void>> = [];
|
||||||
const conversation = window.ConversationController.get(conversationId);
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
if (!conversation) {
|
if (!conversation) {
|
||||||
@@ -4926,11 +4932,7 @@ function onConversationOpened(
|
|||||||
|
|
||||||
const quotedMessageId = conversation.get('quotedMessageId');
|
const quotedMessageId = conversation.get('quotedMessageId');
|
||||||
if (quotedMessageId) {
|
if (quotedMessageId) {
|
||||||
setQuoteByMessageId(conversation.id, quotedMessageId)(
|
dispatch(setQuoteByMessageId(conversation.id, quotedMessageId));
|
||||||
dispatch,
|
|
||||||
getState,
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
promises.push(conversation.fetchLatestGroupV2Data());
|
promises.push(conversation.fetchLatestGroupV2Data());
|
||||||
@@ -4961,10 +4963,12 @@ function onConversationOpened(
|
|||||||
|
|
||||||
promises.push(conversation.updateVerified());
|
promises.push(conversation.updateVerified());
|
||||||
|
|
||||||
replaceAttachments(
|
dispatch(
|
||||||
conversation.get('id'),
|
replaceAttachments(
|
||||||
conversation.get('draftAttachments') || []
|
conversation.get('id'),
|
||||||
)(dispatch, getState, undefined);
|
conversation.get('draftAttachments') || []
|
||||||
|
)
|
||||||
|
);
|
||||||
dispatch(resetComposer(conversationId));
|
dispatch(resetComposer(conversationId));
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
@@ -5113,6 +5117,107 @@ 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<ReplacePinnedMessagesActionType> {
|
||||||
|
return async (dispatch, getState) => {
|
||||||
|
const selectedConversationId = getSelectedConversationId(getState());
|
||||||
|
if (
|
||||||
|
selectedConversationId == null ||
|
||||||
|
selectedConversationId !== conversationId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinnedMessages =
|
||||||
|
await DataReader.getPinnedMessagesForConversation(conversationId);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: REPLACE_PINNED_MESSAGES,
|
||||||
|
payload: {
|
||||||
|
conversationId,
|
||||||
|
pinnedMessages,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPinnedMessageAdd(
|
||||||
|
targetMessageId: string,
|
||||||
|
pinDurationSeconds: DurationInSeconds | null
|
||||||
|
): StateThunk {
|
||||||
|
return async dispatch => {
|
||||||
|
const target = await getPinnedMessageTarget(targetMessageId);
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
dispatch(onPinnedMessagesChanged(target.conversationId));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Reducer
|
// Reducer
|
||||||
|
|
||||||
export function getEmptyState(): ConversationsStateType {
|
export function getEmptyState(): ConversationsStateType {
|
||||||
@@ -5126,6 +5231,7 @@ export function getEmptyState(): ConversationsStateType {
|
|||||||
lastCenterMessageByConversation: {},
|
lastCenterMessageByConversation: {},
|
||||||
messagesByConversation: {},
|
messagesByConversation: {},
|
||||||
messagesLookup: {},
|
messagesLookup: {},
|
||||||
|
pinnedMessages: [],
|
||||||
targetedMessage: undefined,
|
targetedMessage: undefined,
|
||||||
targetedMessageCounter: 0,
|
targetedMessageCounter: 0,
|
||||||
targetedMessageSource: undefined,
|
targetedMessageSource: undefined,
|
||||||
@@ -5532,15 +5638,10 @@ function updateMessageLookup(
|
|||||||
conversationId,
|
conversationId,
|
||||||
messages,
|
messages,
|
||||||
metrics,
|
metrics,
|
||||||
|
pinnedMessages,
|
||||||
scrollToMessageId,
|
scrollToMessageId,
|
||||||
unboundedFetch,
|
unboundedFetch,
|
||||||
}: {
|
}: MessagesResetDataType
|
||||||
conversationId: string;
|
|
||||||
messages: ReadonlyArray<ReadonlyMessageAttributesType>;
|
|
||||||
metrics: MessageMetricsType;
|
|
||||||
scrollToMessageId?: string | undefined;
|
|
||||||
unboundedFetch: boolean;
|
|
||||||
}
|
|
||||||
): ConversationsStateType {
|
): ConversationsStateType {
|
||||||
const { messagesByConversation, messagesLookup } = state;
|
const { messagesByConversation, messagesLookup } = state;
|
||||||
const existingConversation = messagesByConversation[conversationId];
|
const existingConversation = messagesByConversation[conversationId];
|
||||||
@@ -5580,6 +5681,7 @@ function updateMessageLookup(
|
|||||||
targetedMessage: scrollToMessageId,
|
targetedMessage: scrollToMessageId,
|
||||||
targetedMessageCounter: state.targetedMessageCounter + 1,
|
targetedMessageCounter: state.targetedMessageCounter + 1,
|
||||||
targetedMessageSource: TargetedMessageSource.Reset,
|
targetedMessageSource: TargetedMessageSource.Reset,
|
||||||
|
pinnedMessages,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
messagesLookup: {
|
messagesLookup: {
|
||||||
@@ -5897,6 +5999,7 @@ export function reducer(
|
|||||||
messagesByConversation: omit(state.messagesByConversation, [
|
messagesByConversation: omit(state.messagesByConversation, [
|
||||||
conversationId,
|
conversationId,
|
||||||
]),
|
]),
|
||||||
|
pinnedMessages: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (action.type === 'CONVERSATIONS_REMOVE_ALL') {
|
if (action.type === 'CONVERSATIONS_REMOVE_ALL') {
|
||||||
@@ -7490,6 +7593,21 @@ export function reducer(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === REPLACE_PINNED_MESSAGES) {
|
||||||
|
// Discard new pinned messages if the `selectedConversationId` has changed.
|
||||||
|
if (
|
||||||
|
state.selectedConversationId == null ||
|
||||||
|
action.payload.conversationId !== state.selectedConversationId
|
||||||
|
) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
pinnedMessages: action.payload.pinnedMessages,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
action.type === CHANGE_LOCATION &&
|
action.type === CHANGE_LOCATION &&
|
||||||
action.payload.selectedLocation.tab === NavTab.Chats
|
action.payload.selectedLocation.tab === NavTab.Chats
|
||||||
|
|||||||
@@ -231,6 +231,14 @@ export const getTargetedMessageSource = createSelector(
|
|||||||
return state.targetedMessageSource;
|
return state.targetedMessageSource;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
export const getPinnedMessageIds = createSelector(
|
||||||
|
getConversations,
|
||||||
|
(state: ConversationsStateType): ReadonlyArray<string> | null => {
|
||||||
|
return state.pinnedMessages.map(pinnedMessage => {
|
||||||
|
return pinnedMessage.messageId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
export const getSelectedMessageIds = createSelector(
|
export const getSelectedMessageIds = createSelector(
|
||||||
getConversations,
|
getConversations,
|
||||||
(state: ConversationsStateType): ReadonlyArray<string> | undefined => {
|
(state: ConversationsStateType): ReadonlyArray<string> | undefined => {
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ import {
|
|||||||
getMessages,
|
getMessages,
|
||||||
getCachedConversationMemberColorsSelector,
|
getCachedConversationMemberColorsSelector,
|
||||||
getContactNameColor,
|
getContactNameColor,
|
||||||
|
getPinnedMessageIds,
|
||||||
} from './conversations.dom.js';
|
} from './conversations.dom.js';
|
||||||
import {
|
import {
|
||||||
getIntl,
|
getIntl,
|
||||||
@@ -167,6 +168,7 @@ import { getCallIdFromEra } from '../../util/callDisposition.preload.js';
|
|||||||
import { LONG_MESSAGE } from '../../types/MIME.std.js';
|
import { LONG_MESSAGE } from '../../types/MIME.std.js';
|
||||||
import type { MessageRequestResponseNotificationData } from '../../components/conversation/MessageRequestResponseNotification.dom.js';
|
import type { MessageRequestResponseNotificationData } from '../../components/conversation/MessageRequestResponseNotification.dom.js';
|
||||||
import type { PinnedMessageNotificationData } from '../../components/conversation/pinned-messages/PinnedMessageNotification.dom.js';
|
import type { PinnedMessageNotificationData } from '../../components/conversation/pinned-messages/PinnedMessageNotification.dom.js';
|
||||||
|
import { canEditGroupInfo } from '../../util/canEditGroupInfo.preload.js';
|
||||||
|
|
||||||
const { groupBy, isEmpty, isNumber, isObject, map } = lodash;
|
const { groupBy, isEmpty, isNumber, isObject, map } = lodash;
|
||||||
|
|
||||||
@@ -204,6 +206,7 @@ export type GetPropsForBubbleOptions = Readonly<{
|
|||||||
ourPni: PniString | undefined;
|
ourPni: PniString | undefined;
|
||||||
targetedMessageId?: string;
|
targetedMessageId?: string;
|
||||||
targetedMessageCounter?: number;
|
targetedMessageCounter?: number;
|
||||||
|
pinnedMessageIds: ReadonlyArray<string> | null;
|
||||||
selectedMessageIds: ReadonlyArray<string> | undefined;
|
selectedMessageIds: ReadonlyArray<string> | undefined;
|
||||||
regionCode?: string;
|
regionCode?: string;
|
||||||
callSelector: CallSelectorType;
|
callSelector: CallSelectorType;
|
||||||
@@ -775,6 +778,7 @@ export type GetPropsForMessageOptions = Pick<
|
|||||||
| 'ourNumber'
|
| 'ourNumber'
|
||||||
| 'targetedMessageId'
|
| 'targetedMessageId'
|
||||||
| 'targetedMessageCounter'
|
| 'targetedMessageCounter'
|
||||||
|
| 'pinnedMessageIds'
|
||||||
| 'selectedMessageIds'
|
| 'selectedMessageIds'
|
||||||
| 'regionCode'
|
| 'regionCode'
|
||||||
| 'accountSelector'
|
| 'accountSelector'
|
||||||
@@ -877,6 +881,7 @@ export const getPropsForMessage = (
|
|||||||
regionCode,
|
regionCode,
|
||||||
targetedMessageId,
|
targetedMessageId,
|
||||||
targetedMessageCounter,
|
targetedMessageCounter,
|
||||||
|
pinnedMessageIds,
|
||||||
selectedMessageIds,
|
selectedMessageIds,
|
||||||
contactNameColors,
|
contactNameColors,
|
||||||
defaultConversationColor,
|
defaultConversationColor,
|
||||||
@@ -895,6 +900,7 @@ export const getPropsForMessage = (
|
|||||||
const activeCallConversationId = activeCall?.conversationId;
|
const activeCallConversationId = activeCall?.conversationId;
|
||||||
|
|
||||||
const isTargeted = message.id === targetedMessageId;
|
const isTargeted = message.id === targetedMessageId;
|
||||||
|
const isPinned = pinnedMessageIds?.includes(message.id) ?? false;
|
||||||
const isSelected = selectedMessageIds?.includes(message.id) ?? false;
|
const isSelected = selectedMessageIds?.includes(message.id) ?? false;
|
||||||
const isSelectMode = selectedMessageIds != null;
|
const isSelectMode = selectedMessageIds != null;
|
||||||
|
|
||||||
@@ -938,6 +944,7 @@ export const getPropsForMessage = (
|
|||||||
canDownload: canDownload(message, conversationSelector),
|
canDownload: canDownload(message, conversationSelector),
|
||||||
canEndPoll: canEndPoll(message),
|
canEndPoll: canEndPoll(message),
|
||||||
canForward: canForward(message),
|
canForward: canForward(message),
|
||||||
|
canPinMessages: canPinMessages(conversation),
|
||||||
canReact: canReact(message, ourConversationId, conversationSelector),
|
canReact: canReact(message, ourConversationId, conversationSelector),
|
||||||
canReply: canReply(message, ourConversationId, conversationSelector),
|
canReply: canReply(message, ourConversationId, conversationSelector),
|
||||||
canRetry: hasErrors(message),
|
canRetry: hasErrors(message),
|
||||||
@@ -966,6 +973,7 @@ export const getPropsForMessage = (
|
|||||||
isBlocked: conversation.isBlocked || false,
|
isBlocked: conversation.isBlocked || false,
|
||||||
isEditedMessage: Boolean(message.editHistory),
|
isEditedMessage: Boolean(message.editHistory),
|
||||||
isMessageRequestAccepted: conversation?.acceptedMessageRequest ?? true,
|
isMessageRequestAccepted: conversation?.acceptedMessageRequest ?? true,
|
||||||
|
isPinned,
|
||||||
isSelected,
|
isSelected,
|
||||||
isSelectMode,
|
isSelectMode,
|
||||||
isSMS: message.sms === true,
|
isSMS: message.sms === true,
|
||||||
@@ -1003,6 +1011,7 @@ export const getMessagePropsSelector = createSelector(
|
|||||||
getAccountSelector,
|
getAccountSelector,
|
||||||
getCachedConversationMemberColorsSelector,
|
getCachedConversationMemberColorsSelector,
|
||||||
getTargetedMessage,
|
getTargetedMessage,
|
||||||
|
getPinnedMessageIds,
|
||||||
getSelectedMessageIds,
|
getSelectedMessageIds,
|
||||||
getDefaultConversationColor,
|
getDefaultConversationColor,
|
||||||
(
|
(
|
||||||
@@ -1015,6 +1024,7 @@ export const getMessagePropsSelector = createSelector(
|
|||||||
accountSelector,
|
accountSelector,
|
||||||
cachedConversationMemberColorsSelector,
|
cachedConversationMemberColorsSelector,
|
||||||
targetedMessage,
|
targetedMessage,
|
||||||
|
pinnedMessageIds,
|
||||||
selectedMessageIds,
|
selectedMessageIds,
|
||||||
defaultConversationColor
|
defaultConversationColor
|
||||||
) =>
|
) =>
|
||||||
@@ -1033,6 +1043,7 @@ export const getMessagePropsSelector = createSelector(
|
|||||||
regionCode,
|
regionCode,
|
||||||
targetedMessageCounter: targetedMessage?.counter,
|
targetedMessageCounter: targetedMessage?.counter,
|
||||||
targetedMessageId: targetedMessage?.id,
|
targetedMessageId: targetedMessage?.id,
|
||||||
|
pinnedMessageIds,
|
||||||
selectedMessageIds,
|
selectedMessageIds,
|
||||||
defaultConversationColor,
|
defaultConversationColor,
|
||||||
});
|
});
|
||||||
@@ -2391,6 +2402,10 @@ export function canForward(message: ReadonlyMessageAttributesType): boolean {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function canPinMessages(conversation: ConversationType): boolean {
|
||||||
|
return canEditGroupInfo(conversation);
|
||||||
|
}
|
||||||
|
|
||||||
export function getLastChallengeError(
|
export function getLastChallengeError(
|
||||||
message: Pick<MessageWithUIFieldsType, 'errors'>
|
message: Pick<MessageWithUIFieldsType, 'errors'>
|
||||||
): ShallowChallengeError | undefined {
|
): ShallowChallengeError | undefined {
|
||||||
@@ -2430,6 +2445,7 @@ export const getMessageDetails = createSelector(
|
|||||||
getUserPNI,
|
getUserPNI,
|
||||||
getUserConversationId,
|
getUserConversationId,
|
||||||
getUserNumber,
|
getUserNumber,
|
||||||
|
getPinnedMessageIds,
|
||||||
getSelectedMessageIds,
|
getSelectedMessageIds,
|
||||||
getDefaultConversationColor,
|
getDefaultConversationColor,
|
||||||
getHasUnidentifiedDeliveryIndicators,
|
getHasUnidentifiedDeliveryIndicators,
|
||||||
@@ -2444,6 +2460,7 @@ export const getMessageDetails = createSelector(
|
|||||||
ourPni,
|
ourPni,
|
||||||
ourConversationId,
|
ourConversationId,
|
||||||
ourNumber,
|
ourNumber,
|
||||||
|
pinnedMessageIds,
|
||||||
selectedMessageIds,
|
selectedMessageIds,
|
||||||
defaultConversationColor,
|
defaultConversationColor,
|
||||||
hasUnidentifiedDeliveryIndicators
|
hasUnidentifiedDeliveryIndicators
|
||||||
@@ -2578,6 +2595,7 @@ export const getMessageDetails = createSelector(
|
|||||||
ourConversationId,
|
ourConversationId,
|
||||||
ourNumber,
|
ourNumber,
|
||||||
regionCode,
|
regionCode,
|
||||||
|
pinnedMessageIds,
|
||||||
selectedMessageIds,
|
selectedMessageIds,
|
||||||
defaultConversationColor,
|
defaultConversationColor,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
getSelectedMessageIds,
|
getSelectedMessageIds,
|
||||||
getMessages,
|
getMessages,
|
||||||
getCachedConversationMemberColorsSelector,
|
getCachedConversationMemberColorsSelector,
|
||||||
|
getPinnedMessageIds,
|
||||||
} from './conversations.dom.js';
|
} from './conversations.dom.js';
|
||||||
import { getAccountSelector } from './accounts.std.js';
|
import { getAccountSelector } from './accounts.std.js';
|
||||||
import {
|
import {
|
||||||
@@ -53,6 +54,7 @@ const getTimelineItem = (
|
|||||||
const callHistorySelector = getCallHistorySelector(state);
|
const callHistorySelector = getCallHistorySelector(state);
|
||||||
const activeCall = getActiveCall(state);
|
const activeCall = getActiveCall(state);
|
||||||
const accountSelector = getAccountSelector(state);
|
const accountSelector = getAccountSelector(state);
|
||||||
|
const pinnedMessageIds = getPinnedMessageIds(state);
|
||||||
const selectedMessageIds = getSelectedMessageIds(state);
|
const selectedMessageIds = getSelectedMessageIds(state);
|
||||||
const defaultConversationColor = getDefaultConversationColor(state);
|
const defaultConversationColor = getDefaultConversationColor(state);
|
||||||
|
|
||||||
@@ -70,6 +72,7 @@ const getTimelineItem = (
|
|||||||
callHistorySelector,
|
callHistorySelector,
|
||||||
activeCall,
|
activeCall,
|
||||||
accountSelector,
|
accountSelector,
|
||||||
|
pinnedMessageIds,
|
||||||
selectedMessageIds,
|
selectedMessageIds,
|
||||||
defaultConversationColor,
|
defaultConversationColor,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
|||||||
kickOffAttachmentDownload,
|
kickOffAttachmentDownload,
|
||||||
markAttachmentAsCorrupted,
|
markAttachmentAsCorrupted,
|
||||||
messageExpanded,
|
messageExpanded,
|
||||||
|
onPinnedMessageAdd,
|
||||||
|
onPinnedMessageRemove,
|
||||||
openGiftBadge,
|
openGiftBadge,
|
||||||
pushPanelForConversation,
|
pushPanelForConversation,
|
||||||
retryDeleteForEveryone,
|
retryDeleteForEveryone,
|
||||||
@@ -237,6 +239,8 @@ export const SmartTimelineItem = memo(function SmartTimelineItem(
|
|||||||
}
|
}
|
||||||
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
|
onOutgoingAudioCallInConversation={onOutgoingAudioCallInConversation}
|
||||||
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
||||||
|
onPinnedMessageAdd={onPinnedMessageAdd}
|
||||||
|
onPinnedMessageRemove={onPinnedMessageRemove}
|
||||||
scrollToPinnedMessage={scrollToPinnedMessage}
|
scrollToPinnedMessage={scrollToPinnedMessage}
|
||||||
retryDeleteForEveryone={retryDeleteForEveryone}
|
retryDeleteForEveryone={retryDeleteForEveryone}
|
||||||
retryMessageSend={retryMessageSend}
|
retryMessageSend={retryMessageSend}
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ import { VoiceNotesPlaybackContext } from '../../components/VoiceNotesPlaybackCo
|
|||||||
import type { Props as MessageAudioProps } from './MessageAudio.preload.js';
|
import type { Props as MessageAudioProps } from './MessageAudio.preload.js';
|
||||||
import { SmartMessageAudio } from './MessageAudio.preload.js';
|
import { SmartMessageAudio } from './MessageAudio.preload.js';
|
||||||
|
|
||||||
type AudioAttachmentProps = Omit<MessageAudioProps, 'computePeaks'>;
|
export type RenderAudioAttachmentProps = Omit<
|
||||||
|
MessageAudioProps,
|
||||||
|
'computePeaks'
|
||||||
|
>;
|
||||||
|
|
||||||
export function renderAudioAttachment(
|
export function renderAudioAttachment(
|
||||||
props: AudioAttachmentProps
|
props: RenderAudioAttachmentProps
|
||||||
): ReactElement {
|
): ReactElement {
|
||||||
return (
|
return (
|
||||||
<VoiceNotesPlaybackContext.Consumer>
|
<VoiceNotesPlaybackContext.Consumer>
|
||||||
|
|||||||
@@ -5,9 +5,24 @@ import type { ConversationAttributesType } from '../model-types.d.ts';
|
|||||||
import { SignalService as Proto } from '../protobuf/index.std.js';
|
import { SignalService as Proto } from '../protobuf/index.std.js';
|
||||||
import { isGroupV2 } from './whatTypeOfConversation.dom.js';
|
import { isGroupV2 } from './whatTypeOfConversation.dom.js';
|
||||||
import { areWeAdmin } from './areWeAdmin.preload.js';
|
import { areWeAdmin } from './areWeAdmin.preload.js';
|
||||||
|
import type { ConversationType } from '../state/ducks/conversations.preload.js';
|
||||||
|
|
||||||
|
function getConversationAccessControlAttributes(
|
||||||
|
conversation: ConversationAttributesType | ConversationType
|
||||||
|
): number | null {
|
||||||
|
if ('accessControl' in conversation) {
|
||||||
|
return conversation.accessControl?.attributes ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('accessControlAttributes' in conversation) {
|
||||||
|
return conversation.accessControlAttributes ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export function canEditGroupInfo(
|
export function canEditGroupInfo(
|
||||||
conversationAttrs: ConversationAttributesType
|
conversationAttrs: ConversationAttributesType | ConversationType
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!isGroupV2(conversationAttrs)) {
|
if (!isGroupV2(conversationAttrs)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -17,9 +32,11 @@ export function canEditGroupInfo(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (areWeAdmin(conversationAttrs)) {
|
||||||
areWeAdmin(conversationAttrs) ||
|
return true;
|
||||||
conversationAttrs.accessControl?.attributes ===
|
}
|
||||||
Proto.AccessControl.AccessRequired.MEMBER
|
|
||||||
);
|
const accessControlAttributes =
|
||||||
|
getConversationAccessControlAttributes(conversationAttrs);
|
||||||
|
return accessControlAttributes === Proto.AccessControl.AccessRequired.MEMBER;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as RemoteConfig from '../RemoteConfig.dom.js';
|
||||||
|
import { parseIntWithFallback } from './parseIntWithFallback.std.js';
|
||||||
|
|
||||||
|
export function getPinnedMessagesLimit(): number {
|
||||||
|
const remoteValue = RemoteConfig.getValue('global.pinned_message_limit');
|
||||||
|
return parseIntWithFallback(remoteValue, 3);
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { DurationInSeconds } from './durations/duration-in-seconds.std.js';
|
||||||
|
|
||||||
|
export function getPinnedMessageExpiresAt(
|
||||||
|
receivedAtTimestamp: number,
|
||||||
|
pinDuration: DurationInSeconds | null
|
||||||
|
): number | null {
|
||||||
|
if (pinDuration == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const pinDurationMs = DurationInSeconds.toMillis(pinDuration);
|
||||||
|
return receivedAtTimestamp + pinDurationMs;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user