feat: add function to redux to grab group detail outside of store

pull/2873/head
Audric Ackermann 2 years ago
parent bbedfd943c
commit 46e3675c45

@ -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<T>(items: Array<T>, key: string): { [key: string]: T } {
// Yep, we can't index into item without knowing what it is. True. But we want to.
@ -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();
}

@ -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 <div className="module-conversation-header__title">{i18n('noteToSelf')}</div>;
}
let memberCount = 0;
if (isGroup) {
if (isPublic) {
memberCount = subscriberCount || 0;
} else {
memberCount = members.length;
}
if (isPublic) {
memberCount = subscriberCount || 0;
}
let memberCountText = '';

@ -7,30 +7,28 @@ interface Props {
onClose: (attachment: AttachmentType) => void;
}
export class StagedGenericAttachment extends React.Component<Props> {
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 (
<div className="module-staged-generic-attachment">
<div
className="module-staged-generic-attachment__close-button"
role="button"
onClick={() => {
if (onClose) {
onClose(attachment);
}
}}
/>
<div className="module-staged-generic-attachment__icon">
{extension ? (
<div className="module-staged-generic-attachment__icon__extension">{extension}</div>
) : null}
</div>
<div className="module-staged-generic-attachment__filename">{fileName}</div>
return (
<div className="module-staged-generic-attachment">
<div
className="module-staged-generic-attachment__close-button"
role="button"
onClick={() => {
if (onClose) {
onClose(attachment);
}
}}
/>
<div className="module-staged-generic-attachment__icon">
{extension ? (
<div className="module-staged-generic-attachment__icon__extension">{extension}</div>
) : null}
</div>
);
}
}
<div className="module-staged-generic-attachment__filename">{fileName}</div>
</div>
);
};

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

@ -7,10 +7,8 @@ interface Props {
label: string;
}
export class EmptyState extends React.Component<Props> {
public render() {
const { label } = this.props;
export const EmptyState = (props: Props) => {
const { label } = props;
return <div className="module-empty-state">{label}</div>;
}
}
return <div className="module-empty-state">{label}</div>;
};

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

@ -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}
/>
<Flex container={true}>
<SessionButton

@ -10,7 +10,13 @@ import { ToastUtils, UserUtils } from '../../session/utils';
import { updateInviteContactModal } from '../../state/ducks/modalDialog';
import { SpacerLG } from '../basic/Text';
import { useConversationPropsById } from '../../hooks/useParamSelector';
import {
useConversationUsername,
useIsPrivate,
useIsPublic,
useSortedGroupMembers,
useZombies,
} from '../../hooks/useParamSelector';
import { useSet } from '../../hooks/useSet';
import { initiateClosedGroupUpdate } from '../../session/group/closed-group';
import { SessionUtilUserGroups } from '../../session/utils/libsession/libsession_utils_user_groups';
@ -104,27 +110,27 @@ const InviteContactsDialogInner = (props: Props) => {
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<string>();
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);

@ -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 (
<SessionWrapperModal title={title} onClose={closeDialog}>
<Flex container={true} flexDirection="column" alignItems="center">

@ -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<string>) {
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<string>) {
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<string>(
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 (
<SessionWrapperModal title={titleText} onClose={closeDialog}>

@ -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<Props, State> {
private readonly convo: ConversationModel;
@ -35,8 +39,13 @@ export class UpdateGroupNameDialog extends React.Component<Props, State> {
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<Props, State> {
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,

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

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

@ -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<ConfigDumpDataNode> = {
getAllDumpsWithoutDataFor: (pk: string) => {
return channels.getAllDumpsWithoutDataFor(pk);
},
deleteDumpFor: (pk: GroupPubkeyType) => {
return channels.deleteDumpFor(pk);
},
};

@ -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<string> {
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()) || [];
}

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

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

@ -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<ConversationAttributes> {
await deleteExternalFilesOfConversation(this.attributes);
}
public getGroupAdmins(): Array<string> {
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<ConversationAttributes> {
* @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<ConversationAttributes> {
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<ConversationAttributes> {
return this.markConversationReadBouncy(newestUnreadDate);
}
public getGroupAdmins(): Array<string> {
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();

@ -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<ConfigDumpRow, 'data' | 'publicKey' | 'variant'>
@ -114,4 +115,9 @@ export const configDumpData: ConfigDumpDataNode = {
data,
});
},
deleteDumpFor: (publicKey: GroupPubkeyType) => {
assertGlobalInstance()
.prepare(`DELETE FROM ${CONFIG_DUMP_TABLE} WHERE publicKey=$publicKey;`)
.run({ publicKey });
},
};

@ -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') || [];

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

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

@ -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<string> = [];
let admins: Array<string> = [];
@ -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);

@ -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<string
// 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.initNewGroupInfoInWrapper({ members, groupName }));
window.inboxStore.dispatch(groupInfoActions.initNewGroupInWrapper({ members, groupName }));
return;
}

@ -86,7 +86,7 @@ export async function initiateClosedGroupUpdate(
id: groupId,
name: groupName,
members,
admins: convo.get('groupAdmins'),
admins: convo.getGroupAdmins(),
expireTimer: convo.get('expireTimer'),
};
@ -335,7 +335,7 @@ export async function sendRemovedMembers(
return;
}
const ourNumber = UserUtils.getOurPubKeyFromCache();
const admins = convo.get('groupAdmins') || [];
const admins = convo.getGroupAdmins() || [];
const groupId = convo.get('id');
const isCurrentUserAdmin = admins.includes(ourNumber.key);
@ -393,7 +393,7 @@ async function generateAndSendNewEncryptionKeyPair(
}
const ourNumber = UserUtils.getOurPubKeyStrFromCache();
if (!groupConvo.get('groupAdmins')?.includes(ourNumber)) {
if (!groupConvo.getGroupAdmins().includes(ourNumber)) {
window?.log?.warn('generateAndSendNewEncryptionKeyPair: cannot send it as a non admin');
return;
}

@ -2,17 +2,6 @@ import { PubKey } from '../types';
import { getConversationController } from '../conversations';
import { fromHexToArray } from './String';
export function getGroupMembers(groupId: PubKey): Array<PubKey> {
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);

@ -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<GroupSyncPersistedData> {
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);

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

@ -47,13 +47,13 @@ async function insertContactFromDBIntoWrapperAndRefresh(id: string): Promise<voi
return;
}
const dbName = foundConvo.get('displayNameInProfile') || undefined;
const dbNickname = foundConvo.get('nickname') || undefined;
const dbName = foundConvo.getRealSessionUsername() || undefined;
const dbNickname = foundConvo.getNickname();
const dbProfileUrl = foundConvo.get('avatarPointer') || undefined;
const dbProfileKey = foundConvo.get('profileKey') || undefined;
const dbApproved = !!foundConvo.get('isApproved') || false;
const dbApprovedMe = !!foundConvo.get('didApproveMe') || false;
const dbBlocked = !!foundConvo.isBlocked() || false;
const dbApproved = foundConvo.isApproved();
const dbApprovedMe = foundConvo.didApproveMe();
const dbBlocked = foundConvo.isBlocked();
const priority = foundConvo.get('priority') || 0;
// expiration timer is not tracked currently but will be once disappearing message is merged into userconfig
// const expirationTimerSeconds = foundConvo.get('expireTimer') || 0;

@ -122,7 +122,7 @@ async function insertGroupsFromDBIntoWrapperAndRefresh(convoId: string): Promise
id: foundConvo.id,
priority: foundConvo.get('priority'),
members: foundConvo.get('members') || [],
groupAdmins: foundConvo.get('groupAdmins') || [],
groupAdmins: foundConvo.getGroupAdmins(),
// expireTimer: foundConvo.get('expireTimer'),
displayNameInProfile: foundConvo.get('displayNameInProfile'),
encPubkeyHex: encryptionKeyPair?.publicHex || '',

@ -182,7 +182,7 @@ const getValidClosedGroups = async (convos: Array<ConversationModel>) => {
publicKey: groupPubKey,
name: c.get('displayNameInProfile') || '',
members: c.get('members') || [],
admins: c.get('groupAdmins') || [],
admins: c.getGroupAdmins(),
encryptionKeyPair: ECKeyPair.fromHexKeyPair(fetchEncryptionKeyPair),
});
})

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

@ -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<GroupPubkeyType, GroupInfoGetWithId>;
};
export const initialGroupInfosState: GroupInfosState = {
infos: {},
};
const updateGroupInfoInWrapper = createAsyncThunk(
'groupInfos/updateGroupInfoInWrapper',
async ({
id,
data,
}: {
id: GroupPubkeyType;
data: GroupInfoShared;
}): Promise<GroupInfoGetWithId> => {
// 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<string>;
}): Promise<GroupInfoGetWithId> => {
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<Array<GroupInfoGetWithId>>) {
// 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;

@ -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<GroupPubkeyType, GroupInfoGet>;
members: Record<GroupPubkeyType, Array<GroupMemberGet>>;
};
export const initialGroupState: GroupState = {
infos: {},
members: {},
};
type GroupDetailsUpdate = {
groupPk: GroupPubkeyType;
infos: GroupInfoGet;
members: Array<GroupMemberGet>;
};
const initNewGroupInWrapper = createAsyncThunk(
'group/initNewGroupInWrapper',
async (groupDetails: {
groupName: string;
members: Array<string>;
}): Promise<GroupDetailsUpdate> => {
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<Array<GroupDetailsUpdate>> => {
const variantsWithoutData = await ConfigDumpData.getAllDumpsWithoutData();
const allUserGroups = await UserGroupsWrapperActions.getAllGroups();
const toReturn: Array<GroupDetailsUpdate> = [];
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<GroupDetailsUpdate>) {
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;

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

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

@ -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<string> {
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<string> {
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<string> {
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<string> {
return useSelector((state: StateType) => getLibMembersPubkeys(state, convoId));
}
export function useLibGroupAdmins(convoId?: string): Array<string> {
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<string> {
const state = window.inboxStore?.getState();
return state ? getLibMembersPubkeys(state, convoId) : [];
}
export function getLibGroupAdminsOutsideRedux(convoId: string): Array<string> {
const state = window.inboxStore?.getState();
return state ? getLibAdminsPubkeys(state, convoId) : [];
}

@ -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<string> => {
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<string> => {
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;
}

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

@ -4,6 +4,7 @@
import {
ContactInfoSet,
FixedSizeUint8Array,
GroupPubkeyType,
LegacyGroupInfo,
LegacyGroupMemberInfo,
} from 'libsession_util_nodejs';
@ -66,6 +67,7 @@ export type ConfigDumpDataNode = {
getAllDumpsWithData: () => Array<ConfigDumpRow>;
getAllDumpsWithoutData: () => Array<ConfigDumpRowWithoutData>;
getAllDumpsWithoutDataFor: (pk: string) => Array<ConfigDumpRowWithoutData>;
deleteDumpFor: (pk: GroupPubkeyType) => void;
};
// ========== unprocessed

@ -389,6 +389,10 @@ export const MetaGroupWrapperActions: MetaGroupWrapperActionsCalls = {
callLibSessionWorker([`MetaGroupConfig-${groupPk}`, 'metaDump']) as Promise<
ReturnType<MetaGroupWrapperActionsCalls['metaDump']>
>,
metaDebugDump: async (groupPk: GroupPubkeyType) =>
callLibSessionWorker([`MetaGroupConfig-${groupPk}`, 'metaDebugDump']) as Promise<
ReturnType<MetaGroupWrapperActionsCalls['metaDebugDump']>
>,
metaConfirmPushed: async (
groupPk: GroupPubkeyType,
args: Parameters<MetaGroupWrapperActionsCalls['metaConfirmPushed']>[1]

Loading…
Cancel
Save