diff --git a/ts/components/conversation/message/message-content/MessageContextMenu.tsx b/ts/components/conversation/message/message-content/MessageContextMenu.tsx index d31bd7524..25375f5f2 100644 --- a/ts/components/conversation/message/message-content/MessageContextMenu.tsx +++ b/ts/components/conversation/message/message-content/MessageContextMenu.tsx @@ -61,7 +61,6 @@ export const MessageContextMenu = (props: Props) => { isDeletable, isDeletableForEveryone, isPublic, - isOpenGroupV2, weAreAdmin, isSenderAdmin, text, @@ -208,9 +207,7 @@ export const MessageContextMenu = (props: Props) => { ) : null} {weAreAdmin && isPublic ? {window.i18n('banUser')} : null} - {weAreAdmin && isOpenGroupV2 ? ( - {window.i18n('unbanUser')} - ) : null} + {weAreAdmin && isPublic ? {window.i18n('unbanUser')} : null} {weAreAdmin && isPublic && !isSenderAdmin ? ( {window.i18n('addAsModerator')} ) : null} diff --git a/ts/hooks/useParamSelector.ts b/ts/hooks/useParamSelector.ts index 5ae16b199..500fe2389 100644 --- a/ts/hooks/useParamSelector.ts +++ b/ts/hooks/useParamSelector.ts @@ -124,6 +124,11 @@ export function useWeAreAdmin(convoId?: string) { return Boolean(convoProps && convoProps.weAreAdmin); } +export function useWeAreModerator(convoId?: string) { + const convoProps = useConversationPropsById(convoId); + return Boolean(convoProps && (convoProps.weAreAdmin || convoProps.weAreModerator)); +} + export function useExpireTimer(convoId?: string) { const convoProps = useConversationPropsById(convoId); return convoProps && convoProps.expireTimer; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 2d23fffa3..34a5c4cd0 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -1,5 +1,19 @@ import Backbone from 'backbone'; -import _, { isArray, isEmpty, isNumber, isString } from 'lodash'; +import { + debounce, + defaults, + filter, + includes, + isArray, + isEmpty, + isEqual, + isNumber, + isString, + map, + sortBy, + throttle, + uniq, +} from 'lodash'; import { getMessageQueue } from '../session'; import { getConversationController } from '../session/conversations'; import { ClosedGroupVisibleMessage } from '../session/messages/outgoing/visibleMessage/ClosedGroupVisibleMessage'; @@ -99,15 +113,15 @@ export class ConversationModel extends Backbone.Model { this.initialPromise = Promise.resolve(); autoBind(this); - this.throttledBumpTyping = _.throttle(this.bumpTyping, 300); - this.updateLastMessage = _.throttle(this.bouncyUpdateLastMessage.bind(this), 1000, { + this.throttledBumpTyping = throttle(this.bumpTyping, 300); + this.updateLastMessage = throttle(this.bouncyUpdateLastMessage.bind(this), 1000, { trailing: true, leading: true, }); - this.throttledNotify = _.debounce(this.notify, 2000, { maxWait: 2000, trailing: true }); + this.throttledNotify = debounce(this.notify, 2000, { maxWait: 2000, trailing: true }); //start right away the function is called, and wait 1sec before calling it again - const markReadDebounced = _.debounce(this.markReadBouncy, 1000, { + const markReadDebounced = debounce(this.markReadBouncy, 1000, { leading: true, trailing: true, }); @@ -240,9 +254,22 @@ export class ConversationModel extends Backbone.Model { 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 { + const groupModerators = this.get('groupModerators') as Array | undefined; + + return this.isPublic() && groupModerators && groupModerators?.length > 0 ? groupModerators : []; + } + // tslint:disable-next-line: cyclomatic-complexity max-func-body-length public getConversationModelProps(): ReduxConversationType { const groupAdmins = this.getGroupAdmins(); + const groupModerators = this.getGroupModerators(); // tslint:disable-next-line: cyclomatic-complexity const isPublic = this.isPublic(); @@ -253,6 +280,7 @@ export class ConversationModel extends Backbone.Model { const isPrivate = this.isPrivate(); const isGroup = !isPrivate; const weAreAdmin = this.isAdmin(ourNumber); + const weAreModerator = this.isModerator(ourNumber); // only used for sogs const isMe = this.isMe(); const isTyping = !!this.typingTimer; const unreadCount = this.get('unreadCount') || undefined; @@ -290,6 +318,10 @@ export class ConversationModel extends Backbone.Model { toRet.weAreAdmin = true; } + if (weAreModerator) { + toRet.weAreModerator = true; + } + if (isMe) { toRet.isMe = true; } @@ -343,21 +375,29 @@ export class ConversationModel extends Backbone.Model { if (didApproveMe) { toRet.didApproveMe = didApproveMe; } + if (isApproved) { toRet.isApproved = isApproved; } + if (subscriberCount) { toRet.subscriberCount = subscriberCount; } + if (groupAdmins && groupAdmins.length) { - toRet.groupAdmins = _.uniq(groupAdmins); + toRet.groupAdmins = uniq(groupAdmins); } + + if (groupModerators && groupModerators.length) { + toRet.groupModerators = uniq(groupModerators); + } + if (members && members.length) { - toRet.members = _.uniq(members); + toRet.members = uniq(members); } if (zombies && zombies.length) { - toRet.zombies = _.uniq(zombies); + toRet.zombies = uniq(zombies); } if (expireTimer) { @@ -384,10 +424,10 @@ export class ConversationModel extends Backbone.Model { } public async updateGroupAdmins(groupAdmins: Array, shouldCommit: boolean) { - const existingAdmins = _.uniq(_.sortBy(this.getGroupAdmins())); - const newAdmins = _.uniq(_.sortBy(groupAdmins)); + const existingAdmins = uniq(sortBy(this.getGroupAdmins())); + const newAdmins = uniq(sortBy(groupAdmins)); - if (_.isEqual(existingAdmins, newAdmins)) { + if (isEqual(existingAdmins, newAdmins)) { return false; } this.set({ groupAdmins }); @@ -397,6 +437,23 @@ export class ConversationModel extends Backbone.Model { return true; } + public async updateGroupModerators(groupModerators: Array, 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; + } + public async onReadMessage(message: MessageModel, readAt: number) { // We mark as read everything older than this message - to clean up old stuff // still marked unread in the database. If the user generally doesn't read in @@ -819,7 +876,7 @@ export class ConversationModel extends Backbone.Model { const messageModel = await this.addSingleOutgoingMessage({ body, - quote: _.isEmpty(quote) ? undefined : quote, + quote: isEmpty(quote) ? undefined : quote, preview, attachments, sent_at: networkTimestamp, @@ -905,7 +962,7 @@ export class ConversationModel extends Backbone.Model { let expireTimer = providedExpireTimer; let source = providedSource; - _.defaults(options, { fromSync: false }); + defaults(options, { fromSync: false }); if (!expireTimer) { expireTimer = 0; @@ -1088,7 +1145,7 @@ export class ConversationModel extends Backbone.Model { } const options = providedOptions || {}; - _.defaults(options, { sendReadReceipts: true }); + defaults(options, { sendReadReceipts: true }); const conversationId = this.id; Notifications.clearByConversationID(conversationId); @@ -1125,7 +1182,7 @@ export class ConversationModel extends Backbone.Model { window.inboxStore?.dispatch(conversationActions.messagesChanged(allProps)); } // Some messages we're marking read are local notifications with no sender - read = _.filter(read, m => Boolean(m.sender)); + read = filter(read, m => Boolean(m.sender)); const realUnreadCount = await this.getUnreadCount(); if (read.length === 0) { const cachedUnreadCountOnConvo = this.get('unreadCount'); @@ -1165,7 +1222,7 @@ export class ConversationModel extends Backbone.Model { read = read.filter(item => !item.hasErrors); if (read.length && options.sendReadReceipts) { - const timestamps = _.map(read, 'timestamp').filter(t => !!t) as Array; + const timestamps = map(read, 'timestamp').filter(t => !!t) as Array; await this.sendReadReceiptsIfNeeded(timestamps); } } @@ -1233,7 +1290,7 @@ export class ConversationModel extends Backbone.Model { // if you change this behavior, double check all setSessionProfile calls (especially the one in EditProfileDialog) if (newProfile.avatarPath) { const originalAvatar = this.get('avatarInProfile'); - if (!_.isEqual(originalAvatar, newProfile.avatarPath)) { + if (!isEqual(originalAvatar, newProfile.avatarPath)) { this.set({ avatarInProfile: newProfile.avatarPath }); changes = true; } @@ -1306,6 +1363,22 @@ export class ConversationModel extends Backbone.Model { return Array.isArray(groupAdmins) && groupAdmins.includes(pubKey); } + /** + * Check if the provided pubkey is a moderator. + * Being a moderator only makes sense for a sogs as closed groups have their admin under the groupAdmins property + */ + public isModerator(pubKey?: string) { + if (!pubKey) { + throw new Error('isModerator() pubKey is falsy'); + } + if (!this.isPublic()) { + return false; + } + + const groupModerators = this.getGroupModerators(); + return Array.isArray(groupModerators) && groupModerators.includes(pubKey); + } + public async setIsPinned(value: boolean) { if (value !== this.isPinned()) { this.set({ @@ -1363,6 +1436,7 @@ export class ConversationModel extends Backbone.Model { * Saves the infos of that room directly on the conversation table. * This does not write anything to the db if no changes are detected */ + // tslint:disable-next-line: cyclomatic-complexity public async setPollInfo(infos?: { subscriberCount: number; read: boolean; @@ -1371,6 +1445,7 @@ export class ConversationModel extends Backbone.Model { details: { admins?: Array; image_id?: number; + moderators?: Array; }; }) { if (!infos || isEmpty(infos)) { @@ -1403,24 +1478,26 @@ export class ConversationModel extends Backbone.Model { } if (details.admins && isArray(details.admins)) { - const roomInfos = OpenGroupData.getV2OpenGroupRoom(this.id); - const ourBlindedPubkeyForThisSogs = - roomInfos && roomHasBlindEnabled(roomInfos) - ? await findCachedOurBlindedPubkeyOrLookItUp( - roomInfos?.serverPublicKey, - await getSodiumRenderer() - ) - : UserUtils.getOurPubKeyStrFromCache(); - const replacedWithOurRealSessionId = details.admins.map(m => - m === ourBlindedPubkeyForThisSogs ? UserUtils.getOurPubKeyStrFromCache() : m - ); - + const replacedWithOurRealSessionId = await this.replaceWithOurRealSessionId(details.admins); const adminChanged = await this.updateGroupAdmins(replacedWithOurRealSessionId, false); if (adminChanged) { hasChange = adminChanged; } } + if (details.moderators && isArray(details.moderators)) { + const replacedWithOurRealSessionId = await this.replaceWithOurRealSessionId( + details.moderators + ); + const moderatorsChanged = await this.updateGroupModerators( + replacedWithOurRealSessionId, + false + ); + if (moderatorsChanged) { + hasChange = moderatorsChanged; + } + } + if (this.isOpenGroupV2() && details.image_id && isNumber(details.image_id)) { const roomInfos = OpenGroupData.getV2OpenGroupRoom(this.id); if (roomInfos) { @@ -1458,7 +1535,7 @@ export class ConversationModel extends Backbone.Model { } public hasMember(pubkey: string) { - return _.includes(this.get('members'), pubkey); + return includes(this.get('members'), pubkey); } // returns true if this is a closed/medium or open group public isGroup() { @@ -1836,9 +1913,22 @@ export class ConversationModel extends Backbone.Model { .sendToPubKey(device, typingMessage) .catch(window?.log?.error); } + + private async replaceWithOurRealSessionId(toReplace: Array) { + const roomInfos = OpenGroupData.getV2OpenGroupRoom(this.id); + const sodium = await getSodiumRenderer(); + const ourBlindedPubkeyForThisSogs = + roomInfos && roomHasBlindEnabled(roomInfos) + ? await findCachedOurBlindedPubkeyOrLookItUp(roomInfos?.serverPublicKey, sodium) + : UserUtils.getOurPubKeyStrFromCache(); + const replacedWithOurRealSessionId = toReplace.map(m => + m === ourBlindedPubkeyForThisSogs ? UserUtils.getOurPubKeyStrFromCache() : m + ); + return replacedWithOurRealSessionId; + } } -const throttledAllConversationsDispatch = _.debounce( +const throttledAllConversationsDispatch = debounce( () => { if (updatesToDispatch.size === 0) { return; diff --git a/ts/models/conversationAttributes.ts b/ts/models/conversationAttributes.ts index 42335a6ff..69a8e8599 100644 --- a/ts/models/conversationAttributes.ts +++ b/ts/models/conversationAttributes.ts @@ -42,7 +42,8 @@ export interface ConversationAttributes { */ lastMessage: string | null; lastJoinedTimestamp: number; // ClosedGroup: last time we were added to this group - groupAdmins: Array; + groupAdmins: Array; // for sogs and closed group: the admins of that group. + groupModerators: Array; // for sogs only, this is the moderator in that room. isKickedFromGroup: boolean; subscriberCount: number; diff --git a/ts/node/database_utility.ts b/ts/node/database_utility.ts index 18154abe8..d692003be 100644 --- a/ts/node/database_utility.ts +++ b/ts/node/database_utility.ts @@ -28,6 +28,7 @@ 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', @@ -80,7 +81,7 @@ export function formatRowOfConversation(row?: Record): Conversation const convo: ConversationAttributes = omit(row, 'json') as ConversationAttributes; - // if the stringified array of admins/members/zombies length is less than 5, + // if the stringified array of admins/moderators/members/zombies length is less than 5, // we consider there is nothing to parse and just return [] const minLengthNoParsing = 5; @@ -88,6 +89,11 @@ export function formatRowOfConversation(row?: Record): Conversation 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) : []; convo.zombies = @@ -147,6 +153,7 @@ export function formatRowOfConversation(row?: Record): Conversation const allowedKeysOfConversationAttributes = [ 'groupAdmins', + 'groupModerators', 'members', 'zombies', 'isTrustedForAttachmentDownload', diff --git a/ts/node/sql.ts b/ts/node/sql.ts index 7eaf91e25..2cc422381 100644 --- a/ts/node/sql.ts +++ b/ts/node/sql.ts @@ -773,6 +773,7 @@ const LOKI_SCHEMA_VERSIONS = [ updateToLokiSchemaVersion23, updateToLokiSchemaVersion24, updateToLokiSchemaVersion25, + updateToLokiSchemaVersion26, ]; function updateToLokiSchemaVersion1(currentVersion: number, db: BetterSqlite3.Database) { @@ -1588,6 +1589,24 @@ function updateToLokiSchemaVersion25(currentVersion: number, db: BetterSqlite3.D console.log(`updateToLokiSchemaVersion${targetVersion}: success!`); } +function updateToLokiSchemaVersion26(currentVersion: number, db: BetterSqlite3.Database) { + const targetVersion = 26; + if (currentVersion >= targetVersion) { + return; + } + console.log(`updateToLokiSchemaVersion${targetVersion}: starting...`); + + db.transaction(() => { + db.exec(` + ALTER TABLE ${CONVERSATIONS_TABLE} ADD COLUMN groupModerators TEXT DEFAULT "[]"; -- those are for sogs only (for closed groups we only need the groupAdmins) + `); + + writeLokiSchemaVersion(targetVersion, db); + })(); + + console.log(`updateToLokiSchemaVersion${targetVersion}: success!`); +} + // function printTableColumns(table: string, db: BetterSqlite3.Database) { // console.warn(db.pragma(`table_info('${table}');`)); // } @@ -1999,6 +2018,7 @@ function saveConversation(data: ConversationAttributes, instance?: BetterSqlite3 lastMessage, lastJoinedTimestamp, groupAdmins, + groupModerators, isKickedFromGroup, subscriberCount, readCapability, @@ -2039,6 +2059,7 @@ function saveConversation(data: ConversationAttributes, instance?: BetterSqlite3 lastMessage, lastJoinedTimestamp, groupAdmins, + groupModerators, isKickedFromGroup, subscriberCount, readCapability, @@ -2071,6 +2092,7 @@ function saveConversation(data: ConversationAttributes, instance?: BetterSqlite3 $lastMessage, $lastJoinedTimestamp, $groupAdmins, + $groupModerators, $isKickedFromGroup, $subscriberCount, $readCapability, @@ -2106,6 +2128,8 @@ function saveConversation(data: ConversationAttributes, instance?: BetterSqlite3 lastJoinedTimestamp, groupAdmins: groupAdmins && groupAdmins.length ? arrayStrToJson(groupAdmins) : '[]', + groupModerators: + groupModerators && groupModerators.length ? arrayStrToJson(groupModerators) : '[]', isKickedFromGroup: toSqliteBoolean(isKickedFromGroup), subscriberCount, readCapability: toSqliteBoolean(readCapability), @@ -3789,6 +3813,7 @@ function fillWithTestData(numConvosToAdd: number, numMsgsToAdd: number) { didApproveMe: false, expireTimer: 0, groupAdmins: [], + groupModerators: [], isApproved: false, isKickedFromGroup: false, isPinned: false, diff --git a/ts/session/apis/open_group_api/opengroupV2/OpenGroupServerPoller.ts b/ts/session/apis/open_group_api/opengroupV2/OpenGroupServerPoller.ts index b0ab20786..a40fcf3b7 100644 --- a/ts/session/apis/open_group_api/opengroupV2/OpenGroupServerPoller.ts +++ b/ts/session/apis/open_group_api/opengroupV2/OpenGroupServerPoller.ts @@ -12,6 +12,7 @@ import { OpenGroupBatchRow, parseBatchGlobalStatusCode, sogsBatchSend, + SubRequestMessagesObjectType, } from '../sogsv3/sogsV3BatchPoll'; import { handleBatchPollResults } from '../sogsv3/sogsApiV3'; import { @@ -325,7 +326,8 @@ export class OpenGroupServerPoller { export const getRoomAndUpdateLastFetchTimestamp = async ( conversationId: string, - newMessages: Array + newMessages: Array, + subRequest: SubRequestMessagesObjectType ) => { const roomInfos = OpenGroupData.getV2OpenGroupRoom(conversationId); if (!roomInfos || !roomInfos.serverUrl || !roomInfos.roomId) { @@ -336,7 +338,7 @@ export const getRoomAndUpdateLastFetchTimestamp = async ( // if we got no new messages, just write our last update timestamp to the db roomInfos.lastFetchTimestamp = Date.now(); window?.log?.info( - `No new messages for ${roomInfos.roomId}... just updating our last fetched timestamp` + `No new messages for ${subRequest?.roomId}:${subRequest?.sinceSeqNo}... just updating our last fetched timestamp` ); await OpenGroupData.saveV2OpenGroupRoom(roomInfos); return null; diff --git a/ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts b/ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts index 3341d96e4..17f25e847 100644 --- a/ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts +++ b/ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts @@ -73,13 +73,13 @@ async function handlePollInfoResponse( token: string; upload: boolean; write: boolean; - details: { admins?: Array; image_id: number }; + details: { admins?: Array; image_id: number; moderators?: Array }; }, serverUrl: string, roomIdsStillPolled: Set ) { if (statusCode !== 200) { - window.log.info('handlePollInfoResponse subRequest status code is not 200'); + window.log.info('handlePollInfoResponse subRequest status code is not 200:', statusCode); return; } @@ -109,7 +109,7 @@ async function handlePollInfoResponse( write, upload, subscriberCount: active_users, - details: pick(details, 'admins', 'image_id'), + details: pick(details, 'admins', 'image_id', 'moderators'), }); } @@ -190,7 +190,11 @@ const handleMessagesResponseV4 = async ( } const convoId = getOpenGroupV2ConversationId(serverUrl, roomId); - const roomInfos = await getRoomAndUpdateLastFetchTimestamp(convoId, messages); + const roomInfos = await getRoomAndUpdateLastFetchTimestamp( + convoId, + messages, + subrequestOption.messages + ); if (!roomInfos || !roomInfos.conversationId) { return; } diff --git a/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts b/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts index 2c705e3c3..1caaf7b22 100644 --- a/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts +++ b/ts/session/apis/open_group_api/sogsv3/sogsV3BatchPoll.ts @@ -121,12 +121,16 @@ export type SubrequestOptionType = 'capabilities' | 'messages' | 'pollInfo' | 'i export type SubRequestCapabilitiesType = { type: 'capabilities' }; +export type SubRequestMessagesObjectType = + | { + roomId: string; + sinceSeqNo?: number; + } + | undefined; + export type SubRequestMessagesType = { type: 'messages'; - messages?: { - roomId: string; - sinceSeqNo?: number; - }; + messages?: SubRequestMessagesObjectType; }; export type SubRequestPollInfoType = { diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 2c9c08585..f722648b1 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -238,6 +238,7 @@ export interface ReduxConversationType { isGroup?: boolean; isPrivate?: boolean; weAreAdmin?: boolean; + weAreModerator?: boolean; unreadCount?: number; mentionedUs?: boolean; isSelected?: boolean; @@ -249,7 +250,8 @@ export interface ReduxConversationType { subscriberCount?: number; left?: boolean; avatarPath?: string | null; // absolute filepath to the avatar - groupAdmins?: Array; // admins for closed groups and moderators for open groups + groupAdmins?: Array; // admins for closed groups and admins for open groups + groupModerators?: Array; // only for opengroups: moderators members?: Array; // members for closed groups only zombies?: Array; // members for closed groups only diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index e34a534b3..b3a8e7595 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -826,16 +826,21 @@ 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; // 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) - // or the convo is public and we are an admin - const isDeletable = sender === ourPubkey || !isPublic || (isPublic && !!weAreAdmin); + // or the convo is public and we are an admin or moderator + const isDeletable = + sender === ourPubkey || !isPublic || (isPublic && (weAreAdmin || weAreModerator)); // A message is deletable for everyone if // either we sent it no matter what the conversation type, - // or the convo is public and we are an admin - const isDeletableForEveryone = sender === ourPubkey || (isPublic && !!weAreAdmin) || false; + // or the convo is public and we are an admin or moderator + const isDeletableForEveryone = + sender === ourPubkey || (isPublic && (weAreAdmin || weAreModerator)) || false; const isSenderAdmin = groupAdmins.includes(sender); const senderIsUs = sender === ourPubkey; diff --git a/ts/test/session/unit/selectors/conversations_test.ts b/ts/test/session/unit/selectors/conversations_test.ts index 78ee90cc1..2dbe145cf 100644 --- a/ts/test/session/unit/selectors/conversations_test.ts +++ b/ts/test/session/unit/selectors/conversations_test.ts @@ -36,6 +36,7 @@ describe('state/selectors/conversations', () => { avatarPath: '', groupAdmins: [], + groupModerators: [], lastMessage: undefined, members: [], expireTimer: 0, @@ -64,6 +65,7 @@ describe('state/selectors/conversations', () => { avatarPath: '', groupAdmins: [], + groupModerators: [], lastMessage: undefined, members: [], expireTimer: 0, @@ -92,6 +94,7 @@ describe('state/selectors/conversations', () => { avatarPath: '', groupAdmins: [], + groupModerators: [], lastMessage: undefined, members: [], expireTimer: 0, @@ -120,6 +123,7 @@ describe('state/selectors/conversations', () => { avatarPath: '', groupAdmins: [], + groupModerators: [], expireTimer: 0, lastMessage: undefined, members: [], @@ -149,6 +153,7 @@ describe('state/selectors/conversations', () => { avatarPath: '', groupAdmins: [], + groupModerators: [], lastMessage: undefined, members: [], isPinned: false, @@ -192,6 +197,7 @@ describe('state/selectors/conversations', () => { avatarPath: '', groupAdmins: [], + groupModerators: [], lastMessage: undefined, members: [], isPinned: false, @@ -221,6 +227,7 @@ describe('state/selectors/conversations', () => { avatarPath: '', groupAdmins: [], + groupModerators: [], lastMessage: undefined, members: [], isPinned: false, @@ -250,6 +257,7 @@ describe('state/selectors/conversations', () => { avatarPath: '', groupAdmins: [], + groupModerators: [], lastMessage: undefined, members: [], isPinned: true, @@ -278,6 +286,7 @@ describe('state/selectors/conversations', () => { avatarPath: '', groupAdmins: [], + groupModerators: [], lastMessage: undefined, members: [], isPinned: true, @@ -307,6 +316,7 @@ describe('state/selectors/conversations', () => { avatarPath: '', groupAdmins: [], + groupModerators: [], lastMessage: undefined, members: [], isPinned: false,