speed up fetching closed group's members avatar

pull/2045/head
Audric Ackermann 3 years ago
parent af75b6f0e2
commit 5ba7f20162
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -192,27 +192,6 @@
}
});
Whisper.events.on('deleteLocalPublicMessages', async ({ messageServerIds, conversationId }) => {
if (!Array.isArray(messageServerIds)) {
return;
}
const messageIds = await window.Signal.Data.getMessageIdsFromServerIds(
messageServerIds,
conversationId
);
if (messageIds.length === 0) {
return;
}
const conversation = window.getConversationController().get(conversationId);
messageIds.forEach(id => {
if (conversation) {
conversation.removeMessage(id);
}
window.Signal.Data.removeMessage(id);
});
});
function manageExpiringData() {
window.Signal.Data.cleanSeenMessages();
window.Signal.Data.cleanLastHashes();

@ -2,9 +2,9 @@ import React, { useCallback, useState } from 'react';
import classNames from 'classnames';
import { AvatarPlaceHolder, ClosedGroupAvatar } from './AvatarPlaceHolder';
import { ConversationAvatar } from './session/usingClosedConversationDetails';
import { useEncryptedFileFetch } from '../hooks/useEncryptedFileFetch';
import _ from 'underscore';
import { useMembersAvatars } from '../hooks/useMembersAvatars';
export enum AvatarSize {
XS = 28,
@ -21,7 +21,6 @@ type Props = {
pubkey?: string;
size: AvatarSize;
base64Data?: string; // if this is not empty, it will be used to render the avatar with base64 encoded data
memberAvatars?: Array<ConversationAvatar>; // this is added by usingClosedConversationDetails
onAvatarClick?: () => void;
dataTestId?: string;
};
@ -42,21 +41,17 @@ const Identicon = (props: Props) => {
};
const NoImage = (props: {
memberAvatars?: Array<ConversationAvatar>;
name?: string;
pubkey?: string;
size: AvatarSize;
isClosedGroup: boolean;
onAvatarClick?: () => void;
}) => {
const { name, memberAvatars, size, pubkey } = props;
const { name, size, pubkey, isClosedGroup } = props;
// if no image but we have conversations set for the group, renders group members avatars
if (memberAvatars) {
if (pubkey && isClosedGroup) {
return (
<ClosedGroupAvatar
size={size}
memberAvatars={memberAvatars}
onAvatarClick={props.onAvatarClick}
/>
<ClosedGroupAvatar size={size} closedGroupId={pubkey} onAvatarClick={props.onAvatarClick} />
);
}
@ -93,8 +88,10 @@ const AvatarImage = (props: {
};
const AvatarInner = (props: Props) => {
const { avatarPath, base64Data, size, memberAvatars, name, dataTestId } = props;
const { avatarPath, base64Data, size, pubkey, name, dataTestId } = props;
const [imageBroken, setImageBroken] = useState(false);
const closedGroupMembers = useMembersAvatars(pubkey);
// contentType is not important
const { urlToLoad } = useEncryptedFileFetch(avatarPath || '', '');
const handleImageError = () => {
@ -106,7 +103,7 @@ const AvatarInner = (props: Props) => {
setImageBroken(true);
};
const isClosedGroupAvatar = Boolean(memberAvatars?.length);
const isClosedGroupAvatar = Boolean(closedGroupMembers?.length);
const hasImage = (base64Data || urlToLoad) && !imageBroken && !isClosedGroupAvatar;
const isClickable = !!props.onAvatarClick;
@ -134,7 +131,7 @@ const AvatarInner = (props: Props) => {
handleImageError={handleImageError}
/>
) : (
<NoImage {...props} />
<NoImage {...props} isClosedGroup={isClosedGroupAvatar} />
)}
</div>
);

@ -1,59 +1,57 @@
import React from 'react';
import { useMembersAvatars } from '../../hooks/useMembersAvatars';
import { Avatar, AvatarSize } from '../Avatar';
import { ConversationAvatar } from '../session/usingClosedConversationDetails';
interface Props {
type Props = {
size: number;
memberAvatars: Array<ConversationAvatar>; // this is added by usingClosedConversationDetails
closedGroupId: string;
onAvatarClick?: () => void;
}
};
export class ClosedGroupAvatar extends React.PureComponent<Props> {
public getClosedGroupAvatarsSize(size: AvatarSize): AvatarSize {
// Always use the size directly under the one requested
switch (size) {
case AvatarSize.S:
return AvatarSize.XS;
case AvatarSize.M:
return AvatarSize.S;
case AvatarSize.L:
return AvatarSize.M;
case AvatarSize.XL:
return AvatarSize.L;
case AvatarSize.HUGE:
return AvatarSize.XL;
default:
throw new Error(`Invalid size request for closed group avatar: ${size}`);
}
function getClosedGroupAvatarsSize(size: AvatarSize): AvatarSize {
// Always use the size directly under the one requested
switch (size) {
case AvatarSize.XS:
return AvatarSize.XS;
case AvatarSize.S:
return AvatarSize.XS;
case AvatarSize.M:
return AvatarSize.S;
case AvatarSize.L:
return AvatarSize.M;
case AvatarSize.XL:
return AvatarSize.L;
case AvatarSize.HUGE:
return AvatarSize.XL;
default:
throw new Error(`Invalid size request for closed group avatar: ${size}`);
}
}
public render() {
const { memberAvatars, size, onAvatarClick } = this.props;
const avatarsDiameter = this.getClosedGroupAvatarsSize(size);
export const ClosedGroupAvatar = (props: Props) => {
const { closedGroupId, size, onAvatarClick } = props;
const conv1 = memberAvatars.length > 0 ? memberAvatars[0] : undefined;
const conv2 = memberAvatars.length > 1 ? memberAvatars[1] : undefined;
const name1 = conv1?.name || conv1?.id || undefined;
const name2 = conv2?.name || conv2?.id || undefined;
const memberAvatars = useMembersAvatars(closedGroupId);
const avatarsDiameter = getClosedGroupAvatarsSize(size);
const firstMember = memberAvatars?.[0];
const secondMember = memberAvatars?.[1];
// use the 2 first members as group avatars
return (
<div className="module-avatar__icon-closed">
<Avatar
avatarPath={conv1?.avatarPath}
name={name1}
size={avatarsDiameter}
pubkey={conv1?.id}
onAvatarClick={onAvatarClick}
/>
<Avatar
avatarPath={conv2?.avatarPath}
name={name2}
size={avatarsDiameter}
pubkey={conv2?.id}
onAvatarClick={onAvatarClick}
/>
</div>
);
}
}
return (
<div className="module-avatar__icon-closed">
<Avatar
avatarPath={firstMember?.avatarPath}
name={firstMember?.name}
size={avatarsDiameter}
pubkey={firstMember?.id}
onAvatarClick={onAvatarClick}
/>
<Avatar
avatarPath={secondMember?.avatarPath}
name={secondMember?.name}
size={avatarsDiameter}
pubkey={secondMember?.id}
onAvatarClick={onAvatarClick}
/>
</div>
);
};

@ -9,7 +9,6 @@ import { Timestamp } from './conversation/Timestamp';
import { ContactName } from './conversation/ContactName';
import { TypingAnimation } from './conversation/TypingAnimation';
import { ConversationAvatar } from './session/usingClosedConversationDetails';
import { MemoConversationListItemContextMenu } from './session/menu/ConversationListItemContextMenu';
import { createPortal } from 'react-dom';
import { OutgoingMessageStatus } from './conversation/message/OutgoingMessageStatus';
@ -21,7 +20,6 @@ import {
ReduxConversationType,
} from '../state/ducks/conversations';
import _ from 'underscore';
import { useMembersAvatars } from '../hooks/useMembersAvatar';
import { SessionIcon } from './session/icon';
import { useDispatch, useSelector } from 'react-redux';
import { SectionType } from '../state/ducks/section';
@ -217,13 +215,11 @@ const MessageItem = (props: {
const AvatarItem = (props: {
avatarPath: string | null;
conversationId: string;
memberAvatars?: Array<ConversationAvatar>;
name?: string;
profileName?: string;
isPrivate: boolean;
}) => {
const { avatarPath, name, isPrivate, conversationId, profileName, memberAvatars } = props;
const { avatarPath, name, isPrivate, conversationId, profileName } = props;
const userName = name || profileName || conversationId;
const dispatch = useDispatch();
@ -233,7 +229,6 @@ const AvatarItem = (props: {
avatarPath={avatarPath}
name={userName}
size={AvatarSize.S}
memberAvatars={memberAvatars}
pubkey={conversationId}
onAvatarClick={() => {
if (isPrivate) {
@ -278,8 +273,6 @@ const ConversationListItem = (props: Props) => {
const triggerId = `conversation-item-${conversationId}-ctxmenu`;
const key = `conversation-item-${conversationId}`;
const membersAvatar = useMembersAvatars(props);
const openConvo = useCallback(
async (e: React.MouseEvent<HTMLDivElement>) => {
// mousedown is invoked sooner than onClick, but for both right and left click
@ -319,7 +312,6 @@ const ConversationListItem = (props: Props) => {
<AvatarItem
conversationId={conversationId}
avatarPath={avatarPath || null}
memberAvatars={membersAvatar}
profileName={profileName}
name={name}
isPrivate={isPrivate || false}

@ -5,7 +5,6 @@ import { Avatar, AvatarSize } from '../Avatar';
import { SessionIconButton } from '../session/icon';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../session/SessionButton';
import { ConversationAvatar } from '../session/usingClosedConversationDetails';
import { MemoConversationHeaderMenu } from '../session/menu/ConversationHeaderMenu';
import { contextMenu } from 'react-contexify';
import styled from 'styled-components';
@ -16,7 +15,6 @@ import {
getCurrentNotificationSettingText,
getIsSelectedNoteToSelf,
getIsSelectedPrivate,
getSelectedConversation,
getSelectedConversationIsPublic,
getSelectedConversationKey,
getSelectedMessageIds,
@ -25,7 +23,6 @@ import {
isRightPanelShowing,
} from '../../state/selectors/conversations';
import { useDispatch, useSelector } from 'react-redux';
import { useMembersAvatars } from '../../hooks/useMembersAvatar';
import {
deleteMessagesById,
@ -166,14 +163,13 @@ const ExpirationLength = (props: { expirationSettingName?: string }) => {
const AvatarHeader = (props: {
avatarPath: string | null;
memberAvatars?: Array<ConversationAvatar>;
name?: string;
pubkey: string;
profileName?: string;
showBackButton: boolean;
onAvatarClick?: (pubkey: string) => void;
}) => {
const { avatarPath, memberAvatars, name, pubkey, profileName } = props;
const { avatarPath, name, pubkey, profileName } = props;
const userName = name || profileName || pubkey;
return (
@ -188,7 +184,6 @@ const AvatarHeader = (props: {
props.onAvatarClick(pubkey);
}
}}
memberAvatars={memberAvatars}
pubkey={pubkey}
/>
</span>
@ -344,8 +339,6 @@ export const ConversationHeaderWithDetails = () => {
const headerProps = useSelector(getConversationHeaderProps);
const isSelectionMode = useSelector(isMessageSelectionMode);
const selectedConversation = useSelector(getSelectedConversation);
const memberDetails = useMembersAvatars(selectedConversation);
const isMessageDetailOpened = useSelector(isMessageDetailView);
const dispatch = useDispatch();
@ -401,7 +394,6 @@ export const ConversationHeaderWithDetails = () => {
pubkey={conversationKey}
showBackButton={isMessageDetailOpened}
avatarPath={avatarPath}
memberAvatars={memberDetails}
name={name}
profileName={profileName}
/>

@ -31,7 +31,6 @@ import {
getSelectedConversation,
isRightPanelShowing,
} from '../../../state/selectors/conversations';
import { useMembersAvatars } from '../../../hooks/useMembersAvatar';
import { closeRightPanel } from '../../../state/ducks/conversations';
async function getMediaGalleryProps(
@ -110,7 +109,6 @@ async function getMediaGalleryProps(
const HeaderItem = () => {
const selectedConversation = useSelector(getSelectedConversation);
const dispatch = useDispatch();
const memberDetails = useMembersAvatars(selectedConversation);
if (!selectedConversation) {
return null;
@ -139,13 +137,7 @@ const HeaderItem = () => {
dispatch(closeRightPanel());
}}
/>
<Avatar
avatarPath={avatarPath || ''}
name={userName}
size={AvatarSize.XL}
memberAvatars={memberDetails}
pubkey={id}
/>
<Avatar avatarPath={avatarPath || ''} name={userName} size={AvatarSize.XL} pubkey={id} />
<div className="invite-friends-container">
{showInviteContacts && (
<SessionIconButton

@ -1,69 +0,0 @@
import { GroupUtils, UserUtils } from '../../session/utils';
import { PubKey } from '../../session/types';
import React from 'react';
import * as _ from 'lodash';
import { getConversationController } from '../../session/conversations';
export type ConversationAvatar = {
avatarPath?: string;
id?: string; // member's pubkey
name?: string;
};
type State = {
memberAvatars?: Array<ConversationAvatar>; // this is added by usingClosedConversationDetails
};
export function usingClosedConversationDetails(WrappedComponent: any) {
return class extends React.Component<any, State> {
constructor(props: any) {
super(props);
this.state = {
memberAvatars: undefined,
};
}
public componentDidMount() {
this.fetchClosedConversationDetails();
}
public componentWillReceiveProps() {
this.fetchClosedConversationDetails();
}
public render() {
return <WrappedComponent memberAvatars={this.state.memberAvatars} {...this.props} />;
}
private fetchClosedConversationDetails() {
const { isPublic, type, conversationType, isGroup, id } = this.props;
if (!isPublic && (conversationType === 'group' || type === 'group' || isGroup)) {
const groupId = id;
const ourPrimary = UserUtils.getOurPubKeyFromCache();
let members = GroupUtils.getGroupMembers(PubKey.cast(groupId));
const ourself = members.find(m => m.key !== ourPrimary.key);
// add ourself back at the back, so it's shown only if only 1 member and we are still a member
members = members.filter(m => m.key !== ourPrimary.key);
members.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
if (ourself) {
members.push(ourPrimary);
}
// no need to forward more than 2 conversations for rendering the group avatar
members = members.slice(0, 2);
const memberConvos = _.compact(members.map(m => getConversationController().get(m.key)));
const memberAvatars = memberConvos.map(m => {
return {
avatarPath: m.getAvatarPath() || undefined,
id: m.id,
name: m.get('name') || m.get('profileName') || m.id,
};
});
this.setState({ memberAvatars });
} else {
this.setState({ memberAvatars: undefined });
}
}
};
}

@ -1,55 +0,0 @@
import _ from 'lodash';
import { useEffect, useState } from 'react';
import { getConversationController } from '../session/conversations';
import { UserUtils } from '../session/utils';
import { ReduxConversationType } from '../state/ducks/conversations';
export function useMembersAvatars(conversation: ReduxConversationType | undefined) {
const [membersAvatars, setMembersAvatars] = useState<
| Array<{
avatarPath: string | undefined;
id: string;
name: string;
}>
| undefined
>(undefined);
useEffect(
() => {
if (!conversation) {
setMembersAvatars(undefined);
return;
}
const { isPublic, isGroup, members: convoMembers } = conversation;
if (!isPublic && isGroup) {
const ourPrimary = UserUtils.getOurPubKeyStrFromCache();
const ourself = convoMembers?.find(m => m !== ourPrimary) || undefined;
// add ourself back at the back, so it's shown only if only 1 member and we are still a member
let membersFiltered = convoMembers?.filter(m => m !== ourPrimary) || [];
membersFiltered.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
if (ourself) {
membersFiltered.push(ourPrimary);
}
// no need to forward more than 2 conversations for rendering the group avatar
membersFiltered = membersFiltered.slice(0, 2);
const memberConvos = _.compact(
membersFiltered.map(m => getConversationController().get(m))
);
const memberAvatars = memberConvos.map(m => {
return {
avatarPath: m.getAvatarPath() || undefined,
id: m.id as string,
name: (m.get('name') || m.get('profileName') || m.id) as string,
};
});
setMembersAvatars(memberAvatars);
} else {
setMembersAvatars(undefined);
}
},
conversation ? [conversation.members, conversation.id] : []
);
return membersAvatars;
}

@ -0,0 +1,55 @@
import { UserUtils } from '../session/utils';
import * as _ from 'lodash';
import { useSelector } from 'react-redux';
import { StateType } from '../state/reducer';
export type ConversationAvatar = {
avatarPath?: string;
id: string; // member's pubkey
name: string;
};
export function useMembersAvatars(closedGroupPubkey: string | undefined) {
const ourPrimary = UserUtils.getOurPubKeyStrFromCache();
return useSelector((state: StateType): Array<ConversationAvatar> | undefined => {
if (!closedGroupPubkey) {
return undefined;
}
const groupConvo = state.conversations.conversationLookup[closedGroupPubkey];
if (groupConvo.isPrivate || groupConvo.isPublic || !groupConvo.isGroup) {
return undefined;
}
// this must be a closed group
const originalMembers = groupConvo.members;
if (!originalMembers || originalMembers.length === 0) {
return undefined;
}
const allMembersSorted = originalMembers.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
// no need to forward more than 2 conversations for rendering the group avatar
const usAtTheEndMaxTwo = _.sortBy(allMembersSorted, a => (a === ourPrimary ? 1 : 0)).slice(
0,
2
);
const memberConvos = _.compact(
usAtTheEndMaxTwo
.map(m => state.conversations.conversationLookup[m])
.map(m => {
if (!m) {
return undefined;
}
const userName = m.name || m.profileName || m.id;
return {
avatarPath: m.avatarPath || undefined,
id: m.id,
name: userName,
};
})
);
return memberConvos && memberConvos.length ? memberConvos : undefined;
});
}

@ -325,6 +325,7 @@ export const _getConversationComparator = (testingi18n?: LocalizerType) => {
return collator.compare(leftTitle, rightTitle);
};
};
export const getConversationComparator = createSelector(getIntl, _getConversationComparator);
// export only because we use it in some of our tests

Loading…
Cancel
Save