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);