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

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

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

@ -104,7 +104,7 @@ export class LeftPaneMessageSection extends React.Component<Props> {
const length = conversations.length; const length = conversations.length;
// Note: conversations is not a known prop for List, but it is required to ensure that // 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. // on startup and scroll.
return ( return (

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

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

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

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

@ -54,8 +54,6 @@ export const SearchResults = (props: SearchResultsProps) => {
{contactsAndGroups.map(contactOrGroup => ( {contactsAndGroups.map(contactOrGroup => (
<MemoConversationListItemWithDetails <MemoConversationListItemWithDetails
{...contactOrGroup} {...contactOrGroup}
mentionedUs={false}
isBlocked={false}
key={`search-result-convo-${contactOrGroup.id}`} 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 { useSelector } from 'react-redux';
import { ConversationModel } from '../models/conversation'; import { ConversationModel } from '../models/conversation';
import { PubKey } from '../session/types'; import { PubKey } from '../session/types';
import { UserUtils } from '../session/utils'; import { UserUtils } from '../session/utils';
import { StateType } from '../state/reducer'; import { StateType } from '../state/reducer';
import { getMessageReactsProps } from '../state/selectors/conversations'; import {
getMessageReactsProps,
getSelectedConversationKey,
} from '../state/selectors/conversations';
export function useAvatarPath(convoId: string | undefined) { export function useAvatarPath(convoId: string | undefined) {
const convoProps = useConversationPropsById(convoId); const convoProps = useConversationPropsById(convoId);
@ -202,13 +205,13 @@ export function useIsForcedUnreadWithoutUnreadMsg(conversationId?: string): bool
return convoProps?.isMarkedUnread || false; return convoProps?.isMarkedUnread || false;
} }
function useMentionedUsNoUnread(conversationId?: string) { function useMentionedUsUnread(conversationId?: string) {
const convoProps = useConversationPropsById(conversationId); const convoProps = useConversationPropsById(conversationId);
return convoProps?.mentionedUs || false; return convoProps?.mentionedUs || false;
} }
export function useMentionedUs(conversationId?: string): boolean { export function useMentionedUs(conversationId?: string): boolean {
const hasMentionedUs = useMentionedUsNoUnread(conversationId); const hasMentionedUs = useMentionedUsUnread(conversationId);
const hasUnread = useHasUnread(conversationId); const hasUnread = useHasUnread(conversationId);
return hasMentionedUs && hasUnread; return hasMentionedUs && hasUnread;
@ -217,3 +220,8 @@ export function useMentionedUs(conversationId?: string): boolean {
export function useIsTyping(conversationId?: string): boolean { export function useIsTyping(conversationId?: string): boolean {
return useConversationPropsById(conversationId)?.isTyping || false; 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) { export async function showUpdateGroupNameByConvoId(conversationId: string) {
const conversation = getConversationController().get(conversationId); 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 // make sure all the members' convo exists so we can add or remove them
await Promise.all( await Promise.all(
conversation conversation
@ -170,7 +170,7 @@ export async function showUpdateGroupNameByConvoId(conversationId: string) {
export async function showUpdateGroupMembersByConvoId(conversationId: string) { export async function showUpdateGroupMembersByConvoId(conversationId: string) {
const conversation = getConversationController().get(conversationId); 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 // make sure all the members' convo exists so we can add or remove them
await Promise.all( await Promise.all(
conversation conversation
@ -192,7 +192,7 @@ export function showLeaveGroupByConvoId(conversationId: string) {
const message = window.i18n('leaveGroupConfirmation'); const message = window.i18n('leaveGroupConfirmation');
const ourPK = UserUtils.getOurPubKeyStrFromCache(); const ourPK = UserUtils.getOurPubKeyStrFromCache();
const isAdmin = (conversation.get('groupAdmins') || []).includes(ourPK); 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 this is not a closed group, or we are not admin, we can just show a confirmation dialog
if (!isClosedGroup || (isClosedGroup && !isAdmin)) { if (!isClosedGroup || (isClosedGroup && !isAdmin)) {

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

@ -111,6 +111,7 @@ import { SessionUtilUserProfile } from '../session/utils/libsession/libsession_u
import { ReduxSogsRoomInfos } from '../state/ducks/sogsRoomInfo'; import { ReduxSogsRoomInfos } from '../state/ducks/sogsRoomInfo';
import { import {
getCanWriteOutsideRedux, getCanWriteOutsideRedux,
getModeratorsOutsideRedux,
getSubscriberCountOutsideRedux, getSubscriberCountOutsideRedux,
} from '../state/selectors/sogsRoomInfo'; } from '../state/selectors/sogsRoomInfo';
@ -120,6 +121,11 @@ type InMemoryConvoInfos = {
lastReadTimestampMessage: number | null; 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(); const inMemoryConvoInfos: Map<string, InMemoryConvoInfos> = new Map();
export class ConversationModel extends Backbone.Model<ConversationAttributes> { export class ConversationModel extends Backbone.Model<ConversationAttributes> {
@ -222,10 +228,10 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
public isOpenGroupV2(): boolean { public isOpenGroupV2(): boolean {
return OpenGroupUtils.isOpenGroupV2(this.id); return OpenGroupUtils.isOpenGroupV2(this.id);
} }
public isClosedGroup() { public isClosedGroup(): boolean {
return ( return Boolean(
(this.get('type') === ConversationTypeEnum.GROUP && !this.isPublic()) || (this.get('type') === ConversationTypeEnum.GROUP && this.id.startsWith('05')) ||
this.get('type') === ConversationTypeEnum.GROUPV3 (this.get('type') === ConversationTypeEnum.GROUPV3 && this.id.startsWith('03'))
); );
} }
public isPrivate() { public isPrivate() {
@ -242,17 +248,13 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return false; return false;
} }
if (this.isPrivate() || this.isClosedGroup() || this.isMediumGroup()) { if (this.isPrivate() || this.isClosedGroup()) {
return BlockedNumberController.isBlocked(this.id); return BlockedNumberController.isBlocked(this.id);
} }
return false; return false;
} }
public isMediumGroup() {
return this.get('is_medium_group');
}
/** /**
* Returns true if this conversation is active * 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). * 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 : []; 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 // tslint:disable-next-line: cyclomatic-complexity max-func-body-length
public getConversationModelProps(): ReduxConversationType { public getConversationModelProps(): ReduxConversationType {
const groupModerators = this.getGroupModerators();
const isPublic = this.isPublic(); const isPublic = this.isPublic();
const zombies = this.isClosedGroup() ? this.get('zombies') : []; const zombies = this.isClosedGroup() ? this.get('zombies') : [];
@ -429,10 +418,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
if (foundCommunity.priority > 0) { if (foundCommunity.priority > 0) {
toRet.isPinned = true; // TODO priority also handles sorting toRet.isPinned = true; // TODO priority also handles sorting
} }
if (groupModerators?.length) {
toRet.groupModerators = uniq(groupModerators);
}
} }
if (foundVolatileInfo) { if (foundVolatileInfo) {
@ -481,6 +466,12 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return toRet; 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) { public async updateGroupAdmins(groupAdmins: Array<string>, shouldCommit: boolean) {
const sortedExistingAdmins = uniq(sortBy(this.getGroupAdmins())); const sortedExistingAdmins = uniq(sortBy(this.getGroupAdmins()));
const sortedNewAdmins = uniq(sortBy(groupAdmins)); const sortedNewAdmins = uniq(sortBy(groupAdmins));
@ -495,23 +486,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return true; 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 * 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; return;
} }
if (this.isMediumGroup()) { if (this.isClosedGroup()) {
const chatMessageMediumGroup = new VisibleMessage(chatMessageParams); const chatMessageMediumGroup = new VisibleMessage(chatMessageParams);
const closedGroupVisibleMessage = new ClosedGroupVisibleMessage({ const closedGroupVisibleMessage = new ClosedGroupVisibleMessage({
chatMessage: chatMessageMediumGroup, chatMessage: chatMessageMediumGroup,
@ -751,10 +725,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return; 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')}'`); throw new TypeError(`Invalid conversation type: '${this.get('type')}'`);
} catch (e) { } catch (e) {
await message.saveErrors(e); await message.saveErrors(e);
@ -856,7 +826,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return; return;
} }
if (this.isMediumGroup()) { if (this.isClosedGroup()) {
const chatMessageMediumGroup = new VisibleMessage(chatMessageParams); const chatMessageMediumGroup = new VisibleMessage(chatMessageParams);
const closedGroupVisibleMessage = new ClosedGroupVisibleMessage({ const closedGroupVisibleMessage = new ClosedGroupVisibleMessage({
chatMessage: chatMessageMediumGroup, chatMessage: chatMessageMediumGroup,
@ -876,10 +846,6 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
return; 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')}'`); throw new TypeError(`Invalid conversation type: '${this.get('type')}'`);
} catch (e) { } catch (e) {
window.log.error(`Reaction job failed id:${reaction.id} error:`, 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; return false;
} }
const groupModerators = this.getGroupModerators(); const groupModerators = getModeratorsOutsideRedux(this.id as string);
return Array.isArray(groupModerators) && groupModerators.includes(pubKey); return Array.isArray(groupModerators) && groupModerators.includes(pubKey);
} }
@ -2154,11 +2120,12 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
uniq(localModsOrAdmins) uniq(localModsOrAdmins)
); );
const moderatorsOrAdminsChanged = if (type === 'admins') {
type === 'admins' return await this.updateGroupAdmins(replacedWithOurRealSessionId, false);
? await this.updateGroupAdmins(replacedWithOurRealSessionId, false) }
: await this.updateGroupModerators(replacedWithOurRealSessionId, false);
return moderatorsOrAdminsChanged; ReduxSogsRoomInfos.setModeratorsOutsideRedux(this.id, replacedWithOurRealSessionId);
return false;
} }
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) // 0 means inactive (undefined and null too but we try to get rid of them and only have 0 = inactive)
active_at: number; 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; lastMessageStatus: LastMessageStatusType;
/** /**
* lastMessage is actually just a preview of the last message text, shortened to 60 chars. * 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 * 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. * The shortening is made in sql.ts directly.
*/ */
lastMessage: string | null; 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 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; isTrustedForAttachmentDownload: boolean;
/** The community chat this conversation originated from (relevant to **blinded** message requests) */ conversationIdOrigin?: string; // Blinded message requests ONLY: The community from which this conversation originated from
conversationIdOrigin?: string;
// 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. // 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 // 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 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) 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) 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; expireTimer: number;
members: Array<string>; // members are all members for this group. zombies excluded 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 admins of that group. groupAdmins: Array<string>; // for sogs and closed group: the unique admins of that group
isPinned: boolean; 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. 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
markedAsUnread: boolean; 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. 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; 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, isPinned: false,
isApproved: false, isApproved: false,
didApproveMe: false, didApproveMe: false,
is_medium_group: false,
isKickedFromGroup: false, isKickedFromGroup: false,
left: false, left: false,
hidden: true, 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. // Here, the convo is neither an open group, a private convo or ourself. It can only be a closed group.
// For a medium group, retry send only means trigger a send again to all recipients // 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 // as they are all polling from the same group swarm pubkey
if (!conversation.isMediumGroup()) { if (!conversation.isClosedGroup()) {
throw new Error( 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 // this is used to make sure when storing something in the database you remember to add the wrapping for it in formatRowOfConversation
const allowedKeysFormatRowOfConversation = [ const allowedKeysFormatRowOfConversation = [
'groupAdmins', 'groupAdmins',
'groupModerators',
'members', 'members',
'zombies', 'zombies',
'isTrustedForAttachmentDownload', 'isTrustedForAttachmentDownload',
'isPinned', 'isPinned',
'isApproved', 'isApproved',
'didApproveMe', 'didApproveMe',
'is_medium_group',
'mentionedUs', 'mentionedUs',
'isKickedFromGroup', 'isKickedFromGroup',
'left', 'left',
@ -115,10 +113,6 @@ export function formatRowOfConversation(
row.groupAdmins?.length && row.groupAdmins.length > minLengthNoParsing row.groupAdmins?.length && row.groupAdmins.length > minLengthNoParsing
? jsonToArray(row.groupAdmins) ? jsonToArray(row.groupAdmins)
: []; : [];
convo.groupModerators =
row.groupModerators?.length && row.groupModerators.length > minLengthNoParsing
? jsonToArray(row.groupModerators)
: [];
convo.members = convo.members =
row.members?.length && row.members.length > minLengthNoParsing ? jsonToArray(row.members) : []; row.members?.length && row.members.length > minLengthNoParsing ? jsonToArray(row.members) : [];
@ -130,7 +124,6 @@ export function formatRowOfConversation(
convo.isPinned = Boolean(convo.isPinned); convo.isPinned = Boolean(convo.isPinned);
convo.isApproved = Boolean(convo.isApproved); convo.isApproved = Boolean(convo.isApproved);
convo.didApproveMe = Boolean(convo.didApproveMe); convo.didApproveMe = Boolean(convo.didApproveMe);
convo.is_medium_group = Boolean(convo.is_medium_group);
convo.isKickedFromGroup = Boolean(convo.isKickedFromGroup); convo.isKickedFromGroup = Boolean(convo.isKickedFromGroup);
convo.left = Boolean(convo.left); convo.left = Boolean(convo.left);
convo.markedAsUnread = Boolean(convo.markedAsUnread); convo.markedAsUnread = Boolean(convo.markedAsUnread);
@ -173,14 +166,12 @@ export function formatRowOfConversation(
const allowedKeysOfConversationAttributes = [ const allowedKeysOfConversationAttributes = [
'groupAdmins', 'groupAdmins',
'groupModerators',
'members', 'members',
'zombies', 'zombies',
'isTrustedForAttachmentDownload', 'isTrustedForAttachmentDownload',
'isPinned', 'isPinned',
'isApproved', 'isApproved',
'didApproveMe', 'didApproveMe',
'is_medium_group',
'isKickedFromGroup', 'isKickedFromGroup',
'left', 'left',
'lastMessage', 'lastMessage',

@ -1397,6 +1397,9 @@ function updateToSessionSchemaVersion30(currentVersion: number, db: BetterSqlite
ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN writeCapability; ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN writeCapability;
ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN uploadCapability; ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN uploadCapability;
ALTER TABLE ${CONVERSATIONS_TABLE} DROP COLUMN subscriberCount; 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 // mark every "active" private chats as not hidden
db.prepare( db.prepare(

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

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

@ -477,10 +477,10 @@ async function handleConvoInfoVolatileUpdate(
const foundConvo = getConversationController().get(fromWrapper.pubkeyHex); 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? // 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) { if (foundConvo) {
// this should mark all the messages sent before fromWrapper.lastRead as read and update the unreadCount
console.warn( console.warn(
`fromWrapper from getAll1o1: ${fromWrapper.pubkeyHex}: ${fromWrapper.unread}` `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); await foundConvo.markReadFromConfigMessage(fromWrapper.lastRead);
// this commits to the DB, if needed // this commits to the DB, if needed
await foundConvo.markAsUnread(fromWrapper.unread, true); await foundConvo.markAsUnread(fromWrapper.unread, true);

@ -53,7 +53,7 @@ async function decryptForClosedGroup(envelope: EnvelopePlus) {
window?.log?.info('received closed group message'); window?.log?.info('received closed group message');
try { try {
const hexEncodedGroupPublicKey = envelope.source; 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'); window?.log?.warn('received medium group message but not for an existing medium group');
throw new Error('Invalid group public key'); // invalidGroupPublicKey throw new Error('Invalid group public key'); // invalidGroupPublicKey
} }

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

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

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

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

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

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

@ -253,7 +253,6 @@ export interface ReduxConversationType {
left?: boolean; left?: boolean;
avatarPath?: string | null; // absolute filepath to the avatar avatarPath?: string | null; // absolute filepath to the avatar
groupAdmins?: Array<string>; // admins for closed groups and admins for open groups 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 members?: Array<string>; // members for closed groups only
zombies?: 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 { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { sortBy, uniq } from 'lodash';
import {
getCanWriteOutsideRedux,
getCurrentSubscriberCountOutsideRedux,
getModeratorsOutsideRedux,
} from '../selectors/sogsRoomInfo';
type RoomInfo = { type RoomInfo = {
canWrite: boolean; canWrite: boolean;
subscriberCount: number; subscriberCount: number;
moderators: Array<string>;
}; };
export type SogsRoomInfoState = { export type SogsRoomInfoState = {
@ -15,7 +22,7 @@ export const initialSogsRoomInfoState: SogsRoomInfoState = {
function addEmptyEntryIfNeeded(state: any, convoId: string) { function addEmptyEntryIfNeeded(state: any, convoId: string) {
if (!state.rooms[convoId]) { 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 * This slice is the one holding the memory-only infos of sogs room. This includes
* - writeCapability * - writeCapability
* - subscriberCount * - 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({ const sogsRoomInfosSlice = createSlice({
name: 'sogsRoomInfos', name: 'sogsRoomInfos',
@ -39,24 +49,58 @@ const sogsRoomInfosSlice = createSlice({
addEmptyEntryIfNeeded(state, action.payload.convoId); addEmptyEntryIfNeeded(state, action.payload.convoId);
state.rooms[action.payload.convoId].canWrite = !!action.payload.canWrite; 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; return state;
}, },
}, },
}); });
const { actions, reducer } = sogsRoomInfosSlice; const { actions, reducer } = sogsRoomInfosSlice;
const { setSubscriberCount, setCanWrite } = actions; const { setSubscriberCount, setCanWrite, setModerators } = actions;
export const ReduxSogsRoomInfos = { export const ReduxSogsRoomInfos = {
setSubscriberCountOutsideRedux, setSubscriberCountOutsideRedux,
setCanWriteOutsideRedux, setCanWriteOutsideRedux,
setModeratorsOutsideRedux,
sogsRoomInfoReducer: reducer, sogsRoomInfoReducer: reducer,
}; };
function setSubscriberCountOutsideRedux(convoId: string, subscriberCount: number) { function setSubscriberCountOutsideRedux(convoId: string, subscriberCount: number) {
if (subscriberCount === getCurrentSubscriberCountOutsideRedux(convoId)) {
return;
}
window.inboxStore?.dispatch(setSubscriberCount({ convoId, subscriberCount })); window.inboxStore?.dispatch(setSubscriberCount({ convoId, subscriberCount }));
} }
function setCanWriteOutsideRedux(convoId: string, canWrite: boolean) { function setCanWriteOutsideRedux(convoId: string, canWrite: boolean) {
if (getCanWriteOutsideRedux(convoId) === canWrite) {
return;
}
window.inboxStore?.dispatch(setCanWrite({ convoId, canWrite })); 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 { MessageReactsSelectorProps } from '../../components/conversation/message/message-content/MessageReactions';
import { filter, isEmpty, pick, sortBy } from 'lodash'; 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; export const getConversations = (state: StateType): ConversationsStateType => state.conversations;
@ -808,14 +808,14 @@ export const isFirstUnreadMessageIdAbove = createSelector(
const getMessageId = (_whatever: any, id: string) => id; const getMessageId = (_whatever: any, id: string) => id;
// tslint:disable: cyclomatic-complexity
export const getMessagePropsByMessageId = createSelector( export const getMessagePropsByMessageId = createSelector(
getConversations,
getSortedMessagesOfSelectedConversation, getSortedMessagesOfSelectedConversation,
getConversationLookup, getConversationLookup,
getMessageId, getMessageId,
// tslint:disable-next-line: cyclomatic-complexity
( (
_convoState,
messages: Array<SortedMessageModelProps>, messages: Array<SortedMessageModelProps>,
conversations, conversations,
id id
@ -829,6 +829,7 @@ export const getMessagePropsByMessageId = createSelector(
} }
const sender = foundMessageProps?.propsForMessage?.sender; const sender = foundMessageProps?.propsForMessage?.sender;
// foundMessageConversation is the conversation this message is
const foundMessageConversation = conversations[foundMessageProps.propsForMessage.convoId]; const foundMessageConversation = conversations[foundMessageProps.propsForMessage.convoId];
if (!foundMessageConversation || !sender) { if (!foundMessageConversation || !sender) {
return undefined; return undefined;
@ -846,8 +847,9 @@ export const getMessagePropsByMessageId = createSelector(
const groupAdmins = (isGroup && foundMessageConversation.groupAdmins) || []; const groupAdmins = (isGroup && foundMessageConversation.groupAdmins) || [];
const weAreAdmin = groupAdmins.includes(ourPubkey) || false; const weAreAdmin = groupAdmins.includes(ourPubkey) || false;
const groupModerators = (isGroup && foundMessageConversation.groupModerators) || []; const weAreModerator =
const weAreModerator = groupModerators.includes(ourPubkey) || false; (isPublic && getModeratorsOutsideRedux(foundMessageConversation.id).includes(ourPubkey)) ||
false;
// A message is deletable if // A message is deletable if
// either we sent it, // either we sent it,
// or the convo is not a public one (in this case, we will only be able to delete for us) // 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, getSection,
(state: SectionStateType): OverlayMode | undefined => state.overlayMode (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 { SogsRoomInfoState } from '../ducks/sogsRoomInfo';
import { StateType } from '../reducer'; import { StateType } from '../reducer';
@ -24,13 +24,33 @@ export function getSubscriberCount(state: StateType, selectedConvo?: string): nu
return isNil(subscriberCount) ? 0 : subscriberCount; 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(); const state = window.inboxStore?.getState();
return state ? getSubscriberCount(state, convoId) : 0; return state ? getSubscriberCount(state, convoId) : 0;
} }
export function getCanWriteOutsideRedux(convoId: string) { export function getCanWriteOutsideRedux(convoId: string): boolean {
const state = window.inboxStore?.getState(); const state = window.inboxStore?.getState();
return state ? getCanWrite(state, convoId) : false; 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', () => { describe('isKickedFromGroup', () => {
it('initialize isKickedFromGroup if not given', () => { it('initialize isKickedFromGroup if not given', () => {
expect(fillConvoAttributesWithDefaults({} as ConversationAttributes)).to.have.deep.property( 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', () => { describe('mentionedUs', () => {
it('initialize mentionedUs if they are not given', () => { it('initialize mentionedUs if they are not given', () => {
expect(formatRowOfConversation({}, 'test', 0, false)).to.have.deep.property( expect(formatRowOfConversation({}, 'test', 0, false)).to.have.deep.property(

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

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

Loading…
Cancel
Save