fix: add group members sorting

not good but hopefully we won't have to keep for too long
pull/3281/head
Audric Ackermann 6 months ago
parent db22094898
commit cf899ee18c
No known key found for this signature in database

@ -13,11 +13,10 @@ import {
useMemberInviteFailed, useMemberInviteFailed,
useMemberInviteSending, useMemberInviteSending,
useMemberInviteSent, useMemberInviteSent,
useMemberIsPromoted,
useMemberPromoteSending, useMemberPromoteSending,
useMemberPromotionFailed, useMemberPromotionFailed,
useMemberPromotionNotSent,
useMemberPromotionSent, useMemberPromotionSent,
useMemberIsNominatedAdmin,
} from '../state/selectors/groups'; } from '../state/selectors/groups';
import { Avatar, AvatarSize, CrownIcon } from './avatar/Avatar'; import { Avatar, AvatarSize, CrownIcon } from './avatar/Avatar';
import { Flex } from './basic/Flex'; import { Flex } from './basic/Flex';
@ -28,10 +27,6 @@ import {
SessionButtonType, SessionButtonType,
} from './basic/SessionButton'; } from './basic/SessionButton';
import { SessionRadio } from './basic/SessionRadio'; import { SessionRadio } from './basic/SessionRadio';
import { GroupSync } from '../session/utils/job_runners/jobs/GroupSyncJob';
import { RunJobResult } from '../session/utils/job_runners/PersistedJob';
import { SubaccountUnrevokeSubRequest } from '../session/apis/snode_api/SnodeRequestTypes';
import { NetworkTime } from '../util/NetworkTime';
import { import {
MetaGroupWrapperActions, MetaGroupWrapperActions,
UserGroupsWrapperActions, UserGroupsWrapperActions,
@ -221,15 +216,10 @@ const GroupStatusContainer = ({
const ResendButton = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: GroupPubkeyType }) => { const ResendButton = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: GroupPubkeyType }) => {
const acceptedInvite = useMemberHasAcceptedInvite(pubkey, groupPk); const acceptedInvite = useMemberHasAcceptedInvite(pubkey, groupPk);
const promotionFailed = useMemberPromotionFailed(pubkey, groupPk); const nominatedAdmin = useMemberIsNominatedAdmin(pubkey, groupPk);
const promotionSent = useMemberPromotionSent(pubkey, groupPk);
const promotionNotSent = useMemberPromotionNotSent(pubkey, groupPk);
const promoted = useMemberIsPromoted(pubkey, groupPk);
// as soon as the `admin` flag is set in the group for that member, we should be able to resend a promote as we cannot remove an admin. // as soon as the `admin` flag is set in the group for that member, we should be able to resend a promote as we cannot remove an admin.
const canResendPromotion = const canResendPromotion = hasClosedGroupV2QAButtons() && nominatedAdmin;
hasClosedGroupV2QAButtons() &&
(promotionFailed || promotionSent || promotionNotSent || promoted);
// we can always remove/and readd a non-admin member. So we consider that a member who accepted the invite cannot be resent an invite. // we can always remove/and readd a non-admin member. So we consider that a member who accepted the invite cannot be resent an invite.
const canResendInvite = !acceptedInvite; const canResendInvite = !acceptedInvite;
@ -252,32 +242,13 @@ const ResendButton = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: GroupP
window.log.warn('tried to resend invite but we do not have correct details'); window.log.warn('tried to resend invite but we do not have correct details');
return; return;
} }
const token = await MetaGroupWrapperActions.swarmSubAccountToken(groupPk, pubkey);
const unrevokeSubRequest = new SubaccountUnrevokeSubRequest({
groupPk,
revokeTokenHex: [token],
timestamp: NetworkTime.now(),
secretKey: group.secretKey,
});
const sequenceResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
unrevokeSubRequest,
extraStoreRequests: [],
});
if (sequenceResult !== RunJobResult.Success) {
throw new Error('resend invite: pushChangesToGroupSwarmIfNeeded did not return success');
}
// if we tried to invite that member as admin right away, let's retry it as such. // if we tried to invite that member as admin right away, let's retry it as such.
const inviteAsAdmin = const inviteAsAdmin = member.nominatedAdmin;
member.promotionNotSent ||
member.promotionFailed ||
member.promotionPending ||
member.promoted;
await GroupInvite.addJob({ await GroupInvite.addJob({
groupPk, groupPk,
member: pubkey, member: pubkey,
inviteAsAdmin, inviteAsAdmin,
forceUnrevoke: true,
}); });
}} }}
/> />
@ -286,11 +257,11 @@ const ResendButton = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: GroupP
const PromoteButton = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: GroupPubkeyType }) => { const PromoteButton = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: GroupPubkeyType }) => {
const memberAcceptedInvite = useMemberHasAcceptedInvite(pubkey, groupPk); const memberAcceptedInvite = useMemberHasAcceptedInvite(pubkey, groupPk);
const memberIsPromoted = useMemberIsPromoted(pubkey, groupPk); const memberIsNominatedAdmin = useMemberIsNominatedAdmin(pubkey, groupPk);
// When invite-as-admin was used to invite that member, the resend button is available to resend the promote message. // When invite-as-admin was used to invite that member, the resend button is available to resend the promote message.
// We want to show that button only to promote a normal member who accepted a normal invite but wasn't promoted yet. // We want to show that button only to promote a normal member who accepted a normal invite but wasn't promoted yet.
// ^ this is only the case for testing. The UI will be different once we release the promotion process // ^ this is only the case for testing. The UI will be different once we release the promotion process
if (!hasClosedGroupV2QAButtons() || !memberAcceptedInvite || memberIsPromoted) { if (!hasClosedGroupV2QAButtons() || !memberAcceptedInvite || memberIsNominatedAdmin) {
return null; return null;
} }
return ( return (

@ -27,7 +27,10 @@ import { ClosedGroup } from '../../session/group/closed-group';
import { PubKey } from '../../session/types'; import { PubKey } from '../../session/types';
import { hasClosedGroupV2QAButtons } from '../../shared/env_vars'; import { hasClosedGroupV2QAButtons } from '../../shared/env_vars';
import { groupInfoActions } from '../../state/ducks/metaGroups'; import { groupInfoActions } from '../../state/ducks/metaGroups';
import { useMemberGroupChangePending } from '../../state/selectors/groups'; import {
useMemberGroupChangePending,
useStateOf03GroupMembers,
} from '../../state/selectors/groups';
import { useSelectedIsGroupV2 } from '../../state/selectors/selectedConversation'; import { useSelectedIsGroupV2 } from '../../state/selectors/selectedConversation';
import { SessionSpinner } from '../loading'; import { SessionSpinner } from '../loading';
import { SessionToggle } from '../basic/SessionToggle'; import { SessionToggle } from '../basic/SessionToggle';
@ -36,7 +39,7 @@ type Props = {
conversationId: string; conversationId: string;
}; };
const StyledClassicMemberList = styled.div` const StyledMemberList = styled.div`
max-height: 240px; max-height: 240px;
`; `;
@ -44,7 +47,7 @@ const StyledClassicMemberList = styled.div`
* Admins are always put first in the list of group members. * Admins are always put first in the list of group members.
* Also, admins have a little crown on their avatar. * Also, admins have a little crown on their avatar.
*/ */
const ClassicMemberList = (props: { const MemberList = (props: {
convoId: string; convoId: string;
selectedMembers: Array<string>; selectedMembers: Array<string>;
onSelect: (m: string) => void; onSelect: (m: string) => void;
@ -56,12 +59,15 @@ const ClassicMemberList = (props: {
const groupAdmins = useGroupAdmins(convoId); const groupAdmins = useGroupAdmins(convoId);
const groupMembers = useSortedGroupMembers(convoId); const groupMembers = useSortedGroupMembers(convoId);
const groupMembers03Group = useStateOf03GroupMembers(convoId);
const sortedMembers = useMemo( const sortedMembersNon03 = useMemo(
() => [...groupMembers].sort(m => (groupAdmins?.includes(m) ? -1 : 0)), () => [...groupMembers].sort(m => (groupAdmins?.includes(m) ? -1 : 0)),
[groupMembers, groupAdmins] [groupMembers, groupAdmins]
); );
const sortedMembers = isV2Group ? groupMembers03Group.map(m => m.pubkeyHex) : sortedMembersNon03;
return ( return (
<> <>
{sortedMembers.map(member => { {sortedMembers.map(member => {
@ -230,14 +236,14 @@ export const UpdateGroupMembersDialog = (props: Props) => {
/> />
</> </>
) : null} ) : null}
<StyledClassicMemberList className="contact-selection-list"> <StyledMemberList className="contact-selection-list">
<ClassicMemberList <MemberList
convoId={conversationId} convoId={conversationId}
onSelect={onSelect} onSelect={onSelect}
onUnselect={onUnselect} onUnselect={onUnselect}
selectedMembers={membersToRemove} selectedMembers={membersToRemove}
/> />
</StyledClassicMemberList> </StyledMemberList>
{showNoMembersMessage && <p>{window.i18n('groupMembersNone')}</p>} {showNoMembersMessage && <p>{window.i18n('groupMembersNone')}</p>}
<SpacerLG /> <SpacerLG />

@ -125,11 +125,11 @@ async function handleMetaMergeResults(groupPk: GroupPubkeyType) {
} }
} }
// mark ourselves as accepting the invite if needed // mark ourselves as accepting the invite if needed
if (usMember?.invitePending && keysAlreadyHaveAdmin) { if (usMember?.memberStatus === 'INVITE_SENT' && keysAlreadyHaveAdmin) {
await MetaGroupWrapperActions.memberSetAccepted(groupPk, us); await MetaGroupWrapperActions.memberSetAccepted(groupPk, us);
} }
// mark ourselves as accepting the promotion if needed // mark ourselves as accepting the promotion if needed
if (usMember?.promotionPending && keysAlreadyHaveAdmin) { if (usMember?.memberStatus === 'PROMOTION_SENT' && keysAlreadyHaveAdmin) {
await MetaGroupWrapperActions.memberSetPromotionAccepted(groupPk, us); await MetaGroupWrapperActions.memberSetPromotionAccepted(groupPk, us);
} }
// this won't do anything if there is no need for a sync, so we can safely plan one // this won't do anything if there is no need for a sync, so we can safely plan one

@ -327,7 +327,7 @@ class ConvoController {
const us = UserUtils.getOurPubKeyStrFromCache(); const us = UserUtils.getOurPubKeyStrFromCache();
const allMembers = await MetaGroupWrapperActions.memberGetAll(groupPk); const allMembers = await MetaGroupWrapperActions.memberGetAll(groupPk);
const otherAdminsCount = allMembers const otherAdminsCount = allMembers
.filter(m => m.promoted) .filter(m => m.nominatedAdmin)
.filter(m => m.pubkeyHex !== us).length; .filter(m => m.pubkeyHex !== us).length;
const weAreLastAdmin = otherAdminsCount === 0; const weAreLastAdmin = otherAdminsCount === 0;
const infos = await MetaGroupWrapperActions.infoGet(groupPk); const infos = await MetaGroupWrapperActions.infoGet(groupPk);

@ -43,6 +43,7 @@ export interface GroupInvitePersistedData extends PersistedJobData {
groupPk: GroupPubkeyType; groupPk: GroupPubkeyType;
member: PubkeyType; member: PubkeyType;
inviteAsAdmin: boolean; inviteAsAdmin: boolean;
forceUnrevoke: boolean;
} }
export interface GroupPromotePersistedData extends PersistedJobData { export interface GroupPromotePersistedData extends PersistedJobData {

@ -21,6 +21,9 @@ import { LibSessionUtil } from '../../libsession/libsession_utils';
import { showUpdateGroupMembersByConvoId } from '../../../../interactions/conversationInteractions'; import { showUpdateGroupMembersByConvoId } from '../../../../interactions/conversationInteractions';
import { ConvoHub } from '../../../conversations'; import { ConvoHub } from '../../../conversations';
import { MessageQueue } from '../../../sending'; import { MessageQueue } from '../../../sending';
import { NetworkTime } from '../../../../util/NetworkTime';
import { SubaccountUnrevokeSubRequest } from '../../../apis/snode_api/SnodeRequestTypes';
import { GroupSync } from './GroupSyncJob';
const defaultMsBetweenRetries = 10000; const defaultMsBetweenRetries = 10000;
const defaultMaxAttempts = 1; const defaultMaxAttempts = 1;
@ -29,6 +32,12 @@ type JobExtraArgs = {
groupPk: GroupPubkeyType; groupPk: GroupPubkeyType;
member: PubkeyType; member: PubkeyType;
inviteAsAdmin: boolean; inviteAsAdmin: boolean;
/**
* When inviting a member, we usually only want to sent a message to his swarm.
* In the case of a invitation resend process though, we also want to make sure his token is unrevoked from the group's swarm.
*
*/
forceUnrevoke: boolean;
}; };
export function shouldAddJob(args: JobExtraArgs) { export function shouldAddJob(args: JobExtraArgs) {
@ -47,12 +56,13 @@ const invitesFailed = new Map<
} }
>(); >();
async function addJob({ groupPk, member, inviteAsAdmin }: JobExtraArgs) { async function addJob({ groupPk, member, inviteAsAdmin, forceUnrevoke }: JobExtraArgs) {
if (shouldAddJob({ groupPk, member, inviteAsAdmin })) { if (shouldAddJob({ groupPk, member, inviteAsAdmin, forceUnrevoke })) {
const groupInviteJob = new GroupInviteJob({ const groupInviteJob = new GroupInviteJob({
groupPk, groupPk,
member, member,
inviteAsAdmin, inviteAsAdmin,
forceUnrevoke,
nextAttemptTimestamp: Date.now(), nextAttemptTimestamp: Date.now(),
}); });
window.log.debug(`addGroupInviteJob: adding group invite for ${groupPk}:${member} `); window.log.debug(`addGroupInviteJob: adding group invite for ${groupPk}:${member} `);
@ -135,8 +145,9 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
nextAttemptTimestamp, nextAttemptTimestamp,
maxAttempts, maxAttempts,
currentRetry, currentRetry,
forceUnrevoke,
identifier, identifier,
}: Pick<GroupInvitePersistedData, 'groupPk' | 'member' | 'inviteAsAdmin'> & }: Pick<GroupInvitePersistedData, 'groupPk' | 'member' | 'inviteAsAdmin' | 'forceUnrevoke'> &
Partial< Partial<
Pick< Pick<
GroupInvitePersistedData, GroupInvitePersistedData,
@ -153,6 +164,7 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
member, member,
groupPk, groupPk,
inviteAsAdmin, inviteAsAdmin,
forceUnrevoke,
delayBetweenRetries: defaultMsBetweenRetries, delayBetweenRetries: defaultMsBetweenRetries,
maxAttempts: isNumber(maxAttempts) ? maxAttempts : defaultMaxAttempts, maxAttempts: isNumber(maxAttempts) ? maxAttempts : defaultMaxAttempts,
nextAttemptTimestamp: nextAttemptTimestamp || Date.now() + defaultMsBetweenRetries, nextAttemptTimestamp: nextAttemptTimestamp || Date.now() + defaultMsBetweenRetries,
@ -177,6 +189,26 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
} }
let failed = true; let failed = true;
try { try {
if (this.persistedData.forceUnrevoke) {
const token = await MetaGroupWrapperActions.swarmSubAccountToken(groupPk, member);
const unrevokeSubRequest = new SubaccountUnrevokeSubRequest({
groupPk,
revokeTokenHex: [token],
timestamp: NetworkTime.now(),
secretKey: group.secretKey,
});
const sequenceResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({
groupPk,
unrevokeSubRequest,
extraStoreRequests: [],
});
if (sequenceResult !== RunJobResult.Success) {
throw new Error(
'GroupInviteJob: SubaccountUnrevokeSubRequest push() did not return success'
);
}
}
const inviteDetails = inviteAsAdmin const inviteDetails = inviteAsAdmin
? await SnodeGroupSignature.getGroupPromoteMessage({ ? await SnodeGroupSignature.getGroupPromoteMessage({
groupName: group.name, groupName: group.name,

@ -87,7 +87,7 @@ async function checkWeAreAdmin(groupPk: GroupPubkeyType) {
const usInGroup = await MetaGroupWrapperActions.memberGet(groupPk, us); const usInGroup = await MetaGroupWrapperActions.memberGet(groupPk, us);
const inUserGroup = await UserGroupsWrapperActions.getGroup(groupPk); const inUserGroup = await UserGroupsWrapperActions.getGroup(groupPk);
// if the secretKey is not empty AND we are a member of the group, we are a current admin // if the secretKey is not empty AND we are a member of the group, we are a current admin
return Boolean(!isEmpty(inUserGroup?.secretKey) && usInGroup?.promoted); return Boolean(!isEmpty(inUserGroup?.secretKey) && usInGroup?.nominatedAdmin);
} }
async function checkWeAreAdminOrThrow(groupPk: GroupPubkeyType, context: string) { async function checkWeAreAdminOrThrow(groupPk: GroupPubkeyType, context: string) {
@ -243,14 +243,12 @@ const initNewGroupInWrapper = createAsyncThunk(
// privately and asynchronously, and gracefully handle errors with toasts. // privately and asynchronously, and gracefully handle errors with toasts.
// Let's do all of this part of a job to handle app crashes and make sure we // Let's do all of this part of a job to handle app crashes and make sure we
// can update the group wrapper with a failed state if a message fails to be sent. // can update the group wrapper with a failed state if a message fails to be sent.
for (let index = 0; index < membersFromWrapper.length; index++) { await scheduleGroupInviteJobs(
const member = membersFromWrapper[index];
await GroupInvite.addJob({
member: member.pubkeyHex,
groupPk, groupPk,
inviteAsAdmin: window.sessionFeatureFlags.useGroupV2InviteAsAdmin, membersFromWrapper.map(m => m.pubkeyHex),
}); [],
} window.sessionFeatureFlags.useGroupV2InviteAsAdmin
);
await openConversationWithMessages({ conversationKey: groupPk, messageId: null }); await openConversationWithMessages({ conversationKey: groupPk, messageId: null });
@ -1429,6 +1427,8 @@ async function scheduleGroupInviteJobs(
const merged = uniq(concat(withHistory, withoutHistory)); const merged = uniq(concat(withHistory, withoutHistory));
for (let index = 0; index < merged.length; index++) { for (let index = 0; index < merged.length; index++) {
const member = merged[index]; const member = merged[index];
await GroupInvite.addJob({ groupPk, member, inviteAsAdmin }); // Note: forceUnrevoke is false, because `scheduleGroupInviteJobs` is always called after we've done
// a batch unrevoke of all the members' pk
await GroupInvite.addJob({ groupPk, member, inviteAsAdmin, forceUnrevoke: false });
} }
} }

@ -1,8 +1,17 @@
import { GroupMemberGet, GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs'; import {
GroupMemberGet,
GroupPubkeyType,
MemberStateGroupV2,
PubkeyType,
} from 'libsession_util_nodejs';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { compact, concat, differenceBy, sortBy, uniqBy } from 'lodash';
import { PubKey } from '../../session/types'; import { PubKey } from '../../session/types';
import { GroupState } from '../ducks/metaGroups'; import { GroupState } from '../ducks/metaGroups';
import { StateType } from '../reducer'; import { StateType } from '../reducer';
import { assertUnreachable } from '../../types/sqlSharedTypes';
import { UserUtils } from '../../session/utils';
import { useConversationsNicknameRealNameOrShortenPubkey } from '../../hooks/useParamSelector';
const getLibGroupsState = (state: StateType): GroupState => state.groups; const getLibGroupsState = (state: StateType): GroupState => state.groups;
const getInviteSendingState = (state: StateType) => getLibGroupsState(state).membersInviteSending; const getInviteSendingState = (state: StateType) => getLibGroupsState(state).membersInviteSending;
@ -44,48 +53,57 @@ function getGroupNameChangeFromUIPending(state: StateType): boolean {
export function getLibAdminsPubkeys(state: StateType, convo?: string): Array<string> { export function getLibAdminsPubkeys(state: StateType, convo?: string): Array<string> {
const members = getMembersOfGroup(state, convo); const members = getMembersOfGroup(state, convo);
return members.filter(m => m.promoted).map(m => m.pubkeyHex); return members.filter(m => m.nominatedAdmin).map(m => m.pubkeyHex);
} }
function getMemberInviteFailed(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) { function getMemberInviteFailed(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) {
const members = getMembersOfGroup(state, convo); const members = getMembersOfGroup(state, convo);
return findMemberInMembers(members, pubkey)?.inviteFailed || false; return findMemberInMembers(members, pubkey)?.memberStatus === 'INVITE_FAILED' || false;
} }
function getMemberInviteNotSent(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) { function getMemberInviteNotSent(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) {
const members = getMembersOfGroup(state, convo); const members = getMembersOfGroup(state, convo);
return findMemberInMembers(members, pubkey)?.inviteNotSent || false; return findMemberInMembers(members, pubkey)?.memberStatus === 'INVITE_NOT_SENT' || false;
} }
function getMemberInviteSent(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) { function getMemberInviteSent(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) {
const members = getMembersOfGroup(state, convo); const members = getMembersOfGroup(state, convo);
return findMemberInMembers(members, pubkey)?.invitePending || false; return findMemberInMembers(members, pubkey)?.memberStatus === 'INVITE_SENT' || false;
} }
function getMemberIsPromoted(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) { function getMemberHasAcceptedPromotion(
state: StateType,
pubkey: PubkeyType,
convo?: GroupPubkeyType
) {
const members = getMembersOfGroup(state, convo); const members = getMembersOfGroup(state, convo);
return findMemberInMembers(members, pubkey)?.promoted || false; return findMemberInMembers(members, pubkey)?.memberStatus === 'PROMOTION_ACCEPTED' || false;
}
function getMemberIsNominatedAdmin(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) {
const members = getMembersOfGroup(state, convo);
return findMemberInMembers(members, pubkey)?.nominatedAdmin || false;
} }
function getMemberHasAcceptedInvite(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) { function getMemberHasAcceptedInvite(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) {
const members = getMembersOfGroup(state, convo); const members = getMembersOfGroup(state, convo);
return findMemberInMembers(members, pubkey)?.inviteAccepted || false; return findMemberInMembers(members, pubkey)?.memberStatus === 'INVITE_ACCEPTED' || false;
} }
function getMemberPromotionFailed(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) { function getMemberPromotionFailed(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) {
const members = getMembersOfGroup(state, convo); const members = getMembersOfGroup(state, convo);
return findMemberInMembers(members, pubkey)?.promotionFailed || false; return findMemberInMembers(members, pubkey)?.memberStatus === 'PROMOTION_FAILED' || false;
} }
function getMemberPromotionSent(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) { function getMemberPromotionSent(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) {
const members = getMembersOfGroup(state, convo); const members = getMembersOfGroup(state, convo);
return findMemberInMembers(members, pubkey)?.promotionPending || false; return findMemberInMembers(members, pubkey)?.memberStatus === 'PROMOTION_SENT' || false;
} }
function getMemberPromotionNotSent(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) { function getMemberPromotionNotSent(state: StateType, pubkey: PubkeyType, convo?: GroupPubkeyType) {
const members = getMembersOfGroup(state, convo); const members = getMembersOfGroup(state, convo);
return findMemberInMembers(members, pubkey)?.promotionNotSent || false; return findMemberInMembers(members, pubkey)?.memberStatus === 'PROMOTION_NOT_SENT' || false;
} }
export function getLibMembersCount(state: StateType, convo?: GroupPubkeyType): Array<string> { export function getLibMembersCount(state: StateType, convo?: GroupPubkeyType): Array<string> {
@ -155,9 +173,14 @@ export function useMemberInviteNotSent(member: PubkeyType, groupPk: GroupPubkeyT
return useSelector((state: StateType) => getMemberInviteNotSent(state, member, groupPk)); return useSelector((state: StateType) => getMemberInviteNotSent(state, member, groupPk));
} }
export function useMemberIsPromoted(member: PubkeyType, groupPk: GroupPubkeyType) { export function useMemberHasAcceptedPromotion(member: PubkeyType, groupPk: GroupPubkeyType) {
return useSelector((state: StateType) => getMemberIsPromoted(state, member, groupPk)); return useSelector((state: StateType) => getMemberHasAcceptedPromotion(state, member, groupPk));
}
export function useMemberIsNominatedAdmin(member: PubkeyType, groupPk: GroupPubkeyType) {
return useSelector((state: StateType) => getMemberIsNominatedAdmin(state, member, groupPk));
} }
export function useMemberHasAcceptedInvite(member: PubkeyType, groupPk: GroupPubkeyType) { export function useMemberHasAcceptedInvite(member: PubkeyType, groupPk: GroupPubkeyType) {
return useSelector((state: StateType) => getMemberHasAcceptedInvite(state, member, groupPk)); return useSelector((state: StateType) => getMemberHasAcceptedInvite(state, member, groupPk));
} }
@ -188,18 +211,128 @@ export function useGroupNameChangeFromUIPending() {
* An example is the "sending invite" or "sending promote" state of a member in a group. * An example is the "sending invite" or "sending promote" state of a member in a group.
*/ */
function useMembersInviteSending(groupPk: GroupPubkeyType) { function useMembersInviteSending(groupPk?: string) {
return useSelector((state: StateType) => getInviteSendingState(state)[groupPk] || []); return useSelector((state: StateType) =>
groupPk && PubKey.is03Pubkey(groupPk) ? getInviteSendingState(state)[groupPk] || [] : []
);
} }
export function useMemberInviteSending(groupPk: GroupPubkeyType, memberPk: PubkeyType) { export function useMemberInviteSending(groupPk: GroupPubkeyType, memberPk: PubkeyType) {
return useMembersInviteSending(groupPk).includes(memberPk); return useMembersInviteSending(groupPk).includes(memberPk);
} }
function useMembersPromoteSending(groupPk: GroupPubkeyType) { function useMembersPromoteSending(groupPk?: string) {
return useSelector((state: StateType) => getPromoteSendingState(state)[groupPk] || []); return useSelector((state: StateType) =>
groupPk && PubKey.is03Pubkey(groupPk) ? getPromoteSendingState(state)[groupPk] || [] : []
);
} }
export function useMemberPromoteSending(groupPk: GroupPubkeyType, memberPk: PubkeyType) { export function useMemberPromoteSending(groupPk: GroupPubkeyType, memberPk: PubkeyType) {
return useMembersPromoteSending(groupPk).includes(memberPk); return useMembersPromoteSending(groupPk).includes(memberPk);
} }
type MemberStateGroupV2WithSending = MemberStateGroupV2 | 'INVITE_SENDING' | 'PROMOTION_SENDING';
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 = compact(
invitesSendingPk.map(sending => unsortedMembers.find(m => m.pubkeyHex === sending))
);
const promotionSending = compact(
promotionsSendingPk.map(sending => unsortedMembers.find(m => m.pubkeyHex === sending))
);
// 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'
);
}
// 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<GroupMemberGet, 'pubkeyHex'> & { memberStatus: MemberStateGroupV2WithSending }
> = [];
unsortedWithStatuses.push(...promotionSending);
unsortedWithStatuses.push(...differenceBy(invitesSending, promotionSending));
unsortedWithStatuses.push(...differenceBy(unsortedMembers, invitesSending, promotionSending));
const names = useConversationsNicknameRealNameOrShortenPubkey(
unsortedWithStatuses.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 = -5;
break;
case 'INVITE_SENDING':
stateSortingOrder = -4;
break;
case 'INVITE_SENT':
stateSortingOrder = -3;
break;
case 'PROMOTION_FAILED':
case 'PROMOTION_NOT_SENT':
stateSortingOrder = -2;
break;
case 'PROMOTION_SENDING':
stateSortingOrder = -1;
break;
case 'PROMOTION_SENT':
stateSortingOrder = 0;
break;
case 'PROMOTION_ACCEPTED':
stateSortingOrder = 1;
break;
case 'INVITE_ACCEPTED':
stateSortingOrder = 2;
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;
});
return sorted;
}

@ -18,20 +18,14 @@ function profilePicture() {
function emptyMember(pubkeyHex: PubkeyType): GroupMemberGet { function emptyMember(pubkeyHex: PubkeyType): GroupMemberGet {
return { return {
inviteFailed: false, memberStatus: 'INVITE_NOT_SENT',
invitePending: false,
name: '', name: '',
profilePicture: { profilePicture: {
key: null, key: null,
url: null, url: null,
}, },
promoted: false,
promotionFailed: false,
promotionPending: false,
inviteAccepted: false,
inviteNotSent: false,
isRemoved: false, isRemoved: false,
promotionNotSent: false, nominatedAdmin: false,
shouldRemoveMessages: false, shouldRemoveMessages: false,
pubkeyHex, pubkeyHex,
}; };
@ -271,10 +265,8 @@ describe('libsession_metagroup', () => {
expect(metaGroupWrapper.memberGetAll().length).to.be.deep.eq(1); expect(metaGroupWrapper.memberGetAll().length).to.be.deep.eq(1);
const expected: GroupMemberGet = { const expected: GroupMemberGet = {
...emptyMember(member), ...emptyMember(member),
promoted: true, nominatedAdmin: true,
promotionFailed: false, memberStatus: 'PROMOTION_ACCEPTED',
promotionPending: false,
promotionNotSent: false,
}; };
expect(metaGroupWrapper.memberGetAll()[0]).to.be.deep.eq(expected); expect(metaGroupWrapper.memberGetAll()[0]).to.be.deep.eq(expected);

Loading…
Cancel
Save