mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-18 22:33:50 +02:00
refactor store
This commit is contained in:
@@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
93
src/shared/utils/__tests__/cacheUtils.test.js
Normal file
93
src/shared/utils/__tests__/cacheUtils.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
241
src/shared/utils/__tests__/discordPresence.test.js
Normal file
241
src/shared/utils/__tests__/discordPresence.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
418
src/shared/utils/__tests__/entityTransforms.test.js
Normal file
418
src/shared/utils/__tests__/entityTransforms.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
211
src/shared/utils/__tests__/notificationTransforms.test.js
Normal file
211
src/shared/utils/__tests__/notificationTransforms.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
|
||||
67
src/shared/utils/cacheUtils.js
Normal file
67
src/shared/utils/cacheUtils.js
Normal 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 };
|
||||
}
|
||||
228
src/shared/utils/discordPresence.js
Normal file
228
src/shared/utils/discordPresence.js
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
532
src/shared/utils/entityTransforms.js
Normal file
532
src/shared/utils/entityTransforms.js
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 };
|
||||
|
||||
120
src/shared/utils/notificationTransforms.js
Normal file
120
src/shared/utils/notificationTransforms.js
Normal 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}`;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {}) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user