Add group calling events to the message timeline
This commit is contained in:
parent
a2f285d243
commit
0c039bf431
29 changed files with 1275 additions and 239 deletions
|
@ -1166,6 +1166,10 @@
|
||||||
"message": "Join Call",
|
"message": "Join Call",
|
||||||
"description": "Button label in the call lobby for joining a call"
|
"description": "Button label in the call lobby for joining a call"
|
||||||
},
|
},
|
||||||
|
"calling__return": {
|
||||||
|
"message": "Return to Call",
|
||||||
|
"description": "Button label in the call lobby for returning to a call"
|
||||||
|
},
|
||||||
"calling__call-is-full": {
|
"calling__call-is-full": {
|
||||||
"message": "Call is full",
|
"message": "Call is full",
|
||||||
"description": "Button label in the call lobby when you can't join because the call is full"
|
"description": "Button label in the call lobby when you can't join because the call is full"
|
||||||
|
@ -3083,6 +3087,42 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"calling__call-notification__ended": {
|
||||||
|
"message": "The group call has ended",
|
||||||
|
"description": "Notification message when a group call has ended"
|
||||||
|
},
|
||||||
|
"calling__call-notification__started-by-someone": {
|
||||||
|
"message": "A group call was started",
|
||||||
|
"description": "Notification message when a group call has started, but we don't know who started it"
|
||||||
|
},
|
||||||
|
"calling__call-notification__started-by-you": {
|
||||||
|
"message": "You started a group call",
|
||||||
|
"description": "Notification message when a group call has started by you"
|
||||||
|
},
|
||||||
|
"calling__call-notification__started": {
|
||||||
|
"message": "$name$ started a group call",
|
||||||
|
"description": "Notification message when a group call has started",
|
||||||
|
"placeholders": {
|
||||||
|
"name": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "Alice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"calling__call-notification__button__in-another-call-tooltip": {
|
||||||
|
"message": "You are already in a call",
|
||||||
|
"description": "Tooltip in disabled notification button when you're on another call"
|
||||||
|
},
|
||||||
|
"calling__call-notification__button__call-full-tooltip": {
|
||||||
|
"message": "Call has reached capacity of $max$ participants",
|
||||||
|
"description": "Tooltip in disabled notification button when the call is full",
|
||||||
|
"placeholders": {
|
||||||
|
"max": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"calling__pip--on": {
|
"calling__pip--on": {
|
||||||
"message": "Minimize call",
|
"message": "Minimize call",
|
||||||
"description": "Title for picture-in-picture toggle"
|
"description": "Title for picture-in-picture toggle"
|
||||||
|
|
|
@ -57,9 +57,6 @@ const {
|
||||||
const {
|
const {
|
||||||
StagedLinkPreview,
|
StagedLinkPreview,
|
||||||
} = require('../../ts/components/conversation/StagedLinkPreview');
|
} = require('../../ts/components/conversation/StagedLinkPreview');
|
||||||
const {
|
|
||||||
getCallingNotificationText,
|
|
||||||
} = require('../../ts/components/conversation/CallingNotification');
|
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const { createTimeline } = require('../../ts/state/roots/createTimeline');
|
const { createTimeline } = require('../../ts/state/roots/createTimeline');
|
||||||
|
@ -310,7 +307,6 @@ exports.setup = (options = {}) => {
|
||||||
ContactModal,
|
ContactModal,
|
||||||
Emojify,
|
Emojify,
|
||||||
ErrorModal,
|
ErrorModal,
|
||||||
getCallingNotificationText,
|
|
||||||
Lightbox,
|
Lightbox,
|
||||||
LightboxGallery,
|
LightboxGallery,
|
||||||
MediaGallery,
|
MediaGallery,
|
||||||
|
|
|
@ -393,3 +393,39 @@
|
||||||
|
|
||||||
@include button-focus-outline;
|
@include button-focus-outline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin button-green {
|
||||||
|
$background-color: $color-accent-green;
|
||||||
|
|
||||||
|
background-color: $background-color;
|
||||||
|
color: $color-white;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
// We need to include all four here for specificity precedence
|
||||||
|
|
||||||
|
@include mouse-mode {
|
||||||
|
background-color: mix($color-black, $background-color, 25%);
|
||||||
|
}
|
||||||
|
@include dark-mouse-mode {
|
||||||
|
background-color: mix($color-white, $background-color, 25%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include keyboard-mode {
|
||||||
|
background-color: mix($color-black, $background-color, 25%);
|
||||||
|
}
|
||||||
|
@include dark-keyboard-mode {
|
||||||
|
background-color: mix($color-white, $background-color, 25%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include button-focus-outline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin button-small {
|
||||||
|
border-radius: 9999px; // This ensures the borders are completely rounded. (A value like 100% would make it an ellipse.)
|
||||||
|
padding: 7px 14px;
|
||||||
|
}
|
||||||
|
|
|
@ -2432,6 +2432,16 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
||||||
color: $color-gray-05;
|
color: $color-gray-05;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__button {
|
||||||
|
@include button-reset;
|
||||||
|
@include button-small;
|
||||||
|
@include button-green;
|
||||||
|
@include font-body-1-bold;
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
margin: 0.5rem auto 0 auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-safety-number__bold-name {
|
.module-safety-number__bold-name {
|
||||||
|
|
|
@ -2,9 +2,76 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { noop } from 'lodash';
|
||||||
import { Manager, Reference, Popper } from 'react-popper';
|
import { Manager, Reference, Popper } from 'react-popper';
|
||||||
import { Theme, themeClassName } from '../util/theme';
|
import { Theme, themeClassName } from '../util/theme';
|
||||||
|
|
||||||
|
interface EventWrapperPropsType {
|
||||||
|
children: React.ReactNode;
|
||||||
|
onHoverChanged: (_: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// React doesn't reliably fire `onMouseLeave` or `onMouseOut` events if wrapping a
|
||||||
|
// disabled button. This uses native browser events to avoid that.
|
||||||
|
//
|
||||||
|
// See <https://lecstor.com/react-disabled-button-onmouseleave/>.
|
||||||
|
const TooltipEventWrapper = React.forwardRef<
|
||||||
|
HTMLSpanElement,
|
||||||
|
EventWrapperPropsType
|
||||||
|
>(({ onHoverChanged, children }, ref) => {
|
||||||
|
const wrapperRef = React.useRef<HTMLSpanElement | null>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const wrapperEl = wrapperRef.current;
|
||||||
|
|
||||||
|
if (!wrapperEl) {
|
||||||
|
return noop;
|
||||||
|
}
|
||||||
|
|
||||||
|
const on = () => {
|
||||||
|
onHoverChanged(true);
|
||||||
|
};
|
||||||
|
const off = () => {
|
||||||
|
onHoverChanged(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
wrapperEl.addEventListener('focus', on);
|
||||||
|
wrapperEl.addEventListener('blur', off);
|
||||||
|
wrapperEl.addEventListener('mouseenter', on);
|
||||||
|
wrapperEl.addEventListener('mouseleave', off);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
wrapperEl.removeEventListener('focus', on);
|
||||||
|
wrapperEl.removeEventListener('blur', off);
|
||||||
|
wrapperEl.removeEventListener('mouseenter', on);
|
||||||
|
wrapperEl.removeEventListener('mouseleave', off);
|
||||||
|
};
|
||||||
|
}, [onHoverChanged]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
// This is a forward ref that also needs a ref of its own, so we set both here.
|
||||||
|
ref={el => {
|
||||||
|
wrapperRef.current = el;
|
||||||
|
|
||||||
|
// This is a simplified version of [what React does][0] to set a ref.
|
||||||
|
// [0]: https://github.com/facebook/react/blob/29b7b775f2ecf878eaf605be959d959030598b07/packages/react-reconciler/src/ReactFiberCommitWork.js#L661-L677
|
||||||
|
if (typeof ref === 'function') {
|
||||||
|
ref(el);
|
||||||
|
} else if (ref) {
|
||||||
|
// I believe the types for `ref` are wrong in this case, as `ref.current` should
|
||||||
|
// not be `readonly`. That's why we do this cast. See [the React source][1].
|
||||||
|
// [1]: https://github.com/facebook/react/blob/29b7b775f2ecf878eaf605be959d959030598b07/packages/shared/ReactTypes.js#L78-L80
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
(ref as React.MutableRefObject<HTMLSpanElement | null>).current = el;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export enum TooltipPlacement {
|
export enum TooltipPlacement {
|
||||||
Top = 'top',
|
Top = 'top',
|
||||||
Right = 'right',
|
Right = 'right',
|
||||||
|
@ -26,8 +93,9 @@ export const Tooltip: React.FC<PropsType> = ({
|
||||||
sticky,
|
sticky,
|
||||||
theme,
|
theme,
|
||||||
}) => {
|
}) => {
|
||||||
const isSticky = Boolean(sticky);
|
const [isHovering, setIsHovering] = React.useState(false);
|
||||||
const [showTooltip, setShowTooltip] = React.useState(isSticky);
|
|
||||||
|
const showTooltip = isHovering || Boolean(sticky);
|
||||||
|
|
||||||
const tooltipTheme = theme ? themeClassName(theme) : undefined;
|
const tooltipTheme = theme ? themeClassName(theme) : undefined;
|
||||||
|
|
||||||
|
@ -35,31 +103,9 @@ export const Tooltip: React.FC<PropsType> = ({
|
||||||
<Manager>
|
<Manager>
|
||||||
<Reference>
|
<Reference>
|
||||||
{({ ref }) => (
|
{({ ref }) => (
|
||||||
<span
|
<TooltipEventWrapper ref={ref} onHoverChanged={setIsHovering}>
|
||||||
onBlur={() => {
|
|
||||||
if (!isSticky) {
|
|
||||||
setShowTooltip(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onFocus={() => {
|
|
||||||
if (!isSticky) {
|
|
||||||
setShowTooltip(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => {
|
|
||||||
if (!isSticky) {
|
|
||||||
setShowTooltip(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
if (!isSticky) {
|
|
||||||
setShowTooltip(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</TooltipEventWrapper>
|
||||||
)}
|
)}
|
||||||
</Reference>
|
</Reference>
|
||||||
<Popper placement={direction}>
|
<Popper placement={direction}>
|
||||||
|
|
|
@ -1,90 +1,168 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import Measure from 'react-measure';
|
||||||
|
|
||||||
import { Timestamp } from './Timestamp';
|
import { Timestamp } from './Timestamp';
|
||||||
import { LocalizerType } from '../../types/Util';
|
import { LocalizerType } from '../../types/Util';
|
||||||
import { CallHistoryDetailsType } from '../../types/Calling';
|
import { CallMode } from '../../types/Calling';
|
||||||
|
import {
|
||||||
|
CallingNotificationType,
|
||||||
|
getCallingNotificationText,
|
||||||
|
} from '../../util/callingNotification';
|
||||||
|
import { missingCaseError } from '../../util/missingCaseError';
|
||||||
|
import { Tooltip, TooltipPlacement } from '../Tooltip';
|
||||||
|
|
||||||
export type PropsData = {
|
export interface PropsActionsType {
|
||||||
// Can be undefined because it comes from JS.
|
messageSizeChanged: (messageId: string, conversationId: string) => void;
|
||||||
callHistoryDetails?: CallHistoryDetailsType;
|
returnToActiveCall: () => void;
|
||||||
};
|
startCallingLobby: (_: {
|
||||||
|
conversationId: string;
|
||||||
|
isVideoCall: boolean;
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
type PropsHousekeeping = {
|
type PropsHousekeeping = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
conversationId: string;
|
||||||
|
messageId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = PropsData & PropsHousekeeping;
|
type PropsType = CallingNotificationType & PropsActionsType & PropsHousekeeping;
|
||||||
|
|
||||||
export function getCallingNotificationText(
|
export const CallingNotification: React.FC<PropsType> = React.memo(props => {
|
||||||
callHistoryDetails: CallHistoryDetailsType,
|
const { conversationId, i18n, messageId, messageSizeChanged } = props;
|
||||||
i18n: LocalizerType
|
|
||||||
): string {
|
|
||||||
const {
|
|
||||||
wasIncoming,
|
|
||||||
wasVideoCall,
|
|
||||||
wasDeclined,
|
|
||||||
acceptedTime,
|
|
||||||
} = callHistoryDetails;
|
|
||||||
const wasAccepted = Boolean(acceptedTime);
|
|
||||||
|
|
||||||
if (wasIncoming) {
|
const previousHeightRef = useRef<null | number>(null);
|
||||||
if (wasDeclined) {
|
const [height, setHeight] = useState<null | number>(null);
|
||||||
if (wasVideoCall) {
|
|
||||||
return i18n('declinedIncomingVideoCall');
|
|
||||||
}
|
|
||||||
return i18n('declinedIncomingAudioCall');
|
|
||||||
}
|
|
||||||
if (wasAccepted) {
|
|
||||||
if (wasVideoCall) {
|
|
||||||
return i18n('acceptedIncomingVideoCall');
|
|
||||||
}
|
|
||||||
return i18n('acceptedIncomingAudioCall');
|
|
||||||
}
|
|
||||||
if (wasVideoCall) {
|
|
||||||
return i18n('missedIncomingVideoCall');
|
|
||||||
}
|
|
||||||
return i18n('missedIncomingAudioCall');
|
|
||||||
}
|
|
||||||
if (wasAccepted) {
|
|
||||||
if (wasVideoCall) {
|
|
||||||
return i18n('acceptedOutgoingVideoCall');
|
|
||||||
}
|
|
||||||
return i18n('acceptedOutgoingAudioCall');
|
|
||||||
}
|
|
||||||
if (wasVideoCall) {
|
|
||||||
return i18n('missedOrDeclinedOutgoingVideoCall');
|
|
||||||
}
|
|
||||||
return i18n('missedOrDeclinedOutgoingAudioCall');
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CallingNotification = (props: Props): JSX.Element | null => {
|
useEffect(() => {
|
||||||
const { callHistoryDetails, i18n } = props;
|
if (height === null) {
|
||||||
if (!callHistoryDetails) {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
previousHeightRef.current !== null &&
|
||||||
|
height !== previousHeightRef.current
|
||||||
|
) {
|
||||||
|
messageSizeChanged(messageId, conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
previousHeightRef.current = height;
|
||||||
|
}, [height, conversationId, messageId, messageSizeChanged]);
|
||||||
|
|
||||||
|
let timestamp: number;
|
||||||
|
let callType: 'audio' | 'video';
|
||||||
|
switch (props.callMode) {
|
||||||
|
case CallMode.Direct:
|
||||||
|
timestamp = props.acceptedTime || props.endedTime;
|
||||||
|
callType = props.wasVideoCall ? 'video' : 'audio';
|
||||||
|
break;
|
||||||
|
case CallMode.Group:
|
||||||
|
timestamp = props.startedTime;
|
||||||
|
callType = 'video';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
window.log.error(missingCaseError(props));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Measure
|
||||||
|
bounds
|
||||||
|
onResize={({ bounds }) => {
|
||||||
|
if (!bounds) {
|
||||||
|
window.log.error('We should be measuring the bounds');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setHeight(bounds.height);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ measureRef }) => (
|
||||||
|
<div
|
||||||
|
className={`module-message-calling--notification module-message-calling--${callType}`}
|
||||||
|
ref={measureRef}
|
||||||
|
>
|
||||||
|
<div className={`module-message-calling--${callType}__icon`} />
|
||||||
|
{getCallingNotificationText(props, i18n)}
|
||||||
|
<div>
|
||||||
|
<Timestamp
|
||||||
|
i18n={i18n}
|
||||||
|
timestamp={timestamp}
|
||||||
|
extended
|
||||||
|
direction="outgoing"
|
||||||
|
withImageNoCaption={false}
|
||||||
|
withSticker={false}
|
||||||
|
withTapToViewExpired={false}
|
||||||
|
module="module-message__metadata__date"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<CallingNotificationButton {...props} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Measure>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function CallingNotificationButton(props: PropsType) {
|
||||||
|
if (props.callMode !== CallMode.Group || props.ended) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const { acceptedTime, endedTime, wasVideoCall } = callHistoryDetails;
|
|
||||||
const callType = wasVideoCall ? 'video' : 'audio';
|
const {
|
||||||
return (
|
activeCallConversationId,
|
||||||
<div
|
conversationId,
|
||||||
className={`module-message-calling--notification module-message-calling--${callType}`}
|
deviceCount,
|
||||||
|
i18n,
|
||||||
|
maxDevices,
|
||||||
|
returnToActiveCall,
|
||||||
|
startCallingLobby,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
let buttonText: string;
|
||||||
|
let disabledTooltipText: undefined | string;
|
||||||
|
let onClick: undefined | (() => 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'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (deviceCount >= maxDevices) {
|
||||||
|
buttonText = i18n('calling__call-is-full');
|
||||||
|
disabledTooltipText = i18n(
|
||||||
|
'calling__call-notification__button__call-full-tooltip',
|
||||||
|
[String(deviceCount)]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
buttonText = i18n('calling__join');
|
||||||
|
onClick = () => {
|
||||||
|
startCallingLobby({ conversationId, isVideoCall: true });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const button = (
|
||||||
|
<button
|
||||||
|
className="module-message-calling--notification__button"
|
||||||
|
disabled={Boolean(disabledTooltipText)}
|
||||||
|
onClick={onClick}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<div className={`module-message-calling--${callType}__icon`} />
|
{buttonText}
|
||||||
{getCallingNotificationText(callHistoryDetails, i18n)}
|
</button>
|
||||||
<div>
|
|
||||||
<Timestamp
|
|
||||||
i18n={i18n}
|
|
||||||
timestamp={acceptedTime || endedTime}
|
|
||||||
extended
|
|
||||||
direction="outgoing"
|
|
||||||
withImageNoCaption={false}
|
|
||||||
withSticker={false}
|
|
||||||
withTapToViewExpired={false}
|
|
||||||
module="module-message__metadata__date"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
if (disabledTooltipText) {
|
||||||
|
return (
|
||||||
|
<Tooltip content={disabledTooltipText} direction={TooltipPlacement.Top}>
|
||||||
|
{button}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
|
@ -247,6 +247,10 @@ const actions = () => ({
|
||||||
showIdentity: action('showIdentity'),
|
showIdentity: action('showIdentity'),
|
||||||
|
|
||||||
downloadNewVersion: action('downloadNewVersion'),
|
downloadNewVersion: action('downloadNewVersion'),
|
||||||
|
|
||||||
|
messageSizeChanged: action('messageSizeChanged'),
|
||||||
|
startCallingLobby: action('startCallingLobby'),
|
||||||
|
returnToActiveCall: action('returnToActiveCall'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderItem = (id: string) => (
|
const renderItem = (id: string) => (
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { EmojiPicker } from '../emoji/EmojiPicker';
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
import { PropsType as TimelineItemProps, TimelineItem } from './TimelineItem';
|
import { PropsType as TimelineItemProps, TimelineItem } from './TimelineItem';
|
||||||
|
import { CallMode } from '../../types/Calling';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
@ -61,6 +62,9 @@ const getDefaultProps = () => ({
|
||||||
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
scrollToQuotedMessage: action('scrollToQuotedMessage'),
|
||||||
downloadNewVersion: action('downloadNewVersion'),
|
downloadNewVersion: action('downloadNewVersion'),
|
||||||
showIdentity: action('showIdentity'),
|
showIdentity: action('showIdentity'),
|
||||||
|
messageSizeChanged: action('messageSizeChanged'),
|
||||||
|
startCallingLobby: action('startCallingLobby'),
|
||||||
|
returnToActiveCall: action('returnToActiveCall'),
|
||||||
|
|
||||||
renderContact,
|
renderContact,
|
||||||
renderEmojiPicker,
|
renderEmojiPicker,
|
||||||
|
@ -95,149 +99,253 @@ storiesOf('Components/Conversation/TimelineItem', module)
|
||||||
{
|
{
|
||||||
type: 'callHistory',
|
type: 'callHistory',
|
||||||
data: {
|
data: {
|
||||||
callHistoryDetails: {
|
// declined incoming audio
|
||||||
// declined incoming audio
|
callMode: CallMode.Direct,
|
||||||
wasDeclined: true,
|
wasDeclined: true,
|
||||||
wasIncoming: true,
|
wasIncoming: true,
|
||||||
wasVideoCall: false,
|
wasVideoCall: false,
|
||||||
endedTime: Date.now(),
|
endedTime: Date.now(),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'callHistory',
|
type: 'callHistory',
|
||||||
data: {
|
data: {
|
||||||
callHistoryDetails: {
|
// declined incoming video
|
||||||
// declined incoming video
|
callMode: CallMode.Direct,
|
||||||
wasDeclined: true,
|
wasDeclined: true,
|
||||||
wasIncoming: true,
|
wasIncoming: true,
|
||||||
wasVideoCall: true,
|
wasVideoCall: true,
|
||||||
endedTime: Date.now(),
|
endedTime: Date.now(),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'callHistory',
|
type: 'callHistory',
|
||||||
data: {
|
data: {
|
||||||
callHistoryDetails: {
|
// accepted incoming audio
|
||||||
// accepted incoming audio
|
callMode: CallMode.Direct,
|
||||||
acceptedTime: Date.now() - 300,
|
acceptedTime: Date.now() - 300,
|
||||||
wasDeclined: false,
|
wasDeclined: false,
|
||||||
wasIncoming: true,
|
wasIncoming: true,
|
||||||
wasVideoCall: false,
|
wasVideoCall: false,
|
||||||
endedTime: Date.now(),
|
endedTime: Date.now(),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'callHistory',
|
type: 'callHistory',
|
||||||
data: {
|
data: {
|
||||||
callHistoryDetails: {
|
// accepted incoming video
|
||||||
// accepted incoming video
|
callMode: CallMode.Direct,
|
||||||
acceptedTime: Date.now() - 400,
|
acceptedTime: Date.now() - 400,
|
||||||
wasDeclined: false,
|
wasDeclined: false,
|
||||||
wasIncoming: true,
|
wasIncoming: true,
|
||||||
wasVideoCall: true,
|
wasVideoCall: true,
|
||||||
endedTime: Date.now(),
|
endedTime: Date.now(),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'callHistory',
|
type: 'callHistory',
|
||||||
data: {
|
data: {
|
||||||
callHistoryDetails: {
|
// missed (neither accepted nor declined) incoming audio
|
||||||
// missed (neither accepted nor declined) incoming audio
|
callMode: CallMode.Direct,
|
||||||
wasDeclined: false,
|
wasDeclined: false,
|
||||||
wasIncoming: true,
|
wasIncoming: true,
|
||||||
wasVideoCall: false,
|
wasVideoCall: false,
|
||||||
endedTime: Date.now(),
|
endedTime: Date.now(),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'callHistory',
|
type: 'callHistory',
|
||||||
data: {
|
data: {
|
||||||
callHistoryDetails: {
|
// missed (neither accepted nor declined) incoming video
|
||||||
// missed (neither accepted nor declined) incoming video
|
callMode: CallMode.Direct,
|
||||||
wasDeclined: false,
|
wasDeclined: false,
|
||||||
wasIncoming: true,
|
wasIncoming: true,
|
||||||
wasVideoCall: true,
|
wasVideoCall: true,
|
||||||
endedTime: Date.now(),
|
endedTime: Date.now(),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'callHistory',
|
type: 'callHistory',
|
||||||
data: {
|
data: {
|
||||||
callHistoryDetails: {
|
// accepted outgoing audio
|
||||||
// accepted outgoing audio
|
callMode: CallMode.Direct,
|
||||||
acceptedTime: Date.now() - 200,
|
acceptedTime: Date.now() - 200,
|
||||||
wasDeclined: false,
|
wasDeclined: false,
|
||||||
wasIncoming: false,
|
wasIncoming: false,
|
||||||
wasVideoCall: false,
|
wasVideoCall: false,
|
||||||
endedTime: Date.now(),
|
endedTime: Date.now(),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'callHistory',
|
type: 'callHistory',
|
||||||
data: {
|
data: {
|
||||||
callHistoryDetails: {
|
// accepted outgoing video
|
||||||
// accepted outgoing video
|
callMode: CallMode.Direct,
|
||||||
acceptedTime: Date.now() - 200,
|
acceptedTime: Date.now() - 200,
|
||||||
wasDeclined: false,
|
wasDeclined: false,
|
||||||
wasIncoming: false,
|
wasIncoming: false,
|
||||||
wasVideoCall: true,
|
wasVideoCall: true,
|
||||||
endedTime: Date.now(),
|
endedTime: Date.now(),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'callHistory',
|
type: 'callHistory',
|
||||||
data: {
|
data: {
|
||||||
callHistoryDetails: {
|
// declined outgoing audio
|
||||||
// declined outgoing audio
|
callMode: CallMode.Direct,
|
||||||
wasDeclined: true,
|
wasDeclined: true,
|
||||||
wasIncoming: false,
|
wasIncoming: false,
|
||||||
wasVideoCall: false,
|
wasVideoCall: false,
|
||||||
endedTime: Date.now(),
|
endedTime: Date.now(),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'callHistory',
|
type: 'callHistory',
|
||||||
data: {
|
data: {
|
||||||
callHistoryDetails: {
|
// declined outgoing video
|
||||||
// declined outgoing video
|
callMode: CallMode.Direct,
|
||||||
wasDeclined: true,
|
wasDeclined: true,
|
||||||
wasIncoming: false,
|
wasIncoming: false,
|
||||||
wasVideoCall: true,
|
wasVideoCall: true,
|
||||||
endedTime: Date.now(),
|
endedTime: Date.now(),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'callHistory',
|
type: 'callHistory',
|
||||||
data: {
|
data: {
|
||||||
callHistoryDetails: {
|
// missed (neither accepted nor declined) outgoing audio
|
||||||
// missed (neither accepted nor declined) outgoing audio
|
callMode: CallMode.Direct,
|
||||||
wasDeclined: false,
|
wasDeclined: false,
|
||||||
wasIncoming: false,
|
wasIncoming: false,
|
||||||
wasVideoCall: false,
|
wasVideoCall: false,
|
||||||
endedTime: Date.now(),
|
endedTime: Date.now(),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'callHistory',
|
type: 'callHistory',
|
||||||
data: {
|
data: {
|
||||||
callHistoryDetails: {
|
// missed (neither accepted nor declined) outgoing video
|
||||||
// missed (neither accepted nor declined) outgoing video
|
callMode: CallMode.Direct,
|
||||||
wasDeclined: false,
|
wasDeclined: false,
|
||||||
wasIncoming: false,
|
wasIncoming: false,
|
||||||
wasVideoCall: true,
|
wasVideoCall: true,
|
||||||
endedTime: Date.now(),
|
endedTime: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'callHistory',
|
||||||
|
data: {
|
||||||
|
// ongoing group call
|
||||||
|
callMode: CallMode.Group,
|
||||||
|
conversationId: 'abc123',
|
||||||
|
creator: {
|
||||||
|
firstName: 'Luigi',
|
||||||
|
isMe: false,
|
||||||
|
title: 'Luigi Mario',
|
||||||
},
|
},
|
||||||
|
ended: false,
|
||||||
|
deviceCount: 1,
|
||||||
|
maxDevices: 16,
|
||||||
|
startedTime: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'callHistory',
|
||||||
|
data: {
|
||||||
|
// ongoing group call started by you
|
||||||
|
callMode: CallMode.Group,
|
||||||
|
conversationId: 'abc123',
|
||||||
|
creator: {
|
||||||
|
firstName: 'Peach',
|
||||||
|
isMe: true,
|
||||||
|
title: 'Princess Peach',
|
||||||
|
},
|
||||||
|
ended: false,
|
||||||
|
deviceCount: 1,
|
||||||
|
maxDevices: 16,
|
||||||
|
startedTime: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'callHistory',
|
||||||
|
data: {
|
||||||
|
// ongoing group call, creator unknown
|
||||||
|
callMode: CallMode.Group,
|
||||||
|
conversationId: 'abc123',
|
||||||
|
ended: false,
|
||||||
|
deviceCount: 1,
|
||||||
|
maxDevices: 16,
|
||||||
|
startedTime: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'callHistory',
|
||||||
|
data: {
|
||||||
|
// ongoing and active group call
|
||||||
|
callMode: CallMode.Group,
|
||||||
|
activeCallConversationId: 'abc123',
|
||||||
|
conversationId: 'abc123',
|
||||||
|
creator: {
|
||||||
|
firstName: 'Luigi',
|
||||||
|
isMe: false,
|
||||||
|
title: 'Luigi Mario',
|
||||||
|
},
|
||||||
|
ended: false,
|
||||||
|
deviceCount: 1,
|
||||||
|
maxDevices: 16,
|
||||||
|
startedTime: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'callHistory',
|
||||||
|
data: {
|
||||||
|
// ongoing group call, but you're in another one
|
||||||
|
callMode: CallMode.Group,
|
||||||
|
activeCallConversationId: 'abc123',
|
||||||
|
conversationId: 'xyz987',
|
||||||
|
creator: {
|
||||||
|
firstName: 'Luigi',
|
||||||
|
isMe: false,
|
||||||
|
title: 'Luigi Mario',
|
||||||
|
},
|
||||||
|
ended: false,
|
||||||
|
deviceCount: 1,
|
||||||
|
maxDevices: 16,
|
||||||
|
startedTime: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'callHistory',
|
||||||
|
data: {
|
||||||
|
// ongoing full group call
|
||||||
|
callMode: CallMode.Group,
|
||||||
|
conversationId: 'abc123',
|
||||||
|
creator: {
|
||||||
|
firstName: 'Luigi',
|
||||||
|
isMe: false,
|
||||||
|
title: 'Luigi Mario',
|
||||||
|
},
|
||||||
|
ended: false,
|
||||||
|
deviceCount: 16,
|
||||||
|
maxDevices: 16,
|
||||||
|
startedTime: Date.now(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'callHistory',
|
||||||
|
data: {
|
||||||
|
// finished call
|
||||||
|
callMode: CallMode.Group,
|
||||||
|
conversationId: 'abc123',
|
||||||
|
creator: {
|
||||||
|
firstName: 'Luigi',
|
||||||
|
isMe: false,
|
||||||
|
title: 'Luigi Mario',
|
||||||
|
},
|
||||||
|
ended: true,
|
||||||
|
deviceCount: 0,
|
||||||
|
maxDevices: 16,
|
||||||
|
startedTime: Date.now(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -13,8 +13,9 @@ import {
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CallingNotification,
|
CallingNotification,
|
||||||
PropsData as CallingNotificationProps,
|
PropsActionsType as CallingNotificationActionsType,
|
||||||
} from './CallingNotification';
|
} from './CallingNotification';
|
||||||
|
import { CallingNotificationType } from '../../util/callingNotification';
|
||||||
import { InlineNotificationWrapper } from './InlineNotificationWrapper';
|
import { InlineNotificationWrapper } from './InlineNotificationWrapper';
|
||||||
import {
|
import {
|
||||||
PropsActions as UnsupportedMessageActionsType,
|
PropsActions as UnsupportedMessageActionsType,
|
||||||
|
@ -55,7 +56,7 @@ import {
|
||||||
|
|
||||||
type CallHistoryType = {
|
type CallHistoryType = {
|
||||||
type: 'callHistory';
|
type: 'callHistory';
|
||||||
data: CallingNotificationProps;
|
data: CallingNotificationType;
|
||||||
};
|
};
|
||||||
type LinkNotificationType = {
|
type LinkNotificationType = {
|
||||||
type: 'linkNotification';
|
type: 'linkNotification';
|
||||||
|
@ -128,6 +129,7 @@ type PropsLocalType = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsActionsType = MessageActionsType &
|
type PropsActionsType = MessageActionsType &
|
||||||
|
CallingNotificationActionsType &
|
||||||
UnsupportedMessageActionsType &
|
UnsupportedMessageActionsType &
|
||||||
SafetyNumberActionsType;
|
SafetyNumberActionsType;
|
||||||
|
|
||||||
|
@ -143,8 +145,11 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
isSelected,
|
isSelected,
|
||||||
item,
|
item,
|
||||||
i18n,
|
i18n,
|
||||||
|
messageSizeChanged,
|
||||||
renderContact,
|
renderContact,
|
||||||
|
returnToActiveCall,
|
||||||
selectMessage,
|
selectMessage,
|
||||||
|
startCallingLobby,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
|
@ -164,7 +169,17 @@ export class TimelineItem extends React.PureComponent<PropsType> {
|
||||||
<UnsupportedMessage {...this.props} {...item.data} i18n={i18n} />
|
<UnsupportedMessage {...this.props} {...item.data} i18n={i18n} />
|
||||||
);
|
);
|
||||||
} else if (item.type === 'callHistory') {
|
} else if (item.type === 'callHistory') {
|
||||||
notification = <CallingNotification i18n={i18n} {...item.data} />;
|
notification = (
|
||||||
|
<CallingNotification
|
||||||
|
conversationId={conversationId}
|
||||||
|
i18n={i18n}
|
||||||
|
messageId={id}
|
||||||
|
messageSizeChanged={messageSizeChanged}
|
||||||
|
returnToActiveCall={returnToActiveCall}
|
||||||
|
startCallingLobby={startCallingLobby}
|
||||||
|
{...item.data}
|
||||||
|
/>
|
||||||
|
);
|
||||||
} else if (item.type === 'linkNotification') {
|
} else if (item.type === 'linkNotification') {
|
||||||
notification = (
|
notification = (
|
||||||
<div className="module-message-unsynced">
|
<div className="module-message-unsynced">
|
||||||
|
|
4
ts/model-types.d.ts
vendored
4
ts/model-types.d.ts
vendored
|
@ -5,7 +5,7 @@ import * as Backbone from 'backbone';
|
||||||
|
|
||||||
import { GroupV2ChangeType } from './groups';
|
import { GroupV2ChangeType } from './groups';
|
||||||
import { LocalizerType, BodyRangeType, BodyRangesType } from './types/Util';
|
import { LocalizerType, BodyRangeType, BodyRangesType } from './types/Util';
|
||||||
import { CallHistoryDetailsType } from './types/Calling';
|
import { CallHistoryDetailsFromDiskType } from './types/Calling';
|
||||||
import { ColorType } from './types/Colors';
|
import { ColorType } from './types/Colors';
|
||||||
import {
|
import {
|
||||||
ConversationType,
|
ConversationType,
|
||||||
|
@ -59,7 +59,7 @@ export type GroupMigrationType = {
|
||||||
export type MessageAttributesType = {
|
export type MessageAttributesType = {
|
||||||
bodyPending: boolean;
|
bodyPending: boolean;
|
||||||
bodyRanges: BodyRangesType;
|
bodyRanges: BodyRangesType;
|
||||||
callHistoryDetails: CallHistoryDetailsType;
|
callHistoryDetails: CallHistoryDetailsFromDiskType;
|
||||||
changedId: string;
|
changedId: string;
|
||||||
dataMessage: ArrayBuffer | null;
|
dataMessage: ArrayBuffer | null;
|
||||||
decrypted_at: number;
|
decrypted_at: number;
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
ConversationAttributesType,
|
ConversationAttributesType,
|
||||||
VerificationOptions,
|
VerificationOptions,
|
||||||
} from '../model-types.d';
|
} from '../model-types.d';
|
||||||
import { CallHistoryDetailsType } from '../types/Calling';
|
import { CallMode, CallHistoryDetailsType } from '../types/Calling';
|
||||||
import { CallbackResultType, GroupV2InfoType } from '../textsecure/SendMessage';
|
import { CallbackResultType, GroupV2InfoType } from '../textsecure/SendMessage';
|
||||||
import {
|
import {
|
||||||
ConversationType,
|
ConversationType,
|
||||||
|
@ -19,6 +19,7 @@ import {
|
||||||
import { ColorType } from '../types/Colors';
|
import { ColorType } from '../types/Colors';
|
||||||
import { MessageModel } from './messages';
|
import { MessageModel } from './messages';
|
||||||
import { isMuted } from '../util/isMuted';
|
import { isMuted } from '../util/isMuted';
|
||||||
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
import { sniffImageMimeType } from '../util/sniffImageMimeType';
|
import { sniffImageMimeType } from '../util/sniffImageMimeType';
|
||||||
import { MIMEType, IMAGE_WEBP } from '../types/MIME';
|
import { MIMEType, IMAGE_WEBP } from '../types/MIME';
|
||||||
import {
|
import {
|
||||||
|
@ -128,6 +129,8 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
|
|
||||||
intlCollator = new Intl.Collator();
|
intlCollator = new Intl.Collator();
|
||||||
|
|
||||||
|
private cachedLatestGroupCallEraId?: string;
|
||||||
|
|
||||||
// eslint-disable-next-line class-methods-use-this
|
// eslint-disable-next-line class-methods-use-this
|
||||||
defaults(): Partial<ConversationAttributesType> {
|
defaults(): Partial<ConversationAttributesType> {
|
||||||
return {
|
return {
|
||||||
|
@ -2047,14 +2050,36 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
async addCallHistory(
|
async addCallHistory(
|
||||||
callHistoryDetails: CallHistoryDetailsType
|
callHistoryDetails: CallHistoryDetailsType
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { acceptedTime, endedTime, wasDeclined } = callHistoryDetails;
|
let timestamp: number;
|
||||||
|
let unread: boolean;
|
||||||
|
let detailsToSave: CallHistoryDetailsType;
|
||||||
|
|
||||||
|
switch (callHistoryDetails.callMode) {
|
||||||
|
case CallMode.Direct:
|
||||||
|
timestamp = callHistoryDetails.endedTime;
|
||||||
|
unread =
|
||||||
|
!callHistoryDetails.wasDeclined && !callHistoryDetails.acceptedTime;
|
||||||
|
detailsToSave = {
|
||||||
|
...callHistoryDetails,
|
||||||
|
callMode: CallMode.Direct,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case CallMode.Group:
|
||||||
|
timestamp = callHistoryDetails.startedTime;
|
||||||
|
unread = false;
|
||||||
|
detailsToSave = callHistoryDetails;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw missingCaseError(callHistoryDetails);
|
||||||
|
}
|
||||||
|
|
||||||
const message = ({
|
const message = ({
|
||||||
conversationId: this.id,
|
conversationId: this.id,
|
||||||
type: 'call-history',
|
type: 'call-history',
|
||||||
sent_at: endedTime,
|
sent_at: timestamp,
|
||||||
received_at: endedTime,
|
received_at: timestamp,
|
||||||
unread: !wasDeclined && !acceptedTime,
|
unread,
|
||||||
callHistoryDetails,
|
callHistoryDetails: detailsToSave,
|
||||||
// TODO: DESKTOP-722
|
// TODO: DESKTOP-722
|
||||||
} as unknown) as typeof window.Whisper.MessageAttributesType;
|
} as unknown) as typeof window.Whisper.MessageAttributesType;
|
||||||
|
|
||||||
|
@ -2072,6 +2097,27 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
this.trigger('newmessage', model);
|
this.trigger('newmessage', model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateCallHistoryForGroupCall(
|
||||||
|
eraId: string,
|
||||||
|
creatorUuid: string
|
||||||
|
): Promise<void> {
|
||||||
|
const alreadyHasMessage =
|
||||||
|
(this.cachedLatestGroupCallEraId &&
|
||||||
|
this.cachedLatestGroupCallEraId === eraId) ||
|
||||||
|
(await window.Signal.Data.hasGroupCallHistoryMessage(this.id, eraId));
|
||||||
|
|
||||||
|
if (!alreadyHasMessage) {
|
||||||
|
this.addCallHistory({
|
||||||
|
callMode: CallMode.Group,
|
||||||
|
creatorUuid,
|
||||||
|
eraId,
|
||||||
|
startedTime: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cachedLatestGroupCallEraId = eraId;
|
||||||
|
}
|
||||||
|
|
||||||
async addProfileChange(
|
async addProfileChange(
|
||||||
profileChange: unknown,
|
profileChange: unknown,
|
||||||
conversationId?: string
|
conversationId?: string
|
||||||
|
|
|
@ -12,9 +12,13 @@ import {
|
||||||
LastMessageStatus,
|
LastMessageStatus,
|
||||||
ConversationType,
|
ConversationType,
|
||||||
} from '../state/ducks/conversations';
|
} from '../state/ducks/conversations';
|
||||||
|
import { getActiveCall } from '../state/ducks/calling';
|
||||||
|
import { getCallSelector } from '../state/selectors/calling';
|
||||||
import { PropsData } from '../components/conversation/Message';
|
import { PropsData } from '../components/conversation/Message';
|
||||||
import { CallbackResultType } from '../textsecure/SendMessage';
|
import { CallbackResultType } from '../textsecure/SendMessage';
|
||||||
import { ExpirationTimerOptions } from '../util/ExpirationTimerOptions';
|
import { ExpirationTimerOptions } from '../util/ExpirationTimerOptions';
|
||||||
|
import { missingCaseError } from '../util/missingCaseError';
|
||||||
|
import { CallMode } from '../types/Calling';
|
||||||
import { BodyRangesType } from '../types/Util';
|
import { BodyRangesType } from '../types/Util';
|
||||||
import { PropsDataType as GroupsV2Props } from '../components/conversation/GroupV2Change';
|
import { PropsDataType as GroupsV2Props } from '../components/conversation/GroupV2Change';
|
||||||
import {
|
import {
|
||||||
|
@ -29,7 +33,10 @@ import {
|
||||||
ChangeType,
|
ChangeType,
|
||||||
} from '../components/conversation/GroupNotification';
|
} from '../components/conversation/GroupNotification';
|
||||||
import { Props as ResetSessionNotificationProps } from '../components/conversation/ResetSessionNotification';
|
import { Props as ResetSessionNotificationProps } from '../components/conversation/ResetSessionNotification';
|
||||||
import { PropsData as CallingNotificationProps } from '../components/conversation/CallingNotification';
|
import {
|
||||||
|
CallingNotificationType,
|
||||||
|
getCallingNotificationText,
|
||||||
|
} from '../util/callingNotification';
|
||||||
import { PropsType as ProfileChangeNotificationPropsType } from '../components/conversation/ProfileChangeNotification';
|
import { PropsType as ProfileChangeNotificationPropsType } from '../components/conversation/ProfileChangeNotification';
|
||||||
|
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
@ -704,10 +711,67 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getPropsForCallHistory(): CallingNotificationProps {
|
getPropsForCallHistory(): CallingNotificationType | undefined {
|
||||||
return {
|
const callHistoryDetails = this.get('callHistoryDetails');
|
||||||
callHistoryDetails: this.get('callHistoryDetails'),
|
if (!callHistoryDetails) {
|
||||||
};
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (callHistoryDetails.callMode) {
|
||||||
|
// Old messages weren't saved with a call mode.
|
||||||
|
case undefined:
|
||||||
|
case CallMode.Direct:
|
||||||
|
return {
|
||||||
|
...callHistoryDetails,
|
||||||
|
callMode: CallMode.Direct,
|
||||||
|
};
|
||||||
|
case CallMode.Group: {
|
||||||
|
const conversationId = this.get('conversationId');
|
||||||
|
if (!conversationId) {
|
||||||
|
window.log.error(
|
||||||
|
'Message.prototype.getPropsForCallHistory: missing conversation ID; assuming there is no call'
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const creatorConversation = this.findContact(
|
||||||
|
window.ConversationController.ensureContactIds({
|
||||||
|
uuid: callHistoryDetails.creatorUuid,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if (!creatorConversation) {
|
||||||
|
window.log.error(
|
||||||
|
'Message.prototype.getPropsForCallHistory: could not find creator by UUID; bailing'
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reduxState = window.reduxStore.getState();
|
||||||
|
|
||||||
|
let call = getCallSelector(reduxState)(conversationId);
|
||||||
|
if (call && call.callMode !== CallMode.Group) {
|
||||||
|
window.log.error(
|
||||||
|
'Message.prototype.getPropsForCallHistory: there is an unexpected non-group call; pretending it does not exist'
|
||||||
|
);
|
||||||
|
call = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeCallConversationId: getActiveCall(reduxState.calling)
|
||||||
|
?.conversationId,
|
||||||
|
callMode: CallMode.Group,
|
||||||
|
conversationId,
|
||||||
|
creator: creatorConversation.format(),
|
||||||
|
deviceCount: call?.peekInfo.deviceCount ?? 0,
|
||||||
|
ended: callHistoryDetails.eraId !== call?.peekInfo.eraId,
|
||||||
|
maxDevices: call?.peekInfo.maxDevices ?? Infinity,
|
||||||
|
startedTime: callHistoryDetails.startedTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
window.log.error(missingCaseError(callHistoryDetails));
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPropsForProfileChange(): ProfileChangeNotificationPropsType {
|
getPropsForProfileChange(): ProfileChangeNotificationPropsType {
|
||||||
|
@ -1345,12 +1409,16 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isCallHistory()) {
|
if (this.isCallHistory()) {
|
||||||
return {
|
const callingNotification = this.getPropsForCallHistory();
|
||||||
text: window.Signal.Components.getCallingNotificationText(
|
if (callingNotification) {
|
||||||
this.get('callHistoryDetails'),
|
return {
|
||||||
window.i18n
|
text: getCallingNotificationText(callingNotification, window.i18n),
|
||||||
),
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
|
window.log.error(
|
||||||
|
"This call history message doesn't have valid call history"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (this.isExpirationTimerUpdate()) {
|
if (this.isExpirationTimerUpdate()) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
|
|
@ -124,11 +124,17 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
async startCallingLobby(
|
async startCallingLobby(
|
||||||
conversation: ConversationModel,
|
conversationId: string,
|
||||||
isVideoCall: boolean
|
isVideoCall: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
window.log.info('CallingClass.startCallingLobby()');
|
window.log.info('CallingClass.startCallingLobby()');
|
||||||
|
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
if (!conversation) {
|
||||||
|
window.log.error('Could not find conversation, cannot start call lobby');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const conversationProps = conversation.format();
|
const conversationProps = conversation.format();
|
||||||
const callMode = getConversationCallMode(conversationProps);
|
const callMode = getConversationCallMode(conversationProps);
|
||||||
switch (callMode) {
|
switch (callMode) {
|
||||||
|
@ -450,6 +456,10 @@ export class CallingClass {
|
||||||
this.syncGroupCallToRedux(conversationId, groupCall);
|
this.syncGroupCallToRedux(conversationId, groupCall);
|
||||||
},
|
},
|
||||||
onPeekChanged: groupCall => {
|
onPeekChanged: groupCall => {
|
||||||
|
this.updateCallHistoryForGroupCall(
|
||||||
|
conversationId,
|
||||||
|
groupCall.getPeekInfo()
|
||||||
|
);
|
||||||
this.syncGroupCallToRedux(conversationId, groupCall);
|
this.syncGroupCallToRedux(conversationId, groupCall);
|
||||||
},
|
},
|
||||||
async requestMembershipProof(groupCall) {
|
async requestMembershipProof(groupCall) {
|
||||||
|
@ -1459,6 +1469,7 @@ export class CallingClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
conversation.addCallHistory({
|
conversation.addCallHistory({
|
||||||
|
callMode: CallMode.Direct,
|
||||||
wasIncoming: call.isIncoming,
|
wasIncoming: call.isIncoming,
|
||||||
wasVideoCall: call.isVideoCall,
|
wasVideoCall: call.isVideoCall,
|
||||||
wasDeclined,
|
wasDeclined,
|
||||||
|
@ -1472,6 +1483,7 @@ export class CallingClass {
|
||||||
wasVideoCall: boolean
|
wasVideoCall: boolean
|
||||||
) {
|
) {
|
||||||
conversation.addCallHistory({
|
conversation.addCallHistory({
|
||||||
|
callMode: CallMode.Direct,
|
||||||
wasIncoming: true,
|
wasIncoming: true,
|
||||||
wasVideoCall,
|
wasVideoCall,
|
||||||
// Since the user didn't decline, make sure it shows up as a missed call instead
|
// Since the user didn't decline, make sure it shows up as a missed call instead
|
||||||
|
@ -1486,6 +1498,7 @@ export class CallingClass {
|
||||||
_reason: CallEndedReason
|
_reason: CallEndedReason
|
||||||
) {
|
) {
|
||||||
conversation.addCallHistory({
|
conversation.addCallHistory({
|
||||||
|
callMode: CallMode.Direct,
|
||||||
wasIncoming: true,
|
wasIncoming: true,
|
||||||
// We don't actually know, but it doesn't seem that important in this case,
|
// We don't actually know, but it doesn't seem that important in this case,
|
||||||
// but we could maybe plumb this info through RingRTC
|
// but we could maybe plumb this info through RingRTC
|
||||||
|
@ -1496,6 +1509,31 @@ export class CallingClass {
|
||||||
endedTime: Date.now(),
|
endedTime: Date.now(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public updateCallHistoryForGroupCall(
|
||||||
|
conversationId: string,
|
||||||
|
peekInfo: undefined | PeekInfo
|
||||||
|
): void {
|
||||||
|
// If we don't have the necessary pieces to peek, bail. (It's okay if we don't.)
|
||||||
|
if (!peekInfo || !peekInfo.eraId || !peekInfo.creator) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const creatorUuid = arrayBufferToUuid(peekInfo.creator);
|
||||||
|
if (!creatorUuid) {
|
||||||
|
window.log.error('updateCallHistoryForGroupCall(): bad creator UUID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversation = window.ConversationController.get(conversationId);
|
||||||
|
if (!conversation) {
|
||||||
|
window.log.error(
|
||||||
|
'updateCallHistoryForGroupCall(): could not find conversation'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
conversation.updateCallHistoryForGroupCall(peekInfo.eraId, creatorUuid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const calling = new CallingClass();
|
export const calling = new CallingClass();
|
||||||
|
|
|
@ -166,6 +166,7 @@ const dataInterface: ClientInterface = {
|
||||||
getLastConversationActivity,
|
getLastConversationActivity,
|
||||||
getLastConversationPreview,
|
getLastConversationPreview,
|
||||||
getMessageMetricsForConversation,
|
getMessageMetricsForConversation,
|
||||||
|
hasGroupCallHistoryMessage,
|
||||||
migrateConversationMessages,
|
migrateConversationMessages,
|
||||||
|
|
||||||
getUnprocessedCount,
|
getUnprocessedCount,
|
||||||
|
@ -1056,6 +1057,12 @@ async function getMessageMetricsForConversation(conversationId: string) {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
function hasGroupCallHistoryMessage(
|
||||||
|
conversationId: string,
|
||||||
|
eraId: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
return channels.hasGroupCallHistoryMessage(conversationId, eraId);
|
||||||
|
}
|
||||||
async function migrateConversationMessages(
|
async function migrateConversationMessages(
|
||||||
obsoleteId: string,
|
obsoleteId: string,
|
||||||
currentId: string
|
currentId: string
|
||||||
|
|
|
@ -103,6 +103,10 @@ export interface DataInterface {
|
||||||
getMessageMetricsForConversation: (
|
getMessageMetricsForConversation: (
|
||||||
conversationId: string
|
conversationId: string
|
||||||
) => Promise<ConverationMetricsType>;
|
) => Promise<ConverationMetricsType>;
|
||||||
|
hasGroupCallHistoryMessage: (
|
||||||
|
conversationId: string,
|
||||||
|
eraId: string
|
||||||
|
) => Promise<boolean>;
|
||||||
migrateConversationMessages: (
|
migrateConversationMessages: (
|
||||||
obsoleteId: string,
|
obsoleteId: string,
|
||||||
currentId: string
|
currentId: string
|
||||||
|
|
|
@ -145,6 +145,7 @@ const dataInterface: ServerInterface = {
|
||||||
getMessageMetricsForConversation,
|
getMessageMetricsForConversation,
|
||||||
getLastConversationActivity,
|
getLastConversationActivity,
|
||||||
getLastConversationPreview,
|
getLastConversationPreview,
|
||||||
|
hasGroupCallHistoryMessage,
|
||||||
migrateConversationMessages,
|
migrateConversationMessages,
|
||||||
|
|
||||||
getUnprocessedCount,
|
getUnprocessedCount,
|
||||||
|
@ -2880,6 +2881,34 @@ async function getMessageMetricsForConversation(conversationId: string) {
|
||||||
}
|
}
|
||||||
getMessageMetricsForConversation.needsSerial = true;
|
getMessageMetricsForConversation.needsSerial = true;
|
||||||
|
|
||||||
|
async function hasGroupCallHistoryMessage(
|
||||||
|
conversationId: string,
|
||||||
|
eraId: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const db = getInstance();
|
||||||
|
|
||||||
|
const row: unknown = await db.get(
|
||||||
|
`
|
||||||
|
SELECT count(*) FROM messages
|
||||||
|
WHERE conversationId = $conversationId
|
||||||
|
AND type = 'call-history'
|
||||||
|
AND json_extract(json, '$.callHistoryDetails.callMode') = 'Group'
|
||||||
|
AND json_extract(json, '$.callHistoryDetails.eraId') = $eraId
|
||||||
|
LIMIT 1;
|
||||||
|
`,
|
||||||
|
{
|
||||||
|
$conversationId: conversationId,
|
||||||
|
$eraId: eraId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof row === 'object' && row && !Array.isArray(row)) {
|
||||||
|
const count = Number((row as Record<string, unknown>)['count(*)']);
|
||||||
|
return Boolean(count);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async function migrateConversationMessages(
|
async function migrateConversationMessages(
|
||||||
obsoleteId: string,
|
obsoleteId: string,
|
||||||
currentId: string
|
currentId: string
|
||||||
|
|
|
@ -163,6 +163,11 @@ export type SetGroupCallVideoRequestType = {
|
||||||
resolutions: Array<GroupCallVideoRequest>;
|
resolutions: Array<GroupCallVideoRequest>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type StartCallingLobbyType = {
|
||||||
|
conversationId: string;
|
||||||
|
isVideoCall: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type ShowCallLobbyType =
|
export type ShowCallLobbyType =
|
||||||
| {
|
| {
|
||||||
callMode: CallMode.Direct;
|
callMode: CallMode.Direct;
|
||||||
|
@ -220,6 +225,7 @@ const PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED =
|
||||||
'calling/PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED';
|
'calling/PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED';
|
||||||
const REFRESH_IO_DEVICES = 'calling/REFRESH_IO_DEVICES';
|
const REFRESH_IO_DEVICES = 'calling/REFRESH_IO_DEVICES';
|
||||||
const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
|
const REMOTE_VIDEO_CHANGE = 'calling/REMOTE_VIDEO_CHANGE';
|
||||||
|
const RETURN_TO_ACTIVE_CALL = 'calling/RETURN_TO_ACTIVE_CALL';
|
||||||
const SET_LOCAL_AUDIO_FULFILLED = 'calling/SET_LOCAL_AUDIO_FULFILLED';
|
const SET_LOCAL_AUDIO_FULFILLED = 'calling/SET_LOCAL_AUDIO_FULFILLED';
|
||||||
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_FULFILLED';
|
const SET_LOCAL_VIDEO_FULFILLED = 'calling/SET_LOCAL_VIDEO_FULFILLED';
|
||||||
const START_DIRECT_CALL = 'calling/START_DIRECT_CALL';
|
const START_DIRECT_CALL = 'calling/START_DIRECT_CALL';
|
||||||
|
@ -281,7 +287,7 @@ type OutgoingCallActionType = {
|
||||||
payload: StartDirectCallType;
|
payload: StartDirectCallType;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PeekNotConnectedGroupCallFulfilledActionType = {
|
export type PeekNotConnectedGroupCallFulfilledActionType = {
|
||||||
type: 'calling/PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED';
|
type: 'calling/PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED';
|
||||||
payload: {
|
payload: {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
|
@ -300,6 +306,10 @@ type RemoteVideoChangeActionType = {
|
||||||
payload: RemoteVideoChangeType;
|
payload: RemoteVideoChangeType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ReturnToActiveCallActionType = {
|
||||||
|
type: 'calling/RETURN_TO_ACTIVE_CALL';
|
||||||
|
};
|
||||||
|
|
||||||
type SetLocalAudioActionType = {
|
type SetLocalAudioActionType = {
|
||||||
type: 'calling/SET_LOCAL_AUDIO_FULFILLED';
|
type: 'calling/SET_LOCAL_AUDIO_FULFILLED';
|
||||||
payload: SetLocalAudioType;
|
payload: SetLocalAudioType;
|
||||||
|
@ -347,6 +357,7 @@ export type CallingActionType =
|
||||||
| PeekNotConnectedGroupCallFulfilledActionType
|
| PeekNotConnectedGroupCallFulfilledActionType
|
||||||
| RefreshIODevicesActionType
|
| RefreshIODevicesActionType
|
||||||
| RemoteVideoChangeActionType
|
| RemoteVideoChangeActionType
|
||||||
|
| ReturnToActiveCallActionType
|
||||||
| SetLocalAudioActionType
|
| SetLocalAudioActionType
|
||||||
| SetLocalVideoFulfilledActionType
|
| SetLocalVideoFulfilledActionType
|
||||||
| ShowCallLobbyActionType
|
| ShowCallLobbyActionType
|
||||||
|
@ -577,6 +588,8 @@ function peekNotConnectedGroupCall(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
calling.updateCallHistoryForGroupCall(conversationId, peekInfo);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED,
|
type: PEEK_NOT_CONNECTED_GROUP_CALL_FULFILLED,
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -607,6 +620,12 @@ function remoteVideoChange(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function returnToActiveCall(): ReturnToActiveCallActionType {
|
||||||
|
return {
|
||||||
|
type: RETURN_TO_ACTIVE_CALL,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function setLocalPreview(
|
function setLocalPreview(
|
||||||
payload: SetLocalPreviewType
|
payload: SetLocalPreviewType
|
||||||
): ThunkAction<void, RootStateType, unknown, never> {
|
): ThunkAction<void, RootStateType, unknown, never> {
|
||||||
|
@ -695,6 +714,16 @@ function setGroupCallVideoRequest(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startCallingLobby(
|
||||||
|
payload: StartCallingLobbyType
|
||||||
|
): ThunkAction<void, RootStateType, unknown, never> {
|
||||||
|
return () => {
|
||||||
|
calling.startCallingLobby(payload.conversationId, payload.isVideoCall);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This action should be replaced with an action dispatched in the
|
||||||
|
// `startCallingLobby` thunk.
|
||||||
function showCallLobby(payload: ShowCallLobbyType): CallLobbyActionType {
|
function showCallLobby(payload: ShowCallLobbyType): CallLobbyActionType {
|
||||||
return {
|
return {
|
||||||
type: SHOW_CALL_LOBBY,
|
type: SHOW_CALL_LOBBY,
|
||||||
|
@ -765,11 +794,13 @@ export const actions = {
|
||||||
peekNotConnectedGroupCall,
|
peekNotConnectedGroupCall,
|
||||||
refreshIODevices,
|
refreshIODevices,
|
||||||
remoteVideoChange,
|
remoteVideoChange,
|
||||||
|
returnToActiveCall,
|
||||||
setLocalPreview,
|
setLocalPreview,
|
||||||
setRendererCanvas,
|
setRendererCanvas,
|
||||||
setLocalAudio,
|
setLocalAudio,
|
||||||
setLocalVideo,
|
setLocalVideo,
|
||||||
setGroupCallVideoRequest,
|
setGroupCallVideoRequest,
|
||||||
|
startCallingLobby,
|
||||||
showCallLobby,
|
showCallLobby,
|
||||||
startCall,
|
startCall,
|
||||||
toggleParticipants,
|
toggleParticipants,
|
||||||
|
@ -1159,6 +1190,24 @@ export function reducer(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.type === RETURN_TO_ACTIVE_CALL) {
|
||||||
|
const { activeCallState } = state;
|
||||||
|
if (!activeCallState) {
|
||||||
|
window.log.warn(
|
||||||
|
'Cannot return to active call if there is no active call'
|
||||||
|
);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
activeCallState: {
|
||||||
|
...activeCallState,
|
||||||
|
pip: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (action.type === SET_LOCAL_AUDIO_FULFILLED) {
|
if (action.type === SET_LOCAL_AUDIO_FULFILLED) {
|
||||||
if (!state.activeCallState) {
|
if (!state.activeCallState) {
|
||||||
window.log.warn('Cannot set local audio with no active call');
|
window.log.warn('Cannot set local audio with no active call');
|
||||||
|
|
|
@ -20,7 +20,7 @@ import { NoopActionType } from './noop';
|
||||||
import { AttachmentType } from '../../types/Attachment';
|
import { AttachmentType } from '../../types/Attachment';
|
||||||
import { ColorType } from '../../types/Colors';
|
import { ColorType } from '../../types/Colors';
|
||||||
import { BodyRangeType } from '../../types/Util';
|
import { BodyRangeType } from '../../types/Util';
|
||||||
import { CallMode } from '../../types/Calling';
|
import { CallMode, CallHistoryDetailsFromDiskType } from '../../types/Calling';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
|
|
||||||
|
@ -147,6 +147,7 @@ export type MessageType = {
|
||||||
|
|
||||||
errors?: Array<Error>;
|
errors?: Array<Error>;
|
||||||
group_update?: unknown;
|
group_update?: unknown;
|
||||||
|
callHistoryDetails?: CallHistoryDetailsFromDiskType;
|
||||||
|
|
||||||
// No need to go beyond this; unused at this stage, since this goes into
|
// No need to go beyond this; unused at this stage, since this goes into
|
||||||
// a reducer still in plain JavaScript and comes out well-formed
|
// a reducer still in plain JavaScript and comes out well-formed
|
||||||
|
@ -274,6 +275,13 @@ export type MessageDeletedActionType = {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
type MessageSizeChangedActionType = {
|
||||||
|
type: 'MESSAGE_SIZE_CHANGED';
|
||||||
|
payload: {
|
||||||
|
id: string;
|
||||||
|
conversationId: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
export type MessagesAddedActionType = {
|
export type MessagesAddedActionType = {
|
||||||
type: 'MESSAGES_ADDED';
|
type: 'MESSAGES_ADDED';
|
||||||
payload: {
|
payload: {
|
||||||
|
@ -379,6 +387,7 @@ export type ConversationActionType =
|
||||||
| ConversationUnloadedActionType
|
| ConversationUnloadedActionType
|
||||||
| RemoveAllConversationsActionType
|
| RemoveAllConversationsActionType
|
||||||
| MessageSelectedActionType
|
| MessageSelectedActionType
|
||||||
|
| MessageSizeChangedActionType
|
||||||
| MessageChangedActionType
|
| MessageChangedActionType
|
||||||
| MessageDeletedActionType
|
| MessageDeletedActionType
|
||||||
| MessagesAddedActionType
|
| MessagesAddedActionType
|
||||||
|
@ -410,6 +419,7 @@ export const actions = {
|
||||||
selectMessage,
|
selectMessage,
|
||||||
messageDeleted,
|
messageDeleted,
|
||||||
messageChanged,
|
messageChanged,
|
||||||
|
messageSizeChanged,
|
||||||
messagesAdded,
|
messagesAdded,
|
||||||
messagesReset,
|
messagesReset,
|
||||||
setMessagesLoading,
|
setMessagesLoading,
|
||||||
|
@ -514,6 +524,18 @@ function messageDeleted(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function messageSizeChanged(
|
||||||
|
id: string,
|
||||||
|
conversationId: string
|
||||||
|
): MessageSizeChangedActionType {
|
||||||
|
return {
|
||||||
|
type: 'MESSAGE_SIZE_CHANGED',
|
||||||
|
payload: {
|
||||||
|
id,
|
||||||
|
conversationId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
function messagesAdded(
|
function messagesAdded(
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
messages: Array<MessageType>,
|
messages: Array<MessageType>,
|
||||||
|
@ -697,7 +719,7 @@ function showArchivedConversations(): ShowArchivedConversationsActionType {
|
||||||
|
|
||||||
// Reducer
|
// Reducer
|
||||||
|
|
||||||
function getEmptyState(): ConversationsStateType {
|
export function getEmptyState(): ConversationsStateType {
|
||||||
return {
|
return {
|
||||||
conversationLookup: {},
|
conversationLookup: {},
|
||||||
messagesByConversation: {},
|
messagesByConversation: {},
|
||||||
|
@ -926,6 +948,31 @@ export function reducer(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (action.type === 'MESSAGE_SIZE_CHANGED') {
|
||||||
|
const { id, conversationId } = action.payload;
|
||||||
|
|
||||||
|
const existingConversation = getOwn(
|
||||||
|
state.messagesByConversation,
|
||||||
|
conversationId
|
||||||
|
);
|
||||||
|
if (!existingConversation) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
messagesByConversation: {
|
||||||
|
...state.messagesByConversation,
|
||||||
|
[conversationId]: {
|
||||||
|
...existingConversation,
|
||||||
|
heightChangeMessageIds: uniq([
|
||||||
|
...existingConversation.heightChangeMessageIds,
|
||||||
|
id,
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
if (action.type === 'MESSAGES_RESET') {
|
if (action.type === 'MESSAGES_RESET') {
|
||||||
const {
|
const {
|
||||||
conversationId,
|
conversationId,
|
||||||
|
|
|
@ -10,15 +10,22 @@ import {
|
||||||
DirectCallStateType,
|
DirectCallStateType,
|
||||||
} from '../ducks/calling';
|
} from '../ducks/calling';
|
||||||
import { CallMode, CallState } from '../../types/Calling';
|
import { CallMode, CallState } from '../../types/Calling';
|
||||||
|
import { getOwn } from '../../util/getOwn';
|
||||||
|
|
||||||
const getCalling = (state: StateType): CallingStateType => state.calling;
|
const getCalling = (state: StateType): CallingStateType => state.calling;
|
||||||
|
|
||||||
const getCallsByConversation = createSelector(
|
export const getCallsByConversation = createSelector(
|
||||||
getCalling,
|
getCalling,
|
||||||
(state: CallingStateType): CallsByConversationType =>
|
(state: CallingStateType): CallsByConversationType =>
|
||||||
state.callsByConversation
|
state.callsByConversation
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getCallSelector = createSelector(
|
||||||
|
getCallsByConversation,
|
||||||
|
(callsByConversation: CallsByConversationType) => (conversationId: string) =>
|
||||||
|
getOwn(callsByConversation, conversationId)
|
||||||
|
);
|
||||||
|
|
||||||
// In theory, there could be multiple incoming calls. In practice, neither RingRTC nor the
|
// In theory, there could be multiple incoming calls. In practice, neither RingRTC nor the
|
||||||
// UI are ready to handle this.
|
// UI are ready to handle this.
|
||||||
export const getIncomingCall = createSelector(
|
export const getIncomingCall = createSelector(
|
||||||
|
|
|
@ -15,7 +15,8 @@ import {
|
||||||
MessagesByConversationType,
|
MessagesByConversationType,
|
||||||
MessageType,
|
MessageType,
|
||||||
} from '../ducks/conversations';
|
} from '../ducks/conversations';
|
||||||
|
import type { CallsByConversationType } from '../ducks/calling';
|
||||||
|
import { getCallsByConversation } from './calling';
|
||||||
import { getBubbleProps } from '../../shims/Whisper';
|
import { getBubbleProps } from '../../shims/Whisper';
|
||||||
import { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
|
import { PropsDataType as TimelinePropsType } from '../../components/conversation/Timeline';
|
||||||
import { TimelineItemType } from '../../components/conversation/TimelineItem';
|
import { TimelineItemType } from '../../components/conversation/TimelineItem';
|
||||||
|
@ -254,6 +255,7 @@ export function _messageSelector(
|
||||||
_ourNumber: string,
|
_ourNumber: string,
|
||||||
_regionCode: string,
|
_regionCode: string,
|
||||||
interactionMode: 'mouse' | 'keyboard',
|
interactionMode: 'mouse' | 'keyboard',
|
||||||
|
_callsByConversation: CallsByConversationType,
|
||||||
_conversation?: ConversationType,
|
_conversation?: ConversationType,
|
||||||
_author?: ConversationType,
|
_author?: ConversationType,
|
||||||
_quoted?: ConversationType,
|
_quoted?: ConversationType,
|
||||||
|
@ -292,6 +294,7 @@ type CachedMessageSelectorType = (
|
||||||
ourNumber: string,
|
ourNumber: string,
|
||||||
regionCode: string,
|
regionCode: string,
|
||||||
interactionMode: 'mouse' | 'keyboard',
|
interactionMode: 'mouse' | 'keyboard',
|
||||||
|
callsByConversation: CallsByConversationType,
|
||||||
conversation?: ConversationType,
|
conversation?: ConversationType,
|
||||||
author?: ConversationType,
|
author?: ConversationType,
|
||||||
quoted?: ConversationType,
|
quoted?: ConversationType,
|
||||||
|
@ -317,6 +320,7 @@ export const getMessageSelector = createSelector(
|
||||||
getRegionCode,
|
getRegionCode,
|
||||||
getUserNumber,
|
getUserNumber,
|
||||||
getInteractionMode,
|
getInteractionMode,
|
||||||
|
getCallsByConversation,
|
||||||
(
|
(
|
||||||
messageSelector: CachedMessageSelectorType,
|
messageSelector: CachedMessageSelectorType,
|
||||||
messageLookup: MessageLookupType,
|
messageLookup: MessageLookupType,
|
||||||
|
@ -324,7 +328,8 @@ export const getMessageSelector = createSelector(
|
||||||
conversationSelector: GetConversationByIdType,
|
conversationSelector: GetConversationByIdType,
|
||||||
regionCode: string,
|
regionCode: string,
|
||||||
ourNumber: string,
|
ourNumber: string,
|
||||||
interactionMode: 'keyboard' | 'mouse'
|
interactionMode: 'keyboard' | 'mouse',
|
||||||
|
callsByConversation: CallsByConversationType
|
||||||
): GetMessageByIdType => {
|
): GetMessageByIdType => {
|
||||||
return (id: string) => {
|
return (id: string) => {
|
||||||
const message = messageLookup[id];
|
const message = messageLookup[id];
|
||||||
|
@ -352,6 +357,7 @@ export const getMessageSelector = createSelector(
|
||||||
ourNumber,
|
ourNumber,
|
||||||
regionCode,
|
regionCode,
|
||||||
interactionMode,
|
interactionMode,
|
||||||
|
callsByConversation,
|
||||||
conversation,
|
conversation,
|
||||||
author,
|
author,
|
||||||
quoted,
|
quoted,
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
import { set } from 'lodash/fp';
|
||||||
import {
|
import {
|
||||||
actions,
|
actions,
|
||||||
ConversationMessageType,
|
ConversationMessageType,
|
||||||
|
@ -13,7 +14,11 @@ import {
|
||||||
} from '../../../state/ducks/conversations';
|
} from '../../../state/ducks/conversations';
|
||||||
import { CallMode } from '../../../types/Calling';
|
import { CallMode } from '../../../types/Calling';
|
||||||
|
|
||||||
const { repairNewestMessage, repairOldestMessage } = actions;
|
const {
|
||||||
|
messageSizeChanged,
|
||||||
|
repairNewestMessage,
|
||||||
|
repairOldestMessage,
|
||||||
|
} = actions;
|
||||||
|
|
||||||
describe('both/state/ducks/conversations', () => {
|
describe('both/state/ducks/conversations', () => {
|
||||||
describe('helpers', () => {
|
describe('helpers', () => {
|
||||||
|
@ -167,6 +172,76 @@ describe('both/state/ducks/conversations', () => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe('MESSAGE_SIZE_CHANGED', () => {
|
||||||
|
const stateWithActiveConversation = {
|
||||||
|
...getDefaultState(),
|
||||||
|
messagesByConversation: {
|
||||||
|
[conversationId]: {
|
||||||
|
heightChangeMessageIds: [],
|
||||||
|
isLoadingMessages: false,
|
||||||
|
isNearBottom: true,
|
||||||
|
messageIds: [messageId],
|
||||||
|
metrics: { totalUnread: 0 },
|
||||||
|
resetCounter: 0,
|
||||||
|
scrollToMessageCounter: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messagesLookup: {
|
||||||
|
[messageId]: getDefaultMessage(messageId),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('does nothing if no conversation is active', () => {
|
||||||
|
const state = getDefaultState();
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
reducer(state, messageSizeChanged('messageId', 'convoId')),
|
||||||
|
state
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does nothing if a different conversation is active', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
reducer(
|
||||||
|
stateWithActiveConversation,
|
||||||
|
messageSizeChanged(messageId, 'another-conversation-guid')
|
||||||
|
),
|
||||||
|
stateWithActiveConversation
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds the message ID to the list of messages with changed heights', () => {
|
||||||
|
const result = reducer(
|
||||||
|
stateWithActiveConversation,
|
||||||
|
messageSizeChanged(messageId, conversationId)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.sameMembers(
|
||||||
|
result.messagesByConversation[conversationId]
|
||||||
|
?.heightChangeMessageIds || [],
|
||||||
|
[messageId]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("doesn't add duplicates to the list of changed-heights messages", () => {
|
||||||
|
const state = set(
|
||||||
|
['messagesByConversation', conversationId, 'heightChangeMessageIds'],
|
||||||
|
[messageId],
|
||||||
|
stateWithActiveConversation
|
||||||
|
);
|
||||||
|
const result = reducer(
|
||||||
|
state,
|
||||||
|
messageSizeChanged(messageId, conversationId)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.sameMembers(
|
||||||
|
result.messagesByConversation[conversationId]
|
||||||
|
?.heightChangeMessageIds || [],
|
||||||
|
[messageId]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('REPAIR_NEWEST_MESSAGE', () => {
|
describe('REPAIR_NEWEST_MESSAGE', () => {
|
||||||
it('updates newest', () => {
|
it('updates newest', () => {
|
||||||
const action = repairNewestMessage(conversationId);
|
const action = repairNewestMessage(conversationId);
|
||||||
|
|
115
ts/test-both/util/callingNotification_test.ts
Normal file
115
ts/test-both/util/callingNotification_test.ts
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { getCallingNotificationText } from '../../util/callingNotification';
|
||||||
|
import { CallMode } from '../../types/Calling';
|
||||||
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
|
||||||
|
describe('calling notification helpers', () => {
|
||||||
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
describe('getCallingNotificationText', () => {
|
||||||
|
// Direct call behavior is not tested here.
|
||||||
|
|
||||||
|
it('says that the call has ended', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getCallingNotificationText(
|
||||||
|
{
|
||||||
|
callMode: CallMode.Group,
|
||||||
|
conversationId: 'abc123',
|
||||||
|
ended: true,
|
||||||
|
deviceCount: 1,
|
||||||
|
maxDevices: 23,
|
||||||
|
startedTime: Date.now(),
|
||||||
|
},
|
||||||
|
i18n
|
||||||
|
),
|
||||||
|
'The group call has ended'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes the creator's first name when describing a call", () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getCallingNotificationText(
|
||||||
|
{
|
||||||
|
callMode: CallMode.Group,
|
||||||
|
conversationId: 'abc123',
|
||||||
|
creator: {
|
||||||
|
firstName: 'Luigi',
|
||||||
|
isMe: false,
|
||||||
|
title: 'Luigi Mario',
|
||||||
|
},
|
||||||
|
ended: false,
|
||||||
|
deviceCount: 1,
|
||||||
|
maxDevices: 23,
|
||||||
|
startedTime: Date.now(),
|
||||||
|
},
|
||||||
|
i18n
|
||||||
|
),
|
||||||
|
'Luigi started a group call'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("if the creator doesn't have a first name, falls back to their title", () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getCallingNotificationText(
|
||||||
|
{
|
||||||
|
callMode: CallMode.Group,
|
||||||
|
conversationId: 'abc123',
|
||||||
|
creator: {
|
||||||
|
isMe: false,
|
||||||
|
title: 'Luigi Mario',
|
||||||
|
},
|
||||||
|
ended: false,
|
||||||
|
deviceCount: 1,
|
||||||
|
maxDevices: 23,
|
||||||
|
startedTime: Date.now(),
|
||||||
|
},
|
||||||
|
i18n
|
||||||
|
),
|
||||||
|
'Luigi Mario started a group call'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a special message if you were the one to start the call', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getCallingNotificationText(
|
||||||
|
{
|
||||||
|
callMode: CallMode.Group,
|
||||||
|
conversationId: 'abc123',
|
||||||
|
creator: {
|
||||||
|
firstName: 'ShouldBeIgnored',
|
||||||
|
isMe: true,
|
||||||
|
title: 'ShouldBeIgnored Smith',
|
||||||
|
},
|
||||||
|
ended: false,
|
||||||
|
deviceCount: 1,
|
||||||
|
maxDevices: 23,
|
||||||
|
startedTime: Date.now(),
|
||||||
|
},
|
||||||
|
i18n
|
||||||
|
),
|
||||||
|
'You started a group call'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles an unknown creator', () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getCallingNotificationText(
|
||||||
|
{
|
||||||
|
callMode: CallMode.Group,
|
||||||
|
conversationId: 'abc123',
|
||||||
|
ended: false,
|
||||||
|
deviceCount: 1,
|
||||||
|
maxDevices: 23,
|
||||||
|
startedTime: Date.now(),
|
||||||
|
},
|
||||||
|
i18n
|
||||||
|
),
|
||||||
|
'A group call was started'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -27,7 +27,7 @@ describe('calling duck', () => {
|
||||||
...getEmptyState(),
|
...getEmptyState(),
|
||||||
callsByConversation: {
|
callsByConversation: {
|
||||||
'fake-direct-call-conversation-id': {
|
'fake-direct-call-conversation-id': {
|
||||||
callMode: CallMode.Direct,
|
callMode: CallMode.Direct as CallMode.Direct,
|
||||||
conversationId: 'fake-direct-call-conversation-id',
|
conversationId: 'fake-direct-call-conversation-id',
|
||||||
callState: CallState.Accepted,
|
callState: CallState.Accepted,
|
||||||
isIncoming: false,
|
isIncoming: false,
|
||||||
|
@ -37,7 +37,7 @@ describe('calling duck', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateWithActiveDirectCall: CallingStateType = {
|
const stateWithActiveDirectCall = {
|
||||||
...stateWithDirectCall,
|
...stateWithDirectCall,
|
||||||
activeCallState: {
|
activeCallState: {
|
||||||
conversationId: 'fake-direct-call-conversation-id',
|
conversationId: 'fake-direct-call-conversation-id',
|
||||||
|
@ -49,11 +49,11 @@ describe('calling duck', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateWithIncomingDirectCall: CallingStateType = {
|
const stateWithIncomingDirectCall = {
|
||||||
...getEmptyState(),
|
...getEmptyState(),
|
||||||
callsByConversation: {
|
callsByConversation: {
|
||||||
'fake-direct-call-conversation-id': {
|
'fake-direct-call-conversation-id': {
|
||||||
callMode: CallMode.Direct,
|
callMode: CallMode.Direct as CallMode.Direct,
|
||||||
conversationId: 'fake-direct-call-conversation-id',
|
conversationId: 'fake-direct-call-conversation-id',
|
||||||
callState: CallState.Ringing,
|
callState: CallState.Ringing,
|
||||||
isIncoming: true,
|
isIncoming: true,
|
||||||
|
@ -63,11 +63,11 @@ describe('calling duck', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateWithGroupCall: CallingStateType = {
|
const stateWithGroupCall = {
|
||||||
...getEmptyState(),
|
...getEmptyState(),
|
||||||
callsByConversation: {
|
callsByConversation: {
|
||||||
'fake-group-call-conversation-id': {
|
'fake-group-call-conversation-id': {
|
||||||
callMode: CallMode.Group,
|
callMode: CallMode.Group as CallMode.Group,
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
connectionState: GroupCallConnectionState.Connected,
|
connectionState: GroupCallConnectionState.Connected,
|
||||||
joinState: GroupCallJoinState.NotJoined,
|
joinState: GroupCallJoinState.NotJoined,
|
||||||
|
@ -91,7 +91,7 @@ describe('calling duck', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateWithActiveGroupCall: CallingStateType = {
|
const stateWithActiveGroupCall = {
|
||||||
...stateWithGroupCall,
|
...stateWithGroupCall,
|
||||||
activeCallState: {
|
activeCallState: {
|
||||||
conversationId: 'fake-group-call-conversation-id',
|
conversationId: 'fake-group-call-conversation-id',
|
||||||
|
@ -624,6 +624,10 @@ describe('calling duck', () => {
|
||||||
callingService,
|
callingService,
|
||||||
'peekGroupCall'
|
'peekGroupCall'
|
||||||
);
|
);
|
||||||
|
this.callingServiceUpdateCallHistoryForGroupCall = this.sandbox.stub(
|
||||||
|
callingService,
|
||||||
|
'updateCallHistoryForGroupCall'
|
||||||
|
);
|
||||||
this.clock = this.sandbox.useFakeTimers();
|
this.clock = this.sandbox.useFakeTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -677,6 +681,29 @@ describe('calling duck', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('returnToActiveCall', () => {
|
||||||
|
const { returnToActiveCall } = actions;
|
||||||
|
|
||||||
|
it('does nothing if not in PiP mode', () => {
|
||||||
|
const result = reducer(stateWithActiveDirectCall, returnToActiveCall());
|
||||||
|
|
||||||
|
assert.deepEqual(result, stateWithActiveDirectCall);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes the PiP', () => {
|
||||||
|
const state = {
|
||||||
|
...stateWithActiveDirectCall,
|
||||||
|
activeCallState: {
|
||||||
|
...stateWithActiveDirectCall.activeCallState,
|
||||||
|
pip: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = reducer(state, returnToActiveCall());
|
||||||
|
|
||||||
|
assert.deepEqual(result, stateWithActiveDirectCall);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('setLocalAudio', () => {
|
describe('setLocalAudio', () => {
|
||||||
const { setLocalAudio } = actions;
|
const { setLocalAudio } = actions;
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,11 @@ import { assert } from 'chai';
|
||||||
import { reducer as rootReducer } from '../../../state/reducer';
|
import { reducer as rootReducer } from '../../../state/reducer';
|
||||||
import { noopAction } from '../../../state/ducks/noop';
|
import { noopAction } from '../../../state/ducks/noop';
|
||||||
import { CallMode, CallState } from '../../../types/Calling';
|
import { CallMode, CallState } from '../../../types/Calling';
|
||||||
import { getIncomingCall } from '../../../state/selectors/calling';
|
import {
|
||||||
|
getCallsByConversation,
|
||||||
|
getCallSelector,
|
||||||
|
getIncomingCall,
|
||||||
|
} from '../../../state/selectors/calling';
|
||||||
import { getEmptyState, CallingStateType } from '../../../state/ducks/calling';
|
import { getEmptyState, CallingStateType } from '../../../state/ducks/calling';
|
||||||
|
|
||||||
describe('state/selectors/calling', () => {
|
describe('state/selectors/calling', () => {
|
||||||
|
@ -56,6 +60,50 @@ describe('state/selectors/calling', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
describe('getCallsByConversation', () => {
|
||||||
|
it('returns state.calling.callsByConversation', () => {
|
||||||
|
assert.deepEqual(getCallsByConversation(getEmptyRootState()), {});
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
getCallsByConversation(getCallingState(stateWithDirectCall)),
|
||||||
|
{
|
||||||
|
'fake-direct-call-conversation-id': {
|
||||||
|
callMode: CallMode.Direct,
|
||||||
|
conversationId: 'fake-direct-call-conversation-id',
|
||||||
|
callState: CallState.Accepted,
|
||||||
|
isIncoming: false,
|
||||||
|
isVideoCall: false,
|
||||||
|
hasRemoteVideo: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getCallSelector', () => {
|
||||||
|
it('returns a selector that returns undefined if selecting a conversation with no call', () => {
|
||||||
|
assert.isUndefined(
|
||||||
|
getCallSelector(getEmptyRootState())('conversation-id')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a selector that returns a conversation's call", () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
getCallSelector(getCallingState(stateWithDirectCall))(
|
||||||
|
'fake-direct-call-conversation-id'
|
||||||
|
),
|
||||||
|
{
|
||||||
|
callMode: CallMode.Direct,
|
||||||
|
conversationId: 'fake-direct-call-conversation-id',
|
||||||
|
callState: CallState.Accepted,
|
||||||
|
isIncoming: false,
|
||||||
|
isVideoCall: false,
|
||||||
|
hasRemoteVideo: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getIncomingCall', () => {
|
describe('getIncomingCall', () => {
|
||||||
it('returns undefined if there are no calls', () => {
|
it('returns undefined if there are no calls', () => {
|
||||||
assert.isUndefined(getIncomingCall(getEmptyRootState()));
|
assert.isUndefined(getIncomingCall(getEmptyRootState()));
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
import { ColorType } from './Colors';
|
import { ColorType } from './Colors';
|
||||||
import { ConversationType } from '../state/ducks/conversations';
|
import { ConversationType } from '../state/ducks/conversations';
|
||||||
|
|
||||||
|
// These are strings (1) for the database (2) for Storybook.
|
||||||
export enum CallMode {
|
export enum CallMode {
|
||||||
None = 'None',
|
None = 'None',
|
||||||
Direct = 'Direct',
|
Direct = 'Direct',
|
||||||
|
@ -153,13 +154,31 @@ export type MediaDeviceSettings = {
|
||||||
selectedCamera: string | undefined;
|
selectedCamera: string | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CallHistoryDetailsType = {
|
interface DirectCallHistoryDetailsType {
|
||||||
|
callMode: CallMode.Direct;
|
||||||
wasIncoming: boolean;
|
wasIncoming: boolean;
|
||||||
wasVideoCall: boolean;
|
wasVideoCall: boolean;
|
||||||
wasDeclined: boolean;
|
wasDeclined: boolean;
|
||||||
acceptedTime?: number;
|
acceptedTime?: number;
|
||||||
endedTime: number;
|
endedTime: number;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
interface GroupCallHistoryDetailsType {
|
||||||
|
callMode: CallMode.Group;
|
||||||
|
creatorUuid: string;
|
||||||
|
eraId: string;
|
||||||
|
startedTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CallHistoryDetailsType =
|
||||||
|
| DirectCallHistoryDetailsType
|
||||||
|
| GroupCallHistoryDetailsType;
|
||||||
|
|
||||||
|
// Old messages weren't saved with a `callMode`.
|
||||||
|
export type CallHistoryDetailsFromDiskType =
|
||||||
|
| (Omit<DirectCallHistoryDetailsType, 'callMode'> &
|
||||||
|
Partial<Pick<DirectCallHistoryDetailsType, 'callMode'>>)
|
||||||
|
| GroupCallHistoryDetailsType;
|
||||||
|
|
||||||
export type ChangeIODevicePayloadType =
|
export type ChangeIODevicePayloadType =
|
||||||
| { type: CallingDeviceType.CAMERA; selectedDevice: string }
|
| { type: CallingDeviceType.CAMERA; selectedDevice: string }
|
||||||
|
|
108
ts/util/callingNotification.ts
Normal file
108
ts/util/callingNotification.ts
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { LocalizerType } from '../types/Util';
|
||||||
|
import { CallMode } from '../types/Calling';
|
||||||
|
import { missingCaseError } from './missingCaseError';
|
||||||
|
|
||||||
|
interface DirectCallNotificationType {
|
||||||
|
callMode: CallMode.Direct;
|
||||||
|
wasIncoming: boolean;
|
||||||
|
wasVideoCall: boolean;
|
||||||
|
wasDeclined: boolean;
|
||||||
|
acceptedTime?: number;
|
||||||
|
endedTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupCallNotificationType {
|
||||||
|
activeCallConversationId?: string;
|
||||||
|
callMode: CallMode.Group;
|
||||||
|
conversationId: string;
|
||||||
|
creator?: {
|
||||||
|
firstName?: string;
|
||||||
|
isMe?: boolean;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
ended: boolean;
|
||||||
|
deviceCount: number;
|
||||||
|
maxDevices: number;
|
||||||
|
startedTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CallingNotificationType =
|
||||||
|
| DirectCallNotificationType
|
||||||
|
| GroupCallNotificationType;
|
||||||
|
|
||||||
|
function getDirectCallNotificationText(
|
||||||
|
{
|
||||||
|
wasIncoming,
|
||||||
|
wasVideoCall,
|
||||||
|
wasDeclined,
|
||||||
|
acceptedTime,
|
||||||
|
}: DirectCallNotificationType,
|
||||||
|
i18n: LocalizerType
|
||||||
|
): string {
|
||||||
|
const wasAccepted = Boolean(acceptedTime);
|
||||||
|
|
||||||
|
if (wasIncoming) {
|
||||||
|
if (wasDeclined) {
|
||||||
|
if (wasVideoCall) {
|
||||||
|
return i18n('declinedIncomingVideoCall');
|
||||||
|
}
|
||||||
|
return i18n('declinedIncomingAudioCall');
|
||||||
|
}
|
||||||
|
if (wasAccepted) {
|
||||||
|
if (wasVideoCall) {
|
||||||
|
return i18n('acceptedIncomingVideoCall');
|
||||||
|
}
|
||||||
|
return i18n('acceptedIncomingAudioCall');
|
||||||
|
}
|
||||||
|
if (wasVideoCall) {
|
||||||
|
return i18n('missedIncomingVideoCall');
|
||||||
|
}
|
||||||
|
return i18n('missedIncomingAudioCall');
|
||||||
|
}
|
||||||
|
if (wasAccepted) {
|
||||||
|
if (wasVideoCall) {
|
||||||
|
return i18n('acceptedOutgoingVideoCall');
|
||||||
|
}
|
||||||
|
return i18n('acceptedOutgoingAudioCall');
|
||||||
|
}
|
||||||
|
if (wasVideoCall) {
|
||||||
|
return i18n('missedOrDeclinedOutgoingVideoCall');
|
||||||
|
}
|
||||||
|
return i18n('missedOrDeclinedOutgoingAudioCall');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGroupCallNotificationText(
|
||||||
|
notification: GroupCallNotificationType,
|
||||||
|
i18n: LocalizerType
|
||||||
|
): string {
|
||||||
|
if (notification.ended) {
|
||||||
|
return i18n('calling__call-notification__ended');
|
||||||
|
}
|
||||||
|
if (!notification.creator) {
|
||||||
|
return i18n('calling__call-notification__started-by-someone');
|
||||||
|
}
|
||||||
|
if (notification.creator.isMe) {
|
||||||
|
return i18n('calling__call-notification__started-by-you');
|
||||||
|
}
|
||||||
|
return i18n('calling__call-notification__started', [
|
||||||
|
notification.creator.firstName || notification.creator.title,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCallingNotificationText(
|
||||||
|
notification: CallingNotificationType,
|
||||||
|
i18n: LocalizerType
|
||||||
|
): string {
|
||||||
|
switch (notification.callMode) {
|
||||||
|
case CallMode.Direct:
|
||||||
|
return getDirectCallNotificationText(notification, i18n);
|
||||||
|
case CallMode.Group:
|
||||||
|
return getGroupCallNotificationText(notification, i18n);
|
||||||
|
default:
|
||||||
|
window.log.error(missingCaseError(notification));
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -14699,6 +14699,24 @@
|
||||||
"updated": "2020-10-26T19:12:24.410Z",
|
"updated": "2020-10-26T19:12:24.410Z",
|
||||||
"reasonDetail": "Only used to focus the element."
|
"reasonDetail": "Only used to focus the element."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/components/Tooltip.js",
|
||||||
|
"line": " const wrapperRef = react_1.default.useRef(null);",
|
||||||
|
"lineNumber": 17,
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2020-12-04T00:11:08.128Z",
|
||||||
|
"reasonDetail": "Used to add (and remove) event listeners."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/components/conversation/CallingNotification.js",
|
||||||
|
"line": " const previousHeightRef = react_1.useRef(null);",
|
||||||
|
"lineNumber": 24,
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2020-12-04T00:11:08.128Z",
|
||||||
|
"reasonDetail": "Doesn't interact with the DOM."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/conversation/ContactModal.js",
|
"path": "ts/components/conversation/ContactModal.js",
|
||||||
|
@ -15167,4 +15185,4 @@
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2020-09-08T23:07:22.682Z"
|
"updated": "2020-09-08T23:07:22.682Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -478,7 +478,6 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
'onOutgoingAudioCallInConversation: about to start an audio call'
|
'onOutgoingAudioCallInConversation: about to start an audio call'
|
||||||
);
|
);
|
||||||
|
|
||||||
const conversation = this.model;
|
|
||||||
const isVideoCall = false;
|
const isVideoCall = false;
|
||||||
|
|
||||||
if (await this.isCallSafe()) {
|
if (await this.isCallSafe()) {
|
||||||
|
@ -486,7 +485,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
'onOutgoingAudioCallInConversation: call is deemed "safe". Making call'
|
'onOutgoingAudioCallInConversation: call is deemed "safe". Making call'
|
||||||
);
|
);
|
||||||
await window.Signal.Services.calling.startCallingLobby(
|
await window.Signal.Services.calling.startCallingLobby(
|
||||||
conversation,
|
this.model.id,
|
||||||
isVideoCall
|
isVideoCall
|
||||||
);
|
);
|
||||||
window.log.info(
|
window.log.info(
|
||||||
|
@ -503,7 +502,6 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
window.log.info(
|
window.log.info(
|
||||||
'onOutgoingVideoCallInConversation: about to start a video call'
|
'onOutgoingVideoCallInConversation: about to start a video call'
|
||||||
);
|
);
|
||||||
const conversation = this.model;
|
|
||||||
const isVideoCall = true;
|
const isVideoCall = true;
|
||||||
|
|
||||||
if (await this.isCallSafe()) {
|
if (await this.isCallSafe()) {
|
||||||
|
@ -511,7 +509,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
'onOutgoingVideoCallInConversation: call is deemed "safe". Making call'
|
'onOutgoingVideoCallInConversation: call is deemed "safe". Making call'
|
||||||
);
|
);
|
||||||
await window.Signal.Services.calling.startCallingLobby(
|
await window.Signal.Services.calling.startCallingLobby(
|
||||||
conversation,
|
this.model.id,
|
||||||
isVideoCall
|
isVideoCall
|
||||||
);
|
);
|
||||||
window.log.info(
|
window.log.info(
|
||||||
|
|
6
ts/window.d.ts
vendored
6
ts/window.d.ts
vendored
|
@ -27,7 +27,6 @@ import * as Crypto from './Crypto';
|
||||||
import * as RemoteConfig from './RemoteConfig';
|
import * as RemoteConfig from './RemoteConfig';
|
||||||
import * as zkgroup from './util/zkgroup';
|
import * as zkgroup from './util/zkgroup';
|
||||||
import { LocalizerType, BodyRangesType, BodyRangeType } from './types/Util';
|
import { LocalizerType, BodyRangesType, BodyRangeType } from './types/Util';
|
||||||
import { CallHistoryDetailsType } from './types/Calling';
|
|
||||||
import { ColorType } from './types/Colors';
|
import { ColorType } from './types/Colors';
|
||||||
import { ConversationController } from './ConversationController';
|
import { ConversationController } from './ConversationController';
|
||||||
import { ReduxActions } from './state/types';
|
import { ReduxActions } from './state/types';
|
||||||
|
@ -409,11 +408,6 @@ declare global {
|
||||||
ProgressModal: typeof ProgressModal;
|
ProgressModal: typeof ProgressModal;
|
||||||
Quote: any;
|
Quote: any;
|
||||||
StagedLinkPreview: any;
|
StagedLinkPreview: any;
|
||||||
|
|
||||||
getCallingNotificationText: (
|
|
||||||
callHistoryDetails: unknown,
|
|
||||||
i18n: unknown
|
|
||||||
) => string;
|
|
||||||
};
|
};
|
||||||
OS: {
|
OS: {
|
||||||
isLinux: () => boolean;
|
isLinux: () => boolean;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue