feat: preload name of group from usergroup wrapper

until we get the groupinfo name from polling
pull/2873/head
Audric Ackermann 2 years ago
parent cd17a08c2e
commit b53264593b

@ -34,7 +34,7 @@ window.sessionFeatureFlags = {
integrationTestEnv: Boolean(
process.env.NODE_APP_INSTANCE && process.env.NODE_APP_INSTANCE.includes('test-integration')
),
useClosedGroupV3: true,
useClosedGroupV2: true,
debug: {
debugLogging: !_.isEmpty(process.env.SESSION_DEBUG),
debugLibsessionDumps: !_.isEmpty(process.env.SESSION_DEBUG_LIBSESSION_DUMPS),

@ -99,7 +99,7 @@ function setupLeftPane(forceUpdateInboxComponent: () => void) {
window.openConversationWithMessages = openConversationWithMessages;
window.inboxStore = createSessionInboxStore();
window.inboxStore.dispatch(updateAllOnStorageReady());
window.inboxStore.dispatch(groupInfoActions.loadDumpsFromDB());
window.inboxStore.dispatch(groupInfoActions.loadMetaDumpsFromDB()); // this loads the dumps from DB and fills the 03-groups slice with the corresponding details
forceUpdateInboxComponent();
}

@ -13,7 +13,7 @@ import { assertUnreachable } from '../../types/sqlSharedTypes';
import { SessionSearchInput } from '../SessionSearchInput';
import { StyledLeftPaneList } from './LeftPaneList';
import { ConversationListItem } from './conversation-list-item/ConversationListItem';
import { OverlayClosedGroup } from './overlay/OverlayClosedGroup';
import { OverlayLegacyClosedGroup, OverlayClosedGroupV2 } from './overlay/OverlayClosedGroup';
import { OverlayCommunity } from './overlay/OverlayCommunity';
import { OverlayMessage } from './overlay/OverlayMessage';
import { OverlayMessageRequest } from './overlay/OverlayMessageRequest';
@ -50,7 +50,11 @@ const ClosableOverlay = () => {
case 'open-group':
return <OverlayCommunity />;
case 'closed-group':
return <OverlayClosedGroup />;
return window.sessionFeatureFlags.useClosedGroupV2 ? (
<OverlayClosedGroupV2 />
) : (
<OverlayLegacyClosedGroup />
);
case 'message':
return <OverlayMessage />;
case 'message-requests':

@ -1,5 +1,6 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { isEmpty } from 'lodash';
import {
useConversationRealName,
useConversationUsername,
@ -18,8 +19,8 @@ export const UserItem = () => {
const isSearchResultsMode = useSelector(isSearching);
const shortenedPubkey = PubKey.shorten(conversationId);
const isMe = useIsMe(conversationId);
const username = useConversationUsername(conversationId);
const isMe = useIsMe(conversationId);
const realName = useConversationRealName(conversationId);
const hasNickname = useHasNickname(conversationId);
@ -31,7 +32,7 @@ export const UserItem = () => {
: username;
let shouldShowPubkey = false;
if ((!username || username.length === 0) && (!displayName || displayName.length === 0)) {
if (isEmpty(username) && isEmpty(displayName)) {
shouldShowPubkey = true;
}

@ -4,21 +4,25 @@ import { useDispatch, useSelector } from 'react-redux';
import useKey from 'react-use/lib/useKey';
import styled from 'styled-components';
import { concat } from 'lodash';
import { MemberListItem } from '../../MemberListItem';
import { SessionButton } from '../../basic/SessionButton';
import { SessionIdEditable } from '../../basic/SessionIdEditable';
import { SessionSpinner } from '../../basic/SessionSpinner';
import { MemberListItem } from '../../MemberListItem';
import { OverlayHeader } from './OverlayHeader';
import { resetOverlayMode } from '../../../state/ducks/section';
import { getPrivateContactsPubkeys } from '../../../state/selectors/conversations';
import { SpacerLG } from '../../basic/Text';
import { SessionSearchInput } from '../../SessionSearchInput';
import { getSearchResultsContactOnly, isSearching } from '../../../state/selectors/search';
import { useSet } from '../../../hooks/useSet';
import { VALIDATION } from '../../../session/constants';
import { ToastUtils } from '../../../session/utils';
import { createClosedGroup } from '../../../session/conversations/createClosedGroup';
import { ToastUtils } from '../../../session/utils';
import { groupInfoActions } from '../../../state/ducks/groups';
import { resetOverlayMode } from '../../../state/ducks/section';
import { getPrivateContactsPubkeys } from '../../../state/selectors/conversations';
import { useIsCreatingGroupFromUIPending } from '../../../state/selectors/groups';
import { getSearchResultsContactOnly, isSearching } from '../../../state/selectors/search';
import { useOurPkStr } from '../../../state/selectors/user';
import { SessionSearchInput } from '../../SessionSearchInput';
import { SpacerLG } from '../../basic/Text';
const StyledMemberListNoContacts = styled.div`
font-family: var(--font-mono), var(--font-default);
@ -82,12 +86,128 @@ async function createClosedGroupWithToasts(
return false;
}
await createClosedGroup(groupName, groupMemberIds, window.sessionFeatureFlags.useClosedGroupV3);
await createClosedGroup(groupName, groupMemberIds);
return true;
}
export const OverlayClosedGroup = () => {
// duplicated form the legacy one below because this one is a lot more tightly linked with redux async thunks logic
export const OverlayClosedGroupV2 = () => {
const dispatch = useDispatch();
const us = useOurPkStr();
const privateContactsPubkeys = useSelector(getPrivateContactsPubkeys);
const isCreatingGroup = useIsCreatingGroupFromUIPending();
const [groupName, setGroupName] = useState('');
const {
uniqueValues: members,
addTo: addToSelected,
removeFrom: removeFromSelected,
} = useSet<string>([]);
const isSearch = useSelector(isSearching);
const searchResultContactsOnly = useSelector(getSearchResultsContactOnly);
function closeOverlay() {
dispatch(resetOverlayMode());
}
async function onEnterPressed() {
if (isCreatingGroup) {
window?.log?.warn('Closed group creation already in progress');
return;
}
// Validate groupName and groupMembers length
if (groupName.length === 0) {
ToastUtils.pushToastError('invalidGroupName', window.i18n('invalidGroupNameTooShort'));
return;
}
if (groupName.length > VALIDATION.MAX_GROUP_NAME_LENGTH) {
ToastUtils.pushToastError('invalidGroupName', window.i18n('invalidGroupNameTooLong'));
return;
}
// >= because we add ourself as a member AFTER this. so a 10 group is already invalid as it will be 11 with ourself
// the same is valid with groups count < 1
if (members.length < 1) {
ToastUtils.pushToastError('pickClosedGroupMember', window.i18n('pickClosedGroupMember'));
return;
}
if (members.length >= VALIDATION.CLOSED_GROUP_SIZE_LIMIT) {
ToastUtils.pushToastError('closedGroupMaxSize', window.i18n('closedGroupMaxSize'));
return;
}
// trigger the add through redux.
dispatch(
groupInfoActions.initNewGroupInWrapper({
members: concat(members, [us]),
groupName,
us,
}) as any
);
}
useKey('Escape', closeOverlay);
const title = window.i18n('createGroup');
const buttonText = window.i18n('create');
const subtitle = window.i18n('createClosedGroupNamePrompt');
const placeholder = window.i18n('createClosedGroupPlaceholder');
const noContactsForClosedGroup = privateContactsPubkeys.length === 0;
const contactsToRender = isSearch ? searchResultContactsOnly : privateContactsPubkeys;
const disableCreateButton = !members.length && !groupName.length;
return (
<div className="module-left-pane-overlay">
<OverlayHeader title={title} subtitle={subtitle} />
<div className="create-group-name-input">
<SessionIdEditable
editable={!noContactsForClosedGroup}
placeholder={placeholder}
value={groupName}
isGroup={true}
maxLength={VALIDATION.MAX_GROUP_NAME_LENGTH}
onChange={setGroupName}
onPressEnter={onEnterPressed}
dataTestId="new-closed-group-name"
/>
</div>
<SessionSpinner loading={isCreatingGroup} />
<SpacerLG />
<SessionSearchInput />
<StyledGroupMemberListContainer>
{noContactsForClosedGroup ? (
<NoContacts />
) : (
<StyledGroupMemberList className="group-member-list__selection">
{contactsToRender.map((memberPubkey: string) => (
<MemberListItem
pubkey={memberPubkey}
isSelected={members.some(m => m === memberPubkey)}
key={memberPubkey}
onSelect={addToSelected}
onUnselect={removeFromSelected}
disableBg={true}
/>
))}
</StyledGroupMemberList>
)}
</StyledGroupMemberListContainer>
<SpacerLG style={{ flexShrink: 0 }} />
<SessionButton
text={buttonText}
disabled={disableCreateButton}
onClick={onEnterPressed}
dataTestId="next-button"
margin="auto 0 var(--margins-lg) 0 " // just to keep that button at the bottom of the overlay (even with an empty list)
/>
</div>
);
};
export const OverlayLegacyClosedGroup = () => {
const dispatch = useDispatch();
const privateContactsPubkeys = useSelector(getPrivateContactsPubkeys);
const [groupName, setGroupName] = useState('');
@ -111,6 +231,7 @@ export const OverlayClosedGroup = () => {
}
setLoading(true);
const groupCreated = await createClosedGroupWithToasts(groupName, selectedMemberIds);
if (groupCreated) {
closeOverlay();
return;

@ -31,7 +31,10 @@ export function useConversationUsername(convoId?: string) {
const convoProps = useConversationPropsById(convoId);
const groupName = useLibGroupName(convoId);
if (convoId && PubKey.isClosedGroupV2(convoId)) {
if (convoId && PubKey.isClosedGroupV2(convoId) && groupName) {
// when getting a new 03 group from the usergroup wrapper,
// we set the displayNameInProfile with the name from the wrapper.
// So let's keep falling back to convoProps?.displayNameInProfile if groupName is not set yet (it comes later through the groupInfos namespace)
return groupName;
}
return convoProps?.nickname || convoProps?.displayNameInProfile || convoId;

@ -1728,7 +1728,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
public isKickedFromGroup(): boolean {
if (this.isClosedGroup()) {
if (this.isClosedGroupV3()) {
console.info('isKickedFromGroup using lib todo'); // debugger
// console.info('isKickedFromGroup using lib todo'); // debugger
}
return !!this.get('isKickedFromGroup');
}
@ -1738,7 +1738,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
public isLeft(): boolean {
if (this.isClosedGroup()) {
if (this.isClosedGroupV3()) {
console.info('isLeft using lib todo'); // debugger
// console.info('isLeft using lib todo'); // debugger
}
return !!this.get('left');
}
@ -2340,9 +2340,6 @@ export async function commitConversationAndRefreshWrapper(id: string) {
return;
}
// TODOLATER remove duplicates between db and wrapper (and move search by name or nickname to wrapper)
// TODOLATER insertConvoFromDBIntoWrapperAndRefresh and insertContactFromDBIntoWrapperAndRefresh both fetches the same data from the DB. Might be worth fetching it and providing it to both?
// write to db
const savedDetails = await Data.saveConversation(convo.attributes);
await convo.refreshInMemoryDetails(savedDetails);

@ -1,5 +1,5 @@
/* eslint-disable no-await-in-loop */
import { ContactInfo } from 'libsession_util_nodejs';
import { ContactInfo, UserGroupsGet } from 'libsession_util_nodejs';
import { compact, difference, isEmpty, isNil, isNumber, toNumber } from 'lodash';
import { ConfigDumpData } from '../data/configDump/configDump';
import { SettingsKey } from '../data/settings-key';
@ -47,6 +47,7 @@ import { addKeyPairToCacheAndDBIfNeeded } from './closedGroups';
import { HexKeyPair } from './keypairs';
import { queueAllCachedFromSource } from './receiver';
import { HexString } from '../node/hexStrings';
import { groupInfoActions } from '../state/ducks/groups';
type IncomingUserResult = {
needsPush: boolean;
@ -125,7 +126,7 @@ async function mergeUserConfigsWithIncomingUpdates(
const latestEnvelopeTimestamp = Math.max(...sameVariant.map(m => m.envelopeTimestamp));
window.log.debug(
`${variant}: "${publicKey}" needsPush:${needsPush} needsDump:${needsDump}; mergedCount:${mergedCount} `
`${variant}: needsPush:${needsPush} needsDump:${needsDump}; mergedCount:${mergedCount} `
);
if (window.sessionFeatureFlags.debug.debugLibsessionDumps) {
@ -609,12 +610,77 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) {
}
}
async function handleSingleGroupUpdate({
groupInWrapper,
userEdKeypair,
}: {
groupInWrapper: UserGroupsGet;
latestEnvelopeTimestamp: number;
userEdKeypair: UserUtils.ByteKeyPair;
}) {
const groupPk = groupInWrapper.pubkeyHex;
try {
// dump is always empty when creating a new groupInfo
await MetaGroupWrapperActions.init(groupPk, {
metaDumped: null,
userEd25519Secretkey: toFixedUint8ArrayOfLength(userEdKeypair.privKeyBytes, 64),
groupEd25519Secretkey: groupInWrapper.secretKey,
groupEd25519Pubkey: toFixedUint8ArrayOfLength(HexString.fromHexString(groupPk.slice(2)), 32),
});
} catch (e) {
window.log.warn(
`handleSingleGroupUpdate metawrapper init of "${groupPk}" failed with`,
e.message
);
}
if (!getConversationController().get(groupPk)) {
const created = await getConversationController().getOrCreateAndWait(
groupPk,
ConversationTypeEnum.GROUPV3
);
const joinedAt = groupInWrapper.joinedAtSeconds * 1000 || Date.now();
created.set({
active_at: joinedAt,
displayNameInProfile: groupInWrapper.name || undefined,
priority: groupInWrapper.priority,
lastJoinedTimestamp: joinedAt,
});
await created.commit();
getSwarmPollingInstance().addGroupId(PubKey.cast(groupPk));
}
}
async function handleSingleGroupUpdateToLeave(toLeave: string) {
// that group is not in the wrapper but in our local DB. it must be removed and cleaned
try {
window.log.debug(
`About to deleteGroup ${toLeave} via handleSingleGroupUpdateToLeave as in DB but not in wrapper`
);
await getConversationController().deleteClosedGroup(toLeave, {
fromSyncMessage: true,
sendLeaveMessage: false,
});
} catch (e) {
window.log.info('Failed to deleteClosedGroup with: ', e.message);
}
}
/**
* Called when we just got a userGroups merge from the network. We need to apply the changes to our local state. (i.e. DB and redux slice of 03 groups)
*/
async function handleGroupUpdate(latestEnvelopeTimestamp: number) {
// first let's check which groups needs to be joined or left by doing a diff of what is in the wrapper and what is in the DB
const allGoupsInWrapper = await UserGroupsWrapperActions.getAllGroups();
const allGoupsInDb = getConversationController()
.getConversations()
.filter(m => PubKey.isClosedGroupV2(m.id));
const allGoupsIdsInWrapper = allGoupsInWrapper.map(m => m.pubkeyHex);
const allGoupsIdsInDb = allGoupsInDb.map(m => m.id as string);
console.warn('allGoupsIdsInWrapper', stringify(allGoupsIdsInWrapper));
console.warn('allGoupsIdsInDb', stringify(allGoupsIdsInDb));
const userEdKeypair = await UserUtils.getUserED25519KeyPairBytes();
if (!userEdKeypair) {
@ -623,30 +689,19 @@ async function handleGroupUpdate(latestEnvelopeTimestamp: number) {
for (let index = 0; index < allGoupsInWrapper.length; index++) {
const groupInWrapper = allGoupsInWrapper[index];
const groupPk = groupInWrapper.pubkeyHex;
if (!getConversationController().get(groupPk)) {
try {
// dump is always empty when creating a new groupInfo
await MetaGroupWrapperActions.init(groupPk, {
metaDumped: null,
userEd25519Secretkey: toFixedUint8ArrayOfLength(userEdKeypair.privKeyBytes, 64),
groupEd25519Secretkey: groupInWrapper.secretKey,
groupEd25519Pubkey: toFixedUint8ArrayOfLength(
HexString.fromHexString(groupPk.slice(2)),
32
),
});
} catch (e) {
window.log.warn(`MetaGroupWrapperActions.init of "${groupPk}" failed with`, e.message);
}
const created = await getConversationController().getOrCreateAndWait(
groupPk,
ConversationTypeEnum.GROUPV3
);
created.set({ active_at: latestEnvelopeTimestamp });
await created.commit();
getSwarmPollingInstance().addGroupId(PubKey.cast(groupPk));
}
window.inboxStore.dispatch(groupInfoActions.handleUserGroupUpdate(groupInWrapper));
await handleSingleGroupUpdate({ groupInWrapper, latestEnvelopeTimestamp, userEdKeypair });
}
const groupsInDbButNotInWrapper = difference(allGoupsIdsInDb, allGoupsIdsInWrapper);
window.log.info(
`we have to leave ${groupsInDbButNotInWrapper.length} 03 groups in DB compared to what is in the wrapper`
);
for (let index = 0; index < groupsInDbButNotInWrapper.length; index++) {
const toRemove = groupsInDbButNotInWrapper[index];
await handleSingleGroupUpdateToLeave(toRemove);
}
}

@ -27,11 +27,7 @@ export async function doSnodeBatchRequest(
associatedWith: string | null,
method: 'batch' | 'sequence' = 'batch'
): Promise<NotEmptyArrayOfBatchResults> {
console.warn(
`doSnodeBatchRequest "${method}":`,
JSON.stringify(logSubRequests(subRequests))
// subRequests
);
window.log.debug(`doSnodeBatchRequest "${method}":`, JSON.stringify(logSubRequests(subRequests)));
const result = await snodeRpc({
method,
params: { requests: subRequests },

@ -320,7 +320,7 @@ export class SwarmPolling {
await this.notPollingForGroupAsNotInWrapper(pubkey, 'not in wrapper after poll');
return;
}
if (PubKey.isClosedGroupV2(pubkey) && allGroupsInWrapper.some(m => m.pubkeyHex === pubkey)) {
if (PubKey.isClosedGroupV2(pubkey) && !allGroupsInWrapper.some(m => m.pubkeyHex === pubkey)) {
// not tracked anymore in the wrapper. Discard messages and stop polling
await this.notPollingForGroupAsNotInWrapper(pubkey, 'not in wrapper after poll');
return;
@ -456,7 +456,13 @@ export class SwarmPolling {
}
private async notPollingForGroupAsNotInWrapper(pubkey: string, reason: string) {
this.removePubkey(pubkey, reason);
window.log.debug(
`notPollingForGroupAsNotInWrapper ${ed25519Str(pubkey)} with reason:"${reason}"`
);
await getConversationController().deleteClosedGroup(pubkey, {
fromSyncMessage: true,
sendLeaveMessage: false,
});
return Promise.resolve();
}

@ -1,12 +1,11 @@
import { GroupPubkeyType } from 'libsession_util_nodejs';
import { stringify } from '../../../../types/sqlSharedTypes';
import { groupInfoActions } from '../../../../state/ducks/groups';
import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface';
import { ed25519Str } from '../../../onions/onionPath';
import { fromBase64ToArray } from '../../../utils/String';
import { LibSessionUtil } from '../../../utils/libsession/libsession_utils';
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<RetrieveMessageItemWithNamespace>,
@ -43,41 +42,17 @@ async function handleGroupSharedConfigMessages(
groupMember: members,
};
// do the merge with our current state
await MetaGroupWrapperActions.metaMerge(groupPk, toMerge);
// save updated dumps to the DB right away
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');
}
// refresh the redux slice with the merged result
window.inboxStore.dispatch(
groupInfoActions.updateGroupDetailsAfterMerge({
groupInfoActions.refreshGroupDetailsFromWrapper({
groupPk,
infos: updatedInfos,
members: updatedMembers,
})
);
// if (allDecryptedConfigMessages.length) {
// try {
// window.log.info(
// `handleGroupSharedConfigMessages of "${allDecryptedConfigMessages.length}" messages with libsession`
// );
// console.warn('HANDLING OF INCOMING GROUP TODO ');
// // await ConfigMessageHandler.handleUserConfigMessagesViaLibSession(
// // allDecryptedConfigMessages
// // );
// } catch (e) {
// const allMessageHases = allDecryptedConfigMessages.map(m => m.messageHash).join(',');
// window.log.warn(
// `failed to handle group messages hashes "${allMessageHases}" with libsession. Error: "${e.message}"`
// );
// }
// }
} catch (e) {
window.log.warn(
`handleGroupSharedConfigMessages of ${groupConfigMessagesMerged.length} failed with ${e.message}`

@ -19,6 +19,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 { getCurrentlySelectedConversationOutsideRedux } from '../../state/selectors/conversations';
import { assertUnreachable } from '../../types/sqlSharedTypes';
import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';
@ -27,6 +28,7 @@ import { getSwarmPollingInstance } from '../apis/snode_api';
import { GetNetworkTime } from '../apis/snode_api/getNetworkTime';
import { SnodeNamespaces } from '../apis/snode_api/namespaces';
import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage';
import { ed25519Str } from '../onions/onionPath';
import { UserUtils } from '../utils';
import { ConfigurationSync } from '../utils/job_runners/jobs/ConfigurationSyncJob';
import { LibSessionUtil } from '../utils/libsession/libsession_utils';
@ -532,12 +534,13 @@ async function removeLegacyGroupFromWrappers(groupId: string) {
await removeAllClosedGroupEncryptionKeyPairs(groupId);
}
async function remove03GroupFromWrappers(groupId: GroupPubkeyType) {
getSwarmPollingInstance().removePubkey(groupId, 'remove03GroupFromWrappers');
async function remove03GroupFromWrappers(groupPk: GroupPubkeyType) {
getSwarmPollingInstance().removePubkey(groupPk, 'remove03GroupFromWrappers');
await UserGroupsWrapperActions.eraseGroup(groupId);
await SessionUtilConvoInfoVolatile.removeGroupFromWrapper(groupId);
window.log.warn('remove 03 from metagroup wrapper');
await UserGroupsWrapperActions.eraseGroup(groupPk);
await SessionUtilConvoInfoVolatile.removeGroupFromWrapper(groupPk);
window?.inboxStore?.dispatch(groupInfoActions.destroyGroupDetails({ groupPk }));
window.log.info(`removed 03 from metagroup wrapper ${ed25519Str(groupPk)}`);
}
async function removeCommunityFromWrappers(conversationId: string) {

@ -1,4 +1,4 @@
import _, { concat } from 'lodash';
import _ from 'lodash';
import { ClosedGroup, getMessageQueue } from '..';
import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { addKeyPairToCacheAndDBIfNeeded } from '../../receiver/closedGroups';
@ -16,27 +16,13 @@ import { PubKey } from '../types';
import { UserUtils } from '../utils';
import { forceSyncConfigurationNowIfNeeded } from '../utils/sync/syncUtils';
import { getConversationController } from './ConversationController';
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.
* @param groupName the name of this closed group
* @param members the initial members of this closed group
* @param isV3 if this closed group is a v3 closed group or not (has a 03 prefix in the identity keypair)
*/
export async function createClosedGroup(groupName: string, members: Array<string>, isV3: boolean) {
if (isV3) {
const us = UserUtils.getOurPubKeyStrFromCache();
// we need to send a group info and encryption keys message to the batch endpoint with both seqno being 0
console.error('isV3 send invite to group TODO'); // FIXME
// FIXME we should save the key to the wrapper right away? or even to the DB idk
window.inboxStore.dispatch(
groupInfoActions.initNewGroupInWrapper({ members: concat(members, [us]), groupName, us })
);
return;
}
export async function createClosedGroup(groupName: string, members: Array<string>) {
// this is all legacy group logic.
// TODO: To be removed
@ -64,8 +50,6 @@ export async function createClosedGroup(groupName: string, members: Array<string
await convo.setIsApproved(true, false);
console.warn('store the v3 identityPriatekeypair as part of the wrapper only?');
// Ensure the current user is a member
setOfMembers.add(us);
const listOfMembers = [...setOfMembers];

@ -2,7 +2,6 @@
import { GroupPubkeyType } from 'libsession_util_nodejs';
import { isArray, isEmpty, isNumber, isString } from 'lodash';
import { UserUtils } from '../..';
import { ReleasedFeatures } from '../../../../util/releaseFeature';
import { isSignInByLinking } from '../../../../util/storage';
import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface';
import {
@ -69,10 +68,6 @@ function resultsToSuccessfulChange(
for (let j = 0; j < result.length; j++) {
const batchResult = result[j];
const messagePostedHashes = batchResult?.body?.hash;
console.error(
'this might be wrong as the key message is first now messagePostedHashes',
messagePostedHashes
);
if (batchResult.code === 200 && isString(messagePostedHashes) && request.messages?.[j].data) {
// libsession keeps track of the hashes to push and pushed using the hashes now
@ -121,6 +116,52 @@ async function buildAndSaveDumpsToDB(
return LibSessionUtil.saveMetaGroupDumpToDb(groupPk);
}
async function pushChangesToGroupSwarmIfNeeded(groupPk: GroupPubkeyType): Promise<RunJobResult> {
// save the dumps to DB even before trying to push them, so at least we have an up to date dumps in the DB in case of crash, no network etc
await LibSessionUtil.saveMetaGroupDumpToDb(groupPk);
const singleDestChanges = await LibSessionUtil.pendingChangesForGroup(groupPk);
// If there are no pending changes then the job can just complete (next time something
// is updated we want to try and run immediately so don't scuedule another run in this case)
if (isEmpty(singleDestChanges?.messages)) {
return RunJobResult.Success;
}
const oldHashesToDelete = new Set(singleDestChanges.allOldHashes);
const msgs: Array<StoreOnNodeData> = singleDestChanges.messages.map(item => {
return {
namespace: item.namespace,
pubkey: groupPk,
networkTimestamp: GetNetworkTime.getNowWithNetworkOffset(),
ttl: TTL_DEFAULT.TTL_CONFIG,
data: item.data,
};
});
const result = await MessageSender.sendEncryptedDataToSnode(msgs, groupPk, oldHashesToDelete);
const expectedReplyLength = singleDestChanges.messages.length + (oldHashesToDelete.size ? 1 : 0);
// we do a sequence call here. If we do not have the right expected number of results, consider it a failure
if (!isArray(result) || result.length !== expectedReplyLength) {
window.log.info(
`GroupSyncJob: unexpected result length: expected ${expectedReplyLength} but got ${result?.length}`
);
// this might be a 421 error (already handled) so let's retry this request a little bit later
return RunJobResult.RetryJobIfPossible;
}
const changes = resultsToSuccessfulChange(result, singleDestChanges);
if (isEmpty(changes)) {
return RunJobResult.RetryJobIfPossible;
}
// Now that we have the successful changes, we need to mark them as pushed and
// generate any config dumps which need to be stored
await buildAndSaveDumpsToDB(changes, groupPk);
return RunJobResult.Success;
}
class GroupSyncJob extends PersistedJob<GroupSyncPersistedData> {
constructor({
identifier, // this has to be the pubkey to which we
@ -162,59 +203,8 @@ class GroupSyncJob extends PersistedJob<GroupSyncPersistedData> {
return RunJobResult.PermanentFailure;
}
// save the dumps to DB even before trying to push them, so at least we have an up to date dumps in the DB in case of crash, no network etc
await LibSessionUtil.saveMetaGroupDumpToDb(thisJobDestination);
const newGroupsReleased = await ReleasedFeatures.checkIsNewGroupsReleased();
// if the feature flag is not enabled, we want to keep updating the dumps, but just not sync them.
if (!newGroupsReleased) {
return RunJobResult.Success;
}
const singleDestChanges = await LibSessionUtil.pendingChangesForGroup(thisJobDestination);
// If there are no pending changes then the job can just complete (next time something
// is updated we want to try and run immediately so don't scuedule another run in this case)
if (isEmpty(singleDestChanges?.messages)) {
return RunJobResult.Success;
}
const oldHashesToDelete = new Set(singleDestChanges.allOldHashes);
const msgs: Array<StoreOnNodeData> = singleDestChanges.messages.map(item => {
return {
namespace: item.namespace,
pubkey: thisJobDestination,
networkTimestamp: GetNetworkTime.getNowWithNetworkOffset(),
ttl: TTL_DEFAULT.TTL_CONFIG,
data: item.data,
};
});
const result = await MessageSender.sendEncryptedDataToSnode(
msgs,
thisJobDestination,
oldHashesToDelete
);
const expectedReplyLength =
singleDestChanges.messages.length + (oldHashesToDelete.size ? 1 : 0);
// we do a sequence call here. If we do not have the right expected number of results, consider it a failure
if (!isArray(result) || result.length !== expectedReplyLength) {
window.log.info(
`GroupSyncJob: unexpected result length: expected ${expectedReplyLength} but got ${result?.length}`
);
// this might be a 421 error (already handled) so let's retry this request a little bit later
return RunJobResult.RetryJobIfPossible;
}
const changes = resultsToSuccessfulChange(result, singleDestChanges);
if (isEmpty(changes)) {
return RunJobResult.RetryJobIfPossible;
}
// Now that we have the successful changes, we need to mark them as pushed and
// generate any config dumps which need to be stored
return await pushChangesToGroupSwarmIfNeeded(thisJobDestination);
await buildAndSaveDumpsToDB(changes, thisJobDestination);
return RunJobResult.Success;
// eslint-disable-next-line no-useless-catch
} catch (e) {
throw e;
@ -289,6 +279,7 @@ async function queueNewJobIfNeeded(groupPk: GroupPubkeyType) {
export const GroupSync = {
GroupSyncJob,
pushChangesToGroupSwarmIfNeeded,
queueNewJobIfNeeded: (groupPk: GroupPubkeyType) =>
allowOnlyOneAtATime(`GroupSyncJob-oneAtAtTime-${groupPk}`, () => queueNewJobIfNeeded(groupPk)),
};

@ -6,25 +6,17 @@ import { compact, difference, omit } from 'lodash';
import Long from 'long';
import { UserUtils } from '..';
import { ConfigDumpData } from '../../../data/configDump/configDump';
import { HexString } from '../../../node/hexStrings';
import { SignalService } from '../../../protobuf';
import { UserConfigKind } from '../../../types/ProtobufKind';
import {
ConfigDumpRow,
assertUnreachable,
toFixedUint8ArrayOfLength,
} from '../../../types/sqlSharedTypes';
import { assertUnreachable } from '../../../types/sqlSharedTypes';
import {
ConfigWrapperGroupDetailed,
ConfigWrapperUser,
getGroupPubkeyFromWrapperType,
isMetaWrapperType,
isUserConfigWrapperType,
} from '../../../webworker/workers/browser/libsession_worker_functions';
import {
GenericWrapperActions,
MetaGroupWrapperActions,
UserGroupsWrapperActions,
} from '../../../webworker/workers/browser/libsession_worker_interface';
import { GetNetworkTime } from '../../apis/snode_api/getNetworkTime';
import { SnodeNamespaces } from '../../apis/snode_api/namespaces';
@ -34,7 +26,6 @@ import {
} from '../../messages/outgoing/controlMessage/SharedConfigMessage';
import { ed25519Str } from '../../onions/onionPath';
import { PubKey } from '../../types';
import { getUserED25519KeyPairBytes } from '../User';
import { ConfigurationSync } from '../job_runners/jobs/ConfigurationSyncJob';
const requiredUserVariants: Array<ConfigWrapperUser> = [
@ -114,61 +105,7 @@ async function initializeLibSessionUtilWrappers() {
);
}
await loadMetaGroupWrappers(dumps);
}
async function loadMetaGroupWrappers(dumps: Array<ConfigDumpRow>) {
const ed25519KeyPairBytes = await getUserED25519KeyPairBytes();
if (!ed25519KeyPairBytes?.privKeyBytes) {
throw new Error('user has no ed25519KeyPairBytes.');
}
// load the dumps retrieved from the database into their corresponding wrappers
for (let index = 0; index < dumps.length; index++) {
const dump = dumps[index];
const { variant } = dump;
if (!isMetaWrapperType(variant)) {
continue;
}
const groupPk = getGroupPubkeyFromWrapperType(variant);
const groupPkNoPrefix = groupPk.substring(2);
const groupEd25519Pubkey = HexString.fromHexString(groupPkNoPrefix);
try {
const foundInUserGroups = await UserGroupsWrapperActions.getGroup(groupPk);
// 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),
groupEd25519Secretkey: foundInUserGroups?.secretKey || null,
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} `);
// throw new Error(`initializeLibSessionUtilWrappers failed with ${e.message}`);
}
}
// No need to load the meta group wrapper here. We will load them once the SessionInbox is loaded with a redux action
}
async function pendingChangesForUs(): Promise<
@ -304,7 +241,6 @@ async function pendingChangesForGroup(
const infoHashes = compact(groupInfo?.hashes) || [];
const allOldHashes = new Set([...infoHashes, ...memberHashes]);
console.error('compactedHashes', [...allOldHashes]);
return { messages: results, allOldHashes };
}
@ -361,9 +297,9 @@ async function saveMetaGroupDumpToDb(groupPk: GroupPubkeyType) {
publicKey: groupPk,
variant: `MetaGroupConfig-${groupPk}`,
});
window.log.debug(`Saved dumps for metagroup ${groupPk}`);
window.log.debug(`Saved dumps for metagroup ${ed25519Str(groupPk)}`);
} else {
window.log.debug(`meta did not dumps saving for metagroup ${groupPk}`);
window.log.debug(`No need to update local dumps for metagroup ${ed25519Str(groupPk)}`);
}
}

@ -1,14 +1,21 @@
/* eslint-disable no-await-in-loop */
import { PayloadAction, createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import { GroupInfoGet, GroupMemberGet, GroupPubkeyType } from 'libsession_util_nodejs';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import {
GroupInfoGet,
GroupMemberGet,
GroupPubkeyType,
UserGroupsGet,
} from 'libsession_util_nodejs';
import { isEmpty, uniq } 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 { getUserED25519KeyPairBytes } from '../../session/utils/User';
import { PreConditionFailed } from '../../session/utils/errors';
import { GroupSync } from '../../session/utils/job_runners/jobs/GroupConfigJob';
import { toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes';
import { stringify, toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes';
import {
getGroupPubkeyFromWrapperType,
isMetaWrapperType,
@ -17,16 +24,22 @@ import {
MetaGroupWrapperActions,
UserGroupsWrapperActions,
} from '../../webworker/workers/browser/libsession_worker_interface';
import { PreConditionFailed } from '../../session/utils/errors';
import { getSwarmPollingInstance } from '../../session/apis/snode_api';
import { StateType } from '../reducer';
import { RunJobResult } from '../../session/utils/job_runners/PersistedJob';
import { resetOverlayMode } from './section';
import { openConversationWithMessages } from './conversations';
export type GroupState = {
infos: Record<GroupPubkeyType, GroupInfoGet>;
members: Record<GroupPubkeyType, Array<GroupMemberGet>>;
creationFromUIPending: boolean;
};
export const initialGroupState: GroupState = {
infos: {},
members: {},
creationFromUIPending: false,
};
type GroupDetailsUpdate = {
@ -35,17 +48,25 @@ type GroupDetailsUpdate = {
members: Array<GroupMemberGet>;
};
/**
* Create a brand new group with a 03 prefix.
* To be called only when our current logged in user, through the UI, creates a brand new closed group given a name and a list of members.
*
*/
const initNewGroupInWrapper = createAsyncThunk(
'group/initNewGroupInWrapper',
async ({
groupName,
members,
us,
}: {
groupName: string;
members: Array<string>;
us: string;
}): Promise<GroupDetailsUpdate> => {
async (
{
groupName,
members,
us,
}: {
groupName: string;
members: Array<string>;
us: string;
},
{ dispatch }
): Promise<GroupDetailsUpdate> => {
if (!members.includes(us)) {
throw new PreConditionFailed('initNewGroupInWrapper needs us to be a member');
}
@ -54,8 +75,8 @@ const initNewGroupInWrapper = createAsyncThunk(
const groupPk = newGroup.pubkeyHex;
newGroup.name = groupName; // this will be used by the linked devices until they fetch the info from the groups swarm
// the `GroupSync` below will need the secretKey of the group to be saved in the wrapper. So save it!
await UserGroupsWrapperActions.setGroup(newGroup);
const ourEd25519KeypairBytes = await UserUtils.getUserED25519KeyPairBytes();
if (!ourEd25519KeypairBytes) {
throw new Error('Current user has no priv ed25519 key?');
@ -100,70 +121,135 @@ const initNewGroupInWrapper = createAsyncThunk(
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(groupPk);
// const updateGroupDetails: ClosedGroup.GroupInfo = {
// id: newGroup.pubkeyHex,
// name: groupDetails.groupName,
// members,
// admins: [us],
// activeAt: Date.now(),
// expireTimer: 0,
// };
const result = await GroupSync.pushChangesToGroupSwarmIfNeeded(groupPk);
if (result !== RunJobResult.Success) {
window.log.warn('GroupSync.pushChangesToGroupSwarmIfNeeded during create failed');
}
// // 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();
dispatch(resetOverlayMode());
await openConversationWithMessages({ conversationKey: groupPk, messageId: null });
return { groupPk: newGroup.pubkeyHex, infos, members: membersFromWrapper };
}
);
const loadDumpsFromDB = createAsyncThunk(
'group/loadDumpsFromDB',
/**
* Create a brand new group with a 03 prefix.
* To be called only when our current logged in user, through the UI, creates a brand new closed group given a name and a list of members.
*
*/
const handleUserGroupUpdate = createAsyncThunk(
'group/handleUserGroupUpdate',
async (userGroup: UserGroupsGet, payloadCreator): Promise<GroupDetailsUpdate> => {
// if we already have a state for that group here, it means that group was already init, and the data should come from the groupInfos after.
const state = payloadCreator.getState() as StateType;
const groupPk = userGroup.pubkeyHex;
if (state.groups.infos[groupPk] && state.groups.members[groupPk]) {
throw new Error('handleUserGroupUpdate group already present in redux slice');
}
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(groupPk).slice(1); // remove the 03 prefix (single byte once in hex form)
// dump is always empty when creating a new groupInfo
try {
await MetaGroupWrapperActions.init(groupPk, {
metaDumped: null,
userEd25519Secretkey: toFixedUint8ArrayOfLength(userEd25519Secretkey, 64),
groupEd25519Secretkey: userGroup.secretKey,
groupEd25519Pubkey: toFixedUint8ArrayOfLength(groupEd2519Pk, 32),
});
} catch (e) {
window.log.warn(`failed to init metawrapper ${groupPk}`);
}
const convo = await getConversationController().getOrCreateAndWait(
groupPk,
ConversationTypeEnum.GROUPV3
);
await convo.setIsApproved(true, false);
await convo.setPriorityFromWrapper(userGroup.priority, false);
convo.set({
active_at: Date.now(),
displayNameInProfile: userGroup.name || undefined,
});
await convo.commit();
return {
groupPk,
infos: await MetaGroupWrapperActions.infoGet(groupPk),
members: await MetaGroupWrapperActions.memberGetAll(groupPk),
};
}
);
/**
* Called only when the app just loaded the SessionInbox (i.e. user logged in and fully loaded).
* This function populates the slice with any meta-dumps we have in the DB, if they also are part of what is the usergroup wrapper tracking.
*
*/
const loadMetaDumpsFromDB = createAsyncThunk(
'group/loadMetaDumpsFromDB',
async (): Promise<Array<GroupDetailsUpdate>> => {
const variantsWithoutData = await ConfigDumpData.getAllDumpsWithoutData();
const ed25519KeyPairBytes = await getUserED25519KeyPairBytes();
if (!ed25519KeyPairBytes?.privKeyBytes) {
throw new Error('user has no ed25519KeyPairBytes.');
}
const variantsWithData = await ConfigDumpData.getAllDumpsWithData();
const allUserGroups = await UserGroupsWrapperActions.getAllGroups();
const toReturn: Array<GroupDetailsUpdate> = [];
for (let index = 0; index < variantsWithoutData.length; index++) {
const { variant } = variantsWithoutData[index];
for (let index = 0; index < variantsWithData.length; index++) {
const { variant, data } = variantsWithData[index];
if (!isMetaWrapperType(variant)) {
continue;
}
const groupPk = getGroupPubkeyFromWrapperType(variant);
const groupEd25519Pubkey = HexString.fromHexString(groupPk.substring(2));
const foundInUserWrapper = allUserGroups.find(m => m.pubkeyHex === groupPk);
if (!foundInUserWrapper) {
try {
window.log.info(
'metaGroup not found in userGroups. Deleting the corresponding dumps:',
groupPk
);
await ConfigDumpData.deleteDumpFor(groupPk);
} catch (e) {
window.log.warn(`ConfigDumpData.deleteDumpFor for ${groupPk} failed with `, e.message);
}
continue;
}
try {
window.log.debug(
'loadDumpsFromDB loading from metagroup variant: ',
variant,
foundInUserWrapper.pubkeyHex
);
window.log.debug('loadMetaDumpsFromDB initing from metagroup dump', variant);
await MetaGroupWrapperActions.init(groupPk, {
groupEd25519Pubkey: toFixedUint8ArrayOfLength(groupEd25519Pubkey, 32),
groupEd25519Secretkey: foundInUserWrapper?.secretKey || null,
userEd25519Secretkey: toFixedUint8ArrayOfLength(ed25519KeyPairBytes.privKeyBytes, 64),
metaDumped: data,
});
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(
// Note: Don't retrow here, we want to load everything we can
window.log.error(
`initGroup of Group wrapper of variant ${variant} failed with ${e.message} `
);
// throw new Error(`initializeLibSessionUtilWrappers failed with ${e.message}`);
}
}
@ -171,45 +257,141 @@ const loadDumpsFromDB = createAsyncThunk(
}
);
/**
* This action is to be called when we get a merge event from the network.
* It refreshes the state of that particular group (info & members) with the state from the wrapper after the merge is done.
*
*/
const refreshGroupDetailsFromWrapper = createAsyncThunk(
'group/refreshGroupDetailsFromWrapper',
async ({
groupPk,
}: {
groupPk: GroupPubkeyType;
}): Promise<
GroupDetailsUpdate | ({ groupPk: GroupPubkeyType } & Partial<GroupDetailsUpdate>)
> => {
try {
const infos = await MetaGroupWrapperActions.infoGet(groupPk);
const members = await MetaGroupWrapperActions.memberGetAll(groupPk);
return { groupPk, infos, members };
} catch (e) {
window.log.warn('refreshGroupDetailsFromWrapper failed with ', e.message);
return { groupPk };
}
}
);
const destroyGroupDetails = createAsyncThunk(
'group/destroyGroupDetails',
async ({ groupPk }: { groupPk: GroupPubkeyType }) => {
try {
await UserGroupsWrapperActions.eraseGroup(groupPk);
await ConfigDumpData.deleteDumpFor(groupPk);
await MetaGroupWrapperActions.infoDestroy(groupPk);
getSwarmPollingInstance().removePubkey(groupPk, 'destroyGroupDetails');
} catch (e) {
window.log.warn(`destroyGroupDetails for ${groupPk} failed with ${e.message}`);
}
return { groupPk };
}
);
/**
* 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<GroupDetailsUpdate>) {
const { groupPk, infos, members } = action.payload;
state.infos[groupPk] = infos;
state.members[groupPk] = members;
},
},
reducers: {},
extraReducers: builder => {
builder.addCase(initNewGroupInWrapper.fulfilled, (state, action) => {
const { groupPk, infos, members } = action.payload;
state.infos[groupPk] = infos;
state.members[groupPk] = members;
state.creationFromUIPending = false;
});
builder.addCase(initNewGroupInWrapper.rejected, () => {
builder.addCase(initNewGroupInWrapper.rejected, state => {
window.log.error('a initNewGroupInWrapper was rejected');
// FIXME delete the wrapper completely & correspondign dumps, and usergroups entry?
state.creationFromUIPending = false;
throw new Error('initNewGroupInWrapper.rejected');
// FIXME delete the wrapper completely & corresponding dumps, and usergroups entry?
});
builder.addCase(initNewGroupInWrapper.pending, (state, _action) => {
state.creationFromUIPending = true;
window.log.error('a initNewGroupInWrapper is pending');
});
builder.addCase(loadDumpsFromDB.fulfilled, (state, action) => {
builder.addCase(loadMetaDumpsFromDB.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');
builder.addCase(loadMetaDumpsFromDB.rejected, () => {
window.log.error('a loadMetaDumpsFromDB was rejected');
});
builder.addCase(refreshGroupDetailsFromWrapper.fulfilled, (state, action) => {
const { infos, members, groupPk } = action.payload;
if (infos && members) {
state.infos[groupPk] = infos;
state.members[groupPk] = members;
window.log.debug(`groupInfo after merge: ${stringify(infos)}`);
window.log.debug(`groupMembers after merge: ${stringify(members)}`);
} else {
window.log.debug(
`refreshGroupDetailsFromWrapper no details found, removing from slice: ${groupPk}}`
);
delete state.infos[groupPk];
delete state.members[groupPk];
}
});
builder.addCase(refreshGroupDetailsFromWrapper.rejected, () => {
window.log.error('a refreshGroupDetailsFromWrapper was rejected');
});
builder.addCase(destroyGroupDetails.fulfilled, (state, action) => {
const { groupPk } = action.payload;
// FIXME destroyGroupDetails marks the info as destroyed, but does not really remove the wrapper currently
delete state.infos[groupPk];
delete state.members[groupPk];
});
builder.addCase(destroyGroupDetails.rejected, () => {
window.log.error('a destroyGroupDetails was rejected');
});
builder.addCase(handleUserGroupUpdate.fulfilled, (state, action) => {
const { infos, members, groupPk } = action.payload;
if (infos && members) {
state.infos[groupPk] = infos;
state.members[groupPk] = members;
window.log.debug(`groupInfo after handleUserGroupUpdate: ${stringify(infos)}`);
window.log.debug(`groupMembers after handleUserGroupUpdate: ${stringify(members)}`);
} else {
window.log.debug(
`handleUserGroupUpdate no details found, removing from slice: ${groupPk}}`
);
delete state.infos[groupPk];
delete state.members[groupPk];
}
});
builder.addCase(handleUserGroupUpdate.rejected, () => {
window.log.error('a handleUserGroupUpdate was rejected');
});
},
});
export const groupInfoActions = {
initNewGroupInWrapper,
loadDumpsFromDB,
loadMetaDumpsFromDB,
destroyGroupDetails,
refreshGroupDetailsFromWrapper,
handleUserGroupUpdate,
...groupSlice.actions,
};
export const groupReducer = groupSlice.reducer;

@ -23,6 +23,10 @@ export function getLibMembersPubkeys(state: StateType, convo?: string): Array<st
return members.map(m => m.pubkeyHex);
}
function getIsCreatingGroupFromUI(state: StateType): boolean {
return getLibGroupsState(state).creationFromUIPending;
}
export function getLibAdminsPubkeys(state: StateType, convo?: string): Array<string> {
if (!convo) {
return [];
@ -81,3 +85,7 @@ export function getLibGroupAdminsOutsideRedux(convoId: string): Array<string> {
const state = window.inboxStore?.getState();
return state ? getLibAdminsPubkeys(state, convoId) : [];
}
export function useIsCreatingGroupFromUIPending() {
return useSelector(getIsCreatingGroupFromUI);
}

@ -279,8 +279,8 @@ export function toFixedUint8ArrayOfLength<T extends number>(
length: T
): FixedSizeUint8Array<T> {
if (data.length === length) {
return data as any as FixedSizeUint8Array<T>;
}
return data as any as FixedSizeUint8Array<T>;
}
throw new Error(
`toFixedUint8ArrayOfLength invalid. Expected length ${length} but got: ${data.length}`
);

@ -101,17 +101,11 @@ async function checkIsUserConfigFeatureReleased() {
return checkIsFeatureReleased('user_config_libsession');
}
// TODO AUDRIC maybe we want this, maybe we don't?
async function checkIsNewGroupsReleased() {
return true;
}
function isUserConfigFeatureReleasedCached(): boolean {
return !!isUserConfigLibsessionFeatureReleased;
}
export const ReleasedFeatures = {
checkIsUserConfigFeatureReleased,
checkIsNewGroupsReleased,
isUserConfigFeatureReleasedCached,
};

@ -178,7 +178,8 @@ function initGroupWrapper(options: Array<any>, wrapperType: ConfigWrapperGroup)
const wrapper = getGroupWrapper(wrapperType);
if (wrapper) {
throw new Error(`group: "${wrapperType}" already init`);
// console.warn(`group: "${wrapperType}" already init`);
return;
}
if (options.length !== 1) {

2
ts/window.d.ts vendored

@ -33,7 +33,7 @@ declare global {
sessionFeatureFlags: {
useOnionRequests: boolean;
useTestNet: boolean;
useClosedGroupV3: boolean;
useClosedGroupV2: boolean;
integrationTestEnv: boolean;
debug: {
debugLogging: boolean;

Loading…
Cancel
Save