mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-06 06:46:04 +02:00
Introduce coordinator
This commit is contained in:
@@ -0,0 +1,139 @@
|
|||||||
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { createAuthAutoLoginCoordinator } from '../coordinators/authAutoLoginCoordinator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>} Promise flush helper.
|
||||||
|
*/
|
||||||
|
async function flushPromises() {
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {object} overrides Dependency overrides for a specific test case.
|
||||||
|
* @returns {object} Mock dependencies for auth auto-login coordinator.
|
||||||
|
*/
|
||||||
|
function makeDeps(overrides = {}) {
|
||||||
|
let attemptingAutoLogin = false;
|
||||||
|
|
||||||
|
const deps = {
|
||||||
|
getIsAttemptingAutoLogin: vi.fn(() => attemptingAutoLogin),
|
||||||
|
setAttemptingAutoLogin: vi.fn((value) => {
|
||||||
|
attemptingAutoLogin = value;
|
||||||
|
}),
|
||||||
|
getLastUserLoggedIn: vi.fn(() => 'usr_1'),
|
||||||
|
getSavedCredentials: vi.fn().mockResolvedValue({
|
||||||
|
user: { id: 'usr_1' },
|
||||||
|
loginParams: {}
|
||||||
|
}),
|
||||||
|
isPrimaryPasswordEnabled: vi.fn(() => false),
|
||||||
|
handleLogoutEvent: vi.fn().mockResolvedValue(undefined),
|
||||||
|
autoLoginAttempts: new Set(),
|
||||||
|
relogin: vi.fn().mockResolvedValue(undefined),
|
||||||
|
notifyAutoLoginSuccess: vi.fn(),
|
||||||
|
notifyAutoLoginFailed: vi.fn(),
|
||||||
|
notifyOffline: vi.fn(),
|
||||||
|
flashWindow: vi.fn(),
|
||||||
|
isOnline: vi.fn(() => true),
|
||||||
|
now: vi.fn(() => 1000)
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...deps,
|
||||||
|
...overrides
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('createAuthAutoLoginCoordinator', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns early when auto-login is already in progress', async () => {
|
||||||
|
const deps = makeDeps({
|
||||||
|
getIsAttemptingAutoLogin: vi.fn(() => true)
|
||||||
|
});
|
||||||
|
const coordinator = createAuthAutoLoginCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runHandleAutoLoginFlow();
|
||||||
|
|
||||||
|
expect(deps.setAttemptingAutoLogin).not.toHaveBeenCalled();
|
||||||
|
expect(deps.getSavedCredentials).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stops flow when no saved credentials are found', async () => {
|
||||||
|
const deps = makeDeps({
|
||||||
|
getSavedCredentials: vi.fn().mockResolvedValue(null)
|
||||||
|
});
|
||||||
|
const coordinator = createAuthAutoLoginCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runHandleAutoLoginFlow();
|
||||||
|
|
||||||
|
expect(deps.setAttemptingAutoLogin).toHaveBeenNthCalledWith(1, true);
|
||||||
|
expect(deps.setAttemptingAutoLogin).toHaveBeenNthCalledWith(2, false);
|
||||||
|
expect(deps.relogin).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('logs out when primary password is enabled', async () => {
|
||||||
|
const deps = makeDeps({
|
||||||
|
isPrimaryPasswordEnabled: vi.fn(() => true)
|
||||||
|
});
|
||||||
|
const coordinator = createAuthAutoLoginCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runHandleAutoLoginFlow();
|
||||||
|
|
||||||
|
expect(deps.setAttemptingAutoLogin).toHaveBeenNthCalledWith(1, true);
|
||||||
|
expect(deps.setAttemptingAutoLogin).toHaveBeenNthCalledWith(2, false);
|
||||||
|
expect(deps.handleLogoutEvent).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.relogin).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('logs out and flashes window when attempts exceed limit', async () => {
|
||||||
|
const deps = makeDeps({
|
||||||
|
autoLoginAttempts: new Set([100, 200, 300]),
|
||||||
|
now: vi.fn(() => 500)
|
||||||
|
});
|
||||||
|
const coordinator = createAuthAutoLoginCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runHandleAutoLoginFlow();
|
||||||
|
|
||||||
|
expect(deps.handleLogoutEvent).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.flashWindow).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.relogin).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runs relogin and success notify on successful auto-login', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
const coordinator = createAuthAutoLoginCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runHandleAutoLoginFlow();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(deps.autoLoginAttempts.has(1000)).toBe(true);
|
||||||
|
expect(deps.relogin).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.notifyAutoLoginSuccess).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.notifyAutoLoginFailed).not.toHaveBeenCalled();
|
||||||
|
expect(deps.setAttemptingAutoLogin).toHaveBeenLastCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runs failure and offline notifications when relogin fails while offline', async () => {
|
||||||
|
const deps = makeDeps({
|
||||||
|
relogin: vi.fn().mockRejectedValue(new Error('failed')),
|
||||||
|
isOnline: vi.fn(() => false)
|
||||||
|
});
|
||||||
|
const coordinator = createAuthAutoLoginCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runHandleAutoLoginFlow();
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(deps.notifyAutoLoginSuccess).not.toHaveBeenCalled();
|
||||||
|
expect(deps.notifyAutoLoginFailed).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.notifyOffline).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.setAttemptingAutoLogin).toHaveBeenLastCalledWith(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { createAuthCoordinator } from '../coordinators/authCoordinator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {object} Mock dependencies for auth coordinator.
|
||||||
|
*/
|
||||||
|
function makeDeps() {
|
||||||
|
return {
|
||||||
|
userStore: {
|
||||||
|
currentUser: { id: 'usr_1' },
|
||||||
|
setUserDialogVisible: vi.fn(),
|
||||||
|
applyCurrentUser: vi.fn()
|
||||||
|
},
|
||||||
|
notificationStore: {
|
||||||
|
setNotificationInitStatus: vi.fn()
|
||||||
|
},
|
||||||
|
updateLoopStore: {
|
||||||
|
setNextCurrentUserRefresh: vi.fn()
|
||||||
|
},
|
||||||
|
initWebsocket: vi.fn(),
|
||||||
|
updateStoredUser: vi.fn().mockResolvedValue(undefined),
|
||||||
|
webApiService: {
|
||||||
|
clearCookies: vi.fn().mockResolvedValue(undefined)
|
||||||
|
},
|
||||||
|
loginForm: {
|
||||||
|
value: {
|
||||||
|
lastUserLoggedIn: 'usr_1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
configRepository: {
|
||||||
|
remove: vi.fn().mockResolvedValue(undefined)
|
||||||
|
},
|
||||||
|
setAttemptingAutoLogin: vi.fn(),
|
||||||
|
autoLoginAttempts: {
|
||||||
|
clear: vi.fn()
|
||||||
|
},
|
||||||
|
closeWebSocket: vi.fn(),
|
||||||
|
queryClient: {
|
||||||
|
clear: vi.fn()
|
||||||
|
},
|
||||||
|
watchState: {
|
||||||
|
isLoggedIn: true,
|
||||||
|
isFriendsLoaded: true,
|
||||||
|
isFavoritesLoaded: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('createAuthCoordinator', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runLogoutFlow applies all logout side effects', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
const coordinator = createAuthCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runLogoutFlow();
|
||||||
|
|
||||||
|
expect(deps.userStore.setUserDialogVisible).toHaveBeenCalledWith(false);
|
||||||
|
expect(deps.watchState.isLoggedIn).toBe(false);
|
||||||
|
expect(deps.watchState.isFriendsLoaded).toBe(false);
|
||||||
|
expect(deps.watchState.isFavoritesLoaded).toBe(false);
|
||||||
|
expect(
|
||||||
|
deps.notificationStore.setNotificationInitStatus
|
||||||
|
).toHaveBeenCalledWith(false);
|
||||||
|
expect(deps.updateStoredUser).toHaveBeenCalledWith(
|
||||||
|
deps.userStore.currentUser
|
||||||
|
);
|
||||||
|
expect(deps.webApiService.clearCookies).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.loginForm.value.lastUserLoggedIn).toBe('');
|
||||||
|
expect(deps.configRepository.remove).toHaveBeenCalledWith(
|
||||||
|
'lastUserLoggedIn'
|
||||||
|
);
|
||||||
|
expect(deps.setAttemptingAutoLogin).toHaveBeenCalledWith(false);
|
||||||
|
expect(deps.autoLoginAttempts.clear).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.closeWebSocket).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.queryClient.clear).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runLoginSuccessFlow applies login success side effects', () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
const coordinator = createAuthCoordinator(deps);
|
||||||
|
const json = { id: 'usr_2' };
|
||||||
|
|
||||||
|
coordinator.runLoginSuccessFlow(json);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
deps.updateLoopStore.setNextCurrentUserRefresh
|
||||||
|
).toHaveBeenCalledWith(420);
|
||||||
|
expect(deps.userStore.applyCurrentUser).toHaveBeenCalledWith(json);
|
||||||
|
expect(deps.initWebsocket).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import { describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { createFriendPresenceCoordinator } from '../coordinators/friendPresenceCoordinator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {object} Mock dependencies and mutable state for friend presence tests.
|
||||||
|
*/
|
||||||
|
function makeDeps() {
|
||||||
|
const friends = new Map();
|
||||||
|
const cachedUsers = new Map();
|
||||||
|
const pendingOfflineMap = new Map();
|
||||||
|
const ref = {
|
||||||
|
id: 'usr_1',
|
||||||
|
state: 'online',
|
||||||
|
displayName: 'User 1',
|
||||||
|
location: 'wrld_1:1',
|
||||||
|
$location_at: 100,
|
||||||
|
$lastFetch: 0,
|
||||||
|
$online_for: 1,
|
||||||
|
$offline_for: '',
|
||||||
|
$active_for: ''
|
||||||
|
};
|
||||||
|
const ctx = {
|
||||||
|
id: 'usr_1',
|
||||||
|
state: 'online',
|
||||||
|
ref,
|
||||||
|
name: 'User 1',
|
||||||
|
isVIP: false,
|
||||||
|
pendingOffline: false
|
||||||
|
};
|
||||||
|
friends.set('usr_1', ctx);
|
||||||
|
cachedUsers.set('usr_1', ref);
|
||||||
|
|
||||||
|
return {
|
||||||
|
deps: {
|
||||||
|
friends,
|
||||||
|
localFavoriteFriends: new Set(),
|
||||||
|
pendingOfflineMap,
|
||||||
|
pendingOfflineDelay: 100,
|
||||||
|
watchState: { isFriendsLoaded: true },
|
||||||
|
appDebug: { debugFriendState: false },
|
||||||
|
getCachedUsers: vi.fn(() => cachedUsers),
|
||||||
|
isRealInstance: vi.fn(() => false),
|
||||||
|
requestUser: vi.fn(),
|
||||||
|
getWorldName: vi.fn().mockResolvedValue('World 1'),
|
||||||
|
getGroupName: vi.fn().mockResolvedValue('Group 1'),
|
||||||
|
feedStore: {
|
||||||
|
addFeed: vi.fn()
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
addOnlineOfflineToDatabase: vi.fn()
|
||||||
|
},
|
||||||
|
updateOnlineFriendCounter: vi.fn(),
|
||||||
|
now: vi.fn(() => 1000),
|
||||||
|
nowIso: vi.fn(() => '2025-01-01T00:00:00.000Z')
|
||||||
|
},
|
||||||
|
ctx,
|
||||||
|
ref,
|
||||||
|
pendingOfflineMap
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('createFriendPresenceCoordinator', () => {
|
||||||
|
test('queues pending offline transition when friend moves from online to offline', async () => {
|
||||||
|
const { deps, ctx, pendingOfflineMap } = makeDeps();
|
||||||
|
const coordinator = createFriendPresenceCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runUpdateFriendFlow('usr_1', 'offline');
|
||||||
|
|
||||||
|
expect(ctx.pendingOffline).toBe(true);
|
||||||
|
expect(pendingOfflineMap.has('usr_1')).toBe(true);
|
||||||
|
expect(deps.updateOnlineFriendCounter).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('processes pending offline queue and applies delayed transition', async () => {
|
||||||
|
const { deps, ctx, pendingOfflineMap, ref } = makeDeps();
|
||||||
|
pendingOfflineMap.set('usr_1', {
|
||||||
|
startTime: 0,
|
||||||
|
newState: 'offline',
|
||||||
|
previousLocation: 'wrld_1:1',
|
||||||
|
previousLocationAt: 500
|
||||||
|
});
|
||||||
|
const coordinator = createFriendPresenceCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runPendingOfflineTickFlow();
|
||||||
|
|
||||||
|
expect(ctx.state).toBe('offline');
|
||||||
|
expect(ctx.pendingOffline).toBe(false);
|
||||||
|
expect(pendingOfflineMap.has('usr_1')).toBe(false);
|
||||||
|
expect(ref.$offline_for).toBe(1000);
|
||||||
|
expect(deps.feedStore.addFeed).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.feedStore.addFeed).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'Offline',
|
||||||
|
userId: 'usr_1',
|
||||||
|
location: 'wrld_1:1',
|
||||||
|
worldName: 'World 1',
|
||||||
|
groupName: 'Group 1',
|
||||||
|
time: 500
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(deps.database.addOnlineOfflineToDatabase).toHaveBeenCalledTimes(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(deps.updateOnlineFriendCounter).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cancels pending offline transition when online state arrives again', async () => {
|
||||||
|
const { deps, ctx, pendingOfflineMap } = makeDeps();
|
||||||
|
pendingOfflineMap.set('usr_1', {
|
||||||
|
startTime: 900,
|
||||||
|
newState: 'offline',
|
||||||
|
previousLocation: 'wrld_1:1',
|
||||||
|
previousLocationAt: 800
|
||||||
|
});
|
||||||
|
const coordinator = createFriendPresenceCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runUpdateFriendFlow('usr_1', 'online');
|
||||||
|
|
||||||
|
expect(ctx.pendingOffline).toBe(false);
|
||||||
|
expect(pendingOfflineMap.has('usr_1')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('applies offline to online transition contract immediately', async () => {
|
||||||
|
const { deps, ctx, ref } = makeDeps();
|
||||||
|
ctx.state = 'offline';
|
||||||
|
const coordinator = createFriendPresenceCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runUpdateFriendFlow('usr_1', 'online');
|
||||||
|
|
||||||
|
expect(ctx.state).toBe('online');
|
||||||
|
expect(ref.$online_for).toBe(1000);
|
||||||
|
expect(ref.$offline_for).toBe('');
|
||||||
|
expect(deps.feedStore.addFeed).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'Online',
|
||||||
|
userId: 'usr_1',
|
||||||
|
location: 'wrld_1:1',
|
||||||
|
worldName: 'World 1',
|
||||||
|
groupName: 'Group 1'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(deps.database.addOnlineOfflineToDatabase).toHaveBeenCalledTimes(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(deps.updateOnlineFriendCounter).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns safely when friend context does not exist', async () => {
|
||||||
|
const { deps } = makeDeps();
|
||||||
|
deps.friends.clear();
|
||||||
|
const coordinator = createFriendPresenceCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runUpdateFriendFlow('usr_404', 'online');
|
||||||
|
|
||||||
|
expect(deps.feedStore.addFeed).not.toHaveBeenCalled();
|
||||||
|
expect(deps.updateOnlineFriendCounter).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { createFriendRelationshipCoordinator } from '../coordinators/friendRelationshipCoordinator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<void>} Promise flush helper.
|
||||||
|
*/
|
||||||
|
async function flushPromises() {
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {object} Mock dependencies for friend relationship tests.
|
||||||
|
*/
|
||||||
|
function makeDeps() {
|
||||||
|
return {
|
||||||
|
friendLog: new Map(),
|
||||||
|
friendLogTable: {
|
||||||
|
value: {
|
||||||
|
data: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getCurrentUserId: vi.fn(() => 'usr_me'),
|
||||||
|
requestFriendStatus: vi.fn().mockResolvedValue({
|
||||||
|
params: {
|
||||||
|
currentUserId: 'usr_me'
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
isFriend: false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
handleFriendStatus: vi.fn(),
|
||||||
|
addFriendship: vi.fn(),
|
||||||
|
deleteFriend: vi.fn(),
|
||||||
|
database: {
|
||||||
|
addFriendLogHistory: vi.fn(),
|
||||||
|
deleteFriendLogCurrent: vi.fn()
|
||||||
|
},
|
||||||
|
notificationStore: {
|
||||||
|
queueFriendLogNoty: vi.fn()
|
||||||
|
},
|
||||||
|
sharedFeedStore: {
|
||||||
|
addEntry: vi.fn()
|
||||||
|
},
|
||||||
|
favoriteStore: {
|
||||||
|
handleFavoriteDelete: vi.fn()
|
||||||
|
},
|
||||||
|
uiStore: {
|
||||||
|
notifyMenu: vi.fn()
|
||||||
|
},
|
||||||
|
shouldNotifyUnfriend: vi.fn(() => true),
|
||||||
|
nowIso: vi.fn(() => '2026-03-08T00:00:00.000Z')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('createFriendRelationshipCoordinator', () => {
|
||||||
|
test('runDeleteFriendshipFlow applies unfriend side effects after status check', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.friendLog.set('usr_1', {
|
||||||
|
displayName: 'User 1'
|
||||||
|
});
|
||||||
|
const coordinator = createFriendRelationshipCoordinator(deps);
|
||||||
|
|
||||||
|
coordinator.runDeleteFriendshipFlow('usr_1');
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(deps.requestFriendStatus).toHaveBeenCalledWith({
|
||||||
|
userId: 'usr_1',
|
||||||
|
currentUserId: 'usr_me'
|
||||||
|
});
|
||||||
|
expect(deps.handleFriendStatus).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.friendLog.has('usr_1')).toBe(false);
|
||||||
|
expect(deps.database.addFriendLogHistory).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.database.deleteFriendLogCurrent).toHaveBeenCalledWith(
|
||||||
|
'usr_1'
|
||||||
|
);
|
||||||
|
expect(deps.notificationStore.queueFriendLogNoty).toHaveBeenCalledTimes(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(deps.sharedFeedStore.addEntry).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.favoriteStore.handleFavoriteDelete).toHaveBeenCalledWith(
|
||||||
|
'usr_1'
|
||||||
|
);
|
||||||
|
expect(deps.uiStore.notifyMenu).toHaveBeenCalledWith('friend-log');
|
||||||
|
expect(deps.deleteFriend).toHaveBeenCalledWith('usr_1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runUpdateFriendshipsFlow syncs additions and stale removals', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.friendLog.set('usr_me', {
|
||||||
|
displayName: 'Me'
|
||||||
|
});
|
||||||
|
deps.friendLog.set('usr_keep', {
|
||||||
|
displayName: 'Keep'
|
||||||
|
});
|
||||||
|
deps.friendLog.set('usr_drop', {
|
||||||
|
displayName: 'Drop'
|
||||||
|
});
|
||||||
|
const coordinator = createFriendRelationshipCoordinator(deps);
|
||||||
|
|
||||||
|
coordinator.runUpdateFriendshipsFlow({
|
||||||
|
friends: ['usr_keep', 'usr_new']
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(deps.addFriendship).toHaveBeenNthCalledWith(1, 'usr_keep');
|
||||||
|
expect(deps.addFriendship).toHaveBeenNthCalledWith(2, 'usr_new');
|
||||||
|
expect(deps.database.deleteFriendLogCurrent).toHaveBeenCalledWith(
|
||||||
|
'usr_me'
|
||||||
|
);
|
||||||
|
expect(deps.requestFriendStatus).toHaveBeenCalledWith({
|
||||||
|
userId: 'usr_drop',
|
||||||
|
currentUserId: 'usr_me'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ignores delayed status responses from stale current user', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.friendLog.set('usr_1', {
|
||||||
|
displayName: 'User 1'
|
||||||
|
});
|
||||||
|
deps.requestFriendStatus.mockResolvedValue({
|
||||||
|
params: {
|
||||||
|
currentUserId: 'usr_old'
|
||||||
|
},
|
||||||
|
json: {
|
||||||
|
isFriend: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const coordinator = createFriendRelationshipCoordinator(deps);
|
||||||
|
|
||||||
|
coordinator.runDeleteFriendshipFlow('usr_1');
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(deps.handleFriendStatus).not.toHaveBeenCalled();
|
||||||
|
expect(deps.friendLog.has('usr_1')).toBe(true);
|
||||||
|
expect(deps.database.addFriendLogHistory).not.toHaveBeenCalled();
|
||||||
|
expect(deps.deleteFriend).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('respects unfriend notify switch', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.shouldNotifyUnfriend.mockReturnValue(false);
|
||||||
|
deps.friendLog.set('usr_1', {
|
||||||
|
displayName: 'User 1'
|
||||||
|
});
|
||||||
|
const coordinator = createFriendRelationshipCoordinator(deps);
|
||||||
|
|
||||||
|
coordinator.runDeleteFriendshipFlow('usr_1');
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(deps.uiStore.notifyMenu).not.toHaveBeenCalled();
|
||||||
|
expect(deps.favoriteStore.handleFavoriteDelete).toHaveBeenCalledWith(
|
||||||
|
'usr_1'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { createFriendSyncCoordinator } from '../coordinators/friendSyncCoordinator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {object} Mock dependencies for friend sync coordinator.
|
||||||
|
*/
|
||||||
|
function makeDeps() {
|
||||||
|
return {
|
||||||
|
getNextCurrentUserRefresh: vi.fn(() => 999),
|
||||||
|
getCurrentUser: vi.fn().mockResolvedValue(undefined),
|
||||||
|
refreshFriends: vi.fn().mockResolvedValue(undefined),
|
||||||
|
reconnectWebSocket: vi.fn(),
|
||||||
|
getCurrentUserId: vi.fn(() => 'usr_1'),
|
||||||
|
getCurrentUserRef: vi.fn(() => ({ id: 'usr_1' })),
|
||||||
|
setRefreshFriendsLoading: vi.fn(),
|
||||||
|
setFriendsLoaded: vi.fn(),
|
||||||
|
resetFriendLog: vi.fn(),
|
||||||
|
isFriendLogInitialized: vi.fn().mockResolvedValue(true),
|
||||||
|
getFriendLog: vi.fn().mockResolvedValue(undefined),
|
||||||
|
initFriendLog: vi.fn().mockResolvedValue(undefined),
|
||||||
|
isDontLogMeOut: vi.fn(() => false),
|
||||||
|
showLoadFailedToast: vi.fn(),
|
||||||
|
handleLogoutEvent: vi.fn(),
|
||||||
|
tryApplyFriendOrder: vi.fn(),
|
||||||
|
getAllUserStats: vi.fn(),
|
||||||
|
hasLegacyFriendLogData: vi.fn().mockResolvedValue(false),
|
||||||
|
removeLegacyFeedTable: vi.fn(),
|
||||||
|
migrateMemos: vi.fn(),
|
||||||
|
migrateFriendLog: vi.fn()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('createFriendSyncCoordinator', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runRefreshFriendsListFlow refreshes current user before friends when refresh window is small', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.getNextCurrentUserRefresh.mockReturnValue(100);
|
||||||
|
const coordinator = createFriendSyncCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runRefreshFriendsListFlow();
|
||||||
|
|
||||||
|
expect(deps.getCurrentUser).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.refreshFriends).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.reconnectWebSocket).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runRefreshFriendsListFlow skips current user refresh when window is large', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.getNextCurrentUserRefresh.mockReturnValue(300);
|
||||||
|
const coordinator = createFriendSyncCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runRefreshFriendsListFlow();
|
||||||
|
|
||||||
|
expect(deps.getCurrentUser).not.toHaveBeenCalled();
|
||||||
|
expect(deps.refreshFriends).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.reconnectWebSocket).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runInitFriendsListFlow loads existing friend log when initialized', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.isFriendLogInitialized.mockResolvedValue(true);
|
||||||
|
deps.hasLegacyFriendLogData.mockResolvedValue(false);
|
||||||
|
const coordinator = createFriendSyncCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runInitFriendsListFlow();
|
||||||
|
|
||||||
|
expect(deps.setRefreshFriendsLoading).toHaveBeenCalledWith(true);
|
||||||
|
expect(deps.setFriendsLoaded).toHaveBeenCalledWith(false);
|
||||||
|
expect(deps.resetFriendLog).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.isFriendLogInitialized).toHaveBeenCalledWith('usr_1');
|
||||||
|
expect(deps.getFriendLog).toHaveBeenCalledWith({ id: 'usr_1' });
|
||||||
|
expect(deps.initFriendLog).not.toHaveBeenCalled();
|
||||||
|
expect(deps.tryApplyFriendOrder).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.getAllUserStats).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runInitFriendsListFlow initializes new friend log when not initialized', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.isFriendLogInitialized.mockResolvedValue(false);
|
||||||
|
const coordinator = createFriendSyncCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runInitFriendsListFlow();
|
||||||
|
|
||||||
|
expect(deps.getFriendLog).not.toHaveBeenCalled();
|
||||||
|
expect(deps.initFriendLog).toHaveBeenCalledWith({ id: 'usr_1' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runInitFriendsListFlow performs legacy migration when old data exists', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.hasLegacyFriendLogData.mockResolvedValue(true);
|
||||||
|
const coordinator = createFriendSyncCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runInitFriendsListFlow();
|
||||||
|
|
||||||
|
expect(deps.removeLegacyFeedTable).toHaveBeenCalledWith('usr_1');
|
||||||
|
expect(deps.migrateMemos).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.migrateFriendLog).toHaveBeenCalledWith('usr_1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runInitFriendsListFlow logs out and rethrows when load fails and dontLogMeOut is false', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
const err = new Error('load failed');
|
||||||
|
deps.getFriendLog.mockRejectedValue(err);
|
||||||
|
deps.isDontLogMeOut.mockReturnValue(false);
|
||||||
|
const coordinator = createFriendSyncCoordinator(deps);
|
||||||
|
|
||||||
|
await expect(coordinator.runInitFriendsListFlow()).rejects.toThrow(
|
||||||
|
'load failed'
|
||||||
|
);
|
||||||
|
expect(deps.showLoadFailedToast).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.handleLogoutEvent).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.tryApplyFriendOrder).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runInitFriendsListFlow continues when load fails and dontLogMeOut is true', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.getFriendLog.mockRejectedValue(new Error('load failed'));
|
||||||
|
deps.isDontLogMeOut.mockReturnValue(true);
|
||||||
|
const coordinator = createFriendSyncCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runInitFriendsListFlow();
|
||||||
|
|
||||||
|
expect(deps.showLoadFailedToast).not.toHaveBeenCalled();
|
||||||
|
expect(deps.handleLogoutEvent).not.toHaveBeenCalled();
|
||||||
|
expect(deps.tryApplyFriendOrder).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.getAllUserStats).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { createGameCoordinator } from '../coordinators/gameCoordinator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {object} Mock dependencies for game coordinator.
|
||||||
|
*/
|
||||||
|
function makeDeps() {
|
||||||
|
return {
|
||||||
|
userStore: {
|
||||||
|
currentUser: {
|
||||||
|
currentAvatar: 'avtr_1'
|
||||||
|
},
|
||||||
|
markCurrentUserGameStarted: vi.fn(),
|
||||||
|
markCurrentUserGameStopped: vi.fn()
|
||||||
|
},
|
||||||
|
instanceStore: {
|
||||||
|
removeAllQueuedInstances: vi.fn()
|
||||||
|
},
|
||||||
|
updateLoopStore: {
|
||||||
|
setIpcTimeout: vi.fn(),
|
||||||
|
setNextDiscordUpdate: vi.fn()
|
||||||
|
},
|
||||||
|
locationStore: {
|
||||||
|
lastLocationReset: vi.fn()
|
||||||
|
},
|
||||||
|
gameLogStore: {
|
||||||
|
clearNowPlaying: vi.fn()
|
||||||
|
},
|
||||||
|
vrStore: {
|
||||||
|
updateVRLastLocation: vi.fn()
|
||||||
|
},
|
||||||
|
avatarStore: {
|
||||||
|
addAvatarWearTime: vi.fn()
|
||||||
|
},
|
||||||
|
configRepository: {
|
||||||
|
setBool: vi.fn().mockResolvedValue(undefined)
|
||||||
|
},
|
||||||
|
workerTimers: {
|
||||||
|
setTimeout: vi.fn()
|
||||||
|
},
|
||||||
|
checkVRChatDebugLogging: vi.fn(),
|
||||||
|
autoVRChatCacheManagement: vi.fn(),
|
||||||
|
checkIfGameCrashed: vi.fn(),
|
||||||
|
getIsGameNoVR: vi.fn(() => true)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('createGameCoordinator', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runGameRunningChangedFlow(true) runs start + shared side effects', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
const coordinator = createGameCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runGameRunningChangedFlow(true);
|
||||||
|
|
||||||
|
expect(deps.userStore.markCurrentUserGameStarted).toHaveBeenCalledTimes(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(deps.configRepository.setBool).not.toHaveBeenCalled();
|
||||||
|
expect(
|
||||||
|
deps.userStore.markCurrentUserGameStopped
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
expect(
|
||||||
|
deps.instanceStore.removeAllQueuedInstances
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
expect(deps.autoVRChatCacheManagement).not.toHaveBeenCalled();
|
||||||
|
expect(deps.checkIfGameCrashed).not.toHaveBeenCalled();
|
||||||
|
expect(deps.updateLoopStore.setIpcTimeout).not.toHaveBeenCalled();
|
||||||
|
expect(deps.avatarStore.addAvatarWearTime).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(deps.locationStore.lastLocationReset).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.gameLogStore.clearNowPlaying).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.vrStore.updateVRLastLocation).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.updateLoopStore.setNextDiscordUpdate).toHaveBeenCalledWith(
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(deps.workerTimers.setTimeout).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.workerTimers.setTimeout.mock.calls[0][1]).toBe(60000);
|
||||||
|
const timeoutCb = deps.workerTimers.setTimeout.mock.calls[0][0];
|
||||||
|
timeoutCb();
|
||||||
|
expect(deps.checkVRChatDebugLogging).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runGameRunningChangedFlow(false) runs stop + shared side effects', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.getIsGameNoVR.mockReturnValue(false);
|
||||||
|
const coordinator = createGameCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runGameRunningChangedFlow(false);
|
||||||
|
|
||||||
|
expect(deps.getIsGameNoVR).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.configRepository.setBool).toHaveBeenCalledWith(
|
||||||
|
'isGameNoVR',
|
||||||
|
false
|
||||||
|
);
|
||||||
|
expect(deps.userStore.markCurrentUserGameStopped).toHaveBeenCalledTimes(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
deps.instanceStore.removeAllQueuedInstances
|
||||||
|
).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.autoVRChatCacheManagement).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.checkIfGameCrashed).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.updateLoopStore.setIpcTimeout).toHaveBeenCalledWith(0);
|
||||||
|
expect(deps.avatarStore.addAvatarWearTime).toHaveBeenCalledWith(
|
||||||
|
'avtr_1'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(deps.locationStore.lastLocationReset).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.gameLogStore.clearNowPlaying).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.vrStore.updateVRLastLocation).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.updateLoopStore.setNextDiscordUpdate).toHaveBeenCalledWith(
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(deps.workerTimers.setTimeout).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.workerTimers.setTimeout.mock.calls[0][1]).toBe(60000);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
import { describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { createUserEventCoordinator } from '../coordinators/userEventCoordinator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {object} Mock dependencies for user event tests.
|
||||||
|
*/
|
||||||
|
function makeDeps() {
|
||||||
|
const friendRef = {
|
||||||
|
id: 'usr_1'
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
friendStore: {
|
||||||
|
friends: new Map([['usr_1', friendRef]])
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
instancePlayerCount: new Map()
|
||||||
|
},
|
||||||
|
parseLocation: vi.fn((location) => {
|
||||||
|
if (location === 'loc_old') {
|
||||||
|
return {
|
||||||
|
tag: 'loc_old',
|
||||||
|
worldId: 'world_old',
|
||||||
|
groupId: 'group_old'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (location === 'loc_new') {
|
||||||
|
return {
|
||||||
|
tag: 'loc_new',
|
||||||
|
worldId: 'world_new',
|
||||||
|
groupId: 'group_new'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tag: location,
|
||||||
|
worldId: '',
|
||||||
|
groupId: ''
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
userDialog: {
|
||||||
|
value: {
|
||||||
|
$location: {
|
||||||
|
tag: 'loc_new'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
applyUserDialogLocation: vi.fn(),
|
||||||
|
worldStore: {
|
||||||
|
worldDialog: {
|
||||||
|
id: 'world_old'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
groupStore: {
|
||||||
|
groupDialog: {
|
||||||
|
id: 'group_new'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
instanceStore: {
|
||||||
|
applyWorldDialogInstances: vi.fn(),
|
||||||
|
applyGroupDialogInstances: vi.fn()
|
||||||
|
},
|
||||||
|
appDebug: {
|
||||||
|
debugFriendState: false
|
||||||
|
},
|
||||||
|
getWorldName: vi.fn().mockResolvedValue('World'),
|
||||||
|
getGroupName: vi.fn().mockResolvedValue('Group'),
|
||||||
|
feedStore: {
|
||||||
|
addFeed: vi.fn()
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
addGPSToDatabase: vi.fn(),
|
||||||
|
addAvatarToDatabase: vi.fn(),
|
||||||
|
addStatusToDatabase: vi.fn(),
|
||||||
|
addBioToDatabase: vi.fn()
|
||||||
|
},
|
||||||
|
avatarStore: {
|
||||||
|
getAvatarName: vi.fn().mockResolvedValue({
|
||||||
|
ownerId: 'usr_owner',
|
||||||
|
avatarName: 'Avatar'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
generalSettingsStore: {
|
||||||
|
logEmptyAvatars: false
|
||||||
|
},
|
||||||
|
checkNote: vi.fn(),
|
||||||
|
now: vi.fn(() => 1000),
|
||||||
|
nowIso: vi.fn(() => '2025-01-01T00:00:00.000Z')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('createUserEventCoordinator', () => {
|
||||||
|
test('returns early when target user is not in friend map', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.friendStore.friends.clear();
|
||||||
|
const coordinator = createUserEventCoordinator(deps);
|
||||||
|
|
||||||
|
await coordinator.runHandleUserUpdateFlow(
|
||||||
|
{
|
||||||
|
id: 'usr_404',
|
||||||
|
displayName: 'Unknown'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: ['online', 'offline']
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(deps.feedStore.addFeed).not.toHaveBeenCalled();
|
||||||
|
expect(deps.database.addStatusToDatabase).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updates location counters and dialog instance hooks on location change', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.state.instancePlayerCount.set('loc_old', 2);
|
||||||
|
const coordinator = createUserEventCoordinator(deps);
|
||||||
|
const ref = {
|
||||||
|
id: 'usr_1',
|
||||||
|
displayName: 'User 1'
|
||||||
|
};
|
||||||
|
|
||||||
|
await coordinator.runHandleUserUpdateFlow(ref, {
|
||||||
|
location: ['loc_new', 'loc_old', 50],
|
||||||
|
state: ['online', 'online']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deps.state.instancePlayerCount.get('loc_old')).toBe(1);
|
||||||
|
expect(deps.state.instancePlayerCount.get('loc_new')).toBe(1);
|
||||||
|
expect(deps.applyUserDialogLocation).toHaveBeenCalledWith(true);
|
||||||
|
expect(deps.instanceStore.applyWorldDialogInstances).toHaveBeenCalled();
|
||||||
|
expect(deps.instanceStore.applyGroupDialogInstances).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('writes GPS feed with adjusted traveling time contract', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
const coordinator = createUserEventCoordinator(deps);
|
||||||
|
const ref = {
|
||||||
|
id: 'usr_1',
|
||||||
|
displayName: 'User 1',
|
||||||
|
$previousLocation: 'loc_old',
|
||||||
|
$travelingToTime: 900,
|
||||||
|
$location_at: 700
|
||||||
|
};
|
||||||
|
|
||||||
|
await coordinator.runHandleUserUpdateFlow(ref, {
|
||||||
|
location: ['loc_new', 'traveling', 300]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deps.feedStore.addFeed).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'GPS',
|
||||||
|
userId: 'usr_1',
|
||||||
|
previousLocation: 'loc_old',
|
||||||
|
location: 'loc_new',
|
||||||
|
worldName: 'World',
|
||||||
|
groupName: 'Group',
|
||||||
|
time: 200
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(deps.database.addGPSToDatabase).toHaveBeenCalledTimes(1);
|
||||||
|
expect(ref.$previousLocation).toBe('');
|
||||||
|
expect(ref.$travelingToTime).toBe(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('stores previous location while user becomes traveling', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
const coordinator = createUserEventCoordinator(deps);
|
||||||
|
const ref = {
|
||||||
|
id: 'usr_1',
|
||||||
|
displayName: 'User 1',
|
||||||
|
$previousLocation: '',
|
||||||
|
$travelingToTime: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
await coordinator.runHandleUserUpdateFlow(ref, {
|
||||||
|
location: ['traveling', 'loc_old']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ref.$previousLocation).toBe('loc_old');
|
||||||
|
expect(ref.$travelingToTime).toBe(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('writes status and bio feeds and triggers note check', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
const coordinator = createUserEventCoordinator(deps);
|
||||||
|
const ref = {
|
||||||
|
id: 'usr_1',
|
||||||
|
displayName: 'User 1',
|
||||||
|
status: 'busy',
|
||||||
|
statusDescription: 'old',
|
||||||
|
currentAvatarImageUrl: '',
|
||||||
|
currentAvatarThumbnailImageUrl: '',
|
||||||
|
currentAvatarTags: [],
|
||||||
|
profilePicOverride: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
await coordinator.runHandleUserUpdateFlow(ref, {
|
||||||
|
status: ['join me', 'busy'],
|
||||||
|
statusDescription: ['new desc', 'old desc'],
|
||||||
|
bio: ['new bio', 'old bio'],
|
||||||
|
note: ['new note', 'old note']
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deps.feedStore.addFeed).toHaveBeenCalledTimes(2);
|
||||||
|
expect(deps.database.addStatusToDatabase).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.database.addBioToDatabase).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.checkNote).toHaveBeenCalledWith('usr_1', 'new note');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('writes avatar change feed contract', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.generalSettingsStore.logEmptyAvatars = true;
|
||||||
|
deps.avatarStore.getAvatarName
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ownerId: 'usr_owner_new',
|
||||||
|
avatarName: 'Avatar New'
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
ownerId: 'usr_owner_old',
|
||||||
|
avatarName: 'Avatar Old'
|
||||||
|
});
|
||||||
|
const coordinator = createUserEventCoordinator(deps);
|
||||||
|
const ref = {
|
||||||
|
id: 'usr_1',
|
||||||
|
displayName: 'User 1',
|
||||||
|
currentAvatarImageUrl: 'img_old',
|
||||||
|
currentAvatarThumbnailImageUrl: 'thumb_old',
|
||||||
|
currentAvatarTags: ['tag_old'],
|
||||||
|
profilePicOverride: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
await coordinator.runHandleUserUpdateFlow(ref, {
|
||||||
|
currentAvatarImageUrl: ['img_new', 'img_old'],
|
||||||
|
currentAvatarThumbnailImageUrl: ['thumb_new', 'thumb_old'],
|
||||||
|
currentAvatarTags: [['tag_new'], ['tag_old']]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deps.database.addAvatarToDatabase).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.feedStore.addFeed).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'Avatar',
|
||||||
|
userId: 'usr_1',
|
||||||
|
ownerId: 'usr_owner_new',
|
||||||
|
previousOwnerId: 'usr_owner_old',
|
||||||
|
avatarName: 'Avatar New',
|
||||||
|
previousAvatarName: 'Avatar Old',
|
||||||
|
currentAvatarImageUrl: 'img_new',
|
||||||
|
previousCurrentAvatarImageUrl: 'img_old'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { createUserSessionCoordinator } from '../coordinators/userSessionCoordinator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {Promise<void>} Promise flush helper.
|
||||||
|
*/
|
||||||
|
async function flushPromises() {
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @returns {object} Mock dependencies for user session coordinator.
|
||||||
|
*/
|
||||||
|
function makeDeps() {
|
||||||
|
return {
|
||||||
|
avatarStore: {
|
||||||
|
addAvatarToHistory: vi.fn(),
|
||||||
|
addAvatarWearTime: vi.fn()
|
||||||
|
},
|
||||||
|
gameStore: {
|
||||||
|
isGameRunning: false
|
||||||
|
},
|
||||||
|
groupStore: {
|
||||||
|
applyPresenceGroups: vi.fn()
|
||||||
|
},
|
||||||
|
instanceStore: {
|
||||||
|
applyQueuedInstance: vi.fn()
|
||||||
|
},
|
||||||
|
friendStore: {
|
||||||
|
updateUserCurrentStatus: vi.fn(),
|
||||||
|
updateFriendships: vi.fn()
|
||||||
|
},
|
||||||
|
authStore: {
|
||||||
|
loginComplete: vi.fn()
|
||||||
|
},
|
||||||
|
cachedUsers: {
|
||||||
|
clear: vi.fn()
|
||||||
|
},
|
||||||
|
currentUser: {
|
||||||
|
value: {
|
||||||
|
homeLocation: 'wrld_current'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
userDialog: {
|
||||||
|
value: {
|
||||||
|
visible: false,
|
||||||
|
id: '',
|
||||||
|
$homeLocationName: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getWorldName: vi.fn().mockResolvedValue('World Name'),
|
||||||
|
parseLocation: vi.fn((tag) => ({ tag })),
|
||||||
|
now: vi.fn(() => 111)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('createUserSessionCoordinator', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runAvatarSwapFlow does nothing when user is not logged in', () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
const coordinator = createUserSessionCoordinator(deps);
|
||||||
|
const ref = {
|
||||||
|
currentAvatar: 'avtr_old',
|
||||||
|
$previousAvatarSwapTime: null
|
||||||
|
};
|
||||||
|
|
||||||
|
coordinator.runAvatarSwapFlow({
|
||||||
|
json: { currentAvatar: 'avtr_new' },
|
||||||
|
ref,
|
||||||
|
isLoggedIn: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deps.avatarStore.addAvatarToHistory).not.toHaveBeenCalled();
|
||||||
|
expect(deps.avatarStore.addAvatarWearTime).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runAvatarSwapFlow applies avatar swap side effects when game is running', () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.gameStore.isGameRunning = true;
|
||||||
|
const coordinator = createUserSessionCoordinator(deps);
|
||||||
|
const ref = {
|
||||||
|
currentAvatar: 'avtr_old',
|
||||||
|
$previousAvatarSwapTime: null
|
||||||
|
};
|
||||||
|
|
||||||
|
coordinator.runAvatarSwapFlow({
|
||||||
|
json: { currentAvatar: 'avtr_new' },
|
||||||
|
ref,
|
||||||
|
isLoggedIn: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deps.avatarStore.addAvatarToHistory).toHaveBeenCalledWith(
|
||||||
|
'avtr_new'
|
||||||
|
);
|
||||||
|
expect(deps.avatarStore.addAvatarWearTime).toHaveBeenCalledWith(
|
||||||
|
'avtr_old'
|
||||||
|
);
|
||||||
|
expect(ref.$previousAvatarSwapTime).toBe(111);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runFirstLoginFlow clears cache, updates currentUser and completes login', () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.gameStore.isGameRunning = true;
|
||||||
|
const coordinator = createUserSessionCoordinator(deps);
|
||||||
|
const ref = {
|
||||||
|
id: 'usr_1',
|
||||||
|
$previousAvatarSwapTime: null
|
||||||
|
};
|
||||||
|
|
||||||
|
coordinator.runFirstLoginFlow(ref);
|
||||||
|
|
||||||
|
expect(deps.cachedUsers.clear).toHaveBeenCalledTimes(1);
|
||||||
|
expect(deps.currentUser.value).toBe(ref);
|
||||||
|
expect(deps.authStore.loginComplete).toHaveBeenCalledTimes(1);
|
||||||
|
expect(ref.$previousAvatarSwapTime).toBe(111);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runPostApplySyncFlow applies cross-store synchronization', () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
const coordinator = createUserSessionCoordinator(deps);
|
||||||
|
const ref = { queuedInstance: 'wrld_1:123' };
|
||||||
|
|
||||||
|
coordinator.runPostApplySyncFlow(ref);
|
||||||
|
|
||||||
|
expect(deps.groupStore.applyPresenceGroups).toHaveBeenCalledWith(ref);
|
||||||
|
expect(deps.instanceStore.applyQueuedInstance).toHaveBeenCalledWith(
|
||||||
|
'wrld_1:123'
|
||||||
|
);
|
||||||
|
expect(deps.friendStore.updateUserCurrentStatus).toHaveBeenCalledWith(
|
||||||
|
ref
|
||||||
|
);
|
||||||
|
expect(deps.friendStore.updateFriendships).toHaveBeenCalledWith(ref);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('runHomeLocationSyncFlow updates home location and visible dialog name', async () => {
|
||||||
|
const deps = makeDeps();
|
||||||
|
deps.currentUser.value.homeLocation = 'wrld_home';
|
||||||
|
deps.userDialog.value.visible = true;
|
||||||
|
deps.userDialog.value.id = 'usr_1';
|
||||||
|
const coordinator = createUserSessionCoordinator(deps);
|
||||||
|
const ref = {
|
||||||
|
id: 'usr_1',
|
||||||
|
homeLocation: 'wrld_home',
|
||||||
|
$homeLocation: { tag: 'wrld_other' }
|
||||||
|
};
|
||||||
|
|
||||||
|
coordinator.runHomeLocationSyncFlow(ref);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(deps.parseLocation).toHaveBeenCalledWith('wrld_home');
|
||||||
|
expect(ref.$homeLocation).toEqual({ tag: 'wrld_home' });
|
||||||
|
expect(deps.getWorldName).toHaveBeenCalledWith('wrld_home');
|
||||||
|
expect(deps.userDialog.value.$homeLocationName).toBe('World Name');
|
||||||
|
});
|
||||||
|
});
|
||||||
+66
-81
@@ -8,6 +8,8 @@ import Noty from 'noty';
|
|||||||
import { closeWebSocket, initWebsocket } from '../service/websocket';
|
import { closeWebSocket, initWebsocket } from '../service/websocket';
|
||||||
import { AppDebug } from '../service/appConfig';
|
import { AppDebug } from '../service/appConfig';
|
||||||
import { authRequest } from '../api';
|
import { authRequest } from '../api';
|
||||||
|
import { createAuthAutoLoginCoordinator } from './coordinators/authAutoLoginCoordinator';
|
||||||
|
import { createAuthCoordinator } from './coordinators/authCoordinator';
|
||||||
import { database } from '../service/database';
|
import { database } from '../service/database';
|
||||||
import { escapeTag } from '../shared/utils';
|
import { escapeTag } from '../shared/utils';
|
||||||
import { queryClient } from '../query';
|
import { queryClient } from '../query';
|
||||||
@@ -174,20 +176,7 @@ export const useAuthStore = defineStore('Auth', () => {
|
|||||||
})
|
})
|
||||||
}).show();
|
}).show();
|
||||||
}
|
}
|
||||||
userStore.setUserDialogVisible(false);
|
await authCoordinator.runLogoutFlow();
|
||||||
watchState.isLoggedIn = false;
|
|
||||||
watchState.isFriendsLoaded = false;
|
|
||||||
watchState.isFavoritesLoaded = false;
|
|
||||||
notificationStore.setNotificationInitStatus(false);
|
|
||||||
await updateStoredUser(userStore.currentUser);
|
|
||||||
webApiService.clearCookies();
|
|
||||||
loginForm.value.lastUserLoggedIn = '';
|
|
||||||
await configRepository.remove('lastUserLoggedIn');
|
|
||||||
// workerTimers.setTimeout(() => location.reload(), 500);
|
|
||||||
attemptingAutoLogin.value = false;
|
|
||||||
state.autoLoginAttempts.clear();
|
|
||||||
closeWebSocket();
|
|
||||||
queryClient.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -848,9 +837,7 @@ export const useAuthStore = defineStore('Auth', () => {
|
|||||||
} else if (json.requiresTwoFactorAuth) {
|
} else if (json.requiresTwoFactorAuth) {
|
||||||
promptTOTP();
|
promptTOTP();
|
||||||
} else {
|
} else {
|
||||||
updateLoopStore.setNextCurrentUserRefresh(420); // 7mins
|
authCoordinator.runLoginSuccessFlow(json);
|
||||||
userStore.applyCurrentUser(json);
|
|
||||||
initWebsocket();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -858,70 +845,7 @@ export const useAuthStore = defineStore('Auth', () => {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
async function handleAutoLogin() {
|
async function handleAutoLogin() {
|
||||||
if (attemptingAutoLogin.value) {
|
await authAutoLoginCoordinator.runHandleAutoLoginFlow();
|
||||||
return;
|
|
||||||
}
|
|
||||||
attemptingAutoLogin.value = true;
|
|
||||||
const user = await getSavedCredentials(
|
|
||||||
loginForm.value.lastUserLoggedIn
|
|
||||||
);
|
|
||||||
if (!user) {
|
|
||||||
attemptingAutoLogin.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (advancedSettingsStore.enablePrimaryPassword) {
|
|
||||||
console.error(
|
|
||||||
'Primary password is enabled, this disables auto login.'
|
|
||||||
);
|
|
||||||
attemptingAutoLogin.value = false;
|
|
||||||
handleLogoutEvent();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const attemptsInLastHour = Array.from(state.autoLoginAttempts).filter(
|
|
||||||
(timestamp) => timestamp > new Date().getTime() - 3600000
|
|
||||||
).length;
|
|
||||||
if (attemptsInLastHour >= 3) {
|
|
||||||
console.error(
|
|
||||||
'More than 3 auto login attempts within the past hour, logging out instead of attempting auto login.'
|
|
||||||
);
|
|
||||||
attemptingAutoLogin.value = false;
|
|
||||||
handleLogoutEvent();
|
|
||||||
AppApi.FlashWindow();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.autoLoginAttempts.add(new Date().getTime());
|
|
||||||
console.log('Attempting automatic login...');
|
|
||||||
relogin(user)
|
|
||||||
.then(() => {
|
|
||||||
if (AppDebug.errorNoty) {
|
|
||||||
AppDebug.errorNoty.close();
|
|
||||||
}
|
|
||||||
AppDebug.errorNoty = new Noty({
|
|
||||||
type: 'success',
|
|
||||||
text: t('message.auth.auto_login_success')
|
|
||||||
}).show();
|
|
||||||
console.log('Automatically logged in.');
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (AppDebug.errorNoty) {
|
|
||||||
AppDebug.errorNoty.close();
|
|
||||||
}
|
|
||||||
AppDebug.errorNoty = new Noty({
|
|
||||||
type: 'error',
|
|
||||||
text: t('message.auth.auto_login_failed')
|
|
||||||
}).show();
|
|
||||||
console.error('Failed to login automatically.', err);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
attemptingAutoLogin.value = false;
|
|
||||||
if (!navigator.onLine) {
|
|
||||||
AppDebug.errorNoty = new Noty({
|
|
||||||
type: 'error',
|
|
||||||
text: t('message.auth.offline')
|
|
||||||
}).show();
|
|
||||||
console.error(`You're offline.`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -964,15 +888,76 @@ export const useAuthStore = defineStore('Auth', () => {
|
|||||||
AppApi.CheckGameRunning(); // restore state from hot-reload
|
AppApi.CheckGameRunning(); // restore state from hot-reload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} value Latest config payload.
|
||||||
|
*/
|
||||||
function setCachedConfig(value) {
|
function setCachedConfig(value) {
|
||||||
cachedConfig.value = value;
|
cachedConfig.value = value;
|
||||||
state.cachedConfig = value;
|
state.cachedConfig = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} value Auto-login attempt flag.
|
||||||
|
*/
|
||||||
function setAttemptingAutoLogin(value) {
|
function setAttemptingAutoLogin(value) {
|
||||||
attemptingAutoLogin.value = value;
|
attemptingAutoLogin.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authAutoLoginCoordinator = createAuthAutoLoginCoordinator({
|
||||||
|
getIsAttemptingAutoLogin: () => attemptingAutoLogin.value,
|
||||||
|
setAttemptingAutoLogin,
|
||||||
|
getLastUserLoggedIn: () => loginForm.value.lastUserLoggedIn,
|
||||||
|
getSavedCredentials,
|
||||||
|
isPrimaryPasswordEnabled: () =>
|
||||||
|
advancedSettingsStore.enablePrimaryPassword,
|
||||||
|
handleLogoutEvent,
|
||||||
|
autoLoginAttempts: state.autoLoginAttempts,
|
||||||
|
relogin,
|
||||||
|
notifyAutoLoginSuccess: () => {
|
||||||
|
if (AppDebug.errorNoty) {
|
||||||
|
AppDebug.errorNoty.close();
|
||||||
|
}
|
||||||
|
AppDebug.errorNoty = new Noty({
|
||||||
|
type: 'success',
|
||||||
|
text: t('message.auth.auto_login_success')
|
||||||
|
}).show();
|
||||||
|
},
|
||||||
|
notifyAutoLoginFailed: () => {
|
||||||
|
if (AppDebug.errorNoty) {
|
||||||
|
AppDebug.errorNoty.close();
|
||||||
|
}
|
||||||
|
AppDebug.errorNoty = new Noty({
|
||||||
|
type: 'error',
|
||||||
|
text: t('message.auth.auto_login_failed')
|
||||||
|
}).show();
|
||||||
|
},
|
||||||
|
notifyOffline: () => {
|
||||||
|
AppDebug.errorNoty = new Noty({
|
||||||
|
type: 'error',
|
||||||
|
text: t('message.auth.offline')
|
||||||
|
}).show();
|
||||||
|
},
|
||||||
|
flashWindow: () => AppApi.FlashWindow(),
|
||||||
|
isOnline: () => navigator.onLine,
|
||||||
|
now: () => Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
const authCoordinator = createAuthCoordinator({
|
||||||
|
userStore,
|
||||||
|
notificationStore,
|
||||||
|
updateLoopStore,
|
||||||
|
initWebsocket,
|
||||||
|
updateStoredUser,
|
||||||
|
webApiService,
|
||||||
|
loginForm,
|
||||||
|
configRepository,
|
||||||
|
setAttemptingAutoLogin,
|
||||||
|
autoLoginAttempts: state.autoLoginAttempts,
|
||||||
|
closeWebSocket,
|
||||||
|
queryClient,
|
||||||
|
watchState
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* @param {object} deps Coordinator dependencies.
|
||||||
|
* @returns {object} Auto-login flow coordinator methods.
|
||||||
|
*/
|
||||||
|
export function createAuthAutoLoginCoordinator(deps) {
|
||||||
|
const {
|
||||||
|
getIsAttemptingAutoLogin,
|
||||||
|
setAttemptingAutoLogin,
|
||||||
|
getLastUserLoggedIn,
|
||||||
|
getSavedCredentials,
|
||||||
|
isPrimaryPasswordEnabled,
|
||||||
|
handleLogoutEvent,
|
||||||
|
autoLoginAttempts,
|
||||||
|
relogin,
|
||||||
|
notifyAutoLoginSuccess,
|
||||||
|
notifyAutoLoginFailed,
|
||||||
|
notifyOffline,
|
||||||
|
flashWindow,
|
||||||
|
isOnline,
|
||||||
|
now
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the full auto-login orchestration flow.
|
||||||
|
*/
|
||||||
|
async function runHandleAutoLoginFlow() {
|
||||||
|
if (getIsAttemptingAutoLogin()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setAttemptingAutoLogin(true);
|
||||||
|
const user = await getSavedCredentials(getLastUserLoggedIn());
|
||||||
|
if (!user) {
|
||||||
|
setAttemptingAutoLogin(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isPrimaryPasswordEnabled()) {
|
||||||
|
console.error(
|
||||||
|
'Primary password is enabled, this disables auto login.'
|
||||||
|
);
|
||||||
|
setAttemptingAutoLogin(false);
|
||||||
|
await handleLogoutEvent();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currentTimestamp = now();
|
||||||
|
const attemptsInLastHour = Array.from(autoLoginAttempts).filter(
|
||||||
|
(timestamp) => timestamp > currentTimestamp - 3600000
|
||||||
|
).length;
|
||||||
|
if (attemptsInLastHour >= 3) {
|
||||||
|
console.error(
|
||||||
|
'More than 3 auto login attempts within the past hour, logging out instead of attempting auto login.'
|
||||||
|
);
|
||||||
|
setAttemptingAutoLogin(false);
|
||||||
|
await handleLogoutEvent();
|
||||||
|
flashWindow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
autoLoginAttempts.add(currentTimestamp);
|
||||||
|
console.log('Attempting automatic login...');
|
||||||
|
relogin(user)
|
||||||
|
.then(() => {
|
||||||
|
notifyAutoLoginSuccess();
|
||||||
|
console.log('Automatically logged in.');
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
notifyAutoLoginFailed();
|
||||||
|
console.error('Failed to login automatically.', err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setAttemptingAutoLogin(false);
|
||||||
|
if (!isOnline()) {
|
||||||
|
notifyOffline();
|
||||||
|
console.error(`You're offline.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
runHandleAutoLoginFlow
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* @param {object} deps Coordinator dependencies.
|
||||||
|
* @returns {object} Auth flow coordinator methods.
|
||||||
|
*/
|
||||||
|
export function createAuthCoordinator(deps) {
|
||||||
|
const {
|
||||||
|
userStore,
|
||||||
|
notificationStore,
|
||||||
|
updateLoopStore,
|
||||||
|
initWebsocket,
|
||||||
|
updateStoredUser,
|
||||||
|
webApiService,
|
||||||
|
loginForm,
|
||||||
|
configRepository,
|
||||||
|
setAttemptingAutoLogin,
|
||||||
|
autoLoginAttempts,
|
||||||
|
closeWebSocket,
|
||||||
|
queryClient,
|
||||||
|
watchState
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the shared logout side effects.
|
||||||
|
*/
|
||||||
|
async function runLogoutFlow() {
|
||||||
|
userStore.setUserDialogVisible(false);
|
||||||
|
watchState.isLoggedIn = false;
|
||||||
|
watchState.isFriendsLoaded = false;
|
||||||
|
watchState.isFavoritesLoaded = false;
|
||||||
|
notificationStore.setNotificationInitStatus(false);
|
||||||
|
await updateStoredUser(userStore.currentUser);
|
||||||
|
webApiService.clearCookies();
|
||||||
|
loginForm.value.lastUserLoggedIn = '';
|
||||||
|
// workerTimers.setTimeout(() => location.reload(), 500);
|
||||||
|
await configRepository.remove('lastUserLoggedIn');
|
||||||
|
setAttemptingAutoLogin(false);
|
||||||
|
autoLoginAttempts.clear();
|
||||||
|
closeWebSocket();
|
||||||
|
queryClient.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs post-login side effects after a successful auth response.
|
||||||
|
* @param {object} json Current user payload from auth API.
|
||||||
|
*/
|
||||||
|
function runLoginSuccessFlow(json) {
|
||||||
|
updateLoopStore.setNextCurrentUserRefresh(420); // 7mins
|
||||||
|
userStore.applyCurrentUser(json);
|
||||||
|
initWebsocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
runLogoutFlow,
|
||||||
|
runLoginSuccessFlow
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
/**
|
||||||
|
* @param {object} deps Coordinator dependencies.
|
||||||
|
* @returns {object} Friend presence coordinator methods.
|
||||||
|
*/
|
||||||
|
export function createFriendPresenceCoordinator(deps) {
|
||||||
|
const {
|
||||||
|
friends,
|
||||||
|
localFavoriteFriends,
|
||||||
|
pendingOfflineMap,
|
||||||
|
pendingOfflineDelay,
|
||||||
|
watchState,
|
||||||
|
appDebug,
|
||||||
|
getCachedUsers,
|
||||||
|
isRealInstance,
|
||||||
|
requestUser,
|
||||||
|
getWorldName,
|
||||||
|
getGroupName,
|
||||||
|
feedStore,
|
||||||
|
database,
|
||||||
|
updateOnlineFriendCounter,
|
||||||
|
now,
|
||||||
|
nowIso
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} ctx
|
||||||
|
* @param {string} newState
|
||||||
|
* @param {string} location
|
||||||
|
* @param {number} $location_at
|
||||||
|
*/
|
||||||
|
async function runUpdateFriendDelayedCheckFlow(
|
||||||
|
ctx,
|
||||||
|
newState,
|
||||||
|
location,
|
||||||
|
$location_at
|
||||||
|
) {
|
||||||
|
let feed;
|
||||||
|
let groupName;
|
||||||
|
let worldName;
|
||||||
|
const id = ctx.id;
|
||||||
|
if (appDebug.debugFriendState) {
|
||||||
|
console.log(
|
||||||
|
`${ctx.name} updateFriendState ${ctx.state} -> ${newState}`
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
typeof ctx.ref !== 'undefined' &&
|
||||||
|
location !== ctx.ref.location
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`${ctx.name} pendingOfflineLocation ${location} -> ${ctx.ref.location}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!friends.has(id)) {
|
||||||
|
console.log('Friend not found', id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isVIP = localFavoriteFriends.has(id);
|
||||||
|
const ref = ctx.ref;
|
||||||
|
if (ctx.state !== newState && typeof ctx.ref !== 'undefined') {
|
||||||
|
if (
|
||||||
|
(newState === 'offline' || newState === 'active') &&
|
||||||
|
ctx.state === 'online'
|
||||||
|
) {
|
||||||
|
ctx.ref.$online_for = '';
|
||||||
|
ctx.ref.$offline_for = now();
|
||||||
|
ctx.ref.$active_for = '';
|
||||||
|
if (newState === 'active') {
|
||||||
|
ctx.ref.$active_for = now();
|
||||||
|
}
|
||||||
|
const ts = now();
|
||||||
|
const time = ts - $location_at;
|
||||||
|
worldName = await getWorldName(location);
|
||||||
|
groupName = await getGroupName(location);
|
||||||
|
feed = {
|
||||||
|
created_at: nowIso(),
|
||||||
|
type: 'Offline',
|
||||||
|
userId: ref.id,
|
||||||
|
displayName: ref.displayName,
|
||||||
|
location,
|
||||||
|
worldName,
|
||||||
|
groupName,
|
||||||
|
time
|
||||||
|
};
|
||||||
|
feedStore.addFeed(feed);
|
||||||
|
database.addOnlineOfflineToDatabase(feed);
|
||||||
|
} else if (
|
||||||
|
newState === 'online' &&
|
||||||
|
(ctx.state === 'offline' || ctx.state === 'active')
|
||||||
|
) {
|
||||||
|
ctx.ref.$previousLocation = '';
|
||||||
|
ctx.ref.$travelingToTime = now();
|
||||||
|
ctx.ref.$location_at = now();
|
||||||
|
ctx.ref.$online_for = now();
|
||||||
|
ctx.ref.$offline_for = '';
|
||||||
|
ctx.ref.$active_for = '';
|
||||||
|
worldName = await getWorldName(location);
|
||||||
|
groupName = await getGroupName(location);
|
||||||
|
feed = {
|
||||||
|
created_at: nowIso(),
|
||||||
|
type: 'Online',
|
||||||
|
userId: id,
|
||||||
|
displayName: ctx.name,
|
||||||
|
location,
|
||||||
|
worldName,
|
||||||
|
groupName,
|
||||||
|
time: ''
|
||||||
|
};
|
||||||
|
feedStore.addFeed(feed);
|
||||||
|
database.addOnlineOfflineToDatabase(feed);
|
||||||
|
}
|
||||||
|
if (newState === 'active') {
|
||||||
|
ctx.ref.$active_for = now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ctx.state !== newState) {
|
||||||
|
ctx.state = newState;
|
||||||
|
updateOnlineFriendCounter();
|
||||||
|
}
|
||||||
|
if (ref?.displayName) {
|
||||||
|
ctx.name = ref.displayName;
|
||||||
|
}
|
||||||
|
ctx.isVIP = isVIP;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles immediate friend presence updates and pending-offline orchestration.
|
||||||
|
* @param {string} id Friend id.
|
||||||
|
* @param {string | undefined} stateInput Optional incoming state.
|
||||||
|
*/
|
||||||
|
async function runUpdateFriendFlow(id, stateInput = undefined) {
|
||||||
|
const ctx = friends.get(id);
|
||||||
|
if (typeof ctx === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ref = getCachedUsers().get(id);
|
||||||
|
|
||||||
|
if (stateInput === 'online') {
|
||||||
|
const pendingOffline = pendingOfflineMap.get(id);
|
||||||
|
if (appDebug.debugFriendState && pendingOffline) {
|
||||||
|
const time = (now() - pendingOffline.startTime) / 1000;
|
||||||
|
console.log(`${ctx.name} pendingOfflineCancelTime ${time}`);
|
||||||
|
}
|
||||||
|
ctx.pendingOffline = false;
|
||||||
|
pendingOfflineMap.delete(id);
|
||||||
|
}
|
||||||
|
const isVIP = localFavoriteFriends.has(id);
|
||||||
|
let location = '';
|
||||||
|
let $location_at = undefined;
|
||||||
|
if (typeof ref !== 'undefined') {
|
||||||
|
location = ref.location;
|
||||||
|
$location_at = ref.$location_at;
|
||||||
|
|
||||||
|
const currentState = stateInput || ctx.state;
|
||||||
|
// wtf, fetch user if offline in an instance
|
||||||
|
if (
|
||||||
|
currentState !== 'online' &&
|
||||||
|
isRealInstance(ref.location) &&
|
||||||
|
ref.$lastFetch < now() - 10000 // 10 seconds
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`Fetching offline friend in an instance ${ctx.name}`
|
||||||
|
);
|
||||||
|
requestUser(id);
|
||||||
|
}
|
||||||
|
// wtf, fetch user if online in an offline location
|
||||||
|
if (
|
||||||
|
currentState === 'online' &&
|
||||||
|
ref.location === 'offline' &&
|
||||||
|
ref.$lastFetch < now() - 10000 // 10 seconds
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`Fetching online friend in an offline location ${ctx.name}`
|
||||||
|
);
|
||||||
|
requestUser(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof stateInput === 'undefined' || ctx.state === stateInput) {
|
||||||
|
// this is should be: undefined -> user
|
||||||
|
if (ctx.ref !== ref) {
|
||||||
|
ctx.ref = ref;
|
||||||
|
// NOTE
|
||||||
|
// AddFriend (CurrentUser) 이후,
|
||||||
|
// 서버에서 오는 순서라고 보면 될 듯.
|
||||||
|
if (ctx.state === 'online') {
|
||||||
|
if (watchState.isFriendsLoaded) {
|
||||||
|
requestUser(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ctx.isVIP !== isVIP) {
|
||||||
|
ctx.isVIP = isVIP;
|
||||||
|
}
|
||||||
|
if (typeof ref !== 'undefined' && ctx.name !== ref.displayName) {
|
||||||
|
ctx.name = ref.displayName;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
ctx.state === 'online' &&
|
||||||
|
(stateInput === 'active' || stateInput === 'offline')
|
||||||
|
) {
|
||||||
|
ctx.ref = ref;
|
||||||
|
ctx.isVIP = isVIP;
|
||||||
|
if (typeof ref !== 'undefined') {
|
||||||
|
ctx.name = ref.displayName;
|
||||||
|
}
|
||||||
|
if (!watchState.isFriendsLoaded) {
|
||||||
|
await runUpdateFriendDelayedCheckFlow(
|
||||||
|
ctx,
|
||||||
|
stateInput,
|
||||||
|
location,
|
||||||
|
$location_at
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// prevent status flapping
|
||||||
|
if (pendingOfflineMap.has(id)) {
|
||||||
|
if (appDebug.debugFriendState) {
|
||||||
|
console.log(ctx.name, 'pendingOfflineAlreadyWaiting');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (appDebug.debugFriendState) {
|
||||||
|
console.log(ctx.name, 'pendingOfflineBegin');
|
||||||
|
}
|
||||||
|
pendingOfflineMap.set(id, {
|
||||||
|
startTime: now(),
|
||||||
|
newState: stateInput,
|
||||||
|
previousLocation: location,
|
||||||
|
previousLocationAt: $location_at
|
||||||
|
});
|
||||||
|
ctx.pendingOffline = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ctx.ref = ref;
|
||||||
|
ctx.isVIP = isVIP;
|
||||||
|
if (typeof ref !== 'undefined') {
|
||||||
|
ctx.name = ref.displayName;
|
||||||
|
await runUpdateFriendDelayedCheckFlow(
|
||||||
|
ctx,
|
||||||
|
ctx.ref.state,
|
||||||
|
location,
|
||||||
|
$location_at
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes pending-offline entries and executes delayed transitions.
|
||||||
|
*/
|
||||||
|
async function runPendingOfflineTickFlow() {
|
||||||
|
const currentTime = now();
|
||||||
|
for (const [id, pending] of pendingOfflineMap.entries()) {
|
||||||
|
if (currentTime - pending.startTime >= pendingOfflineDelay) {
|
||||||
|
const ctx = friends.get(id);
|
||||||
|
if (typeof ctx === 'undefined') {
|
||||||
|
pendingOfflineMap.delete(id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ctx.pendingOffline = false;
|
||||||
|
if (pending.newState === ctx.state) {
|
||||||
|
console.error(
|
||||||
|
ctx.name,
|
||||||
|
'pendingOfflineCancelledStateMatched, this should never happen'
|
||||||
|
);
|
||||||
|
pendingOfflineMap.delete(id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (appDebug.debugFriendState) {
|
||||||
|
console.log(ctx.name, 'pendingOfflineEnd');
|
||||||
|
}
|
||||||
|
pendingOfflineMap.delete(id);
|
||||||
|
await runUpdateFriendDelayedCheckFlow(
|
||||||
|
ctx,
|
||||||
|
pending.newState,
|
||||||
|
pending.previousLocation,
|
||||||
|
pending.previousLocationAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
runUpdateFriendFlow,
|
||||||
|
runUpdateFriendDelayedCheckFlow,
|
||||||
|
runPendingOfflineTickFlow
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* @param {object} deps Coordinator dependencies.
|
||||||
|
* @returns {object} Friend relationship coordinator methods.
|
||||||
|
*/
|
||||||
|
export function createFriendRelationshipCoordinator(deps) {
|
||||||
|
const {
|
||||||
|
friendLog,
|
||||||
|
friendLogTable,
|
||||||
|
getCurrentUserId,
|
||||||
|
requestFriendStatus,
|
||||||
|
handleFriendStatus,
|
||||||
|
addFriendship,
|
||||||
|
deleteFriend,
|
||||||
|
database,
|
||||||
|
notificationStore,
|
||||||
|
sharedFeedStore,
|
||||||
|
favoriteStore,
|
||||||
|
uiStore,
|
||||||
|
shouldNotifyUnfriend,
|
||||||
|
nowIso
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates and applies unfriend transition side effects.
|
||||||
|
* @param {string} id User id.
|
||||||
|
*/
|
||||||
|
function runDeleteFriendshipFlow(id) {
|
||||||
|
const ctx = friendLog.get(id);
|
||||||
|
if (typeof ctx === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
requestFriendStatus({
|
||||||
|
userId: id,
|
||||||
|
currentUserId: getCurrentUserId()
|
||||||
|
}).then((args) => {
|
||||||
|
if (args.params.currentUserId !== getCurrentUserId()) {
|
||||||
|
// safety check for delayed response
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handleFriendStatus(args);
|
||||||
|
if (!args.json.isFriend && friendLog.has(id)) {
|
||||||
|
const friendLogHistory = {
|
||||||
|
created_at: nowIso(),
|
||||||
|
type: 'Unfriend',
|
||||||
|
userId: id,
|
||||||
|
displayName: ctx.displayName || id
|
||||||
|
};
|
||||||
|
friendLogTable.value.data.push(friendLogHistory);
|
||||||
|
database.addFriendLogHistory(friendLogHistory);
|
||||||
|
notificationStore.queueFriendLogNoty(friendLogHistory);
|
||||||
|
sharedFeedStore.addEntry(friendLogHistory);
|
||||||
|
friendLog.delete(id);
|
||||||
|
database.deleteFriendLogCurrent(id);
|
||||||
|
favoriteStore.handleFavoriteDelete(id);
|
||||||
|
if (shouldNotifyUnfriend()) {
|
||||||
|
uiStore.notifyMenu('friend-log');
|
||||||
|
}
|
||||||
|
deleteFriend(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reconciles current friend list against local friend log.
|
||||||
|
* @param {object} ref Current user reference.
|
||||||
|
*/
|
||||||
|
function runUpdateFriendshipsFlow(ref) {
|
||||||
|
let id;
|
||||||
|
const set = new Set();
|
||||||
|
for (id of ref.friends) {
|
||||||
|
set.add(id);
|
||||||
|
addFriendship(id);
|
||||||
|
}
|
||||||
|
for (id of friendLog.keys()) {
|
||||||
|
if (id === getCurrentUserId()) {
|
||||||
|
friendLog.delete(id);
|
||||||
|
database.deleteFriendLogCurrent(id);
|
||||||
|
} else if (!set.has(id)) {
|
||||||
|
runDeleteFriendshipFlow(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
runDeleteFriendshipFlow,
|
||||||
|
runUpdateFriendshipsFlow
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* @param {object} deps Coordinator dependencies.
|
||||||
|
* @returns {object} Friend sync coordinator methods.
|
||||||
|
*/
|
||||||
|
export function createFriendSyncCoordinator(deps) {
|
||||||
|
const {
|
||||||
|
getNextCurrentUserRefresh,
|
||||||
|
getCurrentUser,
|
||||||
|
refreshFriends,
|
||||||
|
reconnectWebSocket,
|
||||||
|
getCurrentUserId,
|
||||||
|
getCurrentUserRef,
|
||||||
|
setRefreshFriendsLoading,
|
||||||
|
setFriendsLoaded,
|
||||||
|
resetFriendLog,
|
||||||
|
isFriendLogInitialized,
|
||||||
|
getFriendLog,
|
||||||
|
initFriendLog,
|
||||||
|
isDontLogMeOut,
|
||||||
|
showLoadFailedToast,
|
||||||
|
handleLogoutEvent,
|
||||||
|
tryApplyFriendOrder,
|
||||||
|
getAllUserStats,
|
||||||
|
hasLegacyFriendLogData,
|
||||||
|
removeLegacyFeedTable,
|
||||||
|
migrateMemos,
|
||||||
|
migrateFriendLog
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs friend list refresh orchestration.
|
||||||
|
*/
|
||||||
|
async function runRefreshFriendsListFlow() {
|
||||||
|
// If we just got user less then 2 min before code call, don't call it again
|
||||||
|
if (getNextCurrentUserRefresh() < 300) {
|
||||||
|
await getCurrentUser();
|
||||||
|
}
|
||||||
|
await refreshFriends();
|
||||||
|
reconnectWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs full friend list initialization orchestration.
|
||||||
|
*/
|
||||||
|
async function runInitFriendsListFlow() {
|
||||||
|
const userId = getCurrentUserId();
|
||||||
|
setRefreshFriendsLoading(true);
|
||||||
|
setFriendsLoaded(false);
|
||||||
|
resetFriendLog();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentUser = getCurrentUserRef();
|
||||||
|
if (await isFriendLogInitialized(userId)) {
|
||||||
|
await getFriendLog(currentUser);
|
||||||
|
} else {
|
||||||
|
await initFriendLog(currentUser);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (!isDontLogMeOut()) {
|
||||||
|
showLoadFailedToast();
|
||||||
|
handleLogoutEvent();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tryApplyFriendOrder(); // once again
|
||||||
|
getAllUserStats(); // joinCount, lastSeen, timeSpent
|
||||||
|
|
||||||
|
// remove old data from json file and migrate to SQLite (July 2021)
|
||||||
|
if (await hasLegacyFriendLogData(userId)) {
|
||||||
|
removeLegacyFeedTable(userId);
|
||||||
|
migrateMemos();
|
||||||
|
migrateFriendLog(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
runRefreshFriendsListFlow,
|
||||||
|
runInitFriendsListFlow
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* @param {object} deps Coordinator dependencies.
|
||||||
|
* @returns {object} Game flow coordinator methods.
|
||||||
|
*/
|
||||||
|
export function createGameCoordinator(deps) {
|
||||||
|
const {
|
||||||
|
userStore,
|
||||||
|
instanceStore,
|
||||||
|
updateLoopStore,
|
||||||
|
locationStore,
|
||||||
|
gameLogStore,
|
||||||
|
vrStore,
|
||||||
|
avatarStore,
|
||||||
|
configRepository,
|
||||||
|
workerTimers,
|
||||||
|
checkVRChatDebugLogging,
|
||||||
|
autoVRChatCacheManagement,
|
||||||
|
checkIfGameCrashed,
|
||||||
|
getIsGameNoVR
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs shared side effects when game running state changes.
|
||||||
|
* @param {boolean} isGameRunning Whether VRChat is running.
|
||||||
|
*/
|
||||||
|
async function runGameRunningChangedFlow(isGameRunning) {
|
||||||
|
if (isGameRunning) {
|
||||||
|
userStore.markCurrentUserGameStarted();
|
||||||
|
} else {
|
||||||
|
await configRepository.setBool('isGameNoVR', getIsGameNoVR());
|
||||||
|
userStore.markCurrentUserGameStopped();
|
||||||
|
instanceStore.removeAllQueuedInstances();
|
||||||
|
autoVRChatCacheManagement();
|
||||||
|
checkIfGameCrashed();
|
||||||
|
updateLoopStore.setIpcTimeout(0);
|
||||||
|
avatarStore.addAvatarWearTime(userStore.currentUser.currentAvatar);
|
||||||
|
}
|
||||||
|
|
||||||
|
locationStore.lastLocationReset();
|
||||||
|
gameLogStore.clearNowPlaying();
|
||||||
|
vrStore.updateVRLastLocation();
|
||||||
|
workerTimers.setTimeout(() => checkVRChatDebugLogging(), 60000);
|
||||||
|
updateLoopStore.setNextDiscordUpdate(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
runGameRunningChangedFlow
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
/**
|
||||||
|
* @param {object} deps Coordinator dependencies.
|
||||||
|
* @returns {object} User event coordinator methods.
|
||||||
|
*/
|
||||||
|
export function createUserEventCoordinator(deps) {
|
||||||
|
const {
|
||||||
|
friendStore,
|
||||||
|
state,
|
||||||
|
parseLocation,
|
||||||
|
userDialog,
|
||||||
|
applyUserDialogLocation,
|
||||||
|
worldStore,
|
||||||
|
groupStore,
|
||||||
|
instanceStore,
|
||||||
|
appDebug,
|
||||||
|
getWorldName,
|
||||||
|
getGroupName,
|
||||||
|
feedStore,
|
||||||
|
database,
|
||||||
|
avatarStore,
|
||||||
|
generalSettingsStore,
|
||||||
|
checkNote,
|
||||||
|
now,
|
||||||
|
nowIso
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles user diff events and applies cross-store side effects.
|
||||||
|
* @param {object} ref Updated user reference.
|
||||||
|
* @param {object} props Changed props with [new, old] tuples.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function runHandleUserUpdateFlow(ref, props) {
|
||||||
|
let feed;
|
||||||
|
let newLocation;
|
||||||
|
let previousLocation;
|
||||||
|
const friend = friendStore.friends.get(ref.id);
|
||||||
|
if (typeof friend === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (props.location) {
|
||||||
|
// update instancePlayerCount
|
||||||
|
previousLocation = props.location[1];
|
||||||
|
newLocation = props.location[0];
|
||||||
|
let oldCount = state.instancePlayerCount.get(previousLocation);
|
||||||
|
if (typeof oldCount !== 'undefined') {
|
||||||
|
oldCount--;
|
||||||
|
if (oldCount <= 0) {
|
||||||
|
state.instancePlayerCount.delete(previousLocation);
|
||||||
|
} else {
|
||||||
|
state.instancePlayerCount.set(previousLocation, oldCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let newCount = state.instancePlayerCount.get(newLocation);
|
||||||
|
if (typeof newCount === 'undefined') {
|
||||||
|
newCount = 0;
|
||||||
|
}
|
||||||
|
newCount++;
|
||||||
|
state.instancePlayerCount.set(newLocation, newCount);
|
||||||
|
|
||||||
|
const previousLocationL = parseLocation(previousLocation);
|
||||||
|
const newLocationL = parseLocation(newLocation);
|
||||||
|
if (
|
||||||
|
previousLocationL.tag === userDialog.value.$location.tag ||
|
||||||
|
newLocationL.tag === userDialog.value.$location.tag
|
||||||
|
) {
|
||||||
|
// update user dialog instance occupants
|
||||||
|
applyUserDialogLocation(true);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
previousLocationL.worldId === worldStore.worldDialog.id ||
|
||||||
|
newLocationL.worldId === worldStore.worldDialog.id
|
||||||
|
) {
|
||||||
|
instanceStore.applyWorldDialogInstances();
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
previousLocationL.groupId === groupStore.groupDialog.id ||
|
||||||
|
newLocationL.groupId === groupStore.groupDialog.id
|
||||||
|
) {
|
||||||
|
instanceStore.applyGroupDialogInstances();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!props.state &&
|
||||||
|
props.location &&
|
||||||
|
props.location[0] !== 'offline' &&
|
||||||
|
props.location[0] !== '' &&
|
||||||
|
props.location[1] !== 'offline' &&
|
||||||
|
props.location[1] !== '' &&
|
||||||
|
props.location[0] !== 'traveling'
|
||||||
|
) {
|
||||||
|
// skip GPS if user is offline or traveling
|
||||||
|
previousLocation = props.location[1];
|
||||||
|
newLocation = props.location[0];
|
||||||
|
let time = props.location[2];
|
||||||
|
if (previousLocation === 'traveling' && ref.$previousLocation) {
|
||||||
|
previousLocation = ref.$previousLocation;
|
||||||
|
const travelTime = now() - ref.$travelingToTime;
|
||||||
|
time -= travelTime;
|
||||||
|
if (time < 0) {
|
||||||
|
time = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (appDebug.debugFriendState && previousLocation) {
|
||||||
|
console.log(
|
||||||
|
`${ref.displayName} GPS ${previousLocation} -> ${newLocation}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (previousLocation === 'offline') {
|
||||||
|
previousLocation = '';
|
||||||
|
}
|
||||||
|
if (!previousLocation) {
|
||||||
|
// no previous location
|
||||||
|
if (appDebug.debugFriendState) {
|
||||||
|
console.log(
|
||||||
|
ref.displayName,
|
||||||
|
'Ignoring GPS, no previous location',
|
||||||
|
newLocation
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (ref.$previousLocation === newLocation) {
|
||||||
|
// location traveled to is the same
|
||||||
|
ref.$location_at = now() - time;
|
||||||
|
} else {
|
||||||
|
const worldName = await getWorldName(newLocation);
|
||||||
|
const groupName = await getGroupName(newLocation);
|
||||||
|
feed = {
|
||||||
|
created_at: nowIso(),
|
||||||
|
type: 'GPS',
|
||||||
|
userId: ref.id,
|
||||||
|
displayName: ref.displayName,
|
||||||
|
location: newLocation,
|
||||||
|
worldName,
|
||||||
|
groupName,
|
||||||
|
previousLocation,
|
||||||
|
time
|
||||||
|
};
|
||||||
|
feedStore.addFeed(feed);
|
||||||
|
database.addGPSToDatabase(feed);
|
||||||
|
// clear previousLocation after GPS
|
||||||
|
ref.$previousLocation = '';
|
||||||
|
ref.$travelingToTime = now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
props.location &&
|
||||||
|
props.location[0] === 'traveling' &&
|
||||||
|
props.location[1] !== 'traveling'
|
||||||
|
) {
|
||||||
|
// store previous location when user is traveling
|
||||||
|
ref.$previousLocation = props.location[1];
|
||||||
|
ref.$travelingToTime = now();
|
||||||
|
}
|
||||||
|
let imageMatches = false;
|
||||||
|
if (
|
||||||
|
props.currentAvatarThumbnailImageUrl &&
|
||||||
|
props.currentAvatarThumbnailImageUrl[0] &&
|
||||||
|
props.currentAvatarThumbnailImageUrl[1] &&
|
||||||
|
props.currentAvatarThumbnailImageUrl[0] ===
|
||||||
|
props.currentAvatarThumbnailImageUrl[1]
|
||||||
|
) {
|
||||||
|
imageMatches = true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
(((props.currentAvatarImageUrl ||
|
||||||
|
props.currentAvatarThumbnailImageUrl) &&
|
||||||
|
!ref.profilePicOverride) ||
|
||||||
|
props.currentAvatarTags) &&
|
||||||
|
!imageMatches
|
||||||
|
) {
|
||||||
|
let currentAvatarImageUrl = '';
|
||||||
|
let previousCurrentAvatarImageUrl = '';
|
||||||
|
let currentAvatarThumbnailImageUrl = '';
|
||||||
|
let previousCurrentAvatarThumbnailImageUrl = '';
|
||||||
|
let currentAvatarTags = '';
|
||||||
|
let previousCurrentAvatarTags = '';
|
||||||
|
if (props.currentAvatarImageUrl) {
|
||||||
|
currentAvatarImageUrl = props.currentAvatarImageUrl[0];
|
||||||
|
previousCurrentAvatarImageUrl = props.currentAvatarImageUrl[1];
|
||||||
|
} else {
|
||||||
|
currentAvatarImageUrl = ref.currentAvatarImageUrl;
|
||||||
|
previousCurrentAvatarImageUrl = ref.currentAvatarImageUrl;
|
||||||
|
}
|
||||||
|
if (props.currentAvatarThumbnailImageUrl) {
|
||||||
|
currentAvatarThumbnailImageUrl =
|
||||||
|
props.currentAvatarThumbnailImageUrl[0];
|
||||||
|
previousCurrentAvatarThumbnailImageUrl =
|
||||||
|
props.currentAvatarThumbnailImageUrl[1];
|
||||||
|
} else {
|
||||||
|
currentAvatarThumbnailImageUrl =
|
||||||
|
ref.currentAvatarThumbnailImageUrl;
|
||||||
|
previousCurrentAvatarThumbnailImageUrl =
|
||||||
|
ref.currentAvatarThumbnailImageUrl;
|
||||||
|
}
|
||||||
|
if (props.currentAvatarTags) {
|
||||||
|
currentAvatarTags = props.currentAvatarTags[0];
|
||||||
|
previousCurrentAvatarTags = props.currentAvatarTags[1];
|
||||||
|
if (
|
||||||
|
ref.profilePicOverride &&
|
||||||
|
!props.currentAvatarThumbnailImageUrl
|
||||||
|
) {
|
||||||
|
// forget last seen avatar
|
||||||
|
ref.currentAvatarImageUrl = '';
|
||||||
|
ref.currentAvatarThumbnailImageUrl = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentAvatarTags = ref.currentAvatarTags;
|
||||||
|
previousCurrentAvatarTags = ref.currentAvatarTags;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
generalSettingsStore.logEmptyAvatars ||
|
||||||
|
ref.currentAvatarImageUrl
|
||||||
|
) {
|
||||||
|
let avatarInfo = {
|
||||||
|
ownerId: '',
|
||||||
|
avatarName: ''
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
avatarInfo = await avatarStore.getAvatarName(
|
||||||
|
currentAvatarImageUrl
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
let previousAvatarInfo = {
|
||||||
|
ownerId: '',
|
||||||
|
avatarName: ''
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
previousAvatarInfo = await avatarStore.getAvatarName(
|
||||||
|
previousCurrentAvatarImageUrl
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
feed = {
|
||||||
|
created_at: nowIso(),
|
||||||
|
type: 'Avatar',
|
||||||
|
userId: ref.id,
|
||||||
|
displayName: ref.displayName,
|
||||||
|
ownerId: avatarInfo.ownerId,
|
||||||
|
previousOwnerId: previousAvatarInfo.ownerId,
|
||||||
|
avatarName: avatarInfo.avatarName,
|
||||||
|
previousAvatarName: previousAvatarInfo.avatarName,
|
||||||
|
currentAvatarImageUrl,
|
||||||
|
currentAvatarThumbnailImageUrl,
|
||||||
|
previousCurrentAvatarImageUrl,
|
||||||
|
previousCurrentAvatarThumbnailImageUrl,
|
||||||
|
currentAvatarTags,
|
||||||
|
previousCurrentAvatarTags
|
||||||
|
};
|
||||||
|
feedStore.addFeed(feed);
|
||||||
|
database.addAvatarToDatabase(feed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if status is offline, ignore status and statusDescription
|
||||||
|
if (
|
||||||
|
(props.status &&
|
||||||
|
props.status[0] !== 'offline' &&
|
||||||
|
props.status[1] !== 'offline') ||
|
||||||
|
(!props.status && props.statusDescription)
|
||||||
|
) {
|
||||||
|
let status = '';
|
||||||
|
let previousStatus = '';
|
||||||
|
let statusDescription = '';
|
||||||
|
let previousStatusDescription = '';
|
||||||
|
if (props.status) {
|
||||||
|
if (props.status[0]) {
|
||||||
|
status = props.status[0];
|
||||||
|
}
|
||||||
|
if (props.status[1]) {
|
||||||
|
previousStatus = props.status[1];
|
||||||
|
}
|
||||||
|
} else if (ref.status) {
|
||||||
|
status = ref.status;
|
||||||
|
previousStatus = ref.status;
|
||||||
|
}
|
||||||
|
if (props.statusDescription) {
|
||||||
|
if (props.statusDescription[0]) {
|
||||||
|
statusDescription = props.statusDescription[0];
|
||||||
|
}
|
||||||
|
if (props.statusDescription[1]) {
|
||||||
|
previousStatusDescription = props.statusDescription[1];
|
||||||
|
}
|
||||||
|
} else if (ref.statusDescription) {
|
||||||
|
statusDescription = ref.statusDescription;
|
||||||
|
previousStatusDescription = ref.statusDescription;
|
||||||
|
}
|
||||||
|
feed = {
|
||||||
|
created_at: nowIso(),
|
||||||
|
type: 'Status',
|
||||||
|
userId: ref.id,
|
||||||
|
displayName: ref.displayName,
|
||||||
|
status,
|
||||||
|
statusDescription,
|
||||||
|
previousStatus,
|
||||||
|
previousStatusDescription
|
||||||
|
};
|
||||||
|
feedStore.addFeed(feed);
|
||||||
|
database.addStatusToDatabase(feed);
|
||||||
|
}
|
||||||
|
if (props.bio && props.bio[0] && props.bio[1]) {
|
||||||
|
let bio = '';
|
||||||
|
let previousBio = '';
|
||||||
|
if (props.bio[0]) {
|
||||||
|
bio = props.bio[0];
|
||||||
|
}
|
||||||
|
if (props.bio[1]) {
|
||||||
|
previousBio = props.bio[1];
|
||||||
|
}
|
||||||
|
feed = {
|
||||||
|
created_at: nowIso(),
|
||||||
|
type: 'Bio',
|
||||||
|
userId: ref.id,
|
||||||
|
displayName: ref.displayName,
|
||||||
|
bio,
|
||||||
|
previousBio
|
||||||
|
};
|
||||||
|
feedStore.addFeed(feed);
|
||||||
|
database.addBioToDatabase(feed);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
props.note &&
|
||||||
|
props.note[0] !== null &&
|
||||||
|
props.note[0] !== props.note[1]
|
||||||
|
) {
|
||||||
|
checkNote(ref.id, props.note[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
runHandleUserUpdateFlow
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* @param {object} deps Coordinator dependencies.
|
||||||
|
* @returns {object} User session coordinator methods.
|
||||||
|
*/
|
||||||
|
export function createUserSessionCoordinator(deps) {
|
||||||
|
const {
|
||||||
|
avatarStore,
|
||||||
|
gameStore,
|
||||||
|
groupStore,
|
||||||
|
instanceStore,
|
||||||
|
friendStore,
|
||||||
|
authStore,
|
||||||
|
cachedUsers,
|
||||||
|
currentUser,
|
||||||
|
userDialog,
|
||||||
|
getWorldName,
|
||||||
|
parseLocation,
|
||||||
|
now
|
||||||
|
} = deps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs avatar transition side effects for current user updates.
|
||||||
|
* @param {object} args Avatar transition context.
|
||||||
|
* @param {object} args.json Current user payload.
|
||||||
|
* @param {object} args.ref Current user state reference.
|
||||||
|
* @param {boolean} args.isLoggedIn Whether current user is already logged in.
|
||||||
|
*/
|
||||||
|
function runAvatarSwapFlow({ json, ref, isLoggedIn }) {
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (json.currentAvatar !== ref.currentAvatar) {
|
||||||
|
avatarStore.addAvatarToHistory(json.currentAvatar);
|
||||||
|
if (gameStore.isGameRunning) {
|
||||||
|
avatarStore.addAvatarWearTime(ref.currentAvatar);
|
||||||
|
ref.$previousAvatarSwapTime = now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs one-time side effects for first current-user hydration after login.
|
||||||
|
* @param {object} ref Current user state reference.
|
||||||
|
*/
|
||||||
|
function runFirstLoginFlow(ref) {
|
||||||
|
if (gameStore.isGameRunning) {
|
||||||
|
ref.$previousAvatarSwapTime = now();
|
||||||
|
}
|
||||||
|
cachedUsers.clear(); // clear before running applyUser
|
||||||
|
currentUser.value = ref;
|
||||||
|
authStore.loginComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs cross-store synchronization after current-user data is applied.
|
||||||
|
* @param {object} ref Current user state reference.
|
||||||
|
*/
|
||||||
|
function runPostApplySyncFlow(ref) {
|
||||||
|
groupStore.applyPresenceGroups(ref);
|
||||||
|
instanceStore.applyQueuedInstance(ref.queuedInstance);
|
||||||
|
friendStore.updateUserCurrentStatus(ref);
|
||||||
|
friendStore.updateFriendships(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Syncs home location derived state and visible dialog display name.
|
||||||
|
* @param {object} ref Current user state reference.
|
||||||
|
*/
|
||||||
|
function runHomeLocationSyncFlow(ref) {
|
||||||
|
if (ref.homeLocation === ref.$homeLocation?.tag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ref.$homeLocation = parseLocation(ref.homeLocation);
|
||||||
|
// apply home location name to user dialog
|
||||||
|
if (userDialog.value.visible && userDialog.value.id === ref.id) {
|
||||||
|
getWorldName(currentUser.value.homeLocation).then((worldName) => {
|
||||||
|
userDialog.value.$homeLocationName = worldName;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
runAvatarSwapFlow,
|
||||||
|
runFirstLoginFlow,
|
||||||
|
runPostApplySyncFlow,
|
||||||
|
runHomeLocationSyncFlow
|
||||||
|
};
|
||||||
|
}
|
||||||
+81
-334
@@ -18,6 +18,9 @@ import {
|
|||||||
} from '../shared/utils';
|
} from '../shared/utils';
|
||||||
import { friendRequest, userRequest } from '../api';
|
import { friendRequest, userRequest } from '../api';
|
||||||
import { AppDebug } from '../service/appConfig';
|
import { AppDebug } from '../service/appConfig';
|
||||||
|
import { createFriendPresenceCoordinator } from './coordinators/friendPresenceCoordinator';
|
||||||
|
import { createFriendRelationshipCoordinator } from './coordinators/friendRelationshipCoordinator';
|
||||||
|
import { createFriendSyncCoordinator } from './coordinators/friendSyncCoordinator';
|
||||||
import { database } from '../service/database';
|
import { database } from '../service/database';
|
||||||
import { reconnectWebSocket } from '../service/websocket';
|
import { reconnectWebSocket } from '../service/websocket';
|
||||||
import { useAppearanceSettingsStore } from './settings/appearance';
|
import { useAppearanceSettingsStore } from './settings/appearance';
|
||||||
@@ -377,264 +380,15 @@ export const useFriendStore = defineStore('Friend', () => {
|
|||||||
* @param {string?} stateInput
|
* @param {string?} stateInput
|
||||||
*/
|
*/
|
||||||
function updateFriend(id, stateInput = undefined) {
|
function updateFriend(id, stateInput = undefined) {
|
||||||
const ctx = friends.get(id);
|
friendPresenceCoordinator.runUpdateFriendFlow(id, stateInput);
|
||||||
if (typeof ctx === 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const ref = userStore.cachedUsers.get(id);
|
|
||||||
if (stateInput && typeof ref !== 'undefined') {
|
|
||||||
ctx.ref.state = stateInput;
|
|
||||||
}
|
|
||||||
if (stateInput === 'online') {
|
|
||||||
const pendingOffline = pendingOfflineMap.get(id);
|
|
||||||
if (AppDebug.debugFriendState && pendingOffline) {
|
|
||||||
const time = (Date.now() - pendingOffline.startTime) / 1000;
|
|
||||||
console.log(`${ctx.name} pendingOfflineCancelTime ${time}`);
|
|
||||||
}
|
|
||||||
ctx.pendingOffline = false;
|
|
||||||
pendingOfflineMap.delete(id);
|
|
||||||
}
|
|
||||||
const isVIP = localFavoriteFriends.has(id);
|
|
||||||
let location = '';
|
|
||||||
let $location_at = undefined;
|
|
||||||
if (typeof ref !== 'undefined') {
|
|
||||||
location = ref.location;
|
|
||||||
$location_at = ref.$location_at;
|
|
||||||
|
|
||||||
const currentState = stateInput || ctx.state;
|
|
||||||
// wtf, fetch user if offline in an instance
|
|
||||||
if (
|
|
||||||
currentState !== 'online' &&
|
|
||||||
isRealInstance(ref.location) &&
|
|
||||||
ref.$lastFetch < Date.now() - 10000 // 10 seconds
|
|
||||||
) {
|
|
||||||
console.log(
|
|
||||||
`Fetching offline friend in an instance ${ctx.name}`
|
|
||||||
);
|
|
||||||
userRequest.getUser({
|
|
||||||
userId: id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// wtf, fetch user if online in an offline location
|
|
||||||
if (
|
|
||||||
currentState === 'online' &&
|
|
||||||
ref.location === 'offline' &&
|
|
||||||
ref.$lastFetch < Date.now() - 10000 // 10 seconds
|
|
||||||
) {
|
|
||||||
console.log(
|
|
||||||
`Fetching online friend in an offline location ${ctx.name}`
|
|
||||||
);
|
|
||||||
userRequest.getUser({
|
|
||||||
userId: id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (typeof stateInput === 'undefined' || ctx.state === stateInput) {
|
|
||||||
// this is should be: undefined -> user
|
|
||||||
if (ctx.ref !== ref) {
|
|
||||||
ctx.ref = ref;
|
|
||||||
// NOTE
|
|
||||||
// AddFriend (CurrentUser) 이후,
|
|
||||||
// 서버에서 오는 순서라고 보면 될 듯.
|
|
||||||
if (ctx.state === 'online') {
|
|
||||||
if (watchState.isFriendsLoaded) {
|
|
||||||
userRequest.getUser({
|
|
||||||
userId: id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ctx.isVIP !== isVIP) {
|
|
||||||
ctx.isVIP = isVIP;
|
|
||||||
}
|
|
||||||
if (typeof ref !== 'undefined' && ctx.name !== ref.displayName) {
|
|
||||||
ctx.name = ref.displayName;
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
ctx.state === 'online' &&
|
|
||||||
(stateInput === 'active' || stateInput === 'offline')
|
|
||||||
) {
|
|
||||||
ctx.ref = ref;
|
|
||||||
ctx.isVIP = isVIP;
|
|
||||||
if (typeof ref !== 'undefined') {
|
|
||||||
ctx.name = ref.displayName;
|
|
||||||
}
|
|
||||||
if (!watchState.isFriendsLoaded) {
|
|
||||||
updateFriendDelayedCheck(
|
|
||||||
ctx,
|
|
||||||
stateInput,
|
|
||||||
location,
|
|
||||||
$location_at
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// prevent status flapping
|
|
||||||
if (pendingOfflineMap.has(id)) {
|
|
||||||
if (AppDebug.debugFriendState) {
|
|
||||||
console.log(ctx.name, 'pendingOfflineAlreadyWaiting');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (AppDebug.debugFriendState) {
|
|
||||||
console.log(ctx.name, 'pendingOfflineBegin');
|
|
||||||
}
|
|
||||||
pendingOfflineMap.set(id, {
|
|
||||||
startTime: Date.now(),
|
|
||||||
newState: stateInput,
|
|
||||||
previousLocation: location,
|
|
||||||
previousLocationAt: $location_at
|
|
||||||
});
|
|
||||||
ctx.pendingOffline = true;
|
|
||||||
} else {
|
|
||||||
ctx.ref = ref;
|
|
||||||
ctx.isVIP = isVIP;
|
|
||||||
if (typeof ref !== 'undefined') {
|
|
||||||
ctx.name = ref.displayName;
|
|
||||||
updateFriendDelayedCheck(
|
|
||||||
ctx,
|
|
||||||
ctx.ref.state,
|
|
||||||
location,
|
|
||||||
$location_at
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pendingOfflineWorkerFunction() {
|
async function pendingOfflineWorkerFunction() {
|
||||||
pendingOfflineWorker = workerTimers.setInterval(() => {
|
pendingOfflineWorker = workerTimers.setInterval(() => {
|
||||||
const now = Date.now();
|
friendPresenceCoordinator.runPendingOfflineTickFlow();
|
||||||
for (const [id, pending] of pendingOfflineMap.entries()) {
|
|
||||||
if (now - pending.startTime >= pendingOfflineDelay) {
|
|
||||||
const ctx = friends.get(id);
|
|
||||||
if (typeof ctx === 'undefined') {
|
|
||||||
pendingOfflineMap.delete(id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
ctx.pendingOffline = false;
|
|
||||||
if (pending.newState === ctx.state) {
|
|
||||||
console.error(
|
|
||||||
ctx.name,
|
|
||||||
'pendingOfflineCancelledStateMatched, this should never happen'
|
|
||||||
);
|
|
||||||
pendingOfflineMap.delete(id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (AppDebug.debugFriendState) {
|
|
||||||
console.log(ctx.name, 'pendingOfflineEnd');
|
|
||||||
}
|
|
||||||
pendingOfflineMap.delete(id);
|
|
||||||
updateFriendDelayedCheck(
|
|
||||||
ctx,
|
|
||||||
pending.newState,
|
|
||||||
pending.previousLocation,
|
|
||||||
pending.previousLocationAt
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Object} ctx
|
|
||||||
* @param {string} newState
|
|
||||||
* @param {string} location
|
|
||||||
* @param {number} $location_at
|
|
||||||
*/
|
|
||||||
async function updateFriendDelayedCheck(
|
|
||||||
ctx,
|
|
||||||
newState,
|
|
||||||
location,
|
|
||||||
$location_at
|
|
||||||
) {
|
|
||||||
let feed;
|
|
||||||
let groupName;
|
|
||||||
let worldName;
|
|
||||||
const id = ctx.id;
|
|
||||||
if (AppDebug.debugFriendState) {
|
|
||||||
console.log(
|
|
||||||
`${ctx.name} updateFriendState ${ctx.state} -> ${newState}`
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
typeof ctx.ref !== 'undefined' &&
|
|
||||||
location !== ctx.ref.location
|
|
||||||
) {
|
|
||||||
console.log(
|
|
||||||
`${ctx.name} pendingOfflineLocation ${location} -> ${ctx.ref.location}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!friends.has(id)) {
|
|
||||||
console.log('Friend not found', id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const isVIP = localFavoriteFriends.has(id);
|
|
||||||
const ref = ctx.ref;
|
|
||||||
if (ctx.state !== newState && typeof ctx.ref !== 'undefined') {
|
|
||||||
if (
|
|
||||||
(newState === 'offline' || newState === 'active') &&
|
|
||||||
ctx.state === 'online'
|
|
||||||
) {
|
|
||||||
ctx.ref.$online_for = '';
|
|
||||||
ctx.ref.$offline_for = Date.now();
|
|
||||||
ctx.ref.$active_for = '';
|
|
||||||
if (newState === 'active') {
|
|
||||||
ctx.ref.$active_for = Date.now();
|
|
||||||
}
|
|
||||||
const ts = Date.now();
|
|
||||||
const time = ts - $location_at;
|
|
||||||
worldName = await getWorldName(location);
|
|
||||||
groupName = await getGroupName(location);
|
|
||||||
feed = {
|
|
||||||
created_at: new Date().toJSON(),
|
|
||||||
type: 'Offline',
|
|
||||||
userId: ref.id,
|
|
||||||
displayName: ref.displayName,
|
|
||||||
location,
|
|
||||||
worldName,
|
|
||||||
groupName,
|
|
||||||
time
|
|
||||||
};
|
|
||||||
feedStore.addFeed(feed);
|
|
||||||
database.addOnlineOfflineToDatabase(feed);
|
|
||||||
} else if (
|
|
||||||
newState === 'online' &&
|
|
||||||
(ctx.state === 'offline' || ctx.state === 'active')
|
|
||||||
) {
|
|
||||||
ctx.ref.$previousLocation = '';
|
|
||||||
ctx.ref.$travelingToTime = Date.now();
|
|
||||||
ctx.ref.$location_at = Date.now();
|
|
||||||
ctx.ref.$online_for = Date.now();
|
|
||||||
ctx.ref.$offline_for = '';
|
|
||||||
ctx.ref.$active_for = '';
|
|
||||||
worldName = await getWorldName(location);
|
|
||||||
groupName = await getGroupName(location);
|
|
||||||
feed = {
|
|
||||||
created_at: new Date().toJSON(),
|
|
||||||
type: 'Online',
|
|
||||||
userId: id,
|
|
||||||
displayName: ctx.name,
|
|
||||||
location,
|
|
||||||
worldName,
|
|
||||||
groupName,
|
|
||||||
time: ''
|
|
||||||
};
|
|
||||||
feedStore.addFeed(feed);
|
|
||||||
database.addOnlineOfflineToDatabase(feed);
|
|
||||||
}
|
|
||||||
if (newState === 'active') {
|
|
||||||
ctx.ref.$active_for = Date.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ctx.state !== newState) {
|
|
||||||
ctx.state = newState;
|
|
||||||
updateOnlineFriendCounter();
|
|
||||||
}
|
|
||||||
if (ref?.displayName) {
|
|
||||||
ctx.name = ref.displayName;
|
|
||||||
}
|
|
||||||
ctx.isVIP = isVIP;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} id
|
* @param {string} id
|
||||||
*/
|
*/
|
||||||
@@ -903,12 +657,7 @@ export const useFriendStore = defineStore('Friend', () => {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function refreshFriendsList() {
|
async function refreshFriendsList() {
|
||||||
// If we just got user less then 2 min before code call, don't call it again
|
await friendSyncCoordinator.runRefreshFriendsListFlow();
|
||||||
if (updateLoopStore.nextCurrentUserRefresh < 300) {
|
|
||||||
await userStore.getCurrentUser();
|
|
||||||
}
|
|
||||||
await refreshFriends();
|
|
||||||
reconnectWebSocket();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateOnlineFriendCounter(forceUpdate = false) {
|
function updateOnlineFriendCounter(forceUpdate = false) {
|
||||||
@@ -1099,41 +848,7 @@ export const useFriendStore = defineStore('Friend', () => {
|
|||||||
* @param {string} id
|
* @param {string} id
|
||||||
*/
|
*/
|
||||||
function deleteFriendship(id) {
|
function deleteFriendship(id) {
|
||||||
const ctx = friendLog.get(id);
|
friendRelationshipCoordinator.runDeleteFriendshipFlow(id);
|
||||||
if (typeof ctx === 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
friendRequest
|
|
||||||
.getFriendStatus({
|
|
||||||
userId: id,
|
|
||||||
currentUserId: userStore.currentUser.id
|
|
||||||
})
|
|
||||||
.then((args) => {
|
|
||||||
if (args.params.currentUserId !== userStore.currentUser.id) {
|
|
||||||
// safety check for delayed response
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handleFriendStatus(args);
|
|
||||||
if (!args.json.isFriend && friendLog.has(id)) {
|
|
||||||
const friendLogHistory = {
|
|
||||||
created_at: new Date().toJSON(),
|
|
||||||
type: 'Unfriend',
|
|
||||||
userId: id,
|
|
||||||
displayName: ctx.displayName || id
|
|
||||||
};
|
|
||||||
friendLogTable.value.data.push(friendLogHistory);
|
|
||||||
database.addFriendLogHistory(friendLogHistory);
|
|
||||||
notificationStore.queueFriendLogNoty(friendLogHistory);
|
|
||||||
sharedFeedStore.addEntry(friendLogHistory);
|
|
||||||
friendLog.delete(id);
|
|
||||||
database.deleteFriendLogCurrent(id);
|
|
||||||
favoriteStore.handleFavoriteDelete(id);
|
|
||||||
if (!appearanceSettingsStore.hideUnfriends) {
|
|
||||||
uiStore.notifyMenu('friend-log');
|
|
||||||
}
|
|
||||||
deleteFriend(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1141,20 +856,7 @@ export const useFriendStore = defineStore('Friend', () => {
|
|||||||
* @param {object} ref
|
* @param {object} ref
|
||||||
*/
|
*/
|
||||||
function updateFriendships(ref) {
|
function updateFriendships(ref) {
|
||||||
let id;
|
friendRelationshipCoordinator.runUpdateFriendshipsFlow(ref);
|
||||||
const set = new Set();
|
|
||||||
for (id of ref.friends) {
|
|
||||||
set.add(id);
|
|
||||||
addFriendship(id);
|
|
||||||
}
|
|
||||||
for (id of friendLog.keys()) {
|
|
||||||
if (id === userStore.currentUser.id) {
|
|
||||||
friendLog.delete(id);
|
|
||||||
database.deleteFriendLogCurrent(id);
|
|
||||||
} else if (!set.has(id)) {
|
|
||||||
deleteFriendship(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1654,34 +1356,7 @@ export const useFriendStore = defineStore('Friend', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function initFriendsList() {
|
async function initFriendsList() {
|
||||||
const userId = userStore.currentUser.id;
|
await friendSyncCoordinator.runInitFriendsListFlow();
|
||||||
isRefreshFriendsLoading.value = true;
|
|
||||||
watchState.isFriendsLoaded = false;
|
|
||||||
friendLog = new Map();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (await configRepository.getBool(`friendLogInit_${userId}`)) {
|
|
||||||
await getFriendLog(userStore.currentUser);
|
|
||||||
} else {
|
|
||||||
await initFriendLog(userStore.currentUser);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (!AppDebug.dontLogMeOut) {
|
|
||||||
toast.error(t('message.friend.load_failed'));
|
|
||||||
authStore.handleLogoutEvent();
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tryApplyFriendOrder(); // once again
|
|
||||||
getAllUserStats(); // joinCount, lastSeen, timeSpent
|
|
||||||
|
|
||||||
// remove old data from json file and migrate to SQLite (July 2021)
|
|
||||||
if (await VRCXStorage.Get(`${userId}_friendLogUpdatedAt`)) {
|
|
||||||
VRCXStorage.Remove(`${userId}_feedTable`);
|
|
||||||
migrateMemos();
|
|
||||||
migrateFriendLog(userId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1691,6 +1366,78 @@ export const useFriendStore = defineStore('Friend', () => {
|
|||||||
isRefreshFriendsLoading.value = value;
|
isRefreshFriendsLoading.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const friendPresenceCoordinator = createFriendPresenceCoordinator({
|
||||||
|
friends,
|
||||||
|
localFavoriteFriends,
|
||||||
|
pendingOfflineMap,
|
||||||
|
pendingOfflineDelay,
|
||||||
|
watchState,
|
||||||
|
appDebug: AppDebug,
|
||||||
|
getCachedUsers: () => userStore.cachedUsers,
|
||||||
|
isRealInstance,
|
||||||
|
requestUser: (userId) =>
|
||||||
|
userRequest.getUser({
|
||||||
|
userId
|
||||||
|
}),
|
||||||
|
getWorldName,
|
||||||
|
getGroupName,
|
||||||
|
feedStore,
|
||||||
|
database,
|
||||||
|
updateOnlineFriendCounter,
|
||||||
|
now: () => Date.now(),
|
||||||
|
nowIso: () => new Date().toJSON()
|
||||||
|
});
|
||||||
|
|
||||||
|
const friendRelationshipCoordinator = createFriendRelationshipCoordinator({
|
||||||
|
friendLog,
|
||||||
|
friendLogTable,
|
||||||
|
getCurrentUserId: () => userStore.currentUser.id,
|
||||||
|
requestFriendStatus: (params) => friendRequest.getFriendStatus(params),
|
||||||
|
handleFriendStatus,
|
||||||
|
addFriendship,
|
||||||
|
deleteFriend,
|
||||||
|
database,
|
||||||
|
notificationStore,
|
||||||
|
sharedFeedStore,
|
||||||
|
favoriteStore,
|
||||||
|
uiStore,
|
||||||
|
shouldNotifyUnfriend: () => !appearanceSettingsStore.hideUnfriends,
|
||||||
|
nowIso: () => new Date().toJSON()
|
||||||
|
});
|
||||||
|
|
||||||
|
const friendSyncCoordinator = createFriendSyncCoordinator({
|
||||||
|
getNextCurrentUserRefresh: () => updateLoopStore.nextCurrentUserRefresh,
|
||||||
|
getCurrentUser: () => userStore.getCurrentUser(),
|
||||||
|
refreshFriends,
|
||||||
|
reconnectWebSocket,
|
||||||
|
getCurrentUserId: () => userStore.currentUser.id,
|
||||||
|
getCurrentUserRef: () => userStore.currentUser,
|
||||||
|
setRefreshFriendsLoading: (value) => {
|
||||||
|
isRefreshFriendsLoading.value = value;
|
||||||
|
},
|
||||||
|
setFriendsLoaded: (value) => {
|
||||||
|
watchState.isFriendsLoaded = value;
|
||||||
|
},
|
||||||
|
resetFriendLog: () => {
|
||||||
|
friendLog = new Map();
|
||||||
|
},
|
||||||
|
isFriendLogInitialized: (userId) =>
|
||||||
|
configRepository.getBool(`friendLogInit_${userId}`),
|
||||||
|
getFriendLog,
|
||||||
|
initFriendLog,
|
||||||
|
isDontLogMeOut: () => AppDebug.dontLogMeOut,
|
||||||
|
showLoadFailedToast: () => toast.error(t('message.friend.load_failed')),
|
||||||
|
handleLogoutEvent: () => authStore.handleLogoutEvent(),
|
||||||
|
tryApplyFriendOrder,
|
||||||
|
getAllUserStats,
|
||||||
|
hasLegacyFriendLogData: (userId) =>
|
||||||
|
VRCXStorage.Get(`${userId}_friendLogUpdatedAt`),
|
||||||
|
removeLegacyFeedTable: (userId) =>
|
||||||
|
VRCXStorage.Remove(`${userId}_feedTable`),
|
||||||
|
migrateMemos,
|
||||||
|
migrateFriendLog
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
|
|
||||||
|
|||||||
+54
-20
@@ -6,6 +6,7 @@ import {
|
|||||||
deleteVRChatCache as _deleteVRChatCache,
|
deleteVRChatCache as _deleteVRChatCache,
|
||||||
isRealInstance
|
isRealInstance
|
||||||
} from '../shared/utils';
|
} from '../shared/utils';
|
||||||
|
import { createGameCoordinator } from './coordinators/gameCoordinator';
|
||||||
import { database } from '../service/database';
|
import { database } from '../service/database';
|
||||||
import { useAdvancedSettingsStore } from './settings/advanced';
|
import { useAdvancedSettingsStore } from './settings/advanced';
|
||||||
import { useAvatarStore } from './avatar';
|
import { useAvatarStore } from './avatar';
|
||||||
@@ -56,12 +57,18 @@ export const useGameStore = defineStore('Game', () => {
|
|||||||
|
|
||||||
const isHmdAfk = ref(false);
|
const isHmdAfk = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
async function init() {
|
async function init() {
|
||||||
isGameNoVR.value = await configRepository.getBool('isGameNoVR');
|
isGameNoVR.value = await configRepository.getBool('isGameNoVR');
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} ref Avatar or world reference payload.
|
||||||
|
*/
|
||||||
async function deleteVRChatCache(ref) {
|
async function deleteVRChatCache(ref) {
|
||||||
await _deleteVRChatCache(ref);
|
await _deleteVRChatCache(ref);
|
||||||
getVRChatCacheSize();
|
getVRChatCacheSize();
|
||||||
@@ -69,12 +76,18 @@ export const useGameStore = defineStore('Game', () => {
|
|||||||
avatarStore.updateVRChatAvatarCache();
|
avatarStore.updateVRChatAvatarCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
function autoVRChatCacheManagement() {
|
function autoVRChatCacheManagement() {
|
||||||
if (advancedSettingsStore.autoSweepVRChatCache) {
|
if (advancedSettingsStore.autoSweepVRChatCache) {
|
||||||
sweepVRChatCache();
|
sweepVRChatCache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
async function sweepVRChatCache() {
|
async function sweepVRChatCache() {
|
||||||
try {
|
try {
|
||||||
const output = await AssetBundleManager.SweepCache();
|
const output = await AssetBundleManager.SweepCache();
|
||||||
@@ -87,6 +100,9 @@ export const useGameStore = defineStore('Game', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
function checkIfGameCrashed() {
|
function checkIfGameCrashed() {
|
||||||
if (!advancedSettingsStore.relaunchVRChatAfterCrash) {
|
if (!advancedSettingsStore.relaunchVRChatAfterCrash) {
|
||||||
return;
|
return;
|
||||||
@@ -118,6 +134,9 @@ export const useGameStore = defineStore('Game', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} location Last known location to relaunch.
|
||||||
|
*/
|
||||||
function restartCrashedGame(location) {
|
function restartCrashedGame(location) {
|
||||||
if (!isGameNoVR.value && !isSteamVRRunning.value) {
|
if (!isGameNoVR.value && !isSteamVRRunning.value) {
|
||||||
console.log("SteamVR isn't running, not relaunching VRChat");
|
console.log("SteamVR isn't running, not relaunching VRChat");
|
||||||
@@ -137,6 +156,9 @@ export const useGameStore = defineStore('Game', () => {
|
|||||||
launchStore.launchGame(location, '', isGameNoVR.value);
|
launchStore.launchGame(location, '', isGameNoVR.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
async function getVRChatCacheSize() {
|
async function getVRChatCacheSize() {
|
||||||
VRChatCacheSizeLoading.value = true;
|
VRChatCacheSizeLoading.value = true;
|
||||||
const totalCacheSize = 30;
|
const totalCacheSize = 30;
|
||||||
@@ -146,32 +168,34 @@ export const useGameStore = defineStore('Game', () => {
|
|||||||
VRChatCacheSizeLoading.value = false;
|
VRChatCacheSizeLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const gameCoordinator = createGameCoordinator({
|
||||||
|
userStore,
|
||||||
|
instanceStore,
|
||||||
|
updateLoopStore,
|
||||||
|
locationStore,
|
||||||
|
gameLogStore,
|
||||||
|
vrStore,
|
||||||
|
avatarStore,
|
||||||
|
configRepository,
|
||||||
|
workerTimers,
|
||||||
|
checkVRChatDebugLogging,
|
||||||
|
autoVRChatCacheManagement,
|
||||||
|
checkIfGameCrashed,
|
||||||
|
getIsGameNoVR: () => isGameNoVR.value
|
||||||
|
});
|
||||||
|
|
||||||
// use in C#
|
// use in C#
|
||||||
|
/**
|
||||||
|
* @param {boolean} isGameRunningArg Game running flag from IPC.
|
||||||
|
* @param {boolean} isSteamVRRunningArg SteamVR running flag from IPC.
|
||||||
|
*/
|
||||||
async function updateIsGameRunning(isGameRunningArg, isSteamVRRunningArg) {
|
async function updateIsGameRunning(isGameRunningArg, isSteamVRRunningArg) {
|
||||||
const avatarStore = useAvatarStore();
|
|
||||||
if (advancedSettingsStore.gameLogDisabled) {
|
if (advancedSettingsStore.gameLogDisabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isGameRunningArg !== isGameRunning.value) {
|
if (isGameRunningArg !== isGameRunning.value) {
|
||||||
isGameRunning.value = isGameRunningArg;
|
isGameRunning.value = isGameRunningArg;
|
||||||
if (isGameRunningArg) {
|
await gameCoordinator.runGameRunningChangedFlow(isGameRunningArg);
|
||||||
userStore.markCurrentUserGameStarted();
|
|
||||||
} else {
|
|
||||||
await configRepository.setBool('isGameNoVR', isGameNoVR.value);
|
|
||||||
userStore.markCurrentUserGameStopped();
|
|
||||||
instanceStore.removeAllQueuedInstances();
|
|
||||||
autoVRChatCacheManagement();
|
|
||||||
checkIfGameCrashed();
|
|
||||||
updateLoopStore.setIpcTimeout(0);
|
|
||||||
avatarStore.addAvatarWearTime(
|
|
||||||
userStore.currentUser.currentAvatar
|
|
||||||
);
|
|
||||||
}
|
|
||||||
locationStore.lastLocationReset();
|
|
||||||
gameLogStore.clearNowPlaying();
|
|
||||||
vrStore.updateVRLastLocation();
|
|
||||||
workerTimers.setTimeout(() => checkVRChatDebugLogging(), 60000);
|
|
||||||
updateLoopStore.setNextDiscordUpdate(0);
|
|
||||||
console.log(new Date(), 'isGameRunning', isGameRunningArg);
|
console.log(new Date(), 'isGameRunning', isGameRunningArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +207,9 @@ export const useGameStore = defineStore('Game', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// use in C#
|
// use in C#
|
||||||
|
/**
|
||||||
|
* @param {boolean} isHmdAfkArg HMD AFK flag from VR polling.
|
||||||
|
*/
|
||||||
function updateIsHmdAfk(isHmdAfkArg) {
|
function updateIsHmdAfk(isHmdAfkArg) {
|
||||||
if (isHmdAfkArg !== isHmdAfk.value) {
|
if (isHmdAfkArg !== isHmdAfk.value) {
|
||||||
isHmdAfk.value = isHmdAfkArg;
|
isHmdAfk.value = isHmdAfkArg;
|
||||||
@@ -191,12 +218,15 @@ export const useGameStore = defineStore('Game', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {boolean} value
|
* @param {boolean} value Whether game was launched in non-VR mode.
|
||||||
*/
|
*/
|
||||||
function setIsGameNoVR(value) {
|
function setIsGameNoVR(value) {
|
||||||
isGameNoVR.value = value;
|
isGameNoVR.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
async function checkVRChatDebugLogging() {
|
async function checkVRChatDebugLogging() {
|
||||||
if (advancedSettingsStore.gameLogDisabled) {
|
if (advancedSettingsStore.gameLogDisabled) {
|
||||||
return;
|
return;
|
||||||
@@ -241,6 +271,10 @@ export const useGameStore = defineStore('Game', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} key VRChat registry key.
|
||||||
|
* @returns {Promise<unknown>} Registry key value.
|
||||||
|
*/
|
||||||
async function getVRChatRegistryKey(key) {
|
async function getVRChatRegistryKey(key) {
|
||||||
if (LINUX) {
|
if (LINUX) {
|
||||||
return AppApi.GetVRChatRegistryKeyString(key);
|
return AppApi.GetVRChatRegistryKeyString(key);
|
||||||
|
|||||||
+53
-323
@@ -35,6 +35,8 @@ import {
|
|||||||
} from '../api';
|
} from '../api';
|
||||||
import { processBulk, request } from '../service/request';
|
import { processBulk, request } from '../service/request';
|
||||||
import { AppDebug } from '../service/appConfig';
|
import { AppDebug } from '../service/appConfig';
|
||||||
|
import { createUserEventCoordinator } from './coordinators/userEventCoordinator';
|
||||||
|
import { createUserSessionCoordinator } from './coordinators/userSessionCoordinator';
|
||||||
import { database } from '../service/database';
|
import { database } from '../service/database';
|
||||||
import { patchUserFromEvent } from '../query';
|
import { patchUserFromEvent } from '../query';
|
||||||
import { useAppearanceSettingsStore } from './settings/appearance';
|
import { useAppearanceSettingsStore } from './settings/appearance';
|
||||||
@@ -1144,301 +1146,7 @@ export const useUserStore = defineStore('User', () => {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function handleUserUpdate(ref, props) {
|
async function handleUserUpdate(ref, props) {
|
||||||
let feed;
|
await userEventCoordinator.runHandleUserUpdateFlow(ref, props);
|
||||||
let newLocation;
|
|
||||||
let previousLocation;
|
|
||||||
const friend = friendStore.friends.get(ref.id);
|
|
||||||
if (typeof friend === 'undefined') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (props.location) {
|
|
||||||
// update instancePlayerCount
|
|
||||||
previousLocation = props.location[1];
|
|
||||||
newLocation = props.location[0];
|
|
||||||
let oldCount = state.instancePlayerCount.get(previousLocation);
|
|
||||||
if (typeof oldCount !== 'undefined') {
|
|
||||||
oldCount--;
|
|
||||||
if (oldCount <= 0) {
|
|
||||||
state.instancePlayerCount.delete(previousLocation);
|
|
||||||
} else {
|
|
||||||
state.instancePlayerCount.set(previousLocation, oldCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let newCount = state.instancePlayerCount.get(newLocation);
|
|
||||||
if (typeof newCount === 'undefined') {
|
|
||||||
newCount = 0;
|
|
||||||
}
|
|
||||||
newCount++;
|
|
||||||
state.instancePlayerCount.set(newLocation, newCount);
|
|
||||||
|
|
||||||
const previousLocationL = parseLocation(previousLocation);
|
|
||||||
const newLocationL = parseLocation(newLocation);
|
|
||||||
if (
|
|
||||||
previousLocationL.tag === userDialog.value.$location.tag ||
|
|
||||||
newLocationL.tag === userDialog.value.$location.tag
|
|
||||||
) {
|
|
||||||
// update user dialog instance occupants
|
|
||||||
applyUserDialogLocation(true);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
previousLocationL.worldId === worldStore.worldDialog.id ||
|
|
||||||
newLocationL.worldId === worldStore.worldDialog.id
|
|
||||||
) {
|
|
||||||
instanceStore.applyWorldDialogInstances();
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
previousLocationL.groupId === groupStore.groupDialog.id ||
|
|
||||||
newLocationL.groupId === groupStore.groupDialog.id
|
|
||||||
) {
|
|
||||||
instanceStore.applyGroupDialogInstances();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!props.state &&
|
|
||||||
props.location &&
|
|
||||||
props.location[0] !== 'offline' &&
|
|
||||||
props.location[0] !== '' &&
|
|
||||||
props.location[1] !== 'offline' &&
|
|
||||||
props.location[1] !== '' &&
|
|
||||||
props.location[0] !== 'traveling'
|
|
||||||
) {
|
|
||||||
// skip GPS if user is offline or traveling
|
|
||||||
previousLocation = props.location[1];
|
|
||||||
newLocation = props.location[0];
|
|
||||||
let time = props.location[2];
|
|
||||||
if (previousLocation === 'traveling' && ref.$previousLocation) {
|
|
||||||
previousLocation = ref.$previousLocation;
|
|
||||||
const travelTime = Date.now() - ref.$travelingToTime;
|
|
||||||
time -= travelTime;
|
|
||||||
if (time < 0) {
|
|
||||||
time = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (AppDebug.debugFriendState && previousLocation) {
|
|
||||||
console.log(
|
|
||||||
`${ref.displayName} GPS ${previousLocation} -> ${newLocation}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (previousLocation === 'offline') {
|
|
||||||
previousLocation = '';
|
|
||||||
}
|
|
||||||
if (!previousLocation) {
|
|
||||||
// no previous location
|
|
||||||
if (AppDebug.debugFriendState) {
|
|
||||||
console.log(
|
|
||||||
ref.displayName,
|
|
||||||
'Ignoring GPS, no previous location',
|
|
||||||
newLocation
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (ref.$previousLocation === newLocation) {
|
|
||||||
// location traveled to is the same
|
|
||||||
ref.$location_at = Date.now() - time;
|
|
||||||
} else {
|
|
||||||
const worldName = await getWorldName(newLocation);
|
|
||||||
const groupName = await getGroupName(newLocation);
|
|
||||||
feed = {
|
|
||||||
created_at: new Date().toJSON(),
|
|
||||||
type: 'GPS',
|
|
||||||
userId: ref.id,
|
|
||||||
displayName: ref.displayName,
|
|
||||||
location: newLocation,
|
|
||||||
worldName,
|
|
||||||
groupName,
|
|
||||||
previousLocation,
|
|
||||||
time
|
|
||||||
};
|
|
||||||
feedStore.addFeed(feed);
|
|
||||||
database.addGPSToDatabase(feed);
|
|
||||||
// clear previousLocation after GPS
|
|
||||||
ref.$previousLocation = '';
|
|
||||||
ref.$travelingToTime = Date.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
props.location &&
|
|
||||||
props.location[0] === 'traveling' &&
|
|
||||||
props.location[1] !== 'traveling'
|
|
||||||
) {
|
|
||||||
// store previous location when user is traveling
|
|
||||||
ref.$previousLocation = props.location[1];
|
|
||||||
ref.$travelingToTime = Date.now();
|
|
||||||
}
|
|
||||||
let imageMatches = false;
|
|
||||||
if (
|
|
||||||
props.currentAvatarThumbnailImageUrl &&
|
|
||||||
props.currentAvatarThumbnailImageUrl[0] &&
|
|
||||||
props.currentAvatarThumbnailImageUrl[1] &&
|
|
||||||
props.currentAvatarThumbnailImageUrl[0] ===
|
|
||||||
props.currentAvatarThumbnailImageUrl[1]
|
|
||||||
) {
|
|
||||||
imageMatches = true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
(((props.currentAvatarImageUrl ||
|
|
||||||
props.currentAvatarThumbnailImageUrl) &&
|
|
||||||
!ref.profilePicOverride) ||
|
|
||||||
props.currentAvatarTags) &&
|
|
||||||
!imageMatches
|
|
||||||
) {
|
|
||||||
let currentAvatarImageUrl = '';
|
|
||||||
let previousCurrentAvatarImageUrl = '';
|
|
||||||
let currentAvatarThumbnailImageUrl = '';
|
|
||||||
let previousCurrentAvatarThumbnailImageUrl = '';
|
|
||||||
let currentAvatarTags = '';
|
|
||||||
let previousCurrentAvatarTags = '';
|
|
||||||
if (props.currentAvatarImageUrl) {
|
|
||||||
currentAvatarImageUrl = props.currentAvatarImageUrl[0];
|
|
||||||
previousCurrentAvatarImageUrl = props.currentAvatarImageUrl[1];
|
|
||||||
} else {
|
|
||||||
currentAvatarImageUrl = ref.currentAvatarImageUrl;
|
|
||||||
previousCurrentAvatarImageUrl = ref.currentAvatarImageUrl;
|
|
||||||
}
|
|
||||||
if (props.currentAvatarThumbnailImageUrl) {
|
|
||||||
currentAvatarThumbnailImageUrl =
|
|
||||||
props.currentAvatarThumbnailImageUrl[0];
|
|
||||||
previousCurrentAvatarThumbnailImageUrl =
|
|
||||||
props.currentAvatarThumbnailImageUrl[1];
|
|
||||||
} else {
|
|
||||||
currentAvatarThumbnailImageUrl =
|
|
||||||
ref.currentAvatarThumbnailImageUrl;
|
|
||||||
previousCurrentAvatarThumbnailImageUrl =
|
|
||||||
ref.currentAvatarThumbnailImageUrl;
|
|
||||||
}
|
|
||||||
if (props.currentAvatarTags) {
|
|
||||||
currentAvatarTags = props.currentAvatarTags[0];
|
|
||||||
previousCurrentAvatarTags = props.currentAvatarTags[1];
|
|
||||||
if (
|
|
||||||
ref.profilePicOverride &&
|
|
||||||
!props.currentAvatarThumbnailImageUrl
|
|
||||||
) {
|
|
||||||
// forget last seen avatar
|
|
||||||
ref.currentAvatarImageUrl = '';
|
|
||||||
ref.currentAvatarThumbnailImageUrl = '';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
currentAvatarTags = ref.currentAvatarTags;
|
|
||||||
previousCurrentAvatarTags = ref.currentAvatarTags;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
generalSettingsStore.logEmptyAvatars ||
|
|
||||||
ref.currentAvatarImageUrl
|
|
||||||
) {
|
|
||||||
let avatarInfo = {
|
|
||||||
ownerId: '',
|
|
||||||
avatarName: ''
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
avatarInfo = await avatarStore.getAvatarName(
|
|
||||||
currentAvatarImageUrl
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
let previousAvatarInfo = {
|
|
||||||
ownerId: '',
|
|
||||||
avatarName: ''
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
previousAvatarInfo = await avatarStore.getAvatarName(
|
|
||||||
previousCurrentAvatarImageUrl
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
feed = {
|
|
||||||
created_at: new Date().toJSON(),
|
|
||||||
type: 'Avatar',
|
|
||||||
userId: ref.id,
|
|
||||||
displayName: ref.displayName,
|
|
||||||
ownerId: avatarInfo.ownerId,
|
|
||||||
previousOwnerId: previousAvatarInfo.ownerId,
|
|
||||||
avatarName: avatarInfo.avatarName,
|
|
||||||
previousAvatarName: previousAvatarInfo.avatarName,
|
|
||||||
currentAvatarImageUrl,
|
|
||||||
currentAvatarThumbnailImageUrl,
|
|
||||||
previousCurrentAvatarImageUrl,
|
|
||||||
previousCurrentAvatarThumbnailImageUrl,
|
|
||||||
currentAvatarTags,
|
|
||||||
previousCurrentAvatarTags
|
|
||||||
};
|
|
||||||
feedStore.addFeed(feed);
|
|
||||||
database.addAvatarToDatabase(feed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if status is offline, ignore status and statusDescription
|
|
||||||
if (
|
|
||||||
(props.status &&
|
|
||||||
props.status[0] !== 'offline' &&
|
|
||||||
props.status[1] !== 'offline') ||
|
|
||||||
(!props.status && props.statusDescription)
|
|
||||||
) {
|
|
||||||
let status = '';
|
|
||||||
let previousStatus = '';
|
|
||||||
let statusDescription = '';
|
|
||||||
let previousStatusDescription = '';
|
|
||||||
if (props.status) {
|
|
||||||
if (props.status[0]) {
|
|
||||||
status = props.status[0];
|
|
||||||
}
|
|
||||||
if (props.status[1]) {
|
|
||||||
previousStatus = props.status[1];
|
|
||||||
}
|
|
||||||
} else if (ref.status) {
|
|
||||||
status = ref.status;
|
|
||||||
previousStatus = ref.status;
|
|
||||||
}
|
|
||||||
if (props.statusDescription) {
|
|
||||||
if (props.statusDescription[0]) {
|
|
||||||
statusDescription = props.statusDescription[0];
|
|
||||||
}
|
|
||||||
if (props.statusDescription[1]) {
|
|
||||||
previousStatusDescription = props.statusDescription[1];
|
|
||||||
}
|
|
||||||
} else if (ref.statusDescription) {
|
|
||||||
statusDescription = ref.statusDescription;
|
|
||||||
previousStatusDescription = ref.statusDescription;
|
|
||||||
}
|
|
||||||
feed = {
|
|
||||||
created_at: new Date().toJSON(),
|
|
||||||
type: 'Status',
|
|
||||||
userId: ref.id,
|
|
||||||
displayName: ref.displayName,
|
|
||||||
status,
|
|
||||||
statusDescription,
|
|
||||||
previousStatus,
|
|
||||||
previousStatusDescription
|
|
||||||
};
|
|
||||||
feedStore.addFeed(feed);
|
|
||||||
database.addStatusToDatabase(feed);
|
|
||||||
}
|
|
||||||
if (props.bio && props.bio[0] && props.bio[1]) {
|
|
||||||
let bio = '';
|
|
||||||
let previousBio = '';
|
|
||||||
if (props.bio[0]) {
|
|
||||||
bio = props.bio[0];
|
|
||||||
}
|
|
||||||
if (props.bio[1]) {
|
|
||||||
previousBio = props.bio[1];
|
|
||||||
}
|
|
||||||
feed = {
|
|
||||||
created_at: new Date().toJSON(),
|
|
||||||
type: 'Bio',
|
|
||||||
userId: ref.id,
|
|
||||||
displayName: ref.displayName,
|
|
||||||
bio,
|
|
||||||
previousBio
|
|
||||||
};
|
|
||||||
feedStore.addFeed(feed);
|
|
||||||
database.addBioToDatabase(feed);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
props.note &&
|
|
||||||
props.note[0] !== null &&
|
|
||||||
props.note[0] !== props.note[1]
|
|
||||||
) {
|
|
||||||
checkNote(ref.id, props.note[0]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1718,14 +1426,12 @@ export const useUserStore = defineStore('User', () => {
|
|||||||
function applyCurrentUser(json) {
|
function applyCurrentUser(json) {
|
||||||
authStore.setAttemptingAutoLogin(false);
|
authStore.setAttemptingAutoLogin(false);
|
||||||
let ref = currentUser.value;
|
let ref = currentUser.value;
|
||||||
|
userSessionCoordinator.runAvatarSwapFlow({
|
||||||
|
json,
|
||||||
|
ref,
|
||||||
|
isLoggedIn: watchState.isLoggedIn
|
||||||
|
});
|
||||||
if (watchState.isLoggedIn) {
|
if (watchState.isLoggedIn) {
|
||||||
if (json.currentAvatar !== ref.currentAvatar) {
|
|
||||||
avatarStore.addAvatarToHistory(json.currentAvatar);
|
|
||||||
if (gameStore.isGameRunning) {
|
|
||||||
avatarStore.addAvatarWearTime(ref.currentAvatar);
|
|
||||||
ref.$previousAvatarSwapTime = Date.now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const prop in json) {
|
for (const prop in json) {
|
||||||
if (typeof json[prop] !== 'undefined') {
|
if (typeof json[prop] !== 'undefined') {
|
||||||
ref[prop] = json[prop];
|
ref[prop] = json[prop];
|
||||||
@@ -1844,33 +1550,15 @@ export const useUserStore = defineStore('User', () => {
|
|||||||
$travelingToLocation: '',
|
$travelingToLocation: '',
|
||||||
...json
|
...json
|
||||||
};
|
};
|
||||||
if (gameStore.isGameRunning) {
|
userSessionCoordinator.runFirstLoginFlow(ref);
|
||||||
ref.$previousAvatarSwapTime = Date.now();
|
|
||||||
}
|
|
||||||
cachedUsers.clear(); // clear before running applyUser
|
|
||||||
currentUser.value = ref;
|
|
||||||
authStore.loginComplete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.$isVRCPlus = ref.tags.includes('system_supporter');
|
ref.$isVRCPlus = ref.tags.includes('system_supporter');
|
||||||
appearanceSettingsStore.applyUserTrustLevel(ref);
|
appearanceSettingsStore.applyUserTrustLevel(ref);
|
||||||
applyUserLanguage(ref);
|
applyUserLanguage(ref);
|
||||||
applyPresenceLocation(ref);
|
applyPresenceLocation(ref);
|
||||||
groupStore.applyPresenceGroups(ref);
|
userSessionCoordinator.runPostApplySyncFlow(ref);
|
||||||
instanceStore.applyQueuedInstance(ref.queuedInstance);
|
userSessionCoordinator.runHomeLocationSyncFlow(ref);
|
||||||
friendStore.updateUserCurrentStatus(ref);
|
|
||||||
friendStore.updateFriendships(ref);
|
|
||||||
if (ref.homeLocation !== ref.$homeLocation?.tag) {
|
|
||||||
ref.$homeLocation = parseLocation(ref.homeLocation);
|
|
||||||
// apply home location name to user dialog
|
|
||||||
if (userDialog.value.visible && userDialog.value.id === ref.id) {
|
|
||||||
getWorldName(currentUser.value.homeLocation).then(
|
|
||||||
(worldName) => {
|
|
||||||
userDialog.value.$homeLocationName = worldName;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// when isGameRunning use gameLog instead of API
|
// when isGameRunning use gameLog instead of API
|
||||||
const $location = parseLocation(locationStore.lastLocation.location);
|
const $location = parseLocation(locationStore.lastLocation.location);
|
||||||
@@ -2024,12 +1712,18 @@ export const useUserStore = defineStore('User', () => {
|
|||||||
currentUser.value.$travelingToTime = value;
|
currentUser.value.$travelingToTime = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
function markCurrentUserGameStarted() {
|
function markCurrentUserGameStarted() {
|
||||||
currentUser.value.$online_for = Date.now();
|
currentUser.value.$online_for = Date.now();
|
||||||
currentUser.value.$offline_for = '';
|
currentUser.value.$offline_for = '';
|
||||||
currentUser.value.$previousAvatarSwapTime = Date.now();
|
currentUser.value.$previousAvatarSwapTime = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
function markCurrentUserGameStopped() {
|
function markCurrentUserGameStopped() {
|
||||||
currentUser.value.$online_for = 0;
|
currentUser.value.$online_for = 0;
|
||||||
currentUser.value.$offline_for = Date.now();
|
currentUser.value.$offline_for = Date.now();
|
||||||
@@ -2055,6 +1749,42 @@ export const useUserStore = defineStore('User', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userSessionCoordinator = createUserSessionCoordinator({
|
||||||
|
avatarStore,
|
||||||
|
gameStore,
|
||||||
|
groupStore,
|
||||||
|
instanceStore,
|
||||||
|
friendStore,
|
||||||
|
authStore,
|
||||||
|
cachedUsers,
|
||||||
|
currentUser,
|
||||||
|
userDialog,
|
||||||
|
getWorldName,
|
||||||
|
parseLocation,
|
||||||
|
now: () => Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
const userEventCoordinator = createUserEventCoordinator({
|
||||||
|
friendStore,
|
||||||
|
state,
|
||||||
|
parseLocation,
|
||||||
|
userDialog,
|
||||||
|
applyUserDialogLocation,
|
||||||
|
worldStore,
|
||||||
|
groupStore,
|
||||||
|
instanceStore,
|
||||||
|
appDebug: AppDebug,
|
||||||
|
getWorldName,
|
||||||
|
getGroupName,
|
||||||
|
feedStore,
|
||||||
|
database,
|
||||||
|
avatarStore,
|
||||||
|
generalSettingsStore,
|
||||||
|
checkNote,
|
||||||
|
now: () => Date.now(),
|
||||||
|
nowIso: () => new Date().toJSON()
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user