diff --git a/changelog.d/oauth-store-to-pinia.change b/changelog.d/oauth-store-to-pinia.change new file mode 100644 index 000000000..b18a489b5 --- /dev/null +++ b/changelog.d/oauth-store-to-pinia.change @@ -0,0 +1 @@ +Internal: Migrate OAuth store to pinia diff --git a/changelog.d/roundup5.skip b/changelog.d/roundup5.skip new file mode 100644 index 000000000..e69de29bb diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 05aaf7d3b..1b133a089 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -1,3 +1,4 @@ +/* global process */ import { createApp } from 'vue' import { createRouter, createWebHistory } from 'vue-router' import vClickOutside from 'click-outside-vue3' @@ -16,6 +17,7 @@ import { applyConfig } from '../services/style_setter/style_setter.js' import FaviconService from '../services/favicon_service/favicon_service.js' import { initServiceWorker, updateFocus } from '../services/sw/sw.js' +import { useOAuthStore } from 'src/stores/oauth' import { useI18nStore } from 'src/stores/i18n' import { useInterfaceStore } from 'src/stores/interface' import { useAnnouncementsStore } from 'src/stores/announcements' @@ -227,8 +229,9 @@ const getStickers = async ({ store }) => { } const getAppSecret = async ({ store }) => { - if (store.state.oauth.userToken) { - store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) + const oauth = useOAuthStore() + if (oauth.userToken) { + store.commit('setBackendInteractor', backendInteractorService(oauth.getToken)) } } @@ -322,20 +325,65 @@ const setConfig = async ({ store }) => { const apiConfig = configInfos[0] const staticConfig = configInfos[1] - await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store })) + getAppSecret({ store }) + await setSettings({ store, apiConfig, staticConfig }) } const checkOAuthToken = async ({ store }) => { - if (store.getters.getUserToken()) { - return store.dispatch('loginUser', store.getters.getUserToken()) + const oauth = useOAuthStore() + if (oauth.getUserToken) { + return store.dispatch('loginUser', oauth.getUserToken) } return Promise.resolve() } const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { const app = createApp(App) + // Must have app use pinia before we do anything that touches the store + // https://pinia.vuejs.org/core-concepts/plugins.html#Introduction + // "Plugins are only applied to stores created after the plugins themselves, and after pinia is passed to the app, otherwise they won't be applied." app.use(pinia) + const waitForAllStoresToLoad = async () => { + // the stores that do not persist technically do not need to be awaited here, + // but that involves either hard-coding the stores in some place (prone to errors) + // or writing another vite plugin to analyze which stores needs persisting (++load time) + const allStores = import.meta.glob('../stores/*.js', { eager: true }) + if (process.env.NODE_ENV === 'development') { + // do some checks to avoid common errors + if (!Object.keys(allStores).length) { + throw new Error('No stores are available. Check the code in src/boot/after_store.js') + } + } + await Promise.all( + Object.entries(allStores) + .map(async ([name, mod]) => { + const isStoreName = name => name.startsWith('use') + if (process.env.NODE_ENV === 'development') { + if (Object.keys(mod).filter(isStoreName).length !== 1) { + throw new Error('Each store file must export exactly one store as a named export. Check your code in src/stores/') + } + } + const storeFuncName = Object.keys(mod).find(isStoreName) + if (storeFuncName && typeof mod[storeFuncName] === 'function') { + const p = mod[storeFuncName]().$persistLoaded + if (!(p instanceof Promise)) { + throw new Error(`${name} store's $persistLoaded is not a Promise. The persist plugin is not applied.`) + } + await p + } else { + throw new Error(`Store module ${name} does not export a 'use...' function`) + } + })) + } + + try { + await waitForAllStoresToLoad() + } catch (e) { + console.error('Cannot load stores:', e) + storageError = e + } + if (storageError) { useInterfaceStore().pushGlobalNotice({ messageKey: 'errors.storage_unavailable', level: 'error' }) } diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue index f0a4b7322..c8bba4c44 100644 --- a/src/components/checkbox/checkbox.vue +++ b/src/components/checkbox/checkbox.vue @@ -69,15 +69,6 @@ export default { display: inline-block; min-height: 1.2em; - &.-radio { - .checkbox-indicator { - &, - &::before { - border-radius: 9999px; - } - } - } - &-indicator, & .label { vertical-align: middle; @@ -117,6 +108,19 @@ export default { box-sizing: border-box; } + &.-radio { + .checkbox-indicator { + &, + &::before { + border-radius: 9999px; + } + + &::before { + content: "•"; + } + } + } + .disabled { .checkbox-indicator::before { background-color: var(--background); diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js index 23ac86bfb..ed3f5dfc6 100644 --- a/src/components/login_form/login_form.js +++ b/src/components/login_form/login_form.js @@ -1,5 +1,7 @@ import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' +import { mapStores } from 'pinia' import oauthApi from '../../services/new_api/oauth.js' +import { useOAuthStore } from 'src/stores/oauth.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes @@ -17,11 +19,11 @@ const LoginForm = { computed: { isPasswordAuth () { return this.requiredPassword }, isTokenAuth () { return this.requiredToken }, + ...mapStores(useOAuthStore), ...mapState({ registrationOpen: state => state.instance.registrationOpen, instance: state => state.instance, loggingIn: state => state.users.loggingIn, - oauth: state => state.oauth }), ...mapGetters( 'authFlow', ['requiredPassword', 'requiredToken', 'requiredMFA'] @@ -41,37 +43,30 @@ const LoginForm = { // NOTE: we do not really need the app token, but obtaining a token and // calling verify_credentials is the only way to ensure the app still works. - this.$store.dispatch('ensureAppToken') + this.oauthStore.ensureAppToken() .then(() => { const app = { - clientId: this.oauth.clientId, - clientSecret: this.oauth.clientSecret, + clientId: this.oauthStore.clientId, + clientSecret: this.oauthStore.clientSecret, } oauthApi.login({ ...app, ...data }) }) }, submitPassword () { - const { clientId } = this.oauth - const data = { - clientId, - oauth: this.oauth, - instance: this.instance.server, - commit: this.$store.commit - } this.error = false // NOTE: we do not really need the app token, but obtaining a token and // calling verify_credentials is the only way to ensure the app still works. - this.$store.dispatch('ensureAppToken').then(() => { + this.oauthStore.ensureAppToken().then(() => { const app = { - clientId: this.oauth.clientId, - clientSecret: this.oauth.clientSecret, + clientId: this.oauthStore.clientId, + clientSecret: this.oauthStore.clientSecret, } oauthApi.getTokenWithCredentials( { ...app, - instance: data.instance, + instance: this.instance.server, username: this.user.username, password: this.user.password } diff --git a/src/components/oauth_callback/oauth_callback.js b/src/components/oauth_callback/oauth_callback.js index a3c7b7f98..02e4c9ffc 100644 --- a/src/components/oauth_callback/oauth_callback.js +++ b/src/components/oauth_callback/oauth_callback.js @@ -1,10 +1,12 @@ import oauth from '../../services/new_api/oauth.js' +import { useOAuthStore } from 'src/stores/oauth.js' const oac = { props: ['code'], mounted () { if (this.code) { - const { clientId, clientSecret } = this.$store.state.oauth + const oauthStore = useOAuthStore() + const { clientId, clientSecret } = oauthStore oauth.getToken({ clientId, @@ -12,7 +14,7 @@ const oac = { instance: this.$store.state.instance.server, code: this.code }).then((result) => { - this.$store.commit('setToken', result.access_token) + oauthStore.setToken(result.access_token) this.$store.dispatch('loginUser', result.access_token) this.$router.push({ name: 'friends' }) }) diff --git a/src/components/poll/poll.js b/src/components/poll/poll.js index f56b68b83..a1b7808f2 100644 --- a/src/components/poll/poll.js +++ b/src/components/poll/poll.js @@ -2,7 +2,6 @@ import Timeago from 'components/timeago/timeago.vue' import genRandomSeed from '../../services/random_seed/random_seed.service.js' import RichContent from 'components/rich_content/rich_content.jsx' import Checkbox from 'components/checkbox/checkbox.vue' -import { forEach, map } from 'lodash' import { usePollsStore } from 'src/stores/polls' export default { @@ -46,6 +45,13 @@ export default { expired () { return (this.poll && this.poll.expired) || false }, + expirationLabel () { + if (this.$store.getters.mergedConfig.useAbsoluteTimeFormat) { + return this.expired ? 'polls.expired_at' : 'polls.expires_at' + } else { + return this.expired ? 'polls.expired' : 'polls.expires_in' + } + }, loggedIn () { return this.$store.state.users.currentUser }, diff --git a/src/components/poll/poll.scss b/src/components/poll/poll.scss index 4cf13a1fd..d56358420 100644 --- a/src/components/poll/poll.scss +++ b/src/components/poll/poll.scss @@ -47,18 +47,12 @@ width: 3.5em; } - .footer { - display: flex; - align-items: center; - flex-wrap: wrap; - } - &.loading * { cursor: progress; } .poll-vote-button { - padding: 0 0.5em; + padding: 0 1em; margin-right: 0.5em; } diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue index b3289c89c..24de14200 100644 --- a/src/components/poll/poll.vue +++ b/src/components/poll/poll.vue @@ -44,7 +44,6 @@ :model-value="choices[index]" @update:model-value="value => activateOption(index, value)" > - {{ choices[index] }} -