mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-07 06:56:04 +02:00
feat: add functionality to exclude specific friends from the mutual friends graph
This commit is contained in:
@@ -436,7 +436,10 @@
|
|||||||
"edge_curvature_help": "How curved the lines are. Higher means smoother lines in dense areas.",
|
"edge_curvature_help": "How curved the lines are. Higher means smoother lines in dense areas.",
|
||||||
"community_separation": "Community separation",
|
"community_separation": "Community separation",
|
||||||
"community_separation_help": "How far apart different communities are pushed. Higher means more distinct clusters.",
|
"community_separation_help": "How far apart different communities are pushed. Higher means more distinct clusters.",
|
||||||
"reset_defaults": "Reset to defaults"
|
"reset_defaults": "Reset to defaults",
|
||||||
|
"exclude_friends": "Exclude friends",
|
||||||
|
"exclude_friends_placeholder": "Select friends to exclude",
|
||||||
|
"exclude_friends_help": "Selected friends will be hidden from the graph."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -157,6 +157,52 @@
|
|||||||
</Field>
|
</Field>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|
||||||
|
<FieldGroup class="gap-4 p-4">
|
||||||
|
<Field>
|
||||||
|
<FieldLabel>{{
|
||||||
|
t('view.charts.mutual_friend.settings.exclude_friends')
|
||||||
|
}}</FieldLabel>
|
||||||
|
<FieldContent>
|
||||||
|
<VirtualCombobox
|
||||||
|
v-model="excludedFriendIds"
|
||||||
|
:groups="excludePickerGroups"
|
||||||
|
:placeholder="
|
||||||
|
t('view.charts.mutual_friend.settings.exclude_friends_placeholder')
|
||||||
|
"
|
||||||
|
:search-placeholder="t('view.charts.mutual_friend.actions.go_to_friend')"
|
||||||
|
:multiple="true">
|
||||||
|
<template #item="{ item, selected }">
|
||||||
|
<div class="x-friend-item flex w-full items-center">
|
||||||
|
<template v-if="item.user">
|
||||||
|
<div :class="['avatar', userStatusClass(item.user)]">
|
||||||
|
<img :src="userImage(item.user)" loading="lazy" />
|
||||||
|
</div>
|
||||||
|
<div class="detail">
|
||||||
|
<span
|
||||||
|
class="name"
|
||||||
|
:style="{ color: item.user.$userColour }"
|
||||||
|
>{{ item.user.displayName }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
</template>
|
||||||
|
<CheckIcon
|
||||||
|
:class="[
|
||||||
|
'ml-auto size-4',
|
||||||
|
selected ? 'opacity-100' : 'opacity-0'
|
||||||
|
]" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VirtualCombobox>
|
||||||
|
<p class="mt-1 text-xs text-muted-foreground">
|
||||||
|
{{ t('view.charts.mutual_friend.settings.exclude_friends_help') }}
|
||||||
|
</p>
|
||||||
|
</FieldContent>
|
||||||
|
</Field>
|
||||||
|
</FieldGroup>
|
||||||
|
|
||||||
<div class="p-4 pt-0">
|
<div class="p-4 pt-0">
|
||||||
<Button variant="outline" size="sm" class="w-full" @click="resetLayoutSettings">
|
<Button variant="outline" size="sm" class="w-full" @click="resetLayoutSettings">
|
||||||
{{ t('view.charts.mutual_friend.settings.reset_defaults') }}
|
{{ t('view.charts.mutual_friend.settings.reset_defaults') }}
|
||||||
@@ -278,6 +324,7 @@
|
|||||||
let resizeObserver = null;
|
let resizeObserver = null;
|
||||||
let pendingRender = null;
|
let pendingRender = null;
|
||||||
let pendingLayoutUpdate = null;
|
let pendingLayoutUpdate = null;
|
||||||
|
let lastMutualMap = null;
|
||||||
|
|
||||||
const LAYOUT_ITERATIONS_MIN = 300;
|
const LAYOUT_ITERATIONS_MIN = 300;
|
||||||
const LAYOUT_ITERATIONS_MAX = 1500;
|
const LAYOUT_ITERATIONS_MAX = 1500;
|
||||||
@@ -404,6 +451,7 @@
|
|||||||
|
|
||||||
function resetLayoutSettings() {
|
function resetLayoutSettings() {
|
||||||
Object.assign(layoutSettings, LAYOUT_DEFAULTS);
|
Object.assign(layoutSettings, LAYOUT_DEFAULTS);
|
||||||
|
excludedFriendIds.value = [];
|
||||||
persistLayoutSettings();
|
persistLayoutSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,6 +489,51 @@
|
|||||||
|
|
||||||
const selectedFriendId = ref(null);
|
const selectedFriendId = ref(null);
|
||||||
|
|
||||||
|
const EXCLUDED_FRIENDS_KEY = 'VRCX_MutualGraphExcludedFriends';
|
||||||
|
const excludedFriendIds = ref(loadExcludedFriends());
|
||||||
|
|
||||||
|
function loadExcludedFriends() {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(EXCLUDED_FRIENDS_KEY);
|
||||||
|
if (stored) return JSON.parse(stored);
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveExcludedFriends() {
|
||||||
|
localStorage.setItem(EXCLUDED_FRIENDS_KEY, JSON.stringify(excludedFriendIds.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(excludedFriendIds, () => {
|
||||||
|
saveExcludedFriends();
|
||||||
|
if (lastMutualMap) applyGraph(lastMutualMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
const excludePickerGroups = computed(() => {
|
||||||
|
if (!lastMutualMap) return [];
|
||||||
|
const currentUserId = currentUser.value?.id;
|
||||||
|
const seen = new Set();
|
||||||
|
const items = [];
|
||||||
|
for (const [friendId, { mutuals }] of lastMutualMap.entries()) {
|
||||||
|
if (friendId === currentUserId || seen.has(friendId)) continue;
|
||||||
|
seen.add(friendId);
|
||||||
|
const cached = cachedUsers.get(friendId);
|
||||||
|
const displayName = cached?.displayName || friendId;
|
||||||
|
items.push({ value: friendId, label: displayName, search: displayName, user: cached || null });
|
||||||
|
for (const mutual of mutuals) {
|
||||||
|
if (!mutual?.id || mutual.id === currentUserId || seen.has(mutual.id)) continue;
|
||||||
|
seen.add(mutual.id);
|
||||||
|
const mc = cachedUsers.get(mutual.id);
|
||||||
|
const mName = mc?.displayName || mutual.displayName || mutual.id;
|
||||||
|
items.push({ value: mutual.id, label: mName, search: mName, user: mc || null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
return [{ key: 'friends', label: t('side_panel.friends'), items }];
|
||||||
|
});
|
||||||
|
|
||||||
const friendPickerGroups = computed(() => {
|
const friendPickerGroups = computed(() => {
|
||||||
if (!currentGraph || !graphNodeCount.value) return [];
|
if (!currentGraph || !graphNodeCount.value) return [];
|
||||||
const currentUserId = currentUser.value?.id;
|
const currentUserId = currentUser.value?.id;
|
||||||
@@ -693,11 +786,12 @@
|
|||||||
allowSelfLoops: false
|
allowSelfLoops: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const excludeSet = new Set(excludedFriendIds.value);
|
||||||
const nodeDegree = new Map();
|
const nodeDegree = new Map();
|
||||||
const nodeNames = new Map();
|
const nodeNames = new Map();
|
||||||
|
|
||||||
function ensureNode(id, name) {
|
function ensureNode(id, name) {
|
||||||
if (!id) return;
|
if (!id || excludeSet.has(id)) return;
|
||||||
if (!graph.hasNode(id)) {
|
if (!graph.hasNode(id)) {
|
||||||
graph.addNode(id);
|
graph.addNode(id);
|
||||||
nodeDegree.set(id, 0);
|
nodeDegree.set(id, 0);
|
||||||
@@ -707,6 +801,7 @@
|
|||||||
|
|
||||||
function addEdge(source, target) {
|
function addEdge(source, target) {
|
||||||
if (!source || !target || source === target) return;
|
if (!source || !target || source === target) return;
|
||||||
|
if (excludeSet.has(source) || excludeSet.has(target)) return;
|
||||||
const [a, b] = [source, target].sort();
|
const [a, b] = [source, target].sort();
|
||||||
const key = `${a}__${b}`;
|
const key = `${a}__${b}`;
|
||||||
if (graph.hasEdge(key)) return;
|
if (graph.hasEdge(key)) return;
|
||||||
@@ -928,6 +1023,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function applyGraph(mutualMap) {
|
function applyGraph(mutualMap) {
|
||||||
|
lastMutualMap = mutualMap;
|
||||||
const graph = buildGraphFromMutualMap(mutualMap);
|
const graph = buildGraphFromMutualMap(mutualMap);
|
||||||
currentGraph = graph;
|
currentGraph = graph;
|
||||||
renderGraph(graph);
|
renderGraph(graph);
|
||||||
|
|||||||
Reference in New Issue
Block a user