mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-04-02 08:13:37 +01:00
Send View Once Messages
This commit is contained in:
@@ -112,6 +112,9 @@ export default {
|
||||
// MediaQualitySelector
|
||||
setMediaQualitySetting: action('setMediaQualitySetting'),
|
||||
shouldSendHighQualityAttachments: false,
|
||||
// ViewOnce
|
||||
isViewOnce: false,
|
||||
setViewOnce: action('setViewOnce'),
|
||||
// CompositionInput
|
||||
onEditorStateChange: action('onEditorStateChange'),
|
||||
onTextTooLong: action('onTextTooLong'),
|
||||
@@ -217,6 +220,23 @@ export function Attachments(args: Props): React.JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
export function ViewOnceEnabled(args: Props): React.JSX.Element {
|
||||
const theme = useContext(StorybookThemeContext);
|
||||
return (
|
||||
<CompositionArea
|
||||
{...args}
|
||||
theme={theme}
|
||||
isViewOnce
|
||||
draftAttachments={[
|
||||
fakeDraftAttachment({
|
||||
contentType: IMAGE_JPEG,
|
||||
url: landscapeGreenUrl,
|
||||
}),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function PendingApproval(args: Props): React.JSX.Element {
|
||||
const theme = useContext(StorybookThemeContext);
|
||||
return <CompositionArea {...args} theme={theme} areWePendingApproval />;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import type {
|
||||
@@ -32,6 +39,7 @@ import type {
|
||||
InMemoryAttachmentDraftType,
|
||||
} from '../types/Attachment.std.js';
|
||||
import { isImageAttachment, isVoiceMessage } from '../util/Attachment.std.js';
|
||||
import { isViewOnceEligible } from '../util/viewOnceEligibility.std.js';
|
||||
import type { AciString } from '../types/ServiceId.std.js';
|
||||
import { AudioCapture } from './conversation/AudioCapture.dom.js';
|
||||
import { CompositionUpload } from './CompositionUpload.dom.js';
|
||||
@@ -77,8 +85,7 @@ import { ConfirmationDialog } from './ConfirmationDialog.dom.js';
|
||||
import type { EmojiSkinTone } from './fun/data/emojis.std.js';
|
||||
import { FunPickerButton } from './fun/FunButton.dom.js';
|
||||
import { AxoDropdownMenu } from '../axo/AxoDropdownMenu.dom.js';
|
||||
import { AxoSymbol } from '../axo/AxoSymbol.dom.js';
|
||||
import { AxoButton } from '../axo/AxoButton.dom.js';
|
||||
import { AxoIconButton } from '../axo/AxoIconButton.dom.js';
|
||||
import { tw } from '../axo/tw.dom.js';
|
||||
import { isPollSendEnabled, type PollCreateType } from '../types/Polls.dom.js';
|
||||
import { PollCreateModal } from './PollCreateModal.dom.js';
|
||||
@@ -168,6 +175,7 @@ export type OwnProps = Readonly<{
|
||||
options: {
|
||||
draftAttachments?: ReadonlyArray<AttachmentDraftType>;
|
||||
bodyRanges?: DraftBodyRanges;
|
||||
isViewOnce?: boolean;
|
||||
message?: string;
|
||||
timestamp?: number;
|
||||
voiceNoteAttachment?: InMemoryAttachmentDraftType;
|
||||
@@ -195,6 +203,12 @@ export type OwnProps = Readonly<{
|
||||
conversationId: string,
|
||||
messageId: string | undefined
|
||||
): unknown;
|
||||
isViewOnce: boolean;
|
||||
setViewOnce(options: {
|
||||
conversationId: string;
|
||||
value: boolean;
|
||||
toastNotify: boolean;
|
||||
}): unknown;
|
||||
shouldSendHighQualityAttachments: boolean;
|
||||
showConversation: ShowConversationType;
|
||||
startRecording: (id: string) => unknown;
|
||||
@@ -284,6 +298,9 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
quotedMessageAuthorAci,
|
||||
quotedMessageSentAt,
|
||||
scrollToMessage,
|
||||
// View Once
|
||||
isViewOnce,
|
||||
setViewOnce,
|
||||
// MediaQualitySelector
|
||||
setMediaQualitySetting,
|
||||
shouldSendHighQualityAttachments,
|
||||
@@ -407,6 +424,7 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
bodyRanges,
|
||||
message,
|
||||
timestamp,
|
||||
isViewOnce,
|
||||
});
|
||||
}
|
||||
setLarge(false);
|
||||
@@ -418,6 +436,7 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
canSend,
|
||||
draftAttachments,
|
||||
editedMessageId,
|
||||
isViewOnce,
|
||||
quotedMessageSentAt,
|
||||
quotedMessageAuthorAci,
|
||||
sendEditedMessage,
|
||||
@@ -586,8 +605,38 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
|
||||
const showMediaQualitySelector = draftAttachments.some(isImageAttachment);
|
||||
|
||||
const showViewOnceToggle = isViewOnceEligible(
|
||||
draftAttachments,
|
||||
Boolean(quotedMessageId)
|
||||
);
|
||||
|
||||
const isViewOnceActive = isViewOnce && showViewOnceToggle;
|
||||
|
||||
let draftEditMessageForInput = draftEditMessage;
|
||||
let largeForInput = large;
|
||||
let linkPreviewLoadingForInput = linkPreviewLoading;
|
||||
let linkPreviewResultForInput = linkPreviewResult;
|
||||
let quotedMessageIdForInput = quotedMessageId;
|
||||
|
||||
if (isViewOnceActive) {
|
||||
draftEditMessageForInput = null;
|
||||
largeForInput = false;
|
||||
linkPreviewLoadingForInput = false;
|
||||
linkPreviewResultForInput = null;
|
||||
quotedMessageIdForInput = null;
|
||||
}
|
||||
|
||||
const [funPickerOpen, setFunPickerOpen] = useState(false);
|
||||
|
||||
const handleToggleViewOnce = useCallback(() => {
|
||||
setFunPickerOpen(false);
|
||||
setViewOnce({
|
||||
conversationId,
|
||||
value: !isViewOnce,
|
||||
toastNotify: true,
|
||||
});
|
||||
}, [conversationId, isViewOnce, setViewOnce]);
|
||||
|
||||
const handleFunPickerOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
setFunPickerOpen(open);
|
||||
@@ -669,6 +718,27 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
});
|
||||
}, [pushPanelForConversation]);
|
||||
|
||||
const mediaQualitySelectorFragment = useMemo(
|
||||
() =>
|
||||
showMediaQualitySelector ? (
|
||||
<div className="CompositionArea__button-cell">
|
||||
<MediaQualitySelector
|
||||
conversationId={conversationId}
|
||||
i18n={i18n}
|
||||
isHighQuality={shouldSendHighQualityAttachments}
|
||||
onSelectQuality={setMediaQualitySetting}
|
||||
/>
|
||||
</div>
|
||||
) : null,
|
||||
[
|
||||
conversationId,
|
||||
i18n,
|
||||
setMediaQualitySetting,
|
||||
shouldSendHighQualityAttachments,
|
||||
showMediaQualitySelector,
|
||||
]
|
||||
);
|
||||
|
||||
const leftHandSideButtonsFragment = (
|
||||
<>
|
||||
{confirmGifSelection && (
|
||||
@@ -692,7 +762,13 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
{i18n('icu:CompositionArea__ConfirmGifSelection__Body')}
|
||||
</ConfirmationDialog>
|
||||
)}
|
||||
<div className="CompositionArea__button-cell">
|
||||
<div
|
||||
aria-hidden={isViewOnceActive || undefined}
|
||||
className={classNames(
|
||||
'CompositionArea__button-cell',
|
||||
isViewOnceActive ? tw('invisible') : null
|
||||
)}
|
||||
>
|
||||
<FunPicker
|
||||
placement="top start"
|
||||
open={funPickerOpen}
|
||||
@@ -705,16 +781,7 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
<FunPickerButton i18n={i18n} />
|
||||
</FunPicker>
|
||||
</div>
|
||||
{showMediaQualitySelector ? (
|
||||
<div className="CompositionArea__button-cell">
|
||||
<MediaQualitySelector
|
||||
conversationId={conversationId}
|
||||
i18n={i18n}
|
||||
isHighQuality={shouldSendHighQualityAttachments}
|
||||
onSelectQuality={setMediaQualitySetting}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{mediaQualitySelectorFragment}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -752,6 +819,9 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
) : null;
|
||||
|
||||
const isRecording = recordingState === RecordingState.Recording;
|
||||
const actionSlotClassName = tw(
|
||||
'flex size-8 shrink-0 items-center justify-center'
|
||||
);
|
||||
|
||||
let attButton;
|
||||
if (draftEditMessage || linkPreviewResult || isRecording) {
|
||||
@@ -760,15 +830,15 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
attButton = (
|
||||
<div className="CompositionArea__button-cell">
|
||||
<AxoDropdownMenu.Root>
|
||||
<div className={tw('flex h-8 items-center')}>
|
||||
<div className={actionSlotClassName}>
|
||||
<AxoDropdownMenu.Trigger>
|
||||
<AxoButton.Root
|
||||
<AxoIconButton.Root
|
||||
variant="borderless-secondary"
|
||||
size="sm"
|
||||
aria-label={i18n('icu:CompositionArea--attach-plus')}
|
||||
>
|
||||
<AxoSymbol.Icon label={null} symbol="plus" size={20} />
|
||||
</AxoButton.Root>
|
||||
size="md"
|
||||
label={i18n('icu:CompositionArea--attach-plus')}
|
||||
tooltip={false}
|
||||
symbol="plus"
|
||||
/>
|
||||
</AxoDropdownMenu.Trigger>
|
||||
</div>
|
||||
<AxoDropdownMenu.Content>
|
||||
@@ -807,12 +877,14 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
<>
|
||||
<div className="CompositionArea__placeholder" />
|
||||
<div className="CompositionArea__button-cell">
|
||||
<button
|
||||
type="button"
|
||||
className="CompositionArea__send-button"
|
||||
onClick={handleForceSend}
|
||||
aria-label={i18n('icu:sendMessageToContact')}
|
||||
/>
|
||||
<div className={actionSlotClassName}>
|
||||
<button
|
||||
type="button"
|
||||
className="CompositionArea__send-button"
|
||||
onClick={handleForceSend}
|
||||
aria-label={i18n('icu:sendMessageToContact')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : null;
|
||||
@@ -1050,6 +1122,9 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
isCreatingStory={false}
|
||||
isFormattingEnabled={isFormattingEnabled}
|
||||
isSending={false}
|
||||
isHighQuality={shouldSendHighQualityAttachments}
|
||||
isViewOnce={isViewOnce}
|
||||
showViewOnceToggle={showViewOnceToggle}
|
||||
convertDraftBodyRangesIntoHydrated={
|
||||
convertDraftBodyRangesIntoHydrated
|
||||
}
|
||||
@@ -1060,6 +1135,8 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
data,
|
||||
contentType,
|
||||
blurHash,
|
||||
isViewOnce: editorIsViewOnce,
|
||||
isHighQuality: editorIsHighQuality,
|
||||
}) => {
|
||||
const newAttachment = {
|
||||
...attachmentToEdit,
|
||||
@@ -1071,6 +1148,25 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
|
||||
addAttachment(conversationId, newAttachment);
|
||||
setAttachmentToEdit(undefined);
|
||||
|
||||
if (
|
||||
editorIsViewOnce !== undefined &&
|
||||
editorIsViewOnce !== isViewOnce
|
||||
) {
|
||||
setViewOnce({
|
||||
conversationId,
|
||||
value: editorIsViewOnce,
|
||||
toastNotify: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
editorIsHighQuality !== undefined &&
|
||||
editorIsHighQuality !== shouldSendHighQualityAttachments
|
||||
) {
|
||||
setMediaQualitySetting(conversationId, editorIsHighQuality);
|
||||
}
|
||||
|
||||
onEditorStateChange?.({
|
||||
bodyRanges: captionBodyRanges ?? [],
|
||||
conversationId,
|
||||
@@ -1092,42 +1188,46 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
sortedGroupMembers={sortedGroupMembers}
|
||||
/>
|
||||
)}
|
||||
<div className="CompositionArea__toggle-large">
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
'CompositionArea__toggle-large__button',
|
||||
large ? 'CompositionArea__toggle-large__button--large-active' : null
|
||||
)}
|
||||
// This prevents the user from tabbing here
|
||||
tabIndex={-1}
|
||||
onClick={handleToggleLarge}
|
||||
aria-label={i18n('icu:CompositionArea--expand')}
|
||||
/>
|
||||
</div>
|
||||
{isViewOnceActive ? null : (
|
||||
<div className="CompositionArea__toggle-large">
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(
|
||||
'CompositionArea__toggle-large__button',
|
||||
large
|
||||
? 'CompositionArea__toggle-large__button--large-active'
|
||||
: null
|
||||
)}
|
||||
onClick={handleToggleLarge}
|
||||
aria-label={i18n('icu:CompositionArea--expand')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
'CompositionArea__row',
|
||||
'CompositionArea__row--column'
|
||||
)}
|
||||
>
|
||||
{quotedMessageProps && (
|
||||
<div className="quote-wrapper">
|
||||
<Quote
|
||||
isCompose
|
||||
{...quotedMessageProps}
|
||||
i18n={i18n}
|
||||
onClick={
|
||||
quotedMessageId
|
||||
? () => scrollToMessage(conversationId, quotedMessageId)
|
||||
: undefined
|
||||
}
|
||||
onClose={() => {
|
||||
setQuoteByMessageId(conversationId, undefined);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isViewOnceActive
|
||||
? null
|
||||
: quotedMessageProps && (
|
||||
<div className="quote-wrapper">
|
||||
<Quote
|
||||
isCompose
|
||||
{...quotedMessageProps}
|
||||
i18n={i18n}
|
||||
onClick={
|
||||
quotedMessageId
|
||||
? () => scrollToMessage(conversationId, quotedMessageId)
|
||||
: undefined
|
||||
}
|
||||
onClose={() => {
|
||||
setQuoteByMessageId(conversationId, undefined);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{draftAttachments.length ? (
|
||||
<div className="CompositionArea__attachment-list">
|
||||
<AttachmentList
|
||||
@@ -1145,32 +1245,31 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'CompositionArea__row',
|
||||
large ? 'CompositionArea__row--padded' : null
|
||||
)}
|
||||
className={classNames('CompositionArea__row', {
|
||||
'CompositionArea__row--padded': !isViewOnceActive && large,
|
||||
})}
|
||||
>
|
||||
{!large ? leftHandSideButtonsFragment : null}
|
||||
<div
|
||||
className={classNames(
|
||||
'CompositionArea__input',
|
||||
large ? 'CompositionArea__input--padded' : null
|
||||
)}
|
||||
className={classNames('CompositionArea__input', {
|
||||
'CompositionArea__input--padded': !isViewOnceActive && large,
|
||||
})}
|
||||
>
|
||||
<CompositionInput
|
||||
conversationId={conversationId}
|
||||
disabled={isDisabled}
|
||||
draftBodyRanges={draftBodyRanges}
|
||||
draftEditMessage={draftEditMessage}
|
||||
draftText={draftText}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
i18n={i18n}
|
||||
inputApi={inputApiRef}
|
||||
isFormattingEnabled={isFormattingEnabled}
|
||||
isActive={isActive}
|
||||
large={large}
|
||||
linkPreviewLoading={linkPreviewLoading}
|
||||
linkPreviewResult={linkPreviewResult}
|
||||
draftEditMessage={draftEditMessageForInput}
|
||||
large={largeForInput}
|
||||
linkPreviewLoading={linkPreviewLoadingForInput}
|
||||
linkPreviewResult={linkPreviewResultForInput}
|
||||
quotedMessageId={quotedMessageIdForInput}
|
||||
onCloseLinkPreview={onCloseLinkPreview}
|
||||
onDirtyChange={setDirty}
|
||||
onEditorStateChange={onEditorStateChange}
|
||||
@@ -1179,23 +1278,37 @@ export const CompositionArea = memo(function CompositionArea({
|
||||
onTextTooLong={onTextTooLong}
|
||||
ourConversationId={ourConversationId}
|
||||
platform={platform}
|
||||
quotedMessageId={quotedMessageId}
|
||||
sendCounter={sendCounter}
|
||||
shouldHidePopovers={shouldHidePopovers}
|
||||
emojiSkinToneDefault={emojiSkinToneDefault ?? null}
|
||||
sortedGroupMembers={sortedGroupMembers}
|
||||
theme={theme}
|
||||
showViewOnceButton={showViewOnceToggle}
|
||||
isViewOnceActive={isViewOnceActive}
|
||||
onToggleViewOnce={handleToggleViewOnce}
|
||||
/>
|
||||
</div>
|
||||
{!large ? (
|
||||
{isViewOnceActive && (
|
||||
<div className="CompositionArea__button-cell">
|
||||
<div className={actionSlotClassName}>
|
||||
<button
|
||||
type="button"
|
||||
className="CompositionArea__send-button"
|
||||
onClick={handleForceSend}
|
||||
aria-label={i18n('icu:sendMessageToContact')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isViewOnceActive && !large && (
|
||||
<>
|
||||
{!dirty ? micButtonFragment : null}
|
||||
{editMessageFragment}
|
||||
{attButton}
|
||||
</>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
{large ? (
|
||||
{!isViewOnceActive && large ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'CompositionArea__row',
|
||||
|
||||
@@ -53,6 +53,9 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => {
|
||||
inputApi: null,
|
||||
shouldHidePopovers: null,
|
||||
linkPreviewResult: null,
|
||||
showViewOnceButton: false,
|
||||
isViewOnceActive: false,
|
||||
onToggleViewOnce: action('onToggleViewOnce'),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -142,3 +145,30 @@ export function Mentions(): React.JSX.Element {
|
||||
export function NoFormattingMenu(): React.JSX.Element {
|
||||
return <CompositionInput {...useProps({ isFormattingEnabled: false })} />;
|
||||
}
|
||||
|
||||
export function ViewOnceButton(): React.JSX.Element {
|
||||
const [isActive, setIsActive] = React.useState(false);
|
||||
const props = useProps();
|
||||
|
||||
return (
|
||||
<CompositionInput
|
||||
{...props}
|
||||
showViewOnceButton
|
||||
isViewOnceActive={isActive}
|
||||
onToggleViewOnce={() => setIsActive(!isActive)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ViewOnceButtonActive(): React.JSX.Element {
|
||||
const props = useProps();
|
||||
|
||||
return (
|
||||
<CompositionInput
|
||||
{...props}
|
||||
showViewOnceButton
|
||||
isViewOnceActive
|
||||
onToggleViewOnce={action('onToggleViewOnce')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -83,6 +83,9 @@ import type { EmojiCompletionOptions } from '../quill/emoji/completion.dom.js';
|
||||
import { useFunEmojiLocalizer } from './fun/useFunEmojiLocalizer.dom.js';
|
||||
import { MAX_BODY_ATTACHMENT_BYTE_LENGTH } from '../util/longAttachment.std.js';
|
||||
import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.dom.js';
|
||||
import { AxoSymbol } from '../axo/AxoSymbol.dom.js';
|
||||
import { AxoTooltip } from '../axo/AxoTooltip.dom.js';
|
||||
import { tw } from '../axo/tw.dom.js';
|
||||
|
||||
const log = createLogger('CompositionInput');
|
||||
|
||||
@@ -159,6 +162,9 @@ export type Props = Readonly<{
|
||||
linkPreviewLoading?: boolean;
|
||||
linkPreviewResult: LinkPreviewForUIType | null;
|
||||
onCloseLinkPreview?(conversationId: string): unknown;
|
||||
showViewOnceButton: boolean;
|
||||
isViewOnceActive: boolean;
|
||||
onToggleViewOnce: () => void;
|
||||
}>;
|
||||
|
||||
const BASE_CLASS_NAME = 'module-composition-input';
|
||||
@@ -195,6 +201,9 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||
sendCounter,
|
||||
sortedGroupMembers,
|
||||
theme,
|
||||
showViewOnceButton,
|
||||
isViewOnceActive,
|
||||
onToggleViewOnce,
|
||||
} = props;
|
||||
|
||||
const [emojiCompletionElement, setEmojiCompletionElement] =
|
||||
@@ -876,7 +885,7 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||
}}
|
||||
formats={getQuillFormats()}
|
||||
placeholder={placeholder || i18n('icu:sendMessage')}
|
||||
readOnly={disabled}
|
||||
readOnly={disabled || isViewOnceActive}
|
||||
ref={element => {
|
||||
if (!element) {
|
||||
return;
|
||||
@@ -988,16 +997,22 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||
};
|
||||
}, [isMouseDown]);
|
||||
|
||||
const isInputEnabled = !disabled && !isViewOnceActive;
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<div
|
||||
className={getClassName('__input')}
|
||||
className={classNames(
|
||||
getClassName('__input'),
|
||||
showViewOnceButton && getClassName('__input--with-view-once'),
|
||||
isViewOnceActive && getClassName('__input--view-once-active')
|
||||
)}
|
||||
data-supertab
|
||||
ref={ref}
|
||||
data-testid="CompositionInput"
|
||||
data-enabled={disabled ? 'false' : 'true'}
|
||||
data-enabled={isInputEnabled ? 'true' : 'false'}
|
||||
onMouseDown={onMouseDown}
|
||||
>
|
||||
{draftEditMessage && (
|
||||
@@ -1024,6 +1039,11 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
{isViewOnceActive && (
|
||||
<div className={getClassName('__view-once-placeholder')}>
|
||||
{i18n('icu:CompositionArea--viewOnceMediaPlaceholder')}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
onClick={focus}
|
||||
onScroll={onScroll}
|
||||
@@ -1045,6 +1065,31 @@ export function CompositionInput(props: Props): React.ReactElement {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{showViewOnceButton && (
|
||||
<div className={getClassName('__view-once-button')}>
|
||||
<AxoTooltip.Root
|
||||
label={i18n('icu:CompositionArea--viewOnceToggle')}
|
||||
tooltipRepeatsTriggerAccessibleName
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={i18n('icu:CompositionArea--viewOnceToggle')}
|
||||
onClick={onToggleViewOnce}
|
||||
className={tw(
|
||||
'flex cursor-default items-center justify-center rounded-full',
|
||||
'outline-border-focused not-forced-colors:outline-0 not-forced-colors:focused:outline-[2.5px]',
|
||||
'forced-colors:border forced-colors:border-[ButtonBorder]'
|
||||
)}
|
||||
>
|
||||
<AxoSymbol.Icon
|
||||
size={20}
|
||||
symbol={isViewOnceActive ? 'viewonce' : 'viewonce-slash'}
|
||||
label={null}
|
||||
/>
|
||||
</button>
|
||||
</AxoTooltip.Root>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Reference>
|
||||
|
||||
@@ -187,6 +187,9 @@ export function CompositionTextArea({
|
||||
linkPreviewResult={null}
|
||||
// Panels appear behind this modal
|
||||
shouldHidePopovers={null}
|
||||
showViewOnceButton={false}
|
||||
isViewOnceActive={false}
|
||||
onToggleViewOnce={() => undefined}
|
||||
/>
|
||||
<div className="CompositionTextArea__emoji">
|
||||
<FunEmojiPicker
|
||||
|
||||
@@ -20,6 +20,7 @@ export default {
|
||||
component: MediaEditor,
|
||||
args: {
|
||||
getPreferredBadge: () => undefined,
|
||||
isHighQuality: false,
|
||||
i18n,
|
||||
imageToBlurHash: input => Promise.resolve(input.toString()),
|
||||
imageSrc: IMAGE_2,
|
||||
@@ -55,6 +56,12 @@ Portrait.args = {
|
||||
imageSrc: IMAGE_4,
|
||||
};
|
||||
|
||||
export const ViewOnce = Template.bind({});
|
||||
ViewOnce.args = {
|
||||
isViewOnce: false,
|
||||
showViewOnceToggle: true,
|
||||
};
|
||||
|
||||
export const Sending = Template.bind({});
|
||||
Sending.args = {
|
||||
isSending: true,
|
||||
|
||||
@@ -44,7 +44,6 @@ import { ContextMenu } from './ContextMenu.dom.js';
|
||||
import { IMAGE_PNG } from '../types/MIME.std.js';
|
||||
import { SizeObserver } from '../hooks/useSizeObserver.dom.js';
|
||||
import { Slider } from './Slider.dom.js';
|
||||
import { Spinner } from './Spinner.dom.js';
|
||||
import { Theme } from '../util/theme.std.js';
|
||||
import { ThemeType } from '../types/Util.std.js';
|
||||
import { arrow } from '../util/keyboard.dom.js';
|
||||
@@ -62,6 +61,9 @@ import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.dom.js';
|
||||
import { FunStickerPicker } from './fun/FunStickerPicker.dom.js';
|
||||
import type { FunStickerSelection } from './fun/panels/FunPanelStickers.dom.js';
|
||||
import { drop } from '../util/drop.std.js';
|
||||
import { MediaQualitySelector } from './MediaQualitySelector.dom.js';
|
||||
import { AxoButton } from '../axo/AxoButton.dom.js';
|
||||
import { tw } from '../axo/tw.dom.js';
|
||||
import type { FunTimeStickerStyle } from './fun/constants.dom.js';
|
||||
import * as Errors from '../types/errors.std.js';
|
||||
|
||||
@@ -75,6 +77,8 @@ export type MediaEditorResultType = Readonly<{
|
||||
blurHash: string;
|
||||
caption?: string;
|
||||
captionBodyRanges?: DraftBodyRanges;
|
||||
isViewOnce?: boolean;
|
||||
isHighQuality?: boolean;
|
||||
}>;
|
||||
|
||||
export type PropsType = {
|
||||
@@ -89,6 +93,9 @@ export type PropsType = {
|
||||
convertDraftBodyRangesIntoHydrated: (
|
||||
bodyRanges: DraftBodyRanges | undefined
|
||||
) => HydratedBodyRangesType | undefined;
|
||||
isHighQuality?: boolean;
|
||||
isViewOnce?: boolean;
|
||||
showViewOnceToggle?: boolean;
|
||||
} & Pick<
|
||||
CompositionInputProps,
|
||||
| 'draftText'
|
||||
@@ -159,6 +166,9 @@ export function MediaEditor({
|
||||
isSending,
|
||||
onClose,
|
||||
onDone,
|
||||
isHighQuality,
|
||||
isViewOnce,
|
||||
showViewOnceToggle = false,
|
||||
|
||||
// CompositionInput
|
||||
draftText,
|
||||
@@ -182,6 +192,15 @@ export function MediaEditor({
|
||||
const [caption, setCaption] = useState(draftText ?? '');
|
||||
const [captionBodyRanges, setCaptionBodyRanges] =
|
||||
useState<DraftBodyRanges | null>(draftBodyRanges);
|
||||
const [localIsViewOnce, setLocalIsViewOnce] = useState(isViewOnce ?? false);
|
||||
const hasViewOnceChange = localIsViewOnce !== (isViewOnce ?? false);
|
||||
const [localIsHighQuality, setLocalIsHighQuality] = useState(
|
||||
isHighQuality ?? false
|
||||
);
|
||||
const hasHighQualityChange =
|
||||
typeof isHighQuality === 'boolean' && localIsHighQuality !== isHighQuality;
|
||||
const showMediaQualitySelector = typeof isHighQuality === 'boolean';
|
||||
const pickerTheme = ThemeType.dark;
|
||||
|
||||
const hydratedBodyRanges = useMemo(
|
||||
() => convertDraftBodyRangesIntoHydrated(captionBodyRanges ?? undefined),
|
||||
@@ -209,6 +228,10 @@ export function MediaEditor({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSelectQuality = useCallback((_id: string, isHQ: boolean) => {
|
||||
setLocalIsHighQuality(isHQ);
|
||||
}, []);
|
||||
|
||||
const handlePickSticker = useCallback(
|
||||
(_packId: string, _stickerId: number, src: string) => {
|
||||
async function run() {
|
||||
@@ -362,8 +385,18 @@ export function MediaEditor({
|
||||
});
|
||||
|
||||
const onTryClose = useCallback(() => {
|
||||
confirmDiscardIf(canUndo || isCreatingStory, onClose);
|
||||
}, [confirmDiscardIf, canUndo, isCreatingStory, onClose]);
|
||||
confirmDiscardIf(
|
||||
canUndo || isCreatingStory || hasViewOnceChange || hasHighQualityChange,
|
||||
onClose
|
||||
);
|
||||
}, [
|
||||
confirmDiscardIf,
|
||||
canUndo,
|
||||
isCreatingStory,
|
||||
hasViewOnceChange,
|
||||
hasHighQualityChange,
|
||||
onClose,
|
||||
]);
|
||||
tryClose.current = onTryClose;
|
||||
|
||||
// Keyboard support
|
||||
@@ -1317,13 +1350,40 @@ export function MediaEditor({
|
||||
showTimeStickers
|
||||
onSelectTimeSticker={handlePickTimeSticker}
|
||||
placement="top"
|
||||
theme={ThemeType.dark}
|
||||
theme={pickerTheme}
|
||||
>
|
||||
<FunStickerPickerButton i18n={i18n} />
|
||||
</FunStickerPicker>
|
||||
</div>
|
||||
<div className="MediaEditor__tools-row-2">
|
||||
<div className="MediaEditor__tools--input dark-theme">
|
||||
<div
|
||||
className={classNames(
|
||||
tw('mx-1 flex items-center justify-center'),
|
||||
localIsViewOnce ? tw('invisible') : null
|
||||
)}
|
||||
>
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={handleEmojiPickerOpenChange}
|
||||
onSelectEmoji={handleSelectEmoji}
|
||||
placement="top"
|
||||
theme={pickerTheme}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<FunEmojiPickerButton i18n={i18n} />
|
||||
</FunEmojiPicker>
|
||||
</div>
|
||||
{showMediaQualitySelector && (
|
||||
<div className={tw('mx-1 flex items-center justify-center')}>
|
||||
<MediaQualitySelector
|
||||
conversationId=""
|
||||
i18n={i18n}
|
||||
isHighQuality={localIsHighQuality}
|
||||
onSelectQuality={handleSelectQuality}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="MediaEditor__tools--input">
|
||||
<CompositionInput
|
||||
draftText={caption}
|
||||
draftBodyRanges={hydratedBodyRanges ?? null}
|
||||
@@ -1348,7 +1408,7 @@ export function MediaEditor({
|
||||
quotedMessageId={null}
|
||||
sendCounter={0}
|
||||
sortedGroupMembers={sortedGroupMembers}
|
||||
theme={ThemeType.dark}
|
||||
theme={pickerTheme}
|
||||
// Only needed for state updates and we need to override those
|
||||
conversationId={null}
|
||||
// Cannot enter media editor while editing
|
||||
@@ -1359,21 +1419,26 @@ export function MediaEditor({
|
||||
shouldHidePopovers={null}
|
||||
// link previews not displayed with media
|
||||
linkPreviewResult={null}
|
||||
>
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
onOpenChange={handleEmojiPickerOpenChange}
|
||||
onSelectEmoji={handleSelectEmoji}
|
||||
placement="top"
|
||||
theme={ThemeType.dark}
|
||||
closeOnSelect={false}
|
||||
>
|
||||
<FunEmojiPickerButton i18n={i18n} />
|
||||
</FunEmojiPicker>
|
||||
</CompositionInput>
|
||||
showViewOnceButton={showViewOnceToggle}
|
||||
isViewOnceActive={localIsViewOnce}
|
||||
onToggleViewOnce={() => {
|
||||
const newValue = !localIsViewOnce;
|
||||
setLocalIsViewOnce(newValue);
|
||||
if (newValue) {
|
||||
setEmojiPickerOpen(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
<AxoButton.Root
|
||||
disabled={!image || isSaving || isSending}
|
||||
variant="primary"
|
||||
size="md"
|
||||
experimentalSpinner={
|
||||
isSending
|
||||
? { 'aria-label': doneButtonLabel || i18n('icu:save') }
|
||||
: null
|
||||
}
|
||||
onClick={async () => {
|
||||
if (!fabricCanvas) {
|
||||
return;
|
||||
@@ -1438,17 +1503,13 @@ export function MediaEditor({
|
||||
caption: caption !== '' ? caption : undefined,
|
||||
captionBodyRanges: captionBodyRanges ?? undefined,
|
||||
blurHash,
|
||||
isViewOnce: localIsViewOnce,
|
||||
isHighQuality: localIsHighQuality,
|
||||
});
|
||||
}}
|
||||
theme={Theme.Dark}
|
||||
variant={ButtonVariant.Primary}
|
||||
>
|
||||
{isSending ? (
|
||||
<Spinner svgSize="small" />
|
||||
) : (
|
||||
doneButtonLabel || i18n('icu:save')
|
||||
)}
|
||||
</Button>
|
||||
{doneButtonLabel || i18n('icu:save')}
|
||||
</AxoButton.Root>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -2,16 +2,11 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { KeyboardEvent } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import lodash from 'lodash';
|
||||
import { createPortal } from 'react-dom';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import { Popover } from 'radix-ui';
|
||||
import type { LocalizerType } from '../types/Util.std.js';
|
||||
import { useRefMerger } from '../hooks/useRefMerger.std.js';
|
||||
import { handleOutsideClick } from '../util/handleOutsideClick.dom.js';
|
||||
|
||||
const { noop } = lodash;
|
||||
import { AxoIconButton } from '../axo/AxoIconButton.dom.js';
|
||||
|
||||
export type PropsType = {
|
||||
conversationId: string;
|
||||
@@ -26,182 +21,115 @@ export function MediaQualitySelector({
|
||||
isHighQuality,
|
||||
onSelectQuality,
|
||||
}: PropsType): React.JSX.Element {
|
||||
const [menuShowing, setMenuShowing] = useState(false);
|
||||
const [popperRoot, setPopperRoot] = useState<HTMLElement | null>(null);
|
||||
const [focusedOption, setFocusedOption] = useState<0 | 1 | undefined>(
|
||||
undefined
|
||||
const [open, setOpen] = useState(false);
|
||||
const standardRef = useRef<HTMLButtonElement>(null);
|
||||
const highRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleOpenAutoFocus = useCallback(
|
||||
(e: Event) => {
|
||||
e.preventDefault();
|
||||
if (isHighQuality) {
|
||||
highRef.current?.focus();
|
||||
} else {
|
||||
standardRef.current?.focus();
|
||||
}
|
||||
},
|
||||
[isHighQuality]
|
||||
);
|
||||
|
||||
const buttonRef = React.useRef<HTMLButtonElement | null>(null);
|
||||
const refMerger = useRefMerger();
|
||||
|
||||
const handleClick = () => {
|
||||
setMenuShowing(true);
|
||||
};
|
||||
|
||||
const handleKeyDown = (ev: KeyboardEvent) => {
|
||||
if (!popperRoot) {
|
||||
if (ev.key === 'Enter') {
|
||||
setFocusedOption(isHighQuality ? 1 : 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const handleContentKeyDown = useCallback((ev: KeyboardEvent) => {
|
||||
if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {
|
||||
setFocusedOption(oldFocusedOption => (oldFocusedOption === 1 ? 0 : 1));
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
if (ev.key === 'Enter') {
|
||||
onSelectQuality(conversationId, Boolean(focusedOption));
|
||||
setMenuShowing(false);
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setMenuShowing(false);
|
||||
setFocusedOption(undefined);
|
||||
}, [setMenuShowing]);
|
||||
|
||||
useEffect(() => {
|
||||
if (menuShowing) {
|
||||
const root = document.createElement('div');
|
||||
setPopperRoot(root);
|
||||
document.body.appendChild(root);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(root);
|
||||
setPopperRoot(null);
|
||||
};
|
||||
}
|
||||
|
||||
return noop;
|
||||
}, [menuShowing, setPopperRoot, handleClose]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!menuShowing) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
return handleOutsideClick(
|
||||
() => {
|
||||
handleClose();
|
||||
return true;
|
||||
},
|
||||
{
|
||||
containerElements: [popperRoot, buttonRef],
|
||||
name: 'MediaQualitySelector',
|
||||
if (document.activeElement === standardRef.current) {
|
||||
highRef.current?.focus();
|
||||
} else {
|
||||
standardRef.current?.focus();
|
||||
}
|
||||
);
|
||||
}, [menuShowing, popperRoot, handleClose]);
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<button
|
||||
aria-label={i18n('icu:MediaQualitySelector--button')}
|
||||
className={classNames({
|
||||
MediaQualitySelector__button: true,
|
||||
'MediaQualitySelector__button--hq': isHighQuality,
|
||||
'MediaQualitySelector__button--active': menuShowing,
|
||||
})}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleKeyDown}
|
||||
ref={refMerger(buttonRef, ref)}
|
||||
type="button"
|
||||
/>
|
||||
)}
|
||||
</Reference>
|
||||
{menuShowing && popperRoot
|
||||
? createPortal(
|
||||
<Popper placement="top-start" strategy="fixed">
|
||||
{({ ref, style, placement }) => (
|
||||
<div
|
||||
className="MediaQualitySelector__popper"
|
||||
data-placement={placement}
|
||||
ref={ref}
|
||||
style={style}
|
||||
>
|
||||
<div className="MediaQualitySelector__title">
|
||||
{i18n('icu:MediaQualitySelector--title')}
|
||||
</div>
|
||||
<button
|
||||
aria-label={i18n(
|
||||
'icu:MediaQualitySelector--standard-quality-title'
|
||||
)}
|
||||
className={classNames({
|
||||
MediaQualitySelector__option: true,
|
||||
'MediaQualitySelector__option--focused':
|
||||
focusedOption === 0,
|
||||
})}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSelectQuality(conversationId, false);
|
||||
setMenuShowing(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
'MediaQualitySelector__option--checkmark': true,
|
||||
'MediaQualitySelector__option--selected':
|
||||
!isHighQuality,
|
||||
})}
|
||||
/>
|
||||
<div>
|
||||
<div className="MediaQualitySelector__option--title">
|
||||
{i18n(
|
||||
'icu:MediaQualitySelector--standard-quality-title'
|
||||
)}
|
||||
</div>
|
||||
<div className="MediaQualitySelector__option--description">
|
||||
{i18n(
|
||||
'icu:MediaQualitySelector--standard-quality-description'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-label={i18n(
|
||||
'icu:MediaQualitySelector--high-quality-title'
|
||||
)}
|
||||
className={classNames({
|
||||
MediaQualitySelector__option: true,
|
||||
'MediaQualitySelector__option--focused':
|
||||
focusedOption === 1,
|
||||
})}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSelectQuality(conversationId, true);
|
||||
setMenuShowing(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
'MediaQualitySelector__option--checkmark': true,
|
||||
'MediaQualitySelector__option--selected': isHighQuality,
|
||||
})}
|
||||
/>
|
||||
<div>
|
||||
<div className="MediaQualitySelector__option--title">
|
||||
{i18n('icu:MediaQualitySelector--high-quality-title')}
|
||||
</div>
|
||||
<div className="MediaQualitySelector__option--description">
|
||||
{i18n(
|
||||
'icu:MediaQualitySelector--high-quality-description'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<Popover.Root open={open} onOpenChange={setOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
<AxoIconButton.Root
|
||||
variant="borderless-secondary"
|
||||
size="md"
|
||||
symbol={isHighQuality ? 'hd' : 'hd-slash'}
|
||||
label={i18n('icu:MediaQualitySelector--button')}
|
||||
tooltip={false}
|
||||
/>
|
||||
</Popover.Trigger>
|
||||
{open && (
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
className="MediaQualitySelector__popper"
|
||||
side="top"
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
onOpenAutoFocus={handleOpenAutoFocus}
|
||||
onKeyDown={handleContentKeyDown}
|
||||
>
|
||||
<div className="MediaQualitySelector__title">
|
||||
{i18n('icu:MediaQualitySelector--title')}
|
||||
</div>
|
||||
<button
|
||||
ref={standardRef}
|
||||
aria-label={i18n(
|
||||
'icu:MediaQualitySelector--standard-quality-title'
|
||||
)}
|
||||
</Popper>,
|
||||
popperRoot
|
||||
)
|
||||
: null}
|
||||
</Manager>
|
||||
className="MediaQualitySelector__option"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSelectQuality(conversationId, false);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
'MediaQualitySelector__option--checkmark': true,
|
||||
'MediaQualitySelector__option--selected': !isHighQuality,
|
||||
})}
|
||||
/>
|
||||
<div>
|
||||
<div className="MediaQualitySelector__option--title">
|
||||
{i18n('icu:MediaQualitySelector--standard-quality-title')}
|
||||
</div>
|
||||
<div className="MediaQualitySelector__option--description">
|
||||
{i18n(
|
||||
'icu:MediaQualitySelector--standard-quality-description'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
ref={highRef}
|
||||
aria-label={i18n('icu:MediaQualitySelector--high-quality-title')}
|
||||
className="MediaQualitySelector__option"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSelectQuality(conversationId, true);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
'MediaQualitySelector__option--checkmark': true,
|
||||
'MediaQualitySelector__option--selected': isHighQuality,
|
||||
})}
|
||||
/>
|
||||
<div>
|
||||
<div className="MediaQualitySelector__option--title">
|
||||
{i18n('icu:MediaQualitySelector--high-quality-title')}
|
||||
</div>
|
||||
<div className="MediaQualitySelector__option--description">
|
||||
{i18n('icu:MediaQualitySelector--high-quality-description')}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
)}
|
||||
</Popover.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -294,6 +294,9 @@ export function StoryViewsNRepliesModal({
|
||||
large={null}
|
||||
shouldHidePopovers={null}
|
||||
linkPreviewResult={null}
|
||||
showViewOnceButton={false}
|
||||
isViewOnceActive={false}
|
||||
onToggleViewOnce={noop}
|
||||
>
|
||||
<FunEmojiPicker
|
||||
open={emojiPickerOpen}
|
||||
|
||||
@@ -275,6 +275,10 @@ function getToast(toastType: ToastType): AnyToast {
|
||||
group: 'Hike Group 🏔',
|
||||
},
|
||||
};
|
||||
case ToastType.ViewOnceDisabled:
|
||||
return { toastType: ToastType.ViewOnceDisabled };
|
||||
case ToastType.ViewOnceEnabled:
|
||||
return { toastType: ToastType.ViewOnceEnabled };
|
||||
case ToastType.VoiceNoteLimit:
|
||||
return { toastType: ToastType.VoiceNoteLimit };
|
||||
case ToastType.VoiceNoteMustBeTheOnlyAttachment:
|
||||
|
||||
@@ -967,6 +967,22 @@ export function renderToast({
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.ViewOnceEnabled) {
|
||||
return (
|
||||
<Toast onClose={hideToast} timeout={SHORT_TIMEOUT}>
|
||||
{i18n('icu:Toast--viewOnceEnabled')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.ViewOnceDisabled) {
|
||||
return (
|
||||
<Toast onClose={hideToast} timeout={SHORT_TIMEOUT}>
|
||||
{i18n('icu:Toast--viewOnceDisabled')}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
throw missingCaseError(toastType);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user