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