mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-11 19:03:51 +02:00
refactor coordinators
This commit is contained in:
@@ -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.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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: `<strong>${escapeTag(userStore.currentUser.displayName)}</strong>`
|
||||
})
|
||||
}).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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<void>}
|
||||
*/
|
||||
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<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 { 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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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: `<strong>${escapeTag(userStore.currentUser.displayName)}</strong>`
|
||||
})
|
||||
}).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,
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<void>}
|
||||
*/
|
||||
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
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<void>}
|
||||
*/
|
||||
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,
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
:disabled="isRefreshFriendsLoading"
|
||||
@click="refreshFriendsList">
|
||||
@click="runRefreshFriendsListFlow">
|
||||
<Spinner v-if="isRefreshFriendsLoading" />
|
||||
<RefreshCw v-else />
|
||||
</Button>
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user