diff --git a/html/src/app.js b/html/src/app.js index 705c935f..36fd46eb 100644 --- a/html/src/app.js +++ b/html/src/app.js @@ -26901,10 +26901,11 @@ speechSynthesis.getVoices(); groupId: string, params: { visibility: string, - isSubscribedToAnnouncements: bool + isSubscribedToAnnouncements: bool, + managerNotes: string } */ - API.setGroupProps = function (userId, groupId, params) { + API.setGroupMemberProps = function (userId, groupId, params) { return this.call(`groups/${groupId}/members/${userId}`, { method: 'PUT', params @@ -26915,12 +26916,15 @@ speechSynthesis.getVoices(); groupId, params }; - this.$emit('GROUP:PROPS', args); + this.$emit('GROUP:MEMBER:PROPS', args); return args; }); }; - API.$on('GROUP:PROPS', function (args) { + API.$on('GROUP:MEMBER:PROPS', function (args) { + if (args.userId !== this.currentUser.id) { + return; + } var json = args.json; json.$memberId = json.id; json.id = json.groupId; @@ -26945,6 +26949,113 @@ speechSynthesis.getVoices(); }); }); + API.$on('GROUP:MEMBER:PROPS', function (args) { + if ($app.groupDialog.id === args.json.groupId) { + for (var i = 0; i < $app.groupDialog.members.length; ++i) { + var member = $app.groupDialog.members[i]; + if (member.userId === args.json.userId) { + Object.assign(member, this.applyGroupMember(args.json)); + break; + } + } + for ( + var i = 0; + i < $app.groupDialog.memberSearchResults.length; + ++i + ) { + var member = $app.groupDialog.memberSearchResults[i]; + if (member.userId === args.json.userId) { + Object.assign(member, this.applyGroupMember(args.json)); + break; + } + } + } + if ( + $app.groupMemberModeration.visible && + $app.groupMemberModeration.id === args.json.groupId + ) { + // force redraw table + $app.groupMembersSearch(); + } + }); + + /* + params: { + userId: string, + groupId: string, + roleId: string + } + */ + API.addGroupMemberRole = function (params) { + return this.call( + `groups/${params.groupId}/members/${params.userId}/roles/${params.roleId}`, + { + method: 'PUT' + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:MEMBER:ROLE:CHANGE', args); + return args; + }); + }; + + /* + params: { + userId: string, + groupId: string, + roleId: string + } + */ + API.removeGroupMemberRole = function (params) { + return this.call( + `groups/${params.groupId}/members/${params.userId}/roles/${params.roleId}`, + { + method: 'DELETE' + } + ).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:MEMBER:ROLE:CHANGE', args); + return args; + }); + }; + + API.$on('GROUP:MEMBER:ROLE:CHANGE', function (args) { + if ($app.groupDialog.id === args.params.groupId) { + for (var i = 0; i < $app.groupDialog.members.length; ++i) { + var member = $app.groupDialog.members[i]; + if (member.userId === args.params.userId) { + member.roleIds = args.json; + break; + } + } + for ( + var i = 0; + i < $app.groupDialog.memberSearchResults.length; + ++i + ) { + var member = $app.groupDialog.memberSearchResults[i]; + if (member.userId === args.params.userId) { + member.roleIds = args.json; + break; + } + } + } + + if ( + $app.groupMemberModeration.visible && + $app.groupMemberModeration.id === args.params.groupId + ) { + // force redraw table + $app.groupMembersSearch(); + } + }); + /* params: { groupId: string @@ -27065,6 +27176,39 @@ speechSynthesis.getVoices(); args.ref = this.applyGroupMember(args.json); }); + /* + params: { + groupId: string, + query: string, + n: number, + offset: number + } + */ + API.getGroupMembersSearch = function (params) { + return this.call(`groups/${params.groupId}/members/search`, { + method: 'GET', + params + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:MEMBERS:SEARCH', args); + return args; + }); + }; + + API.$on('GROUP:MEMBERS:SEARCH', function (args) { + for (var json of args.json.results) { + this.$emit('GROUP:MEMBER', { + json, + params: { + groupId: args.params.groupId + } + }); + } + }); + /* params: { groupId: string, @@ -27087,6 +27231,47 @@ speechSynthesis.getVoices(); }); }; + /* + params: { + groupId: string, + userId: string + } + */ + API.kickGroupMember = function (params) { + return this.call(`groups/${params.groupId}/members/${params.userId}`, { + method: 'DELETE' + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:MEMBER:KICK', args); + return args; + }); + }; + + /* + params: { + groupId: string, + userId: string + } + */ + API.banGroupMember = function (params) { + return this.call(`groups/${params.groupId}/bans`, { + method: 'POST', + params: { + userId: params.userId + } + }).then((json) => { + var args = { + json, + params + }; + this.$emit('GROUP:MEMBER:BAN', args); + return args; + }); + }; + /* params: { groupId: string @@ -27397,6 +27582,8 @@ speechSynthesis.getVoices(); posts: [], postsFiltered: [], members: [], + memberSearch: '', + memberSearchResults: [], instances: [], memberRoles: [], memberFilter: $app.data.groupDialogFilterOptions.everyone, @@ -27409,6 +27596,12 @@ speechSynthesis.getVoices(); if (!groupId) { return; } + if ( + this.groupMemberModeration.visible && + this.groupMemberModeration.id !== groupId + ) { + this.groupMemberModeration.visible = false; + } this.$nextTick(() => adjustDialogZ(this.$refs.groupDialog.$el)); var D = this.groupDialog; D.visible = true; @@ -27422,6 +27615,8 @@ speechSynthesis.getVoices(); D.postsFiltered = []; D.instances = []; D.memberRoles = []; + D.memberSearch = ''; + D.memberSearchResults = []; if (this.groupDialogLastGallery !== groupId) { D.galleries = {}; } @@ -27553,6 +27748,9 @@ speechSynthesis.getVoices(); case 'Refresh': this.showGroupDialog(D.id); break; + case 'Moderation Tools': + this.showGroupMemberModerationDialog(D.id); + break; case 'Leave Group': this.leaveGroup(D.id); break; @@ -27664,7 +27862,7 @@ speechSynthesis.getVoices(); }; $app.methods.setGroupVisibility = function (groupId, visibility) { - return API.setGroupProps(API.currentUser.id, groupId, { + return API.setGroupMemberProps(API.currentUser.id, groupId, { visibility }).then((args) => { this.$message({ @@ -27676,7 +27874,7 @@ speechSynthesis.getVoices(); }; $app.methods.setGroupSubscription = function (groupId, subscribe) { - return API.setGroupProps(API.currentUser.id, groupId, { + return API.setGroupMemberProps(API.currentUser.id, groupId, { isSubscribedToAnnouncements: subscribe }).then((args) => { this.$message({ @@ -27710,6 +27908,13 @@ speechSynthesis.getVoices(); }; $app.methods.onGroupJoined = function (groupId) { + if ( + this.groupMemberModeration.visible && + this.groupMemberModeration.id === groupId + ) { + // ignore this event if we were the one to trigger it + return; + } if (this.groupDialog.visible && this.groupDialog.id === groupId) { this.showGroupDialog(groupId); } @@ -27732,6 +27937,60 @@ speechSynthesis.getVoices(); API.currentUserGroups.delete(groupId); }; + // group search + + $app.methods.groupMembersSearchDebounce = function () { + var D = this.groupDialog; + var search = D.memberSearch; + D.memberSearchResults = []; + if (!search || search.length < 3) { + this.setGroupMemberModerationTable(D.members); + return; + } + this.isGroupMembersLoading = true; + API.getGroupMembersSearch({ + groupId: D.id, + query: search, + n: 100, + offset: 0 + }) + .then((args) => { + if (D.id === args.params.groupId) { + D.memberSearchResults = args.json.results; + this.setGroupMemberModerationTable(args.json.results); + } + }) + .finally(() => { + this.isGroupMembersLoading = false; + }); + }; + + $app.data.groupMembersSearchTimer = null; + $app.data.groupMembersSearchPending = false; + $app.methods.groupMembersSearch = function () { + if (this.groupMembersSearchTimer) { + this.groupMembersSearchPending = true; + } else { + this.groupMembersSearchExecute(); + this.groupMembersSearchTimer = setTimeout(() => { + if (this.groupMembersSearchPending) { + this.groupMembersSearchExecute(); + } + this.groupMembersSearchTimer = null; + }, 500); + } + }; + + $app.methods.groupMembersSearchExecute = function () { + try { + this.groupMembersSearchDebounce(); + } catch (err) { + console.error(err); + } + this.groupMembersSearchTimer = null; + this.groupMembersSearchPending = false; + }; + // group posts $app.methods.updateGroupPostSearch = function () { @@ -27796,6 +28055,7 @@ speechSynthesis.getVoices(); } var D = this.groupDialog; var params = this.loadMoreGroupMembersParams; + D.memberSearch = ''; this.isGroupMembersLoading = true; await API.getGroupMembers(params) .finally(() => { @@ -27819,6 +28079,7 @@ speechSynthesis.getVoices(); this.isGroupMembersDone = true; } D.members = [...D.members, ...args.json]; + this.setGroupMemberModerationTable(D.members); params.offset += params.n; return args; }) @@ -28785,9 +29046,346 @@ speechSynthesis.getVoices(); ); }; + // #endregion + // #region | Dialog: group member moderation + + $app.data.groupMemberModeration = { + visible: false, + loading: false, + id: '', + groupRef: {}, + note: '', + selectedUsers: new Map(), + selectedUsersArray: [], + selectedRoles: [], + progressCurrent: 0, + progressTotal: 0 + }; + + $app.data.groupMemberModerationTable = { + 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] + }, + key: 0 + }; + + $app.methods.setGroupMemberModerationTable = function (data) { + if (!this.groupMemberModeration.visible) { + return; + } + for (var i = 0; i < data.length; i++) { + var member = data[i]; + member.$selected = this.groupMemberModeration.selectedUsers.has( + member.userId + ); + } + this.groupMemberModerationTable.data = data; + // force redraw + this.groupMemberModerationTable.key++; + }; + + $app.methods.showGroupMemberModerationDialog = function (groupId) { + this.$nextTick(() => + adjustDialogZ(this.$refs.groupMemberModeration.$el) + ); + if (groupId !== this.groupDialog.id) { + return; + } + var D = this.groupMemberModeration; + D.id = groupId; + D.selectedUsers.clear(); + D.selectedUsersArray = []; + D.selectedRoles = []; + D.groupRef = {}; + API.getCachedGroup({ groupId }).then((args) => { + D.groupRef = args.ref; + }); + this.groupMemberModerationTable.key = 0; + D.visible = true; + this.setGroupMemberModerationTable(this.groupDialog.members); + }; + + $app.methods.groupMemberModerationTableSelectionChange = function (row) { + var D = this.groupMemberModeration; + if (row.$selected && !D.selectedUsers.has(row.userId)) { + D.selectedUsers.set(row.userId, row); + } else if (!row.$selected && D.selectedUsers.has(row.userId)) { + D.selectedUsers.delete(row.userId); + } + D.selectedUsersArray = Array.from(D.selectedUsers.values()); + // force redraw + this.groupMemberModerationTable.key++; + }; + + $app.methods.deleteSelectedGroupMember = function (user) { + var D = this.groupMemberModeration; + D.selectedUsers.delete(user.userId); + D.selectedUsersArray = Array.from(D.selectedUsers.values()); + for (var i = 0; i < this.groupMemberModerationTable.data.length; i++) { + var row = this.groupMemberModerationTable.data[i]; + if (row.userId === user.userId) { + row.$selected = false; + break; + } + } + // force redraw + this.groupMemberModerationTable.key++; + }; + + $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; + } + // force redraw + this.groupMemberModerationTable.key++; + }; + + $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.groupMemberModerationTable.key++; + }; + + $app.methods.groupMembersKick = 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.kickGroupMember({ + groupId: D.id, + userId: user.userId + }); + console.log(`Kicking ${user.userId} ${i + 1}/${memberCount}`); + } + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to kick group member: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }; + + $app.methods.groupMembersBan = 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.banGroupMember({ + groupId: D.id, + userId: user.userId + }); + console.log(`Banning ${user.userId} ${i + 1}/${memberCount}`); + } + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to ban group member: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }; + + $app.methods.groupMembersSaveNote = 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.managerNotes === D.note) { + continue; + } + await API.setGroupMemberProps(user.userId, D.id, { + managerNotes: D.note + }); + console.log( + `Setting note ${D.note} ${user.userId} ${ + i + 1 + }/${memberCount}` + ); + } + this.$message({ + message: 'Note saved', + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to set group member note: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }; + + $app.methods.groupMembersAddRoles = 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; + var rolesToAdd = []; + D.selectedRoles.forEach((roleId) => { + if (!user.roleIds.includes(roleId)) { + rolesToAdd.push(roleId); + } + }); + + if (!rolesToAdd.length) { + continue; + } + for (var j = 0; j < rolesToAdd.length; j++) { + var roleId = rolesToAdd[j]; + console.log( + `Adding role: ${roleId} ${user.userId} ${ + i + 1 + }/${memberCount}` + ); + await API.addGroupMemberRole({ + groupId: D.id, + userId: user.userId, + roleId + }); + } + } + this.$message({ + message: 'Added group member roles', + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to add group member roles: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }; + + $app.methods.groupMembersRemoveRoles = 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; + var rolesToRemove = []; + D.selectedRoles.forEach((roleId) => { + if (user.roleIds.includes(roleId)) { + rolesToRemove.push(roleId); + } + }); + if (!rolesToRemove.length) { + continue; + } + for (var j = 0; j < rolesToRemove.length; j++) { + var roleId = rolesToRemove[j]; + console.log( + `Removing role ${roleId} ${user.userId} ${ + i + 1 + }/${memberCount}` + ); + await API.removeGroupMemberRole({ + groupId: D.id, + userId: user.userId, + roleId + }); + } + } + this.$message({ + message: 'Roles removed', + type: 'success' + }); + } catch (err) { + console.error(err); + this.$message({ + message: `Failed to remove group member roles: ${err}`, + type: 'error' + }); + } finally { + D.progressCurrent = 0; + D.progressTotal = 0; + } + }; + // #endregion $app = new Vue($app); window.$app = $app; })(); // #endregion + +// // #endregion +// // #region | Dialog: templateDialog + +// $app.data.templateDialog = { +// visible: false, +// }; + +// $app.methods.showTemplateDialog = function () { +// this.$nextTick(() => adjustDialogZ(this.$refs.templateDialog.$el)); +// var D = this.templateDialog; +// D.visible = true; +// }; + +// // #endregion diff --git a/html/src/index.pug b/html/src/index.pug index 8df85ffe..85f56823 100644 --- a/html/src/index.pug +++ b/html/src/index.pug @@ -441,11 +441,11 @@ html .detail span.name(v-if="userDialog.unFriended") {{ $t('dialog.user.info.unfriended') }} el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") - i.el-icon-warning - span.name(v-else) {{ $t('dialog.user.info.friended') }} - el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") - i.el-icon-warning - span.extra {{ userDialog.dateFriended | formatDate('long') }} + i.el-icon-warning + span.name(v-else) {{ $t('dialog.user.info.friended') }} + el-tooltip(v-if="!hideTooltips" placement="top" style="margin-left:5px" :content="$t('dialog.user.info.accuracy_notice')") + i.el-icon-warning + span.extra {{ userDialog.dateFriended | formatDate('long') }} template(v-if="API.currentUser.id === userDialog.id") .x-friend-item(@click="toggleAvatarCopying") .detail @@ -985,6 +985,8 @@ html el-dropdown-item(v-if="groupDialog.ref.myMember.isSubscribedToAnnouncements" icon="el-icon-close" command="Unsubscribe To Announcements" divided) {{ $t('dialog.group.actions.unsubscribe') }} el-dropdown-item(v-else icon="el-icon-check" command="Subscribe To Announcements" divided) {{ $t('dialog.group.actions.subscribe') }} 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-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') }} @@ -1134,25 +1136,52 @@ html template(v-if="groupDialog.visible") span(v-if="hasGroupPermission(groupDialog.ref, 'group-members-viewall')" style="font-weight:bold;font-size:16px") {{ $t('dialog.group.members.all_members') }} span(v-else style="font-weight:bold;font-size:16px") {{ $t('dialog.group.members.friends_only') }} - br - el-button(type="default" @click="loadAllGroupMembers" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) - el-button(type="default" @click="downloadAndSaveJson(`${groupDialog.id}_members`, groupDialog.members)" size="mini" icon="el-icon-download" circle style="margin-left:5px") - span(style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupDialog.members.length }}/{{ groupDialog.ref.memberCount }} - div(v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')" style="float:right") - 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") - 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") - 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.memberRoles" v-text="item.name" @click.native="setGroupMemberFilter(item)") - ul.infinite-list.x-friend-list(v-if="groupDialog.members.length > 0" v-infinite-scroll="loadMoreGroupMembers" style="margin-top:10px;overflow:auto;max-height:250px;min-width:130px") + div(style="margin-top:10px") + el-button(type="default" @click="loadAllGroupMembers" size="mini" icon="el-icon-refresh" :loading="isGroupMembersLoading" circle) + el-button(type="default" @click="downloadAndSaveJson(`${groupDialog.id}_members`, groupDialog.members)" size="mini" icon="el-icon-download" circle style="margin-left:5px") + span(v-if="groupDialog.memberSearch.length" style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupDialog.memberSearchResults.length }}/{{ groupDialog.ref.memberCount }} + span(v-else style="font-size:14px;margin-left:5px;margin-right:5px") {{ groupDialog.members.length }}/{{ groupDialog.ref.memberCount }} + div(v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')" style="float:right") + 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") + .x-friend-list(v-if="groupDialog.memberSearch.length" v-loading="isGroupMembersLoading" style="margin-top:10px;overflow:auto;max-height:250px;min-width:130px") + .x-friend-item(v-for="user in groupDialog.memberSearchResults" :key="user.id" @click="showUserDialog(user.userId)" class="x-friend-item-border") + .avatar + img(v-lazy="userImage(user.user)") + .detail + span.name(v-text="user.user.displayName" :style="{'color':user.user.$userColour}") + span.extra + template(v-if="hasGroupPermission(groupDialog.ref, 'group-members-manage')") + el-tooltip(v-if="user.isRepresenting" placement="top" :content="$t('dialog.group.members.representing')") + i.el-icon-collection-tag(style="margin-right:5px") + el-tooltip(v-if="user.visibility !== 'visible'" placement="top") + template(#content) + span {{ $t('dialog.group.members.visibility') }} {{ user.visibility }} + i.el-icon-view(style="margin-right:5px") + el-tooltip(v-if="!user.isSubscribedToAnnouncements" placement="top" :content="$t('dialog.group.members.unsubscribed_announcements')") + i.el-icon-chat-line-square(style="margin-right:5px") + el-tooltip(v-if="user.managerNotes" placement="top") + template(#content) + span {{ $t('dialog.group.members.manager_notes') }} + br + span {{ user.managerNotes }} + i.el-icon-edit-outline(style="margin-right:5px") + template(v-for="roleId in user.roleIds" :key="roleId") + span(v-for="(role, rIndex) in groupDialog.ref.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") + span(v-if="user.roleIds.indexOf(roleId) < user.roleIds.length - 1") ,  + ul.infinite-list.x-friend-list(v-else-if="groupDialog.members.length > 0" v-infinite-scroll="loadMoreGroupMembers" style="margin-top:10px;overflow:auto;max-height:250px;min-width:130px") li.infinite-list-item.x-friend-item(v-for="user in groupDialog.members" :key="user.id" @click="showUserDialog(user.userId)" class="x-friend-item-border") .avatar img(v-lazy="userImage(user.user)") @@ -1168,6 +1197,12 @@ html i.el-icon-view(style="margin-right:5px") el-tooltip(v-if="!user.isSubscribedToAnnouncements" placement="top" :content="$t('dialog.group.members.unsubscribed_announcements')") i.el-icon-chat-line-square(style="margin-right:5px") + el-tooltip(v-if="user.managerNotes" placement="top") + template(#content) + span {{ $t('dialog.group.members.manager_notes') }} + br + span {{ user.managerNotes }} + i.el-icon-edit-outline(style="margin-right:5px") template(v-for="roleId in user.roleIds" :key="roleId") span(v-for="(role, rIndex) in groupDialog.ref.roles" :key="rIndex" v-if="role.id === roleId" v-text="role.name") span(v-if="user.roleIds.indexOf(roleId) < user.roleIds.length - 1") ,  @@ -2799,6 +2834,91 @@ html el-tooltip(placement="top" :content="$t('dialog.registry_backup.delete')" :disabled="hideTooltips") el-button(type="text" icon="el-icon-delete" size="mini" @click="deleteVrcRegistryBackup(scope.row)") + //- dialog: group moderation + 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") + 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="groupMemberModerationTable.key") + 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") + 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 }} + br + br + span.name {{ $t('dialog.group_member_moderation.notes') }} + el-input.extra(v-model="groupMemberModeration.note" type="textarea" :rows="2" :autosize="{ minRows: 1, maxRows: 20 }" :placeholder="$t('dialog.group_member_moderation.note_placeholder')" size="mini" resize="none") + br + span.name {{ $t('dialog.group_member_moderation.selected_roles') }} + br + el-select(v-model="groupMemberModeration.selectedRoles" clearable multiple :placeholder="$t('dialog.group_member_moderation.choose_roles_placeholder')" filterable style="margin-top:5px") + el-option-group(:label="$t('dialog.group_member_moderation.roles')") + el-option.x-friend-item(v-for="role in groupMemberModeration.groupRef.roles" :key="role.id" :label="role.name" :value="role.id" style="height:auto") + .detail + span.name(v-text="role.name") + 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') }} + 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') }} + + + //- el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="templateDialog" :visible.sync="templateDialog.visible" :title="$t('dialog.template_dialog.header')" width="450px") + //- dialog: open source software notice el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="ossDialog" :title="$t('dialog.open_source.header')" width="650px") div(v-if="ossDialog" style="height:350px;overflow:hidden scroll;word-break:break-all") diff --git a/html/src/localization/en/en.json b/html/src/localization/en/en.json index 52152981..5286a3fe 100644 --- a/html/src/localization/en/en.json +++ b/html/src/localization/en/en.json @@ -802,6 +802,7 @@ "visibility_everyone": "Visibility Everyone", "visibility_friends": "Visibility Friends", "visibility_hidden": "Visibility Hidden", + "moderation_tools": "Moderation Tools", "leave": "Leave Group" }, "info": { @@ -850,7 +851,9 @@ }, "unsubscribed_announcements": "Unsubscribed from announcements", "visibility": "Visibility:", - "representing": "Representing" + "representing": "Representing", + "manager_notes": "Manager Notes:", + "search": "Search" }, "gallery": { "header": "Photos" @@ -1246,6 +1249,30 @@ "action": "Action", "auto_backup": "Weekly Auto Backup", "restore_prompt": "VRCX has noticed auto backup of VRC registry settings is enabled but this computer dosn't have any, if you'd like to restore from backup you can do so from here." + }, + "group_member_moderation": { + "header": "Group Member Moderation", + "notes": "Manager Note", + "note_placeholder": "Click to add a note", + "actions": "Actions", + "kick": "Kick", + "ban": "Ban", + "save_note": "Save Note", + "group_members": "Group Members", + "progress": "Progress:", + "display_name": "Display Name", + "visibility": "Visibility", + "avatar": "Avatar", + "joined_at": "Joined At", + "note": "Note", + "roles": "Roles", + "selected_users": "Selected Users", + "select_all": "Select All", + "cancel": "Cancel", + "choose_roles_placeholder": "Choose Roles", + "selected_roles": "Selected Roles", + "remove_roles": "Remove Roles", + "add_roles": "Add Roles" } }, "prompt": {