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.", "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.", "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.", "groupOneLeft": "<b>$name$</b> left the group.",
"groupYouLeft": "<b>You</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 React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { Flex } from './Flex'; import { Flex } from './Flex';
import { useConversationsUsernameWithQuoteOrShortPk } from '../../hooks/useParamSelector';
import { SessionIcon, SessionIconType } from '../icon'; 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 // NOTE We don't change the color strip on the left based on the type. 16/09/2022
export enum SessionToastType { export enum SessionToastType {
@ -46,6 +48,29 @@ const IconDiv = styled.div`
margin: 0 var(--margins-xs); 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) => { export const SessionToast = (props: Props) => {
const { title, description, type, icon } = props; const { title, description, type, icon } = props;
@ -93,7 +118,7 @@ export const SessionToast = (props: Props) => {
className="session-toast" className="session-toast"
> >
<TitleDiv>{title}</TitleDiv> <TitleDiv>{title}</TitleDiv>
{toastDesc && <DescriptionDiv>{toastDesc}</DescriptionDiv>} {toastDesc && <DescriptionPubkeysReplaced description={toastDesc} />}
</Flex> </Flex>
</Flex> </Flex>
); );

@ -60,18 +60,44 @@ export function useConversationRealName(convoId?: string) {
return convoProps?.isPrivate ? convoProps?.displayNameInProfile : undefined; 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 * Returns either the nickname, the profileName, in '"' or the full pubkeys given
*/ */
export function useConversationsUsernameWithQuoteOrFullPubkey(pubkeys: Array<string>) { export function useConversationsUsernameWithQuoteOrFullPubkey(pubkeys: Array<string>) {
return useSelector((state: StateType) => { return useSelector((state: StateType) => {
return pubkeys.map(pubkey => { return pubkeys.map(pubkey => {
if (pubkey === UserUtils.getOurPubKeyStrFromCache() || pubkey.toLowerCase() === 'you') { const nameGot = usernameForQuoteOrFullPk(pubkey, state);
return window.i18n('you'); return nameGot?.length ? nameGot : pubkey;
} });
const convo = state.conversations.conversationLookup[pubkey]; });
const nameGot = convo?.displayNameInProfile; }
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 { ClosedGroup, getMessageQueue } from '..';
import { ConversationTypeEnum } from '../../models/conversationAttributes'; import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { addKeyPairToCacheAndDBIfNeeded } from '../../receiver/closedGroups'; import { addKeyPairToCacheAndDBIfNeeded } from '../../receiver/closedGroups';
@ -119,7 +119,9 @@ async function sendToGroupMembers(
window?.log?.info(`Sending invites for group ${groupPublicKey} to ${listOfMembers}`); 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 // evaluating if all invites sent, if failed give the option to retry failed invites via modal dialog
const inviteResults = await Promise.all(promises); 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 (allInvitesSent) {
// if (true) { // if (true) {
@ -157,7 +159,7 @@ async function sendToGroupMembers(
inviteResults.forEach((result, index) => { inviteResults.forEach((result, index) => {
const member = listOfMembers[index]; const member = listOfMembers[index];
// group invite must always contain the admin member. // group invite must always contain the admin member.
if (result !== true || admins.includes(member)) { if (result === null || admins.includes(member)) {
membersToResend.push(member); membersToResend.push(member);
} }
}); });

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

@ -1,8 +1,11 @@
import { GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs'; import { GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs';
import { isNumber } from 'lodash'; import { debounce, difference, isNumber } from 'lodash';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { UserUtils } from '../..'; import { ToastUtils, UserUtils } from '../..';
import { UserGroupsWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface'; import {
MetaGroupWrapperActions,
UserGroupsWrapperActions,
} from '../../../../webworker/workers/browser/libsession_worker_interface';
import { SnodeNamespaces } from '../../../apis/snode_api/namespaces'; import { SnodeNamespaces } from '../../../apis/snode_api/namespaces';
import { SnodeGroupSignature } from '../../../apis/snode_api/signature/groupSignature'; import { SnodeGroupSignature } from '../../../apis/snode_api/signature/groupSignature';
import { getMessageQueue } from '../../../sending'; import { getMessageQueue } from '../../../sending';
@ -31,6 +34,14 @@ export function shouldAddGroupInviteJob(args: JobExtraArgs) {
return true; return true;
} }
const invitesFailed = new Map<
GroupPubkeyType,
{
debouncedCall: (groupPk: GroupPubkeyType) => void;
failedMembers: Array<PubkeyType>;
}
>();
async function addGroupInviteJob({ groupPk, member }: JobExtraArgs) { async function addGroupInviteJob({ groupPk, member }: JobExtraArgs) {
if (shouldAddGroupInviteJob({ groupPk, member })) { if (shouldAddGroupInviteJob({ groupPk, member })) {
const groupInviteJob = new GroupInviteJob({ 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> { class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
constructor({ constructor({
groupPk, groupPk,
@ -75,12 +122,12 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
} }
public async run(): Promise<RunJobResult> { public async run(): Promise<RunJobResult> {
const { groupPk, member } = this.persistedData; const { groupPk, member, jobType, identifier } = this.persistedData;
window.log.info( 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) { if (!group || !group.secretKey || !group.name) {
window.log.warn(`GroupInviteJob: Did not find group in wrapper or no valid info in wrapper`); window.log.warn(`GroupInviteJob: Did not find group in wrapper or no valid info in wrapper`);
return RunJobResult.PermanentFailure; return RunJobResult.PermanentFailure;
@ -89,25 +136,31 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
if (UserUtils.isUsFromCache(member)) { if (UserUtils.isUsFromCache(member)) {
return RunJobResult.Success; // nothing to do for us, we get the update from our user's libsession wrappers return RunJobResult.Success; // nothing to do for us, we get the update from our user's libsession wrappers
} }
let failed = true;
const inviteDetails = await SnodeGroupSignature.getGroupInviteMessage({ try {
groupName: group.name, const inviteDetails = await SnodeGroupSignature.getGroupInviteMessage({
member, groupName: group.name,
secretKey: group.secretKey, member,
groupPk, secretKey: group.secretKey,
}); groupPk,
if (!inviteDetails) { });
window.log.warn(`GroupInviteJob: Did not find group in wrapper or no valid info in wrapper`);
const storedAt = await getMessageQueue().sendToPubKeyNonDurably({
return RunJobResult.PermanentFailure; 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 true so this job is marked as a success and we don't need to retry it
return RunJobResult.Success; return RunJobResult.Success;
} }
@ -142,3 +195,29 @@ export const GroupInvite = {
GroupInviteJob, GroupInviteJob,
addGroupInviteJob, 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' | 'goToReleaseNotes'
| 'goToSupportPage' | 'goToSupportPage'
| 'groupAvatarChange' | 'groupAvatarChange'
| 'groupInviteFailedOne'
| 'groupInviteFailedOthers'
| 'groupInviteFailedTwo'
| 'groupMembers' | 'groupMembers'
| 'groupNameChange' | 'groupNameChange'
| 'groupNameChangeFallback' | 'groupNameChangeFallback'
@ -230,6 +233,7 @@ export type LocalizerKeys =
| 'invalidPubkeyFormat' | 'invalidPubkeyFormat'
| 'invalidSessionId' | 'invalidSessionId'
| 'inviteContacts' | 'inviteContacts'
| 'inviteFailed'
| 'join' | 'join'
| 'joinACommunity' | 'joinACommunity'
| 'joinOpenGroup' | 'joinOpenGroup'

Loading…
Cancel
Save