diff --git a/src/localization/en.json b/src/localization/en.json index cd5195d7..2b2ece93 100644 --- a/src/localization/en.json +++ b/src/localization/en.json @@ -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": { diff --git a/src/services/database/mutualGraph.js b/src/services/database/mutualGraph.js index 0081c586..afb9b1a9 100644 --- a/src/services/database/mutualGraph.js +++ b/src/services/database/mutualGraph.js @@ -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) => { diff --git a/src/shared/utils/userTransforms.js b/src/shared/utils/userTransforms.js index a713a868..590df683 100644 --- a/src/shared/utils/userTransforms.js +++ b/src/shared/utils/userTransforms.js @@ -234,6 +234,7 @@ export function createDefaultUserRef(json) { $timeSpent: 0, $lastSeen: '', $mutualCount: 0, + $mutualOptedOut: false, $nickName: '', $previousLocation: '', $customTag: '', diff --git a/src/stores/charts.js b/src/stores/charts.js index d5e180ca..02615a6b 100644 --- a/src/stores/charts.js +++ b/src/stores/charts.js @@ -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) { diff --git a/src/stores/friend.js b/src/stores/friend.js index 006c6097..fc1b7b96 100644 --- a/src/stores/friend.js +++ b/src/stores/friend.js @@ -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, diff --git a/src/types/api/user.d.ts b/src/types/api/user.d.ts index b2bcc613..97ddcf97 100644 --- a/src/types/api/user.d.ts +++ b/src/types/api/user.d.ts @@ -56,6 +56,7 @@ export interface VrcxUser extends GetUserResponse { $timeSpent: number; $lastSeen: string; $mutualCount: number; + $mutualOptedOut: boolean; $nickName: string; $previousLocation: string; $customTag: string; diff --git a/src/views/FriendList/FriendList.vue b/src/views/FriendList/FriendList.vue index 4aeee6ee..3d23f1ef 100644 --- a/src/views/FriendList/FriendList.vue +++ b/src/views/FriendList/FriendList.vue @@ -78,7 +78,7 @@ @update:modelValue="toggleFriendsListBulkUnfriendMode" />