fix: load mutual friends button and mutual opt-out status in friend list

This commit is contained in:
pa
2026-03-27 12:20:49 +09:00
parent b3b1d68cc9
commit f71ac77377
8 changed files with 94 additions and 21 deletions

View File

@@ -2861,7 +2861,8 @@
"lastLogin": "Last Login",
"dateJoined": "Date Joined",
"unfriend": "Unfriend",
"mutualFriends": "Mutual Friends"
"mutualFriends": "Mutual Friends",
"mutualOptedOut": "This user has disabled mutual friends sharing. Showing cached data."
},
"profile": {
"invite_messages": {

View File

@@ -52,6 +52,21 @@ const mutualGraph = {
await sqliteService.executeNonQuery('COMMIT');
return;
}
// Also clean links for friends in the new entries even if they
// were previously opted_out. We have fresh data for them now so
// old links must not linger.
let idsToClean = '';
pairs.forEach((_, friendId) => {
if (!friendId) return;
const safe = friendId.replace(/'/g, "''");
idsToClean += `'${safe}',`;
});
if (idsToClean) {
idsToClean = idsToClean.slice(0, -1);
await sqliteService.executeNonQuery(
`DELETE FROM ${linkTable} WHERE friend_id IN (${idsToClean})`
);
}
let friendValues = '';
let edgeValues = '';
pairs.forEach((mutualIds, friendId) => {

View File

@@ -234,6 +234,7 @@ export function createDefaultUserRef(json) {
$timeSpent: 0,
$lastSeen: '',
$mutualCount: 0,
$mutualOptedOut: false,
$nickName: '',
$previousLocation: '',
$customTag: '',

View File

@@ -322,6 +322,14 @@ export const useChartsStore = defineStore('Charts', () => {
mutualGraphStatus.friendSignature = friendCount.value;
mutualGraphStatus.needsRefetch = false;
// Write meta first so saveMutualGraphSnapshot's DELETE
// uses up-to-date opted_out flags to decide what to preserve.
// If this fails, we must NOT proceed with snapshot save because
// the DELETE would use stale meta and corrupt data.
if (metaEntries.size > 0) {
await database.bulkUpsertMutualGraphMeta(metaEntries);
}
try {
const entries = new Map();
mutualMap.forEach((value, friendId) => {
@@ -348,17 +356,6 @@ export const useChartsStore = defineStore('Charts', () => {
);
}
try {
if (metaEntries.size > 0) {
await database.bulkUpsertMutualGraphMeta(metaEntries);
}
} catch (metaErr) {
console.error(
'[MutualNetworkGraph] Failed to write meta',
metaErr
);
}
markMutualGraphLoaded({ notify: true });
return mutualMap;
} catch (err) {

View File

@@ -61,6 +61,7 @@ export const useFriendStore = defineStore('Friend', () => {
let pendingSortedFriendsRebuild = false;
let allUserStatsRequestId = 0;
let allUserMutualCountRequestId = 0;
let allUserMutualOptedOutRequestId = 0;
const derivedDebugCounters = reactive({
allFavoriteFriendIds: 0,
@@ -904,6 +905,11 @@ export const useFriendStore = defineStore('Friend', () => {
return;
}
runInSortedFriendsBatch(() => {
for (const ctx of friends.values()) {
if (ctx?.ref) {
ctx.ref.$mutualCount = 0;
}
}
for (const [userId, mutualCount] of mutualCountMap.entries()) {
const ref = friends.get(userId);
if (ref?.ref) {
@@ -914,6 +920,33 @@ export const useFriendStore = defineStore('Friend', () => {
});
}
/**
*
*/
async function getAllUserMutualOptedOut() {
if (!friends.size) {
return;
}
const requestId = ++allUserMutualOptedOutRequestId;
const metaMap = await database.getMutualGraphMeta();
if (requestId !== allUserMutualOptedOutRequestId) {
return;
}
runInSortedFriendsBatch(() => {
for (const ctx of friends.values()) {
if (ctx?.ref) {
ctx.ref.$mutualOptedOut = false;
}
}
for (const [userId, meta] of metaMap.entries()) {
const ref = friends.get(userId);
if (ref?.ref) {
ref.ref.$mutualOptedOut = Boolean(meta.optedOut);
}
}
});
}
/**
*
* @param {string} id
@@ -1398,6 +1431,7 @@ export const useFriendStore = defineStore('Friend', () => {
updateOnlineFriendCounter,
getAllUserStats,
getAllUserMutualCount,
getAllUserMutualOptedOut,
initFriendLog,
migrateFriendLog,
getFriendLog,

View File

@@ -56,6 +56,7 @@ export interface VrcxUser extends GetUserResponse {
$timeSpent: number;
$lastSeen: string;
$mutualCount: number;
$mutualOptedOut: boolean;
$nickName: string;
$previousLocation: string;
$customTag: string;

View File

@@ -78,7 +78,7 @@
@update:modelValue="toggleFriendsListBulkUnfriendMode" />
</div>
<div class="flex items-center">
<Button variant="outline" class="mr-2" @click="openChartsTab">
<Button variant="outline" class="mr-2" :disabled="isMutualFetching || isMutualOptOut" @click="loadMutualFriends">
{{ t('view.friend_list.load_mutual_friends') }}
</Button>
@@ -133,9 +133,11 @@
import {
useAppearanceSettingsStore,
useChartsStore,
useFriendStore,
useModalStore,
useSearchStore
useSearchStore,
useUserStore
} from '../../stores';
import { friendRequest, userRequest } from '../../api';
import { DataTableLayout } from '../../components/ui/data-table';
@@ -144,7 +146,6 @@
import { createColumns } from './columns.jsx';
import { localeIncludes } from '../../shared/utils';
import removeConfusables, { removeWhitespace } from '../../services/confusables';
import { router } from '../../plugins/router';
import { useVrcxVueTable } from '../../lib/table/useVrcxVueTable';
import { showUserDialog } from '../../coordinators/userCoordinator';
import { confirmDeleteFriend, handleFriendDelete } from '../../coordinators/friendRelationshipCoordinator';
@@ -156,7 +157,10 @@
const { friends, allFavoriteFriendIds } = storeToRefs(useFriendStore());
const modalStore = useModalStore();
const { getAllUserStats, getAllUserMutualCount } = useFriendStore();
const { getAllUserStats, getAllUserMutualCount, getAllUserMutualOptedOut } = useFriendStore();
const chartsStore = useChartsStore();
const isMutualFetching = computed(() => chartsStore.mutualGraphStatus.isFetching);
const isMutualOptOut = computed(() => Boolean(useUserStore().currentUser?.hasSharedConnectionsOptOut));
const appearanceSettingsStore = useAppearanceSettingsStore();
const { randomUserColours } = storeToRefs(appearanceSettingsStore);
const { userImage } = useUserDisplay();
@@ -303,7 +307,8 @@
}
friendStatsRefreshInFlight = Promise.allSettled([
getAllUserStats(),
getAllUserMutualCount()
getAllUserMutualCount(),
getAllUserMutualOptedOut()
]).then((results) => {
if (results.every((result) => result.status === 'fulfilled')) {
lastFriendStatsRefreshAt = Date.now();
@@ -556,8 +561,13 @@
/**
*
*/
function openChartsTab() {
router.push({ name: 'charts' });
async function loadMutualFriends() {
if (isMutualFetching.value) return;
await chartsStore.fetchMutualGraph();
await Promise.allSettled([
getAllUserMutualCount(),
getAllUserMutualOptedOut()
]);
}
/**

View File

@@ -1,4 +1,4 @@
import { ArrowUpDown, User, UserMinus } from 'lucide-vue-next';
import { ArrowUpDown, EyeOff, User, UserMinus } from 'lucide-vue-next';
import {
Avatar,
AvatarFallback,
@@ -391,7 +391,21 @@ export const createColumns = ({
},
cell: ({ row }) => {
const count = row.original?.$mutualCount;
return count ? <span>{count}</span> : null;
const optedOut = row.original?.$mutualOptedOut;
if (!count && !optedOut) return null;
return (
<span class="inline-flex items-center gap-1">
{count || null}
{optedOut ? (
<TooltipWrapper
side="top"
content={t('table.friendList.mutualOptedOut')}
>
<EyeOff class="h-3.5 w-3.5 text-muted-foreground" />
</TooltipWrapper>
) : null}
</span>
);
}
},
{