diff --git a/html/src/app.js b/html/src/app.js
index d244866d..d69a9e00 100644
--- a/html/src/app.js
+++ b/html/src/app.js
@@ -5238,6 +5238,9 @@ speechSynthesis.getVoices();
}
return 0;
});
+ if (results.length > 4) {
+ results.length = 4;
+ }
results.push({
value: `search:${query}`,
label: query
@@ -5249,9 +5252,8 @@ speechSynthesis.getVoices();
$app.methods.quickSearchChange = function (value) {
if (value) {
if (value.startsWith('search:')) {
- this.searchText = value.substr(7);
- this.search();
- this.$refs.menu.activeIndex = 'search';
+ this.friendsListSearch = value.substr(7);
+ this.$refs.menu.activeIndex = 'friendsList';
} else {
this.showUserDialog(value);
}
@@ -6620,6 +6622,19 @@ speechSynthesis.getVoices();
},
layout: 'table'
};
+ $app.data.friendsListTable = {
+ visible: false,
+ data: [],
+ tableProps: {
+ stripe: true,
+ size: 'mini',
+ defaultSort: {
+ prop: '$friendNum',
+ order: 'descending'
+ }
+ },
+ layout: 'table'
+ };
$app.data.visits = 0;
$app.data.openVR = configRepository.getBool('openVR');
$app.data.openVRAlways = configRepository.getBool('openVRAlways');
@@ -9142,7 +9157,6 @@ speechSynthesis.getVoices();
} else if (ctx.ref.$offline_for) {
return timeToText(Date.now() - ctx.ref.$offline_for);
}
-
return '-';
};
@@ -9756,6 +9770,138 @@ speechSynthesis.getVoices();
this.sendInviteRequestDialogVisible = true;
};
+ // App: Friends List
+
+ API.$on('LOGIN', function () {
+ $app.friendsListTable.data = [];
+ });
+
+ $app.methods.selectFriendsListRow = function (val) {
+ if (val === null) {
+ return;
+ }
+ this.showUserDialog(val.id);
+ };
+
+ $app.data.friendsListSearch = '';
+ $app.data.friendsListSearchFilterVIP = false;
+ $app.data.friendsListSearchFilters = [ 'User Name', 'Display Name', 'Status', 'Bio', 'Memo' ];
+
+ $app.methods.friendsListSearchChange = function () {
+ var filters = this.friendsListSearchFilters;
+ var results = [];
+ if (this.friendsListSearch) {
+ var query = this.friendsListSearch.toUpperCase();
+ }
+ for (var ctx of this.friends.values()) {
+ if (typeof ctx.ref === 'undefined') {
+ continue;
+ }
+ if (this.friendsListSearchFilterVIP &&
+ !ctx.isVIP) {
+ continue;
+ }
+ if (query && filters) {
+ var match = false;
+ if (!match &&
+ filters.includes('User Name')) {
+ var uname = String(ctx.ref.username);
+ match = uname.toUpperCase().includes(query) &&
+ !uname.startsWith('steam_');
+ }
+ if (!match &&
+ filters.includes('Display Name') &&
+ ctx.ref.displayName) {
+ match = String(ctx.ref.displayName).toUpperCase().includes(query);
+ }
+ if (!match &&
+ filters.includes('Memo') &&
+ ctx.memo) {
+ match = String(ctx.memo).toUpperCase().includes(query);
+ }
+ if (!match &&
+ filters.includes('Bio') &&
+ ctx.ref.bio) {
+ match = String(ctx.ref.bio).toUpperCase().includes(query);
+ }
+ if (!match &&
+ filters.includes('Status') &&
+ ctx.ref.statusDescription) {
+ match = String(ctx.ref.statusDescription).toUpperCase().includes(query);
+ }
+ if (!match) {
+ continue;
+ }
+ }
+ ctx.ref.$friendNum = ctx.no;
+ switch (ctx.ref.$trustLevel) {
+ case 'Nuisance':
+ ctx.ref.$trustNum = '0';
+ break;
+ case 'Visitor':
+ ctx.ref.$trustNum = '1';
+ break;
+ case 'New User':
+ ctx.ref.$trustNum = '2';
+ break;
+ case 'User':
+ ctx.ref.$trustNum = '3';
+ break;
+ case 'Known User':
+ ctx.ref.$trustNum = '4';
+ break;
+ case 'Trusted User':
+ ctx.ref.$trustNum = '5';
+ break;
+ case 'Veteran User':
+ ctx.ref.$trustNum = '6';
+ break;
+ case 'Legendary User':
+ ctx.ref.$trustNum = '7';
+ break;
+ case 'VRChat Team':
+ ctx.ref.$trustNum = '8';
+ break;
+ }
+ results.push(ctx.ref);
+ }
+ this.friendsListTable.data = results;
+ };
+
+ $app.watch.friendsListSearch = $app.methods.friendsListSearchChange;
+ $app.data.friendsListLoading = false;
+ $app.data.friendsListLoadingProgress = '';
+
+ $app.methods.friendsListLoadUsers = async function () {
+ this.friendsListLoading = true;
+ var i = 0;
+ var toFetch = [];
+ for (var ctx of this.friends.values()) {
+ if (!ctx.ref.date_joined) {
+ toFetch.push(ctx.id);
+ }
+ }
+ var length = toFetch.length;
+ for (var userId of toFetch) {
+ if (!this.friendsListLoading) {
+ this.friendsListLoadingProgress = '';
+ return;
+ }
+ i++;
+ this.friendsListLoadingProgress = `${i}/${length}`;
+ await API.getUser({
+ userId: userId
+ });
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ }
+ this.friendsListLoadingProgress = '';
+ this.friendsListLoading = false;
+ };
+
+ $app.methods.sortAlphabetically = function (a, b, field) {
+ return a[field].toLowerCase().localeCompare(b[field].toLowerCase());
+ };
+
$app = new Vue($app);
window.$app = $app;
}());
diff --git a/html/src/app.scss b/html/src/app.scss
index 2c4e2a41..4f5f0901 100644
--- a/html/src/app.scss
+++ b/html/src/app.scss
@@ -282,9 +282,10 @@ a {
margin-right: 8px;
}
-.x-friend-item > img.avatar {
+.x-friend-item > img.avatar,
+img.friends-list-avatar {
width: unset;
- height: 37.5px;
+ height: 22.5px;
margin-right: 0;
margin-left: 5px;
border-radius: 2px;
diff --git a/html/src/index.pug b/html/src/index.pug
index b25d9d2b..59693b34 100644
--- a/html/src/index.pug
+++ b/html/src/index.pug
@@ -61,6 +61,7 @@ html
+menuitem('friendLog', 'Friend Log', 'el-icon-notebook-2')
+menuitem('moderation', 'Moderation', 'el-icon-finished')
+menuitem('notification', 'Notification', 'el-icon-bell')
+ +menuitem('friendsList', 'Friends List', 'el-icon-s-management')
+menuitem('profile', 'Profile', 'el-icon-user')
+menuitem('settings', 'Settings', 'el-icon-s-tools')
@@ -584,6 +585,71 @@ html
span(v-text="scope.data.key" style="font-weight:bold;margin-right:5px")
span(v-if="!scope.data.children" v-text="scope.data.value")
+ //- friends list
+ .x-container(v-show="$refs.menu && $refs.menu.activeIndex === 'friendsList'")
+ div.options-container(style="margin-top:0")
+ span.header Friends List
+ div(style="float:right;font-size:13px")
+ span Load missing entries, don't click this unless you understand the risks:
+ template(v-if="friendsListLoading")
+ span(v-text="friendsListLoadingProgress" style="margin-left:5px")
+ el-tooltip(placement="top")
+ template(#content)
+ span Cancel
+ el-button(@click="friendsListLoading = false" size="mini" icon="el-icon-loading" circle style="margin-left:5px")
+ template(v-else)
+ el-tooltip(placement="top")
+ template(#content)
+ span Load
+ el-button(@click="friendsListLoadUsers" size="mini" icon="el-icon-refresh-left" circle style="margin-left:5px")
+ div(style="margin:10px 0 0 10px;display:flex;align-items:center")
+ div(style="flex:none;margin-right:10px")
+ el-switch(v-model="friendsListSearchFilterVIP" @change="friendsListSearchChange" active-color="#13ce66")
+ el-input(v-model="friendsListSearch" placeholder="Search" @change="friendsListSearchChange" clearable style="flex:1")
+ el-select(v-model="friendsListSearchFilters" multiple clearable collapse-tags style="flex:none;width:200px;margin:0 10px" @change="friendsListSearchChange" placeholder="Filter")
+ el-option(v-once v-for="type in ['User Name', 'Display Name', 'Status', 'Bio', 'Memo']" :key="type" :label="type" :value="type")
+ el-tooltip(placement="top")
+ template(#content)
+ span Refresh
+ el-button(type="default" @click="friendsListSearchChange" icon="el-icon-refresh" circle style="flex:none")
+ el-tooltip(placement="top")
+ template(#content)
+ span Clear results
+ el-button(type="default" @click="friendsListTable.data = []" icon="el-icon-delete" circle style="flex:none;margin-left:5px")
+ data-tables(v-bind="friendsListTable" @row-click="selectFriendsListRow" style="margin-top:10px;cursor:pointer")
+ el-table-column(label="No." width="70" prop="$friendNum" sortable="custom")
+ el-table-column(label="Avatar" width="70" prop="photo")
+ template(v-once #default="scope")
+ el-popover(v-if="displayVRCPlusIconsAsAvatar && scope.row.userIcon" placement="right" height="500px" trigger="hover")
+ img.friends-list-avatar(slot="reference" v-lazy="scope.row.userIcon")
+ img.friends-list-avatar(v-lazy="scope.row.userIcon" style="height:500px;cursor:pointer" @click="openExternalLink(scope.row.userIcon)")
+ el-popover(v-else placement="right" height="500px" trigger="hover")
+ img.friends-list-avatar(slot="reference" v-lazy="scope.row.currentAvatarThumbnailImageUrl")
+ img.friends-list-avatar(v-lazy="scope.row.currentAvatarImageUrl" style="height:500px;cursor:pointer" @click="openExternalLink(scope.row.currentAvatarImageUrl)")
+ el-table-column(label="Display Name" min-width="130" prop="displayName" sortable :sort-method="(a, b) => sortAlphabetically(a, b, 'displayName')")
+ el-table-column(label="User Name" min-width="120" prop="username" sortable :sort-method="(a, b) => sortAlphabetically(a, b, 'username')")
+ el-table-column(label="Rank" width="110" prop="$trustNum" sortable="custom")
+ template(v-once #default="scope")
+ span.name(v-text="scope.row.$trustLevel" :class="scope.row.$trustClass")
+ el-table-column(label="Status" min-width="180" prop="statusDescription" sortable :sort-method="(a, b) => sortAlphabetically(a, b, 'statusDescription')")
+ el-table-column(label="Language" width="100" prop="$languages")
+ template(v-once #default="scope")
+ el-tooltip(v-for="item in scope.row.$languages" :key="item.key" placement="top")
+ template(#content)
+ span {{ item.value }} ({{ item.key }})
+ span.famfamfam-flags(:class="languageClass(item.key)" style="display:inline-block;margin-left:5px")
+ el-table-column(label="Bio Links" width="100" prop="bioLinks")
+ template(v-once #default="scope")
+ el-tooltip(v-if="link" v-for="(link, index) in scope.row.bioLinks" :key="index")
+ template(#content)
+ span(v-text="link")
+ img(:src="getFaviconUrl(link)" style="width:16px;height:16px;vertical-align:middle;margin-right:5px;cursor:pointer" @click.stop="openExternalLink(link)")
+ el-table-column(label="Last Login" width="170" prop="last_login" sortable :sort-method="(a, b) => sortAlphabetically(a, b, 'last_login')")
+ el-table-column(label="Date Joined" width="120" prop="date_joined" sortable :sort-method="(a, b) => sortAlphabetically(a, b, 'date_joined')")
+ el-table-column(label="Unfriend" width="70" align="right")
+ template(v-once #default="scope")
+ el-button(type="text" icon="el-icon-close" size="mini" @click.stop="confirmDeleteFriend(scope.row.id)")
+
//- settings
.x-container(v-show="$refs.menu && $refs.menu.activeIndex === 'settings'")
div.options-container(style="margin-top:0")
@@ -753,11 +819,8 @@ html
el-button(@click="ossDialog = true" size="small") Open Source Software Notice
//- friends
- .x-aside-container
+ .x-aside-container(v-show="$refs.menu && $refs.menu.activeIndex !== 'friendsList'")
el-select(v-model="quickSearch" clearable placeholder="Search" filterable remote :remote-method="quickSearchRemoteMethod" popper-class="x-quick-search" @change="quickSearchChange" @visible-change="quickSearchVisibleChange" style="flex:none;padding:10px")
- .el-select-dropdown__item
- .x-friend-item
- span Results: #[span(v-text="quickSearchItems.length - 1" style="font-weight:bold")]
el-option(v-for="item in quickSearchItems" :key="item.value" :value="item.value" :label="item.label")
.x-friend-item
template(v-if="item.ref")