From 19083cadf7136b62616132c2698ba993d69a3622 Mon Sep 17 00:00:00 2001
From: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com>
Date: Wed, 22 May 2024 09:24:27 -0700
Subject: [PATCH] Init CallLinkDetails view in calls tab
---
_locales/en/messages.json | 20 ++++
stylesheets/components/CallLinkDetails.scss | 47 ++++++++
.../components/ConversationDetails.scss | 15 +++
stylesheets/manifest.scss | 1 +
ts/components/Avatar.tsx | 1 +
ts/components/CallLinkDetails.tsx | 107 +++++++++++++++++
ts/components/CallManager.stories.tsx | 1 -
ts/components/CallManager.tsx | 12 +-
ts/components/CallsList.tsx | 21 ++--
ts/components/CallsNewCall.tsx | 13 +-
ts/components/CallsTab.tsx | 85 +++++++------
ts/components/CompositionArea.tsx | 16 ++-
.../ForwardMessagesModal.stories.tsx | 6 +-
ts/components/ForwardMessagesModal.tsx | 19 ++-
ts/components/Lightbox.tsx | 9 +-
.../conversation/TimelineMessage.tsx | 16 ++-
.../CallHistoryGroupPanelSection.tsx | 91 ++++++++++++++
.../ConversationDetails.tsx | 76 +-----------
.../ConversationDetailsIcon.tsx | 1 +
.../GroupLinkManagement.tsx | 2 +-
ts/services/addGlobalKeyboardShortcuts.ts | 3 +-
ts/sql/Server.ts | 10 +-
ts/sql/server/groupEndorsements.ts | 4 +-
ts/state/ducks/globalModals.ts | 112 +++++++++++++-----
ts/state/smart/CallLinkDetails.tsx | 82 +++++++++++++
ts/state/smart/CallManager.tsx | 3 -
ts/state/smart/CallsTab.tsx | 11 ++
ts/state/smart/ForwardMessagesModal.tsx | 43 ++-----
ts/state/smart/StoriesTab.tsx | 6 +-
.../sql/getCallHistoryGroups_test.ts | 7 +-
ts/types/ForwardDraft.ts | 12 +-
ts/types/Util.ts | 11 +-
...copyGroupLink.ts => copyLinksWithToast.ts} | 5 +
ts/util/maybeForwardMessages.ts | 11 +-
ts/util/setupI18nMain.ts | 8 +-
35 files changed, 665 insertions(+), 222 deletions(-)
create mode 100644 stylesheets/components/CallLinkDetails.scss
create mode 100644 ts/components/CallLinkDetails.tsx
create mode 100644 ts/components/conversation/conversation-details/CallHistoryGroupPanelSection.tsx
create mode 100644 ts/state/smart/CallLinkDetails.tsx
rename ts/util/{copyGroupLink.ts => copyLinksWithToast.ts} (61%)
diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 25c54f45919c..7898a9135758 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -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."
diff --git a/stylesheets/components/CallLinkDetails.scss b/stylesheets/components/CallLinkDetails.scss
new file mode 100644
index 000000000000..ccb00edea2f8
--- /dev/null
+++ b/stylesheets/components/CallLinkDetails.scss
@@ -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;
+}
diff --git a/stylesheets/components/ConversationDetails.scss b/stylesheets/components/ConversationDetails.scss
index 2cf4e1d2caad..da7cccdaf406 100644
--- a/stylesheets/components/ConversationDetails.scss
+++ b/stylesheets/components/ConversationDetails.scss
@@ -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;
}
diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss
index f92aab6697f7..d64a537257c9 100644
--- a/stylesheets/manifest.scss
+++ b/stylesheets/manifest.scss
@@ -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';
diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx
index 178a9682e13a..3e013308cef7 100644
--- a/ts/components/Avatar.tsx
+++ b/ts/components/Avatar.tsx
@@ -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,
diff --git a/ts/components/CallLinkDetails.tsx b/ts/components/CallLinkDetails.tsx
new file mode 100644
index 000000000000..8656cbc74dac
--- /dev/null
+++ b/ts/components/CallLinkDetails.tsx
@@ -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 (
+
+
+
+
+
+ }
+ label={i18n('icu:CallLinkDetails__CopyLink')}
+ onClick={() => {
+ drop(copyCallLink(webUrl.toString()));
+ }}
+ />
+
+ }
+ label={i18n('icu:CallLinkDetails__ShareLinkViaSignal')}
+ onClick={onShareCallLinkViaSignal}
+ />
+
+
+ );
+}
diff --git a/ts/components/CallManager.stories.tsx b/ts/components/CallManager.stories.tsx
index bb2b25dbc101..6c8451e895e5 100644
--- a/ts/components/CallManager.stories.tsx
+++ b/ts/components/CallManager.stories.tsx
@@ -100,7 +100,6 @@ const createProps = (storyProps: Partial = {}): 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'),
diff --git a/ts/components/CallManager.tsx b/ts/components/CallManager.tsx
index 0b7bccbc6aa9..e404474960e0 100644
--- a/ts/components/CallManager.tsx
+++ b/ts/components/CallManager.tsx
@@ -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}
diff --git a/ts/components/CallsList.tsx b/ts/components/CallsList.tsx
index 97fd29f1abb6..03725f4dc3f2 100644
--- a/ts/components/CallsList.tsx
+++ b/ts/components/CallsList.tsx
@@ -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,
+ });
}}
/>
@@ -791,7 +798,7 @@ export function CallsList({
getIsCallActive,
getIsInCall,
selectedCallHistoryGroup,
- onSelectCallHistoryGroup,
+ onChangeCallsTabSelectedView,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
startCallLinkLobbyByRoomId,
diff --git a/ts/components/CallsNewCall.tsx b/ts/components/CallsNewCall.tsx
index d45d78baf6d8..d1c4a45588a9 100644
--- a/ts/components/CallsNewCall.tsx
+++ b/ts/components/CallsNewCall.tsx
@@ -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;
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({
}
onClick={() => {
- onSelectConversation(item.conversation.id);
+ onChangeCallsTabSelectedView({
+ type: 'conversation',
+ conversationId: item.conversation.id,
+ callHistoryGroup: null,
+ });
}}
/>
@@ -272,7 +277,7 @@ export function CallsNewCall({
rows,
i18n,
hasActiveCall,
- onSelectConversation,
+ onChangeCallsTabSelectedView,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
]
diff --git a/ts/components/CallsTab.tsx b/ts/components/CallsTab.tsx
index 25228fb7f92a..479594f7e347 100644
--- a/ts/components/CallsTab.tsx
+++ b/ts/components/CallsTab.tsx
@@ -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(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({
/>
)}
- {selected == null ? (
+ {selectedView == null ? (
@@ -295,13 +306,19 @@ export function CallsTab({
) : (
- {renderConversationDetails(
- selected.conversationId,
- selected.callHistoryGroup
- )}
+ {selectedView.type === 'conversation' &&
+ renderConversationDetails(
+ selectedView.conversationId,
+ selectedView.callHistoryGroup
+ )}
+ {selectedView.type === 'callLink' &&
+ renderCallLinkDetails(
+ selectedView.roomId,
+ selectedView.callHistoryGroup
+ )}
)}
diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx
index cf55506aaa27..2edb66e1e13b 100644
--- a/ts/components/CompositionArea.tsx
+++ b/ts/components/CompositionArea.tsx
@@ -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 | undefined;
toggleSelectMode: (on: boolean) => void;
toggleForwardMessagesModal: (
- messageIds: ReadonlyArray,
+ 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}
diff --git a/ts/components/ForwardMessagesModal.stories.tsx b/ts/components/ForwardMessagesModal.stories.tsx
index 6b7a01bdb36e..2ae62a51b453 100644
--- a/ts/components/ForwardMessagesModal.stories.tsx
+++ b/ts/components/ForwardMessagesModal.stories.tsx
@@ -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 => ({
/>
),
showToast: action('showToast'),
+ type: ForwardMessagesModalType.Forward,
theme: React.useContext(StorybookThemeContext),
regionCode: 'US',
});
diff --git a/ts/components/ForwardMessagesModal.tsx b/ts/components/ForwardMessagesModal.tsx
index 459de6b5fee2..ec07df0dadb8 100644
--- a/ts/components/ForwardMessagesModal.tsx
+++ b/ts/components/ForwardMessagesModal.tsx
@@ -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;
@@ -63,6 +69,7 @@ export type DataPropsType = {
) => unknown;
regionCode: string | undefined;
RenderCompositionTextArea: ComponentType;
+ 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({
);
+ 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}
diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx
index 424c3db30076..733424175c2c 100644
--- a/ts/components/Lightbox.tsx
+++ b/ts/components/Lightbox.tsx
@@ -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) => 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(
diff --git a/ts/components/conversation/TimelineMessage.tsx b/ts/components/conversation/TimelineMessage.tsx
index d1f2343a2b42..dc148d382b01 100644
--- a/ts/components/conversation/TimelineMessage.tsx
+++ b/ts/components/conversation/TimelineMessage.tsx
@@ -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) => 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({
diff --git a/ts/components/conversation/conversation-details/CallHistoryGroupPanelSection.tsx b/ts/components/conversation/conversation-details/CallHistoryGroupPanelSection.tsx
new file mode 100644
index 000000000000..c24cfac6906c
--- /dev/null
+++ b/ts/components/conversation/conversation-details/CallHistoryGroupPanelSection.tsx
@@ -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 (
+
+
+ {callHistoryGroup.children.map(child => {
+ return (
+ -
+
+
+ {describeCallHistory(
+ i18n,
+ callHistoryGroup.type,
+ callHistoryGroup.direction,
+ callHistoryGroup.status
+ )}
+
+
+ {formatTime(i18n, child.timestamp, Date.now(), false)}
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/ts/components/conversation/conversation-details/ConversationDetails.tsx b/ts/components/conversation/conversation-details/ConversationDetails.tsx
index d41635c8463a..64d8bf50629d 100644
--- a/ts/components/conversation/conversation-details/ConversationDetails.tsx
+++ b/ts/components/conversation/conversation-details/ConversationDetails.tsx
@@ -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({
{callHistoryGroup && (
-
-
- {callHistoryGroup.children.map(child => {
- return (
- -
-
-
- {describeCallHistory(
- i18n,
- callHistoryGroup.type,
- callHistoryGroup.direction,
- callHistoryGroup.status
- )}
-
-
- {formatTime(i18n, child.timestamp, Date.now(), false)}
-
-
- );
- })}
-
-
+
)}
diff --git a/ts/components/conversation/conversation-details/ConversationDetailsIcon.tsx b/ts/components/conversation/conversation-details/ConversationDetailsIcon.tsx
index 49f5490a33b0..6854840020ed 100644
--- a/ts/components/conversation/conversation-details/ConversationDetailsIcon.tsx
+++ b/ts/components/conversation/conversation-details/ConversationDetailsIcon.tsx
@@ -13,6 +13,7 @@ export enum IconType {
'unblock' = 'unblock',
'color' = 'color',
'down' = 'down',
+ 'forward' = 'forward',
'invites' = 'invites',
'leave' = 'leave',
'link' = 'link',
diff --git a/ts/components/conversation/conversation-details/GroupLinkManagement.tsx b/ts/components/conversation/conversation-details/GroupLinkManagement.tsx
index 8641a23f964d..358e7400e8b9 100644
--- a/ts/components/conversation/conversation-details/GroupLinkManagement.tsx
+++ b/ts/components/conversation/conversation-details/GroupLinkManagement.tsx
@@ -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';
diff --git a/ts/services/addGlobalKeyboardShortcuts.ts b/ts/services/addGlobalKeyboardShortcuts.ts
index 3d5cb79c2cb6..e151b0045c40 100644
--- a/ts/services/addGlobalKeyboardShortcuts.ts
+++ b/ts/services/addGlobalKeyboardShortcuts.ts
@@ -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);
diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts
index 8d08c488e046..7908282eaf0e 100644
--- a/ts/sql/Server.ts
+++ b/ts/sql/Server.ts
@@ -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();
}
diff --git a/ts/sql/server/groupEndorsements.ts b/ts/sql/server/groupEndorsements.ts
index 4c2342e57279..2916c61a0eec 100644
--- a/ts/sql/server/groupEndorsements.ts
+++ b/ts/sql/server/groupEndorsements.ts
@@ -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>(db, selectGroup)
+ .pluck()
+ .get(selectGroupParams);
if (value == null) {
return null;
}
diff --git a/ts/state/ducks/globalModals.ts b/ts/state/ducks/globalModals.ts
index ae3c0f834542..e4493cd9a323 100644
--- a/ts/state/ducks/globalModals.ts
+++ b/ts/state/ducks/globalModals.ts
@@ -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;
export type ForwardMessagesPropsType = ReadonlyDeep<{
- messages: Array;
+ type: ForwardMessagesModalType;
+ messageDrafts: Array;
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;
+ }
+ | {
+ type: ForwardMessagesModalType.ShareCallLink;
+ draft: MessageForwardDraft;
+ }
+>;
+
function toggleForwardMessagesModal(
- messageIds?: ReadonlyArray,
+ 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;
- 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,
+ messageDrafts: ReadonlyArray,
attributes: ReadonlyDeep
-): ReadonlyArray {
- return messagesProps.map(messageProps => {
- if (messageProps.id !== attributes.id) {
- return messageProps;
+): ReadonlyArray {
+ 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
),
},
diff --git a/ts/state/smart/CallLinkDetails.tsx b/ts/state/smart/CallLinkDetails.tsx
new file mode 100644
index 000000000000..b1de325eb483
--- /dev/null
+++ b/ts/state/smart/CallLinkDetails.tsx
@@ -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 (
+
+ );
+});
diff --git a/ts/state/smart/CallManager.tsx b/ts/state/smart/CallManager.tsx
index 45b593476cd4..27fe4e68150d 100644
--- a/ts/state/smart/CallManager.tsx
+++ b/ts/state/smart/CallManager.tsx
@@ -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}
diff --git a/ts/state/smart/CallsTab.tsx b/ts/state/smart/CallsTab.tsx
index ffbb8baa1c74..3a6f06eccc49 100644
--- a/ts/state/smart/CallsTab.tsx
+++ b/ts/state/smart/CallsTab.tsx
@@ -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 (
+
+ );
+}
+
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}
diff --git a/ts/state/smart/ForwardMessagesModal.tsx b/ts/state/smart/ForwardMessagesModal.tsx
index c9383f1f74e1..d4f39ba77077 100644
--- a/ts/state/smart/ForwardMessagesModal.tsx
+++ b/ts/state/smart/ForwardMessagesModal.tsx
@@ -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>(
() => {
- 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 => {
+ 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}
/>
diff --git a/ts/state/smart/StoriesTab.tsx b/ts/state/smart/StoriesTab.tsx
index 418fc4a118c4..4af7fd7b0e05 100644
--- a/ts/state/smart/StoriesTab.tsx
+++ b/ts/state/smart/StoriesTab.tsx
@@ -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 ;
@@ -95,7 +96,10 @@ export const SmartStoriesTab = memo(function SmartStoriesTab() {
const handleForwardStory = useCallback(
(messageId: string) => {
- toggleForwardMessagesModal([messageId]);
+ toggleForwardMessagesModal({
+ type: ForwardMessagesModalType.Forward,
+ messageIds: [messageId],
+ });
},
[toggleForwardMessagesModal]
);
diff --git a/ts/test-electron/sql/getCallHistoryGroups_test.ts b/ts/test-electron/sql/getCallHistoryGroups_test.ts
index 0277865fdb08..cadf52f8ffba 100644
--- a/ts/test-electron/sql/getCallHistoryGroups_test.ts
+++ b/ts/test-electron/sql/getCallHistoryGroups_test.ts
@@ -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,
+ },
+ ],
};
}
diff --git a/ts/types/ForwardDraft.ts b/ts/types/ForwardDraft.ts
index 91f98520beb2..06348df8986f 100644
--- a/ts/types/ForwardDraft.ts
+++ b/ts/types/ForwardDraft.ts
@@ -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;
}>;
export type ForwardMessageData = Readonly<{
- originalMessage: MessageAttributesType;
+ // only null for new messages
+ originalMessage: MessageAttributesType | null;
draft: MessageForwardDraft;
}>;
@@ -71,11 +72,14 @@ export function sortByMessageOrder(
items: ReadonlyArray,
getMesssage: (
item: T
- ) => Pick
+ ) => Pick | null
): Array {
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']
);
}
diff --git a/ts/types/Util.ts b/ts/types/Util.ts
index b8aba0932641..9b703455be27 100644
--- a/ts/types/Util.ts
+++ b/ts/types/Util.ts
@@ -23,12 +23,19 @@ export type RenderTextCallbackType = (options: {
export { ICUJSXMessageParamsByKeyType, ICUStringMessageParamsByKeyType };
+export type LocalizerOptions = {
+ textIsBidiFreeSkipNormalization?: boolean;
+};
+
export type LocalizerType = {
(
key: Key,
...values: ICUStringMessageParamsByKeyType[Key] extends undefined
- ? [undefined?]
- : [ICUStringMessageParamsByKeyType[Key]]
+ ? [params?: undefined, options?: LocalizerOptions]
+ : [
+ params: ICUStringMessageParamsByKeyType[Key],
+ options?: LocalizerOptions
+ ]
): string;
getIntl(): IntlShape;
getLocale(): string;
diff --git a/ts/util/copyGroupLink.ts b/ts/util/copyLinksWithToast.ts
similarity index 61%
rename from ts/util/copyGroupLink.ts
rename to ts/util/copyLinksWithToast.ts
index e990d70526c5..b5370a532910 100644
--- a/ts/util/copyGroupLink.ts
+++ b/ts/util/copyLinksWithToast.ts
@@ -7,3 +7,8 @@ export async function copyGroupLink(groupLink: string): Promise {
await window.navigator.clipboard.writeText(groupLink);
window.reduxActions.toast.showToast({ toastType: ToastType.GroupLinkCopied });
}
+
+export async function copyCallLink(callLink: string): Promise {
+ await window.navigator.clipboard.writeText(callLink);
+ window.reduxActions.toast.showToast({ toastType: ToastType.CopiedCallLink });
+}
diff --git a/ts/util/maybeForwardMessages.ts b/ts/util/maybeForwardMessages.ts
index d81d4708f6a5..1ff4bf29cb5f 100644
--- a/ts/util/maybeForwardMessages.ts
+++ b/ts/util/maybeForwardMessages.ts
@@ -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)
);
})
diff --git a/ts/util/setupI18nMain.ts b/ts/util/setupI18nMain.ts
index 6ec8f289b53f..541871199974 100644
--- a/ts/util/setupI18nMain.ts
+++ b/ts/util/setupI18nMain.ts
@@ -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}"`);