Send View Once Messages

This commit is contained in:
yash-signal
2026-02-25 13:48:45 -06:00
committed by GitHub
parent 1abce3b627
commit f09d582dec
26 changed files with 735 additions and 365 deletions

View File

@@ -17,9 +17,10 @@ import type {
InMemoryAttachmentDraftType,
} from '../../types/Attachment.std.js';
import {
isVideoAttachment,
isImageAttachment,
isVideoAttachment,
} from '../../util/Attachment.std.js';
import { isViewOnceEligible } from '../../util/viewOnceEligibility.std.js';
import { DataReader, DataWriter } from '../../sql/Client.preload.js';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions.std.js';
import type { DraftBodyRanges } from '../../types/BodyRange.std.js';
@@ -119,6 +120,7 @@ type ComposerStateByConversationType = {
attachments: ReadonlyArray<AttachmentDraftType>;
focusCounter: number;
disabledCounter: number;
isViewOnce: boolean;
linkPreviewLoading: boolean;
linkPreviewResult?: LinkPreviewForUIType;
messageCompositionId: string;
@@ -144,6 +146,7 @@ function getEmptyComposerState(): ComposerStateByConversationType {
attachments: [],
focusCounter: 0,
disabledCounter: 0,
isViewOnce: false,
linkPreviewLoading: false,
messageCompositionId: generateUuid(),
sendCounter: 0,
@@ -166,6 +169,7 @@ const RESET_COMPOSER = 'composer/RESET_COMPOSER';
export const SET_FOCUS = 'composer/SET_FOCUS';
const SET_HIGH_QUALITY_SETTING = 'composer/SET_HIGH_QUALITY_SETTING';
const SET_QUOTED_MESSAGE = 'composer/SET_QUOTED_MESSAGE';
const SET_VIEW_ONCE = 'composer/SET_VIEW_ONCE';
const UPDATE_COMPOSER_DISABLED = 'composer/UPDATE_COMPOSER_DISABLED';
type AddPendingAttachmentActionType = ReadonlyDeep<{
@@ -231,6 +235,14 @@ export type SetQuotedMessageActionType = {
};
};
export type SetViewOnceActionType = ReadonlyDeep<{
type: typeof SET_VIEW_ONCE;
payload: {
conversationId: string;
value: boolean;
};
}>;
// eslint-disable-next-line local-rules/type-alias-readonlydeep
type ComposerActionType =
| AddLinkPreviewActionType
@@ -244,7 +256,8 @@ type ComposerActionType =
| UpdateComposerDisabledActionType
| SetFocusActionType
| SetHighQualitySettingActionType
| SetQuotedMessageActionType;
| SetQuotedMessageActionType
| SetViewOnceActionType;
// Action Creators
@@ -275,6 +288,7 @@ export const actions = {
setMediaQualitySetting,
setQuoteByMessageId,
setQuotedMessage,
setViewOnce,
updateComposerDisabled,
};
@@ -925,6 +939,7 @@ export function setQuoteByMessageId(
quote,
})
);
dispatch(disableViewOnceIfIneligible(conversationId));
dispatch(setComposerFocus(conversation.id));
};
@@ -1377,7 +1392,12 @@ function removeAttachment(
export function replaceAttachments(
conversationId: string,
attachments: ReadonlyArray<AttachmentDraftType>
): ThunkAction<void, RootStateType, unknown, ReplaceAttachmentsActionType> {
): ThunkAction<
void,
RootStateType,
unknown,
ReplaceAttachmentsActionType | SetViewOnceActionType | ShowToastActionType
> {
return (dispatch, getState) => {
// If the call came from a conversation we are no longer in we do not
// update the state.
@@ -1397,6 +1417,7 @@ export function replaceAttachments(
},
});
dispatch(setComposerFocus(conversationId));
dispatch(disableViewOnceIfIneligible(conversationId));
};
}
@@ -1565,6 +1586,95 @@ function setQuotedMessage(
};
}
export function setViewOnce({
conversationId,
value,
toastNotify,
}: {
conversationId: string;
value: boolean;
toastNotify: boolean;
}): ThunkAction<
void,
RootStateType,
unknown,
SetViewOnceActionType | ShowToastActionType
> {
return async (dispatch, getState) => {
const composerState = getComposerStateForConversation(
getState().composer,
conversationId
);
const nextValue =
value &&
isViewOnceEligible(
composerState.attachments,
Boolean(composerState.quotedMessage)
);
if (composerState.isViewOnce !== nextValue) {
dispatch({
type: SET_VIEW_ONCE,
payload: {
conversationId,
value: nextValue,
},
});
if (toastNotify) {
dispatch(
showToast({
toastType: nextValue
? ToastType.ViewOnceEnabled
: ToastType.ViewOnceDisabled,
})
);
}
}
const conversation = window.ConversationController.get(conversationId);
if (conversation && conversation.get('draftIsViewOnce') !== nextValue) {
conversation.set({
draftIsViewOnce: nextValue,
draftChanged: true,
});
await DataWriter.updateConversation(conversation.attributes);
}
};
}
function disableViewOnceIfIneligible(
conversationId: string,
toastNotify = true
): ThunkAction<
void,
RootStateType,
unknown,
SetViewOnceActionType | ShowToastActionType
> {
return (dispatch, getState) => {
const composerState = getComposerStateForConversation(
getState().composer,
conversationId
);
if (
composerState.isViewOnce &&
!isViewOnceEligible(
composerState.attachments,
Boolean(composerState.quotedMessage)
)
) {
dispatch(
setViewOnce({
conversationId,
value: false,
toastNotify,
})
);
}
};
}
// Reducer
export function getEmptyState(): ComposerStateType {
@@ -1702,6 +1812,12 @@ export function reducer(
}));
}
if (action.type === SET_VIEW_ONCE) {
return updateComposerState(state, action, () => ({
isViewOnce: action.payload.value,
}));
}
if (action.type === UPDATE_COMPOSER_DISABLED) {
return updateComposerState(state, action, oldState => ({
disabledCounter:

View File

@@ -167,6 +167,7 @@ import type {
ResetComposerActionType,
SetFocusActionType,
SetQuotedMessageActionType,
SetViewOnceActionType,
} from './composer.preload.js';
import {
SET_FOCUS,
@@ -175,6 +176,7 @@ import {
setQuoteByMessageId,
resetComposer,
saveDraftRecordingIfNeeded,
setViewOnce,
} from './composer.preload.js';
import { ReceiptType } from '../../types/Receipt.std.js';
import { Sound, SoundType } from '../../util/Sound.std.js';
@@ -4883,6 +4885,7 @@ function onConversationOpened(
| ResetComposerActionType
| SetFocusActionType
| SetQuotedMessageActionType
| SetViewOnceActionType
> {
return async dispatch => {
const promises: Array<Promise<void>> = [];
@@ -4974,6 +4977,13 @@ function onConversationOpened(
)
);
dispatch(resetComposer(conversationId));
dispatch(
setViewOnce({
conversationId,
value: conversation.get('draftIsViewOnce') ?? false,
toastNotify: false,
})
);
await Promise.all(promises);
if (window.SignalCI) {

View File

@@ -25,3 +25,10 @@ export const getQuotedMessageSelector = createSelector(
(conversationId: string): QuotedMessageForComposerType | undefined =>
composerStateForConversationIdSelector(conversationId).quotedMessage
);
export const getViewOnceSelector = createSelector(
getComposerStateForConversationIdSelector,
composerStateForConversationIdSelector =>
(conversationId: string): boolean =>
composerStateForConversationIdSelector(conversationId).isViewOnce
);

View File

@@ -112,6 +112,7 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
attachments: draftAttachments,
focusCounter,
disabledCounter,
isViewOnce,
linkPreviewLoading,
linkPreviewResult,
messageCompositionId,
@@ -202,6 +203,7 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
processAttachments,
setMediaQualitySetting,
setQuoteByMessageId,
setViewOnce,
cancelJoinRequest,
sendStickerMessage,
sendEditedMessage,
@@ -301,6 +303,9 @@ export const SmartCompositionArea = memo(function SmartCompositionArea({
quotedMessageAuthorAci={quotedMessage?.quote?.authorAci ?? null}
quotedMessageSentAt={quotedMessage?.quote?.id ?? null}
setQuoteByMessageId={setQuoteByMessageId}
// View Once
isViewOnce={isViewOnce}
setViewOnce={setViewOnce}
// Fun Picker
emojiSkinToneDefault={emojiSkinToneDefault}
onSelectEmoji={onUseEmoji}