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