feat: groups sidebar collapsible (#1094)

* wip

* wip

* add prettier formatter config for pug files

* feat: groups sidebar collapsible (#1079)

* rm old code
This commit is contained in:
pa
2025-01-25 01:30:07 +09:00
committed by GitHub
parent 5c9840ca5e
commit 51a1897a76
7 changed files with 472 additions and 163 deletions

View File

@@ -8,5 +8,24 @@
"bracketSpacing": true,
"arrowParens": "always",
"vueIndentScriptAndStyle": true,
"endOfLine": "auto"
}
"endOfLine": "auto",
"plugins": [
"@prettier/plugin-pug"
],
"overrides": [
{
"files": "*.pug",
"options": {
"pugPrintWidth": 120,
"pugAttributeSeparator": "none",
"pugBracketSameLine": true
}
},
{
"files": "*.vue",
"options": {
"printWidth": 120
}
}
]
}

37
package-lock.json generated
View File

@@ -20,6 +20,7 @@
"@fontsource/noto-sans-sc": "^5.1.0",
"@fontsource/noto-sans-tc": "^5.1.0",
"@infolektuell/noto-color-emoji": "^0.2.0",
"@prettier/plugin-pug": "^3.2.0",
"animate.css": "^4.1.1",
"copy-webpack-plugin": "^12.0.2",
"cross-env": "^7.0.3",
@@ -34,7 +35,7 @@
"mini-css-extract-plugin": "^2.9.2",
"normalize.css": "^8.0.1",
"noty": "^3.2.0-beta-deprecated",
"prettier": "^3.3.3",
"prettier": "^3.4.2",
"pug": "^3.0.3",
"pug-plain-loader": "^1.1.0",
"raw-loader": "^4.0.2",
@@ -2715,6 +2716,33 @@
"node": ">=14"
}
},
"node_modules/@prettier/plugin-pug": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@prettier/plugin-pug/-/plugin-pug-3.2.0.tgz",
"integrity": "sha512-SJEDgNjLpgZ2utQJGAFG96yho1hxX/tXflL24HztHYxzkPlDus2/XcvaW7WKQgMej1I+Pk6hLKp2FWv/zgzvkA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/Shinigami92"
},
{
"type": "paypal",
"url": "https://www.paypal.com/donate/?hosted_button_id=L7GY729FBKTZY"
}
],
"license": "MIT",
"dependencies": {
"pug-lexer": "^5.0.1"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
},
"peerDependencies": {
"prettier": "^3.0.0"
}
},
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
@@ -8535,10 +8563,11 @@
}
},
"node_modules/prettier": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},

View File

@@ -35,6 +35,7 @@
"@fontsource/noto-sans-sc": "^5.1.0",
"@fontsource/noto-sans-tc": "^5.1.0",
"@infolektuell/noto-color-emoji": "^0.2.0",
"@prettier/plugin-pug": "^3.2.0",
"animate.css": "^4.1.1",
"copy-webpack-plugin": "^12.0.2",
"cross-env": "^7.0.3",
@@ -49,7 +50,7 @@
"mini-css-extract-plugin": "^2.9.2",
"normalize.css": "^8.0.1",
"noty": "^3.2.0-beta-deprecated",
"prettier": "^3.3.3",
"prettier": "^3.4.2",
"pug": "^3.0.3",
"pug-plain-loader": "^1.1.0",
"raw-loader": "^4.0.2",

View File

@@ -36,6 +36,7 @@ import ModerationTab from './views/tabs/Moderation.vue';
// components
import SimpleSwitch from './components/settings/SimpleSwitch.vue';
import GroupsSidebar from './components/sidebar/GroupsSidebar.vue';
// main app classes
import _sharedFeed from './classes/sharedFeed.js';
@@ -166,7 +167,11 @@ console.log(`isLinux: ${LINUX}`);
// components
// - settings
SimpleSwitch
SimpleSwitch,
// components
// - sidebar(friendsListSidebar)
GroupsSidebar
},
el: '#x-app',
async mounted() {
@@ -23120,7 +23125,9 @@ console.log(`isLinux: ${LINUX}`);
return LINUX;
};
// friendsListSiderBar
// friendsListSidebar
// - SidebarGroupByInstance
$app.methods.handleSwitchGroupByInstance = async function () {
this.isSidebarGroupByInstance = !this.isSidebarGroupByInstance;
await configRepository.setBool(
@@ -23129,9 +23136,6 @@ console.log(`isLinux: ${LINUX}`);
);
};
// friendsListSidebar
// - SidebarGroupByInstance
$app.data.isSidebarGroupByInstance = await configRepository.getBool(
'VRCX_sidebarGroupByInstance',
true

View File

@@ -1216,7 +1216,7 @@ export default class extends baseClass {
API.$on('GROUP:USER:INSTANCES', function (args) {
$app.groupInstances = [];
for (var json of args.json.instances) {
for (const json of args.json.instances) {
if (args.json.fetchedAt) {
// tack on fetchedAt
json.$fetchedAt = args.json.fetchedAt;
@@ -1227,7 +1227,7 @@ export default class extends baseClass {
fetchedAt: args.json.fetchedAt
}
});
var ref = this.cachedGroups.get(json.ownerId);
const ref = this.cachedGroups.get(json.ownerId);
if (typeof ref === 'undefined') {
if ($app.friendLogInitStatus) {
this.getGroup({ groupId: json.ownerId });

View File

@@ -0,0 +1,94 @@
<template>
<div class="x-friend-list" style="padding: 10px 5px">
<template v-for="(group, index) in groupedGroupInstances">
<div
:key="getGroupId(group)"
class="x-friend-group x-link sidebar-group-title"
:style="{ paddingTop: index === 0 ? '0px' : '10px' }"
>
<div @click="toggleGroupSidebarCollapse(getGroupId(group))" style="display: flex; align-items: center">
<i
class="el-icon-arrow-right"
:class="{ rotate: !groupInstancesCfg[getGroupId(group)].isCollapsed }"
></i>
<span style="margin-left: 5px">{{ group[0].group.name }} {{ group.length }}</span>
</div>
</div>
<div
v-if="!groupInstancesCfg[getGroupId(group)].isCollapsed"
v-for="ref in group"
:key="ref.instance.id"
class="x-friend-item"
@click="showGroupDialog(ref.instance.ownerId)"
>
<div class="avatar">
<img v-lazy="ref.group.iconUrl" />
</div>
<div class="detail">
<span class="name">
<span v-text="ref.group.name"></span>
<span style="font-weight: normal; margin-left: 5px"
>({{ ref.instance.userCount }}/{{ ref.instance.capacity }})</span
>
</span>
<location class="extra" :location="ref.instance.location" :link="false" />
</div>
</div>
</template>
</div>
</template>
<script>
export default {
name: 'GroupsSidebar',
props: {
groupInstances: {
type: Array,
default: () => []
}
},
data() {
return {
// temporary, sort feat not yet done
// may be the data structure to be changed
groupInstancesCfg: []
};
},
computed: {
groupedGroupInstances() {
const groupMap = new Map();
this.groupInstances.forEach((ref) => {
const groupId = ref.group.groupId;
if (!groupMap.has(groupId)) {
groupMap.set(groupId, []);
}
groupMap.get(groupId).push(ref);
if (!this.groupInstancesCfg[ref.group?.groupId]) {
this.groupInstancesCfg = {
[ref.group.groupId]: {
isCollapsed: false
},
...this.groupInstancesCfg
};
}
});
return Array.from(groupMap.values());
}
},
methods: {
toggleGroupSidebarCollapse(groupId) {
this.groupInstancesCfg[groupId].isCollapsed = !this.groupInstancesCfg[groupId].isCollapsed;
},
showGroupDialog(ownerId) {
this.$emit('show-group-dialog', ownerId);
},
getGroupId(group) {
return group[0]?.group?.groupId || '';
}
}
};
</script>

View File

@@ -1,181 +1,343 @@
mixin friendsListSidebar()
.x-aside-container(v-show="$refs.menu && $refs.menu.activeIndex !== 'friendsList'" id="aside")
div(style="display:flex;align-items:baseline")
el-select(v-model="quickSearch" clearable :placeholder="$t('side_panel.search_placeholder')" filterable remote :remote-method="quickSearchRemoteMethod" popper-class="x-quick-search" @change="quickSearchChange" @visible-change="quickSearchVisibleChange" style="flex:1;padding:10px")
el-option(v-for="item in quickSearchItems" :key="item.value" :value="item.value" :label="item.label")
mixin friendsListSidebar
#aside.x-aside-container(v-show='$refs.menu && $refs.menu.activeIndex !== \'friendsList\'')
div(style='display: flex; align-items: baseline')
el-select(
v-model='quickSearch'
clearable
:placeholder='$t("side_panel.search_placeholder")'
filterable
remote
:remote-method='quickSearchRemoteMethod'
popper-class='x-quick-search'
@change='quickSearchChange'
@visible-change='quickSearchVisibleChange'
style='flex: 1; padding: 10px')
el-option(v-for='item in quickSearchItems' :key='item.value' :value='item.value' :label='item.label')
.x-friend-item
template(v-if="item.ref")
template(v-if='item.ref')
.detail
span.name(v-text="item.ref.displayName" :style="{'color':item.ref.$userColour}")
span.extra(v-if="!item.ref.isFriend")
span.extra(v-else-if="item.ref.state === 'offline'") {{ $t('side_panel.search_result_active') }}
span.extra(v-else-if="item.ref.state === 'active'") {{ $t('side_panel.search_result_offline') }}
location.extra(v-else :location="item.ref.location" :traveling="item.ref.travelingToLocation" :link="false")
img.avatar(v-lazy="userImage(item.ref)")
span(v-else) {{ $t('side_panel.search_result_more') }} #[span(v-text="item.label" style="font-weight:bold")]
el-tooltip(placement="bottom" :content="$t('side_panel.direct_access_tooltip')" :disabled="hideTooltips")
el-button(type="default" @click="directAccessPaste" size="mini" icon="el-icon-discover" circle)
el-tooltip(placement="bottom" :content="$t('side_panel.refresh_tooltip')" :disabled="hideTooltips")
el-button(type="default" @click="refreshFriendsList" :loading="API.isRefreshFriendsLoading" size="mini" icon="el-icon-refresh" circle style="margin-right:10px")
el-tabs.zero-margin-tabs(stretch="true" style="height:calc(100% - 60px;margin-top:5px")
span.name(v-text='item.ref.displayName' :style='{ color: item.ref.$userColour }')
span.extra(v-if='!item.ref.isFriend')
span.extra(v-else-if='item.ref.state === "offline"') {{ $t('side_panel.search_result_active') }}
span.extra(v-else-if='item.ref.state === "active"') {{ $t('side_panel.search_result_offline') }}
location.extra(
v-else
:location='item.ref.location'
:traveling='item.ref.travelingToLocation'
:link='false')
img.avatar(v-lazy='userImage(item.ref)')
span(v-else) {{ $t('side_panel.search_result_more') }} #[span(v-text='item.label' style='font-weight: bold')]
el-tooltip(placement='bottom' :content='$t("side_panel.direct_access_tooltip")' :disabled='hideTooltips')
el-button(type='default' @click='directAccessPaste' size='mini' icon='el-icon-discover' circle)
el-tooltip(placement='bottom' :content='$t("side_panel.refresh_tooltip")' :disabled='hideTooltips')
el-button(
type='default'
@click='refreshFriendsList'
:loading='API.isRefreshFriendsLoading'
size='mini'
icon='el-icon-refresh'
circle
style='margin-right: 10px')
el-tabs.zero-margin-tabs(stretch='true' style='height: calc(100% - 60px); margin-top: 5px')
el-tab-pane
template(#label)
span {{ $t('side_panel.friends') }}
span(style="color:#909399;font-size:12px;margin-left:10px") ({{ onlineFriendCount }}/{{ friends.size }})
.x-friend-list(style="padding:10px 5px")
.x-friend-group.x-link(@click="isFriendsGroupMe = !isFriendsGroupMe; saveFriendsGroupStates()" style="padding:0px 0px 5px")
i.el-icon-arrow-right(:class="{ rotate: isFriendsGroupMe }")
span(style="margin-left:5px") {{ $t('side_panel.me') }}
div(v-show="isFriendsGroupMe")
.x-friend-item(:key="API.currentUser.id" @click="showUserDialog(API.currentUser.id)")
.avatar(:class="userStatusClass(API.currentUser)")
img(v-lazy="userImage(API.currentUser)")
span(style='color: #909399; font-size: 12px; margin-left: 10px') ({{ onlineFriendCount }}/{{ friends.size }})
.x-friend-list(style='padding: 10px 5px')
.x-friend-group.x-link(
@click='isFriendsGroupMe = !isFriendsGroupMe; saveFriendsGroupStates()'
style='padding: 0px 0px 5px')
i.el-icon-arrow-right(:class='{ rotate: isFriendsGroupMe }')
span(style='margin-left: 5px') {{ $t('side_panel.me') }}
div(v-show='isFriendsGroupMe')
.x-friend-item(:key='API.currentUser.id' @click='showUserDialog(API.currentUser.id)')
.avatar(:class='userStatusClass(API.currentUser)')
img(v-lazy='userImage(API.currentUser)')
.detail
span.name(v-text="API.currentUser.displayName" :style="{'color':API.currentUser.$userColour}")
location.extra(v-if="isGameRunning && !gameLogDisabled" :location="lastLocation.location" :traveling="lastLocationDestination" :link="false")
location.extra(v-else-if="isRealInstance(API.currentUser.$locationTag) || isRealInstance(API.currentUser.$travelingToLocation)" :location="API.currentUser.$locationTag" :traveling="API.currentUser.$travelingToLocation" :link="false")
span.extra(v-else v-text="API.currentUser.statusDescription")
.x-friend-group.x-link(@click="isVIPFriends = !isVIPFriends; saveFriendsGroupStates()" v-show="vipFriends.length")
i.el-icon-arrow-right(:class="{ rotate: isVIPFriends }")
span(style="margin-left:5px") {{ $t('side_panel.favorite') }} &horbar; {{ vipFriends.length }}
div(v-show="isVIPFriends")
span.name(
v-text='API.currentUser.displayName'
:style='{ color: API.currentUser.$userColour }')
location.extra(
v-if='isGameRunning && !gameLogDisabled'
:location='lastLocation.location'
:traveling='lastLocationDestination'
:link='false')
location.extra(
v-else-if='isRealInstance(API.currentUser.$locationTag) || isRealInstance(API.currentUser.$travelingToLocation)'
:location='API.currentUser.$locationTag'
:traveling='API.currentUser.$travelingToLocation'
:link='false')
span.extra(v-else v-text='API.currentUser.statusDescription')
.x-friend-group.x-link(
@click='isVIPFriends = !isVIPFriends; saveFriendsGroupStates()'
v-show='vipFriends.length')
i.el-icon-arrow-right(:class='{ rotate: isVIPFriends }')
span(style='margin-left: 5px') {{ $t('side_panel.favorite') }} &horbar; {{ vipFriends.length }}
div(v-show='isVIPFriends')
template(v-if='isSidebarGroupByInstance')
div(v-for="(friendArr, idx) in vipFriendsInSameInstance" :key="friendArr[0].ref?.location.tag")
div(style="margin-bottom: 3px")
location.extra(:location="friendArr[0].ref?.location" style="color:#c7c7c7")
span(style="margin-left: 5px") {{ `(${friendArr.length})` }}
div(style="margin-bottom: 10px")
div.x-friend-item(v-if="friendArr && friendArr.length" v-for="friend in friendArr" :key="friend.id" @click="showUserDialog(friend.id)")
template(v-if="friend.ref")
.avatar(:class="userStatusClass(friend.ref, friend.pendingOffline)")
img(v-lazy="userImage(friend.ref)")
div(
v-for='(friendArr, idx) in vipFriendsInSameInstance'
:key='friendArr[0].ref?.location.tag')
div(style='margin-bottom: 3px')
location.extra(:location='friendArr[0].ref?.location' style='color: #c7c7c7')
span(style='margin-left: 5px') {{ `(${friendArr.length})` }}
div(style='margin-bottom: 10px')
.x-friend-item(
v-if='friendArr && friendArr.length'
v-for='friend in friendArr'
:key='friend.id'
@click='showUserDialog(friend.id)')
template(v-if='friend.ref')
.avatar(:class='userStatusClass(friend.ref, friend.pendingOffline)')
img(v-lazy='userImage(friend.ref)')
.detail
span.name(v-if="!hideNicknames && friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }})
span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}")
span.extra(v-if="friend.pendingOffline") #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }}
span.name(
v-if='!hideNicknames && friend.$nickName'
:style='{ color: friend.ref.$userColour }') {{ friend.ref.displayName }} ({{ friend.$nickName }})
span.name(
v-else
v-text='friend.ref.displayName'
:style='{ color: friend.ref.$userColour }')
span.extra(v-if='friend.pendingOffline') #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }}
template(v-else)
i(v-if="friend.ref.travelingToLocation" class="el-icon el-icon-loading" style="display:inline-block;margin-right:5px")
timer(:epoch="friend.ref?.$location_at" style="color:#c7c7c7")
i.el-icon.el-icon-loading(
v-if='friend.ref.travelingToLocation'
style='display: inline-block; margin-right: 5px')
timer(:epoch='friend.ref?.$location_at' style='color: #c7c7c7')
template(v-else)
span(v-text="friend.name || friend.id")
el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(friend.id)" style="margin-left:5px")
div(v-if="idx === vipFriendsInSameInstance.length - 1" style="color:#c7c7c7") {{ "Others" }}
.x-friend-item(v-for="friend in vipFriendsNotInSameInstance" :key="friend.id" @click="showUserDialog(friend.id)")
template(v-if="friend.ref")
.avatar(:class="userStatusClass(friend.ref, friend.pendingOffline)")
img(v-lazy="userImage(friend.ref)")
span(v-text='friend.name || friend.id')
el-button(
type='text'
icon='el-icon-close'
size='mini'
@click.stop='confirmDeleteFriend(friend.id)'
style='margin-left: 5px')
div(v-if='idx === vipFriendsInSameInstance.length - 1' style='color: #c7c7c7') {{ 'Others' }}
.x-friend-item(
v-for='friend in vipFriendsNotInSameInstance'
:key='friend.id'
@click='showUserDialog(friend.id)')
template(v-if='friend.ref')
.avatar(:class='userStatusClass(friend.ref, friend.pendingOffline)')
img(v-lazy='userImage(friend.ref)')
.detail
span.name(v-if="!hideNicknames && friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }})
span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}")
span.extra(v-if="friend.pendingOffline") #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }}
location.extra(v-else :location="friend.ref.location" :traveling="friend.ref.travelingToLocation" :link="false")
span.name(
v-if='!hideNicknames && friend.$nickName'
:style='{ color: friend.ref.$userColour }') {{ friend.ref.displayName }} ({{ friend.$nickName }})
span.name(
v-else
v-text='friend.ref.displayName'
:style='{ color: friend.ref.$userColour }')
span.extra(v-if='friend.pendingOffline') #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }}
location.extra(
v-else
:location='friend.ref.location'
:traveling='friend.ref.travelingToLocation'
:link='false')
template(v-else)
span(v-text="friend.name || friend.id")
el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(friend.id)" style="margin-left:5px")
span(v-text='friend.name || friend.id')
el-button(
type='text'
icon='el-icon-close'
size='mini'
@click.stop='confirmDeleteFriend(friend.id)'
style='margin-left: 5px')
template(v-else)
.x-friend-item(v-for="friend in vipFriends" :key="friend.id" @click="showUserDialog(friend.id)")
template(v-if="friend.ref")
.avatar(:class="userStatusClass(friend.ref, friend.pendingOffline)")
img(v-lazy="userImage(friend.ref)")
.x-friend-item(
v-for='friend in vipFriends'
:key='friend.id'
@click='showUserDialog(friend.id)')
template(v-if='friend.ref')
.avatar(:class='userStatusClass(friend.ref, friend.pendingOffline)')
img(v-lazy='userImage(friend.ref)')
.detail
span.name(v-if="!hideNicknames && friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }})
span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}")
span.extra(v-if="friend.pendingOffline") #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }}
location.extra(v-else :location="friend.ref.location" :traveling="friend.ref.travelingToLocation" :link="false")
span.name(
v-if='!hideNicknames && friend.$nickName'
:style='{ color: friend.ref.$userColour }') {{ friend.ref.displayName }} ({{ friend.$nickName }})
span.name(
v-else
v-text='friend.ref.displayName'
:style='{ color: friend.ref.$userColour }')
span.extra(v-if='friend.pendingOffline') #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }}
location.extra(
v-else
:location='friend.ref.location'
:traveling='friend.ref.travelingToLocation'
:link='false')
template(v-else)
span(v-text="friend.name || friend.id")
el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(friend.id)" style="margin-left:5px")
.x-friend-group.x-link(@click="isOnlineFriends = !isOnlineFriends; saveFriendsGroupStates()" v-show="onlineFriends.length")
i.el-icon-arrow-right(:class="{ rotate: isOnlineFriends }")
span(style="margin-left:5px") {{ $t('side_panel.online') }} &horbar; {{ onlineFriends.length }}
div(v-show="isOnlineFriends")
span(v-text='friend.name || friend.id')
el-button(
type='text'
icon='el-icon-close'
size='mini'
@click.stop='confirmDeleteFriend(friend.id)'
style='margin-left: 5px')
.x-friend-group.x-link(
@click='isOnlineFriends = !isOnlineFriends; saveFriendsGroupStates()'
v-show='onlineFriends.length')
i.el-icon-arrow-right(:class='{ rotate: isOnlineFriends }')
span(style='margin-left: 5px') {{ $t('side_panel.online') }} &horbar; {{ onlineFriends.length }}
div(v-show='isOnlineFriends')
template(v-if='isSidebarGroupByInstance')
div(v-for="(friendArr, idx) in onlineFriendsInSameInstance" :key="friendArr[0].ref?.location.tag")
div(style="margin-bottom: 3px")
location.extra(:location="friendArr[0].ref?.location" style="color:#c7c7c7")
span(style="margin-left: 5px") {{ `(${friendArr.length})` }}
div(style="margin-bottom: 10px")
div.x-friend-item(v-if="friendArr && friendArr.length" v-for="friend in friendArr" :key="friend.id" @click="showUserDialog(friend.id)")
template(v-if="friend.ref")
.avatar(:class="userStatusClass(friend.ref, friend.pendingOffline)")
img(v-lazy="userImage(friend.ref)")
div(
v-for='(friendArr, idx) in onlineFriendsInSameInstance'
:key='friendArr[0].ref?.location.tag')
div(style='margin-bottom: 3px')
location.extra(:location='friendArr[0].ref?.location' style='color: #c7c7c7')
span(style='margin-left: 5px') {{ `(${friendArr.length})` }}
div(style='margin-bottom: 10px')
.x-friend-item(
v-if='friendArr && friendArr.length'
v-for='friend in friendArr'
:key='friend.id'
@click='showUserDialog(friend.id)')
template(v-if='friend.ref')
.avatar(:class='userStatusClass(friend.ref, friend.pendingOffline)')
img(v-lazy='userImage(friend.ref)')
.detail
span.name(v-if="!hideNicknames && friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }})
span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}")
span.extra(v-if="friend.pendingOffline") #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }}
span.name(
v-if='!hideNicknames && friend.$nickName'
:style='{ color: friend.ref.$userColour }') {{ friend.ref.displayName }} ({{ friend.$nickName }})
span.name(
v-else
v-text='friend.ref.displayName'
:style='{ color: friend.ref.$userColour }')
span.extra(v-if='friend.pendingOffline') #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }}
template(v-else)
i(v-if="friend.ref.travelingToLocation" class="el-icon el-icon-loading" style="display:inline-block;margin-right:5px")
timer(:epoch="friend.ref?.$location_at" style="color:#c7c7c7")
i.el-icon.el-icon-loading(
v-if='friend.ref.travelingToLocation'
style='display: inline-block; margin-right: 5px')
timer(:epoch='friend.ref?.$location_at' style='color: #c7c7c7')
template(v-else)
span(v-text="friend.name || friend.id")
el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(friend.id)" style="margin-left:5px")
div(v-if="idx === onlineFriendsInSameInstance.length - 1" style="color:#c7c7c7") {{ "Others" }}
.x-friend-item(v-for="friend in onlineFriendsNotInSameInstance" :key="friend.id" @click="showUserDialog(friend.id)")
template(v-if="friend.ref")
.avatar(:class="userStatusClass(friend.ref, friend.pendingOffline)")
img(v-lazy="userImage(friend.ref)")
span(v-text='friend.name || friend.id')
el-button(
type='text'
icon='el-icon-close'
size='mini'
@click.stop='confirmDeleteFriend(friend.id)'
style='margin-left: 5px')
div(v-if='idx === onlineFriendsInSameInstance.length - 1' style='color: #c7c7c7') {{ 'Others' }}
.x-friend-item(
v-for='friend in onlineFriendsNotInSameInstance'
:key='friend.id'
@click='showUserDialog(friend.id)')
template(v-if='friend.ref')
.avatar(:class='userStatusClass(friend.ref, friend.pendingOffline)')
img(v-lazy='userImage(friend.ref)')
.detail
span.name(v-if="!hideNicknames && friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }})
span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}")
span.extra(v-if="friend.pendingOffline") #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }}
location.extra(v-else :location="friend.ref.location" :traveling="friend.ref.travelingToLocation" :link="false")
span.name(
v-if='!hideNicknames && friend.$nickName'
:style='{ color: friend.ref.$userColour }') {{ friend.ref.displayName }} ({{ friend.$nickName }})
span.name(
v-else
v-text='friend.ref.displayName'
:style='{ color: friend.ref.$userColour }')
span.extra(v-if='friend.pendingOffline') #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }}
location.extra(
v-else
:location='friend.ref.location'
:traveling='friend.ref.travelingToLocation'
:link='false')
template(v-else)
span(v-text="friend.name || friend.id")
el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(friend.id)" style="margin-left:5px")
span(v-text='friend.name || friend.id')
el-button(
type='text'
icon='el-icon-close'
size='mini'
@click.stop='confirmDeleteFriend(friend.id)'
style='margin-left: 5px')
template(v-else)
.x-friend-item(v-for="friend in onlineFriends" :key="friend.id" @click="showUserDialog(friend.id)")
template(v-if="friend.ref")
.avatar(:class="userStatusClass(friend.ref, friend.pendingOffline)")
img(v-lazy="userImage(friend.ref)")
.x-friend-item(
v-for='friend in onlineFriends'
:key='friend.id'
@click='showUserDialog(friend.id)')
template(v-if='friend.ref')
.avatar(:class='userStatusClass(friend.ref, friend.pendingOffline)')
img(v-lazy='userImage(friend.ref)')
.detail
span.name(v-if="!hideNicknames && friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }})
span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}")
span.extra(v-if="friend.pendingOffline") #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }}
location.extra(v-else :location="friend.ref.location" :traveling="friend.ref.travelingToLocation" :link="false")
span.name(
v-if='!hideNicknames && friend.$nickName'
:style='{ color: friend.ref.$userColour }') {{ friend.ref.displayName }} ({{ friend.$nickName }})
span.name(
v-else
v-text='friend.ref.displayName'
:style='{ color: friend.ref.$userColour }')
span.extra(v-if='friend.pendingOffline') #[i.el-icon-warning-outline] {{ $t('side_panel.pending_offline') }}
location.extra(
v-else
:location='friend.ref.location'
:traveling='friend.ref.travelingToLocation'
:link='false')
template(v-else)
span(v-text="friend.name || friend.id")
el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(friend.id)" style="margin-left:5px")
.x-friend-group.x-link(@click="isActiveFriends = !isActiveFriends; saveFriendsGroupStates()" v-show="activeFriends.length")
i.el-icon-arrow-right(:class="{ rotate: isActiveFriends }")
span(style="margin-left:5px") {{ $t('side_panel.active') }} &horbar; {{ activeFriends.length }}
div(v-show="isActiveFriends")
.x-friend-item(v-for="friend in activeFriends" :key="friend.id" @click="showUserDialog(friend.id)")
template(v-if="friend.ref")
span(v-text='friend.name || friend.id')
el-button(
type='text'
icon='el-icon-close'
size='mini'
@click.stop='confirmDeleteFriend(friend.id)'
style='margin-left: 5px')
.x-friend-group.x-link(
@click='isActiveFriends = !isActiveFriends; saveFriendsGroupStates()'
v-show='activeFriends.length')
i.el-icon-arrow-right(:class='{ rotate: isActiveFriends }')
span(style='margin-left: 5px') {{ $t('side_panel.active') }} &horbar; {{ activeFriends.length }}
div(v-show='isActiveFriends')
.x-friend-item(
v-for='friend in activeFriends'
:key='friend.id'
@click='showUserDialog(friend.id)')
template(v-if='friend.ref')
.avatar
img(v-lazy="userImage(friend.ref)")
img(v-lazy='userImage(friend.ref)')
.detail
span.name(v-if="!hideNicknames && friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }})
span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}")
span.extra(v-text="friend.ref.statusDescription" :link="false")
span.name(
v-if='!hideNicknames && friend.$nickName'
:style='{ color: friend.ref.$userColour }') {{ friend.ref.displayName }} ({{ friend.$nickName }})
span.name(
v-else
v-text='friend.ref.displayName'
:style='{ color: friend.ref.$userColour }')
span.extra(v-text='friend.ref.statusDescription' :link='false')
template(v-else)
span(v-text="friend.name || friend.id")
el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(friend.id)" style="margin-left:5px")
.x-friend-group.x-link(@click="isOfflineFriends = !isOfflineFriends; saveFriendsGroupStates()" v-show="offlineFriends.length")
i.el-icon-arrow-right(:class="{ rotate: isOfflineFriends }")
span(style="margin-left:5px") {{ $t('side_panel.offline') }} &horbar; {{ offlineFriends.length }}
div(v-show="isOfflineFriends")
.x-friend-item(v-for="friend in offlineFriends" :key="friend.id" @click="showUserDialog(friend.id)")
template(v-if="friend.ref")
span(v-text='friend.name || friend.id')
el-button(
type='text'
icon='el-icon-close'
size='mini'
@click.stop='confirmDeleteFriend(friend.id)'
style='margin-left: 5px')
.x-friend-group.x-link(
@click='isOfflineFriends = !isOfflineFriends; saveFriendsGroupStates()'
v-show='offlineFriends.length')
i.el-icon-arrow-right(:class='{ rotate: isOfflineFriends }')
span(style='margin-left: 5px') {{ $t('side_panel.offline') }} &horbar; {{ offlineFriends.length }}
div(v-show='isOfflineFriends')
.x-friend-item(
v-for='friend in offlineFriends'
:key='friend.id'
@click='showUserDialog(friend.id)')
template(v-if='friend.ref')
.avatar
img(v-lazy="userImage(friend.ref)")
img(v-lazy='userImage(friend.ref)')
.detail
span.name(v-if="!hideNicknames && friend.$nickName" :style="{'color':friend.ref.$userColour}") {{ friend.ref.displayName }} ({{ friend.$nickName }})
span.name(v-else v-text="friend.ref.displayName" :style="{'color':friend.ref.$userColour}")
span.extra(v-text="friend.ref.statusDescription")
span.name(
v-if='!hideNicknames && friend.$nickName'
:style='{ color: friend.ref.$userColour }') {{ friend.ref.displayName }} ({{ friend.$nickName }})
span.name(
v-else
v-text='friend.ref.displayName'
:style='{ color: friend.ref.$userColour }')
span.extra(v-text='friend.ref.statusDescription')
template(v-else)
span(v-text="friend.name || friend.id")
el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(friend.id)" style="margin-left:5px")
el-tab-pane
span(v-text='friend.name || friend.id')
el-button(
type='text'
icon='el-icon-close'
size='mini'
@click.stop='confirmDeleteFriend(friend.id)'
style='margin-left: 5px')
el-tab-pane(lazy)
template(#label)
span {{ $t('side_panel.groups') }}
span(style="color:#909399;font-size:12px;margin-left:10px") ({{ groupInstances.length }})
.x-friend-list(style="padding:10px 5px")
.x-friend-item(v-for="ref in groupInstances" :key="ref.instance.id" @click="showGroupDialog(ref.instance.ownerId)")
.avatar
img(v-lazy="ref.group.iconUrl")
.detail
span.name
span(v-text="ref.group.name")
span(style="font-weight:normal;margin-left:5px") ({{ ref.instance.userCount }}/{{ ref.instance.capacity }})
location.extra(:location="ref.instance.location" :link="false")
span(style='color: #909399; font-size: 12px; margin-left: 10px') ({{ groupInstances.length }})
groups-sidebar(:group-instances='groupInstances' @show-group-dialog='showGroupDialog')