Mutual friends tab

This commit is contained in:
Natsumi
2025-11-11 15:00:41 +11:00
parent 8789e05eb6
commit 4900432451
8 changed files with 261 additions and 9 deletions

View File

@@ -168,6 +168,44 @@ const userReq = {
};
return args;
});
},
getMutualCounts(params) {
return request(`users/${params.userId}/mutuals`, {
method: 'GET'
}).then((json) => {
const args = {
json,
params
};
return args;
});
},
getMutualFriends(params) {
return request(`users/${params.userId}/mutuals/friends`, {
method: 'GET',
params
}).then((json) => {
const args = {
json,
params
};
return args;
});
},
getMutualGroups(params) {
return request(`users/${params.userId}/mutuals/groups`, {
method: 'GET',
params
}).then((json) => {
const args = {
json,
params
};
return args;
});
}
};

View File

@@ -707,9 +707,9 @@ i.x-status-icon.red {
border-color: rgb(255, 208, 0) !important;
}
.x-tag-vrcplus {
color: rgb(255, 208, 0);
border-color: rgb(255, 208, 0) !important;
.x-tag-mutual-friend {
color: #67c23a;
border-color: #67c23a !important;
}
.x-tag-platform-pc {

View File

@@ -514,6 +514,73 @@
</div>
</el-tab-pane>
<el-tab-pane
name="Mutual Friends"
v-if="userDialog.isFriend"
:label="t('dialog.user.mutual_friends.header')"
lazy>
<div style="display: flex; align-items: center; justify-content: space-between">
<div style="display: flex; align-items: center">
<el-button
type="default"
:loading="userDialog.isMutualFriendsLoading"
size="small"
:icon="Refresh"
circle
@click="getUserMutualFriends(userDialog.id)">
</el-button>
<span style="margin-left: 5px">{{
t('dialog.user.groups.total_count', { count: userDialog.mutualFriends.length })
}}</span>
</div>
<div style="display: flex; align-items: center">
<span style="margin-right: 5px">{{ t('dialog.user.groups.sort_by') }}</span>
<el-dropdown
trigger="click"
size="small"
style="margin-right: 5px"
:disabled="userDialog.isMutualFriendsLoading"
@click.stop>
<el-button size="small">
<span
>{{ t(userDialog.mutualFriendSorting.name) }}
<el-icon style="margin-left: 5px"><ArrowDown /></el-icon>
</span>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="(item, key) in userDialogMutualFriendSortingOptions"
:key="key"
@click="setUserDialogMutualFriendSorting(item)"
>{{ t(item.name) }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<ul
class="x-friend-list"
style="margin-top: 10px; overflow: auto; max-height: 250px; min-width: 130px">
<li
v-for="user in userDialog.mutualFriends"
:key="user.id"
class="x-friend-item x-friend-item-border"
@click="showUserDialog(user.id)">
<div class="avatar">
<img :src="userImage(user)" loading="lazy" />
</div>
<div class="detail">
<span
class="name"
:style="{ color: user.$userColour }"
v-text="user.displayName"></span>
</div>
</li>
</ul>
</el-tab-pane>
<el-tab-pane name="Groups" :label="t('dialog.user.groups.header')" lazy>
<div style="display: flex; align-items: center; justify-content: space-between">
<div style="display: flex; align-items: center">
@@ -1253,6 +1320,9 @@
import { useI18n } from 'vue-i18n';
import {
compareByDisplayName,
compareByFriendOrder,
compareByLastActiveRef,
compareByMemberCount,
compareByName,
copyToClipboard,
@@ -1298,9 +1368,9 @@
} from '../../../api';
import { getNextDialogIndex, redirectToToolsTab } from '../../../shared/utils/base/ui';
import { processBulk, request } from '../../../service/request';
import { userDialogGroupSortingOptions, userDialogMutualFriendSortingOptions } from '../../../shared/constants';
import { userDialogWorldOrderOptions, userDialogWorldSortingOptions } from '../../../shared/constants/';
import { database } from '../../../service/database';
import { userDialogGroupSortingOptions } from '../../../shared/constants';
import SendInviteDialog from '../InviteDialog/SendInviteDialog.vue';
import UserSummaryHeader from './UserSummaryHeader.vue';
@@ -1376,6 +1446,7 @@
const userDialogGroupEditSelectedGroupIds = ref([]); // selected groups in edit mode
const userDialogLastActiveTab = ref('Info');
const userDialogLastMutualFriends = ref('');
const userDialogLastGroup = ref('');
const userDialogLastAvatar = ref('');
const userDialogLastWorld = ref('');
@@ -1499,6 +1570,15 @@
if (vrchatCredit.value === null) {
getVRChatCredits();
}
} else if (tabName === 'Mutual Friends') {
if (userId === currentUser.value.id) {
userDialogLastActiveTab.value = 'Info';
return;
}
if (userDialogLastMutualFriends.value !== userId) {
userDialogLastMutualFriends.value = userId;
getUserMutualFriends(userId);
}
} else if (tabName === 'Groups') {
if (userDialogLastGroup.value !== userId) {
userDialogLastGroup.value = userId;
@@ -2083,6 +2163,38 @@
userDialog.value.isGroupsLoading = false;
}
async function getUserMutualFriends(userId) {
userDialog.value.isMutualFriendsLoading = true;
userDialog.value.mutualFriends = [];
const params = {
userId,
n: 100,
offset: 0
};
processBulk({
fn: userRequest.getMutualFriends,
N: -1,
params,
handle: (args) => {
for (const json of args.json) {
if (userDialog.value.mutualFriends.some((u) => u.id === json.id)) {
continue;
}
const ref = cachedUsers.get(json.id);
if (typeof ref !== 'undefined') {
userDialog.value.mutualFriends.push(ref);
} else {
userDialog.value.mutualFriends.push(json);
}
}
setUserDialogMutualFriendSorting(userDialog.value.mutualFriendSorting);
},
done: () => {
userDialog.value.isMutualFriendsLoading = false;
}
});
}
function sortGroupsByInGame(a, b) {
const aIndex = inGameGroupOrder.value.indexOf(a?.id);
const bIndex = inGameGroupOrder.value.indexOf(b?.id);
@@ -2347,6 +2459,22 @@
await sortCurrentUserGroups();
}
async function setUserDialogMutualFriendSorting(sortOrder) {
const D = userDialog.value;
D.mutualFriendSorting = sortOrder;
switch (sortOrder.value) {
case 'alphabetical':
D.mutualFriends.sort(compareByDisplayName);
break;
case 'lastActive':
D.mutualFriends.sort(compareByLastActiveRef);
break;
case 'friendOrder':
D.mutualFriends.sort(compareByFriendOrder);
break;
}
}
async function exitEditModeCurrentUserGroups() {
userDialogGroupEditMode.value = false;
userDialogGroupEditGroups.value = [];

View File

@@ -116,6 +116,20 @@
{{ userDialog.ref.$friendNumber ? userDialog.ref.$friendNumber : '' }}
</el-tag>
</el-tooltip>
<el-tooltip
v-if="userDialog.mutualFriendCount"
placement="top"
:content="t('dialog.user.tags.mutual_friends')">
<el-tag
type="info"
effect="plain"
size="small"
class="x-tag-mutual-friend"
style="margin-right: 5px; margin-top: 5px">
<i class="ri-group-line"></i>
{{ userDialog.mutualFriendCount }}
</el-tag>
</el-tooltip>
<el-tag
v-if="userDialog.ref.$isTroll"
type="info"

View File

@@ -791,7 +791,8 @@
"vrchat_team": "VRChat Team",
"18_plus_verified": "18+ Verified",
"age_verified": "Age Verified",
"trust_level": "Trust Level"
"trust_level": "Trust Level",
"mutual_friends": "Mutual Friends"
},
"badges": {
"assigned": "Assigned",
@@ -937,6 +938,14 @@
},
"json": {
"header": "JSON"
},
"mutual_friends": {
"header": "Mutual Friends",
"sorting": {
"alphabetical": "Alphabetical",
"last_active": "Last Active",
"friend_order": "Friend Order"
}
}
},
"world": {

View File

@@ -47,8 +47,24 @@ const userDialogGroupSortingOptions = {
}
};
const userDialogMutualFriendSortingOptions = {
alphabetical: {
name: 'dialog.user.mutual_friends.sorting.alphabetical',
value: 'alphabetical'
},
lastActive: {
name: 'dialog.user.mutual_friends.sorting.last_active',
value: 'lastActive'
},
friendOrder: {
name: 'dialog.user.mutual_friends.sorting.friend_order',
value: 'friendOrder'
}
};
export {
userDialogWorldSortingOptions,
userDialogWorldOrderOptions,
userDialogGroupSortingOptions
userDialogGroupSortingOptions,
userDialogMutualFriendSortingOptions
};

View File

@@ -170,7 +170,7 @@ function compareByLastActive(a, b) {
b.ref?.$online_for &&
a.ref.$online_for === b.ref.$online_for
) {
compareByActivityField(a, b, 'last_login');
return compareByActivityField(a, b, 'last_login');
}
return compareByActivityField(a, b, '$online_for');
}
@@ -178,6 +178,16 @@ function compareByLastActive(a, b) {
return compareByActivityField(a, b, 'last_activity');
}
function compareByLastActiveRef(a, b) {
if (a.state === 'online' && b.state === 'online') {
if (a.$online_for && b.$online_for && a.$online_for === b.$online_for) {
return a.last_login < b.last_login ? 1 : -1;
}
return a.$online_for < b.$online_for ? 1 : -1;
}
return a.last_activity < b.last_activity ? 1 : -1;
}
/**
* last seen
* @param {object} a
@@ -259,6 +269,19 @@ function compareByLocation(a, b) {
return a.ref.location.localeCompare(b.ref.location);
}
/**
* $friendNumber friend order
* @param {object} a
* @param {object} b
* @returns
*/
function compareByFriendOrder(a, b) {
if (typeof a === 'undefined' || typeof b === 'undefined') {
return 0;
}
return b.$friendNumber - a.$friendNumber;
}
export {
compareByName,
compareByCreatedAt,
@@ -270,7 +293,9 @@ export {
compareByPrivate,
compareByStatus,
compareByLastActive,
compareByLastActiveRef,
compareByLastSeen,
compareByLocationAt,
compareByLocation
compareByLocation,
compareByFriendOrder
};

View File

@@ -222,6 +222,10 @@ export const useUserStore = defineStore('User', () => {
name: 'dialog.user.groups.sorting.alphabetical',
value: 'alphabetical'
},
mutualFriendSorting: {
name: 'dialog.user.mutual_friends.sorting.alphabetical',
value: 'alphabetical'
},
avatarSorting: 'update',
avatarReleaseStatus: 'all',
treeData: [],
@@ -257,7 +261,11 @@ export const useUserStore = defineStore('User', () => {
previousDisplayNames: [],
dateFriended: '',
unFriended: false,
dateFriendedInfo: []
dateFriendedInfo: [],
mutualFriendCount: 0,
mutualGroupCount: 0,
mutualFriends: [],
isMutualFriendsLoading: false
});
const currentTravelers = reactive(new Map());
@@ -813,6 +821,8 @@ export const useUserStore = defineStore('User', () => {
D.dateFriended = '';
D.unFriended = false;
D.dateFriendedInfo = [];
D.mutualFriendCount = 0;
D.mutualGroupCount = 0;
if (userId === currentUser.value.id) {
getWorldName(currentUser.value.homeLocation).then((worldName) => {
D.$homeLocationName = worldName;
@@ -951,6 +961,18 @@ export const useUserStore = defineStore('User', () => {
D.isShowAvatar = true;
}
});
if (D.isFriend) {
userRequest
.getMutualCounts({ userId })
.then((args) => {
if (args.params.userId === D.id) {
D.mutualFriendCount =
args.json.friends;
D.mutualGroupCount =
args.json.groups;
}
});
}
} else {
D.previousDisplayNames =
currentUser.value.pastDisplayNames;