Multiple person typing indicators in groups
This commit is contained in:
parent
17ea2b58de
commit
e4238de4db
15 changed files with 342 additions and 115 deletions
|
@ -6751,6 +6751,10 @@
|
|||
"messageformat": "Declined {type, select, Audio {voice} Video {video} Group {group} other {}} call",
|
||||
"description": "Call History > Short description of call > When call was declined"
|
||||
},
|
||||
"icu:TypingBubble__avatar--overflow-count": {
|
||||
"messageformat": "{count, plural, one {# other is} other {# others are}} typing.",
|
||||
"description": "Group chat multiple person typing indicator when space isn't available to show every avatar, this is the count of avatars hidden."
|
||||
},
|
||||
"icu:WhatsNew__modal-title": {
|
||||
"messageformat": "What's New",
|
||||
"description": "Title for the whats new modal"
|
||||
|
|
|
@ -1386,6 +1386,64 @@ $message-padding-horizontal: 12px;
|
|||
}
|
||||
}
|
||||
|
||||
.module-message__typing-avatar-container {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: center;
|
||||
margin-inline-end: 8px;
|
||||
|
||||
&--with-reactions {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.module-message__typing-avatar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
z-index: $z-index-base;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-inline-start: -4px;
|
||||
}
|
||||
|
||||
&--overflow-count {
|
||||
.module-Avatar__contents {
|
||||
@include light-theme() {
|
||||
background: $color-gray-05;
|
||||
color: $color-gray-60;
|
||||
}
|
||||
|
||||
@include dark-theme() {
|
||||
background: $color-gray-75;
|
||||
color: $color-gray-25;
|
||||
}
|
||||
}
|
||||
|
||||
.module-Avatar__label {
|
||||
@include font-caption-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.module-Avatar {
|
||||
min-width: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.module-Avatar__contents {
|
||||
outline: 3px solid;
|
||||
|
||||
@include light-theme() {
|
||||
outline-color: $color-white;
|
||||
}
|
||||
|
||||
@include dark-theme() {
|
||||
outline-color: $color-gray-95;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.module-message__unopened-gift-badge {
|
||||
width: 240px;
|
||||
height: 132px;
|
||||
|
|
|
@ -24,6 +24,7 @@ export type ConfigKeyType =
|
|||
| 'desktop.contactManagement'
|
||||
| 'desktop.groupCallOutboundRing2.beta'
|
||||
| 'desktop.groupCallOutboundRing2'
|
||||
| 'desktop.groupMultiTypingIndicators'
|
||||
| 'desktop.internalUser'
|
||||
| 'desktop.mandatoryProfileSharing'
|
||||
| 'desktop.mediaQuality.levels'
|
||||
|
|
|
@ -381,7 +381,7 @@ ConversationsMessageStatuses.story = {
|
|||
|
||||
export const ConversationTypingStatus = (): JSX.Element =>
|
||||
renderConversation({
|
||||
typingContactId: generateUuid(),
|
||||
typingContactIds: [generateUuid()],
|
||||
});
|
||||
|
||||
ConversationTypingStatus.story = {
|
||||
|
|
|
@ -370,7 +370,7 @@ export function ConversationList({
|
|||
'shouldShowDraft',
|
||||
'title',
|
||||
'type',
|
||||
'typingContactId',
|
||||
'typingContactIds',
|
||||
'unblurredAvatarPath',
|
||||
'unreadCount',
|
||||
'unreadMentionsCount',
|
||||
|
|
|
@ -441,16 +441,23 @@ const renderHeroRow = () => {
|
|||
};
|
||||
const renderTypingBubble = () => (
|
||||
<TypingBubble
|
||||
acceptedMessageRequest
|
||||
badge={undefined}
|
||||
color={getRandomColor()}
|
||||
typingContacts={[
|
||||
{
|
||||
acceptedMessageRequest: true,
|
||||
badge: undefined,
|
||||
color: getRandomColor(),
|
||||
phoneNumber: '+18005552222',
|
||||
id: getDefaultConversation().id,
|
||||
isMe: false,
|
||||
sharedGroupNames: [],
|
||||
title: 'title',
|
||||
},
|
||||
]}
|
||||
conversationId="123"
|
||||
conversationType="direct"
|
||||
phoneNumber="+18005552222"
|
||||
showContactModal={action('showContactModal')}
|
||||
i18n={i18n}
|
||||
isMe={false}
|
||||
title="title"
|
||||
theme={ThemeType.light}
|
||||
sharedGroupNames={[]}
|
||||
/>
|
||||
);
|
||||
const renderMiniPlayer = () => (
|
||||
|
|
|
@ -2,11 +2,13 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { select, text } from '@storybook/addon-knobs';
|
||||
import { times } from 'lodash';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { select } from '@storybook/addon-knobs';
|
||||
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import type { Props } from './TypingBubble';
|
||||
import type { PropsType as TypingBubblePropsType } from './TypingBubble';
|
||||
import { TypingBubble } from './TypingBubble';
|
||||
import { AvatarColors } from '../../types/Colors';
|
||||
import { getFakeBadge } from '../../test-both/helpers/getFakeBadge';
|
||||
|
@ -18,25 +20,35 @@ export default {
|
|||
title: 'Components/Conversation/TypingBubble',
|
||||
};
|
||||
|
||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
acceptedMessageRequest: true,
|
||||
badge: overrideProps.badge,
|
||||
isMe: false,
|
||||
type TypingContactType = TypingBubblePropsType['typingContacts'][number];
|
||||
|
||||
const contacts: Array<TypingContactType> = times(10, index => {
|
||||
const letter = (index + 10).toString(36).toUpperCase();
|
||||
return {
|
||||
id: `contact-${index}`,
|
||||
acceptedMessageRequest: false,
|
||||
avatarPath: '',
|
||||
badge: undefined,
|
||||
color: AvatarColors[index],
|
||||
name: `${letter} ${letter}`,
|
||||
phoneNumber: '(202) 555-0001',
|
||||
profileName: `${letter} ${letter}`,
|
||||
isMe: false,
|
||||
sharedGroupNames: [],
|
||||
title: `${letter} ${letter}`,
|
||||
};
|
||||
});
|
||||
|
||||
const createProps = (
|
||||
overrideProps: Partial<TypingBubblePropsType> = {}
|
||||
): TypingBubblePropsType => ({
|
||||
typingContacts: overrideProps.typingContacts || contacts.slice(0, 1),
|
||||
i18n,
|
||||
color: select(
|
||||
'color',
|
||||
AvatarColors.reduce((m, c) => ({ ...m, [c]: c }), {}),
|
||||
overrideProps.color || AvatarColors[0]
|
||||
),
|
||||
avatarPath: text('avatarPath', overrideProps.avatarPath || ''),
|
||||
title: '',
|
||||
profileName: text('profileName', overrideProps.profileName || ''),
|
||||
conversationType: select(
|
||||
'conversationType',
|
||||
{ group: 'group', direct: 'direct' },
|
||||
overrideProps.conversationType || 'direct'
|
||||
),
|
||||
sharedGroupNames: [],
|
||||
conversationId: '123',
|
||||
conversationType:
|
||||
overrideProps.conversationType ||
|
||||
select('conversationType', { group: 'group', direct: 'direct' }, 'direct'),
|
||||
showContactModal: action('showContactModal'),
|
||||
theme: ThemeType.light,
|
||||
});
|
||||
|
||||
|
@ -52,10 +64,25 @@ export function Group(): JSX.Element {
|
|||
return <TypingBubble {...props} />;
|
||||
}
|
||||
|
||||
Group.story = {
|
||||
name: 'Group (1 person typing)',
|
||||
};
|
||||
|
||||
export function GroupMultiTyping2(): JSX.Element {
|
||||
const props = createProps({
|
||||
conversationType: 'group',
|
||||
typingContacts: contacts.slice(0, 2),
|
||||
});
|
||||
|
||||
return <TypingBubble {...props} />;
|
||||
}
|
||||
|
||||
export function GroupWithBadge(): JSX.Element {
|
||||
const props = createProps({
|
||||
badge: getFakeBadge(),
|
||||
conversationType: 'group',
|
||||
typingContacts: contacts
|
||||
.slice(0, 1)
|
||||
.map(contact => ({ ...contact, badge: getFakeBadge() })),
|
||||
});
|
||||
|
||||
return <TypingBubble {...props} />;
|
||||
|
@ -64,3 +91,71 @@ export function GroupWithBadge(): JSX.Element {
|
|||
GroupWithBadge.story = {
|
||||
name: 'Group (with badge)',
|
||||
};
|
||||
|
||||
GroupMultiTyping2.story = {
|
||||
name: 'Group (2 persons typing)',
|
||||
};
|
||||
|
||||
export function GroupMultiTyping3(): JSX.Element {
|
||||
const props = createProps({
|
||||
conversationType: 'group',
|
||||
typingContacts: contacts.slice(0, 3),
|
||||
});
|
||||
|
||||
return <TypingBubble {...props} />;
|
||||
}
|
||||
|
||||
GroupMultiTyping3.story = {
|
||||
name: 'Group (3 persons typing)',
|
||||
};
|
||||
|
||||
export function GroupMultiTyping4(): JSX.Element {
|
||||
const props = createProps({
|
||||
conversationType: 'group',
|
||||
typingContacts: contacts.slice(0, 4),
|
||||
});
|
||||
|
||||
return <TypingBubble {...props} />;
|
||||
}
|
||||
|
||||
GroupMultiTyping4.story = {
|
||||
name: 'Group (4 persons typing)',
|
||||
};
|
||||
|
||||
export function GroupMultiTyping10(): JSX.Element {
|
||||
const props = createProps({
|
||||
conversationType: 'group',
|
||||
typingContacts: contacts.slice(0, 10),
|
||||
});
|
||||
|
||||
return <TypingBubble {...props} />;
|
||||
}
|
||||
|
||||
GroupMultiTyping10.story = {
|
||||
name: 'Group (10 persons typing)',
|
||||
};
|
||||
|
||||
export function GroupMultiTypingWithBadges(): JSX.Element {
|
||||
const props = createProps({
|
||||
conversationType: 'group',
|
||||
typingContacts: [
|
||||
{
|
||||
...contacts[0],
|
||||
badge: getFakeBadge(),
|
||||
},
|
||||
{
|
||||
...contacts[1],
|
||||
},
|
||||
{
|
||||
...contacts[2],
|
||||
badge: getFakeBadge(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return <TypingBubble {...props} />;
|
||||
}
|
||||
|
||||
GroupMultiTypingWithBadges.story = {
|
||||
name: 'Group (3 persons typing, 2 persons have badge)',
|
||||
};
|
||||
|
|
|
@ -12,39 +12,47 @@ import type { LocalizerType, ThemeType } from '../../types/Util';
|
|||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
import type { BadgeType } from '../../badges/types';
|
||||
|
||||
export type Props = Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'isMe'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
> & {
|
||||
badge: undefined | BadgeType;
|
||||
const MAX_AVATARS_COUNT = 3;
|
||||
|
||||
export type PropsType = {
|
||||
conversationId: string;
|
||||
conversationType: 'group' | 'direct';
|
||||
showContactModal: (contactId: string, conversationId?: string) => void;
|
||||
i18n: LocalizerType;
|
||||
theme: ThemeType;
|
||||
typingContacts: Array<
|
||||
Pick<
|
||||
ConversationType,
|
||||
| 'acceptedMessageRequest'
|
||||
| 'avatarPath'
|
||||
| 'color'
|
||||
| 'id'
|
||||
| 'isMe'
|
||||
| 'phoneNumber'
|
||||
| 'profileName'
|
||||
| 'sharedGroupNames'
|
||||
| 'title'
|
||||
> & {
|
||||
badge: undefined | BadgeType;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
export function TypingBubble({
|
||||
acceptedMessageRequest,
|
||||
avatarPath,
|
||||
badge,
|
||||
color,
|
||||
conversationId,
|
||||
conversationType,
|
||||
showContactModal,
|
||||
i18n,
|
||||
isMe,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
sharedGroupNames,
|
||||
theme,
|
||||
title,
|
||||
}: Props): ReactElement {
|
||||
typingContacts,
|
||||
}: PropsType): ReactElement {
|
||||
const isGroup = conversationType === 'group';
|
||||
|
||||
const typingContactsOverflowCount = Math.max(
|
||||
typingContacts.length - MAX_AVATARS_COUNT,
|
||||
0
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -54,22 +62,50 @@ export function TypingBubble({
|
|||
)}
|
||||
>
|
||||
{isGroup && (
|
||||
<div className="module-message__author-avatar-container">
|
||||
<Avatar
|
||||
acceptedMessageRequest={acceptedMessageRequest}
|
||||
avatarPath={avatarPath}
|
||||
badge={badge}
|
||||
color={color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={isMe}
|
||||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
theme={theme}
|
||||
title={title}
|
||||
sharedGroupNames={sharedGroupNames}
|
||||
size={28}
|
||||
/>
|
||||
<div className="module-message__typing-avatar-container">
|
||||
{typingContactsOverflowCount > 0 && (
|
||||
<div
|
||||
className="module-message__typing-avatar module-message__typing-avatar--overflow-count
|
||||
"
|
||||
>
|
||||
<div
|
||||
aria-label={i18n('icu:TypingBubble__avatar--overflow-count', {
|
||||
count: typingContactsOverflowCount,
|
||||
})}
|
||||
className="module-Avatar"
|
||||
>
|
||||
<div className="module-Avatar__contents">
|
||||
<div aria-hidden="true" className="module-Avatar__label">
|
||||
+{typingContactsOverflowCount}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{typingContacts.slice(-1 * MAX_AVATARS_COUNT).map(contact => (
|
||||
<div key={contact.id} className="module-message__typing-avatar">
|
||||
<Avatar
|
||||
acceptedMessageRequest={contact.acceptedMessageRequest}
|
||||
avatarPath={contact.avatarPath}
|
||||
badge={contact.badge}
|
||||
color={contact.color}
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
isMe={contact.isMe}
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
showContactModal(contact.id, conversationId);
|
||||
}}
|
||||
phoneNumber={contact.phoneNumber}
|
||||
profileName={contact.profileName}
|
||||
theme={theme}
|
||||
title={contact.title}
|
||||
sharedGroupNames={contact.sharedGroupNames}
|
||||
size={28}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="module-message__container-outer">
|
||||
|
|
|
@ -60,7 +60,7 @@ export type PropsData = Pick<
|
|||
| 'shouldShowDraft'
|
||||
| 'title'
|
||||
| 'type'
|
||||
| 'typingContactId'
|
||||
| 'typingContactIds'
|
||||
| 'unblurredAvatarPath'
|
||||
| 'unreadCount'
|
||||
| 'unreadMentionsCount'
|
||||
|
@ -104,13 +104,14 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
|||
theme,
|
||||
title,
|
||||
type,
|
||||
typingContactId,
|
||||
typingContactIds,
|
||||
unblurredAvatarPath,
|
||||
unreadCount,
|
||||
unreadMentionsCount,
|
||||
serviceId,
|
||||
}) {
|
||||
const isMuted = Boolean(muteExpiresAt && Date.now() < muteExpiresAt);
|
||||
const isSomeoneTyping = (typingContactIds?.length ?? 0) > 0;
|
||||
const headerName = (
|
||||
<>
|
||||
{isMe ? (
|
||||
|
@ -139,7 +140,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
|||
{i18n('icu:ConversationListItem--message-request')}
|
||||
</span>
|
||||
);
|
||||
} else if (typingContactId) {
|
||||
} else if (isSomeoneTyping) {
|
||||
messageText = <TypingAnimation i18n={i18n} />;
|
||||
} else if (shouldShowDraft && draftPreview) {
|
||||
messageText = (
|
||||
|
|
|
@ -5111,8 +5111,8 @@ export class ConversationModel extends window.Backbone
|
|||
this.clearContactTypingTimer.bind(this, typingToken),
|
||||
15 * 1000
|
||||
);
|
||||
// User was not previously typing before. State change!
|
||||
if (!record) {
|
||||
// User was not previously typing before. State change!
|
||||
this.trigger('change', this, { force: true });
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -298,7 +298,7 @@ export type ConversationType = ReadonlyDeep<
|
|||
unreadMentionsCount?: number;
|
||||
isSelected?: boolean;
|
||||
isFetchingUUID?: boolean;
|
||||
typingContactId?: string;
|
||||
typingContactIds?: Array<string>;
|
||||
recentMediaItems?: ReadonlyArray<MediaItemType>;
|
||||
profileSharing?: boolean;
|
||||
|
||||
|
|
|
@ -98,8 +98,8 @@ function renderHeroRow(id: string): JSX.Element {
|
|||
function renderMiniPlayer(options: { shouldFlow: boolean }): JSX.Element {
|
||||
return <SmartMiniPlayer {...options} />;
|
||||
}
|
||||
function renderTypingBubble(id: string): JSX.Element {
|
||||
return <SmartTypingBubble id={id} />;
|
||||
function renderTypingBubble(conversationId: string): JSX.Element {
|
||||
return <SmartTypingBubble conversationId={conversationId} />;
|
||||
}
|
||||
|
||||
const getWarning = (
|
||||
|
@ -241,13 +241,14 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
'unreadCount',
|
||||
'unreadMentionsCount',
|
||||
'isGroupV1AndDisabled',
|
||||
'typingContactIds',
|
||||
]),
|
||||
isConversationSelected: state.conversations.selectedConversationId === id,
|
||||
isIncomingMessageRequest: Boolean(
|
||||
conversation.messageRequestsEnabled &&
|
||||
!conversation.acceptedMessageRequest
|
||||
),
|
||||
isSomeoneTyping: Boolean(conversation.typingContactId),
|
||||
isSomeoneTyping: Boolean(conversation.typingContactIds?.[0]),
|
||||
...conversationMessages,
|
||||
|
||||
invitedContactsForNewlyCreatedGroup:
|
||||
|
|
|
@ -1,41 +1,62 @@
|
|||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { TypingBubble } from '../../components/conversation/TypingBubble';
|
||||
import { strictAssert } from '../../util/assert';
|
||||
import type { StateType } from '../reducer';
|
||||
|
||||
import { useGlobalModalActions } from '../ducks/globalModals';
|
||||
import { getIntl, getTheme } from '../selectors/user';
|
||||
import { getConversationSelector } from '../selectors/conversations';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||
import { isInternalUser } from '../selectors/items';
|
||||
|
||||
type ExternalProps = {
|
||||
id: string;
|
||||
conversationId: string;
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||
const { id } = props;
|
||||
|
||||
const conversationSelector = getConversationSelector(state);
|
||||
const conversation = conversationSelector(id);
|
||||
export function SmartTypingBubble(props: ExternalProps): JSX.Element {
|
||||
const { conversationId } = props;
|
||||
const i18n = useSelector(getIntl);
|
||||
const theme = useSelector(getTheme);
|
||||
const getConversation = useSelector(getConversationSelector);
|
||||
const conversation = getConversation(conversationId);
|
||||
if (!conversation) {
|
||||
throw new Error(`Did not find conversation ${id} in state!`);
|
||||
throw new Error(`Did not find conversation ${conversationId} in state!`);
|
||||
}
|
||||
|
||||
strictAssert(conversation.typingContactId, 'Missing typing contact ID');
|
||||
const typingContact = conversationSelector(conversation.typingContactId);
|
||||
strictAssert(
|
||||
conversation.typingContactIds?.[0],
|
||||
'Missing typing contact IDs'
|
||||
);
|
||||
|
||||
return {
|
||||
...typingContact,
|
||||
badge: getPreferredBadgeSelector(state)(typingContact.badges),
|
||||
conversationType: conversation.type,
|
||||
i18n: getIntl(state),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
};
|
||||
const { showContactModal } = useGlobalModalActions();
|
||||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
const preferredBadgeSelector = useSelector(getPreferredBadgeSelector);
|
||||
|
||||
export const SmartTypingBubble = smart(TypingBubble);
|
||||
const internalUser = useSelector(isInternalUser);
|
||||
const typingContactIdsVisible = internalUser
|
||||
? conversation.typingContactIds
|
||||
: conversation.typingContactIds.slice(0, 1);
|
||||
|
||||
const typingContacts = typingContactIdsVisible
|
||||
.map(contactId => getConversation(contactId))
|
||||
.map(typingConversation => {
|
||||
return {
|
||||
...typingConversation,
|
||||
badge: preferredBadgeSelector(typingConversation.badges),
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<TypingBubble
|
||||
showContactModal={showContactModal}
|
||||
conversationId={conversationId}
|
||||
conversationType={conversation.type}
|
||||
i18n={i18n}
|
||||
theme={theme}
|
||||
typingContacts={typingContacts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1145,7 +1145,7 @@ describe('both/state/selectors/conversations-extra', () => {
|
|||
title: 'No timestamp',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContactId: generateUuid(),
|
||||
typingContactIds: [generateUuid()],
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
}),
|
||||
|
@ -1166,7 +1166,7 @@ describe('both/state/selectors/conversations-extra', () => {
|
|||
title: 'B',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContactId: generateUuid(),
|
||||
typingContactIds: [generateUuid()],
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
}),
|
||||
|
@ -1187,7 +1187,7 @@ describe('both/state/selectors/conversations-extra', () => {
|
|||
title: 'C',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContactId: generateUuid(),
|
||||
typingContactIds: [generateUuid()],
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
}),
|
||||
|
@ -1208,7 +1208,7 @@ describe('both/state/selectors/conversations-extra', () => {
|
|||
title: 'A',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContactId: generateUuid(),
|
||||
typingContactIds: [generateUuid()],
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
}),
|
||||
|
@ -1229,7 +1229,7 @@ describe('both/state/selectors/conversations-extra', () => {
|
|||
title: 'First!',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContactId: generateUuid(),
|
||||
typingContactIds: [generateUuid()],
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
}),
|
||||
|
@ -1271,7 +1271,7 @@ describe('both/state/selectors/conversations-extra', () => {
|
|||
title: 'Pin Two',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContactId: generateUuid(),
|
||||
typingContactIds: [generateUuid()],
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
}),
|
||||
|
@ -1293,7 +1293,7 @@ describe('both/state/selectors/conversations-extra', () => {
|
|||
title: 'Pin Three',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContactId: generateUuid(),
|
||||
typingContactIds: [generateUuid()],
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
}),
|
||||
|
@ -1315,7 +1315,7 @@ describe('both/state/selectors/conversations-extra', () => {
|
|||
title: 'Pin One',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContactId: generateUuid(),
|
||||
typingContactIds: [generateUuid()],
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
}),
|
||||
|
@ -1354,7 +1354,7 @@ describe('both/state/selectors/conversations-extra', () => {
|
|||
title: 'Pin Two',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContactId: generateUuid(),
|
||||
typingContactIds: [generateUuid()],
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
}),
|
||||
|
@ -1375,7 +1375,7 @@ describe('both/state/selectors/conversations-extra', () => {
|
|||
title: 'Pin Three',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContactId: generateUuid(),
|
||||
typingContactIds: [generateUuid()],
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
}),
|
||||
|
@ -1396,7 +1396,7 @@ describe('both/state/selectors/conversations-extra', () => {
|
|||
title: 'Pin One',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContactId: generateUuid(),
|
||||
typingContactIds: [generateUuid()],
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
}),
|
||||
|
@ -1418,7 +1418,7 @@ describe('both/state/selectors/conversations-extra', () => {
|
|||
title: 'Pin One',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContactId: generateUuid(),
|
||||
typingContactIds: [generateUuid()],
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
}),
|
||||
|
@ -1439,7 +1439,7 @@ describe('both/state/selectors/conversations-extra', () => {
|
|||
title: 'Pin One',
|
||||
unreadCount: 1,
|
||||
isSelected: false,
|
||||
typingContactId: generateUuid(),
|
||||
typingContactIds: [generateUuid()],
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
}),
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import memoizee from 'memoizee';
|
||||
import { head, sortBy } from 'lodash';
|
||||
import { sortBy } from 'lodash';
|
||||
import type { ConversationModel } from '../models/conversations';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import type { ConversationAttributesType } from '../model-types';
|
||||
|
@ -77,8 +77,11 @@ function sortConversationTitles(
|
|||
// `ATTRIBUTES_THAT_DONT_INVALIDATE_PROPS_CACHE`, remove it from that list.
|
||||
export function getConversation(model: ConversationModel): ConversationType {
|
||||
const { attributes } = model;
|
||||
const typingValues = Object.values(model.contactTypingTimers || {});
|
||||
const typingMostRecent = head(sortBy(typingValues, 'timestamp'));
|
||||
const typingValues = sortBy(
|
||||
Object.values(model.contactTypingTimers || {}),
|
||||
'timestamp'
|
||||
);
|
||||
const typingContactIds = typingValues.map(({ senderId }) => senderId);
|
||||
|
||||
const ourAci = window.textsecure.storage.user.getAci();
|
||||
const ourPni = window.textsecure.storage.user.getPni();
|
||||
|
@ -219,7 +222,7 @@ export function getConversation(model: ConversationModel): ConversationType {
|
|||
timestamp: dropNull(timestamp),
|
||||
title: getTitle(attributes),
|
||||
titleNoDefault: getTitleNoDefault(attributes),
|
||||
typingContactId: typingMostRecent?.senderId,
|
||||
typingContactIds,
|
||||
searchableTitle: isMe(attributes)
|
||||
? window.i18n('icu:noteToSelf')
|
||||
: getTitle(attributes),
|
||||
|
|
Loading…
Reference in a new issue