feat: group message requests kind of working

still need to have them visible in the msg request only
pull/2963/head
Audric Ackermann 1 year ago
parent 315bc3ea70
commit e5c76d3b70

@ -504,7 +504,11 @@
"messageRequestAcceptedOurs": "You have accepted $name$'s message request",
"messageRequestAcceptedOursNoName": "You have accepted the message request",
"declineRequestMessage": "Are you sure you want to decline this message request?",
"deleteGroupRequest": "Are you sure you want to delete this group invite?",
"deleteGroupRequestAndBlock": "Are you sure you want to block <b>$name$</b>? Blocked users cannot send you message requests, group invites or call you.",
"respondingToRequestWarning": "Sending a message to this user will automatically accept their message request and reveal your Session ID.",
"respondingToGroupRequestWarning": "Sending a message to this group will automatically accept the group invite.",
"userInvitedYouToGroup": "<b>$name$</b> invited you to join <b>$groupName$</b>.",
"hideRequestBanner": "Hide Message Request Banner",
"openMessageRequestInbox": "Message Requests",
"noMessageRequestsPending": "No pending message requests",

@ -1,12 +1,12 @@
import { fromPairs, map } from 'lodash';
import moment from 'moment';
import React from 'react';
import { Provider } from 'react-redux';
import styled from 'styled-components';
import { fromPairs, map } from 'lodash';
import useMount from 'react-use/lib/useMount';
import useUpdate from 'react-use/lib/useUpdate';
import { persistStore } from 'redux-persist';
import { PersistGate } from 'redux-persist/integration/react';
import useUpdate from 'react-use/lib/useUpdate';
import useMount from 'react-use/lib/useMount';
import styled from 'styled-components';
import { LeftPane } from './leftpane/LeftPane';
// moment does not support es-419 correctly (and cause white screen on app start)
@ -33,13 +33,14 @@ import { ExpirationTimerOptions } from '../util/expiringMessages';
import { SessionMainPanel } from './SessionMainPanel';
import { SettingsKey } from '../data/settings-key';
import { groupInfoActions, initialGroupState } from '../state/ducks/metaGroups';
import { getSettingsInitialState, updateAllOnStorageReady } from '../state/ducks/settings';
import { initialSogsRoomInfoState } from '../state/ducks/sogsRoomInfo';
import { useHasDeviceOutdatedSyncing } from '../state/selectors/settings';
import { Storage } from '../util/storage';
import { UserGroupsWrapperActions } from '../webworker/workers/browser/libsession_worker_interface';
import { NoticeBanner } from './NoticeBanner';
import { Flex } from './basic/Flex';
import { groupInfoActions, initialGroupState } from '../state/ducks/groups';
function makeLookup<T>(items: Array<T>, key: string): { [key: string]: T } {
// Yep, we can't index into item without knowing what it is. True. But we want to.
@ -59,12 +60,18 @@ const StyledGutter = styled.div`
transition: none;
`;
function createSessionInboxStore() {
async function createSessionInboxStore() {
// Here we set up a full redux store with initial state for our LeftPane Root
const conversations = ConvoHub.use()
.getConversations()
.map(conversation => conversation.getConversationModelProps());
const userGroups: Record<string, any> = {};
(await UserGroupsWrapperActions.getAllGroups()).forEach(m => {
userGroups[m.pubkeyHex] = m;
});
const timerOptions: TimerOptionsArray = ExpirationTimerOptions.getTimerSecondsWithName();
const initialState: StateType = {
conversations: {
@ -90,14 +97,15 @@ function createSessionInboxStore() {
sogsRoomInfo: initialSogsRoomInfoState,
settings: getSettingsInitialState(),
groups: initialGroupState,
userGroups: { userGroups },
};
return createStore(initialState);
}
function setupLeftPane(forceUpdateInboxComponent: () => void) {
async function setupLeftPane(forceUpdateInboxComponent: () => void) {
window.openConversationWithMessages = openConversationWithMessages;
window.inboxStore = createSessionInboxStore();
window.inboxStore = await createSessionInboxStore();
window.inboxStore.dispatch(updateAllOnStorageReady());
window.inboxStore.dispatch(groupInfoActions.loadMetaDumpsFromDB()); // this loads the dumps from DB and fills the 03-groups slice with the corresponding details
forceUpdateInboxComponent();
@ -125,7 +133,7 @@ export const SessionInboxView = () => {
const update = useUpdate();
// run only on mount
useMount(() => {
setupLeftPane(update);
void setupLeftPane(update);
});
if (!window.inboxStore) {

@ -1,18 +1,29 @@
import React from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { useIsIncomingRequest } from '../../hooks/useParamSelector';
import {
approveConvoAndSendResponse,
declineConversationWithConfirm,
} from '../../interactions/conversationInteractions';
import { getSwarmPollingInstance } from '../../session/apis/snode_api/swarmPolling';
import { ConvoHub } from '../../session/conversations';
import { hasSelectedConversationIncomingMessages } from '../../state/selectors/conversations';
import { useSelectedConversationKey } from '../../state/selectors/selectedConversation';
import { PubKey } from '../../session/types';
import {
useSelectedConversationIdOrigin,
useSelectedConversationKey,
useSelectedIsGroupV2,
useSelectedIsPrivateFriend,
} from '../../state/selectors/selectedConversation';
import { useLibGroupInvitePending } from '../../state/selectors/userGroups';
import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';
import { SessionButton, SessionButtonColor } from '../basic/SessionButton';
import { ConversationRequestExplanation } from './SubtleNotification';
import {
ConversationRequestExplanation,
GroupRequestExplanation,
InvitedToGroupControlMessage,
} from './SubtleNotification';
const ConversationRequestBanner = styled.div`
const MessageRequestContainer = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
@ -40,24 +51,31 @@ const StyledBlockUserText = styled.span`
font-weight: 700;
`;
const handleDeclineConversationRequest = (convoId: string, currentSelected: string | undefined) => {
const handleDeclineConversationRequest = (
convoId: string,
currentSelected: string | undefined,
conversationIdOrigin: string | null
) => {
declineConversationWithConfirm({
conversationId: convoId,
syncToDevices: true,
blockContact: false,
alsoBlock: false,
currentlySelectedConvo: currentSelected,
conversationIdOrigin,
});
};
const handleDeclineAndBlockConversationRequest = (
convoId: string,
currentSelected: string | undefined
currentSelected: string | undefined,
conversationIdOrigin: string | null
) => {
declineConversationWithConfirm({
conversationId: convoId,
syncToDevices: true,
blockContact: true,
alsoBlock: true,
currentlySelectedConvo: currentSelected,
conversationIdOrigin,
});
};
@ -69,17 +87,34 @@ const handleAcceptConversationRequest = async (convoId: string) => {
await convo.setDidApproveMe(true, false);
await convo.setIsApproved(true, false);
await convo.commit();
await convo.addOutgoingApprovalMessage(Date.now());
await approveConvoAndSendResponse(convoId, true);
if (convo.isPrivate()) {
await convo.addOutgoingApprovalMessage(Date.now());
await approveConvoAndSendResponse(convoId, true);
} else if (PubKey.is03Pubkey(convoId)) {
const found = await UserGroupsWrapperActions.getGroup(convoId);
if (!found) {
window.log.warn('cannot approve a non existing group in usergroup');
return;
}
// this updates the wrapper and refresh the redux slice
await UserGroupsWrapperActions.setGroup({ ...found, invitePending: false });
getSwarmPollingInstance().addGroupId(convoId);
}
};
export const ConversationMessageRequestButtons = () => {
const selectedConvoId = useSelectedConversationKey();
const hasIncomingMessages = useSelector(hasSelectedConversationIncomingMessages);
const isIncomingRequest = useIsIncomingRequest(selectedConvoId);
const isGroupV2 = useSelectedIsGroupV2();
const isPrivateAndFriend = useSelectedIsPrivateFriend();
const isGroupPendingInvite = useLibGroupInvitePending(selectedConvoId);
const convoOrigin = useSelectedConversationIdOrigin() ?? null;
if (!selectedConvoId || !hasIncomingMessages) {
if (
!selectedConvoId ||
isPrivateAndFriend || // if we are already friends, there is no need for the msg request buttons
(isGroupV2 && !isGroupPendingInvite)
) {
return null;
}
@ -88,18 +123,8 @@ export const ConversationMessageRequestButtons = () => {
}
return (
<ConversationRequestBanner>
<StyledBlockUserText
onClick={() => {
handleDeclineAndBlockConversationRequest(selectedConvoId, selectedConvoId);
}}
data-testid="decline-and-block-message-request"
>
{window.i18n('block')}
</StyledBlockUserText>
<ConversationRequestExplanation />
<MessageRequestContainer>
<InvitedToGroupControlMessage />
<ConversationBannerRow>
<SessionButton
onClick={async () => {
@ -110,13 +135,25 @@ export const ConversationMessageRequestButtons = () => {
/>
<SessionButton
buttonColor={SessionButtonColor.Danger}
text={window.i18n('decline')}
text={isGroupV2 ? window.i18n('delete') : window.i18n('decline')}
onClick={() => {
handleDeclineConversationRequest(selectedConvoId, selectedConvoId);
handleDeclineConversationRequest(selectedConvoId, selectedConvoId, convoOrigin);
}}
dataTestId="decline-message-request"
/>
</ConversationBannerRow>
</ConversationRequestBanner>
{isGroupV2 ? <GroupRequestExplanation /> : <ConversationRequestExplanation />}
{(isGroupV2 && !!convoOrigin) || !isGroupV2 ? (
<StyledBlockUserText
onClick={() => {
handleDeclineAndBlockConversationRequest(selectedConvoId, selectedConvoId, convoOrigin);
}}
data-testid="decline-and-block-message-request"
>
{window.i18n('block')}
</StyledBlockUserText>
) : null}
</MessageRequestContainer>
);
};

@ -55,6 +55,7 @@ import { MessageDetail } from './message/message-item/MessageDetail';
import { HTMLDirection } from '../../util/i18n';
import { SessionSpinner } from '../basic/SessionSpinner';
import { ConversationMessageRequestButtons } from './MessageRequestButtons';
const DEFAULT_JPEG_QUALITY = 0.85;
@ -270,7 +271,7 @@ export class SessionConversation extends React.Component<Props, State> {
<div className="conversation-messages">
<NoMessageInConversation />
<ConversationMessageRequestButtons />
<SplitViewContainer
top={<InConversationCallContainer />}
bottom={

@ -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<HTMLDivElement>;
@ -126,7 +125,6 @@ class SessionMessagesListContainerInner extends React.Component<Props> {
isTyping={!!conversation.isTyping}
key="typing-bubble"
/>
<ConversationMessageRequestButtons />
<ScrollToLoadedMessageContext.Provider value={this.scrollToLoadedMessage}>
<SessionMessagesList

@ -1,18 +1,29 @@
import React from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { useIsIncomingRequest } from '../../hooks/useParamSelector';
import {
getSelectedHasMessages,
useConversationUsernameOrShorten,
useIsIncomingRequest,
} from '../../hooks/useParamSelector';
import { PubKey } from '../../session/types';
import {
hasSelectedConversationIncomingMessages,
useSelectedHasMessages,
} from '../../state/selectors/conversations';
import {
getSelectedCanWrite,
useSelectedConversationIdOrigin,
useSelectedConversationKey,
useSelectedHasDisabledBlindedMsgRequests,
useSelectedIsApproved,
useSelectedIsGroupV2,
useSelectedIsNoteToSelf,
useSelectedNicknameOrProfileNameOrShortenedPubkey,
} from '../../state/selectors/selectedConversation';
import {
useLibGroupInviteGroupName,
useLibGroupInvitePending,
} from '../../state/selectors/userGroups';
import { LocalizerKeys } from '../../types/LocalizerKeys';
import { SessionHtmlRenderer } from '../basic/SessionHTMLRenderer';
@ -30,9 +41,18 @@ const TextInner = styled.div`
max-width: 390px;
`;
function TextNotification({ html, dataTestId }: { html: string; dataTestId: string }) {
return (
<Container data-testid={dataTestId}>
<TextInner>
<SessionHtmlRenderer html={html} />
</TextInner>
</Container>
);
}
/**
* 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 (
<Container>
<TextInner>{window.i18n('respondingToRequestWarning')}</TextInner>
</Container>
<TextNotification
dataTestId="conversation-request-explanation"
html={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 (
<TextNotification
dataTestId="group-request-explanation"
html={window.i18n('respondingToGroupRequestWarning')}
/>
);
};
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 (
<TextNotification
dataTestId="group-invite-control-message"
html={window.i18n('userInvitedYouToGroup', [adminNameInvitedUs, groupName])}
/>
);
};
@ -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 <GroupRequestExplanation />
if (!selectedConversation || hasMessage || (isGroupV2 && isInvitePending)) {
return null;
}
let localizedKey: LocalizerKeys = 'noMessagesInEverythingElse';
@ -81,10 +159,9 @@ export const NoMessageInConversation = () => {
}
return (
<Container data-testid="empty-conversation-notification">
<TextInner>
<SessionHtmlRenderer html={window.i18n(localizedKey, [nameToRender])} />
</TextInner>
</Container>
<TextNotification
dataTestId="empty-conversation-notification"
html={window.i18n(localizedKey, [nameToRender])}
/>
);
};

@ -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.

@ -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';

@ -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';

@ -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';

@ -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';

@ -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
});
}

@ -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 (
<Item
onClick={() => {
declineConversationWithConfirm({
conversationId: convoId,
syncToDevices: true,
blockContact: false,
alsoBlock: false,
currentlySelectedConvo: selected || undefined,
conversationIdOrigin: null,
});
}}
>
{window.i18n('decline')}
{isGroupV2 ? window.i18n('delete') : window.i18n('decline')}
</Item>
);
}
@ -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 (
<Item
onClick={() => {
declineConversationWithConfirm({
conversationId: convoId,
syncToDevices: true,
blockContact: true,
alsoBlock: true,
currentlySelectedConvo: selected || undefined,
conversationIdOrigin: convoOrigin ?? null,
});
}}
>

@ -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,

@ -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: () => {

@ -237,6 +237,10 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
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<ConversationAttributes> {
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<ConversationAttributes> {
*/
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<ConversationAttributes> {
}
}
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<ConversationAttributes> {
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<ConversationAttributes> {
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<ConversationAttributes> {
}
/**
*
* @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
);
}

@ -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

@ -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,

@ -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({

@ -436,7 +436,7 @@ async function handleInboxOutboxMessages(
messageHash: '',
sentAt: postedAtInMs,
});
await outboxConversationModel.setOriginConversationID(serverConversationId);
await outboxConversationModel.setOriginConversationID(serverConversationId, true);
await handleOutboxMessageModel(
msgModel,

@ -189,6 +189,9 @@ export class SwarmPolling {
}
public async getPollingDetails(pollingEntries: Array<GroupPollingEntry>) {
// 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<PollForUs | PollForLegacy | PollForGroup> = [];
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 => {

@ -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

@ -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);
}

@ -153,6 +153,7 @@ class GroupInviteJob extends PersistedJob<GroupInvitePersistedData> {
} finally {
updateFailedStateForMember(groupPk, member, failed);
try {
debugger;
await MetaGroupWrapperActions.memberSetInvited(groupPk, member, failed);
} catch (e) {
window.log.warn('GroupInviteJob memberSetInvited failed with', e.message);

@ -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 {

@ -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;

@ -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;

@ -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<GroupPubkeyType, UserGroupsGet>;
};
export const initialUserGroupState: UserGroupState = {
userGroups: {},
};
const userGroupSlice = createSlice({
name: 'userGroup',
initialState: initialUserGroupState,
reducers: {
refreshUserGroupsSlice(
state: UserGroupState,
action: PayloadAction<{ groups: Array<UserGroupsGet> }>
) {
state.userGroups = {};
action.payload.groups.forEach(m => {
state.userGroups[m.pubkeyHex] = m;
});
return state;
},
},
});
export const userGroupsActions = {
...userGroupSlice.actions,
};
export const userGroupReducer = userGroupSlice.reducer;

@ -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

@ -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<MessageModelPropsWithoutConvoProps>): 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<ReduxConversationType>
): Array<ReduxConversationType> => {
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
);
}

@ -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;

@ -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);
}

@ -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);
}

@ -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'

@ -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<MergeSingle>) => 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<MergeSingle>) => 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<GroupPubkeyType, UserGroupsGet> = 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<MergeSingle>) => {
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<UserGroupsWrapperActionsCalls['eraseLegacyGroup']>
>,
createGroup: async () =>
callLibSessionWorker(['UserGroupsConfig', 'createGroup']) as Promise<
createGroup: async () => {
const group = (await callLibSessionWorker(['UserGroupsConfig', 'createGroup'])) as Awaited<
ReturnType<UserGroupsWrapperActionsCalls['createGroup']>
>,
>;
groups.set(group.pubkeyHex, group);
dispatchCachedGroupsToRedux();
return cloneDeep(group);
},
getGroup: async (pubkeyHex: GroupPubkeyType) =>
callLibSessionWorker(['UserGroupsConfig', 'getGroup', pubkeyHex]) as Promise<
ReturnType<UserGroupsWrapperActionsCalls['getGroup']>
>,
getGroup: async (pubkeyHex: GroupPubkeyType) => {
const group = (await callLibSessionWorker([
'UserGroupsConfig',
'getGroup',
pubkeyHex,
])) as Awaited<ReturnType<UserGroupsWrapperActionsCalls['getGroup']>>;
if (group) {
groups.set(group.pubkeyHex, group);
} else {
groups.delete(pubkeyHex);
}
dispatchCachedGroupsToRedux();
return cloneDeep(group);
},
getAllGroups: async () =>
callLibSessionWorker(['UserGroupsConfig', 'getAllGroups']) as Promise<
ReturnType<UserGroupsWrapperActionsCalls['getAllGroups']>
>,
getAllGroups: async () => {
const groupsFetched = (await callLibSessionWorker([
'UserGroupsConfig',
'getAllGroups',
])) as Awaited<ReturnType<UserGroupsWrapperActionsCalls['getAllGroups']>>;
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<UserGroupsWrapperActionsCalls['setGroup']>
>,
>;
groups.set(group.pubkeyHex, group);
dispatchCachedGroupsToRedux();
return cloneDeep(group);
},
eraseGroup: async (pubkeyHex: GroupPubkeyType) =>
callLibSessionWorker(['UserGroupsConfig', 'eraseGroup', pubkeyHex]) as Promise<
ReturnType<UserGroupsWrapperActionsCalls['eraseGroup']>
>,
eraseGroup: async (pubkeyHex: GroupPubkeyType) => {
const ret = (await callLibSessionWorker([
'UserGroupsConfig',
'eraseGroup',
pubkeyHex,
])) as Awaited<ReturnType<UserGroupsWrapperActionsCalls['eraseGroup']>>;
groups.delete(pubkeyHex);
dispatchCachedGroupsToRedux();
return ret;
},
};
export const ConvoInfoVolatileWrapperActions: ConvoInfoVolatileWrapperActionsCalls = {
@ -541,7 +590,7 @@ export const MetaGroupWrapperActions: MetaGroupWrapperActionsCalls = {
'swarmVerifySubAccount',
signingValue,
]) as Promise<ReturnType<MetaGroupWrapperActionsCalls['swarmVerifySubAccount']>>,
loadAdminKeys: async (groupPk: GroupPubkeyType, secret: Uint8ArrayLen64) => {
loadAdminKeys: async (groupPk: GroupPubkeyType, secret: Uint8ArrayLen64) => {
return callLibSessionWorker([`MetaGroupConfig-${groupPk}`, 'loadAdminKeys', secret]) as Promise<
ReturnType<MetaGroupWrapperActionsCalls['loadAdminKeys']>
>;

Loading…
Cancel
Save