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

388 lines
12 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import React 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 { TimerOption } from '../../conversation/ConversationHeader';
import { Constants } from '../../../session';
import {
ConversationAvatar,
usingClosedConversationDetails,
} from '../usingClosedConversationDetails';
import { save } from '../../../types/Attachment';
import { DefaultTheme, 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';
interface Props {
id: string;
name?: string;
profileName?: string;
phoneNumber: string;
memberCount: number;
description: string;
avatarPath: string;
timerOptions: Array<TimerOption>;
isPublic: boolean;
isAdmin: boolean;
isKickedFromGroup: boolean;
left: boolean;
isBlocked: boolean;
isGroup: boolean;
memberAvatars?: Array<ConversationAvatar>; // this is added by usingClosedConversationDetails
onGoBack: () => void;
onInviteContacts: () => void;
onLeaveGroup: () => void;
onDeleteContact: () => void;
onUpdateGroupName: () => void;
onAddModerators: () => void;
onRemoveModerators: () => void;
onUpdateGroupMembers: () => void;
onShowLightBox: (lightboxOptions?: LightBoxOptions) => void;
onSetDisappearingMessages: (seconds: number) => void;
theme: DefaultTheme;
}
interface State {
documents: Array<any>;
media: Array<any>;
onItemClick: any;
}
class SessionRightPanel extends React.Component<Props, State> {
public constructor(props: Props) {
super(props);
this.state = {
documents: Array<any>(),
media: Array<any>(),
onItemClick: undefined,
};
}
public componentWillMount() {
void this.getMediaGalleryProps().then(({ documents, media, onItemClick }) => {
this.setState({
documents,
media,
onItemClick,
});
});
}
public componentDidUpdate() {
const mediaScanInterval = 1000;
setTimeout(() => {
void this.getMediaGalleryProps().then(({ documents, media, onItemClick }) => {
const { documents: oldDocs, media: oldMedias } = this.state;
if (oldDocs.length !== documents.length || oldMedias.length !== media.length) {
this.setState({
documents,
media,
onItemClick,
});
}
});
}, mediaScanInterval);
}
public async getMediaGalleryProps(): Promise<{
documents: Array<any>;
media: Array<any>;
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 conversationId = this.props.id;
const rawMedia = await getMessagesWithVisualMediaAttachments(conversationId, {
limit: Constants.CONVERSATION.DEFAULT_MEDIA_FETCH_COUNT,
});
const rawDocuments = await getMessagesWithFileAttachments(conversationId, {
limit: Constants.CONVERSATION.DEFAULT_DOCUMENTS_FETCH_COUNT,
});
// First we upgrade these messages to ensure that they have thumbnails
const max = rawMedia.length;
for (let i = 0; i < max; i += 1) {
const message = rawMedia[i];
const { schemaVersion } = message;
if (schemaVersion < message.VERSION_NEEDED_FOR_DISPLAY) {
// Yep, we really do want to wait for each of these
// eslint-disable-next-line no-await-in-loop
rawMedia[i] = await window.Signal.Migrations.upgradeMessageSchema(message);
// eslint-disable-next-line no-await-in-loop
await rawMedia[i].commit();
}
}
const media = _.flatten(
rawMedia.map((message: { attachments: any }) => {
const { attachments } = message;
return (attachments || [])
.filter(
(attachment: { thumbnail: any; pending: any; error: any }) =>
attachment.thumbnail && !attachment.pending && !attachment.error
)
.map((attachment: { path?: any; contentType?: any; thumbnail?: any }, index: any) => {
const { thumbnail } = attachment;
return {
objectURL: window.Signal.Migrations.getAbsoluteAttachmentPath(attachment.path),
thumbnailObjectUrl: thumbnail
? window.Signal.Migrations.getAbsoluteAttachmentPath(thumbnail.path)
: null,
contentType: attachment.contentType,
index,
attachment,
message,
};
});
})
);
// Unlike visual media, only one non-image attachment is supported
const documents = rawDocuments.map((message: { attachments: Array<any> }) => {
// this is to not fail if the attachment is invalid (could be a Long Attachment type which is not supported)
if (!message.attachments?.length) {
// window?.log?.info(
// 'Got a message with an empty list of attachment. Skipping...'
// );
return null;
}
const attachment = message.attachments[0];
return {
contentType: attachment.contentType,
index: 0,
attachment,
message,
};
});
const saveAttachment = async ({ attachment, message }: any = {}) => {
const timestamp = message.received_at as number | undefined;
attachment.url = await getDecryptedMediaUrl(attachment.url, attachment.contentType);
save({
attachment,
document,
getAbsolutePath: window.Signal.Migrations.getAbsoluteAttachmentPath,
timestamp,
});
await sendDataExtractionNotification(this.props.id, message?.source, timestamp);
};
const onItemClick = ({ message, attachment, type }: any) => {
switch (type) {
case 'documents': {
void saveAttachment({ message, attachment });
break;
}
case 'media': {
// don't set the messageTimestamp when we are the sender, so we don't trigger a notification when
// we save the same attachment we sent ourself to another user
const messageTimestamp =
message.source !== UserUtils.getOurPubKeyStrFromCache()
? message.sent_at || message.received_at
: undefined;
const lightBoxOptions = {
media,
attachment,
messageTimestamp,
} as LightBoxOptions;
this.onShowLightBox(lightBoxOptions);
break;
}
default:
throw new TypeError(`Unknown attachment type: '${type}'`);
}
};
return {
media,
documents: _.compact(documents), // remove null
onItemClick,
};
}
public onShowLightBox(lightboxOptions: LightBoxOptions) {
this.props.onShowLightBox(lightboxOptions);
}
// tslint:disable-next-line: cyclomatic-complexity
public render() {
const {
memberCount,
name,
timerOptions,
onLeaveGroup,
onDeleteContact,
isKickedFromGroup,
left,
isPublic,
isAdmin,
isBlocked,
isGroup,
} = this.props;
const { documents, media, onItemClick } = this.state;
const showMemberCount = !!(memberCount && memberCount > 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 disappearingMessagesOptions = timerOptions.map(option => {
return {
content: option.name,
onClick: () => {
this.props.onSetDisappearingMessages(option.value);
},
};
});
const showUpdateGroupNameButton = isAdmin && !commonNoShow;
const showAddRemoveModeratorsButton = isAdmin && !commonNoShow && isPublic;
const showUpdateGroupMembersButton = !isPublic && isGroup && !commonNoShow;
return (
<div className="group-settings">
{this.renderHeader()}
<h2>{name}</h2>
{showMemberCount && (
<>
<SpacerLG />
<div role="button" className="subtle">
{window.i18n('members', memberCount)}
</div>
<SpacerLG />
</>
)}
<input className="description" placeholder={window.i18n('description')} />
{showUpdateGroupNameButton && (
<div className="group-settings-item" role="button" onClick={this.props.onUpdateGroupName}>
{isPublic ? window.i18n('editGroup') : window.i18n('editGroupName')}
</div>
)}
{showAddRemoveModeratorsButton && (
<>
<div className="group-settings-item" role="button" onClick={this.props.onAddModerators}>
{window.i18n('addModerators')}
</div>
<div
className="group-settings-item"
role="button"
onClick={this.props.onRemoveModerators}
>
{window.i18n('removeModerators')}
</div>
</>
)}
{showUpdateGroupMembersButton && (
<div
className="group-settings-item"
role="button"
onClick={this.props.onUpdateGroupMembers}
>
{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={isPublic ? onDeleteContact : onLeaveGroup}
/>
)}
</div>
);
}
private renderHeader() {
const {
memberAvatars,
id,
onGoBack,
onInviteContacts,
avatarPath,
isAdmin,
isPublic,
isKickedFromGroup,
isBlocked,
name,
profileName,
phoneNumber,
left,
} = this.props;
const showInviteContacts = (isPublic || isAdmin) && !isKickedFromGroup && !isBlocked && !left;
const userName = name || profileName || phoneNumber;
return (
<div className="group-settings-header">
<SessionIconButton
iconType={SessionIconType.Chevron}
iconSize={SessionIconSize.Medium}
iconRotation={270}
onClick={onGoBack}
theme={this.props.theme}
/>
<Avatar
avatarPath={avatarPath}
name={userName}
size={AvatarSize.XL}
memberAvatars={memberAvatars}
pubkey={id}
/>
<div className="invite-friends-container">
{showInviteContacts && (
<SessionIconButton
iconType={SessionIconType.AddUser}
iconSize={SessionIconSize.Medium}
onClick={onInviteContacts}
theme={this.props.theme}
/>
)}
</div>
</div>
);
}
}
export const SessionRightPanelWithDetails = usingClosedConversationDetails(
withTheme(SessionRightPanel)
);