diff --git a/js/background.js b/js/background.js index 092edf848..bbf003ee3 100644 --- a/js/background.js +++ b/js/background.js @@ -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(); diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index 0c347cce8..8597d6850 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -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; // this is added by usingClosedConversationDetails onAvatarClick?: () => void; dataTestId?: string; }; @@ -42,21 +41,17 @@ const Identicon = (props: Props) => { }; const NoImage = (props: { - memberAvatars?: Array; 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 ( - + ); } @@ -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} /> ) : ( - + )} ); diff --git a/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx b/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx index 4dc344564..be3ef30d1 100644 --- a/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx +++ b/ts/components/AvatarPlaceHolder/ClosedGroupAvatar.tsx @@ -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; // this is added by usingClosedConversationDetails + closedGroupId: string; onAvatarClick?: () => void; -} +}; -export class ClosedGroupAvatar extends React.PureComponent { - 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 ( -
- - -
- ); - } -} + return ( +
+ + +
+ ); +}; diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 58c7401ae..f8b8a37d1 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -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; 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) => { // mousedown is invoked sooner than onClick, but for both right and left click @@ -319,7 +312,6 @@ const ConversationListItem = (props: Props) => { { const AvatarHeader = (props: { avatarPath: string | null; - memberAvatars?: Array; 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} /> @@ -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} /> diff --git a/ts/components/session/conversation/SessionRightPanel.tsx b/ts/components/session/conversation/SessionRightPanel.tsx index a0897b3ee..a2eb5d61c 100644 --- a/ts/components/session/conversation/SessionRightPanel.tsx +++ b/ts/components/session/conversation/SessionRightPanel.tsx @@ -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()); }} /> - +
{showInviteContacts && ( ; // this is added by usingClosedConversationDetails -}; - -export function usingClosedConversationDetails(WrappedComponent: any) { - return class extends React.Component { - constructor(props: any) { - super(props); - this.state = { - memberAvatars: undefined, - }; - } - - public componentDidMount() { - this.fetchClosedConversationDetails(); - } - - public componentWillReceiveProps() { - this.fetchClosedConversationDetails(); - } - - public render() { - return ; - } - - 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 }); - } - } - }; -} diff --git a/ts/hooks/useMembersAvatar.tsx b/ts/hooks/useMembersAvatar.tsx deleted file mode 100644 index 76386fe83..000000000 --- a/ts/hooks/useMembersAvatar.tsx +++ /dev/null @@ -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; -} diff --git a/ts/hooks/useMembersAvatars.tsx b/ts/hooks/useMembersAvatars.tsx new file mode 100644 index 000000000..39e972a21 --- /dev/null +++ b/ts/hooks/useMembersAvatars.tsx @@ -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 | 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; + }); +} diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index de9fc35a4..16016868a 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -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