From acc6e8462700cd0a04cda1ec2c42967eef873d84 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 18 Dec 2024 17:26:53 +1100 Subject: [PATCH] feat: add not_sent & unknown string also remove the desktop sending state. this will soon by in libsession --- _locales/en/messages.json | 4 + ts/components/MemberListItem.tsx | 76 +++++++----- ts/state/ducks/metaGroups.ts | 2 - ts/state/selectors/groups.ts | 204 ++++++++++--------------------- 4 files changed, 111 insertions(+), 175 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e53b3c776..409381a54 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -25,7 +25,9 @@ "adminPromotionFailedDescription": "Failed to promote {name} in {group_name}", "adminPromotionFailedDescriptionMultiple": "Failed to promote {name} and {count} others in {group_name}", "adminPromotionFailedDescriptionTwo": "Failed to promote {name} and {other_name} in {group_name}", + "adminPromotionNotSent": "Promotion not sent", "adminPromotionSent": "Admin promotion sent", + "adminPromotionStatusUnknown": "Promotion status unknown", "adminRemove": "Remove Admins", "adminRemoveAsAdmin": "Remove as Admin", "adminRemoveCommunityNone": "There are no Admins in this Community.", @@ -394,10 +396,12 @@ "groupInviteFailedMultiple": "Failed to invite {name} and {count} others to {group_name}", "groupInviteFailedTwo": "Failed to invite {name} and {other_name} to {group_name}", "groupInviteFailedUser": "Failed to invite {name} to {group_name}", + "groupInviteNotSent": "Invite not sent", "groupInviteReinvite": "{name} invited you to rejoin {group_name}, where you are an Admin.", "groupInviteReinviteYou": "You were invited to rejoin {group_name}, where you are an Admin.", "groupInviteSending": "{count, plural, one [Sending invite] other [Sending invites]}", "groupInviteSent": "Invite sent", + "groupInviteStatusUnknown": "Invite status unknown", "groupInviteSuccessful": "Group invite successful", "groupInviteVersion": "Users must have the latest release to receive invitations", "groupInviteYou": "You were invited to join the group.", diff --git a/ts/components/MemberListItem.tsx b/ts/components/MemberListItem.tsx index 6fafbf41e..f3eda0516 100644 --- a/ts/components/MemberListItem.tsx +++ b/ts/components/MemberListItem.tsx @@ -1,6 +1,6 @@ import styled from 'styled-components'; -import { GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs'; +import { GroupPubkeyType, MemberStateGroupV2, PubkeyType } from 'libsession_util_nodejs'; import { isEmpty } from 'lodash'; import { useNicknameOrProfileNameOrShortenedPubkey, @@ -13,14 +13,9 @@ import { GroupInvite } from '../session/utils/job_runners/jobs/GroupInviteJob'; import { hasClosedGroupV2QAButtons } from '../shared/env_vars'; import { useMemberHasAcceptedInvite, - useMemberInviteFailed, - useMemberInviteSending, - useMemberInviteSent, - useMemberPromoteSending, - useMemberPromotionFailed, - useMemberPromotionSent, useMemberIsNominatedAdmin, useMemberPendingRemoval, + useMemberStatus, } from '../state/selectors/groups'; import { Avatar, AvatarSize, CrownIcon } from './avatar/Avatar'; import { Flex } from './basic/Flex'; @@ -35,6 +30,7 @@ import { MetaGroupWrapperActions, UserGroupsWrapperActions, } from '../webworker/workers/browser/libsession_worker_interface'; +import { assertUnreachable } from '../types/sqlSharedTypes'; const AvatarContainer = styled.div` position: relative; @@ -161,43 +157,61 @@ const StyledGroupStatusText = styled.span<{ isFailure: boolean }>` text-align: start; `; +function localisedStatusFromMemberStatus(memberStatus: MemberStateGroupV2) { + switch (memberStatus) { + case 'INVITE_FAILED': + return window.i18n('groupInviteFailed'); + case 'INVITE_NOT_SENT': + return window.i18n('groupInviteNotSent'); + case 'INVITE_SENDING': + return window.i18n('groupInviteSending', { count: 1 }); + case 'INVITE_SENT': + return window.i18n('groupInviteSent'); + case 'INVITE_UNKNOWN': // fallback, hopefully won't happen in production + return window.i18n('groupInviteStatusUnknown'); + case 'PROMOTION_UNKNOWN': // fallback, hopefully won't happen in production + return window.i18n('adminPromotionStatusUnknown'); + case 'REMOVED_UNKNOWN': // fallback, hopefully won't happen in production + case 'REMOVED_MEMBER': // we want pending removal members at the end of the "invite" states + case 'REMOVED_MEMBER_AND_MESSAGES': + return null; // no text for those 3 pending removal states + case 'PROMOTION_FAILED': + return window.i18n('adminPromotionFailed'); + case 'PROMOTION_NOT_SENT': + return window.i18n('adminPromotionNotSent'); + case 'PROMOTION_SENDING': + return window.i18n('adminSendingPromotion', { count: 1 }); + case 'PROMOTION_SENT': + return window.i18n('adminPromotionSent'); + case 'PROMOTION_ACCEPTED': + return null; // no statuses for accepted state; + case 'INVITE_ACCEPTED': + return null; // no statuses for accepted state + default: + assertUnreachable(memberStatus, 'Unhandled switch case'); + return Number.MAX_SAFE_INTEGER; + } +} + const GroupStatusText = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: GroupPubkeyType }) => { - const groupInviteFailed = useMemberInviteFailed(pubkey, groupPk); - const groupPromotionFailed = useMemberPromotionFailed(pubkey, groupPk); - const groupPromotionSending = useMemberPromoteSending(groupPk, pubkey); + const memberStatus = useMemberStatus(pubkey, groupPk); - const groupInviteSent = useMemberInviteSent(pubkey, groupPk); - const groupPromotionSent = useMemberPromotionSent(pubkey, groupPk); - const groupInviteSending = useMemberInviteSending(groupPk, pubkey); + if (!memberStatus) { + return null; + } /** * Note: Keep the "sending" checks here first, as we might be "sending" when we've previously failed. * If we were to have the "failed" checks first, we'd hide the "sending" state when we are retrying. */ - const statusText = groupInviteSending - ? window.i18n('groupInviteSending', { count: 1 }) - : groupPromotionSending - ? window.i18n('adminSendingPromotion', { count: 1 }) - : groupPromotionFailed - ? window.i18n('adminPromotionFailed') - : groupInviteFailed - ? window.i18n('groupInviteFailed') - : groupInviteSent - ? window.i18n('groupInviteSent') - : groupPromotionSent - ? window.i18n('adminPromotionSent') - : null; - + const statusText = localisedStatusFromMemberStatus(memberStatus); if (!statusText) { return null; } return ( {statusText} diff --git a/ts/state/ducks/metaGroups.ts b/ts/state/ducks/metaGroups.ts index e35b9c70e..0c28dae07 100644 --- a/ts/state/ducks/metaGroups.ts +++ b/ts/state/ducks/metaGroups.ts @@ -1202,8 +1202,6 @@ const currentDeviceGroupNameChange = createAsyncThunk( function deleteGroupPkEntriesFromState(state: GroupState, groupPk: GroupPubkeyType) { delete state.infos[groupPk]; delete state.members[groupPk]; - delete state.membersInviteSending[groupPk]; - delete state.membersPromoteSending[groupPk]; } function applySendingStateChange({ diff --git a/ts/state/selectors/groups.ts b/ts/state/selectors/groups.ts index 5bf75f9c3..f5efbe451 100644 --- a/ts/state/selectors/groups.ts +++ b/ts/state/selectors/groups.ts @@ -5,7 +5,8 @@ import { PubkeyType, } from 'libsession_util_nodejs'; import { useSelector } from 'react-redux'; -import { compact, concat, differenceBy, sortBy, uniqBy } from 'lodash'; +import { sortBy } from 'lodash'; +import { useMemo } from 'react'; import { PubKey } from '../../session/types'; import { GroupState } from '../ducks/metaGroups'; import { StateType } from '../reducer'; @@ -14,8 +15,6 @@ import { UserUtils } from '../../session/utils'; import { useConversationsNicknameRealNameOrShortenPubkey } from '../../hooks/useParamSelector'; const getLibGroupsState = (state: StateType): GroupState => state.groups; -const getInviteSendingState = (state: StateType) => getLibGroupsState(state).membersInviteSending; -const getPromoteSendingState = (state: StateType) => getLibGroupsState(state).membersPromoteSending; function getMembersOfGroup(state: StateType, convo?: string): Array { if (!convo) { @@ -116,6 +115,11 @@ function getMemberPendingRemoval(state: StateType, pubkey: PubkeyType, convo?: G ); } +function getMemberStatus(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) { + const members = getMembersOfGroup(state, convo); + return findMemberInMembers(members, pubkey)?.memberStatus; +} + export function getLibMembersCount(state: StateType, convo?: GroupPubkeyType): Array { return getLibMembersPubkeys(state, convo); } @@ -171,6 +175,10 @@ export function useIsCreatingGroupFromUIPending() { return useSelector(getIsCreatingGroupFromUI); } +export function useMemberStatus(member: PubkeyType, groupPk: GroupPubkeyType) { + return useSelector((state: StateType) => getMemberStatus(state, member, groupPk)); +} + export function useMemberInviteFailed(member: PubkeyType, groupPk: GroupPubkeyType) { return useSelector((state: StateType) => getMemberInviteFailed(state, member, groupPk)); } @@ -219,152 +227,64 @@ export function useGroupNameChangeFromUIPending() { return useSelector(getGroupNameChangeFromUIPending); } -/** - * The selectors above are all deriving data from libsession. - * There is also some data that we only need in memory, not part of libsession (and so unsaved). - * An example is the "sending invite" or "sending promote" state of a member in a group. - */ - -function useMembersInviteSending(groupPk?: string) { - return useSelector((state: StateType) => - groupPk && PubKey.is03Pubkey(groupPk) ? getInviteSendingState(state)[groupPk] || [] : [] - ); -} - -export function useMemberInviteSending(groupPk: GroupPubkeyType, memberPk: PubkeyType) { - return useMembersInviteSending(groupPk).includes(memberPk); -} - -function useMembersPromoteSending(groupPk?: string) { - return useSelector((state: StateType) => - groupPk && PubKey.is03Pubkey(groupPk) ? getPromoteSendingState(state)[groupPk] || [] : [] - ); -} - -export function useMemberPromoteSending(groupPk: GroupPubkeyType, memberPk: PubkeyType) { - return useMembersPromoteSending(groupPk).includes(memberPk); +function getSortingOrderForStatus(memberStatus: MemberStateGroupV2) { + switch (memberStatus) { + case 'INVITE_FAILED': + return 0; + case 'INVITE_NOT_SENT': + return 10; + case 'INVITE_SENDING': + return 20; + case 'INVITE_SENT': + return 30; + case 'INVITE_UNKNOWN': // fallback, hopefully won't happen in production + return 40; + case 'REMOVED_UNKNOWN': // fallback, hopefully won't happen in production + case 'REMOVED_MEMBER': // we want pending removal members at the end of the "invite" states + case 'REMOVED_MEMBER_AND_MESSAGES': + return 50; + case 'PROMOTION_FAILED': + return 60; + case 'PROMOTION_NOT_SENT': + return 70; + case 'PROMOTION_SENDING': + return 80; + case 'PROMOTION_SENT': + return 90; + case 'PROMOTION_UNKNOWN': // fallback, hopefully won't happen in production + return 100; + case 'PROMOTION_ACCEPTED': + return 110; + case 'INVITE_ACCEPTED': + return 120; + default: + assertUnreachable(memberStatus, 'Unhandled switch case'); + return Number.MAX_SAFE_INTEGER; + } } -type MemberStateGroupV2WithSending = MemberStateGroupV2 | 'INVITE_SENDING' | 'PROMOTION_SENDING'; -type MemberWithV2Sending = Pick & { - memberStatus: MemberStateGroupV2WithSending; -}; - export function useStateOf03GroupMembers(convoId?: string) { const us = UserUtils.getOurPubKeyStrFromCache(); - let unsortedMembers = useSelector((state: StateType) => getMembersOfGroup(state, convoId)); - const invitesSendingPk = useMembersInviteSending(convoId); - const promotionsSendingPk = useMembersPromoteSending(convoId); - let invitesSending: Array = compact( - invitesSendingPk - .map(sending => unsortedMembers.find(m => m.pubkeyHex === sending)) - .map(m => { - return m ? { ...m, memberStatus: 'INVITE_SENDING' as const } : null; - }) - ); - const promotionSending: Array = compact( - promotionsSendingPk - .map(sending => unsortedMembers.find(m => m.pubkeyHex === sending)) - .map(m => { - return m ? { ...m, memberStatus: 'PROMOTION_SENDING' as const } : null; - }) - ); - - // promotionSending has priority against invitesSending, so removing anything in invitesSending found in promotionSending - invitesSending = differenceBy(invitesSending, promotionSending, value => value.pubkeyHex); - - const bothSending = concat(promotionSending, invitesSending); - - // promotionSending and invitesSending has priority against anything else, so remove anything found in one of those two - // from the unsorted list of members - unsortedMembers = differenceBy(unsortedMembers, bothSending, value => value.pubkeyHex); - - // at this point, merging invitesSending, promotionSending and unsortedMembers should create an array of unique members - const sortedByPriorities = concat(bothSending, unsortedMembers); - if (sortedByPriorities.length !== uniqBy(sortedByPriorities, m => m.pubkeyHex).length) { - throw new Error( - 'merging invitesSending, promotionSending and unsortedMembers should create an array of unique members' - ); - } + const unsortedMembers = useSelector((state: StateType) => getMembersOfGroup(state, convoId)); - // This could have been done now with a `sortedByPriorities.map()` call, - // but we don't want the order as sorted by `sortedByPriorities`, **only** to respect the priorities from it. - // What that means is that a member with a state as inviteSending, should have that state, but not be sorted first. - - // The order we (for now) want is: - // - (Invite failed + Invite Not Sent) merged together, sorted as NameSortingOrder - // - Sending invite, sorted as NameSortingOrder - // - Invite sent, sorted as NameSortingOrder - // - (Promotion failed + Promotion Not Sent) merged together, sorted as NameSortingOrder - // - Sending invite, sorted as NameSortingOrder - // - Invite sent, sorted as NameSortingOrder - // - Admin, sorted as NameSortingOrder - // - Accepted Member, sorted as NameSortingOrder - // NameSortingOrder: You first, then "nickname || name || pubkey -> aA-zZ" - - const unsortedWithStatuses: Array< - Pick & { memberStatus: MemberStateGroupV2WithSending } - > = []; - unsortedWithStatuses.push(...promotionSending); - unsortedWithStatuses.push(...differenceBy(invitesSending, promotionSending)); - unsortedWithStatuses.push(...differenceBy(unsortedMembers, invitesSending, promotionSending)); const names = useConversationsNicknameRealNameOrShortenPubkey( - unsortedWithStatuses.map(m => m.pubkeyHex) + unsortedMembers.map(m => m.pubkeyHex) ); - // needing an index like this outside of lodash is not pretty, - // but sortBy doesn't provide the index in the callback - let index = 0; - - const sorted = sortBy(unsortedWithStatuses, item => { - let stateSortingOrder = 0; - switch (item.memberStatus) { - case 'INVITE_FAILED': - case 'INVITE_NOT_SENT': - stateSortingOrder = -50; - break; - case 'INVITE_SENDING': - stateSortingOrder = -40; - break; - case 'INVITE_SENT': - stateSortingOrder = -30; - break; - case 'REMOVED_UNKNOWN': // fallback, hopefully won't happen in production - case 'REMOVED_MEMBER': // we want pending removal members at the end - case 'REMOVED_MEMBER_AND_MESSAGES': - stateSortingOrder = -20; - break; - case 'PROMOTION_FAILED': - case 'PROMOTION_NOT_SENT': - stateSortingOrder = -15; - break; - case 'PROMOTION_SENDING': - stateSortingOrder = -10; - break; - case 'PROMOTION_SENT': - stateSortingOrder = 0; - break; - case 'PROMOTION_ACCEPTED': - stateSortingOrder = 10; - break; - case 'INVITE_ACCEPTED': - stateSortingOrder = 20; - break; - case 'INVITE_UNKNOWN': // fallback, hopefully won't happen in production - case 'PROMOTION_UNKNOWN': // fallback, hopefully won't happen in production - stateSortingOrder = 50; - break; - - default: - assertUnreachable(item.memberStatus, 'Unhandled switch case'); - } - const sortingOrder = [ - stateSortingOrder, - // per section, we want "us first", then "nickname || displayName || pubkey" - item.pubkeyHex === us ? -1 : names[index]?.toLocaleLowerCase(), - ]; - index++; - return sortingOrder; - }); + const sorted = useMemo(() => { + // needing an index like this outside of lodash is not pretty, + // but sortBy doesn't provide the index in the callback + let index = 0; + return sortBy(unsortedMembers, item => { + const stateSortingOrder = getSortingOrderForStatus(item.memberStatus); + const sortingOrder = [ + stateSortingOrder, + // per section, we want "us" first, then "nickname || displayName || pubkey" + item.pubkeyHex === us ? -1 : names[index], + ]; + index++; + return sortingOrder; + }); + }, [unsortedMembers, us, names]); return sorted; }