mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 14:53:50 +02:00
fix: activity tab
This commit is contained in:
@@ -23,7 +23,6 @@
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="180">{{ t('dialog.user.activity.period_180') }}</SelectItem>
|
|
||||||
<SelectItem value="90">{{ t('dialog.user.activity.period_90') }}</SelectItem>
|
<SelectItem value="90">{{ t('dialog.user.activity.period_90') }}</SelectItem>
|
||||||
<SelectItem value="30">{{ t('dialog.user.activity.period_30') }}</SelectItem>
|
<SelectItem value="30">{{ t('dialog.user.activity.period_30') }}</SelectItem>
|
||||||
<SelectItem value="7">{{ t('dialog.user.activity.period_7') }}</SelectItem>
|
<SelectItem value="7">{{ t('dialog.user.activity.period_7') }}</SelectItem>
|
||||||
@@ -241,6 +240,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
let activeRequestId = 0;
|
let activeRequestId = 0;
|
||||||
|
let activeOverlapRequestId = 0;
|
||||||
let lastLoadedUserId = '';
|
let lastLoadedUserId = '';
|
||||||
const pendingWorldThumbnailFetches = new Set();
|
const pendingWorldThumbnailFetches = new Set();
|
||||||
|
|
||||||
@@ -281,6 +281,7 @@
|
|||||||
mainHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
|
mainHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
|
||||||
overlapHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
|
overlapHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
|
||||||
activeRequestId++;
|
activeRequestId++;
|
||||||
|
activeOverlapRequestId++;
|
||||||
lastLoadedUserId = '';
|
lastLoadedUserId = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,6 +292,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const requestId = ++activeRequestId;
|
const requestId = ++activeRequestId;
|
||||||
|
++activeOverlapRequestId;
|
||||||
if (!silent) {
|
if (!silent) {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
}
|
}
|
||||||
@@ -386,20 +388,56 @@
|
|||||||
await refreshData();
|
await refreshData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshOverlapOnly() {
|
||||||
|
const userId = userDialog.value.id;
|
||||||
|
if (!userId || isSelf.value || !hasAnyData.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = ++activeOverlapRequestId;
|
||||||
|
isOverlapLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rangeDays = parseInt(selectedPeriod.value, 10) || 30;
|
||||||
|
const overlapView = await activityStore.loadOverlapView({
|
||||||
|
currentUserId: currentUser.value.id,
|
||||||
|
targetUserId: userId,
|
||||||
|
rangeDays,
|
||||||
|
dayLabels: dayLabels.value,
|
||||||
|
forceRefresh: false,
|
||||||
|
excludeHours: {
|
||||||
|
enabled: excludeHoursEnabled.value,
|
||||||
|
startHour: parseInt(excludeStartHour.value, 10),
|
||||||
|
endHour: parseInt(excludeEndHour.value, 10)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (requestId !== activeOverlapRequestId || userDialog.value.id !== userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
overlapHeatmapView.value = {
|
||||||
|
rawBuckets: overlapView.rawBuckets,
|
||||||
|
normalizedBuckets: overlapView.normalizedBuckets
|
||||||
|
};
|
||||||
|
hasOverlapData.value = overlapView.hasOverlapData;
|
||||||
|
overlapPercent.value = overlapView.overlapPercent;
|
||||||
|
bestOverlapTime.value = overlapView.bestOverlapTime;
|
||||||
|
} finally {
|
||||||
|
if (requestId === activeOverlapRequestId) {
|
||||||
|
isOverlapLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function onExcludeToggle(value) {
|
async function onExcludeToggle(value) {
|
||||||
excludeHoursEnabled.value = value;
|
excludeHoursEnabled.value = value;
|
||||||
await configRepository.setBool('VRCX_overlapExcludeEnabled', value);
|
await configRepository.setBool('VRCX_overlapExcludeEnabled', value);
|
||||||
if (!isSelf.value && hasAnyData.value) {
|
await refreshOverlapOnly();
|
||||||
await refreshData({ silent: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onExcludeRangeChange() {
|
async function onExcludeRangeChange() {
|
||||||
await configRepository.setString('VRCX_overlapExcludeStart', excludeStartHour.value);
|
await configRepository.setString('VRCX_overlapExcludeStart', excludeStartHour.value);
|
||||||
await configRepository.setString('VRCX_overlapExcludeEnd', excludeEndHour.value);
|
await configRepository.setString('VRCX_overlapExcludeEnd', excludeEndHour.value);
|
||||||
if (!isSelf.value && hasAnyData.value) {
|
await refreshOverlapOnly();
|
||||||
await refreshData({ silent: true });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchMissingTopWorldThumbnails(worlds) {
|
async function fetchMissingTopWorldThumbnails(worlds) {
|
||||||
@@ -510,19 +548,34 @@
|
|||||||
ensureActivityChart();
|
ensureActivityChart();
|
||||||
if (!activityChart) return;
|
if (!activityChart) return;
|
||||||
|
|
||||||
activityChart.setOption(buildHeatmapOption({
|
activityChart.setOption(
|
||||||
data: toHeatmapSeriesData(mainHeatmapView.value.normalizedBuckets, weekStartsOn.value),
|
buildHeatmapOption({
|
||||||
rawBuckets: mainHeatmapView.value.rawBuckets,
|
data: toHeatmapSeriesData(mainHeatmapView.value.normalizedBuckets, weekStartsOn.value),
|
||||||
dayLabels: displayDayLabels.value,
|
rawBuckets: mainHeatmapView.value.rawBuckets,
|
||||||
hourLabels,
|
dayLabels: displayDayLabels.value,
|
||||||
weekStartsOn: weekStartsOn.value,
|
hourLabels,
|
||||||
isDarkMode: isDarkMode.value,
|
weekStartsOn: weekStartsOn.value,
|
||||||
emptyColor: isDarkMode.value ? 'hsl(220, 15%, 12%)' : 'hsl(210, 30%, 95%)',
|
isDarkMode: isDarkMode.value,
|
||||||
scaleColors: isDarkMode.value
|
emptyColor: isDarkMode.value ? 'hsl(220, 15%, 12%)' : 'hsl(210, 30%, 95%)',
|
||||||
? ['hsl(160, 40%, 24%)', 'hsl(150, 48%, 32%)', 'hsl(142, 55%, 38%)', 'hsl(142, 65%, 46%)', 'hsl(142, 80%, 55%)']
|
scaleColors: isDarkMode.value
|
||||||
: ['hsl(160, 40%, 82%)', 'hsl(155, 45%, 68%)', 'hsl(142, 55%, 55%)', 'hsl(142, 65%, 40%)', 'hsl(142, 76%, 30%)'],
|
? [
|
||||||
unitLabel: t('dialog.user.activity.minutes_online')
|
'hsl(160, 40%, 24%)',
|
||||||
}), { notMerge: true });
|
'hsl(150, 48%, 32%)',
|
||||||
|
'hsl(142, 55%, 38%)',
|
||||||
|
'hsl(142, 65%, 46%)',
|
||||||
|
'hsl(142, 80%, 55%)'
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'hsl(160, 40%, 82%)',
|
||||||
|
'hsl(155, 45%, 68%)',
|
||||||
|
'hsl(142, 55%, 55%)',
|
||||||
|
'hsl(142, 65%, 40%)',
|
||||||
|
'hsl(142, 76%, 30%)'
|
||||||
|
],
|
||||||
|
unitLabel: t('dialog.user.activity.minutes_online')
|
||||||
|
}),
|
||||||
|
{ replaceMerge: ['series'] }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderOverlapChart() {
|
function renderOverlapChart() {
|
||||||
@@ -533,19 +586,34 @@
|
|||||||
ensureOverlapChart();
|
ensureOverlapChart();
|
||||||
if (!overlapChart) return;
|
if (!overlapChart) return;
|
||||||
|
|
||||||
overlapChart.setOption(buildHeatmapOption({
|
overlapChart.setOption(
|
||||||
data: toHeatmapSeriesData(overlapHeatmapView.value.normalizedBuckets, weekStartsOn.value),
|
buildHeatmapOption({
|
||||||
rawBuckets: overlapHeatmapView.value.rawBuckets,
|
data: toHeatmapSeriesData(overlapHeatmapView.value.normalizedBuckets, weekStartsOn.value),
|
||||||
dayLabels: displayDayLabels.value,
|
rawBuckets: overlapHeatmapView.value.rawBuckets,
|
||||||
hourLabels,
|
dayLabels: displayDayLabels.value,
|
||||||
weekStartsOn: weekStartsOn.value,
|
hourLabels,
|
||||||
isDarkMode: isDarkMode.value,
|
weekStartsOn: weekStartsOn.value,
|
||||||
emptyColor: isDarkMode.value ? 'hsl(220, 15%, 12%)' : 'hsl(210, 30%, 95%)',
|
isDarkMode: isDarkMode.value,
|
||||||
scaleColors: isDarkMode.value
|
emptyColor: isDarkMode.value ? 'hsl(220, 15%, 12%)' : 'hsl(210, 30%, 95%)',
|
||||||
? ['hsl(260, 30%, 26%)', 'hsl(260, 42%, 36%)', 'hsl(260, 50%, 45%)', 'hsl(260, 60%, 54%)', 'hsl(260, 70%, 62%)']
|
scaleColors: isDarkMode.value
|
||||||
: ['hsl(260, 35%, 85%)', 'hsl(260, 42%, 70%)', 'hsl(260, 48%, 58%)', 'hsl(260, 55%, 48%)', 'hsl(260, 60%, 38%)'],
|
? [
|
||||||
unitLabel: t('dialog.user.activity.overlap.minutes_overlap')
|
'hsl(260, 30%, 26%)',
|
||||||
}), { notMerge: true });
|
'hsl(260, 42%, 36%)',
|
||||||
|
'hsl(260, 50%, 45%)',
|
||||||
|
'hsl(260, 60%, 54%)',
|
||||||
|
'hsl(260, 70%, 62%)'
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'hsl(260, 35%, 85%)',
|
||||||
|
'hsl(260, 42%, 70%)',
|
||||||
|
'hsl(260, 48%, 58%)',
|
||||||
|
'hsl(260, 55%, 48%)',
|
||||||
|
'hsl(260, 60%, 38%)'
|
||||||
|
],
|
||||||
|
unitLabel: t('dialog.user.activity.overlap.minutes_overlap')
|
||||||
|
}),
|
||||||
|
{ replaceMerge: ['series'] }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function rebuildCharts() {
|
function rebuildCharts() {
|
||||||
@@ -557,7 +625,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onChartRightClick() {
|
function onChartRightClick() {
|
||||||
toast(t('dialog.user.activity.easter_egg'), { position: 'bottom-center', icon: h(Tractor) });
|
toast(t('dialog.user.activity.chart_hint'), { position: 'bottom-center', icon: h(Tractor) });
|
||||||
clearTimeout(easterEggTimer);
|
clearTimeout(easterEggTimer);
|
||||||
easterEggTimer = setTimeout(() => {
|
easterEggTimer = setTimeout(() => {
|
||||||
easterEggTimer = null;
|
easterEggTimer = null;
|
||||||
@@ -566,7 +634,7 @@
|
|||||||
|
|
||||||
function onOverlapChartRightClick() {
|
function onOverlapChartRightClick() {
|
||||||
if (easterEggTimer) {
|
if (easterEggTimer) {
|
||||||
toast(t('dialog.user.activity.easter_egg_reply'), { position: 'bottom-center', icon: h(Sprout) });
|
toast(t('dialog.user.activity.chart_hint_reply'), { position: 'bottom-center', icon: h(Sprout) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,40 +645,60 @@
|
|||||||
void loadForVisibleTab();
|
void loadForVisibleTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => userDialog.value.id, () => {
|
watch(
|
||||||
resetActivityState();
|
() => userDialog.value.id,
|
||||||
rebuildCharts();
|
() => {
|
||||||
if (userDialog.value.visible && userDialog.value.activeTab === 'Activity') {
|
resetActivityState();
|
||||||
void nextTick(() => loadForVisibleTab());
|
rebuildCharts();
|
||||||
|
if (userDialog.value.visible && userDialog.value.activeTab === 'Activity') {
|
||||||
|
void nextTick(() => loadForVisibleTab());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
watch([locale, isDarkMode, weekStartsOn], rebuildCharts);
|
watch([locale, isDarkMode, weekStartsOn], rebuildCharts);
|
||||||
watch(() => selectedPeriod.value, () => {
|
watch(
|
||||||
if (userDialog.value.visible && userDialog.value.activeTab === 'Activity') {
|
() => selectedPeriod.value,
|
||||||
void onPeriodChange();
|
() => {
|
||||||
|
if (userDialog.value.visible && userDialog.value.activeTab === 'Activity') {
|
||||||
|
void onPeriodChange();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
watch(() => mainHeatmapView.value, () => {
|
watch(
|
||||||
nextTick(() => renderActivityChart());
|
() => mainHeatmapView.value,
|
||||||
}, { deep: true });
|
() => {
|
||||||
watch(() => overlapHeatmapView.value, () => {
|
nextTick(() => renderActivityChart());
|
||||||
nextTick(() => renderOverlapChart());
|
},
|
||||||
}, { deep: true });
|
{ deep: true }
|
||||||
watch(() => userDialog.value.visible, (visible) => {
|
);
|
||||||
if (!visible) return;
|
watch(
|
||||||
nextTick(() => {
|
() => overlapHeatmapView.value,
|
||||||
activityChart?.resize();
|
() => {
|
||||||
overlapChart?.resize();
|
nextTick(() => renderOverlapChart());
|
||||||
});
|
},
|
||||||
if (userDialog.value.activeTab === 'Activity') {
|
{ deep: true }
|
||||||
void loadForVisibleTab();
|
);
|
||||||
|
watch(
|
||||||
|
() => userDialog.value.visible,
|
||||||
|
(visible) => {
|
||||||
|
if (!visible) return;
|
||||||
|
nextTick(() => {
|
||||||
|
activityChart?.resize();
|
||||||
|
overlapChart?.resize();
|
||||||
|
});
|
||||||
|
if (userDialog.value.activeTab === 'Activity') {
|
||||||
|
void loadForVisibleTab();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
watch(() => userDialog.value.activeTab, (activeTab) => {
|
watch(
|
||||||
if (activeTab === 'Activity' && userDialog.value.visible) {
|
() => userDialog.value.activeTab,
|
||||||
void loadForVisibleTab();
|
(activeTab) => {
|
||||||
|
if (activeTab === 'Activity' && userDialog.value.visible) {
|
||||||
|
void loadForVisibleTab();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await initializeSettings();
|
await initializeSettings();
|
||||||
|
|||||||
@@ -1435,7 +1435,6 @@
|
|||||||
"most_active_day": "Most active day:",
|
"most_active_day": "Most active day:",
|
||||||
"most_active_time": "Peak hours:",
|
"most_active_time": "Peak hours:",
|
||||||
"period": "Period:",
|
"period": "Period:",
|
||||||
"period_180": "Last 180 Days",
|
|
||||||
"period_90": "Last 90 Days",
|
"period_90": "Last 90 Days",
|
||||||
"period_30": "Last 30 Days",
|
"period_30": "Last 30 Days",
|
||||||
"period_7": "Last 7 Days",
|
"period_7": "Last 7 Days",
|
||||||
@@ -1450,8 +1449,8 @@
|
|||||||
"sat": "Sat",
|
"sat": "Sat",
|
||||||
"sun": "Sun"
|
"sun": "Sun"
|
||||||
},
|
},
|
||||||
"easter_egg": "Did you farm your green squares today?",
|
"chart_hint": "Did you farm your green squares today?",
|
||||||
"easter_egg_reply": "You can't farm this.",
|
"chart_hint_reply": "You can't farm this.",
|
||||||
"overlap": {
|
"overlap": {
|
||||||
"header": "Online Overlap",
|
"header": "Online Overlap",
|
||||||
"peak_overlap": "Peak overlap:",
|
"peak_overlap": "Peak overlap:",
|
||||||
|
|||||||
@@ -134,8 +134,21 @@ export const useActivityStore = defineStore('Activity', () => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadActivity(userId, { isSelf = false, rangeDays = 30, normalizeConfig, dayLabels, forceRefresh = false }) {
|
async function loadActivity(
|
||||||
const snapshot = await ensureSnapshot(userId, { isSelf, rangeDays, forceRefresh });
|
userId,
|
||||||
|
{
|
||||||
|
isSelf = false,
|
||||||
|
rangeDays = 30,
|
||||||
|
normalizeConfig,
|
||||||
|
dayLabels,
|
||||||
|
forceRefresh = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const snapshot = await ensureSnapshot(userId, {
|
||||||
|
isSelf,
|
||||||
|
rangeDays,
|
||||||
|
forceRefresh
|
||||||
|
});
|
||||||
const cacheKey = String(rangeDays);
|
const cacheKey = String(rangeDays);
|
||||||
const currentCursor = snapshot.sync.sourceLastCreatedAt || '';
|
const currentCursor = snapshot.sync.sourceLastCreatedAt || '';
|
||||||
|
|
||||||
@@ -175,38 +188,55 @@ export const useActivityStore = defineStore('Activity', () => {
|
|||||||
builtAt: new Date().toISOString()
|
builtAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
snapshot.activityViews.set(cacheKey, view);
|
snapshot.activityViews.set(cacheKey, view);
|
||||||
deferWrite(() => database.upsertActivityBucketCacheV2({
|
deferWrite(() =>
|
||||||
ownerUserId: userId,
|
database.upsertActivityBucketCacheV2({
|
||||||
rangeDays,
|
ownerUserId: userId,
|
||||||
viewKind: database.ACTIVITY_VIEW_KIND.ACTIVITY,
|
rangeDays,
|
||||||
builtFromCursor: currentCursor,
|
viewKind: database.ACTIVITY_VIEW_KIND.ACTIVITY,
|
||||||
rawBuckets: view.rawBuckets,
|
builtFromCursor: currentCursor,
|
||||||
normalizedBuckets: view.normalizedBuckets,
|
rawBuckets: view.rawBuckets,
|
||||||
summary: {
|
normalizedBuckets: view.normalizedBuckets,
|
||||||
peakDay: view.peakDay,
|
summary: {
|
||||||
peakTime: view.peakTime,
|
peakDay: view.peakDay,
|
||||||
filteredEventCount: view.filteredEventCount
|
peakTime: view.peakTime,
|
||||||
},
|
filteredEventCount: view.filteredEventCount
|
||||||
builtAt: view.builtAt
|
},
|
||||||
}));
|
builtAt: view.builtAt
|
||||||
|
})
|
||||||
|
);
|
||||||
return buildActivityResponse(snapshot, view);
|
return buildActivityResponse(snapshot, view);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadOverlap(currentUserId, targetUserId, {
|
async function loadOverlap(
|
||||||
rangeDays = 30,
|
currentUserId,
|
||||||
dayLabels,
|
targetUserId,
|
||||||
normalizeConfig,
|
{
|
||||||
excludeHours,
|
rangeDays = 30,
|
||||||
forceRefresh = false
|
dayLabels,
|
||||||
}) {
|
normalizeConfig,
|
||||||
|
excludeHours,
|
||||||
|
forceRefresh = false
|
||||||
|
}
|
||||||
|
) {
|
||||||
const [selfSnapshot, targetSnapshot] = await Promise.all([
|
const [selfSnapshot, targetSnapshot] = await Promise.all([
|
||||||
ensureSnapshot(currentUserId, { isSelf: true, rangeDays, forceRefresh }),
|
ensureSnapshot(currentUserId, {
|
||||||
ensureSnapshot(targetUserId, { isSelf: false, rangeDays, forceRefresh })
|
isSelf: true,
|
||||||
|
rangeDays,
|
||||||
|
forceRefresh
|
||||||
|
}),
|
||||||
|
ensureSnapshot(targetUserId, {
|
||||||
|
isSelf: false,
|
||||||
|
rangeDays,
|
||||||
|
forceRefresh
|
||||||
|
})
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const excludeKey = overlapExcludeKey(excludeHours);
|
const excludeKey = overlapExcludeKey(excludeHours);
|
||||||
const cacheKey = `${targetUserId}:${rangeDays}:${excludeKey}`;
|
const cacheKey = `${targetUserId}:${rangeDays}:${excludeKey}`;
|
||||||
const cursor = pairCursor(selfSnapshot.sync.sourceLastCreatedAt, targetSnapshot.sync.sourceLastCreatedAt);
|
const cursor = pairCursor(
|
||||||
|
selfSnapshot.sync.sourceLastCreatedAt,
|
||||||
|
targetSnapshot.sync.sourceLastCreatedAt
|
||||||
|
);
|
||||||
|
|
||||||
let view = targetSnapshot.overlapViews.get(cacheKey);
|
let view = targetSnapshot.overlapViews.get(cacheKey);
|
||||||
if (view?.builtFromCursor === cursor) {
|
if (view?.builtFromCursor === cursor) {
|
||||||
@@ -246,26 +276,35 @@ export const useActivityStore = defineStore('Activity', () => {
|
|||||||
builtAt: new Date().toISOString()
|
builtAt: new Date().toISOString()
|
||||||
};
|
};
|
||||||
targetSnapshot.overlapViews.set(cacheKey, view);
|
targetSnapshot.overlapViews.set(cacheKey, view);
|
||||||
deferWrite(() => database.upsertActivityBucketCacheV2({
|
deferWrite(() =>
|
||||||
ownerUserId: currentUserId,
|
database.upsertActivityBucketCacheV2({
|
||||||
targetUserId,
|
ownerUserId: currentUserId,
|
||||||
rangeDays,
|
targetUserId,
|
||||||
viewKind: database.ACTIVITY_VIEW_KIND.OVERLAP,
|
rangeDays,
|
||||||
excludeKey,
|
viewKind: database.ACTIVITY_VIEW_KIND.OVERLAP,
|
||||||
builtFromCursor: cursor,
|
excludeKey,
|
||||||
rawBuckets: view.rawBuckets,
|
builtFromCursor: cursor,
|
||||||
normalizedBuckets: view.normalizedBuckets,
|
rawBuckets: view.rawBuckets,
|
||||||
summary: {
|
normalizedBuckets: view.normalizedBuckets,
|
||||||
overlapPercent: view.overlapPercent,
|
summary: {
|
||||||
bestOverlapTime: view.bestOverlapTime
|
overlapPercent: view.overlapPercent,
|
||||||
},
|
bestOverlapTime: view.bestOverlapTime
|
||||||
builtAt: view.builtAt
|
},
|
||||||
}));
|
builtAt: view.builtAt
|
||||||
|
})
|
||||||
|
);
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTopWorlds(userId, { rangeDays = 30, limit = 5, isSelf = true, forceRefresh = false }) {
|
async function loadTopWorlds(
|
||||||
const snapshot = await ensureSnapshot(userId, { isSelf, rangeDays, forceRefresh });
|
userId,
|
||||||
|
{ rangeDays = 30, limit = 5, isSelf = true, forceRefresh = false }
|
||||||
|
) {
|
||||||
|
const snapshot = await ensureSnapshot(userId, {
|
||||||
|
isSelf,
|
||||||
|
rangeDays,
|
||||||
|
forceRefresh
|
||||||
|
});
|
||||||
const cacheKey = `${rangeDays}:${limit}`;
|
const cacheKey = `${rangeDays}:${limit}`;
|
||||||
const currentCursor = snapshot.sync.sourceLastCreatedAt || '';
|
const currentCursor = snapshot.sync.sourceLastCreatedAt || '';
|
||||||
|
|
||||||
@@ -275,7 +314,10 @@ export const useActivityStore = defineStore('Activity', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!forceRefresh) {
|
if (!forceRefresh) {
|
||||||
const persisted = await database.getActivityTopWorldsCacheV2(userId, rangeDays);
|
const persisted = await database.getActivityTopWorldsCacheV2(
|
||||||
|
userId,
|
||||||
|
rangeDays
|
||||||
|
);
|
||||||
if (persisted?.builtFromCursor === currentCursor) {
|
if (persisted?.builtFromCursor === currentCursor) {
|
||||||
snapshot.topWorldsViews.set(cacheKey, persisted);
|
snapshot.topWorldsViews.set(cacheKey, persisted);
|
||||||
return persisted.worlds;
|
return persisted.worlds;
|
||||||
@@ -292,14 +334,16 @@ export const useActivityStore = defineStore('Activity', () => {
|
|||||||
};
|
};
|
||||||
snapshot.topWorldsViews.set(cacheKey, entry);
|
snapshot.topWorldsViews.set(cacheKey, entry);
|
||||||
deferWrite(() => database.replaceActivityTopWorldsCacheV2(entry));
|
deferWrite(() => database.replaceActivityTopWorldsCacheV2(entry));
|
||||||
deferWrite(() => database.upsertActivityRangeCacheV2({
|
deferWrite(() =>
|
||||||
userId,
|
database.upsertActivityRangeCacheV2({
|
||||||
rangeDays,
|
userId,
|
||||||
cacheKind: database.ACTIVITY_RANGE_CACHE_KIND.TOP_WORLDS,
|
rangeDays,
|
||||||
isComplete: true,
|
cacheKind: database.ACTIVITY_RANGE_CACHE_KIND.TOP_WORLDS,
|
||||||
builtFromCursor: currentCursor,
|
isComplete: true,
|
||||||
builtAt: entry.builtAt
|
builtFromCursor: currentCursor,
|
||||||
}));
|
builtAt: entry.builtAt
|
||||||
|
})
|
||||||
|
);
|
||||||
return worlds;
|
return worlds;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,7 +351,13 @@ export const useActivityStore = defineStore('Activity', () => {
|
|||||||
return loadActivity(userId, { ...options, forceRefresh: true });
|
return loadActivity(userId, { ...options, forceRefresh: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadActivityView({ userId, isSelf = false, rangeDays = 30, dayLabels, forceRefresh = false }) {
|
async function loadActivityView({
|
||||||
|
userId,
|
||||||
|
isSelf = false,
|
||||||
|
rangeDays = 30,
|
||||||
|
dayLabels,
|
||||||
|
forceRefresh = false
|
||||||
|
}) {
|
||||||
const response = await loadActivity(userId, {
|
const response = await loadActivity(userId, {
|
||||||
isSelf,
|
isSelf,
|
||||||
rangeDays,
|
rangeDays,
|
||||||
@@ -325,7 +375,14 @@ export const useActivityStore = defineStore('Activity', () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadOverlapView({ currentUserId, targetUserId, rangeDays = 30, dayLabels, excludeHours, forceRefresh = false }) {
|
async function loadOverlapView({
|
||||||
|
currentUserId,
|
||||||
|
targetUserId,
|
||||||
|
rangeDays = 30,
|
||||||
|
dayLabels,
|
||||||
|
excludeHours,
|
||||||
|
forceRefresh = false
|
||||||
|
}) {
|
||||||
const response = await loadOverlap(currentUserId, targetUserId, {
|
const response = await loadOverlap(currentUserId, targetUserId, {
|
||||||
rangeDays,
|
rangeDays,
|
||||||
dayLabels,
|
dayLabels,
|
||||||
@@ -342,7 +399,12 @@ export const useActivityStore = defineStore('Activity', () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTopWorldsView({ userId, rangeDays = 30, limit = 5, forceRefresh = false }) {
|
async function loadTopWorldsView({
|
||||||
|
userId,
|
||||||
|
rangeDays = 30,
|
||||||
|
limit = 5,
|
||||||
|
forceRefresh = false
|
||||||
|
}) {
|
||||||
return loadTopWorlds(userId, {
|
return loadTopWorlds(userId, {
|
||||||
rangeDays,
|
rangeDays,
|
||||||
limit,
|
limit,
|
||||||
@@ -399,7 +461,10 @@ async function hydrateSnapshot(userId, isSelf) {
|
|||||||
snapshot.sync = {
|
snapshot.sync = {
|
||||||
...snapshot.sync,
|
...snapshot.sync,
|
||||||
...syncState,
|
...syncState,
|
||||||
isSelf: typeof syncState.isSelf === 'boolean' ? syncState.isSelf : snapshot.isSelf
|
isSelf:
|
||||||
|
typeof syncState.isSelf === 'boolean'
|
||||||
|
? syncState.isSelf
|
||||||
|
: snapshot.isSelf
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (sessions.length > 0) {
|
if (sessions.length > 0) {
|
||||||
@@ -408,7 +473,10 @@ async function hydrateSnapshot(userId, isSelf) {
|
|||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureSnapshot(userId, { isSelf, rangeDays, forceRefresh = false }) {
|
async function ensureSnapshot(
|
||||||
|
userId,
|
||||||
|
{ isSelf, rangeDays, forceRefresh = false }
|
||||||
|
) {
|
||||||
const jobKey = `${userId}:${isSelf}:${rangeDays}:${forceRefresh ? 'force' : 'normal'}`;
|
const jobKey = `${userId}:${isSelf}:${rangeDays}:${forceRefresh ? 'force' : 'normal'}`;
|
||||||
const existingJob = inFlightJobs.get(jobKey);
|
const existingJob = inFlightJobs.get(jobKey);
|
||||||
if (existingJob) {
|
if (existingJob) {
|
||||||
@@ -440,7 +508,10 @@ async function fullRefresh(snapshot, rangeDays) {
|
|||||||
isSelf: snapshot.isSelf,
|
isSelf: snapshot.isSelf,
|
||||||
fromDays: rangeDays
|
fromDays: rangeDays
|
||||||
});
|
});
|
||||||
const sourceLastCreatedAt = sourceItems.length > 0 ? sourceItems[sourceItems.length - 1].created_at : '';
|
const sourceLastCreatedAt =
|
||||||
|
sourceItems.length > 0
|
||||||
|
? sourceItems[sourceItems.length - 1].created_at
|
||||||
|
: '';
|
||||||
const result = await workerCall('computeSessionsSnapshot', {
|
const result = await workerCall('computeSessionsSnapshot', {
|
||||||
sourceType: snapshot.isSelf ? 'self_gamelog' : 'friend_presence',
|
sourceType: snapshot.isSelf ? 'self_gamelog' : 'friend_presence',
|
||||||
rows: snapshot.isSelf ? sourceItems : undefined,
|
rows: snapshot.isSelf ? sourceItems : undefined,
|
||||||
@@ -462,16 +533,20 @@ async function fullRefresh(snapshot, rangeDays) {
|
|||||||
};
|
};
|
||||||
clearDerivedViews(snapshot);
|
clearDerivedViews(snapshot);
|
||||||
|
|
||||||
deferWrite(() => database.replaceActivitySessionsV2(snapshot.userId, snapshot.sessions));
|
deferWrite(() =>
|
||||||
|
database.replaceActivitySessionsV2(snapshot.userId, snapshot.sessions)
|
||||||
|
);
|
||||||
deferWrite(() => database.upsertActivitySyncStateV2(snapshot.sync));
|
deferWrite(() => database.upsertActivitySyncStateV2(snapshot.sync));
|
||||||
deferWrite(() => database.upsertActivityRangeCacheV2({
|
deferWrite(() =>
|
||||||
userId: snapshot.userId,
|
database.upsertActivityRangeCacheV2({
|
||||||
rangeDays,
|
userId: snapshot.userId,
|
||||||
cacheKind: database.ACTIVITY_RANGE_CACHE_KIND.SESSIONS,
|
rangeDays,
|
||||||
isComplete: true,
|
cacheKind: database.ACTIVITY_RANGE_CACHE_KIND.SESSIONS,
|
||||||
builtFromCursor: snapshot.sync.sourceLastCreatedAt,
|
isComplete: true,
|
||||||
builtAt: snapshot.sync.updatedAt
|
builtFromCursor: snapshot.sync.sourceLastCreatedAt,
|
||||||
}));
|
builtAt: snapshot.sync.updatedAt
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function incrementalRefresh(snapshot) {
|
async function incrementalRefresh(snapshot) {
|
||||||
@@ -496,15 +571,18 @@ async function incrementalRefresh(snapshot) {
|
|||||||
sourceType: snapshot.isSelf ? 'self_gamelog' : 'friend_presence',
|
sourceType: snapshot.isSelf ? 'self_gamelog' : 'friend_presence',
|
||||||
rows: snapshot.isSelf ? sourceItems : undefined,
|
rows: snapshot.isSelf ? sourceItems : undefined,
|
||||||
events: snapshot.isSelf ? undefined : sourceItems,
|
events: snapshot.isSelf ? undefined : sourceItems,
|
||||||
initialStart: snapshot.isSelf ? null : snapshot.sync.pendingSessionStartAt,
|
initialStart: snapshot.isSelf
|
||||||
|
? null
|
||||||
|
: snapshot.sync.pendingSessionStartAt,
|
||||||
nowMs: Date.now(),
|
nowMs: Date.now(),
|
||||||
mayHaveOpenTail: snapshot.isSelf,
|
mayHaveOpenTail: snapshot.isSelf,
|
||||||
sourceRevision: sourceLastCreatedAt
|
sourceRevision: sourceLastCreatedAt
|
||||||
});
|
});
|
||||||
|
|
||||||
const replaceFromStartAt = snapshot.sessions.length > 0
|
const replaceFromStartAt =
|
||||||
? snapshot.sessions[Math.max(snapshot.sessions.length - 1, 0)].start
|
snapshot.sessions.length > 0
|
||||||
: null;
|
? snapshot.sessions[Math.max(snapshot.sessions.length - 1, 0)].start
|
||||||
|
: null;
|
||||||
const merged = mergeSessions(snapshot.sessions, result.sessions);
|
const merged = mergeSessions(snapshot.sessions, result.sessions);
|
||||||
snapshot.sessions = merged;
|
snapshot.sessions = merged;
|
||||||
snapshot.sync = {
|
snapshot.sync = {
|
||||||
@@ -515,14 +593,17 @@ async function incrementalRefresh(snapshot) {
|
|||||||
};
|
};
|
||||||
clearDerivedViews(snapshot);
|
clearDerivedViews(snapshot);
|
||||||
|
|
||||||
const tailSessions = replaceFromStartAt === null
|
const tailSessions =
|
||||||
? merged
|
replaceFromStartAt === null
|
||||||
: merged.filter((session) => session.start >= replaceFromStartAt);
|
? merged
|
||||||
deferWrite(() => database.appendActivitySessionsV2({
|
: merged.filter((session) => session.start >= replaceFromStartAt);
|
||||||
userId: snapshot.userId,
|
deferWrite(() =>
|
||||||
sessions: tailSessions,
|
database.appendActivitySessionsV2({
|
||||||
replaceFromStartAt
|
userId: snapshot.userId,
|
||||||
}));
|
sessions: tailSessions,
|
||||||
|
replaceFromStartAt
|
||||||
|
})
|
||||||
|
);
|
||||||
deferWrite(() => database.upsertActivitySyncStateV2(snapshot.sync));
|
deferWrite(() => database.upsertActivitySyncStateV2(snapshot.sync));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,58 +631,120 @@ async function expandRange(snapshot, rangeDays) {
|
|||||||
|
|
||||||
if (result.sessions.length > 0) {
|
if (result.sessions.length > 0) {
|
||||||
snapshot.sessions = mergeSessions(result.sessions, snapshot.sessions);
|
snapshot.sessions = mergeSessions(result.sessions, snapshot.sessions);
|
||||||
deferWrite(() => database.replaceActivitySessionsV2(snapshot.userId, snapshot.sessions));
|
deferWrite(() =>
|
||||||
|
database.replaceActivitySessionsV2(
|
||||||
|
snapshot.userId,
|
||||||
|
snapshot.sessions
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
snapshot.sync.cachedRangeDays = rangeDays;
|
snapshot.sync.cachedRangeDays = rangeDays;
|
||||||
snapshot.sync.updatedAt = new Date().toISOString();
|
snapshot.sync.updatedAt = new Date().toISOString();
|
||||||
clearDerivedViews(snapshot);
|
clearDerivedViews(snapshot);
|
||||||
|
|
||||||
deferWrite(() => database.upsertActivitySyncStateV2(snapshot.sync));
|
deferWrite(() => database.upsertActivitySyncStateV2(snapshot.sync));
|
||||||
deferWrite(() => database.upsertActivityRangeCacheV2({
|
deferWrite(() =>
|
||||||
userId: snapshot.userId,
|
database.upsertActivityRangeCacheV2({
|
||||||
rangeDays,
|
userId: snapshot.userId,
|
||||||
cacheKind: database.ACTIVITY_RANGE_CACHE_KIND.SESSIONS,
|
rangeDays,
|
||||||
isComplete: true,
|
cacheKind: database.ACTIVITY_RANGE_CACHE_KIND.SESSIONS,
|
||||||
builtFromCursor: snapshot.sync.sourceLastCreatedAt,
|
isComplete: true,
|
||||||
builtAt: snapshot.sync.updatedAt
|
builtFromCursor: snapshot.sync.sourceLastCreatedAt,
|
||||||
}));
|
builtAt: snapshot.sync.updatedAt
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickActivityNormalizeConfig(isSelf, rangeDays) {
|
function pickActivityNormalizeConfig(isSelf, rangeDays) {
|
||||||
const role = isSelf ? 'self' : 'friend';
|
const role = isSelf ? 'self' : 'friend';
|
||||||
return {
|
return (
|
||||||
self: {
|
{
|
||||||
7: { floorPercentile: 10, capPercentile: 80, rankWeight: 0.15, targetCoverage: 0.12, targetVolume: 40 },
|
self: {
|
||||||
30: { floorPercentile: 15, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.25, targetVolume: 60 },
|
7: {
|
||||||
90: { floorPercentile: 15, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.30, targetVolume: 50 },
|
floorPercentile: 10,
|
||||||
180: { floorPercentile: 20, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.35, targetVolume: 40 }
|
capPercentile: 80,
|
||||||
},
|
rankWeight: 0.15,
|
||||||
friend: {
|
targetCoverage: 0.12,
|
||||||
7: { floorPercentile: 10, capPercentile: 80, rankWeight: 0.15, targetCoverage: 0.12, targetVolume: 40 },
|
targetVolume: 40
|
||||||
30: { floorPercentile: 15, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.25, targetVolume: 60 },
|
},
|
||||||
90: { floorPercentile: 15, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.30, targetVolume: 50 },
|
30: {
|
||||||
180: { floorPercentile: 20, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.35, targetVolume: 40 }
|
floorPercentile: 15,
|
||||||
|
capPercentile: 85,
|
||||||
|
rankWeight: 0.2,
|
||||||
|
targetCoverage: 0.25,
|
||||||
|
targetVolume: 60
|
||||||
|
},
|
||||||
|
90: {
|
||||||
|
floorPercentile: 15,
|
||||||
|
capPercentile: 85,
|
||||||
|
rankWeight: 0.2,
|
||||||
|
targetCoverage: 0.3,
|
||||||
|
targetVolume: 50
|
||||||
|
}
|
||||||
|
},
|
||||||
|
friend: {
|
||||||
|
7: {
|
||||||
|
floorPercentile: 10,
|
||||||
|
capPercentile: 80,
|
||||||
|
rankWeight: 0.15,
|
||||||
|
targetCoverage: 0.12,
|
||||||
|
targetVolume: 40
|
||||||
|
},
|
||||||
|
30: {
|
||||||
|
floorPercentile: 15,
|
||||||
|
capPercentile: 85,
|
||||||
|
rankWeight: 0.2,
|
||||||
|
targetCoverage: 0.25,
|
||||||
|
targetVolume: 60
|
||||||
|
},
|
||||||
|
90: {
|
||||||
|
floorPercentile: 15,
|
||||||
|
capPercentile: 85,
|
||||||
|
rankWeight: 0.2,
|
||||||
|
targetCoverage: 0.3,
|
||||||
|
targetVolume: 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}[role][rangeDays] || {
|
||||||
|
floorPercentile: 15,
|
||||||
|
capPercentile: 85,
|
||||||
|
rankWeight: 0.2,
|
||||||
|
targetCoverage: 0.25,
|
||||||
|
targetVolume: 60
|
||||||
}
|
}
|
||||||
}[role][rangeDays] || {
|
);
|
||||||
floorPercentile: 15,
|
|
||||||
capPercentile: 85,
|
|
||||||
rankWeight: 0.20,
|
|
||||||
targetCoverage: 0.25,
|
|
||||||
targetVolume: 60
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickOverlapNormalizeConfig(rangeDays) {
|
function pickOverlapNormalizeConfig(rangeDays) {
|
||||||
return {
|
return (
|
||||||
7: { floorPercentile: 10, capPercentile: 80, rankWeight: 0.15, targetCoverage: 0.08, targetVolume: 15 },
|
{
|
||||||
30: { floorPercentile: 15, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.15, targetVolume: 25 },
|
7: {
|
||||||
90: { floorPercentile: 15, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.18, targetVolume: 20 },
|
floorPercentile: 10,
|
||||||
180: { floorPercentile: 20, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.20, targetVolume: 15 }
|
capPercentile: 80,
|
||||||
}[rangeDays] || {
|
rankWeight: 0.15,
|
||||||
floorPercentile: 15,
|
targetCoverage: 0.08,
|
||||||
capPercentile: 85,
|
targetVolume: 15
|
||||||
rankWeight: 0.20,
|
},
|
||||||
targetCoverage: 0.15,
|
30: {
|
||||||
targetVolume: 25
|
floorPercentile: 15,
|
||||||
};
|
capPercentile: 85,
|
||||||
|
rankWeight: 0.2,
|
||||||
|
targetCoverage: 0.15,
|
||||||
|
targetVolume: 25
|
||||||
|
},
|
||||||
|
90: {
|
||||||
|
floorPercentile: 15,
|
||||||
|
capPercentile: 85,
|
||||||
|
rankWeight: 0.2,
|
||||||
|
targetCoverage: 0.18,
|
||||||
|
targetVolume: 20
|
||||||
|
}
|
||||||
|
}[rangeDays] || {
|
||||||
|
floorPercentile: 15,
|
||||||
|
capPercentile: 85,
|
||||||
|
rankWeight: 0.2,
|
||||||
|
targetCoverage: 0.15,
|
||||||
|
targetVolume: 25
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user