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,