mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-04 13:56:07 +02:00
feat: mutual friend graph (#1491)
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import { ElMessage, ElNotification } from 'element-plus';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useFriendStore } from './friend';
|
||||
|
||||
function createDefaultFetchState() {
|
||||
return {
|
||||
processedFriends: 0
|
||||
};
|
||||
}
|
||||
|
||||
function createDefaultPayload() {
|
||||
return {
|
||||
nodes: [],
|
||||
links: []
|
||||
};
|
||||
}
|
||||
|
||||
export const useChartsStore = defineStore('Charts', () => {
|
||||
const friendStore = useFriendStore();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const activeTab = ref('instance');
|
||||
const mutualGraphPayload = ref(createDefaultPayload());
|
||||
const mutualGraphFetchState = reactive(createDefaultFetchState());
|
||||
const mutualGraphStatus = reactive({
|
||||
isFetching: false,
|
||||
hasFetched: false,
|
||||
fetchError: '',
|
||||
completionNotified: false,
|
||||
friendSignature: 0,
|
||||
needsRefetch: false,
|
||||
cancelRequested: false
|
||||
});
|
||||
|
||||
const friendCount = computed(() => friendStore.friends.size || 0);
|
||||
|
||||
function showInfoMessage(message, type) {
|
||||
ElMessage({
|
||||
message,
|
||||
type,
|
||||
duration: 4000,
|
||||
grouping: true
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => mutualGraphStatus.isFetching,
|
||||
(isFetching, wasFetching) => {
|
||||
if (isFetching) {
|
||||
showInfoMessage(
|
||||
t('view.charts.mutual_friend.notifications.start_fetching'),
|
||||
'info'
|
||||
);
|
||||
mutualGraphStatus.completionNotified = false;
|
||||
} else if (
|
||||
wasFetching &&
|
||||
mutualGraphStatus.hasFetched &&
|
||||
!mutualGraphStatus.completionNotified
|
||||
) {
|
||||
mutualGraphStatus.completionNotified = true;
|
||||
ElNotification({
|
||||
title: t(
|
||||
'view.charts.mutual_friend.notifications.mutual_friend_graph_ready_title'
|
||||
),
|
||||
message: t(
|
||||
'view.charts.mutual_friend.notifications.mutual_friend_graph_ready_message'
|
||||
),
|
||||
type: 'success',
|
||||
position: 'top-right',
|
||||
duration: 5000,
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(friendCount, (count) => {
|
||||
if (
|
||||
!mutualGraphStatus.hasFetched ||
|
||||
mutualGraphStatus.isFetching ||
|
||||
!mutualGraphStatus.friendSignature ||
|
||||
mutualGraphStatus.needsRefetch
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (count !== mutualGraphStatus.friendSignature) {
|
||||
mutualGraphStatus.needsRefetch = true;
|
||||
showInfoMessage(
|
||||
t(
|
||||
'view.charts.mutual_friend.notifications.friend_list_changed_fetch_again'
|
||||
),
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function resetMutualGraphState() {
|
||||
mutualGraphPayload.value = createDefaultPayload();
|
||||
Object.assign(mutualGraphFetchState, createDefaultFetchState());
|
||||
mutualGraphStatus.isFetching = false;
|
||||
mutualGraphStatus.hasFetched = false;
|
||||
mutualGraphStatus.fetchError = '';
|
||||
mutualGraphStatus.completionNotified = false;
|
||||
mutualGraphStatus.friendSignature = 0;
|
||||
mutualGraphStatus.needsRefetch = false;
|
||||
mutualGraphStatus.cancelRequested = false;
|
||||
}
|
||||
|
||||
return {
|
||||
activeTab,
|
||||
mutualGraphPayload,
|
||||
mutualGraphFetchState,
|
||||
mutualGraphStatus,
|
||||
resetMutualGraphState
|
||||
};
|
||||
});
|
||||
+25
-32
@@ -5,6 +5,8 @@ import { useI18n } from 'vue-i18n';
|
||||
|
||||
import {
|
||||
compareByCreatedAtAscending,
|
||||
createRateLimiter,
|
||||
executeWithBackoff,
|
||||
getFriendsSortFunction,
|
||||
getGroupName,
|
||||
getNameColour,
|
||||
@@ -690,39 +692,30 @@ export const useFriendStore = defineStore('Friend', () => {
|
||||
const MAX_RETRY = 5;
|
||||
const RETRY_BASE_DELAY = 1000;
|
||||
|
||||
const stamps = [];
|
||||
async function throttle() {
|
||||
const now = Date.now();
|
||||
while (stamps.length && now - stamps[0] > 60_000) stamps.shift();
|
||||
if (stamps.length >= RATE_PER_MINUTE) {
|
||||
const wait = 60_000 - (now - stamps[0]);
|
||||
await new Promise((r) => setTimeout(r, wait));
|
||||
}
|
||||
stamps.push(Date.now());
|
||||
}
|
||||
const rateLimiter = createRateLimiter({
|
||||
limitPerInterval: RATE_PER_MINUTE,
|
||||
intervalMs: 60_000
|
||||
});
|
||||
|
||||
async function fetchPage(offset, retries = MAX_RETRY) {
|
||||
try {
|
||||
const { json } = await friendRequest.getFriends({
|
||||
...args,
|
||||
n: PAGE_SIZE,
|
||||
offset
|
||||
});
|
||||
return Array.isArray(json) ? json : [];
|
||||
} catch (err) {
|
||||
const is429 =
|
||||
err.status === 429 || (err.message || '').includes('429');
|
||||
if (is429 && retries > 0) {
|
||||
await new Promise((r) =>
|
||||
setTimeout(
|
||||
r,
|
||||
RETRY_BASE_DELAY * Math.pow(2, MAX_RETRY - retries)
|
||||
)
|
||||
);
|
||||
return fetchPage(offset, retries - 1);
|
||||
async function fetchPage(offset) {
|
||||
const result = await executeWithBackoff(
|
||||
async () => {
|
||||
const { json } = await friendRequest.getFriends({
|
||||
...args,
|
||||
n: PAGE_SIZE,
|
||||
offset
|
||||
});
|
||||
return Array.isArray(json) ? json : [];
|
||||
},
|
||||
{
|
||||
maxRetries: MAX_RETRY,
|
||||
baseDelay: RETRY_BASE_DELAY,
|
||||
shouldRetry: (err) =>
|
||||
err?.status === 429 ||
|
||||
(err?.message || '').includes('429')
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
let nextOffset = 0;
|
||||
@@ -742,7 +735,7 @@ export const useFriendStore = defineStore('Friend', () => {
|
||||
const offset = getNextOffset();
|
||||
if (offset === null) break;
|
||||
|
||||
await throttle();
|
||||
await rateLimiter.wait();
|
||||
|
||||
const page = await fetchPage(offset);
|
||||
if (page.length === 0) {
|
||||
|
||||
+4
-2
@@ -1,5 +1,4 @@
|
||||
import { createPinia } from 'pinia';
|
||||
import { createSentryPiniaPlugin } from '@sentry/vue';
|
||||
|
||||
import { isSentryEnabled } from '../plugin';
|
||||
import { useAdvancedSettingsStore } from './settings/advanced';
|
||||
@@ -7,6 +6,7 @@ import { useAppearanceSettingsStore } from './settings/appearance';
|
||||
import { useAuthStore } from './auth';
|
||||
import { useAvatarProviderStore } from './avatarProvider';
|
||||
import { useAvatarStore } from './avatar';
|
||||
import { useChartsStore } from './charts';
|
||||
import { useDiscordPresenceSettingsStore } from './settings/discordPresence';
|
||||
import { useFavoriteStore } from './favorite';
|
||||
import { useFeedStore } from './feed';
|
||||
@@ -152,7 +152,8 @@ export function createGlobalStores() {
|
||||
sharedFeed: useSharedFeedStore(),
|
||||
updateLoop: useUpdateLoopStore(),
|
||||
auth: useAuthStore(),
|
||||
vrcStatus: useVrcStatusStore()
|
||||
vrcStatus: useVrcStatusStore(),
|
||||
charts: useChartsStore()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -175,6 +176,7 @@ export {
|
||||
useNotificationStore,
|
||||
usePhotonStore,
|
||||
useSearchStore,
|
||||
useChartsStore,
|
||||
useAdvancedSettingsStore,
|
||||
useAppearanceSettingsStore,
|
||||
useDiscordPresenceSettingsStore,
|
||||
|
||||
Reference in New Issue
Block a user