diff --git a/src/App.js b/src/App.js index ac6885f3..f283b3f1 100644 --- a/src/App.js +++ b/src/App.js @@ -18,6 +18,7 @@ import { windowWidth, windowHeight } from './services/window_utils/window_utils' import { mapGetters } from 'vuex' import { defineAsyncComponent } from 'vue' import { useShoutStore } from './stores/shout' +import { useInterfaceStore } from './stores/interface' export default { name: 'app', @@ -113,7 +114,7 @@ export default { hideShoutbox () { return this.$store.getters.mergedConfig.hideShoutbox }, - layoutType () { return this.$store.state.interface.layoutType }, + layoutType () { return useInterfaceStore().layoutType }, privateMode () { return this.$store.state.instance.private }, reverseLayout () { const { thirdColumnMode, sidebarRight: reverseSetting } = this.$store.getters.mergedConfig @@ -129,8 +130,8 @@ export default { }, methods: { updateMobileState () { - this.$store.dispatch('setLayoutWidth', windowWidth()) - this.$store.dispatch('setLayoutHeight', windowHeight()) + useInterfaceStore().setLayoutWidth(windowWidth()) + useInterfaceStore().setLayoutHeight(windowHeight()) } } } diff --git a/src/boot/after_store.js b/src/boot/after_store.js index bc9b9996..6a722aff 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -18,6 +18,7 @@ import { applyTheme, applyConfig } from '../services/style_setter/style_setter.j import FaviconService from '../services/favicon_service/favicon_service.js' import { useI18nStore } from '../stores/i18n' +import { useInterfaceStore } from '../stores/interface' let staticInitialResults = null @@ -340,12 +341,16 @@ const checkOAuthToken = async ({ store }) => { }) } -const afterStoreSetup = async ({ pinia, store, i18n }) => { +const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { const app = createApp(App) app.use(pinia) - store.dispatch('setLayoutWidth', windowWidth()) - store.dispatch('setLayoutHeight', windowHeight()) + if (storageError) { + useInterfaceStore().pushGlobalNotice({ messageKey: 'errors.storage_unavailable', level: 'error' }) + } + + useInterfaceStore().setLayoutWidth(windowWidth()) + useInterfaceStore().setLayoutHeight(windowHeight()) FaviconService.initFaviconService() diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js index 79f24771..28c2be89 100644 --- a/src/components/chat/chat.js +++ b/src/components/chat/chat.js @@ -1,6 +1,7 @@ import _ from 'lodash' import { WSConnectionStatus } from '../../services/api/api.service.js' import { mapGetters, mapState } from 'vuex' +import { mapState as mapPiniaState } from 'pinia' import ChatMessage from '../chat_message/chat_message.vue' import PostStatusForm from '../post_status_form/post_status_form.vue' import ChatTitle from '../chat_title/chat_title.vue' @@ -13,6 +14,7 @@ import { faChevronLeft } from '@fortawesome/free-solid-svg-icons' import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js' +import { useInterfaceStore } from '../../stores/interface.js' library.add( faChevronDown, @@ -90,10 +92,12 @@ const Chat = { 'findOpenedChatByRecipientId', 'mergedConfig' ]), + ...mapPiniaState(useInterfaceStore, { + mobileLayout: store => store.layoutType === 'mobile' + }), ...mapState({ backendInteractor: state => state.api.backendInteractor, mastoUserSocketStatus: state => state.api.mastoUserSocketStatus, - mobileLayout: state => state.interface.layoutType === 'mobile', currentUser: state => state.users.currentUser }) }, diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js index ebe09814..9d84811d 100644 --- a/src/components/chat_message/chat_message.js +++ b/src/components/chat_message/chat_message.js @@ -1,4 +1,5 @@ import { mapState, mapGetters } from 'vuex' +import { mapState as mapPiniaState } from 'pinia' import Popover from '../popover/popover.vue' import Attachment from '../attachment/attachment.vue' import UserAvatar from '../user_avatar/user_avatar.vue' @@ -12,6 +13,7 @@ import { faTimes, faEllipsisH } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from '../../stores/interface' library.add( faTimes, @@ -65,8 +67,10 @@ const ChatMessage = { hasAttachment () { return this.message.attachments.length > 0 }, + ...mapPiniaState(useInterfaceStore, { + betterShadow: store => store.browserSupport.cssFilter + }), ...mapState({ - betterShadow: state => state.interface.browserSupport.cssFilter, currentUser: state => state.users.currentUser, restrictedNicknames: state => state.instance.restrictedNicknames }), diff --git a/src/components/desktop_nav/desktop_nav.js b/src/components/desktop_nav/desktop_nav.js index 745b1a81..69ac7b73 100644 --- a/src/components/desktop_nav/desktop_nav.js +++ b/src/components/desktop_nav/desktop_nav.js @@ -14,6 +14,7 @@ import { faCog, faInfoCircle } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from '../../stores/interface' library.add( faSignInAlt, @@ -107,7 +108,7 @@ export default { this.searchBarHidden = hidden }, openSettingsModal () { - this.$store.dispatch('openSettingsModal') + useInterfaceStore().openSettingsModal() } } } diff --git a/src/components/global_notice_list/global_notice_list.js b/src/components/global_notice_list/global_notice_list.js index e93fba75..0d67eef7 100644 --- a/src/components/global_notice_list/global_notice_list.js +++ b/src/components/global_notice_list/global_notice_list.js @@ -2,6 +2,7 @@ import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from '../../stores/interface' library.add( faTimes @@ -10,12 +11,12 @@ library.add( const GlobalNoticeList = { computed: { notices () { - return this.$store.state.interface.globalNotices + return useInterfaceStore().globalNotices } }, methods: { closeNotice (notice) { - this.$store.dispatch('removeGlobalNotice', notice) + useInterfaceStore().removeGlobalNotice(notice) } } } diff --git a/src/components/lists_edit/lists_edit.js b/src/components/lists_edit/lists_edit.js index c33659df..d929dbdf 100644 --- a/src/components/lists_edit/lists_edit.js +++ b/src/components/lists_edit/lists_edit.js @@ -9,6 +9,7 @@ import { faSearch, faChevronLeft } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from '../../stores/interface' library.add( faSearch, @@ -128,7 +129,7 @@ const ListsNew = { this.$router.push({ name: 'lists-timeline', params: { id: listId } }) }) .catch((e) => { - this.$store.dispatch('pushGlobalNotice', { + useInterfaceStore().pushGlobalNotice({ messageKey: 'lists.error', messageArgs: [e.message], level: 'error' diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index 420db4f0..d5b7fd26 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -25,6 +25,7 @@ import { faExpandAlt, faCompressAlt } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from '../../stores/interface' library.add( faCheck, @@ -43,7 +44,7 @@ const Notification = { data () { return { statusExpanded: false, - betterShadow: this.$store.state.interface.browserSupport.cssFilter, + betterShadow: useInterfaceStore().browserSupport.cssFilter, unmuted: false, showingApproveConfirmDialog: false, showingDenyConfirmDialog: false diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index d499d3d6..e334d517 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -11,6 +11,7 @@ import { import FaviconService from '../../services/favicon_service/favicon_service.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faCircleNotch, faArrowUp, faMinus } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from '../../stores/interface' library.add( faCircleNotch, @@ -75,11 +76,11 @@ const Notifications = { return this.$store.state.statuses.notifications.loading }, noHeading () { - const { layoutType } = this.$store.state.interface + const { layoutType } = useInterfaceStore() return this.minimalMode || layoutType === 'mobile' }, teleportTarget () { - const { layoutType } = this.$store.state.interface + const { layoutType } = useInterfaceStore() const map = { wide: '#notifs-column', mobile: '#mobile-notifications' @@ -87,7 +88,7 @@ const Notifications = { return map[layoutType] || '#notifs-sidebar' }, popoversZLayer () { - const { layoutType } = this.$store.state.interface + const { layoutType } = useInterfaceStore() return layoutType === 'mobile' ? 'navbar' : null }, notificationsToDisplay () { @@ -114,10 +115,10 @@ const Notifications = { unseenCountTitle (count) { if (count > 0) { FaviconService.drawFaviconBadge() - this.$store.dispatch('setPageTitle', `(${count})`) + useInterfaceStore().setPageTitle(`(${count})`) } else { FaviconService.clearFaviconBadge() - this.$store.dispatch('setPageTitle', '') + useInterfaceStore().setPageTitle('') } }, teleportTarget () { diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index b75fee69..69eb21af 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -11,7 +11,8 @@ import { findOffset } from '../../services/offset_finder/offset_finder.service.j import { propsToNative } from '../../services/attributes_helper/attributes_helper.service.js' import { reject, map, uniqBy, debounce } from 'lodash' import suggestor from '../emoji_input/suggestor.js' -import { mapGetters, mapState } from 'vuex' +import { mapGetters } from 'vuex' +import { mapState } from 'pinia' import Checkbox from '../checkbox/checkbox.vue' import Select from '../select/select.vue' @@ -24,6 +25,7 @@ import { faTimes, faCircleNotch } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from '../../stores/interface.js' library.add( faSmileBeam, @@ -266,8 +268,8 @@ const PostStatusForm = { return typeof this.statusId !== 'undefined' && this.statusId.trim() !== '' }, ...mapGetters(['mergedConfig']), - ...mapState({ - mobileLayout: state => state.interface.mobileLayout + ...mapState(useInterfaceStore, { + mobileLayout: store => store.mobileLayout }) }, watch: { @@ -629,7 +631,7 @@ const PostStatusForm = { this.idempotencyKey = Date.now().toString() }, openProfileTab () { - this.$store.dispatch('openSettingsModalTab', 'profile') + useInterfaceStore().openSettingsModalTab('profile') }, propsToNative (props) { return propsToNative(props) diff --git a/src/components/quick_filter_settings/quick_filter_settings.js b/src/components/quick_filter_settings/quick_filter_settings.js index e67e3a4b..6986ee55 100644 --- a/src/components/quick_filter_settings/quick_filter_settings.js +++ b/src/components/quick_filter_settings/quick_filter_settings.js @@ -2,6 +2,7 @@ import Popover from '../popover/popover.vue' import { mapGetters } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' import { faFilter, faFont, faWrench } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from '../../stores/interface' library.add( faFilter, @@ -22,7 +23,7 @@ const QuickFilterSettings = { this.$store.dispatch('queueFlushAll') }, openTab (tab) { - this.$store.dispatch('openSettingsModalTab', tab) + useInterfaceStore().openSettingsModalTab(tab) } }, computed: { diff --git a/src/components/quick_view_settings/quick_view_settings.js b/src/components/quick_view_settings/quick_view_settings.js index 2798f37a..e1054dca 100644 --- a/src/components/quick_view_settings/quick_view_settings.js +++ b/src/components/quick_view_settings/quick_view_settings.js @@ -2,6 +2,7 @@ import Popover from '../popover/popover.vue' import { mapGetters } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' import { faList, faFolderTree, faBars, faWrench } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from '../../stores/interface' library.add( faList, @@ -22,7 +23,7 @@ const QuickViewSettings = { this.$store.dispatch('setOption', { name: 'conversationDisplay', value: visibility }) }, openTab (tab) { - this.$store.dispatch('openSettingsModalTab', tab) + useInterfaceStore().openSettingsModalTab(tab) } }, computed: { diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js index 0a72dca1..95edfeb7 100644 --- a/src/components/settings_modal/settings_modal.js +++ b/src/components/settings_modal/settings_modal.js @@ -19,6 +19,7 @@ import { import { faWindowMinimize } from '@fortawesome/free-regular-svg-icons' +import { useInterfaceStore } from '../../stores/interface' const PLEROMAFE_SETTINGS_MAJOR_VERSION = 1 const PLEROMAFE_SETTINGS_MINOR_VERSION = 0 @@ -64,10 +65,10 @@ const SettingsModal = { }, methods: { closeModal () { - this.$store.dispatch('closeSettingsModal') + useInterfaceStore().closeSettingsModal() }, peekModal () { - this.$store.dispatch('togglePeekSettingsModal') + useInterfaceStore().togglePeekSettingsModal() }, importValidator (data) { if (!Array.isArray(data._pleroma_settings_version)) { @@ -99,7 +100,7 @@ const SettingsModal = { } if (minor > PLEROMAFE_SETTINGS_MINOR_VERSION) { - this.$store.dispatch('pushGlobalNotice', { + useInterfaceStore().pushGlobalNotice({ level: 'warning', messageKey: 'settings.file_export_import.errors.file_slightly_new' }) @@ -109,9 +110,9 @@ const SettingsModal = { }, onImportFailure (result) { if (result.error) { - this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_settings_imported', level: 'error' }) + useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_settings_imported', level: 'error' }) } else { - this.$store.dispatch('pushGlobalNotice', { ...result.validationResult, level: 'error' }) + useInterfaceStore().pushGlobalNotice({ ...result.validationResult, level: 'error' }) } }, onImport (data) { @@ -151,16 +152,16 @@ const SettingsModal = { }, computed: { currentSaveStateNotice () { - return this.$store.state.interface.settings.currentSaveStateNotice + return useInterfaceStore().settings.currentSaveStateNotice }, modalActivated () { - return this.$store.state.interface.settingsModalState !== 'hidden' + return useInterfaceStore().settingsModalState !== 'hidden' }, modalOpenedOnce () { - return this.$store.state.interface.settingsModalLoaded + return useInterfaceStore().settingsModalLoaded }, modalPeeked () { - return this.$store.state.interface.settingsModalState === 'minimized' + return useInterfaceStore().settingsModalState === 'minimized' }, expertLevel: { get () { diff --git a/src/components/settings_modal/settings_modal_content.js b/src/components/settings_modal/settings_modal_content.js index 9ac0301f..ca933f6a 100644 --- a/src/components/settings_modal/settings_modal_content.js +++ b/src/components/settings_modal/settings_modal_content.js @@ -21,6 +21,7 @@ import { faEyeSlash, faInfo } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from '../../stores/interface' library.add( faWrench, @@ -52,15 +53,15 @@ const SettingsModalContent = { return !!this.$store.state.users.currentUser }, open () { - return this.$store.state.interface.settingsModalState !== 'hidden' + return useInterfaceStore().settingsModalState !== 'hidden' }, bodyLock () { - return this.$store.state.interface.settingsModalState === 'visible' + return useInterfaceStore().settingsModalState === 'visible' } }, methods: { onOpen () { - const targetTab = this.$store.state.interface.settingsModalTargetTab + const targetTab = useInterfaceStore().settingsModalTargetTab // We're being told to open in specific tab if (targetTab) { const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => { @@ -72,7 +73,7 @@ const SettingsModalContent = { } // Clear the state of target tab, so that next time settings is opened // it doesn't force it. - this.$store.dispatch('clearSettingsModalTargetTab') + useInterfaceStore().clearSettingsModalTargetTab() } }, mounted () { diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js index eeacad48..35b78d71 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -20,6 +20,7 @@ import { faPlus, faCircleNotch } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from '../../../stores/interface' library.add( faTimes, @@ -166,7 +167,7 @@ const ProfileTab = { if (file.size > this.$store.state.instance[slot + 'limit']) { const filesize = fileSizeFormatService.fileSizeFormat(file.size) const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit']) - this.$store.dispatch('pushGlobalNotice', { + useInterfaceStore().pushGlobalNotice({ messageKey: 'upload.error.message', messageArgs: [ this.$t('upload.error.file_too_big', { @@ -257,7 +258,7 @@ const ProfileTab = { .finally(() => { this.backgroundUploading = false }) }, displayUploadError (error) { - this.$store.dispatch('pushGlobalNotice', { + useInterfaceStore().pushGlobalNotice({ messageKey: 'upload.error.message', messageArgs: [error.message], level: 'error' diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js index 4a739f73..47f6417c 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -38,6 +38,7 @@ import Checkbox from 'src/components/checkbox/checkbox.vue' import Select from 'src/components/select/select.vue' import Preview from './preview.vue' +import { useInterfaceStore } from '../../../../stores/interface' // List of color values used in v1 const v1OnlyNames = [ @@ -548,7 +549,7 @@ export default { this.loadTheme(parsed, 'file', forceSource) }, onImportFailure (result) { - this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' }) + useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_theme_imported', level: 'error' }) }, importValidator (parsed) { const version = parsed._pleroma_theme_version diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index a0ab0ccb..5c50eef9 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -20,6 +20,7 @@ import { faList } from '@fortawesome/free-solid-svg-icons' import { useShoutStore } from '../../stores/shout' +import { useInterfaceStore } from '../../stores/interface' library.add( faSignInAlt, @@ -85,8 +86,8 @@ const SideDrawer = { }, timelinesRoute () { let name - if (this.$store.state.interface.lastTimeline) { - name = this.$store.state.interface.lastTimeline + if (useInterfaceStore().lastTimeline) { + name = useInterfaceStore().lastTimeline } name = this.currentUser ? 'friends' : 'public-timeline' if (USERNAME_ROUTES.has(name)) { @@ -116,7 +117,7 @@ const SideDrawer = { GestureService.updateSwipe(e, this.closeGesture) }, openSettingsModal () { - this.$store.dispatch('openSettingsModal') + useInterfaceStore().openSettingsModal() } } } diff --git a/src/components/status/status.js b/src/components/status/status.js index 9a9bca7a..bb86e8c9 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -20,6 +20,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { muteWordHits } from '../../services/status_parser/status_parser.js' import { unescape, uniqBy } from 'lodash' +import { useInterfaceStore } from '../../stores/interface' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -379,7 +380,7 @@ const Status = { return this.$store.state.users.currentUser }, betterShadow () { - return this.$store.state.interface.browserSupport.cssFilter + return useInterfaceStore().browserSupport.cssFilter }, mergedConfig () { return this.$store.getters.mergedConfig diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx index a7ef8560..cbc7809a 100644 --- a/src/components/tab_switcher/tab_switcher.jsx +++ b/src/components/tab_switcher/tab_switcher.jsx @@ -1,9 +1,10 @@ // eslint-disable-next-line no-unused import { h, Fragment } from 'vue' -import { mapState } from 'vuex' +import { mapState } from 'pinia' import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome' import './tab_switcher.scss' +import { useInterfaceStore } from '../../stores/interface' const findFirstUsable = (slots) => slots.findIndex(_ => _.props) @@ -64,8 +65,8 @@ export default { settingsModalVisible () { return this.settingsModalState === 'visible' }, - ...mapState({ - settingsModalState: state => state.interface.settingsModalState + ...mapState(useInterfaceStore, { + settingsModalState: store => store.settingsModalState }) }, beforeUpdate () { diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index b7414610..e2f4d033 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -1,5 +1,5 @@ import Status from '../status/status.vue' -import { mapState } from 'vuex' +import { mapState } from 'pinia' import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js' import Conversation from '../conversation/conversation.vue' import TimelineMenu from '../timeline_menu/timeline_menu.vue' @@ -8,6 +8,7 @@ import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue' import { debounce, throttle, keyBy } from 'lodash' import { library } from '@fortawesome/fontawesome-svg-core' import { faCircleNotch, faCirclePlus, faCog, faMinus, faArrowUp, faCheck } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from '../../stores/interface' library.add( faCircleNotch, @@ -101,8 +102,8 @@ const Timeline = { virtualScrollingEnabled () { return this.$store.getters.mergedConfig.virtualScrolling }, - ...mapState({ - mobileLayout: state => state.interface.layoutType === 'mobile' + ...mapState(useInterfaceStore, { + mobileLayout: store => store.layoutType === 'mobile' }) }, created () { diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js index 5a2a86c2..a9e7893c 100644 --- a/src/components/timeline_menu/timeline_menu.js +++ b/src/components/timeline_menu/timeline_menu.js @@ -8,6 +8,7 @@ import { filterNavigation } from 'src/components/navigation/filter.js' import { faChevronDown } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from '../../stores/interface' library.add(faChevronDown) @@ -36,7 +37,7 @@ const TimelineMenu = { }, created () { if (timelineNames()[this.$route.name]) { - this.$store.dispatch('setLastTimeline', this.$route.name) + useInterfaceStore().setLastTimeline(this.$route.name) } }, computed: { diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index c2db9104..1bc65337 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -24,6 +24,7 @@ import { faExpandAlt } from '@fortawesome/free-solid-svg-icons' import { useMediaViewerStore } from '../../stores/media_viewer' +import { useInterfaceStore } from '../../stores/interface' library.add( faRss, @@ -50,7 +51,7 @@ export default { data () { return { followRequestInProgress: false, - betterShadow: this.$store.state.interface.browserSupport.cssFilter, + betterShadow: useInterfaceStore().browserSupport.cssFilter, showingConfirmMute: false, muteExpiryAmount: 0, muteExpiryUnit: 'minutes' @@ -216,7 +217,7 @@ export default { ) }, openProfileTab () { - this.$store.dispatch('openSettingsModalTab', 'profile') + useInterfaceStore().openSettingsModalTab('profile') }, zoomAvatar () { const attachment = { diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js index 6d59c595..ab9a79b3 100644 --- a/src/lib/persisted_state.js +++ b/src/lib/persisted_state.js @@ -1,6 +1,7 @@ import merge from 'lodash.merge' import localforage from 'localforage' import { each, get, set, cloneDeep } from 'lodash' +import { useInterfaceStore } from '../stores/interface' let loaded = false @@ -76,12 +77,12 @@ export default function createPersistedState ({ .then(success => { if (typeof success !== 'undefined') { if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') { - store.dispatch('settingsSaved', { success }) + useInterfaceStore().settingsSaved({ success }) } } }, error => { if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') { - store.dispatch('settingsSaved', { error }) + useInterfaceStore().settingsSaved({ error }) } }) } diff --git a/src/lib/push_notifications_plugin.js b/src/lib/push_notifications_plugin.js index f75bb823..57258b4e 100644 --- a/src/lib/push_notifications_plugin.js +++ b/src/lib/push_notifications_plugin.js @@ -1,8 +1,10 @@ +import { useInterfaceStore } from '../stores/interface' + export default (store) => { store.subscribe((mutation, state) => { const vapidPublicKey = state.instance.vapidPublicKey const webPushNotification = state.config.webPushNotifications - const permission = state.interface.notificationPermission === 'granted' + const permission = useInterfaceStore().notificationPermission === 'granted' const user = state.users.currentUser const isUserMutation = mutation.type === 'setCurrentUser' diff --git a/src/main.js b/src/main.js index 8c291f54..030d70e6 100644 --- a/src/main.js +++ b/src/main.js @@ -4,7 +4,6 @@ import { createPinia } from 'pinia' import 'custom-event-polyfill' import './lib/event_target_polyfill.js' -import interfaceModule from './modules/interface.js' import instanceModule from './modules/instance.js' import statusesModule from './modules/statuses.js' import listsModule from './modules/lists.js' @@ -62,9 +61,10 @@ const persistedStateOptions = { console.error(e) storageError = true } - const store = createStore({ + + // Temporarily storing as a global variable while we migrate to Pinia + window.vuex = createStore({ modules: { - interface: interfaceModule, instance: instanceModule, // TODO refactor users/statuses modules, they depend on each other users: usersModule, @@ -87,12 +87,10 @@ const persistedStateOptions = { // strict: process.env.NODE_ENV !== 'production' }) - if (storageError) { - store.dispatch('pushGlobalNotice', { messageKey: 'errors.storage_unavailable', level: 'error' }) - } + const store = window.vuex - // Temporarily passing both vuex and pinia stores until migration is fully complete. - afterStoreSetup({ pinia, store, i18n }) + // Temporarily passing pinia and vuex stores along with storageError result until migration is fully complete. + afterStoreSetup({ pinia, store, storageError, i18n }) })() // These are inlined by webpack's DefinePlugin diff --git a/src/modules/api.js b/src/modules/api.js index d6cef55f..0000ddc8 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -3,6 +3,7 @@ import { WSConnectionStatus } from '../services/api/api.service.js' import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js' import { Socket } from 'phoenix' import { useShoutStore } from '../stores/shout.js' +import { useInterfaceStore } from '../stores/interface.js' const retryTimeout = (multiplier) => 1000 * multiplier @@ -132,7 +133,7 @@ const api = { state.mastoUserSocket.addEventListener('open', () => { // Do not show notification when we just opened up the page if (state.mastoUserSocketStatus !== WSConnectionStatus.STARTING_INITIAL) { - dispatch('pushGlobalNotice', { + useInterfaceStore().pushGlobalNotice({ level: 'success', messageKey: 'timeline.socket_reconnected', timeout: 5000 @@ -174,7 +175,7 @@ const api = { dispatch('startFetchingTimeline', { timeline: 'friends' }) dispatch('startFetchingNotifications') dispatch('startFetchingChats') - dispatch('pushGlobalNotice', { + useInterfaceStore().pushGlobalNotice({ level: 'error', messageKey: 'timeline.socket_broke', messageArgs: [code], diff --git a/src/modules/config.js b/src/modules/config.js index c60ff11e..bbabebad 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -3,6 +3,7 @@ import { setPreset, applyTheme, applyConfig } from '../services/style_setter/sty import messages from '../i18n/messages' import localeService from '../services/locale/locale.service.js' import { useI18nStore } from '../stores/i18n.js' +import { useInterfaceStore } from '../stores/interface.js' const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage' @@ -203,7 +204,7 @@ const config = { ) break case 'thirdColumnMode': - dispatch('setLayoutWidth', undefined) + useInterfaceStore().setLayoutWidth(undefined) break } } diff --git a/src/modules/instance.js b/src/modules/instance.js index bb0292da..4a417d83 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -3,6 +3,7 @@ import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js' import apiService from '../services/api/api.service.js' import { instanceDefaultProperties } from './config.js' import { langCodeToCldrName, ensureFinalFallback } from '../i18n/languages.js' +import { useInterfaceStore } from '../stores/interface.js' const SORTED_EMOJI_GROUP_IDS = [ 'smileys-and-emotion', @@ -261,7 +262,7 @@ const instance = { commit('setInstanceOption', { name, value }) switch (name) { case 'name': - dispatch('setPageTitle') + useInterfaceStore().setPageTitle() break case 'shoutAvailable': if (value) { diff --git a/src/modules/reports.js b/src/modules/reports.js index 925792c0..c75377cd 100644 --- a/src/modules/reports.js +++ b/src/modules/reports.js @@ -1,4 +1,5 @@ import filter from 'lodash/filter' +import { useInterfaceStore } from '../stores/interface' const reports = { state: { @@ -46,7 +47,7 @@ const reports = { commit('setReportState', { id, state }) rootState.api.backendInteractor.setReportState({ id, state }).catch(e => { console.error('Failed to set report state', e) - dispatch('pushGlobalNotice', { + useInterfaceStore().pushGlobalNotice({ level: 'error', messageKey: 'general.generic_error_message', messageArgs: [e.message], diff --git a/src/modules/users.js b/src/modules/users.js index a1316ba2..e2204bb1 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -3,6 +3,7 @@ import { windowWidth, windowHeight } from '../services/window_utils/window_utils import oauthApi from '../services/new_api/oauth.js' import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash' import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js' +import { useInterfaceStore } from '../stores/interface.js' // TODO: Unify with mergeOrAdd in statuses.js export const mergeOrAdd = (arr, obj, item) => { @@ -542,9 +543,9 @@ const users = { store.commit('clearNotifications') store.commit('resetStatuses') store.dispatch('resetChats') - store.dispatch('setLastTimeline', 'public-timeline') - store.dispatch('setLayoutWidth', windowWidth()) - store.dispatch('setLayoutHeight', windowHeight()) + useInterfaceStore().setLastTimeline('public-timeline') + useInterfaceStore().setLayoutWidth(windowWidth()) + useInterfaceStore().setLayoutHeight(windowHeight()) store.commit('clearServerSideStorage') }) }, @@ -568,7 +569,7 @@ const users = { store.dispatch('fetchEmoji') getNotificationPermission() - .then(permission => commit('setNotificationPermission', permission)) + .then(permission => useInterfaceStore().setNotificationPermission(permission)) // Set our new backend interactor commit('setBackendInteractor', backendInteractorService(accessToken)) @@ -614,8 +615,8 @@ const users = { // Get user mutes store.dispatch('fetchMutes') - store.dispatch('setLayoutWidth', windowWidth()) - store.dispatch('setLayoutHeight', windowHeight()) + useInterfaceStore().setLayoutWidth(windowWidth()) + useInterfaceStore().setLayoutHeight(windowHeight()) // Fetch our friends store.rootState.api.backendInteractor.fetchFriends({ id: user.id }) diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index 6c247210..3b4f9f7b 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -1,3 +1,4 @@ +import { useInterfaceStore } from '../../stores/interface.js' import apiService from '../api/api.service.js' import { promiseInterval } from '../promise_interval/promise_interval.js' @@ -70,7 +71,7 @@ const fetchNotifications = ({ store, args, older }) => { return notifications }) .catch((error) => { - store.dispatch('pushGlobalNotice', { + useInterfaceStore().pushGlobalNotice({ level: 'error', messageKey: 'notifications.error', messageArgs: [error.message], diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 8501907e..c79590df 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -2,6 +2,7 @@ import { camelCase } from 'lodash' import apiService from '../api/api.service.js' import { promiseInterval } from '../promise_interval/promise_interval.js' +import { useInterfaceStore } from '../../stores/interface.js' const update = ({ store, statuses, timeline, showImmediately, userId, listId, pagination }) => { const ccTimeline = camelCase(timeline) @@ -69,7 +70,7 @@ const fetchAndUpdate = ({ return { statuses, pagination } }) .catch((error) => { - store.dispatch('pushGlobalNotice', { + useInterfaceStore().pushGlobalNotice({ level: 'error', messageKey: 'timeline.error', messageArgs: [error.message], diff --git a/src/stores/interface.js b/src/stores/interface.js new file mode 100644 index 00000000..7a7195d3 --- /dev/null +++ b/src/stores/interface.js @@ -0,0 +1,126 @@ +import { defineStore } from 'pinia' + +export const useInterfaceStore = defineStore('interface', { + state: () => ({ + settingsModalState: 'hidden', + settingsModalLoaded: false, + settingsModalTargetTab: null, + settings: { + currentSaveStateNotice: null, + noticeClearTimeout: null, + notificationPermission: null + }, + browserSupport: { + cssFilter: window.CSS && window.CSS.supports && ( + window.CSS.supports('filter', 'drop-shadow(0 0)') || + window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)') + ) + }, + layoutType: 'normal', + globalNotices: [], + layoutHeight: 0, + lastTimeline: null + }), + actions: { + setPageTitle (option = '') { + try { + document.title = `${option} ${window.vuex.state.instance.name}` + } catch (error) { + console.error(`${error}`) + } + }, + settingsSaved ({ success, error }) { + if (success) { + if (this.noticeClearTimeout) { + clearTimeout(this.noticeClearTimeout) + } + this.settings.currentSaveStateNotice = { error: false, data: success } + this.settings.noticeClearTimeout = setTimeout(() => delete this.settings.currentSaveStateNotice, 2000) + } else { + this.settings.currentSaveStateNotice = { error: true, errorData: error } + } + }, + setNotificationPermission (permission) { + this.notificationPermission = permission + }, + closeSettingsModal () { + this.settingsModalState = 'hidden' + }, + openSettingsModal () { + this.settingsModalState = 'visible' + if (!this.settingsModalLoaded) { + this.settingsModalLoaded = true + } + }, + togglePeekSettingsModal () { + switch (this.settingsModalState) { + case 'minimized': + this.settingsModalState = 'visible' + return + case 'visible': + this.settingsModalState = 'minimized' + return + default: + throw new Error('Illegal minimization state of settings modal') + } + }, + clearSettingsModalTargetTab () { + this.settingsModalTargetTab = null + }, + openSettingsModalTab (value) { + this.settingsModalTargetTab = value + this.openSettingsModal() + }, + removeGlobalNotice (notice) { + this.globalNotices = this.globalNotices.filter(n => n !== notice) + }, + pushGlobalNotice ( + { + messageKey, + messageArgs = {}, + level = 'error', + timeout = 0 + }) { + const notice = { + messageKey, + messageArgs, + level + } + + this.globalNotices.push(notice) + + // Adding a new element to array wraps it in a Proxy, which breaks the comparison + // TODO: Generate UUID or something instead or relying on !== operator? + const newNotice = this.globalNotices[this.globalNotices.length - 1] + if (timeout) { + setTimeout(() => this.removeGlobalNotice(newNotice), timeout) + } + + return newNotice + }, + setLayoutHeight (value) { + this.layoutHeight = value + }, + setLayoutWidth (value) { + let width = value + if (value !== undefined) { + this.layoutWidth = value + } else { + width = this.layoutWidth + } + + const mobileLayout = width <= 800 + const normalOrMobile = mobileLayout ? 'mobile' : 'normal' + const { thirdColumnMode } = window.vuex.getters.mergedConfig + if (thirdColumnMode === 'none' || !window.vuex.state.users.currentUser) { + this.layoutType = normalOrMobile + } else { + const wideLayout = width >= 1300 + this.layoutType = wideLayout ? 'wide' : normalOrMobile + } + }, + setLastTimeline (value) { + this.lastTimeline = value + } + } +})