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.
		
		
		
		
		
			
		
			
				
	
	
		
			293 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			293 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			TypeScript
		
	
| import React from 'react';
 | |
| import styled from 'styled-components';
 | |
| 
 | |
| import { GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs';
 | |
| import { useNicknameOrProfileNameOrShortenedPubkey } from '../hooks/useParamSelector';
 | |
| import { PubKey } from '../session/types';
 | |
| import { UserUtils } from '../session/utils';
 | |
| import { GroupInvite } from '../session/utils/job_runners/jobs/GroupInviteJob';
 | |
| import { GroupPromote } from '../session/utils/job_runners/jobs/GroupPromoteJob';
 | |
| import {
 | |
|   useMemberInviteFailed,
 | |
|   useMemberInviteSending,
 | |
|   useMemberInviteSent,
 | |
|   useMemberPromoteSending,
 | |
|   useMemberPromotionFailed,
 | |
|   useMemberPromotionSent,
 | |
| } from '../state/selectors/groups';
 | |
| import { Avatar, AvatarSize, CrownIcon } from './avatar/Avatar';
 | |
| import { Flex } from './basic/Flex';
 | |
| import {
 | |
|   SessionButton,
 | |
|   SessionButtonColor,
 | |
|   SessionButtonShape,
 | |
|   SessionButtonType,
 | |
| } from './basic/SessionButton';
 | |
| import { SessionRadio } from './basic/SessionRadio';
 | |
| 
 | |
| const AvatarContainer = styled.div`
 | |
|   position: relative;
 | |
| `;
 | |
| 
 | |
| const AvatarItem = (props: { memberPubkey: string; isAdmin: boolean }) => {
 | |
|   const { memberPubkey, isAdmin } = props;
 | |
|   return (
 | |
|     <AvatarContainer>
 | |
|       <Avatar size={AvatarSize.XS} pubkey={memberPubkey} />
 | |
|       {isAdmin && <CrownIcon />}
 | |
|     </AvatarContainer>
 | |
|   );
 | |
| };
 | |
| 
 | |
| const StyledSessionMemberItem = styled.button<{
 | |
|   inMentions?: boolean;
 | |
|   zombie?: boolean;
 | |
|   selected?: boolean;
 | |
|   disableBg?: boolean;
 | |
| }>`
 | |
|   cursor: pointer;
 | |
|   display: flex;
 | |
|   align-items: center;
 | |
|   justify-content: space-between;
 | |
|   flex-shrink: 0;
 | |
|   flex-grow: 1;
 | |
|   font-family: var(--font-default);
 | |
|   padding: 0px var(--margins-sm);
 | |
|   height: ${props => (props.inMentions ? '40px' : '50px')};
 | |
|   width: 100%;
 | |
|   transition: var(--default-duration);
 | |
|   opacity: ${props => (props.zombie ? 0.5 : 1)};
 | |
|   background-color: ${props =>
 | |
|     !props.disableBg && props.selected
 | |
|       ? 'var(--conversation-tab-background-selected-color) !important'
 | |
|       : null};
 | |
| 
 | |
|   :not(:last-child) {
 | |
|     border-bottom: 1px solid var(--border-color);
 | |
|   }
 | |
| `;
 | |
| 
 | |
| const StyledInfo = styled.div`
 | |
|   display: flex;
 | |
|   align-items: center;
 | |
|   min-width: 0;
 | |
| `;
 | |
| 
 | |
| const StyledName = styled.span`
 | |
|   font-weight: bold;
 | |
|   white-space: nowrap;
 | |
|   overflow: hidden;
 | |
|   text-overflow: ellipsis;
 | |
| `;
 | |
| 
 | |
| const StyledCheckContainer = styled.div`
 | |
|   display: flex;
 | |
|   align-items: center;
 | |
| `;
 | |
| 
 | |
| type MemberListItemProps = {
 | |
|   pubkey: string;
 | |
|   isSelected: boolean;
 | |
|   // this bool is used to make a zombie appear with less opacity than a normal member
 | |
|   isZombie?: boolean;
 | |
|   inMentions?: boolean; // set to true if we are rendering members but in the Mentions picker
 | |
|   disableBg?: boolean;
 | |
|   isAdmin?: boolean; // if true,  we add a small crown on top of their avatar
 | |
|   onSelect?: (pubkey: string) => void;
 | |
|   onUnselect?: (pubkey: string) => void;
 | |
|   dataTestId?: React.SessionDataTestId;
 | |
|   displayGroupStatus?: boolean;
 | |
|   groupPk?: string;
 | |
| };
 | |
| 
 | |
| const ResendContainer = ({
 | |
|   displayGroupStatus,
 | |
|   groupPk,
 | |
|   pubkey,
 | |
| }: Pick<MemberListItemProps, 'displayGroupStatus' | 'pubkey' | 'groupPk'>) => {
 | |
|   if (
 | |
|     displayGroupStatus &&
 | |
|     groupPk &&
 | |
|     PubKey.is03Pubkey(groupPk) &&
 | |
|     PubKey.is05Pubkey(pubkey) &&
 | |
|     !UserUtils.isUsFromCache(pubkey)
 | |
|   ) {
 | |
|     return (
 | |
|       <Flex
 | |
|         container={true}
 | |
|         margin="0 0 0 auto"
 | |
|         padding="0 var(--margins-lg)"
 | |
|         gap="var(--margins-sm)"
 | |
|       >
 | |
|         <ResendInviteButton groupPk={groupPk} pubkey={pubkey} />
 | |
|         <ResendPromoteButton groupPk={groupPk} pubkey={pubkey} />
 | |
|       </Flex>
 | |
|     );
 | |
|   }
 | |
|   return null;
 | |
| };
 | |
| 
 | |
| const StyledGroupStatusText = styled.span<{ isFailure: boolean }>`
 | |
|   color: ${props => (props.isFailure ? 'var(--danger-color)' : 'var(--text-secondary-color)')};
 | |
|   font-size: var(--font-size-xs);
 | |
|   margin-top: var(--margins-xs);
 | |
|   min-width: 100px; // min-width so that the dialog does not resize when the status change to sending
 | |
|   text-align: left;
 | |
| `;
 | |
| 
 | |
| const GroupStatusText = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: GroupPubkeyType }) => {
 | |
|   const groupInviteFailed = useMemberInviteFailed(pubkey, groupPk);
 | |
|   const groupPromotionFailed = useMemberPromotionFailed(pubkey, groupPk);
 | |
|   const groupPromotionSending = useMemberPromoteSending(groupPk, pubkey);
 | |
| 
 | |
|   const groupInviteSent = useMemberInviteSent(pubkey, groupPk);
 | |
|   const groupPromotionSent = useMemberPromotionSent(pubkey, groupPk);
 | |
|   const groupInviteSending = useMemberInviteSending(groupPk, pubkey);
 | |
| 
 | |
|   const statusText = groupPromotionFailed
 | |
|     ? window.i18n('promotionFailed')
 | |
|     : groupInviteFailed
 | |
|       ? window.i18n('inviteFailed')
 | |
|       : groupInviteSending
 | |
|         ? window.i18n('inviteSending')
 | |
|         : groupPromotionSending
 | |
|           ? window.i18n('promotionSending')
 | |
|           : groupInviteSent
 | |
|             ? window.i18n('inviteSent')
 | |
|             : groupPromotionSent
 | |
|               ? window.i18n('promotionSent')
 | |
|               : null;
 | |
| 
 | |
|   if (!statusText) {
 | |
|     return null;
 | |
|   }
 | |
|   return (
 | |
|     <StyledGroupStatusText
 | |
|       data-testid={'group_member_status_text'}
 | |
|       isFailure={groupPromotionFailed || groupInviteFailed}
 | |
|     >
 | |
|       {statusText}
 | |
|     </StyledGroupStatusText>
 | |
|   );
 | |
| };
 | |
| 
 | |
| const GroupStatusContainer = ({
 | |
|   displayGroupStatus,
 | |
|   groupPk,
 | |
|   pubkey,
 | |
| }: Pick<MemberListItemProps, 'displayGroupStatus' | 'pubkey' | 'groupPk'>) => {
 | |
|   if (
 | |
|     displayGroupStatus &&
 | |
|     groupPk &&
 | |
|     PubKey.is03Pubkey(groupPk) &&
 | |
|     PubKey.is05Pubkey(pubkey) &&
 | |
|     !UserUtils.isUsFromCache(pubkey)
 | |
|   ) {
 | |
|     return <GroupStatusText groupPk={groupPk} pubkey={pubkey} />;
 | |
|   }
 | |
|   return null;
 | |
| };
 | |
| 
 | |
| const ResendInviteButton = ({
 | |
|   groupPk,
 | |
|   pubkey,
 | |
| }: {
 | |
|   pubkey: PubkeyType;
 | |
|   groupPk: GroupPubkeyType;
 | |
| }) => {
 | |
|   return (
 | |
|     <SessionButton
 | |
|       dataTestId={'resend_invite_button'}
 | |
|       buttonShape={SessionButtonShape.Square}
 | |
|       buttonType={SessionButtonType.Solid}
 | |
|       text={window.i18n('resend')}
 | |
|       onClick={() => {
 | |
|         void GroupInvite.addJob({ groupPk, member: pubkey }); // TODO audric: do we need to take care if that user was invited withHistory or not
 | |
|       }}
 | |
|     />
 | |
|   );
 | |
| };
 | |
| 
 | |
| const ResendPromoteButton = ({
 | |
|   groupPk,
 | |
|   pubkey,
 | |
| }: {
 | |
|   pubkey: PubkeyType;
 | |
|   groupPk: GroupPubkeyType;
 | |
| }) => {
 | |
|   return (
 | |
|     <SessionButton
 | |
|       dataTestId={'resend_promote_button'}
 | |
|       buttonShape={SessionButtonShape.Square}
 | |
|       buttonType={SessionButtonType.Solid}
 | |
|       buttonColor={SessionButtonColor.Danger}
 | |
|       text="PrOmOtE"
 | |
|       onClick={() => {
 | |
|         void GroupPromote.addJob({ groupPk, member: pubkey });
 | |
|       }}
 | |
|     />
 | |
|   );
 | |
| };
 | |
| 
 | |
| export const MemberListItem = ({
 | |
|   isSelected,
 | |
|   pubkey,
 | |
|   dataTestId,
 | |
|   disableBg,
 | |
|   displayGroupStatus,
 | |
|   inMentions,
 | |
|   isAdmin,
 | |
|   isZombie,
 | |
|   onSelect,
 | |
|   onUnselect,
 | |
|   groupPk,
 | |
| }: MemberListItemProps) => {
 | |
|   const memberName = useNicknameOrProfileNameOrShortenedPubkey(pubkey);
 | |
| 
 | |
|   return (
 | |
|     <StyledSessionMemberItem
 | |
|       onClick={() => {
 | |
|         // eslint-disable-next-line no-unused-expressions
 | |
|         isSelected ? onUnselect?.(pubkey) : onSelect?.(pubkey);
 | |
|       }}
 | |
|       style={
 | |
|         !inMentions && !disableBg
 | |
|           ? {
 | |
|               backgroundColor: 'var(--background-primary-color)',
 | |
|             }
 | |
|           : {}
 | |
|       }
 | |
|       data-testid={dataTestId}
 | |
|       zombie={isZombie}
 | |
|       inMentions={inMentions}
 | |
|       selected={isSelected}
 | |
|       disableBg={disableBg}
 | |
|     >
 | |
|       <StyledInfo>
 | |
|         <AvatarItem memberPubkey={pubkey} isAdmin={isAdmin || false} />
 | |
|         <Flex
 | |
|           container={true}
 | |
|           flexDirection="column"
 | |
|           margin="0 var(--margins-md)"
 | |
|           alignItems="flex-start"
 | |
|         >
 | |
|           <StyledName data-testid={'group_member_name'}>{memberName}</StyledName>
 | |
|           <GroupStatusContainer
 | |
|             pubkey={pubkey}
 | |
|             displayGroupStatus={displayGroupStatus}
 | |
|             groupPk={groupPk}
 | |
|           />
 | |
|         </Flex>
 | |
|       </StyledInfo>
 | |
| 
 | |
|       <ResendContainer pubkey={pubkey} displayGroupStatus={displayGroupStatus} groupPk={groupPk} />
 | |
| 
 | |
|       {!inMentions && (
 | |
|         <StyledCheckContainer>
 | |
|           <SessionRadio active={isSelected} value={pubkey} inputName={pubkey} label="" />
 | |
|         </StyledCheckContainer>
 | |
|       )}
 | |
|     </StyledSessionMemberItem>
 | |
|   );
 | |
| };
 |