mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-18 22:33:50 +02:00
refactor utils
This commit is contained in:
20
src/shared/utils/__tests__/avatarTransforms.test.js
Normal file
20
src/shared/utils/__tests__/avatarTransforms.test.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createDefaultAvatarRef } from '../avatarTransforms';
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -1,66 +1,9 @@
|
||||
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 = {
|
||||
@@ -82,286 +25,6 @@ describe('sanitizeEntityJson', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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({});
|
||||
|
||||
20
src/shared/utils/__tests__/groupTransforms.test.js
Normal file
20
src/shared/utils/__tests__/groupTransforms.test.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createDefaultGroupRef } from '../groupTransforms';
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
52
src/shared/utils/__tests__/instanceTransforms.test.js
Normal file
52
src/shared/utils/__tests__/instanceTransforms.test.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
computeDisabledContentSettings,
|
||||
createDefaultInstanceRef
|
||||
} from '../instanceTransforms';
|
||||
|
||||
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('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);
|
||||
});
|
||||
});
|
||||
229
src/shared/utils/__tests__/userTransforms.test.js
Normal file
229
src/shared/utils/__tests__/userTransforms.test.js
Normal file
@@ -0,0 +1,229 @@
|
||||
import {
|
||||
sanitizeUserJson,
|
||||
computeTrustLevel,
|
||||
computeUserPlatform,
|
||||
diffObjectProps,
|
||||
createDefaultUserRef
|
||||
} from '../userTransforms';
|
||||
|
||||
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('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('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('');
|
||||
});
|
||||
});
|
||||
20
src/shared/utils/__tests__/worldTransforms.test.js
Normal file
20
src/shared/utils/__tests__/worldTransforms.test.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createDefaultWorldRef } from '../worldTransforms';
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
37
src/shared/utils/avatarTransforms.js
Normal file
37
src/shared/utils/avatarTransforms.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
@@ -1,31 +1,4 @@
|
||||
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;
|
||||
}
|
||||
import { replaceBioSymbols } from './base/string';
|
||||
|
||||
/**
|
||||
* Sanitize arbitrary entity JSON fields via replaceBioSymbols.
|
||||
@@ -42,455 +15,6 @@ export function sanitizeEntityJson(json, fields) {
|
||||
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
|
||||
|
||||
64
src/shared/utils/groupTransforms.js
Normal file
64
src/shared/utils/groupTransforms.js
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
@@ -25,6 +25,11 @@ export * from './throttle';
|
||||
export * from './retry';
|
||||
export * from './gameLog';
|
||||
export * from './entityTransforms';
|
||||
export * from './userTransforms';
|
||||
export * from './worldTransforms';
|
||||
export * from './avatarTransforms';
|
||||
export * from './groupTransforms';
|
||||
export * from './instanceTransforms';
|
||||
export * from './cacheUtils';
|
||||
export * from './notificationTransforms';
|
||||
export * from './discordPresence';
|
||||
|
||||
77
src/shared/utils/instanceTransforms.js
Normal file
77
src/shared/utils/instanceTransforms.js
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
247
src/shared/utils/userTransforms.js
Normal file
247
src/shared/utils/userTransforms.js
Normal file
@@ -0,0 +1,247 @@
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
48
src/shared/utils/worldTransforms.js
Normal file
48
src/shared/utils/worldTransforms.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user