diff --git a/ts/components/SessionInboxView.tsx b/ts/components/SessionInboxView.tsx index 26c59411c..bc44721ed 100644 --- a/ts/components/SessionInboxView.tsx +++ b/ts/components/SessionInboxView.tsx @@ -39,7 +39,7 @@ import { useHasDeviceOutdatedSyncing } from '../state/selectors/settings'; import { Storage } from '../util/storage'; import { NoticeBanner } from './NoticeBanner'; import { Flex } from './basic/Flex'; -import { initialGroupInfosState } from '../state/ducks/groupInfos'; +import { groupInfoActions, initialGroupState } from '../state/ducks/groups'; function makeLookup(items: Array, key: string): { [key: string]: T } { // Yep, we can't index into item without knowing what it is. True. But we want to. @@ -89,7 +89,7 @@ function createSessionInboxStore() { call: initialCallState, sogsRoomInfo: initialSogsRoomInfoState, settings: getSettingsInitialState(), - groupInfos: initialGroupInfosState, + groups: initialGroupState, }; return createStore(initialState); @@ -99,6 +99,7 @@ function setupLeftPane(forceUpdateInboxComponent: () => void) { window.openConversationWithMessages = openConversationWithMessages; window.inboxStore = createSessionInboxStore(); window.inboxStore.dispatch(updateAllOnStorageReady()); + window.inboxStore.dispatch(groupInfoActions.loadDumpsFromDB()); forceUpdateInboxComponent(); } diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 79ab31144..1d81aec0c 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -39,7 +39,7 @@ import { useSelectedIsPrivate, useSelectedIsPrivateFriend, useSelectedIsPublic, - useSelectedMembers, + useSelectedMembersCount, useSelectedNotificationSetting, useSelectedSubscriberCount, useSelectedisNoteToSelf, @@ -297,7 +297,7 @@ const ConversationHeaderTitle = () => { const isKickedFromGroup = useSelectedIsKickedFromGroup(); const isMe = useSelectedisNoteToSelf(); const isGroup = useSelectedIsGroup(); - const members = useSelectedMembers(); + let memberCount = useSelectedMembersCount(); if (!selectedConvoKey) { return null; @@ -309,13 +309,8 @@ const ConversationHeaderTitle = () => { return
{i18n('noteToSelf')}
; } - let memberCount = 0; - if (isGroup) { - if (isPublic) { - memberCount = subscriberCount || 0; - } else { - memberCount = members.length; - } + if (isPublic) { + memberCount = subscriberCount || 0; } let memberCountText = ''; diff --git a/ts/components/conversation/StagedGenericAttachment.tsx b/ts/components/conversation/StagedGenericAttachment.tsx index 7ce239c49..564a70cae 100644 --- a/ts/components/conversation/StagedGenericAttachment.tsx +++ b/ts/components/conversation/StagedGenericAttachment.tsx @@ -7,30 +7,28 @@ interface Props { onClose: (attachment: AttachmentType) => void; } -export class StagedGenericAttachment extends React.Component { - public render() { - const { attachment, onClose } = this.props; - const { fileName, contentType } = attachment; - const extension = getExtensionForDisplay({ contentType, fileName }); +export const StagedGenericAttachment = (props: Props) => { + const { attachment, onClose } = props; + const { fileName, contentType } = attachment; + const extension = getExtensionForDisplay({ contentType, fileName }); - return ( -
-
{ - if (onClose) { - onClose(attachment); - } - }} - /> -
- {extension ? ( -
{extension}
- ) : null} -
-
{fileName}
+ return ( +
+
{ + if (onClose) { + onClose(attachment); + } + }} + /> +
+ {extension ? ( +
{extension}
+ ) : null}
- ); - } -} +
{fileName}
+
+ ); +}; diff --git a/ts/components/conversation/SubtleNotification.tsx b/ts/components/conversation/SubtleNotification.tsx index 0e3eb862d..598deb784 100644 --- a/ts/components/conversation/SubtleNotification.tsx +++ b/ts/components/conversation/SubtleNotification.tsx @@ -64,7 +64,7 @@ export const NoMessageInConversation = () => { const canWrite = useSelector(getSelectedCanWrite); const privateBlindedAndBlockingMsgReqs = useSelectedHasDisabledBlindedMsgRequests(); // TODOLATER use this selector accross the whole application (left pane excluded) - const nameToRender = useSelectedNicknameOrProfileNameOrShortenedPubkey(); + const nameToRender = useSelectedNicknameOrProfileNameOrShortenedPubkey() || ''; if (!selectedConversation || hasMessage) { return null; diff --git a/ts/components/conversation/media-gallery/EmptyState.tsx b/ts/components/conversation/media-gallery/EmptyState.tsx index 17b9a7afa..550f4ebe0 100644 --- a/ts/components/conversation/media-gallery/EmptyState.tsx +++ b/ts/components/conversation/media-gallery/EmptyState.tsx @@ -7,10 +7,8 @@ interface Props { label: string; } -export class EmptyState extends React.Component { - public render() { - const { label } = this.props; +export const EmptyState = (props: Props) => { + const { label } = props; - return
{label}
; - } -} + return
{label}
; +}; diff --git a/ts/components/dialog/AdminLeaveClosedGroupDialog.tsx b/ts/components/dialog/AdminLeaveClosedGroupDialog.tsx index d5e7a6f9b..0e697dc1f 100644 --- a/ts/components/dialog/AdminLeaveClosedGroupDialog.tsx +++ b/ts/components/dialog/AdminLeaveClosedGroupDialog.tsx @@ -7,6 +7,7 @@ import { SessionWrapperModal } from '../SessionWrapperModal'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; import { SpacerLG } from '../basic/Text'; import { SessionSpinner } from '../basic/SessionSpinner'; +import { useConversationUsername } from '../../hooks/useParamSelector'; const StyledWarning = styled.p` max-width: 500px; @@ -15,9 +16,9 @@ const StyledWarning = styled.p` export const AdminLeaveClosedGroupDialog = (props: { conversationId: string }) => { const dispatch = useDispatch(); - const convo = getConversationController().get(props.conversationId); + const displayName = useConversationUsername(props.conversationId); const [loading, setLoading] = useState(false); - const titleText = `${window.i18n('leaveGroup')} ${convo?.getRealSessionUsername() || ''}`; + const titleText = `${window.i18n('leaveGroup')} ${displayName || ''}`; const closeDialog = () => { dispatch(adminLeaveClosedGroup(null)); diff --git a/ts/components/dialog/BanOrUnbanUserDialog.tsx b/ts/components/dialog/BanOrUnbanUserDialog.tsx index 41348238c..98409af92 100644 --- a/ts/components/dialog/BanOrUnbanUserDialog.tsx +++ b/ts/components/dialog/BanOrUnbanUserDialog.tsx @@ -2,7 +2,7 @@ import React, { useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useFocusMount } from '../../hooks/useFocusMount'; -import { useConversationPropsById } from '../../hooks/useParamSelector'; +import { useConversationUsername } from '../../hooks/useParamSelector'; import { ConversationModel } from '../../models/conversation'; import { sogsV3BanUser, @@ -73,16 +73,13 @@ export const BanOrUnBanUserDialog = (props: { const inputRef = useRef(null); useFocusMount(inputRef, true); - const wasGivenAPubkey = Boolean(pubkey?.length); const [inputBoxValue, setInputBoxValue] = useState(''); const [inProgress, setInProgress] = useState(false); - const sourceConvoProps = useConversationPropsById(pubkey); + const displayName = useConversationUsername(pubkey); const inputTextToDisplay = - wasGivenAPubkey && sourceConvoProps - ? `${sourceConvoProps.displayNameInProfile} ${PubKey.shorten(sourceConvoProps.id)}` - : undefined; + !!pubkey && displayName ? `${displayName} ${PubKey.shorten(pubkey)}` : undefined; /** * Ban or Unban a user from an open group @@ -97,7 +94,7 @@ export const BanOrUnBanUserDialog = (props: { if (isBanned) { // clear input box setInputBoxValue(''); - if (wasGivenAPubkey) { + if (pubkey) { dispatch(updateBanOrUnbanUserModal(null)); } } @@ -137,8 +134,8 @@ export const BanOrUnBanUserDialog = (props: { placeholder={i18n('enterSessionID')} dir="auto" onChange={onPubkeyBoxChanges} - disabled={inProgress || wasGivenAPubkey} - value={wasGivenAPubkey ? inputTextToDisplay : inputBoxValue} + disabled={inProgress || !!pubkey} + value={pubkey ? inputTextToDisplay : inputBoxValue} /> { const privateContactPubkeys = useSelector(getPrivateContactsPubkeys); let validContactsForInvite = _.clone(privateContactPubkeys); - const convoProps = useConversationPropsById(conversationId); + const isPrivate = useIsPrivate(conversationId); + const isPublic = useIsPublic(conversationId); + const membersFromRedux = useSortedGroupMembers(conversationId); + const zombiesFromRedux = useZombies(conversationId); + const displayName = useConversationUsername(conversationId); const { uniqueValues: selectedContacts, addTo, removeFrom } = useSet(); - if (!convoProps) { - throw new Error('InviteContactsDialogInner not a valid convoId given'); - } - if (convoProps.isPrivate) { + if (isPrivate) { throw new Error('InviteContactsDialogInner must be a group'); } - if (!convoProps.isPublic) { + if (!isPublic) { // filter our zombies and current members from the list of contact we can add - const members = convoProps.members || []; - const zombies = convoProps.zombies || []; + const members = membersFromRedux || []; + const zombies = zombiesFromRedux || []; validContactsForInvite = validContactsForInvite.filter( d => !members.includes(d) && !zombies.includes(d) ); } - const chatName = convoProps.displayNameInProfile || window.i18n('unknown'); - const isPublicConvo = convoProps.isPublic; + const chatName = displayName || window.i18n('unknown'); const closeDialog = () => { dispatch(updateInviteContactModal(null)); @@ -132,7 +138,7 @@ const InviteContactsDialogInner = (props: Props) => { const onClickOK = () => { if (selectedContacts.length > 0) { - if (isPublicConvo) { + if (isPublic) { void submitForOpenGroup(conversationId, selectedContacts); } else { void submitForClosedGroup(conversationId, selectedContacts); diff --git a/ts/components/dialog/ModeratorsRemoveDialog.tsx b/ts/components/dialog/ModeratorsRemoveDialog.tsx index 7436b0eda..984705dcb 100644 --- a/ts/components/dialog/ModeratorsRemoveDialog.tsx +++ b/ts/components/dialog/ModeratorsRemoveDialog.tsx @@ -1,5 +1,5 @@ -import React, { useState } from 'react'; import { compact } from 'lodash'; +import React, { useState } from 'react'; import { useDispatch } from 'react-redux'; import { getConversationController } from '../../session/conversations'; @@ -7,13 +7,18 @@ import { PubKey } from '../../session/types'; import { ToastUtils } from '../../session/utils'; import { Flex } from '../basic/Flex'; +import { + useConversationRealName, + useGroupAdmins, + useIsPublic, + useWeAreAdmin, +} from '../../hooks/useParamSelector'; +import { sogsV3RemoveAdmins } from '../../session/apis/open_group_api/sogsv3/sogsV3AddRemoveMods'; import { updateRemoveModeratorsModal } from '../../state/ducks/modalDialog'; +import { MemberListItem } from '../MemberListItem'; import { SessionWrapperModal } from '../SessionWrapperModal'; import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; import { SessionSpinner } from '../basic/SessionSpinner'; -import { MemberListItem } from '../MemberListItem'; -import { useConversationPropsById } from '../../hooks/useParamSelector'; -import { sogsV3RemoveAdmins } from '../../session/apis/open_group_api/sogsv3/sogsV3AddRemoveMods'; type Props = { conversationId: string; @@ -58,6 +63,11 @@ export const RemoveModeratorsDialog = (props: Props) => { dispatch(updateRemoveModeratorsModal(null)); }; + const weAreAdmin = useWeAreAdmin(conversationId); + const isPublic = useIsPublic(conversationId); + const displayName = useConversationRealName(conversationId); + const groupAdmins = useGroupAdmins(conversationId); + const removeModsCall = async () => { if (modsToRemove.length) { setRemovingInProgress(true); @@ -69,15 +79,14 @@ export const RemoveModeratorsDialog = (props: Props) => { } }; - const convoProps = useConversationPropsById(conversationId); - if (!convoProps || !convoProps.isPublic || !convoProps.weAreAdmin) { + if (!isPublic || !weAreAdmin) { throw new Error('RemoveModeratorsDialog: convoProps invalid'); } - const existingMods = convoProps.groupAdmins || []; + const existingMods = groupAdmins || []; const hasMods = existingMods.length !== 0; - const title = `${i18n('removeModerators')}: ${convoProps.displayNameInProfile}`; + const title = `${i18n('removeModerators')}: ${displayName}`; return ( diff --git a/ts/components/dialog/UpdateGroupMembersDialog.tsx b/ts/components/dialog/UpdateGroupMembersDialog.tsx index 070a1c2b0..dfd58a7af 100644 --- a/ts/components/dialog/UpdateGroupMembersDialog.tsx +++ b/ts/components/dialog/UpdateGroupMembersDialog.tsx @@ -1,18 +1,26 @@ -import React from 'react'; import _ from 'lodash'; +import React from 'react'; import { useDispatch } from 'react-redux'; import useKey from 'react-use/lib/useKey'; import styled from 'styled-components'; import { ToastUtils, UserUtils } from '../../session/utils'; -import { SpacerLG, Text } from '../basic/Text'; import { updateGroupMembersModal } from '../../state/ducks/modalDialog'; -import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; import { MemberListItem } from '../MemberListItem'; import { SessionWrapperModal } from '../SessionWrapperModal'; +import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; +import { SpacerLG, Text } from '../basic/Text'; -import { useConversationPropsById, useWeAreAdmin } from '../../hooks/useParamSelector'; +import { + useConversationUsername, + useGroupAdmins, + useIsPrivate, + useIsPublic, + useSortedGroupMembers, + useWeAreAdmin, + useZombies, +} from '../../hooks/useParamSelector'; import { useSet } from '../../hooks/useSet'; import { getConversationController } from '../../session/conversations'; @@ -38,12 +46,11 @@ const ClassicMemberList = (props: { }) => { const { onSelect, convoId, onUnselect, selectedMembers } = props; const weAreAdmin = useWeAreAdmin(convoId); - const convoProps = useConversationPropsById(convoId); - if (!convoProps) { - throw new Error('MemberList needs convoProps'); - } - let currentMembers = convoProps.members || []; - const { groupAdmins } = convoProps; + + const groupAdmins = useGroupAdmins(convoId); + const groupMembers = useSortedGroupMembers(convoId); + + let currentMembers = groupMembers || []; currentMembers = [...currentMembers].sort(m => (groupAdmins?.includes(m) ? -1 : 0)); return ( @@ -69,17 +76,17 @@ const ClassicMemberList = (props: { }; const ZombiesList = ({ convoId }: { convoId: string }) => { - const convoProps = useConversationPropsById(convoId); + const weAreAdmin = useWeAreAdmin(convoId); + const zombies = useZombies(convoId); function onZombieClicked() { - if (!convoProps?.weAreAdmin) { + if (!weAreAdmin) { ToastUtils.pushOnlyAdminCanRemove(); } } - if (!convoProps || !convoProps.zombies?.length) { + if (!zombies?.length) { return null; } - const { zombies, weAreAdmin } = convoProps; const zombieElements = zombies.map((zombie: string) => { const isSelected = weAreAdmin || false; // && !member.checkmarked; @@ -116,7 +123,7 @@ async function onSubmit(convoId: string, membersAfterUpdate: Array) { if (!convoFound || !convoFound.isGroup()) { throw new Error('Invalid convo for updateGroupMembersDialog'); } - if (!convoFound.isAdmin(UserUtils.getOurPubKeyStrFromCache())) { + if (!convoFound.weAreAdminUnblinded()) { window.log.warn('Skipping update of members, we are not the admin'); return; } @@ -168,8 +175,12 @@ async function onSubmit(convoId: string, membersAfterUpdate: Array) { export const UpdateGroupMembersDialog = (props: Props) => { const { conversationId } = props; - const convoProps = useConversationPropsById(conversationId); - const existingMembers = convoProps?.members || []; + const isPrivate = useIsPrivate(conversationId); + const isPublic = useIsPublic(conversationId); + const weAreAdmin = useWeAreAdmin(conversationId); + const existingMembers = useSortedGroupMembers(conversationId) || []; + const displayName = useConversationUsername(conversationId); + const groupAdmins = useGroupAdmins(conversationId); const { addTo, removeFrom, uniqueValues: membersToKeepWithUpdate } = useSet( existingMembers @@ -177,12 +188,10 @@ export const UpdateGroupMembersDialog = (props: Props) => { const dispatch = useDispatch(); - if (!convoProps || convoProps.isPrivate || convoProps.isPublic) { + if (isPrivate || isPublic) { throw new Error('UpdateGroupMembersDialog invalid convoProps'); } - const weAreAdmin = convoProps.weAreAdmin || false; - const closeDialog = () => { dispatch(updateGroupMembersModal(null)); }; @@ -214,7 +223,7 @@ export const UpdateGroupMembersDialog = (props: Props) => { ToastUtils.pushOnlyAdminCanRemove(); return; } - if (convoProps.groupAdmins?.includes(member)) { + if (groupAdmins?.includes(member)) { ToastUtils.pushCannotRemoveCreatorFromGroup(); window?.log?.warn( `User ${member} cannot be removed as they are the creator of the closed group.` @@ -228,7 +237,7 @@ export const UpdateGroupMembersDialog = (props: Props) => { const showNoMembersMessage = existingMembers.length === 0; const okText = window.i18n('ok'); const cancelText = window.i18n('cancel'); - const titleText = window.i18n('updateGroupDialogTitle', [convoProps.displayNameInProfile || '']); + const titleText = window.i18n('updateGroupDialogTitle', [displayName || '']); return ( diff --git a/ts/components/dialog/UpdateGroupNameDialog.tsx b/ts/components/dialog/UpdateGroupNameDialog.tsx index 63b59276f..3676c60a7 100644 --- a/ts/components/dialog/UpdateGroupNameDialog.tsx +++ b/ts/components/dialog/UpdateGroupNameDialog.tsx @@ -1,18 +1,20 @@ /* eslint-disable @typescript-eslint/no-misused-promises */ -import React from 'react'; -import classNames from 'classnames'; import autoBind from 'auto-bind'; +import classNames from 'classnames'; +import React from 'react'; -import { Avatar, AvatarSize } from '../avatar/Avatar'; -import { SpacerMD } from '../basic/Text'; -import { updateGroupNameModal } from '../../state/ducks/modalDialog'; +import { clone } from 'lodash'; import { ConversationModel } from '../../models/conversation'; import { getConversationController } from '../../session/conversations'; -import { SessionWrapperModal } from '../SessionWrapperModal'; -import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; -import { initiateOpenGroupUpdate } from '../../session/group/open-group'; import { initiateClosedGroupUpdate } from '../../session/group/closed-group'; +import { initiateOpenGroupUpdate } from '../../session/group/open-group'; +import { updateGroupNameModal } from '../../state/ducks/modalDialog'; +import { getLibGroupNameOutsideRedux } from '../../state/selectors/groups'; import { pickFileForAvatar } from '../../types/attachments/VisualAttachment'; +import { SessionWrapperModal } from '../SessionWrapperModal'; +import { Avatar, AvatarSize } from '../avatar/Avatar'; +import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton'; +import { SpacerMD } from '../basic/Text'; type Props = { conversationId: string; @@ -20,12 +22,14 @@ type Props = { interface State { groupName: string | undefined; + originalGroupName: string; errorDisplayed: boolean; errorMessage: string; oldAvatarPath: string | null; newAvatarObjecturl: string | null; } +// TODO break those last class bases components into functional ones (search for `extends React`) export class UpdateGroupNameDialog extends React.Component { private readonly convo: ConversationModel; @@ -35,8 +39,13 @@ export class UpdateGroupNameDialog extends React.Component { autoBind(this); this.convo = getConversationController().get(props.conversationId); + const libGroupName = getLibGroupNameOutsideRedux(props.conversationId); + const groupNameFromConvo = this.convo.getRealSessionUsername(); + const groupName = libGroupName || groupNameFromConvo; + this.state = { - groupName: this.convo.getRealSessionUsername(), + groupName: clone(groupName), + originalGroupName: clone(groupName) || '', errorDisplayed: false, errorMessage: 'placeholder', oldAvatarPath: this.convo.getAvatarPath(), @@ -61,10 +70,7 @@ export class UpdateGroupNameDialog extends React.Component { return; } - if ( - trimmedGroupName !== this.convo.getRealSessionUsername() || - newAvatarObjecturl !== oldAvatarPath - ) { + if (trimmedGroupName !== this.state.originalGroupName || newAvatarObjecturl !== oldAvatarPath) { if (this.convo.isPublic()) { void initiateOpenGroupUpdate(this.convo.id, trimmedGroupName, { objectUrl: newAvatarObjecturl, diff --git a/ts/components/leftpane/conversation-list-item/HeaderItem.tsx b/ts/components/leftpane/conversation-list-item/HeaderItem.tsx index 1f435bfb1..de338da5c 100644 --- a/ts/components/leftpane/conversation-list-item/HeaderItem.tsx +++ b/ts/components/leftpane/conversation-list-item/HeaderItem.tsx @@ -5,11 +5,11 @@ import styled from 'styled-components'; import { Data } from '../../../data/data'; import { useActiveAt, - useConversationPropsById, useHasUnread, useIsForcedUnreadWithoutUnreadMsg, useIsPinned, useMentionedUs, + useNotificationSetting, useUnreadCount, } from '../../../hooks/useParamSelector'; import { @@ -26,7 +26,7 @@ import { UserItem } from './UserItem'; const NotificationSettingIcon = () => { const isMessagesSection = useSelector(getIsMessageSection); const convoId = useConvoIdFromContext(); - const convoSetting = useConversationPropsById(convoId)?.currentNotificationSetting; + const convoSetting = useNotificationSetting(convoId); if (!isMessagesSection) { return null; diff --git a/ts/components/leftpane/conversation-list-item/MessageItem.tsx b/ts/components/leftpane/conversation-list-item/MessageItem.tsx index dbffb1b45..db82ed9e8 100644 --- a/ts/components/leftpane/conversation-list-item/MessageItem.tsx +++ b/ts/components/leftpane/conversation-list-item/MessageItem.tsx @@ -3,10 +3,10 @@ import { isEmpty } from 'lodash'; import React from 'react'; import { useSelector } from 'react-redux'; import { - useConversationPropsById, useHasUnread, useIsPrivate, useIsTyping, + useLastMessage, } from '../../../hooks/useParamSelector'; import { isSearching } from '../../../state/selectors/search'; import { getIsMessageRequestOverlayShown } from '../../../state/selectors/section'; @@ -15,17 +15,9 @@ import { MessageBody } from '../../conversation/message/message-content/MessageB import { OutgoingMessageStatus } from '../../conversation/message/message-content/OutgoingMessageStatus'; import { useConvoIdFromContext } from './ConvoIdContext'; -function useLastMessageFromConvo(convoId: string) { - const convoProps = useConversationPropsById(convoId); - if (!convoProps) { - return null; - } - return convoProps.lastMessage; -} - export const MessageItem = () => { const conversationId = useConvoIdFromContext(); - const lastMessage = useLastMessageFromConvo(conversationId); + const lastMessage = useLastMessage(conversationId); const isGroup = !useIsPrivate(conversationId); const hasUnread = useHasUnread(conversationId); diff --git a/ts/data/configDump/configDump.ts b/ts/data/configDump/configDump.ts index 777437353..ae3f43b20 100644 --- a/ts/data/configDump/configDump.ts +++ b/ts/data/configDump/configDump.ts @@ -1,3 +1,4 @@ +import { GroupPubkeyType } from 'libsession_util_nodejs'; import { AsyncObjectWrapper, ConfigDumpDataNode, ConfigDumpRow } from '../../types/sqlSharedTypes'; // eslint-disable-next-line import/no-unresolved, import/extensions import { ConfigWrapperObjectTypesMeta } from '../../webworker/workers/browser/libsession_worker_functions'; @@ -20,4 +21,7 @@ export const ConfigDumpData: AsyncObjectWrapper = { getAllDumpsWithoutDataFor: (pk: string) => { return channels.getAllDumpsWithoutDataFor(pk); }, + deleteDumpFor: (pk: GroupPubkeyType) => { + return channels.deleteDumpFor(pk); + }, }; diff --git a/ts/hooks/useParamSelector.ts b/ts/hooks/useParamSelector.ts index a04e8b04d..dc34f7123 100644 --- a/ts/hooks/useParamSelector.ts +++ b/ts/hooks/useParamSelector.ts @@ -4,13 +4,14 @@ import { hasValidIncomingRequestValues, hasValidOutgoingRequestValues, } from '../models/conversation'; +import { isUsAnySogsFromCache } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys'; +import { CONVERSATION } from '../session/constants'; import { PubKey } from '../session/types'; import { UserUtils } from '../session/utils'; import { StateType } from '../state/reducer'; import { getMessageReactsProps } from '../state/selectors/conversations'; +import { useLibGroupAdmins, useLibGroupMembers, useLibGroupName } from '../state/selectors/groups'; import { isPrivateAndFriend } from '../state/selectors/selectedConversation'; -import { CONVERSATION } from '../session/constants'; -import { isUsAnySogsFromCache } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys'; export function useAvatarPath(convoId: string | undefined) { const convoProps = useConversationPropsById(convoId); @@ -27,7 +28,11 @@ export function useOurAvatarPath() { */ export function useConversationUsername(convoId?: string) { const convoProps = useConversationPropsById(convoId); + const groupName = useLibGroupName(convoId); + if (convoId && PubKey.isClosedGroupV2(convoId)) { + return groupName; + } return convoProps?.nickname || convoProps?.displayNameInProfile || convoId; } @@ -147,6 +152,18 @@ export function useWeAreAdmin(convoId?: string) { return Boolean(convoProps && convoProps.weAreAdmin); } +export function useGroupAdmins(convoId?: string) { + const convoProps = useConversationPropsById(convoId); + + const libMembers = useLibGroupAdmins(convoId); + + if (convoId && PubKey.isClosedGroupV2(convoId)) { + return compact(libMembers?.slice()?.sort()) || []; + } + + return convoProps?.groupAdmins || []; +} + export function useExpireTimer(convoId?: string) { const convoProps = useConversationPropsById(convoId); return convoProps && convoProps.expireTimer; @@ -203,7 +220,12 @@ export function useIsOutgoingRequest(convoId?: string) { ); } -export function useConversationPropsById(convoId?: string) { +/** + * Not to be exported: This selector is too generic and needs to be broken node in individual fields selectors. + * Make sure when writing a selector that you fetch the data from libsession if needed. + * (check useSortedGroupMembers() as an example) + */ +function useConversationPropsById(convoId?: string) { return useSelector((state: StateType) => { if (!convoId) { return null; @@ -216,6 +238,32 @@ export function useConversationPropsById(convoId?: string) { }); } +export function useZombies(convoId?: string) { + return useSelector((state: StateType) => { + if (!convoId) { + return null; + } + const convo = state.conversations.conversationLookup[convoId]; + if (!convo) { + return null; + } + return convo.zombies; + }); +} + +export function useLastMessage(convoId?: string) { + return useSelector((state: StateType) => { + if (!convoId) { + return null; + } + const convo = state.conversations.conversationLookup[convoId]; + if (!convo) { + return null; + } + return convo.lastMessage; + }); +} + export function useMessageReactsPropsById(messageId?: string) { return useSelector((state: StateType) => { if (!messageId) { @@ -279,15 +327,26 @@ export function useQuoteAuthorName( return { authorName, isMe }; } +function useMembers(convoId: string | undefined) { + const props = useConversationPropsById(convoId); + return props?.members || undefined; +} + /** * Get the list of members of a closed group or [] * @param convoId the closed group id to extract members from */ export function useSortedGroupMembers(convoId: string | undefined): Array { - const convoProps = useConversationPropsById(convoId); - if (!convoProps || convoProps.isPrivate || convoProps.isPublic) { + const members = useMembers(convoId); + const isPublic = useIsPublic(convoId); + const isPrivate = useIsPrivate(convoId); + const libMembers = useLibGroupMembers(convoId); + if (isPrivate || isPublic) { return []; } + if (convoId && PubKey.isClosedGroupV2(convoId)) { + return compact(libMembers?.slice()?.sort()) || []; + } // we need to clone the array before being able to call sort() it - return compact(convoProps.members?.slice()?.sort()) || []; + return compact(members?.slice()?.sort()) || []; } diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 8ded7592b..fa143f70b 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -234,9 +234,7 @@ export function showLeaveGroupByConvoId(conversationId: string) { const title = window.i18n('leaveGroup'); const message = window.i18n('leaveGroupConfirmation'); - const isAdmin = (conversation.get('groupAdmins') || []).includes( - UserUtils.getOurPubKeyStrFromCache() - ); + const isAdmin = conversation.getGroupAdmins().includes(UserUtils.getOurPubKeyStrFromCache()); const isClosedGroup = conversation.isClosedGroup() || false; const isPublic = conversation.isPublic() || false; diff --git a/ts/interactions/conversations/unsendingInteractions.ts b/ts/interactions/conversations/unsendingInteractions.ts index 626dcfb50..577bbcb65 100644 --- a/ts/interactions/conversations/unsendingInteractions.ts +++ b/ts/interactions/conversations/unsendingInteractions.ts @@ -249,7 +249,7 @@ const doDeleteSelectedMessagesInSOGS = async ( } // #region open group v2 deletion // Get our Moderator status - const isAdmin = conversation.isAdmin(ourDevicePubkey); + const isAdmin = conversation.weAreAdminUnblinded(); const isModerator = conversation.isModerator(ourDevicePubkey); if (!isAllOurs && !(isAdmin || isModerator)) { @@ -302,16 +302,16 @@ const doDeleteSelectedMessages = async ({ return; } - const isAllOurs = selectedMessages.every(message => ourDevicePubkey === message.getSource()); + const areAllOurs = selectedMessages.every(message => ourDevicePubkey === message.getSource()); if (conversation.isPublic()) { - await doDeleteSelectedMessagesInSOGS(selectedMessages, conversation, isAllOurs); + await doDeleteSelectedMessagesInSOGS(selectedMessages, conversation, areAllOurs); return; } // #region deletion for 1-1 and closed groups if (deleteForEveryone) { - if (!isAllOurs) { + if (!areAllOurs) { ToastUtils.pushMessageDeleteForbidden(); window.inboxStore?.dispatch(resetSelectedMessageIds()); return; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 43ffde283..1cac2c805 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -1,5 +1,6 @@ import autoBind from 'auto-bind'; import Backbone from 'backbone'; +import { from_hex } from 'libsodium-wrappers-sumo'; import { debounce, defaults, @@ -16,7 +17,6 @@ import { uniq, xor, } from 'lodash'; -import { from_hex } from 'libsodium-wrappers-sumo'; import { SignalService } from '../protobuf'; import { getMessageQueue } from '../session'; @@ -115,12 +115,17 @@ import { import { LibSessionUtil } from '../session/utils/libsession/libsession_utils'; import { SessionUtilUserProfile } from '../session/utils/libsession/libsession_utils_user_profile'; import { ReduxSogsRoomInfos } from '../state/ducks/sogsRoomInfo'; +import { + getLibGroupAdminsOutsideRedux, + getLibGroupNameOutsideRedux, +} from '../state/selectors/groups'; import { getCanWriteOutsideRedux, getModeratorsOutsideRedux, getSubscriberCountOutsideRedux, } from '../state/selectors/sogsRoomInfo'; import { markAttributesAsReadIfNeeded } from './messageFactory'; +import { PreConditionFailed } from '../session/utils/errors'; type InMemoryConvoInfos = { mentionedUs: boolean; @@ -276,19 +281,12 @@ export class ConversationModel extends Backbone.Model { await deleteExternalFilesOfConversation(this.attributes); } - public getGroupAdmins(): Array { - const groupAdmins = this.get('groupAdmins'); - - return groupAdmins && groupAdmins.length > 0 ? groupAdmins : []; - } - public getConversationModelProps(): ReduxConversationType { const isPublic = this.isPublic(); - const ourNumber = UserUtils.getOurPubKeyStrFromCache(); const avatarPath = this.getAvatarPath(); const isPrivate = this.isPrivate(); - const weAreAdmin = this.isAdmin(ourNumber); + const weAreAdmin = this.weAreAdminUnblinded(); const currentNotificationSetting = this.get('triggerNotificationsFor'); const priorityFromDb = this.get('priority'); @@ -1120,7 +1118,7 @@ export class ConversationModel extends Backbone.Model { * @returns `displayNameInProfile` so the real username as defined by that user/group */ public getRealSessionUsername(): string | undefined { - return this.get('displayNameInProfile'); + return getLibGroupNameOutsideRedux(this.id) || this.get('displayNameInProfile'); } /** @@ -1161,10 +1159,18 @@ export class ConversationModel extends Backbone.Model { if (!pubKey) { throw new Error('isAdmin() pubKey is falsy'); } - const groupAdmins = this.getGroupAdmins(); + const groupAdmins = getLibGroupAdminsOutsideRedux(this.id) || this.getGroupAdmins(); return Array.isArray(groupAdmins) && groupAdmins.includes(pubKey); } + public weAreAdminUnblinded() { + const us = UserUtils.getOurPubKeyStrFromCache(); + if (!us) { + throw new PreConditionFailed('weAreAdminUnblinded: our pubkey is not set'); + } + return this.isAdmin(us); + } + /** * Check if the provided pubkey is a moderator. * Being a moderator only makes sense for a sogs as closed groups have their admin under the groupAdmins property @@ -1692,6 +1698,12 @@ export class ConversationModel extends Backbone.Model { return this.markConversationReadBouncy(newestUnreadDate); } + public getGroupAdmins(): Array { + const groupAdmins = this.get('groupAdmins'); + + return groupAdmins && groupAdmins.length > 0 ? groupAdmins : []; + } + private async sendMessageJob(message: MessageModel, expireTimer: number | undefined) { try { const { body, attachments, preview, quote, fileIdsToLink } = await message.uploadData(); diff --git a/ts/node/sql_calls/config_dump.ts b/ts/node/sql_calls/config_dump.ts index aa00f21e1..78fe4f382 100644 --- a/ts/node/sql_calls/config_dump.ts +++ b/ts/node/sql_calls/config_dump.ts @@ -12,6 +12,7 @@ import { // eslint-disable-next-line import/no-unresolved, import/extensions import { ConfigWrapperObjectTypesMeta } from '../../webworker/workers/browser/libsession_worker_functions'; import { assertGlobalInstance } from '../sqlInstance'; +import { GroupPubkeyType } from 'libsession_util_nodejs'; function parseRow( row: Pick @@ -114,4 +115,9 @@ export const configDumpData: ConfigDumpDataNode = { data, }); }, + deleteDumpFor: (publicKey: GroupPubkeyType) => { + assertGlobalInstance() + .prepare(`DELETE FROM ${CONFIG_DUMP_TABLE} WHERE publicKey=$publicKey;`) + .run({ publicKey }); + }, }; diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index c5ba8b879..ea0be9c0f 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -431,7 +431,7 @@ async function handleClosedGroupEncryptionKeyPair( await removeFromCache(envelope); return; } - if (!groupConvo.get('groupAdmins')?.includes(sender)) { + if (!groupConvo.getGroupAdmins().includes(sender)) { window?.log?.warn( `Ignoring closed group encryption key pair from non-admin. ${groupPublicKey}` ); @@ -684,7 +684,7 @@ async function areWeAdmin(groupConvo: ConversationModel) { throw new Error('areWeAdmin needs a convo'); } - const groupAdmins = groupConvo.get('groupAdmins'); + const groupAdmins = groupConvo.getGroupAdmins(); const ourNumber = UserUtils.getOurPubKeyStrFromCache(); return groupAdmins?.includes(ourNumber) || false; } @@ -706,7 +706,7 @@ async function handleClosedGroupMembersRemoved( window?.log?.info(`Got a group update for group ${envelope.source}, type: MEMBERS_REMOVED`); const membersAfterUpdate = _.difference(currentMembers, removedMembers); - const groupAdmins = convo.get('groupAdmins'); + const groupAdmins = convo.getGroupAdmins(); if (!groupAdmins?.length) { throw new Error('No admins found for closed group member removed update.'); } @@ -837,7 +837,7 @@ async function handleClosedGroupMemberLeft( ) { const sender = envelope.senderIdentity; const groupPublicKey = envelope.source; - const didAdminLeave = convo.get('groupAdmins')?.includes(sender) || false; + const didAdminLeave = convo.getGroupAdmins().includes(sender) || false; // If the admin leaves the group is disbanded // otherwise, we remove the sender from the list of current members in this group const oldMembers = convo.get('members') || []; diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index 99a95b8f5..d705280e7 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -86,10 +86,9 @@ async function printDumpForDebug(prefix: string, variant: ConfigWrapperObjectTyp window.log.info(prefix, StringUtils.toHex(await GenericWrapperActions.dump(variant))); return; } - const metaGroupDumps = await MetaGroupWrapperActions.metaDump( + const metaGroupDumps = await MetaGroupWrapperActions.metaDebugDump( getGroupPubkeyFromWrapperType(variant) ); - window.log.info(prefix, StringUtils.toHex(metaGroupDumps)); } 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 b35f8b890..9f0f20ef8 100644 --- a/ts/session/apis/snode_api/swarm_polling_config/SwarmPollingGroupConfig.ts +++ b/ts/session/apis/snode_api/swarm_polling_config/SwarmPollingGroupConfig.ts @@ -5,6 +5,8 @@ import { ed25519Str } from '../../../onions/onionPath'; import { fromBase64ToArray } from '../../../utils/String'; import { SnodeNamespaces } from '../namespaces'; import { RetrieveMessageItemWithNamespace } from '../types'; +import { groupInfoActions } from '../../../../state/ducks/groups'; +import { LibSessionUtil } from '../../../utils/libsession/libsession_utils'; async function handleGroupSharedConfigMessages( groupConfigMessagesMerged: Array, @@ -40,18 +42,28 @@ async function handleGroupSharedConfigMessages( groupKeys: keys, groupMember: members, }; - console.info(`About to merge ${stringify(toMerge)}`); - console.info(`dumps before ${stringify(await MetaGroupWrapperActions.metaDump(groupPk))}`); console.info( `groupInfo before merge: ${stringify(await MetaGroupWrapperActions.infoGet(groupPk))}` ); - const countMerged = await MetaGroupWrapperActions.metaMerge(groupPk, toMerge); - console.info( - `countMerged ${countMerged}, groupInfo after merge: ${stringify( - await MetaGroupWrapperActions.infoGet(groupPk) - )}` + + await MetaGroupWrapperActions.metaMerge(groupPk, toMerge); + await LibSessionUtil.saveMetaGroupDumpToDb(groupPk); + + const updatedInfos = await MetaGroupWrapperActions.infoGet(groupPk); + const updatedMembers = await MetaGroupWrapperActions.memberGetAll(groupPk); + console.info(`groupInfo after merge: ${stringify(updatedInfos)}`); + console.info(`groupMembers after merge: ${stringify(updatedMembers)}`); + if (!updatedInfos || !updatedMembers) { + throw new Error('updatedInfos or updatedMembers is null but we just created them'); + } + + window.inboxStore.dispatch( + groupInfoActions.updateGroupDetailsAfterMerge({ + groupPk, + infos: updatedInfos, + members: updatedMembers, + }) ); - console.info(`dumps after ${stringify(await MetaGroupWrapperActions.metaDump(groupPk))}`); // if (allDecryptedConfigMessages.length) { // try { diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 38b7fec95..cca58fae7 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -459,7 +459,7 @@ async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) { } const ourNumber = UserUtils.getOurPubKeyStrFromCache(); - const isCurrentUserAdmin = convo.get('groupAdmins')?.includes(ourNumber); + const isCurrentUserAdmin = convo.weAreAdminUnblinded(); let members: Array = []; let admins: Array = []; @@ -474,7 +474,7 @@ async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) { // otherwise, just the exclude ourself from the members and trigger an update with this convo.set({ left: true }); members = (convo.get('members') || []).filter((m: string) => m !== ourNumber); - admins = convo.get('groupAdmins') || []; + admins = convo.getGroupAdmins(); } convo.set({ members }); await convo.updateGroupAdmins(admins, false); diff --git a/ts/session/conversations/createClosedGroup.ts b/ts/session/conversations/createClosedGroup.ts index fa7f48782..85f568238 100644 --- a/ts/session/conversations/createClosedGroup.ts +++ b/ts/session/conversations/createClosedGroup.ts @@ -16,7 +16,7 @@ import { PubKey } from '../types'; import { UserUtils } from '../utils'; import { forceSyncConfigurationNowIfNeeded } from '../utils/sync/syncUtils'; import { getConversationController } from './ConversationController'; -import { groupInfoActions } from '../../state/ducks/groupInfos'; +import { groupInfoActions } from '../../state/ducks/groups'; /** * Creates a brand new closed group from user supplied details. This function generates a new identityKeyPair so cannot be used to restore a closed group. @@ -29,7 +29,7 @@ export async function createClosedGroup(groupName: string, members: Array { - const groupConversation = getConversationController().get(groupId.key); - const groupMembers = groupConversation ? groupConversation.get('members') : undefined; - - if (!groupMembers) { - return []; - } - - return groupMembers.map(PubKey.cast); -} - export function isClosedGroup(groupId: PubKey): boolean { const conversation = getConversationController().get(groupId.key); diff --git a/ts/session/utils/job_runners/jobs/GroupConfigJob.ts b/ts/session/utils/job_runners/jobs/GroupConfigJob.ts index 974d69bea..46b3edd25 100644 --- a/ts/session/utils/job_runners/jobs/GroupConfigJob.ts +++ b/ts/session/utils/job_runners/jobs/GroupConfigJob.ts @@ -2,7 +2,6 @@ import { GroupPubkeyType } from 'libsession_util_nodejs'; import { isArray, isEmpty, isNumber, isString } from 'lodash'; import { UserUtils } from '../..'; -import { stringify } from '../../../../types/sqlSharedTypes'; import { ReleasedFeatures } from '../../../../util/releaseFeature'; import { isSignInByLinking } from '../../../../util/storage'; import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface'; @@ -183,14 +182,12 @@ class GroupSyncJob extends PersistedJob { data: item.data, }; }); - console.warn(`msgs ${stringify(msgs)}`); const result = await MessageSender.sendEncryptedDataToSnode( msgs, thisJobDestination, oldHashesToDelete ); - console.warn(`result ${stringify(result)}`); const expectedReplyLength = singleDestChanges.messages.length + (oldHashesToDelete.size ? 1 : 0); diff --git a/ts/session/utils/libsession/libsession_utils.ts b/ts/session/utils/libsession/libsession_utils.ts index 205fac53d..7e70a0769 100644 --- a/ts/session/utils/libsession/libsession_utils.ts +++ b/ts/session/utils/libsession/libsession_utils.ts @@ -9,7 +9,11 @@ import { ConfigDumpData } from '../../../data/configDump/configDump'; import { HexString } from '../../../node/hexStrings'; import { SignalService } from '../../../protobuf'; import { UserConfigKind } from '../../../types/ProtobufKind'; -import { assertUnreachable, toFixedUint8ArrayOfLength } from '../../../types/sqlSharedTypes'; +import { + ConfigDumpRow, + assertUnreachable, + toFixedUint8ArrayOfLength, +} from '../../../types/sqlSharedTypes'; import { ConfigWrapperGroupDetailed, ConfigWrapperUser, @@ -109,11 +113,15 @@ async function initializeLibSessionUtilWrappers() { `initializeLibSessionUtilWrappers: missingRequiredVariants "${missingVariant}" created` ); } + + await loadMetaGroupWrappers(dumps); +} + +async function loadMetaGroupWrappers(dumps: Array) { const ed25519KeyPairBytes = await getUserED25519KeyPairBytes(); if (!ed25519KeyPairBytes?.privKeyBytes) { throw new Error('user has no ed25519KeyPairBytes.'); } - // TODO then load the Group wrappers (not handled yet) into memory // load the dumps retrieved from the database into their corresponding wrappers for (let index = 0; index < dumps.length; index++) { const dump = dumps[index]; @@ -128,7 +136,23 @@ async function initializeLibSessionUtilWrappers() { try { const foundInUserGroups = await UserGroupsWrapperActions.getGroup(groupPk); - window.log.debug('initializeLibSessionUtilWrappers initing from dump', variant); + // remove it right away, and skip it entirely + if (!foundInUserGroups) { + try { + window.log.info( + 'metaGroup not found in userGroups. Deleting the corresponding dumps:', + groupPk + ); + + await ConfigDumpData.deleteDumpFor(groupPk); + } catch (e) { + window.log.warn('deleteDumpFor failed with ', e.message); + } + // await UserGroupsWrapperActions.eraseGroup(groupPk); + continue; + } + + window.log.debug('initializeLibSessionUtilWrappers initing from metagroup dump', variant); // TODO we need to fetch the admin key here if we have it, maybe from the usergroup wrapper? await MetaGroupWrapperActions.init(groupPk, { groupEd25519Pubkey: toFixedUint8ArrayOfLength(groupEd25519Pubkey, 32), @@ -136,6 +160,9 @@ async function initializeLibSessionUtilWrappers() { userEd25519Secretkey: toFixedUint8ArrayOfLength(ed25519KeyPairBytes.privKeyBytes, 64), metaDumped: dump.data, }); + + // Annoyingly, the redux store is not initialized when this current funciton is called, + // so we need to init the group wrappers here, but load them in their redux slice later } catch (e) { // TODO should not throw in this case? we should probably just try to load what we manage to load window.log.warn(`initGroup of Group wrapper of variant ${variant} failed with ${e.message} `); @@ -332,6 +359,9 @@ async function saveMetaGroupDumpToDb(groupPk: GroupPubkeyType) { publicKey: groupPk, variant: `MetaGroupConfig-${groupPk}`, }); + window.log.debug(`Saved dumps for metagroup ${groupPk}`); + } else { + window.log.debug(`meta did not dumps saving for metagroup ${groupPk}`); } } diff --git a/ts/session/utils/libsession/libsession_utils_contacts.ts b/ts/session/utils/libsession/libsession_utils_contacts.ts index 0db5442c3..db45b1bdb 100644 --- a/ts/session/utils/libsession/libsession_utils_contacts.ts +++ b/ts/session/utils/libsession/libsession_utils_contacts.ts @@ -47,13 +47,13 @@ async function insertContactFromDBIntoWrapperAndRefresh(id: string): Promise) => { publicKey: groupPubKey, name: c.get('displayNameInProfile') || '', members: c.get('members') || [], - admins: c.get('groupAdmins') || [], + admins: c.getGroupAdmins(), encryptionKeyPair: ECKeyPair.fromHexKeyPair(fetchEncryptionKeyPair), }); }) diff --git a/ts/state/actions.ts b/ts/state/actions.ts index c017e50a9..921a3d714 100644 --- a/ts/state/actions.ts +++ b/ts/state/actions.ts @@ -7,7 +7,7 @@ import { actions as sections } from './ducks/section'; import { actions as theme } from './ducks/theme'; import { actions as modalDialog } from './ducks/modalDialog'; import { actions as primaryColor } from './ducks/primaryColor'; -import { groupInfoActions } from './ducks/groupInfos'; +import { groupInfoActions } from './ducks/groups'; export function mapDispatchToProps(dispatch: Dispatch): object { return { diff --git a/ts/state/ducks/groupInfos.ts b/ts/state/ducks/groupInfos.ts deleted file mode 100644 index 996edcb1e..000000000 --- a/ts/state/ducks/groupInfos.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { GroupInfoGet, GroupInfoShared, GroupPubkeyType } from 'libsession_util_nodejs'; -import { uniq } from 'lodash'; -import { ConversationTypeEnum } from '../../models/conversationAttributes'; -import { HexString } from '../../node/hexStrings'; -import { ClosedGroup } from '../../session'; -import { getConversationController } from '../../session/conversations'; -import { UserUtils } from '../../session/utils'; -import { GroupSync } from '../../session/utils/job_runners/jobs/GroupConfigJob'; -import { toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes'; -import { - MetaGroupWrapperActions, - UserGroupsWrapperActions, -} from '../../webworker/workers/browser/libsession_worker_interface'; - -type GroupInfoGetWithId = GroupInfoGet & { id: GroupPubkeyType }; - -export type GroupInfosState = { - infos: Record; -}; - -export const initialGroupInfosState: GroupInfosState = { - infos: {}, -}; - -const updateGroupInfoInWrapper = createAsyncThunk( - 'groupInfos/updateGroupInfoInWrapper', - async ({ - id, - data, - }: { - id: GroupPubkeyType; - data: GroupInfoShared; - }): Promise => { - // TODO this will throw if the wrapper is not init yet... how to make sure it does exist? - const infos = await MetaGroupWrapperActions.infoSet(id, data); - return { id, ...infos }; - } -); - -const initNewGroupInfoInWrapper = createAsyncThunk( - 'groupInfos/initNewGroupInfoInWrapper', - async (groupDetails: { - groupName: string; - members: Array; - }): Promise => { - try { - const newGroup = await UserGroupsWrapperActions.createGroup(); - - await UserGroupsWrapperActions.setGroup(newGroup); - - const ourEd25519KeypairBytes = await UserUtils.getUserED25519KeyPairBytes(); - if (!ourEd25519KeypairBytes) { - throw new Error('Current user has no priv ed25519 key?'); - } - const userEd25519Secretkey = ourEd25519KeypairBytes.privKeyBytes; - const groupEd2519Pk = HexString.fromHexString(newGroup.pubkeyHex).slice(1); // remove the 03 prefix (single byte once in hex form) - - // dump is always empty when creating a new groupInfo - await MetaGroupWrapperActions.init(newGroup.pubkeyHex, { - metaDumped: null, - userEd25519Secretkey: toFixedUint8ArrayOfLength(userEd25519Secretkey, 64), - groupEd25519Secretkey: newGroup.secretKey, - groupEd25519Pubkey: toFixedUint8ArrayOfLength(groupEd2519Pk, 32), - }); - - await Promise.all( - groupDetails.members.map(async member => { - const created = await MetaGroupWrapperActions.memberGetOrConstruct( - newGroup.pubkeyHex, - member - ); - await MetaGroupWrapperActions.memberSetInvited( - newGroup.pubkeyHex, - created.pubkeyHex, - false - ); - }) - ); - const infos = await MetaGroupWrapperActions.infoGet(newGroup.pubkeyHex); - - if (!infos) { - throw new Error( - `getInfos of ${newGroup.pubkeyHex} returned empty result even if it was just init.` - ); - } - infos.name = groupDetails.groupName; - await MetaGroupWrapperActions.infoSet(newGroup.pubkeyHex, infos); - - const convo = await getConversationController().getOrCreateAndWait( - newGroup.pubkeyHex, - ConversationTypeEnum.GROUPV3 - ); - - await convo.setIsApproved(true, false); - - console.warn('store the v3 identityPrivatekeypair as part of the wrapper only?'); - // the sync below will need the secretKey of the group to be saved in the wrapper. So save it! - await UserGroupsWrapperActions.setGroup(newGroup); - - await GroupSync.queueNewJobIfNeeded(newGroup.pubkeyHex); - - const us = UserUtils.getOurPubKeyStrFromCache(); - // Ensure the current user is a member and admin - const members = uniq([...groupDetails.members, us]); - - const updateGroupDetails: ClosedGroup.GroupInfo = { - id: newGroup.pubkeyHex, - name: groupDetails.groupName, - members, - admins: [us], - activeAt: Date.now(), - expireTimer: 0, - }; - - // be sure to call this before sending the message. - // the sending pipeline needs to know from GroupUtils when a message is for a medium group - await ClosedGroup.updateOrCreateClosedGroup(updateGroupDetails); - await convo.commit(); - convo.updateLastMessage(); - - return { id: newGroup.pubkeyHex, ...infos }; - } catch (e) { - throw e; - } - } -); - -/** - * This slice is the one holding the default joinable rooms fetched once in a while from the default opengroup v2 server. - */ -const groupInfosSlice = createSlice({ - name: 'groupInfos', - initialState: initialGroupInfosState, - reducers: { - updateGroupInfosFromMergeResults(state, action: PayloadAction>) { - // anything not in the results should not be in the state here - state.infos = {}; - action.payload.forEach(infos => { - state.infos[infos.id] = infos; - }); - return state; - }, - }, - extraReducers: builder => { - builder.addCase(updateGroupInfoInWrapper.fulfilled, (state, action) => { - state.infos[action.payload.id] = action.payload; - }); - builder.addCase(initNewGroupInfoInWrapper.fulfilled, (state, action) => { - state.infos[action.payload.id] = action.payload; - }); - builder.addCase(updateGroupInfoInWrapper.rejected, () => { - window.log.error('a updateGroupInfoInWrapper was rejected'); - }); - builder.addCase(initNewGroupInfoInWrapper.rejected, () => { - window.log.error('a initNewGroupInfoInWrapper was rejected'); - }); - }, -}); - -export const groupInfoActions = { - initNewGroupInfoInWrapper, - updateGroupInfoInWrapper, - ...groupInfosSlice.actions, -}; -export const groupInfosReducer = groupInfosSlice.reducer; diff --git a/ts/state/ducks/groups.ts b/ts/state/ducks/groups.ts new file mode 100644 index 000000000..1ea7919ac --- /dev/null +++ b/ts/state/ducks/groups.ts @@ -0,0 +1,217 @@ +import { PayloadAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit'; +import { GroupInfoGet, GroupMemberGet, GroupPubkeyType } from 'libsession_util_nodejs'; +import { isEmpty } from 'lodash'; +import { ConfigDumpData } from '../../data/configDump/configDump'; +import { ConversationTypeEnum } from '../../models/conversationAttributes'; +import { HexString } from '../../node/hexStrings'; +import { getConversationController } from '../../session/conversations'; +import { UserUtils } from '../../session/utils'; +import { GroupSync } from '../../session/utils/job_runners/jobs/GroupConfigJob'; +import { toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes'; +import { + getGroupPubkeyFromWrapperType, + isMetaWrapperType, +} from '../../webworker/workers/browser/libsession_worker_functions'; +import { + MetaGroupWrapperActions, + UserGroupsWrapperActions, +} from '../../webworker/workers/browser/libsession_worker_interface'; + +export type GroupState = { + infos: Record; + members: Record>; +}; + +export const initialGroupState: GroupState = { + infos: {}, + members: {}, +}; + +type GroupDetailsUpdate = { + groupPk: GroupPubkeyType; + infos: GroupInfoGet; + members: Array; +}; + +const initNewGroupInWrapper = createAsyncThunk( + 'group/initNewGroupInWrapper', + async (groupDetails: { + groupName: string; + members: Array; + }): Promise => { + try { + const newGroup = await UserGroupsWrapperActions.createGroup(); + + await UserGroupsWrapperActions.setGroup(newGroup); + + const ourEd25519KeypairBytes = await UserUtils.getUserED25519KeyPairBytes(); + if (!ourEd25519KeypairBytes) { + throw new Error('Current user has no priv ed25519 key?'); + } + const userEd25519Secretkey = ourEd25519KeypairBytes.privKeyBytes; + const groupEd2519Pk = HexString.fromHexString(newGroup.pubkeyHex).slice(1); // remove the 03 prefix (single byte once in hex form) + + // dump is always empty when creating a new groupInfo + await MetaGroupWrapperActions.init(newGroup.pubkeyHex, { + metaDumped: null, + userEd25519Secretkey: toFixedUint8ArrayOfLength(userEd25519Secretkey, 64), + groupEd25519Secretkey: newGroup.secretKey, + groupEd25519Pubkey: toFixedUint8ArrayOfLength(groupEd2519Pk, 32), + }); + + await Promise.all( + groupDetails.members.map(async member => { + const created = await MetaGroupWrapperActions.memberGetOrConstruct( + newGroup.pubkeyHex, + member + ); + await MetaGroupWrapperActions.memberSetInvited( + newGroup.pubkeyHex, + created.pubkeyHex, + false + ); + }) + ); + const infos = await MetaGroupWrapperActions.infoGet(newGroup.pubkeyHex); + if (!infos) { + throw new Error( + `getInfos of ${newGroup.pubkeyHex} returned empty result even if it was just init.` + ); + } + infos.name = groupDetails.groupName; + await MetaGroupWrapperActions.infoSet(newGroup.pubkeyHex, infos); + + const members = await MetaGroupWrapperActions.memberGetAll(newGroup.pubkeyHex); + if (!members || isEmpty(members)) { + throw new Error( + `memberGetAll of ${newGroup.pubkeyHex} returned empty result even if it was just init.` + ); + } + + const convo = await getConversationController().getOrCreateAndWait( + newGroup.pubkeyHex, + ConversationTypeEnum.GROUPV3 + ); + + await convo.setIsApproved(true, false); + + // console.warn('store the v3 identityPrivatekeypair as part of the wrapper only?'); + // // the sync below will need the secretKey of the group to be saved in the wrapper. So save it! + await UserGroupsWrapperActions.setGroup(newGroup); + + await GroupSync.queueNewJobIfNeeded(newGroup.pubkeyHex); + + // const us = UserUtils.getOurPubKeyStrFromCache(); + // // Ensure the current user is a member and admin + // const members = uniq([...groupDetails.members, us]); + + // const updateGroupDetails: ClosedGroup.GroupInfo = { + // id: newGroup.pubkeyHex, + // name: groupDetails.groupName, + // members, + // admins: [us], + // activeAt: Date.now(), + // expireTimer: 0, + // }; + + // // be sure to call this before sending the message. + // // the sending pipeline needs to know from GroupUtils when a message is for a medium group + // await ClosedGroup.updateOrCreateClosedGroup(updateGroupDetails); + await convo.unhideIfNeeded(); + convo.set({ active_at: Date.now() }); + await convo.commit(); + convo.updateLastMessage(); + + return { groupPk: newGroup.pubkeyHex, infos, members }; + } catch (e) { + throw e; + } + } +); + +const loadDumpsFromDB = createAsyncThunk( + 'group/loadDumpsFromDB', + async (): Promise> => { + const variantsWithoutData = await ConfigDumpData.getAllDumpsWithoutData(); + const allUserGroups = await UserGroupsWrapperActions.getAllGroups(); + const toReturn: Array = []; + for (let index = 0; index < variantsWithoutData.length; index++) { + const { variant } = variantsWithoutData[index]; + if (!isMetaWrapperType(variant)) { + continue; + } + const groupPk = getGroupPubkeyFromWrapperType(variant); + const foundInUserWrapper = allUserGroups.find(m => m.pubkeyHex === groupPk); + if (!foundInUserWrapper) { + continue; + } + + try { + window.log.debug( + 'loadDumpsFromDB loading from metagroup variant: ', + variant, + foundInUserWrapper.pubkeyHex + ); + + const infos = await MetaGroupWrapperActions.infoGet(groupPk); + const members = await MetaGroupWrapperActions.memberGetAll(groupPk); + + toReturn.push({ groupPk, infos, members }); + + // Annoyingly, the redux store is not initialized when this current funciton is called, + // so we need to init the group wrappers here, but load them in their redux slice later + } catch (e) { + // TODO should not throw in this case? we should probably just try to load what we manage to load + window.log.warn( + `initGroup of Group wrapper of variant ${variant} failed with ${e.message} ` + ); + // throw new Error(`initializeLibSessionUtilWrappers failed with ${e.message}`); + } + } + + return toReturn; + } +); + +/** + * 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', + initialState: initialGroupState, + reducers: { + updateGroupDetailsAfterMerge(state, action: PayloadAction) { + const { groupPk, infos, members } = action.payload; + state.infos[groupPk] = infos; + state.members[groupPk] = members; + }, + }, + extraReducers: builder => { + builder.addCase(initNewGroupInWrapper.fulfilled, (state, action) => { + const { groupPk, infos, members } = action.payload; + state.infos[groupPk] = infos; + state.members[groupPk] = members; + }); + builder.addCase(initNewGroupInWrapper.rejected, () => { + window.log.error('a initNewGroupInWrapper was rejected'); + // FIXME delete the wrapper completely & correspondign dumps, and usergroups entry? + }); + builder.addCase(loadDumpsFromDB.fulfilled, (state, action) => { + const loaded = action.payload; + loaded.forEach(element => { + state.infos[element.groupPk] = element.infos; + state.members[element.groupPk] = element.members; + }); + }); + builder.addCase(loadDumpsFromDB.rejected, () => { + window.log.error('a loadDumpsFromDB was rejected'); + }); + }, +}); + +export const groupInfoActions = { + initNewGroupInWrapper, + loadDumpsFromDB, + ...groupSlice.actions, +}; +export const groupReducer = groupSlice.reducer; diff --git a/ts/state/ducks/modalDialog.tsx b/ts/state/ducks/modalDialog.tsx index be1cb8b92..257cd9cfd 100644 --- a/ts/state/ducks/modalDialog.tsx +++ b/ts/state/ducks/modalDialog.tsx @@ -6,16 +6,19 @@ import { Noop } from '../../types/Util'; export type BanType = 'ban' | 'unban'; export type ConfirmModalState = SessionConfirmDialogProps | null; -export type InviteContactModalState = { conversationId: string } | null; -export type BanOrUnbanUserModalState = { - conversationId: string; - banType: BanType; - pubkey?: string; -} | null; + +type WithConvoId = { conversationId: string }; +export type InviteContactModalState = WithConvoId | null; +export type BanOrUnbanUserModalState = + | (WithConvoId & { + banType: BanType; + pubkey?: string; + }) + | null; export type AddModeratorsModalState = InviteContactModalState; export type RemoveModeratorsModalState = InviteContactModalState; export type UpdateGroupMembersModalState = InviteContactModalState; -export type UpdateGroupNameModalState = InviteContactModalState; +export type UpdateGroupNameModalState = WithConvoId | null; export type ChangeNickNameModalState = InviteContactModalState; export type AdminLeaveClosedGroupModalState = InviteContactModalState; export type EditProfileModalState = object | null; diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts index 0ddaf641a..3ab29c048 100644 --- a/ts/state/reducer.ts +++ b/ts/state/reducer.ts @@ -20,7 +20,7 @@ import { } from './ducks/stagedAttachments'; import { PrimaryColorStateType, ThemeStateType } from '../themes/constants/colors'; import { settingsReducer, SettingsState } from './ducks/settings'; -import { groupInfosReducer, GroupInfosState } from './ducks/groupInfos'; +import { groupReducer, GroupState } from './ducks/groups'; export type StateType = { search: SearchStateType; @@ -38,7 +38,7 @@ export type StateType = { call: CallStateType; sogsRoomInfo: SogsRoomInfoState; settings: SettingsState; - groupInfos: GroupInfosState; + groups: GroupState; }; export const reducers = { @@ -57,7 +57,7 @@ export const reducers = { call, sogsRoomInfo: ReduxSogsRoomInfos.sogsRoomInfoReducer, settings: settingsReducer, - groupInfos: groupInfosReducer, + groups: groupReducer, }; // Making this work would require that our reducer signature supported AnyAction, not diff --git a/ts/state/selectors/groups.ts b/ts/state/selectors/groups.ts new file mode 100644 index 000000000..f8dec3e65 --- /dev/null +++ b/ts/state/selectors/groups.ts @@ -0,0 +1,83 @@ +import { GroupPubkeyType } from 'libsession_util_nodejs'; +import { isEmpty } from 'lodash'; +import { PubKey } from '../../session/types'; +import { GroupState } from '../ducks/groups'; +import { StateType } from '../reducer'; +import { useSelector } from 'react-redux'; + +const getLibGroupsState = (state: StateType): GroupState => state.groups; + +export function getLibMembersPubkeys(state: StateType, convo?: string): Array { + if (!convo) { + return []; + } + if (!PubKey.isClosedGroupV2(convo)) { + return []; + } + + const members = getLibGroupsState(state).members[convo]; + if (isEmpty(members)) { + return []; + } + + return members.map(m => m.pubkeyHex); +} + +export function getLibAdminsPubkeys(state: StateType, convo?: string): Array { + if (!convo) { + return []; + } + if (!PubKey.isClosedGroupV2(convo)) { + return []; + } + + const members = getLibGroupsState(state).members[convo]; + if (isEmpty(members)) { + return []; + } + + return members.filter(m => m.promoted).map(m => m.pubkeyHex); +} + +export function getLibMembersCount(state: StateType, convo?: GroupPubkeyType): Array { + return getLibMembersPubkeys(state, convo); +} + +function getLibGroupName(state: StateType, convo?: string): string | undefined { + if (!convo) { + return undefined; + } + if (!PubKey.isClosedGroupV2(convo)) { + return undefined; + } + + const name = getLibGroupsState(state).infos[convo]?.name; + return name || undefined; +} + +export function useLibGroupName(convoId?: string): string | undefined { + return useSelector((state: StateType) => getLibGroupName(state, convoId)); +} + +export function useLibGroupMembers(convoId?: string): Array { + return useSelector((state: StateType) => getLibMembersPubkeys(state, convoId)); +} + +export function useLibGroupAdmins(convoId?: string): Array { + return useSelector((state: StateType) => getLibAdminsPubkeys(state, convoId)); +} + +export function getLibGroupNameOutsideRedux(convoId: string): string | undefined { + const state = window.inboxStore?.getState(); + return state ? getLibGroupName(state, convoId) : undefined; +} + +export function getLibGroupMembersOutsideRedux(convoId: string): Array { + const state = window.inboxStore?.getState(); + return state ? getLibMembersPubkeys(state, convoId) : []; +} + +export function getLibGroupAdminsOutsideRedux(convoId: string): Array { + const state = window.inboxStore?.getState(); + return state ? getLibAdminsPubkeys(state, convoId) : []; +} diff --git a/ts/state/selectors/selectedConversation.ts b/ts/state/selectors/selectedConversation.ts index 6c23c6bab..09762da3f 100644 --- a/ts/state/selectors/selectedConversation.ts +++ b/ts/state/selectors/selectedConversation.ts @@ -6,6 +6,7 @@ import { UserUtils } from '../../session/utils'; import { StateType } from '../reducer'; import { getCanWrite, getModerators, getSubscriberCount } from './sogsRoomInfo'; import { getIsMessageSelectionMode, getSelectedConversation } from './conversations'; +import { getLibMembersPubkeys, useLibGroupName } from './groups'; /** * Returns the formatted text for notification setting. @@ -138,12 +139,27 @@ export const isClosedGroupConversation = (state: StateType): boolean => { ); }; -const getSelectedGroupMembers = (state: StateType): Array => { +const getSelectedMembersCount = (state: StateType): number => { + const selected = getSelectedConversation(state); + if (!selected) { + return 0; + } + if (PubKey.isClosedGroupV2(selected.id)) { + return getLibMembersPubkeys(state, selected.id).length || 0; + } + if (selected.isPrivate || selected.isPublic) { + return 0; + } + return selected.members?.length || 0; +}; + +const getSelectedGroupAdmins = (state: StateType): Array => { const selected = getSelectedConversation(state); if (!selected) { return []; } - return selected.members || []; + + return selected.groupAdmins || []; }; const getSelectedSubscriberCount = (state: StateType): number | undefined => { @@ -221,8 +237,12 @@ export function useSelectedisNoteToSelf() { return useSelector(getIsSelectedNoteToSelf); } -export function useSelectedMembers() { - return useSelector(getSelectedGroupMembers); +export function useSelectedMembersCount() { + return useSelector(getSelectedMembersCount); +} + +export function useSelectedGroupAdmins() { + return useSelector(getSelectedGroupAdmins); } export function useSelectedSubscriberCount() { @@ -273,13 +293,18 @@ export function useSelectedShortenedPubkeyOrFallback() { * This also returns the localized "Note to Self" if the conversation is the note to self. */ export function useSelectedNicknameOrProfileNameOrShortenedPubkey() { + const selectedId = useSelectedConversationKey(); const nickname = useSelectedNickname(); const profileName = useSelectedDisplayNameInProfile(); const shortenedPubkey = useSelectedShortenedPubkeyOrFallback(); const isMe = useSelectedisNoteToSelf(); + const libGroupName = useLibGroupName(selectedId); if (isMe) { return window.i18n('noteToSelf'); } + if (selectedId && PubKey.isClosedGroupV2(selectedId)) { + return libGroupName; + } return nickname || profileName || shortenedPubkey; } diff --git a/ts/test/session/unit/sending/MessageQueue_test.ts b/ts/test/session/unit/sending/MessageQueue_test.ts index 5588e46f5..26e0eaf8d 100644 --- a/ts/test/session/unit/sending/MessageQueue_test.ts +++ b/ts/test/session/unit/sending/MessageQueue_test.ts @@ -9,22 +9,22 @@ import { randomBytes } from 'crypto'; import chai from 'chai'; -import Sinon, * as sinon from 'sinon'; -import { describe } from 'mocha'; import chaiAsPromised from 'chai-as-promised'; +import { describe } from 'mocha'; +import Sinon, * as sinon from 'sinon'; -import { GroupUtils, PromiseUtils, UserUtils } from '../../../../session/utils'; -import { TestUtils } from '../../../test-utils'; -import { MessageQueue } from '../../../../session/sending/MessageQueue'; import { ContentMessage } from '../../../../session/messages/outgoing'; -import { PubKey, RawMessage } from '../../../../session/types'; +import { ClosedGroupMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupMessage'; import { MessageSender } from '../../../../session/sending'; +import { MessageQueue } from '../../../../session/sending/MessageQueue'; +import { PubKey, RawMessage } from '../../../../session/types'; +import { PromiseUtils, UserUtils } from '../../../../session/utils'; +import { TestUtils } from '../../../test-utils'; import { PendingMessageCacheStub } from '../../../test-utils/stubs'; -import { ClosedGroupMessage } from '../../../../session/messages/outgoing/controlMessage/group/ClosedGroupMessage'; +import { SnodeNamespaces } from '../../../../session/apis/snode_api/namespaces'; import { MessageSentHandler } from '../../../../session/sending/MessageSentHandler'; import { stubData } from '../../../test-utils/utils'; -import { SnodeNamespaces } from '../../../../session/apis/snode_api/namespaces'; chai.use(chaiAsPromised as any); chai.should(); @@ -209,9 +209,6 @@ describe('MessageQueue', () => { describe('closed groups', () => { it('can send to closed group', async () => { - const members = TestUtils.generateFakePubKeys(4).map(p => new PubKey(p.key)); - Sinon.stub(GroupUtils, 'getGroupMembers').returns(members); - const send = Sinon.stub(messageQueueStub, 'sendToPubKey').resolves(); const message = TestUtils.generateClosedGroupMessage(); diff --git a/ts/types/sqlSharedTypes.ts b/ts/types/sqlSharedTypes.ts index 7a5d2d7ab..05a31e154 100644 --- a/ts/types/sqlSharedTypes.ts +++ b/ts/types/sqlSharedTypes.ts @@ -4,6 +4,7 @@ import { ContactInfoSet, FixedSizeUint8Array, + GroupPubkeyType, LegacyGroupInfo, LegacyGroupMemberInfo, } from 'libsession_util_nodejs'; @@ -66,6 +67,7 @@ export type ConfigDumpDataNode = { getAllDumpsWithData: () => Array; getAllDumpsWithoutData: () => Array; getAllDumpsWithoutDataFor: (pk: string) => Array; + deleteDumpFor: (pk: GroupPubkeyType) => void; }; // ========== unprocessed diff --git a/ts/webworker/workers/browser/libsession_worker_interface.ts b/ts/webworker/workers/browser/libsession_worker_interface.ts index 9733aaa8b..e92413ece 100644 --- a/ts/webworker/workers/browser/libsession_worker_interface.ts +++ b/ts/webworker/workers/browser/libsession_worker_interface.ts @@ -389,6 +389,10 @@ export const MetaGroupWrapperActions: MetaGroupWrapperActionsCalls = { callLibSessionWorker([`MetaGroupConfig-${groupPk}`, 'metaDump']) as Promise< ReturnType >, + metaDebugDump: async (groupPk: GroupPubkeyType) => + callLibSessionWorker([`MetaGroupConfig-${groupPk}`, 'metaDebugDump']) as Promise< + ReturnType + >, metaConfirmPushed: async ( groupPk: GroupPubkeyType, args: Parameters[1]