Init CallLinkDetails view in calls tab
This commit is contained in:
parent
e9b661873b
commit
19083cadf7
35 changed files with 665 additions and 222 deletions
|
@ -5134,6 +5134,10 @@
|
|||
"messageformat": "Forward To",
|
||||
"description": "Title for the forward a message modal dialog"
|
||||
},
|
||||
"icu:ForwardMessageModal__ShareCallLink": {
|
||||
"messageformat": "Share call link",
|
||||
"description": "Title for the forward a message modal dialog in share call link mode"
|
||||
},
|
||||
"icu:ForwardMessageModal--continue": {
|
||||
"messageformat": "Continue",
|
||||
"description": "aria-label for the 'next' button in the forward a message modal dialog"
|
||||
|
@ -5142,6 +5146,10 @@
|
|||
"messageformat": "Cannot forward empty or deleted messages",
|
||||
"description": "Toast message shown when trying to forward an empty or deleted message"
|
||||
},
|
||||
"icu:ShareCallLinkViaSignal__DraftMessageText": {
|
||||
"messageformat": "Use this link to join a Signal call: {url}",
|
||||
"description": "Draft message text for sharing a call link"
|
||||
},
|
||||
"icu:MessageRequestWarning__learn-more": {
|
||||
"messageformat": "Learn more",
|
||||
"description": "Shown on the message request warning. Clicking this button will open a dialog with more information"
|
||||
|
@ -7128,6 +7136,18 @@
|
|||
"messageformat": "Call link",
|
||||
"description": "Call History > Short description of call > When you joined a call link call"
|
||||
},
|
||||
"icu:CallLinkDetails__Join": {
|
||||
"messageformat": "Join",
|
||||
"description": "Call History > Call Link Details > Join Button"
|
||||
},
|
||||
"icu:CallLinkDetails__CopyLink": {
|
||||
"messageformat": "Copy link",
|
||||
"description": "Call History > Call Link Details > Copy Link Button"
|
||||
},
|
||||
"icu:CallLinkDetails__ShareLinkViaSignal": {
|
||||
"messageformat": "Share link via Signal",
|
||||
"description": "Call History > Call Link Details > Share Link via Signal Button"
|
||||
},
|
||||
"icu:TypingBubble__avatar--overflow-count": {
|
||||
"messageformat": "{count, plural, one {# other is} other {# others are}} typing.",
|
||||
"description": "Group chat multiple person typing indicator when space isn't available to show every avatar, this is the count of avatars hidden."
|
||||
|
|
47
stylesheets/components/CallLinkDetails.scss
Normal file
47
stylesheets/components/CallLinkDetails.scss
Normal file
|
@ -0,0 +1,47 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.CallLinkDetails__Container {
|
||||
max-width: 750px;
|
||||
margin-block: 0;
|
||||
margin-inline: auto;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.CallLinkDetails__Header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.CallLinkDetails__HeaderAvatar,
|
||||
.CallLinkDetails__HeaderActions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.CallLinkDetails__HeaderDetails {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.CallLinkDetails__HeaderTitle {
|
||||
margin: 0;
|
||||
@include font-title-medium;
|
||||
}
|
||||
|
||||
.CallLinkDetails__HeaderDescription {
|
||||
margin: 0;
|
||||
user-select: text;
|
||||
@include font-body-1;
|
||||
@include light-theme {
|
||||
color: $color-gray-60;
|
||||
}
|
||||
@include dark-theme {
|
||||
color: $color-gray-25;
|
||||
}
|
||||
}
|
||||
|
||||
.CallLinkDetails__HeaderButton {
|
||||
font-weight: 600;
|
||||
}
|
|
@ -189,6 +189,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
&--forward {
|
||||
&::after {
|
||||
@include details-icon('../images/icons/v3/forward/forward.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&--down {
|
||||
border-radius: 18px;
|
||||
@include light-theme {
|
||||
|
@ -486,6 +492,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
.ConversationDetails__CallHistoryGroup__ItemIcon--Adhoc {
|
||||
@include light-theme {
|
||||
@include color-svg('../images/icons/v3/link/link.svg', $color-black);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg('../images/icons/v3/link/link.svg', $color-gray-15);
|
||||
}
|
||||
}
|
||||
|
||||
.ConversationDetails__CallHistoryGroup__ItemLabel {
|
||||
flex: 1;
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
@import './components/CallingScreenSharingController.scss';
|
||||
@import './components/CallingSelectPresentingSourcesModal.scss';
|
||||
@import './components/CallingToast.scss';
|
||||
@import './components/CallLinkDetails.scss';
|
||||
@import './components/CallingRaisedHandsList.scss';
|
||||
@import './components/CallingRaisedHandsToasts.scss';
|
||||
@import './components/CallingReactionsToasts.scss';
|
||||
|
|
|
@ -43,6 +43,7 @@ export enum AvatarSize {
|
|||
FORTY = 40,
|
||||
FORTY_EIGHT = 48,
|
||||
FIFTY_TWO = 52,
|
||||
SIXTY_FOUR = 64,
|
||||
EIGHTY = 80,
|
||||
NINETY_SIX = 96,
|
||||
TWO_HUNDRED_SIXTEEN = 216,
|
||||
|
|
107
ts/components/CallLinkDetails.tsx
Normal file
107
ts/components/CallLinkDetails.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React from 'react';
|
||||
import type { CallHistoryGroup } from '../types/CallDisposition';
|
||||
import type { LocalizerType } from '../types/I18N';
|
||||
import { CallHistoryGroupPanelSection } from './conversation/conversation-details/CallHistoryGroupPanelSection';
|
||||
import { PanelSection } from './conversation/conversation-details/PanelSection';
|
||||
import {
|
||||
ConversationDetailsIcon,
|
||||
IconType,
|
||||
} from './conversation/conversation-details/ConversationDetailsIcon';
|
||||
import { PanelRow } from './conversation/conversation-details/PanelRow';
|
||||
import type { CallLinkType } from '../types/CallLink';
|
||||
import { linkCallRoute } from '../util/signalRoutes';
|
||||
import { drop } from '../util/drop';
|
||||
import { Avatar, AvatarSize } from './Avatar';
|
||||
import { Button, ButtonSize, ButtonVariant } from './Button';
|
||||
import { copyCallLink } from '../util/copyLinksWithToast';
|
||||
|
||||
function toUrlWithoutProtocol(url: URL): string {
|
||||
return `${url.hostname}${url.pathname}${url.search}${url.hash}`;
|
||||
}
|
||||
|
||||
export type CallLinkDetailsProps = Readonly<{
|
||||
callHistoryGroup: CallHistoryGroup;
|
||||
callLink: CallLinkType;
|
||||
i18n: LocalizerType;
|
||||
onStartCallLinkLobby: () => void;
|
||||
onShareCallLinkViaSignal: () => void;
|
||||
}>;
|
||||
|
||||
export function CallLinkDetails({
|
||||
callHistoryGroup,
|
||||
callLink,
|
||||
i18n,
|
||||
onStartCallLinkLobby,
|
||||
onShareCallLinkViaSignal,
|
||||
}: CallLinkDetailsProps): JSX.Element {
|
||||
const webUrl = linkCallRoute.toWebUrl({
|
||||
key: callLink.rootKey,
|
||||
});
|
||||
return (
|
||||
<div className="CallLinkDetails__Container">
|
||||
<header className="CallLinkDetails__Header">
|
||||
<Avatar
|
||||
className="CallLinkDetails__HeaderAvatar"
|
||||
i18n={i18n}
|
||||
badge={undefined}
|
||||
conversationType="callLink"
|
||||
size={AvatarSize.SIXTY_FOUR}
|
||||
acceptedMessageRequest
|
||||
isMe={false}
|
||||
sharedGroupNames={[]}
|
||||
title={callLink.name ?? i18n('icu:calling__call-link-default-title')}
|
||||
/>
|
||||
<div className="CallLinkDetails__HeaderDetails">
|
||||
<h1 className="CallLinkDetails__HeaderTitle">
|
||||
{callLink.name === ''
|
||||
? i18n('icu:calling__call-link-default-title')
|
||||
: callLink.name}
|
||||
</h1>
|
||||
<p className="CallLinkDetails__HeaderDescription">
|
||||
{toUrlWithoutProtocol(webUrl)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="CallLinkDetails__HeaderActions">
|
||||
<Button
|
||||
className="CallLinkDetails__HeaderButton"
|
||||
variant={ButtonVariant.SecondaryAffirmative}
|
||||
size={ButtonSize.Small}
|
||||
onClick={onStartCallLinkLobby}
|
||||
>
|
||||
{i18n('icu:CallLinkDetails__Join')}
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
<CallHistoryGroupPanelSection
|
||||
callHistoryGroup={callHistoryGroup}
|
||||
i18n={i18n}
|
||||
/>
|
||||
<PanelSection>
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('icu:CallLinkDetails__CopyLink')}
|
||||
icon={IconType.share}
|
||||
/>
|
||||
}
|
||||
label={i18n('icu:CallLinkDetails__CopyLink')}
|
||||
onClick={() => {
|
||||
drop(copyCallLink(webUrl.toString()));
|
||||
}}
|
||||
/>
|
||||
<PanelRow
|
||||
icon={
|
||||
<ConversationDetailsIcon
|
||||
ariaLabel={i18n('icu:CallLinkDetails__ShareLinkViaSignal')}
|
||||
icon={IconType.forward}
|
||||
/>
|
||||
}
|
||||
label={i18n('icu:CallLinkDetails__ShareLinkViaSignal')}
|
||||
onClick={onShareCallLinkViaSignal}
|
||||
/>
|
||||
</PanelSection>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -100,7 +100,6 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
|
|||
setPresenting: action('toggle-presenting'),
|
||||
setRendererCanvas: action('set-renderer-canvas'),
|
||||
setOutgoingRing: action('set-outgoing-ring'),
|
||||
showToast: action('show-toast'),
|
||||
startCall: action('start-call'),
|
||||
stopRingtone: action('stop-ringtone'),
|
||||
switchToPresentationView: action('switch-to-presentation-view'),
|
||||
|
|
|
@ -53,10 +53,9 @@ import * as log from '../logging/log';
|
|||
import { isGroupOrAdhocActiveCall } from '../util/isGroupOrAdhocCall';
|
||||
import { CallingAdhocCallInfo } from './CallingAdhocCallInfo';
|
||||
import { callLinkRootKeyToUrl } from '../util/callLinkRootKeyToUrl';
|
||||
import { ToastType } from '../types/Toast';
|
||||
import type { ShowToastAction } from '../state/ducks/toast';
|
||||
import { isSharingPhoneNumberWithEverybody } from '../util/phoneNumberSharingMode';
|
||||
import { usePrevious } from '../hooks/usePrevious';
|
||||
import { copyCallLink } from '../util/copyLinksWithToast';
|
||||
|
||||
const GROUP_CALL_RING_DURATION = 60 * 1000;
|
||||
|
||||
|
@ -127,7 +126,6 @@ export type PropsType = {
|
|||
setOutgoingRing: (_: boolean) => void;
|
||||
setPresenting: (_?: PresentedSource) => void;
|
||||
setRendererCanvas: (_: SetRendererCanvasType) => void;
|
||||
showToast: ShowToastAction;
|
||||
stopRingtone: () => unknown;
|
||||
switchToPresentationView: () => void;
|
||||
switchFromPresentationView: () => void;
|
||||
|
@ -186,7 +184,6 @@ function ActiveCallManager({
|
|||
setPresenting,
|
||||
setRendererCanvas,
|
||||
setOutgoingRing,
|
||||
showToast,
|
||||
startCall,
|
||||
switchToPresentationView,
|
||||
switchFromPresentationView,
|
||||
|
@ -266,10 +263,9 @@ function ActiveCallManager({
|
|||
|
||||
const link = callLinkRootKeyToUrl(callLink.rootKey);
|
||||
if (link) {
|
||||
await window.navigator.clipboard.writeText(link);
|
||||
showToast({ toastType: ToastType.CopiedCallLink });
|
||||
await copyCallLink(link);
|
||||
}
|
||||
}, [callLink, showToast]);
|
||||
}, [callLink]);
|
||||
|
||||
let isCallFull: boolean;
|
||||
let showCallLobby: boolean;
|
||||
|
@ -528,7 +524,6 @@ export function CallManager({
|
|||
setOutgoingRing,
|
||||
setPresenting,
|
||||
setRendererCanvas,
|
||||
showToast,
|
||||
startCall,
|
||||
stopRingtone,
|
||||
switchFromPresentationView,
|
||||
|
@ -615,7 +610,6 @@ export function CallManager({
|
|||
setOutgoingRing={setOutgoingRing}
|
||||
setPresenting={setPresenting}
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
showToast={showToast}
|
||||
startCall={startCall}
|
||||
switchFromPresentationView={switchFromPresentationView}
|
||||
switchToPresentationView={switchToPresentationView}
|
||||
|
|
|
@ -53,6 +53,7 @@ import {
|
|||
callLinkToConversation,
|
||||
getPlaceholderCallLinkConversation,
|
||||
} from '../util/callLinks';
|
||||
import type { CallsTabSelectedView } from './CallsTab';
|
||||
import type { CallStateType } from '../state/selectors/calling';
|
||||
import {
|
||||
isGroupOrAdhocCallMode,
|
||||
|
@ -143,10 +144,7 @@ type CallsListProps = Readonly<{
|
|||
selectedCallHistoryGroup: CallHistoryGroup | null;
|
||||
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
||||
onOutgoingVideoCallInConversation: (conversationId: string) => void;
|
||||
onSelectCallHistoryGroup: (
|
||||
conversationId: string,
|
||||
selectedCallHistoryGroup: CallHistoryGroup
|
||||
) => void;
|
||||
onChangeCallsTabSelectedView: (selectedView: CallsTabSelectedView) => void;
|
||||
peekNotConnectedGroupCall: (options: PeekNotConnectedGroupCallType) => void;
|
||||
startCallLinkLobbyByRoomId: (roomId: string) => void;
|
||||
togglePip: () => void;
|
||||
|
@ -184,7 +182,7 @@ export function CallsList({
|
|||
selectedCallHistoryGroup,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
onSelectCallHistoryGroup,
|
||||
onChangeCallsTabSelectedView,
|
||||
peekNotConnectedGroupCall,
|
||||
startCallLinkLobbyByRoomId,
|
||||
togglePip,
|
||||
|
@ -771,13 +769,22 @@ export function CallsList({
|
|||
}
|
||||
onClick={() => {
|
||||
if (isAdhoc) {
|
||||
onChangeCallsTabSelectedView({
|
||||
type: 'callLink',
|
||||
roomId: item.peerId,
|
||||
callHistoryGroup: item,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (conversation == null) {
|
||||
return;
|
||||
}
|
||||
onSelectCallHistoryGroup(conversation.id, item);
|
||||
onChangeCallsTabSelectedView({
|
||||
type: 'conversation',
|
||||
conversationId: conversation.id,
|
||||
callHistoryGroup: item,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -791,7 +798,7 @@ export function CallsList({
|
|||
getIsCallActive,
|
||||
getIsInCall,
|
||||
selectedCallHistoryGroup,
|
||||
onSelectCallHistoryGroup,
|
||||
onChangeCallsTabSelectedView,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
startCallLinkLobbyByRoomId,
|
||||
|
|
|
@ -19,13 +19,14 @@ import { Avatar, AvatarSize } from './Avatar';
|
|||
import { I18n } from './I18n';
|
||||
import { SizeObserver } from '../hooks/useSizeObserver';
|
||||
import { CallType } from '../types/CallDisposition';
|
||||
import type { CallsTabSelectedView } from './CallsTab';
|
||||
import { Tooltip, TooltipPlacement } from './Tooltip';
|
||||
|
||||
type CallsNewCallProps = Readonly<{
|
||||
hasActiveCall: boolean;
|
||||
allConversations: ReadonlyArray<ConversationType>;
|
||||
i18n: LocalizerType;
|
||||
onSelectConversation: (conversationId: string) => void;
|
||||
onChangeCallsTabSelectedView: (selectedView: CallsTabSelectedView) => void;
|
||||
onOutgoingAudioCallInConversation: (conversationId: string) => void;
|
||||
onOutgoingVideoCallInConversation: (conversationId: string) => void;
|
||||
regionCode: string | undefined;
|
||||
|
@ -106,7 +107,7 @@ export function CallsNewCall({
|
|||
hasActiveCall,
|
||||
allConversations,
|
||||
i18n,
|
||||
onSelectConversation,
|
||||
onChangeCallsTabSelectedView,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
regionCode,
|
||||
|
@ -262,7 +263,11 @@ export function CallsNewCall({
|
|||
</div>
|
||||
}
|
||||
onClick={() => {
|
||||
onSelectConversation(item.conversation.id);
|
||||
onChangeCallsTabSelectedView({
|
||||
type: 'conversation',
|
||||
conversationId: item.conversation.id,
|
||||
callHistoryGroup: null,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -272,7 +277,7 @@ export function CallsNewCall({
|
|||
rows,
|
||||
i18n,
|
||||
hasActiveCall,
|
||||
onSelectConversation,
|
||||
onChangeCallsTabSelectedView,
|
||||
onOutgoingAudioCallInConversation,
|
||||
onOutgoingVideoCallInConversation,
|
||||
]
|
||||
|
|
|
@ -57,6 +57,10 @@ type CallsTabProps = Readonly<{
|
|||
onOutgoingVideoCallInConversation: (conversationId: string) => void;
|
||||
peekNotConnectedGroupCall: (options: PeekNotConnectedGroupCallType) => void;
|
||||
preferredLeftPaneWidth: number;
|
||||
renderCallLinkDetails: (
|
||||
roomId: string,
|
||||
callHistoryGroup: CallHistoryGroup
|
||||
) => JSX.Element;
|
||||
renderConversationDetails: (
|
||||
conversationId: string,
|
||||
callHistoryGroup: CallHistoryGroup | null
|
||||
|
@ -70,6 +74,18 @@ type CallsTabProps = Readonly<{
|
|||
togglePip: () => void;
|
||||
}>;
|
||||
|
||||
export type CallsTabSelectedView =
|
||||
| {
|
||||
type: 'conversation';
|
||||
conversationId: string;
|
||||
callHistoryGroup: CallHistoryGroup | null;
|
||||
}
|
||||
| {
|
||||
type: 'callLink';
|
||||
roomId: string;
|
||||
callHistoryGroup: CallHistoryGroup;
|
||||
};
|
||||
|
||||
export function CallsTab({
|
||||
activeCall,
|
||||
allConversations,
|
||||
|
@ -93,6 +109,7 @@ export function CallsTab({
|
|||
onOutgoingVideoCallInConversation,
|
||||
peekNotConnectedGroupCall,
|
||||
preferredLeftPaneWidth,
|
||||
renderCallLinkDetails,
|
||||
renderConversationDetails,
|
||||
renderToastManager,
|
||||
regionCode,
|
||||
|
@ -103,37 +120,31 @@ export function CallsTab({
|
|||
const [sidebarView, setSidebarView] = useState(
|
||||
CallsTabSidebarView.CallsListView
|
||||
);
|
||||
const [selected, setSelected] = useState<{
|
||||
conversationId: string;
|
||||
callHistoryGroup: CallHistoryGroup | null;
|
||||
} | null>(null);
|
||||
const [selectedView, setSelectedViewInner] =
|
||||
useState<CallsTabSelectedView | null>(null);
|
||||
const [selectedViewKey, setSelectedViewKey] = useState(() => 1);
|
||||
|
||||
const [
|
||||
confirmClearCallHistoryDialogOpen,
|
||||
setConfirmClearCallHistoryDialogOpen,
|
||||
] = useState(false);
|
||||
|
||||
const updateSelectedView = useCallback(
|
||||
(nextSelected: CallsTabSelectedView | null) => {
|
||||
setSelectedViewInner(nextSelected);
|
||||
setSelectedViewKey(key => key + 1);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const updateSidebarView = useCallback(
|
||||
(newSidebarView: CallsTabSidebarView) => {
|
||||
setSidebarView(newSidebarView);
|
||||
setSelected(null);
|
||||
updateSelectedView(null);
|
||||
},
|
||||
[]
|
||||
[updateSelectedView]
|
||||
);
|
||||
|
||||
const handleSelectCallHistoryGroup = useCallback(
|
||||
(conversationId: string, callHistoryGroup: CallHistoryGroup) => {
|
||||
setSelected({
|
||||
conversationId,
|
||||
callHistoryGroup,
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSelectConversation = useCallback((conversationId: string) => {
|
||||
setSelected({ conversationId, callHistoryGroup: null });
|
||||
}, []);
|
||||
|
||||
useEscapeHandling(
|
||||
sidebarView === CallsTabSidebarView.NewCallView
|
||||
? () => {
|
||||
|
@ -167,12 +178,12 @@ export function CallsTab({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selected?.callHistoryGroup != null) {
|
||||
selected.callHistoryGroup.children.forEach(child => {
|
||||
onMarkCallHistoryRead(selected.conversationId, child.callId);
|
||||
if (selectedView?.type === 'conversation') {
|
||||
selectedView.callHistoryGroup?.children.forEach(child => {
|
||||
onMarkCallHistoryRead(selectedView.conversationId, child.callId);
|
||||
});
|
||||
}
|
||||
}, [selected, onMarkCallHistoryRead]);
|
||||
}, [selectedView, onMarkCallHistoryRead]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -255,8 +266,8 @@ export function CallsTab({
|
|||
getConversation={getConversation}
|
||||
hangUpActiveCall={hangUpActiveCall}
|
||||
i18n={i18n}
|
||||
selectedCallHistoryGroup={selected?.callHistoryGroup ?? null}
|
||||
onSelectCallHistoryGroup={handleSelectCallHistoryGroup}
|
||||
selectedCallHistoryGroup={selectedView?.callHistoryGroup ?? null}
|
||||
onChangeCallsTabSelectedView={updateSelectedView}
|
||||
onOutgoingAudioCallInConversation={
|
||||
handleOutgoingAudioCallInConversation
|
||||
}
|
||||
|
@ -275,7 +286,7 @@ export function CallsTab({
|
|||
allConversations={allConversations}
|
||||
i18n={i18n}
|
||||
regionCode={regionCode}
|
||||
onSelectConversation={handleSelectConversation}
|
||||
onChangeCallsTabSelectedView={updateSelectedView}
|
||||
onOutgoingAudioCallInConversation={
|
||||
handleOutgoingAudioCallInConversation
|
||||
}
|
||||
|
@ -285,7 +296,7 @@ export function CallsTab({
|
|||
/>
|
||||
)}
|
||||
</NavSidebar>
|
||||
{selected == null ? (
|
||||
{selectedView == null ? (
|
||||
<div className="CallsTab__EmptyState">
|
||||
<div className="CallsTab__EmptyStateIcon" />
|
||||
<p className="CallsTab__EmptyStateLabel">
|
||||
|
@ -295,13 +306,19 @@ export function CallsTab({
|
|||
) : (
|
||||
<div
|
||||
className="CallsTab__ConversationCallDetails"
|
||||
// Force scrolling to top when a new conversation is selected.
|
||||
key={selected.conversationId}
|
||||
// Force scrolling to top when selection changes
|
||||
key={selectedViewKey}
|
||||
>
|
||||
{renderConversationDetails(
|
||||
selected.conversationId,
|
||||
selected.callHistoryGroup
|
||||
)}
|
||||
{selectedView.type === 'conversation' &&
|
||||
renderConversationDetails(
|
||||
selectedView.conversationId,
|
||||
selectedView.callHistoryGroup
|
||||
)}
|
||||
{selectedView.type === 'callLink' &&
|
||||
renderCallLinkDetails(
|
||||
selectedView.roomId,
|
||||
selectedView.callHistoryGroup
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -72,6 +72,8 @@ import type { SmartCompositionRecordingProps } from '../state/smart/CompositionR
|
|||
import SelectModeActions from './conversation/SelectModeActions';
|
||||
import type { ShowToastAction } from '../state/ducks/toast';
|
||||
import type { DraftEditMessageType } from '../model-types.d';
|
||||
import type { ForwardMessagesPayload } from '../state/ducks/globalModals';
|
||||
import { ForwardMessagesModalType } from './ForwardMessagesModal';
|
||||
|
||||
export type OwnProps = Readonly<{
|
||||
acceptedMessageRequest: boolean | null;
|
||||
|
@ -181,7 +183,7 @@ export type OwnProps = Readonly<{
|
|||
selectedMessageIds: ReadonlyArray<string> | undefined;
|
||||
toggleSelectMode: (on: boolean) => void;
|
||||
toggleForwardMessagesModal: (
|
||||
messageIds: ReadonlyArray<string>,
|
||||
payload: ForwardMessagesPayload,
|
||||
onForward: () => void
|
||||
) => void;
|
||||
}>;
|
||||
|
@ -725,9 +727,15 @@ export const CompositionArea = memo(function CompositionArea({
|
|||
}}
|
||||
onForwardMessages={() => {
|
||||
if (selectedMessageIds.length > 0) {
|
||||
toggleForwardMessagesModal(selectedMessageIds, () => {
|
||||
toggleSelectMode(false);
|
||||
});
|
||||
toggleForwardMessagesModal(
|
||||
{
|
||||
type: ForwardMessagesModalType.Forward,
|
||||
messageIds: selectedMessageIds,
|
||||
},
|
||||
() => {
|
||||
toggleSelectMode(false);
|
||||
}
|
||||
);
|
||||
}
|
||||
}}
|
||||
showToast={showToast}
|
||||
|
|
|
@ -7,7 +7,10 @@ import type { Meta } from '@storybook/react';
|
|||
import enMessages from '../../_locales/en/messages.json';
|
||||
import type { AttachmentType } from '../types/Attachment';
|
||||
import type { PropsType } from './ForwardMessagesModal';
|
||||
import { ForwardMessagesModal } from './ForwardMessagesModal';
|
||||
import {
|
||||
ForwardMessagesModal,
|
||||
ForwardMessagesModalType,
|
||||
} from './ForwardMessagesModal';
|
||||
import { IMAGE_JPEG, VIDEO_MP4, stringToMIMEType } from '../types/MIME';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
|
@ -67,6 +70,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
/>
|
||||
),
|
||||
showToast: action('showToast'),
|
||||
type: ForwardMessagesModalType.Forward,
|
||||
theme: React.useContext(StorybookThemeContext),
|
||||
regionCode: 'US',
|
||||
});
|
||||
|
|
|
@ -42,6 +42,12 @@ import {
|
|||
isDraftForwardable,
|
||||
type MessageForwardDraft,
|
||||
} from '../types/ForwardDraft';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
|
||||
export enum ForwardMessagesModalType {
|
||||
Forward,
|
||||
ShareCallLink,
|
||||
}
|
||||
|
||||
export type DataPropsType = {
|
||||
candidateConversations: ReadonlyArray<ConversationType>;
|
||||
|
@ -63,6 +69,7 @@ export type DataPropsType = {
|
|||
) => unknown;
|
||||
regionCode: string | undefined;
|
||||
RenderCompositionTextArea: ComponentType<SmartCompositionTextAreaProps>;
|
||||
type: ForwardMessagesModalType;
|
||||
showToast: ShowToastAction;
|
||||
theme: ThemeType;
|
||||
};
|
||||
|
@ -76,6 +83,7 @@ export type PropsType = DataPropsType & ActionPropsType;
|
|||
const MAX_FORWARD = 5;
|
||||
|
||||
export function ForwardMessagesModal({
|
||||
type,
|
||||
drafts,
|
||||
candidateConversations,
|
||||
doForwardMessages,
|
||||
|
@ -292,6 +300,15 @@ export function ForwardMessagesModal({
|
|||
</div>
|
||||
);
|
||||
|
||||
let title: string;
|
||||
if (type === ForwardMessagesModalType.Forward) {
|
||||
title = i18n('icu:ForwardMessageModal__title');
|
||||
} else if (type === ForwardMessagesModalType.ShareCallLink) {
|
||||
title = i18n('icu:ForwardMessageModal__ShareCallLink');
|
||||
} else {
|
||||
throw missingCaseError(type);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{cannotMessage && (
|
||||
|
@ -311,7 +328,7 @@ export function ForwardMessagesModal({
|
|||
onClose={onClose}
|
||||
onBackButtonClick={isEditingMessage ? handleBackOrClose : undefined}
|
||||
moduleClassName="module-ForwardMessageModal"
|
||||
title={i18n('icu:ForwardMessageModal__title')}
|
||||
title={title}
|
||||
useFocusTrap={false}
|
||||
padded={false}
|
||||
modalFooter={footer}
|
||||
|
|
|
@ -28,6 +28,8 @@ import { usePrevious } from '../hooks/usePrevious';
|
|||
import { arrow } from '../util/keyboard';
|
||||
import { drop } from '../util/drop';
|
||||
import { isCmdOrCtrl } from '../hooks/useKeyboardShortcuts';
|
||||
import type { ForwardMessagesPayload } from '../state/ducks/globalModals';
|
||||
import { ForwardMessagesModalType } from './ForwardMessagesModal';
|
||||
|
||||
export type PropsType = {
|
||||
children?: ReactNode;
|
||||
|
@ -39,7 +41,7 @@ export type PropsType = {
|
|||
playbackDisabled: boolean;
|
||||
saveAttachment: SaveAttachmentActionCreatorType;
|
||||
selectedIndex: number;
|
||||
toggleForwardMessagesModal: (messageIds: ReadonlyArray<string>) => unknown;
|
||||
toggleForwardMessagesModal: (payload: ForwardMessagesPayload) => unknown;
|
||||
onMediaPlaybackStart: () => void;
|
||||
onNextAttachment: () => void;
|
||||
onPrevAttachment: () => void;
|
||||
|
@ -195,7 +197,10 @@ export function Lightbox({
|
|||
|
||||
closeLightbox();
|
||||
const mediaItem = media[selectedIndex];
|
||||
toggleForwardMessagesModal([mediaItem.message.id]);
|
||||
toggleForwardMessagesModal({
|
||||
type: ForwardMessagesModalType.Forward,
|
||||
messageIds: [mediaItem.message.id],
|
||||
});
|
||||
};
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
|
|
|
@ -32,13 +32,17 @@ import {
|
|||
useToggleReactionPicker,
|
||||
} from '../../hooks/useKeyboardShortcuts';
|
||||
import { PanelType } from '../../types/Panels';
|
||||
import type { DeleteMessagesPropsType } from '../../state/ducks/globalModals';
|
||||
import type {
|
||||
DeleteMessagesPropsType,
|
||||
ForwardMessagesPayload,
|
||||
} from '../../state/ducks/globalModals';
|
||||
import { useScrollerLock } from '../../hooks/useScrollLock';
|
||||
import {
|
||||
type ContextMenuTriggerType,
|
||||
MessageContextMenu,
|
||||
useHandleMessageContextMenu,
|
||||
} from './MessageContextMenu';
|
||||
import { ForwardMessagesModalType } from '../ForwardMessagesModal';
|
||||
|
||||
export type PropsData = {
|
||||
canDownload: boolean;
|
||||
|
@ -55,7 +59,7 @@ export type PropsData = {
|
|||
export type PropsActions = {
|
||||
pushPanelForConversation: PushPanelForConversationActionType;
|
||||
toggleDeleteMessagesModal: (props: DeleteMessagesPropsType) => void;
|
||||
toggleForwardMessagesModal: (messageIds: Array<string>) => void;
|
||||
toggleForwardMessagesModal: (payload: ForwardMessagesPayload) => void;
|
||||
reactToMessage: (
|
||||
id: string,
|
||||
{ emoji, remove }: { emoji: string; remove: boolean }
|
||||
|
@ -372,7 +376,13 @@ export function TimelineMessage(props: Props): JSX.Element {
|
|||
onCopy={canCopy ? () => copyMessageText(id) : undefined}
|
||||
onSelect={() => toggleSelectMessage(conversationId, id, false, true)}
|
||||
onForward={
|
||||
canForward ? () => toggleForwardMessagesModal([id]) : undefined
|
||||
canForward
|
||||
? () =>
|
||||
toggleForwardMessagesModal({
|
||||
type: ForwardMessagesModalType.Forward,
|
||||
messageIds: [id],
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
onDeleteMessage={() => {
|
||||
toggleDeleteMessagesModal({
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import type { CallStatus } from '../../../types/CallDisposition';
|
||||
import {
|
||||
CallDirection,
|
||||
CallType,
|
||||
DirectCallStatus,
|
||||
GroupCallStatus,
|
||||
type CallHistoryGroup,
|
||||
} from '../../../types/CallDisposition';
|
||||
import type { LocalizerType } from '../../../types/I18N';
|
||||
import { formatDate, formatTime } from '../../../util/timestamp';
|
||||
import { PanelSection } from './PanelSection';
|
||||
|
||||
function describeCallHistory(
|
||||
i18n: LocalizerType,
|
||||
type: CallType,
|
||||
direction: CallDirection,
|
||||
status: CallStatus
|
||||
): string {
|
||||
if (type === CallType.Adhoc) {
|
||||
return i18n('icu:CallHistory__Description--Adhoc');
|
||||
}
|
||||
|
||||
if (status === DirectCallStatus.Missed || status === GroupCallStatus.Missed) {
|
||||
if (direction === CallDirection.Incoming) {
|
||||
return i18n('icu:CallHistory__Description--Missed', { type });
|
||||
}
|
||||
return i18n('icu:CallHistory__Description--Unanswered', { type });
|
||||
}
|
||||
if (
|
||||
status === DirectCallStatus.Declined ||
|
||||
status === GroupCallStatus.Declined
|
||||
) {
|
||||
return i18n('icu:CallHistory__Description--Declined', { type });
|
||||
}
|
||||
return i18n('icu:CallHistory__Description--Default', { type, direction });
|
||||
}
|
||||
|
||||
export type CallHistoryPanelSectionProps = Readonly<{
|
||||
callHistoryGroup: CallHistoryGroup;
|
||||
i18n: LocalizerType;
|
||||
}>;
|
||||
|
||||
export function CallHistoryGroupPanelSection({
|
||||
callHistoryGroup,
|
||||
i18n,
|
||||
}: CallHistoryPanelSectionProps): JSX.Element {
|
||||
return (
|
||||
<PanelSection title={formatDate(i18n, callHistoryGroup.timestamp)}>
|
||||
<ol className="ConversationDetails__CallHistoryGroup__List">
|
||||
{callHistoryGroup.children.map(child => {
|
||||
return (
|
||||
<li
|
||||
key={child.callId}
|
||||
className="ConversationDetails__CallHistoryGroup__Item"
|
||||
>
|
||||
<span
|
||||
className={classNames(
|
||||
'ConversationDetails__CallHistoryGroup__ItemIcon',
|
||||
{
|
||||
'ConversationDetails__CallHistoryGroup__ItemIcon--Audio':
|
||||
callHistoryGroup.type === CallType.Audio,
|
||||
'ConversationDetails__CallHistoryGroup__ItemIcon--Video':
|
||||
callHistoryGroup.type === CallType.Video ||
|
||||
callHistoryGroup.type === CallType.Group,
|
||||
'ConversationDetails__CallHistoryGroup__ItemIcon--Adhoc':
|
||||
callHistoryGroup.type === CallType.Adhoc,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<span className="ConversationDetails__CallHistoryGroup__ItemLabel">
|
||||
{describeCallHistory(
|
||||
i18n,
|
||||
callHistoryGroup.type,
|
||||
callHistoryGroup.direction,
|
||||
callHistoryGroup.status
|
||||
)}
|
||||
</span>
|
||||
<span className="ConversationDetails__CallHistoryGroup__ItemTimestamp">
|
||||
{formatTime(i18n, child.timestamp, Date.now(), false)}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</PanelSection>
|
||||
);
|
||||
}
|
|
@ -4,7 +4,6 @@
|
|||
import type { ReactNode } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Button, ButtonIconType, ButtonVariant } from '../../Button';
|
||||
import { Tooltip } from '../../Tooltip';
|
||||
import type {
|
||||
|
@ -53,43 +52,11 @@ import type {
|
|||
import { isConversationMuted } from '../../../util/isConversationMuted';
|
||||
import { ConversationDetailsGroups } from './ConversationDetailsGroups';
|
||||
import { PanelType } from '../../../types/Panels';
|
||||
import type { CallStatus } from '../../../types/CallDisposition';
|
||||
import {
|
||||
CallType,
|
||||
type CallHistoryGroup,
|
||||
CallDirection,
|
||||
DirectCallStatus,
|
||||
GroupCallStatus,
|
||||
} from '../../../types/CallDisposition';
|
||||
import { formatDate, formatTime } from '../../../util/timestamp';
|
||||
import { type CallHistoryGroup } from '../../../types/CallDisposition';
|
||||
import { NavTab } from '../../../state/ducks/nav';
|
||||
import { ContextMenu } from '../../ContextMenu';
|
||||
import { canHaveNicknameAndNote } from '../../../util/nicknames';
|
||||
|
||||
function describeCallHistory(
|
||||
i18n: LocalizerType,
|
||||
type: CallType,
|
||||
direction: CallDirection,
|
||||
status: CallStatus
|
||||
): string {
|
||||
if (type === CallType.Adhoc) {
|
||||
return i18n('icu:CallHistory__Description--Adhoc');
|
||||
}
|
||||
|
||||
if (status === DirectCallStatus.Missed || status === GroupCallStatus.Missed) {
|
||||
if (direction === CallDirection.Incoming) {
|
||||
return i18n('icu:CallHistory__Description--Missed', { type });
|
||||
}
|
||||
return i18n('icu:CallHistory__Description--Unanswered', { type });
|
||||
}
|
||||
if (
|
||||
status === DirectCallStatus.Declined ||
|
||||
status === GroupCallStatus.Declined
|
||||
) {
|
||||
return i18n('icu:CallHistory__Description--Declined', { type });
|
||||
}
|
||||
return i18n('icu:CallHistory__Description--Default', { type, direction });
|
||||
}
|
||||
import { CallHistoryGroupPanelSection } from './CallHistoryGroupPanelSection';
|
||||
|
||||
enum ModalState {
|
||||
AddingGroupMembers,
|
||||
|
@ -501,41 +468,10 @@ export function ConversationDetails({
|
|||
</div>
|
||||
|
||||
{callHistoryGroup && (
|
||||
<PanelSection title={formatDate(i18n, callHistoryGroup.timestamp)}>
|
||||
<ol className="ConversationDetails__CallHistoryGroup__List">
|
||||
{callHistoryGroup.children.map(child => {
|
||||
return (
|
||||
<li
|
||||
key={child.callId}
|
||||
className="ConversationDetails__CallHistoryGroup__Item"
|
||||
>
|
||||
<span
|
||||
className={classNames(
|
||||
'ConversationDetails__CallHistoryGroup__ItemIcon',
|
||||
{
|
||||
'ConversationDetails__CallHistoryGroup__ItemIcon--Audio':
|
||||
callHistoryGroup.type === CallType.Audio,
|
||||
'ConversationDetails__CallHistoryGroup__ItemIcon--Video':
|
||||
callHistoryGroup.type !== CallType.Audio,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<span className="ConversationDetails__CallHistoryGroup__ItemLabel">
|
||||
{describeCallHistory(
|
||||
i18n,
|
||||
callHistoryGroup.type,
|
||||
callHistoryGroup.direction,
|
||||
callHistoryGroup.status
|
||||
)}
|
||||
</span>
|
||||
<span className="ConversationDetails__CallHistoryGroup__ItemTimestamp">
|
||||
{formatTime(i18n, child.timestamp, Date.now(), false)}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</PanelSection>
|
||||
<CallHistoryGroupPanelSection
|
||||
callHistoryGroup={callHistoryGroup}
|
||||
i18n={i18n}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PanelSection>
|
||||
|
|
|
@ -13,6 +13,7 @@ export enum IconType {
|
|||
'unblock' = 'unblock',
|
||||
'color' = 'color',
|
||||
'down' = 'down',
|
||||
'forward' = 'forward',
|
||||
'invites' = 'invites',
|
||||
'leave' = 'leave',
|
||||
'link' = 'link',
|
||||
|
|
|
@ -13,7 +13,7 @@ import { PanelSection } from './PanelSection';
|
|||
import { Select } from '../../Select';
|
||||
import { SignalService as Proto } from '../../../protobuf';
|
||||
|
||||
import { copyGroupLink } from '../../../util/copyGroupLink';
|
||||
import { copyGroupLink } from '../../../util/copyLinksWithToast';
|
||||
import { useDelayedRestoreFocus } from '../../../hooks/useRestoreFocus';
|
||||
import { useUniqueId } from '../../../hooks/useUniqueId';
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { drop } from '../util/drop';
|
|||
import { matchOrQueryFocusable } from '../util/focusableSelectors';
|
||||
import { getQuotedMessageSelector } from '../state/selectors/composer';
|
||||
import { removeLinkPreview } from './LinkPreview';
|
||||
import { ForwardMessagesModalType } from '../components/ForwardMessagesModal';
|
||||
|
||||
export function addGlobalKeyboardShortcuts(): void {
|
||||
const isMacOS = window.platform === 'darwin';
|
||||
|
@ -492,7 +493,7 @@ export function addGlobalKeyboardShortcuts(): void {
|
|||
event.stopPropagation();
|
||||
|
||||
window.reduxActions.globalModals.toggleForwardMessagesModal(
|
||||
messageIds,
|
||||
{ type: ForwardMessagesModalType.Forward, messageIds },
|
||||
() => {
|
||||
if (selectedMessageIds != null) {
|
||||
window.reduxActions.conversations.toggleSelectMode(false);
|
||||
|
|
|
@ -3772,8 +3772,6 @@ function getCallHistoryGroupDataSync(
|
|||
-- Desktop Constraints:
|
||||
AND callsHistory.status IS c.status
|
||||
AND ${filterClause}
|
||||
-- Skip grouping logic for adhoc calls
|
||||
AND callsHistory.mode IS NOT ${CALL_MODE_ADHOC}
|
||||
ORDER BY timestamp DESC
|
||||
) as possibleChildren,
|
||||
|
||||
|
@ -3893,17 +3891,21 @@ async function getCallHistoryGroups(
|
|||
})
|
||||
.reverse()
|
||||
.map(group => {
|
||||
const { possibleChildren, inPeriod, ...rest } = group;
|
||||
const { possibleChildren, inPeriod, type, ...rest } = group;
|
||||
const children = [];
|
||||
|
||||
for (const child of possibleChildren) {
|
||||
if (!taken.has(child.callId) && inPeriod.has(child.callId)) {
|
||||
children.push(child);
|
||||
taken.add(child.callId);
|
||||
if (type === CallType.Adhoc) {
|
||||
// By spec, limit adhoc call history to the most recent call
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return callHistoryGroupSchema.parse({ ...rest, children });
|
||||
return callHistoryGroupSchema.parse({ ...rest, type, children });
|
||||
})
|
||||
.reverse();
|
||||
}
|
||||
|
|
|
@ -79,7 +79,9 @@ export async function getGroupSendCombinedEndorsementExpiration(
|
|||
SELECT expiration FROM groupSendCombinedEndorsement
|
||||
WHERE groupId = ${groupId};
|
||||
`;
|
||||
const value = prepare(db, selectGroup).pluck().get(selectGroupParams);
|
||||
const value = prepare<Array<unknown>>(db, selectGroup)
|
||||
.pluck()
|
||||
.get(selectGroupParams);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -37,6 +37,14 @@ import {
|
|||
import { isDownloaded } from '../../types/Attachment';
|
||||
import type { ButtonVariant } from '../../components/Button';
|
||||
import type { MessageRequestState } from '../../components/conversation/MessageRequestActionsConfirmation';
|
||||
import type { MessageForwardDraft } from '../../types/ForwardDraft';
|
||||
import { hydrateRanges } from '../../types/BodyRange';
|
||||
import {
|
||||
getConversationSelector,
|
||||
type GetConversationByIdType,
|
||||
} from '../selectors/conversations';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { ForwardMessagesModalType } from '../../components/ForwardMessagesModal';
|
||||
|
||||
// State
|
||||
|
||||
|
@ -53,7 +61,8 @@ export type DeleteMessagesPropsType = ReadonlyDeep<{
|
|||
}>;
|
||||
export type ForwardMessagePropsType = ReadonlyDeep<MessagePropsType>;
|
||||
export type ForwardMessagesPropsType = ReadonlyDeep<{
|
||||
messages: Array<ForwardMessagePropsType>;
|
||||
type: ForwardMessagesModalType;
|
||||
messageDrafts: Array<MessageForwardDraft>;
|
||||
onForward?: () => void;
|
||||
}>;
|
||||
export type MessageRequestActionsConfirmationPropsType = ReadonlyDeep<{
|
||||
|
@ -522,8 +531,34 @@ function toggleDeleteMessagesModal(
|
|||
};
|
||||
}
|
||||
|
||||
function toMessageForwardDraft(
|
||||
props: ForwardMessagePropsType,
|
||||
getConversation: GetConversationByIdType
|
||||
): MessageForwardDraft {
|
||||
return {
|
||||
attachments: props.attachments ?? [],
|
||||
bodyRanges: hydrateRanges(props.bodyRanges, getConversation),
|
||||
hasContact: Boolean(props.contact),
|
||||
isSticker: Boolean(props.isSticker),
|
||||
messageBody: props.text,
|
||||
originalMessageId: props.id,
|
||||
previews: props.previews ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
export type ForwardMessagesPayload = ReadonlyDeep<
|
||||
| {
|
||||
type: ForwardMessagesModalType.Forward;
|
||||
messageIds: ReadonlyArray<string>;
|
||||
}
|
||||
| {
|
||||
type: ForwardMessagesModalType.ShareCallLink;
|
||||
draft: MessageForwardDraft;
|
||||
}
|
||||
>;
|
||||
|
||||
function toggleForwardMessagesModal(
|
||||
messageIds?: ReadonlyArray<string>,
|
||||
payload: ForwardMessagesPayload | null,
|
||||
onForward?: () => void
|
||||
): ThunkAction<
|
||||
void,
|
||||
|
@ -532,7 +567,7 @@ function toggleForwardMessagesModal(
|
|||
ToggleForwardMessagesModalActionType
|
||||
> {
|
||||
return async (dispatch, getState) => {
|
||||
if (!messageIds) {
|
||||
if (payload == null) {
|
||||
dispatch({
|
||||
type: TOGGLE_FORWARD_MESSAGES_MODAL,
|
||||
payload: undefined,
|
||||
|
@ -540,31 +575,46 @@ function toggleForwardMessagesModal(
|
|||
return;
|
||||
}
|
||||
|
||||
const messagesProps = await Promise.all(
|
||||
messageIds.map(async messageId => {
|
||||
const messageAttributes = await window.MessageCache.resolveAttributes(
|
||||
'toggleForwardMessagesModal',
|
||||
messageId
|
||||
);
|
||||
let messageDrafts: ReadonlyArray<MessageForwardDraft>;
|
||||
|
||||
const { attachments = [] } = messageAttributes;
|
||||
|
||||
if (!attachments.every(isDownloaded)) {
|
||||
dispatch(
|
||||
conversationsActions.kickOffAttachmentDownload({ messageId })
|
||||
if (payload.type === ForwardMessagesModalType.Forward) {
|
||||
messageDrafts = await Promise.all(
|
||||
payload.messageIds.map(async messageId => {
|
||||
const messageAttributes = await window.MessageCache.resolveAttributes(
|
||||
'toggleForwardMessagesModal',
|
||||
messageId
|
||||
);
|
||||
}
|
||||
|
||||
const messagePropsSelector = getMessagePropsSelector(getState());
|
||||
const messageProps = messagePropsSelector(messageAttributes);
|
||||
const { attachments = [] } = messageAttributes;
|
||||
|
||||
return messageProps;
|
||||
})
|
||||
);
|
||||
if (!attachments.every(isDownloaded)) {
|
||||
dispatch(
|
||||
conversationsActions.kickOffAttachmentDownload({ messageId })
|
||||
);
|
||||
}
|
||||
|
||||
const state = getState();
|
||||
const messagePropsSelector = getMessagePropsSelector(state);
|
||||
const conversationSelector = getConversationSelector(state);
|
||||
|
||||
const messageProps = messagePropsSelector(messageAttributes);
|
||||
const messageDraft = toMessageForwardDraft(
|
||||
messageProps,
|
||||
conversationSelector
|
||||
);
|
||||
|
||||
return messageDraft;
|
||||
})
|
||||
);
|
||||
} else if (payload.type === ForwardMessagesModalType.ShareCallLink) {
|
||||
messageDrafts = [payload.draft];
|
||||
} else {
|
||||
throw missingCaseError(payload);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: TOGGLE_FORWARD_MESSAGES_MODAL,
|
||||
payload: { messages: messagesProps, onForward },
|
||||
payload: { type: payload.type, messageDrafts, onForward },
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -802,15 +852,15 @@ function closeEditHistoryModal(): CloseEditHistoryModalActionType {
|
|||
}
|
||||
|
||||
function copyOverMessageAttributesIntoForwardMessages(
|
||||
messagesProps: ReadonlyArray<ForwardMessagePropsType>,
|
||||
messageDrafts: ReadonlyArray<MessageForwardDraft>,
|
||||
attributes: ReadonlyDeep<MessageAttributesType>
|
||||
): ReadonlyArray<ForwardMessagePropsType> {
|
||||
return messagesProps.map(messageProps => {
|
||||
if (messageProps.id !== attributes.id) {
|
||||
return messageProps;
|
||||
): ReadonlyArray<MessageForwardDraft> {
|
||||
return messageDrafts.map(messageDraft => {
|
||||
if (messageDraft.originalMessageId !== attributes.id) {
|
||||
return messageDraft;
|
||||
}
|
||||
return {
|
||||
...messageProps,
|
||||
...messageDraft,
|
||||
attachments: attributes.attachments,
|
||||
};
|
||||
});
|
||||
|
@ -1078,8 +1128,8 @@ export function reducer(
|
|||
if (state.forwardMessagesProps != null) {
|
||||
if (action.type === MESSAGE_CHANGED) {
|
||||
if (
|
||||
!state.forwardMessagesProps.messages.some(message => {
|
||||
return message.id === action.payload.id;
|
||||
!state.forwardMessagesProps.messageDrafts.some(message => {
|
||||
return message.originalMessageId === action.payload.id;
|
||||
})
|
||||
) {
|
||||
return state;
|
||||
|
@ -1089,8 +1139,8 @@ export function reducer(
|
|||
...state,
|
||||
forwardMessagesProps: {
|
||||
...state.forwardMessagesProps,
|
||||
messages: copyOverMessageAttributesIntoForwardMessages(
|
||||
state.forwardMessagesProps.messages,
|
||||
messageDrafts: copyOverMessageAttributesIntoForwardMessages(
|
||||
state.forwardMessagesProps.messageDrafts,
|
||||
action.payload.data
|
||||
),
|
||||
},
|
||||
|
|
82
ts/state/smart/CallLinkDetails.tsx
Normal file
82
ts/state/smart/CallLinkDetails.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import type { CallHistoryGroup } from '../../types/CallDisposition';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { CallLinkDetails } from '../../components/CallLinkDetails';
|
||||
import { getCallLinkSelector } from '../selectors/calling';
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { useCallingActions } from '../ducks/calling';
|
||||
import * as log from '../../logging/log';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import { linkCallRoute } from '../../util/signalRoutes';
|
||||
import { ForwardMessagesModalType } from '../../components/ForwardMessagesModal';
|
||||
|
||||
export type SmartCallLinkDetailsProps = Readonly<{
|
||||
roomId: string;
|
||||
callHistoryGroup: CallHistoryGroup;
|
||||
}>;
|
||||
|
||||
export const SmartCallLinkDetails = memo(function SmartCallLinkDetails({
|
||||
roomId,
|
||||
callHistoryGroup,
|
||||
}: SmartCallLinkDetailsProps) {
|
||||
const i18n = useSelector(getIntl);
|
||||
const callLinkSelector = useSelector(getCallLinkSelector);
|
||||
const { startCallLinkLobby } = useCallingActions();
|
||||
const { toggleForwardMessagesModal } = useGlobalModalActions();
|
||||
|
||||
const callLink = callLinkSelector(roomId);
|
||||
|
||||
const handleShareCallLinkViaSignal = useCallback(() => {
|
||||
strictAssert(callLink != null, 'callLink not found');
|
||||
const url = linkCallRoute
|
||||
.toWebUrl({
|
||||
key: callLink.rootKey,
|
||||
})
|
||||
.toString();
|
||||
toggleForwardMessagesModal({
|
||||
type: ForwardMessagesModalType.ShareCallLink,
|
||||
draft: {
|
||||
originalMessageId: null,
|
||||
hasContact: false,
|
||||
isSticker: false,
|
||||
previews: [
|
||||
{
|
||||
title: callLink.name,
|
||||
url,
|
||||
isCallLink: true,
|
||||
},
|
||||
],
|
||||
messageBody: i18n(
|
||||
'icu:ShareCallLinkViaSignal__DraftMessageText',
|
||||
{
|
||||
url,
|
||||
},
|
||||
{ textIsBidiFreeSkipNormalization: true }
|
||||
),
|
||||
},
|
||||
});
|
||||
}, [callLink, i18n, toggleForwardMessagesModal]);
|
||||
|
||||
const handleStartCallLinkLobby = useCallback(() => {
|
||||
strictAssert(callLink != null, 'callLink not found');
|
||||
startCallLinkLobby({ rootKey: callLink.rootKey });
|
||||
}, [callLink, startCallLinkLobby]);
|
||||
|
||||
if (callLink == null) {
|
||||
log.error(`SmartCallLinkDetails: callLink not found for room ${roomId}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CallLinkDetails
|
||||
callHistoryGroup={callHistoryGroup}
|
||||
callLink={callLink}
|
||||
i18n={i18n}
|
||||
onStartCallLinkLobby={handleStartCallLinkLobby}
|
||||
onShareCallLinkViaSignal={handleShareCallLinkViaSignal}
|
||||
/>
|
||||
);
|
||||
});
|
|
@ -42,7 +42,6 @@ import { missingCaseError } from '../../util/missingCaseError';
|
|||
import { useAudioPlayerActions } from '../ducks/audioPlayer';
|
||||
import { getActiveCall, useCallingActions } from '../ducks/calling';
|
||||
import type { ConversationType } from '../ducks/conversations';
|
||||
import { useToastActions } from '../ducks/toast';
|
||||
import type { StateType } from '../reducer';
|
||||
import { getHasInitialLoadCompleted } from '../selectors/app';
|
||||
import {
|
||||
|
@ -453,7 +452,6 @@ export const SmartCallManager = memo(function SmartCallManager() {
|
|||
toggleScreenRecordingPermissionsDialog,
|
||||
toggleSettings,
|
||||
} = useCallingActions();
|
||||
const { showToast } = useToastActions();
|
||||
const { pauseVoiceNotePlayer } = useAudioPlayerActions();
|
||||
|
||||
return (
|
||||
|
@ -497,7 +495,6 @@ export const SmartCallManager = memo(function SmartCallManager() {
|
|||
setOutgoingRing={setOutgoingRing}
|
||||
setPresenting={setPresenting}
|
||||
setRendererCanvas={setRendererCanvas}
|
||||
showToast={showToast}
|
||||
startCall={startCall}
|
||||
stopRingtone={stopRingtone}
|
||||
switchFromPresentationView={switchFromPresentationView}
|
||||
|
|
|
@ -37,6 +37,7 @@ import { getCallHistoryEdition } from '../selectors/callHistory';
|
|||
import { getHasPendingUpdate } from '../selectors/updates';
|
||||
import { getHasAnyFailedStorySends } from '../selectors/stories';
|
||||
import { getOtherTabsUnreadStats } from '../selectors/nav';
|
||||
import { SmartCallLinkDetails } from './CallLinkDetails';
|
||||
import type { CallLinkType } from '../../types/CallLink';
|
||||
import { filterCallLinks } from '../../util/filterCallLinks';
|
||||
|
||||
|
@ -101,6 +102,15 @@ function getCallHistoryFilter({
|
|||
};
|
||||
}
|
||||
|
||||
function renderCallLinkDetails(
|
||||
roomId: string,
|
||||
callHistoryGroup: CallHistoryGroup
|
||||
): JSX.Element {
|
||||
return (
|
||||
<SmartCallLinkDetails roomId={roomId} callHistoryGroup={callHistoryGroup} />
|
||||
);
|
||||
}
|
||||
|
||||
function renderConversationDetails(
|
||||
conversationId: string,
|
||||
callHistoryGroup: CallHistoryGroup | null
|
||||
|
@ -225,6 +235,7 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
|
|||
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
|
||||
peekNotConnectedGroupCall={peekNotConnectedGroupCall}
|
||||
preferredLeftPaneWidth={preferredLeftPaneWidth}
|
||||
renderCallLinkDetails={renderCallLinkDetails}
|
||||
renderConversationDetails={renderConversationDetails}
|
||||
renderToastManager={renderToastManager}
|
||||
regionCode={regionCode}
|
||||
|
|
|
@ -3,19 +3,12 @@
|
|||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import type {
|
||||
ForwardMessagePropsType,
|
||||
ForwardMessagesPropsType,
|
||||
} from '../ducks/globalModals';
|
||||
import type { ForwardMessagesPropsType } from '../ducks/globalModals';
|
||||
import * as log from '../../logging/log';
|
||||
import { ForwardMessagesModal } from '../../components/ForwardMessagesModal';
|
||||
import { LinkPreviewSourceType } from '../../types/LinkPreview';
|
||||
import * as Errors from '../../types/errors';
|
||||
import type { GetConversationByIdType } from '../selectors/conversations';
|
||||
import {
|
||||
getAllComposableConversations,
|
||||
getConversationSelector,
|
||||
} from '../selectors/conversations';
|
||||
import { getAllComposableConversations } from '../selectors/conversations';
|
||||
import { getIntl, getTheme, getRegionCode } from '../selectors/user';
|
||||
import { getLinkPreview } from '../selectors/linkPreviews';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||
|
@ -28,7 +21,6 @@ import { useGlobalModalActions } from '../ducks/globalModals';
|
|||
import { useLinkPreviewActions } from '../ducks/linkPreviews';
|
||||
import { SmartCompositionTextArea } from './CompositionTextArea';
|
||||
import { useToastActions } from '../ducks/toast';
|
||||
import { hydrateRanges } from '../../types/BodyRange';
|
||||
import { isDownloaded } from '../../types/Attachment';
|
||||
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
|
@ -38,21 +30,6 @@ import type {
|
|||
} from '../../types/ForwardDraft';
|
||||
import { getForwardMessagesProps } from '../selectors/globalModals';
|
||||
|
||||
function toMessageForwardDraft(
|
||||
props: ForwardMessagePropsType,
|
||||
getConversation: GetConversationByIdType
|
||||
): MessageForwardDraft {
|
||||
return {
|
||||
attachments: props.attachments ?? [],
|
||||
bodyRanges: hydrateRanges(props.bodyRanges, getConversation),
|
||||
hasContact: Boolean(props.contact),
|
||||
isSticker: Boolean(props.isSticker),
|
||||
messageBody: props.text,
|
||||
originalMessageId: props.id,
|
||||
previews: props.previews ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
export function SmartForwardMessagesModal(): JSX.Element | null {
|
||||
const forwardMessagesProps = useSelector(getForwardMessagesProps);
|
||||
|
||||
|
@ -61,8 +38,8 @@ export function SmartForwardMessagesModal(): JSX.Element | null {
|
|||
}
|
||||
|
||||
if (
|
||||
!forwardMessagesProps.messages.every(message => {
|
||||
return message.attachments?.every(isDownloaded) ?? true;
|
||||
!forwardMessagesProps.messageDrafts.every(messageDraft => {
|
||||
return messageDraft.attachments?.every(isDownloaded) ?? true;
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
|
@ -82,7 +59,6 @@ function SmartForwardMessagesModalInner({
|
|||
}): JSX.Element | null {
|
||||
const candidateConversations = useSelector(getAllComposableConversations);
|
||||
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
|
||||
const getConversation = useSelector(getConversationSelector);
|
||||
const i18n = useSelector(getIntl);
|
||||
const linkPreviewForSource = useSelector(getLinkPreview);
|
||||
const regionCode = useSelector(getRegionCode);
|
||||
|
@ -91,12 +67,11 @@ function SmartForwardMessagesModalInner({
|
|||
const { removeLinkPreview } = useLinkPreviewActions();
|
||||
const { toggleForwardMessagesModal } = useGlobalModalActions();
|
||||
const { showToast } = useToastActions();
|
||||
const { type } = forwardMessagesProps;
|
||||
|
||||
const [drafts, setDrafts] = useState<ReadonlyArray<MessageForwardDraft>>(
|
||||
() => {
|
||||
return forwardMessagesProps.messages.map((props): MessageForwardDraft => {
|
||||
return toMessageForwardDraft(props, getConversation);
|
||||
});
|
||||
return forwardMessagesProps.messageDrafts;
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -125,7 +100,7 @@ function SmartForwardMessagesModalInner({
|
|||
|
||||
const closeModal = useCallback(() => {
|
||||
resetLinkPreview();
|
||||
toggleForwardMessagesModal();
|
||||
toggleForwardMessagesModal(null);
|
||||
}, [toggleForwardMessagesModal]);
|
||||
|
||||
const doForwardMessages = useCallback(
|
||||
|
@ -136,6 +111,9 @@ function SmartForwardMessagesModalInner({
|
|||
try {
|
||||
const messages = await Promise.all(
|
||||
finalDrafts.map(async (draft): Promise<ForwardMessageData> => {
|
||||
if (draft.originalMessageId == null) {
|
||||
return { draft, originalMessage: null };
|
||||
}
|
||||
const message = await __DEPRECATED$getMessageById(
|
||||
draft.originalMessageId
|
||||
);
|
||||
|
@ -180,6 +158,7 @@ function SmartForwardMessagesModalInner({
|
|||
regionCode={regionCode}
|
||||
RenderCompositionTextArea={SmartCompositionTextArea}
|
||||
removeLinkPreview={removeLinkPreview}
|
||||
type={type}
|
||||
showToast={showToast}
|
||||
theme={theme}
|
||||
/>
|
||||
|
|
|
@ -34,6 +34,7 @@ import { getHasPendingUpdate } from '../selectors/updates';
|
|||
import { getOtherTabsUnreadStats } from '../selectors/nav';
|
||||
import { getIsStoriesSettingsVisible } from '../selectors/globalModals';
|
||||
import type { StoryViewType } from '../../types/Stories';
|
||||
import { ForwardMessagesModalType } from '../../components/ForwardMessagesModal';
|
||||
|
||||
function renderStoryCreator(): JSX.Element {
|
||||
return <SmartStoryCreator />;
|
||||
|
@ -95,7 +96,10 @@ export const SmartStoriesTab = memo(function SmartStoriesTab() {
|
|||
|
||||
const handleForwardStory = useCallback(
|
||||
(messageId: string) => {
|
||||
toggleForwardMessagesModal([messageId]);
|
||||
toggleForwardMessagesModal({
|
||||
type: ForwardMessagesModalType.Forward,
|
||||
messageIds: [messageId],
|
||||
});
|
||||
},
|
||||
[toggleForwardMessagesModal]
|
||||
);
|
||||
|
|
|
@ -62,7 +62,12 @@ function toAdhocGroup(call: CallHistoryDetails): CallHistoryGroup {
|
|||
direction: call.direction,
|
||||
timestamp: call.timestamp,
|
||||
status: call.status,
|
||||
children: [],
|
||||
children: [
|
||||
{
|
||||
callId: call.callId,
|
||||
timestamp: call.timestamp,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -17,12 +17,13 @@ export type MessageForwardDraft = Readonly<{
|
|||
hasContact: boolean;
|
||||
isSticker: boolean;
|
||||
messageBody?: string;
|
||||
originalMessageId: string;
|
||||
originalMessageId: string | null; // null for new messages
|
||||
previews: ReadonlyArray<LinkPreviewType>;
|
||||
}>;
|
||||
|
||||
export type ForwardMessageData = Readonly<{
|
||||
originalMessage: MessageAttributesType;
|
||||
// only null for new messages
|
||||
originalMessage: MessageAttributesType | null;
|
||||
draft: MessageForwardDraft;
|
||||
}>;
|
||||
|
||||
|
@ -71,11 +72,14 @@ export function sortByMessageOrder<T>(
|
|||
items: ReadonlyArray<T>,
|
||||
getMesssage: (
|
||||
item: T
|
||||
) => Pick<MessageAttributesType, 'sent_at' | 'received_at'>
|
||||
) => Pick<MessageAttributesType, 'sent_at' | 'received_at'> | null
|
||||
): Array<T> {
|
||||
return orderBy(
|
||||
items,
|
||||
[item => getMesssage(item).received_at, item => getMesssage(item).sent_at],
|
||||
[
|
||||
item => getMesssage(item)?.received_at,
|
||||
item => getMesssage(item)?.sent_at,
|
||||
],
|
||||
['ASC', 'ASC']
|
||||
);
|
||||
}
|
||||
|
|
|
@ -23,12 +23,19 @@ export type RenderTextCallbackType = (options: {
|
|||
|
||||
export { ICUJSXMessageParamsByKeyType, ICUStringMessageParamsByKeyType };
|
||||
|
||||
export type LocalizerOptions = {
|
||||
textIsBidiFreeSkipNormalization?: boolean;
|
||||
};
|
||||
|
||||
export type LocalizerType = {
|
||||
<Key extends keyof ICUStringMessageParamsByKeyType>(
|
||||
key: Key,
|
||||
...values: ICUStringMessageParamsByKeyType[Key] extends undefined
|
||||
? [undefined?]
|
||||
: [ICUStringMessageParamsByKeyType[Key]]
|
||||
? [params?: undefined, options?: LocalizerOptions]
|
||||
: [
|
||||
params: ICUStringMessageParamsByKeyType[Key],
|
||||
options?: LocalizerOptions
|
||||
]
|
||||
): string;
|
||||
getIntl(): IntlShape;
|
||||
getLocale(): string;
|
||||
|
|
|
@ -7,3 +7,8 @@ export async function copyGroupLink(groupLink: string): Promise<void> {
|
|||
await window.navigator.clipboard.writeText(groupLink);
|
||||
window.reduxActions.toast.showToast({ toastType: ToastType.GroupLinkCopied });
|
||||
}
|
||||
|
||||
export async function copyCallLink(callLink: string): Promise<void> {
|
||||
await window.navigator.clipboard.writeText(callLink);
|
||||
window.reduxActions.toast.showToast({ toastType: ToastType.CopiedCallLink });
|
||||
}
|
|
@ -78,10 +78,13 @@ export async function maybeForwardMessages(
|
|||
const preparedMessages = await Promise.all(
|
||||
messages.map(async message => {
|
||||
const { draft, originalMessage } = message;
|
||||
const { sticker, contact } = originalMessage;
|
||||
const { sticker, contact } = originalMessage ?? {};
|
||||
const { attachments, bodyRanges, messageBody, previews } = draft;
|
||||
|
||||
const idForLogging = getMessageIdForLogging(originalMessage);
|
||||
const idForLogging =
|
||||
originalMessage != null
|
||||
? getMessageIdForLogging(originalMessage)
|
||||
: '(new message)';
|
||||
log.info(`maybeForwardMessage: Forwarding ${idForLogging}`);
|
||||
|
||||
const attachmentLookup = new Set();
|
||||
|
@ -180,7 +183,9 @@ export async function maybeForwardMessages(
|
|||
log.error(
|
||||
'maybeForwardMessage: message send error',
|
||||
getConversationIdForLogging(conversation.attributes),
|
||||
getMessageIdForLogging(originalMessage),
|
||||
originalMessage != null
|
||||
? getMessageIdForLogging(originalMessage)
|
||||
: '(new message)',
|
||||
toLogFormat(error)
|
||||
);
|
||||
})
|
||||
|
|
|
@ -7,6 +7,7 @@ import type { LocaleMessageType, LocaleMessagesType } from '../types/I18N';
|
|||
import type {
|
||||
LocalizerType,
|
||||
ICUStringMessageParamsByKeyType,
|
||||
LocalizerOptions,
|
||||
} from '../types/Util';
|
||||
import { strictAssert } from './assert';
|
||||
import * as log from '../logging/log';
|
||||
|
@ -117,11 +118,14 @@ export function setupI18n(
|
|||
Key extends keyof ICUStringMessageParamsByKeyType
|
||||
>(
|
||||
key: Key,
|
||||
substitutions: ICUStringMessageParamsByKeyType[Key]
|
||||
substitutions: ICUStringMessageParamsByKeyType[Key],
|
||||
options?: LocalizerOptions
|
||||
) => {
|
||||
const result = intl.formatMessage(
|
||||
{ id: key },
|
||||
normalizeSubstitutions(substitutions)
|
||||
options?.textIsBidiFreeSkipNormalization
|
||||
? substitutions
|
||||
: normalizeSubstitutions(substitutions)
|
||||
);
|
||||
|
||||
strictAssert(result !== key, `i18n: missing translation for "${key}"`);
|
||||
|
|
Loading…
Reference in a new issue