feat: force group msg request unread when handling invite

pull/2963/head
Audric Ackermann 2 years ago
parent 4357f5cc1f
commit 77fdc97bcf

@ -5,9 +5,11 @@ import {
approveConvoAndSendResponse,
declineConversationWithConfirm,
} from '../../interactions/conversationInteractions';
import { GroupV2Receiver } from '../../receiver/groupv2/handleGroupV2Message';
import { getSwarmPollingInstance } from '../../session/apis/snode_api/swarmPolling';
import { ConvoHub } from '../../session/conversations';
import { PubKey } from '../../session/types';
import { sleepFor } from '../../session/utils/Promise';
import {
useSelectedConversationIdOrigin,
useSelectedConversationKey,
@ -94,7 +96,12 @@ const handleAcceptConversationRequest = async (convoId: string) => {
}
// this updates the wrapper and refresh the redux slice
await UserGroupsWrapperActions.setGroup({ ...found, invitePending: false });
getSwarmPollingInstance().addGroupId(convoId);
getSwarmPollingInstance().addGroupId(convoId, async () => {
// we need to do a first poll to fetch the keys etc before we can send our invite response
// this is pretty hacky, but also an admin seeing a message from that user in the group will mark it as not pending anymore
await sleepFor(2000);
await GroupV2Receiver.sendInviteResponseToGroup({ groupPk: convoId });
});
}
};

@ -22,6 +22,7 @@ import {
import { useLibGroupAdmins, useLibGroupMembers, useLibGroupName } from '../state/selectors/groups';
import { isPrivateAndFriend } from '../state/selectors/selectedConversation';
import { useOurPkStr } from '../state/selectors/user';
import { useLibGroupInvitePending } from '../state/selectors/userGroups';
export function useAvatarPath(convoId: string | undefined) {
const convoProps = useConversationPropsById(convoId);
@ -235,6 +236,7 @@ export function useIsApproved(convoId?: string) {
export function useIsIncomingRequest(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
const invitePending = useLibGroupInvitePending(convoId) || false;
if (!convoProps) {
return false;
}
@ -248,6 +250,7 @@ export function useIsIncomingRequest(convoId?: string) {
isBlocked: convoProps.isBlocked || false,
didApproveMe: convoProps.didApproveMe || false,
activeAt: convoProps.activeAt || 0,
invitePending,
})
);
}

@ -132,6 +132,7 @@ import { DisappearingMessageConversationModeType } from '../session/disappearing
import { FetchMsgExpirySwarm } from '../session/utils/job_runners/jobs/FetchMsgExpirySwarmJob';
import { UpdateMsgExpirySwarm } from '../session/utils/job_runners/jobs/UpdateMsgExpirySwarmJob';
import { ReleasedFeatures } from '../util/releaseFeature';
import { UserGroupsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface';
import { markAttributesAsReadIfNeeded } from './messageFactory';
type InMemoryConvoInfos = {
@ -692,14 +693,19 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
* Does this conversation contain the properties to be considered a message request
*/
public isIncomingRequest(): boolean {
const id = this.id;
const invitePending = PubKey.is03Pubkey(id)
? UserGroupsWrapperActions.getCachedGroup(id)?.invitePending || false
: false;
return hasValidIncomingRequestValues({
id: this.id,
id,
isMe: this.isMe(),
isApproved: this.isApproved(),
isBlocked: this.isBlocked(),
isPrivate: this.isPrivate(),
activeAt: this.getActiveAt(),
didApproveMe: this.didApproveMe(),
invitePending,
});
}
@ -2708,6 +2714,7 @@ export function hasValidIncomingRequestValues({
isPrivate,
activeAt,
didApproveMe,
invitePending,
}: {
id: string;
isMe: boolean;
@ -2715,12 +2722,13 @@ export function hasValidIncomingRequestValues({
isBlocked: boolean;
isPrivate: boolean;
didApproveMe: boolean;
invitePending: boolean;
activeAt: number | undefined;
}): boolean {
// if a convo is not active, it means we didn't get any messages nor sent any.
const isActive = activeAt && isFinite(activeAt) && activeAt > 0;
return Boolean(
(isPrivate || PubKey.is03Pubkey(id)) &&
(isPrivate || (PubKey.is03Pubkey(id) && invitePending)) &&
!isMe &&
!isApproved &&
!isBlocked &&

@ -18,6 +18,7 @@ import { stringToUint8Array } from '../../session/utils/String';
import { PreConditionFailed } from '../../session/utils/errors';
import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob';
import { LibSessionUtil } from '../../session/utils/libsession/libsession_utils';
import { SessionUtilConvoInfoVolatile } from '../../session/utils/libsession/libsession_utils_convo_info_volatile';
import { groupInfoActions } from '../../state/ducks/metaGroups';
import { toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes';
import { BlockedNumberController } from '../../util';
@ -43,7 +44,14 @@ type GroupUpdateDetails = {
updateMessage: SignalService.GroupUpdateMessage;
} & WithEnvelopeTimestamp;
/**
* Send the invite response to the group's swarm. An admin will handle it and update our invite pending state to not pending.
* NOTE:
* This message can only be sent once we got the keys for the group, through a poll of the swarm.
*/
async function sendInviteResponseToGroup({ groupPk }: { groupPk: GroupPubkeyType }) {
window.log.info(`sendInviteResponseToGroup for group ${ed25519Str(groupPk)}`);
await getMessageQueue().sendToGroupV2({
message: new GroupUpdateInviteResponseMessage({
groupPk,
@ -53,10 +61,6 @@ async function sendInviteResponseToGroup({ groupPk }: { groupPk: GroupPubkeyType
expireTimer: 0,
}),
});
// TODO use the pending so we actually don't start polling here unless it is not in the pending state.
// once everything is ready, start polling using that authData to get the keys, members, details of that group, and its messages.
getSwarmPollingInstance().addGroupId(groupPk);
}
async function handleGroupInviteMessage({
@ -92,6 +96,7 @@ async function handleGroupInviteMessage({
}
window.log.debug(`received invite to group ${ed25519Str(groupPk)} by user:${ed25519Str(author)}`);
const convo = await ConvoHub.use().getOrCreateAndWait(groupPk, ConversationTypeEnum.GROUPV2);
convo.set({
active_at: envelopeTimestamp,
@ -104,7 +109,6 @@ async function handleGroupInviteMessage({
displayNameInProfile: inviteMessage.name,
});
}
await convo.commit();
const userEd25519Secretkey = (await UserUtils.getUserED25519KeyPairBytes()).privKeyBytes;
let found = await UserGroupsWrapperActions.getGroup(groupPk);
@ -131,6 +135,12 @@ async function handleGroupInviteMessage({
found.authData = inviteMessage.memberAuthData;
await UserGroupsWrapperActions.setGroup(found);
// force markedAsUnread to be true so it shows the unread banner (we only show the banner if there are unread messages on at least one msg/group request)
await convo.markAsUnread(true, false);
await convo.commit();
await SessionUtilConvoInfoVolatile.insertConvoFromDBIntoWrapperAndRefresh(convo.id);
await MetaGroupWrapperActions.init(groupPk, {
metaDumped: null,
groupEd25519Secretkey: null,
@ -142,7 +152,7 @@ async function handleGroupInviteMessage({
await LibSessionUtil.saveDumpsToDb(UserUtils.getOurPubKeyStrFromCache());
await UserSync.queueNewJobIfNeeded();
if (!found.invitePending) {
// if this group should already be polling
// if this group should already be polling based on if that author is pre-approved or we've already approved that group from another device.
getSwarmPollingInstance().addGroupId(groupPk, async () => {
// we need to do a first poll to fetch the keys etc before we can send our invite response
// this is pretty hacky, but also an admin seeing a message from that user in the group will mark it as not pending anymore
@ -526,4 +536,4 @@ async function handleGroupUpdateMessage(
window.log.warn('received group update of unknown type. Discarding...');
}
export const GroupV2Receiver = { handleGroupUpdateMessage };
export const GroupV2Receiver = { handleGroupUpdateMessage, sendInviteResponseToGroup };

@ -11,6 +11,7 @@ import {
} from '../../../webworker/workers/browser/libsession_worker_interface';
import { OpenGroupUtils } from '../../apis/open_group_api/utils';
import { ConvoHub } from '../../conversations';
import { PubKey } from '../../types';
import { SessionUtilContact } from './libsession_utils_contacts';
import { SessionUtilUserGroups } from './libsession_utils_user_groups';
import { SessionUtilUserProfile } from './libsession_utils_user_profile';
@ -57,7 +58,9 @@ function getConvoType(convo: ConversationModel): ConvoVolatileType {
? '1o1'
: SessionUtilUserGroups.isCommunityToStoreInWrapper(convo)
? 'Community'
: 'LegacyGroup';
: SessionUtilUserGroups.isLegacyGroupToStoreInWrapper(convo)
? 'LegacyGroup'
: 'Group';
return convoType;
}
@ -119,7 +122,21 @@ async function insertConvoFromDBIntoWrapperAndRefresh(convoId: string): Promise<
}
break;
case 'Group':
// we need to keep track of the convo volatile info for the new group now. // TODO AUDRIC debugger
try {
if (!PubKey.is03Pubkey(convoId)) {
throw new Error('group but not with 03 prefix');
}
await ConvoInfoVolatileWrapperActions.setGroup(
convoId,
lastReadMessageTimestamp,
isForcedUnread
);
await refreshConvoVolatileCached(convoId, true, false);
} catch (e) {
window.log.warn(
`ConvoInfoVolatileWrapperActions.setGroup of ${convoId} failed with ${e.message}`
);
}
break;
case 'Community':
try {
@ -175,6 +192,8 @@ async function refreshConvoVolatileCached(
convoType = 'Community';
} else if (convoId.startsWith('05') && isLegacyGroup) {
convoType = 'LegacyGroup';
} else if (PubKey.is03Pubkey(convoId)) {
convoType = 'Group';
} else if (convoId.startsWith('05')) {
convoType = '1o1';
}
@ -195,6 +214,16 @@ async function refreshConvoVolatileCached(
}
refreshed = true;
break;
case 'Group':
if (!PubKey.is03Pubkey(convoId)) {
throw new Error('expected a 03 group');
}
const fromWrapperGroup = await ConvoInfoVolatileWrapperActions.getGroup(convoId);
if (fromWrapperGroup) {
mappedGroupWrapperValues.set(convoId, fromWrapperGroup);
}
refreshed = true;
break;
case 'Community':
const fromWrapperCommunity = await ConvoInfoVolatileWrapperActions.getCommunity(convoId);
if (fromWrapperCommunity && fromWrapperCommunity.fullUrlWithPubkey) {

@ -17,7 +17,11 @@ import { PubKey } from '../../types';
* Returns true if that conversation is an active group
*/
function isUserGroupToStoreInWrapper(convo: ConversationModel): boolean {
return isCommunityToStoreInWrapper(convo) || isLegacyGroupToStoreInWrapper(convo);
return (
isCommunityToStoreInWrapper(convo) ||
isLegacyGroupToStoreInWrapper(convo) ||
isGroupToStoreInWrapper(convo)
);
}
function isCommunityToStoreInWrapper(convo: ConversationModel): boolean {
@ -36,7 +40,7 @@ function isLegacyGroupToStoreInWrapper(convo: ConversationModel): boolean {
}
function isGroupToStoreInWrapper(convo: ConversationModel): boolean {
return convo.isGroup() && PubKey.is03Pubkey(convo.id) && convo.isActive(); // TODO should we filter by left/kicked or they are on the wrapper itself?
return convo.isGroup() && PubKey.is03Pubkey(convo.id) && convo.isActive(); // debugger TODO should we filter by left/kicked or they are on the wrapper itself?
}
/**
@ -155,7 +159,36 @@ async function insertGroupsFromDBIntoWrapperAndRefresh(
}
break;
case 'Group':
// debugger;
// The 03-group is a bit different that the others as most fields are not to be updated.
// Indeed, they are more up to date on the group's swarm than ours and we don't want to keep both in sync.
if (!PubKey.is03Pubkey(convoId)) {
throw new Error('not a 03 group');
}
const groupInfo = {
pubkeyHex: convoId,
authData: null, // only updated when we process a new invite
invitePending: null, // only updated when we accept an invite
disappearingTimerSeconds: null, // not updated except when we process an invite/create a group
joinedAtSeconds: null, // no need to update this one except when we process an invite, maybe
name: null, // not updated except when we process an invite/create a group
secretKey: null, // not updated except when we process an promote/create a group
kicked: foundConvo.isKickedFromGroup() ?? null,
priority: foundConvo.getPriority() ?? null,
};
try {
window.log.debug(
`inserting into usergroup wrapper "${foundConvo.id}"... }`,
JSON.stringify(groupInfo)
);
// this does the create or the update of the matching existing group
await UserGroupsWrapperActions.setGroup(groupInfo);
// returned for testing purposes only
return null;
} catch (e) {
window.log.warn(`UserGroupsWrapperActions.set of ${convoId} failed with ${e.message}`);
// we still let this go through
}
break;
default:

@ -396,6 +396,9 @@ const _getConversationRequests = (
): Array<ReduxConversationType> => {
return filter(sortedConversations, conversation => {
const { isApproved, isBlocked, isPrivate, isMe, activeAt, didApproveMe, id } = conversation;
const invitePending = PubKey.is03Pubkey(id)
? UserGroupsWrapperActions.getCachedGroup(id)?.invitePending || false
: false;
const isIncomingRequest = hasValidIncomingRequestValues({
id,
isApproved: isApproved || false,
@ -404,29 +407,26 @@ const _getConversationRequests = (
isMe: isMe || false,
activeAt: activeAt || 0,
didApproveMe: didApproveMe || false,
invitePending,
});
return isIncomingRequest;
});
};
export const getConversationRequests = createSelector(
getSortedConversations,
_getConversationRequests
);
const getConversationRequests = createSelector(getSortedConversations, _getConversationRequests);
export const getConversationRequestsIds = createSelector(getConversationRequests, requests =>
requests.map(m => m.id)
);
export const hasConversationRequests = (state: StateType) => {
return !!getConversationRequests(state).length;
};
const _getUnreadConversationRequests = (
sortedConversationRequests: Array<ReduxConversationType>
): Array<ReduxConversationType> => {
return filter(sortedConversationRequests, conversation => {
return Boolean(conversation && conversation.unreadCount && conversation.unreadCount > 0);
return Boolean(
conversation &&
((conversation.unreadCount && conversation.unreadCount > 0) || conversation.isMarkedUnread)
);
});
};

@ -266,6 +266,61 @@ export function getLegacyGroupInfoFromDBValues({
return legacyGroup;
}
/**
* This function should only be used to update the libsession fields of a 03-group.
* Most of the fields tracked in the usergroup wrapper in libsession are actually not updated
* once the entry is created, but some of them needs to be updated.
*/
export function getGroupInfoFromDBValues({
id,
priority,
members: maybeMembers,
displayNameInProfile,
expirationMode,
expireTimer,
encPubkeyHex,
encSeckeyHex,
groupAdmins: maybeAdmins,
lastJoinedTimestamp,
}: {
id: string;
priority: number;
displayNameInProfile: string | undefined;
expirationMode: DisappearingMessageConversationModeType | undefined;
expireTimer: number | undefined;
encPubkeyHex: string;
encSeckeyHex: string;
members: string | Array<string>;
groupAdmins: string | Array<string>;
lastJoinedTimestamp: number;
}) {
const admins: Array<string> = maybeArrayJSONtoArray(maybeAdmins);
const members: Array<string> = maybeArrayJSONtoArray(maybeMembers);
const wrappedMembers: Array<LegacyGroupMemberInfo> = (members || []).map(m => {
return {
isAdmin: admins.includes(m),
pubkeyHex: m,
};
});
const legacyGroup: LegacyGroupInfo = {
pubkeyHex: id,
name: displayNameInProfile || '',
priority: priority || 0,
members: wrappedMembers,
disappearingTimerSeconds:
expirationMode && expirationMode !== 'off' && !!expireTimer && expireTimer > 0
? expireTimer
: 0,
encPubkey: !isEmpty(encPubkeyHex) ? from_hex(encPubkeyHex) : new Uint8Array(),
encSeckey: !isEmpty(encSeckeyHex) ? from_hex(encSeckeyHex) : new Uint8Array(),
joinedAtSeconds: Math.floor(lastJoinedTimestamp / 1000),
};
return legacyGroup;
}
/**
* This function can be used to make sure all the possible values as input of a switch as taken care off, without having a default case.
*

@ -387,6 +387,30 @@ export const ConvoInfoVolatileWrapperActions: ConvoInfoVolatileWrapperActionsCal
callLibSessionWorker(['ConvoInfoVolatileConfig', 'eraseLegacyGroup', pubkeyHex]) as Promise<
ReturnType<ConvoInfoVolatileWrapperActionsCalls['eraseLegacyGroup']>
>,
// groups
getGroup: async (pubkeyHex: GroupPubkeyType) =>
callLibSessionWorker(['ConvoInfoVolatileConfig', 'getGroup', pubkeyHex]) as Promise<
ReturnType<ConvoInfoVolatileWrapperActionsCalls['getGroup']>
>,
getAllGroups: async () =>
callLibSessionWorker(['ConvoInfoVolatileConfig', 'getAllGroups']) as Promise<
ReturnType<ConvoInfoVolatileWrapperActionsCalls['getAllGroups']>
>,
setGroup: async (pubkeyHex: GroupPubkeyType, lastRead: number, unread: boolean) =>
callLibSessionWorker([
'ConvoInfoVolatileConfig',
'setGroup',
pubkeyHex,
lastRead,
unread,
]) as Promise<ReturnType<ConvoInfoVolatileWrapperActionsCalls['setGroup']>>,
eraseGroup: async (pubkeyHex: GroupPubkeyType) =>
callLibSessionWorker(['ConvoInfoVolatileConfig', 'eraseGroup', pubkeyHex]) as Promise<
ReturnType<ConvoInfoVolatileWrapperActionsCalls['eraseGroup']>
>,
// communities
getCommunity: async (communityFullUrl: string) =>

Loading…
Cancel
Save