mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-18 06:13:52 +02:00
Group member moderation dialog, group member search
This commit is contained in:
610
html/src/app.js
610
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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user