chore: remove groupModerators sogs from the db, store in redux only

pull/2620/head
Audric Ackermann 2 years ago
parent bf2456df8e
commit 1c50aacc34

@ -81,7 +81,7 @@ export const MessageAvatar = (props: Props) => {
}
if (isPublic && !isTypingEnabled) {
window.log.info('onMessageAvatarClick: no typing enabled. Dropping...');
window.log.info('onMessageAvatarClick: typing is disabled...');
return;
}

@ -104,7 +104,7 @@ export class LeftPaneMessageSection extends React.Component<Props> {
const length = conversations.length;
// Note: conversations is not a known prop for List, but it is required to ensure that
// it re-renders when our conversation data changes. Otherwise it would just render
// it re-renders when our conversations data changes. Otherwise it would just render
// on startup and scroll.
return (

@ -1,14 +1,14 @@
import React from 'react';
import styled from 'styled-components';
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { recoveryPhraseModal } from '../../state/ducks/modalDialog';
import { SectionType } from '../../state/ducks/section';
import { disableRecoveryPhrasePrompt } from '../../state/ducks/userConfig';
import { getFocusedSection, getIsMessageRequestOverlayShown } from '../../state/selectors/section';
import { getShowRecoveryPhrasePrompt } from '../../state/selectors/userConfig';
import { recoveryPhraseModal } from '../../state/ducks/modalDialog';
import { isSignWithRecoveryPhrase } from '../../util/storage';
import { Flex } from '../basic/Flex';
import { getFocusedSection, getOverlayMode } from '../../state/selectors/section';
import { SectionType } from '../../state/ducks/section';
import { SessionButton } from '../basic/SessionButton';
import { isSignWithRecoveryPhrase } from '../../util/storage';
import { MenuButton } from '../button/MenuButton';
const SectionTitle = styled.h1`
@ -110,19 +110,18 @@ export const LeftPaneBanner = () => {
export const LeftPaneSectionHeader = () => {
const showRecoveryPhrasePrompt = useSelector(getShowRecoveryPhrasePrompt);
const focusedSection = useSelector(getFocusedSection);
const overlayMode = useSelector(getOverlayMode);
const isMessageRequestOverlayShown = useSelector(getIsMessageRequestOverlayShown);
let label: string | undefined;
const isMessageSection = focusedSection === SectionType.Message;
const isMessageRequestOverlay = overlayMode && overlayMode === 'message-requests';
switch (focusedSection) {
case SectionType.Settings:
label = window.i18n('settingsHeader');
break;
case SectionType.Message:
label = isMessageRequestOverlay
label = isMessageRequestOverlayShown
? window.i18n('messageRequests')
: window.i18n('messagesHeader');
break;

@ -1,32 +1,35 @@
import React, { useCallback, useContext } from 'react';
import classNames from 'classnames';
import React, { useCallback, useContext } from 'react';
import { contextMenu } from 'react-contexify';
import { Avatar, AvatarSize } from '../../avatar/Avatar';
import { createPortal } from 'react-dom';
import { useDispatch } from 'react-redux';
import {
openConversationWithMessages,
ReduxConversationType,
} from '../../../state/ducks/conversations';
import { useDispatch } from 'react-redux';
import { updateUserDetailsModal } from '../../../state/ducks/modalDialog';
import _ from 'lodash';
import { useSelector } from 'react-redux';
import {
useAvatarPath,
useConversationUsername,
useHasUnread,
useIsBlocked,
useIsPrivate,
useIsSelectedConversation,
useMentionedUs,
} from '../../../hooks/useParamSelector';
import { isSearching } from '../../../state/selectors/search';
import { MemoConversationListItemContextMenu } from '../../menu/ConversationListItemContextMenu';
import { ConversationListItemHeaderItem } from './HeaderItem';
import { MessageItem } from './MessageItem';
import _ from 'lodash';
// tslint:disable-next-line: no-empty-interface
export type ConversationListItemProps = Pick<
ReduxConversationType,
'id' | 'isSelected' | 'isBlocked' | 'mentionedUs' | 'unreadCount' | 'displayNameInProfile'
>;
export type ConversationListItemProps = Pick<ReduxConversationType, 'id'>;
/**
* This React context is used to share deeply in the tree of the ConversationListItem what is the ID we are currently rendering.
@ -36,7 +39,6 @@ export const ContextConversationId = React.createContext('');
type PropsHousekeeping = {
style?: Object;
isMessageRequest?: boolean;
};
// tslint:disable: use-simple-attributes
@ -74,19 +76,23 @@ const AvatarItem = () => {
);
};
// tslint:disable: max-func-body-length
const ConversationListItem = (props: Props) => {
const {
unreadCount,
id: conversationId,
isSelected,
isBlocked,
style,
mentionedUs,
isMessageRequest,
} = props;
const { id: conversationId, style } = props;
const key = `conversation-item-${conversationId}`;
const hasUnread = useHasUnread(conversationId);
let hasUnreadMentionedUs = useMentionedUs(conversationId);
let isBlocked = useIsBlocked(conversationId);
const isSearch = useSelector(isSearching);
const isSelectedConvo = useIsSelectedConversation(conversationId);
if (isSearch) {
// force isBlocked and hasUnreadMentionedUs to be false, we just want to display the row without any special style when showing search results
hasUnreadMentionedUs = false;
isBlocked = false;
}
const triggerId = `${key}-ctxmenu`;
const openConvo = useCallback(
@ -118,18 +124,16 @@ const ConversationListItem = (props: Props) => {
style={style}
className={classNames(
'module-conversation-list-item',
unreadCount && unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null,
unreadCount && unreadCount > 0 && mentionedUs
? 'module-conversation-list-item--mentioned-us'
: null,
isSelected ? 'module-conversation-list-item--is-selected' : null,
hasUnread ? 'module-conversation-list-item--has-unread' : null,
hasUnreadMentionedUs ? 'module-conversation-list-item--mentioned-us' : null,
isSelectedConvo ? 'module-conversation-list-item--is-selected' : null,
isBlocked ? 'module-conversation-list-item--is-blocked' : null
)}
>
<AvatarItem />
<div className="module-conversation-list-item__content">
<ConversationListItemHeaderItem />
<MessageItem isMessageRequest={Boolean(isMessageRequest)} />
<MessageItem />
</div>
</div>
<Portal>

@ -13,6 +13,7 @@ import { TypingAnimation } from '../../conversation/TypingAnimation';
import { ContextConversationId } from './ConversationListItem';
import { useSelector } from 'react-redux';
import { isSearching } from '../../../state/selectors/search';
import { getIsMessageRequestOverlayShown } from '../../../state/selectors/section';
function useLastMessageFromConvo(convoId: string) {
const convoProps = useConversationPropsById(convoId);
@ -22,13 +23,14 @@ function useLastMessageFromConvo(convoId: string) {
return convoProps.lastMessage;
}
export const MessageItem = (props: { isMessageRequest: boolean }) => {
export const MessageItem = () => {
const conversationId = useContext(ContextConversationId);
const lastMessage = useLastMessageFromConvo(conversationId);
const isGroup = !useIsPrivate(conversationId);
const hasUnread = useHasUnread(conversationId);
const isConvoTyping = useIsTyping(conversationId);
const isMessageRequest = useSelector(getIsMessageRequestOverlayShown);
const isSearchingMode = useSelector(isSearching);
@ -55,7 +57,7 @@ export const MessageItem = (props: { isMessageRequest: boolean }) => {
<MessageBody text={text} disableJumbomoji={true} disableLinks={true} isGroup={isGroup} />
)}
</div>
{!isSearchingMode && lastMessage && lastMessage.status && !props.isMessageRequest ? (
{!isSearchingMode && lastMessage && lastMessage.status && !isMessageRequest ? (
<OutgoingMessageStatus status={lastMessage.status} />
) : null}
</div>

@ -41,13 +41,7 @@ const MessageRequestList = () => {
return (
<MessageRequestListContainer>
{conversationRequests.map(conversation => {
return (
<MemoConversationListItemWithDetails
key={conversation.id}
isMessageRequest={true}
{...conversation}
/>
);
return <MemoConversationListItemWithDetails key={conversation.id} {...conversation} />;
})}
</MessageRequestListContainer>
);

@ -54,8 +54,6 @@ export const SearchResults = (props: SearchResultsProps) => {
{contactsAndGroups.map(contactOrGroup => (
<MemoConversationListItemWithDetails
{...contactOrGroup}
mentionedUs={false}
isBlocked={false}
key={`search-result-convo-${contactOrGroup.id}`}
/>
))}

@ -1,10 +1,13 @@
import { isEmpty, pick } from 'lodash';
import { isEmpty, isNil, pick } from 'lodash';
import { useSelector } from 'react-redux';
import { ConversationModel } from '../models/conversation';
import { PubKey } from '../session/types';
import { UserUtils } from '../session/utils';
import { StateType } from '../state/reducer';
import { getMessageReactsProps } from '../state/selectors/conversations';
import {
getMessageReactsProps,
getSelectedConversationKey,
} from '../state/selectors/conversations';
export function useAvatarPath(convoId: string | undefined) {
const convoProps = useConversationPropsById(convoId);
@ -202,13 +205,13 @@ export function useIsForcedUnreadWithoutUnreadMsg(conversationId?: string): bool
return convoProps?.isMarkedUnread || false;
}
function useMentionedUsNoUnread(conversationId?: string) {
function useMentionedUsUnread(conversationId?: string) {
const convoProps = useConversationPropsById(conversationId);
return convoProps?.mentionedUs || false;
}
export function useMentionedUs(conversationId?: string): boolean {
const hasMentionedUs = useMentionedUsNoUnread(conversationId);
const hasMentionedUs = useMentionedUsUnread(conversationId);
const hasUnread = useHasUnread(conversationId);
return hasMentionedUs && hasUnread;
@ -217,3 +220,8 @@ export function useMentionedUs(conversationId?: string): boolean {
export function useIsTyping(conversationId?: string): boolean {
return useConversationPropsById(conversationId)?.isTyping || false;
}
export function useIsSelectedConversation(conversation?: string): boolean {
const selectedConvo = useSelector(getSelectedConversationKey);
return !isNil(selectedConvo) && !isNil(conversation) && selectedConvo === conversation;
}

@ -157,7 +157,7 @@ export const declineConversationWithoutConfirm = async (
export async function showUpdateGroupNameByConvoId(conversationId: string) {
const conversation = getConversationController().get(conversationId);
if (conversation.isMediumGroup()) {
if (conversation.isClosedGroup()) {
// make sure all the members' convo exists so we can add or remove them
await Promise.all(
conversation
@ -170,7 +170,7 @@ export async function showUpdateGroupNameByConvoId(conversationId: string) {
export async function showUpdateGroupMembersByConvoId(conversationId: string) {
const conversation = getConversationController().get(conversationId);
if (conversation.isMediumGroup()) {
if (conversation.isClosedGroup()) {
// make sure all the members' convo exists so we can add or remove them
await Promise.all(
conversation
@ -192,7 +192,7 @@ export function showLeaveGroupByConvoId(conversationId: string) {
const message = window.i18n('leaveGroupConfirmation');
const ourPK = UserUtils.getOurPubKeyStrFromCache();
const isAdmin = (conversation.get('groupAdmins') || []).includes(ourPK);
const isClosedGroup = conversation.get('is_medium_group') || false;
const isClosedGroup = conversation.isClosedGroup() || false;
// if this is not a closed group, or we are not admin, we can just show a confirmation dialog
if (!isClosedGroup || (isClosedGroup && !isAdmin)) {

@ -126,7 +126,7 @@ export async function deleteMessagesFromSwarmAndCompletelyLocally(
conversation: ConversationModel,
messages: Array<MessageModel>
) {
if (conversation.isMediumGroup()) {
if (conversation.isClosedGroup()) {
window.log.info('Cannot delete message from a closed group swarm, so we just complete delete.');
await Promise.all(
messages.map(async message => {
@ -162,7 +162,7 @@ export async function deleteMessagesFromSwarmAndMarkAsDeletedLocally(
conversation: ConversationModel,
messages: Array<MessageModel>
) {
if (conversation.isMediumGroup()) {
if (conversation.isClosedGroup()) {
window.log.info('Cannot delete messages from a closed group swarm, so we just markDeleted.');
await Promise.all(
messages.map(async message => {

@ -111,6 +111,7 @@ import { SessionUtilUserProfile } from '../session/utils/libsession/libsession_u
import { ReduxSogsRoomInfos } from '../state/ducks/sogsRoomInfo';
import {
getCanWriteOutsideRedux,
getModeratorsOutsideRedux,
getSubscriberCountOutsideRedux,
} from '../state/selectors/sogsRoomInfo';
@ -120,6 +121,11 @@ type InMemoryConvoInfos = {
lastReadTimestampMessage: number | null;
};
// TODO decide it it makes sense to move this to a redux slice?
/**
* Some fields are not stored in the database, but are kept in memory.
* We use this map to keep track of them. The key is the conversation id.
*/
const inMemoryConvoInfos: Map<string, InMemoryConvoInfos> = new Map();
export class ConversationModel extends Backbone.Model<ConversationAttributes> {
@ -222,10 +228,10 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
public isOpenGroupV2(): boolean {
return OpenGroupUtils.isOpenGroupV2(this.id);
}
public isClosedGroup() {
return (
(this.get('type') === ConversationTypeEnum.GROUP && !this.isPublic()) ||
this.get('type') === ConversationTypeEnum.GROUPV3
public isClosedGroup(): boolean {
return Boolean(
(this.get('type') === ConversationTypeEnum.GROUP && this.id.startsWith('05')) ||
(this.get('type') === ConversationTypeEnum.GROUPV3 && this.id.startsWith('03'))
);
}
public isPrivate() {
@ -242,17 +248,13 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return false;
}
if (this.isPrivate() || this.isClosedGroup() || this.isMediumGroup()) {
if (this.isPrivate() || this.isClosedGroup()) {
return BlockedNumberController.isBlocked(this.id);
}
return false;
}
public isMediumGroup() {
return this.get('is_medium_group');
}
/**
* Returns true if this conversation is active
* i.e. the conversation is visibie on the left pane. (Either we or another user created this convo).
@ -277,21 +279,8 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return groupAdmins && groupAdmins?.length > 0 ? groupAdmins : [];
}
/**
* Get the list of moderators in that room, or an empty array
* Only to be called for opengroup conversations.
* This makes no sense for a private chat or an closed group, as closed group admins must be stored with getGroupAdmins
* @returns the list of moderators for the conversation if the conversation is public, or []
*/
public getGroupModerators(): Array<string> {
const groupModerators = this.get('groupModerators') as Array<string> | undefined;
return this.isPublic() && groupModerators && groupModerators?.length > 0 ? groupModerators : [];
}
// tslint:disable-next-line: cyclomatic-complexity max-func-body-length
public getConversationModelProps(): ReduxConversationType {
const groupModerators = this.getGroupModerators();
const isPublic = this.isPublic();
const zombies = this.isClosedGroup() ? this.get('zombies') : [];
@ -429,10 +418,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
if (foundCommunity.priority > 0) {
toRet.isPinned = true; // TODO priority also handles sorting
}
if (groupModerators?.length) {
toRet.groupModerators = uniq(groupModerators);
}
}
if (foundVolatileInfo) {
@ -481,6 +466,12 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return toRet;
}
/**
*
* @param groupAdmins the Array of group admins, where, if we are a group admin, we are present unblinded.
* @param shouldCommit set this to true to auto commit changes
* @returns true if the groupAdmins where not the same (and thus updated)
*/
public async updateGroupAdmins(groupAdmins: Array<string>, shouldCommit: boolean) {
const sortedExistingAdmins = uniq(sortBy(this.getGroupAdmins()));
const sortedNewAdmins = uniq(sortBy(groupAdmins));
@ -495,23 +486,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return true;
}
public async updateGroupModerators(groupModerators: Array<string>, shouldCommit: boolean) {
if (!this.isPublic()) {
throw new Error('group moderators are only possible on SOGS');
}
const existingModerators = uniq(sortBy(this.getGroupModerators()));
const newModerators = uniq(sortBy(groupModerators));
if (isEqual(existingModerators, newModerators)) {
return false;
}
this.set({ groupModerators: newModerators });
if (shouldCommit) {
await this.commit();
}
return true;
}
/**
* Fetches from the Database an update of what are the memory only informations like mentionedUs and the unreadCount, etc
*/
@ -736,7 +710,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return;
}
if (this.isMediumGroup()) {
if (this.isClosedGroup()) {
const chatMessageMediumGroup = new VisibleMessage(chatMessageParams);
const closedGroupVisibleMessage = new ClosedGroupVisibleMessage({
chatMessage: chatMessageMediumGroup,
@ -751,10 +725,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return;
}
if (this.isClosedGroup()) {
throw new Error('Legacy group are not supported anymore. You need to recreate this group.');
}
throw new TypeError(`Invalid conversation type: '${this.get('type')}'`);
} catch (e) {
await message.saveErrors(e);
@ -856,7 +826,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return;
}
if (this.isMediumGroup()) {
if (this.isClosedGroup()) {
const chatMessageMediumGroup = new VisibleMessage(chatMessageParams);
const closedGroupVisibleMessage = new ClosedGroupVisibleMessage({
chatMessage: chatMessageMediumGroup,
@ -876,10 +846,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return;
}
if (this.isClosedGroup()) {
throw new Error('Legacy group are not supported anymore. You need to recreate this group.');
}
throw new TypeError(`Invalid conversation type: '${this.get('type')}'`);
} catch (e) {
window.log.error(`Reaction job failed id:${reaction.id} error:`, e);
@ -1470,7 +1436,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return false;
}
const groupModerators = this.getGroupModerators();
const groupModerators = getModeratorsOutsideRedux(this.id as string);
return Array.isArray(groupModerators) && groupModerators.includes(pubKey);
}
@ -2154,11 +2120,12 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
uniq(localModsOrAdmins)
);
const moderatorsOrAdminsChanged =
type === 'admins'
? await this.updateGroupAdmins(replacedWithOurRealSessionId, false)
: await this.updateGroupModerators(replacedWithOurRealSessionId, false);
return moderatorsOrAdminsChanged;
if (type === 'admins') {
return await this.updateGroupAdmins(replacedWithOurRealSessionId, false);
}
ReduxSogsRoomInfos.setModeratorsOutsideRedux(this.id, replacedWithOurRealSessionId);
return false;
}
return false;
}

@ -55,10 +55,7 @@ export interface ConversationAttributes {
// 0 means inactive (undefined and null too but we try to get rid of them and only have 0 = inactive)
active_at: number;
zombies: Array<string>; // only used for closed groups. Zombies are users which left but not yet removed by the admin TODO to remove
left: boolean;
lastMessageStatus: LastMessageStatusType;
/**
* lastMessage is actually just a preview of the last message text, shortened to 60 chars.
* This is to avoid filling the redux store with a huge last message when it's only used in the
@ -66,24 +63,26 @@ export interface ConversationAttributes {
* The shortening is made in sql.ts directly.
*/
lastMessage: string | null;
lastJoinedTimestamp: number; // ClosedGroup: last time we were added to this group
groupModerators: Array<string>; // for sogs only, this is the moderators in that room.
isKickedFromGroup: boolean;
is_medium_group: boolean;
avatarImageId?: number; // SOGS ONLY: avatar imageID is currently used only for sogs. It's the fileID of the image uploaded and set as the sogs avatar
left: boolean; // GROUPS ONLY: if we left the group (communities are removed right away so it not relevant to communities)
isKickedFromGroup: boolean; // GROUPS ONLY: if we got kicked from the group (communities just stop polling and a message sent get rejected, so not relevant to communities)
avatarInProfile?: string; // this is the avatar path locally once downloaded and stored in the application attachments folder
avatarImageId?: number; // avatar imageID is currently used only for sogs. It's the fileID of the image uploaded and set as the sogs avatar
isTrustedForAttachmentDownload: boolean;
/** The community chat this conversation originated from (relevant to **blinded** message requests) */
conversationIdOrigin?: string;
conversationIdOrigin?: string; // Blinded message requests ONLY: The community from which this conversation originated from
// TODO those two items are only used for legacy closed groups and will be removed when we get rid of the legacy closed groups support
lastJoinedTimestamp: number; // ClosedGroup: last time we were added to this group // TODO to remove after legacy closed group are dropped
zombies: Array<string>; // only used for closed groups. Zombies are users which left but not yet removed by the admin // TODO to remove after legacy closed group are dropped
// ===========================================================================
// All of the items below are duplicated one way or the other with libsession.
// It would be nice to at some point be able to only rely on libsession dumps
// for those so there is no need to keep them in sync.
// for those so there is no need to keep them in sync, but just have them in the dumps
displayNameInProfile?: string; // no matter the type of conversation, this is the real name as set by the user/name of the open or closed group
nickname?: string; // this is the name WE gave to that user (only applicable to private chats, not closed group neither opengroups)
@ -92,18 +91,16 @@ export interface ConversationAttributes {
avatarPointer?: string; // this is the url of the avatar on the file server v2. we use this to detect if we need to redownload the avatar from someone (not used for opengroups)
expireTimer: number;
members: Array<string>; // members are all members for this group. zombies excluded
groupAdmins: Array<string>; // for sogs and closed group: the admins of that group.
members: Array<string>; // groups only members are all members for this group. zombies excluded (not used for communities)
groupAdmins: Array<string>; // for sogs and closed group: the unique admins of that group
isPinned: boolean;
isApproved: boolean;
didApproveMe: boolean;
// Force the conversation as unread even if all the messages are read. Used to highlight a conversation the user wants to check again later, synced.
markedAsUnread: boolean;
isApproved: boolean; // if we sent a message request or sent a message to this contact, we approve them. If isApproved & didApproveMe, a message request becomes a contact
didApproveMe: boolean; // if our message request was approved already (or they've sent us a message request/message themselves). If isApproved & didApproveMe, a message request becomes a contact
// hides a conversation, but keep it the history and nicknames, etc.
hidden: boolean;
markedAsUnread: boolean; // Force the conversation as unread even if all the messages are read. Used to highlight a conversation the user wants to check again later, synced.
hidden: boolean; // hides a conversation, but keep it the history and nicknames, etc. Currently only supported for contacts
}
/**
@ -133,7 +130,6 @@ export const fillConvoAttributesWithDefaults = (
isPinned: false,
isApproved: false,
didApproveMe: false,
is_medium_group: false,
isKickedFromGroup: false,
left: false,
hidden: true,

@ -943,12 +943,12 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
);
}
// Here, the convo is neither an open group, a private convo or ourself. It can only be a medium group.
// For a medium group, retry send only means trigger a send again to all recipients
// Here, the convo is neither an open group, a private convo or ourself. It can only be a closed group.
// For a closed group, retry send only means trigger a send again to all recipients
// as they are all polling from the same group swarm pubkey
if (!conversation.isMediumGroup()) {
if (!conversation.isClosedGroup()) {
throw new Error(
'We should only end up with a medium group here. Anything else is an error'
'We should only end up with a closed group here. Anything else is an error'
);
}

@ -48,14 +48,12 @@ export function toSqliteBoolean(val: boolean): number {
// this is used to make sure when storing something in the database you remember to add the wrapping for it in formatRowOfConversation
const allowedKeysFormatRowOfConversation = [
'groupAdmins',
'groupModerators',
'members',
'zombies',
'isTrustedForAttachmentDownload',
'isPinned',
'isApproved',
'didApproveMe',
'is_medium_group',
'mentionedUs',
'isKickedFromGroup',
'left',
@ -115,10 +113,6 @@ export function formatRowOfConversation(
row.groupAdmins?.length && row.groupAdmins.length > minLengthNoParsing
? jsonToArray(row.groupAdmins)
: [];
convo.groupModerators =
row.groupModerators?.length && row.groupModerators.length > minLengthNoParsing
? jsonToArray(row.groupModerators)
: [];
convo.members =
row.members?.length && row.members.length > minLengthNoParsing ? jsonToArray(row.members) : [];
@ -130,7 +124,6 @@ export function formatRowOfConversation(
convo.isPinned = Boolean(convo.isPinned);
convo.isApproved = Boolean(convo.isApproved);
convo.didApproveMe = Boolean(convo.didApproveMe);
convo.is_medium_group = Boolean(convo.is_medium_group);
convo.isKickedFromGroup = Boolean(convo.isKickedFromGroup);
convo.left = Boolean(convo.left);
convo.markedAsUnread = Boolean(convo.markedAsUnread);
@ -173,14 +166,12 @@ export function formatRowOfConversation(
const allowedKeysOfConversationAttributes = [
'groupAdmins',
'groupModerators',
'members',
'zombies',
'isTrustedForAttachmentDownload',
'isPinned',
'isApproved',
'didApproveMe',
'is_medium_group',
'isKickedFromGroup',
'left',
'lastMessage',

@ -1397,6 +1397,9 @@ function updateToSessionSchemaVersion30(currentVersion: number, db: BetterSqlite
ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN writeCapability;
ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN uploadCapability;
ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN subscriberCount;
ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN is_medium_group;
ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN groupModerators;
`);
// mark every "active" private chats as not hidden
db.prepare(

@ -431,9 +431,7 @@ function saveConversation(data: ConversationAttributes): SaveConversationReturn
lastMessage,
lastJoinedTimestamp,
groupAdmins,
groupModerators,
isKickedFromGroup,
is_medium_group,
avatarPointer,
avatarImageId,
triggerNotificationsFor,
@ -483,11 +481,7 @@ function saveConversation(data: ConversationAttributes): SaveConversationReturn
lastJoinedTimestamp,
groupAdmins: groupAdmins && groupAdmins.length ? arrayStrToJson(groupAdmins) : '[]',
groupModerators:
groupModerators && groupModerators.length ? arrayStrToJson(groupModerators) : '[]',
isKickedFromGroup: toSqliteBoolean(isKickedFromGroup),
is_medium_group: toSqliteBoolean(is_medium_group),
avatarPointer,
avatarImageId,
triggerNotificationsFor,

@ -392,7 +392,7 @@ async function handleClosedGroupEncryptionKeyPair(
await removeFromCache(envelope);
return;
}
if (!groupConvo.isMediumGroup()) {
if (!groupConvo.isClosedGroup()) {
window?.log?.warn(
`Ignoring closed group encryption key pair for nonexistent medium group. ${groupPublicKey}`
);

@ -477,10 +477,10 @@ async function handleConvoInfoVolatileUpdate(
const foundConvo = getConversationController().get(fromWrapper.pubkeyHex);
// TODO should we create the conversation if the conversation does not exist locally? Or assume that it should be coming from a contacts update?
if (foundConvo) {
// this should mark all the messages sent before fromWrapper.lastRead as read and update the unreadCount
console.warn(
`fromWrapper from getAll1o1: ${fromWrapper.pubkeyHex}: ${fromWrapper.unread}`
);
// this should mark all the messages sent before fromWrapper.lastRead as read and update the unreadCount
await foundConvo.markReadFromConfigMessage(fromWrapper.lastRead);
// this commits to the DB, if needed
await foundConvo.markAsUnread(fromWrapper.unread, true);

@ -53,7 +53,7 @@ async function decryptForClosedGroup(envelope: EnvelopePlus) {
window?.log?.info('received closed group message');
try {
const hexEncodedGroupPublicKey = envelope.source;
if (!GroupUtils.isMediumGroup(PubKey.cast(hexEncodedGroupPublicKey))) {
if (!GroupUtils.isClosedGroup(PubKey.cast(hexEncodedGroupPublicKey))) {
window?.log?.warn('received medium group message but not for an existing medium group');
throw new Error('Invalid group public key'); // invalidGroupPublicKey
}

@ -454,10 +454,7 @@ export class SwarmPolling {
const closedGroupsOnly = convos.filter(
(c: ConversationModel) =>
(c.isMediumGroup() || PubKey.isClosedGroupV3(c.id)) &&
!c.isBlocked() &&
!c.get('isKickedFromGroup') &&
!c.get('left')
c.isClosedGroup() && !c.isBlocked() && !c.get('isKickedFromGroup') && !c.get('left')
);
closedGroupsOnly.forEach((c: any) => {

@ -145,14 +145,6 @@ export class ConversationController {
return conversation.getContactProfileNameOrShortenedPubKey();
}
public isMediumGroup(hexEncodedGroupPublicKey: string): boolean {
const convo = this.conversations.get(hexEncodedGroupPublicKey);
if (convo) {
return !!convo.isMediumGroup();
}
return false;
}
public async getOrCreateAndWait(
id: string | PubKey,
type: ConversationTypeEnum

@ -73,10 +73,6 @@ export async function initiateClosedGroupUpdate(
isGroupV3 ? ConversationTypeEnum.GROUPV3 : ConversationTypeEnum.GROUP
);
if (!convo.isMediumGroup()) {
throw new Error('Legacy group are not supported anymore.');
}
// do not give an admins field here. We don't want to be able to update admins and
// updateOrCreateClosedGroup() will update them if given the choice.
const groupDetails: GroupInfo = {
@ -227,7 +223,6 @@ export async function updateOrCreateClosedGroup(details: GroupInfo) {
| 'type'
| 'members'
| 'displayNameInProfile'
| 'is_medium_group'
| 'active_at'
| 'left'
| 'lastJoinedTimestamp'
@ -236,7 +231,6 @@ export async function updateOrCreateClosedGroup(details: GroupInfo) {
displayNameInProfile: details.name,
members: details.members,
type: ConversationTypeEnum.GROUP,
is_medium_group: true,
active_at: details.activeAt ? details.activeAt : 0,
left: details.activeAt ? false : true,
lastJoinedTimestamp: details.activeAt && weWereJustAdded ? Date.now() : details.activeAt || 0,
@ -246,7 +240,7 @@ export async function updateOrCreateClosedGroup(details: GroupInfo) {
conversation.set(updates);
const isBlocked = details.blocked || false;
if (conversation.isClosedGroup() || conversation.isMediumGroup()) {
if (conversation.isClosedGroup()) {
await BlockedNumberController.setBlocked(conversation.id as string, isBlocked);
}
@ -272,7 +266,7 @@ export async function updateOrCreateClosedGroup(details: GroupInfo) {
export async function leaveClosedGroup(groupId: string) {
const convo = getConversationController().get(groupId);
if (!convo || !convo.isMediumGroup()) {
if (!convo || !convo.isClosedGroup()) {
window?.log?.error('Cannot leave non-existing group');
return;
}
@ -296,7 +290,7 @@ export async function leaveClosedGroup(groupId: string) {
admins = convo.get('groupAdmins') || [];
}
convo.set({ members });
convo.set({ groupAdmins: admins });
await convo.updateGroupAdmins(admins, false);
await convo.commit();
const source = UserUtils.getOurPubKeyStrFromCache();
@ -468,7 +462,7 @@ async function generateAndSendNewEncryptionKeyPair(
);
return;
}
if (!groupConvo.isMediumGroup()) {
if (!groupConvo.isClosedGroup()) {
window?.log?.warn(
'generateAndSendNewEncryptionKeyPair: conversation not a closed group',
groupPublicKey

@ -13,14 +13,14 @@ export function getGroupMembers(groupId: PubKey): Array<PubKey> {
return groupMembers.map(PubKey.cast);
}
export function isMediumGroup(groupId: PubKey): boolean {
export function isClosedGroup(groupId: PubKey): boolean {
const conversation = getConversationController().get(groupId.key);
if (!conversation) {
return false;
}
return Boolean(conversation.isMediumGroup());
return Boolean(conversation.isClosedGroup());
}
export function encodeGroupPubKeyFromHex(hexGroupPublicKey: string | PubKey) {

@ -189,7 +189,7 @@ async function refreshConvoVolatileCached(
} else if (convoId.startsWith('05')) {
const fromWrapper = await ConvoInfoVolatileWrapperActions.get1o1(convoId);
console.warn(
`refreshMappedValues from get1o1 ${fromWrapper?.pubkeyHex} : ${fromWrapper?.unread}`
`refreshConvoVolatileCached from get1o1 ${fromWrapper?.pubkeyHex} : ${fromWrapper?.unread}`
);
if (fromWrapper) {
mapped1o1WrapperValues.set(convoId, fromWrapper);

@ -138,7 +138,7 @@ const getValidClosedGroups = async (convos: Array<ConversationModel>) => {
const closedGroupModels = convos.filter(
c =>
!!c.get('active_at') &&
c.isMediumGroup() &&
c.isClosedGroup() &&
c.get('members')?.includes(ourPubKey) &&
!c.get('left') &&
!c.get('isKickedFromGroup') &&

@ -253,7 +253,6 @@ export interface ReduxConversationType {
left?: boolean;
avatarPath?: string | null; // absolute filepath to the avatar
groupAdmins?: Array<string>; // admins for closed groups and admins for open groups
groupModerators?: Array<string>; // only for opengroups: moderators
members?: Array<string>; // members for closed groups only
zombies?: Array<string>; // members for closed groups only

@ -1,8 +1,15 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { sortBy, uniq } from 'lodash';
import {
getCanWriteOutsideRedux,
getCurrentSubscriberCountOutsideRedux,
getModeratorsOutsideRedux,
} from '../selectors/sogsRoomInfo';
type RoomInfo = {
canWrite: boolean;
subscriberCount: number;
moderators: Array<string>;
};
export type SogsRoomInfoState = {
@ -15,7 +22,7 @@ export const initialSogsRoomInfoState: SogsRoomInfoState = {
function addEmptyEntryIfNeeded(state: any, convoId: string) {
if (!state.rooms[convoId]) {
state.rooms[convoId] = { canWrite: true, subscriberCount: 0 };
state.rooms[convoId] = { canWrite: true, subscriberCount: 0, moderators: [] };
}
}
@ -23,6 +30,9 @@ function addEmptyEntryIfNeeded(state: any, convoId: string) {
* This slice is the one holding the memory-only infos of sogs room. This includes
* - writeCapability
* - subscriberCount
* - moderators
*
* Note: moderators are almost never used for sogs. We mostly rely on admins, which are tracked through the conversationModel.groupAdmins attributes (and saved to DB)
*/
const sogsRoomInfosSlice = createSlice({
name: 'sogsRoomInfos',
@ -39,24 +49,58 @@ const sogsRoomInfosSlice = createSlice({
addEmptyEntryIfNeeded(state, action.payload.convoId);
state.rooms[action.payload.convoId].canWrite = !!action.payload.canWrite;
return state;
},
setModerators(state, action: PayloadAction<{ convoId: string; moderators: Array<string> }>) {
addEmptyEntryIfNeeded(state, action.payload.convoId);
state.rooms[action.payload.convoId].moderators = sortBy(uniq(action.payload.moderators));
return state;
},
},
});
const { actions, reducer } = sogsRoomInfosSlice;
const { setSubscriberCount, setCanWrite } = actions;
const { setSubscriberCount, setCanWrite, setModerators } = actions;
export const ReduxSogsRoomInfos = {
setSubscriberCountOutsideRedux,
setCanWriteOutsideRedux,
setModeratorsOutsideRedux,
sogsRoomInfoReducer: reducer,
};
function setSubscriberCountOutsideRedux(convoId: string, subscriberCount: number) {
if (subscriberCount === getCurrentSubscriberCountOutsideRedux(convoId)) {
return;
}
window.inboxStore?.dispatch(setSubscriberCount({ convoId, subscriberCount }));
}
function setCanWriteOutsideRedux(convoId: string, canWrite: boolean) {
if (getCanWriteOutsideRedux(convoId) === canWrite) {
return;
}
window.inboxStore?.dispatch(setCanWrite({ convoId, canWrite }));
}
/**
*
* @param convoId the convoId of the room to set the moderators
* @param moderators the updated list of moderators
* Note: if we are a moderator that room and the room is blinded, this update needs to contain our unblinded pubkey, NOT the blinded one
*/
function setModeratorsOutsideRedux(convoId: string, moderators: Array<string>) {
const currentMods = getModeratorsOutsideRedux(convoId);
if (sortBy(uniq(currentMods)) === sortBy(uniq(moderators))) {
return;
}
window.inboxStore?.dispatch(
setModerators({
convoId,
moderators,
})
);
return undefined;
}

@ -37,7 +37,7 @@ import { ConversationTypeEnum, isOpenOrClosedGroup } from '../../models/conversa
import { MessageReactsSelectorProps } from '../../components/conversation/message/message-content/MessageReactions';
import { filter, isEmpty, pick, sortBy } from 'lodash';
import { getCanWrite, getSubscriberCount } from './sogsRoomInfo';
import { getCanWrite, getModeratorsOutsideRedux, getSubscriberCount } from './sogsRoomInfo';
export const getConversations = (state: StateType): ConversationsStateType => state.conversations;
@ -808,14 +808,14 @@ export const isFirstUnreadMessageIdAbove = createSelector(
const getMessageId = (_whatever: any, id: string) => id;
// tslint:disable: cyclomatic-complexity
export const getMessagePropsByMessageId = createSelector(
getConversations,
getSortedMessagesOfSelectedConversation,
getConversationLookup,
getMessageId,
// tslint:disable-next-line: cyclomatic-complexity
(
_convoState,
messages: Array<SortedMessageModelProps>,
conversations,
id
@ -829,6 +829,7 @@ export const getMessagePropsByMessageId = createSelector(
}
const sender = foundMessageProps?.propsForMessage?.sender;
// foundMessageConversation is the conversation this message is
const foundMessageConversation = conversations[foundMessageProps.propsForMessage.convoId];
if (!foundMessageConversation || !sender) {
return undefined;
@ -846,8 +847,9 @@ export const getMessagePropsByMessageId = createSelector(
const groupAdmins = (isGroup && foundMessageConversation.groupAdmins) || [];
const weAreAdmin = groupAdmins.includes(ourPubkey) || false;
const groupModerators = (isGroup && foundMessageConversation.groupModerators) || [];
const weAreModerator = groupModerators.includes(ourPubkey) || false;
const weAreModerator =
(isPublic && getModeratorsOutsideRedux(foundMessageConversation.id).includes(ourPubkey)) ||
false;
// A message is deletable if
// either we sent it,
// or the convo is not a public one (in this case, we will only be able to delete for us)

@ -29,3 +29,10 @@ export const getOverlayMode = createSelector(
getSection,
(state: SectionStateType): OverlayMode | undefined => state.overlayMode
);
export const getIsMessageRequestOverlayShown = (state: StateType) => {
const focusedSection = getFocusedSection(state);
const overlayMode = getOverlayMode(state);
return focusedSection === SectionType.Message && overlayMode === 'message-requests';
};

@ -1,4 +1,4 @@
import { isNil } from 'lodash';
import { isEmpty, isNil } from 'lodash';
import { SogsRoomInfoState } from '../ducks/sogsRoomInfo';
import { StateType } from '../reducer';
@ -24,13 +24,33 @@ export function getSubscriberCount(state: StateType, selectedConvo?: string): nu
return isNil(subscriberCount) ? 0 : subscriberCount;
}
export function getSubscriberCountOutsideRedux(convoId: string) {
export function getModerators(state: StateType, selectedConvo?: string): Array<string> {
if (!selectedConvo) {
return [];
}
const moderators = getSogsRoomInfoState(state).rooms[selectedConvo]?.moderators;
return isEmpty(moderators) ? [] : moderators;
}
export function getSubscriberCountOutsideRedux(convoId: string): number {
const state = window.inboxStore?.getState();
return state ? getSubscriberCount(state, convoId) : 0;
}
export function getCanWriteOutsideRedux(convoId: string) {
export function getCanWriteOutsideRedux(convoId: string): boolean {
const state = window.inboxStore?.getState();
return state ? getCanWrite(state, convoId) : false;
}
export function getModeratorsOutsideRedux(convoId: string): Array<string> {
const state = window.inboxStore?.getState();
return state ? getModerators(state, convoId) : [];
}
export const getCurrentSubscriberCountOutsideRedux = (convoId?: string): number | undefined => {
const state = window.inboxStore?.getState();
return getSubscriberCount(state, convoId);
};

@ -229,40 +229,6 @@ describe('fillConvoAttributesWithDefaults', () => {
});
});
describe('is_medium_group', () => {
it('initialize is_medium_group if not given', () => {
expect(fillConvoAttributesWithDefaults({} as ConversationAttributes)).to.have.deep.property(
'is_medium_group',
false
);
});
it('do not override is_medium_group if given', () => {
expect(
fillConvoAttributesWithDefaults({
is_medium_group: true,
} as ConversationAttributes)
).to.have.deep.property('is_medium_group', true);
});
});
// describe('mentionedUs', () => {
// it('initialize mentionedUs if not given', () => {
// expect(fillConvoAttributesWithDefaults({} as ConversationAttributes)).to.have.deep.property(
// 'mentionedUs',
// false
// );
// });
// it('do not override mentionedUs if given', () => {
// expect(
// fillConvoAttributesWithDefaults({
// mentionedUs: true,
// } as ConversationAttributes)
// ).to.have.deep.property('mentionedUs', true);
// });
// });
describe('isKickedFromGroup', () => {
it('initialize isKickedFromGroup if not given', () => {
expect(fillConvoAttributesWithDefaults({} as ConversationAttributes)).to.have.deep.property(

@ -101,27 +101,6 @@ describe('formatRowOfConversation', () => {
});
});
describe('is_medium_group', () => {
it('initialize is_medium_group if they are not given', () => {
expect(formatRowOfConversation({}, 'test', 0, false)).to.have.deep.property(
'is_medium_group',
false
);
});
it('do not override is_medium_group if they are set in the row as integer: true', () => {
expect(
formatRowOfConversation({ is_medium_group: 1 }, 'test', 0, false)
).to.have.deep.property('is_medium_group', true);
});
it('do not override is_medium_group if they are set in the row as integer: false', () => {
expect(
formatRowOfConversation({ is_medium_group: 0 }, 'test', 0, false)
).to.have.deep.property('is_medium_group', false);
});
});
describe('mentionedUs', () => {
it('initialize mentionedUs if they are not given', () => {
expect(formatRowOfConversation({}, 'test', 0, false)).to.have.deep.property(

@ -34,7 +34,6 @@ describe('state/selectors/conversations', () => {
avatarPath: '',
groupAdmins: [],
groupModerators: [],
lastMessage: undefined,
members: [],
expireTimer: 0,
@ -59,10 +58,8 @@ describe('state/selectors/conversations', () => {
weAreAdmin: false,
isGroup: false,
isPrivate: false,
avatarPath: '',
groupAdmins: [],
groupModerators: [],
lastMessage: undefined,
members: [],
expireTimer: 0,
@ -87,10 +84,8 @@ describe('state/selectors/conversations', () => {
weAreAdmin: false,
isGroup: false,
isPrivate: false,
avatarPath: '',
groupAdmins: [],
groupModerators: [],
lastMessage: undefined,
members: [],
expireTimer: 0,
@ -118,7 +113,6 @@ describe('state/selectors/conversations', () => {
avatarPath: '',
groupAdmins: [],
groupModerators: [],
expireTimer: 0,
lastMessage: undefined,
members: [],
@ -147,7 +141,6 @@ describe('state/selectors/conversations', () => {
avatarPath: '',
groupAdmins: [],
groupModerators: [],
lastMessage: undefined,
members: [],
isPinned: false,
@ -191,7 +184,6 @@ describe('state/selectors/conversations', () => {
avatarPath: '',
groupAdmins: [],
groupModerators: [],
lastMessage: undefined,
members: [],
isPinned: false,
@ -220,7 +212,6 @@ describe('state/selectors/conversations', () => {
avatarPath: '',
groupAdmins: [],
groupModerators: [],
lastMessage: undefined,
members: [],
isPinned: false,
@ -249,7 +240,6 @@ describe('state/selectors/conversations', () => {
avatarPath: '',
groupAdmins: [],
groupModerators: [],
lastMessage: undefined,
members: [],
isPinned: true,
@ -277,7 +267,6 @@ describe('state/selectors/conversations', () => {
avatarPath: '',
groupAdmins: [],
groupModerators: [],
lastMessage: undefined,
members: [],
isPinned: true,
@ -306,7 +295,6 @@ describe('state/selectors/conversations', () => {
avatarPath: '',
groupAdmins: [],
groupModerators: [],
lastMessage: undefined,
members: [],
isPinned: false,

@ -225,7 +225,7 @@ export async function deleteExternalFilesOfConversation(
const { avatarInProfile } = conversationAttributes;
if (isString(avatarInProfile)) {
if (isString(avatarInProfile) && avatarInProfile.length) {
await deleteOnDisk(avatarInProfile);
}
}

Loading…
Cancel
Save