diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index a8c8e31bb5..21541a5da8 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -1322,6 +1322,14 @@
"message": "Calling",
"description": "Header for calling options on the settings screen"
},
+ "calling__call-back": {
+ "message": "Call Back",
+ "description": "Button to call someone back"
+ },
+ "calling__call-again": {
+ "message": "Call Again",
+ "description": "Button to call someone again"
+ },
"calling__start": {
"message": "Start Call",
"description": "Button label in the call lobby for starting a call"
diff --git a/ts/components/conversation/CallingNotification.stories.tsx b/ts/components/conversation/CallingNotification.stories.tsx
index d642ec4479..a522cdaf7f 100644
--- a/ts/components/conversation/CallingNotification.stories.tsx
+++ b/ts/components/conversation/CallingNotification.stories.tsx
@@ -9,6 +9,7 @@ import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json';
import { CallMode } from '../../types/Calling';
import { CallingNotification } from './CallingNotification';
+import type { CallingNotificationType } from '../../util/callingNotification';
const i18n = setupI18n('en', enMessages);
@@ -19,6 +20,7 @@ const getCommonProps = () => ({
i18n,
messageId: 'fake-message-id',
messageSizeChanged: action('messageSizeChanged'),
+ nextItem: undefined,
returnToActiveCall: action('returnToActiveCall'),
startCallingLobby: action('startCallingLobby'),
});
@@ -46,6 +48,64 @@ const getCommonProps = () => ({
});
});
+story.add('Two incoming direct calls back-to-back', () => {
+ const call1: CallingNotificationType = {
+ callMode: CallMode.Direct,
+ wasIncoming: true,
+ wasVideoCall: true,
+ wasDeclined: false,
+ acceptedTime: 1618894800000,
+ endedTime: 1618894800000,
+ };
+ const call2: CallingNotificationType = {
+ callMode: CallMode.Direct,
+ wasIncoming: true,
+ wasVideoCall: false,
+ wasDeclined: false,
+ endedTime: 1618894800000,
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+});
+
+story.add('Two outgoing direct calls back-to-back', () => {
+ const call1: CallingNotificationType = {
+ callMode: CallMode.Direct,
+ wasIncoming: false,
+ wasVideoCall: true,
+ wasDeclined: false,
+ acceptedTime: 1618894800000,
+ endedTime: 1618894800000,
+ };
+ const call2: CallingNotificationType = {
+ callMode: CallMode.Direct,
+ wasIncoming: false,
+ wasVideoCall: false,
+ wasDeclined: false,
+ endedTime: 1618894800000,
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+});
+
[
undefined,
{ isMe: false, title: 'Alice' },
diff --git a/ts/components/conversation/CallingNotification.tsx b/ts/components/conversation/CallingNotification.tsx
index 259f90fa6f..e9200f7cdd 100644
--- a/ts/components/conversation/CallingNotification.tsx
+++ b/ts/components/conversation/CallingNotification.tsx
@@ -1,7 +1,7 @@
// Copyright 2020-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
-import React, { useState, useEffect } from 'react';
+import React, { ReactNode, useState, useEffect } from 'react';
import Measure from 'react-measure';
import { noop } from 'lodash';
@@ -18,6 +18,7 @@ import {
import { usePrevious } from '../../util/hooks';
import { missingCaseError } from '../../util/missingCaseError';
import { Tooltip, TooltipPlacement } from '../Tooltip';
+import type { TimelineItemType } from './TimelineItem';
export type PropsActionsType = {
messageSizeChanged: (messageId: string, conversationId: string) => void;
@@ -32,6 +33,7 @@ type PropsHousekeeping = {
i18n: LocalizerType;
conversationId: string;
messageId: string;
+ nextItem: undefined | TimelineItemType;
};
type PropsType = CallingNotificationType & PropsActionsType & PropsHousekeeping;
@@ -52,7 +54,6 @@ export const CallingNotification: React.FC = React.memo(props => {
}
}, [height, previousHeight, conversationId, messageId, messageSizeChanged]);
- let hasButton = false;
let timestamp: number;
let wasMissed = false;
switch (props.callMode) {
@@ -62,7 +63,6 @@ export const CallingNotification: React.FC = React.memo(props => {
props.wasIncoming && !props.acceptedTime && !props.wasDeclined;
break;
case CallMode.Group:
- hasButton = !props.ended;
timestamp = props.startedTime;
break;
default:
@@ -87,9 +87,7 @@ export const CallingNotification: React.FC = React.memo(props => {
>
{({ measureRef }) => (
: undefined
- }
+ button={renderCallingNotificationButton(props)}
contents={
<>
{getCallingNotificationText(props, i18n)} ·{' '}
@@ -113,47 +111,70 @@ export const CallingNotification: React.FC = React.memo(props => {
);
});
-function CallingNotificationButton(props: PropsType) {
- if (props.callMode !== CallMode.Group || props.ended) {
- return null;
- }
-
+function renderCallingNotificationButton(
+ props: Readonly
+): ReactNode {
const {
- activeCallConversationId,
conversationId,
- deviceCount,
i18n,
- maxDevices,
+ nextItem,
returnToActiveCall,
startCallingLobby,
} = props;
+ if (nextItem?.type === 'callHistory') {
+ return null;
+ }
+
let buttonText: string;
let disabledTooltipText: undefined | string;
let onClick: () => void;
- if (activeCallConversationId) {
- if (activeCallConversationId === conversationId) {
- buttonText = i18n('calling__return');
- onClick = returnToActiveCall;
- } else {
- buttonText = i18n('calling__join');
- disabledTooltipText = i18n(
- 'calling__call-notification__button__in-another-call-tooltip'
- );
- onClick = noop;
+
+ switch (props.callMode) {
+ case CallMode.Direct: {
+ const { wasIncoming, wasVideoCall } = props;
+ buttonText = wasIncoming
+ ? i18n('calling__call-back')
+ : i18n('calling__call-again');
+ onClick = () => {
+ startCallingLobby({ conversationId, isVideoCall: wasVideoCall });
+ };
+ break;
}
- } else if (deviceCount >= maxDevices) {
- buttonText = i18n('calling__call-is-full');
- disabledTooltipText = i18n(
- 'calling__call-notification__button__call-full-tooltip',
- [String(deviceCount)]
- );
- onClick = noop;
- } else {
- buttonText = i18n('calling__join');
- onClick = () => {
- startCallingLobby({ conversationId, isVideoCall: true });
- };
+ case CallMode.Group: {
+ if (props.ended) {
+ return null;
+ }
+ const { activeCallConversationId, deviceCount, maxDevices } = props;
+ if (activeCallConversationId) {
+ if (activeCallConversationId === conversationId) {
+ buttonText = i18n('calling__return');
+ onClick = returnToActiveCall;
+ } else {
+ buttonText = i18n('calling__join');
+ disabledTooltipText = i18n(
+ 'calling__call-notification__button__in-another-call-tooltip'
+ );
+ onClick = noop;
+ }
+ } else if (deviceCount >= maxDevices) {
+ buttonText = i18n('calling__call-is-full');
+ disabledTooltipText = i18n(
+ 'calling__call-notification__button__call-full-tooltip',
+ [String(deviceCount)]
+ );
+ onClick = noop;
+ } else {
+ buttonText = i18n('calling__join');
+ onClick = () => {
+ startCallingLobby({ conversationId, isVideoCall: true });
+ };
+ }
+ break;
+ }
+ default:
+ window.log.error(missingCaseError(props));
+ return null;
}
const button = (
diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx
index e2e3514c04..1bf59c6e00 100644
--- a/ts/components/conversation/Timeline.stories.tsx
+++ b/ts/components/conversation/Timeline.stories.tsx
@@ -376,19 +376,21 @@ const actions = () => ({
unblurAvatar: action('unblurAvatar'),
});
-const renderItem = (
- id: string,
- _conversationId: unknown,
- _onHeightChange: unknown,
- _actionProps: unknown,
- containerElementRef: React.RefObject
-) => (
+const renderItem = ({
+ messageId,
+ containerElementRef,
+}: {
+ messageId: string;
+ containerElementRef: React.RefObject;
+}) => (
}
renderReactionPicker={() => }
- item={items[id]}
+ item={items[messageId]}
+ previousItem={undefined}
+ nextItem={undefined}
i18n={i18n}
interactionMode="keyboard"
containerElementRef={containerElementRef}
diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx
index b8ed557285..04f845d744 100644
--- a/ts/components/conversation/Timeline.tsx
+++ b/ts/components/conversation/Timeline.tsx
@@ -103,13 +103,15 @@ type PropsHousekeepingType = {
i18n: LocalizerType;
- renderItem: (
- id: string,
- conversationId: string,
- onHeightChange: (messageId: string) => unknown,
- actions: PropsActionsType,
- containerElementRef: RefObject
- ) => JSX.Element;
+ renderItem: (props: {
+ actions: PropsActionsType;
+ containerElementRef: RefObject;
+ conversationId: string;
+ messageId: string;
+ nextMessageId: undefined | string;
+ onHeightChange: (messageId: string) => unknown;
+ previousMessageId: undefined | string;
+ }) => JSX.Element;
renderLastSeenIndicator: (id: string) => JSX.Element;
renderHeroRow: (
id: string,
@@ -797,7 +799,9 @@ export class Timeline extends React.PureComponent {
`Attempted to render item with undefined index - row ${row}`
);
}
+ const previousMessageId: undefined | string = items[itemIndex - 1];
const messageId = items[itemIndex];
+ const nextMessageId: undefined | string = items[itemIndex + 1];
stableKey = messageId;
const actions = getActions(this.props);
@@ -811,13 +815,15 @@ export class Timeline extends React.PureComponent {
role="row"
>
window.showDebugLog()}>
- {renderItem(
- messageId,
- id,
- this.resizeMessage,
+ {renderItem({
actions,
- this.containerRef
- )}
+ containerElementRef: this.containerRef,
+ conversationId: id,
+ messageId,
+ nextMessageId,
+ onHeightChange: this.resizeMessage,
+ previousMessageId,
+ })}
);
diff --git a/ts/components/conversation/TimelineItem.stories.tsx b/ts/components/conversation/TimelineItem.stories.tsx
index 8f50c42d4f..e9e2a60ac3 100644
--- a/ts/components/conversation/TimelineItem.stories.tsx
+++ b/ts/components/conversation/TimelineItem.stories.tsx
@@ -86,6 +86,8 @@ const getDefaultProps = () => ({
messageSizeChanged: action('messageSizeChanged'),
startCallingLobby: action('startCallingLobby'),
returnToActiveCall: action('returnToActiveCall'),
+ previousItem: undefined,
+ nextItem: undefined,
renderContact,
renderUniversalTimerNotification,
diff --git a/ts/components/conversation/TimelineItem.tsx b/ts/components/conversation/TimelineItem.tsx
index cb8aed0585..5fa7d2e466 100644
--- a/ts/components/conversation/TimelineItem.tsx
+++ b/ts/components/conversation/TimelineItem.tsx
@@ -165,6 +165,8 @@ type PropsLocalType = {
i18n: LocalizerType;
interactionMode: InteractionModeType;
theme?: ThemeType;
+ previousItem: undefined | TimelineItemType;
+ nextItem: undefined | TimelineItemType;
};
type PropsActionsType = MessageActionsType &
@@ -192,6 +194,7 @@ export class TimelineItem extends React.PureComponent {
i18n,
theme,
messageSizeChanged,
+ nextItem,
renderContact,
renderUniversalTimerNotification,
returnToActiveCall,
@@ -231,6 +234,7 @@ export class TimelineItem extends React.PureComponent {
i18n={i18n}
messageId={id}
messageSizeChanged={messageSizeChanged}
+ nextItem={nextItem}
returnToActiveCall={returnToActiveCall}
startCallingLobby={startCallingLobby}
{...item.data}
diff --git a/ts/state/smart/Timeline.tsx b/ts/state/smart/Timeline.tsx
index a6bfeed324..7778954740 100644
--- a/ts/state/smart/Timeline.tsx
+++ b/ts/state/smart/Timeline.tsx
@@ -63,19 +63,31 @@ const createBoundOnHeightChange = memoizee(
{ max: 500 }
);
-function renderItem(
- messageId: string,
- conversationId: string,
- onHeightChange: (messageId: string) => unknown,
- actionProps: TimelineActionsType,
- containerElementRef: RefObject
-): JSX.Element {
+function renderItem({
+ actionProps,
+ containerElementRef,
+ conversationId,
+ messageId,
+ nextMessageId,
+ onHeightChange,
+ previousMessageId,
+}: {
+ actionProps: TimelineActionsType;
+ containerElementRef: RefObject;
+ conversationId: string;
+ messageId: string;
+ nextMessageId: undefined | string;
+ onHeightChange: (messageId: string) => unknown;
+ previousMessageId: undefined | string;
+}): JSX.Element {
return (
;
+ conversationId: string;
+ messageId: string;
+ nextMessageId: undefined | string;
+ previousMessageId: undefined | string;
};
// Workaround: A react component's required properties are filtering up through connect()
@@ -39,19 +41,34 @@ function renderUniversalTimerNotification(): JSX.Element {
}
const mapStateToProps = (state: StateType, props: ExternalProps) => {
- const { id, conversationId, containerElementRef } = props;
+ const {
+ containerElementRef,
+ conversationId,
+ messageId,
+ nextMessageId,
+ previousMessageId,
+ } = props;
const messageSelector = getMessageSelector(state);
- const item = messageSelector(id);
+
+ const item = messageSelector(messageId);
+ const previousItem = previousMessageId
+ ? messageSelector(previousMessageId)
+ : undefined;
+ const nextItem = nextMessageId ? messageSelector(nextMessageId) : undefined;
const selectedMessage = getSelectedMessage(state);
- const isSelected = Boolean(selectedMessage && id === selectedMessage.id);
+ const isSelected = Boolean(
+ selectedMessage && messageId === selectedMessage.id
+ );
const conversation = getConversationSelector(state)(conversationId);
return {
item,
- id,
+ previousItem,
+ nextItem,
+ id: messageId,
containerElementRef,
conversationId,
conversationColor: conversation?.conversationColor,