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")