+
{{ 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();