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;
}