Hide call buttons when on call

This commit is contained in:
Evan Hahn 2020-10-30 12:52:21 -05:00 committed by Evan Hahn
parent a7854c6083
commit decc93532b
18 changed files with 622 additions and 366 deletions

View File

@ -1,9 +1,6 @@
/* global
_,
Backbone,
i18n,
MessageController,
moment,
Whisper
*/
@ -98,59 +95,4 @@
},
update: debouncedCheckExpiringMessages,
};
const TimerOption = Backbone.Model.extend({
getName() {
return (
i18n(['timerOption', this.get('time'), this.get('unit')].join('_')) ||
moment.duration(this.get('time'), this.get('unit')).humanize()
);
},
getAbbreviated() {
return i18n(
['timerOption', this.get('time'), this.get('unit'), 'abbreviated'].join(
'_'
)
);
},
});
Whisper.ExpirationTimerOptions = new (Backbone.Collection.extend({
model: TimerOption,
getName(seconds = 0) {
const o = this.findWhere({ seconds });
if (o) {
return o.getName();
}
return [seconds, 'seconds'].join(' ');
},
getAbbreviated(seconds = 0) {
const o = this.findWhere({ seconds });
if (o) {
return o.getAbbreviated();
}
return [seconds, 's'].join('');
},
}))(
[
[0, 'seconds'],
[5, 'seconds'],
[10, 'seconds'],
[30, 'seconds'],
[1, 'minute'],
[5, 'minutes'],
[30, 'minutes'],
[1, 'hour'],
[6, 'hours'],
[12, 'hours'],
[1, 'day'],
[1, 'week'],
].map(o => {
const duration = moment.duration(o[0], o[1]); // 5, 'seconds'
return {
time: o[0],
unit: o[1],
seconds: duration.asSeconds(),
};
})
);
})();

View File

@ -33,9 +33,6 @@ const {
ContactDetail,
} = require('../../ts/components/conversation/ContactDetail');
const { ContactListItem } = require('../../ts/components/ContactListItem');
const {
ConversationHeader,
} = require('../../ts/components/conversation/ConversationHeader');
const { Emojify } = require('../../ts/components/conversation/Emojify');
const { ErrorModal } = require('../../ts/components/ErrorModal');
const { Lightbox } = require('../../ts/components/Lightbox');
@ -63,6 +60,9 @@ const { createTimeline } = require('../../ts/state/roots/createTimeline');
const {
createCompositionArea,
} = require('../../ts/state/roots/createCompositionArea');
const {
createConversationHeader,
} = require('../../ts/state/roots/createConversationHeader');
const { createCallManager } = require('../../ts/state/roots/createCallManager');
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
const {
@ -295,7 +295,6 @@ exports.setup = (options = {}) => {
ConfirmationModal,
ContactDetail,
ContactListItem,
ConversationHeader,
Emojify,
ErrorModal,
getCallingNotificationText,
@ -315,6 +314,7 @@ exports.setup = (options = {}) => {
const Roots = {
createCallManager,
createCompositionArea,
createConversationHeader,
createLeftPane,
createSafetyNumberViewer,
createShortcutGuideModal,

View File

@ -124,7 +124,7 @@ type WhatIsThis = typeof window.WhatIsThis;
};
// Keyboard/mouse mode
let interactionMode = 'mouse';
let interactionMode: 'mouse' | 'keyboard' = 'mouse';
$(document.body).addClass('mouse-mode');
window.enterKeyboardMode = () => {
@ -664,8 +664,8 @@ type WhatIsThis = typeof window.WhatIsThis;
conversationLookup: window.Signal.Util.makeLookup(conversations, 'id'),
messagesByConversation: {},
messagesLookup: {},
selectedConversation: null,
selectedMessage: null,
selectedConversation: undefined,
selectedMessage: undefined,
selectedMessageCounter: 0,
showArchived: false,
},
@ -822,7 +822,7 @@ type WhatIsThis = typeof window.WhatIsThis;
const conversationsToSearch = getConversationsToSearch();
const increment = direction === 'up' ? -1 : 1;
let startIndex;
let startIndex: WhatIsThis;
if (conversationId) {
const index = conversationsToSearch.findIndex(
@ -844,7 +844,7 @@ type WhatIsThis = typeof window.WhatIsThis;
if (!unreadOnly) {
return target.id;
}
if (target.unreadCount > 0) {
if ((target.unreadCount || 0) > 0) {
return target.id;
}
}

View File

@ -1,16 +1,11 @@
import * as React from 'react';
import React, { ComponentProps } from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { setup as setupI18n } from '../../../js/modules/i18n';
import enMessages from '../../../_locales/en/messages.json';
import {
ConversationHeader,
PropsActionsType,
PropsHousekeepingType,
PropsType,
} from './ConversationHeader';
import { ConversationHeader } from './ConversationHeader';
import { gifUrl } from '../../storybook/Fixtures';
const book = storiesOf('Components/Conversation/ConversationHeader', module);
@ -21,11 +16,17 @@ type ConversationHeaderStory = {
description: string;
items: Array<{
title: string;
props: PropsType;
props: ComponentProps<typeof ConversationHeader>;
}>;
};
const actionProps: PropsActionsType = {
const commonProps = {
showBackButton: false,
showCallButtons: true,
markedUnread: false,
i18n,
onSetDisappearingMessages: action('onSetDisappearingMessages'),
onDeleteMessages: action('onDeleteMessages'),
onResetSession: action('onResetSession'),
@ -49,10 +50,6 @@ const actionProps: PropsActionsType = {
onSetPin: action('onSetPin'),
};
const housekeepingProps: PropsHousekeepingType = {
i18n,
};
const stories: Array<ConversationHeaderStory> = [
{
title: '1:1 conversation',
@ -62,6 +59,7 @@ const stories: Array<ConversationHeaderStory> = [
{
title: 'With name and profile, verified',
props: {
...commonProps,
color: 'red',
isVerified: true,
avatarPath: gifUrl,
@ -72,13 +70,12 @@ const stories: Array<ConversationHeaderStory> = [
id: '1',
profileName: '🔥Flames🔥',
acceptedMessageRequest: true,
...actionProps,
...housekeepingProps,
},
},
{
title: 'With name, not verified, no avatar',
props: {
...commonProps,
color: 'blue',
isVerified: false,
title: 'Someone 🔥 Somewhere',
@ -87,13 +84,12 @@ const stories: Array<ConversationHeaderStory> = [
type: 'direct',
id: '2',
acceptedMessageRequest: true,
...actionProps,
...housekeepingProps,
},
},
{
title: 'With name, not verified, descenders',
props: {
...commonProps,
color: 'blue',
isVerified: false,
title: 'Joyrey 🔥 Leppey',
@ -102,13 +98,12 @@ const stories: Array<ConversationHeaderStory> = [
type: 'direct',
id: '2',
acceptedMessageRequest: true,
...actionProps,
...housekeepingProps,
},
},
{
title: 'Profile, no name',
props: {
...commonProps,
color: 'teal',
isVerified: false,
phoneNumber: '(202) 555-0003',
@ -117,25 +112,23 @@ const stories: Array<ConversationHeaderStory> = [
title: '🔥Flames🔥',
profileName: '🔥Flames🔥',
acceptedMessageRequest: true,
...actionProps,
...housekeepingProps,
},
},
{
title: 'No name, no profile, no color',
props: {
...commonProps,
title: '(202) 555-0011',
phoneNumber: '(202) 555-0011',
type: 'direct',
id: '11',
acceptedMessageRequest: true,
...actionProps,
...housekeepingProps,
},
},
{
title: 'With back button',
props: {
...commonProps,
showBackButton: true,
color: 'deep_orange',
phoneNumber: '(202) 555-0004',
@ -143,46 +136,32 @@ const stories: Array<ConversationHeaderStory> = [
type: 'direct',
id: '4',
acceptedMessageRequest: true,
...actionProps,
...housekeepingProps,
},
},
{
title: 'Disappearing messages set',
props: {
...commonProps,
color: 'indigo',
title: '(202) 555-0005',
phoneNumber: '(202) 555-0005',
type: 'direct',
id: '5',
expirationSettingName: '10 seconds',
timerOptions: [
{
name: 'off',
value: 0,
},
{
name: '10 seconds',
value: 10,
},
],
expireTimer: 10,
acceptedMessageRequest: true,
...actionProps,
...housekeepingProps,
},
},
{
title: 'Muting Conversation',
props: {
...commonProps,
color: 'ultramarine',
title: '(202) 555-0006',
phoneNumber: '(202) 555-0006',
type: 'direct',
id: '6',
muteExpirationLabel: '10/18/3000, 11:11 AM',
acceptedMessageRequest: true,
...actionProps,
...housekeepingProps,
muteExpiresAt: new Date('3000-10-18T11:11:11Z').valueOf(),
},
},
],
@ -195,52 +174,30 @@ const stories: Array<ConversationHeaderStory> = [
{
title: 'Basic',
props: {
...commonProps,
color: 'signal-blue',
title: 'Typescript support group',
name: 'Typescript support group',
phoneNumber: '',
id: '1',
type: 'group',
expirationSettingName: '10 seconds',
timerOptions: [
{
name: 'off',
value: 0,
},
{
name: '10 seconds',
value: 10,
},
],
expireTimer: 10,
acceptedMessageRequest: true,
...actionProps,
...housekeepingProps,
},
},
{
title: 'In a group you left - no disappearing messages',
props: {
...commonProps,
color: 'signal-blue',
title: 'Typescript support group',
name: 'Typescript support group',
phoneNumber: '',
id: '2',
type: 'group',
disableTimerChanges: true,
expirationSettingName: '10 seconds',
timerOptions: [
{
name: 'off',
value: 0,
},
{
name: '10 seconds',
value: 10,
},
],
left: true,
expireTimer: 10,
acceptedMessageRequest: true,
...actionProps,
...housekeepingProps,
},
},
],
@ -252,6 +209,7 @@ const stories: Array<ConversationHeaderStory> = [
{
title: 'In chat with yourself',
props: {
...commonProps,
color: 'blue',
title: '(202) 555-0007',
phoneNumber: '(202) 555-0007',
@ -259,8 +217,6 @@ const stories: Array<ConversationHeaderStory> = [
type: 'direct',
isMe: true,
acceptedMessageRequest: true,
...actionProps,
...housekeepingProps,
},
},
],
@ -272,6 +228,7 @@ const stories: Array<ConversationHeaderStory> = [
{
title: '1:1 conversation',
props: {
...commonProps,
color: 'blue',
title: '(202) 555-0007',
phoneNumber: '(202) 555-0007',
@ -279,8 +236,6 @@ const stories: Array<ConversationHeaderStory> = [
type: 'direct',
isMe: false,
acceptedMessageRequest: false,
...actionProps,
...housekeepingProps,
},
},
],

View File

@ -1,4 +1,5 @@
import React from 'react';
import moment from 'moment';
import classNames from 'classnames';
import {
ContextMenu,
@ -14,11 +15,11 @@ import { InContactsIcon } from '../InContactsIcon';
import { LocalizerType } from '../../types/Util';
import { ColorType } from '../../types/Colors';
import { getMuteOptions } from '../../util/getMuteOptions';
interface TimerOption {
name: string;
value: number;
}
import {
ExpirationTimerOptions,
TimerOption,
} from '../../util/ExpirationTimerOptions';
import { isMuted } from '../../util/isMuted';
export interface PropsDataType {
id: string;
@ -36,13 +37,16 @@ export interface PropsDataType {
isMe?: boolean;
isArchived?: boolean;
isPinned?: boolean;
isMissingMandatoryProfileSharing?: boolean;
left?: boolean;
markedUnread?: boolean;
disableTimerChanges?: boolean;
expirationSettingName?: string;
muteExpirationLabel?: string;
canChangeTimer?: boolean;
expireTimer?: number;
muteExpiresAt?: number;
showBackButton?: boolean;
timerOptions?: Array<TimerOption>;
showCallButtons?: boolean;
}
export interface PropsActionsType {
@ -186,8 +190,11 @@ export class ConversationHeader extends React.Component<PropsType> {
}
public renderExpirationLength(): JSX.Element | null {
const { expirationSettingName, showBackButton } = this.props;
const { i18n, expireTimer, showBackButton } = this.props;
const expirationSettingName = expireTimer
? ExpirationTimerOptions.getName(i18n, expireTimer)
: undefined;
if (!expirationSettingName) {
return null;
}
@ -249,71 +256,62 @@ export class ConversationHeader extends React.Component<PropsType> {
);
}
public renderOutgoingAudioCallButton(): JSX.Element | null {
private renderOutgoingCallButtons(): JSX.Element | null {
const {
i18n,
isMe,
onOutgoingAudioCallInConversation,
onOutgoingVideoCallInConversation,
showCallButtons,
showBackButton,
type,
} = this.props;
if (type === 'group' || isMe) {
if (!showCallButtons) {
return null;
}
return (
<button
type="button"
onClick={onOutgoingAudioCallInConversation}
className={classNames(
'module-conversation-header__audio-calling-button',
showBackButton
? null
: 'module-conversation-header__audio-calling-button--show'
)}
disabled={showBackButton}
aria-label={i18n('makeOutgoingCall')}
/>
);
}
public renderOutgoingVideoCallButton(): JSX.Element | null {
const { i18n, isMe, type } = this.props;
if (type === 'group' || isMe) {
return null;
}
const { onOutgoingVideoCallInConversation, showBackButton } = this.props;
return (
<button
type="button"
onClick={onOutgoingVideoCallInConversation}
className={classNames(
'module-conversation-header__video-calling-button',
showBackButton
? null
: 'module-conversation-header__video-calling-button--show'
)}
disabled={showBackButton}
aria-label={i18n('makeOutgoingVideoCall')}
/>
<>
<button
type="button"
onClick={onOutgoingVideoCallInConversation}
className={classNames(
'module-conversation-header__video-calling-button',
showBackButton
? null
: 'module-conversation-header__video-calling-button--show'
)}
disabled={showBackButton}
aria-label={i18n('makeOutgoingVideoCall')}
/>
<button
type="button"
onClick={onOutgoingAudioCallInConversation}
className={classNames(
'module-conversation-header__audio-calling-button',
showBackButton
? null
: 'module-conversation-header__audio-calling-button--show'
)}
disabled={showBackButton}
aria-label={i18n('makeOutgoingCall')}
/>
</>
);
}
public renderMenu(triggerId: string): JSX.Element {
const {
disableTimerChanges,
i18n,
acceptedMessageRequest,
canChangeTimer,
isArchived,
isMe,
isPinned,
type,
isArchived,
markedUnread,
muteExpirationLabel,
muteExpiresAt,
isMissingMandatoryProfileSharing,
left,
onDeleteMessages,
onResetSession,
onSetDisappearingMessages,
@ -325,11 +323,15 @@ export class ConversationHeader extends React.Component<PropsType> {
onMarkUnread,
onSetPin,
onMoveToInbox,
timerOptions,
} = this.props;
const muteOptions = [];
if (muteExpirationLabel) {
if (isMuted(muteExpiresAt)) {
const expires = moment(muteExpiresAt);
const muteExpirationLabel = moment().isSame(expires, 'day')
? expires.format('hh:mm A')
: expires.format('M/D/YY, hh:mm A');
muteOptions.push(
...[
{
@ -352,18 +354,25 @@ export class ConversationHeader extends React.Component<PropsType> {
const muteTitle = i18n('muteNotificationsTitle') as any;
const isGroup = type === 'group';
const disableTimerChanges = Boolean(
!canChangeTimer ||
!acceptedMessageRequest ||
left ||
isMissingMandatoryProfileSharing
);
return (
<ContextMenu id={triggerId}>
{disableTimerChanges ? null : (
<SubMenu title={disappearingTitle}>
{(timerOptions || []).map(item => (
{ExpirationTimerOptions.map((item: typeof TimerOption) => (
<MenuItem
key={item.value}
key={item.get('seconds')}
onClick={() => {
onSetDisappearingMessages(item.value);
onSetDisappearingMessages(item.get('seconds'));
}}
>
{item.name}
{item.getName(i18n)}
</MenuItem>
))}
</SubMenu>
@ -435,8 +444,7 @@ export class ConversationHeader extends React.Component<PropsType> {
</div>
{this.renderExpirationLength()}
{this.renderSearchButton()}
{this.renderOutgoingVideoCallButton()}
{this.renderOutgoingAudioCallButton()}
{this.renderOutgoingCallButtons()}
{this.renderMoreButton(triggerId)}
{this.renderMenu(triggerId)}
</div>

View File

@ -15,6 +15,7 @@ import {
} from '../state/ducks/conversations';
import { ColorType } from '../types/Colors';
import { MessageModel } from './messages';
import { isMuted } from '../util/isMuted';
import { sniffImageMimeType } from '../util/sniffImageMimeType';
import { MIMEType, IMAGE_WEBP } from '../types/MIME';
import {
@ -1118,6 +1119,7 @@ export class ConversationModel extends window.Backbone.Model<
areWePending: Boolean(
ourConversationId && this.isMemberPending(ourConversationId)
),
canChangeTimer: this.canChangeTimer(),
avatarPath: this.getAvatarPath()!,
color,
draftPreview,
@ -1137,11 +1139,13 @@ export class ConversationModel extends window.Backbone.Model<
deletedForEveryone: this.get('lastMessageDeletedForEveryone')!,
},
lastUpdated: this.get('timestamp')!,
left: Boolean(this.get('left')),
markedUnread: this.get('markedUnread')!,
membersCount: this.isPrivate()
? undefined
: (this.get('membersV2')! || this.get('members')! || []).length,
messageRequestsEnabled,
expireTimer: this.get('expireTimer'),
muteExpiresAt: this.get('muteExpiresAt')!,
name: this.get('name')!,
phoneNumber: this.getNumber()!,
@ -3957,7 +3961,7 @@ export class ConversationModel extends window.Backbone.Model<
return getAbsoluteAttachmentPath(avatar.path);
}
canChangeTimer(): boolean {
private canChangeTimer(): boolean {
if (this.isPrivate()) {
return true;
}
@ -4027,10 +4031,7 @@ export class ConversationModel extends window.Backbone.Model<
}
isMuted(): boolean {
return (
Boolean(this.get('muteExpiresAt')) &&
Date.now() < this.get('muteExpiresAt')
);
return isMuted(this.get('muteExpiresAt'));
}
getMuteTimeoutId(): string {

View File

@ -10,6 +10,7 @@ import {
} from '../state/ducks/conversations';
import { PropsData } from '../components/conversation/Message';
import { CallbackResultType } from '../textsecure/SendMessage';
import { ExpirationTimerOptions } from '../util/ExpirationTimerOptions';
import { BodyRangesType } from '../types/Util';
import { PropsDataType as GroupsV2Props } from '../components/conversation/GroupV2Change';
import {
@ -504,7 +505,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
const { expireTimer, fromSync, source, sourceUuid } = timerUpdate;
const timespan = window.Whisper.ExpirationTimerOptions.getName(
const timespan = ExpirationTimerOptions.getName(
window.i18n,
expireTimer || 0
);
const disabled = !expireTimer;
@ -1300,9 +1302,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
return {
text: window.i18n('timerSetTo', [
window.Whisper.ExpirationTimerOptions.getAbbreviated(
expireTimer || 0
),
ExpirationTimerOptions.getAbbreviated(window.i18n, expireTimer || 0),
]),
};
}

View File

@ -96,6 +96,21 @@ export type SetRendererCanvasType = {
element: React.RefObject<HTMLCanvasElement> | undefined;
};
// Helpers
export function isCallActive({
callDetails,
callState,
}: CallingStateType): boolean {
return Boolean(
callDetails &&
((!callDetails.isIncoming &&
(callState === CallState.Prering || callState === CallState.Ringing)) ||
callState === CallState.Accepted ||
callState === CallState.Reconnecting)
);
}
// Actions
const ACCEPT_CALL = 'calling/ACCEPT_CALL';
@ -522,7 +537,7 @@ export type ActionsType = typeof actions;
// Reducer
function getEmptyState(): CallingStateType {
export function getEmptyState(): CallingStateType {
return {
availableCameras: [],
availableMicrophones: [],

View File

@ -43,7 +43,9 @@ export type ConversationType = {
profileName?: string;
avatarPath?: string;
areWePending?: boolean;
canChangeTimer?: boolean;
color?: ColorType;
isAccepted?: boolean;
isArchived?: boolean;
isBlocked?: boolean;
isPinned?: boolean;
@ -51,6 +53,7 @@ export type ConversationType = {
activeAt?: number;
timestamp?: number;
inboxPosition?: number;
left?: boolean;
lastMessage?: {
status: LastMessageStatus;
text: string;
@ -59,6 +62,7 @@ export type ConversationType = {
markedUnread: boolean;
phoneNumber?: string;
membersCount?: number;
expireTimer?: number;
muteExpiresAt?: number;
type: ConversationTypeType;
isMe?: boolean;
@ -168,6 +172,7 @@ export type ConversationsStateType = {
selectedConversation?: string;
selectedMessage?: string;
selectedMessageCounter: number;
selectedConversationPanelDepth: number;
showArchived: boolean;
// Note: it's very important that both of these locations are always kept up to date
@ -271,6 +276,10 @@ export type SetIsNearBottomActionType = {
isNearBottom: boolean;
};
};
export type SetSelectedConversationPanelDepthActionType = {
type: 'SET_SELECTED_CONVERSATION_PANEL_DEPTH';
payload: { panelDepth: number };
};
export type ScrollToMessageActionType = {
type: 'SCROLL_TO_MESSAGE';
payload: {
@ -328,6 +337,7 @@ export type ConversationActionType =
| ClearSelectedMessageActionType
| ClearUnreadMetricsActionType
| ScrollToMessageActionType
| SetSelectedConversationPanelDepthActionType
| SelectedConversationChangedActionType
| MessageDeletedActionType
| SelectedConversationChangedActionType
@ -350,6 +360,7 @@ export const actions = {
setMessagesLoading,
setLoadCountdownStart,
setIsNearBottom,
setSelectedConversationPanelDepth,
clearChangedMessages,
clearSelectedMessage,
clearUnreadMetrics,
@ -516,6 +527,14 @@ function setIsNearBottom(
},
};
}
function setSelectedConversationPanelDepth(
panelDepth: number
): SetSelectedConversationPanelDepthActionType {
return {
type: 'SET_SELECTED_CONVERSATION_PANEL_DEPTH',
payload: { panelDepth },
};
}
function clearChangedMessages(
conversationId: string
): ClearChangedMessagesActionType {
@ -606,6 +625,7 @@ function getEmptyState(): ConversationsStateType {
messagesLookup: {},
selectedMessageCounter: 0,
showArchived: false,
selectedConversationPanelDepth: 0,
};
}
@ -761,6 +781,7 @@ export function reducer(
return {
...state,
selectedConversation,
selectedConversationPanelDepth: 0,
messagesLookup: omit(state.messagesLookup, messageIds),
messagesByConversation: omit(state.messagesByConversation, [id]),
};
@ -768,6 +789,12 @@ export function reducer(
if (action.type === 'CONVERSATIONS_REMOVE_ALL') {
return getEmptyState();
}
if (action.type === 'SET_SELECTED_CONVERSATION_PANEL_DEPTH') {
return {
...state,
selectedConversationPanelDepth: action.payload.panelDepth,
};
}
if (action.type === 'MESSAGE_SELECTED') {
const { messageId, conversationId } = action.payload;

View File

@ -0,0 +1,14 @@
import React from 'react';
import { Store } from 'redux';
import { Provider } from 'react-redux';
import { SmartConversationHeader, OwnProps } from '../smart/ConversationHeader';
export const createConversationHeader = (
store: Store,
props: OwnProps
): React.ReactElement => (
<Provider store={store}>
<SmartConversationHeader {...props} />
</Provider>
);

View File

@ -0,0 +1,68 @@
import { connect } from 'react-redux';
import { pick } from 'lodash';
import { ConversationHeader } from '../../components/conversation/ConversationHeader';
import { getConversationSelector } from '../selectors/conversations';
import { StateType } from '../reducer';
import { isCallActive } from '../ducks/calling';
import { getIntl } from '../selectors/user';
export interface OwnProps {
id: string;
onDeleteMessages: () => void;
onGoBack: () => void;
onOutgoingAudioCallInConversation: () => void;
onOutgoingVideoCallInConversation: () => void;
onResetSession: () => void;
onSearchInConversation: () => void;
onSetDisappearingMessages: (seconds: number) => void;
onSetMuteNotifications: (seconds: number) => void;
onSetPin: (value: boolean) => void;
onShowAllMedia: () => void;
onShowGroupMembers: () => void;
onArchive: () => void;
onMarkUnread: () => void;
onMoveToInbox: () => void;
onShowSafetyNumber: () => void;
}
const mapStateToProps = (state: StateType, ownProps: OwnProps) => {
const conversation = getConversationSelector(state)(ownProps.id);
if (!conversation) {
throw new Error('Could not find conversation');
}
return {
...pick(conversation, [
'acceptedMessageRequest',
'avatarPath',
'canChangeTimer',
'color',
'expireTimer',
'isArchived',
'isMe',
'isMissingMandatoryProfileSharing',
'isPinned',
'isVerified',
'left',
'markedUnread',
'muteExpiresAt',
'name',
'phoneNumber',
'profileName',
'title',
'type',
]),
i18n: getIntl(state),
showBackButton: state.conversations.selectedConversationPanelDepth > 0,
showCallButtons:
conversation.type === 'direct' &&
!conversation.isMe &&
!isCallActive(state.calling),
};
};
const smart = connect(mapStateToProps, {});
export const SmartConversationHeader = smart(ConversationHeader);

View File

@ -0,0 +1,107 @@
import { assert } from 'chai';
import {
CallDetailsType,
getEmptyState,
isCallActive,
} from '../../../state/ducks/calling';
import { CallState } from '../../../types/Calling';
describe('calling duck', () => {
describe('helpers', () => {
describe('isCallActive', () => {
const fakeCallDetails: CallDetailsType = {
id: 'fake-call',
title: 'Fake Call',
callId: 123,
isIncoming: false,
isVideoCall: false,
};
it('returns false if there are no call details', () => {
assert.isFalse(isCallActive(getEmptyState()));
});
it('returns false if an incoming call is in a pre-reing state', () => {
assert.isFalse(
isCallActive({
...getEmptyState(),
callDetails: {
...fakeCallDetails,
isIncoming: true,
},
callState: CallState.Prering,
})
);
});
it('returns true if an outgoing call is in a pre-reing state', () => {
assert.isTrue(
isCallActive({
...getEmptyState(),
callDetails: {
...fakeCallDetails,
isIncoming: false,
},
callState: CallState.Prering,
})
);
});
it('returns false if an incoming call is ringing', () => {
assert.isFalse(
isCallActive({
...getEmptyState(),
callDetails: {
...fakeCallDetails,
isIncoming: true,
},
callState: CallState.Ringing,
})
);
});
it('returns true if an outgoing call is ringing', () => {
assert.isTrue(
isCallActive({
...getEmptyState(),
callDetails: {
...fakeCallDetails,
isIncoming: false,
},
callState: CallState.Ringing,
})
);
});
it('returns true if a call is in an accepted state', () => {
assert.isTrue(
isCallActive({
...getEmptyState(),
callDetails: fakeCallDetails,
callState: CallState.Accepted,
})
);
});
it('returns true if a call is in a reconnecting state', () => {
assert.isTrue(
isCallActive({
...getEmptyState(),
callDetails: fakeCallDetails,
callState: CallState.Reconnecting,
})
);
});
it('returns false if a call is in an ended state', () => {
assert.isFalse(
isCallActive({
...getEmptyState(),
callDetails: fakeCallDetails,
callState: CallState.Ended,
})
);
});
});
});
});

View File

@ -0,0 +1,19 @@
import { assert } from 'chai';
import { isMuted } from '../../util/isMuted';
describe('isMuted', () => {
it('returns false if passed undefined', () => {
assert.isFalse(isMuted(undefined));
});
it('returns false if passed a date in the past', () => {
assert.isFalse(isMuted(0));
assert.isFalse(isMuted(Date.now() - 123));
});
it('returns false if passed a date in the future', () => {
assert.isTrue(isMuted(Date.now() + 123));
assert.isTrue(isMuted(Date.now() + 123456));
});
});

View File

@ -0,0 +1,74 @@
import * as Backbone from 'backbone';
import * as moment from 'moment';
import { LocalizerType } from '../types/Util';
type ExpirationTime = [
number,
(
| 'second'
| 'seconds'
| 'minute'
| 'minutes'
| 'hour'
| 'hours'
| 'day'
| 'week'
)
];
const EXPIRATION_TIMES: Array<ExpirationTime> = [
[0, 'seconds'],
[5, 'seconds'],
[10, 'seconds'],
[30, 'seconds'],
[1, 'minute'],
[5, 'minutes'],
[30, 'minutes'],
[1, 'hour'],
[6, 'hours'],
[12, 'hours'],
[1, 'day'],
[1, 'week'],
];
export const TimerOption = Backbone.Model.extend({
getName(i18n: LocalizerType) {
return (
i18n(['timerOption', this.get('time'), this.get('unit')].join('_')) ||
moment.duration(this.get('time'), this.get('unit')).humanize()
);
},
getAbbreviated(i18n: LocalizerType) {
return i18n(
['timerOption', this.get('time'), this.get('unit'), 'abbreviated'].join(
'_'
)
);
},
});
export const ExpirationTimerOptions = new (Backbone.Collection.extend({
model: TimerOption,
getName(i18n: LocalizerType, seconds = 0) {
const o = this.findWhere({ seconds });
if (o) {
return o.getName(i18n);
}
return [seconds, 'seconds'].join(' ');
},
getAbbreviated(i18n: LocalizerType, seconds = 0) {
const o = this.findWhere({ seconds });
if (o) {
return o.getAbbreviated(i18n);
}
return [seconds, 's'].join('');
},
}))(
EXPIRATION_TIMES.map(o => {
const duration = moment.duration(o[0], o[1]); // 5, 'seconds'
return {
time: o[0],
unit: o[1],
seconds: duration.asSeconds(),
};
})
);

3
ts/util/isMuted.ts Normal file
View File

@ -0,0 +1,3 @@
export function isMuted(muteExpiresAt: undefined | number): boolean {
return Boolean(muteExpiresAt && Date.now() < muteExpiresAt);
}

View File

@ -14694,7 +14694,7 @@
"rule": "React-createRef",
"path": "ts/components/conversation/ConversationHeader.js",
"line": " this.menuTriggerRef = react_1.default.createRef();",
"lineNumber": 16,
"lineNumber": 19,
"reasonCategory": "usageTrusted",
"updated": "2020-08-28T16:12:19.904Z",
"reasonDetail": "Used to reference popup menu"
@ -14703,7 +14703,7 @@
"rule": "React-createRef",
"path": "ts/components/conversation/ConversationHeader.tsx",
"line": " this.menuTriggerRef = React.createRef();",
"lineNumber": 86,
"lineNumber": 90,
"reasonCategory": "usageTrusted",
"updated": "2020-05-20T20:10:43.540Z",
"reasonDetail": "Used to reference popup menu"
@ -15116,4 +15116,4 @@
"reasonCategory": "falseMatch",
"updated": "2020-09-08T23:07:22.682Z"
}
]
]

View File

@ -435,146 +435,120 @@ Whisper.ConversationView = Whisper.View.extend({
},
setupHeader() {
const getHeaderProps = (_unknown?: unknown) => {
const expireTimer = this.model.get('expireTimer');
const expirationSettingName = expireTimer
? Whisper.ExpirationTimerOptions.getName(expireTimer || 0)
: null;
return {
...this.model.format(),
leftGroup: this.model.get('left'),
disableTimerChanges:
this.model.isMissingRequiredProfileSharing() ||
this.model.get('left') ||
!this.model.getAccepted() ||
!this.model.canChangeTimer(),
showBackButton: Boolean(this.panels && this.panels.length),
expirationSettingName,
timerOptions: Whisper.ExpirationTimerOptions.map((item: any) => ({
name: item.getName(),
value: item.get('seconds'),
})),
muteExpirationLabel: this.getMuteExpirationLabel(),
onSetDisappearingMessages: (seconds: number) =>
this.setDisappearingMessages(seconds),
onDeleteMessages: () => this.destroyMessages(),
onResetSession: () => this.endSession(),
onSearchInConversation: () => {
const { searchInConversation } = window.reduxActions.search;
const name = this.model.isMe()
? window.i18n('noteToSelf')
: this.model.getTitle();
searchInConversation(this.model.id, name);
},
onSetMuteNotifications: (ms: number) => this.setMuteNotifications(ms),
onSetPin: this.setPin.bind(this),
// These are view only and don't update the Conversation model, so they
// need a manual update call.
onOutgoingAudioCallInConversation: async () => {
window.log.info(
'onOutgoingAudioCallInConversation: about to start an audio call'
);
const conversation = this.model;
const isVideoCall = false;
if (await this.isCallSafe()) {
window.log.info(
'onOutgoingAudioCallInConversation: call is deemed "safe". Making call'
);
await window.Signal.Services.calling.startCallingLobby(
conversation,
isVideoCall
);
window.log.info(
'onOutgoingAudioCallInConversation: started the call'
);
} else {
window.log.info(
'onOutgoingAudioCallInConversation: call is deemed "unsafe". Stopping'
);
}
},
onOutgoingVideoCallInConversation: async () => {
window.log.info(
'onOutgoingVideoCallInConversation: about to start a video call'
);
const conversation = this.model;
const isVideoCall = true;
if (await this.isCallSafe()) {
window.log.info(
'onOutgoingVideoCallInConversation: call is deemed "safe". Making call'
);
await window.Signal.Services.calling.startCallingLobby(
conversation,
isVideoCall
);
window.log.info(
'onOutgoingVideoCallInConversation: started the call'
);
} else {
window.log.info(
'onOutgoingVideoCallInConversation: call is deemed "unsafe". Stopping'
);
}
},
onShowSafetyNumber: () => {
this.showSafetyNumber();
},
onShowAllMedia: () => {
this.showAllMedia();
},
onShowGroupMembers: async () => {
await this.showMembers();
this.updateHeader();
},
onGoBack: () => {
this.resetPanel();
},
onArchive: () => {
this.model.setArchived(true);
this.model.trigger('unload', 'archive');
Whisper.ToastView.show(
Whisper.ConversationArchivedToast,
document.body
);
},
onMarkUnread: () => {
this.model.setMarkedUnread(true);
Whisper.ToastView.show(
Whisper.ConversationMarkedUnreadToast,
document.body
);
},
onMoveToInbox: () => {
this.model.setArchived(false);
Whisper.ToastView.show(
Whisper.ConversationUnarchivedToast,
document.body
);
},
};
};
this.titleView = new Whisper.ReactWrapperView({
className: 'title-wrapper',
Component: window.Signal.Components.ConversationHeader,
props: getHeaderProps(this.model),
JSX: window.Signal.State.Roots.createConversationHeader(
window.reduxStore,
{
id: this.model.id,
onSetDisappearingMessages: (seconds: number) =>
this.setDisappearingMessages(seconds),
onDeleteMessages: () => this.destroyMessages(),
onResetSession: () => this.endSession(),
onSearchInConversation: () => {
const { searchInConversation } = window.reduxActions.search;
const name = this.model.isMe()
? window.i18n('noteToSelf')
: this.model.getTitle();
searchInConversation(this.model.id, name);
},
onSetMuteNotifications: (ms: number) => this.setMuteNotifications(ms),
onSetPin: this.setPin.bind(this),
// These are view only and don't update the Conversation model, so they
// need a manual update call.
onOutgoingAudioCallInConversation: async () => {
window.log.info(
'onOutgoingAudioCallInConversation: about to start an audio call'
);
const conversation = this.model;
const isVideoCall = false;
if (await this.isCallSafe()) {
window.log.info(
'onOutgoingAudioCallInConversation: call is deemed "safe". Making call'
);
await window.Signal.Services.calling.startCallingLobby(
conversation,
isVideoCall
);
window.log.info(
'onOutgoingAudioCallInConversation: started the call'
);
} else {
window.log.info(
'onOutgoingAudioCallInConversation: call is deemed "unsafe". Stopping'
);
}
},
onOutgoingVideoCallInConversation: async () => {
window.log.info(
'onOutgoingVideoCallInConversation: about to start a video call'
);
const conversation = this.model;
const isVideoCall = true;
if (await this.isCallSafe()) {
window.log.info(
'onOutgoingVideoCallInConversation: call is deemed "safe". Making call'
);
await window.Signal.Services.calling.startCallingLobby(
conversation,
isVideoCall
);
window.log.info(
'onOutgoingVideoCallInConversation: started the call'
);
} else {
window.log.info(
'onOutgoingVideoCallInConversation: call is deemed "unsafe". Stopping'
);
}
},
onShowSafetyNumber: () => {
this.showSafetyNumber();
},
onShowAllMedia: () => {
this.showAllMedia();
},
onShowGroupMembers: async () => {
await this.showMembers();
},
onGoBack: () => {
this.resetPanel();
},
onArchive: () => {
this.model.setArchived(true);
this.model.trigger('unload', 'archive');
Whisper.ToastView.show(
Whisper.ConversationArchivedToast,
document.body
);
},
onMarkUnread: () => {
this.model.setMarkedUnread(true);
Whisper.ToastView.show(
Whisper.ConversationMarkedUnreadToast,
document.body
);
},
onMoveToInbox: () => {
this.model.setArchived(false);
Whisper.ToastView.show(
Whisper.ConversationUnarchivedToast,
document.body
);
},
}
),
});
this.updateHeader = () => this.titleView.update(getHeaderProps());
this.listenTo(this.model, 'change', this.updateHeader);
this.$('.conversation-header').append(this.titleView.el);
},
@ -1268,6 +1242,7 @@ Whisper.ConversationView = Whisper.View.extend({
const panel = this.panels[i];
panel.remove();
}
window.reduxActions.conversations.setSelectedConversationPanelDepth(0);
}
this.remove();
@ -2209,7 +2184,6 @@ Whisper.ConversationView = Whisper.View.extend({
this.listenTo(this.model.messageCollection, 'remove', update);
this.listenBack(view);
this.updateHeader();
},
focusMessageField() {
@ -2319,7 +2293,6 @@ Whisper.ConversationView = Whisper.View.extend({
model: conversation,
});
this.listenBack(view);
this.updateHeader();
}
},
@ -2638,7 +2611,6 @@ Whisper.ConversationView = Whisper.View.extend({
// We could listen to all involved contacts, but we'll call that overkill
this.listenBack(view);
this.updateHeader();
view.render();
},
@ -2652,7 +2624,6 @@ Whisper.ConversationView = Whisper.View.extend({
});
this.listenBack(view);
this.updateHeader();
view.render();
},
@ -2675,7 +2646,6 @@ Whisper.ConversationView = Whisper.View.extend({
});
this.listenBack(view);
this.updateHeader();
},
async openConversation(number: any) {
@ -2694,6 +2664,10 @@ Whisper.ConversationView = Whisper.View.extend({
view.$el.one('animationend', () => {
view.$el.addClass('panel--static');
});
window.reduxActions.conversations.setSelectedConversationPanelDepth(
this.panels.length
);
},
resetPanel() {
if (!this.panels || !this.panels.length) {
@ -2714,7 +2688,6 @@ Whisper.ConversationView = Whisper.View.extend({
if (this.panels.length > 0) {
this.panels[0].$el.fadeIn(250);
}
this.updateHeader();
view.$el.addClass('panel--remove').one('transitionend', () => {
view.remove();
@ -2724,6 +2697,10 @@ Whisper.ConversationView = Whisper.View.extend({
window.dispatchEvent(new Event('resize'));
}
});
window.reduxActions.conversations.setSelectedConversationPanelDepth(
this.panels.length
);
},
endSession() {

64
ts/window.d.ts vendored
View File

@ -3,6 +3,7 @@
import * as Backbone from 'backbone';
import * as Underscore from 'underscore';
import { Ref } from 'react';
import { bindActionCreators } from 'redux';
import * as LinkPreviews from '../js/modules/link_previews.d';
import * as Util from './util';
import {
@ -27,6 +28,28 @@ import { CallHistoryDetailsType } from './types/Calling';
import { ColorType } from './types/Colors';
import { ConversationController } from './ConversationController';
import { ReduxActions } from './state/types';
import { createStore } from './state/createStore';
import { createCallManager } from './state/roots/createCallManager';
import { createCompositionArea } from './state/roots/createCompositionArea';
import { createConversationHeader } from './state/roots/createConversationHeader';
import { createLeftPane } from './state/roots/createLeftPane';
import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
import { createStickerManager } from './state/roots/createStickerManager';
import { createStickerPreviewModal } from './state/roots/createStickerPreviewModal';
import { createTimeline } from './state/roots/createTimeline';
import * as callingDuck from './state/ducks/calling';
import * as conversationsDuck from './state/ducks/conversations';
import * as emojisDuck from './state/ducks/emojis';
import * as expirationDuck from './state/ducks/expiration';
import * as itemsDuck from './state/ducks/items';
import * as networkDuck from './state/ducks/network';
import * as updatesDuck from './state/ducks/updates';
import * as userDuck from './state/ducks/user';
import * as searchDuck from './state/ducks/search';
import * as stickersDuck from './state/ducks/stickers';
import * as conversationsSelectors from './state/selectors/conversations';
import * as searchSelectors from './state/selectors/search';
import { SendOptionsType } from './textsecure/SendMessage';
import AccountManager from './textsecure/AccountManager';
import Data from './sql/Client';
@ -81,7 +104,7 @@ declare global {
getGuid: () => string;
getInboxCollection: () => ConversationModelCollectionType;
getIncomingCallNotification: () => Promise<boolean>;
getInteractionMode: () => string;
getInteractionMode: () => 'mouse' | 'keyboard';
getMediaCameraPermissions: () => Promise<boolean>;
getMediaPermissions: () => Promise<boolean>;
getServerPublicParams: () => string;
@ -367,7 +390,6 @@ declare global {
AttachmentList: any;
CaptionEditor: any;
ContactDetail: any;
ConversationHeader: any;
ErrorModal: typeof ErrorModal;
Lightbox: any;
LightboxGallery: any;
@ -394,7 +416,37 @@ declare global {
doesDatabaseExist: WhatIsThis;
};
Views: WhatIsThis;
State: WhatIsThis;
State: {
bindActionCreators: typeof bindActionCreators;
createStore: typeof createStore;
Roots: {
createCallManager: typeof createCallManager;
createCompositionArea: typeof createCompositionArea;
createConversationHeader: typeof createConversationHeader;
createLeftPane: typeof createLeftPane;
createSafetyNumberViewer: typeof createSafetyNumberViewer;
createShortcutGuideModal: typeof createShortcutGuideModal;
createStickerManager: typeof createStickerManager;
createStickerPreviewModal: typeof createStickerPreviewModal;
createTimeline: typeof createTimeline;
};
Ducks: {
calling: typeof callingDuck;
conversations: typeof conversationsDuck;
emojis: typeof emojisDuck;
expiration: typeof expirationDuck;
items: typeof itemsDuck;
network: typeof networkDuck;
updates: typeof updatesDuck;
user: typeof userDuck;
search: typeof searchDuck;
stickers: typeof stickersDuck;
};
Selectors: {
conversations: typeof conversationsSelectors;
search: typeof searchSelectors;
};
};
Logs: WhatIsThis;
conversationControllerStart: WhatIsThis;
Emojis: {
@ -555,12 +607,6 @@ export type WhisperType = {
KeyVerificationPanelView: any;
SafetyNumberChangeDialogView: any;
ExpirationTimerOptions: {
map: any;
getName: (number: number) => string;
getAbbreviated: (number: number) => string;
};
Notifications: {
removeBy: (filter: Partial<unknown>) => void;
add: (notification: unknown) => void;