From 01cbb5fbf3d2683894a0061630ae658ebd5bd66c Mon Sep 17 00:00:00 2001 From: Natsumi Date: Tue, 23 Apr 2024 16:27:16 +1200 Subject: [PATCH] Group audit logs, ban list, invites, etc --- Dotnet/ImageCache.cs | 15 +- html/src/app.js | 1158 ++++++++++++++++++++++++++---- html/src/index.pug | 261 +++++-- html/src/localization/en/en.json | 21 + html/src/repository/database.js | 2 +- 5 files changed, 1249 insertions(+), 208 deletions(-) diff --git a/Dotnet/ImageCache.cs b/Dotnet/ImageCache.cs index 1a5a576b5..012d84727 100644 --- a/Dotnet/ImageCache.cs +++ b/Dotnet/ImageCache.cs @@ -46,10 +46,17 @@ public static async Task GetImage(string url, string fileId, string vers foreach (Cookie cookie in cookies) cookieString += $"{cookie.Name}={cookie.Value};"; } - - httpClient.DefaultRequestHeaders.Add("Cookie", cookieString); - httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(Program.Version); - using (var response = await httpClient.GetAsync(url)) + + var request = new HttpRequestMessage(HttpMethod.Get, url) + { + Headers = + { + { "Cookie", cookieString }, + { "User-Agent", Program.Version } + }, + + }; + using (var response = await httpClient.SendAsync(request)) { response.EnsureSuccessStatusCode(); await using (var fileStream = new FileStream(fileLocation, FileMode.Create, FileAccess.Write, FileShare.None)) diff --git a/html/src/app.js b/html/src/app.js index 9726dd8c1..f38a04aa1 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -1656,6 +1656,10 @@ speechSynthesis.getVoices(); console.error('API.applyPresenceGroups: invalid groups', ref); return; } + if (groups.length === 0) { + // as it turns out, this is not the most trust worthly source of info + return; + } // update group list for (var groupId of groups) { @@ -5041,7 +5045,7 @@ speechSynthesis.getVoices(); $app.groupDialog.visible && $app.groupDialog.id === groupId ) { - API.getGroup({ groupId, includeRoles: true }); + $app.getGroupDialogGroup(groupId); } $app.onGroupJoined(groupId); this.$emit('GROUP:MEMBER', { @@ -5918,7 +5922,9 @@ speechSynthesis.getVoices(); for (var k = w - 1; k > -1; k--) { var feedItem = wristArr[k]; if ( - feedItem.type === 'OnPlayerLeft' && + (feedItem.type === 'OnPlayerLeft' || + feedItem.type === 'BlockedOnPlayerLeft' || + feedItem.type === 'MutedOnPlayerLeft') && Date.parse(feedItem.created_at) >= currentUserLeaveTime && Date.parse(feedItem.created_at) <= @@ -5936,7 +5942,9 @@ speechSynthesis.getVoices(); for (var k = w - 1; k > -1; k--) { var feedItem = wristArr[k]; if ( - feedItem.type === 'OnPlayerJoined' && + (feedItem.type === 'OnPlayerJoined' || + feedItem.type === 'BlockedOnPlayerJoined' || + feedItem.type === 'MutedOnPlayerJoined') && Date.parse(feedItem.created_at) >= locationJoinTime && Date.parse(feedItem.created_at) <= locationJoinTimeOffset @@ -6011,7 +6019,7 @@ speechSynthesis.getVoices(); ) { wristArr.unshift(entry); } - this.queueFeedNoty(entry); + this.queueGameLogNoty(entry); } } // when too many user joins happen at once when switching instances @@ -6041,13 +6049,21 @@ speechSynthesis.getVoices(); $app.methods.queueGameLogNoty = function (noty) { // remove join/leave notifications when switching worlds - if (noty.type === 'OnPlayerJoined') { + if ( + noty.type === 'OnPlayerJoined' || + noty.type === 'BlockedOnPlayerJoined' || + noty.type === 'MutedOnPlayerJoined' + ) { var bias = this.lastLocation.date + 30 * 1000; // 30 secs if (Date.parse(noty.created_at) <= bias) { return; } } - if (noty.type === 'OnPlayerLeft') { + if ( + noty.type === 'OnPlayerLeft' || + noty.type === 'BlockedOnPlayerLeft' || + noty.type === 'MutedOnPlayerLeft' + ) { var bias = this.lastLocationDestinationTime + 5 * 1000; // 5 secs if (Date.parse(noty.created_at) <= bias) { return; @@ -9961,9 +9977,6 @@ speechSynthesis.getVoices(); ); } await $app.getCurrentUserGroups(); - // eslint-disable-next-line require-atomic-updates - $app.notificationTable.data = await database.getNotifications(); - await this.refreshNotifications(); try { if ( await configRepository.getBool(`friendLogInit_${args.json.id}`) @@ -9980,6 +9993,9 @@ speechSynthesis.getVoices(); this.logout(); throw err; } + // eslint-disable-next-line require-atomic-updates + $app.notificationTable.data = await database.getNotifications(); + await this.refreshNotifications(); $app.getAvatarHistory(); $app.getAllMemos(); if ($app.randomUserColours) { @@ -26279,7 +26295,7 @@ speechSynthesis.getVoices(); ); $app.methods.updateDatabaseVersion = async function () { - var databaseVersion = 7; + var databaseVersion = 8; if (this.databaseVersion < databaseVersion) { if (this.databaseVersion) { var msgBox = this.$message({ @@ -26300,6 +26316,7 @@ speechSynthesis.getVoices(); await database.fixBrokenGroupInvites(); // fix notification v2 in wrong table await database.updateTableForGroupNames(); // alter tables to include group name await database.fixBrokenNotifications(); // fix notifications being null + await database.fixBrokenGroupChange(); // fix spam group left & name change await database.vacuum(); // succ await database.setWal(); // https://www.sqlite.org/wal.html await configRepository.setInt( @@ -28045,7 +28062,9 @@ speechSynthesis.getVoices(); $app.groupDialog.inGroup = json.membershipStatus === 'member'; $app.getGroupDialogGroup(groupId); } - this.currentUserGroups.set(groupId, json); + if (json.membershipStatus === 'member') { + $app.onGroupJoined(groupId); + } }); /** @@ -28078,7 +28097,7 @@ speechSynthesis.getVoices(); ) { $app.getCurrentUserRepresentedGroup(); } - this.currentUserGroups.delete(groupId); + $app.onGroupLeft(groupId); }); /** @@ -28101,7 +28120,7 @@ speechSynthesis.getVoices(); API.$on('GROUP:CANCELJOINREQUEST', function (args) { var groupId = args.params.groupId; if ($app.groupDialog.visible && $app.groupDialog.id === groupId) { - $app.groupDialog.ref.membershipStatus = 'inactive'; + $app.getGroupDialogGroup(groupId); } }); @@ -28678,182 +28697,552 @@ speechSynthesis.getVoices(); }; /** - * @param {{ groupId: string }} params + * @param {{ groupId: string, userId: string }} params * @return { Promise<{json: any, params}> } */ - - API.getGroupInstances = function (params) { - return this.call( - `users/${this.currentUser.id}/instances/groups/${params.groupId}`, - { - method: 'GET' - } - ).then((json) => { + API.unbanGroupMember = function (params) { + return this.call(`groups/${params.groupId}/bans/${params.userId}`, { + method: 'DELETE' + }).then((json) => { var args = { json, params }; - this.$emit('GROUP:INSTANCES', args); + this.$emit('GROUP:MEMBER:UNBAN', args); return args; }); }; - API.$on('GROUP:INSTANCES', function (args) { - if ($app.groupDialog.id === args.params.groupId) { - $app.applyGroupDialogInstances(args.json.instances); - } - }); - - API.$on('GROUP:INSTANCES', function (args) { - for (var json of args.json.instances) { - this.$emit('INSTANCE', { + API.deleteSentGroupInvite = function (params) { + return this.call(`groups/${params.groupId}/invites/${params.userId}`, { + method: 'DELETE' + }).then((json) => { + var args = { json, - params: { - fetchedAt: args.json.fetchedAt - } - }); - this.getCachedWorld({ - worldId: json.world.id - }).then((args1) => { - json.world = args1.ref; - return args1; - }); - // get queue size etc - this.getInstance({ - worldId: json.worldId, - instanceId: json.instanceId - }); - } - }); - - /** - * @param {{ groupId: string }} params - * @return { Promise<{json: any, params}> } - */ + params + }; + this.$emit('GROUP:INVITE:DELETE', args); + return args; + }); + }; - API.getGroupRoles = function (params) { - return this.call(`groups/${params.groupId}/roles`, { - method: 'GET', - params + API.deleteBlockedGroupRequest = function (params) { + return this.call(`groups/${params.groupId}/members/${params.userId}`, { + method: 'DELETE' }).then((json) => { var args = { json, params }; - this.$emit('GROUP:ROLES', args); + this.$emit('GROUP:BLOCKED:DELETE', args); return args; }); }; - API.getRequestedGroups = function () { - return this.call(`users/${this.currentUser.id}/groups/requested`, { - method: 'GET' + API.acceptGroupInviteRequest = function (params) { + return this.call(`groups/${params.groupId}/requests/${params.userId}`, { + method: 'PUT', + params: { + action: 'accept' + } }).then((json) => { var args = { - json + json, + params }; - this.$emit('GROUP:REQUESTED', args); + this.$emit('GROUP:INVITE:ACCEPT', args); return args; }); }; - API.getUsersGroupInstances = function () { - return this.call(`users/${this.currentUser.id}/instances/groups`, { - method: 'GET' + API.rejectGroupInviteRequest = function (params) { + return this.call(`groups/${params.groupId}/requests/${params.userId}`, { + method: 'PUT', + params: { + action: 'reject' + } }).then((json) => { var args = { - json + json, + params }; - this.$emit('GROUP:USER:INSTANCES', args); + this.$emit('GROUP:INVITE:REJECT', args); return args; }); }; - API.$on('GROUP:USER:INSTANCES', function (args) { - $app.groupInstances = []; - for (var json of args.json.instances) { - if (args.json.fetchedAt) { - // tack on fetchedAt - json.$fetchedAt = args.json.fetchedAt; + API.blockGroupInviteRequest = function (params) { + return this.call(`groups/${params.groupId}/requests/${params.userId}`, { + method: 'PUT', + params: { + action: 'reject', + block: true } - this.$emit('INSTANCE', { + }).then((json) => { + var args = { json, - params: { - fetchedAt: args.json.fetchedAt + params + }; + this.$emit('GROUP:INVITE:BLOCK', args); + return args; + }); + }; + + API.getGroupBans = function (params) { + return this.call(`groups/${params.groupId}/bans`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:BANS', args); + return args; + }); + }; + + $app.methods.getAllGroupBans = async function (groupId) { + this.groupBansModerationTable.data = []; + var params = { + groupId, + n: 100, + offset: 0 + }; + var count = 50; // 5000 max + this.isGroupMembersLoading = true; + try { + for (var i = 0; i < count; i++) { + var args = await API.getGroupBans(params); + params.offset += params.n; + if (args.json.length < params.n) { + break; } - }); - var ref = this.cachedGroups.get(json.ownerId); - if (typeof ref === 'undefined') { - if ($app.friendLogInitStatus) { - this.getGroup({ groupId: json.ownerId }); + if (!this.groupMemberModeration.visible) { + break; } - return; } - $app.groupInstances.push({ - group: ref, - instance: this.applyInstance(json) + } catch (err) { + this.$message({ + message: 'Failed to get group bans', + type: 'error' }); + } finally { + this.isGroupMembersLoading = false; + } + }; + + API.$on('GROUP:BANS', function (args) { + if ($app.groupMemberModeration.id !== args.params.groupId) { + return; + } + + for (var json of args.json) { + var ref = this.applyGroupMember(json); + $app.groupBansModerationTable.data.push(ref); } + // $app.groupBansModerationTable.data = + // $app.groupBansModerationTable.data.concat(args.json); }); + $app.methods.getAllGroupLogs = async function (groupId) { + this.groupLogsModerationTable.data = []; + var params = { + groupId, + n: 100, + offset: 0 + }; + var count = 50; // 5000 max + this.isGroupMembersLoading = true; + try { + for (var i = 0; i < count; i++) { + var args = await API.getGroupLogs(params); + params.offset += params.n; + if (!args.json.hasNext) { + break; + } + if (!this.groupMemberModeration.visible) { + break; + } + } + } catch (err) { + this.$message({ + message: 'Failed to get group logs', + type: 'error' + }); + } finally { + this.isGroupMembersLoading = false; + } + }; + /** - * @param {{ - query: string, - n: number, - offset: number, - order: string, - sortBy: string - }} params - * @return { Promise<{json: any, params}> } - */ - API.groupSearch = function (params) { - return this.call(`groups`, { - method: 'GET', - params + * @param {{ groupId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.getGroupLogs = function (params) { + return this.call(`groups/${params.groupId}/auditLogs`, { + method: 'GET' }).then((json) => { var args = { json, params }; - this.$emit('GROUP:SEARCH', args); + this.$emit('GROUP:LOGS', args); return args; }); }; - API.$on('GROUP:SEARCH', function (args) { - for (var json of args.json) { - this.$emit('GROUP', { - json, - params: { - groupId: json.id + API.$on('GROUP:LOGS', function (args) { + if ($app.groupMemberModeration.id !== args.params.groupId) { + return; + } + + for (var json of args.json.results) { + $app.groupLogsModerationTable.data.push(json); + } + }); + + $app.methods.getAllGroupInvitesAndJoinRequests = async function (groupId) { + await this.getAllGroupInvites(groupId); + await this.getAllGroupJoinRequests(groupId); + await this.getAllGroupBlockedRequests(groupId); + }; + + $app.methods.getAllGroupInvites = async function (groupId) { + this.groupInvitesModerationTable.data = []; + var params = { + groupId, + n: 100, + offset: 0 + }; + var count = 50; // 5000 max + this.isGroupMembersLoading = true; + try { + for (var i = 0; i < count; i++) { + var args = await API.getGroupInvites(params); + params.offset += params.n; + if (args.json.length < params.n) { + break; + } + if (!this.groupMemberModeration.visible) { + break; } + } + } catch (err) { + this.$message({ + message: 'Failed to get group invites', + type: 'error' }); + } finally { + this.isGroupMembersLoading = false; } - }); + }; /** * @param {{ groupId: string }} params * @return { Promise<{json: any, params}> } */ - API.getCachedGroup = function (params) { - return new Promise((resolve, reject) => { - var ref = this.cachedGroups.get(params.groupId); - if (typeof ref === 'undefined') { - this.getGroup(params).catch(reject).then(resolve); - } else { - resolve({ - cache: true, - json: ref, - params, - ref - }); - } + API.getGroupInvites = function (params) { + return this.call(`groups/${params.groupId}/invites`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:INVITES', args); + return args; }); }; - API.applyGroup = function (json) { - var ref = this.cachedGroups.get(json.id); - if (typeof ref === 'undefined') { + API.$on('GROUP:INVITES', function (args) { + if ($app.groupMemberModeration.id !== args.params.groupId) { + return; + } + + for (var json of args.json) { + var ref = this.applyGroupMember(json); + $app.groupInvitesModerationTable.data.push(ref); + } + }); + + $app.methods.getAllGroupJoinRequests = async function (groupId) { + this.groupJoinRequestsModerationTable.data = []; + var params = { + groupId, + n: 100, + offset: 0 + }; + var count = 50; // 5000 max + this.isGroupMembersLoading = true; + try { + for (var i = 0; i < count; i++) { + var args = await API.getGroupJoinRequests(params); + params.offset += params.n; + if (args.json.length < params.n) { + break; + } + if (!this.groupMemberModeration.visible) { + break; + } + } + } catch (err) { + this.$message({ + message: 'Failed to get group join requests', + type: 'error' + }); + } finally { + this.isGroupMembersLoading = false; + } + }; + + $app.methods.getAllGroupBlockedRequests = async function (groupId) { + this.groupBlockedModerationTable.data = []; + var params = { + groupId, + n: 100, + offset: 0, + blocked: true + }; + var count = 50; // 5000 max + this.isGroupMembersLoading = true; + try { + for (var i = 0; i < count; i++) { + var args = await API.getGroupJoinRequests(params); + params.offset += params.n; + if (args.json.length < params.n) { + break; + } + if (!this.groupMemberModeration.visible) { + break; + } + } + } catch (err) { + this.$message({ + message: 'Failed to get group join requests', + type: 'error' + }); + } finally { + this.isGroupMembersLoading = false; + } + }; + + /** + * @param {{ groupId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.getGroupJoinRequests = function (params) { + return this.call(`groups/${params.groupId}/requests`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:JOINREQUESTS', args); + return args; + }); + }; + + API.$on('GROUP:JOINREQUESTS', function (args) { + if ($app.groupMemberModeration.id !== args.params.groupId) { + return; + } + + if (!args.params.blocked) { + for (var json of args.json) { + var ref = this.applyGroupMember(json); + $app.groupJoinRequestsModerationTable.data.push(ref); + } + } else { + for (var json of args.json) { + var ref = this.applyGroupMember(json); + $app.groupBlockedModerationTable.data.push(ref); + } + } + }); + + /** + * @param {{ groupId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.getGroupInstances = function (params) { + return this.call( + `users/${this.currentUser.id}/instances/groups/${params.groupId}`, + { + method: 'GET' + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:INSTANCES', args); + return args; + }); + }; + + API.$on('GROUP:INSTANCES', function (args) { + if ($app.groupDialog.id === args.params.groupId) { + $app.applyGroupDialogInstances(args.json.instances); + } + }); + + API.$on('GROUP:INSTANCES', function (args) { + for (var json of args.json.instances) { + this.$emit('INSTANCE', { + json, + params: { + fetchedAt: args.json.fetchedAt + } + }); + this.getCachedWorld({ + worldId: json.world.id + }).then((args1) => { + json.world = args1.ref; + return args1; + }); + // get queue size etc + this.getInstance({ + worldId: json.worldId, + instanceId: json.instanceId + }); + } + }); + + /** + * @param {{ groupId: string }} params + * @return { Promise<{json: any, params}> } + */ + + API.getGroupRoles = function (params) { + return this.call(`groups/${params.groupId}/roles`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:ROLES', args); + return args; + }); + }; + + API.getRequestedGroups = function () { + return this.call(`users/${this.currentUser.id}/groups/requested`, { + method: 'GET' + }).then((json) => { + var args = { + json + }; + this.$emit('GROUP:REQUESTED', args); + return args; + }); + }; + + API.getUsersGroupInstances = function () { + return this.call(`users/${this.currentUser.id}/instances/groups`, { + method: 'GET' + }).then((json) => { + var args = { + json + }; + this.$emit('GROUP:USER:INSTANCES', args); + return args; + }); + }; + + API.$on('GROUP:USER:INSTANCES', function (args) { + $app.groupInstances = []; + for (var json of args.json.instances) { + if (args.json.fetchedAt) { + // tack on fetchedAt + json.$fetchedAt = args.json.fetchedAt; + } + this.$emit('INSTANCE', { + json, + params: { + fetchedAt: args.json.fetchedAt + } + }); + var ref = this.cachedGroups.get(json.ownerId); + if (typeof ref === 'undefined') { + if ($app.friendLogInitStatus) { + this.getGroup({ groupId: json.ownerId }); + } + return; + } + $app.groupInstances.push({ + group: ref, + instance: this.applyInstance(json) + }); + } + }); + + /** + * @param {{ + query: string, + n: number, + offset: number, + order: string, + sortBy: string + }} params + * @return { Promise<{json: any, params}> } + */ + API.groupSearch = function (params) { + return this.call(`groups`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:SEARCH', args); + return args; + }); + }; + + API.$on('GROUP:SEARCH', function (args) { + for (var json of args.json) { + this.$emit('GROUP', { + json, + params: { + groupId: json.id + } + }); + } + }); + + /** + * @param {{ groupId: string }} params + * @return { Promise<{json: any, params}> } + */ + API.getCachedGroup = function (params) { + return new Promise((resolve, reject) => { + var ref = this.cachedGroups.get(params.groupId); + if (typeof ref === 'undefined') { + this.getGroup(params).catch(reject).then(resolve); + } else { + resolve({ + cache: true, + json: ref, + params, + ref + }); + } + }); + }; + + API.applyGroup = function (json) { + var ref = this.cachedGroups.get(json.id); + json.rules = $app.replaceBioSymbols(json.rules); + json.name = $app.replaceBioSymbols(json.name); + json.description = $app.replaceBioSymbols(json.description); + if (typeof ref === 'undefined') { ref = { id: '', name: '', @@ -28952,9 +29341,6 @@ speechSynthesis.getVoices(); } Object.assign(ref, json); } - ref.rules = $app.replaceBioSymbols(ref.rules); - ref.name = $app.replaceBioSymbols(ref.name); - ref.description = $app.replaceBioSymbols(ref.description); ref.$url = `https://vrc.group/${ref.shortCode}.${ref.discriminator}`; this.applyGroupLanguage(ref); return ref; @@ -29063,9 +29449,6 @@ speechSynthesis.getVoices(); `VRCX_currentUserGroupsInit_${userId}` )) ) { - // REMOVE ME: clean up 18/04/2024 mess - await database.fixBrokenGroupChange(); - // fetch every group with roles for storing and comparing later for (var i = 0; i < groups.length; i++) { var groupId = groups[i]; @@ -29114,13 +29497,17 @@ speechSynthesis.getVoices(); }; API.applyGroupMember = function (json) { - if (typeof json.user !== 'undefined') { - var ref = this.cachedUsers.get(json.user.id); - if (typeof ref !== 'undefined') { - json.user = ref; + if (typeof json?.user !== 'undefined') { + if (json.userId === this.currentUser.id) { + json.user = this.currentUser; + json.$displayName = this.currentUser.displayName; + } else { + var ref = this.cachedUsers.get(json.user.id); + if (typeof ref !== 'undefined') { + json.user = ref; + json.$displayName = ref.displayName; + } } - } else if (json.userId === this.currentUser.id) { - json.user = this.currentUser; } return json; }; @@ -30732,7 +31119,8 @@ speechSynthesis.getVoices(); selectedUsersArray: [], selectedRoles: [], progressCurrent: 0, - progressTotal: 0 + progressTotal: 0, + selectUserId: '' }; $app.data.groupMemberModerationTable = { @@ -30749,6 +31137,82 @@ speechSynthesis.getVoices(); } }; + $app.data.groupBansModerationTable = { + data: [], + filters: [ + { + prop: ['$displayName'], + value: '' + } + ], + tableProps: { + stripe: true, + size: 'mini' + }, + pageSize: $app.data.tablePageSize, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 15, 25, 50, 100] + } + }; + + $app.data.groupLogsModerationTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + pageSize: $app.data.tablePageSize, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 15, 25, 50, 100] + } + }; + + $app.data.groupInvitesModerationTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + pageSize: $app.data.tablePageSize, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 15, 25, 50, 100] + } + }; + + $app.data.groupJoinRequestsModerationTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + pageSize: $app.data.tablePageSize, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 15, 25, 50, 100] + } + }; + + $app.data.groupBlockedModerationTable = { + data: [], + tableProps: { + stripe: true, + size: 'mini' + }, + pageSize: $app.data.tablePageSize, + paginationProps: { + small: true, + layout: 'sizes,prev,pager,next,total', + pageSizes: [10, 15, 25, 50, 100] + } + }; + $app.data.groupMemberModerationTableForceUpdate = 0; $app.methods.setGroupMemberModerationTable = function (data) { @@ -30810,26 +31274,131 @@ speechSynthesis.getVoices(); break; } } + for (var i = 0; i < this.groupBansModerationTable.data.length; i++) { + var row = this.groupBansModerationTable.data[i]; + if (row.userId === user.userId) { + row.$selected = false; + break; + } + } + for (var i = 0; i < this.groupInvitesModerationTable.data.length; i++) { + var row = this.groupInvitesModerationTable.data[i]; + if (row.userId === user.userId) { + row.$selected = false; + break; + } + } + for ( + var i = 0; + i < this.groupJoinRequestsModerationTable.data.length; + i++ + ) { + var row = this.groupJoinRequestsModerationTable.data[i]; + if (row.userId === user.userId) { + row.$selected = false; + break; + } + } + for (var i = 0; i < this.groupBlockedModerationTable.data.length; i++) { + var row = this.groupBlockedModerationTable.data[i]; + if (row.userId === user.userId) { + row.$selected = false; + break; + } + } + + // force redraw + this.groupMemberModerationTableForceUpdate++; + }; + + $app.methods.clearSelectedGroupMembers = function () { + var D = this.groupMemberModeration; + D.selectedUsers.clear(); + D.selectedUsersArray = []; + for (var i = 0; i < this.groupMemberModerationTable.data.length; i++) { + var row = this.groupMemberModerationTable.data[i]; + row.$selected = false; + } + for (var i = 0; i < this.groupBansModerationTable.data.length; i++) { + var row = this.groupBansModerationTable.data[i]; + row.$selected = false; + } + for (var i = 0; i < this.groupInvitesModerationTable.data.length; i++) { + var row = this.groupInvitesModerationTable.data[i]; + row.$selected = false; + } + for ( + var i = 0; + i < this.groupJoinRequestsModerationTable.data.length; + i++ + ) { + var row = this.groupJoinRequestsModerationTable.data[i]; + row.$selected = false; + } + for (var i = 0; i < this.groupBlockedModerationTable.data.length; i++) { + var row = this.groupBlockedModerationTable.data[i]; + row.$selected = false; + } + // force redraw + this.groupMemberModerationTableForceUpdate++; + }; + + $app.methods.selectAllGroupMembers = function () { + var D = this.groupMemberModeration; + for (var i = 0; i < this.groupMemberModerationTable.data.length; i++) { + var row = this.groupMemberModerationTable.data[i]; + row.$selected = true; + D.selectedUsers.set(row.userId, row); + } + D.selectedUsersArray = Array.from(D.selectedUsers.values()); + // force redraw + this.groupMemberModerationTableForceUpdate++; + }; + + $app.methods.selectAllGroupBans = function () { + var D = this.groupMemberModeration; + for (var i = 0; i < this.groupBansModerationTable.data.length; i++) { + var row = this.groupBansModerationTable.data[i]; + row.$selected = true; + D.selectedUsers.set(row.userId, row); + } + D.selectedUsersArray = Array.from(D.selectedUsers.values()); + // force redraw + this.groupMemberModerationTableForceUpdate++; + }; + + $app.methods.selectAllGroupInvites = function () { + var D = this.groupMemberModeration; + for (var i = 0; i < this.groupInvitesModerationTable.data.length; i++) { + var row = this.groupInvitesModerationTable.data[i]; + row.$selected = true; + D.selectedUsers.set(row.userId, row); + } + D.selectedUsersArray = Array.from(D.selectedUsers.values()); // force redraw this.groupMemberModerationTableForceUpdate++; }; - $app.methods.clearSelectedGroupMembers = function () { + $app.methods.selectAllGroupJoinRequests = function () { var D = this.groupMemberModeration; - D.selectedUsers.clear(); - D.selectedUsersArray = []; - for (var i = 0; i < this.groupMemberModerationTable.data.length; i++) { - var row = this.groupMemberModerationTable.data[i]; - row.$selected = false; + for ( + var i = 0; + i < this.groupJoinRequestsModerationTable.data.length; + i++ + ) { + var row = this.groupJoinRequestsModerationTable.data[i]; + row.$selected = true; + D.selectedUsers.set(row.userId, row); } + D.selectedUsersArray = Array.from(D.selectedUsers.values()); // force redraw this.groupMemberModerationTableForceUpdate++; }; - $app.methods.selectAllGroupMembers = function () { + $app.methods.selectAllGroupBlocked = function () { var D = this.groupMemberModeration; - for (var i = 0; i < this.groupMemberModerationTable.data.length; i++) { - var row = this.groupMemberModerationTable.data[i]; + for (var i = 0; i < this.groupBlockedModerationTable.data.length; i++) { + var row = this.groupBlockedModerationTable.data[i]; row.$selected = true; D.selectedUsers.set(row.userId, row); } @@ -30858,6 +31427,10 @@ speechSynthesis.getVoices(); }); console.log(`Kicking ${user.userId} ${i + 1}/${memberCount}`); } + this.$message({ + message: `Kicked ${memberCount} group members`, + type: 'success' + }); } catch (err) { console.error(err); this.$message({ @@ -30890,6 +31463,10 @@ speechSynthesis.getVoices(); }); console.log(`Banning ${user.userId} ${i + 1}/${memberCount}`); } + this.$message({ + message: `Banned ${memberCount} group members`, + type: 'success' + }); } catch (err) { console.error(err); this.$message({ @@ -30902,6 +31479,232 @@ speechSynthesis.getVoices(); } }; + $app.methods.groupMembersUnban = async function () { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + if (user.userId === API.currentUser.id) { + continue; + } + await API.unbanGroupMember({ + groupId: D.id, + userId: user.userId + }); + console.log(`Unbanning ${user.userId} ${i + 1}/${memberCount}`); + } + this.$message({ + message: `Unbanned ${memberCount} group members`, + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to unban group member: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }; + + $app.methods.groupMembersDeleteSentInvite = async function () { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + if (user.userId === API.currentUser.id) { + continue; + } + await API.deleteSentGroupInvite({ + groupId: D.id, + userId: user.userId + }); + console.log( + `Deleting group invite ${user.userId} ${i + 1}/${memberCount}` + ); + } + this.$message({ + message: `Deleted ${memberCount} group invites`, + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to delete group invites: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }; + + $app.methods.groupMembersDeleteBlockedRequest = async function () { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + if (user.userId === API.currentUser.id) { + continue; + } + await API.deleteBlockedGroupRequest({ + groupId: D.id, + userId: user.userId + }); + console.log( + `Deleting blocked group request ${user.userId} ${i + 1}/${memberCount}` + ); + } + this.$message({ + message: `Deleted ${memberCount} blocked group requests`, + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to delete blocked group requests: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }; + + $app.methods.groupMembersAcceptInviteRequest = async function () { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + if (user.userId === API.currentUser.id) { + continue; + } + await API.acceptGroupInviteRequest({ + groupId: D.id, + userId: user.userId + }); + console.log( + `Accepting group join request ${user.userId} ${i + 1}/${memberCount}` + ); + } + this.$message({ + message: `Accepted ${memberCount} group join requests`, + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to accept group join requests: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }; + + $app.methods.groupMembersRejectInviteRequest = async function () { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + if (user.userId === API.currentUser.id) { + continue; + } + await API.rejectGroupInviteRequest({ + groupId: D.id, + userId: user.userId + }); + console.log( + `Rejecting group join request ${user.userId} ${i + 1}/${memberCount}` + ); + } + this.$message({ + message: `Rejected ${memberCount} group join requests`, + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to reject group join requests: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }; + + $app.methods.groupMembersBlockJoinRequest = async function () { + var D = this.groupMemberModeration; + var memberCount = D.selectedUsersArray.length; + D.progressTotal = memberCount; + try { + for (var i = 0; i < memberCount; i++) { + if (!D.visible || !D.progressTotal) { + break; + } + var user = D.selectedUsersArray[i]; + D.progressCurrent = i + 1; + if (user.userId === API.currentUser.id) { + continue; + } + await API.blockGroupInviteRequest({ + groupId: D.id, + userId: user.userId + }); + console.log( + `Blocking group join request ${user.userId} ${i + 1}/${memberCount}` + ); + } + this.$message({ + message: `Blocked ${memberCount} group join requests`, + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to block group join requests: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }; + $app.methods.groupMembersSaveNote = async function () { var D = this.groupMemberModeration; var memberCount = D.selectedUsersArray.length; @@ -30926,7 +31729,7 @@ speechSynthesis.getVoices(); ); } this.$message({ - message: 'Note saved', + message: `Saved notes for ${memberCount} group members`, type: 'success' }); } catch (err) { @@ -31042,6 +31845,67 @@ speechSynthesis.getVoices(); } }; + $app.methods.selectGroupMemberUserId = async function () { + var D = this.groupMemberModeration; + if (!D.selectUserId) { + return; + } + + var regexUserId = + /usr_[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}/g; + var match = []; + var userIdList = new Set(); + while ((match = regexUserId.exec(D.selectUserId)) !== null) { + userIdList.add(match[0]); + } + if (userIdList.size === 0) { + // for those users missing the usr_ prefix + userIdList.add(D.selectUserId); + } + for (var userId of userIdList) { + try { + await this.addGroupMemberToSelection(userId); + } catch { + console.error(`Failed to add user ${userId}`); + } + } + + D.selectUserId = ''; + }; + + $app.methods.addGroupMemberToSelection = async function (userId) { + var D = this.groupMemberModeration; + + // fetch memeber if there is one + // banned members don't have a user object + + var memeber = {}; + var memeberArgs = await API.getGroupMember({ + groupId: D.id, + userId + }); + if (memeberArgs.json) { + memeber = API.applyGroupMember(memeberArgs.json); + } + if (memeber.user) { + D.selectedUsers.set(memeber.userId, memeber); + D.selectedUsersArray = Array.from(D.selectedUsers.values()); + this.groupMemberModerationTableForceUpdate++; + return; + } + + var userArgs = await API.getCachedUser({ + userId + }); + memeber.userId = userArgs.json.id; + memeber.user = userArgs.json; + memeber.displayName = userArgs.json.displayName; + + D.selectedUsers.set(memeber.userId, memeber); + D.selectedUsersArray = Array.from(D.selectedUsers.values()); + this.groupMemberModerationTableForceUpdate++; + }; + $app.data.groupPostEditDialog = { visible: false, groupRef: {}, diff --git a/html/src/index.pug b/html/src/index.pug index 0554efc32..954b71396 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -1007,8 +1007,8 @@ html el-dropdown-item(v-if="hasGroupPermission(groupDialog.ref, 'group-invites-manage')" icon="el-icon-message" command="Invite To Group") {{ $t('dialog.group.actions.invite_to_group') }} template(v-if="hasGroupPermission(groupDialog.ref, 'group-announcement-manage')") el-dropdown-item(icon="el-icon-tickets" command="Create Post") {{ $t('dialog.group.actions.create_post') }} - template(v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')") - el-dropdown-item(icon="el-icon-s-operation" command="Moderation Tools") {{ $t('dialog.group.actions.moderation_tools') }} + //- template(v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')") + el-dropdown-item(icon="el-icon-s-operation" command="Moderation Tools") {{ $t('dialog.group.actions.moderation_tools') }} template(v-if="groupDialog.ref.myMember && groupDialog.ref.privacy === 'default'") el-dropdown-item(icon="el-icon-view" command="Visibility Everyone" divided) #[i.el-icon-check(v-if="groupDialog.ref.myMember.visibility === 'visible'")] {{ $t('dialog.group.actions.visibility_everyone') }} el-dropdown-item(icon="el-icon-view" command="Visibility Friends") #[i.el-icon-check(v-if="groupDialog.ref.myMember.visibility === 'friends'")] {{ $t('dialog.group.actions.visibility_friends') }} @@ -2935,61 +2935,209 @@ html el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="groupMemberModeration" :visible.sync="groupMemberModeration.visible" :title="$t('dialog.group_member_moderation.header')" width="90vw") div(v-if="groupMemberModeration.visible") h3(v-text="groupMemberModeration.groupRef.name") - div(style="margin-top:10px") - el-button(type="default" @click="loadAllGroupMembers" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) - span(style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupMemberModerationTable.data.length }}/{{ groupMemberModeration.groupRef.memberCount }} - div(style="float:right;margin-top:5px") - span(style="margin-right:5px") {{ $t('dialog.group.members.sort_by') }} - el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="isGroupMembersLoading || groupDialog.memberSearch.length") - el-button(size="mini") - span {{ groupDialog.memberSortOrder.name }} #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - el-dropdown-item(v-for="(item) in groupDialogSortingOptions" v-text="item.name" @click.native="setGroupMemberSortOrder(item)") - span(style="margin-right:5px") {{ $t('dialog.group.members.filter') }} - el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="isGroupMembersLoading || groupDialog.memberSearch.length") - el-button(size="mini") - span {{ groupDialog.memberFilter.name }} #[i.el-icon-arrow-down.el-icon--right] - el-dropdown-menu(#default="dropdown") - el-dropdown-item(v-for="(item) in groupDialogFilterOptions" v-text="item.name" @click.native="setGroupMemberFilter(item)") - el-dropdown-item(v-for="(item) in groupDialog.ref.roles" v-text="item.name" @click.native="setGroupMemberFilter(item)") - el-input(v-model="groupDialog.memberSearch" @input="groupMembersSearch" clearable size="mini" :placeholder="$t('dialog.group.members.search')" style="margin-top:10px;margin-bottom:10px") + el-tabs(type="card" style="height:100%") + el-tab-pane(:label="$t('dialog.group_member_moderation.members')") + div(style="margin-top:10px") + el-button(type="default" @click="loadAllGroupMembers" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) + span(style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupMemberModerationTable.data.length }}/{{ groupMemberModeration.groupRef.memberCount }} + div(style="float:right;margin-top:5px") + span(style="margin-right:5px") {{ $t('dialog.group.members.sort_by') }} + el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="isGroupMembersLoading || groupDialog.memberSearch.length || !hasGroupPermission(groupDialog.ref, 'group-bans-manage')") + el-button(size="mini") + span {{ groupDialog.memberSortOrder.name }} #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(v-for="(item) in groupDialogSortingOptions" v-text="item.name" @click.native="setGroupMemberSortOrder(item)") + span(style="margin-right:5px") {{ $t('dialog.group.members.filter') }} + el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="isGroupMembersLoading || groupDialog.memberSearch.length || !hasGroupPermission(groupDialog.ref, 'group-bans-manage')") + el-button(size="mini") + span {{ groupDialog.memberFilter.name }} #[i.el-icon-arrow-down.el-icon--right] + el-dropdown-menu(#default="dropdown") + el-dropdown-item(v-for="(item) in groupDialogFilterOptions" v-text="item.name" @click.native="setGroupMemberFilter(item)") + el-dropdown-item(v-for="(item) in groupDialog.ref.roles" v-text="item.name" @click.native="setGroupMemberFilter(item)") + el-input(v-model="groupDialog.memberSearch" :disabled="!hasGroupPermission(groupDialog.ref, 'group-bans-manage')" @input="groupMembersSearch" clearable size="mini" :placeholder="$t('dialog.group.members.search')" style="margin-top:10px;margin-bottom:10px") + br + el-button(size="small" @click="selectAllGroupMembers") {{ $t('dialog.group_member_moderation.select_all') }} + data-tables(v-bind="groupMemberModerationTable" style="margin-top:10px") + el-table-column(width="55" prop="$selected" :key="groupMemberModerationTableForceUpdate") + template(v-once #default="scope") + el-button(type="text" size="mini" @click.stop) + el-checkbox(v-model="scope.row.$selected" @change="groupMemberModerationTableSelectionChange(scope.row)") + el-table-column(:label="$t('dialog.group_member_moderation.avatar')" width="70" prop="photo") + template(v-once #default="scope") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.user)") + img.friends-list-avatar(v-lazy="userImageFull(scope.row.user)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.user))") + el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="$displayName" sortable) + template(v-once #default="scope") + span(style="cursor:pointer" @click="showUserDialog(scope.row.userId)") + span(v-if="randomUserColours" v-text="scope.row.user.displayName" :style="{'color':scope.row.user.$userColour}") + span(v-else v-text="scope.row.user.displayName") + el-table-column(:label="$t('dialog.group_member_moderation.roles')" prop="roleIds" sortable) + template(v-once #default="scope") + template(v-for="roleId in scope.row.roleIds" :key="roleId") + span(v-for="(role, rIndex) in groupMemberModeration.groupRef.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") + span(v-if="scope.row.roleIds.indexOf(roleId) < scope.row.roleIds.length - 1") ,  + el-table-column(:label="$t('dialog.group_member_moderation.notes')" prop="managerNotes" sortable) + template(v-once #default="scope") + span(v-text="scope.row.managerNotes" @click.stop) + el-table-column(:label="$t('dialog.group_member_moderation.joined_at')" width="170" prop="joinedAt" sortable) + template(v-once #default="scope") + span {{ scope.row.joinedAt | formatDate('long') }} + el-table-column(:label="$t('dialog.group_member_moderation.visibility')" width="120" prop="visibility" sortable) + template(v-once #default="scope") + span(v-text="scope.row.visibility") + el-tab-pane(:label="$t('dialog.group_member_moderation.bans')" :disabled="!hasGroupPermission(groupDialog.ref, 'group-bans-manage')") + div(style="margin-top:10px") + el-button(type="default" @click="getAllGroupBans(groupMemberModeration.id)" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) + span(style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupBansModerationTable.data.length }} + br + el-input(v-model="groupBansModerationTable.filters[0].value" clearable size="mini" :placeholder="$t('dialog.group.members.search')" style="margin-top:10px;margin-bottom:10px") + br + el-button(size="small" @click="selectAllGroupBans") {{ $t('dialog.group_member_moderation.select_all') }} + data-tables(v-bind="groupBansModerationTable" style="margin-top:10px") + el-table-column(width="55" prop="$selected" :key="groupMemberModerationTableForceUpdate") + template(v-once #default="scope") + el-button(type="text" size="mini" @click.stop) + el-checkbox(v-model="scope.row.$selected" @change="groupMemberModerationTableSelectionChange(scope.row)") + el-table-column(:label="$t('dialog.group_member_moderation.avatar')" width="70" prop="photo") + template(v-once #default="scope") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.user)") + img.friends-list-avatar(v-lazy="userImageFull(scope.row.user)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.user))") + el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="$displayName" sortable) + template(v-once #default="scope") + span(style="cursor:pointer" @click="showUserDialog(scope.row.userId)") + span(v-if="randomUserColours" v-text="scope.row.user.displayName" :style="{'color':scope.row.user.$userColour}") + span(v-else v-text="scope.row.user.displayName") + el-table-column(:label="$t('dialog.group_member_moderation.roles')" prop="roleIds" sortable) + template(v-once #default="scope") + template(v-for="roleId in scope.row.roleIds" :key="roleId") + span(v-for="(role, rIndex) in groupMemberModeration.groupRef.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") + span(v-if="scope.row.roleIds.indexOf(roleId) < scope.row.roleIds.length - 1") ,  + el-table-column(:label="$t('dialog.group_member_moderation.notes')" prop="managerNotes" sortable) + template(v-once #default="scope") + span(v-text="scope.row.managerNotes" @click.stop) + el-table-column(:label="$t('dialog.group_member_moderation.joined_at')" width="170" prop="joinedAt" sortable) + template(v-once #default="scope") + span {{ scope.row.joinedAt | formatDate('long') }} + el-table-column(:label="$t('dialog.group_member_moderation.banned_at')" width="170" prop="joinedAt" sortable) + template(v-once #default="scope") + span {{ scope.row.bannedAt | formatDate('long') }} + el-tab-pane(:label="$t('dialog.group_member_moderation.invites')" :disabled="!hasGroupPermission(groupDialog.ref, 'group-invites-manage')") + div(style="margin-top:10px") + el-button(type="default" @click="getAllGroupInvitesAndJoinRequests(groupMemberModeration.id)" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) + br + el-tabs + el-tab-pane + span(slot="label") + span(v-text="$t('dialog.group_member_moderation.sent_invites')" style="font-weight:bold;font-size:16px") + span(style="color:#909399;font-size:12px;margin-left:5px") {{ groupInvitesModerationTable.data.length }} + el-button(size="small" @click="selectAllGroupInvites") {{ $t('dialog.group_member_moderation.select_all') }} + data-tables(v-bind="groupInvitesModerationTable" style="margin-top:10px") + el-table-column(width="55" prop="$selected" :key="groupMemberModerationTableForceUpdate") + template(v-once #default="scope") + el-button(type="text" size="mini" @click.stop) + el-checkbox(v-model="scope.row.$selected" @change="groupMemberModerationTableSelectionChange(scope.row)") + el-table-column(:label="$t('dialog.group_member_moderation.avatar')" width="70" prop="photo") + template(v-once #default="scope") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.user)") + img.friends-list-avatar(v-lazy="userImageFull(scope.row.user)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.user))") + el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="$displayName" sortable) + template(v-once #default="scope") + span(style="cursor:pointer" @click="showUserDialog(scope.row.userId)") + span(v-if="randomUserColours" v-text="scope.row.user.displayName" :style="{'color':scope.row.user.$userColour}") + span(v-else v-text="scope.row.user.displayName") + el-table-column(:label="$t('dialog.group_member_moderation.notes')" prop="managerNotes" sortable) + template(v-once #default="scope") + span(v-text="scope.row.managerNotes" @click.stop) + br + el-button(@click="groupMembersDeleteSentInvite" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-invites-manage')") {{ $t('dialog.group_member_moderation.delete_sent_invite') }} + el-tab-pane + span(slot="label") + span(v-text="$t('dialog.group_member_moderation.join_requests')" style="font-weight:bold;font-size:16px") + span(style="color:#909399;font-size:12px;margin-left:5px") {{ groupJoinRequestsModerationTable.data.length }} + el-button(size="small" @click="selectAllGroupJoinRequests") {{ $t('dialog.group_member_moderation.select_all') }} + data-tables(v-bind="groupJoinRequestsModerationTable" style="margin-top:10px") + el-table-column(width="55" prop="$selected" :key="groupMemberModerationTableForceUpdate") + template(v-once #default="scope") + el-button(type="text" size="mini" @click.stop) + el-checkbox(v-model="scope.row.$selected" @change="groupMemberModerationTableSelectionChange(scope.row)") + el-table-column(:label="$t('dialog.group_member_moderation.avatar')" width="70" prop="photo") + template(v-once #default="scope") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.user)") + img.friends-list-avatar(v-lazy="userImageFull(scope.row.user)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.user))") + el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="$displayName" sortable) + template(v-once #default="scope") + span(style="cursor:pointer" @click="showUserDialog(scope.row.userId)") + span(v-if="randomUserColours" v-text="scope.row.user.displayName" :style="{'color':scope.row.user.$userColour}") + span(v-else v-text="scope.row.user.displayName") + el-table-column(:label="$t('dialog.group_member_moderation.notes')" prop="managerNotes" sortable) + template(v-once #default="scope") + span(v-text="scope.row.managerNotes" @click.stop) + br + el-button(@click="groupMembersAcceptInviteRequest" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-invites-manage')") {{ $t('dialog.group_member_moderation.accept_join_requests') }} + el-button(@click="groupMembersRejectInviteRequest" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-invites-manage')") {{ $t('dialog.group_member_moderation.reject_join_requests') }} + el-button(@click="groupMembersBlockJoinRequest" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-invites-manage')") {{ $t('dialog.group_member_moderation.block_join_requests') }} + el-tab-pane + span(slot="label") + span(v-text="$t('dialog.group_member_moderation.blocked_requests')" style="font-weight:bold;font-size:16px") + span(style="color:#909399;font-size:12px;margin-left:5px") {{ groupBlockedModerationTable.data.length }} + el-button(size="small" @click="selectAllGroupBlocked") {{ $t('dialog.group_member_moderation.select_all') }} + data-tables(v-bind="groupBlockedModerationTable" style="margin-top:10px") + el-table-column(width="55" prop="$selected" :key="groupMemberModerationTableForceUpdate") + template(v-once #default="scope") + el-button(type="text" size="mini" @click.stop) + el-checkbox(v-model="scope.row.$selected" @change="groupMemberModerationTableSelectionChange(scope.row)") + el-table-column(:label="$t('dialog.group_member_moderation.avatar')" width="70" prop="photo") + template(v-once #default="scope") + el-popover(placement="right" height="500px" trigger="hover") + img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.user)") + img.friends-list-avatar(v-lazy="userImageFull(scope.row.user)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.user))") + el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="$displayName" sortable) + template(v-once #default="scope") + span(style="cursor:pointer" @click="showUserDialog(scope.row.userId)") + span(v-if="randomUserColours" v-text="scope.row.user.displayName" :style="{'color':scope.row.user.$userColour}") + span(v-else v-text="scope.row.user.displayName") + el-table-column(:label="$t('dialog.group_member_moderation.notes')" prop="managerNotes" sortable) + template(v-once #default="scope") + span(v-text="scope.row.managerNotes" @click.stop) + br + el-button(@click="groupMembersDeleteBlockedRequest" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-invites-manage')") {{ $t('dialog.group_member_moderation.delete_blocked_requests') }} + el-tab-pane(:label="$t('dialog.group_member_moderation.logs')" :disabled="!hasGroupPermission(groupDialog.ref, 'group-audit-view')") + div(style="margin-top:10px") + el-button(type="default" @click="getAllGroupLogs(groupMemberModeration.id)" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) + span(style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupLogsModerationTable.data.length }} + br + data-tables(v-bind="groupLogsModerationTable" style="margin-top:10px") + el-table-column(:label="$t('dialog.group_member_moderation.created_at')" width="170" prop="created_at" sortable) + template(v-once #default="scope") + span {{ scope.row.created_at | formatDate('long') }} + el-table-column(:label="$t('dialog.group_member_moderation.type')" width="190" prop="eventType" sortable) + template(v-once #default="scope") + span(v-text="scope.row.eventType") + el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="actorDisplayName" sortable) + template(v-once #default="scope") + span(style="cursor:pointer" @click="showUserDialog(scope.row.actorId)") + span(v-text="scope.row.actorDisplayName") + el-table-column(:label="$t('dialog.group_member_moderation.description')" prop="description") + template(v-once #default="scope") + span(v-text="scope.row.description") + el-table-column(:label="$t('dialog.group_member_moderation.data')" prop="data") + template(v-once #default="scope") + span(v-if="Object.keys(scope.row.data).length" v-text="JSON.stringify(scope.row.data)") br - el-button(size="small" @click="selectAllGroupMembers") {{ $t('dialog.group_member_moderation.select_all') }} - data-tables(v-bind="groupMemberModerationTable" style="margin-top:10px") - el-table-column(width="55" prop="$selected" :key="groupMemberModerationTableForceUpdate") - template(v-once #default="scope") - el-button(type="text" size="mini" @click.stop) - el-checkbox(v-model="scope.row.$selected" @change="groupMemberModerationTableSelectionChange(scope.row)") - el-table-column(:label="$t('dialog.group_member_moderation.avatar')" width="70" prop="photo") - template(v-once #default="scope") - el-popover(placement="right" height="500px" trigger="hover") - img.friends-list-avatar(slot="reference" v-lazy="userImage(scope.row.user)") - img.friends-list-avatar(v-lazy="userImageFull(scope.row.user)" style="height:500px;cursor:pointer" @click="showFullscreenImageDialog(userImageFull(scope.row.user))") - el-table-column(:label="$t('dialog.group_member_moderation.display_name')" width="160" prop="displayName" sortable :sort-method="(a, b) => sortAlphabetically(a.user, b.user, 'displayName')") - template(v-once #default="scope") - span(style="cursor:pointer" @click="showUserDialog(scope.row.userId)") - span(v-if="randomUserColours" v-text="scope.row.user.displayName" :style="{'color':scope.row.user.$userColour}") - span(v-else v-text="scope.row.user.displayName") - el-table-column(:label="$t('dialog.group_member_moderation.roles')" prop="roleIds" sortable) - template(v-once #default="scope") - template(v-for="roleId in scope.row.roleIds" :key="roleId") - span(v-for="(role, rIndex) in groupMemberModeration.groupRef.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") - span(v-if="scope.row.roleIds.indexOf(roleId) < scope.row.roleIds.length - 1") ,  - el-table-column(:label="$t('dialog.group_member_moderation.notes')" prop="managerNotes" sortable) - template(v-once #default="scope") - span(v-text="scope.row.managerNotes" @click.stop) - el-table-column(:label="$t('dialog.group_member_moderation.joined_at')" width="170" prop="joinedAt" sortable) - template(v-once #default="scope") - span {{ scope.row.joinedAt | formatDate('long') }} - el-table-column(:label="$t('dialog.group_member_moderation.visibility')" width="120" prop="visibility" sortable) - template(v-once #default="scope") - span(v-text="scope.row.visibility") + span.name {{ $t('dialog.group_member_moderation.user_id') }} + br + el-input(v-model="groupMemberModeration.selectUserId" size="mini" style="margin-top:5px;width:340px" :placeholder="$t('dialog.group_member_moderation.user_id_placeholder')" clearable) + el-button(size="small" @click="selectGroupMemberUserId" :disabled="!groupMemberModeration.selectUserId") {{ $t('dialog.group_member_moderation.select_user') }} br span.name {{ $t('dialog.group_member_moderation.selected_users') }} el-button(type="default" @click="clearSelectedGroupMembers" size="mini" icon="el-icon-delete" circle style="margin-left:5px") br el-tag(v-for="user in groupMemberModeration.selectedUsersArray" type="info" disable-transitions="true" :key="user.id" style="margin-right:5px;margin-top:5px" closable @close="deleteSelectedGroupMember(user)") - span {{ user.user.displayName }} + span {{ user.user?.displayName }} #[i.el-icon-warning(v-if="user.membershipStatus !== 'member'" style="margin-left:5px")] br br span.name {{ $t('dialog.group_member_moderation.notes') }} @@ -3005,11 +3153,12 @@ html br span.name {{ $t('dialog.group_member_moderation.actions') }} br - el-button(@click="groupMembersAddRoles" :disabled="!groupMemberModeration.selectedRoles.length") {{ $t('dialog.group_member_moderation.add_roles') }} - el-button(@click="groupMembersRemoveRoles" :disabled="!groupMemberModeration.selectedRoles.length") {{ $t('dialog.group_member_moderation.remove_roles') }} - el-button(@click="groupMembersSaveNote" :disabled="groupMemberModeration.progressCurrent") {{ $t('dialog.group_member_moderation.save_note') }} - el-button(@click="groupMembersKick" :disabled="groupMemberModeration.progressCurrent") {{ $t('dialog.group_member_moderation.kick') }} - el-button(@click="groupMembersBan" :disabled="groupMemberModeration.progressCurrent") {{ $t('dialog.group_member_moderation.ban') }} + el-button(@click="groupMembersAddRoles" :disabled="!groupMemberModeration.selectedRoles.length || groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-roles-assign')") {{ $t('dialog.group_member_moderation.add_roles') }} + el-button(@click="groupMembersRemoveRoles" :disabled="!groupMemberModeration.selectedRoles.length || groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-roles-assign')") {{ $t('dialog.group_member_moderation.remove_roles') }} + el-button(@click="groupMembersSaveNote" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-members-manage')") {{ $t('dialog.group_member_moderation.save_note') }} + el-button(@click="groupMembersKick" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-members-remove')") {{ $t('dialog.group_member_moderation.kick') }} + el-button(@click="groupMembersBan" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-bans-manage')") {{ $t('dialog.group_member_moderation.ban') }} + el-button(@click="groupMembersUnban" :disabled="groupMemberModeration.progressCurrent || !hasGroupPermission(groupDialog.ref, 'group-bans-manage')") {{ $t('dialog.group_member_moderation.unban') }} span(v-if="groupMemberModeration.progressCurrent" style="margin-top:10px") #[i.el-icon-loading(style="margin-left:5px;margin-right:5px")] {{ $t('dialog.group_member_moderation.progress') }} {{ groupMemberModeration.progressCurrent }}/{{ groupMemberModeration.progressTotal }} el-button(v-if="groupMemberModeration.progressCurrent" @click="groupMemberModeration.progressTotal = 0" style="margin-left:5px") {{ $t('dialog.group_member_moderation.cancel') }} diff --git a/html/src/localization/en/en.json b/html/src/localization/en/en.json index c809af335..689643872 100644 --- a/html/src/localization/en/en.json +++ b/html/src/localization/en/en.json @@ -1288,20 +1288,41 @@ }, "group_member_moderation": { "header": "Group Member Moderation", + "members": "Members", + "bans": "Bans", + "invites": "Invites", + "logs": "Logs", + "sent_invites": "Sent Invites", + "join_requests": "Join Requests", + "blocked_requests": "Blocked Requests", "notes": "Manager Note", "note_placeholder": "Click to add a note", "actions": "Actions", "kick": "Kick", "ban": "Ban", + "unban": "Unban", "save_note": "Save Note", + "delete_sent_invite": "Delete Sent Invite", + "delete_blocked_requests": "Delete Blocked Requests", + "accept_join_requests": "Accept Join Requests", + "reject_join_requests": "Reject Join Requests", + "block_join_requests": "Block Join Requests", "group_members": "Group Members", "progress": "Progress:", "display_name": "Display Name", "visibility": "Visibility", "avatar": "Avatar", "joined_at": "Joined At", + "banned_at": "Banned At", + "created_at": "Created At", + "type": "Type", + "description": "Description", + "data": "Data", "note": "Note", "roles": "Roles", + "user_id": "User ID", + "user_id_placeholder": "Enter User ID(s)", + "select_user": "Select User", "selected_users": "Selected Users", "select_all": "Select All", "cancel": "Cancel", diff --git a/html/src/repository/database.js b/html/src/repository/database.js index 9956b2bf0..c914b4c09 100644 --- a/html/src/repository/database.js +++ b/html/src/repository/database.js @@ -2482,7 +2482,7 @@ class Database { async fixBrokenGroupChange() { await sqliteService.executeNonQuery( - `DELETE FROM ${Database.userPrefix}_notifications WHERE type = 'groupChange'` + `DELETE FROM ${Database.userPrefix}_notifications WHERE type = 'groupChange' AND created_at < '2024-04-23T03:00:00.000Z'` ); }