diff --git a/src/coordinators/authAutoLoginCoordinator.js b/src/coordinators/authAutoLoginCoordinator.js index 07510862..b97acfa9 100644 --- a/src/coordinators/authAutoLoginCoordinator.js +++ b/src/coordinators/authAutoLoginCoordinator.js @@ -1,80 +1,82 @@ +import { toast } from 'vue-sonner'; +import { useI18n } from 'vue-i18n'; + +import { AppDebug } from '../service/appConfig'; +import { useAdvancedSettingsStore } from '../stores/settings/advanced'; +import { useAuthStore } from '../stores/auth'; + /** - * @param {object} deps Coordinator dependencies. - * @returns {object} Auto-login flow coordinator methods. + * Runs the full auto-login orchestration flow. + * @param {object} [options] Test seams. + * @param {function} [options.now] Timestamp provider. + * @param {function} [options.isOnline] Online-check provider. */ -export function createAuthAutoLoginCoordinator(deps) { - const { - getIsAttemptingAutoLogin, - setAttemptingAutoLogin, - getLastUserLoggedIn, - getSavedCredentials, - isPrimaryPasswordEnabled, - handleLogoutEvent, - autoLoginAttempts, - relogin, - notifyAutoLoginSuccess, - notifyAutoLoginFailed, - notifyOffline, - flashWindow, - isOnline, - now - } = deps; +export async function runHandleAutoLoginFlow({ + now = Date.now, + isOnline = () => navigator.onLine +} = {}) { + const authStore = useAuthStore(); + const advancedSettingsStore = useAdvancedSettingsStore(); + const { t } = useI18n(); - /** - * 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.`); - } - }); + if (authStore.attemptingAutoLogin) { + return; } - - return { - runHandleAutoLoginFlow - }; + authStore.setAttemptingAutoLogin(true); + const user = await authStore.getSavedCredentials( + authStore.loginForm.lastUserLoggedIn + ); + if (!user) { + authStore.setAttemptingAutoLogin(false); + return; + } + if (advancedSettingsStore.enablePrimaryPassword) { + console.error('Primary password is enabled, this disables auto login.'); + authStore.setAttemptingAutoLogin(false); + await authStore.handleLogoutEvent(); + return; + } + const currentTimestamp = now(); + const autoLoginAttempts = authStore.state.autoLoginAttempts; + 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.' + ); + authStore.setAttemptingAutoLogin(false); + await authStore.handleLogoutEvent(); + AppApi.FlashWindow(); + return; + } + autoLoginAttempts.add(currentTimestamp); + console.log('Attempting automatic login...'); + authStore + .relogin(user) + .then(() => { + if (AppDebug.errorNoty) { + toast.dismiss(AppDebug.errorNoty); + } + AppDebug.errorNoty = toast.success( + t('message.auth.auto_login_success') + ); + console.log('Automatically logged in.'); + }) + .catch((err) => { + if (AppDebug.errorNoty) { + toast.dismiss(AppDebug.errorNoty); + } + AppDebug.errorNoty = toast.error( + t('message.auth.auto_login_failed') + ); + console.error('Failed to login automatically.', err); + }) + .finally(() => { + authStore.setAttemptingAutoLogin(false); + if (!isOnline()) { + AppDebug.errorNoty = toast.error(t('message.auth.offline')); + console.error(`You're offline.`); + } + }); } diff --git a/src/coordinators/authCoordinator.js b/src/coordinators/authCoordinator.js index 72277691..b229902c 100644 --- a/src/coordinators/authCoordinator.js +++ b/src/coordinators/authCoordinator.js @@ -1,56 +1,61 @@ +import { useI18n } from 'vue-i18n'; + +import Noty from 'noty'; + +import { closeWebSocket, initWebsocket } from '../service/websocket'; +import { escapeTag } from '../shared/utils'; +import { queryClient } from '../queries'; +import { useAuthStore } from '../stores/auth'; +import { useNotificationStore } from '../stores/notification'; +import { useUpdateLoopStore } from '../stores/updateLoop'; +import { useUserStore } from '../stores/user'; +import { watchState } from '../service/watchState'; + +import configRepository from '../service/config'; +import webApiService from '../service/webapi'; + /** - * @param {object} deps Coordinator dependencies. - * @returns {object} Auth flow coordinator methods. + * Runs the shared logout side effects (including goodbye notification). */ -export function createAuthCoordinator(deps) { - const { - userStore, - notificationStore, - updateLoopStore, - initWebsocket, - updateStoredUser, - webApiService, - loginForm, - configRepository, - setAttemptingAutoLogin, - autoLoginAttempts, - closeWebSocket, - queryClient, - watchState - } = deps; +export async function runLogoutFlow() { + const authStore = useAuthStore(); + const userStore = useUserStore(); + const notificationStore = useNotificationStore(); + const { t } = useI18n(); - /** - * 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(); + if (watchState.isLoggedIn) { + new Noty({ + type: 'success', + text: t('message.auth.logout_greeting', { + name: `${escapeTag(userStore.currentUser.displayName)}` + }) + }).show(); } - /** - * 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 - }; + userStore.setUserDialogVisible(false); + watchState.isLoggedIn = false; + watchState.isFriendsLoaded = false; + watchState.isFavoritesLoaded = false; + notificationStore.setNotificationInitStatus(false); + await authStore.updateStoredUser(userStore.currentUser); + webApiService.clearCookies(); + authStore.loginForm.lastUserLoggedIn = ''; + await configRepository.remove('lastUserLoggedIn'); + authStore.setAttemptingAutoLogin(false); + authStore.state.autoLoginAttempts.clear(); + closeWebSocket(); + queryClient.clear(); +} + +/** + * Runs post-login side effects after a successful auth response. + * @param {object} json Current user payload from auth API. + */ +export function runLoginSuccessFlow(json) { + const updateLoopStore = useUpdateLoopStore(); + const userStore = useUserStore(); + + updateLoopStore.setNextCurrentUserRefresh(420); // 7mins + userStore.applyCurrentUser(json); + initWebsocket(); } diff --git a/src/coordinators/friendPresenceCoordinator.js b/src/coordinators/friendPresenceCoordinator.js index cb193ebc..b7dd6f46 100644 --- a/src/coordinators/friendPresenceCoordinator.js +++ b/src/coordinators/friendPresenceCoordinator.js @@ -1,289 +1,298 @@ +import { getGroupName, getWorldName, isRealInstance } from '../shared/utils'; +import { AppDebug } from '../service/appConfig'; +import { database } from '../service/database'; +import { useFeedStore } from '../stores/feed'; +import { useFriendStore } from '../stores/friend'; +import { useUserStore } from '../stores/user'; +import { userRequest } from '../api'; +import { watchState } from '../service/watchState'; + /** - * @param {object} deps Coordinator dependencies. - * @returns {object} Friend presence coordinator methods. + * @param {object} ctx + * @param {string} newState + * @param {string} location + * @param {number} $location_at + * @param {object} [options] Test seams. + * @param {function} [options.now] Timestamp provider. + * @param {function} [options.nowIso] ISO timestamp provider. */ -export function createFriendPresenceCoordinator(deps) { +export async function runUpdateFriendDelayedCheckFlow( + ctx, + newState, + location, + $location_at, + { now = Date.now, nowIso = () => new Date().toJSON() } = {} +) { + const friendStore = useFriendStore(); + const feedStore = useFeedStore(); + const { friends, localFavoriteFriends } = friendStore; + + 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; + friendStore.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. + * @param {object} [options] Test seams. + * @param {function} [options.now] Timestamp provider. + * @param {function} [options.nowIso] ISO timestamp provider. + */ +export async function runUpdateFriendFlow( + id, + stateInput = undefined, + { now = Date.now, nowIso = () => new Date().toJSON() } = {} +) { + const friendStore = useFriendStore(); + const userStore = useUserStore(); const { friends, localFavoriteFriends, pendingOfflineMap, - pendingOfflineDelay, - watchState, - appDebug, - getCachedUsers, - isRealInstance, - requestUser, - getWorldName, - getGroupName, - feedStore, - database, - updateOnlineFriendCounter, - now, - nowIso - } = deps; + pendingOfflineDelay + } = friendStore; - /** - * @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) { + const ctx = friends.get(id); + if (typeof ctx === 'undefined') { + return; + } + const ref = userStore.cachedUsers.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}`); + userRequest.getUser({ userId: id }); + } + // wtf, fetch user if online in an offline location + if ( + currentState === 'online' && + ref.location === 'offline' && + ref.$lastFetch < now() - 10000 // 10 seconds + ) { console.log( - `${ctx.name} updateFriendState ${ctx.state} -> ${newState}` + `Fetching online friend in an offline location ${ctx.name}` ); - if ( - typeof ctx.ref !== 'undefined' && - location !== ctx.ref.location - ) { - console.log( - `${ctx.name} pendingOfflineLocation ${location} -> ${ctx.ref.location}` - ); - } + userRequest.getUser({ userId: id }); } - 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(); + } + 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 }); } - 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 (ctx.isVIP !== isVIP) { + ctx.isVIP = isVIP; } - if (ref?.displayName) { + if (typeof ref !== 'undefined' && ctx.name !== ref.displayName) { ctx.name = ref.displayName; } - ctx.isVIP = isVIP; + return; } - - /** - * 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; - } + 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, - ctx.ref.state, + stateInput, location, - $location_at + $location_at, + { now, nowIso } + ); + 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, + { now, nowIso } + ); + } +} + +/** + * Processes pending-offline entries and executes delayed transitions. + * @param {object} [options] Test seams. + * @param {function} [options.now] Timestamp provider. + * @param {function} [options.nowIso] ISO timestamp provider. + */ +export async function runPendingOfflineTickFlow({ + now = Date.now, + nowIso = () => new Date().toJSON() +} = {}) { + const friendStore = useFriendStore(); + const { friends, pendingOfflineMap, pendingOfflineDelay } = friendStore; + + 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, + { now, nowIso } ); } } - - /** - * 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 - }; } diff --git a/src/coordinators/friendRelationshipCoordinator.js b/src/coordinators/friendRelationshipCoordinator.js index 0811680b..71b4cff2 100644 --- a/src/coordinators/friendRelationshipCoordinator.js +++ b/src/coordinators/friendRelationshipCoordinator.js @@ -1,43 +1,48 @@ -/** - * @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; +import { database } from '../service/database'; +import { friendRequest } from '../api'; +import { useAppearanceSettingsStore } from '../stores/settings/appearance'; +import { useFavoriteStore } from '../stores/favorite'; +import { useFriendStore } from '../stores/friend'; +import { useNotificationStore } from '../stores/notification'; +import { useSharedFeedStore } from '../stores/sharedFeed'; +import { useUiStore } from '../stores/ui'; +import { useUserStore } from '../stores/user'; - /** - * 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({ +/** + * Validates and applies unfriend transition side effects. + * @param {string} id User id. + * @param {object} [options] Test seams. + * @param {function} [options.nowIso] ISO timestamp provider. + */ +export function runDeleteFriendshipFlow( + id, + { nowIso = () => new Date().toJSON() } = {} +) { + const friendStore = useFriendStore(); + const userStore = useUserStore(); + const notificationStore = useNotificationStore(); + const sharedFeedStore = useSharedFeedStore(); + const favoriteStore = useFavoriteStore(); + const uiStore = useUiStore(); + const appearanceSettingsStore = useAppearanceSettingsStore(); + + const { friendLog, friendLogTable } = friendStore; + + const ctx = friendLog.get(id); + if (typeof ctx === 'undefined') { + return; + } + friendRequest + .getFriendStatus({ userId: id, - currentUserId: getCurrentUserId() - }).then((args) => { - if (args.params.currentUserId !== getCurrentUserId()) { + currentUserId: userStore.currentUser.id + }) + .then((args) => { + if (args.params.currentUserId !== userStore.currentUser.id) { // safety check for delayed response return; } - handleFriendStatus(args); + friendStore.handleFriendStatus(args); if (!args.json.isFriend && friendLog.has(id)) { const friendLogHistory = { created_at: nowIso(), @@ -45,44 +50,47 @@ export function createFriendRelationshipCoordinator(deps) { userId: id, displayName: ctx.displayName || id }; - friendLogTable.value.data.push(friendLogHistory); + friendLogTable.data.push(friendLogHistory); database.addFriendLogHistory(friendLogHistory); notificationStore.queueFriendLogNoty(friendLogHistory); sharedFeedStore.addEntry(friendLogHistory); friendLog.delete(id); database.deleteFriendLogCurrent(id); favoriteStore.handleFavoriteDelete(id); - if (shouldNotifyUnfriend()) { + if (!appearanceSettingsStore.hideUnfriends) { uiStore.notifyMenu('friend-log'); } - deleteFriend(id); + friendStore.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 - }; +} + +/** + * Reconciles current friend list against local friend log. + * @param {object} ref Current user reference. + * @param {object} [options] Test seams. + * @param {function} [options.nowIso] ISO timestamp provider. + */ +export function runUpdateFriendshipsFlow( + ref, + { nowIso = () => new Date().toJSON() } = {} +) { + const friendStore = useFriendStore(); + const userStore = useUserStore(); + const { friendLog } = friendStore; + + let id; + const set = new Set(); + for (id of ref.friends) { + set.add(id); + friendStore.addFriendship(id); + } + for (id of friendLog.keys()) { + if (id === userStore.currentUser.id) { + friendLog.delete(id); + database.deleteFriendLogCurrent(id); + } else if (!set.has(id)) { + runDeleteFriendshipFlow(id, { nowIso }); + } + } } diff --git a/src/coordinators/friendSyncCoordinator.js b/src/coordinators/friendSyncCoordinator.js index 927d43db..c07c9f49 100644 --- a/src/coordinators/friendSyncCoordinator.js +++ b/src/coordinators/friendSyncCoordinator.js @@ -1,81 +1,68 @@ +import { toast } from 'vue-sonner'; + +import { AppDebug } from '../service/appConfig'; +import { migrateMemos } from '../shared/utils'; +import { reconnectWebSocket } from '../service/websocket'; +import { useAuthStore } from '../stores/auth'; +import { useFriendStore } from '../stores/friend'; +import { useUpdateLoopStore } from '../stores/updateLoop'; +import { useUserStore } from '../stores/user'; +import { watchState } from '../service/watchState'; + +import configRepository from '../service/config'; + /** - * @param {object} deps Coordinator dependencies. - * @returns {object} Friend sync coordinator methods. + * Runs friend list refresh orchestration. */ -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; +export async function runRefreshFriendsListFlow() { + const updateLoopStore = useUpdateLoopStore(); + const userStore = useUserStore(); + const friendStore = useFriendStore(); - /** - * 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(); + // If we just got user less then 2 min before code call, don't call it again + if (updateLoopStore.nextCurrentUserRefresh < 300) { + await userStore.getCurrentUser(); + } + await friendStore.refreshFriends(); + reconnectWebSocket(); +} + +/** + * Runs full friend list initialization orchestration. + * @param t + */ +export async function runInitFriendsListFlow(t) { + const userStore = useUserStore(); + const friendStore = useFriendStore(); + const authStore = useAuthStore(); + + const userId = userStore.currentUser.id; + friendStore.isRefreshFriendsLoading = true; + watchState.isFriendsLoaded = false; + friendStore.resetFriendLog(); + + try { + const currentUser = userStore.currentUser; + if (await configRepository.getBool(`friendLogInit_${userId}`)) { + await friendStore.getFriendLog(currentUser); + } else { + await friendStore.initFriendLog(currentUser); + } + } catch (err) { + if (!AppDebug.dontLogMeOut) { + toast.error(t('message.friend.load_failed')); + authStore.handleLogoutEvent(); + throw err; + } + } + + friendStore.tryApplyFriendOrder(); // once again + friendStore.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(); + friendStore.migrateFriendLog(userId); } - - /** - * 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 - }; } diff --git a/src/coordinators/gameCoordinator.js b/src/coordinators/gameCoordinator.js index 694e2193..cb183dd5 100644 --- a/src/coordinators/gameCoordinator.js +++ b/src/coordinators/gameCoordinator.js @@ -1,49 +1,45 @@ +import { useAvatarStore } from '../stores/avatar'; +import { useGameLogStore } from '../stores/gameLog'; +import { useGameStore } from '../stores/game'; +import { useInstanceStore } from '../stores/instance'; +import { useLocationStore } from '../stores/location'; +import { useUpdateLoopStore } from '../stores/updateLoop'; +import { useUserStore } from '../stores/user'; +import { useVrStore } from '../stores/vr'; + +import configRepository from '../service/config'; + +import * as workerTimers from 'worker-timers'; + /** - * @param {object} deps Coordinator dependencies. - * @returns {object} Game flow coordinator methods. + * Runs shared side effects when game running state changes. + * @param {boolean} isGameRunning Whether VRChat is running. */ -export function createGameCoordinator(deps) { - const { - userStore, - instanceStore, - updateLoopStore, - locationStore, - gameLogStore, - vrStore, - avatarStore, - configRepository, - workerTimers, - checkVRChatDebugLogging, - autoVRChatCacheManagement, - checkIfGameCrashed, - getIsGameNoVR - } = deps; +export async function runGameRunningChangedFlow(isGameRunning) { + const userStore = useUserStore(); + const instanceStore = useInstanceStore(); + const updateLoopStore = useUpdateLoopStore(); + const locationStore = useLocationStore(); + const gameLogStore = useGameLogStore(); + const vrStore = useVrStore(); + const avatarStore = useAvatarStore(); + const gameStore = useGameStore(); - /** - * 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); + if (isGameRunning) { + userStore.markCurrentUserGameStarted(); + } else { + await configRepository.setBool('isGameNoVR', gameStore.isGameNoVR); + userStore.markCurrentUserGameStopped(); + instanceStore.removeAllQueuedInstances(); + gameStore.autoVRChatCacheManagement(); + gameStore.checkIfGameCrashed(); + updateLoopStore.setIpcTimeout(0); + avatarStore.addAvatarWearTime(userStore.currentUser.currentAvatar); } - return { - runGameRunningChangedFlow - }; + locationStore.lastLocationReset(); + gameLogStore.clearNowPlaying(); + vrStore.updateVRLastLocation(); + workerTimers.setTimeout(() => gameStore.checkVRChatDebugLogging(), 60000); + updateLoopStore.setNextDiscordUpdate(0); } diff --git a/src/coordinators/userEventCoordinator.js b/src/coordinators/userEventCoordinator.js index 852c4ddf..8cd127d3 100644 --- a/src/coordinators/userEventCoordinator.js +++ b/src/coordinators/userEventCoordinator.js @@ -1,334 +1,329 @@ +import { getGroupName, getWorldName, parseLocation } from '../shared/utils'; +import { AppDebug } from '../service/appConfig'; +import { database } from '../service/database'; +import { useAvatarStore } from '../stores/avatar'; +import { useFeedStore } from '../stores/feed'; +import { useFriendStore } from '../stores/friend'; +import { useGeneralSettingsStore } from '../stores/settings/general'; +import { useGroupStore } from '../stores/group'; +import { useInstanceStore } from '../stores/instance'; +import { useUserStore } from '../stores/user'; +import { useWorldStore } from '../stores/world'; + /** - * @param {object} deps Coordinator dependencies. - * @returns {object} User event coordinator methods. + * 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. + * @param {object} [options] Test seams. + * @param {function} [options.now] Timestamp provider. + * @param {function} [options.nowIso] ISO timestamp provider. + * @returns {Promise} */ -export function createUserEventCoordinator(deps) { - const { - friendStore, - state, - parseLocation, - userDialog, - applyUserDialogLocation, - worldStore, - groupStore, - instanceStore, - appDebug, - getWorldName, - getGroupName, - feedStore, - database, - avatarStore, - generalSettingsStore, - checkNote, - now, - nowIso - } = deps; +export async function runHandleUserUpdateFlow( + ref, + props, + { now = Date.now, nowIso = () => new Date().toJSON() } = {} +) { + const friendStore = useFriendStore(); + const userStore = useUserStore(); + const worldStore = useWorldStore(); + const groupStore = useGroupStore(); + const instanceStore = useInstanceStore(); + const feedStore = useFeedStore(); + const avatarStore = useAvatarStore(); + const generalSettingsStore = useGeneralSettingsStore(); - /** - * 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} - */ - 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 { state, userDialog, applyUserDialogLocation, checkNote } = userStore; - 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; + 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 { - 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(); + 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.$location.tag || + newLocationL.tag === userDialog.$location.tag + ) { + // update user dialog instance occupants + applyUserDialogLocation(true); + } if ( - props.location && - props.location[0] === 'traveling' && - props.location[1] !== 'traveling' + previousLocationL.worldId === worldStore.worldDialog.id || + newLocationL.worldId === worldStore.worldDialog.id ) { - // 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; + instanceStore.applyWorldDialogInstances(); } if ( - (((props.currentAvatarImageUrl || - props.currentAvatarThumbnailImageUrl) && - !ref.profilePicOverride) || - props.currentAvatarTags) && - !imageMatches + previousLocationL.groupId === groupStore.groupDialog.id || + newLocationL.groupId === groupStore.groupDialog.id ) { - 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]); + instanceStore.applyGroupDialogInstances(); } } - - return { - runHandleUserUpdateFlow - }; + 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]); + } } diff --git a/src/coordinators/userSessionCoordinator.js b/src/coordinators/userSessionCoordinator.js index 4e77d0a2..377c3ac0 100644 --- a/src/coordinators/userSessionCoordinator.js +++ b/src/coordinators/userSessionCoordinator.js @@ -1,88 +1,92 @@ +import { getWorldName, parseLocation } from '../shared/utils'; +import { runUpdateFriendshipsFlow } from './friendRelationshipCoordinator'; +import { useAuthStore } from '../stores/auth'; +import { useAvatarStore } from '../stores/avatar'; +import { useFriendStore } from '../stores/friend'; +import { useGameStore } from '../stores/game'; +import { useGroupStore } from '../stores/group'; +import { useInstanceStore } from '../stores/instance'; +import { useUserStore } from '../stores/user'; + /** - * @param {object} deps Coordinator dependencies. - * @returns {object} User session coordinator methods. + * 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. + * @param {object} [options] Test seams. + * @param {function} [options.now] Timestamp provider. */ -export function createUserSessionCoordinator(deps) { - const { - avatarStore, - gameStore, - groupStore, - instanceStore, - friendStore, - authStore, - cachedUsers, - currentUser, - userDialog, - getWorldName, - parseLocation, - now - } = deps; +export function runAvatarSwapFlow( + { json, ref, isLoggedIn }, + { now = Date.now } = {} +) { + const avatarStore = useAvatarStore(); + const gameStore = useGameStore(); - /** - * 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(); - } - } + if (!isLoggedIn) { + return; } - - /** - * Runs one-time side effects for first current-user hydration after login. - * @param {object} ref Current user state reference. - */ - function runFirstLoginFlow(ref) { + if (json.currentAvatar !== ref.currentAvatar) { + avatarStore.addAvatarToHistory(json.currentAvatar); if (gameStore.isGameRunning) { + avatarStore.addAvatarWearTime(ref.currentAvatar); 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 - }; +} + +/** + * Runs one-time side effects for first current-user hydration after login. + * @param {object} ref Current user state reference. + * @param {object} [options] Test seams. + * @param {function} [options.now] Timestamp provider. + */ +export function runFirstLoginFlow(ref, { now = Date.now } = {}) { + const gameStore = useGameStore(); + const authStore = useAuthStore(); + const userStore = useUserStore(); + + if (gameStore.isGameRunning) { + ref.$previousAvatarSwapTime = now(); + } + userStore.cachedUsers.clear(); // clear before running applyUser + userStore.currentUser = ref; + authStore.loginComplete(); +} + +/** + * Runs cross-store synchronization after current-user data is applied. + * @param {object} ref Current user state reference. + */ +export function runPostApplySyncFlow(ref) { + const groupStore = useGroupStore(); + const instanceStore = useInstanceStore(); + const friendStore = useFriendStore(); + + groupStore.applyPresenceGroups(ref); + instanceStore.applyQueuedInstance(ref.queuedInstance); + friendStore.updateUserCurrentStatus(ref); + if (typeof ref.friends !== 'undefined') { + runUpdateFriendshipsFlow(ref); + } +} + +/** + * Syncs home location derived state and visible dialog display name. + * @param {object} ref Current user state reference. + */ +export function runHomeLocationSyncFlow(ref) { + const userStore = useUserStore(); + + if (ref.homeLocation === ref.$homeLocation?.tag) { + return; + } + ref.$homeLocation = parseLocation(ref.homeLocation); + // apply home location name to user dialog + if (userStore.userDialog.visible && userStore.userDialog.id === ref.id) { + getWorldName(userStore.currentUser.homeLocation).then((worldName) => { + userStore.userDialog.$homeLocationName = worldName; + }); + } } diff --git a/src/service/websocket.js b/src/service/websocket.js index 4c262c5f..30648469 100644 --- a/src/service/websocket.js +++ b/src/service/websocket.js @@ -1,5 +1,4 @@ import { reactive } from 'vue'; - import { toast } from 'vue-sonner'; import { @@ -17,6 +16,7 @@ import { escapeTag, parseLocation } from '../shared/utils'; import { AppDebug } from './appConfig'; import { groupRequest } from '../api'; import { request } from './request'; +import { runUpdateFriendFlow } from '../coordinators/friendPresenceCoordinator'; import { watchState } from './watchState'; import * as workerTimers from 'worker-timers'; @@ -300,7 +300,7 @@ function handlePipeline(args) { userStore.applyUser(onlineJson); } else { console.error('friend-online missing user id', content); - friendStore.updateFriend(content.userId, 'online'); + runUpdateFriendFlow(content.userId, 'online'); } break; @@ -323,7 +323,7 @@ function handlePipeline(args) { userStore.applyUser(activeJson); } else { console.error('friend-active missing user id', content); - friendStore.updateFriend(content.userId, 'active'); + runUpdateFriendFlow(content.userId, 'active'); } break; diff --git a/src/stores/auth.js b/src/stores/auth.js index cea0b13c..f955af8c 100644 --- a/src/stores/auth.js +++ b/src/stores/auth.js @@ -5,19 +5,20 @@ import { useI18n } from 'vue-i18n'; import Noty from 'noty'; -import { closeWebSocket, initWebsocket } from '../service/websocket'; +import { + runLoginSuccessFlow, + runLogoutFlow +} from '../coordinators/authCoordinator'; import { AppDebug } from '../service/appConfig'; import { authRequest } from '../api'; -import { createAuthAutoLoginCoordinator } from '../coordinators/authAutoLoginCoordinator'; -import { createAuthCoordinator } from '../coordinators/authCoordinator'; import { database } from '../service/database'; import { escapeTag } from '../shared/utils'; -import { queryClient } from '../queries'; +import { initWebsocket } from '../service/websocket'; import { request } from '../service/request'; +import { runHandleAutoLoginFlow } from '../coordinators/authAutoLoginCoordinator'; import { useAdvancedSettingsStore } from './settings/advanced'; import { useGeneralSettingsStore } from './settings/general'; import { useModalStore } from './modal'; -import { useNotificationStore } from './notification'; import { useUpdateLoopStore } from './updateLoop'; import { useUserStore } from './user'; import { watchState } from '../service/watchState'; @@ -31,7 +32,6 @@ import * as workerTimers from 'worker-timers'; export const useAuthStore = defineStore('Auth', () => { const advancedSettingsStore = useAdvancedSettingsStore(); const generalSettingsStore = useGeneralSettingsStore(); - const notificationStore = useNotificationStore(); const userStore = useUserStore(); const updateLoopStore = useUpdateLoopStore(); const modalStore = useModalStore(); @@ -168,15 +168,7 @@ export const useAuthStore = defineStore('Auth', () => { * */ async function handleLogoutEvent() { - if (watchState.isLoggedIn) { - new Noty({ - type: 'success', - text: t('message.auth.logout_greeting', { - name: `${escapeTag(userStore.currentUser.displayName)}` - }) - }).show(); - } - await authCoordinator.runLogoutFlow(); + await runLogoutFlow(); } /** @@ -828,7 +820,7 @@ export const useAuthStore = defineStore('Auth', () => { } else if (json.requiresTwoFactorAuth) { promptTOTP(); } else { - authCoordinator.runLoginSuccessFlow(json); + runLoginSuccessFlow(json); } } @@ -836,7 +828,7 @@ export const useAuthStore = defineStore('Auth', () => { * */ async function handleAutoLogin() { - await authAutoLoginCoordinator.runHandleAutoLoginFlow(); + await runHandleAutoLoginFlow(); } /** @@ -894,56 +886,6 @@ export const useAuthStore = defineStore('Auth', () => { 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) { - toast.dismiss(AppDebug.errorNoty); - } - AppDebug.errorNoty = toast.success( - t('message.auth.auto_login_success') - ); - }, - notifyAutoLoginFailed: () => { - if (AppDebug.errorNoty) { - toast.dismiss(AppDebug.errorNoty); - } - AppDebug.errorNoty = toast.error( - t('message.auth.auto_login_failed') - ); - }, - notifyOffline: () => { - AppDebug.errorNoty = toast.error(t('message.auth.offline')); - }, - 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 { state, diff --git a/src/stores/favorite.js b/src/stores/favorite.js index 5b0e595e..74f92b48 100644 --- a/src/stores/favorite.js +++ b/src/stores/favorite.js @@ -13,6 +13,7 @@ import { import { avatarRequest, favoriteRequest, queryRequest } from '../api'; import { database } from '../service/database'; import { processBulk } from '../service/request'; +import { runUpdateFriendFlow } from '../coordinators/friendPresenceCoordinator'; import { useAppearanceSettingsStore } from './settings/appearance'; import { useAvatarStore } from './avatar'; import { useFriendStore } from './friend'; @@ -339,7 +340,7 @@ export const useFavoriteStore = defineStore('Favorite', () => { function handleFavorite(args) { args.ref = applyFavoriteCached(args.json); applyFavorite(args.ref.type, args.ref.favoriteId); - friendStore.updateFriend(args.ref.favoriteId); + runUpdateFriendFlow(args.ref.favoriteId); const { ref } = args; const userDialog = userStore.userDialog; if (userDialog.visible && ref.favoriteId === userDialog.id) { @@ -437,7 +438,7 @@ export const useFavoriteStore = defineStore('Favorite', () => { (id) => id !== ref.favoriteId ); - friendStore.updateFriend(ref.favoriteId); + runUpdateFriendFlow(ref.favoriteId); friendStore.updateSidebarFavorites(); const userDialog = userStore.userDialog; if (userDialog.visible && userDialog.id === ref.favoriteId) { diff --git a/src/stores/friend.js b/src/stores/friend.js index 91c0090e..46120af1 100644 --- a/src/stores/friend.js +++ b/src/stores/friend.js @@ -1,6 +1,5 @@ import { computed, reactive, ref, watch } from 'vue'; import { defineStore } from 'pinia'; -import { toast } from 'vue-sonner'; import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; @@ -9,24 +8,27 @@ import { createRateLimiter, executeWithBackoff, getFriendsSortFunction, - getGroupName, getNameColour, getUserMemo, - getWorldName, - isRealInstance, - migrateMemos + isRealInstance } from '../shared/utils'; import { friendRequest, userRequest } from '../api'; +import { + runDeleteFriendshipFlow, + runUpdateFriendshipsFlow +} from '../coordinators/friendRelationshipCoordinator'; +import { + runInitFriendsListFlow, + runRefreshFriendsListFlow +} from '../coordinators/friendSyncCoordinator'; +import { + runPendingOfflineTickFlow, + runUpdateFriendFlow +} from '../coordinators/friendPresenceCoordinator'; 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 { reconnectWebSocket } from '../service/websocket'; import { useAppearanceSettingsStore } from './settings/appearance'; -import { useAuthStore } from './auth'; import { useFavoriteStore } from './favorite'; -import { useFeedStore } from './feed'; import { useGeneralSettingsStore } from './settings/general'; import { useGroupStore } from './group'; import { useLocationStore } from './location'; @@ -34,7 +36,6 @@ import { useModalStore } from './modal'; import { useNotificationStore } from './notification'; import { useSharedFeedStore } from './sharedFeed'; import { useUiStore } from './ui'; -import { useUpdateLoopStore } from './updateLoop'; import { useUserStore } from './user'; import { watchState } from '../service/watchState'; @@ -47,12 +48,9 @@ export const useFriendStore = defineStore('Friend', () => { const generalSettingsStore = useGeneralSettingsStore(); const userStore = useUserStore(); const notificationStore = useNotificationStore(); - const feedStore = useFeedStore(); const uiStore = useUiStore(); const groupStore = useGroupStore(); const sharedFeedStore = useSharedFeedStore(); - const updateLoopStore = useUpdateLoopStore(); - const authStore = useAuthStore(); const locationStore = useLocationStore(); const favoriteStore = useFavoriteStore(); const modalStore = useModalStore(); @@ -64,7 +62,7 @@ export const useFriendStore = defineStore('Friend', () => { friendNumber: 0 }); - let friendLog = new Map(); + const friendLog = new Map(); const friends = reactive(new Map()); @@ -237,7 +235,7 @@ export const useFriendStore = defineStore('Friend', () => { onlineFriendCount.value = 0; pendingOfflineMap.clear(); if (isLoggedIn) { - initFriendsList(); + runInitFriendsListFlow(t); pendingOfflineWorkerFunction(); } else { if (pendingOfflineWorker !== null) { @@ -313,7 +311,7 @@ export const useFriendStore = defineStore('Friend', () => { return; } D.isFriend = false; - deleteFriendship(args.params.userId); + runDeleteFriendshipFlow(args.params.userId); deleteFriend(args.params.userId); } @@ -404,20 +402,12 @@ export const useFriendStore = defineStore('Friend', () => { } } - /** - * @param {string} id - * @param {string?} stateInput - */ - function updateFriend(id, stateInput = undefined) { - friendPresenceCoordinator.runUpdateFriendFlow(id, stateInput); - } - /** * */ async function pendingOfflineWorkerFunction() { pendingOfflineWorker = workerTimers.setInterval(() => { - friendPresenceCoordinator.runPendingOfflineTickFlow(); + runPendingOfflineTickFlow(); }, 1000); } @@ -454,7 +444,7 @@ export const useFriendStore = defineStore('Friend', () => { for (const friend of map) { const [id, state_input] = friend; if (friends.has(id)) { - updateFriend(id, state_input); + runUpdateFriendFlow(id, state_input); } else { addFriend(id, state_input); } @@ -698,10 +688,6 @@ export const useFriendStore = defineStore('Friend', () => { /** * @returns {Promise} */ - async function refreshFriendsList() { - await friendSyncCoordinator.runRefreshFriendsListFlow(); - } - /** * * @param forceUpdate @@ -895,22 +881,6 @@ export const useFriendStore = defineStore('Friend', () => { } } - /** - * - * @param {string} id - */ - function deleteFriendship(id) { - friendRelationshipCoordinator.runDeleteFriendshipFlow(id); - } - - /** - * - * @param {object} ref - */ - function updateFriendships(ref) { - friendRelationshipCoordinator.runUpdateFriendshipsFlow(ref); - } - /** * * @param {object} ref @@ -1075,7 +1045,7 @@ export const useFriendStore = defineStore('Friend', () => { } } if (typeof currentUser.friends !== 'undefined') { - updateFriendships(currentUser); + runUpdateFriendshipsFlow(currentUser); } } @@ -1440,91 +1410,14 @@ export const useFriendStore = defineStore('Friend', () => { } /** - * + * Clears all entries in friendLog. + * Uses .clear() instead of reassignment to keep the same Map reference, + * so that coordinators reading friendStore.friendLog stay in sync. */ - async function initFriendsList() { - await friendSyncCoordinator.runInitFriendsListFlow(); + function resetFriendLog() { + friendLog.clear(); } - /** - * @param {boolean} value - */ - function setRefreshFriendsLoading(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 { state, @@ -1543,16 +1436,15 @@ export const useFriendStore = defineStore('Friend', () => { onlineFriendCount, friendLog, friendLogTable, + pendingOfflineMap, + pendingOfflineDelay, - initFriendsList, updateLocalFavoriteFriends, updateSidebarFavorites, - updateFriend, deleteFriend, refreshFriendsStatus, addFriend, refreshFriends, - refreshFriendsList, updateOnlineFriendCounter, getAllUserStats, getAllUserMutualCount, @@ -1562,11 +1454,13 @@ export const useFriendStore = defineStore('Friend', () => { getFriendRequest, userOnFriend, confirmDeleteFriend, - updateFriendships, updateUserCurrentStatus, handleFriendAdd, handleFriendDelete, - initFriendLogHistoryTable, - setRefreshFriendsLoading + handleFriendStatus, + addFriendship, + tryApplyFriendOrder, + resetFriendLog, + initFriendLogHistoryTable }; }); diff --git a/src/stores/game.js b/src/stores/game.js index eda02a06..bf896fab 100644 --- a/src/stores/game.js +++ b/src/stores/game.js @@ -6,18 +6,15 @@ import { deleteVRChatCache as _deleteVRChatCache, isRealInstance } from '../shared/utils'; -import { createGameCoordinator } from '../coordinators/gameCoordinator'; import { database } from '../service/database'; +import { runGameRunningChangedFlow } from '../coordinators/gameCoordinator'; import { useAdvancedSettingsStore } from './settings/advanced'; import { useAvatarStore } from './avatar'; import { useGameLogStore } from './gameLog'; -import { useInstanceStore } from './instance'; import { useLaunchStore } from './launch'; import { useLocationStore } from './location'; import { useModalStore } from './modal'; import { useNotificationStore } from './notification'; -import { useUpdateLoopStore } from './updateLoop'; -import { useUserStore } from './user'; import { useVrStore } from './vr'; import { useWorldStore } from './world'; @@ -32,11 +29,8 @@ export const useGameStore = defineStore('Game', () => { const avatarStore = useAvatarStore(); const launchStore = useLaunchStore(); const worldStore = useWorldStore(); - const instanceStore = useInstanceStore(); const gameLogStore = useGameLogStore(); const vrStore = useVrStore(); - const userStore = useUserStore(); - const updateLoopStore = useUpdateLoopStore(); const modalStore = useModalStore(); const state = reactive({ @@ -168,22 +162,6 @@ export const useGameStore = defineStore('Game', () => { VRChatCacheSizeLoading.value = false; } - const gameCoordinator = createGameCoordinator({ - userStore, - instanceStore, - updateLoopStore, - locationStore, - gameLogStore, - vrStore, - avatarStore, - configRepository, - workerTimers, - checkVRChatDebugLogging, - autoVRChatCacheManagement, - checkIfGameCrashed, - getIsGameNoVR: () => isGameNoVR.value - }); - // use in C# /** * @param {boolean} isGameRunningArg Game running flag from IPC. @@ -195,7 +173,7 @@ export const useGameStore = defineStore('Game', () => { } if (isGameRunningArg !== isGameRunning.value) { isGameRunning.value = isGameRunningArg; - await gameCoordinator.runGameRunningChangedFlow(isGameRunningArg); + await runGameRunningChangedFlow(isGameRunningArg); console.log(new Date(), 'isGameRunning', isGameRunningArg); } @@ -300,6 +278,8 @@ export const useGameStore = defineStore('Game', () => { setIsGameNoVR, getVRChatRegistryKey, checkVRChatDebugLogging, + autoVRChatCacheManagement, + checkIfGameCrashed, updateIsHmdAfk }; }); diff --git a/src/stores/updateLoop.js b/src/stores/updateLoop.js index ea6e00ed..2b2017b1 100644 --- a/src/stores/updateLoop.js +++ b/src/stores/updateLoop.js @@ -3,6 +3,7 @@ import { watch } from 'vue'; import { database } from '../service/database'; import { groupRequest } from '../api'; +import { runRefreshFriendsListFlow } from '../coordinators/friendSyncCoordinator'; import { useAuthStore } from './auth'; import { useDiscordPresenceSettingsStore } from './settings/discordPresence'; import { useFriendStore } from './friend'; @@ -62,6 +63,9 @@ export const useUpdateLoopStore = defineStore('UpdateLoop', () => { const ipcTimeout = state.ipcTimeout; + /** + * + */ async function updateLoop() { try { if (watchState.isLoggedIn) { @@ -71,7 +75,7 @@ export const useUpdateLoopStore = defineStore('UpdateLoop', () => { } if (--state.nextFriendsRefresh <= 0) { state.nextFriendsRefresh = 3600; // 1hour - friendStore.refreshFriendsList(); + runRefreshFriendsListFlow(); authStore.updateStoredUser(userStore.currentUser); if ( userStore.currentUser.last_activity && @@ -141,28 +145,48 @@ export const useUpdateLoopStore = defineStore('UpdateLoop', () => { } } } catch (err) { - friendStore.setRefreshFriendsLoading(false); + friendStore.isRefreshFriendsLoading = false; console.error(err); } workerTimers.setTimeout(() => updateLoop(), 1000); } + /** + * + * @param value + */ function setNextClearVRCXCacheCheck(value) { state.nextClearVRCXCacheCheck = value; } + /** + * + * @param value + */ function setNextGroupInstanceRefresh(value) { state.nextGroupInstanceRefresh = value; } + /** + * + * @param value + */ function setNextDiscordUpdate(value) { state.nextDiscordUpdate = value; } + /** + * + * @param value + */ function setIpcTimeout(value) { state.ipcTimeout = value; } + /** + * + * @param value + */ function setNextCurrentUserRefresh(value) { state.nextCurrentUserRefresh = value; } diff --git a/src/stores/user.js b/src/stores/user.js index 6a5d9346..d8964cc9 100644 --- a/src/stores/user.js +++ b/src/stores/user.js @@ -17,7 +17,6 @@ import { extractFileId, findUserByDisplayName, getAllUserMemos, - getGroupName, getUserMemo, getWorldName, isRealInstance, @@ -27,22 +26,26 @@ import { } from '../shared/utils'; import { avatarRequest, - groupRequest, instanceRequest, queryRequest, userRequest } from '../api'; +import { + runAvatarSwapFlow, + runFirstLoginFlow, + runHomeLocationSyncFlow, + runPostApplySyncFlow +} from '../coordinators/userSessionCoordinator'; import { processBulk, request } from '../service/request'; import { AppDebug } from '../service/appConfig'; -import { createUserEventCoordinator } from '../coordinators/userEventCoordinator'; -import { createUserSessionCoordinator } from '../coordinators/userSessionCoordinator'; import { database } from '../service/database'; import { patchUserFromEvent } from '../queries'; +import { runHandleUserUpdateFlow } from '../coordinators/userEventCoordinator'; +import { runUpdateFriendFlow } from '../coordinators/friendPresenceCoordinator'; import { useAppearanceSettingsStore } from './settings/appearance'; import { useAuthStore } from './auth'; import { useAvatarStore } from './avatar'; import { useFavoriteStore } from './favorite'; -import { useFeedStore } from './feed'; import { useFriendStore } from './friend'; import { useGameStore } from './game'; import { useGeneralSettingsStore } from './settings/general'; @@ -55,7 +58,6 @@ import { usePhotonStore } from './photon'; import { useSearchStore } from './search'; import { useSharedFeedStore } from './sharedFeed'; import { useUiStore } from './ui'; -import { useWorldStore } from './world'; import { watchState } from '../service/watchState'; import * as workerTimers from 'worker-timers'; @@ -73,8 +75,6 @@ export const useUserStore = defineStore('User', () => { const notificationStore = useNotificationStore(); const authStore = useAuthStore(); const groupStore = useGroupStore(); - const feedStore = useFeedStore(); - const worldStore = useWorldStore(); const uiStore = useUiStore(); const moderationStore = useModerationStore(); const photonStore = usePhotonStore(); @@ -472,11 +472,11 @@ export const useUserStore = defineStore('User', () => { { logLabel: 'User cache cleanup' } ); cachedUsers.set(ref.id, ref); - friendStore.updateFriend(ref.id); + runUpdateFriendFlow(ref.id); } else { if (json.state !== 'online') { // offline event before GPS to offline location - friendStore.updateFriend(ref.id, json.state); + runUpdateFriendFlow(ref.id, json.state); } const { hasPropChanged: _hasPropChanged, @@ -584,7 +584,7 @@ export const useUserStore = defineStore('User', () => { instanceStore.getCurrentInstanceUserList(); } if (ref.state === 'online') { - friendStore.updateFriend(ref.id, ref.state); // online/offline + runUpdateFriendFlow(ref.id, ref.state); // online/offline } favoriteStore.applyFavorite('friend', ref.id); friendStore.userOnFriend(ref); @@ -1145,7 +1145,7 @@ export const useUserStore = defineStore('User', () => { * @returns {Promise} */ async function handleUserUpdate(ref, props) { - await userEventCoordinator.runHandleUserUpdateFlow(ref, props); + await runHandleUserUpdateFlow(ref, props); } /** @@ -1421,7 +1421,7 @@ export const useUserStore = defineStore('User', () => { function applyCurrentUser(json) { authStore.setAttemptingAutoLogin(false); let ref = currentUser.value; - userSessionCoordinator.runAvatarSwapFlow({ + runAvatarSwapFlow({ json, ref, isLoggedIn: watchState.isLoggedIn @@ -1545,15 +1545,15 @@ export const useUserStore = defineStore('User', () => { $travelingToLocation: '', ...json }; - userSessionCoordinator.runFirstLoginFlow(ref); + runFirstLoginFlow(ref); } ref.$isVRCPlus = ref.tags.includes('system_supporter'); appearanceSettingsStore.applyUserTrustLevel(ref); applyUserLanguage(ref); applyPresenceLocation(ref); - userSessionCoordinator.runPostApplySyncFlow(ref); - userSessionCoordinator.runHomeLocationSyncFlow(ref); + runPostApplySyncFlow(ref); + runHomeLocationSyncFlow(ref); // when isGameRunning use gameLog instead of API const $location = parseLocation(locationStore.lastLocation.location); @@ -1744,42 +1744,6 @@ 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 { state, diff --git a/src/views/Sidebar/Sidebar.vue b/src/views/Sidebar/Sidebar.vue index ccab1e4d..0d019236 100644 --- a/src/views/Sidebar/Sidebar.vue +++ b/src/views/Sidebar/Sidebar.vue @@ -21,7 +21,7 @@ variant="ghost" size="icon-sm" :disabled="isRefreshFriendsLoading" - @click="refreshFriendsList"> + @click="runRefreshFriendsListFlow"> @@ -267,6 +267,7 @@ useGroupStore, useNotificationStore } from '../../stores'; + import { runRefreshFriendsListFlow } from '../../coordinators/friendSyncCoordinator'; import { normalizeFavoriteGroupsChange, resolveFavoriteGroups } from './sidebarSettingsUtils'; import { useGlobalSearchStore } from '../../stores/globalSearch'; @@ -277,7 +278,6 @@ import NotificationCenterSheet from './components/NotificationCenterSheet.vue'; const { friends, isRefreshFriendsLoading, onlineFriendCount } = storeToRefs(useFriendStore()); - const { refreshFriendsList } = useFriendStore(); const { groupInstances } = storeToRefs(useGroupStore()); const notificationStore = useNotificationStore(); const { isNotificationCenterOpen, hasUnseenNotifications } = storeToRefs(notificationStore);