feat: add invite failed toast debounced

also make the toast replace pubkeys with nicknames/names or shortened
pks
pull/2963/head
Audric Ackermann 2 years ago
parent f17beaf852
commit 1dbcd157a0

@ -258,6 +258,11 @@
"groupTwoPromoted": "<b>$first$</b> and <b>$second$</b> were promoted to Admin.",
"groupOthersPromoted": "<b>$name$</b> and <b>$count$ others</b> were promoted to Admin.",
"inviteFailed": "Invite Failed",
"groupInviteFailedOne": "Failed to invite <b>$name$</b> to <b>$groupname$</b>",
"groupInviteFailedTwo": "Failed to invite <b>$first$</b> and <b>$second$</b> to <b>$groupname$</b>",
"groupInviteFailedOthers": "Failed to invite <b>$first$</b> and <b>$count$ others</b> to <b>$groupname$</b>",
"groupOneLeft": "<b>$name$</b> left the group.",
"groupYouLeft": "<b>You</b> left the group.",

@ -1,10 +1,12 @@
import { noop } from 'lodash';
import { clone, noop } from 'lodash';
import React from 'react';
import styled from 'styled-components';
import { Flex } from './Flex';
import { useConversationsUsernameWithQuoteOrShortPk } from '../../hooks/useParamSelector';
import { SessionIcon, SessionIconType } from '../icon';
import { SessionHtmlRenderer } from './SessionHTMLRenderer';
// NOTE We don't change the color strip on the left based on the type. 16/09/2022
export enum SessionToastType {
@ -46,6 +48,29 @@ const IconDiv = styled.div`
margin: 0 var(--margins-xs);
`;
function useReplacePkInTextWithNames(description: string) {
const pubkeysToLookup = [...description.matchAll(/0[3,5][0-9a-fA-F]{64}/g)] || [];
const memberNames = useConversationsUsernameWithQuoteOrShortPk(pubkeysToLookup.map(m => m[0]));
let replacedWithNames = clone(description);
for (let index = 0; index < memberNames.length; index++) {
const name = memberNames[index];
const pk = pubkeysToLookup[index][0];
replacedWithNames = replacedWithNames.replace(pk, name);
}
return replacedWithNames;
}
function DescriptionPubkeysReplaced({ description }: { description: string }) {
const replacedWithNames = useReplacePkInTextWithNames(description);
return (
<DescriptionDiv>
<SessionHtmlRenderer html={replacedWithNames} />
</DescriptionDiv>
);
}
export const SessionToast = (props: Props) => {
const { title, description, type, icon } = props;
@ -93,7 +118,7 @@ export const SessionToast = (props: Props) => {
className="session-toast"
>
<TitleDiv>{title}</TitleDiv>
{toastDesc && <DescriptionDiv>{toastDesc}</DescriptionDiv>}
{toastDesc && <DescriptionPubkeysReplaced description={toastDesc} />}
</Flex>
</Flex>
);

@ -60,18 +60,44 @@ export function useConversationRealName(convoId?: string) {
return convoProps?.isPrivate ? convoProps?.displayNameInProfile : undefined;
}
function usernameForQuoteOrFullPk(pubkey: string, state: StateType) {
if (pubkey === UserUtils.getOurPubKeyStrFromCache() || pubkey.toLowerCase() === 'you') {
return window.i18n('you');
}
// use the name from the cached libsession wrappers if available
if (PubKey.isClosedGroupV2(pubkey)) {
const info = state.groups.infos[pubkey];
if (info && info.name) {
return info.name;
}
}
const convo = state.conversations.conversationLookup[pubkey];
const nameGot = convo?.nickname || convo?.displayNameInProfile;
return nameGot?.length ? nameGot : null;
}
/**
* Returns either the nickname, the profileName, in '"' or the full pubkeys given
*/
export function useConversationsUsernameWithQuoteOrFullPubkey(pubkeys: Array<string>) {
return useSelector((state: StateType) => {
return pubkeys.map(pubkey => {
if (pubkey === UserUtils.getOurPubKeyStrFromCache() || pubkey.toLowerCase() === 'you') {
return window.i18n('you');
}
const convo = state.conversations.conversationLookup[pubkey];
const nameGot = convo?.displayNameInProfile;
return nameGot?.length ? `"${nameGot}"` : pubkey;
const nameGot = usernameForQuoteOrFullPk(pubkey, state);
return nameGot?.length ? nameGot : pubkey;
});
});
}
/**
* Returns either the nickname, the profileName, a shortened pubkey, or "you" for our own pubkey
*/
export function useConversationsUsernameWithQuoteOrShortPk(pubkeys: Array<string>) {
return useSelector((state: StateType) => {
return pubkeys.map(pubkey => {
const nameGot = usernameForQuoteOrFullPk(pubkey, state);
return nameGot?.length ? nameGot : PubKey.shorten(pubkey);
});
});
}

@ -1,4 +1,4 @@
import _ from 'lodash';
import _, { isFinite, isNumber } from 'lodash';
import { ClosedGroup, getMessageQueue } from '..';
import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { addKeyPairToCacheAndDBIfNeeded } from '../../receiver/closedGroups';
@ -119,7 +119,9 @@ async function sendToGroupMembers(
window?.log?.info(`Sending invites for group ${groupPublicKey} to ${listOfMembers}`);
// evaluating if all invites sent, if failed give the option to retry failed invites via modal dialog
const inviteResults = await Promise.all(promises);
const allInvitesSent = _.every(inviteResults, inviteResult => inviteResult !== false);
const allInvitesSent = _.every(inviteResults, inviteResult => {
return isNumber(inviteResult) && isFinite(inviteResult);
});
if (allInvitesSent) {
// if (true) {
@ -157,7 +159,7 @@ async function sendToGroupMembers(
inviteResults.forEach((result, index) => {
const member = listOfMembers[index];
// group invite must always contain the admin member.
if (result !== true || admins.includes(member)) {
if (result === null || admins.includes(member)) {
membersToResend.push(member);
}
});

@ -256,7 +256,7 @@ export class MessageQueue {
| ClosedGroupMemberLeftMessage
| GroupUpdateInviteMessage;
namespace: SnodeNamespaces;
}): Promise<boolean | number> {
}): Promise<number | null> {
let rawMessage;
try {
rawMessage = await MessageUtils.toRawMessage(pubkey, message, namespace);
@ -271,7 +271,7 @@ export class MessageQueue {
if (rawMessage) {
await MessageSentHandler.handleMessageSentFailure(rawMessage, error);
}
return false;
return null;
}
}

@ -1,8 +1,11 @@
import { GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs';
import { isNumber } from 'lodash';
import { debounce, difference, isNumber } from 'lodash';
import { v4 } from 'uuid';
import { UserUtils } from '../..';
import { UserGroupsWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface';
import { ToastUtils, UserUtils } from '../..';
import {
MetaGroupWrapperActions,
UserGroupsWrapperActions,
} from '../../../../webworker/workers/browser/libsession_worker_interface';
import { SnodeNamespaces } from '../../../apis/snode_api/namespaces';
import { SnodeGroupSignature } from '../../../apis/snode_api/signature/groupSignature';
import { getMessageQueue } from '../../../sending';
@ -31,6 +34,14 @@ export function shouldAddGroupInviteJob(args: JobExtraArgs) {
return true;
}
const invitesFailed = new Map<
GroupPubkeyType,
{
debouncedCall: (groupPk: GroupPubkeyType) => void;
failedMembers: Array<PubkeyType>;
}
>();
async function addGroupInviteJob({ groupPk, member }: JobExtraArgs) {
if (shouldAddGroupInviteJob({ groupPk, member })) {
const groupInviteJob = new GroupInviteJob({
@ -43,6 +54,42 @@ async function addGroupInviteJob({ groupPk, member }: JobExtraArgs) {
}
}
function displayFailedInvitesForGroup(groupPk: GroupPubkeyType) {
const thisGroupFailures = invitesFailed.get(groupPk);
if (!thisGroupFailures || thisGroupFailures.failedMembers.length === 0) {
return;
}
const count = thisGroupFailures.failedMembers.length;
switch (count) {
case 1:
ToastUtils.pushToastWarning(
`invite-failed${groupPk}`,
window.i18n('inviteFailed'),
window.i18n('groupInviteFailedOne', [...thisGroupFailures.failedMembers, groupPk])
);
break;
case 2:
ToastUtils.pushToastWarning(
`invite-failed${groupPk}`,
window.i18n('inviteFailed'),
window.i18n('groupInviteFailedTwo', [...thisGroupFailures.failedMembers, groupPk])
);
break;
default:
ToastUtils.pushToastWarning(
`invite-failed${groupPk}`,
window.i18n('inviteFailed'),
window.i18n('groupInviteFailedOthers', [
thisGroupFailures.failedMembers[0],
`${thisGroupFailures.failedMembers.length - 1}`,
groupPk,
])
);
}
// toast was displayed empty the list
thisGroupFailures.failedMembers = [];
}
class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
constructor({
groupPk,
@ -75,12 +122,12 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
}
public async run(): Promise<RunJobResult> {
const { groupPk, member } = this.persistedData;
const { groupPk, member, jobType, identifier } = this.persistedData;
window.log.info(
`running job ${this.persistedData.jobType} with groupPk:"${groupPk}" member: ${member} id:"${this.persistedData.identifier}" `
`running job ${jobType} with groupPk:"${groupPk}" member: ${member} id:"${identifier}" `
);
const group = await UserGroupsWrapperActions.getGroup(this.persistedData.groupPk);
const group = await UserGroupsWrapperActions.getGroup(groupPk);
if (!group || !group.secretKey || !group.name) {
window.log.warn(`GroupInviteJob: Did not find group in wrapper or no valid info in wrapper`);
return RunJobResult.PermanentFailure;
@ -89,25 +136,31 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
if (UserUtils.isUsFromCache(member)) {
return RunJobResult.Success; // nothing to do for us, we get the update from our user's libsession wrappers
}
const inviteDetails = await SnodeGroupSignature.getGroupInviteMessage({
groupName: group.name,
member,
secretKey: group.secretKey,
groupPk,
});
if (!inviteDetails) {
window.log.warn(`GroupInviteJob: Did not find group in wrapper or no valid info in wrapper`);
return RunJobResult.PermanentFailure;
let failed = true;
try {
const inviteDetails = await SnodeGroupSignature.getGroupInviteMessage({
groupName: group.name,
member,
secretKey: group.secretKey,
groupPk,
});
const storedAt = await getMessageQueue().sendToPubKeyNonDurably({
message: inviteDetails,
namespace: SnodeNamespaces.Default,
pubkey: PubKey.cast(member),
});
if (storedAt !== null) {
failed = false;
}
} finally {
updateFailedStateForMember(groupPk, member, failed);
try {
await MetaGroupWrapperActions.memberSetInvited(groupPk, member, failed);
} catch (e) {
window.log.warn('GroupInviteJob memberSetInvited failed with', e.message);
}
}
await getMessageQueue().sendToPubKeyNonDurably({
message: inviteDetails,
namespace: SnodeNamespaces.Default,
pubkey: PubKey.cast(member),
});
// return true so this job is marked as a success and we don't need to retry it
return RunJobResult.Success;
}
@ -142,3 +195,29 @@ export const GroupInvite = {
GroupInviteJob,
addGroupInviteJob,
};
function updateFailedStateForMember(groupPk: GroupPubkeyType, member: PubkeyType, failed: boolean) {
let thisGroupFailure = invitesFailed.get(groupPk);
if (!failed) {
// invite sent success, remove a pending failure state from the list of toasts to display
if (thisGroupFailure) {
thisGroupFailure.failedMembers = difference(thisGroupFailure.failedMembers, [member]);
}
return;
}
// invite sent failed, append the member to that groupFailure member list, and trigger the debounce call
if (!thisGroupFailure) {
thisGroupFailure = {
failedMembers: [],
debouncedCall: debounce(displayFailedInvitesForGroup, 1000), // TODO change to 5000
};
}
if (!thisGroupFailure.failedMembers.includes(member)) {
thisGroupFailure.failedMembers.push(member);
}
invitesFailed.set(groupPk, thisGroupFailure);
thisGroupFailure.debouncedCall(groupPk);
}

@ -190,6 +190,9 @@ export type LocalizerKeys =
| 'goToReleaseNotes'
| 'goToSupportPage'
| 'groupAvatarChange'
| 'groupInviteFailedOne'
| 'groupInviteFailedOthers'
| 'groupInviteFailedTwo'
| 'groupMembers'
| 'groupNameChange'
| 'groupNameChangeFallback'
@ -230,6 +233,7 @@ export type LocalizerKeys =
| 'invalidPubkeyFormat'
| 'invalidSessionId'
| 'inviteContacts'
| 'inviteFailed'
| 'join'
| 'joinACommunity'
| 'joinOpenGroup'

Loading…
Cancel
Save