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

398 lines
11 KiB
TypeScript

import React from 'react';
import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
import { Avatar } from '../../Avatar';
5 years ago
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';
5 years ago
import { Constants } from '../../../session';
import {
ConversationAvatar,
usingClosedConversationDetails,
} from '../usingClosedConversationDetails';
import { save } from '../../../types/Attachment';
import { DefaultTheme, withTheme } from 'styled-components';
interface Props {
5 years ago
id: string;
name?: string;
profileName?: string;
phoneNumber: string;
5 years ago
memberCount: number;
5 years ago
description: string;
5 years ago
avatarPath: string;
timerOptions: Array<TimerOption>;
isPublic: boolean;
5 years ago
isAdmin: boolean;
amMod: boolean;
isKickedFromGroup: boolean;
left: boolean;
5 years ago
isBlocked: boolean;
isGroup: boolean;
memberAvatars?: Array<ConversationAvatar>; // this is added by usingClosedConversationDetails
5 years ago
onGoBack: () => void;
onInviteContacts: () => void;
5 years ago
onLeaveGroup: () => void;
onUpdateGroupName: () => void;
onUpdateGroupMembers: () => void;
onShowLightBox: (options: any) => void;
onSetDisappearingMessages: (seconds: number) => void;
theme: DefaultTheme;
}
interface State {
documents: Array<any>;
media: Array<any>;
onItemClick: any;
}
class SessionRightPanel extends React.Component<Props, State> {
5 years ago
public constructor(props: Props) {
super(props);
this.state = {
documents: Array<any>(),
media: Array<any>(),
onItemClick: undefined,
};
}
public componentWillMount() {
this.getMediaGalleryProps()
.then(({ documents, media, onItemClick }) => {
this.setState({
documents,
media,
onItemClick,
});
})
.ignore();
}
public componentDidUpdate() {
const mediaScanInterval = 1000;
setTimeout(() => {
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,
});
}
})
.ignore();
}, mediaScanInterval);
}
public async getMediaGalleryProps(): Promise<{
documents: Array<any>;
media: Array<any>;
onItemClick: any;
}> {
5 years ago
// 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 window.Signal.Data.getMessagesWithVisualMediaAttachments(
conversationId,
{
5 years ago
limit: Constants.CONVERSATION.DEFAULT_MEDIA_FETCH_COUNT,
5 years ago
MessageCollection: window.Whisper.MessageCollection,
}
);
const rawDocuments = await window.Signal.Data.getMessagesWithFileAttachments(
conversationId,
{
5 years ago
limit: Constants.CONVERSATION.DEFAULT_DOCUMENTS_FETCH_COUNT,
5 years ago
MessageCollection: window.Whisper.MessageCollection,
}
);
// 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
);
5 years ago
// eslint-disable-next-line no-await-in-loop
await rawMedia[i].commit();
5 years ago
}
}
5 years ago
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,
};
}
5 years ago
);
})
);
5 years ago
// 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];
5 years ago
return {
contentType: attachment.contentType,
index: 0,
attachment,
message,
};
5 years ago
}
);
const saveAttachment = async ({ attachment, message }: any = {}) => {
const timestamp = message.received_at;
save({
5 years ago
attachment,
document,
getAbsolutePath: window.Signal.Migrations.getAbsoluteAttachmentPath,
timestamp,
});
};
const onItemClick = async ({ message, attachment, type }: any) => {
switch (type) {
case 'documents': {
5 years ago
saveAttachment({ message, attachment }).ignore();
break;
}
5 years ago
case 'media': {
const lightBoxOptions = {
media,
attachment,
message,
};
this.onShowLightBox(lightBoxOptions);
break;
}
5 years ago
default:
throw new TypeError(`Unknown attachment type: '${type}'`);
}
};
return {
5 years ago
media,
documents: _.compact(documents), // remove null
5 years ago
onItemClick,
5 years ago
};
}
public onShowLightBox(options: any) {
this.props.onShowLightBox(options);
}
// tslint:disable-next-line: cyclomatic-complexity
5 years ago
public render() {
const {
memberCount,
name,
timerOptions,
onLeaveGroup,
5 years ago
isKickedFromGroup,
left,
isPublic,
isAdmin,
amMod,
5 years ago
isBlocked,
isGroup,
} = this.props;
5 years ago
const { documents, media, onItemClick } = this.state;
const showMemberCount = !!(memberCount && memberCount > 0);
const commonNoShow = isKickedFromGroup || left || isBlocked;
const hasDisappearingMessages = !isPublic && !commonNoShow;
5 years ago
const leaveGroupString = isPublic
? window.i18n('leaveGroup')
: isKickedFromGroup
? window.i18n('youGotKickedFromGroup')
: left
? window.i18n('youLeftTheGroup')
: window.i18n('leaveGroup');
5 years ago
const disappearingMessagesOptions = timerOptions.map(option => {
return {
content: option.name,
onClick: () => {
this.props.onSetDisappearingMessages(option.value);
},
};
});
const showUpdateGroupNameButton =
isPublic && !commonNoShow
? amMod && !commonNoShow
: isAdmin && !commonNoShow;
const showUpdateGroupMembersButton = !isPublic && !commonNoShow && isAdmin;
5 years ago
return (
<div className="group-settings">
{this.renderHeader()}
<h2>{name}</h2>
{showMemberCount && (
<>
<div className="spacer-lg" />
<div role="button" className="subtle">
{window.i18n('members', memberCount)}
</div>
<div className="spacer-lg" />
</>
5 years ago
)}
<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>
)}
{showUpdateGroupMembersButton && (
<div
className="group-settings-item"
role="button"
onClick={this.props.onUpdateGroupMembers}
>
{window.i18n('groupMembers')}
</div>
)}
{hasDisappearingMessages && (
<SessionDropdown
label={window.i18n('disappearingMessages')}
options={disappearingMessagesOptions}
/>
)}
5 years ago
<MediaGallery
documents={documents}
media={media}
onItemClick={onItemClick}
/>
{isGroup && (
<SessionButton
text={leaveGroupString}
buttonColor={SessionButtonColor.Danger}
disabled={isKickedFromGroup || left}
buttonType={SessionButtonType.SquareOutline}
onClick={onLeaveGroup}
/>
)}
5 years ago
</div>
);
}
private renderHeader() {
const {
memberAvatars,
id,
onGoBack,
onInviteContacts,
avatarPath,
isAdmin,
isPublic,
isKickedFromGroup,
5 years ago
isBlocked,
name,
profileName,
phoneNumber,
left,
} = this.props;
5 years ago
const showInviteContacts =
(isPublic || isAdmin) && !isKickedFromGroup && !isBlocked && !left;
const userName = name || profileName || phoneNumber;
5 years ago
return (
<div className="group-settings-header">
<SessionIconButton
iconType={SessionIconType.Chevron}
iconSize={SessionIconSize.Medium}
iconRotation={270}
5 years ago
onClick={onGoBack}
theme={this.props.theme}
5 years ago
/>
<Avatar
avatarPath={avatarPath}
name={userName}
5 years ago
size={80}
memberAvatars={memberAvatars}
pubkey={id}
5 years ago
/>
<div className="invite-friends-container">
{showInviteContacts && (
<SessionIconButton
iconType={SessionIconType.AddUser}
iconSize={SessionIconSize.Medium}
onClick={onInviteContacts}
theme={this.props.theme}
/>
)}
</div>
5 years ago
</div>
);
}
}
export const SessionRightPanelWithDetails = usingClosedConversationDetails(
withTheme(SessionRightPanel)
);