refactor store

This commit is contained in:
pa
2026-03-06 22:42:43 +09:00
parent e665b3815d
commit 8ddedb2d2d
29 changed files with 3269 additions and 888 deletions

View File

@@ -35,7 +35,14 @@
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { getGroupName, getWorldName, parseLocation, resolveRegion, translateAccessType } from '../shared/utils';
import {
getGroupName,
getLocationText,
getWorldName,
parseLocation,
resolveRegion,
translateAccessType
} from '../shared/utils';
import {
useAppearanceSettingsStore,
useGroupStore,
@@ -229,41 +236,27 @@
*/
function setText(L) {
const accessTypeLabel = getAccessTypeLabel(L.accessTypeName);
const cachedRef = L.worldId ? cachedWorlds.get(L.worldId) : undefined;
const worldName = typeof cachedRef !== 'undefined' ? cachedRef.name : undefined;
if (L.isOffline) {
text.value = t('location.offline');
} else if (L.isPrivate) {
text.value = t('location.private');
} else if (L.isTraveling) {
text.value = t('location.traveling');
} else if (typeof props.hint === 'string' && props.hint !== '') {
if (L.instanceId) {
text.value = `${props.hint} · ${accessTypeLabel}`;
} else {
text.value = props.hint;
}
} else if (L.worldId) {
if (L.instanceId) {
text.value = `${L.worldId} · ${accessTypeLabel}`;
} else {
text.value = L.worldId;
}
const ref = cachedWorlds.get(L.worldId);
if (typeof ref === 'undefined') {
getWorldName(L.worldId).then((name) => {
if (!isDisposed && name && currentInstanceId() === L.tag) {
if (L.instanceId) {
text.value = `${name} · ${getAccessTypeLabel(L.accessTypeName)}`;
} else {
text.value = name;
}
}
});
} else if (L.instanceId) {
text.value = `${ref.name} · ${accessTypeLabel}`;
} else {
text.value = ref.name;
}
text.value = getLocationText(L, {
hint: props.hint,
worldName,
accessTypeLabel,
t
});
if (L.worldId && typeof cachedRef === 'undefined') {
getWorldName(L.worldId).then((name) => {
if (!isDisposed && name && currentInstanceId() === L.tag) {
text.value = getLocationText(L, {
hint: props.hint,
worldName: name,
accessTypeLabel: getAccessTypeLabel(L.accessTypeName),
t
});
}
});
}
}

View File

@@ -179,5 +179,8 @@ export function isEntryNotified(entry, notifiedMenus) {
targets.push(lastSegment);
}
}
if (!Array.isArray(notifiedMenus)) {
return false;
}
return targets.some((key) => notifiedMenus.includes(key));
}

View File

@@ -0,0 +1,93 @@
import { describe, expect, it, vi } from 'vitest';
import { evictMapCache } from '../cacheUtils';
describe('evictMapCache', () => {
it('does nothing when cache is under maxSize', () => {
const cache = new Map([
['a', 1],
['b', 2]
]);
const result = evictMapCache(cache, 5, () => false);
expect(result.deletedCount).toBe(0);
expect(cache.size).toBe(2);
});
it('evicts entries when cache exceeds maxSize', () => {
const cache = new Map();
for (let i = 0; i < 10; i++) {
cache.set(`key_${i}`, i);
}
const result = evictMapCache(cache, 5, () => false);
expect(result.deletedCount).toBe(5);
expect(cache.size).toBe(5);
});
it('retains entries matching isRetainedFn', () => {
const cache = new Map([
['keep_1', 'retained'],
['keep_2', 'retained'],
['evict_1', 'evictable'],
['evict_2', 'evictable'],
['evict_3', 'evictable']
]);
const result = evictMapCache(cache, 2, (_value, key) =>
key.startsWith('keep_')
);
// Should have evicted evictable entries but retained keep entries
expect(cache.has('keep_1')).toBe(true);
expect(cache.has('keep_2')).toBe(true);
expect(result.deletedCount).toBe(3);
});
it('uses custom sortFn for eviction order', () => {
const cache = new Map([
['old', { age: 1 }],
['new', { age: 100 }],
['medium', { age: 50 }]
]);
const result = evictMapCache(cache, 1, () => false, {
sortFn: (a, b) => a.value.age - b.value.age
});
// Should evict oldest first
expect(result.deletedCount).toBe(2);
expect(cache.has('new')).toBe(true);
expect(cache.has('old')).toBe(false);
expect(cache.has('medium')).toBe(false);
});
it('logs when logLabel is provided', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
const cache = new Map([
['a', 1],
['b', 2],
['c', 3]
]);
evictMapCache(cache, 1, () => false, { logLabel: 'Test cleanup' });
expect(spy).toHaveBeenCalledWith(
expect.stringContaining('Test cleanup')
);
spy.mockRestore();
});
it('does not evict retained entries even when all need eviction', () => {
const cache = new Map([
['a', 1],
['b', 2],
['c', 3]
]);
const result = evictMapCache(cache, 1, () => true);
// All entries are retained
expect(result.deletedCount).toBe(0);
expect(cache.size).toBe(3);
});
it('handles exact maxSize (no eviction needed)', () => {
const cache = new Map([
['a', 1],
['b', 2]
]);
const result = evictMapCache(cache, 2, () => false);
expect(result.deletedCount).toBe(0);
expect(cache.size).toBe(2);
});
});

View File

@@ -0,0 +1,241 @@
import { describe, expect, test, vi } from 'vitest';
import {
getPlatformLabel,
getStatusInfo,
getRpcWorldConfig,
isPopcornPalaceWorld
} from '../discordPresence';
import { ActivityType, StatusDisplayType } from '../../constants/discord';
const t = (key) => key;
describe('getPlatformLabel', () => {
test('returns VR label when game is running in VR', () => {
const result = getPlatformLabel('standalonewindows', true, false, t);
expect(result).toBe(' (view.settings.discord_presence.rpc.vr)');
});
test('returns desktop label when game is running in desktop mode', () => {
const result = getPlatformLabel('standalonewindows', true, true, t);
expect(result).toBe(' (view.settings.discord_presence.rpc.desktop)');
});
test('returns empty string for web platform', () => {
expect(getPlatformLabel('web', false, false, t)).toBe('');
});
test('returns (PC) for standalonewindows', () => {
expect(getPlatformLabel('standalonewindows', false, false, t)).toBe(
' (PC)'
);
});
test('returns (Android) for android', () => {
expect(getPlatformLabel('android', false, false, t)).toBe(' (Android)');
});
test('returns (iOS) for ios', () => {
expect(getPlatformLabel('ios', false, false, t)).toBe(' (iOS)');
});
test('returns platform name in parens for unknown platform', () => {
expect(getPlatformLabel('quest', false, false, t)).toBe(' (quest)');
});
test('returns empty string for empty/falsy platform when not game running', () => {
expect(getPlatformLabel('', false, false, t)).toBe('');
expect(getPlatformLabel(undefined, false, false, t)).toBe('');
});
});
describe('getStatusInfo', () => {
test('active status', () => {
const result = getStatusInfo('active', false, t);
expect(result).toEqual({
statusName: 'dialog.user.status.active',
statusImage: 'active',
hidePrivate: false
});
});
test('join me status', () => {
const result = getStatusInfo('join me', false, t);
expect(result).toEqual({
statusName: 'dialog.user.status.join_me',
statusImage: 'joinme',
hidePrivate: false
});
});
test('ask me status without hide invite', () => {
const result = getStatusInfo('ask me', false, t);
expect(result).toEqual({
statusName: 'dialog.user.status.ask_me',
statusImage: 'askme',
hidePrivate: false
});
});
test('ask me status with hide invite', () => {
const result = getStatusInfo('ask me', true, t);
expect(result).toEqual({
statusName: 'dialog.user.status.ask_me',
statusImage: 'askme',
hidePrivate: true
});
});
test('busy status always hides private', () => {
const result = getStatusInfo('busy', false, t);
expect(result).toEqual({
statusName: 'dialog.user.status.busy',
statusImage: 'busy',
hidePrivate: true
});
});
test('unknown status defaults to offline', () => {
const result = getStatusInfo('unknown', false, t);
expect(result).toEqual({
statusName: 'dialog.user.status.offline',
statusImage: 'offline',
hidePrivate: true
});
});
test('empty status defaults to offline', () => {
const result = getStatusInfo('', false, t);
expect(result).toEqual({
statusName: 'dialog.user.status.offline',
statusImage: 'offline',
hidePrivate: true
});
});
});
describe('getRpcWorldConfig', () => {
test('returns PyPyDance config for known PyPyDance world', () => {
const config = getRpcWorldConfig(
'wrld_f20326da-f1ac-45fc-a062-609723b097b1'
);
expect(config).toEqual({
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '784094509008551956',
bigIcon: 'pypy'
});
});
test('returns VR Dancing config', () => {
const config = getRpcWorldConfig(
'wrld_42377cf1-c54f-45ed-8996-5875b0573a83'
);
expect(config).toEqual({
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '846232616054030376',
bigIcon: 'vr_dancing'
});
});
test('returns ZuwaZuwa Dance config', () => {
const config = getRpcWorldConfig(
'wrld_52bdcdab-11cd-4325-9655-0fb120846945'
);
expect(config).toEqual({
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '939473404808007731',
bigIcon: 'zuwa_zuwa_dance'
});
});
test('returns LS Media config', () => {
const config = getRpcWorldConfig(
'wrld_74970324-58e8-4239-a17b-2c59dfdf00db'
);
expect(config).toEqual({
activityType: ActivityType.Watching,
statusDisplayType: StatusDisplayType.Details,
appId: '968292722391785512',
bigIcon: 'ls_media'
});
});
test('returns Popcorn Palace config', () => {
const config = getRpcWorldConfig(
'wrld_266523e8-9161-40da-acd0-6bd82e075833'
);
expect(config).toEqual({
activityType: ActivityType.Watching,
statusDisplayType: StatusDisplayType.Details,
appId: '1095440531821170820',
bigIcon: 'popcorn_palace'
});
});
test('returns null for unknown world', () => {
expect(getRpcWorldConfig('wrld_unknown')).toBeNull();
});
test('returns null for empty string', () => {
expect(getRpcWorldConfig('')).toBeNull();
});
test('returns a copy, not the original object', () => {
const a = getRpcWorldConfig(
'wrld_f20326da-f1ac-45fc-a062-609723b097b1'
);
const b = getRpcWorldConfig(
'wrld_f20326da-f1ac-45fc-a062-609723b097b1'
);
expect(a).toEqual(b);
expect(a).not.toBe(b);
});
test('covers all PyPyDance world IDs', () => {
const pypyIds = [
'wrld_f20326da-f1ac-45fc-a062-609723b097b1',
'wrld_10e5e467-fc65-42ed-8957-f02cace1398c',
'wrld_04899f23-e182-4a8d-b2c7-2c74c7c15534'
];
for (const id of pypyIds) {
const config = getRpcWorldConfig(id);
expect(config.appId).toBe('784094509008551956');
expect(config.bigIcon).toBe('pypy');
}
});
test('covers all LS Media world IDs', () => {
const lsIds = [
'wrld_74970324-58e8-4239-a17b-2c59dfdf00db',
'wrld_db9d878f-6e76-4776-8bf2-15bcdd7fc445',
'wrld_435bbf25-f34f-4b8b-82c6-cd809057eb8e',
'wrld_f767d1c8-b249-4ecc-a56f-614e433682c8'
];
for (const id of lsIds) {
const config = getRpcWorldConfig(id);
expect(config.appId).toBe('968292722391785512');
expect(config.bigIcon).toBe('ls_media');
}
});
});
describe('isPopcornPalaceWorld', () => {
test('returns true for Popcorn Palace worlds', () => {
expect(
isPopcornPalaceWorld('wrld_266523e8-9161-40da-acd0-6bd82e075833')
).toBe(true);
expect(
isPopcornPalaceWorld('wrld_27c7e6b2-d938-447e-a270-3d1a873e2cf3')
).toBe(true);
});
test('returns false for non-Popcorn Palace worlds', () => {
expect(
isPopcornPalaceWorld('wrld_f20326da-f1ac-45fc-a062-609723b097b1')
).toBe(false);
expect(isPopcornPalaceWorld('wrld_unknown')).toBe(false);
});
});

View File

@@ -0,0 +1,418 @@
import {
sanitizeUserJson,
sanitizeEntityJson,
computeTrustLevel,
computeUserPlatform,
computeDisabledContentSettings,
diffObjectProps,
createDefaultUserRef,
createDefaultWorldRef,
createDefaultAvatarRef,
createDefaultGroupRef,
createDefaultInstanceRef,
createDefaultFavoriteGroupRef,
createDefaultFavoriteCachedRef
} from '../entityTransforms';
describe('sanitizeUserJson', () => {
it('applies replaceBioSymbols to statusDescription, bio, note', () => {
const json = {
statusDescription: 'hello world',
bio: 'test bio',
note: 'test note'
};
sanitizeUserJson(json, '');
// replaceBioSymbols replaces Unicode look-alikes with ASCII
expect(json.statusDescription).toContain('?');
expect(json.bio).toContain('#');
expect(json.note).toContain('@');
});
it('removes emojis from statusDescription', () => {
const json = { statusDescription: 'hello 🎉 world' };
sanitizeUserJson(json, '');
// removeEmojis removes emoji then collapses whitespace
expect(json.statusDescription).toBe('hello world');
});
it('strips robot avatar URL', () => {
const robotUrl = 'https://example.com/robot.png';
const json = {
currentAvatarImageUrl: robotUrl,
currentAvatarThumbnailImageUrl: 'thumb.png'
};
sanitizeUserJson(json, robotUrl);
expect(json.currentAvatarImageUrl).toBeUndefined();
expect(json.currentAvatarThumbnailImageUrl).toBeUndefined();
});
it('keeps avatar URL when it does not match robot', () => {
const json = {
currentAvatarImageUrl: 'https://example.com/user.png',
currentAvatarThumbnailImageUrl: 'thumb.png'
};
sanitizeUserJson(json, 'https://example.com/robot.png');
expect(json.currentAvatarImageUrl).toBe('https://example.com/user.png');
});
it('handles missing fields gracefully', () => {
const json = { id: 'usr_123' };
expect(() => sanitizeUserJson(json, '')).not.toThrow();
});
});
describe('sanitizeEntityJson', () => {
it('applies replaceBioSymbols to specified fields', () => {
const json = {
name: 'hello',
description: 'test',
other: 'unchanged'
};
sanitizeEntityJson(json, ['name', 'description']);
expect(json.name).toContain('?');
expect(json.description).toContain('#');
expect(json.other).toContain(''); // not sanitized, still has Unicode
});
it('skips falsy fields', () => {
const json = { name: '', description: null };
expect(() =>
sanitizeEntityJson(json, ['name', 'description'])
).not.toThrow();
});
});
describe('computeTrustLevel', () => {
it('returns Visitor for empty tags', () => {
const result = computeTrustLevel([], '');
expect(result.trustLevel).toBe('Visitor');
expect(result.trustClass).toBe('x-tag-untrusted');
expect(result.trustColorKey).toBe('untrusted');
expect(result.trustSortNum).toBe(1);
});
it('returns Trusted User for veteran tags', () => {
const result = computeTrustLevel(['system_trust_veteran'], '');
expect(result.trustLevel).toBe('Trusted User');
expect(result.trustClass).toBe('x-tag-veteran');
expect(result.trustColorKey).toBe('veteran');
expect(result.trustSortNum).toBe(5);
});
it('returns Known User for trusted tags', () => {
const result = computeTrustLevel(['system_trust_trusted'], '');
expect(result.trustLevel).toBe('Known User');
expect(result.trustSortNum).toBe(4);
});
it('returns User for known tags', () => {
const result = computeTrustLevel(['system_trust_known'], '');
expect(result.trustLevel).toBe('User');
expect(result.trustSortNum).toBe(3);
});
it('returns New User for basic tags', () => {
const result = computeTrustLevel(['system_trust_basic'], '');
expect(result.trustLevel).toBe('New User');
expect(result.trustSortNum).toBe(2);
});
it('detects troll status', () => {
const result = computeTrustLevel(
['system_troll', 'system_trust_known'],
''
);
expect(result.isTroll).toBe(true);
expect(result.trustColorKey).toBe('troll');
expect(result.trustSortNum).toBeCloseTo(3.1); // 3 + 0.1
});
it('detects probable troll when not already troll', () => {
const result = computeTrustLevel(
['system_probable_troll', 'system_trust_basic'],
''
);
expect(result.isProbableTroll).toBe(true);
expect(result.isTroll).toBe(false);
expect(result.trustColorKey).toBe('troll');
});
it('probable troll is not set when already troll', () => {
const result = computeTrustLevel(
['system_troll', 'system_probable_troll'],
''
);
expect(result.isTroll).toBe(true);
expect(result.isProbableTroll).toBe(false);
});
it('detects moderator from developerType', () => {
const result = computeTrustLevel([], 'internal');
expect(result.isModerator).toBe(true);
expect(result.trustColorKey).toBe('vip');
expect(result.trustSortNum).toBeCloseTo(1.3); // 1 + 0.3
});
it('detects moderator from admin_moderator tag', () => {
const result = computeTrustLevel(
['admin_moderator', 'system_trust_veteran'],
''
);
expect(result.isModerator).toBe(true);
expect(result.trustColorKey).toBe('vip');
});
it('does not treat "none" developerType as moderator', () => {
const result = computeTrustLevel([], 'none');
expect(result.isModerator).toBe(false);
});
});
describe('computeUserPlatform', () => {
it('returns platform when valid', () => {
expect(computeUserPlatform('standalonewindows', 'android')).toBe(
'standalonewindows'
);
});
it('falls back to last_platform when platform is "offline"', () => {
expect(computeUserPlatform('offline', 'android')).toBe('android');
});
it('falls back to last_platform when platform is "web"', () => {
expect(computeUserPlatform('web', 'ios')).toBe('ios');
});
it('falls back to last_platform when platform is empty', () => {
expect(computeUserPlatform('', 'standalonewindows')).toBe(
'standalonewindows'
);
});
it('returns empty string when both are empty', () => {
expect(computeUserPlatform('', '')).toBe('');
});
});
describe('computeDisabledContentSettings', () => {
const settingsList = ['gore', 'nudity', 'violence'];
it('returns empty for null contentSettings', () => {
expect(computeDisabledContentSettings(null, settingsList)).toEqual([]);
});
it('returns empty for empty object', () => {
expect(computeDisabledContentSettings({}, settingsList)).toEqual([]);
});
it('returns disabled settings (false values)', () => {
const result = computeDisabledContentSettings(
{ gore: false, nudity: true, violence: false },
settingsList
);
expect(result).toEqual(['gore', 'violence']);
});
it('skips undefined settings', () => {
const result = computeDisabledContentSettings(
{ gore: true },
settingsList
);
expect(result).toEqual([]);
});
});
describe('diffObjectProps', () => {
const arraysMatch = (a, b) =>
a.length === b.length && a.every((v, i) => v === b[i]);
it('detects changed primitive props', () => {
const ref = { name: 'old', id: '1' };
const json = { name: 'new', id: '1' };
const result = diffObjectProps(ref, json, arraysMatch);
expect(result.hasPropChanged).toBe(true);
expect(result.changedProps.name).toEqual(['new', 'old']);
});
it('detects unchanged props', () => {
const ref = { name: 'same', id: '1' };
const json = { name: 'same', id: '1' };
const result = diffObjectProps(ref, json, arraysMatch);
expect(result.hasPropChanged).toBe(false);
});
it('detects changed arrays', () => {
const ref = { tags: ['a', 'b'] };
const json = { tags: ['a', 'c'] };
const result = diffObjectProps(ref, json, arraysMatch);
expect(result.hasPropChanged).toBe(true);
expect(result.changedProps.tags).toBeDefined();
});
it('ignores props only in json (not in ref)', () => {
const ref = { id: '1' };
const json = { id: '1', newProp: 'value' };
const result = diffObjectProps(ref, json, arraysMatch);
expect(result.hasPropChanged).toBe(false);
});
it('ignores props only in ref (not in json)', () => {
const ref = { id: '1', extra: 'value' };
const json = { id: '1' };
const result = diffObjectProps(ref, json, arraysMatch);
expect(result.hasPropChanged).toBe(false);
});
});
describe('createDefaultUserRef', () => {
it('creates object with defaults', () => {
const ref = createDefaultUserRef({});
expect(ref.id).toBe('');
expect(ref.displayName).toBe('');
expect(ref.tags).toEqual([]);
expect(ref.$trustLevel).toBe('Visitor');
expect(ref.$platform).toBe('');
});
it('spreads json over defaults', () => {
const ref = createDefaultUserRef({
id: 'usr_123',
displayName: 'Test'
});
expect(ref.id).toBe('usr_123');
expect(ref.displayName).toBe('Test');
expect(ref.bio).toBe('');
});
});
describe('createDefaultWorldRef', () => {
it('creates object with defaults', () => {
const ref = createDefaultWorldRef({});
expect(ref.id).toBe('');
expect(ref.name).toBe('');
expect(ref.capacity).toBe(0);
expect(ref.$isLabs).toBe(false);
});
it('spreads json over defaults', () => {
const ref = createDefaultWorldRef({
id: 'wrld_123',
name: 'Test World'
});
expect(ref.id).toBe('wrld_123');
expect(ref.name).toBe('Test World');
});
});
describe('createDefaultAvatarRef', () => {
it('creates object with defaults', () => {
const ref = createDefaultAvatarRef({});
expect(ref.id).toBe('');
expect(ref.name).toBe('');
expect(ref.version).toBe(0);
expect(ref.tags).toEqual([]);
});
it('spreads json over defaults', () => {
const ref = createDefaultAvatarRef({
id: 'avtr_123',
name: 'My Avatar'
});
expect(ref.id).toBe('avtr_123');
expect(ref.name).toBe('My Avatar');
});
});
describe('createDefaultGroupRef', () => {
it('creates object with defaults including myMember', () => {
const ref = createDefaultGroupRef({});
expect(ref.id).toBe('');
expect(ref.name).toBe('');
expect(ref.myMember).toBeDefined();
expect(ref.myMember.roleIds).toEqual([]);
expect(ref.roles).toEqual([]);
});
it('spreads json over defaults', () => {
const ref = createDefaultGroupRef({
id: 'grp_123',
name: 'Test Group'
});
expect(ref.id).toBe('grp_123');
});
});
describe('createDefaultInstanceRef', () => {
it('creates object with defaults', () => {
const ref = createDefaultInstanceRef({});
expect(ref.id).toBe('');
expect(ref.capacity).toBe(0);
expect(ref.hasCapacityForYou).toBe(true);
expect(ref.$fetchedAt).toBe('');
expect(ref.$disabledContentSettings).toEqual([]);
});
it('spreads json over defaults', () => {
const ref = createDefaultInstanceRef({
id: 'wrld_123:12345',
capacity: 40
});
expect(ref.id).toBe('wrld_123:12345');
expect(ref.capacity).toBe(40);
});
});
describe('createDefaultFavoriteGroupRef', () => {
it('creates object with defaults', () => {
const ref = createDefaultFavoriteGroupRef({});
expect(ref.id).toBe('');
expect(ref.name).toBe('');
expect(ref.displayName).toBe('');
expect(ref.type).toBe('');
expect(ref.visibility).toBe('');
expect(ref.tags).toEqual([]);
});
it('spreads json over defaults', () => {
const ref = createDefaultFavoriteGroupRef({
id: 'fvgrp_1',
name: 'group_0',
displayName: 'Group 1',
type: 'friend'
});
expect(ref.id).toBe('fvgrp_1');
expect(ref.name).toBe('group_0');
expect(ref.displayName).toBe('Group 1');
expect(ref.type).toBe('friend');
});
});
describe('createDefaultFavoriteCachedRef', () => {
it('creates object with defaults and computes $groupKey', () => {
const ref = createDefaultFavoriteCachedRef({});
expect(ref.id).toBe('');
expect(ref.type).toBe('');
expect(ref.favoriteId).toBe('');
expect(ref.tags).toEqual([]);
expect(ref.$groupKey).toBe(':undefined');
});
it('computes $groupKey from type and first tag', () => {
const ref = createDefaultFavoriteCachedRef({
id: 'fav_1',
type: 'friend',
favoriteId: 'usr_123',
tags: ['group_0']
});
expect(ref.$groupKey).toBe('friend:group_0');
expect(ref.favoriteId).toBe('usr_123');
});
it('handles multiple tags (uses first)', () => {
const ref = createDefaultFavoriteCachedRef({
type: 'world',
tags: ['worlds1', 'worlds2']
});
expect(ref.$groupKey).toBe('world:worlds1');
});
});

View File

@@ -1,7 +1,13 @@
import {
compareGameLogRows,
createJoinLeaveEntry,
createLocationEntry,
createPortalSpawnEntry,
createResourceLoadEntry,
gameLogSearchFilter,
getGameLogCreatedAtTs
getGameLogCreatedAtTs,
parseInventoryFromUrl,
parsePrintFromUrl
} from '../gameLog';
describe('gameLogSearchFilter', () => {
@@ -184,3 +190,159 @@ describe('compareGameLogRows', () => {
expect(compareGameLogRows(a, b)).toBe(0);
});
});
describe('createLocationEntry', () => {
test('creates entry with correct shape', () => {
const entry = createLocationEntry(
'2024-01-15T12:00:00Z',
'wrld_abc123~12345',
'wrld_abc123',
'Test World'
);
expect(entry).toEqual({
created_at: '2024-01-15T12:00:00Z',
type: 'Location',
location: 'wrld_abc123~12345',
worldId: 'wrld_abc123',
worldName: 'Test World',
groupName: '',
time: 0
});
});
});
describe('createJoinLeaveEntry', () => {
test('creates OnPlayerJoined entry with default time', () => {
const entry = createJoinLeaveEntry(
'OnPlayerJoined',
'2024-01-15T12:00:00Z',
'Alice',
'wrld_abc~123',
'usr_abc'
);
expect(entry).toEqual({
created_at: '2024-01-15T12:00:00Z',
type: 'OnPlayerJoined',
displayName: 'Alice',
location: 'wrld_abc~123',
userId: 'usr_abc',
time: 0
});
});
test('creates OnPlayerLeft entry with custom time', () => {
const entry = createJoinLeaveEntry(
'OnPlayerLeft',
'2024-01-15T12:30:00Z',
'Bob',
'wrld_xyz~456',
'usr_xyz',
1800000
);
expect(entry.type).toBe('OnPlayerLeft');
expect(entry.time).toBe(1800000);
});
});
describe('createPortalSpawnEntry', () => {
test('creates portal spawn entry with empty defaults', () => {
const entry = createPortalSpawnEntry(
'2024-01-15T12:00:00Z',
'wrld_abc~123'
);
expect(entry).toEqual({
created_at: '2024-01-15T12:00:00Z',
type: 'PortalSpawn',
location: 'wrld_abc~123',
displayName: '',
userId: '',
instanceId: '',
worldName: ''
});
});
});
describe('createResourceLoadEntry', () => {
test('maps resource-load-string to StringLoad', () => {
const entry = createResourceLoadEntry(
'resource-load-string',
'2024-01-15T12:00:00Z',
'https://cdn.example.com/res.json',
'wrld_abc~123'
);
expect(entry.type).toBe('StringLoad');
expect(entry.resourceUrl).toBe('https://cdn.example.com/res.json');
});
test('maps resource-load-image to ImageLoad', () => {
const entry = createResourceLoadEntry(
'resource-load-image',
'2024-01-15T12:00:00Z',
'https://cdn.example.com/img.png',
'wrld_abc~123'
);
expect(entry.type).toBe('ImageLoad');
});
});
describe('parseInventoryFromUrl', () => {
test('parses valid inventory URL', () => {
const url =
'https://api.vrchat.cloud/api/1/user/usr_032383a7-748c-4fb2-94e4-bcb928e5de6b/inventory/inv_75781d65-92fe-4a80-a1ff-27ee6e843b08';
const result = parseInventoryFromUrl(url);
expect(result).toEqual({
userId: 'usr_032383a7-748c-4fb2-94e4-bcb928e5de6b',
inventoryId: 'inv_75781d65-92fe-4a80-a1ff-27ee6e843b08'
});
});
test('returns null for non-inventory URL', () => {
expect(
parseInventoryFromUrl(
'https://api.vrchat.cloud/api/1/user/usr_abc/avatar'
)
).toBeNull();
});
test('returns null for invalid URL', () => {
expect(parseInventoryFromUrl('not a url')).toBeNull();
});
test('returns null for empty string', () => {
expect(parseInventoryFromUrl('')).toBeNull();
});
test('returns null if inventoryId length is wrong', () => {
expect(
parseInventoryFromUrl(
'https://api.vrchat.cloud/api/1/user/usr_abc/inventory/inv_short'
)
).toBeNull();
});
});
describe('parsePrintFromUrl', () => {
test('parses valid print URL', () => {
// printId is 41 chars: prnt_ (5) + UUID (36)
const printId = 'prnt_aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';
const url = `https://api.vrchat.cloud/api/1/prints/${printId}`;
const result = parsePrintFromUrl(url);
expect(result).toBe(printId);
});
test('returns null for non-print URL', () => {
expect(
parsePrintFromUrl('https://api.vrchat.cloud/api/1/user/usr_abc')
).toBeNull();
});
test('returns null for invalid URL', () => {
expect(parsePrintFromUrl('not a url')).toBeNull();
});
test('returns null if printId has wrong length', () => {
expect(
parsePrintFromUrl('https://api.vrchat.cloud/api/1/prints/short')
).toBeNull();
});
});

View File

@@ -1,9 +1,21 @@
import { vi } from 'vitest';
// Mock transitive deps from location.js → stores → columns.jsx → i18n
vi.mock('../../../views/Feed/Feed.vue', () => ({
default: { template: '<div />' }
}));
vi.mock('../../../views/Feed/columns.jsx', () => ({ columns: [] }));
vi.mock('../../../plugin/router', () => ({
default: { push: vi.fn(), currentRoute: { value: {} } }
}));
import {
displayLocation,
parseLocation,
resolveRegion,
translateAccessType
} from '../locationParser';
import { getLocationText } from '../location';
import { accessTypeLocaleKeyMap } from '../../constants';
describe('Location Utils', () => {
@@ -508,4 +520,78 @@ describe('Location Utils', () => {
);
});
});
describe('getLocationText', () => {
const t = (key) => key;
const opts = (overrides = {}) => ({
hint: '',
worldName: undefined,
accessTypeLabel: 'Public',
t,
...overrides
});
test('returns offline label', () => {
const L = parseLocation('offline');
expect(getLocationText(L, opts())).toBe('location.offline');
});
test('returns private label', () => {
const L = parseLocation('private');
expect(getLocationText(L, opts())).toBe('location.private');
});
test('returns traveling label', () => {
const L = parseLocation('traveling');
expect(getLocationText(L, opts())).toBe('location.traveling');
});
test('returns hint with access type when instance exists', () => {
const L = parseLocation('wrld_12345:67890');
expect(getLocationText(L, opts({ hint: 'My World' }))).toBe(
'My World · Public'
);
});
test('returns hint alone when no instance', () => {
const L = parseLocation('wrld_12345');
expect(getLocationText(L, opts({ hint: 'My World' }))).toBe(
'My World'
);
});
test('returns world name with access type when cached', () => {
const L = parseLocation('wrld_12345:67890');
expect(getLocationText(L, opts({ worldName: 'Cool World' }))).toBe(
'Cool World · Public'
);
});
test('returns world name alone when no instance', () => {
const L = parseLocation('wrld_12345');
expect(getLocationText(L, opts({ worldName: 'Cool World' }))).toBe(
'Cool World'
);
});
test('falls back to worldId when no cached name', () => {
const L = parseLocation('wrld_12345:67890');
expect(getLocationText(L, opts())).toBe('wrld_12345 · Public');
});
test('returns empty string for empty location', () => {
const L = parseLocation('');
expect(getLocationText(L, opts())).toBe('');
});
test('hint takes priority over worldName', () => {
const L = parseLocation('wrld_12345:67890');
expect(
getLocationText(
L,
opts({ hint: 'Hint Text', worldName: 'World Name' })
)
).toBe('Hint Text · Public');
});
});
});

View File

@@ -0,0 +1,211 @@
import { describe, it, expect, vi } from 'vitest';
import {
sanitizeNotificationJson,
parseNotificationDetails,
createDefaultNotificationRef,
createDefaultNotificationV2Ref,
applyBoopLegacyHandling
} from '../notificationTransforms';
describe('sanitizeNotificationJson', () => {
it('should remove null and undefined values', () => {
const json = { id: '1', message: null, type: undefined, seen: false };
const result = sanitizeNotificationJson(json);
expect(result).not.toHaveProperty('message');
expect(result).not.toHaveProperty('type');
expect(result).toHaveProperty('id', '1');
expect(result).toHaveProperty('seen', false);
});
it('should apply replaceBioSymbols to message', () => {
// replaceBioSymbols replaces Unicode look-alikes with ASCII, not zero-width spaces
const json = { message: 'hello world' };
const result = sanitizeNotificationJson(json);
expect(result.message).toContain('?');
});
it('should apply replaceBioSymbols to title', () => {
const json = { title: 'hello world' };
const result = sanitizeNotificationJson(json);
expect(result.title).toContain('?');
});
it('should not touch other fields', () => {
const json = { id: 'abc', seen: true, details: { x: 1 } };
const result = sanitizeNotificationJson(json);
expect(result).toEqual({ id: 'abc', seen: true, details: { x: 1 } });
});
it('should mutate and return the same object', () => {
const json = { id: '1', bad: null };
const result = sanitizeNotificationJson(json);
expect(result).toBe(json);
});
});
describe('parseNotificationDetails', () => {
it('should return object details as-is', () => {
const details = { worldId: 'wrld_123' };
expect(parseNotificationDetails(details)).toBe(details);
});
it('should parse JSON string details', () => {
const details = '{"worldId":"wrld_123"}';
expect(parseNotificationDetails(details)).toEqual({
worldId: 'wrld_123'
});
});
it('should return empty object for "{}"', () => {
expect(parseNotificationDetails('{}')).toEqual({});
});
it('should return empty object for invalid JSON', () => {
const spy = vi.spyOn(console, 'log').mockImplementation(() => {});
expect(parseNotificationDetails('not json')).toEqual({});
spy.mockRestore();
});
it('should return parsed array for JSON array string (arrays are objects)', () => {
expect(parseNotificationDetails('[1,2]')).toEqual([1, 2]);
});
it('should return empty object for null', () => {
expect(parseNotificationDetails(null)).toEqual({});
});
it('should return empty object for undefined', () => {
expect(parseNotificationDetails(undefined)).toEqual({});
});
});
describe('createDefaultNotificationRef', () => {
it('should create a ref with all default fields', () => {
const ref = createDefaultNotificationRef({});
expect(ref).toEqual({
id: '',
senderUserId: '',
senderUsername: '',
type: '',
message: '',
details: {},
seen: false,
created_at: '',
$isExpired: false
});
});
it('should merge json over defaults', () => {
const ref = createDefaultNotificationRef({
id: 'noti_1',
type: 'friendRequest',
senderUserId: 'usr_abc'
});
expect(ref.id).toBe('noti_1');
expect(ref.type).toBe('friendRequest');
expect(ref.senderUserId).toBe('usr_abc');
expect(ref.message).toBe('');
});
it('should parse string details', () => {
const ref = createDefaultNotificationRef({
details: '{"worldId":"wrld_1"}'
});
expect(ref.details).toEqual({ worldId: 'wrld_1' });
});
it('should keep object details', () => {
const details = { worldId: 'wrld_1' };
const ref = createDefaultNotificationRef({ details });
expect(ref.details).toBe(details);
});
});
describe('createDefaultNotificationV2Ref', () => {
it('should create a ref with all default V2 fields', () => {
const ref = createDefaultNotificationV2Ref({});
expect(ref).toMatchObject({
id: '',
createdAt: '',
updatedAt: '',
expiresAt: '',
type: '',
link: '',
linkText: '',
message: '',
title: '',
imageUrl: '',
seen: false,
senderUserId: '',
senderUsername: '',
version: 2
});
expect(ref.data).toEqual({});
expect(ref.responses).toEqual([]);
expect(ref.details).toEqual({});
});
it('should merge json over defaults', () => {
const ref = createDefaultNotificationV2Ref({
id: 'noti_v2',
type: 'boop',
seen: true
});
expect(ref.id).toBe('noti_v2');
expect(ref.type).toBe('boop');
expect(ref.seen).toBe(true);
});
});
describe('applyBoopLegacyHandling', () => {
it('should not modify non-boop notifications', () => {
const ref = {
type: 'friendRequest',
title: 'Hello',
message: '',
imageUrl: ''
};
applyBoopLegacyHandling(ref, 'https://api.example.com');
expect(ref.title).toBe('Hello');
expect(ref.message).toBe('');
});
it('should not modify boop without title', () => {
const ref = {
type: 'boop',
title: '',
message: 'existing',
imageUrl: ''
};
applyBoopLegacyHandling(ref, 'https://api.example.com');
expect(ref.message).toBe('existing');
});
it('should handle default emoji boops', () => {
const ref = {
type: 'boop',
title: 'Boop!',
message: '',
imageUrl: '',
details: { emojiId: 'default_wave', emojiVersion: '1' }
};
applyBoopLegacyHandling(ref, 'https://api.example.com');
expect(ref.title).toBe('');
expect(ref.message).toBe('Boop! wave');
expect(ref.imageUrl).toBe('default_wave');
});
it('should handle custom emoji boops', () => {
const ref = {
type: 'boop',
title: 'Boop!',
message: '',
imageUrl: '',
details: { emojiId: 'emj_123', emojiVersion: '5' }
};
applyBoopLegacyHandling(ref, 'https://api.example.com');
expect(ref.title).toBe('');
expect(ref.message).toBe('Boop!');
expect(ref.imageUrl).toBe('https://api.example.com/file/emj_123/5');
});
});

View File

@@ -145,6 +145,23 @@ function replaceBioSymbols(text) {
return newText.replace(/ {1,}/g, ' ').trimRight();
}
/**
* @param {string} text
* @returns {string}
*/
function removeEmojis(text) {
if (!text) {
return '';
}
return text
.replace(
/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g,
''
)
.replace(/\s+/g, ' ')
.trim();
}
export {
escapeTag,
escapeTagRecursive,
@@ -152,5 +169,6 @@ export {
commaNumber,
localeIncludes,
changeLogRemoveLinks,
replaceBioSymbols
replaceBioSymbols,
removeEmojis
};

View File

@@ -0,0 +1,67 @@
/**
* Evict entries from a Map cache when it exceeds maxSize.
* Entries matching isRetainedFn are kept; the rest are evicted oldest-first
* (or by the provided sortFn).
* @param {Map} cache - The cache Map to evict from
* @param {number} maxSize - Maximum allowed size
* @param {(value: any, key: string) => boolean} isRetainedFn - Return true to keep the entry
* @param {object} [opts] - Options
* @param {(a: {key: string, value: any}, b: {key: string, value: any}) => number} [opts.sortFn] -
* Custom sort for eviction order (entries sorted ascending; first entries evicted first).
* If not provided, entries are evicted in insertion order.
* @param {string} [opts.logLabel] - Label for console.log output
* @returns {{ deletedCount: number }}
*/
export function evictMapCache(cache, maxSize, isRetainedFn, opts = {}) {
if (cache.size <= maxSize) {
return { deletedCount: 0 };
}
const { sortFn, logLabel } = opts;
const overBy = cache.size - maxSize;
if (sortFn) {
// Collect removable entries, sort, then evict
const removable = [];
for (const [key, value] of cache) {
if (isRetainedFn(value, key)) {
continue;
}
removable.push({ key, value });
}
removable.sort(sortFn);
const toDelete = Math.min(overBy, removable.length);
for (let i = 0; i < toDelete; i++) {
cache.delete(removable[i].key);
}
if (logLabel) {
console.log(
`${logLabel}: Deleted ${toDelete}. Current cache size: ${cache.size}`
);
}
return { deletedCount: toDelete };
}
// Default: evict in insertion order (skip retained entries)
let deletedCount = 0;
const keysToDelete = [];
for (const [key, value] of cache) {
if (isRetainedFn(value, key)) {
continue;
}
if (deletedCount >= overBy) {
break;
}
keysToDelete.push(key);
deletedCount++;
}
for (const key of keysToDelete) {
cache.delete(key);
}
if (logLabel) {
console.log(
`${logLabel}: Deleted ${deletedCount}. Current cache size: ${cache.size}`
);
}
return { deletedCount };
}

View File

@@ -0,0 +1,228 @@
import { ActivityType, StatusDisplayType } from '../constants/discord';
/**
* RPC world configuration table.
* Maps worldId → { activityType, statusDisplayType, appId, bigIcon }.
*/
const RPC_WORLD_CONFIGS = new Map([
// PyPyDance
[
'wrld_f20326da-f1ac-45fc-a062-609723b097b1',
{
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '784094509008551956',
bigIcon: 'pypy'
}
],
[
'wrld_10e5e467-fc65-42ed-8957-f02cace1398c',
{
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '784094509008551956',
bigIcon: 'pypy'
}
],
[
'wrld_04899f23-e182-4a8d-b2c7-2c74c7c15534',
{
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '784094509008551956',
bigIcon: 'pypy'
}
],
// VR Dancing
[
'wrld_42377cf1-c54f-45ed-8996-5875b0573a83',
{
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '846232616054030376',
bigIcon: 'vr_dancing'
}
],
[
'wrld_dd6d2888-dbdc-47c2-bc98-3d631b2acd7c',
{
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '846232616054030376',
bigIcon: 'vr_dancing'
}
],
// ZuwaZuwa Dance
[
'wrld_52bdcdab-11cd-4325-9655-0fb120846945',
{
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '939473404808007731',
bigIcon: 'zuwa_zuwa_dance'
}
],
[
'wrld_2d40da63-8f1f-4011-8a9e-414eb8530acd',
{
activityType: ActivityType.Listening,
statusDisplayType: StatusDisplayType.Details,
appId: '939473404808007731',
bigIcon: 'zuwa_zuwa_dance'
}
],
// LS Media
[
'wrld_74970324-58e8-4239-a17b-2c59dfdf00db',
{
activityType: ActivityType.Watching,
statusDisplayType: StatusDisplayType.Details,
appId: '968292722391785512',
bigIcon: 'ls_media'
}
],
[
'wrld_db9d878f-6e76-4776-8bf2-15bcdd7fc445',
{
activityType: ActivityType.Watching,
statusDisplayType: StatusDisplayType.Details,
appId: '968292722391785512',
bigIcon: 'ls_media'
}
],
[
'wrld_435bbf25-f34f-4b8b-82c6-cd809057eb8e',
{
activityType: ActivityType.Watching,
statusDisplayType: StatusDisplayType.Details,
appId: '968292722391785512',
bigIcon: 'ls_media'
}
],
[
'wrld_f767d1c8-b249-4ecc-a56f-614e433682c8',
{
activityType: ActivityType.Watching,
statusDisplayType: StatusDisplayType.Details,
appId: '968292722391785512',
bigIcon: 'ls_media'
}
],
// Popcorn Palace
[
'wrld_266523e8-9161-40da-acd0-6bd82e075833',
{
activityType: ActivityType.Watching,
statusDisplayType: StatusDisplayType.Details,
appId: '1095440531821170820',
bigIcon: 'popcorn_palace'
}
],
[
'wrld_27c7e6b2-d938-447e-a270-3d1a873e2cf3',
{
activityType: ActivityType.Watching,
statusDisplayType: StatusDisplayType.Details,
appId: '1095440531821170820',
bigIcon: 'popcorn_palace'
}
]
]);
/** Set of Popcorn Palace world IDs (big icon can be overridden by thumbnail) */
const POPCORN_PALACE_WORLD_IDS = new Set([
'wrld_266523e8-9161-40da-acd0-6bd82e075833',
'wrld_27c7e6b2-d938-447e-a270-3d1a873e2cf3'
]);
/**
* Get custom world rpc configuration for a specific world ID.
* @param {string} worldId
* @returns {{ activityType: number, statusDisplayType: number, appId: string, bigIcon: string } | null}
*/
export function getRpcWorldConfig(worldId) {
const config = RPC_WORLD_CONFIGS.get(worldId);
if (!config) {
return null;
}
return { ...config };
}
/**
* Check if a world ID is a Popcorn Palace world.
* @param {string} worldId
* @returns {boolean}
*/
export function isPopcornPalaceWorld(worldId) {
return POPCORN_PALACE_WORLD_IDS.has(worldId);
}
/**
* Get the platform display label for Discord RPC.
* @param {string} platform - VRC platform string (e.g. 'standalonewindows', 'android')
* @param {boolean} isGameRunning
* @param {boolean} isGameNoVR
* @param {Function} t - i18n translate function
* @returns {string} Platform label string (e.g. ' (VR)', ' (PC)'), or empty string
*/
export function getPlatformLabel(platform, isGameRunning, isGameNoVR, t) {
if (isGameRunning) {
return isGameNoVR
? ` (${t('view.settings.discord_presence.rpc.desktop')})`
: ` (${t('view.settings.discord_presence.rpc.vr')})`;
}
switch (platform) {
case 'web':
return '';
case 'standalonewindows':
return ` (PC)`;
case 'android':
return ` (Android)`;
case 'ios':
return ` (iOS)`;
default:
return platform ? ` (${platform})` : '';
}
}
/**
* Get Discord status info from VRC user status.
* @param {string} status - VRC user status ('active', 'join me', 'ask me', 'busy')
* @param {boolean} discordHideInvite - Whether invite-hiding is enabled
* @param {Function} t - i18n translate function
* @returns {{ statusName: string, statusImage: string, hidePrivate: boolean }}
*/
export function getStatusInfo(status, discordHideInvite, t) {
switch (status) {
case 'active':
return {
statusName: t('dialog.user.status.active'),
statusImage: 'active',
hidePrivate: false
};
case 'join me':
return {
statusName: t('dialog.user.status.join_me'),
statusImage: 'joinme',
hidePrivate: false
};
case 'ask me':
return {
statusName: t('dialog.user.status.ask_me'),
statusImage: 'askme',
hidePrivate: discordHideInvite
};
case 'busy':
return {
statusName: t('dialog.user.status.busy'),
statusImage: 'busy',
hidePrivate: true
};
default:
return {
statusName: t('dialog.user.status.offline'),
statusImage: 'offline',
hidePrivate: true
};
}
}

View File

@@ -0,0 +1,532 @@
import { removeEmojis, replaceBioSymbols } from './base/string';
/**
* Sanitize user JSON fields before applying to cache.
* Applies replaceBioSymbols to statusDescription, bio, note;
* removeEmojis to statusDescription;
* strips robot avatar URL.
* @param {object} json - Raw user API response
* @param {string} robotUrl - The robot/default avatar URL to strip
* @returns {object} The mutated json (same reference)
*/
export function sanitizeUserJson(json, robotUrl) {
if (json.statusDescription) {
json.statusDescription = replaceBioSymbols(json.statusDescription);
json.statusDescription = removeEmojis(json.statusDescription);
}
if (json.bio) {
json.bio = replaceBioSymbols(json.bio);
}
if (json.note) {
json.note = replaceBioSymbols(json.note);
}
if (robotUrl && json.currentAvatarImageUrl === robotUrl) {
delete json.currentAvatarImageUrl;
delete json.currentAvatarThumbnailImageUrl;
}
return json;
}
/**
* Sanitize arbitrary entity JSON fields via replaceBioSymbols.
* @param {object} json - Raw API response
* @param {string[]} fields - Field names to sanitize
* @returns {object} The mutated json
*/
export function sanitizeEntityJson(json, fields) {
for (const field of fields) {
if (json[field]) {
json[field] = replaceBioSymbols(json[field]);
}
}
return json;
}
/**
* Compute trust level, moderator status, and troll status from user tags.
* Pure function — no store dependencies.
* @param {string[]} tags - User tags array
* @param {string} developerType - User's developerType field
* @returns {{
* trustLevel: string,
* trustClass: string,
* trustSortNum: number,
* isModerator: boolean,
* isTroll: boolean,
* isProbableTroll: boolean,
* trustColorKey: string
* }}
*/
export function computeTrustLevel(tags, developerType) {
let isModerator = Boolean(developerType) && developerType !== 'none';
let isTroll = false;
let isProbableTroll = false;
let trustLevel = 'Visitor';
let trustClass = 'x-tag-untrusted';
let trustColorKey = 'untrusted';
let trustSortNum = 1;
if (tags.includes('admin_moderator')) {
isModerator = true;
}
if (tags.includes('system_troll')) {
isTroll = true;
}
if (tags.includes('system_probable_troll') && !isTroll) {
isProbableTroll = true;
}
if (tags.includes('system_trust_veteran')) {
trustLevel = 'Trusted User';
trustClass = 'x-tag-veteran';
trustColorKey = 'veteran';
trustSortNum = 5;
} else if (tags.includes('system_trust_trusted')) {
trustLevel = 'Known User';
trustClass = 'x-tag-trusted';
trustColorKey = 'trusted';
trustSortNum = 4;
} else if (tags.includes('system_trust_known')) {
trustLevel = 'User';
trustClass = 'x-tag-known';
trustColorKey = 'known';
trustSortNum = 3;
} else if (tags.includes('system_trust_basic')) {
trustLevel = 'New User';
trustClass = 'x-tag-basic';
trustColorKey = 'basic';
trustSortNum = 2;
}
if (isTroll || isProbableTroll) {
trustColorKey = 'troll';
trustSortNum += 0.1;
}
if (isModerator) {
trustColorKey = 'vip';
trustSortNum += 0.3;
}
return {
trustLevel,
trustClass,
trustSortNum,
isModerator,
isTroll,
isProbableTroll,
trustColorKey
};
}
/**
* Determine the effective user platform.
* @param {string} platform - Current platform
* @param {string} lastPlatform - Last known platform
* @returns {string} Resolved platform
*/
export function computeUserPlatform(platform, lastPlatform) {
if (platform && platform !== 'offline' && platform !== 'web') {
return platform;
}
return lastPlatform || '';
}
/**
* Compute which content settings are disabled for an instance.
* @param {object} contentSettings - The instance's contentSettings object
* @param {string[]} settingsList - List of all possible content setting keys
* @returns {string[]} Array of disabled setting keys
*/
export function computeDisabledContentSettings(contentSettings, settingsList) {
const disabled = [];
if (!contentSettings || Object.keys(contentSettings).length === 0) {
return disabled;
}
for (const setting of settingsList) {
if (
typeof contentSettings[setting] === 'undefined' ||
contentSettings[setting] === true
) {
continue;
}
disabled.push(setting);
}
return disabled;
}
/**
* Detect which properties changed between an existing ref and incoming JSON.
* Compares primitives directly; arrays via arraysMatchFn.
* @param {object} ref - The existing cached object
* @param {object} json - The incoming update
* @param {(a: any[], b: any[]) => boolean} arraysMatchFn - Function to compare arrays
* @returns {{ hasPropChanged: boolean, changedProps: object }}
*/
export function diffObjectProps(ref, json, arraysMatchFn) {
const changedProps = {};
let hasPropChanged = false;
// Only compare primitive values
for (const prop in ref) {
if (typeof json[prop] === 'undefined') {
continue;
}
if (ref[prop] === null || typeof ref[prop] !== 'object') {
changedProps[prop] = true;
}
}
// Check json props against ref (including array comparison)
for (const prop in json) {
if (typeof ref[prop] === 'undefined') {
continue;
}
if (Array.isArray(json[prop]) && Array.isArray(ref[prop])) {
if (!arraysMatchFn(json[prop], ref[prop])) {
changedProps[prop] = true;
}
} else if (json[prop] === null || typeof json[prop] !== 'object') {
changedProps[prop] = true;
}
}
// Resolve actual changes
for (const prop in changedProps) {
const asIs = ref[prop];
const toBe = json[prop];
if (asIs === toBe) {
delete changedProps[prop];
} else {
hasPropChanged = true;
changedProps[prop] = [toBe, asIs];
}
}
return { hasPropChanged, changedProps };
}
/**
* Create a default user ref object with all expected fields.
* Returns a plain object (caller wraps in reactive() if needed).
* @param {object} json - API response to merge
* @returns {object} Default user object with json spread on top
*/
export function createDefaultUserRef(json) {
return {
ageVerificationStatus: '',
ageVerified: false,
allowAvatarCopying: false,
badges: [],
bio: '',
bioLinks: [],
currentAvatarImageUrl: '',
currentAvatarTags: [],
currentAvatarThumbnailImageUrl: '',
date_joined: '',
developerType: '',
discordId: '',
displayName: '',
friendKey: '',
friendRequestStatus: '',
id: '',
instanceId: '',
isFriend: false,
last_activity: '',
last_login: '',
last_mobile: null,
last_platform: '',
location: '',
platform: '',
note: null,
profilePicOverride: '',
profilePicOverrideThumbnail: '',
pronouns: '',
state: '',
status: '',
statusDescription: '',
tags: [],
travelingToInstance: '',
travelingToLocation: '',
travelingToWorld: '',
userIcon: '',
worldId: '',
// only in bulk request
fallbackAvatar: '',
// VRCX
$location: {},
$location_at: Date.now(),
$online_for: Date.now(),
$travelingToTime: Date.now(),
$offline_for: null,
$active_for: Date.now(),
$isVRCPlus: false,
$isModerator: false,
$isTroll: false,
$isProbableTroll: false,
$trustLevel: 'Visitor',
$trustClass: 'x-tag-untrusted',
$userColour: '',
$trustSortNum: 1,
$languages: [],
$joinCount: 0,
$timeSpent: 0,
$lastSeen: '',
$mutualCount: 0,
$nickName: '',
$previousLocation: '',
$customTag: '',
$customTagColour: '',
$friendNumber: 0,
$platform: '',
$moderations: {},
//
...json
};
}
/**
* Create a default world ref object.
* @param {object} json - API response to merge
* @returns {object}
*/
export function createDefaultWorldRef(json) {
return {
id: '',
name: '',
description: '',
defaultContentSettings: {},
authorId: '',
authorName: '',
capacity: 0,
recommendedCapacity: 0,
tags: [],
releaseStatus: '',
imageUrl: '',
thumbnailImageUrl: '',
assetUrl: '',
assetUrlObject: {},
pluginUrl: '',
pluginUrlObject: {},
unityPackageUrl: '',
unityPackageUrlObject: {},
unityPackages: [],
version: 0,
favorites: 0,
created_at: '',
updated_at: '',
publicationDate: '',
labsPublicationDate: '',
visits: 0,
popularity: 0,
heat: 0,
publicOccupants: 0,
privateOccupants: 0,
occupants: 0,
instances: [],
featured: false,
organization: '',
previewYoutubeId: '',
// VRCX
$isLabs: false,
//
...json
};
}
/**
* Create a default avatar ref object.
* @param {object} json - API response to merge
* @returns {object}
*/
export function createDefaultAvatarRef(json) {
return {
acknowledgements: '',
authorId: '',
authorName: '',
created_at: '',
description: '',
featured: false,
highestPrice: null,
id: '',
imageUrl: '',
listingDate: null,
lock: false,
lowestPrice: null,
name: '',
pendingUpload: false,
performance: {},
productId: null,
publishedListings: [],
releaseStatus: '',
searchable: false,
styles: [],
tags: [],
thumbnailImageUrl: '',
unityPackageUrl: '',
unityPackageUrlObject: {},
unityPackages: [],
updated_at: '',
version: 0,
...json
};
}
/**
* Create a default group ref object.
* @param {object} json - API response to merge
* @returns {object}
*/
export function createDefaultGroupRef(json) {
return {
id: '',
name: '',
shortCode: '',
description: '',
bannerId: '',
bannerUrl: '',
createdAt: '',
discriminator: '',
galleries: [],
iconId: '',
iconUrl: '',
isVerified: false,
joinState: '',
languages: [],
links: [],
memberCount: 0,
memberCountSyncedAt: '',
membershipStatus: '',
onlineMemberCount: 0,
ownerId: '',
privacy: '',
rules: null,
tags: [],
// in group
initialRoleIds: [],
myMember: {
bannedAt: null,
groupId: '',
has2FA: false,
id: '',
isRepresenting: false,
isSubscribedToAnnouncements: false,
joinedAt: '',
managerNotes: '',
membershipStatus: '',
permissions: [],
roleIds: [],
userId: '',
visibility: '',
_created_at: '',
_id: '',
_updated_at: ''
},
updatedAt: '',
// includeRoles: true
roles: [],
// group list
$memberId: '',
groupId: '',
isRepresenting: false,
memberVisibility: false,
mutualGroup: false,
// VRCX
$languages: [],
...json
};
}
/**
* Create a default instance ref object.
* @param {object} json - API response to merge
* @returns {object}
*/
export function createDefaultInstanceRef(json) {
return {
id: '',
location: '',
instanceId: '',
name: '',
worldId: '',
type: '',
ownerId: '',
tags: [],
active: false,
full: false,
n_users: 0,
hasCapacityForYou: true, // not present depending on endpoint
capacity: 0,
recommendedCapacity: 0,
userCount: 0,
queueEnabled: false, // only present with group instance type
queueSize: 0, // only present when queuing is enabled
platforms: {},
gameServerVersion: 0,
hardClose: null, // boolean or null
closedAt: null, // string or null
secureName: '',
shortName: '',
world: {},
users: [], // only present when you're the owner
clientNumber: '',
contentSettings: {},
photonRegion: '',
region: '',
canRequestInvite: false,
permanent: false,
private: '', // part of instance tag
hidden: '', // part of instance tag
nonce: '', // only present when you're the owner
strict: false, // deprecated
displayName: null,
groupAccessType: null, // only present with group instance type
roleRestricted: false, // only present with group instance type
instancePersistenceEnabled: null,
playerPersistenceEnabled: null,
ageGate: null,
// VRCX
$fetchedAt: '',
$disabledContentSettings: [],
...json
};
}
/**
* Build a default favorite group ref from JSON data.
* @param {object} json
* @returns {object}
*/
export function createDefaultFavoriteGroupRef(json) {
return {
id: '',
ownerId: '',
ownerDisplayName: '',
name: '',
displayName: '',
type: '',
visibility: '',
tags: [],
...json
};
}
/**
* Build a default cached favorite ref from JSON data.
* Computes $groupKey from type and first tag.
* @param {object} json
* @returns {object}
*/
export function createDefaultFavoriteCachedRef(json) {
const ref = {
id: '',
type: '',
favoriteId: '',
tags: [],
// VRCX
$groupKey: '',
//
...json
};
ref.$groupKey = `${ref.type}:${String(ref.tags[0])}`;
return ref;
}

View File

@@ -76,7 +76,6 @@ function gameLogSearchFilter(row, searchQuery) {
/**
* Extract a millisecond timestamp from a game log row.
* Handles numeric (seconds or millis), ISO string, and dayjs-parseable formats.
*
* @param {object} row
* @returns {number} millisecond timestamp, or 0 if unparseable
*/
@@ -105,7 +104,6 @@ function getGameLogCreatedAtTs(row) {
* Primary key: created_at timestamp (newest first).
* Secondary: rowId (highest first).
* Tertiary: uid string (reverse lexicographic).
*
* @param {object} a
* @param {object} b
* @returns {number} negative if a should come first, positive if b first
@@ -129,3 +127,136 @@ function compareGameLogRows(a, b) {
}
export { gameLogSearchFilter, getGameLogCreatedAtTs, compareGameLogRows };
/**
* Create a Location game log entry.
* @param {string} dt
* @param {string} location
* @param {string} worldId
* @param {string} worldName
* @returns {object}
*/
export function createLocationEntry(dt, location, worldId, worldName) {
return {
created_at: dt,
type: 'Location',
location,
worldId,
worldName,
groupName: '',
time: 0
};
}
/**
* Create a player join or leave game log entry.
* @param {'OnPlayerJoined'|'OnPlayerLeft'} type
* @param {string} dt
* @param {string} displayName
* @param {string} location
* @param {string} userId
* @param {number} [time]
* @returns {object}
*/
export function createJoinLeaveEntry(
type,
dt,
displayName,
location,
userId,
time = 0
) {
return {
created_at: dt,
type,
displayName,
location,
userId,
time
};
}
/**
* Create a PortalSpawn game log entry.
* @param {string} dt
* @param {string} location
* @returns {object}
*/
export function createPortalSpawnEntry(dt, location) {
return {
created_at: dt,
type: 'PortalSpawn',
location,
displayName: '',
userId: '',
instanceId: '',
worldName: ''
};
}
/**
* Create a resource load game log entry.
* @param {string} rawType - 'resource-load-string' or 'resource-load-image'
* @param {string} dt
* @param {string} resourceUrl
* @param {string} location
* @returns {object}
*/
export function createResourceLoadEntry(rawType, dt, resourceUrl, location) {
return {
created_at: dt,
type: rawType === 'resource-load-string' ? 'StringLoad' : 'ImageLoad',
resourceUrl,
location
};
}
/**
* Parse an API request URL for inventory info.
* Matches: /api/1/user/{userId}/inventory/{inventoryId}
* @example
* // https://api.vrchat.cloud/api/1/user/usr_032383a7-748c-4fb2-94e4-bcb928e5de6b/inventory/inv_75781d65-92fe-4a80-a1ff-27ee6e843b08
* @param {string} url
* @returns {{ userId: string, inventoryId: string } | null}
*/
export function parseInventoryFromUrl(url) {
try {
const parsed = new URL(url);
if (
parsed.pathname.substring(0, 12) === '/api/1/user/' &&
parsed.pathname.includes('/inventory/inv_')
) {
const pathArray = parsed.pathname.split('/');
const userId = pathArray[4];
const inventoryId = pathArray[6];
if (userId && inventoryId && inventoryId.length === 40) {
return { userId, inventoryId };
}
}
} catch {
// invalid URL
}
return null;
}
/**
* Parse an API request URL for print info.
* Matches: /api/1/prints/{printId}
* @param {string} url
* @returns {string|null} printId or null
*/
export function parsePrintFromUrl(url) {
try {
const parsed = new URL(url);
if (parsed.pathname.substring(0, 14) === '/api/1/prints/') {
const pathArray = parsed.pathname.split('/');
const printId = pathArray[4];
if (printId && printId.length === 41) {
return printId;
}
}
} catch {
// invalid URL
}
return null;
}

View File

@@ -24,3 +24,7 @@ export * from './memos';
export * from './throttle';
export * from './retry';
export * from './gameLog';
export * from './entityTransforms';
export * from './cacheUtils';
export * from './notificationTransforms';
export * from './discordPresence';

View File

@@ -37,3 +37,36 @@ function getFriendsLocations(friendsArr) {
}
export { getFriendsLocations };
/**
* Get the display text for a location — synchronous, pure function.
* Does NOT handle async world name lookups (those stay in the component).
* @param {object} L - Parsed location object from parseLocation()
* @param {object} options
* @param {string} [options.hint] - Hint string (e.g. from props)
* @param {string|undefined} [options.worldName] - Cached world name, if available
* @param {string} options.accessTypeLabel - Translated access type label
* @param {Function} options.t - i18n translate function
* @returns {string} Display text for the location
*/
function getLocationText(L, { hint, worldName, accessTypeLabel, t }) {
if (L.isOffline) {
return t('location.offline');
}
if (L.isPrivate) {
return t('location.private');
}
if (L.isTraveling) {
return t('location.traveling');
}
if (typeof hint === 'string' && hint !== '') {
return L.instanceId ? `${hint} · ${accessTypeLabel}` : hint;
}
if (L.worldId) {
const name = worldName || L.worldId;
return L.instanceId ? `${name} · ${accessTypeLabel}` : name;
}
return '';
}
export { getLocationText };

View File

@@ -0,0 +1,120 @@
import { replaceBioSymbols } from './base/string';
/**
* Remove null/undefined keys from a notification JSON object
* and sanitize message/title fields with replaceBioSymbols.
* @param {object} json - notification data (mutated in place)
* @returns {object} the same json reference
*/
export function sanitizeNotificationJson(json) {
for (const key in json) {
if (json[key] === null || typeof json[key] === 'undefined') {
delete json[key];
}
}
if (json.message) {
json.message = replaceBioSymbols(json.message);
}
if (json.title) {
json.title = replaceBioSymbols(json.title);
}
return json;
}
/**
* Parse a notification's details field from string to object if needed.
* @param {*} details - raw details value
* @returns {object} parsed details object
*/
export function parseNotificationDetails(details) {
if (details === Object(details)) {
return details;
}
if (details !== '{}' && typeof details === 'string') {
try {
const object = JSON.parse(details);
if (object === Object(object)) {
return object;
}
} catch (err) {
console.log(err);
}
}
return {};
}
/**
* Build a default V1 notification ref from JSON data.
* Does NOT perform cache lookup — caller is responsible for
* checking existing refs and merging.
* @param {object} json - sanitized notification JSON
* @returns {object} default notification ref
*/
export function createDefaultNotificationRef(json) {
const ref = {
id: '',
senderUserId: '',
senderUsername: '',
type: '',
message: '',
details: {},
seen: false,
created_at: '',
// VRCX
$isExpired: false,
//
...json
};
ref.details = parseNotificationDetails(ref.details);
return ref;
}
/**
* Build a default V2 notification ref from JSON data.
* Handles boop legacy formatting.
* @param {object} json - sanitized notification JSON
* @param {string} endpointDomain - API endpoint domain for emoji URLs
* @returns {object} default notification V2 ref
*/
export function createDefaultNotificationV2Ref(json) {
return {
id: '',
createdAt: '',
updatedAt: '',
expiresAt: '',
type: '',
link: '',
linkText: '',
message: '',
title: '',
imageUrl: '',
seen: false,
senderUserId: '',
senderUsername: '',
data: {},
responses: [],
details: {},
version: 2,
...json
};
}
/**
* Apply legacy boop formatting to a V2 notification ref.
* Mutates the ref in place.
* @param {object} ref - notification V2 ref
* @param {string} endpointDomain - API endpoint domain for emoji URLs
*/
export function applyBoopLegacyHandling(ref, endpointDomain) {
if (ref.type !== 'boop' || !ref.title) {
return;
}
ref.message = ref.title;
ref.title = '';
if (ref.details?.emojiId?.startsWith('default_')) {
ref.imageUrl = ref.details.emojiId;
ref.message += ` ${ref.details.emojiId.replace('default_', '')}`;
} else {
ref.imageUrl = `${endpointDomain}/file/${ref.details.emojiId}/${ref.details.emojiVersion}`;
}
}

View File

@@ -2,6 +2,7 @@ import { useAppearanceSettingsStore, useUserStore } from '../../stores';
import { HueToHex } from './base/ui';
import { convertFileUrlToImageUrl } from './common';
import { languageMappings } from '../constants';
import { removeEmojis } from './base/string';
import { timeToText } from './base/format';
/**
@@ -46,24 +47,6 @@ async function getNameColour(userId) {
return HueToHex(hue);
}
/**
*
* @param {string} text
* @returns
*/
function removeEmojis(text) {
if (!text) {
return '';
}
return text
.replace(
/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g,
''
)
.replace(/\s+/g, ' ')
.trim();
}
/**
*
* @param {object} user

View File

@@ -5,17 +5,19 @@ import { useI18n } from 'vue-i18n';
import {
checkVRChatCache,
createDefaultAvatarRef,
extractFileId,
getAvailablePlatforms,
getBundleDateSize,
getPlatformInfo,
replaceBioSymbols,
sanitizeEntityJson,
storeAvatarImage
} from '../shared/utils';
import { avatarRequest, miscRequest } from '../api';
import { patchAvatarFromEvent } from '../query';
import { AppDebug } from '../service/appConfig';
import { database } from '../service/database';
import { patchAvatarFromEvent } from '../query';
import { processBulk } from '../service/request';
import { useAdvancedSettingsStore } from './settings/advanced';
import { useAvatarProviderStore } from './avatarProvider';
@@ -92,40 +94,10 @@ export const useAvatarStore = defineStore('Avatar', () => {
* @returns {object} ref
*/
function applyAvatar(json) {
json.name = replaceBioSymbols(json.name);
json.description = replaceBioSymbols(json.description);
sanitizeEntityJson(json, ['name', 'description']);
let ref = cachedAvatars.get(json.id);
if (typeof ref === 'undefined') {
ref = {
acknowledgements: '',
authorId: '',
authorName: '',
created_at: '',
description: '',
featured: false,
highestPrice: null,
id: '',
imageUrl: '',
listingDate: null,
lock: false,
lowestPrice: null,
name: '',
pendingUpload: false,
performance: {},
productId: null,
publishedListings: [],
releaseStatus: '',
searchable: false,
styles: [],
tags: [],
thumbnailImageUrl: '',
unityPackageUrl: '',
unityPackageUrlObject: {},
unityPackages: [],
updated_at: '',
version: 0,
...json
};
ref = createDefaultAvatarRef(json);
cachedAvatars.set(ref.id, ref);
} else {
const { unityPackages } = ref;
@@ -178,6 +150,7 @@ export const useAvatarStore = defineStore('Avatar', () => {
/**
*
* @param {string} avatarId
* @param options
* @returns
*/
function showAvatarDialog(avatarId, options = {}) {

View File

@@ -5,6 +5,8 @@ import { useI18n } from 'vue-i18n';
import {
compareByName,
createDefaultFavoriteCachedRef,
createDefaultFavoriteGroupRef,
removeFromArray,
replaceReactiveObject
} from '../shared/utils';
@@ -209,6 +211,11 @@ export const useFavoriteStore = defineStore('Favorite', () => {
return favoriteGroup.length;
});
/**
*
* @param list
* @param selectionRef
*/
function syncFavoriteSelection(list, selectionRef) {
if (!Array.isArray(list)) {
selectionRef.value = [];
@@ -255,6 +262,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
{ flush: 'sync' }
);
/**
*
*/
function getCachedFavoriteGroupsByTypeName() {
const group = {};
@@ -274,10 +284,18 @@ export const useFavoriteStore = defineStore('Favorite', () => {
return group;
}
/**
*
* @param objectId
*/
function getCachedFavoritesByObjectId(objectId) {
return cachedFavoritesByObjectId.get(objectId);
}
/**
*
* @param args
*/
function handleFavoriteAdd(args) {
handleFavorite({
json: args.json,
@@ -310,6 +328,10 @@ export const useFavoriteStore = defineStore('Favorite', () => {
updateFavoriteDialog(args.params.objectId);
}
/**
*
* @param args
*/
function handleFavorite(args) {
args.ref = applyFavoriteCached(args.json);
applyFavorite(args.ref.type, args.ref.favoriteId);
@@ -329,6 +351,10 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
}
/**
*
* @param objectId
*/
function handleFavoriteDelete(objectId) {
const ref = getCachedFavoritesByObjectId(objectId);
if (typeof ref === 'undefined') {
@@ -337,10 +363,18 @@ export const useFavoriteStore = defineStore('Favorite', () => {
handleFavoriteAtDelete(ref);
}
/**
*
* @param args
*/
function handleFavoriteGroup(args) {
args.ref = applyFavoriteGroup(args.json);
}
/**
*
* @param args
*/
function handleFavoriteGroupClear(args) {
const key = `${args.params.type}:${args.params.group}`;
for (const ref of cachedFavorites.values()) {
@@ -351,6 +385,10 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
}
/**
*
* @param args
*/
function handleFavoriteWorldList(args) {
for (const json of args.json) {
if (json.id === '???') {
@@ -360,6 +398,10 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
}
/**
*
* @param args
*/
function handleFavoriteAvatarList(args) {
for (const json of args.json) {
if (json.releaseStatus === 'hidden') {
@@ -369,6 +411,10 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
}
/**
*
* @param ref
*/
function handleFavoriteAtDelete(ref) {
const favorite = state.favoriteObjects.get(ref.favoriteId);
removeFromArray(state.favoriteFriends_, favorite);
@@ -536,6 +582,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
}
/**
*
*/
function refreshFavoriteGroups() {
if (isFavoriteGroupLoading.value) {
return;
@@ -567,6 +616,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
});
}
/**
*
*/
function buildFavoriteGroups() {
let group;
let groups;
@@ -683,6 +735,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
countFavoriteGroups();
}
/**
*
*/
function countFavoriteGroups() {
const cachedFavoriteGroups = getCachedFavoriteGroupsByTypeName();
for (const key in cachedFavoriteGroups) {
@@ -764,17 +819,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
function applyFavoriteGroup(json) {
let ref = cachedFavoriteGroups.value[json.id];
if (typeof ref === 'undefined') {
ref = {
id: '',
ownerId: '',
ownerDisplayName: '',
name: '',
displayName: '',
type: '',
visibility: '',
tags: [],
...json
};
ref = createDefaultFavoriteGroupRef(json);
cachedFavoriteGroups.value[ref.id] = ref;
} else {
Object.assign(ref, json);
@@ -790,19 +835,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
function applyFavoriteCached(json) {
let ref = cachedFavorites.get(json.id);
if (typeof ref === 'undefined') {
ref = {
id: '',
type: '',
favoriteId: '',
tags: [],
// VRCX
$groupKey: '',
//
...json
};
ref = createDefaultFavoriteCachedRef(json);
cachedFavorites.set(ref.id, ref);
cachedFavoritesByObjectId.set(ref.favoriteId, ref);
ref.$groupKey = `${ref.type}:${String(ref.tags[0])}`;
if (
ref.type === 'friend' &&
(!generalSettingsStore.localFavoriteFriendsGroups.some(
@@ -893,14 +928,23 @@ export const useFavoriteStore = defineStore('Favorite', () => {
}
}
/**
*
*/
function showWorldImportDialog() {
worldImportDialogVisible.value = true;
}
/**
*
*/
function showAvatarImportDialog() {
avatarImportDialogVisible.value = true;
}
/**
*
*/
function showFriendImportDialog() {
friendImportDialogVisible.value = true;
}
@@ -1016,6 +1060,10 @@ export const useFavoriteStore = defineStore('Favorite', () => {
return false;
}
/**
*
* @param objectId
*/
function updateFavoriteDialog(objectId) {
const D = favoriteDialog.value;
if (!D.visible || D.objectId !== objectId) {
@@ -1108,6 +1156,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
});
}
/**
*
*/
function sortLocalAvatarFavorites() {
if (!appearanceSettingsStore.sortFavorites) {
for (let i = 0; i < localAvatarFavoriteGroups.value.length; ++i) {
@@ -1294,6 +1345,9 @@ export const useFavoriteStore = defineStore('Favorite', () => {
});
}
/**
*
*/
function sortLocalWorldFavorites() {
if (!appearanceSettingsStore.sortFavorites) {
for (let i = 0; i < localWorldFavoriteGroups.value.length; ++i) {
@@ -1718,6 +1772,11 @@ export const useFavoriteStore = defineStore('Favorite', () => {
});
}
/**
*
* @param type
* @param objectId
*/
function showFavoriteDialog(type, objectId) {
const D = favoriteDialog.value;
D.type = type;
@@ -1726,12 +1785,18 @@ export const useFavoriteStore = defineStore('Favorite', () => {
updateFavoriteDialog(objectId);
}
/**
*
*/
async function saveSortFavoritesOption() {
getLocalWorldFavorites();
getLocalFriendFavorites();
appearanceSettingsStore.setSortFavorites();
}
/**
*
*/
async function initFavorites() {
refreshFavorites();
getLocalWorldFavorites();
@@ -1739,6 +1804,11 @@ export const useFavoriteStore = defineStore('Favorite', () => {
getLocalFriendFavorites();
}
/**
*
* @param a
* @param b
*/
function compareByFavoriteSortOrder(a, b) {
const indexA = favoritesSortOrder.value.indexOf(a.id);
const indexB = favoritesSortOrder.value.indexOf(b.id);

View File

@@ -8,11 +8,17 @@ import dayjs from 'dayjs';
import {
compareGameLogRows,
createJoinLeaveEntry,
createLocationEntry,
createPortalSpawnEntry,
createResourceLoadEntry,
findUserByDisplayName,
formatSeconds,
gameLogSearchFilter,
getGroupName,
parseInventoryFromUrl,
parseLocation,
parsePrintFromUrl,
replaceBioSymbols
} from '../../shared/utils';
import { AppDebug } from '../../service/appConfig';
@@ -131,6 +137,9 @@ export const useGameLogStore = defineStore('GameLog', () => {
{ flush: 'sync' }
);
/**
*
*/
async function init() {
gameLogTable.value.filter = JSON.parse(
await configRepository.getString('VRCX_gameLogTableFilters', '[]')
@@ -143,6 +152,10 @@ export const useGameLogStore = defineStore('GameLog', () => {
init();
/**
*
* @param entry
*/
function insertGameLogSorted(entry) {
const arr = gameLogTableData.value;
if (arr.length === 0) {
@@ -170,6 +183,9 @@ export const useGameLogStore = defineStore('GameLog', () => {
gameLogTableData.value = [...arr, entry];
}
/**
*
*/
function clearNowPlaying() {
nowPlaying.value = {
url: '',
@@ -186,6 +202,10 @@ export const useGameLogStore = defineStore('GameLog', () => {
vrStore.updateVrNowPlaying();
}
/**
*
* @param data
*/
function setNowPlaying(data) {
const ctx = structuredClone(data);
if (nowPlaying.value.url !== ctx.videoUrl) {
@@ -257,6 +277,9 @@ export const useGameLogStore = defineStore('GameLog', () => {
advancedSettingsStore
});
/**
*
*/
function updateNowPlaying() {
const np = nowPlaying.value;
if (!nowPlaying.value.playing) {
@@ -275,6 +298,9 @@ export const useGameLogStore = defineStore('GameLog', () => {
workerTimers.setTimeout(() => updateNowPlaying(), 1000);
}
/**
*
*/
async function tryLoadPlayerList() {
// TODO: make this work again
if (!gameStore.isGameRunning) {
@@ -355,6 +381,10 @@ export const useGameLogStore = defineStore('GameLog', () => {
}
}
/**
*
* @param row
*/
function gameLogIsFriend(row) {
if (typeof row.isFriend !== 'undefined') {
return row.isFriend;
@@ -365,6 +395,10 @@ export const useGameLogStore = defineStore('GameLog', () => {
return friendStore.friends.has(row.userId);
}
/**
*
* @param row
*/
function gameLogIsFavorite(row) {
if (typeof row.isFavorite !== 'undefined') {
return row.isFavorite;
@@ -375,6 +409,9 @@ export const useGameLogStore = defineStore('GameLog', () => {
return friendStore.localFavoriteFriends.has(row.userId);
}
/**
*
*/
async function gameLogTableLookup() {
await configRepository.setString(
'VRCX_gameLogTableFilters',
@@ -416,6 +453,10 @@ export const useGameLogStore = defineStore('GameLog', () => {
}
}
/**
*
* @param entry
*/
function addGameLog(entry) {
entry.isFriend = gameLogIsFriend(entry);
entry.isFavorite = gameLogIsFavorite(entry);
@@ -456,6 +497,10 @@ export const useGameLogStore = defineStore('GameLog', () => {
uiStore.notifyMenu('game-log');
}
/**
*
* @param input
*/
async function addGamelogLocationToDatabase(input) {
const groupName = await getGroupName(input.location);
const entry = {
@@ -465,10 +510,17 @@ export const useGameLogStore = defineStore('GameLog', () => {
database.addGamelogLocationToDatabase(entry);
}
/**
*
* @param row
*/
function gameLogSearch(row) {
return gameLogSearchFilter(row, gameLogTable.value.search);
}
/**
*
*/
function sweepGameLog() {
const j = gameLogTableData.value.length;
if (j > vrcxStore.maxTableSize + 50) {
@@ -476,6 +528,11 @@ export const useGameLogStore = defineStore('GameLog', () => {
}
}
/**
*
* @param gameLog
* @param location
*/
function addGameLogEntry(gameLog, location) {
let entry = undefined;
if (advancedSettingsStore.gameLogDisabled) {
@@ -543,15 +600,12 @@ export const useGameLogStore = defineStore('GameLog', () => {
gameLog.dt
);
const L = parseLocation(gameLog.location);
entry = {
created_at: gameLog.dt,
type: 'Location',
location: gameLog.location,
worldId: L.worldId,
worldName,
groupName: '',
time: 0
};
entry = createLocationEntry(
gameLog.dt,
gameLog.location,
L.worldId,
worldName
);
getGroupName(gameLog.location).then((groupName) => {
entry.groupName = groupName;
});
@@ -595,14 +649,13 @@ export const useGameLogStore = defineStore('GameLog', () => {
}
vrStore.updateVRLastLocation();
instanceStore.getCurrentInstanceUserList();
entry = {
created_at: gameLog.dt,
type: 'OnPlayerJoined',
displayName: gameLog.displayName,
entry = createJoinLeaveEntry(
'OnPlayerJoined',
gameLog.dt,
gameLog.displayName,
location,
userId,
time: 0
};
userId
);
database.addGamelogJoinLeaveToDatabase(entry);
break;
case 'player-left':
@@ -617,29 +670,21 @@ export const useGameLogStore = defineStore('GameLog', () => {
photonStore.photonLobbyAvatars.delete(userId);
vrStore.updateVRLastLocation();
instanceStore.getCurrentInstanceUserList();
entry = {
created_at: gameLog.dt,
type: 'OnPlayerLeft',
displayName: gameLog.displayName,
entry = createJoinLeaveEntry(
'OnPlayerLeft',
gameLog.dt,
gameLog.displayName,
location,
userId,
time
};
);
database.addGamelogJoinLeaveToDatabase(entry);
break;
case 'portal-spawn':
if (vrcxStore.ipcEnabled && gameStore.isGameRunning) {
break;
}
entry = {
created_at: gameLog.dt,
type: 'PortalSpawn',
location,
displayName: '',
userId: '',
instanceId: '',
worldName: ''
};
entry = createPortalSpawnEntry(gameLog.dt, location);
database.addGamelogPortalSpawnToDatabase(entry);
break;
case 'video-play':
@@ -665,15 +710,12 @@ export const useGameLogStore = defineStore('GameLog', () => {
break;
}
lastResourceloadUrl.value = gameLog.resourceUrl;
entry = {
created_at: gameLog.dt,
type:
gameLog.type === 'resource-load-string'
? 'StringLoad'
: 'ImageLoad',
resourceUrl: gameLog.resourceUrl,
entry = createResourceLoadEntry(
gameLog.type,
gameLog.dt,
gameLog.resourceUrl,
location
};
);
database.addGamelogResourceLoadToDatabase(entry);
break;
case 'screenshot':
@@ -711,42 +753,18 @@ export const useGameLogStore = defineStore('GameLog', () => {
// }
if (advancedSettingsStore.saveInstanceEmoji) {
try {
// https://api.vrchat.cloud/api/1/user/usr_032383a7-748c-4fb2-94e4-bcb928e5de6b/inventory/inv_75781d65-92fe-4a80-a1ff-27ee6e843b08
const url = new URL(gameLog.url);
if (
url.pathname.substring(0, 12) === '/api/1/user/' &&
url.pathname.includes('/inventory/inv_')
) {
const pathArray = url.pathname.split('/');
const userId = pathArray[4];
const inventoryId = pathArray[6];
if (userId && inventoryId.length === 40) {
galleryStore.queueCheckInstanceInventory(
inventoryId,
userId
);
}
}
} catch (err) {
console.error(err);
const inv = parseInventoryFromUrl(gameLog.url);
if (inv) {
galleryStore.queueCheckInstanceInventory(
inv.inventoryId,
inv.userId
);
}
}
if (advancedSettingsStore.saveInstancePrints) {
try {
let printId = '';
const url1 = new URL(gameLog.url);
if (
url1.pathname.substring(0, 14) === '/api/1/prints/'
) {
const pathArray = url1.pathname.split('/');
printId = pathArray[4];
}
if (printId && printId.length === 41) {
galleryStore.queueSavePrintToFile(printId);
}
} catch (err) {
console.error(err);
const printId = parsePrintFromUrl(gameLog.url);
if (printId) {
galleryStore.queueSavePrintToFile(printId);
}
}
break;
@@ -902,12 +920,19 @@ export const useGameLogStore = defineStore('GameLog', () => {
}
}
/**
*
*/
async function getGameLogTable() {
await database.initTables();
const dateTill = await database.getLastDateGameLogDatabase();
updateGameLog(dateTill);
}
/**
*
* @param dateTill
*/
async function updateGameLog(dateTill) {
await gameLogService.setDateTill(dateTill);
await new Promise((resolve) => {
@@ -923,6 +948,10 @@ export const useGameLogStore = defineStore('GameLog', () => {
}
// use in C#
/**
*
* @param json
*/
function addGameLogEvent(json) {
const rawLogs = JSON.parse(json);
const gameLog = gameLogService.parseRawGameLog(
@@ -941,6 +970,9 @@ export const useGameLogStore = defineStore('GameLog', () => {
addGameLogEntry(gameLog, locationStore.lastLocation.location);
}
/**
*
*/
async function disableGameLogDialog() {
if (gameStore.isGameRunning) {
toast.error(t('message.gamelog.vrchat_must_be_closed'));
@@ -962,6 +994,9 @@ export const useGameLogStore = defineStore('GameLog', () => {
}
}
/**
*
*/
async function initGameLogTable() {
gameLogTable.value.loading = true;
const rows = await database.lookupGameLogDatabase(

View File

@@ -3,20 +3,22 @@ import { defineStore } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';
import {
convertFileUrlToImageUrl,
createDefaultGroupRef,
hasGroupPermission,
replaceBioSymbols,
sanitizeEntityJson
} from '../shared/utils';
import {
groupRequest,
instanceRequest,
userRequest,
worldRequest
} from '../api';
import { patchGroupFromEvent } from '../query';
import {
convertFileUrlToImageUrl,
hasGroupPermission,
replaceBioSymbols
} from '../shared/utils';
import { database } from '../service/database';
import { groupDialogFilterOptions } from '../shared/constants/';
import { patchGroupFromEvent } from '../query';
import { useGameStore } from './game';
import { useInstanceStore } from './instance';
import { useModalStore } from './modal';
@@ -128,6 +130,11 @@ export const useGroupStore = defineStore('Group', () => {
{ flush: 'sync' }
);
/**
*
* @param groupId
* @param options
*/
function showGroupDialog(groupId, options = {}) {
if (!groupId) {
return;
@@ -232,6 +239,11 @@ export const useGroupStore = defineStore('Group', () => {
);
}
/**
*
* @param ref
* @param message
*/
function groupChange(ref, message) {
if (!currentUserGroupsInit.value) {
return;
@@ -260,6 +272,9 @@ export const useGroupStore = defineStore('Group', () => {
workerTimers.setTimeout(saveCurrentUserGroups, 100);
}
/**
*
*/
function saveCurrentUserGroups() {
if (!currentUserGroupsInit.value) {
return;
@@ -284,10 +299,10 @@ export const useGroupStore = defineStore('Group', () => {
/**
*
* @param {object} ref
* @param {array} oldRoles
* @param {array} newRoles
* @param {array} oldRoleIds
* @param {array} newRoleIds
* @param {Array} oldRoles
* @param {Array} newRoles
* @param {Array} oldRoleIds
* @param {Array} newRoleIds
*/
function groupRoleChange(ref, oldRoles, newRoles, oldRoleIds, newRoleIds) {
// check for removed/added roleIds
@@ -401,7 +416,7 @@ export const useGroupStore = defineStore('Group', () => {
/**
*
* @param {{ groupId: string }} params
* @return { Promise<{posts: any, params}> }
* @returns { Promise<{posts: any, params}> }
*/
async function getAllGroupPosts(params) {
const n = 100;
@@ -442,6 +457,10 @@ export const useGroupStore = defineStore('Group', () => {
return returnArgs;
}
/**
*
* @param groupId
*/
function getGroupDialogGroup(groupId) {
const D = groupDialog.value;
D.isGetGroupDialogGroupLoading = false;
@@ -497,32 +516,38 @@ export const useGroupStore = defineStore('Group', () => {
});
}
});
groupRequest.getCachedGroupCalendar(groupId).then((args) => {
if (groupDialog.value.id === args.params.groupId) {
D.calendar = args.json.results;
for (const event of D.calendar) {
applyGroupEvent(event);
// fetch again for isFollowing
groupRequest
.getCachedGroupCalendarEvent({
groupId,
eventId: event.id
})
.then((args) => {
Object.assign(
event,
applyGroupEvent(args.json)
);
});
groupRequest
.getCachedGroupCalendar(groupId)
.then((args) => {
if (groupDialog.value.id === args.params.groupId) {
D.calendar = args.json.results;
for (const event of D.calendar) {
applyGroupEvent(event);
// fetch again for isFollowing
groupRequest
.getCachedGroupCalendarEvent({
groupId,
eventId: event.id
})
.then((args) => {
Object.assign(
event,
applyGroupEvent(args.json)
);
});
}
}
}
});
});
}
nextTick(() => (D.isGetGroupDialogGroupLoading = false));
return args;
});
}
/**
*
* @param event
*/
function applyGroupEvent(event) {
return {
userInterest: {
@@ -536,6 +561,9 @@ export const useGroupStore = defineStore('Group', () => {
};
}
/**
*
*/
async function updateInGameGroupOrder() {
inGameGroupOrder.value = [];
try {
@@ -551,6 +579,11 @@ export const useGroupStore = defineStore('Group', () => {
}
}
/**
*
* @param a
* @param b
*/
function sortGroupInstancesByInGame(a, b) {
const aIndex = inGameGroupOrder.value.indexOf(a?.group?.id);
const bIndex = inGameGroupOrder.value.indexOf(b?.group?.id);
@@ -566,6 +599,10 @@ export const useGroupStore = defineStore('Group', () => {
return aIndex - bIndex;
}
/**
*
* @param groupId
*/
function leaveGroup(groupId) {
groupRequest
.leaveGroup({
@@ -590,6 +627,10 @@ export const useGroupStore = defineStore('Group', () => {
});
}
/**
*
* @param groupId
*/
function leaveGroupPrompt(groupId) {
modalStore
.confirm({
@@ -603,6 +644,9 @@ export const useGroupStore = defineStore('Group', () => {
.catch(() => {});
}
/**
*
*/
function updateGroupPostSearch() {
const D = groupDialog.value;
const search = D.postsSearch.toLowerCase();
@@ -620,6 +664,11 @@ export const useGroupStore = defineStore('Group', () => {
});
}
/**
*
* @param groupId
* @param visibility
*/
function setGroupVisibility(groupId, visibility) {
return groupRequest
.setGroupMemberProps(userStore.currentUser.id, groupId, {
@@ -632,6 +681,11 @@ export const useGroupStore = defineStore('Group', () => {
});
}
/**
*
* @param groupId
* @param subscribe
*/
function setGroupSubscription(groupId, subscribe) {
return groupRequest
.setGroupMemberProps(userStore.currentUser.id, groupId, {
@@ -651,73 +705,9 @@ export const useGroupStore = defineStore('Group', () => {
*/
function applyGroup(json) {
let ref = cachedGroups.get(json.id);
if (json.rules) {
json.rules = replaceBioSymbols(json.rules);
}
if (json.name) {
json.name = replaceBioSymbols(json.name);
}
if (json.description) {
json.description = replaceBioSymbols(json.description);
}
sanitizeEntityJson(json, ['rules', 'name', 'description']);
if (typeof ref === 'undefined') {
ref = {
id: '',
name: '',
shortCode: '',
description: '',
bannerId: '',
bannerUrl: '',
createdAt: '',
discriminator: '',
galleries: [],
iconId: '',
iconUrl: '',
isVerified: false,
joinState: '',
languages: [],
links: [],
memberCount: 0,
memberCountSyncedAt: '',
membershipStatus: '',
onlineMemberCount: 0,
ownerId: '',
privacy: '',
rules: null,
tags: [],
// in group
initialRoleIds: [],
myMember: {
bannedAt: null,
groupId: '',
has2FA: false,
id: '',
isRepresenting: false,
isSubscribedToAnnouncements: false,
joinedAt: '',
managerNotes: '',
membershipStatus: '',
permissions: [],
roleIds: [],
userId: '',
visibility: '',
_created_at: '',
_id: '',
_updated_at: ''
},
updatedAt: '',
// includeRoles: true
roles: [],
// group list
$memberId: '',
groupId: '',
isRepresenting: false,
memberVisibility: false,
mutualGroup: false,
// VRCX
$languages: [],
...json
};
ref = createDefaultGroupRef(json);
cachedGroups.set(ref.id, ref);
} else {
if (currentUserGroups.has(ref.id)) {
@@ -796,6 +786,10 @@ export const useGroupStore = defineStore('Group', () => {
return ref;
}
/**
*
* @param args
*/
function handleGroupRepresented(args) {
const D = userStore.userDialog;
const json = args.json;
@@ -819,6 +813,10 @@ export const useGroupStore = defineStore('Group', () => {
applyGroup(json);
}
/**
*
* @param args
*/
function handleGroupList(args) {
for (const json of args.json) {
json.$memberId = json.id;
@@ -827,6 +825,10 @@ export const useGroupStore = defineStore('Group', () => {
}
}
/**
*
* @param args
*/
function handleGroupMemberProps(args) {
if (args.userId === userStore.currentUser.id) {
const json = args.json;
@@ -873,6 +875,10 @@ export const useGroupStore = defineStore('Group', () => {
}
}
/**
*
* @param args
*/
function handleGroupPermissions(args) {
if (args.params.userId !== userStore.currentUser.id) {
return;
@@ -919,10 +925,18 @@ export const useGroupStore = defineStore('Group', () => {
updateGroupPostSearch();
}
/**
*
* @param args
*/
function handleGroupMember(args) {
args.ref = applyGroupMember(args.json);
}
/**
*
* @param args
*/
async function handleGroupUserInstances(args) {
groupInstances.value = [];
for (const json of args.json.instances) {
@@ -990,6 +1004,10 @@ export const useGroupStore = defineStore('Group', () => {
return json;
}
/**
*
* @param ref
*/
function applyGroupLanguage(ref) {
ref.$languages = [];
const { languages } = ref;
@@ -1008,6 +1026,11 @@ export const useGroupStore = defineStore('Group', () => {
}
}
/**
*
* @param userId
* @param groups
*/
async function loadCurrentUserGroups(userId, groups) {
const savedGroups = JSON.parse(
await configRepository.getString(
@@ -1063,6 +1086,9 @@ export const useGroupStore = defineStore('Group', () => {
getCurrentUserGroups();
}
/**
*
*/
async function getCurrentUserGroups() {
const args = await groupRequest.getGroups({
userId: userStore.currentUser.id
@@ -1082,6 +1108,9 @@ export const useGroupStore = defineStore('Group', () => {
saveCurrentUserGroups();
}
/**
*
*/
function getCurrentUserRepresentedGroup() {
return groupRequest
.getRepresentedGroup({
@@ -1093,6 +1122,9 @@ export const useGroupStore = defineStore('Group', () => {
});
}
/**
*
*/
async function initUserGroups() {
updateInGameGroupOrder();
loadCurrentUserGroups(
@@ -1101,6 +1133,10 @@ export const useGroupStore = defineStore('Group', () => {
);
}
/**
*
* @param userId
*/
function showModerateGroupDialog(userId) {
const D = moderateGroupDialog.value;
D.userId = userId;
@@ -1108,6 +1144,11 @@ export const useGroupStore = defineStore('Group', () => {
D.visible = true;
}
/**
*
* @param groupId
* @param userId
*/
function showGroupMemberModerationDialog(groupId, userId = '') {
const D = groupMemberModeration.value;
D.id = groupId;

View File

@@ -8,8 +8,11 @@ import {
compareByDisplayName,
compareById,
compareByLocationAt,
computeDisabledContentSettings,
createDefaultInstanceRef,
debounce,
displayLocation,
evictMapCache,
getAvailablePlatforms,
getBundleDateSize,
getGroupName,
@@ -25,12 +28,12 @@ import {
userRequest,
worldRequest
} from '../api';
import { patchInstanceFromEvent } from '../query';
import {
accessTypeLocaleKeyMap,
instanceContentSettings
} from '../shared/constants';
import { database } from '../service/database';
import { patchInstanceFromEvent } from '../query';
import { resolveRef } from '../shared/utils/resolveRef';
import { useAppearanceSettingsStore } from './settings/appearance';
import { useFriendStore } from './friend';
@@ -66,30 +69,26 @@ export const useInstanceStore = defineStore('Instance', () => {
let cachedInstances = new Map();
/**
*
*/
function cleanInstanceCache() {
const maxSize = 200;
if (cachedInstances.size <= maxSize) {
return;
}
const removable = [];
cachedInstances.forEach((ref, id) => {
if (
[...friendStore.friends.values()].some(
(f) => f.$location?.tag === id
)
) {
return;
const friendLocationTags = new Set(
[...friendStore.friends.values()]
.map((f) => f.$location?.tag)
.filter(Boolean)
);
evictMapCache(
cachedInstances,
200,
(_value, key) => friendLocationTags.has(key),
{
sortFn: (a, b) =>
(Date.parse(a.value.$fetchedAt) || 0) -
(Date.parse(b.value.$fetchedAt) || 0),
logLabel: 'Instance cache cleanup'
}
removable.push({
id,
fetchedAt: Date.parse(ref.$fetchedAt) || 0
});
});
removable.sort((a, b) => a.fetchedAt - b.fetchedAt);
const overBy = cachedInstances.size - maxSize;
for (let i = 0; i < overBy && i < removable.length; i++) {
cachedInstances.delete(removable[i].id);
}
);
}
const lastInstanceApplied = ref('');
@@ -183,6 +182,9 @@ export const useInstanceStore = defineStore('Instance', () => {
{ flush: 'sync' }
);
/**
*
*/
async function getInstanceJoinHistory() {
try {
const data = await database.getInstanceJoinHistory();
@@ -195,6 +197,11 @@ export const useInstanceStore = defineStore('Instance', () => {
}
}
/**
*
* @param location
* @param dateTime
*/
function addInstanceJoinHistory(location, dateTime) {
if (!location || !dateTime) {
return;
@@ -208,11 +215,18 @@ export const useInstanceStore = defineStore('Instance', () => {
instanceJoinHistory.set(location, epoch);
}
/**
*
*/
function hidePreviousInstancesDialogs() {
previousInstancesInfoDialog.value.visible = false;
previousInstancesListDialog.value.visible = false;
}
/**
*
* @param input
*/
function resolveUserRef(input) {
return resolveRef(input, {
emptyDefault: { id: '', displayName: '' },
@@ -222,6 +236,10 @@ export const useInstanceStore = defineStore('Instance', () => {
});
}
/**
*
* @param input
*/
function resolveWorldRef(input) {
return resolveRef(input, {
emptyDefault: { id: '', name: '' },
@@ -231,6 +249,10 @@ export const useInstanceStore = defineStore('Instance', () => {
});
}
/**
*
* @param input
*/
function resolveGroupRef(input) {
return resolveRef(input, {
emptyDefault: { id: '', name: '' },
@@ -240,6 +262,10 @@ export const useInstanceStore = defineStore('Instance', () => {
});
}
/**
*
* @param accessTypeNameRaw
*/
function translateAccessType(accessTypeNameRaw) {
const key = accessTypeLocaleKeyMap[accessTypeNameRaw];
if (!key) {
@@ -255,6 +281,11 @@ export const useInstanceStore = defineStore('Instance', () => {
return t(key);
}
/**
*
* @param instanceId
* @param worldNameOverride
*/
function formatPreviousInstancesInfoLabel(
instanceId,
worldNameOverride = ''
@@ -275,6 +306,10 @@ export const useInstanceStore = defineStore('Instance', () => {
return `${baseLabel} · ${accessTypeLabel}`;
}
/**
*
* @param instanceId
*/
function showPreviousInstancesInfoDialog(instanceId) {
previousInstancesInfoDialog.value.visible = true;
previousInstancesInfoDialog.value.instanceId = instanceId;
@@ -308,6 +343,11 @@ export const useInstanceStore = defineStore('Instance', () => {
}
}
/**
*
* @param variant
* @param targetRef
*/
async function showPreviousInstancesListDialog(variant, targetRef) {
previousInstancesListDialog.value.variant = variant;
let resolved = null;
@@ -335,6 +375,9 @@ export const useInstanceStore = defineStore('Instance', () => {
});
}
/**
*
*/
function updateCurrentInstanceWorld() {
let L;
let instanceId = locationStore.lastLocation.location;
@@ -472,53 +515,7 @@ export const useInstanceStore = defineStore('Instance', () => {
}
let ref = cachedInstances.get(json.id);
if (typeof ref === 'undefined') {
ref = {
id: '',
location: '',
instanceId: '',
name: '',
worldId: '',
type: '',
ownerId: '',
tags: [],
active: false,
full: false,
n_users: 0,
hasCapacityForYou: true, // not present depending on endpoint
capacity: 0,
recommendedCapacity: 0,
userCount: 0,
queueEnabled: false, // only present with group instance type
queueSize: 0, // only present when queuing is enabled
platforms: {},
gameServerVersion: 0,
hardClose: null, // boolean or null
closedAt: null, // string or null
secureName: '',
shortName: '',
world: {},
users: [], // only present when you're the owner
clientNumber: '',
contentSettings: {},
photonRegion: '',
region: '',
canRequestInvite: false,
permanent: false,
private: '', // part of instance tag
hidden: '', // part of instance tag
nonce: '', // only present when you're the owner
strict: false, // deprecated
displayName: null,
groupAccessType: null, // only present with group instance type
roleRestricted: false, // only present with group instance type
instancePersistenceEnabled: null,
playerPersistenceEnabled: null,
ageGate: null,
// VRCX
$fetchedAt: '',
$disabledContentSettings: [],
...json
};
ref = createDefaultInstanceRef(json);
cachedInstances.set(ref.id, ref);
cleanInstanceCache();
} else {
@@ -535,18 +532,10 @@ export const useInstanceStore = defineStore('Instance', () => {
return args;
});
}
ref.$disabledContentSettings = [];
if (json.contentSettings && Object.keys(json.contentSettings).length) {
for (const setting of instanceContentSettings) {
if (
typeof json.contentSettings[setting] === 'undefined' ||
json.contentSettings[setting] === true
) {
continue;
}
ref.$disabledContentSettings.push(setting);
}
}
ref.$disabledContentSettings = computeDisabledContentSettings(
json.contentSettings,
instanceContentSettings
);
if (ref.displayName) {
ref.displayName = replaceBioSymbols(ref.displayName);
}
@@ -578,6 +567,10 @@ export const useInstanceStore = defineStore('Instance', () => {
return ref;
}
/**
*
* @param location
*/
async function getInstanceName(location) {
let instanceName = '';
@@ -701,10 +694,16 @@ export const useInstanceStore = defineStore('Instance', () => {
}
}
/**
*
*/
function applyWorldDialogInstances() {
debounce(applyWorldDialogInstancesDebounced, 100)();
}
/**
*
*/
function applyWorldDialogInstancesDebounced() {
let ref;
let instance;
@@ -1079,6 +1078,9 @@ export const useInstanceStore = defineStore('Instance', () => {
D.instances = rooms;
}
/**
*
*/
function removeAllQueuedInstances() {
queuedInstances.forEach((ref) => {
toast.info(`Removed instance ${ref.$worldName} from queue`);
@@ -1233,6 +1235,9 @@ export const useInstanceStore = defineStore('Instance', () => {
// workerTimers.setTimeout(this.instanceQueueTimeout, 3600000);
}
/**
*
*/
function getCurrentInstanceUserList() {
if (!watchState.isFriendsLoaded) {
return;
@@ -1250,6 +1255,9 @@ export const useInstanceStore = defineStore('Instance', () => {
}
}
/**
*
*/
function updatePlayerListExecute() {
try {
updatePlayerListDebounce();
@@ -1260,6 +1268,9 @@ export const useInstanceStore = defineStore('Instance', () => {
state.updatePlayerListPending = false;
}
/**
*
*/
function updatePlayerListDebounce() {
const users = [];
const pushUser = function (ref) {

View File

@@ -7,14 +7,18 @@ import Noty from 'noty';
import dayjs from 'dayjs';
import {
applyBoopLegacyHandling,
checkCanInvite,
createDefaultNotificationRef,
createDefaultNotificationV2Ref,
escapeTag,
executeWithBackoff,
findUserByDisplayName,
getUserMemo,
parseLocation,
parseNotificationDetails,
removeFromArray,
replaceBioSymbols
sanitizeNotificationJson
} from '../../shared/utils';
import {
friendRequest,
@@ -163,6 +167,9 @@ export const useNotificationStore = defineStore('Notification', () => {
{ flush: 'sync' }
);
/**
*
*/
async function init() {
notificationTable.value.filters[0].value = JSON.parse(
await configRepository.getString(
@@ -174,6 +181,10 @@ export const useNotificationStore = defineStore('Notification', () => {
init();
/**
*
* @param args
*/
function handleNotification(args) {
args.ref = applyNotification(args.json);
const { ref } = args;
@@ -234,6 +245,10 @@ export const useNotificationStore = defineStore('Notification', () => {
D.incomingRequest = true;
}
/**
*
* @param notificationId
*/
function handleNotificationHide(notificationId) {
const ref = notificationTable.value.data.find(
(n) => n.id === notificationId
@@ -259,6 +274,10 @@ export const useNotificationStore = defineStore('Notification', () => {
});
}
/**
*
* @param args
*/
function handlePipelineNotification(args) {
const ref = args.json;
if (
@@ -373,6 +392,10 @@ export const useNotificationStore = defineStore('Notification', () => {
});
}
/**
*
* @param notificationId
*/
function handleNotificationSee(notificationId) {
removeFromArray(unseenNotifications.value, notificationId);
if (unseenNotifications.value.length === 0) {
@@ -392,6 +415,9 @@ export const useNotificationStore = defineStore('Notification', () => {
let seeProcessing = false;
const SEE_CONCURRENCY = 2;
/**
*
*/
async function processSeeQueue() {
if (seeProcessing) return;
seeProcessing = true;
@@ -443,7 +469,7 @@ export const useNotificationStore = defineStore('Notification', () => {
/**
* Queue a notification to be marked as seen.
* @param {string} notificationId
* @param {number} [version=1]
* @param {number} [version]
*/
function queueMarkAsSeen(notificationId, version = 1) {
if (seenIds.has(notificationId)) return;
@@ -452,6 +478,10 @@ export const useNotificationStore = defineStore('Notification', () => {
processSeeQueue();
}
/**
*
* @param args
*/
function handleNotificationAccept(args) {
let ref;
const array = notificationTable.value.data;
@@ -490,6 +520,10 @@ export const useNotificationStore = defineStore('Notification', () => {
D.isFriend = true;
}
/**
*
* @param args
*/
function handleNotificationExpire(args) {
const { ref } = args;
const D = userStore.userDialog;
@@ -509,10 +543,7 @@ export const useNotificationStore = defineStore('Notification', () => {
* @returns {object}
*/
function applyNotification(data) {
const json = { ...data };
if (json.message) {
json.message = replaceBioSymbols(json.message);
}
const json = sanitizeNotificationJson({ ...data });
let ref;
const array = notificationTable.value.data;
for (let i = array.length - 1; i >= 0; i--) {
@@ -521,102 +552,37 @@ export const useNotificationStore = defineStore('Notification', () => {
break;
}
}
// delete any null in json
for (const key in json) {
if (json[key] === null) {
delete json[key];
}
}
if (typeof ref === 'undefined') {
ref = {
id: '',
senderUserId: '',
senderUsername: '',
type: '',
message: '',
details: {},
seen: false,
created_at: '',
// VRCX
$isExpired: false,
//
...json
};
ref = createDefaultNotificationRef(json);
} else {
Object.assign(ref, json);
ref.$isExpired = false;
}
if (ref.details !== Object(ref.details)) {
let details = {};
if (ref.details !== '{}') {
try {
const object = JSON.parse(ref.details);
if (object === Object(object)) {
details = object;
}
} catch (err) {
console.log(err);
}
}
ref.details = details;
}
ref.details = parseNotificationDetails(ref.details);
return ref;
}
/**
*
* @param data
*/
function applyNotificationV2(data) {
const json = { ...data };
// delete any null in json
for (const key in json) {
if (json[key] === null || typeof json[key] === 'undefined') {
delete json[key];
}
}
if (json.message) {
json.message = replaceBioSymbols(json.message);
}
if (json.title) {
json.title = replaceBioSymbols(json.title);
}
const json = sanitizeNotificationJson({ ...data });
let ref = notificationTable.value.data.find((n) => n.id === json.id);
if (typeof ref === 'undefined') {
ref = {
id: '',
createdAt: '',
updatedAt: '',
expiresAt: '',
type: '',
link: '',
linkText: '',
message: '',
title: '',
imageUrl: '',
seen: false,
senderUserId: '',
senderUsername: '',
data: {},
responses: [],
details: {},
version: 2,
...json
};
ref = createDefaultNotificationV2Ref(json);
} else {
Object.assign(ref, json);
}
ref.created_at = ref.createdAt; // for table
// legacy handling of boops
if (ref.type === 'boop' && ref.title) {
ref.message = ref.title;
ref.title = '';
if (ref.details?.emojiId?.startsWith('default_')) {
ref.imageUrl = ref.details.emojiId;
ref.message += ` ${ref.details.emojiId.replace('default_', '')}`;
} else {
ref.imageUrl = `${AppDebug.endpointDomain}/file/${ref.details.emojiId}/${ref.details.emojiVersion}`;
}
}
applyBoopLegacyHandling(ref, AppDebug.endpointDomain);
return ref;
}
/**
*
* @param args
*/
function handleNotificationV2(args) {
const ref = applyNotificationV2(args.json);
if (ref.seen) {
@@ -645,6 +611,10 @@ export const useNotificationStore = defineStore('Notification', () => {
sharedFeedStore.addEntry(ref);
}
/**
*
* @param args
*/
function handleNotificationV2Update(args) {
const notificationId = args.params.notificationId;
const json = { ...args.json };
@@ -663,6 +633,10 @@ export const useNotificationStore = defineStore('Notification', () => {
}
}
/**
*
* @param notificationId
*/
function handleNotificationV2Hide(notificationId) {
database.expireNotificationV2(notificationId);
const ref = notificationTable.value.data.find(
@@ -674,6 +648,9 @@ export const useNotificationStore = defineStore('Notification', () => {
}
}
/**
*
*/
function expireFriendRequestNotifications() {
const array = notificationTable.value.data;
for (let i = array.length - 1; i >= 0; i--) {
@@ -813,6 +790,10 @@ export const useNotificationStore = defineStore('Notification', () => {
}
}
/**
*
* @param noty
*/
function playNoty(noty) {
if (
userStore.currentUser.status === 'busy' ||
@@ -1014,6 +995,10 @@ export const useNotificationStore = defineStore('Notification', () => {
return '';
}
/**
*
* @param gamelog
*/
function queueGameLogNoty(gamelog) {
const noty = structuredClone(gamelog);
let bias;
@@ -1090,6 +1075,10 @@ export const useNotificationStore = defineStore('Notification', () => {
}
}
/**
*
* @param feed
*/
function queueFeedNoty(feed) {
const noty = { ...feed };
if (noty.type === 'Avatar') {
@@ -1116,6 +1105,10 @@ export const useNotificationStore = defineStore('Notification', () => {
}
}
/**
*
* @param noty
*/
function queueFriendLogNoty(noty) {
if (noty.type === 'FriendRequest') {
return;
@@ -1133,6 +1126,10 @@ export const useNotificationStore = defineStore('Notification', () => {
}
}
/**
*
* @param noty
*/
function queueModerationNoty(noty) {
noty.isFriend = false;
noty.isFavorite = false;
@@ -1146,6 +1143,9 @@ export const useNotificationStore = defineStore('Notification', () => {
}
}
/**
*
*/
async function initNotifications() {
notificationInitStatus.value = false;
let tableData = await database.getNotificationsV2();
@@ -1161,6 +1161,9 @@ export const useNotificationStore = defineStore('Notification', () => {
refreshNotifications();
}
/**
*
*/
function testNotification() {
playNoty({
type: 'Event',
@@ -1169,6 +1172,10 @@ export const useNotificationStore = defineStore('Notification', () => {
});
}
/**
*
* @param row
*/
function acceptFriendRequestNotification(row) {
modalStore
.confirm({
@@ -1193,6 +1200,10 @@ export const useNotificationStore = defineStore('Notification', () => {
.catch(() => {});
}
/**
*
* @param row
*/
async function hideNotification(row) {
if (row.type === 'ignoredFriendRequest') {
await friendRequest.deleteHiddenFriendRequest(
@@ -1211,6 +1222,10 @@ export const useNotificationStore = defineStore('Notification', () => {
}
}
/**
*
* @param row
*/
function hideNotificationPrompt(row) {
modalStore
.confirm({
@@ -1223,6 +1238,10 @@ export const useNotificationStore = defineStore('Notification', () => {
.catch(() => {});
}
/**
*
* @param row
*/
function acceptRequestInvite(row) {
modalStore
.confirm({
@@ -1268,6 +1287,12 @@ export const useNotificationStore = defineStore('Notification', () => {
.catch(() => {});
}
/**
*
* @param notificationId
* @param responses
* @param responseType
*/
function sendNotificationResponse(notificationId, responses, responseType) {
if (!Array.isArray(responses) || responses.length === 0) return;
let responseData = '';
@@ -1295,6 +1320,10 @@ export const useNotificationStore = defineStore('Notification', () => {
});
}
/**
*
* @param row
*/
function deleteNotificationLog(row) {
const idx = notificationTable.value.data.findIndex(
(e) => e.id === row.id
@@ -1314,6 +1343,10 @@ export const useNotificationStore = defineStore('Notification', () => {
}
}
/**
*
* @param row
*/
function deleteNotificationLogPrompt(row) {
modalStore
.confirm({
@@ -1327,6 +1360,10 @@ export const useNotificationStore = defineStore('Notification', () => {
.catch(() => {});
}
/**
*
* @param notification
*/
function isNotificationExpired(notification) {
if (notification.$isExpired !== undefined) {
return notification.$isExpired;
@@ -1338,6 +1375,10 @@ export const useNotificationStore = defineStore('Notification', () => {
return expiresAt.isValid() && dayjs().isSameOrAfter(expiresAt);
}
/**
*
* @param link
*/
function openNotificationLink(link) {
if (!link) {
return;

View File

@@ -20,8 +20,8 @@ import {
getThemeMode,
updateTrustColorClasses
} from '../../shared/utils/base/ui';
import { computeTrustLevel, getNameColour } from '../../shared/utils';
import { database } from '../../service/database';
import { getNameColour } from '../../shared/utils';
import { languageCodes } from '../../localization';
import { loadLocalizedStrings } from '../../plugin';
import { useFeedStore } from '../feed';
@@ -127,6 +127,9 @@ export const useAppearanceSettingsStore = defineStore(
: fallback;
};
/**
*
*/
async function initAppearanceSettings() {
const { initThemeMode, isDarkMode: initDarkMode } =
await getThemeMode(configRepository);
@@ -410,6 +413,10 @@ export const useAppearanceSettingsStore = defineStore(
updateTrustColorClasses(trustColor.value);
}
/**
*
* @param customFunc
*/
async function userColourInit(customFunc) {
let dictObject = null;
if (typeof customFunc === 'function') {
@@ -440,55 +447,13 @@ export const useAppearanceSettingsStore = defineStore(
* @param {object} ref
*/
function applyUserTrustLevel(ref) {
ref.$isModerator =
ref.developerType && ref.developerType !== 'none';
ref.$isTroll = false;
ref.$isProbableTroll = false;
let trustColorTemp = '';
const { tags } = ref;
if (tags.includes('admin_moderator')) {
ref.$isModerator = true;
}
if (tags.includes('system_troll')) {
ref.$isTroll = true;
}
if (tags.includes('system_probable_troll') && !ref.$isTroll) {
ref.$isProbableTroll = true;
}
if (tags.includes('system_trust_veteran')) {
ref.$trustLevel = 'Trusted User';
ref.$trustClass = 'x-tag-veteran';
trustColorTemp = 'veteran';
ref.$trustSortNum = 5;
} else if (tags.includes('system_trust_trusted')) {
ref.$trustLevel = 'Known User';
ref.$trustClass = 'x-tag-trusted';
trustColorTemp = 'trusted';
ref.$trustSortNum = 4;
} else if (tags.includes('system_trust_known')) {
ref.$trustLevel = 'User';
ref.$trustClass = 'x-tag-known';
trustColorTemp = 'known';
ref.$trustSortNum = 3;
} else if (tags.includes('system_trust_basic')) {
ref.$trustLevel = 'New User';
ref.$trustClass = 'x-tag-basic';
trustColorTemp = 'basic';
ref.$trustSortNum = 2;
} else {
ref.$trustLevel = 'Visitor';
ref.$trustClass = 'x-tag-untrusted';
trustColorTemp = 'untrusted';
ref.$trustSortNum = 1;
}
if (ref.$isTroll || ref.$isProbableTroll) {
trustColorTemp = 'troll';
ref.$trustSortNum += 0.1;
}
if (ref.$isModerator) {
trustColorTemp = 'vip';
ref.$trustSortNum += 0.3;
}
const trust = computeTrustLevel(ref.tags, ref.developerType);
ref.$isModerator = trust.isModerator;
ref.$isTroll = trust.isTroll;
ref.$isProbableTroll = trust.isProbableTroll;
ref.$trustLevel = trust.trustLevel;
ref.$trustClass = trust.trustClass;
ref.$trustSortNum = trust.trustSortNum;
if (randomUserColours.value && watchState.isFriendsLoaded) {
if (!ref.$userColour) {
getNameColour(ref.id).then((colour) => {
@@ -496,7 +461,7 @@ export const useAppearanceSettingsStore = defineStore(
});
}
} else {
ref.$userColour = trustColor.value[trustColorTemp];
ref.$userColour = trustColor.value[trust.trustColorKey];
}
}
@@ -525,6 +490,9 @@ export const useAppearanceSettingsStore = defineStore(
updateTrustColor(undefined, undefined);
}
/**
*
*/
function toggleThemeMode() {
const nextMode = isDarkMode.value
? 'light'
@@ -532,12 +500,20 @@ export const useAppearanceSettingsStore = defineStore(
setThemeMode(nextMode);
}
/**
*
* @param value
*/
function normalizeAppFontFamily(value) {
return APP_FONT_FAMILIES.includes(value)
? value
: APP_FONT_DEFAULT_KEY;
}
/**
*
* @param value
*/
function setAppFontFamily(value) {
const normalized = normalizeAppFontFamily(value);
appFontFamily.value = normalized;
@@ -545,6 +521,9 @@ export const useAppearanceSettingsStore = defineStore(
applyAppFontFamily(normalized);
}
/**
*
*/
function setDisplayVRCPlusIconsAsAvatar() {
displayVRCPlusIconsAsAvatar.value =
!displayVRCPlusIconsAsAvatar.value;
@@ -553,6 +532,9 @@ export const useAppearanceSettingsStore = defineStore(
displayVRCPlusIconsAsAvatar.value
);
}
/**
*
*/
function setNotificationIconDot() {
notificationIconDot.value = !notificationIconDot.value;
configRepository.setBool(
@@ -561,10 +543,16 @@ export const useAppearanceSettingsStore = defineStore(
);
uiStore.updateTrayIconNotify();
}
/**
*
*/
function setHideNicknames() {
hideNicknames.value = !hideNicknames.value;
configRepository.setBool('VRCX_hideNicknames', hideNicknames.value);
}
/**
*
*/
function setShowInstanceIdInLocation() {
showInstanceIdInLocation.value = !showInstanceIdInLocation.value;
configRepository.setBool(
@@ -572,6 +560,9 @@ export const useAppearanceSettingsStore = defineStore(
showInstanceIdInLocation.value
);
}
/**
*
*/
function setIsAgeGatedInstancesVisible() {
isAgeGatedInstancesVisible.value =
!isAgeGatedInstancesVisible.value;
@@ -580,10 +571,16 @@ export const useAppearanceSettingsStore = defineStore(
isAgeGatedInstancesVisible.value
);
}
/**
*
*/
function setSortFavorites() {
sortFavorites.value = !sortFavorites.value;
configRepository.setBool('VRCX_sortFavorites', sortFavorites.value);
}
/**
*
*/
function setInstanceUsersSortAlphabetical() {
instanceUsersSortAlphabetical.value =
!instanceUsersSortAlphabetical.value;
@@ -593,6 +590,10 @@ export const useAppearanceSettingsStore = defineStore(
);
}
/**
*
* @param size
*/
function setTablePageSize(size) {
const processedSize = clampInt(size, 1, MAX_TABLE_PAGE_SIZE);
tablePageSize.value = processedSize;
@@ -601,6 +602,10 @@ export const useAppearanceSettingsStore = defineStore(
return processedSize;
}
/**
*
* @param input
*/
function normalizeTablePageSizes(input) {
const values = (
Array.isArray(input) ? input : DEFAULT_TABLE_PAGE_SIZES
@@ -629,10 +634,16 @@ export const useAppearanceSettingsStore = defineStore(
setTablePageSize(tablePageSizes.value[0]);
}
}
/**
*
*/
function setDtHour12() {
dtHour12.value = !dtHour12.value;
configRepository.setBool('VRCX_dtHour12', dtHour12.value);
}
/**
*
*/
function setDtIsoFormat() {
dtIsoFormat.value = !dtIsoFormat.value;
configRepository.setBool('VRCX_dtIsoFormat', dtIsoFormat.value);
@@ -668,13 +679,24 @@ export const useAppearanceSettingsStore = defineStore(
JSON.stringify(methods)
);
}
/**
*
* @param collapsed
*/
function setNavCollapsed(collapsed) {
isNavCollapsed.value = collapsed;
configRepository.setBool('VRCX_navIsCollapsed', collapsed);
}
/**
*
*/
function toggleNavCollapsed() {
setNavCollapsed(!isNavCollapsed.value);
}
/**
*
* @param widthOrArray
*/
function setNavWidth(widthOrArray) {
let width = null;
if (Array.isArray(widthOrArray) && widthOrArray.length) {
@@ -692,6 +714,9 @@ export const useAppearanceSettingsStore = defineStore(
});
}
}
/**
*
*/
function setIsSidebarGroupByInstance() {
isSidebarGroupByInstance.value = !isSidebarGroupByInstance.value;
configRepository.setBool(
@@ -699,6 +724,9 @@ export const useAppearanceSettingsStore = defineStore(
isSidebarGroupByInstance.value
);
}
/**
*
*/
function setIsHideFriendsInSameInstance() {
isHideFriendsInSameInstance.value =
!isHideFriendsInSameInstance.value;
@@ -707,6 +735,9 @@ export const useAppearanceSettingsStore = defineStore(
isHideFriendsInSameInstance.value
);
}
/**
*
*/
function setIsSidebarDivideByFriendGroup() {
isSidebarDivideByFriendGroup.value =
!isSidebarDivideByFriendGroup.value;
@@ -735,18 +766,30 @@ export const useAppearanceSettingsStore = defineStore(
JSON.stringify(value)
);
}
/**
*
*/
function setHideUserNotes() {
hideUserNotes.value = !hideUserNotes.value;
configRepository.setBool('VRCX_hideUserNotes', hideUserNotes.value);
}
/**
*
*/
function setHideUserMemos() {
hideUserMemos.value = !hideUserMemos.value;
configRepository.setBool('VRCX_hideUserMemos', hideUserMemos.value);
}
/**
*
*/
function setHideUnfriends() {
hideUnfriends.value = !hideUnfriends.value;
configRepository.setBool('VRCX_hideUnfriends', hideUnfriends.value);
}
/**
*
*/
function setRandomUserColours() {
randomUserColours.value = !randomUserColours.value;
configRepository.setBool(
@@ -754,6 +797,10 @@ export const useAppearanceSettingsStore = defineStore(
randomUserColours.value
);
}
/**
*
* @param value
*/
function normalizeTableDensity(value) {
if (
value === 'compact' ||
@@ -765,6 +812,10 @@ export const useAppearanceSettingsStore = defineStore(
return 'standard';
}
/**
*
* @param density
*/
function setTableDensity(density) {
const normalized = normalizeTableDensity(density);
tableDensity.value = normalized;
@@ -772,6 +823,9 @@ export const useAppearanceSettingsStore = defineStore(
configRepository.setString('VRCX_tableDensity', tableDensity.value);
}
/**
*
*/
function toggleStripedDataTable() {
isDataTableStriped.value = !isDataTableStriped.value;
configRepository.setBool(
@@ -781,6 +835,9 @@ export const useAppearanceSettingsStore = defineStore(
}
// FIXME: this is nasty, there should be a better way of doing this
/**
*
*/
function applyPointerHoverClass() {
const classList = document.documentElement.classList;
classList.remove('force-pointer-on-hover');
@@ -790,6 +847,9 @@ export const useAppearanceSettingsStore = defineStore(
}
}
/**
*
*/
function togglePointerOnHover() {
showPointerOnHover.value = !showPointerOnHover.value;
configRepository.setBool(
@@ -811,6 +871,9 @@ export const useAppearanceSettingsStore = defineStore(
);
}
/**
*
*/
function handleSaveSidebarSortOrder() {
if (sidebarSortMethod1.value === sidebarSortMethod2.value) {
sidebarSortMethod2.value = '';
@@ -835,6 +898,9 @@ export const useAppearanceSettingsStore = defineStore(
setSidebarSortMethods(sidebarSortMethods);
}
/**
*
*/
async function mergeOldSortMethodsSettings() {
const orderFriendsGroupPrivate = await configRepository.getBool(
'orderFriendGroupPrivate'
@@ -897,6 +963,9 @@ export const useAppearanceSettingsStore = defineStore(
return n;
};
/**
*
*/
function showTableLimitsDialog() {
tableLimitsDialog.value.maxTableSize = Number(
vrcxStore.maxTableSize ?? 500
@@ -907,10 +976,16 @@ export const useAppearanceSettingsStore = defineStore(
tableLimitsDialog.value.visible = true;
}
/**
*
*/
function closeTableLimitsDialog() {
tableLimitsDialog.value.visible = false;
}
/**
*
*/
async function saveTableLimitsDialog() {
const nextMaxTableSize = clampLimit(
tableLimitsDialog.value.maxTableSize,
@@ -949,6 +1024,9 @@ export const useAppearanceSettingsStore = defineStore(
tableLimitsDialog.value.visible = false;
}
/**
*
*/
async function tryInitUserColours() {
if (!randomUserColours.value) {
return;
@@ -958,6 +1036,10 @@ export const useAppearanceSettingsStore = defineStore(
await userColourInit();
}
/**
*
* @param density
*/
function applyTableDensity(density) {
const classList = document.documentElement.classList;
classList.remove('is-compact-table', 'is-comfortable-table');

View File

@@ -9,6 +9,12 @@ import {
isRpcWorld,
parseLocation
} from '../../shared/utils';
import {
getPlatformLabel,
getRpcWorldConfig,
getStatusInfo,
isPopcornPalaceWorld
} from '../../shared/utils/discordPresence';
import {
ActivityType,
StatusDisplayType
@@ -59,14 +65,23 @@ export const useDiscordPresenceSettingsStore = defineStore(
const discordWorldIntegration = ref(true);
const discordWorldNameAsDiscordStatus = ref(false);
/**
*
*/
function setDiscordActive() {
discordActive.value = !discordActive.value;
configRepository.setBool('discordActive', discordActive.value);
}
/**
*
*/
function setDiscordInstance() {
discordInstance.value = !discordInstance.value;
configRepository.setBool('discordInstance', discordInstance.value);
}
/**
*
*/
function setDiscordHideInvite() {
discordHideInvite.value = !discordHideInvite.value;
configRepository.setBool(
@@ -74,6 +89,9 @@ export const useDiscordPresenceSettingsStore = defineStore(
discordHideInvite.value
);
}
/**
*
*/
function setDiscordJoinButton() {
discordJoinButton.value = !discordJoinButton.value;
configRepository.setBool(
@@ -81,6 +99,9 @@ export const useDiscordPresenceSettingsStore = defineStore(
discordJoinButton.value
);
}
/**
*
*/
function setDiscordHideImage() {
discordHideImage.value = !discordHideImage.value;
configRepository.setBool(
@@ -88,6 +109,9 @@ export const useDiscordPresenceSettingsStore = defineStore(
discordHideImage.value
);
}
/**
*
*/
function setDiscordShowPlatform() {
discordShowPlatform.value = !discordShowPlatform.value;
configRepository.setBool(
@@ -95,6 +119,9 @@ export const useDiscordPresenceSettingsStore = defineStore(
discordShowPlatform.value
);
}
/**
*
*/
function setDiscordWorldIntegration() {
discordWorldIntegration.value = !discordWorldIntegration.value;
configRepository.setBool(
@@ -102,6 +129,9 @@ export const useDiscordPresenceSettingsStore = defineStore(
discordWorldIntegration.value
);
}
/**
*
*/
function setDiscordWorldNameAsDiscordStatus() {
discordWorldNameAsDiscordStatus.value =
!discordWorldNameAsDiscordStatus.value;
@@ -111,6 +141,9 @@ export const useDiscordPresenceSettingsStore = defineStore(
);
}
/**
*
*/
async function initDiscordPresenceSettings() {
const [
discordActiveConfig,
@@ -148,6 +181,9 @@ export const useDiscordPresenceSettingsStore = defineStore(
initDiscordPresenceSettings();
/**
*
*/
async function updateDiscord() {
let currentLocation = locationStore.lastLocation.location;
let startTime = locationStore.lastLocation.date;
@@ -204,27 +240,12 @@ export const useDiscordPresenceSettingsStore = defineStore(
let platform = '';
if (discordShowPlatform.value) {
if (gameStore.isGameRunning) {
platform = gameStore.isGameNoVR
? ` (${t('view.settings.discord_presence.rpc.desktop')})`
: ` (${t('view.settings.discord_presence.rpc.vr')})`;
} else {
switch (userStore.currentUser.presence.platform) {
case 'web':
break;
case 'standalonewindows':
platform = ` (PC)`;
break;
case 'android':
platform = ` (Android)`;
break;
case 'ios':
platform = ` (iOS)`;
break;
default:
platform = ` (${userStore.currentUser.presence.platform})`;
}
}
platform = getPlatformLabel(
userStore.currentUser.presence.platform,
gameStore.isGameRunning,
gameStore.isGameNoVR,
t
);
}
state.lastLocationDetails.groupAccessType = L.groupAccessType;
if (L.groupAccessType) {
@@ -281,34 +302,14 @@ export const useDiscordPresenceSettingsStore = defineStore(
) {
hidePrivate = true;
}
let statusName = '';
let statusImage = '';
switch (userStore.currentUser.status) {
case 'active':
statusName = t('dialog.user.status.active');
statusImage = 'active';
break;
case 'join me':
statusName = t('dialog.user.status.join_me');
statusImage = 'joinme';
break;
case 'ask me':
statusName = t('dialog.user.status.ask_me');
statusImage = 'askme';
if (discordHideInvite.value) {
hidePrivate = true;
}
break;
case 'busy':
statusName = t('dialog.user.status.busy');
statusImage = 'busy';
hidePrivate = true;
break;
default:
statusName = t('dialog.user.status.offline');
statusImage = 'offline';
hidePrivate = true;
break;
const statusInfo = getStatusInfo(
userStore.currentUser.status,
discordHideInvite.value,
t
);
const { statusName, statusImage } = statusInfo;
if (statusInfo.hidePrivate) {
hidePrivate = true;
}
let details = state.lastLocationDetails.worldName;
let stateText = state.lastLocationDetails.accessName;
@@ -345,74 +346,23 @@ export const useDiscordPresenceSettingsStore = defineStore(
buttonUrl = '';
}
if (
const rpcConfig =
isRpcWorld(state.lastLocationDetails.tag) &&
discordWorldIntegration.value
) {
// custom world rpc
? getRpcWorldConfig(state.lastLocationDetails.worldId)
: null;
if (rpcConfig) {
activityType = rpcConfig.activityType;
statusDisplayType = rpcConfig.statusDisplayType;
appId = rpcConfig.appId;
bigIcon = rpcConfig.bigIcon;
if (
state.lastLocationDetails.worldId ===
'wrld_f20326da-f1ac-45fc-a062-609723b097b1' ||
state.lastLocationDetails.worldId ===
'wrld_10e5e467-fc65-42ed-8957-f02cace1398c' ||
state.lastLocationDetails.worldId ===
'wrld_04899f23-e182-4a8d-b2c7-2c74c7c15534'
isPopcornPalaceWorld(state.lastLocationDetails.worldId) &&
!discordHideImage.value &&
gameLogStore.nowPlaying.thumbnailUrl
) {
activityType = ActivityType.Listening;
statusDisplayType = StatusDisplayType.Details;
appId = '784094509008551956';
bigIcon = 'pypy';
} else if (
state.lastLocationDetails.worldId ===
'wrld_42377cf1-c54f-45ed-8996-5875b0573a83' ||
state.lastLocationDetails.worldId ===
'wrld_dd6d2888-dbdc-47c2-bc98-3d631b2acd7c'
) {
activityType = ActivityType.Listening;
statusDisplayType = StatusDisplayType.Details;
appId = '846232616054030376';
bigIcon = 'vr_dancing';
} else if (
state.lastLocationDetails.worldId ===
'wrld_52bdcdab-11cd-4325-9655-0fb120846945' ||
state.lastLocationDetails.worldId ===
'wrld_2d40da63-8f1f-4011-8a9e-414eb8530acd'
) {
activityType = ActivityType.Listening;
statusDisplayType = StatusDisplayType.Details;
appId = '939473404808007731';
bigIcon = 'zuwa_zuwa_dance';
} else if (
state.lastLocationDetails.worldId ===
'wrld_74970324-58e8-4239-a17b-2c59dfdf00db' ||
state.lastLocationDetails.worldId ===
'wrld_db9d878f-6e76-4776-8bf2-15bcdd7fc445' ||
state.lastLocationDetails.worldId ===
'wrld_435bbf25-f34f-4b8b-82c6-cd809057eb8e' ||
state.lastLocationDetails.worldId ===
'wrld_f767d1c8-b249-4ecc-a56f-614e433682c8'
) {
activityType = ActivityType.Watching;
statusDisplayType = StatusDisplayType.Details;
appId = '968292722391785512';
bigIcon = 'ls_media';
} else if (
state.lastLocationDetails.worldId ===
'wrld_266523e8-9161-40da-acd0-6bd82e075833' ||
state.lastLocationDetails.worldId ===
'wrld_27c7e6b2-d938-447e-a270-3d1a873e2cf3'
) {
activityType = ActivityType.Watching;
statusDisplayType = StatusDisplayType.Details;
appId = '1095440531821170820';
if (
!discordHideImage.value &&
gameLogStore.nowPlaying.thumbnailUrl
) {
bigIcon = gameLogStore.nowPlaying.thumbnailUrl;
} else {
bigIcon = 'popcorn_palace';
}
bigIcon = gameLogStore.nowPlaying.thumbnailUrl;
}
if (gameLogStore.nowPlaying.name) {
details = gameLogStore.nowPlaying.name;
@@ -476,12 +426,20 @@ export const useDiscordPresenceSettingsStore = defineStore(
);
}
/**
*
* @param active
*/
async function setIsDiscordActive(active) {
if (active !== state.isDiscordActive) {
state.isDiscordActive = await Discord.SetActive(active);
}
}
/**
*
* @param configLabel
*/
async function saveDiscordOption(configLabel = '') {
state.lastLocationDetails.tag = '';
updateLoopStore.nextDiscordUpdate = 3;

View File

@@ -12,6 +12,10 @@ import {
compareByLocationAt,
compareByName,
compareByUpdatedAt,
computeUserPlatform,
createDefaultUserRef,
diffObjectProps,
evictMapCache,
extractFileId,
findUserByDisplayName,
getAllUserMemos,
@@ -20,8 +24,8 @@ import {
getWorldName,
isRealInstance,
parseLocation,
removeEmojis,
replaceBioSymbols
replaceBioSymbols,
sanitizeUserJson
} from '../shared/utils';
import {
avatarRequest,
@@ -29,10 +33,10 @@ import {
instanceRequest,
userRequest
} from '../api';
import { patchUserFromEvent } from '../query';
import { processBulk, request } from '../service/request';
import { AppDebug } from '../service/appConfig';
import { database } from '../service/database';
import { patchUserFromEvent } from '../query';
import { useAppearanceSettingsStore } from './settings/appearance';
import { useAuthStore } from './auth';
import { useAvatarStore } from './avatar';
@@ -344,6 +348,10 @@ export const useUserStore = defineStore('User', () => {
{ flush: 'sync' }
);
/**
*
* @param args
*/
function handleConfig(args) {
const authStore = useAuthStore();
const ref = {
@@ -419,143 +427,18 @@ export const useUserStore = defineStore('User', () => {
}
const robotUrl = `${AppDebug.endpointDomain}/file/file_0e8c4e32-7444-44ea-ade4-313c010d4bae/1/file`;
/**
*
* @param {Map<string, any>} userCache
* @param {Map<string, any>} friendMap
*/
function cleanupUserCache(userCache, friendMap) {
const bufferSize = 300;
const currentFriendCount = friendMap.size;
const currentTotalSize = userCache.size;
const effectiveMaxSize = currentFriendCount + bufferSize;
if (currentTotalSize <= effectiveMaxSize) {
return;
}
const targetDeleteCount = currentTotalSize - effectiveMaxSize;
let deletedCount = 0;
const keysToDelete = [];
for (const userId of userCache.keys()) {
if (friendMap.has(userId)) {
continue;
}
if (deletedCount >= targetDeleteCount) {
break;
}
keysToDelete.push(userId);
deletedCount++;
}
for (const id of keysToDelete) {
userCache.delete(id);
}
console.log(
`User cache cleanup: Deleted ${deletedCount}. Current cache size: ${userCache.size}`
);
}
/**
*
* @param {import('../types/api/user').GetUserResponse} json
* @returns {import('../types/api/user').VrcxUser}
*/
function applyUser(json) {
let hasPropChanged = false;
const changedProps = {};
let ref = cachedUsers.get(json.id);
if (json.statusDescription) {
json.statusDescription = replaceBioSymbols(json.statusDescription);
json.statusDescription = removeEmojis(json.statusDescription);
}
if (json.bio) {
json.bio = replaceBioSymbols(json.bio);
}
if (json.note) {
json.note = replaceBioSymbols(json.note);
}
if (json.currentAvatarImageUrl === robotUrl) {
delete json.currentAvatarImageUrl;
delete json.currentAvatarThumbnailImageUrl;
}
let hasPropChanged = false;
let changedProps = {};
sanitizeUserJson(json, robotUrl);
if (typeof ref === 'undefined') {
ref = reactive({
ageVerificationStatus: '',
ageVerified: false,
allowAvatarCopying: false,
badges: [],
bio: '',
bioLinks: [],
currentAvatarImageUrl: '',
currentAvatarTags: [],
currentAvatarThumbnailImageUrl: '',
date_joined: '',
developerType: '',
discordId: '',
displayName: '',
friendKey: '',
friendRequestStatus: '',
id: '',
instanceId: '',
isFriend: false,
last_activity: '',
last_login: '',
last_mobile: null,
last_platform: '',
location: '',
platform: '',
note: null,
profilePicOverride: '',
profilePicOverrideThumbnail: '',
pronouns: '',
state: '',
status: '',
statusDescription: '',
tags: [],
travelingToInstance: '',
travelingToLocation: '',
travelingToWorld: '',
userIcon: '',
worldId: '',
// only in bulk request
fallbackAvatar: '',
// VRCX
$location: {},
$location_at: Date.now(),
$online_for: Date.now(),
$travelingToTime: Date.now(),
$offline_for: null,
$active_for: Date.now(),
$isVRCPlus: false,
$isModerator: false,
$isTroll: false,
$isProbableTroll: false,
$trustLevel: 'Visitor',
$trustClass: 'x-tag-untrusted',
$userColour: '',
$trustSortNum: 1,
$languages: [],
$joinCount: 0,
$timeSpent: 0,
$lastSeen: '',
$mutualCount: 0,
$nickName: '',
$previousLocation: '',
$customTag: '',
$customTagColour: '',
$friendNumber: 0,
$platform: '',
$moderations: {},
//
...json
});
ref = reactive(createDefaultUserRef(json));
if (locationStore.lastLocation.playerList.has(json.id)) {
// update $location_at from instance join time
const player = locationStore.lastLocation.playerList.get(
@@ -581,7 +464,12 @@ export const useUserStore = defineStore('User', () => {
ref.$customTag = '';
ref.$customTagColour = '';
}
cleanupUserCache(cachedUsers, friendStore.friends);
evictMapCache(
cachedUsers,
friendStore.friends.size + 300,
(_value, key) => friendStore.friends.has(key),
{ logLabel: 'User cache cleanup' }
);
cachedUsers.set(ref.id, ref);
friendStore.updateFriend(ref.id);
} else {
@@ -589,59 +477,23 @@ export const useUserStore = defineStore('User', () => {
// offline event before GPS to offline location
friendStore.updateFriend(ref.id, json.state);
}
for (const prop in ref) {
if (typeof json[prop] === 'undefined') {
continue;
}
// Only compare primitive values
if (ref[prop] === null || typeof ref[prop] !== 'object') {
changedProps[prop] = true;
}
}
for (const prop in json) {
if (typeof ref[prop] === 'undefined') {
continue;
}
if (Array.isArray(json[prop]) && Array.isArray(ref[prop])) {
if (!arraysMatch(json[prop], ref[prop])) {
changedProps[prop] = true;
}
} else if (
json[prop] === null ||
typeof json[prop] !== 'object'
) {
changedProps[prop] = true;
}
}
for (const prop in changedProps) {
const asIs = ref[prop];
const toBe = json[prop];
if (asIs === toBe) {
delete changedProps[prop];
} else {
hasPropChanged = true;
changedProps[prop] = [toBe, asIs];
}
}
const {
hasPropChanged: _hasPropChanged,
changedProps: _changedProps
} = diffObjectProps(ref, json, arraysMatch);
for (const prop in json) {
if (typeof json[prop] !== 'undefined') {
ref[prop] = json[prop];
}
}
hasPropChanged = _hasPropChanged;
changedProps = _changedProps;
}
ref.$moderations = moderationStore.getUserModerations(ref.id);
ref.$isVRCPlus = ref.tags.includes('system_supporter');
appearanceSettingsStore.applyUserTrustLevel(ref);
applyUserLanguage(ref);
if (
ref.platform &&
ref.platform !== 'offline' &&
ref.platform !== 'web'
) {
ref.$platform = ref.platform;
} else {
ref.$platform = ref.last_platform;
}
ref.$platform = computeUserPlatform(ref.platform, ref.last_platform);
// traveling
if (ref.location === 'traveling') {
ref.$location = parseLocation(ref.travelingToLocation);
@@ -1180,6 +1032,10 @@ export const useUserStore = defineStore('User', () => {
D.instance.friendCount = friendCount;
}
/**
*
* @param array
*/
function sortUserDialogAvatars(array) {
const D = userDialog.value;
if (D.avatarSorting === 'update') {
@@ -1192,6 +1048,10 @@ export const useUserStore = defineStore('User', () => {
D.avatars = array;
}
/**
*
* @param fileId
*/
async function refreshUserDialogAvatars(fileId) {
const D = userDialog.value;
const userId = D.id;
@@ -1248,6 +1108,10 @@ export const useUserStore = defineStore('User', () => {
});
}
/**
*
* @param ref
*/
async function lookupUser(ref) {
let ctx;
if (ref.userId) {
@@ -1577,6 +1441,9 @@ export const useUserStore = defineStore('User', () => {
}
}
/**
*
*/
function updateAutoStateChange() {
if (
!generalSettingsStore.autoStateChangeEnabled ||
@@ -1683,6 +1550,10 @@ export const useUserStore = defineStore('User', () => {
});
}
/**
*
* @param data
*/
function addCustomTag(data) {
if (data.Tag) {
customUserTags.set(data.UserId, {
@@ -1708,6 +1579,9 @@ export const useUserStore = defineStore('User', () => {
sharedFeedStore.addTag(data.UserId, data.TagColour);
}
/**
*
*/
async function initUserNotes() {
state.lastNoteCheck = new Date();
state.lastDbNoteDate = null;
@@ -1735,6 +1609,9 @@ export const useUserStore = defineStore('User', () => {
}
}
/**
*
*/
async function getLatestUserNotes() {
state.lastNoteCheck = new Date();
const params = {
@@ -1793,6 +1670,11 @@ export const useUserStore = defineStore('User', () => {
}
}
/**
*
* @param userId
* @param newNote
*/
async function checkNote(userId, newNote) {
// last check was more than than 5 minutes ago
if (
@@ -1814,6 +1696,9 @@ export const useUserStore = defineStore('User', () => {
}
}
/**
*
*/
function getCurrentUser() {
return request('auth/user', {
method: 'GET'
@@ -2079,11 +1964,18 @@ export const useUserStore = defineStore('User', () => {
return ref;
}
/**
*
* @param userId
*/
function showSendBoopDialog(userId) {
sendBoopDialog.value.userId = userId;
sendBoopDialog.value.visible = true;
}
/**
*
*/
function toggleSharedConnectionsOptOut() {
userRequest.saveCurrentUser({
hasSharedConnectionsOptOut:
@@ -2091,6 +1983,9 @@ export const useUserStore = defineStore('User', () => {
});
}
/**
*
*/
function toggleDiscordFriendsOptOut() {
userRequest.saveCurrentUser({
hasDiscordFriendsOptOut: !currentUser.value.hasDiscordFriendsOptOut

View File

@@ -5,16 +5,18 @@ import { useI18n } from 'vue-i18n';
import {
checkVRChatCache,
createDefaultWorldRef,
evictMapCache,
getAvailablePlatforms,
getBundleDateSize,
getWorldMemo,
isRealInstance,
parseLocation,
replaceBioSymbols
sanitizeEntityJson
} from '../shared/utils';
import { instanceRequest, miscRequest, worldRequest } from '../api';
import { patchWorldFromEvent } from '../query';
import { database } from '../service/database';
import { patchWorldFromEvent } from '../query';
import { processBulk } from '../service/request';
import { useFavoriteStore } from './favorite';
import { useInstanceStore } from './instance';
@@ -76,6 +78,7 @@ export const useWorldStore = defineStore('World', () => {
*
* @param {string} tag
* @param {string} shortName
* @param options
*/
function showWorldDialog(tag, shortName = null, options = {}) {
const D = worldDialog;
@@ -204,7 +207,6 @@ export const useWorldStore = defineStore('World', () => {
args.json !== false;
}
});
}
});
}
@@ -235,20 +237,9 @@ export const useWorldStore = defineStore('World', () => {
* @param WorldCache
*/
function cleanupWorldCache(WorldCache) {
const maxCacheSize = 10000;
if (WorldCache.size <= maxCacheSize) {
return;
}
const deletedCount = WorldCache.size - maxCacheSize;
while (WorldCache.size > maxCacheSize) {
const deletedKey = WorldCache.keys().next().value;
WorldCache.delete(deletedKey);
}
console.log(
`World cache cleanup: Deleted ${deletedCount}. Current cache size: ${WorldCache.size}`
);
evictMapCache(WorldCache, 10000, () => false, {
logLabel: 'World cache cleanup'
});
}
/**
@@ -257,55 +248,10 @@ export const useWorldStore = defineStore('World', () => {
* @returns {object} ref
*/
function applyWorld(json) {
if (json.name) {
json.name = replaceBioSymbols(json.name);
}
if (json.description) {
json.description = replaceBioSymbols(json.description);
}
sanitizeEntityJson(json, ['name', 'description']);
let ref = cachedWorlds.get(json.id);
if (typeof ref === 'undefined') {
ref = {
id: '',
name: '',
description: '',
defaultContentSettings: {},
authorId: '',
authorName: '',
capacity: 0,
recommendedCapacity: 0,
tags: [],
releaseStatus: '',
imageUrl: '',
thumbnailImageUrl: '',
assetUrl: '',
assetUrlObject: {},
pluginUrl: '',
pluginUrlObject: {},
unityPackageUrl: '',
unityPackageUrlObject: {},
unityPackages: [],
version: 0,
favorites: 0,
created_at: '',
updated_at: '',
publicationDate: '',
labsPublicationDate: '',
visits: 0,
popularity: 0,
heat: 0,
publicOccupants: 0,
privateOccupants: 0,
occupants: 0,
instances: [],
featured: false,
organization: '',
previewYoutubeId: '',
// VRCX
$isLabs: false,
//
...json
};
ref = createDefaultWorldRef(json);
cleanupWorldCache(cachedWorlds);
cachedWorlds.set(ref.id, ref);
} else {