import React, { useEffect, useState } from 'react'; import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon'; import { Avatar, AvatarSize } from '../../Avatar'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../SessionButton'; import { SessionDropdown } from '../SessionDropdown'; import { MediaGallery } from '../../conversation/media-gallery/MediaGallery'; import _ from 'lodash'; import { Constants } from '../../../session'; import { AttachmentTypeWithPath } from '../../../types/Attachment'; import { useTheme } from 'styled-components'; import { getMessagesWithFileAttachments, getMessagesWithVisualMediaAttachments, } from '../../../data/data'; import { SpacerLG } from '../../basic/Text'; import { deleteMessagesByConvoIdWithConfirmation, setDisappearingMessagesByConvoId, showAddModeratorsByConvoId, showInviteContactByConvoId, showLeaveGroupByConvoId, showRemoveModeratorsByConvoId, showUpdateGroupMembersByConvoId, showUpdateGroupNameByConvoId, } from '../../../interactions/conversationInteractions'; import { MediaItemType } from '../../LightboxGallery'; // tslint:disable-next-line: no-submodule-imports import useInterval from 'react-use/lib/useInterval'; import { useDispatch, useSelector } from 'react-redux'; import { getTimerOptions } from '../../../state/selectors/timerOptions'; import { getSelectedConversation, isRightPanelShowing, } from '../../../state/selectors/conversations'; import { useMembersAvatars } from '../../../hooks/useMembersAvatar'; import { closeRightPanel } from '../../../state/ducks/conversations'; async function getMediaGalleryProps( conversationId: string ): Promise<{ documents: Array<MediaItemType>; media: Array<MediaItemType>; }> { // We fetch more documents than media as they don’t require to be loaded // into memory right away. Revisit this once we have infinite scrolling: const rawMedia = await getMessagesWithVisualMediaAttachments(conversationId, { limit: Constants.CONVERSATION.DEFAULT_MEDIA_FETCH_COUNT, }); const rawDocuments = await getMessagesWithFileAttachments(conversationId, { limit: Constants.CONVERSATION.DEFAULT_DOCUMENTS_FETCH_COUNT, }); const media = _.flatten( rawMedia.map(attributes => { const { attachments, source, id, timestamp, serverTimestamp, received_at } = attributes; return (attachments || []) .filter( (attachment: AttachmentTypeWithPath) => attachment.thumbnail && !attachment.pending && !attachment.error ) .map((attachment: AttachmentTypeWithPath, index: number) => { const { thumbnail } = attachment; const mediaItem: MediaItemType = { objectURL: window.Signal.Migrations.getAbsoluteAttachmentPath(attachment.path), thumbnailObjectUrl: thumbnail ? window.Signal.Migrations.getAbsoluteAttachmentPath(thumbnail.path) : null, contentType: attachment.contentType || '', index, messageTimestamp: timestamp || serverTimestamp || received_at || 0, messageSender: source, messageId: id, attachment, }; return mediaItem; }); }) ); // Unlike visual media, only one non-image attachment is supported const documents = rawDocuments.map(attributes => { // this is to not fail if the attachment is invalid (could be a Long Attachment type which is not supported) if (!attributes.attachments?.length) { // window?.log?.info( // 'Got a message with an empty list of attachment. Skipping...' // ); return null; } const attachment = attributes.attachments[0]; const { source, id, timestamp, serverTimestamp, received_at } = attributes; return { contentType: attachment.contentType, index: 0, attachment, messageTimestamp: timestamp || serverTimestamp || received_at || 0, messageSender: source, messageId: id, }; }); return { media, documents: _.compact(documents), // remove null }; } const HeaderItem = () => { const selectedConversation = useSelector(getSelectedConversation); const theme = useTheme(); const dispatch = useDispatch(); const memberDetails = useMembersAvatars(selectedConversation); if (!selectedConversation) { return null; } const { avatarPath, id, isGroup, isKickedFromGroup, profileName, phoneNumber, isBlocked, left, name, } = selectedConversation; const showInviteContacts = isGroup && !isKickedFromGroup && !isBlocked && !left; const userName = name || profileName || phoneNumber; return ( <div className="group-settings-header"> <SessionIconButton iconType={SessionIconType.Chevron} iconSize={SessionIconSize.Medium} iconRotation={270} onClick={() => { dispatch(closeRightPanel()); }} theme={theme} /> <Avatar avatarPath={avatarPath || ''} name={userName} size={AvatarSize.XL} memberAvatars={memberDetails} pubkey={id} /> <div className="invite-friends-container"> {showInviteContacts && ( <SessionIconButton iconType={SessionIconType.AddUser} iconSize={SessionIconSize.Medium} onClick={() => { if (selectedConversation) { showInviteContactByConvoId(selectedConversation.id); } }} theme={theme} /> )} </div> </div> ); }; // tslint:disable: cyclomatic-complexity // tslint:disable: max-func-body-length export const SessionRightPanelWithDetails = () => { const [documents, setDocuments] = useState<Array<MediaItemType>>([]); const [media, setMedia] = useState<Array<MediaItemType>>([]); const selectedConversation = useSelector(getSelectedConversation); const isShowing = useSelector(isRightPanelShowing); useEffect(() => { let isRunning = true; if (isShowing && selectedConversation) { void getMediaGalleryProps(selectedConversation.id).then(results => { if (isRunning) { if (!_.isEqual(documents, results.documents)) { setDocuments(results.documents); } if (!_.isEqual(media, results.media)) { setMedia(results.media); } } }); } return () => { isRunning = false; return; }; }, [isShowing, selectedConversation?.id]); useInterval(async () => { if (isShowing && selectedConversation) { const results = await getMediaGalleryProps(selectedConversation.id); if (results.documents.length !== documents.length || results.media.length !== media.length) { setDocuments(results.documents); setMedia(results.media); } } }, 10000); if (!selectedConversation) { return null; } const { id, subscriberCount, name, isKickedFromGroup, left, isPublic, weAreAdmin, isBlocked, isGroup, } = selectedConversation; const showMemberCount = !!(subscriberCount && subscriberCount > 0); const commonNoShow = isKickedFromGroup || left || isBlocked; const hasDisappearingMessages = !isPublic && !commonNoShow; const leaveGroupString = isPublic ? window.i18n('leaveGroup') : isKickedFromGroup ? window.i18n('youGotKickedFromGroup') : left ? window.i18n('youLeftTheGroup') : window.i18n('leaveGroup'); const timerOptions = useSelector(getTimerOptions).timerOptions; const disappearingMessagesOptions = timerOptions.map(option => { return { content: option.name, onClick: () => { void setDisappearingMessagesByConvoId(id, option.value); }, }; }); const showUpdateGroupNameButton = isGroup && (!isPublic || (isPublic && weAreAdmin)) && !commonNoShow; const showAddRemoveModeratorsButton = weAreAdmin && !commonNoShow && isPublic; const showUpdateGroupMembersButton = !isPublic && isGroup && !commonNoShow; const deleteConvoAction = isPublic ? () => { deleteMessagesByConvoIdWithConfirmation(id); } : () => { showLeaveGroupByConvoId(id); }; return ( <div className="group-settings"> <HeaderItem /> <h2>{name}</h2> {showMemberCount && ( <> <SpacerLG /> <div role="button" className="subtle"> {window.i18n('members', subscriberCount)} </div> <SpacerLG /> </> )} {showUpdateGroupNameButton && ( <div className="group-settings-item" role="button" onClick={async () => { await showUpdateGroupNameByConvoId(id); }} > {isPublic ? window.i18n('editGroup') : window.i18n('editGroupName')} </div> )} {showAddRemoveModeratorsButton && ( <> <div className="group-settings-item" role="button" onClick={() => { showAddModeratorsByConvoId(id); }} > {window.i18n('addModerators')} </div> <div className="group-settings-item" role="button" onClick={() => { showRemoveModeratorsByConvoId(id); }} > {window.i18n('removeModerators')} </div> </> )} {showUpdateGroupMembersButton && ( <div className="group-settings-item" role="button" onClick={async () => { await showUpdateGroupMembersByConvoId(id); }} > {window.i18n('groupMembers')} </div> )} {hasDisappearingMessages && ( <SessionDropdown label={window.i18n('disappearingMessages')} options={disappearingMessagesOptions} /> )} <MediaGallery documents={documents} media={media} /> {isGroup && ( // tslint:disable-next-line: use-simple-attributes <SessionButton text={leaveGroupString} buttonColor={SessionButtonColor.Danger} disabled={isKickedFromGroup || left} buttonType={SessionButtonType.SquareOutline} onClick={deleteConvoAction} /> )} </div> ); };