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.
		
		
		
		
		
			
		
			
				
	
	
		
			384 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			384 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
| import { isEmpty, isEqual } from 'lodash';
 | |
| import React, { ReactElement, useEffect, useState } from 'react';
 | |
| import { useDispatch, useSelector } from 'react-redux';
 | |
| import styled from 'styled-components';
 | |
| import { Data } from '../../data/data';
 | |
| import { useMessageReactsPropsById, useWeAreModerator } from '../../hooks/useParamSelector';
 | |
| import { isUsAnySogsFromCache } from '../../session/apis/open_group_api/sogsv3/knownBlindedkeys';
 | |
| import { UserUtils } from '../../session/utils';
 | |
| import {
 | |
|   updateReactClearAllModal,
 | |
|   updateReactListModal,
 | |
|   updateUserDetailsModal,
 | |
| } from '../../state/ducks/modalDialog';
 | |
| import { getSelectedConversationIsPublic } from '../../state/selectors/conversations';
 | |
| import { SortedReactionList } from '../../types/Reaction';
 | |
| import { nativeEmojiData } from '../../util/emoji';
 | |
| import { Reactions } from '../../util/reactions';
 | |
| import { Avatar, AvatarSize } from '../avatar/Avatar';
 | |
| import { Flex } from '../basic/Flex';
 | |
| import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
 | |
| import { SessionHtmlRenderer } from '../basic/SessionHTMLRenderer';
 | |
| import { ContactName } from '../conversation/ContactName';
 | |
| import { MessageReactions } from '../conversation/message/message-content/MessageReactions';
 | |
| import { SessionIconButton } from '../icon';
 | |
| import { SessionWrapperModal } from '../SessionWrapperModal';
 | |
| 
 | |
| const StyledReactListContainer = styled(Flex)`
 | |
|   width: 376px;
 | |
| `;
 | |
| 
 | |
| const StyledReactionsContainer = styled.div`
 | |
|   background-color: var(--modal-background-content-color);
 | |
|   border-bottom: 1px solid var(--border-color);
 | |
|   width: 100%;
 | |
|   overflow-x: auto;
 | |
|   padding: 12px 8px 0;
 | |
| `;
 | |
| 
 | |
| const StyledSendersContainer = styled(Flex)`
 | |
|   width: 100%;
 | |
|   min-height: 332px;
 | |
|   height: 100%;
 | |
|   max-height: 496px;
 | |
|   overflow-x: hidden;
 | |
|   overflow-y: auto;
 | |
|   padding: 0 16px 16px;
 | |
| `;
 | |
| 
 | |
| const StyledReactionBar = styled(Flex)`
 | |
|   width: 100%;
 | |
|   margin: 12px 0 20px 4px;
 | |
| 
 | |
|   p {
 | |
|     color: var(--text-secondary-color);
 | |
|     margin: 0;
 | |
| 
 | |
|     span:nth-child(1) {
 | |
|       margin: 0 8px;
 | |
|       color: var(--text-primary-color);
 | |
|       white-space: nowrap;
 | |
|     }
 | |
| 
 | |
|     span:nth-child(2) {
 | |
|       margin-right: 8px;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   .session-button {
 | |
|     font-weight: 400;
 | |
|     padding: 0px;
 | |
|   }
 | |
| `;
 | |
| 
 | |
| const StyledReactionSender = styled(Flex)`
 | |
|   width: 100%;
 | |
|   margin-bottom: 12px;
 | |
|   .module-avatar {
 | |
|     margin-right: 12px;
 | |
|   }
 | |
| 
 | |
|   .module-conversation__user__profile-name {
 | |
|     color: var(--text-primary-color);
 | |
|     font-weight: normal;
 | |
|   }
 | |
| `;
 | |
| 
 | |
| type ReactionSendersProps = {
 | |
|   messageId: string;
 | |
|   currentReact: string;
 | |
|   senders: Array<string>;
 | |
|   me: string;
 | |
|   handleClose: () => void;
 | |
| };
 | |
| 
 | |
| const ReactionSenders = (props: ReactionSendersProps) => {
 | |
|   const { messageId, currentReact, senders, me, handleClose } = props;
 | |
|   const dispatch = useDispatch();
 | |
| 
 | |
|   const handleAvatarClick = async (sender: string) => {
 | |
|     const message = await Data.getMessageById(messageId);
 | |
|     if (message) {
 | |
|       handleClose();
 | |
|       const contact = message.findAndFormatContact(sender);
 | |
|       dispatch(
 | |
|         updateUserDetailsModal({
 | |
|           conversationId: sender,
 | |
|           userName: contact.name || contact.profileName || sender,
 | |
|           authorAvatarPath: contact.avatarPath,
 | |
|         })
 | |
|       );
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   const handleRemoveReaction = async () => {
 | |
|     await Reactions.sendMessageReaction(messageId, currentReact);
 | |
| 
 | |
|     if (senders.length <= 1) {
 | |
|       dispatch(updateReactListModal(null));
 | |
|     }
 | |
|   };
 | |
| 
 | |
|   return (
 | |
|     <>
 | |
|       {senders.map((sender: string) => (
 | |
|         <StyledReactionSender
 | |
|           key={`${messageId}-${sender}`}
 | |
|           container={true}
 | |
|           justifyContent={'space-between'}
 | |
|           alignItems={'center'}
 | |
|         >
 | |
|           <Flex container={true} alignItems={'center'}>
 | |
|             <Avatar
 | |
|               size={AvatarSize.XS}
 | |
|               pubkey={sender}
 | |
|               onAvatarClick={async () => {
 | |
|                 await handleAvatarClick(sender);
 | |
|               }}
 | |
|             />
 | |
|             {sender === me ? (
 | |
|               window.i18n('you')
 | |
|             ) : (
 | |
|               <ContactName
 | |
|                 pubkey={sender}
 | |
|                 module="module-conversation__user"
 | |
|                 shouldShowPubkey={false}
 | |
|               />
 | |
|             )}
 | |
|           </Flex>
 | |
|           {sender === me && (
 | |
|             <SessionIconButton
 | |
|               iconType="exit"
 | |
|               iconSize="small"
 | |
|               onClick={async () => {
 | |
|                 await handleRemoveReaction();
 | |
|               }}
 | |
|             />
 | |
|           )}
 | |
|         </StyledReactionSender>
 | |
|       ))}
 | |
|     </>
 | |
|   );
 | |
| };
 | |
| 
 | |
| const StyledCountText = styled.p`
 | |
|   color: var(--text-secondary-color);
 | |
|   text-align: center;
 | |
|   margin: 16px auto 0;
 | |
| 
 | |
|   span {
 | |
|     color: var(--text-primary);
 | |
|   }
 | |
| `;
 | |
| // tslint:disable: use-simple-attributes
 | |
| 
 | |
| const CountText = ({ count, emoji }: { count: number; emoji: string }) => {
 | |
|   return (
 | |
|     <StyledCountText>
 | |
|       <SessionHtmlRenderer
 | |
|         html={
 | |
|           count > Reactions.SOGSReactorsFetchCount + 1
 | |
|             ? window.i18n('reactionListCountPlural', [
 | |
|                 window.i18n('otherPlural', [String(count - Reactions.SOGSReactorsFetchCount)]),
 | |
|                 emoji,
 | |
|               ])
 | |
|             : window.i18n('reactionListCountSingular', [
 | |
|                 window.i18n('otherSingular', [String(count - Reactions.SOGSReactorsFetchCount)]),
 | |
|                 emoji,
 | |
|               ])
 | |
|         }
 | |
|       />
 | |
|     </StyledCountText>
 | |
|   );
 | |
| };
 | |
| 
 | |
| type Props = {
 | |
|   reaction: string;
 | |
|   messageId: string;
 | |
| };
 | |
| 
 | |
| const handleSenders = (senders: Array<string>, me: string) => {
 | |
|   let updatedSenders = [...senders];
 | |
|   const blindedMe = updatedSenders.filter(isUsAnySogsFromCache);
 | |
| 
 | |
|   let meIndex = -1;
 | |
|   if (blindedMe && blindedMe[0]) {
 | |
|     meIndex = updatedSenders.indexOf(blindedMe[0]);
 | |
|   } else {
 | |
|     meIndex = updatedSenders.indexOf(me);
 | |
|   }
 | |
|   if (meIndex >= 0) {
 | |
|     updatedSenders.splice(meIndex, 1);
 | |
|     updatedSenders = [me, ...updatedSenders];
 | |
|   }
 | |
| 
 | |
|   return updatedSenders;
 | |
| };
 | |
| 
 | |
| // tslint:disable-next-line: max-func-body-length
 | |
| export const ReactListModal = (props: Props): ReactElement => {
 | |
|   const { reaction, messageId } = props;
 | |
| 
 | |
|   const dispatch = useDispatch();
 | |
|   const [reactions, setReactions] = useState<SortedReactionList>([]);
 | |
|   const reactionsMap = (reactions && Object.fromEntries(reactions)) || {};
 | |
|   const [currentReact, setCurrentReact] = useState('');
 | |
|   const [reactAriaLabel, setReactAriaLabel] = useState<string | undefined>();
 | |
|   const [count, setCount] = useState<number | null>(null);
 | |
|   const [senders, setSenders] = useState<Array<string>>([]);
 | |
| 
 | |
|   const msgProps = useMessageReactsPropsById(messageId);
 | |
|   const isPublic = useSelector(getSelectedConversationIsPublic);
 | |
|   const weAreModerator = useWeAreModerator(msgProps?.convoId);
 | |
|   const me = UserUtils.getOurPubKeyStrFromCache();
 | |
| 
 | |
|   // tslint:disable: cyclomatic-complexity
 | |
|   useEffect(() => {
 | |
|     if (currentReact === '' && currentReact !== reaction) {
 | |
|       setReactAriaLabel(
 | |
|         nativeEmojiData?.ariaLabels ? nativeEmojiData.ariaLabels[reaction] : undefined
 | |
|       );
 | |
|       setCurrentReact(reaction);
 | |
|     }
 | |
| 
 | |
|     if (msgProps?.sortedReacts && !isEqual(reactions, msgProps?.sortedReacts)) {
 | |
|       setReactions(msgProps?.sortedReacts);
 | |
|     }
 | |
| 
 | |
|     if (
 | |
|       reactions &&
 | |
|       reactions.length > 0 &&
 | |
|       ((msgProps?.sortedReacts && msgProps.sortedReacts.length === 0) ||
 | |
|         msgProps?.sortedReacts === undefined)
 | |
|     ) {
 | |
|       setReactions([]);
 | |
|     }
 | |
| 
 | |
|     let _senders =
 | |
|       reactionsMap && reactionsMap[currentReact] && reactionsMap[currentReact].senders
 | |
|         ? reactionsMap[currentReact].senders
 | |
|         : null;
 | |
| 
 | |
|     if (_senders && !isEqual(senders, _senders)) {
 | |
|       if (_senders.length > 0) {
 | |
|         _senders = handleSenders(_senders, me);
 | |
|       }
 | |
| 
 | |
|       // make sure to deep compare here otherwise we get stuck in a ever rendering look (only happens when we are one of the reactor)
 | |
|       if (!isEqual(_senders, senders)) {
 | |
|         setSenders(_senders);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (senders.length > 0 && (!reactionsMap[currentReact]?.senders || isEmpty(_senders))) {
 | |
|       setSenders([]);
 | |
|     }
 | |
| 
 | |
|     if (reactionsMap[currentReact]?.count && count !== reactionsMap[currentReact]?.count) {
 | |
|       setCount(reactionsMap[currentReact].count);
 | |
|     }
 | |
|   }, [
 | |
|     count,
 | |
|     currentReact,
 | |
|     me,
 | |
|     reaction,
 | |
|     reactionsMap[currentReact]?.count,
 | |
|     msgProps?.sortedReacts,
 | |
|     reactionsMap,
 | |
|     senders,
 | |
|   ]);
 | |
| 
 | |
|   if (!msgProps) {
 | |
|     return <></>;
 | |
|   }
 | |
| 
 | |
|   const handleSelectedReaction = (emoji: string): boolean => {
 | |
|     return currentReact === emoji;
 | |
|   };
 | |
| 
 | |
|   const handleReactionClick = (emoji: string) => {
 | |
|     setReactAriaLabel(nativeEmojiData?.ariaLabels ? nativeEmojiData.ariaLabels[emoji] : undefined);
 | |
|     setCurrentReact(emoji);
 | |
|   };
 | |
| 
 | |
|   const handleClose = () => {
 | |
|     dispatch(updateReactListModal(null));
 | |
|   };
 | |
| 
 | |
|   const handleClearReactions = () => {
 | |
|     handleClose();
 | |
|     dispatch(
 | |
|       updateReactClearAllModal({
 | |
|         reaction: currentReact,
 | |
|         messageId,
 | |
|       })
 | |
|     );
 | |
|   };
 | |
| 
 | |
|   return (
 | |
|     <SessionWrapperModal
 | |
|       additionalClassName={'reaction-list-modal'}
 | |
|       showHeader={false}
 | |
|       onClose={handleClose}
 | |
|     >
 | |
|       <StyledReactListContainer container={true} flexDirection={'column'} alignItems={'flex-start'}>
 | |
|         <StyledReactionsContainer>
 | |
|           <MessageReactions
 | |
|             messageId={messageId}
 | |
|             hasReactLimit={false}
 | |
|             inModal={true}
 | |
|             onSelected={handleSelectedReaction}
 | |
|             onClick={handleReactionClick}
 | |
|             noAvatar={true}
 | |
|           />
 | |
|         </StyledReactionsContainer>
 | |
|         {reactionsMap && currentReact && (
 | |
|           <StyledSendersContainer
 | |
|             container={true}
 | |
|             flexDirection={'column'}
 | |
|             alignItems={'flex-start'}
 | |
|           >
 | |
|             <StyledReactionBar
 | |
|               container={true}
 | |
|               justifyContent={'space-between'}
 | |
|               alignItems={'center'}
 | |
|             >
 | |
|               <p>
 | |
|                 <span role={'img'} aria-label={reactAriaLabel}>
 | |
|                   {currentReact}
 | |
|                 </span>
 | |
|                 {reactionsMap[currentReact].count && (
 | |
|                   <>
 | |
|                     <span>•</span>
 | |
|                     <span>{reactionsMap[currentReact].count}</span>
 | |
|                   </>
 | |
|                 )}
 | |
|               </p>
 | |
|               {isPublic && weAreModerator && (
 | |
|                 <SessionButton
 | |
|                   text={window.i18n('clearAll')}
 | |
|                   buttonColor={SessionButtonColor.Danger}
 | |
|                   buttonType={SessionButtonType.Simple}
 | |
|                   onClick={handleClearReactions}
 | |
|                 />
 | |
|               )}
 | |
|             </StyledReactionBar>
 | |
|             {senders && senders.length > 0 && (
 | |
|               <ReactionSenders
 | |
|                 messageId={messageId}
 | |
|                 currentReact={currentReact}
 | |
|                 senders={senders}
 | |
|                 me={me}
 | |
|                 handleClose={handleClose}
 | |
|               />
 | |
|             )}
 | |
|             {isPublic && currentReact && count && count > Reactions.SOGSReactorsFetchCount && (
 | |
|               <CountText count={count} emoji={currentReact} />
 | |
|             )}
 | |
|           </StyledSendersContainer>
 | |
|         )}
 | |
|       </StyledReactListContainer>
 | |
|     </SessionWrapperModal>
 | |
|   );
 | |
| };
 |