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.
		
		
		
		
		
			
		
			
				
	
	
		
			417 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			417 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
| import React from 'react';
 | |
| 
 | |
| import { Avatar, AvatarSize } from '../avatar/Avatar';
 | |
| 
 | |
| import { contextMenu } from 'react-contexify';
 | |
| import styled from 'styled-components';
 | |
| import { ConversationNotificationSettingType } from '../../models/conversationAttributes';
 | |
| import {
 | |
|   getConversationHeaderTitleProps,
 | |
|   getCurrentNotificationSettingText,
 | |
|   getIsSelectedActive,
 | |
|   getIsSelectedBlocked,
 | |
|   getIsSelectedNoteToSelf,
 | |
|   getIsSelectedPrivate,
 | |
|   getSelectedConversationIsPublic,
 | |
|   getSelectedConversationKey,
 | |
|   getSelectedMessageIds,
 | |
|   isMessageDetailView,
 | |
|   isMessageSelectionMode,
 | |
|   isRightPanelShowing,
 | |
| } from '../../state/selectors/conversations';
 | |
| import { useDispatch, useSelector } from 'react-redux';
 | |
| 
 | |
| import {
 | |
|   deleteMessagesById,
 | |
|   deleteMessagesByIdForEveryone,
 | |
| } from '../../interactions/conversations/unsendingInteractions';
 | |
| import {
 | |
|   closeMessageDetailsView,
 | |
|   closeRightPanel,
 | |
|   openRightPanel,
 | |
|   resetSelectedMessageIds,
 | |
| } from '../../state/ducks/conversations';
 | |
| import { callRecipient } from '../../interactions/conversationInteractions';
 | |
| import { getHasIncomingCall, getHasOngoingCall } from '../../state/selectors/call';
 | |
| import {
 | |
|   useConversationUsername,
 | |
|   useExpireTimer,
 | |
|   useIsKickedFromGroup,
 | |
|   useIsRequest,
 | |
| } from '../../hooks/useParamSelector';
 | |
| import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
 | |
| import { SessionIconButton } from '../icon';
 | |
| import { ConversationHeaderMenu } from '../menu/ConversationHeaderMenu';
 | |
| import { Flex } from '../basic/Flex';
 | |
| import { ExpirationTimerOptions } from '../../util/expiringMessages';
 | |
| 
 | |
| export interface TimerOption {
 | |
|   name: string;
 | |
|   value: number;
 | |
| }
 | |
| 
 | |
| export type ConversationHeaderProps = {
 | |
|   conversationKey: string;
 | |
|   name?: string;
 | |
| 
 | |
|   profileName?: string;
 | |
|   avatarPath: string | null;
 | |
| 
 | |
|   isMe: boolean;
 | |
|   isGroup: boolean;
 | |
|   isPrivate: boolean;
 | |
|   isPublic: boolean;
 | |
|   weAreAdmin: boolean;
 | |
| 
 | |
|   // We might not always have the full list of members,
 | |
|   // e.g. for open groups where we could have thousands
 | |
|   // of members. We'll keep this for now (for closed chats)
 | |
|   members: Array<any>;
 | |
| 
 | |
|   // not equal members.length (see above)
 | |
|   subscriberCount?: number;
 | |
| 
 | |
|   expirationSettingName?: string;
 | |
|   currentNotificationSetting: ConversationNotificationSettingType;
 | |
|   hasNickname: boolean;
 | |
| 
 | |
|   isBlocked: boolean;
 | |
| 
 | |
|   isKickedFromGroup: boolean;
 | |
|   left: boolean;
 | |
| };
 | |
| 
 | |
| const SelectionOverlay = () => {
 | |
|   const selectedMessageIds = useSelector(getSelectedMessageIds);
 | |
|   const selectedConversationKey = useSelector(getSelectedConversationKey);
 | |
|   const isPublic = useSelector(getSelectedConversationIsPublic);
 | |
|   const dispatch = useDispatch();
 | |
| 
 | |
|   const { i18n } = window;
 | |
| 
 | |
|   function onCloseOverlay() {
 | |
|     dispatch(resetSelectedMessageIds());
 | |
|   }
 | |
| 
 | |
|   function onDeleteSelectedMessages() {
 | |
|     if (selectedConversationKey) {
 | |
|       void deleteMessagesById(selectedMessageIds, selectedConversationKey);
 | |
|     }
 | |
|   }
 | |
|   function onDeleteSelectedMessagesForEveryone() {
 | |
|     if (selectedConversationKey) {
 | |
|       void deleteMessagesByIdForEveryone(selectedMessageIds, selectedConversationKey);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   const isOnlyServerDeletable = isPublic;
 | |
|   const deleteMessageButtonText = i18n('delete');
 | |
|   const deleteForEveryoneMessageButtonText = i18n('deleteForEveryone');
 | |
| 
 | |
|   return (
 | |
|     <div className="message-selection-overlay">
 | |
|       <div className="close-button">
 | |
|         <SessionIconButton iconType="exit" iconSize="medium" onClick={onCloseOverlay} />
 | |
|       </div>
 | |
| 
 | |
|       <div className="button-group">
 | |
|         {!isOnlyServerDeletable && (
 | |
|           <SessionButton
 | |
|             buttonType={SessionButtonType.Default}
 | |
|             buttonColor={SessionButtonColor.Danger}
 | |
|             text={deleteMessageButtonText}
 | |
|             onClick={onDeleteSelectedMessages}
 | |
|           />
 | |
|         )}
 | |
|         <SessionButton
 | |
|           buttonType={SessionButtonType.Default}
 | |
|           buttonColor={SessionButtonColor.Danger}
 | |
|           text={deleteForEveryoneMessageButtonText}
 | |
|           onClick={onDeleteSelectedMessagesForEveryone}
 | |
|         />
 | |
|       </div>
 | |
|     </div>
 | |
|   );
 | |
| };
 | |
| 
 | |
| const TripleDotsMenu = (props: { triggerId: string; showBackButton: boolean }) => {
 | |
|   const { showBackButton } = props;
 | |
|   if (showBackButton) {
 | |
|     return null;
 | |
|   }
 | |
|   return (
 | |
|     <TripleDotContainer
 | |
|       role="button"
 | |
|       onClick={(e: any) => {
 | |
|         contextMenu.show({
 | |
|           id: props.triggerId,
 | |
|           event: e,
 | |
|         });
 | |
|       }}
 | |
|       data-testid="three-dots-conversation-options"
 | |
|     >
 | |
|       <SessionIconButton iconType="ellipses" iconSize="medium" />
 | |
|     </TripleDotContainer>
 | |
|   );
 | |
| };
 | |
| 
 | |
| const TripleDotContainer = styled.div`
 | |
|   user-select: none;
 | |
|   flex-grow: 0;
 | |
|   flex-shrink: 0;
 | |
| `;
 | |
| 
 | |
| const ExpirationLength = (props: { expirationSettingName?: string }) => {
 | |
|   const { expirationSettingName } = props;
 | |
| 
 | |
|   if (!expirationSettingName) {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   return (
 | |
|     <div className="module-conversation-header__expiration">
 | |
|       <div className="module-conversation-header__expiration__clock-icon" />
 | |
|       <div
 | |
|         className="module-conversation-header__expiration__setting"
 | |
|         data-testid="disappearing-messages-indicator"
 | |
|       >
 | |
|         {expirationSettingName}
 | |
|       </div>
 | |
|     </div>
 | |
|   );
 | |
| };
 | |
| 
 | |
| const AvatarHeader = (props: {
 | |
|   pubkey: string;
 | |
|   showBackButton: boolean;
 | |
|   onAvatarClick?: (pubkey: string) => void;
 | |
| }) => {
 | |
|   const { pubkey, onAvatarClick, showBackButton } = props;
 | |
| 
 | |
|   return (
 | |
|     <span className="module-conversation-header__avatar">
 | |
|       <Avatar
 | |
|         size={AvatarSize.S}
 | |
|         onAvatarClick={() => {
 | |
|           // do not allow right panel to appear if another button is shown on the SessionConversation
 | |
|           if (onAvatarClick && !showBackButton) {
 | |
|             onAvatarClick(pubkey);
 | |
|           }
 | |
|         }}
 | |
|         pubkey={pubkey}
 | |
|         dataTestId="conversation-options-avatar"
 | |
|       />
 | |
|     </span>
 | |
|   );
 | |
| };
 | |
| 
 | |
| const BackButton = (props: { onGoBack: () => void; showBackButton: boolean }) => {
 | |
|   const { onGoBack, showBackButton } = props;
 | |
|   if (!showBackButton) {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   return (
 | |
|     <SessionIconButton
 | |
|       iconType="chevron"
 | |
|       iconSize="large"
 | |
|       iconRotation={90}
 | |
|       onClick={onGoBack}
 | |
|       dataTestId="back-button-message-details"
 | |
|     />
 | |
|   );
 | |
| };
 | |
| 
 | |
| const CallButton = () => {
 | |
|   const isPrivate = useSelector(getIsSelectedPrivate);
 | |
|   const isBlocked = useSelector(getIsSelectedBlocked);
 | |
|   const activeAt = useSelector(getIsSelectedActive);
 | |
|   const isMe = useSelector(getIsSelectedNoteToSelf);
 | |
|   const selectedConvoKey = useSelector(getSelectedConversationKey);
 | |
| 
 | |
|   const hasIncomingCall = useSelector(getHasIncomingCall);
 | |
|   const hasOngoingCall = useSelector(getHasOngoingCall);
 | |
|   const canCall = !(hasIncomingCall || hasOngoingCall);
 | |
| 
 | |
|   const isRequest = useIsRequest(selectedConvoKey);
 | |
| 
 | |
|   if (!isPrivate || isMe || !selectedConvoKey || isBlocked || !activeAt || isRequest) {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   return (
 | |
|     <SessionIconButton
 | |
|       iconType="phone"
 | |
|       iconSize="large"
 | |
|       iconPadding="2px"
 | |
|       margin="0 10px 0 0"
 | |
|       onClick={() => {
 | |
|         void callRecipient(selectedConvoKey, canCall);
 | |
|       }}
 | |
|     />
 | |
|   );
 | |
| };
 | |
| 
 | |
| export const StyledSubtitleContainer = styled.div`
 | |
|   display: flex;
 | |
|   flex-direction: row;
 | |
|   align-items: center;
 | |
|   justify-content: space-between;
 | |
| 
 | |
|   span:last-child {
 | |
|     margin-bottom: 0;
 | |
|   }
 | |
| `;
 | |
| 
 | |
| export type ConversationHeaderTitleProps = {
 | |
|   conversationKey: string;
 | |
|   isMe: boolean;
 | |
|   isGroup: boolean;
 | |
|   isPublic: boolean;
 | |
|   members: Array<any>;
 | |
|   subscriberCount?: number;
 | |
|   isKickedFromGroup: boolean;
 | |
|   currentNotificationSetting?: ConversationNotificationSettingType;
 | |
| };
 | |
| 
 | |
| const ConversationHeaderTitle = () => {
 | |
|   const headerTitleProps = useSelector(getConversationHeaderTitleProps);
 | |
|   const notificationSetting = useSelector(getCurrentNotificationSettingText);
 | |
|   const isRightPanelOn = useSelector(isRightPanelShowing);
 | |
| 
 | |
|   const convoName = useConversationUsername(headerTitleProps?.conversationKey);
 | |
|   const dispatch = useDispatch();
 | |
|   if (!headerTitleProps) {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   const { isGroup, isPublic, members, subscriberCount, isMe, isKickedFromGroup } = headerTitleProps;
 | |
| 
 | |
|   const { i18n } = window;
 | |
| 
 | |
|   if (isMe) {
 | |
|     return <div className="module-conversation-header__title">{i18n('noteToSelf')}</div>;
 | |
|   }
 | |
| 
 | |
|   let memberCount = 0;
 | |
|   if (isGroup) {
 | |
|     if (isPublic) {
 | |
|       memberCount = subscriberCount || 0;
 | |
|     } else {
 | |
|       memberCount = members.length;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   let memberCountText = '';
 | |
|   if (isGroup && memberCount > 0 && !isKickedFromGroup) {
 | |
|     const count = String(memberCount);
 | |
|     memberCountText = i18n('members', [count]);
 | |
|   }
 | |
| 
 | |
|   const notificationSubtitle = notificationSetting
 | |
|     ? window.i18n('notificationSubtitle', [notificationSetting])
 | |
|     : null;
 | |
|   const fullTextSubtitle = memberCountText
 | |
|     ? `${memberCountText} ● ${notificationSubtitle}`
 | |
|     : `${notificationSubtitle}`;
 | |
| 
 | |
|   return (
 | |
|     <div
 | |
|       className="module-conversation-header__title"
 | |
|       onClick={() => {
 | |
|         if (isRightPanelOn) {
 | |
|           dispatch(closeRightPanel());
 | |
|         } else {
 | |
|           dispatch(openRightPanel());
 | |
|         }
 | |
|       }}
 | |
|       role="button"
 | |
|     >
 | |
|       <span className="module-contact-name__profile-name" data-testid="header-conversation-name">
 | |
|         {convoName}
 | |
|       </span>
 | |
|       <StyledSubtitleContainer>
 | |
|         <ConversationHeaderSubtitle text={fullTextSubtitle} />
 | |
|       </StyledSubtitleContainer>
 | |
|     </div>
 | |
|   );
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * The subtitle beneath a conversation title when looking at a conversation screen.
 | |
|  * @param props props for subtitle. Text to be displayed
 | |
|  * @returns JSX Element of the subtitle of conversation header
 | |
|  */
 | |
| export const ConversationHeaderSubtitle = (props: { text?: string | null }): JSX.Element | null => {
 | |
|   const { text } = props;
 | |
|   if (!text) {
 | |
|     return null;
 | |
|   }
 | |
|   return <span className="module-conversation-header__title-text">{text}</span>;
 | |
| };
 | |
| 
 | |
| export const ConversationHeaderWithDetails = () => {
 | |
|   const isSelectionMode = useSelector(isMessageSelectionMode);
 | |
|   const isMessageDetailOpened = useSelector(isMessageDetailView);
 | |
|   const selectedConvoKey = useSelector(getSelectedConversationKey);
 | |
|   const dispatch = useDispatch();
 | |
| 
 | |
|   if (!selectedConvoKey) {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   const isKickedFromGroup = useIsKickedFromGroup(selectedConvoKey);
 | |
|   const expireTimerSetting = useExpireTimer(selectedConvoKey);
 | |
|   const expirationSettingName = expireTimerSetting
 | |
|     ? ExpirationTimerOptions.getName(expireTimerSetting || 0)
 | |
|     : undefined;
 | |
| 
 | |
|   const triggerId = 'conversation-header';
 | |
| 
 | |
|   return (
 | |
|     <div className="module-conversation-header">
 | |
|       <div className="conversation-header--items-wrapper">
 | |
|         <BackButton
 | |
|           onGoBack={() => {
 | |
|             dispatch(closeMessageDetailsView());
 | |
|           }}
 | |
|           showBackButton={isMessageDetailOpened}
 | |
|         />
 | |
|         <TripleDotsMenu triggerId={triggerId} showBackButton={isMessageDetailOpened} />
 | |
| 
 | |
|         <div className="module-conversation-header__title-container">
 | |
|           <div className="module-conversation-header__title-flex">
 | |
|             <ConversationHeaderTitle />
 | |
|           </div>
 | |
|         </div>
 | |
| 
 | |
|         {!isSelectionMode && (
 | |
|           <Flex
 | |
|             container={true}
 | |
|             flexDirection="row"
 | |
|             alignItems="center"
 | |
|             flexGrow={0}
 | |
|             flexShrink={0}
 | |
|           >
 | |
|             {!isKickedFromGroup && (
 | |
|               <ExpirationLength expirationSettingName={expirationSettingName} />
 | |
|             )}
 | |
|             <CallButton />
 | |
|             <AvatarHeader
 | |
|               onAvatarClick={() => {
 | |
|                 dispatch(openRightPanel());
 | |
|               }}
 | |
|               pubkey={selectedConvoKey}
 | |
|               showBackButton={isMessageDetailOpened}
 | |
|             />
 | |
|           </Flex>
 | |
|         )}
 | |
| 
 | |
|         <ConversationHeaderMenu triggerId={triggerId} />
 | |
|       </div>
 | |
| 
 | |
|       {isSelectionMode && <SelectionOverlay />}
 | |
|     </div>
 | |
|   );
 | |
| };
 |