Init CallLinkDetails view in calls tab

This commit is contained in:
Jamie Kyle 2024-05-22 09:24:27 -07:00 committed by GitHub
parent e9b661873b
commit 19083cadf7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 665 additions and 222 deletions

View file

@ -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."

View 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;
}

View file

@ -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;
}

View file

@ -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';

View file

@ -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,

View 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>
);
}

View file

@ -100,7 +100,6 @@ const createProps = (storyProps: Partial<PropsType> = {}): PropsType => ({
setPresenting: action('toggle-presenting'),
setRendererCanvas: action('set-renderer-canvas'),
setOutgoingRing: action('set-outgoing-ring'),
showToast: action('show-toast'),
startCall: action('start-call'),
stopRingtone: action('stop-ringtone'),
switchToPresentationView: action('switch-to-presentation-view'),

View file

@ -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}

View file

@ -53,6 +53,7 @@ import {
callLinkToConversation,
getPlaceholderCallLinkConversation,
} from '../util/callLinks';
import type { CallsTabSelectedView } from './CallsTab';
import type { CallStateType } from '../state/selectors/calling';
import {
isGroupOrAdhocCallMode,
@ -143,10 +144,7 @@ type CallsListProps = Readonly<{
selectedCallHistoryGroup: CallHistoryGroup | null;
onOutgoingAudioCallInConversation: (conversationId: string) => void;
onOutgoingVideoCallInConversation: (conversationId: string) => void;
onSelectCallHistoryGroup: (
conversationId: string,
selectedCallHistoryGroup: CallHistoryGroup
) => void;
onChangeCallsTabSelectedView: (selectedView: CallsTabSelectedView) => void;
peekNotConnectedGroupCall: (options: PeekNotConnectedGroupCallType) => void;
startCallLinkLobbyByRoomId: (roomId: string) => void;
togglePip: () => void;
@ -184,7 +182,7 @@ export function CallsList({
selectedCallHistoryGroup,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
onSelectCallHistoryGroup,
onChangeCallsTabSelectedView,
peekNotConnectedGroupCall,
startCallLinkLobbyByRoomId,
togglePip,
@ -771,13 +769,22 @@ export function CallsList({
}
onClick={() => {
if (isAdhoc) {
onChangeCallsTabSelectedView({
type: 'callLink',
roomId: item.peerId,
callHistoryGroup: item,
});
return;
}
if (conversation == null) {
return;
}
onSelectCallHistoryGroup(conversation.id, item);
onChangeCallsTabSelectedView({
type: 'conversation',
conversationId: conversation.id,
callHistoryGroup: item,
});
}}
/>
</div>
@ -791,7 +798,7 @@ export function CallsList({
getIsCallActive,
getIsInCall,
selectedCallHistoryGroup,
onSelectCallHistoryGroup,
onChangeCallsTabSelectedView,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
startCallLinkLobbyByRoomId,

View file

@ -19,13 +19,14 @@ import { Avatar, AvatarSize } from './Avatar';
import { I18n } from './I18n';
import { SizeObserver } from '../hooks/useSizeObserver';
import { CallType } from '../types/CallDisposition';
import type { CallsTabSelectedView } from './CallsTab';
import { Tooltip, TooltipPlacement } from './Tooltip';
type CallsNewCallProps = Readonly<{
hasActiveCall: boolean;
allConversations: ReadonlyArray<ConversationType>;
i18n: LocalizerType;
onSelectConversation: (conversationId: string) => void;
onChangeCallsTabSelectedView: (selectedView: CallsTabSelectedView) => void;
onOutgoingAudioCallInConversation: (conversationId: string) => void;
onOutgoingVideoCallInConversation: (conversationId: string) => void;
regionCode: string | undefined;
@ -106,7 +107,7 @@ export function CallsNewCall({
hasActiveCall,
allConversations,
i18n,
onSelectConversation,
onChangeCallsTabSelectedView,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
regionCode,
@ -262,7 +263,11 @@ export function CallsNewCall({
</div>
}
onClick={() => {
onSelectConversation(item.conversation.id);
onChangeCallsTabSelectedView({
type: 'conversation',
conversationId: item.conversation.id,
callHistoryGroup: null,
});
}}
/>
</div>
@ -272,7 +277,7 @@ export function CallsNewCall({
rows,
i18n,
hasActiveCall,
onSelectConversation,
onChangeCallsTabSelectedView,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
]

View file

@ -57,6 +57,10 @@ type CallsTabProps = Readonly<{
onOutgoingVideoCallInConversation: (conversationId: string) => void;
peekNotConnectedGroupCall: (options: PeekNotConnectedGroupCallType) => void;
preferredLeftPaneWidth: number;
renderCallLinkDetails: (
roomId: string,
callHistoryGroup: CallHistoryGroup
) => JSX.Element;
renderConversationDetails: (
conversationId: string,
callHistoryGroup: CallHistoryGroup | null
@ -70,6 +74,18 @@ type CallsTabProps = Readonly<{
togglePip: () => void;
}>;
export type CallsTabSelectedView =
| {
type: 'conversation';
conversationId: string;
callHistoryGroup: CallHistoryGroup | null;
}
| {
type: 'callLink';
roomId: string;
callHistoryGroup: CallHistoryGroup;
};
export function CallsTab({
activeCall,
allConversations,
@ -93,6 +109,7 @@ export function CallsTab({
onOutgoingVideoCallInConversation,
peekNotConnectedGroupCall,
preferredLeftPaneWidth,
renderCallLinkDetails,
renderConversationDetails,
renderToastManager,
regionCode,
@ -103,37 +120,31 @@ export function CallsTab({
const [sidebarView, setSidebarView] = useState(
CallsTabSidebarView.CallsListView
);
const [selected, setSelected] = useState<{
conversationId: string;
callHistoryGroup: CallHistoryGroup | null;
} | null>(null);
const [selectedView, setSelectedViewInner] =
useState<CallsTabSelectedView | null>(null);
const [selectedViewKey, setSelectedViewKey] = useState(() => 1);
const [
confirmClearCallHistoryDialogOpen,
setConfirmClearCallHistoryDialogOpen,
] = useState(false);
const updateSelectedView = useCallback(
(nextSelected: CallsTabSelectedView | null) => {
setSelectedViewInner(nextSelected);
setSelectedViewKey(key => key + 1);
},
[]
);
const updateSidebarView = useCallback(
(newSidebarView: CallsTabSidebarView) => {
setSidebarView(newSidebarView);
setSelected(null);
updateSelectedView(null);
},
[]
[updateSelectedView]
);
const handleSelectCallHistoryGroup = useCallback(
(conversationId: string, callHistoryGroup: CallHistoryGroup) => {
setSelected({
conversationId,
callHistoryGroup,
});
},
[]
);
const handleSelectConversation = useCallback((conversationId: string) => {
setSelected({ conversationId, callHistoryGroup: null });
}, []);
useEscapeHandling(
sidebarView === CallsTabSidebarView.NewCallView
? () => {
@ -167,12 +178,12 @@ export function CallsTab({
);
useEffect(() => {
if (selected?.callHistoryGroup != null) {
selected.callHistoryGroup.children.forEach(child => {
onMarkCallHistoryRead(selected.conversationId, child.callId);
if (selectedView?.type === 'conversation') {
selectedView.callHistoryGroup?.children.forEach(child => {
onMarkCallHistoryRead(selectedView.conversationId, child.callId);
});
}
}, [selected, onMarkCallHistoryRead]);
}, [selectedView, onMarkCallHistoryRead]);
return (
<>
@ -255,8 +266,8 @@ export function CallsTab({
getConversation={getConversation}
hangUpActiveCall={hangUpActiveCall}
i18n={i18n}
selectedCallHistoryGroup={selected?.callHistoryGroup ?? null}
onSelectCallHistoryGroup={handleSelectCallHistoryGroup}
selectedCallHistoryGroup={selectedView?.callHistoryGroup ?? null}
onChangeCallsTabSelectedView={updateSelectedView}
onOutgoingAudioCallInConversation={
handleOutgoingAudioCallInConversation
}
@ -275,7 +286,7 @@ export function CallsTab({
allConversations={allConversations}
i18n={i18n}
regionCode={regionCode}
onSelectConversation={handleSelectConversation}
onChangeCallsTabSelectedView={updateSelectedView}
onOutgoingAudioCallInConversation={
handleOutgoingAudioCallInConversation
}
@ -285,7 +296,7 @@ export function CallsTab({
/>
)}
</NavSidebar>
{selected == null ? (
{selectedView == null ? (
<div className="CallsTab__EmptyState">
<div className="CallsTab__EmptyStateIcon" />
<p className="CallsTab__EmptyStateLabel">
@ -295,13 +306,19 @@ export function CallsTab({
) : (
<div
className="CallsTab__ConversationCallDetails"
// Force scrolling to top when a new conversation is selected.
key={selected.conversationId}
// Force scrolling to top when selection changes
key={selectedViewKey}
>
{renderConversationDetails(
selected.conversationId,
selected.callHistoryGroup
)}
{selectedView.type === 'conversation' &&
renderConversationDetails(
selectedView.conversationId,
selectedView.callHistoryGroup
)}
{selectedView.type === 'callLink' &&
renderCallLinkDetails(
selectedView.roomId,
selectedView.callHistoryGroup
)}
</div>
)}
</div>

View file

@ -72,6 +72,8 @@ import type { SmartCompositionRecordingProps } from '../state/smart/CompositionR
import SelectModeActions from './conversation/SelectModeActions';
import type { ShowToastAction } from '../state/ducks/toast';
import type { DraftEditMessageType } from '../model-types.d';
import type { ForwardMessagesPayload } from '../state/ducks/globalModals';
import { ForwardMessagesModalType } from './ForwardMessagesModal';
export type OwnProps = Readonly<{
acceptedMessageRequest: boolean | null;
@ -181,7 +183,7 @@ export type OwnProps = Readonly<{
selectedMessageIds: ReadonlyArray<string> | undefined;
toggleSelectMode: (on: boolean) => void;
toggleForwardMessagesModal: (
messageIds: ReadonlyArray<string>,
payload: ForwardMessagesPayload,
onForward: () => void
) => void;
}>;
@ -725,9 +727,15 @@ export const CompositionArea = memo(function CompositionArea({
}}
onForwardMessages={() => {
if (selectedMessageIds.length > 0) {
toggleForwardMessagesModal(selectedMessageIds, () => {
toggleSelectMode(false);
});
toggleForwardMessagesModal(
{
type: ForwardMessagesModalType.Forward,
messageIds: selectedMessageIds,
},
() => {
toggleSelectMode(false);
}
);
}
}}
showToast={showToast}

View file

@ -7,7 +7,10 @@ import type { Meta } from '@storybook/react';
import enMessages from '../../_locales/en/messages.json';
import type { AttachmentType } from '../types/Attachment';
import type { PropsType } from './ForwardMessagesModal';
import { ForwardMessagesModal } from './ForwardMessagesModal';
import {
ForwardMessagesModal,
ForwardMessagesModalType,
} from './ForwardMessagesModal';
import { IMAGE_JPEG, VIDEO_MP4, stringToMIMEType } from '../types/MIME';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { setupI18n } from '../util/setupI18n';
@ -67,6 +70,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
/>
),
showToast: action('showToast'),
type: ForwardMessagesModalType.Forward,
theme: React.useContext(StorybookThemeContext),
regionCode: 'US',
});

View file

@ -42,6 +42,12 @@ import {
isDraftForwardable,
type MessageForwardDraft,
} from '../types/ForwardDraft';
import { missingCaseError } from '../util/missingCaseError';
export enum ForwardMessagesModalType {
Forward,
ShareCallLink,
}
export type DataPropsType = {
candidateConversations: ReadonlyArray<ConversationType>;
@ -63,6 +69,7 @@ export type DataPropsType = {
) => unknown;
regionCode: string | undefined;
RenderCompositionTextArea: ComponentType<SmartCompositionTextAreaProps>;
type: ForwardMessagesModalType;
showToast: ShowToastAction;
theme: ThemeType;
};
@ -76,6 +83,7 @@ export type PropsType = DataPropsType & ActionPropsType;
const MAX_FORWARD = 5;
export function ForwardMessagesModal({
type,
drafts,
candidateConversations,
doForwardMessages,
@ -292,6 +300,15 @@ export function ForwardMessagesModal({
</div>
);
let title: string;
if (type === ForwardMessagesModalType.Forward) {
title = i18n('icu:ForwardMessageModal__title');
} else if (type === ForwardMessagesModalType.ShareCallLink) {
title = i18n('icu:ForwardMessageModal__ShareCallLink');
} else {
throw missingCaseError(type);
}
return (
<>
{cannotMessage && (
@ -311,7 +328,7 @@ export function ForwardMessagesModal({
onClose={onClose}
onBackButtonClick={isEditingMessage ? handleBackOrClose : undefined}
moduleClassName="module-ForwardMessageModal"
title={i18n('icu:ForwardMessageModal__title')}
title={title}
useFocusTrap={false}
padded={false}
modalFooter={footer}

View file

@ -28,6 +28,8 @@ import { usePrevious } from '../hooks/usePrevious';
import { arrow } from '../util/keyboard';
import { drop } from '../util/drop';
import { isCmdOrCtrl } from '../hooks/useKeyboardShortcuts';
import type { ForwardMessagesPayload } from '../state/ducks/globalModals';
import { ForwardMessagesModalType } from './ForwardMessagesModal';
export type PropsType = {
children?: ReactNode;
@ -39,7 +41,7 @@ export type PropsType = {
playbackDisabled: boolean;
saveAttachment: SaveAttachmentActionCreatorType;
selectedIndex: number;
toggleForwardMessagesModal: (messageIds: ReadonlyArray<string>) => unknown;
toggleForwardMessagesModal: (payload: ForwardMessagesPayload) => unknown;
onMediaPlaybackStart: () => void;
onNextAttachment: () => void;
onPrevAttachment: () => void;
@ -195,7 +197,10 @@ export function Lightbox({
closeLightbox();
const mediaItem = media[selectedIndex];
toggleForwardMessagesModal([mediaItem.message.id]);
toggleForwardMessagesModal({
type: ForwardMessagesModalType.Forward,
messageIds: [mediaItem.message.id],
});
};
const onKeyDown = useCallback(

View file

@ -32,13 +32,17 @@ import {
useToggleReactionPicker,
} from '../../hooks/useKeyboardShortcuts';
import { PanelType } from '../../types/Panels';
import type { DeleteMessagesPropsType } from '../../state/ducks/globalModals';
import type {
DeleteMessagesPropsType,
ForwardMessagesPayload,
} from '../../state/ducks/globalModals';
import { useScrollerLock } from '../../hooks/useScrollLock';
import {
type ContextMenuTriggerType,
MessageContextMenu,
useHandleMessageContextMenu,
} from './MessageContextMenu';
import { ForwardMessagesModalType } from '../ForwardMessagesModal';
export type PropsData = {
canDownload: boolean;
@ -55,7 +59,7 @@ export type PropsData = {
export type PropsActions = {
pushPanelForConversation: PushPanelForConversationActionType;
toggleDeleteMessagesModal: (props: DeleteMessagesPropsType) => void;
toggleForwardMessagesModal: (messageIds: Array<string>) => void;
toggleForwardMessagesModal: (payload: ForwardMessagesPayload) => void;
reactToMessage: (
id: string,
{ emoji, remove }: { emoji: string; remove: boolean }
@ -372,7 +376,13 @@ export function TimelineMessage(props: Props): JSX.Element {
onCopy={canCopy ? () => copyMessageText(id) : undefined}
onSelect={() => toggleSelectMessage(conversationId, id, false, true)}
onForward={
canForward ? () => toggleForwardMessagesModal([id]) : undefined
canForward
? () =>
toggleForwardMessagesModal({
type: ForwardMessagesModalType.Forward,
messageIds: [id],
})
: undefined
}
onDeleteMessage={() => {
toggleDeleteMessagesModal({

View file

@ -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>
);
}

View file

@ -4,7 +4,6 @@
import type { ReactNode } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import classNames from 'classnames';
import { Button, ButtonIconType, ButtonVariant } from '../../Button';
import { Tooltip } from '../../Tooltip';
import type {
@ -53,43 +52,11 @@ import type {
import { isConversationMuted } from '../../../util/isConversationMuted';
import { ConversationDetailsGroups } from './ConversationDetailsGroups';
import { PanelType } from '../../../types/Panels';
import type { CallStatus } from '../../../types/CallDisposition';
import {
CallType,
type CallHistoryGroup,
CallDirection,
DirectCallStatus,
GroupCallStatus,
} from '../../../types/CallDisposition';
import { formatDate, formatTime } from '../../../util/timestamp';
import { type CallHistoryGroup } from '../../../types/CallDisposition';
import { NavTab } from '../../../state/ducks/nav';
import { ContextMenu } from '../../ContextMenu';
import { canHaveNicknameAndNote } from '../../../util/nicknames';
function describeCallHistory(
i18n: LocalizerType,
type: CallType,
direction: CallDirection,
status: CallStatus
): string {
if (type === CallType.Adhoc) {
return i18n('icu:CallHistory__Description--Adhoc');
}
if (status === DirectCallStatus.Missed || status === GroupCallStatus.Missed) {
if (direction === CallDirection.Incoming) {
return i18n('icu:CallHistory__Description--Missed', { type });
}
return i18n('icu:CallHistory__Description--Unanswered', { type });
}
if (
status === DirectCallStatus.Declined ||
status === GroupCallStatus.Declined
) {
return i18n('icu:CallHistory__Description--Declined', { type });
}
return i18n('icu:CallHistory__Description--Default', { type, direction });
}
import { CallHistoryGroupPanelSection } from './CallHistoryGroupPanelSection';
enum ModalState {
AddingGroupMembers,
@ -501,41 +468,10 @@ export function ConversationDetails({
</div>
{callHistoryGroup && (
<PanelSection title={formatDate(i18n, callHistoryGroup.timestamp)}>
<ol className="ConversationDetails__CallHistoryGroup__List">
{callHistoryGroup.children.map(child => {
return (
<li
key={child.callId}
className="ConversationDetails__CallHistoryGroup__Item"
>
<span
className={classNames(
'ConversationDetails__CallHistoryGroup__ItemIcon',
{
'ConversationDetails__CallHistoryGroup__ItemIcon--Audio':
callHistoryGroup.type === CallType.Audio,
'ConversationDetails__CallHistoryGroup__ItemIcon--Video':
callHistoryGroup.type !== CallType.Audio,
}
)}
/>
<span className="ConversationDetails__CallHistoryGroup__ItemLabel">
{describeCallHistory(
i18n,
callHistoryGroup.type,
callHistoryGroup.direction,
callHistoryGroup.status
)}
</span>
<span className="ConversationDetails__CallHistoryGroup__ItemTimestamp">
{formatTime(i18n, child.timestamp, Date.now(), false)}
</span>
</li>
);
})}
</ol>
</PanelSection>
<CallHistoryGroupPanelSection
callHistoryGroup={callHistoryGroup}
i18n={i18n}
/>
)}
<PanelSection>

View file

@ -13,6 +13,7 @@ export enum IconType {
'unblock' = 'unblock',
'color' = 'color',
'down' = 'down',
'forward' = 'forward',
'invites' = 'invites',
'leave' = 'leave',
'link' = 'link',

View file

@ -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';

View file

@ -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);

View file

@ -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();
}

View file

@ -79,7 +79,9 @@ export async function getGroupSendCombinedEndorsementExpiration(
SELECT expiration FROM groupSendCombinedEndorsement
WHERE groupId = ${groupId};
`;
const value = prepare(db, selectGroup).pluck().get(selectGroupParams);
const value = prepare<Array<unknown>>(db, selectGroup)
.pluck()
.get(selectGroupParams);
if (value == null) {
return null;
}

View file

@ -37,6 +37,14 @@ import {
import { isDownloaded } from '../../types/Attachment';
import type { ButtonVariant } from '../../components/Button';
import type { MessageRequestState } from '../../components/conversation/MessageRequestActionsConfirmation';
import type { MessageForwardDraft } from '../../types/ForwardDraft';
import { hydrateRanges } from '../../types/BodyRange';
import {
getConversationSelector,
type GetConversationByIdType,
} from '../selectors/conversations';
import { missingCaseError } from '../../util/missingCaseError';
import { ForwardMessagesModalType } from '../../components/ForwardMessagesModal';
// State
@ -53,7 +61,8 @@ export type DeleteMessagesPropsType = ReadonlyDeep<{
}>;
export type ForwardMessagePropsType = ReadonlyDeep<MessagePropsType>;
export type ForwardMessagesPropsType = ReadonlyDeep<{
messages: Array<ForwardMessagePropsType>;
type: ForwardMessagesModalType;
messageDrafts: Array<MessageForwardDraft>;
onForward?: () => void;
}>;
export type MessageRequestActionsConfirmationPropsType = ReadonlyDeep<{
@ -522,8 +531,34 @@ function toggleDeleteMessagesModal(
};
}
function toMessageForwardDraft(
props: ForwardMessagePropsType,
getConversation: GetConversationByIdType
): MessageForwardDraft {
return {
attachments: props.attachments ?? [],
bodyRanges: hydrateRanges(props.bodyRanges, getConversation),
hasContact: Boolean(props.contact),
isSticker: Boolean(props.isSticker),
messageBody: props.text,
originalMessageId: props.id,
previews: props.previews ?? [],
};
}
export type ForwardMessagesPayload = ReadonlyDeep<
| {
type: ForwardMessagesModalType.Forward;
messageIds: ReadonlyArray<string>;
}
| {
type: ForwardMessagesModalType.ShareCallLink;
draft: MessageForwardDraft;
}
>;
function toggleForwardMessagesModal(
messageIds?: ReadonlyArray<string>,
payload: ForwardMessagesPayload | null,
onForward?: () => void
): ThunkAction<
void,
@ -532,7 +567,7 @@ function toggleForwardMessagesModal(
ToggleForwardMessagesModalActionType
> {
return async (dispatch, getState) => {
if (!messageIds) {
if (payload == null) {
dispatch({
type: TOGGLE_FORWARD_MESSAGES_MODAL,
payload: undefined,
@ -540,31 +575,46 @@ function toggleForwardMessagesModal(
return;
}
const messagesProps = await Promise.all(
messageIds.map(async messageId => {
const messageAttributes = await window.MessageCache.resolveAttributes(
'toggleForwardMessagesModal',
messageId
);
let messageDrafts: ReadonlyArray<MessageForwardDraft>;
const { attachments = [] } = messageAttributes;
if (!attachments.every(isDownloaded)) {
dispatch(
conversationsActions.kickOffAttachmentDownload({ messageId })
if (payload.type === ForwardMessagesModalType.Forward) {
messageDrafts = await Promise.all(
payload.messageIds.map(async messageId => {
const messageAttributes = await window.MessageCache.resolveAttributes(
'toggleForwardMessagesModal',
messageId
);
}
const messagePropsSelector = getMessagePropsSelector(getState());
const messageProps = messagePropsSelector(messageAttributes);
const { attachments = [] } = messageAttributes;
return messageProps;
})
);
if (!attachments.every(isDownloaded)) {
dispatch(
conversationsActions.kickOffAttachmentDownload({ messageId })
);
}
const state = getState();
const messagePropsSelector = getMessagePropsSelector(state);
const conversationSelector = getConversationSelector(state);
const messageProps = messagePropsSelector(messageAttributes);
const messageDraft = toMessageForwardDraft(
messageProps,
conversationSelector
);
return messageDraft;
})
);
} else if (payload.type === ForwardMessagesModalType.ShareCallLink) {
messageDrafts = [payload.draft];
} else {
throw missingCaseError(payload);
}
dispatch({
type: TOGGLE_FORWARD_MESSAGES_MODAL,
payload: { messages: messagesProps, onForward },
payload: { type: payload.type, messageDrafts, onForward },
});
};
}
@ -802,15 +852,15 @@ function closeEditHistoryModal(): CloseEditHistoryModalActionType {
}
function copyOverMessageAttributesIntoForwardMessages(
messagesProps: ReadonlyArray<ForwardMessagePropsType>,
messageDrafts: ReadonlyArray<MessageForwardDraft>,
attributes: ReadonlyDeep<MessageAttributesType>
): ReadonlyArray<ForwardMessagePropsType> {
return messagesProps.map(messageProps => {
if (messageProps.id !== attributes.id) {
return messageProps;
): ReadonlyArray<MessageForwardDraft> {
return messageDrafts.map(messageDraft => {
if (messageDraft.originalMessageId !== attributes.id) {
return messageDraft;
}
return {
...messageProps,
...messageDraft,
attachments: attributes.attachments,
};
});
@ -1078,8 +1128,8 @@ export function reducer(
if (state.forwardMessagesProps != null) {
if (action.type === MESSAGE_CHANGED) {
if (
!state.forwardMessagesProps.messages.some(message => {
return message.id === action.payload.id;
!state.forwardMessagesProps.messageDrafts.some(message => {
return message.originalMessageId === action.payload.id;
})
) {
return state;
@ -1089,8 +1139,8 @@ export function reducer(
...state,
forwardMessagesProps: {
...state.forwardMessagesProps,
messages: copyOverMessageAttributesIntoForwardMessages(
state.forwardMessagesProps.messages,
messageDrafts: copyOverMessageAttributesIntoForwardMessages(
state.forwardMessagesProps.messageDrafts,
action.payload.data
),
},

View 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}
/>
);
});

View file

@ -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}

View file

@ -37,6 +37,7 @@ import { getCallHistoryEdition } from '../selectors/callHistory';
import { getHasPendingUpdate } from '../selectors/updates';
import { getHasAnyFailedStorySends } from '../selectors/stories';
import { getOtherTabsUnreadStats } from '../selectors/nav';
import { SmartCallLinkDetails } from './CallLinkDetails';
import type { CallLinkType } from '../../types/CallLink';
import { filterCallLinks } from '../../util/filterCallLinks';
@ -101,6 +102,15 @@ function getCallHistoryFilter({
};
}
function renderCallLinkDetails(
roomId: string,
callHistoryGroup: CallHistoryGroup
): JSX.Element {
return (
<SmartCallLinkDetails roomId={roomId} callHistoryGroup={callHistoryGroup} />
);
}
function renderConversationDetails(
conversationId: string,
callHistoryGroup: CallHistoryGroup | null
@ -225,6 +235,7 @@ export const SmartCallsTab = memo(function SmartCallsTab() {
onOutgoingVideoCallInConversation={onOutgoingVideoCallInConversation}
peekNotConnectedGroupCall={peekNotConnectedGroupCall}
preferredLeftPaneWidth={preferredLeftPaneWidth}
renderCallLinkDetails={renderCallLinkDetails}
renderConversationDetails={renderConversationDetails}
renderToastManager={renderToastManager}
regionCode={regionCode}

View file

@ -3,19 +3,12 @@
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import type {
ForwardMessagePropsType,
ForwardMessagesPropsType,
} from '../ducks/globalModals';
import type { ForwardMessagesPropsType } from '../ducks/globalModals';
import * as log from '../../logging/log';
import { ForwardMessagesModal } from '../../components/ForwardMessagesModal';
import { LinkPreviewSourceType } from '../../types/LinkPreview';
import * as Errors from '../../types/errors';
import type { GetConversationByIdType } from '../selectors/conversations';
import {
getAllComposableConversations,
getConversationSelector,
} from '../selectors/conversations';
import { getAllComposableConversations } from '../selectors/conversations';
import { getIntl, getTheme, getRegionCode } from '../selectors/user';
import { getLinkPreview } from '../selectors/linkPreviews';
import { getPreferredBadgeSelector } from '../selectors/badges';
@ -28,7 +21,6 @@ import { useGlobalModalActions } from '../ducks/globalModals';
import { useLinkPreviewActions } from '../ducks/linkPreviews';
import { SmartCompositionTextArea } from './CompositionTextArea';
import { useToastActions } from '../ducks/toast';
import { hydrateRanges } from '../../types/BodyRange';
import { isDownloaded } from '../../types/Attachment';
import { __DEPRECATED$getMessageById } from '../../messages/getMessageById';
import { strictAssert } from '../../util/assert';
@ -38,21 +30,6 @@ import type {
} from '../../types/ForwardDraft';
import { getForwardMessagesProps } from '../selectors/globalModals';
function toMessageForwardDraft(
props: ForwardMessagePropsType,
getConversation: GetConversationByIdType
): MessageForwardDraft {
return {
attachments: props.attachments ?? [],
bodyRanges: hydrateRanges(props.bodyRanges, getConversation),
hasContact: Boolean(props.contact),
isSticker: Boolean(props.isSticker),
messageBody: props.text,
originalMessageId: props.id,
previews: props.previews ?? [],
};
}
export function SmartForwardMessagesModal(): JSX.Element | null {
const forwardMessagesProps = useSelector(getForwardMessagesProps);
@ -61,8 +38,8 @@ export function SmartForwardMessagesModal(): JSX.Element | null {
}
if (
!forwardMessagesProps.messages.every(message => {
return message.attachments?.every(isDownloaded) ?? true;
!forwardMessagesProps.messageDrafts.every(messageDraft => {
return messageDraft.attachments?.every(isDownloaded) ?? true;
})
) {
return null;
@ -82,7 +59,6 @@ function SmartForwardMessagesModalInner({
}): JSX.Element | null {
const candidateConversations = useSelector(getAllComposableConversations);
const getPreferredBadge = useSelector(getPreferredBadgeSelector);
const getConversation = useSelector(getConversationSelector);
const i18n = useSelector(getIntl);
const linkPreviewForSource = useSelector(getLinkPreview);
const regionCode = useSelector(getRegionCode);
@ -91,12 +67,11 @@ function SmartForwardMessagesModalInner({
const { removeLinkPreview } = useLinkPreviewActions();
const { toggleForwardMessagesModal } = useGlobalModalActions();
const { showToast } = useToastActions();
const { type } = forwardMessagesProps;
const [drafts, setDrafts] = useState<ReadonlyArray<MessageForwardDraft>>(
() => {
return forwardMessagesProps.messages.map((props): MessageForwardDraft => {
return toMessageForwardDraft(props, getConversation);
});
return forwardMessagesProps.messageDrafts;
}
);
@ -125,7 +100,7 @@ function SmartForwardMessagesModalInner({
const closeModal = useCallback(() => {
resetLinkPreview();
toggleForwardMessagesModal();
toggleForwardMessagesModal(null);
}, [toggleForwardMessagesModal]);
const doForwardMessages = useCallback(
@ -136,6 +111,9 @@ function SmartForwardMessagesModalInner({
try {
const messages = await Promise.all(
finalDrafts.map(async (draft): Promise<ForwardMessageData> => {
if (draft.originalMessageId == null) {
return { draft, originalMessage: null };
}
const message = await __DEPRECATED$getMessageById(
draft.originalMessageId
);
@ -180,6 +158,7 @@ function SmartForwardMessagesModalInner({
regionCode={regionCode}
RenderCompositionTextArea={SmartCompositionTextArea}
removeLinkPreview={removeLinkPreview}
type={type}
showToast={showToast}
theme={theme}
/>

View file

@ -34,6 +34,7 @@ import { getHasPendingUpdate } from '../selectors/updates';
import { getOtherTabsUnreadStats } from '../selectors/nav';
import { getIsStoriesSettingsVisible } from '../selectors/globalModals';
import type { StoryViewType } from '../../types/Stories';
import { ForwardMessagesModalType } from '../../components/ForwardMessagesModal';
function renderStoryCreator(): JSX.Element {
return <SmartStoryCreator />;
@ -95,7 +96,10 @@ export const SmartStoriesTab = memo(function SmartStoriesTab() {
const handleForwardStory = useCallback(
(messageId: string) => {
toggleForwardMessagesModal([messageId]);
toggleForwardMessagesModal({
type: ForwardMessagesModalType.Forward,
messageIds: [messageId],
});
},
[toggleForwardMessagesModal]
);

View file

@ -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,
},
],
};
}

View file

@ -17,12 +17,13 @@ export type MessageForwardDraft = Readonly<{
hasContact: boolean;
isSticker: boolean;
messageBody?: string;
originalMessageId: string;
originalMessageId: string | null; // null for new messages
previews: ReadonlyArray<LinkPreviewType>;
}>;
export type ForwardMessageData = Readonly<{
originalMessage: MessageAttributesType;
// only null for new messages
originalMessage: MessageAttributesType | null;
draft: MessageForwardDraft;
}>;
@ -71,11 +72,14 @@ export function sortByMessageOrder<T>(
items: ReadonlyArray<T>,
getMesssage: (
item: T
) => Pick<MessageAttributesType, 'sent_at' | 'received_at'>
) => Pick<MessageAttributesType, 'sent_at' | 'received_at'> | null
): Array<T> {
return orderBy(
items,
[item => getMesssage(item).received_at, item => getMesssage(item).sent_at],
[
item => getMesssage(item)?.received_at,
item => getMesssage(item)?.sent_at,
],
['ASC', 'ASC']
);
}

View file

@ -23,12 +23,19 @@ export type RenderTextCallbackType = (options: {
export { ICUJSXMessageParamsByKeyType, ICUStringMessageParamsByKeyType };
export type LocalizerOptions = {
textIsBidiFreeSkipNormalization?: boolean;
};
export type LocalizerType = {
<Key extends keyof ICUStringMessageParamsByKeyType>(
key: Key,
...values: ICUStringMessageParamsByKeyType[Key] extends undefined
? [undefined?]
: [ICUStringMessageParamsByKeyType[Key]]
? [params?: undefined, options?: LocalizerOptions]
: [
params: ICUStringMessageParamsByKeyType[Key],
options?: LocalizerOptions
]
): string;
getIntl(): IntlShape;
getLocale(): string;

View file

@ -7,3 +7,8 @@ export async function copyGroupLink(groupLink: string): Promise<void> {
await window.navigator.clipboard.writeText(groupLink);
window.reduxActions.toast.showToast({ toastType: ToastType.GroupLinkCopied });
}
export async function copyCallLink(callLink: string): Promise<void> {
await window.navigator.clipboard.writeText(callLink);
window.reduxActions.toast.showToast({ toastType: ToastType.CopiedCallLink });
}

View file

@ -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)
);
})

View file

@ -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}"`);