-
+
}
bottom={
diff --git a/ts/components/conversation/SessionMessagesListContainer.tsx b/ts/components/conversation/SessionMessagesListContainer.tsx
index 92941a56d..1c693051e 100644
--- a/ts/components/conversation/SessionMessagesListContainer.tsx
+++ b/ts/components/conversation/SessionMessagesListContainer.tsx
@@ -24,7 +24,6 @@ import {
import { getSelectedConversationKey } from '../../state/selectors/selectedConversation';
import { SessionMessagesList } from './SessionMessagesList';
import { TypingBubble } from './TypingBubble';
-import { ConversationMessageRequestButtons } from './MessageRequestButtons';
export type SessionMessageListProps = {
messageContainerRef: React.RefObject
;
@@ -126,7 +125,6 @@ class SessionMessagesListContainerInner extends React.Component {
isTyping={!!conversation.isTyping}
key="typing-bubble"
/>
-
+
+
+
+
+ );
+}
+
/**
* This component is used to display a warning when the user is responding to a message request.
- *
*/
export const ConversationRequestExplanation = () => {
const selectedConversation = useSelectedConversationKey();
@@ -46,9 +66,64 @@ export const ConversationRequestExplanation = () => {
}
return (
-
- {window.i18n('respondingToRequestWarning')}
-
+
+ );
+};
+
+/**
+ * This component is used to display a warning when the user is responding to a group message request.
+ */
+export const GroupRequestExplanation = () => {
+ const selectedConversation = useSelectedConversationKey();
+ const isIncomingMessageRequest = useIsIncomingRequest(selectedConversation);
+ const isGroupV2 = useSelectedIsGroupV2();
+ const showMsgRequestUI = selectedConversation && isIncomingMessageRequest;
+ // isApproved in DB is tracking the pending state for a group
+ const isApproved = useSelectedIsApproved();
+ const isGroupPendingInvite = useLibGroupInvitePending(selectedConversation);
+
+ if (!showMsgRequestUI || isApproved || !isGroupV2 || !isGroupPendingInvite) {
+ return null;
+ }
+ return (
+
+ );
+};
+
+export const InvitedToGroupControlMessage = () => {
+ const selectedConversation = useSelectedConversationKey();
+ const isGroupV2 = useSelectedIsGroupV2();
+ const hasMessages = useSelectedHasMessages();
+ const isApproved = useSelectedIsApproved();
+
+ const groupName = useLibGroupInviteGroupName(selectedConversation) || window.i18n('unknown');
+ const conversationOrigin = useSelectedConversationIdOrigin();
+ const adminNameInvitedUs =
+ useConversationUsernameOrShorten(conversationOrigin) || window.i18n('unknown');
+ const isGroupPendingInvite = useLibGroupInvitePending(selectedConversation);
+ if (
+ !selectedConversation ||
+ isApproved ||
+ hasMessages || // we don't want to display that "xx invited you" message if there are already other messages (incoming or outgoing)
+ !isGroupV2 ||
+ !conversationOrigin ||
+ !PubKey.is05Pubkey(conversationOrigin) ||
+ !isGroupPendingInvite
+ ) {
+ return null;
+ }
+
+ return (
+
);
};
@@ -58,7 +133,9 @@ export const ConversationRequestExplanation = () => {
export const NoMessageInConversation = () => {
const selectedConversation = useSelectedConversationKey();
- const hasMessage = useSelector(getSelectedHasMessages);
+ const hasMessage = useSelectedHasMessages();
+ const isGroupV2 = useSelectedIsGroupV2();
+ const isInvitePending = useLibGroupInvitePending(selectedConversation);
const isMe = useSelectedIsNoteToSelf();
const canWrite = useSelector(getSelectedCanWrite);
@@ -66,7 +143,8 @@ export const NoMessageInConversation = () => {
// TODOLATER use this selector accross the whole application (left pane excluded)
const nameToRender = useSelectedNicknameOrProfileNameOrShortenedPubkey() || '';
- if (!selectedConversation || hasMessage) {
+ // groupV2 use its own invite logic as part of
+ if (!selectedConversation || hasMessage || (isGroupV2 && isInvitePending)) {
return null;
}
let localizedKey: LocalizerKeys = 'noMessagesInEverythingElse';
@@ -81,10 +159,9 @@ export const NoMessageInConversation = () => {
}
return (
-
-
-
-
-
+
);
};
diff --git a/ts/components/conversation/message/message-content/MessageAvatar.tsx b/ts/components/conversation/message/message-content/MessageAvatar.tsx
index bec16a9cb..8454e186d 100644
--- a/ts/components/conversation/message/message-content/MessageAvatar.tsx
+++ b/ts/components/conversation/message/message-content/MessageAvatar.tsx
@@ -100,7 +100,7 @@ export const MessageAvatar = (props: Props) => {
privateConvoToOpen = foundRealSessionId || privateConvoToOpen;
}
- await ConvoHub.use().get(privateConvoToOpen).setOriginConversationID(selectedConvoKey);
+ await ConvoHub.use().get(privateConvoToOpen).setOriginConversationID(selectedConvoKey, true);
// public and blinded key for that message, we should open the convo as is and see if the user wants
// to send a sogs blinded message request.
diff --git a/ts/components/dialog/InviteContactsDialog.tsx b/ts/components/dialog/InviteContactsDialog.tsx
index f90e1bb57..61f3c352c 100644
--- a/ts/components/dialog/InviteContactsDialog.tsx
+++ b/ts/components/dialog/InviteContactsDialog.tsx
@@ -22,7 +22,7 @@ import { useSet } from '../../hooks/useSet';
import { ClosedGroup } from '../../session/group/closed-group';
import { PubKey } from '../../session/types';
import { SessionUtilUserGroups } from '../../session/utils/libsession/libsession_utils_user_groups';
-import { groupInfoActions } from '../../state/ducks/groups';
+import { groupInfoActions } from '../../state/ducks/metaGroups';
import { getPrivateContactsPubkeys } from '../../state/selectors/conversations';
import { useMemberGroupChangePending } from '../../state/selectors/groups';
import { MemberListItem } from '../MemberListItem';
diff --git a/ts/components/dialog/UpdateGroupMembersDialog.tsx b/ts/components/dialog/UpdateGroupMembersDialog.tsx
index d8d9997d8..1fdad5c58 100644
--- a/ts/components/dialog/UpdateGroupMembersDialog.tsx
+++ b/ts/components/dialog/UpdateGroupMembersDialog.tsx
@@ -27,7 +27,7 @@ import { useSet } from '../../hooks/useSet';
import { ConvoHub } from '../../session/conversations';
import { ClosedGroup } from '../../session/group/closed-group';
import { PubKey } from '../../session/types';
-import { groupInfoActions } from '../../state/ducks/groups';
+import { groupInfoActions } from '../../state/ducks/metaGroups';
import { useMemberGroupChangePending } from '../../state/selectors/groups';
import { useSelectedIsGroupV2 } from '../../state/selectors/selectedConversation';
import { SessionSpinner } from '../basic/SessionSpinner';
diff --git a/ts/components/dialog/UpdateGroupNameDialog.tsx b/ts/components/dialog/UpdateGroupNameDialog.tsx
index 59c18883f..2a047899d 100644
--- a/ts/components/dialog/UpdateGroupNameDialog.tsx
+++ b/ts/components/dialog/UpdateGroupNameDialog.tsx
@@ -9,7 +9,7 @@ import { ConvoHub } from '../../session/conversations';
import { ClosedGroup } from '../../session/group/closed-group';
import { initiateOpenGroupUpdate } from '../../session/group/open-group';
import { PubKey } from '../../session/types';
-import { groupInfoActions } from '../../state/ducks/groups';
+import { groupInfoActions } from '../../state/ducks/metaGroups';
import { updateGroupNameModal } from '../../state/ducks/modalDialog';
import { getLibGroupNameOutsideRedux } from '../../state/selectors/groups';
import { pickFileForAvatar } from '../../types/attachments/VisualAttachment';
diff --git a/ts/components/leftpane/overlay/OverlayClosedGroup.tsx b/ts/components/leftpane/overlay/OverlayClosedGroup.tsx
index f9c54fdbd..386932230 100644
--- a/ts/components/leftpane/overlay/OverlayClosedGroup.tsx
+++ b/ts/components/leftpane/overlay/OverlayClosedGroup.tsx
@@ -15,7 +15,7 @@ import { useSet } from '../../../hooks/useSet';
import { VALIDATION } from '../../../session/constants';
import { createClosedGroup } from '../../../session/conversations/createClosedGroup';
import { ToastUtils } from '../../../session/utils';
-import { groupInfoActions } from '../../../state/ducks/groups';
+import { groupInfoActions } from '../../../state/ducks/metaGroups';
import { resetOverlayMode } from '../../../state/ducks/section';
import { getPrivateContactsPubkeys } from '../../../state/selectors/conversations';
import { useIsCreatingGroupFromUIPending } from '../../../state/selectors/groups';
diff --git a/ts/components/leftpane/overlay/OverlayMessageRequest.tsx b/ts/components/leftpane/overlay/OverlayMessageRequest.tsx
index 529ffd619..99a164698 100644
--- a/ts/components/leftpane/overlay/OverlayMessageRequest.tsx
+++ b/ts/components/leftpane/overlay/OverlayMessageRequest.tsx
@@ -78,10 +78,11 @@ export const OverlayMessageRequest = () => {
const convoId = messageRequests[index];
// eslint-disable-next-line no-await-in-loop
await declineConversationWithoutConfirm({
- blockContact: false,
+ alsoBlock: false,
conversationId: convoId,
currentlySelectedConvo,
syncToDevices: false,
+ conversationIdOrigin: null, // block is false, no need for conversationIdOrigin
});
}
diff --git a/ts/components/menu/Menu.tsx b/ts/components/menu/Menu.tsx
index 235f43251..aa0cd739c 100644
--- a/ts/components/menu/Menu.tsx
+++ b/ts/components/menu/Menu.tsx
@@ -9,6 +9,7 @@ import {
useIsActive,
useIsBlinded,
useIsBlocked,
+ useIsGroupV2,
useIsIncomingRequest,
useIsKickedFromGroup,
useIsLeft,
@@ -48,6 +49,7 @@ import {
updateConfirmModal,
updateUserDetailsModal,
} from '../../state/ducks/modalDialog';
+import { useConversationIdOrigin } from '../../state/selectors/conversations';
import { getIsMessageSection } from '../../state/selectors/section';
import { useSelectedConversationKey } from '../../state/selectors/selectedConversation';
import { LocalizerKeys } from '../../types/LocalizerKeys';
@@ -502,20 +504,21 @@ export const DeclineMsgRequestMenuItem = () => {
const isRequest = useIsIncomingRequest(convoId);
const isPrivate = useIsPrivate(convoId);
const selected = useSelectedConversationKey();
-
- if (isPrivate && isRequest) {
+ const isGroupV2 = useIsGroupV2(convoId);
+ if ((isPrivate || isGroupV2) && isRequest) {
return (
- {
declineConversationWithConfirm({
conversationId: convoId,
syncToDevices: true,
- blockContact: false,
+ alsoBlock: false,
currentlySelectedConvo: selected || undefined,
+ conversationIdOrigin: null,
});
}}
>
- {window.i18n('decline')}
+ {isGroupV2 ? window.i18n('delete') : window.i18n('decline')}
);
}
@@ -527,16 +530,20 @@ export const DeclineAndBlockMsgRequestMenuItem = () => {
const isRequest = useIsIncomingRequest(convoId);
const selected = useSelectedConversationKey();
const isPrivate = useIsPrivate(convoId);
+ const isGroupV2 = useIsGroupV2(convoId);
+ const convoOrigin = useConversationIdOrigin(convoId);
- if (isRequest && isPrivate) {
+ if (isRequest && (isPrivate || (isGroupV2 && convoOrigin))) {
+ // to block the author of a groupv2 invitge we need the convoOrigin set
return (
- {
declineConversationWithConfirm({
conversationId: convoId,
syncToDevices: true,
- blockContact: true,
+ alsoBlock: true,
currentlySelectedConvo: selected || undefined,
+ conversationIdOrigin: convoOrigin ?? null,
});
}}
>
diff --git a/ts/hooks/useParamSelector.ts b/ts/hooks/useParamSelector.ts
index c68358eec..2b5d11818 100644
--- a/ts/hooks/useParamSelector.ts
+++ b/ts/hooks/useParamSelector.ts
@@ -231,6 +231,7 @@ export function useIsIncomingRequest(convoId?: string) {
return Boolean(
convoProps &&
hasValidIncomingRequestValues({
+ id: convoProps.id,
isMe: convoProps.isMe || false,
isApproved: convoProps.isApproved || false,
isPrivate: convoProps.isPrivate || false,
diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts
index 7c1f47287..c3e3011b0 100644
--- a/ts/interactions/conversationInteractions.ts
+++ b/ts/interactions/conversationInteractions.ts
@@ -14,6 +14,7 @@ import { OpenGroupUtils } from '../session/apis/open_group_api/utils';
import { ConvoHub } from '../session/conversations';
import { getSodiumRenderer } from '../session/crypto';
import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager';
+import { PubKey } from '../session/types';
import { perfEnd, perfStart } from '../session/utils/Performance';
import { fromHexToArray, toHex } from '../session/utils/String';
import { UserSync } from '../session/utils/job_runners/jobs/UserSyncJob';
@@ -36,6 +37,7 @@ import {
updateRemoveModeratorsModal,
} from '../state/ducks/modalDialog';
import { MIME } from '../types';
+import { LocalizerKeys } from '../types/LocalizerKeys';
import { IMAGE_JPEG } from '../types/MIME';
import { processNewAttachment } from '../types/MessageAttachment';
import { urlToBlob } from '../types/attachments/VisualAttachment';
@@ -70,11 +72,6 @@ export async function blockConvoById(conversationId: string) {
return;
}
- // I don't think we want to reset the approved fields when blocking a contact
- // if (conversation.isPrivate()) {
- // await conversation.setIsApproved(false);
- // }
-
await BlockedNumberController.block(conversation.id);
await conversation.commit();
ToastUtils.pushToastSuccess('blocked', window.i18n('blocked'));
@@ -124,19 +121,24 @@ export const approveConvoAndSendResponse = async (
};
export async function declineConversationWithoutConfirm({
- blockContact,
+ alsoBlock,
conversationId,
currentlySelectedConvo,
syncToDevices,
+ conversationIdOrigin,
}: {
conversationId: string;
currentlySelectedConvo: string | undefined;
syncToDevices: boolean;
- blockContact: boolean; // if set to false, the contact will just be set to not approved
+ alsoBlock: boolean;
+ conversationIdOrigin: string | null;
}) {
const conversationToDecline = ConvoHub.use().get(conversationId);
- if (!conversationToDecline || !conversationToDecline.isPrivate()) {
+ if (
+ !conversationToDecline ||
+ (!conversationToDecline.isPrivate() && !conversationToDecline.isClosedGroupV2())
+ ) {
window?.log?.info('No conversation to decline.');
return;
}
@@ -144,10 +146,20 @@ export async function declineConversationWithoutConfirm({
// Note: do not set the active_at undefined as this would make that conversation not synced with the libsession wrapper
await conversationToDecline.setIsApproved(false, false);
await conversationToDecline.setDidApproveMe(false, false);
+ await conversationToDecline.setOriginConversationID('', false);
// this will update the value in the wrapper if needed but not remove the entry if we want it gone. The remove is done below with removeContactFromWrapper
await conversationToDecline.commit();
- if (blockContact) {
- await blockConvoById(conversationId);
+ if (alsoBlock) {
+ if (PubKey.is03Pubkey(conversationId)) {
+ // Note: if we do want to block this convo, we actually want to block the person who invited us, not the 03 pubkey itself
+ if (conversationIdOrigin && !PubKey.is03Pubkey(conversationIdOrigin)) {
+ // restoring from seed we can be missing the conversationIdOrigin, so we wouldn't be able to block the person who invited us
+
+ await blockConvoById(conversationIdOrigin);
+ }
+ } else {
+ await blockConvoById(conversationId);
+ }
}
// when removing a message request, without blocking it, we actually have no need to store the conversation in the wrapper. So just remove the entry
@@ -158,6 +170,10 @@ export async function declineConversationWithoutConfirm({
await SessionUtilContact.removeContactFromWrapper(conversationToDecline.id);
}
+ if (PubKey.is03Pubkey(conversationId)) {
+ await UserGroupsWrapperActions.eraseGroup(conversationId);
+ }
+
if (syncToDevices) {
await forceSyncConfigurationNowIfNeeded();
}
@@ -169,25 +185,58 @@ export async function declineConversationWithoutConfirm({
export const declineConversationWithConfirm = ({
conversationId,
syncToDevices,
- blockContact,
+ alsoBlock,
currentlySelectedConvo,
+ conversationIdOrigin,
}: {
conversationId: string;
currentlySelectedConvo: string | undefined;
syncToDevices: boolean;
- blockContact: boolean; // if set to false, the contact will just be set to not approved
+ alsoBlock: boolean;
+ conversationIdOrigin: string | null;
}) => {
+ const isGroupV2 = PubKey.is03Pubkey(conversationId);
+
+ const okKey: LocalizerKeys = alsoBlock ? 'block' : isGroupV2 ? 'delete' : 'decline';
+ const nameToBlock =
+ alsoBlock && !!conversationIdOrigin
+ ? ConvoHub.use().get(conversationIdOrigin)?.getContactProfileNameOrShortenedPubKey()
+ : null;
+ const messageKey: LocalizerKeys = isGroupV2
+ ? alsoBlock && nameToBlock
+ ? 'deleteGroupRequestAndBlock'
+ : 'deleteGroupRequest'
+ : 'declineRequestMessage';
+
+ let message = '';
+ // restoring from seeed we might not have the sender of that invite, so we need to take care of not having one (and not block)
+ if (isGroupV2 && messageKey === 'deleteGroupRequestAndBlock') {
+ if (!nameToBlock) {
+ throw new Error(
+ 'deleteGroupRequestAndBlock needs a nameToBlock (or block should not be visible)'
+ );
+ }
+
+ message = window.i18n(
+ messageKey,
+ messageKey === 'deleteGroupRequestAndBlock' ? [nameToBlock] : []
+ );
+ } else {
+ message = window.i18n(messageKey);
+ }
+
window?.inboxStore?.dispatch(
updateConfirmModal({
- okText: blockContact ? window.i18n('block') : window.i18n('decline'),
+ okText: window.i18n(okKey),
cancelText: window.i18n('cancel'),
- message: window.i18n('declineRequestMessage'),
+ message,
onClickOk: async () => {
await declineConversationWithoutConfirm({
conversationId,
currentlySelectedConvo,
- blockContact,
+ alsoBlock,
syncToDevices,
+ conversationIdOrigin,
});
},
onClickCancel: () => {
diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts
index 5de2f69e7..292a98987 100644
--- a/ts/models/conversation.ts
+++ b/ts/models/conversation.ts
@@ -237,6 +237,10 @@ export class ConversationModel extends Backbone.Model {
return isDirectConversation(this.get('type'));
}
+ public isPrivateAndBlinded() {
+ return this.isPrivate() && PubKey.isBlinded(this.id);
+ }
+
// returns true if this is a closed/medium or open group
public isGroup() {
return isOpenOrClosedGroup(this.get('type'));
@@ -383,8 +387,11 @@ export class ConversationModel extends Backbone.Model {
toRet.groupAdmins = this.getGroupAdmins();
}
- // those are values coming only from the DB when this is a closed group
+ if (this.isClosedGroupV2() || this.isPrivateAndBlinded()) {
+ toRet.conversationIdOrigin = this.getConversationIdOrigin();
+ }
if (this.isClosedGroup()) {
+ // those are values coming only from the DB when this is a closed group
if (this.isKickedFromGroup()) {
toRet.isKickedFromGroup = this.isKickedFromGroup();
}
@@ -660,6 +667,7 @@ export class ConversationModel extends Backbone.Model {
*/
public isIncomingRequest(): boolean {
return hasValidIncomingRequestValues({
+ id: this.id,
isMe: this.isMe(),
isApproved: this.isApproved(),
isBlocked: this.isBlocked(),
@@ -1359,14 +1367,29 @@ export class ConversationModel extends Backbone.Model {
}
}
- public async setOriginConversationID(conversationIdOrigin: string) {
- if (conversationIdOrigin === this.get('conversationIdOrigin')) {
+ public async setOriginConversationID(conversationIdOrigin: string, shouldCommit: boolean) {
+ if (conversationIdOrigin === this.getConversationIdOrigin()) {
return;
}
+ // conversationIdOrigin can only be a 05 pubkey (invite to a 03 group from a 05 person, or a sogs url), or undefined
+ if (
+ conversationIdOrigin &&
+ !PubKey.is05Pubkey(conversationIdOrigin) &&
+ !OpenGroupUtils.isOpenGroupV2(conversationIdOrigin)
+ ) {
+ window.log.warn(
+ 'tried to setOriginConversationID with invalid parameter:',
+ conversationIdOrigin
+ );
+ throw new Error('tried to setOriginConversationID with invalid parameter ');
+ }
this.set({
conversationIdOrigin,
});
- await this.commit();
+
+ if (shouldCommit) {
+ await this.commit();
+ }
}
/**
@@ -1735,6 +1758,7 @@ export class ConversationModel extends Backbone.Model {
public isLeft(): boolean {
if (this.isClosedGroup()) {
if (this.isClosedGroupV2()) {
+ // getLibGroupNameOutsideRedux(this.id) ||
// console.info('isLeft using lib todo'); // debugger
}
return !!this.get('left');
@@ -1931,7 +1955,7 @@ export class ConversationModel extends Backbone.Model {
private async sendBlindedMessageRequest(messageParams: VisibleMessageParams) {
const ourSignKeyBytes = await UserUtils.getUserED25519KeyPairBytes();
- const groupUrl = this.getSogsOriginMessage();
+ const groupUrl = this.getConversationIdOrigin();
if (!PubKey.isBlinded(this.id)) {
window?.log?.warn('sendBlindedMessageRequest - convo is not a blinded one');
@@ -2091,10 +2115,18 @@ export class ConversationModel extends Backbone.Model {
}
/**
- *
- * @returns The open group conversationId this conversation originated from
+ * @link ConversationAttributes#conversationIdOrigin
*/
- private getSogsOriginMessage() {
+ private getConversationIdOrigin() {
+ if (!this.isClosedGroupV2() && !this.isPrivateAndBlinded()) {
+ window.log.warn(
+ 'getConversationIdOrigin can only be set with 03-group or blinded conversation (15 prefix), got:',
+ this.id
+ );
+ throw new Error(
+ 'getConversationIdOrigin can only be set with 03-group or blinded conversation (15 prefix)'
+ );
+ }
return this.get('conversationIdOrigin');
}
@@ -2443,6 +2475,7 @@ export function hasValidOutgoingRequestValues({
* @param values Required properties to evaluate if this is a message request
*/
export function hasValidIncomingRequestValues({
+ id,
isMe,
isApproved,
isBlocked,
@@ -2450,6 +2483,7 @@ export function hasValidIncomingRequestValues({
activeAt,
didApproveMe,
}: {
+ id: string;
isMe: boolean;
isApproved: boolean;
isBlocked: boolean;
@@ -2459,5 +2493,12 @@ export function hasValidIncomingRequestValues({
}): 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 && !isMe && !isApproved && !isBlocked && isActive && didApproveMe);
+ return Boolean(
+ (isPrivate || PubKey.is03Pubkey(id)) &&
+ !isMe &&
+ !isApproved &&
+ !isBlocked &&
+ isActive &&
+ didApproveMe
+ );
}
diff --git a/ts/models/conversationAttributes.ts b/ts/models/conversationAttributes.ts
index 80f0cd4e7..0450bbf57 100644
--- a/ts/models/conversationAttributes.ts
+++ b/ts/models/conversationAttributes.ts
@@ -73,7 +73,7 @@ export interface ConversationAttributes {
isTrustedForAttachmentDownload: boolean; // not synced accross devices, this field is used if we should auto download attachments from this conversation or not
- conversationIdOrigin?: string; // Blinded message requests ONLY: The community from which this conversation originated from
+ conversationIdOrigin?: string; // The conversation from which this conversation originated from: blinded message request or 03-group admin who invited us
// TODOLATER those two items are only used for legacy closed groups and will be removed when we get rid of the legacy closed groups support
lastJoinedTimestamp: number; // ClosedGroup: last time we were added to this group // TODOLATER to remove after legacy closed group are dropped
diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts
index 08aace3f7..3019f1992 100644
--- a/ts/receiver/configMessage.ts
+++ b/ts/receiver/configMessage.ts
@@ -34,7 +34,7 @@ import {
} from '../session/apis/snode_api/namespaces';
import { RetrieveMessageItemWithNamespace } from '../session/apis/snode_api/types';
import { ClosedGroup, GroupInfo } from '../session/group/closed-group';
-import { groupInfoActions } from '../state/ducks/groups';
+import { groupInfoActions } from '../state/ducks/metaGroups';
import {
ConfigWrapperObjectTypesMeta,
ConfigWrapperUser,
diff --git a/ts/receiver/groupv2/handleGroupV2Message.ts b/ts/receiver/groupv2/handleGroupV2Message.ts
index 72efa8b8d..e97c3648c 100644
--- a/ts/receiver/groupv2/handleGroupV2Message.ts
+++ b/ts/receiver/groupv2/handleGroupV2Message.ts
@@ -3,6 +3,7 @@ import { isEmpty, isFinite, isNumber } from 'lodash';
import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { HexString } from '../../node/hexStrings';
import { SignalService } from '../../protobuf';
+import { getMessageQueue } from '../../session';
import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { GetNetworkTime } from '../../session/apis/snode_api/getNetworkTime';
import { ConvoHub } from '../../session/conversations';
@@ -10,14 +11,13 @@ import { getSodiumRenderer } from '../../session/crypto';
import { ClosedGroup } from '../../session/group/closed-group';
import { GroupUpdateInviteResponseMessage } from '../../session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateInviteResponseMessage';
import { ed25519Str } from '../../session/onions/onionPath';
-import { getMessageQueue } from '../../session/sending';
import { PubKey } from '../../session/types';
import { UserUtils } from '../../session/utils';
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 { groupInfoActions } from '../../state/ducks/groups';
+import { groupInfoActions } from '../../state/ducks/metaGroups';
import { toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes';
import { BlockedNumberController } from '../../util';
import {
@@ -42,6 +42,20 @@ type GroupUpdateDetails = {
updateMessage: SignalService.GroupUpdateMessage;
} & WithEnvelopeTimestamp;
+async function sendInviteResponseToGroup({ groupPk }: { groupPk: GroupPubkeyType }) {
+ await getMessageQueue().sendToGroupV2({
+ message: new GroupUpdateInviteResponseMessage({
+ groupPk,
+ isApproved: true,
+ createAtNetworkTimestamp: GetNetworkTime.now(),
+ }),
+ });
+
+ // 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({
inviteMessage,
author,
@@ -82,6 +96,8 @@ async function handleGroupInviteMessage({
);
convo.set({
active_at: envelopeTimestamp,
+ didApproveMe: true,
+ conversationIdOrigin: author,
});
if (inviteMessage.name && isEmpty(convo.getRealSessionUsername())) {
@@ -90,6 +106,7 @@ async function handleGroupInviteMessage({
});
}
await convo.commit();
+ const userEd25519Secretkey = (await UserUtils.getUserED25519KeyPairBytes()).privKeyBytes;
let found = await UserGroupsWrapperActions.getGroup(inviteMessage.groupSessionId);
if (!found) {
@@ -100,12 +117,16 @@ async function handleGroupInviteMessage({
priority: 0,
pubkeyHex: inviteMessage.groupSessionId,
secretKey: null,
+ kicked: false,
+ invitePending: true,
};
+ } else {
+ found.kicked = false;
+ found.name = inviteMessage.name;
}
// not sure if we should drop it, or set it again? They should be the same anyway
found.authData = inviteMessage.memberAuthData;
- const userEd25519Secretkey = (await UserUtils.getUserED25519KeyPairBytes()).privKeyBytes;
await UserGroupsWrapperActions.setGroup(found);
await MetaGroupWrapperActions.init(inviteMessage.groupSessionId, {
metaDumped: null,
@@ -118,20 +139,10 @@ async function handleGroupInviteMessage({
});
await LibSessionUtil.saveDumpsToDb(UserUtils.getOurPubKeyStrFromCache());
await UserSync.queueNewJobIfNeeded();
-
- // TODO currently sending auto-accept of invite. needs to be removed once we get the Group message request logic
- console.warn('currently sending auto accept invite response');
- await getMessageQueue().sendToGroupV2({
- message: new GroupUpdateInviteResponseMessage({
- groupPk: inviteMessage.groupSessionId,
- isApproved: true,
- createAtNetworkTimestamp: GetNetworkTime.now(),
- }),
- });
-
- // 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(inviteMessage.groupSessionId);
+ if (!found.invitePending) {
+ // if this group should already be polling
+ getSwarmPollingInstance().addGroupId(inviteMessage.groupSessionId);
+ }
}
async function verifySig({
diff --git a/ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts b/ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts
index 38c0f36d5..6f8b71a5f 100644
--- a/ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts
+++ b/ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts
@@ -436,7 +436,7 @@ async function handleInboxOutboxMessages(
messageHash: '',
sentAt: postedAtInMs,
});
- await outboxConversationModel.setOriginConversationID(serverConversationId);
+ await outboxConversationModel.setOriginConversationID(serverConversationId, true);
await handleOutboxMessageModel(
msgModel,
diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts
index eccd290bd..001f2ba7c 100644
--- a/ts/session/apis/snode_api/swarmPolling.ts
+++ b/ts/session/apis/snode_api/swarmPolling.ts
@@ -189,6 +189,9 @@ export class SwarmPolling {
}
public async getPollingDetails(pollingEntries: Array) {
+ // Note: all of those checks are explicitely made only based on the libsession wrappers data, and NOT the DB.
+ // Eventually, we want to get rid of the duplication between the DB and libsession wrappers.
+ // If you need to add a check based on the DB, this is code smell.
let toPollDetails: Array = [];
const ourPubkey = UserUtils.getOurPubKeyStrFromCache();
@@ -230,7 +233,12 @@ export class SwarmPolling {
const allGroupsTracked = groups
.filter(m => this.shouldPollByTimeout(m)) // should we poll from it depending on this group activity?
- .filter(m => allGroupsInWrapper.some(w => w.pubkeyHex === m.pubkey.key)) // we don't poll from groups which are not in the usergroup wrapper
+ .filter(m => {
+ // We don't poll from groups which are not in the usergroup wrapper, and for those which are not marked as accepted
+ // We don't want to leave them, we just don't want to poll from them.
+ const found = allGroupsInWrapper.find(w => w.pubkeyHex === m.pubkey.key);
+ return found && !found.invitePending;
+ })
.map(m => m.pubkey.key as GroupPubkeyType) // extract the pubkey
.map(m => [m, ConversationTypeEnum.GROUPV2] as PollForGroup);
@@ -560,7 +568,12 @@ export class SwarmPolling {
const closedGroupsOnly = convos.filter(
(c: ConversationModel) =>
- c.isClosedGroup() && !c.isBlocked() && !c.isKickedFromGroup() && !c.isLeft()
+ (c.isClosedGroupV2() &&
+ !c.isBlocked() &&
+ !c.isKickedFromGroup() &&
+ !c.isLeft() &&
+ c.isApproved()) ||
+ (c.isClosedGroup() && !c.isBlocked() && !c.isKickedFromGroup() && !c.isLeft())
);
closedGroupsOnly.forEach(c => {
diff --git a/ts/session/apis/snode_api/swarm_polling_config/SwarmPollingGroupConfig.ts b/ts/session/apis/snode_api/swarm_polling_config/SwarmPollingGroupConfig.ts
index 5f460acbd..f3c9d618d 100644
--- a/ts/session/apis/snode_api/swarm_polling_config/SwarmPollingGroupConfig.ts
+++ b/ts/session/apis/snode_api/swarm_polling_config/SwarmPollingGroupConfig.ts
@@ -1,5 +1,5 @@
import { GroupPubkeyType } from 'libsession_util_nodejs';
-import { groupInfoActions } from '../../../../state/ducks/groups';
+import { groupInfoActions } from '../../../../state/ducks/metaGroups';
import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface';
import { ed25519Str } from '../../../onions/onionPath';
import { fromBase64ToArray } from '../../../utils/String';
@@ -19,7 +19,6 @@ async function handleGroupSharedConfigMessages(
);
if (groupConfigMessages.find(m => !m.storedAt)) {
- debugger;
throw new Error('all incoming group config message should have a timestamp');
}
const infos = groupConfigMessages
diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts
index 8efcb22fa..b9b8ff5b9 100644
--- a/ts/session/conversations/ConversationController.ts
+++ b/ts/session/conversations/ConversationController.ts
@@ -18,7 +18,7 @@ import { getMessageQueue } from '..';
import { deleteAllMessagesByConvoIdNoConfirmation } from '../../interactions/conversationInteractions';
import { CONVERSATION_PRIORITIES, ConversationTypeEnum } from '../../models/conversationAttributes';
import { removeAllClosedGroupEncryptionKeyPairs } from '../../receiver/closedGroups';
-import { groupInfoActions } from '../../state/ducks/groups';
+import { groupInfoActions } from '../../state/ducks/metaGroups';
import { getCurrentlySelectedConversationOutsideRedux } from '../../state/selectors/conversations';
import { assertUnreachable } from '../../types/sqlSharedTypes';
import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';
@@ -212,7 +212,6 @@ class ConvoController {
}
window.log.info(`deleteClosedGroup: ${groupId}, sendLeaveMessage?:${options.sendLeaveMessage}`);
getSwarmPollingInstance().removePubkey(groupId, 'deleteClosedGroup'); // we don't need to keep polling anymore.
-
if (options.sendLeaveMessage) {
await leaveClosedGroup(groupId, options.fromSyncMessage);
}
diff --git a/ts/session/utils/job_runners/jobs/GroupInviteJob.ts b/ts/session/utils/job_runners/jobs/GroupInviteJob.ts
index 4fb1636a5..bfa75c18b 100644
--- a/ts/session/utils/job_runners/jobs/GroupInviteJob.ts
+++ b/ts/session/utils/job_runners/jobs/GroupInviteJob.ts
@@ -153,6 +153,7 @@ class GroupInviteJob extends PersistedJob {
} finally {
updateFailedStateForMember(groupPk, member, failed);
try {
+ debugger;
await MetaGroupWrapperActions.memberSetInvited(groupPk, member, failed);
} catch (e) {
window.log.warn('GroupInviteJob memberSetInvited failed with', e.message);
diff --git a/ts/state/actions.ts b/ts/state/actions.ts
index 921a3d714..a5609fbc8 100644
--- a/ts/state/actions.ts
+++ b/ts/state/actions.ts
@@ -1,13 +1,13 @@
import { bindActionCreators, Dispatch } from '@reduxjs/toolkit';
-import { actions as search } from './ducks/search';
import { actions as conversations } from './ducks/conversations';
-import { actions as user } from './ducks/user';
-import { actions as sections } from './ducks/section';
-import { actions as theme } from './ducks/theme';
+import { groupInfoActions } from './ducks/metaGroups';
import { actions as modalDialog } from './ducks/modalDialog';
import { actions as primaryColor } from './ducks/primaryColor';
-import { groupInfoActions } from './ducks/groups';
+import { actions as search } from './ducks/search';
+import { actions as sections } from './ducks/section';
+import { actions as theme } from './ducks/theme';
+import { actions as user } from './ducks/user';
export function mapDispatchToProps(dispatch: Dispatch): object {
return {
diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts
index 93ce75ae2..f879c5bcd 100644
--- a/ts/state/ducks/conversations.ts
+++ b/ts/state/ducks/conversations.ts
@@ -8,6 +8,8 @@ import { LightBoxOptions } from '../../components/conversation/SessionConversati
import { Data } from '../../data/data';
import {
CONVERSATION_PRIORITIES,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ ConversationAttributes,
ConversationNotificationSettingType,
ConversationTypeEnum,
} from '../../models/conversationAttributes';
@@ -253,6 +255,10 @@ export interface ReduxConversationType {
* If this is undefined, it means all notification are enabled
*/
currentNotificationSetting?: ConversationNotificationSettingType;
+ /**
+ * @see {@link ConversationAttributes#conversationIdOrigin}.
+ */
+ conversationIdOrigin?: string;
priority?: number; // undefined means 0
isInitialFetchingInProgress?: boolean;
diff --git a/ts/state/ducks/groups.ts b/ts/state/ducks/metaGroups.ts
similarity index 99%
rename from ts/state/ducks/groups.ts
rename to ts/state/ducks/metaGroups.ts
index ac7c1628e..b3b60b3a4 100644
--- a/ts/state/ducks/groups.ts
+++ b/ts/state/ducks/metaGroups.ts
@@ -34,6 +34,7 @@ import { RunJobResult } from '../../session/utils/job_runners/PersistedJob';
import { GroupInvite } from '../../session/utils/job_runners/jobs/GroupInviteJob';
import { GroupSync } from '../../session/utils/job_runners/jobs/GroupSyncJob';
import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob';
+import { LibSessionUtil } from '../../session/utils/libsession/libsession_utils';
import { stringify, toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes';
import {
getGroupPubkeyFromWrapperType,
@@ -585,6 +586,8 @@ async function handleMemberChangeFromUIOrNot({
// this removes them from the wrapper
await handleRemoveMembers({ groupPk, removed, secretKey: group.secretKey, fromCurrentDevice });
+ await LibSessionUtil.saveDumpsToDb(groupPk);
+
// push new members & key supplement in a single batch call
const batchResult = await GroupSync.pushChangesToGroupSwarmIfNeeded(groupPk, supplementKeys);
if (batchResult !== RunJobResult.Success) {
@@ -644,6 +647,7 @@ async function handleMemberChangeFromUIOrNot({
}),
});
}
+ await LibSessionUtil.saveDumpsToDb(groupPk);
convo.set({
active_at: createAtNetworkTimestamp,
@@ -862,8 +866,8 @@ const currentDeviceGroupNameChange = createAsyncThunk(
/**
* This slice is the one holding the default joinable rooms fetched once in a while from the default opengroup v2 server.
*/
-const groupSlice = createSlice({
- name: 'group',
+const metaGroupSlice = createSlice({
+ name: 'metaGroup',
initialState: initialGroupState,
reducers: {},
extraReducers: builder => {
@@ -1019,6 +1023,6 @@ export const groupInfoActions = {
markUsAsAdmin,
inviteResponseReceived,
currentDeviceGroupNameChange,
- ...groupSlice.actions,
+ ...metaGroupSlice.actions,
};
-export const groupReducer = groupSlice.reducer;
+export const groupReducer = metaGroupSlice.reducer;
diff --git a/ts/state/ducks/userGroups.ts b/ts/state/ducks/userGroups.ts
new file mode 100644
index 000000000..4a24beee0
--- /dev/null
+++ b/ts/state/ducks/userGroups.ts
@@ -0,0 +1,35 @@
+/* eslint-disable no-await-in-loop */
+import { PayloadAction, createSlice } from '@reduxjs/toolkit';
+import { GroupPubkeyType, UserGroupsGet } from 'libsession_util_nodejs';
+
+export type UserGroupState = {
+ userGroups: Record;
+};
+
+export const initialUserGroupState: UserGroupState = {
+ userGroups: {},
+};
+
+const userGroupSlice = createSlice({
+ name: 'userGroup',
+ initialState: initialUserGroupState,
+
+ reducers: {
+ refreshUserGroupsSlice(
+ state: UserGroupState,
+ action: PayloadAction<{ groups: Array }>
+ ) {
+ state.userGroups = {};
+ action.payload.groups.forEach(m => {
+ state.userGroups[m.pubkeyHex] = m;
+ });
+
+ return state;
+ },
+ },
+});
+
+export const userGroupsActions = {
+ ...userGroupSlice.actions,
+};
+export const userGroupReducer = userGroupSlice.reducer;
diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts
index 3ab29c048..9c9eb18de 100644
--- a/ts/state/reducer.ts
+++ b/ts/state/reducer.ts
@@ -1,26 +1,27 @@
import { combineReducers } from '@reduxjs/toolkit';
-import { reducer as search, SearchStateType } from './ducks/search';
-import { ConversationsStateType, reducer as conversations } from './ducks/conversations';
-import { reducer as user, UserStateType } from './ducks/user';
-import { reducer as theme } from './ducks/theme';
+import { callReducer as call, CallStateType } from './ducks/call';
+import { reducer as conversations, ConversationsStateType } from './ducks/conversations';
+import { defaultRoomReducer as defaultRooms, DefaultRoomsState } from './ducks/defaultRooms';
import { reducer as primaryColor } from './ducks/primaryColor';
+import { reducer as search, SearchStateType } from './ducks/search';
import { reducer as section, SectionStateType } from './ducks/section';
-import { defaultRoomReducer as defaultRooms, DefaultRoomsState } from './ducks/defaultRooms';
import { ReduxSogsRoomInfos, SogsRoomInfoState } from './ducks/sogsRoomInfo';
-import { callReducer as call, CallStateType } from './ducks/call';
+import { reducer as theme } from './ducks/theme';
+import { reducer as user, UserStateType } from './ducks/user';
-import { defaultOnionReducer as onionPaths, OnionState } from './ducks/onion';
+import { PrimaryColorStateType, ThemeStateType } from '../themes/constants/colors';
+import { groupReducer, GroupState } from './ducks/metaGroups';
import { modalReducer as modals, ModalState } from './ducks/modalDialog';
-import { userConfigReducer as userConfig, UserConfigState } from './ducks/userConfig';
-import { timerOptionReducer as timerOptions, TimerOptionsState } from './ducks/timerOptions';
+import { defaultOnionReducer as onionPaths, OnionState } from './ducks/onion';
+import { settingsReducer, SettingsState } from './ducks/settings';
import {
reducer as stagedAttachments,
StagedAttachmentsStateType,
} from './ducks/stagedAttachments';
-import { PrimaryColorStateType, ThemeStateType } from '../themes/constants/colors';
-import { settingsReducer, SettingsState } from './ducks/settings';
-import { groupReducer, GroupState } from './ducks/groups';
+import { timerOptionReducer as timerOptions, TimerOptionsState } from './ducks/timerOptions';
+import { userConfigReducer as userConfig, UserConfigState } from './ducks/userConfig';
+import { userGroupReducer, UserGroupState } from './ducks/userGroups';
export type StateType = {
search: SearchStateType;
@@ -39,6 +40,7 @@ export type StateType = {
sogsRoomInfo: SogsRoomInfoState;
settings: SettingsState;
groups: GroupState;
+ userGroups: UserGroupState;
};
export const reducers = {
@@ -58,6 +60,7 @@ export const reducers = {
sogsRoomInfo: ReduxSogsRoomInfos.sogsRoomInfoReducer,
settings: settingsReducer,
groups: groupReducer,
+ userGroups: userGroupReducer,
};
// Making this work would require that our reducer signature supported AnyAction, not
diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts
index d60c5a6a2..8c9dab9d7 100644
--- a/ts/state/selectors/conversations.ts
+++ b/ts/state/selectors/conversations.ts
@@ -2,6 +2,7 @@
import { createSelector } from '@reduxjs/toolkit';
import { filter, isEmpty, isFinite, isNumber, pick, sortBy, toNumber } from 'lodash';
+import { useSelector } from 'react-redux';
import {
ConversationLookupType,
ConversationsStateType,
@@ -93,6 +94,13 @@ export const hasSelectedConversationIncomingMessages = createSelector(
}
);
+export const hasSelectedConversationOutgoingMessages = createSelector(
+ getSortedMessagesOfSelectedConversation,
+ (messages: Array): boolean => {
+ return messages.some(m => m.propsForMessage.direction === 'outgoing');
+ }
+);
+
export const getFirstUnreadMessageId = (state: StateType): string | undefined => {
return state.conversations.firstUnreadMessageId;
};
@@ -379,8 +387,9 @@ const _getConversationRequests = (
sortedConversations: Array
): Array => {
return filter(sortedConversations, conversation => {
- const { isApproved, isBlocked, isPrivate, isMe, activeAt, didApproveMe } = conversation;
+ const { isApproved, isBlocked, isPrivate, isMe, activeAt, didApproveMe, id } = conversation;
const isIncomingRequest = hasValidIncomingRequestValues({
+ id,
isApproved: isApproved || false,
isBlocked: isBlocked || false,
isPrivate: isPrivate || false,
@@ -613,8 +622,8 @@ export function getLoadedMessagesLength(state: StateType) {
return getMessagesFromState(state).length;
}
-export function getSelectedHasMessages(state: StateType): boolean {
- return !isEmpty(getMessagesFromState(state));
+export function useSelectedHasMessages(): boolean {
+ return useSelector((state: StateType) => !isEmpty(getMessagesFromState(state)));
}
export const isFirstUnreadMessageIdAbove = createSelector(
@@ -962,3 +971,9 @@ export const getIsSelectedConvoInitialLoadingInProgress = (state: StateType): bo
export function getCurrentlySelectedConversationOutsideRedux() {
return window?.inboxStore?.getState().conversations.selectedConversation as string | undefined;
}
+
+export function useConversationIdOrigin(convoId: string | undefined) {
+ return useSelector((state: StateType) =>
+ convoId ? state.conversations.conversationLookup?.[convoId]?.conversationIdOrigin : undefined
+ );
+}
diff --git a/ts/state/selectors/groups.ts b/ts/state/selectors/groups.ts
index 333dd5003..1903aef7c 100644
--- a/ts/state/selectors/groups.ts
+++ b/ts/state/selectors/groups.ts
@@ -1,7 +1,7 @@
import { GroupMemberGet, GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs';
import { useSelector } from 'react-redux';
import { PubKey } from '../../session/types';
-import { GroupState } from '../ducks/groups';
+import { GroupState } from '../ducks/metaGroups';
import { StateType } from '../reducer';
const getLibGroupsState = (state: StateType): GroupState => state.groups;
diff --git a/ts/state/selectors/selectedConversation.ts b/ts/state/selectors/selectedConversation.ts
index d4e51397d..a8dd33e75 100644
--- a/ts/state/selectors/selectedConversation.ts
+++ b/ts/state/selectors/selectedConversation.ts
@@ -36,10 +36,6 @@ const getIsSelectedBlocked = (state: StateType): boolean => {
return Boolean(getSelectedConversation(state)?.isBlocked) || false;
};
-const getSelectedIsApproved = (state: StateType): boolean => {
- return Boolean(getSelectedConversation(state)?.isApproved) || false;
-};
-
const getSelectedApprovedMe = (state: StateType): boolean => {
return Boolean(getSelectedConversation(state)?.didApproveMe) || false;
};
@@ -210,7 +206,9 @@ export function useSelectedIsBlocked() {
}
export function useSelectedIsApproved() {
- return useSelector(getSelectedIsApproved);
+ return useSelector((state: StateType): boolean => {
+ return !!(getSelectedConversation(state)?.isApproved || false);
+ });
}
export function useSelectedApprovedMe() {
@@ -280,6 +278,10 @@ export function useSelectedIsLeft() {
return useSelector((state: StateType) => Boolean(getSelectedConversation(state)?.left) || false);
}
+export function useSelectedConversationIdOrigin() {
+ return useSelector((state: StateType) => getSelectedConversation(state)?.conversationIdOrigin);
+}
+
export function useSelectedNickname() {
return useSelector((state: StateType) => getSelectedConversation(state)?.nickname);
}
diff --git a/ts/state/selectors/userGroups.ts b/ts/state/selectors/userGroups.ts
new file mode 100644
index 000000000..801217b3a
--- /dev/null
+++ b/ts/state/selectors/userGroups.ts
@@ -0,0 +1,20 @@
+import { useSelector } from 'react-redux';
+import { PubKey } from '../../session/types';
+import { UserGroupState } from '../ducks/userGroups';
+import { StateType } from '../reducer';
+
+const getUserGroupState = (state: StateType): UserGroupState => state.userGroups;
+
+const getGroupById = (state: StateType, convoId?: string) => {
+ return convoId && PubKey.is03Pubkey(convoId)
+ ? getUserGroupState(state).userGroups[convoId]
+ : undefined;
+};
+
+export function useLibGroupInvitePending(convoId?: string) {
+ return useSelector((state: StateType) => getGroupById(state, convoId)?.invitePending);
+}
+
+export function useLibGroupInviteGroupName(convoId?: string) {
+ return useSelector((state: StateType) => getGroupById(state, convoId)?.name);
+}
diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts
index 7e20bb973..b094f1391 100644
--- a/ts/types/LocalizerKeys.ts
+++ b/ts/types/LocalizerKeys.ts
@@ -126,6 +126,8 @@ export type LocalizerKeys =
| 'deleteConversationConfirmation'
| 'deleteForEveryone'
| 'deleteFromAllMyDevices'
+ | 'deleteGroupRequest'
+ | 'deleteGroupRequestAndBlock'
| 'deleteJustForMe'
| 'deleteMessageQuestion'
| 'deleteMessages'
@@ -397,6 +399,7 @@ export type LocalizerKeys =
| 'requestsPlaceholder'
| 'requestsSubtitle'
| 'resend'
+ | 'respondingToGroupRequestWarning'
| 'respondingToRequestWarning'
| 'restoreUsingRecoveryPhrase'
| 'ringing'
@@ -502,6 +505,7 @@ export type LocalizerKeys =
| 'userAddedToModerators'
| 'userBanFailed'
| 'userBanned'
+ | 'userInvitedYouToGroup'
| 'userRemovedFromModerators'
| 'userUnbanFailed'
| 'userUnbanned'
diff --git a/ts/webworker/workers/browser/libsession_worker_interface.ts b/ts/webworker/workers/browser/libsession_worker_interface.ts
index 77a5d9186..b2e9c8baf 100644
--- a/ts/webworker/workers/browser/libsession_worker_interface.ts
+++ b/ts/webworker/workers/browser/libsession_worker_interface.ts
@@ -16,13 +16,16 @@ import {
Uint8ArrayLen100,
Uint8ArrayLen64,
UserConfigWrapperActionsCalls,
+ UserGroupsGet,
UserGroupsSet,
UserGroupsWrapperActionsCalls,
} from 'libsession_util_nodejs';
// eslint-disable-next-line import/order
import { join } from 'path';
+import { cloneDeep } from 'lodash';
import { getAppRootPath } from '../../../node/getRootPath';
+import { userGroupsActions } from '../../../state/ducks/userGroups';
import { WorkerInterface } from '../../worker_interface';
import { ConfigWrapperUser, LibSessionWorkerFunctions } from './libsession_worker_functions';
@@ -115,11 +118,11 @@ function createBaseActionsFor(wrapperType: ConfigWrapperUser) {
GenericWrapperActions.confirmPushed(wrapperType, seqno, hash),
dump: async () => GenericWrapperActions.dump(wrapperType),
makeDump: async () => GenericWrapperActions.makeDump(wrapperType),
- merge: async (toMerge: Array) => GenericWrapperActions.merge(wrapperType, toMerge),
needsDump: async () => GenericWrapperActions.needsDump(wrapperType),
needsPush: async () => GenericWrapperActions.needsPush(wrapperType),
push: async () => GenericWrapperActions.push(wrapperType),
currentHashes: async () => GenericWrapperActions.currentHashes(wrapperType),
+ merge: async (toMerge: Array) => GenericWrapperActions.merge(wrapperType, toMerge),
};
}
@@ -184,9 +187,24 @@ export const ContactsWrapperActions: ContactsWrapperActionsCalls = {
>,
};
+// this is a cache of the new groups only. Anytime we create, update, delete, or merge a group, we update this
+const groups: Map = new Map();
+
+function dispatchCachedGroupsToRedux() {
+ window?.inboxStore?.dispatch?.(
+ userGroupsActions.refreshUserGroupsSlice({ groups: [...groups.values()] })
+ );
+}
+
export const UserGroupsWrapperActions: UserGroupsWrapperActionsCalls = {
/* Reuse the GenericWrapperActions with the UserGroupsConfig argument */
...createBaseActionsFor('UserGroupsConfig'),
+ // override the merge() as we need to refresh the cached groups
+ merge: async (toMerge: Array) => {
+ const mergeRet = await GenericWrapperActions.merge('UserGroupsConfig', toMerge);
+ await UserGroupsWrapperActions.getAllGroups(); // this refreshes the cached data after merge
+ return mergeRet;
+ },
/** UserGroups wrapper specific actions */
@@ -245,30 +263,61 @@ export const UserGroupsWrapperActions: UserGroupsWrapperActionsCalls = {
ReturnType
>,
- createGroup: async () =>
- callLibSessionWorker(['UserGroupsConfig', 'createGroup']) as Promise<
+ createGroup: async () => {
+ const group = (await callLibSessionWorker(['UserGroupsConfig', 'createGroup'])) as Awaited<
ReturnType
- >,
+ >;
+ groups.set(group.pubkeyHex, group);
+ dispatchCachedGroupsToRedux();
+ return cloneDeep(group);
+ },
- getGroup: async (pubkeyHex: GroupPubkeyType) =>
- callLibSessionWorker(['UserGroupsConfig', 'getGroup', pubkeyHex]) as Promise<
- ReturnType
- >,
+ getGroup: async (pubkeyHex: GroupPubkeyType) => {
+ const group = (await callLibSessionWorker([
+ 'UserGroupsConfig',
+ 'getGroup',
+ pubkeyHex,
+ ])) as Awaited>;
+ if (group) {
+ groups.set(group.pubkeyHex, group);
+ } else {
+ groups.delete(pubkeyHex);
+ }
+ dispatchCachedGroupsToRedux();
+ return cloneDeep(group);
+ },
- getAllGroups: async () =>
- callLibSessionWorker(['UserGroupsConfig', 'getAllGroups']) as Promise<
- ReturnType
- >,
+ getAllGroups: async () => {
+ const groupsFetched = (await callLibSessionWorker([
+ 'UserGroupsConfig',
+ 'getAllGroups',
+ ])) as Awaited>;
+ groups.clear();
+ groupsFetched.forEach(f => groups.set(f.pubkeyHex, f));
+ dispatchCachedGroupsToRedux();
+ return cloneDeep(groupsFetched);
+ },
- setGroup: async (info: UserGroupsSet) =>
- callLibSessionWorker(['UserGroupsConfig', 'setGroup', info]) as Promise<
+ setGroup: async (info: UserGroupsSet) => {
+ const group = (await callLibSessionWorker(['UserGroupsConfig', 'setGroup', info])) as Awaited<
ReturnType
- >,
+ >;
+ groups.set(group.pubkeyHex, group);
+ dispatchCachedGroupsToRedux();
+ return cloneDeep(group);
+ },
- eraseGroup: async (pubkeyHex: GroupPubkeyType) =>
- callLibSessionWorker(['UserGroupsConfig', 'eraseGroup', pubkeyHex]) as Promise<
- ReturnType
- >,
+ eraseGroup: async (pubkeyHex: GroupPubkeyType) => {
+ const ret = (await callLibSessionWorker([
+ 'UserGroupsConfig',
+ 'eraseGroup',
+ pubkeyHex,
+ ])) as Awaited>;
+
+ groups.delete(pubkeyHex);
+ dispatchCachedGroupsToRedux();
+ return ret;
+ },
};
export const ConvoInfoVolatileWrapperActions: ConvoInfoVolatileWrapperActionsCalls = {
@@ -541,7 +590,7 @@ export const MetaGroupWrapperActions: MetaGroupWrapperActionsCalls = {
'swarmVerifySubAccount',
signingValue,
]) as Promise>,
- loadAdminKeys: async (groupPk: GroupPubkeyType, secret: Uint8ArrayLen64) => {
+ loadAdminKeys: async (groupPk: GroupPubkeyType, secret: Uint8ArrayLen64) => {
return callLibSessionWorker([`MetaGroupConfig-${groupPk}`, 'loadAdminKeys', secret]) as Promise<
ReturnType
>;