You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
session-desktop/ts/components/session/conversation/SessionRightPanel.tsx

421 lines
13 KiB
TypeScript

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 _, { noop } from 'lodash';
import { TimerOption } from '../../conversation/ConversationHeader';
5 years ago
import { Constants } from '../../../session';
import {
ConversationAvatar,
usingClosedConversationDetails,
} from '../usingClosedConversationDetails';
import { AttachmentTypeWithPath, save } from '../../../types/Attachment';
import { DefaultTheme, useTheme, withTheme } from 'styled-components';
import {
getMessagesWithFileAttachments,
getMessagesWithVisualMediaAttachments,
} from '../../../data/data';
import { getDecryptedMediaUrl } from '../../../session/crypto/DecryptedAttachmentsManager';
import { LightBoxOptions } from './SessionConversation';
import { UserUtils } from '../../../session/utils';
import { sendDataExtractionNotification } from '../../../session/messages/outgoing/controlMessage/DataExtractionNotificationMessage';
import { SpacerLG } from '../../basic/Text';
import {
deleteMessagesByConvoIdWithConfirmation,
setDisappearingMessagesByConvoId,
showAddModeratorsByConvoId,
showInviteContactByConvoId,
showLeaveGroupByConvoId,
showRemoveModeratorsByConvoId,
showUpdateGroupMembersByConvoId,
showUpdateGroupNameByConvoId,
} from '../../../interactions/conversationInteractions';
import { ItemClickEvent } from '../../conversation/media-gallery/types/ItemClickEvent';
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';
type Props = {
memberAvatars?: Array<ConversationAvatar>; // this is added by usingClosedConversationDetails
5 years ago
onShowLightBox: (lightboxOptions?: LightBoxOptions) => void;
};
async function getMediaGalleryProps(
conversationId: string,
medias: Array<MediaItemType>,
onShowLightBox: (lightboxOptions?: LightBoxOptions) => void
): Promise<{
documents: Array<MediaItemType>;
media: Array<MediaItemType>;
onItemClick: any;
}> {
// We fetch more documents than media as they dont 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;
5 years ago
return {
contentType: attachment.contentType,
index: 0,
attachment,
messageTimestamp: timestamp || serverTimestamp || received_at || 0,
messageSender: source,
messageId: id,
5 years ago
};
});
const saveAttachment = async ({
attachment,
messageTimestamp,
messageSender,
}: {
attachment: AttachmentTypeWithPath;
messageTimestamp: number;
messageSender: string;
}) => {
const timestamp = messageTimestamp;
attachment.url = await getDecryptedMediaUrl(attachment.url, attachment.contentType);
save({
attachment,
document,
getAbsolutePath: window.Signal.Migrations.getAbsoluteAttachmentPath,
timestamp,
});
await sendDataExtractionNotification(conversationId, messageSender, timestamp);
};
5 years ago
const onItemClick = (event: ItemClickEvent) => {
if (!event) {
console.warn('no event');
return;
5 years ago
}
const { mediaItem, type } = event;
switch (type) {
case 'documents': {
void saveAttachment({
messageSender: mediaItem.messageSender,
messageTimestamp: mediaItem.messageTimestamp,
attachment: mediaItem.attachment,
});
break;
5 years ago
}
case 'media': {
const lightBoxOptions: LightBoxOptions = {
media: medias,
attachment: mediaItem.attachment,
};
onShowLightBox(lightBoxOptions);
break;
5 years ago
}
default:
throw new TypeError(`Unknown attachment type: '${type}'`);
}
};
return {
media,
documents: _.compact(documents), // remove null
onItemClick,
};
}
const HeaderItem = () => {
const selectedConversation = useSelector(getSelectedConversation);
const theme = useTheme();
const dispatch = useDispatch();
const memberDetails = useMembersAvatars(selectedConversation);
if (!selectedConversation) {
return null;
}
const {
avatarPath,
isPublic,
id,
weAreAdmin,
isKickedFromGroup,
profileName,
phoneNumber,
isBlocked,
left,
name,
} = selectedConversation;
const showInviteContacts = (isPublic || weAreAdmin) && !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 = (props: Props) => {
const [documents, setDocuments] = useState<Array<MediaItemType>>([]);
const [media, setMedia] = useState<Array<MediaItemType>>([]);
const [onItemClick, setOnItemClick] = useState<any>(undefined);
const selectedConversation = useSelector(getSelectedConversation);
const isShowing = useSelector(isRightPanelShowing);
console.warn('props', props);
useEffect(() => {
let isRunning = true;
if (isShowing && selectedConversation) {
void getMediaGalleryProps(selectedConversation.id, media, props.onShowLightBox).then(
results => {
console.warn('results2', results);
if (isRunning) {
setDocuments(results.documents);
setMedia(results.media);
setOnItemClick(results.onItemClick);
}
}
);
}
return () => {
isRunning = false;
return;
};
}, [isShowing, selectedConversation?.id]);
useInterval(async () => {
if (isShowing && selectedConversation) {
const results = await getMediaGalleryProps(
selectedConversation.id,
media,
props.onShowLightBox
);
console.warn('results', results);
if (results.documents.length !== documents.length || results.media.length !== media.length) {
setDocuments(results.documents);
setMedia(results.media);
setOnItemClick(results.onItemClick);
}
}
}, 10000);
5 years ago
if (!selectedConversation) {
return null;
5 years ago
}
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: async () => {
await setDisappearingMessagesByConvoId(id, option.value);
},
};
});
const showUpdateGroupNameButton = weAreAdmin && !commonNoShow;
const showAddRemoveModeratorsButton = weAreAdmin && !commonNoShow && isPublic;
const showUpdateGroupMembersButton = !isPublic && isGroup && !commonNoShow;
const deleteConvoAction = isPublic
? () => {
deleteMessagesByConvoIdWithConfirmation(id);
}
: () => {
showLeaveGroupByConvoId(id);
};
console.warn('onItemClick', onItemClick);
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} onItemClick={onItemClick} />
{isGroup && (
// tslint:disable-next-line: use-simple-attributes
<SessionButton
text={leaveGroupString}
buttonColor={SessionButtonColor.Danger}
disabled={isKickedFromGroup || left}
buttonType={SessionButtonType.SquareOutline}
onClick={deleteConvoAction}
/>
)}
</div>
);
};