import { login as loginMasto } from 'masto' import type { Account, AccountCredentials, Instance, WsEvents } from 'masto' import type { Ref } from 'vue' import type { UserLogin } from '~/types' import { DEFAULT_POST_CHARS_LIMIT, DEFAULT_SERVER, STORAGE_KEY_CURRENT_USER, STORAGE_KEY_NOTIFICATION, STORAGE_KEY_NOTIFICATION_POLICY, STORAGE_KEY_SERVERS, STORAGE_KEY_USERS, } from '~/constants' import type { PushNotificationPolicy, PushNotificationRequest } from '~/composables/push-notifications/types' const mock = process.mock const users = useLocalStorage(STORAGE_KEY_USERS, mock ? [mock.user] : [], { deep: true }) const servers = useLocalStorage>(STORAGE_KEY_SERVERS, mock ? mock.server : {}, { deep: true }) const currentUserId = useLocalStorage(STORAGE_KEY_CURRENT_USER, mock ? mock.user.account.id : '') export const currentUser = computed(() => { let user: UserLogin | undefined if (currentUserId.value) { user = users.value.find(user => user.account?.id === currentUserId.value) if (user) return user } // Fallback to the first account return users.value[0] }) export const currentUserHandle = computed(() => currentUser.value?.account.id ? `${currentUser.value.account.acct}@${currentUser.value.server}` : '[anonymous]', ) export const publicServer = ref(DEFAULT_SERVER) const publicInstance = ref(null) export const currentServer = computed(() => currentUser.value?.server || publicServer.value) export const useUsers = () => users export const currentInstance = computed(() => currentUserId.value ? servers.value[currentUserId.value] ?? null : publicInstance.value) export const characterLimit = computed(() => currentInstance.value?.configuration.statuses.maxCharacters ?? DEFAULT_POST_CHARS_LIMIT) export async function loginTo(user?: Omit & { account?: AccountCredentials }) { const config = useRuntimeConfig() const route = useRoute() const router = useRouter() const server = user?.server || route.params.server as string || publicServer.value const masto = await loginMasto({ url: `https://${server}`, accessToken: user?.token, disableVersionCheck: !!config.public.disableVersionCheck, }) if (!user?.token) { publicServer.value = server publicInstance.value = await masto.instances.fetch() } else { try { const [me, server, pushSubscription] = await Promise.all([ masto.accounts.verifyCredentials(), masto.instances.fetch(), // we get 404 response instead empty data masto.pushSubscriptions.fetch().catch(() => Promise.resolve(undefined)), ]) user.account = me user.pushSubscription = pushSubscription currentUserId.value = me.id servers.value[me.id] = server if (!user.account.acct.includes('@')) user.account.acct = `${user.account.acct}@${server.uri}` if (!users.value.some(u => u.server === user.server && u.token === user.token)) users.value.push(user as UserLogin) } catch { await signout() } } if ('server' in route.params && user?.token) { await router.push({ ...route, force: true, }) } return masto } export async function removePushNotifications(user: UserLogin, fromSWPushManager = true) { if (!useRuntimeConfig().public.pwaEnabled || !user.pushSubscription) return // unsubscribe push notifications try { await useMasto().pushSubscriptions.remove() } catch { // ignore } // clear push subscription user.pushSubscription = undefined const { acct } = user.account // clear request notification permission delete useLocalStorage(STORAGE_KEY_NOTIFICATION, {}).value[acct] // clear push notification policy delete useLocalStorage(STORAGE_KEY_NOTIFICATION_POLICY, {}).value[acct] // we remove the sw push manager if required and there are no more accounts with subscriptions if (fromSWPushManager && (users.value.length === 0 || users.value.every(u => !u.pushSubscription))) { // clear sw push subscription try { const registration = await navigator.serviceWorker.ready const subscription = await registration.pushManager.getSubscription() if (subscription) await subscription.unsubscribe() } catch { // juts ignore } } } export async function signout() { // TODO: confirm if (!currentUser.value) return const _currentUserId = currentUser.value.account.id const index = users.value.findIndex(u => u.account?.id === _currentUserId) if (index !== -1) { // Clear stale data clearUserLocalStorage() delete servers.value[_currentUserId] await removePushNotifications(currentUser.value) currentUserId.value = '' // Remove the current user from the users users.value.splice(index, 1) } // Set currentUserId to next user if available currentUserId.value = users.value[0]?.account?.id if (!currentUserId.value) await useRouter().push('/') await loginTo(currentUser.value) } const notifications = reactive, number]>>({}) export const useNotifications = () => { const id = currentUser.value?.account.id const masto = useMasto() const clearNotifications = () => { if (!id || !notifications[id]) return notifications[id]![1] = 0 } async function connect(): Promise { if (!isMastoInitialised.value || !id || notifications[id] || !currentUser.value?.token) return const stream = masto.stream.streamUser() notifications[id] = [stream, 0] ;(await stream).on('notification', () => { if (notifications[id]) notifications[id]![1]++ }) } function disconnect(): void { if (!id || !notifications[id]) return notifications[id]![0].then(stream => stream.disconnect()) notifications[id] = undefined } watch(currentUser, disconnect) connect() return { notifications: computed(() => id ? notifications[id]?.[1] ?? 0 : 0), disconnect, clearNotifications, } } export function checkLogin() { if (!currentUser.value) { openSigninDialog() return false } return true } /** * Create reactive storage for the current user */ export function useUserLocalStorage(key: string, initial: () => T) { // @ts-expect-error bind value to the function const storages = useUserLocalStorage._ = useUserLocalStorage._ || new Map>>() if (!storages.has(key)) storages.set(key, useLocalStorage(key, {}, { deep: true })) const all = storages.get(key) as Ref> return computed(() => { const id = currentUser.value?.account.id ? `${currentUser.value.account.acct}@${currentUser.value.server}` : '[anonymous]' all.value[id] = Object.assign(initial(), all.value[id] || {}) return all.value[id] }) } /** * Clear all storages for the given account */ export function clearUserLocalStorage(account?: Account) { if (!account) account = currentUser.value?.account if (!account) return const id = `${account.acct}@${currentUser.value?.server}` // @ts-expect-error bind value to the function ;(useUserLocalStorage._ as Map>>).forEach((storage) => { if (storage.value[id]) delete storage.value[id] }) }