diff --git a/src/components/dialogs/UserDialog/UserDialogActivityTab.vue b/src/components/dialogs/UserDialog/UserDialogActivityTab.vue index 6c4dc240..4d89d2bf 100644 --- a/src/components/dialogs/UserDialog/UserDialogActivityTab.vue +++ b/src/components/dialogs/UserDialog/UserDialogActivityTab.vue @@ -7,12 +7,12 @@ variant="ghost" size="icon-sm" :disabled="isLoading" - :title="hasRequestedLoad ? t('dialog.user.activity.refresh_hint') : t('dialog.user.activity.load')" + :title="t('dialog.user.activity.refresh_hint')" @click="loadData"> - + {{ t('dialog.user.activity.refresh_hint') }} @@ -45,28 +45,18 @@ {{ peakTimeText }} -
- - - {{ t('dialog.user.activity.load_hint') }} - -
-
- -
-
+
{{ t('dialog.user.activity.preparing_data') }} {{ t('dialog.user.activity.preparing_data_hint') }}
+ +
+
{{ t('dialog.user.activity.no_data_in_period') }}
@@ -224,6 +214,7 @@ import { timeToText } from '../../../shared/utils'; import { useCurrentUserSessions } from '../../../composables/useCurrentUserSessions'; import { + buildSessionsFromGamelog, calculateOverlapGrid, filterSessionsByPeriod, findBestOverlapTime, @@ -244,8 +235,6 @@ const peakTimeText = ref(''); const selectedPeriod = ref('all'); const filteredEventCount = ref(0); - const isSessionCacheLoading = ref(false); - const hasRequestedLoad = ref(false); const isSelf = computed(() => userDialog.value.id === currentUser.value.id); const topWorlds = ref([]); @@ -579,8 +568,6 @@ const userId = userDialog.value.id; if (!userId) return; - hasRequestedLoad.value = true; - if (userId !== lastLoadedUserId) { selectedPeriod.value = 'all'; } @@ -588,10 +575,17 @@ const requestId = ++activeRequestId; isLoading.value = true; try { - const entry = await activityStore.refreshActivityCache(userId, isSelf.value, { - notifyStart: hasAnyData.value, - notifyComplete: true - }); + if (isSelf.value) { + const rows = await database.getCurrentUserOnlineSessions(); + if (requestId !== activeRequestId) return; + if (userDialog.value.id !== userId) return; + + cachedTargetSessions = buildSessionsFromGamelog(rows); + await finishLoadData(userId); + return; + } + + const entry = await activityStore.refreshActivityCache(userId); if (requestId !== activeRequestId) return; if (userDialog.value.id !== userId) return; hydrateFromCacheEntry(entry); @@ -652,9 +646,7 @@ } function resetActivityState() { - hasRequestedLoad.value = false; isLoading.value = false; - isSessionCacheLoading.value = false; hasAnyData.value = false; peakDayText.value = ''; peakTimeText.value = ''; @@ -673,13 +665,11 @@ function hydrateFromCacheEntry(entry) { cachedTargetSessions = Array.isArray(entry?.sessions) ? entry.sessions : []; - hasRequestedLoad.value = Boolean(entry); } async function loadCachedActivity(userId) { const entry = await activityStore.getCache(userId); if (!entry) { - hasRequestedLoad.value = false; return null; } @@ -691,24 +681,6 @@ return entry; } - async function scheduleAutoRefresh(userId) { - isLoading.value = true; - try { - const entry = await activityStore.refreshActivityCache(userId, isSelf.value, { - notifyComplete: true - }); - if (userDialog.value.id !== userId) return; - hydrateFromCacheEntry(entry); - await finishLoadData(userId); - } catch (error) { - console.error('Error auto-refreshing activity data:', error); - } finally { - if (userDialog.value.id === userId) { - isLoading.value = false; - } - } - } - function getFilteredEventCount() { if (selectedPeriod.value === 'all') return cachedTargetSessions.length; const days = parseInt(selectedPeriod.value, 10); @@ -726,31 +698,22 @@ if (!userId) { return; } + if (isSelf.value) { + if (lastLoadedUserId === userId && (hasAnyData.value || isLoading.value)) { + return; + } + void loadData(); + return; + } void (async () => { const cacheEntry = await loadCachedActivity(userId); - if (!cacheEntry) { - if (activityStore.isRefreshing(userId)) { - hasRequestedLoad.value = true; - isSessionCacheLoading.value = true; - try { - const entry = await activityStore.refreshActivityCache(userId, isSelf.value, { - notifyComplete: true - }); - if (userDialog.value.id !== userId) return; - hydrateFromCacheEntry(entry); - await finishLoadData(userId); - } finally { - if (userDialog.value.id === userId) { - isSessionCacheLoading.value = false; - } - } + if (cacheEntry || activityStore.isRefreshing(userId)) { + if (!cacheEntry && !isLoading.value) { + void loadData(); } return; } - - if (activityStore.isExpired(cacheEntry)) { - void scheduleAutoRefresh(userId); - } + void loadData(); })(); } diff --git a/src/localization/en.json b/src/localization/en.json index 0af2765c..45995389 100644 --- a/src/localization/en.json +++ b/src/localization/en.json @@ -1430,8 +1430,6 @@ "load": "Load Activity", "load_hint": "Load activity data from the local database when needed.", "refresh_hint": "Rebuild activity cache", - "refresh_started": "Rebuilding activity data. Please wait...", - "refresh_complete": "Activity refresh complete", "total_events": "{count} online events", "times_online": "times online", "most_active_day": "Most active day:", diff --git a/src/services/database/activityCache.js b/src/services/database/activityCache.js index 10a1597a..1557192c 100644 --- a/src/services/database/activityCache.js +++ b/src/services/database/activityCache.js @@ -195,16 +195,23 @@ async function upsertMeta(entry) { } async function upsertSessions(userId, sessions = []) { - for (const session of sessions) { + const chunkSize = 250; + for (let chunkStart = 0; chunkStart < sessions.length; chunkStart += chunkSize) { + const chunk = sessions.slice(chunkStart, chunkStart + chunkSize); + const args = {}; + const values = chunk.map((session, index) => { + const suffix = `${chunkStart + index}`; + args[`@user_id_${suffix}`] = userId; + args[`@start_at_${suffix}`] = session.start; + args[`@end_at_${suffix}`] = session.end; + return `(@user_id_${suffix}, @start_at_${suffix}, @end_at_${suffix})`; + }); + await sqliteService.executeNonQuery( `INSERT OR REPLACE INTO ${dbVars.userPrefix}_activity_cache_sessions (user_id, start_at, end_at) - VALUES (@user_id, @start_at, @end_at)`, - { - '@user_id': userId, - '@start_at': session.start, - '@end_at': session.end - } + VALUES ${values.join(', ')}`, + args ); } } diff --git a/src/services/database/index.js b/src/services/database/index.js index 0e3d4269..f8098dad 100644 --- a/src/services/database/index.js +++ b/src/services/database/index.js @@ -72,6 +72,9 @@ const database = { await sqliteService.executeNonQuery( `CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_feed_online_offline (id INTEGER PRIMARY KEY, created_at TEXT, user_id TEXT, display_name TEXT, type TEXT, location TEXT, world_name TEXT, time INTEGER, group_name TEXT)` ); + await sqliteService.executeNonQuery( + `CREATE INDEX IF NOT EXISTS ${dbVars.userPrefix}_feed_online_offline_user_created_idx ON ${dbVars.userPrefix}_feed_online_offline (user_id, created_at)` + ); await sqliteService.executeNonQuery( `CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_activity_cache_meta (user_id TEXT PRIMARY KEY, updated_at TEXT, is_self INTEGER DEFAULT 0, source_last_created_at TEXT, pending_session_start_at INTEGER)` ); diff --git a/src/services/database/tableAlter.js b/src/services/database/tableAlter.js index d6a3bc00..3eb0e054 100644 --- a/src/services/database/tableAlter.js +++ b/src/services/database/tableAlter.js @@ -15,6 +15,11 @@ const tableAlter = { // await sqliteService.executeNonQuery('PRAGMA user_version = 1'); }, + async updateActivityTabDatabaseVersion() { + await this.ensureActivityCacheTables(); + await this.ensureFeedOnlineOfflineIndexes(); + }, + async updateTableForGroupNames() { var tables = []; await sqliteService.execute((dbRow) => { @@ -100,6 +105,18 @@ const tableAlter = { `CREATE INDEX IF NOT EXISTS ${userPrefix}_activity_cache_sessions_user_start_idx ON ${userPrefix}_activity_cache_sessions (user_id, start_at)` ); } + }, + + async ensureFeedOnlineOfflineIndexes() { + const tables = []; + await sqliteService.execute((dbRow) => { + tables.push(dbRow[0]); + }, `SELECT name FROM sqlite_schema WHERE type='table' AND name LIKE '%_feed_online_offline'`); + for (const tableName of tables) { + await sqliteService.executeNonQuery( + `CREATE INDEX IF NOT EXISTS ${tableName}_user_created_idx ON ${tableName} (user_id, created_at)` + ); + } } }; diff --git a/src/stores/activity.js b/src/stores/activity.js index 033d26c1..1bc952fb 100644 --- a/src/stores/activity.js +++ b/src/stores/activity.js @@ -1,14 +1,7 @@ import { defineStore } from 'pinia'; -import { toast } from 'vue-sonner'; -import { useI18n } from 'vue-i18n'; import { database } from '../services/database'; -import { - buildSessionsFromGamelog, - ONLINE_SESSION_MERGE_GAP_MS -} from '../shared/utils/overlapCalculator'; - -const ACTIVITY_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000; +import { ONLINE_SESSION_MERGE_GAP_MS } from '../shared/utils/overlapCalculator'; const refreshJobs = new Map(); function buildSessionsAndPendingFromEvents(events, initialStart = null) { @@ -35,74 +28,25 @@ function buildSessionsAndPendingFromEvents(events, initialStart = null) { }; } -function mergeWithLastSession(lastSession, newSessions) { - if (!lastSession || newSessions.length === 0) { - return { replaceLastSession: null, sessions: newSessions }; - } - - const firstSession = newSessions[0]; - if (firstSession.start > lastSession.end + ONLINE_SESSION_MERGE_GAP_MS) { - return { replaceLastSession: null, sessions: newSessions }; - } - - const mergedFirst = { - start: Math.min(lastSession.start, firstSession.start), - end: Math.max(lastSession.end, firstSession.end) - }; - return { - replaceLastSession: lastSession, - sessions: [mergedFirst, ...newSessions.slice(1)] - }; -} - export const useActivityStore = defineStore('Activity', () => { - const { t } = useI18n(); - function getCache(userId) { return database.getActivityCache(userId); } - function isExpired(cacheEntry) { - if (!cacheEntry?.updatedAt) { - return true; - } - const updatedAtMs = Date.parse(cacheEntry.updatedAt); - if (Number.isNaN(updatedAtMs)) { - return true; - } - return Date.now() - updatedAtMs >= ACTIVITY_CACHE_TTL_MS; - } - function isRefreshing(userId) { return refreshJobs.has(userId); } - async function fullRefresh(userId, isSelf) { - if (isSelf) { - const rows = await database.getCurrentUserOnlineSessions(); - const sessions = buildSessionsFromGamelog(rows); - const sourceLastCreatedAt = - rows.length > 0 ? rows[rows.length - 1].created_at : ''; - const entry = { - userId, - updatedAt: new Date().toISOString(), - isSelf, - sourceLastCreatedAt, - pendingSessionStartAt: null, - sessions - }; - await database.replaceActivityCache(entry); - return database.getActivityCache(userId); - } - + async function fullRefresh(userId) { const events = await database.getOnlineOfflineSessions(userId); - const { sessions, pendingSessionStartAt } = buildSessionsAndPendingFromEvents(events); + const { sessions, pendingSessionStartAt } = + buildSessionsAndPendingFromEvents(events); const sourceLastCreatedAt = events.length > 0 ? events[events.length - 1].created_at : ''; const entry = { userId, updatedAt: new Date().toISOString(), - isSelf, + isSelf: false, sourceLastCreatedAt, pendingSessionStartAt, sessions @@ -114,40 +58,8 @@ export const useActivityStore = defineStore('Activity', () => { async function incrementalRefresh(meta) { const updatedAt = new Date().toISOString(); - if (meta.isSelf) { - if (!meta.sourceLastCreatedAt) { - return fullRefresh(meta.userId, true); - } - - const rows = await database.getCurrentUserOnlineSessionsAfter( - meta.sourceLastCreatedAt - ); - if (rows.length === 0) { - await database.touchActivityCacheMeta({ - ...meta, - updatedAt - }); - return database.getActivityCache(meta.userId); - } - - const sourceLastCreatedAt = rows[rows.length - 1].created_at; - const newSessionsRaw = buildSessionsFromGamelog(rows); - const lastSession = await database.getLastActivityCacheSession(meta.userId); - const merged = mergeWithLastSession(lastSession, newSessionsRaw); - - await database.appendActivityCache({ - ...meta, - updatedAt, - sourceLastCreatedAt, - pendingSessionStartAt: null, - sessions: merged.sessions, - replaceLastSession: merged.replaceLastSession - }); - return database.getActivityCache(meta.userId); - } - if (!meta.sourceLastCreatedAt) { - return fullRefresh(meta.userId, false); + return fullRefresh(meta.userId); } const events = await database.getOnlineOfflineSessionsAfter( @@ -162,10 +74,11 @@ export const useActivityStore = defineStore('Activity', () => { return database.getActivityCache(meta.userId); } - const { sessions, pendingSessionStartAt } = buildSessionsAndPendingFromEvents( - events, - meta.pendingSessionStartAt - ); + const { sessions, pendingSessionStartAt } = + buildSessionsAndPendingFromEvents( + events, + meta.pendingSessionStartAt + ); const sourceLastCreatedAt = events[events.length - 1].created_at; await database.appendActivityCache({ @@ -178,32 +91,18 @@ export const useActivityStore = defineStore('Activity', () => { return database.getActivityCache(meta.userId); } - function refreshActivityCache(userId, isSelf, options = {}) { - const { notifyStart = false, notifyComplete = false } = options; - + function refreshActivityCache(userId) { const existing = refreshJobs.get(userId); if (existing) { return existing; } - if (notifyStart) { - toast.info(t('dialog.user.activity.refresh_started'), { - position: 'bottom-center' - }); - } - const job = (async () => { const meta = await database.getActivityCacheMeta(userId); - const entry = - meta && meta.isSelf === isSelf - ? await incrementalRefresh(meta) - : await fullRefresh(userId, isSelf); - if (notifyComplete) { - toast.success(t('dialog.user.activity.refresh_complete'), { - position: 'bottom-center' - }); + if (!meta || meta.isSelf) { + return fullRefresh(userId); } - return entry; + return incrementalRefresh(meta); })().finally(() => { refreshJobs.delete(userId); }); @@ -214,9 +113,7 @@ export const useActivityStore = defineStore('Activity', () => { return { getCache, - isExpired, isRefreshing, - refreshActivityCache, - ttlMs: ACTIVITY_CACHE_TTL_MS + refreshActivityCache }; }); diff --git a/src/stores/vrcx.js b/src/stores/vrcx.js index e8f2653a..8485c5b2 100644 --- a/src/stores/vrcx.js +++ b/src/stores/vrcx.js @@ -187,7 +187,7 @@ export const useVrcxStore = defineStore('Vrcx', () => { */ async function updateDatabaseVersion() { // requires dbVars.userPrefix to be already set - const databaseVersion = 14; + const databaseVersion = 15; if (state.databaseVersion < databaseVersion) { databaseUpgradeState.value = { visible: state.databaseVersion > 0, @@ -208,6 +208,9 @@ export const useVrcxStore = defineStore('Vrcx', () => { await database.fixCancelFriendRequestTypo(); // fix CancelFriendRequst typo await database.fixBrokenGameLogDisplayNames(); // fix gameLog display names "DisplayName (userId)" await database.upgradeDatabaseVersion(); // update database version + if (state.databaseVersion < 15) { + await database.updateActivityTabDatabaseVersion(); // improve activity tab performance, ver 15 + } await database.vacuum(); // succ await database.optimize(); await configRepository.setInt( @@ -222,7 +225,9 @@ export const useVrcxStore = defineStore('Vrcx', () => { databaseUpgradeState.value.visible = false; await modalStore.alert({ title: t('message.database.upgrade_failed_title'), - description: t('message.database.upgrade_failed_description'), + description: t( + 'message.database.upgrade_failed_description' + ), dismissible: false }); AppApi.ShowDevTools();