diff --git a/html/src/app.js b/html/src/app.js
index 6ce17755..a161da40 100644
--- a/html/src/app.js
+++ b/html/src/app.js
@@ -9362,6 +9362,7 @@ speechSynthesis.getVoices();
D.dateFriended = '';
D.unFriended = false;
D.dateFriendedInfo = [];
+ this.userDialogGroupEditMode = false;
if (userId === API.currentUser.id) {
this.getWorldName(API.currentUser.homeLocation).then(
(worldName) => {
@@ -16981,9 +16982,37 @@ speechSynthesis.getVoices();
this.saveCurrentUserGroups();
};
+ $app.data.inGameGroupOrder = [];
+
+ $app.methods.updateInGameGroupOrder = async function () {
+ this.inGameGroupOrder = [];
+ try {
+ var json = await AppApi.GetVRChatRegistryKey(
+ `VRC_GROUP_ORDER_${API.currentUser.id}`
+ );
+ this.inGameGroupOrder = JSON.parse(json);
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ $app.methods.sortGroupsByInGame = function (a, b) {
+ var aIndex = this.inGameGroupOrder.indexOf(a?.id);
+ var bIndex = this.inGameGroupOrder.indexOf(b?.id);
+ if (aIndex === -1 && bIndex === -1) {
+ return 0;
+ }
+ if (aIndex === -1) {
+ return 1;
+ }
+ if (bIndex === -1) {
+ return -1;
+ }
+ return aIndex - bIndex;
+ };
+
$app.methods.sortCurrentUserGroups = async function () {
var D = this.userDialog;
- var inGameGroupList = [];
var sortMethod = function () {};
switch (D.groupSorting.value) {
@@ -16994,28 +17023,8 @@ speechSynthesis.getVoices();
sortMethod = compareByMemberCount;
break;
case 'inGame':
- sortMethod = function (a, b) {
- var aIndex = inGameGroupList.indexOf(a?.id);
- var bIndex = inGameGroupList.indexOf(b?.id);
- if (aIndex === -1 && bIndex === -1) {
- return 0;
- }
- if (aIndex === -1) {
- return 1;
- }
- if (bIndex === -1) {
- return -1;
- }
- return aIndex - bIndex;
- };
- try {
- var json = await AppApi.GetVRChatRegistryKey(
- `VRC_GROUP_ORDER_${API.currentUser.id}`
- );
- inGameGroupList = JSON.parse(json);
- } catch (err) {
- console.error(err);
- }
+ sortMethod = this.sortGroupsByInGame;
+ await this.updateInGameGroupOrder();
break;
}
@@ -17024,6 +17033,77 @@ speechSynthesis.getVoices();
this.userGroups.remainingGroups.sort(sortMethod);
};
+ $app.data.userDialogGroupEditMode = false;
+ $app.data.userDialogGroupEditGroups = [];
+
+ $app.methods.editModeCurrentUserGroups = async function () {
+ await this.updateInGameGroupOrder();
+ this.userDialogGroupEditGroups = Array.from(
+ API.currentUserGroups.values()
+ );
+ this.userDialogGroupEditGroups.sort(this.sortGroupsByInGame);
+ this.userDialogGroupEditMode = true;
+ };
+
+ $app.methods.exitEditModeCurrentUserGroups = async function () {
+ this.userDialogGroupEditMode = false;
+ this.userDialogGroupEditGroups = [];
+ await this.sortCurrentUserGroups();
+ };
+
+ $app.methods.moveGroupUp = function (groupId) {
+ var index = this.inGameGroupOrder.indexOf(groupId);
+ if (index > 0) {
+ this.inGameGroupOrder.splice(index, 1);
+ this.inGameGroupOrder.splice(index - 1, 0, groupId);
+ this.saveInGameGroupOrder();
+ }
+ };
+
+ $app.methods.moveGroupDown = function (groupId) {
+ var index = this.inGameGroupOrder.indexOf(groupId);
+ if (index < this.inGameGroupOrder.length - 1) {
+ this.inGameGroupOrder.splice(index, 1);
+ this.inGameGroupOrder.splice(index + 1, 0, groupId);
+ this.saveInGameGroupOrder();
+ }
+ };
+
+ $app.methods.moveGroupTop = function (groupId) {
+ var index = this.inGameGroupOrder.indexOf(groupId);
+ if (index > 0) {
+ this.inGameGroupOrder.splice(index, 1);
+ this.inGameGroupOrder.unshift(groupId);
+ this.saveInGameGroupOrder();
+ }
+ };
+
+ $app.methods.moveGroupBottom = function (groupId) {
+ var index = this.inGameGroupOrder.indexOf(groupId);
+ if (index < this.inGameGroupOrder.length - 1) {
+ this.inGameGroupOrder.splice(index, 1);
+ this.inGameGroupOrder.push(groupId);
+ this.saveInGameGroupOrder();
+ }
+ };
+
+ $app.methods.saveInGameGroupOrder = async function () {
+ this.userDialogGroupEditGroups.sort(this.sortGroupsByInGame);
+ try {
+ await AppApi.SetVRChatRegistryKey(
+ `VRC_GROUP_ORDER_${API.currentUser.id}`,
+ JSON.stringify(this.inGameGroupOrder),
+ 3
+ );
+ } catch (err) {
+ console.error(err);
+ this.$message({
+ message: 'Failed to save in-game group order',
+ type: 'error'
+ });
+ }
+ };
+
// #endregion
// #region | Gallery
diff --git a/html/src/classes/groups.js b/html/src/classes/groups.js
index c23e2cfb..165fc6fd 100644
--- a/html/src/classes/groups.js
+++ b/html/src/classes/groups.js
@@ -2284,6 +2284,23 @@ export default class extends baseClass {
});
},
+ leaveGroupPrompt(groupId) {
+ this.$confirm(
+ 'Are you sure you want to leave this group?',
+ 'Confirm',
+ {
+ confirmButtonText: 'Confirm',
+ cancelButtonText: 'Cancel',
+ type: 'info',
+ callback: (action) => {
+ if (action === 'confirm') {
+ this.leaveGroup(groupId);
+ }
+ }
+ }
+ );
+ },
+
cancelGroupRequest(groupId) {
return API.cancelGroupRequest({
groupId
diff --git a/html/src/localization/en/en.json b/html/src/localization/en/en.json
index 7c065a0e..30a889ad 100644
--- a/html/src/localization/en/en.json
+++ b/html/src/localization/en/en.json
@@ -646,6 +646,8 @@
"header": "Groups",
"total_count": "Total {count}",
"sort_by": "Sort by:",
+ "edit_mode": "Edit Mode",
+ "exit_edit_mode": "Exit Edit Mode",
"own_groups": "Own Groups",
"mutual_groups": "Mutual Groups",
"groups": "Groups",
@@ -653,7 +655,8 @@
"alphabetical": "Alphabetical",
"members": "Members",
"in_game": "In-Game Order"
- }
+ },
+ "leave_group_tooltip": "Leave Group"
},
"worlds": {
"header": "Worlds",
diff --git a/html/src/mixins/dialogs/userDialog.pug b/html/src/mixins/dialogs/userDialog.pug
index 6e75558c..45d58d91 100644
--- a/html/src/mixins/dialogs/userDialog.pug
+++ b/html/src/mixins/dialogs/userDialog.pug
@@ -299,58 +299,25 @@ mixin userDialog()
el-button(type="default" :loading="userDialog.isGroupsLoading" @click="getUserGroups(userDialog.id)" size="mini" icon="el-icon-refresh" circle)
span(style="margin-left:5px") {{ $t('dialog.user.groups.total_count', { count: userGroups.groups.length }) }}
div(style="float:right")
- span(style="margin-right:5px") {{ $t('dialog.user.groups.sort_by') }}
- el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="userDialog.isGroupsLoading")
- el-button(size="mini")
- span {{ userDialog.groupSorting.name }} #[i.el-icon-arrow-down.el-icon--right]
- el-dropdown-menu(#default="dropdown")
- el-dropdown-item(:disabled="item === userDialogGroupSortingOptions.inGame && userDialog.id !== API.currentUser.id" v-for="(item) in userDialogGroupSortingOptions" v-text="item.name" @click.native="setUserDialogGroupSorting(item)")
+ template(v-if="!userDialogGroupEditMode")
+ span(style="margin-right:5px") {{ $t('dialog.user.groups.sort_by') }}
+ el-dropdown(@click.native.stop trigger="click" size="small" style="margin-right:5px" :disabled="userDialog.isGroupsLoading")
+ el-button(size="mini")
+ span {{ userDialog.groupSorting.name }} #[i.el-icon-arrow-down.el-icon--right]
+ el-dropdown-menu(#default="dropdown")
+ el-dropdown-item(:disabled="item === userDialogGroupSortingOptions.inGame && userDialog.id !== API.currentUser.id" v-for="(item) in userDialogGroupSortingOptions" v-text="item.name" @click.native="setUserDialogGroupSorting(item)")
+ el-button(v-if="userDialogGroupEditMode" size="small" @click="exitEditModeCurrentUserGroups" icon="el-icon-edit" style="margin-right:5px") {{ $t('dialog.user.groups.exit_edit_mode') }}
+ el-button(v-else-if="API.currentUser.id === userDialog.id" size="small" @click="editModeCurrentUserGroups" icon="el-icon-edit" style="margin-right:5px") {{ $t('dialog.user.groups.edit_mode') }}
div(v-loading="userDialog.isGroupsLoading" style="margin-top:10px")
- template(v-if="userGroups.ownGroups.length > 0")
- span(style="font-weight:bold;font-size:16px") {{ $t('dialog.user.groups.own_groups') }}
- span(style="color:#909399;font-size:12px;margin-left:5px") {{ userGroups.ownGroups.length }}/{{ API.cachedConfig?.constants?.GROUPS?.MAX_OWNED }}
- .x-friend-list(style="margin-top:10px;margin-bottom:15px;min-height:60px")
- .x-friend-item(v-for="group in userGroups.ownGroups" :key="group.id" @click="showGroupDialog(group.id)" class="x-friend-item-border")
- .avatar
- img(v-lazy="group.iconUrl")
- .detail
- span.name(v-text="group.name")
- span.extra
- el-tooltip(v-if="group.isRepresenting" placement="top" :content="$t('dialog.group.members.representing')")
- i.el-icon-collection-tag(style="margin-right:5px")
- el-tooltip(v-if="group.memberVisibility !== 'visible'" placement="top")
- template(#content)
- span {{ $t('dialog.group.members.visibility') }} {{ group.memberVisibility }}
- i.el-icon-view(style="margin-right:5px")
- span ({{ group.memberCount }})
- template(v-if="userGroups.mutualGroups.length > 0")
- span(style="font-weight:bold;font-size:16px") {{ $t('dialog.user.groups.mutual_groups') }}
- span(style="color:#909399;font-size:12px;margin-left:5px") {{ userGroups.mutualGroups.length }}
- .x-friend-list(style="margin-top:10px;margin-bottom:15px;min-height:60px")
- .x-friend-item(v-for="group in userGroups.mutualGroups" :key="group.id" @click="showGroupDialog(group.id)" class="x-friend-item-border")
- .avatar
- img(v-lazy="group.iconUrl")
- .detail
- span.name(v-text="group.name")
- span.extra
- el-tooltip(v-if="group.isRepresenting" placement="top" :content="$t('dialog.group.members.representing')")
- i.el-icon-collection-tag(style="margin-right:5px")
- el-tooltip(v-if="group.memberVisibility !== 'visible'" placement="top")
- template(#content)
- span {{ $t('dialog.group.members.visibility') }} {{ group.memberVisibility }}
- i.el-icon-view(style="margin-right:5px")
- span ({{ group.memberCount }})
- template(v-if="userGroups.remainingGroups.length > 0")
- span(style="font-weight:bold;font-size:16px") {{ $t('dialog.user.groups.groups') }}
- span(style="color:#909399;font-size:12px;margin-left:5px") {{ userGroups.remainingGroups.length }}
- template(v-if="API.currentUser.id === userDialog.id")
- |/
- template(v-if="API.currentUser.$isVRCPlus")
- | {{ API.cachedConfig?.constants?.GROUPS?.MAX_JOINED_PLUS }}
- template(v-else)
- | {{ API.cachedConfig?.constants?.GROUPS?.MAX_JOINED }}
- .x-friend-list(style="margin-top:10px;margin-bottom:15px;min-height:60px")
- .x-friend-item(v-for="group in userGroups.remainingGroups" :key="group.id" @click="showGroupDialog(group.id)" class="x-friend-item-border")
+ template(v-if="userDialogGroupEditMode")
+ .x-friend-list(style="margin-top:10px;margin-bottom:15px;max-height:unset")
+ .x-friend-item(v-for="group in userDialogGroupEditGroups" :key="group.id" @click="showGroupDialog(group.id)" class="x-friend-item-border" style="width:100%")
+ div(@click.stop style="margin-right:3px;margin-left:5px")
+ el-button(@click="moveGroupUp(group.id)" size="mini" icon="el-icon-arrow-up" style="display:block;padding:7px;font-size:9px;margin-left:0")
+ el-button(@click="moveGroupDown(group.id)" size="mini" icon="el-icon-arrow-down" style="display:block;padding:7px;font-size:9px;margin-left:0")
+ div(@click.stop style="margin-right:10px")
+ el-button(@click="moveGroupTop(group.id)" size="mini" icon="el-icon-top" style="display:block;padding:7px;font-size:9px;margin-left:0")
+ el-button(@click="moveGroupBottom(group.id)" size="mini" icon="el-icon-bottom" style="display:block;padding:7px;font-size:9px;margin-left:0")
.avatar
img(v-lazy="group.iconUrl")
.detail
@@ -363,6 +330,67 @@ mixin userDialog()
span {{ $t('dialog.group.members.visibility') }} {{ group.memberVisibility }}
i.el-icon-view(style="margin-right:5px")
span ({{ group.memberCount }})
+ el-tooltip(placement="right" :content="$t('dialog.user.groups.leave_group_tooltip')" :disabled="hideTooltips")
+ el-button(v-if="shiftHeld" @click.stop="leaveGroupPrompt(group.id)" size="mini" icon="el-icon-close" circle style="color:#f56c6c;margin-left:5px")
+ el-button(v-else @click.stop="leaveGroupPrompt(group.id)" size="mini" icon="el-icon-delete" circle style="margin-left:5px")
+ template(v-else)
+ template(v-if="userGroups.ownGroups.length > 0")
+ span(style="font-weight:bold;font-size:16px") {{ $t('dialog.user.groups.own_groups') }}
+ span(style="color:#909399;font-size:12px;margin-left:5px") {{ userGroups.ownGroups.length }}/{{ API.cachedConfig?.constants?.GROUPS?.MAX_OWNED }}
+ .x-friend-list(style="margin-top:10px;margin-bottom:15px;min-height:60px")
+ .x-friend-item(v-for="group in userGroups.ownGroups" :key="group.id" @click="showGroupDialog(group.id)" class="x-friend-item-border")
+ .avatar
+ img(v-lazy="group.iconUrl")
+ .detail
+ span.name(v-text="group.name")
+ span.extra
+ el-tooltip(v-if="group.isRepresenting" placement="top" :content="$t('dialog.group.members.representing')")
+ i.el-icon-collection-tag(style="margin-right:5px")
+ el-tooltip(v-if="group.memberVisibility !== 'visible'" placement="top")
+ template(#content)
+ span {{ $t('dialog.group.members.visibility') }} {{ group.memberVisibility }}
+ i.el-icon-view(style="margin-right:5px")
+ span ({{ group.memberCount }})
+ template(v-if="userGroups.mutualGroups.length > 0")
+ span(style="font-weight:bold;font-size:16px") {{ $t('dialog.user.groups.mutual_groups') }}
+ span(style="color:#909399;font-size:12px;margin-left:5px") {{ userGroups.mutualGroups.length }}
+ .x-friend-list(style="margin-top:10px;margin-bottom:15px;min-height:60px")
+ .x-friend-item(v-for="group in userGroups.mutualGroups" :key="group.id" @click="showGroupDialog(group.id)" class="x-friend-item-border")
+ .avatar
+ img(v-lazy="group.iconUrl")
+ .detail
+ span.name(v-text="group.name")
+ span.extra
+ el-tooltip(v-if="group.isRepresenting" placement="top" :content="$t('dialog.group.members.representing')")
+ i.el-icon-collection-tag(style="margin-right:5px")
+ el-tooltip(v-if="group.memberVisibility !== 'visible'" placement="top")
+ template(#content)
+ span {{ $t('dialog.group.members.visibility') }} {{ group.memberVisibility }}
+ i.el-icon-view(style="margin-right:5px")
+ span ({{ group.memberCount }})
+ template(v-if="userGroups.remainingGroups.length > 0")
+ span(style="font-weight:bold;font-size:16px") {{ $t('dialog.user.groups.groups') }}
+ span(style="color:#909399;font-size:12px;margin-left:5px") {{ userGroups.remainingGroups.length }}
+ template(v-if="API.currentUser.id === userDialog.id")
+ |/
+ template(v-if="API.currentUser.$isVRCPlus")
+ | {{ API.cachedConfig?.constants?.GROUPS?.MAX_JOINED_PLUS }}
+ template(v-else)
+ | {{ API.cachedConfig?.constants?.GROUPS?.MAX_JOINED }}
+ .x-friend-list(style="margin-top:10px;margin-bottom:15px;min-height:60px")
+ .x-friend-item(v-for="group in userGroups.remainingGroups" :key="group.id" @click="showGroupDialog(group.id)" class="x-friend-item-border")
+ .avatar
+ img(v-lazy="group.iconUrl")
+ .detail
+ span.name(v-text="group.name")
+ span.extra
+ el-tooltip(v-if="group.isRepresenting" placement="top" :content="$t('dialog.group.members.representing')")
+ i.el-icon-collection-tag(style="margin-right:5px")
+ el-tooltip(v-if="group.memberVisibility !== 'visible'" placement="top")
+ template(#content)
+ span {{ $t('dialog.group.members.visibility') }} {{ group.memberVisibility }}
+ i.el-icon-view(style="margin-right:5px")
+ span ({{ group.memberCount }})
el-tab-pane(:label="$t('dialog.user.worlds.header')")
el-button(type="default" :loading="userDialog.isWorldsLoading" @click="refreshUserDialogWorlds()" size="mini" icon="el-icon-refresh" circle)
span(style="margin-left:5px") {{ $t('dialog.user.worlds.total_count', { count: userDialog.worlds.length }) }}