refactor coordinators

This commit is contained in:
pa
2026-03-10 11:54:09 +09:00
parent 648fcde085
commit 2fffadfbcf
16 changed files with 1068 additions and 1257 deletions

View File

@@ -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. * Runs the full auto-login orchestration flow.
* @returns {object} Auto-login flow coordinator methods. * @param {object} [options] Test seams.
* @param {function} [options.now] Timestamp provider.
* @param {function} [options.isOnline] Online-check provider.
*/ */
export function createAuthAutoLoginCoordinator(deps) { export async function runHandleAutoLoginFlow({
const { now = Date.now,
getIsAttemptingAutoLogin, isOnline = () => navigator.onLine
setAttemptingAutoLogin, } = {}) {
getLastUserLoggedIn, const authStore = useAuthStore();
getSavedCredentials, const advancedSettingsStore = useAdvancedSettingsStore();
isPrimaryPasswordEnabled, const { t } = useI18n();
handleLogoutEvent,
autoLoginAttempts,
relogin,
notifyAutoLoginSuccess,
notifyAutoLoginFailed,
notifyOffline,
flashWindow,
isOnline,
now
} = deps;
/** if (authStore.attemptingAutoLogin) {
* Runs the full auto-login orchestration flow. return;
*/
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.`);
}
});
} }
authStore.setAttemptingAutoLogin(true);
return { const user = await authStore.getSavedCredentials(
runHandleAutoLoginFlow 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.`);
}
});
} }

View File

@@ -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. * Runs the shared logout side effects (including goodbye notification).
* @returns {object} Auth flow coordinator methods.
*/ */
export function createAuthCoordinator(deps) { export async function runLogoutFlow() {
const { const authStore = useAuthStore();
userStore, const userStore = useUserStore();
notificationStore, const notificationStore = useNotificationStore();
updateLoopStore, const { t } = useI18n();
initWebsocket,
updateStoredUser,
webApiService,
loginForm,
configRepository,
setAttemptingAutoLogin,
autoLoginAttempts,
closeWebSocket,
queryClient,
watchState
} = deps;
/** if (watchState.isLoggedIn) {
* Runs the shared logout side effects. new Noty({
*/ type: 'success',
async function runLogoutFlow() { text: t('message.auth.logout_greeting', {
userStore.setUserDialogVisible(false); name: `<strong>${escapeTag(userStore.currentUser.displayName)}</strong>`
watchState.isLoggedIn = false; })
watchState.isFriendsLoaded = false; }).show();
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();
} }
/** userStore.setUserDialogVisible(false);
* Runs post-login side effects after a successful auth response. watchState.isLoggedIn = false;
* @param {object} json Current user payload from auth API. watchState.isFriendsLoaded = false;
*/ watchState.isFavoritesLoaded = false;
function runLoginSuccessFlow(json) { notificationStore.setNotificationInitStatus(false);
updateLoopStore.setNextCurrentUserRefresh(420); // 7mins await authStore.updateStoredUser(userStore.currentUser);
userStore.applyCurrentUser(json); webApiService.clearCookies();
initWebsocket(); authStore.loginForm.lastUserLoggedIn = '';
} await configRepository.remove('lastUserLoggedIn');
authStore.setAttemptingAutoLogin(false);
return { authStore.state.autoLoginAttempts.clear();
runLogoutFlow, closeWebSocket();
runLoginSuccessFlow 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();
} }

View File

@@ -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. * @param {object} ctx
* @returns {object} Friend presence coordinator methods. * @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 { const {
friends, friends,
localFavoriteFriends, localFavoriteFriends,
pendingOfflineMap, pendingOfflineMap,
pendingOfflineDelay, pendingOfflineDelay
watchState, } = friendStore;
appDebug,
getCachedUsers,
isRealInstance,
requestUser,
getWorldName,
getGroupName,
feedStore,
database,
updateOnlineFriendCounter,
now,
nowIso
} = deps;
/** const ctx = friends.get(id);
* @param {object} ctx if (typeof ctx === 'undefined') {
* @param {string} newState return;
* @param {string} location }
* @param {number} $location_at const ref = userStore.cachedUsers.get(id);
*/
async function runUpdateFriendDelayedCheckFlow( if (stateInput === 'online') {
ctx, const pendingOffline = pendingOfflineMap.get(id);
newState, if (AppDebug.debugFriendState && pendingOffline) {
location, const time = (now() - pendingOffline.startTime) / 1000;
$location_at console.log(`${ctx.name} pendingOfflineCancelTime ${time}`);
) { }
let feed; ctx.pendingOffline = false;
let groupName; pendingOfflineMap.delete(id);
let worldName; }
const id = ctx.id; const isVIP = localFavoriteFriends.has(id);
if (appDebug.debugFriendState) { 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( console.log(
`${ctx.name} updateFriendState ${ctx.state} -> ${newState}` `Fetching online friend in an offline location ${ctx.name}`
); );
if ( userRequest.getUser({ userId: id });
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); if (typeof stateInput === 'undefined' || ctx.state === stateInput) {
return; // this is should be: undefined -> user
} if (ctx.ref !== ref) {
const isVIP = localFavoriteFriends.has(id); ctx.ref = ref;
const ref = ctx.ref; // NOTE
if (ctx.state !== newState && typeof ctx.ref !== 'undefined') { // AddFriend (CurrentUser) 이후,
if ( // 서버에서 오는 순서라고 보면 될 듯.
(newState === 'offline' || newState === 'active') && if (ctx.state === 'online') {
ctx.state === 'online' if (watchState.isFriendsLoaded) {
) { userRequest.getUser({ userId: id });
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) { if (ctx.isVIP !== isVIP) {
ctx.state = newState; ctx.isVIP = isVIP;
updateOnlineFriendCounter();
} }
if (ref?.displayName) { if (typeof ref !== 'undefined' && ctx.name !== ref.displayName) {
ctx.name = ref.displayName; ctx.name = ref.displayName;
} }
ctx.isVIP = isVIP; return;
} }
if (
/** ctx.state === 'online' &&
* Handles immediate friend presence updates and pending-offline orchestration. (stateInput === 'active' || stateInput === 'offline')
* @param {string} id Friend id. ) {
* @param {string | undefined} stateInput Optional incoming state.
*/
async function runUpdateFriendFlow(id, stateInput = undefined) {
const ctx = friends.get(id);
if (typeof ctx === 'undefined') {
return;
}
const ref = getCachedUsers().get(id);
if (stateInput === 'online') {
const pendingOffline = pendingOfflineMap.get(id);
if (appDebug.debugFriendState && pendingOffline) {
const time = (now() - pendingOffline.startTime) / 1000;
console.log(`${ctx.name} pendingOfflineCancelTime ${time}`);
}
ctx.pendingOffline = false;
pendingOfflineMap.delete(id);
}
const isVIP = localFavoriteFriends.has(id);
let location = '';
let $location_at = undefined;
if (typeof ref !== 'undefined') {
location = ref.location;
$location_at = ref.$location_at;
const currentState = stateInput || ctx.state;
// wtf, fetch user if offline in an instance
if (
currentState !== 'online' &&
isRealInstance(ref.location) &&
ref.$lastFetch < now() - 10000 // 10 seconds
) {
console.log(
`Fetching offline friend in an instance ${ctx.name}`
);
requestUser(id);
}
// wtf, fetch user if online in an offline location
if (
currentState === 'online' &&
ref.location === 'offline' &&
ref.$lastFetch < now() - 10000 // 10 seconds
) {
console.log(
`Fetching online friend in an offline location ${ctx.name}`
);
requestUser(id);
}
}
if (typeof stateInput === 'undefined' || ctx.state === stateInput) {
// this is should be: undefined -> user
if (ctx.ref !== ref) {
ctx.ref = ref;
// NOTE
// AddFriend (CurrentUser) 이후,
// 서버에서 오는 순서라고 보면 될 듯.
if (ctx.state === 'online') {
if (watchState.isFriendsLoaded) {
requestUser(id);
}
}
}
if (ctx.isVIP !== isVIP) {
ctx.isVIP = isVIP;
}
if (typeof ref !== 'undefined' && ctx.name !== ref.displayName) {
ctx.name = ref.displayName;
}
return;
}
if (
ctx.state === 'online' &&
(stateInput === 'active' || stateInput === 'offline')
) {
ctx.ref = ref;
ctx.isVIP = isVIP;
if (typeof ref !== 'undefined') {
ctx.name = ref.displayName;
}
if (!watchState.isFriendsLoaded) {
await runUpdateFriendDelayedCheckFlow(
ctx,
stateInput,
location,
$location_at
);
return;
}
// prevent status flapping
if (pendingOfflineMap.has(id)) {
if (appDebug.debugFriendState) {
console.log(ctx.name, 'pendingOfflineAlreadyWaiting');
}
return;
}
if (appDebug.debugFriendState) {
console.log(ctx.name, 'pendingOfflineBegin');
}
pendingOfflineMap.set(id, {
startTime: now(),
newState: stateInput,
previousLocation: location,
previousLocationAt: $location_at
});
ctx.pendingOffline = true;
return;
}
ctx.ref = ref; ctx.ref = ref;
ctx.isVIP = isVIP; ctx.isVIP = isVIP;
if (typeof ref !== 'undefined') { if (typeof ref !== 'undefined') {
ctx.name = ref.displayName; ctx.name = ref.displayName;
}
if (!watchState.isFriendsLoaded) {
await runUpdateFriendDelayedCheckFlow( await runUpdateFriendDelayedCheckFlow(
ctx, ctx,
ctx.ref.state, stateInput,
location, 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
};
} }

View File

@@ -1,43 +1,48 @@
/** import { database } from '../service/database';
* @param {object} deps Coordinator dependencies. import { friendRequest } from '../api';
* @returns {object} Friend relationship coordinator methods. import { useAppearanceSettingsStore } from '../stores/settings/appearance';
*/ import { useFavoriteStore } from '../stores/favorite';
export function createFriendRelationshipCoordinator(deps) { import { useFriendStore } from '../stores/friend';
const { import { useNotificationStore } from '../stores/notification';
friendLog, import { useSharedFeedStore } from '../stores/sharedFeed';
friendLogTable, import { useUiStore } from '../stores/ui';
getCurrentUserId, import { useUserStore } from '../stores/user';
requestFriendStatus,
handleFriendStatus,
addFriendship,
deleteFriend,
database,
notificationStore,
sharedFeedStore,
favoriteStore,
uiStore,
shouldNotifyUnfriend,
nowIso
} = deps;
/** /**
* Validates and applies unfriend transition side effects. * Validates and applies unfriend transition side effects.
* @param {string} id User id. * @param {string} id User id.
*/ * @param {object} [options] Test seams.
function runDeleteFriendshipFlow(id) { * @param {function} [options.nowIso] ISO timestamp provider.
const ctx = friendLog.get(id); */
if (typeof ctx === 'undefined') { export function runDeleteFriendshipFlow(
return; id,
} { nowIso = () => new Date().toJSON() } = {}
requestFriendStatus({ ) {
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, userId: id,
currentUserId: getCurrentUserId() currentUserId: userStore.currentUser.id
}).then((args) => { })
if (args.params.currentUserId !== getCurrentUserId()) { .then((args) => {
if (args.params.currentUserId !== userStore.currentUser.id) {
// safety check for delayed response // safety check for delayed response
return; return;
} }
handleFriendStatus(args); friendStore.handleFriendStatus(args);
if (!args.json.isFriend && friendLog.has(id)) { if (!args.json.isFriend && friendLog.has(id)) {
const friendLogHistory = { const friendLogHistory = {
created_at: nowIso(), created_at: nowIso(),
@@ -45,44 +50,47 @@ export function createFriendRelationshipCoordinator(deps) {
userId: id, userId: id,
displayName: ctx.displayName || id displayName: ctx.displayName || id
}; };
friendLogTable.value.data.push(friendLogHistory); friendLogTable.data.push(friendLogHistory);
database.addFriendLogHistory(friendLogHistory); database.addFriendLogHistory(friendLogHistory);
notificationStore.queueFriendLogNoty(friendLogHistory); notificationStore.queueFriendLogNoty(friendLogHistory);
sharedFeedStore.addEntry(friendLogHistory); sharedFeedStore.addEntry(friendLogHistory);
friendLog.delete(id); friendLog.delete(id);
database.deleteFriendLogCurrent(id); database.deleteFriendLogCurrent(id);
favoriteStore.handleFavoriteDelete(id); favoriteStore.handleFavoriteDelete(id);
if (shouldNotifyUnfriend()) { if (!appearanceSettingsStore.hideUnfriends) {
uiStore.notifyMenu('friend-log'); uiStore.notifyMenu('friend-log');
} }
deleteFriend(id); friendStore.deleteFriend(id);
} }
}); });
} }
/** /**
* Reconciles current friend list against local friend log. * Reconciles current friend list against local friend log.
* @param {object} ref Current user reference. * @param {object} ref Current user reference.
*/ * @param {object} [options] Test seams.
function runUpdateFriendshipsFlow(ref) { * @param {function} [options.nowIso] ISO timestamp provider.
let id; */
const set = new Set(); export function runUpdateFriendshipsFlow(
for (id of ref.friends) { ref,
set.add(id); { nowIso = () => new Date().toJSON() } = {}
addFriendship(id); ) {
} const friendStore = useFriendStore();
for (id of friendLog.keys()) { const userStore = useUserStore();
if (id === getCurrentUserId()) { const { friendLog } = friendStore;
friendLog.delete(id);
database.deleteFriendLogCurrent(id); let id;
} else if (!set.has(id)) { const set = new Set();
runDeleteFriendshipFlow(id); for (id of ref.friends) {
} set.add(id);
} friendStore.addFriendship(id);
} }
for (id of friendLog.keys()) {
return { if (id === userStore.currentUser.id) {
runDeleteFriendshipFlow, friendLog.delete(id);
runUpdateFriendshipsFlow database.deleteFriendLogCurrent(id);
}; } else if (!set.has(id)) {
runDeleteFriendshipFlow(id, { nowIso });
}
}
} }

View File

@@ -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. * Runs friend list refresh orchestration.
* @returns {object} Friend sync coordinator methods.
*/ */
export function createFriendSyncCoordinator(deps) { export async function runRefreshFriendsListFlow() {
const { const updateLoopStore = useUpdateLoopStore();
getNextCurrentUserRefresh, const userStore = useUserStore();
getCurrentUser, const friendStore = useFriendStore();
refreshFriends,
reconnectWebSocket,
getCurrentUserId,
getCurrentUserRef,
setRefreshFriendsLoading,
setFriendsLoaded,
resetFriendLog,
isFriendLogInitialized,
getFriendLog,
initFriendLog,
isDontLogMeOut,
showLoadFailedToast,
handleLogoutEvent,
tryApplyFriendOrder,
getAllUserStats,
hasLegacyFriendLogData,
removeLegacyFeedTable,
migrateMemos,
migrateFriendLog
} = deps;
/** // If we just got user less then 2 min before code call, don't call it again
* Runs friend list refresh orchestration. if (updateLoopStore.nextCurrentUserRefresh < 300) {
*/ await userStore.getCurrentUser();
async function runRefreshFriendsListFlow() { }
// If we just got user less then 2 min before code call, don't call it again await friendStore.refreshFriends();
if (getNextCurrentUserRefresh() < 300) { reconnectWebSocket();
await getCurrentUser(); }
}
await 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
};
} }

View File

@@ -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. * Runs shared side effects when game running state changes.
* @returns {object} Game flow coordinator methods. * @param {boolean} isGameRunning Whether VRChat is running.
*/ */
export function createGameCoordinator(deps) { export async function runGameRunningChangedFlow(isGameRunning) {
const { const userStore = useUserStore();
userStore, const instanceStore = useInstanceStore();
instanceStore, const updateLoopStore = useUpdateLoopStore();
updateLoopStore, const locationStore = useLocationStore();
locationStore, const gameLogStore = useGameLogStore();
gameLogStore, const vrStore = useVrStore();
vrStore, const avatarStore = useAvatarStore();
avatarStore, const gameStore = useGameStore();
configRepository,
workerTimers,
checkVRChatDebugLogging,
autoVRChatCacheManagement,
checkIfGameCrashed,
getIsGameNoVR
} = deps;
/** if (isGameRunning) {
* Runs shared side effects when game running state changes. userStore.markCurrentUserGameStarted();
* @param {boolean} isGameRunning Whether VRChat is running. } else {
*/ await configRepository.setBool('isGameNoVR', gameStore.isGameNoVR);
async function runGameRunningChangedFlow(isGameRunning) { userStore.markCurrentUserGameStopped();
if (isGameRunning) { instanceStore.removeAllQueuedInstances();
userStore.markCurrentUserGameStarted(); gameStore.autoVRChatCacheManagement();
} else { gameStore.checkIfGameCrashed();
await configRepository.setBool('isGameNoVR', getIsGameNoVR()); updateLoopStore.setIpcTimeout(0);
userStore.markCurrentUserGameStopped(); avatarStore.addAvatarWearTime(userStore.currentUser.currentAvatar);
instanceStore.removeAllQueuedInstances();
autoVRChatCacheManagement();
checkIfGameCrashed();
updateLoopStore.setIpcTimeout(0);
avatarStore.addAvatarWearTime(userStore.currentUser.currentAvatar);
}
locationStore.lastLocationReset();
gameLogStore.clearNowPlaying();
vrStore.updateVRLastLocation();
workerTimers.setTimeout(() => checkVRChatDebugLogging(), 60000);
updateLoopStore.setNextDiscordUpdate(0);
} }
return { locationStore.lastLocationReset();
runGameRunningChangedFlow gameLogStore.clearNowPlaying();
}; vrStore.updateVRLastLocation();
workerTimers.setTimeout(() => gameStore.checkVRChatDebugLogging(), 60000);
updateLoopStore.setNextDiscordUpdate(0);
} }

View File

@@ -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. * Handles user diff events and applies cross-store side effects.
* @returns {object} User event coordinator methods. * @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<void>}
*/ */
export function createUserEventCoordinator(deps) { export async function runHandleUserUpdateFlow(
const { ref,
friendStore, props,
state, { now = Date.now, nowIso = () => new Date().toJSON() } = {}
parseLocation, ) {
userDialog, const friendStore = useFriendStore();
applyUserDialogLocation, const userStore = useUserStore();
worldStore, const worldStore = useWorldStore();
groupStore, const groupStore = useGroupStore();
instanceStore, const instanceStore = useInstanceStore();
appDebug, const feedStore = useFeedStore();
getWorldName, const avatarStore = useAvatarStore();
getGroupName, const generalSettingsStore = useGeneralSettingsStore();
feedStore,
database,
avatarStore,
generalSettingsStore,
checkNote,
now,
nowIso
} = deps;
/** const { state, userDialog, applyUserDialogLocation, checkNote } = userStore;
* Handles user diff events and applies cross-store side effects.
* @param {object} ref Updated user reference.
* @param {object} props Changed props with [new, old] tuples.
* @returns {Promise<void>}
*/
async function runHandleUserUpdateFlow(ref, props) {
let feed;
let newLocation;
let previousLocation;
const friend = friendStore.friends.get(ref.id);
if (typeof friend === 'undefined') {
return;
}
if (props.location) {
// update instancePlayerCount
previousLocation = props.location[1];
newLocation = props.location[0];
let oldCount = state.instancePlayerCount.get(previousLocation);
if (typeof oldCount !== 'undefined') {
oldCount--;
if (oldCount <= 0) {
state.instancePlayerCount.delete(previousLocation);
} else {
state.instancePlayerCount.set(previousLocation, oldCount);
}
}
let newCount = state.instancePlayerCount.get(newLocation);
if (typeof newCount === 'undefined') {
newCount = 0;
}
newCount++;
state.instancePlayerCount.set(newLocation, newCount);
const previousLocationL = parseLocation(previousLocation); let feed;
const newLocationL = parseLocation(newLocation); let newLocation;
if ( let previousLocation;
previousLocationL.tag === userDialog.value.$location.tag || const friend = friendStore.friends.get(ref.id);
newLocationL.tag === userDialog.value.$location.tag if (typeof friend === 'undefined') {
) { return;
// update user dialog instance occupants }
applyUserDialogLocation(true); if (props.location) {
} // update instancePlayerCount
if ( previousLocation = props.location[1];
previousLocationL.worldId === worldStore.worldDialog.id || newLocation = props.location[0];
newLocationL.worldId === worldStore.worldDialog.id let oldCount = state.instancePlayerCount.get(previousLocation);
) { if (typeof oldCount !== 'undefined') {
instanceStore.applyWorldDialogInstances(); oldCount--;
} if (oldCount <= 0) {
if ( state.instancePlayerCount.delete(previousLocation);
previousLocationL.groupId === groupStore.groupDialog.id ||
newLocationL.groupId === groupStore.groupDialog.id
) {
instanceStore.applyGroupDialogInstances();
}
}
if (
!props.state &&
props.location &&
props.location[0] !== 'offline' &&
props.location[0] !== '' &&
props.location[1] !== 'offline' &&
props.location[1] !== '' &&
props.location[0] !== 'traveling'
) {
// skip GPS if user is offline or traveling
previousLocation = props.location[1];
newLocation = props.location[0];
let time = props.location[2];
if (previousLocation === 'traveling' && ref.$previousLocation) {
previousLocation = ref.$previousLocation;
const travelTime = now() - ref.$travelingToTime;
time -= travelTime;
if (time < 0) {
time = 0;
}
}
if (appDebug.debugFriendState && previousLocation) {
console.log(
`${ref.displayName} GPS ${previousLocation} -> ${newLocation}`
);
}
if (previousLocation === 'offline') {
previousLocation = '';
}
if (!previousLocation) {
// no previous location
if (appDebug.debugFriendState) {
console.log(
ref.displayName,
'Ignoring GPS, no previous location',
newLocation
);
}
} else if (ref.$previousLocation === newLocation) {
// location traveled to is the same
ref.$location_at = now() - time;
} else { } else {
const worldName = await getWorldName(newLocation); state.instancePlayerCount.set(previousLocation, oldCount);
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();
} }
} }
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 ( if (
props.location && previousLocationL.worldId === worldStore.worldDialog.id ||
props.location[0] === 'traveling' && newLocationL.worldId === worldStore.worldDialog.id
props.location[1] !== 'traveling'
) { ) {
// store previous location when user is traveling instanceStore.applyWorldDialogInstances();
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 ( if (
(((props.currentAvatarImageUrl || previousLocationL.groupId === groupStore.groupDialog.id ||
props.currentAvatarThumbnailImageUrl) && newLocationL.groupId === groupStore.groupDialog.id
!ref.profilePicOverride) ||
props.currentAvatarTags) &&
!imageMatches
) { ) {
let currentAvatarImageUrl = ''; instanceStore.applyGroupDialogInstances();
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]);
} }
} }
if (
return { !props.state &&
runHandleUserUpdateFlow 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]);
}
} }

View File

@@ -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. * Runs avatar transition side effects for current user updates.
* @returns {object} User session coordinator methods. * @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) { export function runAvatarSwapFlow(
const { { json, ref, isLoggedIn },
avatarStore, { now = Date.now } = {}
gameStore, ) {
groupStore, const avatarStore = useAvatarStore();
instanceStore, const gameStore = useGameStore();
friendStore,
authStore,
cachedUsers,
currentUser,
userDialog,
getWorldName,
parseLocation,
now
} = deps;
/** if (!isLoggedIn) {
* Runs avatar transition side effects for current user updates. return;
* @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 (json.currentAvatar !== ref.currentAvatar) {
/** avatarStore.addAvatarToHistory(json.currentAvatar);
* Runs one-time side effects for first current-user hydration after login.
* @param {object} ref Current user state reference.
*/
function runFirstLoginFlow(ref) {
if (gameStore.isGameRunning) { if (gameStore.isGameRunning) {
avatarStore.addAvatarWearTime(ref.currentAvatar);
ref.$previousAvatarSwapTime = now(); 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. * Runs one-time side effects for first current-user hydration after login.
*/ * @param {object} ref Current user state reference.
function runPostApplySyncFlow(ref) { * @param {object} [options] Test seams.
groupStore.applyPresenceGroups(ref); * @param {function} [options.now] Timestamp provider.
instanceStore.applyQueuedInstance(ref.queuedInstance); */
friendStore.updateUserCurrentStatus(ref); export function runFirstLoginFlow(ref, { now = Date.now } = {}) {
friendStore.updateFriendships(ref); const gameStore = useGameStore();
} const authStore = useAuthStore();
const userStore = useUserStore();
/**
* Syncs home location derived state and visible dialog display name. if (gameStore.isGameRunning) {
* @param {object} ref Current user state reference. ref.$previousAvatarSwapTime = now();
*/ }
function runHomeLocationSyncFlow(ref) { userStore.cachedUsers.clear(); // clear before running applyUser
if (ref.homeLocation === ref.$homeLocation?.tag) { userStore.currentUser = ref;
return; authStore.loginComplete();
} }
ref.$homeLocation = parseLocation(ref.homeLocation);
// apply home location name to user dialog /**
if (userDialog.value.visible && userDialog.value.id === ref.id) { * Runs cross-store synchronization after current-user data is applied.
getWorldName(currentUser.value.homeLocation).then((worldName) => { * @param {object} ref Current user state reference.
userDialog.value.$homeLocationName = worldName; */
}); export function runPostApplySyncFlow(ref) {
} const groupStore = useGroupStore();
} const instanceStore = useInstanceStore();
const friendStore = useFriendStore();
return {
runAvatarSwapFlow, groupStore.applyPresenceGroups(ref);
runFirstLoginFlow, instanceStore.applyQueuedInstance(ref.queuedInstance);
runPostApplySyncFlow, friendStore.updateUserCurrentStatus(ref);
runHomeLocationSyncFlow 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;
});
}
} }

View File

@@ -1,5 +1,4 @@
import { reactive } from 'vue'; import { reactive } from 'vue';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { import {
@@ -17,6 +16,7 @@ import { escapeTag, parseLocation } from '../shared/utils';
import { AppDebug } from './appConfig'; import { AppDebug } from './appConfig';
import { groupRequest } from '../api'; import { groupRequest } from '../api';
import { request } from './request'; import { request } from './request';
import { runUpdateFriendFlow } from '../coordinators/friendPresenceCoordinator';
import { watchState } from './watchState'; import { watchState } from './watchState';
import * as workerTimers from 'worker-timers'; import * as workerTimers from 'worker-timers';
@@ -300,7 +300,7 @@ function handlePipeline(args) {
userStore.applyUser(onlineJson); userStore.applyUser(onlineJson);
} else { } else {
console.error('friend-online missing user id', content); console.error('friend-online missing user id', content);
friendStore.updateFriend(content.userId, 'online'); runUpdateFriendFlow(content.userId, 'online');
} }
break; break;
@@ -323,7 +323,7 @@ function handlePipeline(args) {
userStore.applyUser(activeJson); userStore.applyUser(activeJson);
} else { } else {
console.error('friend-active missing user id', content); console.error('friend-active missing user id', content);
friendStore.updateFriend(content.userId, 'active'); runUpdateFriendFlow(content.userId, 'active');
} }
break; break;

View File

@@ -5,19 +5,20 @@ import { useI18n } from 'vue-i18n';
import Noty from 'noty'; import Noty from 'noty';
import { closeWebSocket, initWebsocket } from '../service/websocket'; import {
runLoginSuccessFlow,
runLogoutFlow
} from '../coordinators/authCoordinator';
import { AppDebug } from '../service/appConfig'; import { AppDebug } from '../service/appConfig';
import { authRequest } from '../api'; import { authRequest } from '../api';
import { createAuthAutoLoginCoordinator } from '../coordinators/authAutoLoginCoordinator';
import { createAuthCoordinator } from '../coordinators/authCoordinator';
import { database } from '../service/database'; import { database } from '../service/database';
import { escapeTag } from '../shared/utils'; import { escapeTag } from '../shared/utils';
import { queryClient } from '../queries'; import { initWebsocket } from '../service/websocket';
import { request } from '../service/request'; import { request } from '../service/request';
import { runHandleAutoLoginFlow } from '../coordinators/authAutoLoginCoordinator';
import { useAdvancedSettingsStore } from './settings/advanced'; import { useAdvancedSettingsStore } from './settings/advanced';
import { useGeneralSettingsStore } from './settings/general'; import { useGeneralSettingsStore } from './settings/general';
import { useModalStore } from './modal'; import { useModalStore } from './modal';
import { useNotificationStore } from './notification';
import { useUpdateLoopStore } from './updateLoop'; import { useUpdateLoopStore } from './updateLoop';
import { useUserStore } from './user'; import { useUserStore } from './user';
import { watchState } from '../service/watchState'; import { watchState } from '../service/watchState';
@@ -31,7 +32,6 @@ import * as workerTimers from 'worker-timers';
export const useAuthStore = defineStore('Auth', () => { export const useAuthStore = defineStore('Auth', () => {
const advancedSettingsStore = useAdvancedSettingsStore(); const advancedSettingsStore = useAdvancedSettingsStore();
const generalSettingsStore = useGeneralSettingsStore(); const generalSettingsStore = useGeneralSettingsStore();
const notificationStore = useNotificationStore();
const userStore = useUserStore(); const userStore = useUserStore();
const updateLoopStore = useUpdateLoopStore(); const updateLoopStore = useUpdateLoopStore();
const modalStore = useModalStore(); const modalStore = useModalStore();
@@ -168,15 +168,7 @@ export const useAuthStore = defineStore('Auth', () => {
* *
*/ */
async function handleLogoutEvent() { async function handleLogoutEvent() {
if (watchState.isLoggedIn) { await runLogoutFlow();
new Noty({
type: 'success',
text: t('message.auth.logout_greeting', {
name: `<strong>${escapeTag(userStore.currentUser.displayName)}</strong>`
})
}).show();
}
await authCoordinator.runLogoutFlow();
} }
/** /**
@@ -828,7 +820,7 @@ export const useAuthStore = defineStore('Auth', () => {
} else if (json.requiresTwoFactorAuth) { } else if (json.requiresTwoFactorAuth) {
promptTOTP(); promptTOTP();
} else { } else {
authCoordinator.runLoginSuccessFlow(json); runLoginSuccessFlow(json);
} }
} }
@@ -836,7 +828,7 @@ export const useAuthStore = defineStore('Auth', () => {
* *
*/ */
async function handleAutoLogin() { async function handleAutoLogin() {
await authAutoLoginCoordinator.runHandleAutoLoginFlow(); await runHandleAutoLoginFlow();
} }
/** /**
@@ -894,56 +886,6 @@ export const useAuthStore = defineStore('Auth', () => {
attemptingAutoLogin.value = value; attemptingAutoLogin.value = value;
} }
const authAutoLoginCoordinator = createAuthAutoLoginCoordinator({
getIsAttemptingAutoLogin: () => attemptingAutoLogin.value,
setAttemptingAutoLogin,
getLastUserLoggedIn: () => loginForm.value.lastUserLoggedIn,
getSavedCredentials,
isPrimaryPasswordEnabled: () =>
advancedSettingsStore.enablePrimaryPassword,
handleLogoutEvent,
autoLoginAttempts: state.autoLoginAttempts,
relogin,
notifyAutoLoginSuccess: () => {
if (AppDebug.errorNoty) {
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 { return {
state, state,

View File

@@ -13,6 +13,7 @@ import {
import { avatarRequest, favoriteRequest, queryRequest } from '../api'; import { avatarRequest, favoriteRequest, queryRequest } from '../api';
import { database } from '../service/database'; import { database } from '../service/database';
import { processBulk } from '../service/request'; import { processBulk } from '../service/request';
import { runUpdateFriendFlow } from '../coordinators/friendPresenceCoordinator';
import { useAppearanceSettingsStore } from './settings/appearance'; import { useAppearanceSettingsStore } from './settings/appearance';
import { useAvatarStore } from './avatar'; import { useAvatarStore } from './avatar';
import { useFriendStore } from './friend'; import { useFriendStore } from './friend';
@@ -339,7 +340,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
function handleFavorite(args) { function handleFavorite(args) {
args.ref = applyFavoriteCached(args.json); args.ref = applyFavoriteCached(args.json);
applyFavorite(args.ref.type, args.ref.favoriteId); applyFavorite(args.ref.type, args.ref.favoriteId);
friendStore.updateFriend(args.ref.favoriteId); runUpdateFriendFlow(args.ref.favoriteId);
const { ref } = args; const { ref } = args;
const userDialog = userStore.userDialog; const userDialog = userStore.userDialog;
if (userDialog.visible && ref.favoriteId === userDialog.id) { if (userDialog.visible && ref.favoriteId === userDialog.id) {
@@ -437,7 +438,7 @@ export const useFavoriteStore = defineStore('Favorite', () => {
(id) => id !== ref.favoriteId (id) => id !== ref.favoriteId
); );
friendStore.updateFriend(ref.favoriteId); runUpdateFriendFlow(ref.favoriteId);
friendStore.updateSidebarFavorites(); friendStore.updateSidebarFavorites();
const userDialog = userStore.userDialog; const userDialog = userStore.userDialog;
if (userDialog.visible && userDialog.id === ref.favoriteId) { if (userDialog.visible && userDialog.id === ref.favoriteId) {

View File

@@ -1,6 +1,5 @@
import { computed, reactive, ref, watch } from 'vue'; import { computed, reactive, ref, watch } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@@ -9,24 +8,27 @@ import {
createRateLimiter, createRateLimiter,
executeWithBackoff, executeWithBackoff,
getFriendsSortFunction, getFriendsSortFunction,
getGroupName,
getNameColour, getNameColour,
getUserMemo, getUserMemo,
getWorldName, isRealInstance
isRealInstance,
migrateMemos
} from '../shared/utils'; } from '../shared/utils';
import { friendRequest, userRequest } from '../api'; 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 { AppDebug } from '../service/appConfig';
import { createFriendPresenceCoordinator } from '../coordinators/friendPresenceCoordinator';
import { createFriendRelationshipCoordinator } from '../coordinators/friendRelationshipCoordinator';
import { createFriendSyncCoordinator } from '../coordinators/friendSyncCoordinator';
import { database } from '../service/database'; import { database } from '../service/database';
import { reconnectWebSocket } from '../service/websocket';
import { useAppearanceSettingsStore } from './settings/appearance'; import { useAppearanceSettingsStore } from './settings/appearance';
import { useAuthStore } from './auth';
import { useFavoriteStore } from './favorite'; import { useFavoriteStore } from './favorite';
import { useFeedStore } from './feed';
import { useGeneralSettingsStore } from './settings/general'; import { useGeneralSettingsStore } from './settings/general';
import { useGroupStore } from './group'; import { useGroupStore } from './group';
import { useLocationStore } from './location'; import { useLocationStore } from './location';
@@ -34,7 +36,6 @@ import { useModalStore } from './modal';
import { useNotificationStore } from './notification'; import { useNotificationStore } from './notification';
import { useSharedFeedStore } from './sharedFeed'; import { useSharedFeedStore } from './sharedFeed';
import { useUiStore } from './ui'; import { useUiStore } from './ui';
import { useUpdateLoopStore } from './updateLoop';
import { useUserStore } from './user'; import { useUserStore } from './user';
import { watchState } from '../service/watchState'; import { watchState } from '../service/watchState';
@@ -47,12 +48,9 @@ export const useFriendStore = defineStore('Friend', () => {
const generalSettingsStore = useGeneralSettingsStore(); const generalSettingsStore = useGeneralSettingsStore();
const userStore = useUserStore(); const userStore = useUserStore();
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
const feedStore = useFeedStore();
const uiStore = useUiStore(); const uiStore = useUiStore();
const groupStore = useGroupStore(); const groupStore = useGroupStore();
const sharedFeedStore = useSharedFeedStore(); const sharedFeedStore = useSharedFeedStore();
const updateLoopStore = useUpdateLoopStore();
const authStore = useAuthStore();
const locationStore = useLocationStore(); const locationStore = useLocationStore();
const favoriteStore = useFavoriteStore(); const favoriteStore = useFavoriteStore();
const modalStore = useModalStore(); const modalStore = useModalStore();
@@ -64,7 +62,7 @@ export const useFriendStore = defineStore('Friend', () => {
friendNumber: 0 friendNumber: 0
}); });
let friendLog = new Map(); const friendLog = new Map();
const friends = reactive(new Map()); const friends = reactive(new Map());
@@ -237,7 +235,7 @@ export const useFriendStore = defineStore('Friend', () => {
onlineFriendCount.value = 0; onlineFriendCount.value = 0;
pendingOfflineMap.clear(); pendingOfflineMap.clear();
if (isLoggedIn) { if (isLoggedIn) {
initFriendsList(); runInitFriendsListFlow(t);
pendingOfflineWorkerFunction(); pendingOfflineWorkerFunction();
} else { } else {
if (pendingOfflineWorker !== null) { if (pendingOfflineWorker !== null) {
@@ -313,7 +311,7 @@ export const useFriendStore = defineStore('Friend', () => {
return; return;
} }
D.isFriend = false; D.isFriend = false;
deleteFriendship(args.params.userId); runDeleteFriendshipFlow(args.params.userId);
deleteFriend(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() { async function pendingOfflineWorkerFunction() {
pendingOfflineWorker = workerTimers.setInterval(() => { pendingOfflineWorker = workerTimers.setInterval(() => {
friendPresenceCoordinator.runPendingOfflineTickFlow(); runPendingOfflineTickFlow();
}, 1000); }, 1000);
} }
@@ -454,7 +444,7 @@ export const useFriendStore = defineStore('Friend', () => {
for (const friend of map) { for (const friend of map) {
const [id, state_input] = friend; const [id, state_input] = friend;
if (friends.has(id)) { if (friends.has(id)) {
updateFriend(id, state_input); runUpdateFriendFlow(id, state_input);
} else { } else {
addFriend(id, state_input); addFriend(id, state_input);
} }
@@ -698,10 +688,6 @@ export const useFriendStore = defineStore('Friend', () => {
/** /**
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function refreshFriendsList() {
await friendSyncCoordinator.runRefreshFriendsListFlow();
}
/** /**
* *
* @param forceUpdate * @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 * @param {object} ref
@@ -1075,7 +1045,7 @@ export const useFriendStore = defineStore('Friend', () => {
} }
} }
if (typeof currentUser.friends !== 'undefined') { 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() { function resetFriendLog() {
await friendSyncCoordinator.runInitFriendsListFlow(); 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 { return {
state, state,
@@ -1543,16 +1436,15 @@ export const useFriendStore = defineStore('Friend', () => {
onlineFriendCount, onlineFriendCount,
friendLog, friendLog,
friendLogTable, friendLogTable,
pendingOfflineMap,
pendingOfflineDelay,
initFriendsList,
updateLocalFavoriteFriends, updateLocalFavoriteFriends,
updateSidebarFavorites, updateSidebarFavorites,
updateFriend,
deleteFriend, deleteFriend,
refreshFriendsStatus, refreshFriendsStatus,
addFriend, addFriend,
refreshFriends, refreshFriends,
refreshFriendsList,
updateOnlineFriendCounter, updateOnlineFriendCounter,
getAllUserStats, getAllUserStats,
getAllUserMutualCount, getAllUserMutualCount,
@@ -1562,11 +1454,13 @@ export const useFriendStore = defineStore('Friend', () => {
getFriendRequest, getFriendRequest,
userOnFriend, userOnFriend,
confirmDeleteFriend, confirmDeleteFriend,
updateFriendships,
updateUserCurrentStatus, updateUserCurrentStatus,
handleFriendAdd, handleFriendAdd,
handleFriendDelete, handleFriendDelete,
initFriendLogHistoryTable, handleFriendStatus,
setRefreshFriendsLoading addFriendship,
tryApplyFriendOrder,
resetFriendLog,
initFriendLogHistoryTable
}; };
}); });

View File

@@ -6,18 +6,15 @@ import {
deleteVRChatCache as _deleteVRChatCache, deleteVRChatCache as _deleteVRChatCache,
isRealInstance isRealInstance
} from '../shared/utils'; } from '../shared/utils';
import { createGameCoordinator } from '../coordinators/gameCoordinator';
import { database } from '../service/database'; import { database } from '../service/database';
import { runGameRunningChangedFlow } from '../coordinators/gameCoordinator';
import { useAdvancedSettingsStore } from './settings/advanced'; import { useAdvancedSettingsStore } from './settings/advanced';
import { useAvatarStore } from './avatar'; import { useAvatarStore } from './avatar';
import { useGameLogStore } from './gameLog'; import { useGameLogStore } from './gameLog';
import { useInstanceStore } from './instance';
import { useLaunchStore } from './launch'; import { useLaunchStore } from './launch';
import { useLocationStore } from './location'; import { useLocationStore } from './location';
import { useModalStore } from './modal'; import { useModalStore } from './modal';
import { useNotificationStore } from './notification'; import { useNotificationStore } from './notification';
import { useUpdateLoopStore } from './updateLoop';
import { useUserStore } from './user';
import { useVrStore } from './vr'; import { useVrStore } from './vr';
import { useWorldStore } from './world'; import { useWorldStore } from './world';
@@ -32,11 +29,8 @@ export const useGameStore = defineStore('Game', () => {
const avatarStore = useAvatarStore(); const avatarStore = useAvatarStore();
const launchStore = useLaunchStore(); const launchStore = useLaunchStore();
const worldStore = useWorldStore(); const worldStore = useWorldStore();
const instanceStore = useInstanceStore();
const gameLogStore = useGameLogStore(); const gameLogStore = useGameLogStore();
const vrStore = useVrStore(); const vrStore = useVrStore();
const userStore = useUserStore();
const updateLoopStore = useUpdateLoopStore();
const modalStore = useModalStore(); const modalStore = useModalStore();
const state = reactive({ const state = reactive({
@@ -168,22 +162,6 @@ export const useGameStore = defineStore('Game', () => {
VRChatCacheSizeLoading.value = false; VRChatCacheSizeLoading.value = false;
} }
const gameCoordinator = createGameCoordinator({
userStore,
instanceStore,
updateLoopStore,
locationStore,
gameLogStore,
vrStore,
avatarStore,
configRepository,
workerTimers,
checkVRChatDebugLogging,
autoVRChatCacheManagement,
checkIfGameCrashed,
getIsGameNoVR: () => isGameNoVR.value
});
// use in C# // use in C#
/** /**
* @param {boolean} isGameRunningArg Game running flag from IPC. * @param {boolean} isGameRunningArg Game running flag from IPC.
@@ -195,7 +173,7 @@ export const useGameStore = defineStore('Game', () => {
} }
if (isGameRunningArg !== isGameRunning.value) { if (isGameRunningArg !== isGameRunning.value) {
isGameRunning.value = isGameRunningArg; isGameRunning.value = isGameRunningArg;
await gameCoordinator.runGameRunningChangedFlow(isGameRunningArg); await runGameRunningChangedFlow(isGameRunningArg);
console.log(new Date(), 'isGameRunning', isGameRunningArg); console.log(new Date(), 'isGameRunning', isGameRunningArg);
} }
@@ -300,6 +278,8 @@ export const useGameStore = defineStore('Game', () => {
setIsGameNoVR, setIsGameNoVR,
getVRChatRegistryKey, getVRChatRegistryKey,
checkVRChatDebugLogging, checkVRChatDebugLogging,
autoVRChatCacheManagement,
checkIfGameCrashed,
updateIsHmdAfk updateIsHmdAfk
}; };
}); });

View File

@@ -3,6 +3,7 @@ import { watch } from 'vue';
import { database } from '../service/database'; import { database } from '../service/database';
import { groupRequest } from '../api'; import { groupRequest } from '../api';
import { runRefreshFriendsListFlow } from '../coordinators/friendSyncCoordinator';
import { useAuthStore } from './auth'; import { useAuthStore } from './auth';
import { useDiscordPresenceSettingsStore } from './settings/discordPresence'; import { useDiscordPresenceSettingsStore } from './settings/discordPresence';
import { useFriendStore } from './friend'; import { useFriendStore } from './friend';
@@ -62,6 +63,9 @@ export const useUpdateLoopStore = defineStore('UpdateLoop', () => {
const ipcTimeout = state.ipcTimeout; const ipcTimeout = state.ipcTimeout;
/**
*
*/
async function updateLoop() { async function updateLoop() {
try { try {
if (watchState.isLoggedIn) { if (watchState.isLoggedIn) {
@@ -71,7 +75,7 @@ export const useUpdateLoopStore = defineStore('UpdateLoop', () => {
} }
if (--state.nextFriendsRefresh <= 0) { if (--state.nextFriendsRefresh <= 0) {
state.nextFriendsRefresh = 3600; // 1hour state.nextFriendsRefresh = 3600; // 1hour
friendStore.refreshFriendsList(); runRefreshFriendsListFlow();
authStore.updateStoredUser(userStore.currentUser); authStore.updateStoredUser(userStore.currentUser);
if ( if (
userStore.currentUser.last_activity && userStore.currentUser.last_activity &&
@@ -141,28 +145,48 @@ export const useUpdateLoopStore = defineStore('UpdateLoop', () => {
} }
} }
} catch (err) { } catch (err) {
friendStore.setRefreshFriendsLoading(false); friendStore.isRefreshFriendsLoading = false;
console.error(err); console.error(err);
} }
workerTimers.setTimeout(() => updateLoop(), 1000); workerTimers.setTimeout(() => updateLoop(), 1000);
} }
/**
*
* @param value
*/
function setNextClearVRCXCacheCheck(value) { function setNextClearVRCXCacheCheck(value) {
state.nextClearVRCXCacheCheck = value; state.nextClearVRCXCacheCheck = value;
} }
/**
*
* @param value
*/
function setNextGroupInstanceRefresh(value) { function setNextGroupInstanceRefresh(value) {
state.nextGroupInstanceRefresh = value; state.nextGroupInstanceRefresh = value;
} }
/**
*
* @param value
*/
function setNextDiscordUpdate(value) { function setNextDiscordUpdate(value) {
state.nextDiscordUpdate = value; state.nextDiscordUpdate = value;
} }
/**
*
* @param value
*/
function setIpcTimeout(value) { function setIpcTimeout(value) {
state.ipcTimeout = value; state.ipcTimeout = value;
} }
/**
*
* @param value
*/
function setNextCurrentUserRefresh(value) { function setNextCurrentUserRefresh(value) {
state.nextCurrentUserRefresh = value; state.nextCurrentUserRefresh = value;
} }

View File

@@ -17,7 +17,6 @@ import {
extractFileId, extractFileId,
findUserByDisplayName, findUserByDisplayName,
getAllUserMemos, getAllUserMemos,
getGroupName,
getUserMemo, getUserMemo,
getWorldName, getWorldName,
isRealInstance, isRealInstance,
@@ -27,22 +26,26 @@ import {
} from '../shared/utils'; } from '../shared/utils';
import { import {
avatarRequest, avatarRequest,
groupRequest,
instanceRequest, instanceRequest,
queryRequest, queryRequest,
userRequest userRequest
} from '../api'; } from '../api';
import {
runAvatarSwapFlow,
runFirstLoginFlow,
runHomeLocationSyncFlow,
runPostApplySyncFlow
} from '../coordinators/userSessionCoordinator';
import { processBulk, request } from '../service/request'; import { processBulk, request } from '../service/request';
import { AppDebug } from '../service/appConfig'; import { AppDebug } from '../service/appConfig';
import { createUserEventCoordinator } from '../coordinators/userEventCoordinator';
import { createUserSessionCoordinator } from '../coordinators/userSessionCoordinator';
import { database } from '../service/database'; import { database } from '../service/database';
import { patchUserFromEvent } from '../queries'; import { patchUserFromEvent } from '../queries';
import { runHandleUserUpdateFlow } from '../coordinators/userEventCoordinator';
import { runUpdateFriendFlow } from '../coordinators/friendPresenceCoordinator';
import { useAppearanceSettingsStore } from './settings/appearance'; import { useAppearanceSettingsStore } from './settings/appearance';
import { useAuthStore } from './auth'; import { useAuthStore } from './auth';
import { useAvatarStore } from './avatar'; import { useAvatarStore } from './avatar';
import { useFavoriteStore } from './favorite'; import { useFavoriteStore } from './favorite';
import { useFeedStore } from './feed';
import { useFriendStore } from './friend'; import { useFriendStore } from './friend';
import { useGameStore } from './game'; import { useGameStore } from './game';
import { useGeneralSettingsStore } from './settings/general'; import { useGeneralSettingsStore } from './settings/general';
@@ -55,7 +58,6 @@ import { usePhotonStore } from './photon';
import { useSearchStore } from './search'; import { useSearchStore } from './search';
import { useSharedFeedStore } from './sharedFeed'; import { useSharedFeedStore } from './sharedFeed';
import { useUiStore } from './ui'; import { useUiStore } from './ui';
import { useWorldStore } from './world';
import { watchState } from '../service/watchState'; import { watchState } from '../service/watchState';
import * as workerTimers from 'worker-timers'; import * as workerTimers from 'worker-timers';
@@ -73,8 +75,6 @@ export const useUserStore = defineStore('User', () => {
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
const authStore = useAuthStore(); const authStore = useAuthStore();
const groupStore = useGroupStore(); const groupStore = useGroupStore();
const feedStore = useFeedStore();
const worldStore = useWorldStore();
const uiStore = useUiStore(); const uiStore = useUiStore();
const moderationStore = useModerationStore(); const moderationStore = useModerationStore();
const photonStore = usePhotonStore(); const photonStore = usePhotonStore();
@@ -472,11 +472,11 @@ export const useUserStore = defineStore('User', () => {
{ logLabel: 'User cache cleanup' } { logLabel: 'User cache cleanup' }
); );
cachedUsers.set(ref.id, ref); cachedUsers.set(ref.id, ref);
friendStore.updateFriend(ref.id); runUpdateFriendFlow(ref.id);
} else { } else {
if (json.state !== 'online') { if (json.state !== 'online') {
// offline event before GPS to offline location // offline event before GPS to offline location
friendStore.updateFriend(ref.id, json.state); runUpdateFriendFlow(ref.id, json.state);
} }
const { const {
hasPropChanged: _hasPropChanged, hasPropChanged: _hasPropChanged,
@@ -584,7 +584,7 @@ export const useUserStore = defineStore('User', () => {
instanceStore.getCurrentInstanceUserList(); instanceStore.getCurrentInstanceUserList();
} }
if (ref.state === 'online') { if (ref.state === 'online') {
friendStore.updateFriend(ref.id, ref.state); // online/offline runUpdateFriendFlow(ref.id, ref.state); // online/offline
} }
favoriteStore.applyFavorite('friend', ref.id); favoriteStore.applyFavorite('friend', ref.id);
friendStore.userOnFriend(ref); friendStore.userOnFriend(ref);
@@ -1145,7 +1145,7 @@ export const useUserStore = defineStore('User', () => {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function handleUserUpdate(ref, props) { 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) { function applyCurrentUser(json) {
authStore.setAttemptingAutoLogin(false); authStore.setAttemptingAutoLogin(false);
let ref = currentUser.value; let ref = currentUser.value;
userSessionCoordinator.runAvatarSwapFlow({ runAvatarSwapFlow({
json, json,
ref, ref,
isLoggedIn: watchState.isLoggedIn isLoggedIn: watchState.isLoggedIn
@@ -1545,15 +1545,15 @@ export const useUserStore = defineStore('User', () => {
$travelingToLocation: '', $travelingToLocation: '',
...json ...json
}; };
userSessionCoordinator.runFirstLoginFlow(ref); runFirstLoginFlow(ref);
} }
ref.$isVRCPlus = ref.tags.includes('system_supporter'); ref.$isVRCPlus = ref.tags.includes('system_supporter');
appearanceSettingsStore.applyUserTrustLevel(ref); appearanceSettingsStore.applyUserTrustLevel(ref);
applyUserLanguage(ref); applyUserLanguage(ref);
applyPresenceLocation(ref); applyPresenceLocation(ref);
userSessionCoordinator.runPostApplySyncFlow(ref); runPostApplySyncFlow(ref);
userSessionCoordinator.runHomeLocationSyncFlow(ref); runHomeLocationSyncFlow(ref);
// when isGameRunning use gameLog instead of API // when isGameRunning use gameLog instead of API
const $location = parseLocation(locationStore.lastLocation.location); const $location = parseLocation(locationStore.lastLocation.location);
@@ -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 { return {
state, state,

View File

@@ -21,7 +21,7 @@
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
:disabled="isRefreshFriendsLoading" :disabled="isRefreshFriendsLoading"
@click="refreshFriendsList"> @click="runRefreshFriendsListFlow">
<Spinner v-if="isRefreshFriendsLoading" /> <Spinner v-if="isRefreshFriendsLoading" />
<RefreshCw v-else /> <RefreshCw v-else />
</Button> </Button>
@@ -267,6 +267,7 @@
useGroupStore, useGroupStore,
useNotificationStore useNotificationStore
} from '../../stores'; } from '../../stores';
import { runRefreshFriendsListFlow } from '../../coordinators/friendSyncCoordinator';
import { normalizeFavoriteGroupsChange, resolveFavoriteGroups } from './sidebarSettingsUtils'; import { normalizeFavoriteGroupsChange, resolveFavoriteGroups } from './sidebarSettingsUtils';
import { useGlobalSearchStore } from '../../stores/globalSearch'; import { useGlobalSearchStore } from '../../stores/globalSearch';
@@ -277,7 +278,6 @@
import NotificationCenterSheet from './components/NotificationCenterSheet.vue'; import NotificationCenterSheet from './components/NotificationCenterSheet.vue';
const { friends, isRefreshFriendsLoading, onlineFriendCount } = storeToRefs(useFriendStore()); const { friends, isRefreshFriendsLoading, onlineFriendCount } = storeToRefs(useFriendStore());
const { refreshFriendsList } = useFriendStore();
const { groupInstances } = storeToRefs(useGroupStore()); const { groupInstances } = storeToRefs(useGroupStore());
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
const { isNotificationCenterOpen, hasUnseenNotifications } = storeToRefs(notificationStore); const { isNotificationCenterOpen, hasUnseenNotifications } = storeToRefs(notificationStore);