diff --git a/public/sw.js b/public/sw.js index 1f63d459..31ba4e73 100644 --- a/public/sw.js +++ b/public/sw.js @@ -94,3 +94,100 @@ const apiRoute = new RegExpRoute( }), ); registerRoute(apiRoute); + +// PUSH NOTIFICATIONS +// ================== + +self.addEventListener('push', (event) => { + const { data } = event; + if (data) { + const payload = data.json(); + console.log('PUSH payload', payload); + const { + access_token, + title, + body, + icon, + notification_id, + notification_type, + preferred_locale, + } = payload; + + if (!!navigator.setAppBadge) { + if (notification_type === 'mention') { + navigator.setAppBadge(1); + } + } + + event.waitUntil( + self.registration.showNotification(title, { + body, + icon, + dir: 'auto', + badge: '/logo-192.png', + lang: preferred_locale, + tag: notification_id, + timestamp: Date.now(), + data: { + access_token, + notification_type, + }, + }), + ); + } +}); + +self.addEventListener('notificationclick', (event) => { + const payload = event.notification; + console.log('NOTIFICATION CLICK payload', payload); + const { badge, body, data, dir, icon, lang, tag, timestamp, title } = payload; + const { access_token, notification_type } = data; + const actions = new Promise((resolve) => { + event.notification.close(); + const url = `/#/notifications?id=${tag}&access_token=${btoa(access_token)}`; + self.clients + .matchAll({ + type: 'window', + includeUncontrolled: true, + }) + .then((clients) => { + console.log('NOTIFICATION CLICK clients 1', clients); + if (clients.length && 'navigate' in clients[0]) { + console.log('NOTIFICATION CLICK clients 2', clients); + const bestClient = + clients.find( + (client) => + client.focused || client.visibilityState === 'visible', + ) || clients[0]; + console.log('NOTIFICATION CLICK navigate', url); + // Check if URL is root / or /notifications + // const clientURL = new URL(bestClient.url); + // if ( + // /^#\/?$/.test(clientURL.hash) || + // /^#\/notifications/i.test(clientURL.hash) + // ) { + // bestClient.navigate(url).then((client) => client?.focus()); + // } else { + // User might be on a different page (e.g. composing a post), so don't navigate anywhere else + if (bestClient) { + console.log('NOTIFICATION CLICK postMessage', bestClient); + bestClient.postMessage?.({ + type: 'notification', + id: tag, + accessToken: access_token, + }); + bestClient.focus(); + } else { + console.log('NOTIFICATION CLICK openWindow', url); + self.clients.openWindow(url); + } + // } + } else { + console.log('NOTIFICATION CLICK openWindow', url); + self.clients.openWindow(url); + } + resolve(); + }); + }); + event.waitUntil(actions); +}); diff --git a/src/app.jsx b/src/app.jsx index 3cdbbdf0..73a56b85 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -22,9 +22,11 @@ import AccountSheet from './components/account-sheet'; import Compose from './components/compose'; import Drafts from './components/drafts'; import Icon, { ICONS } from './components/icon'; +import Link from './components/link'; import Loader from './components/loader'; import MediaModal from './components/media-modal'; import Modal from './components/modal'; +import Notification from './components/notification'; import Shortcuts from './components/shortcuts'; import ShortcutsSettings from './components/shortcuts-settings'; import NotFound from './pages/404'; @@ -60,7 +62,11 @@ import openCompose from './utils/open-compose'; import showToast from './utils/show-toast'; import states, { initStates, saveStatus } from './utils/states'; import store from './utils/store'; -import { getCurrentAccount } from './utils/store-utils'; +import { + getAccountByAccessToken, + getCurrentAccount, +} from './utils/store-utils'; +import './utils/toast-alert'; import useInterval from './utils/useInterval'; import usePageVisibility from './utils/usePageVisibility'; @@ -115,6 +121,7 @@ function App() { const clientID = store.session.get('clientID'); const clientSecret = store.session.get('clientSecret'); + const vapidKey = store.session.get('vapidKey'); (async () => { setUIState('loading'); @@ -128,7 +135,7 @@ function App() { const masto = initClient({ instance: instanceURL, accessToken }); await Promise.allSettled([ initInstance(masto, instanceURL), - initAccount(masto, instanceURL, accessToken), + initAccount(masto, instanceURL, accessToken, vapidKey), ]); initStates(); initPreferences(masto); @@ -446,6 +453,7 @@ function App() { /> )} + ); @@ -537,6 +545,166 @@ function BackgroundService({ isLoggedIn }) { return null; } +function NotificationService() { + if (!('serviceWorker' in navigator)) return null; + + const snapStates = useSnapshot(states); + const { routeNotification } = snapStates; + + console.log('🛎️ Notification service', routeNotification); + + const { id, accessToken } = routeNotification || {}; + const [showNotificationSheet, setShowNotificationSheet] = useState(false); + + useLayoutEffect(() => { + if (!id || !accessToken) return; + const { instance: currentInstance } = api(); + const { masto, instance } = api({ + accessToken, + }); + console.log('API', { accessToken, currentInstance, instance }); + const sameInstance = currentInstance === instance; + const account = accessToken + ? getAccountByAccessToken(accessToken) + : getCurrentAccount(); + (async () => { + const notification = await masto.v1.notifications.fetch(id); + if (notification && account) { + console.log('🛎️ Notification', { id, notification, account }); + const accountInstance = account.instanceURL; + const { type, status, account: notificationAccount } = notification; + const hasModal = !!document.querySelector('#modal-container > *'); + const isFollow = type === 'follow' && !!notificationAccount?.id; + const hasAccount = !!notificationAccount?.id; + const hasStatus = !!status?.id; + if (isFollow && sameInstance) { + // Show account sheet, can handle different instances + states.showAccount = { + account: notificationAccount, + instance: accountInstance, + }; + } else if (hasModal || !sameInstance || (hasAccount && hasStatus)) { + // Show sheet of notification, if + // - there is a modal open + // - the notification is from another instance + // - the notification has both account and status, gives choice for users to go to account or status + setShowNotificationSheet({ + id, + account, + notification, + sameInstance, + }); + } else { + if (hasStatus) { + // Go to status page + location.hash = `/${currentInstance}/s/${status.id}`; + } else if (isFollow) { + // Go to profile page + location.hash = `/${currentInstance}/a/${notificationAccount.id}`; + } else { + // Go to notifications page + location.hash = '/notifications'; + } + } + } else { + console.warn( + '🛎️ Notification not found', + notificationID, + notificationAccessToken, + ); + } + })(); + }, [id, accessToken]); + + useLayoutEffect(() => { + // Listen to message from service worker + const handleMessage = (event) => { + console.log('💥💥💥 Message event', event); + const { type, id, accessToken } = event?.data || {}; + if (type === 'notification') { + states.routeNotification = { + id, + accessToken, + }; + } + }; + console.log('👂👂👂 Listen to message'); + navigator.serviceWorker.addEventListener('message', handleMessage); + return () => { + console.log('👂👂👂 Remove listen to message'); + navigator.serviceWorker.removeEventListener('message', handleMessage); + }; + }, []); + + const onClose = () => { + setShowNotificationSheet(false); + states.routeNotification = null; + + // If url is #/notifications?id=123, go to #/notifications + if (/\/notifications\?id=/i.test(location.hash)) { + location.hash = '/notifications'; + } + }; + + if (showNotificationSheet) { + const { id, account, notification, sameInstance } = showNotificationSheet; + return ( + { + if (e.target === e.currentTarget) { + onClose(); + } + }} + > +
+ +
+ Notification +
+
+ {!sameInstance && ( +

This notification is from your other account.

+ )} +
{ + const { target } = e; + // If button or links + if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A') { + onClose(); + } + }} + > + +
+
+ + View all notifications + +
+
+
+
+ ); + } + + return null; +} + function StatusRoute() { const params = useParams(); const { id, instance } = params; diff --git a/src/components/notification.jsx b/src/components/notification.jsx index b7a50523..5b26aa74 100644 --- a/src/components/notification.jsx +++ b/src/components/notification.jsx @@ -56,12 +56,13 @@ const contentText = { 'favourite+reblog_reply': 'boosted & favourited your reply.', }; -function Notification({ notification, instance, reload }) { +function Notification({ notification, instance, reload, isStatic }) { const { id, status, account, _accounts, _statuses } = notification; let { type } = notification; // status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update - const actualStatusID = status?.reblog?.id || status?.id; + const actualStatus = status?.reblog || status; + const actualStatusID = actualStatus?.id; const currentAccount = store.session.get('currentAccount'); const isSelf = currentAccount === account?.id; @@ -242,7 +243,11 @@ function Notification({ notification, instance, reload }) { : `/s/${actualStatusID}` } > - + {isStatic ? ( + + ) : ( + + )} )} diff --git a/src/pages/login.jsx b/src/pages/login.jsx index 53f9f388..ce5ec8c0 100644 --- a/src/pages/login.jsx +++ b/src/pages/login.jsx @@ -1,6 +1,7 @@ import './login.css'; import { useEffect, useRef, useState } from 'preact/hooks'; +import { useSearchParams } from 'react-router-dom'; import Link from '../components/link'; import Loader from '../components/loader'; @@ -14,8 +15,10 @@ function Login() { const instanceURLRef = useRef(); const cachedInstanceURL = store.local.get('instanceURL'); const [uiState, setUIState] = useState('default'); + const [searchParams] = useSearchParams(); + const instance = searchParams.get('instance'); const [instanceText, setInstanceText] = useState( - cachedInstanceURL?.toLowerCase() || '', + instance || cachedInstanceURL?.toLowerCase() || '', ); const [instancesList, setInstancesList] = useState([]); @@ -44,13 +47,15 @@ function Login() { (async () => { setUIState('loading'); try { - const { client_id, client_secret } = await registerApplication({ - instanceURL, - }); + const { client_id, client_secret, vapid_key } = + await registerApplication({ + instanceURL, + }); if (client_id && client_secret) { store.session.set('clientID', client_id); store.session.set('clientSecret', client_secret); + store.session.set('vapidKey', vapid_key); location.href = await getAuthorizationURL({ instanceURL, diff --git a/src/pages/notifications.jsx b/src/pages/notifications.jsx index 2228d4fd..7e3edfbb 100644 --- a/src/pages/notifications.jsx +++ b/src/pages/notifications.jsx @@ -3,6 +3,7 @@ import './notifications.css'; import { useIdle } from '@uidotdev/usehooks'; import { memo } from 'preact/compat'; import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; +import { useSearchParams } from 'react-router-dom'; import { useSnapshot } from 'valtio'; import AccountBlock from '../components/account-block'; @@ -17,6 +18,7 @@ import enhanceContent from '../utils/enhance-content'; import groupNotifications from '../utils/group-notifications'; import handleContentLinks from '../utils/handle-content-links'; import niceDateTime from '../utils/nice-date-time'; +import { getRegistration } from '../utils/push-notifications'; import shortenNumber from '../utils/shorten-number'; import states, { saveStatus } from '../utils/states'; import { getCurrentInstance } from '../utils/store-utils'; @@ -24,12 +26,16 @@ import useScroll from '../utils/useScroll'; import useTitle from '../utils/useTitle'; const LIMIT = 30; // 30 is the maximum limit :( +const emptySearchParams = new URLSearchParams(); -function Notifications() { +function Notifications({ columnMode }) { useTitle('Notifications', '/notifications'); const { masto, instance } = api(); const snapStates = useSnapshot(states); const [uiState, setUIState] = useState('default'); + const [searchParams] = columnMode ? [emptySearchParams] : useSearchParams(); + const notificationID = searchParams.get('id'); + const notificationAccessToken = searchParams.get('access_token'); const [showMore, setShowMore] = useState(false); const [onlyMentions, setOnlyMentions] = useState(false); const scrollableRef = useRef(); @@ -188,6 +194,31 @@ function Notifications() { const announcementsListRef = useRef(); + useEffect(() => { + if (notificationID) { + states.routeNotification = { + id: notificationID, + accessToken: atob(notificationAccessToken), + }; + } + }, [notificationID, notificationAccessToken]); + + useEffect(() => { + if (uiState === 'default') { + (async () => { + const registration = await getRegistration(); + if (registration) { + const notifications = await registration.getNotifications(); + console.log('🔔 Push notifications', notifications); + // Close all notifications? + // notifications.forEach((notification) => { + // notification.close(); + // }); + } + })(); + } + }, [uiState]); + return (
+

About

{ + (async () => { + setUIState('loading'); + try { + const { subscription, backendSubscription } = await initSubscription(); + if ( + backendSubscription?.policy && + backendSubscription.policy !== 'none' + ) { + setAllowNotifications(true); + const { alerts, policy } = backendSubscription; + previousPolicyRef.current = policy; + const { elements } = pushFormRef.current; + const policyEl = elements.namedItem(policy); + if (policyEl) policyEl.value = policy; + // alerts is {}, iterate it + Object.keys(alerts).forEach((alert) => { + const el = elements.namedItem(alert); + if (el?.type === 'checkbox') { + el.checked = true; + } + }); + } + setUIState('default'); + } catch (err) { + console.warn(err); + if (/outside.*authorized/i.test(err.message)) { + setNeedRelogin(true); + } else { + alert(err?.message || err); + } + setUIState('error'); + } + })(); + }, []); + + const isLoading = uiState === 'loading'; + + return ( +
{ + const values = Object.fromEntries(new FormData(pushFormRef.current)); + const allowNofitications = !!values['policy-allow']; + const params = { + policy: values.policy, + data: { + alerts: { + mention: !!values.mention, + favourite: !!values.favourite, + reblog: !!values.reblog, + follow: !!values.follow, + follow_request: !!values.followRequest, + poll: !!values.poll, + update: !!values.update, + status: !!values.status, + }, + }, + }; + + let alertsCount = 0; + // Remove false values from data.alerts + // API defaults to false anyway + Object.keys(params.data.alerts).forEach((key) => { + if (!params.data.alerts[key]) { + delete params.data.alerts[key]; + } else { + alertsCount++; + } + }); + const policyChanged = previousPolicyRef.current !== params.policy; + + console.log('PN Form', { values, allowNofitications, params }); + + if (allowNofitications && alertsCount > 0) { + if (policyChanged) { + console.debug('Policy changed.'); + removeSubscription() + .then(() => { + updateSubscription(params); + }) + .catch((err) => { + console.warn(err); + alert('Failed to update subscription. Please try again.'); + }); + } else { + updateSubscription(params).catch((err) => { + console.warn(err); + alert('Failed to update subscription. Please try again.'); + }); + } + } else { + removeSubscription().catch((err) => { + console.warn(err); + alert('Failed to remove subscription. Please try again.'); + }); + } + }} + > +

Push Notifications (beta)

+
+
    +
  • + + + {needRelogin && ( +
    +

    + Push permission was not granted since your last login. You'll + need to{' '} + + log in again to grant push permission + + . +

    +
    + )} +
  • +
+
+

+ + NOTE: Push notifications only works for one account. + +

+
+ ); +} + export default Settings; diff --git a/src/utils/api.js b/src/utils/api.js index c9c6a589..c1d73889 100644 --- a/src/utils/api.js +++ b/src/utils/api.js @@ -1,7 +1,12 @@ import { createClient } from 'masto'; import store from './store'; -import { getAccount, getCurrentAccount, saveAccount } from './store-utils'; +import { + getAccount, + getAccountByAccessToken, + getCurrentAccount, + saveAccount, +} from './store-utils'; // Default *fallback* instance const DEFAULT_INSTANCE = 'mastodon.social'; @@ -18,6 +23,7 @@ const apis = {}; // Just in case if I need this one day. // E.g. accountApis['mastodon.social']['ACCESS_TOKEN'] const accountApis = {}; +window.__ACCOUNT_APIS__ = accountApis; // Current account masto instance let currentAccountApi; @@ -92,7 +98,7 @@ export async function initInstance(client, instance) { } // Get the account information and store it -export async function initAccount(client, instance, accessToken) { +export async function initAccount(client, instance, accessToken, vapidKey) { const masto = client; const mastoAccount = await masto.v1.accounts.verifyCredentials(); @@ -102,6 +108,7 @@ export async function initAccount(client, instance, accessToken) { info: mastoAccount, instanceURL: instance.toLowerCase(), accessToken, + vapidKey, }); } @@ -136,6 +143,35 @@ export function api({ instance, accessToken, accountID, account } = {}) { }; } + if (accessToken) { + // If only accessToken is provided, get the masto instance for that accessToken + console.log('X 1', accountApis); + for (const instance in accountApis) { + if (accountApis[instance][accessToken]) { + console.log('X 2', accountApis, instance, accessToken); + return { + masto: accountApis[instance][accessToken], + authenticated: true, + instance, + }; + } else { + console.log('X 3', accountApis, instance, accessToken); + const account = getAccountByAccessToken(accessToken); + if (account) { + const accessToken = account.accessToken; + const instance = account.instanceURL.toLowerCase().trim(); + return { + masto: initClient({ instance, accessToken }), + authenticated: true, + instance, + }; + } else { + throw new Error(`Access token ${accessToken} not found`); + } + } + } + } + // If account is provided, get the masto instance for that account if (account || accountID) { account = account || getAccount(accountID); diff --git a/src/utils/auth.js b/src/utils/auth.js index 600b0c0c..4c0ddee8 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -1,11 +1,13 @@ const { VITE_CLIENT_NAME: CLIENT_NAME, VITE_WEBSITE: WEBSITE } = import.meta .env; +const SCOPES = 'read write follow push'; + export async function registerApplication({ instanceURL }) { const registrationParams = new URLSearchParams({ client_name: CLIENT_NAME, - scopes: 'read write follow', redirect_uris: location.origin + location.pathname, + scopes: SCOPES, website: WEBSITE, }); const registrationResponse = await fetch( @@ -26,7 +28,7 @@ export async function registerApplication({ instanceURL }) { export async function getAuthorizationURL({ instanceURL, client_id }) { const authorizationParams = new URLSearchParams({ client_id, - scope: 'read write follow', + scope: SCOPES, redirect_uri: location.origin + location.pathname, // redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', response_type: 'code', @@ -47,7 +49,7 @@ export async function getAccessToken({ redirect_uri: location.origin + location.pathname, grant_type: 'authorization_code', code, - scope: 'read write follow', + scope: SCOPES, }); const tokenResponse = await fetch(`https://${instanceURL}/oauth/token`, { method: 'POST', diff --git a/src/utils/push-notifications.js b/src/utils/push-notifications.js new file mode 100644 index 00000000..013dd5b3 --- /dev/null +++ b/src/utils/push-notifications.js @@ -0,0 +1,233 @@ +// Utils for push notifications +import { api } from './api'; +import { getCurrentAccount } from './store-utils'; + +// Subscription is an object with the following structure: +// { +// data: { +// alerts: { +// admin: { +// report: boolean, +// signUp: boolean, +// }, +// favourite: boolean, +// follow: boolean, +// mention: boolean, +// poll: boolean, +// reblog: boolean, +// status: boolean, +// update: boolean, +// } +// }, +// policy: "all" | "followed" | "follower" | "none", +// subscription: { +// endpoint: string, +// keys: { +// auth: string, +// p256dh: string, +// }, +// }, +// } + +// Back-end CRUD +// ============= + +function createBackendPushSubscription(subscription) { + const { masto } = api(); + return masto.v1.webPushSubscriptions.create(subscription); +} + +function fetchBackendPushSubscription() { + const { masto } = api(); + return masto.v1.webPushSubscriptions.fetch(); +} + +function updateBackendPushSubscription(subscription) { + const { masto } = api(); + return masto.v1.webPushSubscriptions.update(subscription); +} + +function removeBackendPushSubscription() { + const { masto } = api(); + return masto.v1.webPushSubscriptions.remove(); +} + +// Front-end +// ========= + +export function isPushSupported() { + return 'serviceWorker' in navigator && 'PushManager' in window; +} + +export function getRegistration() { + // return navigator.serviceWorker.ready; + return navigator.serviceWorker.getRegistration(); +} + +async function getSubscription() { + const registration = await getRegistration(); + const subscription = registration + ? await registration.pushManager.getSubscription() + : undefined; + return { registration, subscription }; +} + +function urlBase64ToUint8Array(base64String) { + const padding = '='.repeat((4 - (base64String.length % 4)) % 4); + const base64 = `${base64String}${padding}` + .replace(/-/g, '+') + .replace(/_/g, '/'); + + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + + return outputArray; +} + +// Front-end <-> back-end +// ====================== + +export async function initSubscription() { + if (!isPushSupported()) return; + const { subscription } = await getSubscription(); + let backendSubscription = null; + try { + backendSubscription = await fetchBackendPushSubscription(); + } catch (err) { + if (/(not found|unknown)/i.test(err.message)) { + // No subscription found + } else { + // Other error + throw err; + } + } + console.log('INIT subscription', { + subscription, + backendSubscription, + }); + + // Check if the subscription changed + if (backendSubscription && subscription) { + const sameEndpoint = backendSubscription.endpoint === subscription.endpoint; + const { vapidKey } = getCurrentAccount(); + const sameKey = backendSubscription.serverKey === vapidKey; + if (!sameEndpoint) { + throw new Error('Backend subscription endpoint changed'); + } + if (sameKey) { + // Subscription didn't change + } else { + // Subscription changed + console.error('🔔 Subscription changed', { + sameEndpoint, + serverKey: backendSubscription.serverKey, + vapIdKey: vapidKey, + endpoint1: backendSubscription.endpoint, + endpoint2: subscription.endpoint, + sameKey, + key1: backendSubscription.serverKey, + key2: vapidKey, + }); + throw new Error('Backend subscription key and vapid key changed'); + // Only unsubscribe from backend, not from browser + // await removeBackendPushSubscription(); + // // Now let's resubscribe + // // NOTE: I have no idea if this works + // return await updateSubscription({ + // data: backendSubscription.data, + // policy: backendSubscription.policy, + // }); + } + } + + if (subscription && !backendSubscription) { + // check if account's vapidKey is same as subscription's applicationServerKey + const { vapidKey } = getCurrentAccount(); + const { applicationServerKey } = subscription.options; + const vapidKeyStr = urlBase64ToUint8Array(vapidKey).toString(); + const applicationServerKeyStr = new Uint8Array( + applicationServerKey, + ).toString(); + const sameKey = vapidKeyStr === applicationServerKeyStr; + if (sameKey) { + // Subscription didn't change + } else { + // Subscription changed + console.error('🔔 Subscription changed', { + vapidKeyStr, + applicationServerKeyStr, + sameKey, + }); + // Unsubscribe since backend doesn't have a subscription + await subscription.unsubscribe(); + throw new Error('Subscription key and vapid key changed'); + } + } + + // Check if backend subscription returns 404 + // if (subscription && !backendSubscription) { + // // Re-subscribe to backend + // backendSubscription = await createBackendPushSubscription({ + // subscription, + // data: {}, + // policy: 'all', + // }); + // } + + return { subscription, backendSubscription }; +} + +export async function updateSubscription({ data, policy }) { + console.log('🔔 Updating subscription', { data, policy }); + if (!isPushSupported()) return; + let { registration, subscription } = await getSubscription(); + let backendSubscription = null; + + if (subscription) { + try { + backendSubscription = await updateBackendPushSubscription({ + data, + policy, + }); + // TODO: save subscription in user settings + } catch (error) { + // Backend doesn't have a subscription for this user + // Create a new one + backendSubscription = await createBackendPushSubscription({ + subscription, + data, + policy, + }); + // TODO: save subscription in user settings + } + } else { + // User is not subscribed + const { vapidKey } = getCurrentAccount(); + if (!vapidKey) throw new Error('No server key found'); + subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(vapidKey), + }); + backendSubscription = await createBackendPushSubscription({ + subscription, + data, + policy, + }); + // TODO: save subscription in user settings + } + + return { subscription, backendSubscription }; +} + +export async function removeSubscription() { + if (!isPushSupported()) return; + const { subscription } = await getSubscription(); + if (subscription) { + await removeBackendPushSubscription(); + await subscription.unsubscribe(); + } +} diff --git a/src/utils/states.js b/src/utils/states.js index dde99563..bcb56d9e 100644 --- a/src/utils/states.js +++ b/src/utils/states.js @@ -29,6 +29,7 @@ const states = proxy({ unfurledLinks: {}, statusQuotes: {}, accounts: {}, + routeNotification: null, // Modals showCompose: false, showSettings: false, diff --git a/src/utils/store-utils.js b/src/utils/store-utils.js index f624f9cd..bd228392 100644 --- a/src/utils/store-utils.js +++ b/src/utils/store-utils.js @@ -5,6 +5,11 @@ export function getAccount(id) { return accounts.find((a) => a.info.id === id) || accounts[0]; } +export function getAccountByAccessToken(accessToken) { + const accounts = store.local.getJSON('accounts') || []; + return accounts.find((a) => a.accessToken === accessToken); +} + export function getCurrentAccount() { const currentAccount = store.session.get('currentAccount'); const account = getAccount(currentAccount); @@ -27,6 +32,7 @@ export function saveAccount(account) { acc.info = account.info; acc.instanceURL = account.instanceURL; acc.accessToken = account.accessToken; + acc.vapidKey = account.vapidKey; } else { accounts.push(account); }