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/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx

495 lines
16 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 { compact, flatten, isEqual } from 'lodash';
import { SessionDataTestId, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import useInterval from 'react-use/lib/useInterval';
import styled from 'styled-components';
import { Data } from '../../../../data/data';
import { SessionIconButton } from '../../../icon';
import {
useConversationUsername,
useDisappearingMessageSettingText,
useIsClosedGroup,
useIsGroupDestroyed,
useIsKickedFromGroup,
useIsPublic,
} from '../../../../hooks/useParamSelector';
import { useIsRightPanelShowing } from '../../../../hooks/useUI';
import {
showAddModeratorsByConvoId,
showDeleteGroupByConvoId,
showInviteContactByConvoId,
showLeaveGroupByConvoId,
showRemoveModeratorsByConvoId,
showUpdateGroupMembersByConvoId,
showUpdateGroupNameByConvoId,
} from '../../../../interactions/conversationInteractions';
import { Constants } from '../../../../session';
import { PubKey } from '../../../../session/types';
import { hasClosedGroupV2QAButtons } from '../../../../shared/env_vars';
import { closeRightPanel } from '../../../../state/ducks/conversations';
import { groupInfoActions } from '../../../../state/ducks/metaGroups';
import { resetRightOverlayMode, setRightOverlayMode } from '../../../../state/ducks/section';
import {
useSelectedConversationKey,
useSelectedDisplayNameInProfile,
useSelectedIsActive,
useSelectedIsBlocked,
useSelectedIsGroupDestroyed,
useSelectedIsGroupOrCommunity,
useSelectedIsGroupV2,
useSelectedIsKickedFromGroup,
useSelectedIsPublic,
useSelectedSubscriberCount,
useSelectedWeAreAdmin,
} from '../../../../state/selectors/selectedConversation';
import { AttachmentTypeWithPath } from '../../../../types/Attachment';
import { getAbsoluteAttachmentPath } from '../../../../types/MessageAttachment';
import { Avatar, AvatarSize } from '../../../avatar/Avatar';
import { Flex } from '../../../basic/Flex';
import { SpacerLG, SpacerMD, SpacerXL } from '../../../basic/Text';
import { PanelButtonGroup, PanelIconButton } from '../../../buttons';
import { MediaItemType } from '../../../lightbox/LightboxGallery';
import { MediaGallery } from '../../media-gallery/MediaGallery';
import { Header, StyledScrollContainer } from './components';
import { Localizer } from '../../../basic/Localizer';
import {
showDeleteGroupItem,
showLeaveGroupItem,
} from '../../../menu/items/LeaveAndDeleteGroup/guard';
import { useIsMessageRequestOverlayShown } from '../../../../state/selectors/section';
import { showLeaveCommunityItem } from '../../../menu/items/LeaveCommunity/guard';
async function getMediaGalleryProps(conversationId: string): Promise<{
documents: Array<MediaItemType>;
media: Array<MediaItemType>;
}> {
// 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 Data.getMessagesWithVisualMediaAttachments(
conversationId,
Constants.CONVERSATION.DEFAULT_MEDIA_FETCH_COUNT
);
const rawDocuments = await Data.getMessagesWithFileAttachments(
conversationId,
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: getAbsoluteAttachmentPath(attachment.path),
thumbnailObjectUrl: thumbnail ? getAbsoluteAttachmentPath(thumbnail.path) : undefined,
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 selectedConvoKey = useSelectedConversationKey();
const displayNameInProfile = useSelectedDisplayNameInProfile();
const dispatch = useDispatch();
const isBlocked = useSelectedIsBlocked();
const isKickedFromGroup = useSelectedIsKickedFromGroup();
const isGroupDestroyed = useSelectedIsGroupDestroyed();
const isGroup = useSelectedIsGroupOrCommunity();
const isGroupV2 = useSelectedIsGroupV2();
const isPublic = useSelectedIsPublic();
const subscriberCount = useSelectedSubscriberCount();
const weAreAdmin = useSelectedWeAreAdmin();
if (!selectedConvoKey) {
return null;
}
const showInviteLegacyGroup =
!isPublic && !isGroupV2 && isGroup && !isKickedFromGroup && !isBlocked;
const showInviteGroupV2 =
isGroupV2 && !isKickedFromGroup && !isBlocked && weAreAdmin && !isGroupDestroyed;
const showInviteContacts = isPublic || showInviteLegacyGroup || showInviteGroupV2;
const showMemberCount = !!(subscriberCount && subscriberCount > 0);
return (
<Header
backButtonDirection="right"
backButtonOnClick={() => {
dispatch(closeRightPanel());
dispatch(resetRightOverlayMode());
}}
hideCloseButton={true}
>
<Flex
container={true}
justifyContent={'center'}
alignItems={'center'}
width={'100%'}
style={{ position: 'relative' }}
>
<Avatar size={AvatarSize.XL} pubkey={selectedConvoKey} />
{showInviteContacts && (
<SessionIconButton
iconType="addUser"
iconSize="medium"
onClick={() => {
if (selectedConvoKey) {
showInviteContactByConvoId(selectedConvoKey);
}
}}
style={{ position: 'absolute', right: '0px', top: '4px' }}
dataTestId="add-user-button"
/>
)}
</Flex>
<StyledName data-testid="right-panel-group-name">{displayNameInProfile}</StyledName>
{showMemberCount && (
<Flex container={true} flexDirection={'column'}>
<div role="button" className="subtle">
<Localizer token="members" args={{ count: subscriberCount }} />
</div>
<SpacerMD />
</Flex>
)}
</Header>
);
};
const StyledName = styled.h4`
padding-inline: var(--margins-md);
font-size: var(--font-size-md);
`;
const LeaveCommunityPanelButton = () => {
const selectedConvoKey = useSelectedConversationKey();
const selectedUsername = useConversationUsername(selectedConvoKey) || selectedConvoKey;
const isPublic = useIsPublic(selectedConvoKey);
const showItem = showLeaveCommunityItem({ isPublic });
if (!selectedConvoKey || !showItem) {
return null;
}
return (
<PanelIconButton
text={window.i18n('communityLeave')}
dataTestId="leave-group-button"
onClick={() => void showLeaveGroupByConvoId(selectedConvoKey, selectedUsername)}
color={'var(--danger-color)'}
iconType={'delete'}
/>
);
};
const DeleteGroupPanelButton = () => {
const convoId = useSelectedConversationKey();
const isGroup = useIsClosedGroup(convoId);
const isMessageRequestShown = useIsMessageRequestOverlayShown();
const isKickedFromGroup = useIsKickedFromGroup(convoId) || false;
const selectedUsername = useConversationUsername(convoId) || convoId;
const isPublic = useIsPublic(convoId);
const isGroupDestroyed = useIsGroupDestroyed(convoId);
const showItem = showDeleteGroupItem({
isGroup,
isKickedFromGroup,
isMessageRequestShown,
isPublic,
isGroupDestroyed,
});
if (!showItem || !convoId) {
return null;
}
const token = PubKey.is03Pubkey(convoId) ? 'groupDelete' : 'conversationsDelete';
return (
<PanelIconButton
text={window.i18n(token)}
dataTestId="leave-group-button"
onClick={() => void showDeleteGroupByConvoId(convoId, selectedUsername)}
color={'var(--danger-color)'}
iconType={'delete'}
/>
);
};
const LeaveGroupPanelButton = () => {
const selectedConvoKey = useSelectedConversationKey();
const isGroup = useIsClosedGroup(selectedConvoKey);
const username = useConversationUsername(selectedConvoKey) || selectedConvoKey;
const isMessageRequestShown = useIsMessageRequestOverlayShown();
const isKickedFromGroup = useIsKickedFromGroup(selectedConvoKey) || false;
const isPublic = useIsPublic(selectedConvoKey);
const isGroupDestroyed = useIsGroupDestroyed(selectedConvoKey);
const showItem = showLeaveGroupItem({
isGroup,
isKickedFromGroup,
isMessageRequestShown,
isPublic,
isGroupDestroyed,
});
if (!selectedConvoKey || !showItem) {
return null;
}
return (
<PanelIconButton
text={window.i18n('groupLeave')}
dataTestId="leave-group-button"
onClick={() => void showLeaveGroupByConvoId(selectedConvoKey, username)}
color={'var(--danger-color)'}
iconType={'delete'}
/>
);
};
export const OverlayRightPanelSettings = () => {
const [documents, setDocuments] = useState<Array<MediaItemType>>([]);
const [media, setMedia] = useState<Array<MediaItemType>>([]);
const selectedConvoKey = useSelectedConversationKey();
const isShowing = useIsRightPanelShowing();
const dispatch = useDispatch();
const isActive = useSelectedIsActive();
const isBlocked = useSelectedIsBlocked();
const isKickedFromGroup = useSelectedIsKickedFromGroup();
const isGroup = useSelectedIsGroupOrCommunity();
const isGroupV2 = useSelectedIsGroupV2();
const isPublic = useSelectedIsPublic();
const weAreAdmin = useSelectedWeAreAdmin();
const disappearingMessagesSubtitle = useDisappearingMessageSettingText({
convoId: selectedConvoKey,
});
useEffect(() => {
let isCancelled = false;
const loadDocumentsOrMedia = async () => {
try {
if (isShowing && selectedConvoKey) {
const results = await getMediaGalleryProps(selectedConvoKey);
if (!isCancelled) {
if (!isEqual(documents, results.documents)) {
setDocuments(results.documents);
}
if (!isEqual(media, results.media)) {
setMedia(results.media);
}
}
}
} catch (error) {
if (!isCancelled) {
window.log.debug(`OverlayRightPanelSettings loadDocumentsOrMedia: ${error}`);
}
}
};
void loadDocumentsOrMedia();
return () => {
isCancelled = true;
};
}, [documents, isShowing, media, selectedConvoKey]);
useInterval(async () => {
if (isShowing && selectedConvoKey) {
const results = await getMediaGalleryProps(selectedConvoKey);
if (results.documents.length !== documents.length || results.media.length !== media.length) {
setDocuments(results.documents);
setMedia(results.media);
}
}
}, 10000);
if (!selectedConvoKey) {
return null;
}
const commonNoShow = isKickedFromGroup || isBlocked || !isActive;
const hasDisappearingMessages = !isPublic && !commonNoShow;
const showUpdateGroupNameButton = isGroup && weAreAdmin && !commonNoShow; // legacy groups non-admin cannot change groupname anymore
const showAddRemoveModeratorsButton = weAreAdmin && !commonNoShow && isPublic;
const showUpdateGroupMembersButton = !isPublic && isGroup && !commonNoShow;
return (
<StyledScrollContainer>
<Flex container={true} flexDirection={'column'} alignItems={'center'}>
<HeaderItem />
<PanelButtonGroup style={{ margin: '0 var(--margins-lg)' }}>
{showUpdateGroupNameButton && (
<PanelIconButton
iconType={'groupMembers'}
text={window.i18n('groupEdit')}
onClick={() => {
void showUpdateGroupNameByConvoId(selectedConvoKey);
}}
dataTestId="edit-group-name"
/>
)}
{hasClosedGroupV2QAButtons() && isGroupV2 ? (
<>
<PanelIconButton
iconType={'group'}
text={'trigger avatar message'}
onClick={() => {
if (!PubKey.is03Pubkey(selectedConvoKey)) {
throw new Error('triggerFakeAvatarUpdate needs a 03 pubkey');
}
window.inboxStore?.dispatch(
groupInfoActions.triggerFakeAvatarUpdate({ groupPk: selectedConvoKey }) as any
);
}}
dataTestId={'' as SessionDataTestId}
/>
<PanelIconButton
iconType={'group'}
text={'trigger delete message before now'}
onClick={() => {
if (!PubKey.is03Pubkey(selectedConvoKey)) {
throw new Error('We need a 03 pubkey');
}
window.inboxStore?.dispatch(
groupInfoActions.triggerFakeDeleteMsgBeforeNow({
groupPk: selectedConvoKey,
messagesWithAttachmentsOnly: false,
}) as any
);
}}
dataTestId={'' as SessionDataTestId}
/>
<PanelIconButton
iconType={'group'}
text={'delete message with attachments before now'}
onClick={() => {
if (!PubKey.is03Pubkey(selectedConvoKey)) {
throw new Error('We need a 03 pubkey');
}
window.inboxStore?.dispatch(
groupInfoActions.triggerFakeDeleteMsgBeforeNow({
groupPk: selectedConvoKey,
messagesWithAttachmentsOnly: true,
}) as any
);
}}
dataTestId={'' as SessionDataTestId}
/>
</>
) : null}
{showAddRemoveModeratorsButton && (
<>
<PanelIconButton
iconType={'addModerator'}
text={window.i18n('adminPromote')}
onClick={() => {
showAddModeratorsByConvoId(selectedConvoKey);
}}
dataTestId="add-moderators"
/>
<PanelIconButton
iconType={'deleteModerator'}
text={window.i18n('adminRemove')}
onClick={() => {
showRemoveModeratorsByConvoId(selectedConvoKey);
}}
dataTestId="remove-moderators"
/>
</>
)}
{showUpdateGroupMembersButton && (
<PanelIconButton
iconType={'groupMembers'}
text={window.i18n('groupMembers')}
onClick={() => {
void showUpdateGroupMembersByConvoId(selectedConvoKey);
}}
dataTestId="group-members"
/>
)}
{hasDisappearingMessages && (
<PanelIconButton
iconType={'timer50'}
text={window.i18n('disappearingMessages')}
subtitle={disappearingMessagesSubtitle}
dataTestId="disappearing-messages"
onClick={() => {
dispatch(setRightOverlayMode({ type: 'disappearing_messages', params: null }));
}}
/>
)}
<MediaGallery documents={documents} media={media} />
{isGroup && (
<>
<LeaveGroupPanelButton />
<DeleteGroupPanelButton />
<LeaveCommunityPanelButton />
</>
)}
</PanelButtonGroup>
<SpacerLG />
<SpacerXL />
</Flex>
</StyledScrollContainer>
);
};