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

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(
3 years ago
conversationId,
Constants.CONVERSATION.DEFAULT_MEDIA_FETCH_COUNT
);
const rawDocuments = await Data.getMessagesWithFileAttachments(
3 years ago
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;
5 years ago
return {
contentType: attachment.contentType,
index: 0,
attachment,
messageTimestamp: timestamp || serverTimestamp || received_at || 0,
messageSender: source,
messageId: id,
5 years ago
};
});
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);
5 years ago
if (!selectedConvoKey) {
return null;
5 years ago
}
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>
);
};