logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://anongit.hacktivis.me/git/pleroma-fe.git/
commit: 1f56401a8ef04a1dfdc8e6405aa43a022588cbb2
parent a5c4853987fe1382cc6ceeca2abde19a94280258
Author: HJ <30-hj@users.noreply.git.pleroma.social>
Date:   Mon,  3 Feb 2025 15:52:22 +0000

Merge branch 'migrate/vuex-to-pinia' into 'develop'

Migrate from vuex to pinia

Closes #1202

See merge request pleroma/pleroma-fe!1807

Diffstat:

Achangelog.d/pinia.change1+
Mpackage.json1+
Msrc/App.js14++++++++------
Msrc/App.vue2+-
Msrc/boot/after_store.js29++++++++++++++++++++---------
Msrc/components/account_actions/account_actions.js3++-
Msrc/components/announcement/announcement.js7++++---
Msrc/components/announcements_page/announcements_page.js7++++---
Msrc/components/attachment/attachment.js5+++--
Msrc/components/bookmark_folder_edit/bookmark_folder_edit.js2+-
Msrc/components/chat/chat.js6+++++-
Msrc/components/chat_message/chat_message.js5+++++
Msrc/components/conversation/conversation.js6+++++-
Msrc/components/desktop_nav/desktop_nav.js5+++--
Msrc/components/edit_status_modal/edit_status_modal.js7++++---
Msrc/components/extra_notifications/extra_notifications.js11+++++++++--
Msrc/components/font_control/font_control.js7++++---
Msrc/components/gallery/gallery.js7++++---
Msrc/components/global_notice_list/global_notice_list.js5+++--
Msrc/components/lists/lists.js3++-
Msrc/components/lists_edit/lists_edit.js27+++++++++++++++------------
Msrc/components/lists_menu/lists_menu_content.js6+++++-
Msrc/components/lists_timeline/lists_timeline.js5+++--
Msrc/components/media_modal/media_modal.js13+++++++------
Msrc/components/mobile_nav/mobile_nav.js5++++-
Msrc/components/mobile_post_status_button/mobile_post_status_button.js3++-
Msrc/components/nav_panel/nav_panel.js9+++++++--
Msrc/components/navigation/filter.js2+-
Msrc/components/navigation/navigation.js1+
Msrc/components/navigation/navigation_entry.js3+++
Msrc/components/navigation/navigation_entry.vue6++++++
Msrc/components/navigation/navigation_pins.js11+++++++++--
Msrc/components/notifications/notifications.js16++++++++++------
Msrc/components/poll/poll.js17+++++++----------
Msrc/components/post_status_form/post_status_form.js13+++++++++----
Msrc/components/post_status_form/post_status_form.vue2+-
Msrc/components/post_status_modal/post_status_modal.js11++++++-----
Msrc/components/quick_filter_settings/quick_filter_settings.js10++++++----
Msrc/components/quick_view_settings/quick_view_settings.js10++++++----
Msrc/components/report/report.js5+++--
Msrc/components/settings_modal/admin_tabs/emoji_tab.js2+-
Msrc/components/settings_modal/admin_tabs/frontends_tab.js4++--
Msrc/components/settings_modal/settings_modal.js39++++++++++++++++-----------------------
Msrc/components/settings_modal/settings_modal.vue2+-
Msrc/components/settings_modal/settings_modal_admin_content.js9+++++----
Msrc/components/settings_modal/settings_modal_user_content.js11++++++-----
Msrc/components/settings_modal/tabs/appearance_tab.js50++++++++++++++++++++++----------------------------
Msrc/components/settings_modal/tabs/profile_tab.js5+++--
Msrc/components/settings_modal/tabs/style_tab/style_tab.js16++++++++--------
Msrc/components/settings_modal/tabs/theme_tab/theme_tab.js9+++++----
Msrc/components/shout_panel/shout_panel.js5+++--
Msrc/components/side_drawer/side_drawer.js23+++++++++++++++--------
Msrc/components/status_content/status_content.js3++-
Msrc/components/status_history_modal/status_history_modal.js7++++---
Msrc/components/tab_switcher/tab_switcher.jsx3++-
Msrc/components/timeline/timeline.js7++++---
Msrc/components/timeline_menu/timeline_menu.js6++++--
Msrc/components/user_avatar/user_avatar.js3++-
Msrc/components/user_card/user_card.js12++++++++----
Msrc/components/user_list_menu/user_list_menu.js11++++++-----
Msrc/components/user_reporting_modal/user_reporting_modal.js5+++--
Msrc/lib/persisted_state.js5+++--
Msrc/lib/push_notifications_plugin.js4+++-
Msrc/main.js34+++++-----------------------------
Dsrc/modules/announcements.js135-------------------------------------------------------------------------------
Msrc/modules/api.js8+++++---
Msrc/modules/config.js176++++---------------------------------------------------------------------------
Asrc/modules/default_config_state.js165+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/modules/editStatus.js25-------------------------
Msrc/modules/instance.js3++-
Dsrc/modules/interface.js724-------------------------------------------------------------------------------
Dsrc/modules/lists.js130-------------------------------------------------------------------------------
Dsrc/modules/media_viewer.js40----------------------------------------
Msrc/modules/notifications.js4+++-
Dsrc/modules/polls.js69---------------------------------------------------------------------
Dsrc/modules/postStatus.js31-------------------------------
Dsrc/modules/reports.js64----------------------------------------------------------------
Dsrc/modules/shout.js46----------------------------------------------
Dsrc/modules/statusHistory.js25-------------------------
Msrc/modules/statuses.js3++-
Msrc/modules/users.js13+++++++------
Msrc/services/lists_fetcher/lists_fetcher.service.js3++-
Msrc/services/notification_utils/notification_utils.js7++++---
Msrc/services/notifications_fetcher/notifications_fetcher.service.js3++-
Msrc/services/style_setter/style_setter.js5++++-
Msrc/services/timeline_fetcher/timeline_fetcher.service.js3++-
Asrc/stores/announcements.js115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/stores/editStatus.js17+++++++++++++++++
Asrc/stores/i18n.js14++++++++++++++
Asrc/stores/interface.js674+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/stores/lists.js110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/stores/media_viewer.js30++++++++++++++++++++++++++++++
Asrc/stores/polls.js57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/stores/postStatus.js17+++++++++++++++++
Asrc/stores/reports.js52++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/stores/shout.js32++++++++++++++++++++++++++++++++
Asrc/stores/statusHistory.js17+++++++++++++++++
Dtest/unit/specs/modules/lists.spec.js83-------------------------------------------------------------------------------
Mtest/unit/specs/modules/serverSideStorage.spec.js2+-
Atest/unit/specs/stores/lists.spec.js93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Myarn.lock15++++++++++++++-
101 files changed, 1781 insertions(+), 1812 deletions(-)

diff --git a/changelog.d/pinia.change b/changelog.d/pinia.change @@ -0,0 +1 @@ +Partially migrated from vuex to pinia diff --git a/package.json b/package.json @@ -38,6 +38,7 @@ "pako": "^2.1.0", "parse-link-header": "2.0.0", "phoenix": "1.7.19", + "pinia": "^2.0.33", "punycode.js": "2.3.1", "qrcode": "1.5.4", "querystring-es3": "0.2.1", diff --git a/src/App.js b/src/App.js @@ -17,6 +17,8 @@ import GlobalNoticeList from './components/global_notice_list/global_notice_list 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', @@ -60,7 +62,7 @@ export default { document.getElementById('modal').classList = ['-' + this.layoutType] }, mounted () { - if (this.$store.state.interface.themeApplied) { + if (useInterfaceStore().themeApplied) { this.removeSplash() } }, @@ -69,7 +71,7 @@ export default { }, computed: { themeApplied () { - return this.$store.state.interface.themeApplied + return useInterfaceStore().themeApplied }, layoutModalClass () { return '-' + this.layoutType @@ -106,7 +108,7 @@ export default { } } }, - shout () { return this.$store.state.shout.joined }, + shout () { return useShoutStore().joined }, suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, showInstanceSpecificPanel () { return this.$store.state.instance.showInstanceSpecificPanel && @@ -132,7 +134,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 @@ -148,8 +150,8 @@ export default { }, methods: { updateMobileState () { - this.$store.dispatch('setLayoutWidth', windowWidth()) - this.$store.dispatch('setLayoutHeight', windowHeight()) + useInterfaceStore().setLayoutWidth(windowWidth()) + useInterfaceStore().setLayoutHeight(windowHeight()) }, removeSplash () { document.querySelector('#status').textContent = this.$t('splash.fun_' + Math.ceil(Math.random() * 4)) diff --git a/src/App.vue b/src/App.vue @@ -1,6 +1,6 @@ <template> <div - v-show="$store.state.interface.themeApplied" + v-show="themeApplied" id="app-loaded" :style="bgStyle" > diff --git a/src/boot/after_store.js b/src/boot/after_store.js @@ -17,6 +17,10 @@ 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 { useI18nStore } from 'src/stores/i18n' +import { useInterfaceStore } from 'src/stores/interface' +import { useAnnouncementsStore } from 'src/stores/announcements' + let staticInitialResults = null const parsedInitialResults = () => { @@ -333,9 +337,16 @@ const checkOAuthToken = async ({ store }) => { return Promise.resolve() } -const afterStoreSetup = async ({ store, i18n }) => { - store.dispatch('setLayoutWidth', windowWidth()) - store.dispatch('setLayoutHeight', windowHeight()) +const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { + const app = createApp(App) + app.use(pinia) + + if (storageError) { + useInterfaceStore().pushGlobalNotice({ messageKey: 'errors.storage_unavailable', level: 'error' }) + } + + useInterfaceStore().setLayoutWidth(windowWidth()) + useInterfaceStore().setLayoutHeight(windowHeight()) FaviconService.initFaviconService() initServiceWorker(store) @@ -348,7 +359,7 @@ const afterStoreSetup = async ({ store, i18n }) => { await setConfig({ store }) try { - await store.dispatch('applyTheme').catch((e) => { console.error('Error setting theme', e) }) + await useInterfaceStore().applyTheme().catch((e) => { console.error('Error setting theme', e) }) } catch (e) { window.splashError(e) return Promise.reject(e) @@ -369,7 +380,7 @@ const afterStoreSetup = async ({ store, i18n }) => { // Start fetching things that don't need to block the UI store.dispatch('fetchMutes') - store.dispatch('startFetchingAnnouncements') + useAnnouncementsStore().startFetchingAnnouncements() getTOS({ store }) getStickers({ store }) @@ -384,7 +395,7 @@ const afterStoreSetup = async ({ store, i18n }) => { } }) - const app = createApp(App) + useI18nStore().setI18n(i18n) app.use(router) app.use(store) @@ -392,9 +403,9 @@ const afterStoreSetup = async ({ store, i18n }) => { // Little thing to get out of invalid theme state window.resetThemes = () => { - store.dispatch('resetThemeV3') - store.dispatch('resetThemeV3Palette') - store.dispatch('resetThemeV2') + useInterfaceStore().resetThemeV3() + useInterfaceStore().resetThemeV3Palette() + useInterfaceStore().resetThemeV2() } app.use(vClickOutside) diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js @@ -7,6 +7,7 @@ import { library } from '@fortawesome/fontawesome-svg-core' import { faEllipsisV } from '@fortawesome/free-solid-svg-icons' +import { useReportsStore } from 'src/stores/reports' library.add( faEllipsisV @@ -73,7 +74,7 @@ const AccountActions = { this.hideConfirmRemoveUserFromFollowers() }, reportUser () { - this.$store.dispatch('openUserReportingModal', { userId: this.user.id }) + useReportsStore().openUserReportingModal({ userId: this.user.id }) }, openChat () { this.$router.push({ diff --git a/src/components/announcement/announcement.js b/src/components/announcement/announcement.js @@ -2,6 +2,7 @@ import { mapState } from 'vuex' import AnnouncementEditor from '../announcement_editor/announcement_editor.vue' import RichContent from '../rich_content/rich_content.jsx' import localeService from '../../services/locale/locale.service.js' +import { useAnnouncementsStore } from 'src/stores/announcements' const Announcement = { components: { @@ -67,11 +68,11 @@ const Announcement = { methods: { markAsRead () { if (!this.isRead) { - return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id) + return useAnnouncementsStore().markAnnouncementAsRead(this.announcement.id) } }, deleteAnnouncement () { - return this.$store.dispatch('deleteAnnouncement', this.announcement.id) + return useAnnouncementsStore().deleteAnnouncement(this.announcement.id) }, formatTimeOrDate (time, locale) { const d = new Date(time) @@ -85,7 +86,7 @@ const Announcement = { this.editing = true }, submitEdit () { - this.$store.dispatch('editAnnouncement', { + useAnnouncementsStore().editAnnouncement({ id: this.announcement.id, ...this.editedAnnouncement }) diff --git a/src/components/announcements_page/announcements_page.js b/src/components/announcements_page/announcements_page.js @@ -1,6 +1,7 @@ import { mapState } from 'vuex' import Announcement from '../announcement/announcement.vue' import AnnouncementEditor from '../announcement_editor/announcement_editor.vue' +import { useAnnouncementsStore } from 'src/stores/announcements' const AnnouncementsPage = { components: { @@ -20,14 +21,14 @@ const AnnouncementsPage = { } }, mounted () { - this.$store.dispatch('fetchAnnouncements') + useAnnouncementsStore().fetchAnnouncements() }, computed: { ...mapState({ currentUser: state => state.users.currentUser }), announcements () { - return this.$store.state.announcements.announcements + return useAnnouncementsStore().announcements }, canPostAnnouncement () { return this.currentUser && this.currentUser.privileges.includes('announcements_manage_announcements') @@ -36,7 +37,7 @@ const AnnouncementsPage = { methods: { postAnnouncement () { this.posting = true - this.$store.dispatch('postAnnouncement', this.newAnnouncement) + useAnnouncementsStore().postAnnouncement(this.newAnnouncement) .then(() => { this.newAnnouncement.content = '' this.startsAt = undefined diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js @@ -18,6 +18,7 @@ import { faPencilAlt, faAlignRight } from '@fortawesome/free-solid-svg-icons' +import { useMediaViewerStore } from 'src/stores/media_viewer' library.add( faFile, @@ -147,14 +148,14 @@ const Attachment = { openModal (event) { if (this.useModal) { this.$emit('setMedia') - this.$store.dispatch('setCurrentMedia', this.attachment) + useMediaViewerStore().setCurrentMedia(this.attachment) } else if (this.type === 'unknown') { window.open(this.attachment.url) } }, openModalForce (event) { this.$emit('setMedia') - this.$store.dispatch('setCurrentMedia', this.attachment) + useMediaViewerStore().setCurrentMedia(this.attachment) }, onEdit (event) { this.edit && this.edit(this.attachment, event) diff --git a/src/components/bookmark_folder_edit/bookmark_folder_edit.js b/src/components/bookmark_folder_edit/bookmark_folder_edit.js @@ -63,7 +63,7 @@ const BookmarkFolderEdit = { this.$router.push({ name: 'bookmark-folders' }) }) .catch((e) => { - this.$store.dispatch('pushGlobalNotice', { + this.$store.useInterfaceStore().pushGlobalNotice({ messageKey: 'bookmark_folders.error', messageArgs: [e.message], level: 'error' diff --git 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 'src/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 @@ -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 'src/stores/interface' library.add( faTimes, @@ -65,6 +67,9 @@ const ChatMessage = { hasAttachment () { return this.message.attachments.length > 0 }, + ...mapPiniaState(useInterfaceStore, { + betterShadow: store => store.browserSupport.cssFilter + }), ...mapState({ currentUser: state => state.users.currentUser, restrictedNicknames: state => state.instance.restrictedNicknames diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js @@ -3,8 +3,10 @@ import Status from '../status/status.vue' import ThreadTree from '../thread_tree/thread_tree.vue' import { WSConnectionStatus } from '../../services/api/api.service.js' import { mapGetters, mapState } from 'vuex' +import { mapState as mapPiniaState } from 'pinia' import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue' import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue' +import { useInterfaceStore } from 'src/stores/interface' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -350,8 +352,10 @@ const conversation = { }, ...mapGetters(['mergedConfig']), ...mapState({ - mobileLayout: state => state.interface.layoutType === 'mobile', mastoUserSocketStatus: state => state.api.mastoUserSocketStatus + }), + ...mapPiniaState(useInterfaceStore, { + mobileLayout: store => store.layoutType === 'mobile' }) }, components: { diff --git 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 'src/stores/interface' library.add( faSignInAlt, @@ -107,10 +108,10 @@ export default { this.searchBarHidden = hidden }, openSettingsModal () { - this.$store.dispatch('openSettingsModal', 'user') + useInterfaceStore().openSettingsModal('user') }, openAdminModal () { - this.$store.dispatch('openSettingsModal', 'admin') + useInterfaceStore().openSettingsModal('admin') } } } diff --git a/src/components/edit_status_modal/edit_status_modal.js b/src/components/edit_status_modal/edit_status_modal.js @@ -1,6 +1,7 @@ import EditStatusForm from '../edit_status_form/edit_status_form.vue' import Modal from '../modal/modal.vue' import get from 'lodash/get' +import { useEditStatusStore } from 'src/stores/editStatus' const EditStatusModal = { components: { @@ -17,13 +18,13 @@ const EditStatusModal = { return !!this.$store.state.users.currentUser }, modalActivated () { - return this.$store.state.editStatus.modalActivated + return useEditStatusStore().modalActivated }, isFormVisible () { return this.isLoggedIn && !this.resettingForm && this.modalActivated }, params () { - return this.$store.state.editStatus.params || {} + return useEditStatusStore().params || {} } }, watch: { @@ -46,7 +47,7 @@ const EditStatusModal = { this.$refs.editStatusForm.requestClose() }, doCloseModal () { - this.$store.dispatch('closeEditStatusModal') + useEditStatusStore().closeEditStatusModal() } } } diff --git a/src/components/extra_notifications/extra_notifications.js b/src/components/extra_notifications/extra_notifications.js @@ -1,4 +1,6 @@ import { mapGetters } from 'vuex' +import { mapState as mapPiniaState } from 'pinia' +import { useAnnouncementsStore } from 'src/stores/announcements' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -7,6 +9,8 @@ import { faBullhorn } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from 'src/stores/interface' + library.add( faUserPlus, faComments, @@ -33,11 +37,14 @@ const ExtraNotifications = { currentUser () { return this.$store.state.users.currentUser }, - ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount', 'followRequestCount', 'mergedConfig']) + ...mapGetters(['unreadChatCount', 'followRequestCount', 'mergedConfig']), + ...mapPiniaState(useAnnouncementsStore, { + unreadAnnouncementCount: 'unreadAnnouncementCount' + }) }, methods: { openNotificationSettings () { - return this.$store.dispatch('openSettingsModalTab', 'notifications') + return useInterfaceStore().openSettingsModalTab('notifications') }, dismissConfigurationTip () { return this.$store.dispatch('setOption', { name: 'showExtraNotificationsTip', value: false }) diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js @@ -1,6 +1,7 @@ import Select from '../select/select.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' import Popover from 'src/components/popover/popover.vue' +import { useInterfaceStore } from 'src/stores/interface' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -25,7 +26,7 @@ export default { 'name', 'label', 'modelValue', 'fallback', 'options', 'no-inherit' ], mounted () { - this.$store.dispatch('queryLocalFonts') + useInterfaceStore().queryLocalFonts() }, emits: ['update:modelValue'], data () { @@ -50,10 +51,10 @@ export default { return typeof this.modelValue !== 'undefined' }, localFontsList () { - return this.$store.state.interface.localFonts + return useInterfaceStore().localFonts }, localFontsSize () { - return this.$store.state.interface.localFonts?.length + return useInterfaceStore().localFonts?.length } } } diff --git a/src/components/gallery/gallery.js b/src/components/gallery/gallery.js @@ -1,3 +1,4 @@ +import { useMediaViewerStore } from 'src/stores/media_viewer' import Attachment from '../attachment/attachment.vue' import { sumBy, set } from 'lodash' @@ -107,11 +108,11 @@ const Gallery = { this.hidingLong = event }, openGallery () { - this.$store.dispatch('setMedia', this.attachments) - this.$store.dispatch('setCurrentMedia', this.attachments[0]) + useMediaViewerStore().setMedia(this.attachments) + useMediaViewerStore().setCurrentMedia(this.attachments[0]) }, onMedia () { - this.$store.dispatch('setMedia', this.attachments) + useMediaViewerStore().setMedia(this.attachments) } } } diff --git 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 'src/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/lists.js b/src/components/lists/lists.js @@ -1,3 +1,4 @@ +import { useListsStore } from 'src/stores/lists' import ListsCard from '../lists_card/lists_card.vue' const Lists = { @@ -11,7 +12,7 @@ const Lists = { }, computed: { lists () { - return this.$store.state.lists.allLists + return useListsStore().allLists } }, methods: { diff --git a/src/components/lists_edit/lists_edit.js b/src/components/lists_edit/lists_edit.js @@ -1,4 +1,5 @@ import { mapState, mapGetters } from 'vuex' +import { mapState as mapPiniaState } from 'pinia' import BasicUserCard from '../basic_user_card/basic_user_card.vue' import ListsUserSearch from '../lists_user_search/lists_user_search.vue' import PanelLoading from 'src/components/panel_loading/panel_loading.vue' @@ -9,6 +10,8 @@ import { faSearch, faChevronLeft } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from 'src/stores/interface' +import { useListsStore } from 'src/stores/lists' library.add( faSearch, @@ -37,12 +40,12 @@ const ListsNew = { }, created () { if (!this.id) return - this.$store.dispatch('fetchList', { listId: this.id }) + useListsStore().fetchList({ listId: this.id }) .then(() => { this.title = this.findListTitle(this.id) this.titleDraft = this.title }) - this.$store.dispatch('fetchListAccounts', { listId: this.id }) + useListsStore().fetchListAccounts({ listId: this.id }) .then(() => { this.membersUserIds = this.findListAccounts(this.id) this.membersUserIds.forEach(userId => { @@ -64,7 +67,8 @@ const ListsNew = { ...mapState({ currentUser: state => state.users.currentUser }), - ...mapGetters(['findUser', 'findListTitle', 'findListAccounts']) + ...mapPiniaState(useListsStore, ['findListTitle', 'findListAccounts']), + ...mapGetters(['findUser']) }, methods: { onInput () { @@ -95,10 +99,10 @@ const ListsNew = { return this.addedUserIds.has(user.id) }, addUser (user) { - this.$store.dispatch('addListAccount', { accountId: user.id, listId: this.id }) + useListsStore().addListAccount({ accountId: user.id, listId: this.id }) }, removeUser (userId) { - this.$store.dispatch('removeListAccount', { accountId: userId, listId: this.id }) + useListsStore().removeListAccount({ accountId: userId, listId: this.id }) }, onSearchLoading (results) { this.searchLoading = true @@ -111,24 +115,23 @@ const ListsNew = { this.searchUserIds = results }, updateListTitle () { - this.$store.dispatch('setList', { listId: this.id, title: this.titleDraft }) + useListsStore().setList({ listId: this.id, title: this.titleDraft }) .then(() => { this.title = this.findListTitle(this.id) }) }, createList () { - this.$store.dispatch('createList', { title: this.titleDraft }) + useListsStore().createList({ title: this.titleDraft }) .then((list) => { - return this - .$store - .dispatch('setListAccounts', { listId: list.id, accountIds: [...this.addedUserIds] }) + return useListsStore() + .setListAccounts({ listId: list.id, accountIds: [...this.addedUserIds] }) .then(() => list.id) }) .then((listId) => { 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' @@ -136,7 +139,7 @@ const ListsNew = { }) }, deleteList () { - this.$store.dispatch('deleteList', { listId: this.id }) + useListsStore().deleteList({ listId: this.id }) this.$router.push({ name: 'lists' }) } } diff --git a/src/components/lists_menu/lists_menu_content.js b/src/components/lists_menu/lists_menu_content.js @@ -1,6 +1,8 @@ import { mapState } from 'vuex' +import { mapState as mapPiniaState } from 'pinia' import NavigationEntry from 'src/components/navigation/navigation_entry.vue' import { getListEntries } from 'src/components/navigation/filter.js' +import { useListsStore } from 'src/stores/lists' export const ListsMenuContent = { props: [ @@ -10,8 +12,10 @@ export const ListsMenuContent = { NavigationEntry }, computed: { + ...mapPiniaState(useListsStore, { + lists: getListEntries + }), ...mapState({ - lists: getListEntries, currentUser: state => state.users.currentUser, privateMode: state => state.instance.private, federating: state => state.instance.federating diff --git a/src/components/lists_timeline/lists_timeline.js b/src/components/lists_timeline/lists_timeline.js @@ -1,3 +1,4 @@ +import { useListsStore } from 'src/stores/lists' import Timeline from '../timeline/timeline.vue' const ListsTimeline = { data () { @@ -17,14 +18,14 @@ const ListsTimeline = { this.listId = route.params.id this.$store.dispatch('stopFetchingTimeline', 'list') this.$store.commit('clearTimeline', { timeline: 'list' }) - this.$store.dispatch('fetchList', { listId: this.listId }) + useListsStore().fetchList({ listId: this.listId }) this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId }) } } }, created () { this.listId = this.$route.params.id - this.$store.dispatch('fetchList', { listId: this.listId }) + useListsStore().fetchList({ listId: this.listId }) this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId }) }, unmounted () { diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js @@ -13,6 +13,7 @@ import { faCircleNotch, faTimes } from '@fortawesome/free-solid-svg-icons' +import { useMediaViewerStore } from 'src/stores/media_viewer' library.add( faChevronLeft, @@ -44,16 +45,16 @@ const MediaModal = { }, computed: { showing () { - return this.$store.state.mediaViewer.activated + return useMediaViewerStore().activated }, media () { - return this.$store.state.mediaViewer.media + return useMediaViewerStore().media }, description () { return this.currentMedia.description }, currentIndex () { - return this.$store.state.mediaViewer.currentIndex + return useMediaViewerStore().currentIndex }, currentMedia () { return this.media[this.currentIndex] @@ -79,7 +80,7 @@ const MediaModal = { // to be processed on the content below the overlay const transitionTime = 100 // ms setTimeout(() => { - this.$store.dispatch('closeMediaViewer') + useMediaViewerStore().closeMediaViewer() }, transitionTime) }, hideIfNotSwiped (event) { @@ -98,7 +99,7 @@ const MediaModal = { if (this.getType(newMedia) === 'image') { this.loading = true } - this.$store.dispatch('setCurrentMedia', newMedia) + useMediaViewerStore().setCurrentMedia(newMedia) } }, goNext () { @@ -108,7 +109,7 @@ const MediaModal = { if (this.getType(newMedia) === 'image') { this.loading = true } - this.$store.dispatch('setCurrentMedia', newMedia) + useMediaViewerStore().setCurrentMedia(newMedia) } }, onImageLoaded () { diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js @@ -8,6 +8,7 @@ import { import GestureService from '../../services/gesture_service/gesture_service' import NavigationPins from 'src/components/navigation/navigation_pins.vue' import { mapGetters } from 'vuex' +import { mapState } from 'pinia' import { library } from '@fortawesome/fontawesome-svg-core' import { faTimes, @@ -17,6 +18,7 @@ import { faMinus, faCheckDouble } from '@fortawesome/free-solid-svg-icons' +import { useAnnouncementsStore } from 'src/stores/announcements' library.add( faTimes, @@ -68,7 +70,8 @@ const MobileNav = { isChat () { return this.$route.name === 'chat' }, - ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount']), + ...mapState(useAnnouncementsStore, ['unreadAnnouncementCount']), + ...mapGetters(['unreadChatCount']), chatsPinned () { return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats') }, diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js @@ -3,6 +3,7 @@ import { library } from '@fortawesome/fontawesome-svg-core' import { faPen } from '@fortawesome/free-solid-svg-icons' +import { usePostStatusStore } from 'src/stores/postStatus' library.add( faPen @@ -71,7 +72,7 @@ const MobilePostStatusButton = { window.removeEventListener('scroll', this.handleScrollEnd) }, openPostForm () { - this.$store.dispatch('openPostStatusModal') + usePostStatusStore().openPostStatusModal() }, handleOSK () { // This is a big hack: we're guessing from changed window sizes if the diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js @@ -1,11 +1,13 @@ import BookmarkFoldersMenuContent from 'src/components/bookmark_folders_menu/bookmark_folders_menu_content.vue' import ListsMenuContent from 'src/components/lists_menu/lists_menu_content.vue' import { mapState, mapGetters } from 'vuex' +import { mapState as mapPiniaState } from 'pinia' import { TIMELINES, ROOT_ITEMS } from 'src/components/navigation/navigation.js' import { filterNavigation } from 'src/components/navigation/filter.js' import NavigationEntry from 'src/components/navigation/navigation_entry.vue' import NavigationPins from 'src/components/navigation/navigation_pins.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' +import { useAnnouncementsStore } from 'src/stores/announcements' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -90,13 +92,16 @@ const NavPanel = { } }, computed: { + ...mapPiniaState(useAnnouncementsStore, { + unreadAnnouncementCount: 'unreadAnnouncementCount', + supportsAnnouncements: store => store.supportsAnnouncements + }), ...mapState({ currentUser: state => state.users.currentUser, followRequestCount: state => state.api.followRequests.length, privateMode: state => state.instance.private, federating: state => state.instance.federating, pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable, - supportsAnnouncements: state => state.announcements.supportsAnnouncements, pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems), collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav, bookmarkFolders: state => state.instance.pleromaBookmarkFoldersAvailable @@ -131,7 +136,7 @@ const NavPanel = { } ) }, - ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount']) + ...mapGetters(['unreadChatCount']) } } diff --git a/src/components/navigation/filter.js b/src/components/navigation/filter.js @@ -12,7 +12,7 @@ export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFede }) } -export const getListEntries = state => state.lists.allLists.map(list => ({ +export const getListEntries = store => store.allLists.map(list => ({ name: 'list-' + list.id, routeObject: { name: 'lists-timeline', params: { id: list.id } }, labelRaw: list.title, diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js @@ -84,6 +84,7 @@ export const ROOT_ITEMS = { route: 'announcements', icon: 'bullhorn', label: 'nav.announcements', + store: 'announcements', badgeStyle: 'notification', badgeGetter: 'unreadAnnouncementCount', criteria: ['announcements'] diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js @@ -3,6 +3,8 @@ import { routeTo } from 'src/components/navigation/navigation.js' import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faThumbtack } from '@fortawesome/free-solid-svg-icons' +import { mapStores } from 'pinia' +import { useAnnouncementsStore } from 'src/stores/announcements' library.add(faThumbtack) @@ -31,6 +33,7 @@ const NavigationEntry = { getters () { return this.$store.getters }, + ...mapStores(useAnnouncementsStore), ...mapState({ currentUser: state => state.users.currentUser, pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems) diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue @@ -53,6 +53,12 @@ > {{ getters[item.badgeGetter] }} </div> + <div + v-else-if="item.badgeGetter && item.store && this[`${item.store}Store`][item.badgeGetter]" + class="badge badge-notification" + > + {{ this[`${item.store}Store`][item.badgeGetter] }} + </div> <button v-if="showPin && currentUser" type="button" diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js @@ -1,4 +1,5 @@ import { mapState } from 'vuex' +import { mapState as mapPiniaState } from 'pinia' import { TIMELINES, ROOT_ITEMS, routeTo } from 'src/components/navigation/navigation.js' import { getBookmarkFolderEntries, getListEntries, filterNavigation } from 'src/components/navigation/filter.js' @@ -16,6 +17,8 @@ import { faStream, faList } from '@fortawesome/free-solid-svg-icons' +import { useListsStore } from 'src/stores/lists' +import { useAnnouncementsStore } from 'src/stores/announcements' library.add( faUsers, @@ -43,15 +46,19 @@ const NavPanel = { getters () { return this.$store.getters }, + ...mapPiniaState(useListsStore, { + lists: getListEntries + }), + ...mapPiniaState(useAnnouncementsStore, { + supportsAnnouncements: store => store.supportsAnnouncements + }), ...mapState({ - lists: getListEntries, bookmarks: getBookmarkFolderEntries, currentUser: state => state.users.currentUser, followRequestCount: state => state.api.followRequests.length, privateMode: state => state.instance.private, federating: state => state.instance.federating, pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable, - supportsAnnouncements: state => state.announcements.supportsAnnouncements, pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems) }), pinnedList () { diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js @@ -1,5 +1,6 @@ import { computed } from 'vue' import { mapGetters } from 'vuex' +import { mapState } from 'pinia' import Notification from '../notification/notification.vue' import ExtraNotifications from '../extra_notifications/extra_notifications.vue' import NotificationFilters from './notification_filters.vue' @@ -14,6 +15,8 @@ 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 'src/stores/interface' +import { useAnnouncementsStore } from 'src/stores/announcements' library.add( faCircleNotch, @@ -98,11 +101,11 @@ const Notifications = { return this.$store.state.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' @@ -110,7 +113,7 @@ const Notifications = { return map[layoutType] || '#notifs-sidebar' }, popoversZLayer () { - const { layoutType } = this.$store.state.interface + const { layoutType } = useInterfaceStore() return layoutType === 'mobile' ? 'navbar' : null }, notificationsToDisplay () { @@ -121,7 +124,8 @@ const Notifications = { showExtraNotifications () { return !this.noExtra }, - ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount']) + ...mapState(useAnnouncementsStore, ['unreadAnnouncementCount']), + ...mapGetters(['unreadChatCount']) }, mounted () { this.scrollerRef = this.$refs.root.closest('.column.-scrollable') @@ -141,10 +145,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/poll/poll.js b/src/components/poll/poll.js @@ -2,6 +2,7 @@ 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 { forEach, map } from 'lodash' +import { usePollsStore } from 'src/stores/polls' export default { name: 'Poll', @@ -18,20 +19,20 @@ export default { } }, created () { - if (!this.$store.state.polls.pollsObject[this.pollId]) { - this.$store.dispatch('mergeOrAddPoll', this.basePoll) + if (!usePollsStore().pollsObject[this.pollId]) { + usePollsStore().mergeOrAddPoll(this.basePoll) } - this.$store.dispatch('trackPoll', this.pollId) + usePollsStore().trackPoll(this.pollId) }, unmounted () { - this.$store.dispatch('untrackPoll', this.pollId) + usePollsStore().untrackPoll(this.pollId) }, computed: { pollId () { return this.basePoll.id }, poll () { - const storePoll = this.$store.state.polls.pollsObject[this.pollId] + const storePoll = usePollsStore().pollsObject[this.pollId] return storePoll || {} }, options () { @@ -77,9 +78,6 @@ export default { resultTitle (option) { return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}` }, - fetchPoll () { - this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id }) - }, activateOption (index) { // forgive me father: doing checking the radio/checkboxes // in code because of customized input elements need either @@ -107,8 +105,7 @@ export default { vote () { if (this.choiceIndices.length === 0) return this.loading = true - this.$store.dispatch( - 'votePoll', + usePollsStore().votePoll( { id: this.statusId, pollId: this.poll.id, choices: this.choiceIndices } ).then(poll => { this.loading = false diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js @@ -14,7 +14,8 @@ import { propsToNative } from '../../services/attributes_helper/attributes_helpe import { pollFormToMasto } from 'src/services/poll/poll.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, mapActions } from 'pinia' import Checkbox from '../checkbox/checkbox.vue' import Select from '../select/select.vue' import DraftCloser from 'src/components/draft_closer/draft_closer.vue' @@ -32,6 +33,9 @@ import { faChevronRight } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from 'src/stores/interface.js' +import { useMediaViewerStore } from 'src/stores/media_viewer.js' + library.add( faSmileBeam, faPollH, @@ -367,8 +371,8 @@ const PostStatusForm = { ) && this.saveable }, ...mapGetters(['mergedConfig']), - ...mapState({ - mobileLayout: state => state.interface.mobileLayout + ...mapState(useInterfaceStore, { + mobileLayout: store => store.mobileLayout }) }, watch: { @@ -393,6 +397,7 @@ const PostStatusForm = { this.removeBeforeUnloadListener() }, methods: { + ...mapActions(useMediaViewerStore, ['increment']), statusChanged () { this.autoPreview() this.updateIdempotencyKey() @@ -753,7 +758,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/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue @@ -386,7 +386,7 @@ :nsfw="false" :attachments="newStatus.files" :descriptions="newStatus.mediaDescriptions" - :set-media="() => $store.dispatch('setMedia', newStatus.files)" + :set-media="() => setMedia()" :editable="true" :edit-attachment="editAttachment" :remove-attachment="removeMediaFile" diff --git a/src/components/post_status_modal/post_status_modal.js b/src/components/post_status_modal/post_status_modal.js @@ -1,6 +1,7 @@ import PostStatusForm from '../post_status_form/post_status_form.vue' import Modal from '../modal/modal.vue' import get from 'lodash/get' +import { usePostStatusStore } from 'src/stores/postStatus' const PostStatusModal = { components: { @@ -17,13 +18,13 @@ const PostStatusModal = { return !!this.$store.state.users.currentUser }, modalActivated () { - return this.$store.state.postStatus.modalActivated + return usePostStatusStore().modalActivated }, isFormVisible () { return this.isLoggedIn && !this.resettingForm && this.modalActivated }, params () { - return this.$store.state.postStatus.params || {} + return usePostStatusStore().params || {} } }, watch: { @@ -43,11 +44,11 @@ const PostStatusModal = { }, methods: { closeModal () { - this.$store.dispatch('closePostStatusModal') + usePostStatusStore().closePostStatusModal() }, resetAndClose () { - this.$store.dispatch('resetPostStatusModal') - this.$store.dispatch('closePostStatusModal') + usePostStatusStore().resetPostStatusModal() + usePostStatusStore().closePostStatusModal() } } } diff --git a/src/components/quick_filter_settings/quick_filter_settings.js b/src/components/quick_filter_settings/quick_filter_settings.js @@ -1,7 +1,9 @@ import Popover from '../popover/popover.vue' -import { mapGetters, mapState } from 'vuex' +import { mapGetters } from 'vuex' +import { mapState } from 'pinia' import { library } from '@fortawesome/fontawesome-svg-core' import { faFilter, faFont, faWrench } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from 'src/stores/interface' library.add( faFilter, @@ -23,13 +25,13 @@ const QuickFilterSettings = { this.$store.dispatch('queueFlushAll') }, openTab (tab) { - this.$store.dispatch('openSettingsModalTab', tab) + useInterfaceStore().openSettingsModalTab(tab) } }, computed: { ...mapGetters(['mergedConfig']), - ...mapState({ - mobileLayout: state => state.interface.layoutType === 'mobile' + ...mapState(useInterfaceStore, { + mobileLayout: state => state.layoutType === 'mobile' }), triggerAttrs () { if (this.mobileLayout) { diff --git a/src/components/quick_view_settings/quick_view_settings.js b/src/components/quick_view_settings/quick_view_settings.js @@ -1,8 +1,10 @@ import Popover from 'src/components/popover/popover.vue' import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filter_settings.vue' -import { mapGetters, mapState } from 'vuex' +import { mapGetters } from 'vuex' +import { mapState } from 'pinia' import { library } from '@fortawesome/fontawesome-svg-core' import { faList, faFolderTree, faBars, faWrench } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from 'src/stores/interface' library.add( faList, @@ -24,13 +26,13 @@ const QuickViewSettings = { this.$store.dispatch('setOption', { name: 'conversationDisplay', value: visibility }) }, openTab (tab) { - this.$store.dispatch('openSettingsModalTab', tab) + useInterfaceStore().openSettingsModalTab(tab) } }, computed: { ...mapGetters(['mergedConfig']), - ...mapState({ - mobileLayout: state => state.interface.layoutType === 'mobile' + ...mapState(useInterfaceStore, { + mobileLayout: state => state.layoutType === 'mobile' }), loggedIn () { return !!this.$store.state.users.currentUser diff --git a/src/components/report/report.js b/src/components/report/report.js @@ -1,3 +1,4 @@ +import { useReportsStore } from 'src/stores/reports' import Select from '../select/select.vue' import StatusContent from '../status_content/status_content.vue' import Timeago from '../timeago/timeago.vue' @@ -16,7 +17,7 @@ const Report = { }, computed: { report () { - return this.$store.state.reports.reports[this.reportId] || {} + return useReportsStore().reports[this.reportId] || {} }, state: { get: function () { return this.report.state }, @@ -28,7 +29,7 @@ const Report = { return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) }, setReportState (state) { - return this.$store.dispatch('setReportState', { id: this.report.id, state }) + return useReportsStore().setReportState({ id: this.report.id, state }) } } } diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.js b/src/components/settings_modal/admin_tabs/emoji_tab.js @@ -232,7 +232,7 @@ const EmojiTab = { }) }, displayError (msg) { - this.$store.dispatch('pushGlobalNotice', { + this.$store.useInterfaceStore().pushGlobalNotice({ messageKey: 'admin_dash.emoji.error', messageArgs: [msg], level: 'error' diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.js b/src/components/settings_modal/admin_tabs/frontends_tab.js @@ -80,7 +80,7 @@ const FrontendsTab = { this.$store.dispatch('loadFrontendsStuff') if (response.error) { const reason = await response.error.json() - this.$store.dispatch('pushGlobalNotice', { + this.$store.useInterfaceStore().pushGlobalNotice({ level: 'error', messageKey: 'admin_dash.frontend.failure_installing_frontend', messageArgs: { @@ -90,7 +90,7 @@ const FrontendsTab = { timeout: 5000 }) } else { - this.$store.dispatch('pushGlobalNotice', { + this.$store.useInterfaceStore().pushGlobalNotice({ level: 'success', messageKey: 'admin_dash.frontend.success_installing_frontend', messageArgs: { diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js @@ -7,6 +7,7 @@ import Checkbox from 'src/components/checkbox/checkbox.vue' import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { cloneDeep, isEqual } from 'lodash' +import { mapState as mapPiniaState } from 'pinia' import { newImporter, newExporter @@ -20,6 +21,7 @@ import { import { faWindowMinimize } from '@fortawesome/free-regular-svg-icons' +import { useInterfaceStore } from 'src/stores/interface' const PLEROMAFE_SETTINGS_MAJOR_VERSION = 1 const PLEROMAFE_SETTINGS_MINOR_VERSION = 0 @@ -74,10 +76,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)) { @@ -109,7 +111,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' }) @@ -119,9 +121,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) { @@ -166,24 +168,15 @@ const SettingsModal = { } }, computed: { - currentSaveStateNotice () { - return this.$store.state.interface.settings.currentSaveStateNotice - }, - modalActivated () { - return this.$store.state.interface.settingsModalState !== 'hidden' - }, - modalMode () { - return this.$store.state.interface.settingsModalMode - }, - modalOpenedOnceUser () { - return this.$store.state.interface.settingsModalLoadedUser - }, - modalOpenedOnceAdmin () { - return this.$store.state.interface.settingsModalLoadedAdmin - }, - modalPeeked () { - return this.$store.state.interface.settingsModalState === 'minimized' - }, + ...mapPiniaState(useInterfaceStore, { + temporaryChangesTimeoutId: store => store.layoutType === 'mobile', + currentSaveStateNotice: store => store.settings.currentSaveStateNotice, + modalActivated: store => store.settingsModalState !== 'hidden', + modalMode: store => store.settingsModalMode, + modalOpenedOnceUser: store => store.settingsModalLoadedUser, + modalOpenedOnceAdmin: store => store.settingsModalLoadedAdmin, + modalPeeked: store => store.settingsModalState === 'minimized' + }), expertLevel: { get () { return this.$store.state.config.expertLevel > 0 diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue @@ -158,7 +158,7 @@ </div> <teleport to="#modal"> <ConfirmModal - v-if="$store.state.interface.temporaryChangesTimeoutId" + v-if="temporaryChangesTimeoutId" :title="$t('settings.confirm_new_setting')" :cancel-text="$t('settings.revert')" :confirm-text="$t('settings.confirm')" diff --git a/src/components/settings_modal/settings_modal_admin_content.js b/src/components/settings_modal/settings_modal_admin_content.js @@ -4,6 +4,7 @@ import InstanceTab from './admin_tabs/instance_tab.vue' import LimitsTab from './admin_tabs/limits_tab.vue' import FrontendsTab from './admin_tabs/frontends_tab.vue' import EmojiTab from './admin_tabs/emoji_tab.vue' +import { useInterfaceStore } from 'src/stores/interface' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -45,10 +46,10 @@ const SettingsModalAdminContent = { 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' }, adminDbLoaded () { return this.$store.state.adminSettings.loaded @@ -67,7 +68,7 @@ const SettingsModalAdminContent = { }, 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 => { @@ -79,7 +80,7 @@ const SettingsModalAdminContent = { } // 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/settings_modal_user_content.js b/src/components/settings_modal/settings_modal_user_content.js @@ -25,6 +25,7 @@ import { faInfo, faWindowRestore } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from 'src/stores/interface' library.add( faWrench, @@ -60,21 +61,21 @@ 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' }, expertLevel () { return this.$store.state.config.expertLevel }, isMobileLayout () { - return this.$store.state.interface.layoutType === 'mobile' + return useInterfaceStore().layoutType === 'mobile' } }, 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 => { @@ -86,7 +87,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/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js @@ -4,11 +4,9 @@ import IntegerSetting from '../helpers/integer_setting.vue' import FloatSetting from '../helpers/float_setting.vue' import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue' import PaletteEditor from 'src/components/palette_editor/palette_editor.vue' - +import Preview from './theme_tab/theme_preview.vue' import FontControl from 'src/components/font_control/font_control.vue' -import { normalizeThemeData } from 'src/modules/interface' - import { newImporter } from 'src/services/export_import/export_import.js' import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' import { init } from 'src/services/theme_data/theme_data_3.service.js' @@ -20,17 +18,15 @@ import { deserialize } from 'src/services/theme_data/iss_deserializer.js' import SharedComputedObject from '../helpers/shared_computed_object.js' import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue' + +import { mapActions } from 'pinia' +import { useInterfaceStore, normalizeThemeData } from 'src/stores/interface' + import { library } from '@fortawesome/fontawesome-svg-core' import { faGlobe } from '@fortawesome/free-solid-svg-icons' -import Preview from './theme_tab/theme_preview.vue' - -// helper for debugging -// eslint-disable-next-line no-unused-vars -const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x)) - library.add( faGlobe ) @@ -90,7 +86,7 @@ const AppearanceTab = { PaletteEditor }, mounted () { - this.$store.dispatch('getThemeData') + useInterfaceStore().getThemeData() const updateIndex = (resource) => { const capitalizedResource = resource[0].toUpperCase() + resource.slice(1) @@ -100,7 +96,7 @@ const AppearanceTab = { if (currentIndex) { promise = Promise.resolve(currentIndex) } else { - promise = this.$store.dispatch(`fetch${capitalizedResource}sIndex`) + promise = useInterfaceStore()[`fetch${capitalizedResource}sIndex`]() } return promise.then(index => { @@ -131,7 +127,7 @@ const AppearanceTab = { })) }) - this.userPalette = this.$store.state.interface.paletteDataUsed || {} + this.userPalette = useInterfaceStore().paletteDataUsed || {} updateIndex('palette').then(bundledPalettes => { bundledPalettes.forEach(([key, palettePromise]) => palettePromise.then(v => { @@ -187,10 +183,10 @@ const AppearanceTab = { }, computed: { switchInProgress () { - return this.$store.state.interface.themeChangeInProgress + return useInterfaceStore().themeChangeInProgress }, paletteDataUsed () { - return this.$store.state.interface.paletteDataUsed + return useInterfaceStore().paletteDataUsed }, availableStyles () { return [ @@ -205,7 +201,7 @@ const AppearanceTab = { ] }, stylePalettes () { - const ruleset = this.$store.state.interface.styleDataUsed || [] + const ruleset = useInterfaceStore().styleDataUsed || [] if (!ruleset && ruleset.length === 0) return const meta = ruleset.find(x => x.component === '@meta') const result = ruleset.filter(x => x.component.startsWith('@palette')) @@ -273,7 +269,7 @@ const AppearanceTab = { } }, customThemeVersion () { - const { themeVersion } = this.$store.state.interface + const { themeVersion } = useInterfaceStore() return themeVersion }, isCustomThemeUsed () { @@ -311,14 +307,14 @@ const AppearanceTab = { }, onImport (parsed, filename) { if (filename.endsWith('.json')) { - this.$store.dispatch('setThemeCustom', parsed.source || parsed.theme) + useInterfaceStore().setThemeCustom(parsed.source || parsed.theme) } else if (filename.endsWith('.iss')) { - this.$store.dispatch('setStyleCustom', parsed) + useInterfaceStore().setStyleCustom(parsed) } }, onImportFailure (result) { console.error('Failure importing theme:', result) - this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' }) + this.$store.useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_theme_imported', level: 'error' }) }, importValidator (parsed, filename) { if (filename.endsWith('.json')) { @@ -340,22 +336,20 @@ const AppearanceTab = { isPaletteActive (key) { return key === (this.mergedConfig.palette || this.$store.state.instance.palette) }, - setStyle (name) { - this.$store.dispatch('setStyle', name) - }, - setTheme (name) { - this.$store.dispatch('setTheme', name) - }, + ...mapActions(useInterfaceStore, [ + 'setStyle', + 'setTheme' + ]), setPalette (name, data) { - this.$store.dispatch('setPalette', name) + useInterfaceStore().setPalette(name) this.userPalette = data }, setPaletteCustom (data) { - this.$store.dispatch('setPaletteCustom', data) + useInterfaceStore().setPaletteCustom(data) this.userPalette = data }, resetTheming (name) { - this.$store.dispatch('setStyle', 'stock') + useInterfaceStore().setStyle('stock') }, previewTheme (key, version, input) { let theme3 diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js @@ -21,6 +21,7 @@ import { faPlus, faCircleNotch } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from 'src/stores/interface' library.add( faTimes, @@ -175,7 +176,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', { @@ -266,7 +267,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/style_tab/style_tab.js b/src/components/settings_modal/tabs/style_tab/style_tab.js @@ -1,5 +1,5 @@ import { ref, reactive, computed, watch, watchEffect, provide, getCurrentInstance } from 'vue' -import { useStore } from 'vuex' +import { useInterfaceStore } from 'src/stores/interface' import { get, set, unset, throttle } from 'lodash' import Select from 'src/components/select/select.vue' @@ -80,13 +80,13 @@ export default { }, setup (props, context) { const exports = {} - const store = useStore() + const interfaceStore = useInterfaceStore() // All rules that are made by editor - const allEditedRules = ref(store.state.interface.styleDataUsed || {}) - const styleDataUsed = computed(() => store.state.interface.styleDataUsed) + const allEditedRules = ref(interfaceStore.styleDataUsed || {}) + const styleDataUsed = computed(() => interfaceStore.styleDataUsed) watch([styleDataUsed], (value) => { - onImport(store.state.interface.styleDataUsed) + onImport(interfaceStore.styleDataUsed) }, { once: true }) exports.isActive = computed(() => { @@ -640,7 +640,7 @@ export default { parser (string) { return deserialize(string) }, onImportFailure (result) { console.error('Failure importing style:', result) - this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' }) + this.$store.useInterfaceStore().pushGlobalNotice({ messageKey: 'settings.invalid_theme_imported', level: 'error' }) }, onImport }) @@ -664,7 +664,7 @@ export default { }) exports.clearStyle = () => { - onImport(store.state.interface.styleDataUsed) + onImport(interfaceStore().styleDataUsed) } exports.exportStyle = () => { @@ -676,7 +676,7 @@ export default { } exports.applyStyle = () => { - store.dispatch('setStyleCustom', exportRules.value) + useInterfaceStore().setStyleCustom(exportRules.value) } const overallPreviewRules = ref([]) diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -43,6 +43,7 @@ import Checkbox from 'src/components/checkbox/checkbox.vue' import Select from 'src/components/select/select.vue' import Preview from './theme_preview.vue' +import { useInterfaceStore } from 'src/stores/interface' // List of color values used in v1 const v1OnlyNames = [ @@ -126,7 +127,7 @@ export default { if (currentIndex) { promise = Promise.resolve(currentIndex) } else { - promise = this.$store.dispatch('fetchThemesIndex') + promise = useInterfaceStore().fetchThemesIndex() } promise.then(themesIndex => { @@ -296,7 +297,7 @@ export default { } }, themeDataUsed () { - return this.$store.state.interface.themeDataUsed + return useInterfaceStore().themeDataUsed }, shadowsAvailable () { return Object.keys(DEFAULT_SHADOWS).sort() @@ -492,7 +493,7 @@ export default { } }, setCustomTheme () { - this.$store.dispatch('setThemeV2', { + useInterfaceStore().setThemeV2({ customTheme: { ignore: true, themeFileVersion: this.selectedVersion, @@ -536,7 +537,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/shout_panel/shout_panel.js b/src/components/shout_panel/shout_panel.js @@ -4,6 +4,7 @@ import { faBullhorn, faTimes } from '@fortawesome/free-solid-svg-icons' +import { useShoutStore } from 'src/stores/shout' library.add( faBullhorn, @@ -21,12 +22,12 @@ const shoutPanel = { }, computed: { messages () { - return this.$store.state.shout.messages + return useShoutStore().messages } }, methods: { submit (message) { - this.$store.state.shout.channel.push('new_msg', { text: message }, 10000) + useShoutStore().channel.push('new_msg', { text: message }, 10000) this.currentMessage = '' }, togglePanel () { diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js @@ -1,4 +1,5 @@ import { mapState, mapGetters } from 'vuex' +import { mapState as mapPiniaState } from 'pinia' import UserCard from '../user_card/user_card.vue' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' import GestureService from '../../services/gesture_service/gesture_service' @@ -20,6 +21,9 @@ import { faList, faFilePen } from '@fortawesome/free-solid-svg-icons' +import { useShoutStore } from 'src/stores/shout' +import { useInterfaceStore } from 'src/stores/interface' +import { useAnnouncementsStore } from 'src/stores/announcements' library.add( faSignInAlt, @@ -56,7 +60,7 @@ const SideDrawer = { currentUser () { return this.$store.state.users.currentUser }, - shout () { return this.$store.state.shout.joined }, + shout () { return useShoutStore().joined }, unseenNotifications () { return unseenNotificationsFromStore(this.$store) }, @@ -86,8 +90,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)) { @@ -96,11 +100,14 @@ const SideDrawer = { return { name } } }, + ...mapPiniaState(useAnnouncementsStore, { + supportsAnnouncements: store => store.supportsAnnouncements, + unreadAnnouncementCount: 'unreadAnnouncementCount' + }), ...mapState({ - pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable, - supportsAnnouncements: state => state.announcements.supportsAnnouncements + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable }), - ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount', 'draftCount']) + ...mapGetters(['unreadChatCount', 'draftCount']) }, methods: { toggleDrawer () { @@ -117,10 +124,10 @@ const SideDrawer = { GestureService.updateSwipe(e, this.closeGesture) }, openSettingsModal () { - this.$store.dispatch('openSettingsModal', 'user') + useInterfaceStore().openSettingsModal('user') }, openAdminModal () { - this.$store.dispatch('openSettingsModal', 'admin') + useInterfaceStore().openSettingsModal('admin') } } } diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js @@ -13,6 +13,7 @@ import { faLink, faPollH } from '@fortawesome/free-solid-svg-icons' +import { useMediaViewerStore } from 'src/stores/media_viewer' library.add( faCircleNotch, @@ -130,7 +131,7 @@ const StatusContent = { }, setMedia () { const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments - return () => this.$store.dispatch('setMedia', attachments) + return () => useMediaViewerStore().setMedia(attachments) } } } diff --git a/src/components/status_history_modal/status_history_modal.js b/src/components/status_history_modal/status_history_modal.js @@ -1,6 +1,7 @@ import { get } from 'lodash' import Modal from '../modal/modal.vue' import Status from '../status/status.vue' +import { useStatusHistoryStore } from 'src/stores/statusHistory' const StatusHistoryModal = { components: { @@ -14,10 +15,10 @@ const StatusHistoryModal = { }, computed: { modalActivated () { - return this.$store.state.statusHistory.modalActivated + return useStatusHistoryStore().modalActivated }, params () { - return this.$store.state.statusHistory.params + return useStatusHistoryStore().params }, statusId () { return this.params.id @@ -52,7 +53,7 @@ const StatusHistoryModal = { }) }, closeModal () { - this.$store.dispatch('closeStatusHistoryModal') + useStatusHistoryStore().closeStatusHistoryModal() } } } diff --git 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 'src/stores/interface' const findFirstUsable = (slots) => slots.findIndex(_ => _.props) diff --git 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 'src/stores/interface' library.add( faCircleNotch, @@ -103,8 +104,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 @@ -9,6 +9,8 @@ import { filterNavigation } from 'src/components/navigation/filter.js' import { faChevronDown } from '@fortawesome/free-solid-svg-icons' +import { useInterfaceStore } from 'src/stores/interface' +import { useListsStore } from 'src/stores/lists' library.add(faChevronDown) @@ -39,7 +41,7 @@ const TimelineMenu = { }, created () { if (timelineNames(this.bookmarkFolders)[this.$route.name]) { - this.$store.dispatch('setLastTimeline', this.$route.name) + useInterfaceStore().setLastTimeline(this.$route.name) } }, computed: { @@ -95,7 +97,7 @@ const TimelineMenu = { return '#' + this.$route.params.tag } if (route === 'lists-timeline') { - return this.$store.getters.findListTitle(this.$route.params.id) + return useListsStore().findListTitle(this.$route.params.id) } if (route === 'bookmark-folder') { return this.$store.getters.findBookmarkFolderName(this.$route.params.id) diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js @@ -1,4 +1,5 @@ import StillImage from '../still-image/still-image.vue' +import { useInterfaceStore } from 'src/stores/interface' import { library } from '@fortawesome/fontawesome-svg-core' @@ -22,7 +23,7 @@ const UserAvatar = { return { showPlaceholder: false, defaultAvatar: `${this.$store.state.instance.server + this.$store.state.instance.defaultAvatar}`, - betterShadow: this.$store.state.interface.browserSupport.cssFilter + betterShadow: useInterfaceStore().browserSupport.cssFilter } }, components: { diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js @@ -11,6 +11,7 @@ import RichContent from 'src/components/rich_content/rich_content.jsx' import MuteConfirm from '../confirm_modal/mute_confirm.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import { mapGetters } from 'vuex' +import { usePostStatusStore } from 'src/stores/postStatus' import { library } from '@fortawesome/fontawesome-svg-core' import { faBell, @@ -22,6 +23,9 @@ import { faExpandAlt } from '@fortawesome/free-solid-svg-icons' +import { useMediaViewerStore } from '../../stores/media_viewer' +import { useInterfaceStore } from '../../stores/interface' + library.add( faRss, faBell, @@ -188,18 +192,18 @@ export default { ) }, openProfileTab () { - this.$store.dispatch('openSettingsModalTab', 'profile') + useInterfaceStore().openSettingsModalTab('profile') }, zoomAvatar () { const attachment = { url: this.user.profile_image_url_original, mimetype: 'image' } - this.$store.dispatch('setMedia', [attachment]) - this.$store.dispatch('setCurrentMedia', attachment) + useMediaViewerStore().setMedia([attachment]) + useMediaViewerStore().setCurrentMedia(attachment) }, mentionUser () { - this.$store.dispatch('openPostStatusModal', { profileMention: true, repliedUser: this.user }) + usePostStatusStore().openPostStatusModal({ profileMention: true, repliedUser: this.user }) }, onAvatarClickHandler (e) { if (this.onAvatarClick) { diff --git a/src/components/user_list_menu/user_list_menu.js b/src/components/user_list_menu/user_list_menu.js @@ -1,9 +1,10 @@ import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronRight } from '@fortawesome/free-solid-svg-icons' -import { mapState } from 'vuex' +import { mapState } from 'pinia' import DialogModal from '../dialog_modal/dialog_modal.vue' import Popover from '../popover/popover.vue' +import { useListsStore } from 'src/stores/lists' library.add(faChevronRight) @@ -22,8 +23,8 @@ const UserListMenu = { this.$store.dispatch('fetchUserInLists', this.user.id) }, computed: { - ...mapState({ - allLists: state => state.lists.allLists + ...mapState(useListsStore, { + allLists: store => store.allLists }), inListsSet () { return new Set(this.user.inLists.map(x => x.id)) @@ -44,12 +45,12 @@ const UserListMenu = { methods: { toggleList (listId) { if (this.inListsSet.has(listId)) { - this.$store.dispatch('removeListAccount', { accountId: this.user.id, listId }).then((response) => { + useListsStore().removeListAccount({ accountId: this.user.id, listId }).then((response) => { if (!response.ok) { return } this.$store.dispatch('fetchUserInLists', this.user.id) }) } else { - this.$store.dispatch('addListAccount', { accountId: this.user.id, listId }).then((response) => { + useListsStore().addListAccount({ accountId: this.user.id, listId }).then((response) => { if (!response.ok) { return } this.$store.dispatch('fetchUserInLists', this.user.id) }) diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js @@ -3,6 +3,7 @@ import List from '../list/list.vue' import Checkbox from '../checkbox/checkbox.vue' import Modal from '../modal/modal.vue' import UserLink from '../user_link/user_link.vue' +import { useReportsStore } from 'src/stores/reports' const UserReportingModal = { components: { @@ -23,7 +24,7 @@ const UserReportingModal = { }, computed: { reportModal () { - return this.$store.state.reports.reportModal + return useReportsStore().reportModal }, isLoggedIn () { return !!this.$store.state.users.currentUser @@ -63,7 +64,7 @@ const UserReportingModal = { this.error = false }, closeModal () { - this.$store.dispatch('closeUserReportingModal') + useReportsStore().closeUserReportingModal() }, reportUser () { this.processing = true diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js @@ -1,5 +1,6 @@ import merge from 'lodash.merge' import { each, get, set, cloneDeep } from 'lodash' +import { useInterfaceStore } from 'src/stores/interface' import { storage } from './storage.js' 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 @@ -1,8 +1,10 @@ +import { useInterfaceStore } from 'src/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 @@ -1,32 +1,23 @@ import { createStore } from 'vuex' +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 notificationsModule from './modules/notifications.js' -import listsModule from './modules/lists.js' import usersModule from './modules/users.js' import apiModule from './modules/api.js' import configModule from './modules/config.js' import profileConfigModule from './modules/profileConfig.js' import serverSideStorageModule from './modules/serverSideStorage.js' import adminSettingsModule from './modules/adminSettings.js' -import shoutModule from './modules/shout.js' import oauthModule from './modules/oauth.js' import authFlowModule from './modules/auth_flow.js' -import mediaViewerModule from './modules/media_viewer.js' import oauthTokensModule from './modules/oauth_tokens.js' -import reportsModule from './modules/reports.js' -import pollsModule from './modules/polls.js' -import postStatusModule from './modules/postStatus.js' -import editStatusModule from './modules/editStatus.js' -import statusHistoryModule from './modules/statusHistory.js' import draftsModule from './modules/drafts.js' import chatsModule from './modules/chats.js' -import announcementsModule from './modules/announcements.js' import bookmarkFoldersModule from './modules/bookmark_folders.js' import { createI18n } from 'vue-i18n' @@ -85,6 +76,7 @@ const persistedStateOptions = { try { let storageError const plugins = [pushNotifications] + const pinia = createPinia() try { const persistedState = await createPersistedState(persistedStateOptions) plugins.push(persistedState) @@ -98,36 +90,21 @@ const persistedStateOptions = { document.querySelector('#splash-credit').textContent = i18n.global.t('update.art_by', { linkToArtist: 'pipivovott' }) const store = createStore({ modules: { - i18n: { - getters: { - i18n: () => i18n.global - } - }, - interface: interfaceModule, instance: instanceModule, // TODO refactor users/statuses modules, they depend on each other users: usersModule, statuses: statusesModule, notifications: notificationsModule, - lists: listsModule, api: apiModule, config: configModule, profileConfig: profileConfigModule, serverSideStorage: serverSideStorageModule, adminSettings: adminSettingsModule, - shout: shoutModule, oauth: oauthModule, authFlow: authFlowModule, - mediaViewer: mediaViewerModule, oauthTokens: oauthTokensModule, - reports: reportsModule, - polls: pollsModule, - postStatus: postStatusModule, - editStatus: editStatusModule, - statusHistory: statusHistoryModule, drafts: draftsModule, chats: chatsModule, - announcements: announcementsModule, bookmarkFolders: bookmarkFoldersModule }, plugins, @@ -137,10 +114,9 @@ const persistedStateOptions = { strict: false // Socket modifies itself, let's ignore this for now. // strict: process.env.NODE_ENV !== 'production' }) - if (storageError) { - store.dispatch('pushGlobalNotice', { messageKey: 'errors.storage_unavailable', level: 'error' }) - } - return await afterStoreSetup({ store, i18n }) + window.vuex = store + // Temporarily passing pinia and vuex stores along with storageError result until migration is fully complete. + return await afterStoreSetup({ pinia, store, storageError, i18n }) } catch (e) { splashError(i18n, e) } diff --git a/src/modules/announcements.js b/src/modules/announcements.js @@ -1,135 +0,0 @@ -const FETCH_ANNOUNCEMENT_INTERVAL_MS = 1000 * 60 * 5 - -export const defaultState = { - announcements: [], - supportsAnnouncements: true, - fetchAnnouncementsTimer: undefined -} - -export const mutations = { - setAnnouncements (state, announcements) { - state.announcements = announcements - }, - setAnnouncementRead (state, { id, read }) { - const index = state.announcements.findIndex(a => a.id === id) - - if (index < 0) { - return - } - - state.announcements[index].read = read - }, - setFetchAnnouncementsTimer (state, timer) { - state.fetchAnnouncementsTimer = timer - }, - setSupportsAnnouncements (state, supportsAnnouncements) { - state.supportsAnnouncements = supportsAnnouncements - } -} - -export const getters = { - unreadAnnouncementCount (state, _getters, rootState) { - if (!rootState.users.currentUser) { - return 0 - } - - const unread = state.announcements.filter(announcement => !(announcement.inactive || announcement.read)) - return unread.length - } -} - -const announcements = { - state: defaultState, - mutations, - getters, - actions: { - fetchAnnouncements (store) { - if (!store.state.supportsAnnouncements) { - return Promise.resolve() - } - - const currentUser = store.rootState.users.currentUser - const isAdmin = currentUser && currentUser.privileges.includes('announcements_manage_announcements') - - const getAnnouncements = async () => { - if (!isAdmin) { - return store.rootState.api.backendInteractor.fetchAnnouncements() - } - - const all = await store.rootState.api.backendInteractor.adminFetchAnnouncements() - const visible = await store.rootState.api.backendInteractor.fetchAnnouncements() - const visibleObject = visible.reduce((a, c) => { - a[c.id] = c - return a - }, {}) - const getWithinVisible = announcement => visibleObject[announcement.id] - - all.forEach(announcement => { - const visibleAnnouncement = getWithinVisible(announcement) - if (!visibleAnnouncement) { - announcement.inactive = true - } else { - announcement.read = visibleAnnouncement.read - } - }) - - return all - } - - return getAnnouncements() - .then(announcements => { - store.commit('setAnnouncements', announcements) - }) - .catch(error => { - // If and only if backend does not support announcements, it would return 404. - // In this case, silently ignores it. - if (error && error.statusCode === 404) { - store.commit('setSupportsAnnouncements', false) - } else { - throw error - } - }) - }, - markAnnouncementAsRead (store, id) { - return store.rootState.api.backendInteractor.dismissAnnouncement({ id }) - .then(() => { - store.commit('setAnnouncementRead', { id, read: true }) - }) - }, - startFetchingAnnouncements (store) { - if (store.state.fetchAnnouncementsTimer) { - return - } - - const interval = setInterval(() => store.dispatch('fetchAnnouncements'), FETCH_ANNOUNCEMENT_INTERVAL_MS) - store.commit('setFetchAnnouncementsTimer', interval) - - return store.dispatch('fetchAnnouncements') - }, - stopFetchingAnnouncements (store) { - const interval = store.state.fetchAnnouncementsTimer - store.commit('setFetchAnnouncementsTimer', undefined) - clearInterval(interval) - }, - postAnnouncement (store, { content, startsAt, endsAt, allDay }) { - return store.rootState.api.backendInteractor.postAnnouncement({ content, startsAt, endsAt, allDay }) - .then(() => { - return store.dispatch('fetchAnnouncements') - }) - }, - editAnnouncement (store, { id, content, startsAt, endsAt, allDay }) { - return store.rootState.api.backendInteractor.editAnnouncement({ id, content, startsAt, endsAt, allDay }) - .then(() => { - return store.dispatch('fetchAnnouncements') - }) - }, - deleteAnnouncement (store, id) { - return store.rootState.api.backendInteractor.deleteAnnouncement({ id }) - .then(() => { - return store.dispatch('fetchAnnouncements') - }) - } - } -} - -export default announcements diff --git a/src/modules/api.js b/src/modules/api.js @@ -2,6 +2,8 @@ import backendInteractorService from '../services/backend_interactor_service/bac import { WSConnectionStatus } from '../services/api/api.service.js' import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js' import { Socket } from 'phoenix' +import { useShoutStore } from 'src/stores/shout.js' +import { useInterfaceStore } from 'src/stores/interface.js' const retryTimeout = (multiplier) => 1000 * multiplier @@ -134,7 +136,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 @@ -176,7 +178,7 @@ const api = { dispatch('startFetchingTimeline', { timeline: 'friends' }) dispatch('startFetchingNotifications') dispatch('startFetchingChats') - dispatch('pushGlobalNotice', { + useInterfaceStore().pushGlobalNotice({ level: 'error', messageKey: 'timeline.socket_broke', messageArgs: [code], @@ -300,7 +302,7 @@ const api = { socket.connect() commit('setSocket', socket) - dispatch('initializeShout', socket) + useShoutStore().initializeShout(socket) } }, disconnectFromSocket ({ commit, state }) { diff --git a/src/modules/config.js b/src/modules/config.js @@ -3,6 +3,10 @@ import { applyConfig } from '../services/style_setter/style_setter.js' import messages from '../i18n/messages' import { set } from 'lodash' import localeService from '../services/locale/locale.service.js' +import { useI18nStore } from 'src/stores/i18n.js' +import { useInterfaceStore } from 'src/stores/interface.js' + +import { defaultState } from './default_config_state.js' const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage' const APPEARANCE_SETTINGS_KEYS = new Set([ @@ -17,8 +21,6 @@ const APPEARANCE_SETTINGS_KEYS = new Set([ 'emojiReactionsScale' ]) -const browserLocale = (window.navigator.language || 'en').split('-')[0] - /* TODO this is a bit messy. * We need to declare settings with their types and also deal with * instance-default settings in some way, hopefully try to avoid copy-pasta @@ -34,170 +36,6 @@ export const multiChoiceProperties = [ 'unsavedPostAction' // save | discard | confirm ] -export const defaultState = { - expertLevel: 0, // used to track which settings to show and hide - - // Theme stuff - theme: undefined, // Very old theme store, stores preset name, still in use - - // V1 - colors: {}, // VERY old theme store, just colors of V1, probably not even used anymore - - // V2 - customTheme: undefined, // "snapshot", previously was used as actual theme store for V2 so it's still used in case of PleromaFE downgrade event. - customThemeSource: undefined, // "source", stores original theme data - - // V3 - style: null, - styleCustomData: null, - palette: null, - paletteCustomData: null, - themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions - forceThemeRecompilation: false, // flag that forces recompilation on boot even if cache exists - theme3hacks: { // Hacks, user overrides that are independent of theme used - underlay: 'none', - fonts: { - interface: undefined, - input: undefined, - post: undefined, - monospace: undefined - } - }, - - hideISP: false, - hideInstanceWallpaper: false, - hideShoutbox: false, - // bad name: actually hides posts of muted USERS - hideMutedPosts: undefined, // instance default - hideMutedThreads: undefined, // instance default - hideWordFilteredPosts: undefined, // instance default - muteBotStatuses: undefined, // instance default - muteSensitiveStatuses: undefined, // instance default - collapseMessageWithSubject: undefined, // instance default - padEmoji: true, - hideAttachments: false, - hideAttachmentsInConv: false, - hideScrobbles: false, - hideScrobblesAfter: '2d', - maxThumbnails: 16, - hideNsfw: true, - preloadImage: true, - loopVideo: true, - loopVideoSilentOnly: true, - streaming: false, - emojiReactionsOnTimeline: true, - alwaysShowNewPostButton: false, - autohideFloatingPostButton: false, - pauseOnUnfocused: true, - stopGifs: true, - replyVisibility: 'all', - thirdColumnMode: 'notifications', - notificationVisibility: { - follows: true, - mentions: true, - statuses: true, - likes: true, - repeats: true, - moves: true, - emojiReactions: true, - followRequest: true, - reports: true, - chatMention: true, - polls: true - }, - notificationNative: { - follows: true, - mentions: true, - statuses: true, - likes: false, - repeats: false, - moves: false, - emojiReactions: false, - followRequest: true, - reports: true, - chatMention: true, - polls: true - }, - webPushNotifications: false, - webPushAlwaysShowNotifications: false, - muteWords: [], - highlight: {}, - interfaceLanguage: browserLocale, - hideScopeNotice: false, - useStreamingApi: false, - sidebarRight: undefined, // instance default - scopeCopy: undefined, // instance default - subjectLineBehavior: undefined, // instance default - alwaysShowSubjectInput: undefined, // instance default - postContentType: undefined, // instance default - minimalScopesMode: undefined, // instance default - // This hides statuses filtered via a word filter - hideFilteredStatuses: undefined, // instance default - modalOnRepeat: undefined, // instance default - modalOnUnfollow: undefined, // instance default - modalOnBlock: undefined, // instance default - modalOnMute: undefined, // instance default - modalOnMuteConversation: undefined, // instance default - modalOnMuteDomain: undefined, // instance default - modalOnDelete: undefined, // instance default - modalOnLogout: undefined, // instance default - modalOnApproveFollow: undefined, // instance default - modalOnDenyFollow: undefined, // instance default - modalOnRemoveUserFromFollowers: undefined, // instance default - modalMobileCenter: undefined, - playVideosInModal: false, - useOneClickNsfw: false, - useContainFit: true, - disableStickyHeaders: false, - showScrollbars: false, - userPopoverAvatarAction: 'open', - userPopoverOverlay: false, - sidebarColumnWidth: '25rem', - contentColumnWidth: '45rem', - notifsColumnWidth: '25rem', - emojiReactionsScale: undefined, - textSize: undefined, // instance default - emojiSize: undefined, // instance default - navbarSize: undefined, // instance default - panelHeaderSize: undefined, // instance default - forcedRoundness: undefined, // instance default - navbarColumnStretch: false, - greentext: undefined, // instance default - useAtIcon: undefined, // instance default - mentionLinkDisplay: undefined, // instance default - mentionLinkShowTooltip: undefined, // instance default - mentionLinkShowAvatar: undefined, // instance default - mentionLinkFadeDomain: undefined, // instance default - mentionLinkShowYous: undefined, // instance default - mentionLinkBoldenYou: undefined, // instance default - hidePostStats: undefined, // instance default - hideBotIndication: undefined, // instance default - hideUserStats: undefined, // instance default - virtualScrolling: undefined, // instance default - sensitiveByDefault: undefined, // instance default - conversationDisplay: undefined, // instance default - conversationTreeAdvanced: undefined, // instance default - conversationOtherRepliesButton: undefined, // instance default - conversationTreeFadeAncestors: undefined, // instance default - showExtraNotifications: undefined, // instance default - showExtraNotificationsTip: undefined, // instance default - showChatsInExtraNotifications: undefined, // instance default - showAnnouncementsInExtraNotifications: undefined, // instance default - showFollowRequestsInExtraNotifications: undefined, // instance default - maxDepthInThread: undefined, // instance default - autocompleteSelect: undefined, // instance default - closingDrawerMarksAsSeen: undefined, // instance default - unseenAtTop: undefined, // instance default - ignoreInactionableSeen: undefined, // instance default - unsavedPostAction: undefined, // instance default - autoSaveDraft: undefined, // instance default - useAbsoluteTimeFormat: undefined, // instance default - absoluteTimeFormatMinAge: undefined, // instance default - absoluteTime12h: undefined, // instance default - imageCompression: true, - alwaysUseJpeg: false -} - // caching the instance default properties export const instanceDefaultProperties = Object.entries(defaultState) .filter(([key, value]) => value === undefined) @@ -260,7 +98,7 @@ const config = { commit('setHighlight', { user, color, type }) }, setOptionTemporarily ({ commit, dispatch, state, rootState }, { name, value }) { - if (rootState.interface.temporaryChangesTimeoutId !== null) { + if (useInterfaceStore().temporaryChangesTimeoutId !== null) { console.warn('Can\'t track more than one temporary change') return } @@ -328,7 +166,7 @@ const config = { break } case 'interfaceLanguage': - messages.setLanguage(this.getters.i18n, value) + messages.setLanguage(useI18nStore().i18n, value) dispatch('loadUnicodeEmojiData', value) Cookies.set( BACKEND_LANGUAGE_COOKIE_NAME, @@ -336,7 +174,7 @@ const config = { ) break case 'thirdColumnMode': - dispatch('setLayoutWidth', undefined) + useInterfaceStore().setLayoutWidth(undefined) break } } diff --git a/src/modules/default_config_state.js b/src/modules/default_config_state.js @@ -0,0 +1,165 @@ +const browserLocale = (window.navigator.language || 'en').split('-')[0] + +export const defaultState = { + expertLevel: 0, // used to track which settings to show and hide + + // Theme stuff + theme: undefined, // Very old theme store, stores preset name, still in use + + // V1 + colors: {}, // VERY old theme store, just colors of V1, probably not even used anymore + + // V2 + customTheme: undefined, // "snapshot", previously was used as actual theme store for V2 so it's still used in case of PleromaFE downgrade event. + customThemeSource: undefined, // "source", stores original theme data + + // V3 + style: null, + styleCustomData: null, + palette: null, + paletteCustomData: null, + themeDebug: false, // debug mode that uses computed backgrounds instead of real ones to debug contrast functions + forceThemeRecompilation: false, // flag that forces recompilation on boot even if cache exists + theme3hacks: { // Hacks, user overrides that are independent of theme used + underlay: 'none', + fonts: { + interface: undefined, + input: undefined, + post: undefined, + monospace: undefined + } + }, + + hideISP: false, + hideInstanceWallpaper: false, + hideShoutbox: false, + // bad name: actually hides posts of muted USERS + hideMutedPosts: undefined, // instance default + hideMutedThreads: undefined, // instance default + hideWordFilteredPosts: undefined, // instance default + muteBotStatuses: undefined, // instance default + muteSensitiveStatuses: undefined, // instance default + collapseMessageWithSubject: undefined, // instance default + padEmoji: true, + hideAttachments: false, + hideAttachmentsInConv: false, + hideScrobbles: false, + hideScrobblesAfter: '2d', + maxThumbnails: 16, + hideNsfw: true, + preloadImage: true, + loopVideo: true, + loopVideoSilentOnly: true, + streaming: false, + emojiReactionsOnTimeline: true, + alwaysShowNewPostButton: false, + autohideFloatingPostButton: false, + pauseOnUnfocused: true, + stopGifs: true, + replyVisibility: 'all', + thirdColumnMode: 'notifications', + notificationVisibility: { + follows: true, + mentions: true, + statuses: true, + likes: true, + repeats: true, + moves: true, + emojiReactions: true, + followRequest: true, + reports: true, + chatMention: true, + polls: true + }, + notificationNative: { + follows: true, + mentions: true, + statuses: true, + likes: false, + repeats: false, + moves: false, + emojiReactions: false, + followRequest: true, + reports: true, + chatMention: true, + polls: true + }, + webPushNotifications: false, + webPushAlwaysShowNotifications: false, + muteWords: [], + highlight: {}, + interfaceLanguage: browserLocale, + hideScopeNotice: false, + useStreamingApi: false, + sidebarRight: undefined, // instance default + scopeCopy: undefined, // instance default + subjectLineBehavior: undefined, // instance default + alwaysShowSubjectInput: undefined, // instance default + postContentType: undefined, // instance default + minimalScopesMode: undefined, // instance default + // This hides statuses filtered via a word filter + hideFilteredStatuses: undefined, // instance default + modalOnRepeat: undefined, // instance default + modalOnUnfollow: undefined, // instance default + modalOnBlock: undefined, // instance default + modalOnMute: undefined, // instance default + modalOnMuteConversation: undefined, // instance default + modalOnMuteDomain: undefined, // instance default + modalOnDelete: undefined, // instance default + modalOnLogout: undefined, // instance default + modalOnApproveFollow: undefined, // instance default + modalOnDenyFollow: undefined, // instance default + modalOnRemoveUserFromFollowers: undefined, // instance default + modalMobileCenter: undefined, + playVideosInModal: false, + useOneClickNsfw: false, + useContainFit: true, + disableStickyHeaders: false, + showScrollbars: false, + userPopoverAvatarAction: 'open', + userPopoverOverlay: false, + sidebarColumnWidth: '25rem', + contentColumnWidth: '45rem', + notifsColumnWidth: '25rem', + emojiReactionsScale: undefined, + textSize: undefined, // instance default + emojiSize: undefined, // instance default + navbarSize: undefined, // instance default + panelHeaderSize: undefined, // instance default + forcedRoundness: undefined, // instance default + navbarColumnStretch: false, + greentext: undefined, // instance default + useAtIcon: undefined, // instance default + mentionLinkDisplay: undefined, // instance default + mentionLinkShowTooltip: undefined, // instance default + mentionLinkShowAvatar: undefined, // instance default + mentionLinkFadeDomain: undefined, // instance default + mentionLinkShowYous: undefined, // instance default + mentionLinkBoldenYou: undefined, // instance default + hidePostStats: undefined, // instance default + hideBotIndication: undefined, // instance default + hideUserStats: undefined, // instance default + virtualScrolling: undefined, // instance default + sensitiveByDefault: undefined, // instance default + conversationDisplay: undefined, // instance default + conversationTreeAdvanced: undefined, // instance default + conversationOtherRepliesButton: undefined, // instance default + conversationTreeFadeAncestors: undefined, // instance default + showExtraNotifications: undefined, // instance default + showExtraNotificationsTip: undefined, // instance default + showChatsInExtraNotifications: undefined, // instance default + showAnnouncementsInExtraNotifications: undefined, // instance default + showFollowRequestsInExtraNotifications: undefined, // instance default + maxDepthInThread: undefined, // instance default + autocompleteSelect: undefined, // instance default + closingDrawerMarksAsSeen: undefined, // instance default + unseenAtTop: undefined, // instance default + ignoreInactionableSeen: undefined, // instance default + unsavedPostAction: undefined, // instance default + autoSaveDraft: undefined, // instance default + useAbsoluteTimeFormat: undefined, // instance default + absoluteTimeFormatMinAge: undefined, // instance default + absoluteTime12h: undefined, // instance default + imageCompression: true, + alwaysUseJpeg: false +} diff --git a/src/modules/editStatus.js b/src/modules/editStatus.js @@ -1,25 +0,0 @@ -const editStatus = { - state: { - params: null, - modalActivated: false - }, - mutations: { - openEditStatusModal (state, params) { - state.params = params - state.modalActivated = true - }, - closeEditStatusModal (state) { - state.modalActivated = false - } - }, - actions: { - openEditStatusModal ({ commit }, params) { - commit('openEditStatusModal', params) - }, - closeEditStatusModal ({ commit }) { - commit('closeEditStatusModal') - } - } -} - -export default editStatus diff --git a/src/modules/instance.js b/src/modules/instance.js @@ -1,6 +1,7 @@ import apiService from '../services/api/api.service.js' import { instanceDefaultProperties } from './config.js' import { langCodeToCldrName, ensureFinalFallback } from '../i18n/languages.js' +import { useInterfaceStore } from 'src/stores/interface.js' const SORTED_EMOJI_GROUP_IDS = [ 'smileys-and-emotion', @@ -292,7 +293,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/interface.js b/src/modules/interface.js @@ -1,724 +0,0 @@ -import { getResourcesIndex, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js' -import { CURRENT_VERSION, generatePreset } from 'src/services/theme_data/theme_data.service.js' -import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' -import { deserialize } from '../services/theme_data/iss_deserializer.js' - -// helper for debugging -// eslint-disable-next-line no-unused-vars -const toValue = (x) => JSON.parse(JSON.stringify(x === undefined ? 'null' : x)) - -const defaultState = { - localFonts: null, - themeApplied: false, - themeChangeInProgress: false, - themeVersion: 'v3', - styleNameUsed: null, - styleDataUsed: null, - useStylePalette: false, // hack for applying styles from appearance tab - paletteNameUsed: null, - paletteDataUsed: null, - themeNameUsed: null, - themeDataUsed: null, - temporaryChangesTimeoutId: null, // used for temporary options that revert after a timeout - temporaryChangesConfirm: () => {}, // used for applying temporary options - temporaryChangesRevert: () => {}, // used for reverting temporary options - settingsModalState: 'hidden', - settingsModalLoadedUser: false, - settingsModalLoadedAdmin: false, - settingsModalTargetTab: null, - settingsModalMode: 'user', - 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)') - ), - localFonts: typeof window.queryLocalFonts === 'function' - }, - layoutType: 'normal', - globalNotices: [], - layoutHeight: 0, - lastTimeline: null -} - -const interfaceMod = { - state: defaultState, - mutations: { - settingsSaved (state, { success, error }) { - if (success) { - if (state.noticeClearTimeout) { - clearTimeout(state.noticeClearTimeout) - } - state.settings.currentSaveStateNotice = { error: false, data: success } - state.settings.noticeClearTimeout = setTimeout(() => delete state.settings.currentSaveStateNotice, 2000) - } else { - state.settings.currentSaveStateNotice = { error: true, errorData: error } - } - }, - setTemporaryChanges (state, { timeoutId, confirm, revert }) { - state.temporaryChangesTimeoutId = timeoutId - state.temporaryChangesConfirm = confirm - state.temporaryChangesRevert = revert - }, - clearTemporaryChanges (state) { - clearTimeout(state.temporaryChangesTimeoutId) - state.temporaryChangesTimeoutId = null - state.temporaryChangesConfirm = () => {} - state.temporaryChangesRevert = () => {} - }, - setThemeApplied (state) { - state.themeApplied = true - }, - setNotificationPermission (state, permission) { - state.notificationPermission = permission - }, - setLayoutType (state, value) { - state.layoutType = value - }, - closeSettingsModal (state) { - state.settingsModalState = 'hidden' - }, - togglePeekSettingsModal (state) { - switch (state.settingsModalState) { - case 'minimized': - state.settingsModalState = 'visible' - return - case 'visible': - state.settingsModalState = 'minimized' - return - default: - throw new Error('Illegal minimization state of settings modal') - } - }, - openSettingsModal (state, value) { - state.settingsModalMode = value - state.settingsModalState = 'visible' - if (value === 'user') { - if (!state.settingsModalLoadedUser) { - state.settingsModalLoadedUser = true - } - } else if (value === 'admin') { - if (!state.settingsModalLoadedAdmin) { - state.settingsModalLoadedAdmin = true - } - } - }, - setSettingsModalTargetTab (state, value) { - state.settingsModalTargetTab = value - }, - pushGlobalNotice (state, notice) { - state.globalNotices.push(notice) - }, - removeGlobalNotice (state, notice) { - state.globalNotices = state.globalNotices.filter(n => n !== notice) - }, - setLayoutHeight (state, value) { - state.layoutHeight = value - }, - setLayoutWidth (state, value) { - state.layoutWidth = value - }, - setLastTimeline (state, value) { - state.lastTimeline = value - }, - setFontsList (state, value) { - // Set is used here so that we filter out duplicate fonts (possibly same font but with different weight) - state.localFonts = [...(new Set(value.map(font => font.family))).values()] - } - }, - actions: { - setPageTitle ({ rootState }, option = '') { - document.title = `${option} ${rootState.instance.name}` - }, - settingsSaved ({ commit, dispatch }, { success, error }) { - commit('settingsSaved', { success, error }) - }, - setNotificationPermission ({ commit }, permission) { - commit('setNotificationPermission', permission) - }, - closeSettingsModal ({ commit }) { - commit('closeSettingsModal') - }, - openSettingsModal ({ commit }, value = 'user') { - commit('openSettingsModal', value) - }, - togglePeekSettingsModal ({ commit }) { - commit('togglePeekSettingsModal') - }, - clearSettingsModalTargetTab ({ commit }) { - commit('setSettingsModalTargetTab', null) - }, - openSettingsModalTab ({ commit }, value) { - commit('setSettingsModalTargetTab', value) - commit('openSettingsModal', 'user') - }, - pushGlobalNotice ( - { commit, dispatch, state }, - { - messageKey, - messageArgs = {}, - level = 'error', - timeout = 0 - }) { - const notice = { - messageKey, - messageArgs, - level - } - commit('pushGlobalNotice', 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 = state.globalNotices[state.globalNotices.length - 1] - if (timeout) { - setTimeout(() => dispatch('removeGlobalNotice', newNotice), timeout) - } - return newNotice - }, - removeGlobalNotice ({ commit }, notice) { - commit('removeGlobalNotice', notice) - }, - setLayoutHeight ({ commit }, value) { - commit('setLayoutHeight', value) - }, - // value is optional, assuming it was cached prior - setLayoutWidth ({ commit, state, rootGetters, rootState }, value) { - let width = value - if (value !== undefined) { - commit('setLayoutWidth', value) - } else { - width = state.layoutWidth - } - const mobileLayout = width <= 800 - const normalOrMobile = mobileLayout ? 'mobile' : 'normal' - const { thirdColumnMode } = rootGetters.mergedConfig - if (thirdColumnMode === 'none' || !rootState.users.currentUser) { - commit('setLayoutType', normalOrMobile) - } else { - const wideLayout = width >= 1300 - commit('setLayoutType', wideLayout ? 'wide' : normalOrMobile) - } - }, - queryLocalFonts ({ commit, dispatch, state }) { - if (state.localFonts !== null) return - commit('setFontsList', []) - if (!state.browserSupport.localFonts) { - return - } - window - .queryLocalFonts() - .then((fonts) => { - commit('setFontsList', fonts) - }) - .catch((e) => { - dispatch('pushGlobalNotice', { - messageKey: 'settings.style.themes3.font.font_list_unavailable', - messageArgs: { - error: e - }, - level: 'error' - }) - }) - }, - setLastTimeline ({ commit }, value) { - commit('setLastTimeline', value) - }, - async fetchPalettesIndex ({ commit, state }) { - try { - const value = await getResourcesIndex('/static/palettes/index.json') - commit('setInstanceOption', { name: 'palettesIndex', value }) - return value - } catch (e) { - console.error('Could not fetch palettes index', e) - commit('setInstanceOption', { name: 'palettesIndex', value: { _error: e } }) - return Promise.resolve({}) - } - }, - setPalette ({ dispatch, commit }, value) { - dispatch('resetThemeV3Palette') - dispatch('resetThemeV2') - - commit('setOption', { name: 'palette', value }) - - dispatch('applyTheme', { recompile: true }) - }, - setPaletteCustom ({ dispatch, commit }, value) { - dispatch('resetThemeV3Palette') - dispatch('resetThemeV2') - - commit('setOption', { name: 'paletteCustomData', value }) - - dispatch('applyTheme', { recompile: true }) - }, - async fetchStylesIndex ({ commit, state }) { - try { - const value = await getResourcesIndex( - '/static/styles/index.json', - deserialize - ) - commit('setInstanceOption', { name: 'stylesIndex', value }) - return value - } catch (e) { - console.error('Could not fetch styles index', e) - commit('setInstanceOption', { name: 'stylesIndex', value: { _error: e } }) - return Promise.resolve({}) - } - }, - setStyle ({ dispatch, commit, state }, value) { - dispatch('resetThemeV3') - dispatch('resetThemeV2') - dispatch('resetThemeV3Palette') - - commit('setOption', { name: 'style', value }) - state.useStylePalette = true - - dispatch('applyTheme', { recompile: true }).then(() => { - state.useStylePalette = false - }) - }, - setStyleCustom ({ dispatch, commit, state }, value) { - dispatch('resetThemeV3') - dispatch('resetThemeV2') - dispatch('resetThemeV3Palette') - - commit('setOption', { name: 'styleCustomData', value }) - - state.useStylePalette = true - dispatch('applyTheme', { recompile: true }).then(() => { - state.useStylePalette = false - }) - }, - async fetchThemesIndex ({ commit, state }) { - try { - const value = await getResourcesIndex('/static/styles.json') - commit('setInstanceOption', { name: 'themesIndex', value }) - return value - } catch (e) { - console.error('Could not fetch themes index', e) - commit('setInstanceOption', { name: 'themesIndex', value: { _error: e } }) - return Promise.resolve({}) - } - }, - setTheme ({ dispatch, commit }, value) { - dispatch('resetThemeV3') - dispatch('resetThemeV3Palette') - dispatch('resetThemeV2') - - commit('setOption', { name: 'theme', value }) - - dispatch('applyTheme', { recompile: true }) - }, - setThemeCustom ({ dispatch, commit }, value) { - dispatch('resetThemeV3') - dispatch('resetThemeV3Palette') - dispatch('resetThemeV2') - - commit('setOption', { name: 'customTheme', value }) - commit('setOption', { name: 'customThemeSource', value }) - - dispatch('applyTheme', { recompile: true }) - }, - resetThemeV3 ({ dispatch, commit }) { - commit('setOption', { name: 'style', value: null }) - commit('setOption', { name: 'styleCustomData', value: null }) - }, - resetThemeV3Palette ({ dispatch, commit }) { - commit('setOption', { name: 'palette', value: null }) - commit('setOption', { name: 'paletteCustomData', value: null }) - }, - resetThemeV2 ({ dispatch, commit }) { - commit('setOption', { name: 'theme', value: null }) - commit('setOption', { name: 'customTheme', value: null }) - commit('setOption', { name: 'customThemeSource', value: null }) - }, - async getThemeData ({ dispatch, commit, rootState, state }) { - const getData = async (resource, index, customData, name) => { - const capitalizedResource = resource[0].toUpperCase() + resource.slice(1) - const result = {} - - if (customData) { - result.nameUsed = 'custom' // custom data overrides name - result.dataUsed = customData - } else { - result.nameUsed = name - - if (result.nameUsed == null) { - result.dataUsed = null - return result - } - - let fetchFunc = index[result.nameUsed] - // Fallbacks - if (!fetchFunc) { - if (resource === 'style' || resource === 'palette') { - return result - } - const newName = Object.keys(index)[0] - fetchFunc = index[newName] - console.warn(`${capitalizedResource} with id '${state.styleNameUsed}' not found, trying back to '${newName}'`) - if (!fetchFunc) { - console.warn(`${capitalizedResource} doesn't have a fallback, defaulting to stock.`) - fetchFunc = () => Promise.resolve(null) - } - } - result.dataUsed = await fetchFunc() - } - return result - } - - const { - style: instanceStyleName, - palette: instancePaletteName - } = rootState.instance - - let { - theme: instanceThemeV2Name, - themesIndex, - stylesIndex, - palettesIndex - } = rootState.instance - - const { - style: userStyleName, - styleCustomData: userStyleCustomData, - palette: userPaletteName, - paletteCustomData: userPaletteCustomData - } = rootState.config - - let { - theme: userThemeV2Name, - customTheme: userThemeV2Snapshot, - customThemeSource: userThemeV2Source - } = rootState.config - - let majorVersionUsed - - console.debug( - `User V3 palette: ${userPaletteName}, style: ${userStyleName} , custom: ${!!userStyleCustomData}` - ) - console.debug( - `User V2 name: ${userThemeV2Name}, source: ${!!userThemeV2Source}, snapshot: ${!!userThemeV2Snapshot}` - ) - - console.debug(`Instance V3 palette: ${instancePaletteName}, style: ${instanceStyleName}`) - console.debug('Instance V2 theme: ' + instanceThemeV2Name) - - if (userPaletteName || userPaletteCustomData || - userStyleName || userStyleCustomData || - ( - // User V2 overrides instance V3 - (instancePaletteName || - instanceStyleName) && - instanceThemeV2Name == null && - userThemeV2Name == null - ) - ) { - // Palette and/or style overrides V2 themes - instanceThemeV2Name = null - userThemeV2Name = null - userThemeV2Source = null - userThemeV2Snapshot = null - - majorVersionUsed = 'v3' - } else if ( - (userThemeV2Name || - userThemeV2Snapshot || - userThemeV2Source || - instanceThemeV2Name) - ) { - majorVersionUsed = 'v2' - } else { - // if all fails fallback to v3 - majorVersionUsed = 'v3' - } - - if (majorVersionUsed === 'v3') { - const result = await Promise.all([ - dispatch('fetchPalettesIndex'), - dispatch('fetchStylesIndex') - ]) - - palettesIndex = result[0] - stylesIndex = result[1] - } else { - // Promise.all just to be uniform with v3 - const result = await Promise.all([ - dispatch('fetchThemesIndex') - ]) - - themesIndex = result[0] - } - - state.themeVersion = majorVersionUsed - - console.debug('Version used', majorVersionUsed) - - if (majorVersionUsed === 'v3') { - state.themeDataUsed = null - state.themeNameUsed = null - - const style = await getData( - 'style', - stylesIndex, - userStyleCustomData, - userStyleName || instanceStyleName - ) - state.styleNameUsed = style.nameUsed - state.styleDataUsed = style.dataUsed - - let firstStylePaletteName = null - style - .dataUsed - ?.filter(x => x.component === '@palette') - .map(x => { - const cleanDirectives = Object.fromEntries( - Object - .entries(x.directives) - .filter(([k, v]) => k) - ) - - return { name: x.variant, ...cleanDirectives } - }) - .forEach(palette => { - const key = 'style.' + palette.name.toLowerCase().replace(/ /g, '_') - if (!firstStylePaletteName) firstStylePaletteName = key - palettesIndex[key] = () => Promise.resolve(palette) - }) - - const palette = await getData( - 'palette', - palettesIndex, - userPaletteCustomData, - state.useStylePalette ? firstStylePaletteName : (userPaletteName || instancePaletteName) - ) - - if (state.useStylePalette) { - commit('setOption', { name: 'palette', value: firstStylePaletteName }) - } - - state.paletteNameUsed = palette.nameUsed - state.paletteDataUsed = palette.dataUsed - - if (state.paletteDataUsed) { - state.paletteDataUsed.link = state.paletteDataUsed.link || state.paletteDataUsed.accent - state.paletteDataUsed.accent = state.paletteDataUsed.accent || state.paletteDataUsed.link - } - if (Array.isArray(state.paletteDataUsed)) { - const [ - name, - bg, - fg, - text, - link, - cRed = '#FF0000', - cGreen = '#00FF00', - cBlue = '#0000FF', - cOrange = '#E3FF00' - ] = palette.dataUsed - state.paletteDataUsed = { - name, - bg, - fg, - text, - link, - accent: link, - cRed, - cBlue, - cGreen, - cOrange - } - } - console.debug('Palette data used', palette.dataUsed) - } else { - state.styleNameUsed = null - state.styleDataUsed = null - state.paletteNameUsed = null - state.paletteDataUsed = null - - const theme = await getData( - 'theme', - themesIndex, - userThemeV2Source || userThemeV2Snapshot, - userThemeV2Name || instanceThemeV2Name - ) - state.themeNameUsed = theme.nameUsed - state.themeDataUsed = theme.dataUsed - } - }, - async applyTheme ( - { dispatch, commit, rootState, state }, - { recompile = false } = {} - ) { - const { - forceThemeRecompilation, - themeDebug, - theme3hacks - } = rootState.config - state.themeChangeInProgress = true - // If we're not not forced to recompile try using - // cache (tryLoadCache return true if load successful) - - const forceRecompile = forceThemeRecompilation || recompile - if (!forceRecompile && !themeDebug && await tryLoadCache()) { - state.themeChangeInProgress = false - return commit('setThemeApplied') - } - window.splashUpdate('splash.theme') - await dispatch('getThemeData') - - try { - const paletteIss = (() => { - if (!state.paletteDataUsed) return null - const result = { - component: 'Root', - directives: {} - } - - Object - .entries(state.paletteDataUsed) - .filter(([k]) => k !== 'name') - .forEach(([k, v]) => { - let issRootDirectiveName - switch (k) { - case 'background': - issRootDirectiveName = 'bg' - break - case 'foreground': - issRootDirectiveName = 'fg' - break - default: - issRootDirectiveName = k - } - result.directives['--' + issRootDirectiveName] = 'color | ' + v - }) - return result - })() - - const theme2ruleset = state.themeDataUsed && convertTheme2To3(normalizeThemeData(state.themeDataUsed)) - const hacks = [] - - Object.entries(theme3hacks).forEach(([key, value]) => { - switch (key) { - case 'fonts': { - Object.entries(theme3hacks.fonts).forEach(([fontKey, font]) => { - if (!font?.family) return - switch (fontKey) { - case 'interface': - hacks.push({ - component: 'Root', - directives: { - '--font': 'generic | ' + font.family - } - }) - break - case 'input': - hacks.push({ - component: 'Input', - directives: { - '--font': 'generic | ' + font.family - } - }) - break - case 'post': - hacks.push({ - component: 'RichContent', - directives: { - '--font': 'generic | ' + font.family - } - }) - break - case 'monospace': - hacks.push({ - component: 'Root', - directives: { - '--monoFont': 'generic | ' + font.family - } - }) - break - } - }) - break - } - case 'underlay': { - if (value !== 'none') { - const newRule = { - component: 'Underlay', - directives: {} - } - if (value === 'opaque') { - newRule.directives.opacity = 1 - newRule.directives.background = '--wallpaper' - } - if (value === 'transparent') { - newRule.directives.opacity = 0 - } - hacks.push(newRule) - } - break - } - } - }) - - const rulesetArray = [ - theme2ruleset, - state.styleDataUsed, - paletteIss, - hacks - ].filter(x => x) - - return applyTheme( - rulesetArray.flat(), - () => commit('setThemeApplied'), - () => { - state.themeChangeInProgress = false - }, - themeDebug - ) - } catch (e) { - window.splashError(e) - } - } - } -} - -export default interfaceMod - -export const normalizeThemeData = (input) => { - let themeData, themeSource - - if (input.themeFileVerison === 1) { - // this might not be even used at all, some leftover of unimplemented code in V2 editor - return generatePreset(input).theme - } else if ( - Object.prototype.hasOwnProperty.call(input, '_pleroma_theme_version') || - Object.prototype.hasOwnProperty.call(input, 'source') || - Object.prototype.hasOwnProperty.call(input, 'theme') - ) { - // We got passed a full theme file - themeData = input.theme - themeSource = input.source - } else if ( - Object.prototype.hasOwnProperty.call(input, 'themeEngineVersion') || - Object.prototype.hasOwnProperty.call(input, 'colors') - ) { - // We got passed a source/snapshot - themeData = input - themeSource = input - } - // New theme presets don't have 'theme' property, they use 'source' - - let out // shout, shout let it all out - if (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION) { - // There are some themes in wild that have completely broken source - out = { ...(themeData || {}), ...themeSource } - } else { - out = themeData - } - - // generatePreset here basically creates/updates "snapshot", - // while also fixing the 2.2 -> 2.3 colors/shadows/etc - return generatePreset(out).theme -} diff --git a/src/modules/lists.js b/src/modules/lists.js @@ -1,130 +0,0 @@ -import { remove, find } from 'lodash' - -export const defaultState = { - allLists: [], - allListsObject: {} -} - -export const mutations = { - setLists (state, value) { - state.allLists = value - }, - setList (state, { listId, title }) { - if (!state.allListsObject[listId]) { - state.allListsObject[listId] = { accountIds: [] } - } - state.allListsObject[listId].title = title - - const entry = find(state.allLists, { id: listId }) - if (!entry) { - state.allLists.push({ id: listId, title }) - } else { - entry.title = title - } - }, - setListAccounts (state, { listId, accountIds }) { - if (!state.allListsObject[listId]) { - state.allListsObject[listId] = { accountIds: [] } - } - state.allListsObject[listId].accountIds = accountIds - }, - addListAccount (state, { listId, accountId }) { - if (!state.allListsObject[listId]) { - state.allListsObject[listId] = { accountIds: [] } - } - state.allListsObject[listId].accountIds.push(accountId) - }, - removeListAccount (state, { listId, accountId }) { - if (!state.allListsObject[listId]) { - state.allListsObject[listId] = { accountIds: [] } - } - const { accountIds } = state.allListsObject[listId] - const set = new Set(accountIds) - set.delete(accountId) - state.allListsObject[listId].accountIds = [...set] - }, - deleteList (state, { listId }) { - delete state.allListsObject[listId] - remove(state.allLists, list => list.id === listId) - } -} - -const actions = { - setLists ({ commit }, value) { - commit('setLists', value) - }, - createList ({ rootState, commit }, { title }) { - return rootState.api.backendInteractor.createList({ title }) - .then((list) => { - commit('setList', { listId: list.id, title }) - return list - }) - }, - fetchList ({ rootState, commit }, { listId }) { - return rootState.api.backendInteractor.getList({ listId }) - .then((list) => commit('setList', { listId: list.id, title: list.title })) - }, - fetchListAccounts ({ rootState, commit }, { listId }) { - return rootState.api.backendInteractor.getListAccounts({ listId }) - .then((accountIds) => commit('setListAccounts', { listId, accountIds })) - }, - setList ({ rootState, commit }, { listId, title }) { - rootState.api.backendInteractor.updateList({ listId, title }) - commit('setList', { listId, title }) - }, - setListAccounts ({ rootState, commit }, { listId, accountIds }) { - const saved = rootState.lists.allListsObject[listId].accountIds || [] - const added = accountIds.filter(id => !saved.includes(id)) - const removed = saved.filter(id => !accountIds.includes(id)) - commit('setListAccounts', { listId, accountIds }) - if (added.length > 0) { - rootState.api.backendInteractor.addAccountsToList({ listId, accountIds: added }) - } - if (removed.length > 0) { - rootState.api.backendInteractor.removeAccountsFromList({ listId, accountIds: removed }) - } - }, - addListAccount ({ rootState, commit }, { listId, accountId }) { - return rootState - .api - .backendInteractor - .addAccountsToList({ listId, accountIds: [accountId] }) - .then((result) => { - commit('addListAccount', { listId, accountId }) - return result - }) - }, - removeListAccount ({ rootState, commit }, { listId, accountId }) { - return rootState - .api - .backendInteractor - .removeAccountsFromList({ listId, accountIds: [accountId] }) - .then((result) => { - commit('removeListAccount', { listId, accountId }) - return result - }) - }, - deleteList ({ rootState, commit }, { listId }) { - rootState.api.backendInteractor.deleteList({ listId }) - commit('deleteList', { listId }) - } -} - -export const getters = { - findListTitle: state => id => { - if (!state.allListsObject[id]) return - return state.allListsObject[id].title - }, - findListAccounts: state => id => { - return [...state.allListsObject[id].accountIds] - } -} - -const lists = { - state: defaultState, - mutations, - actions, - getters -} - -export default lists diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js @@ -1,40 +0,0 @@ -import fileTypeService from '../services/file_type/file_type.service.js' -const supportedTypes = new Set(['image', 'video', 'audio', 'flash']) - -const mediaViewer = { - state: { - media: [], - currentIndex: 0, - activated: false - }, - mutations: { - setMedia (state, media) { - state.media = media - }, - setCurrentMedia (state, index) { - state.activated = true - state.currentIndex = index - }, - close (state) { - state.activated = false - } - }, - actions: { - setMedia ({ commit }, attachments) { - const media = attachments.filter(attachment => { - const type = fileTypeService.fileType(attachment.mimetype) - return supportedTypes.has(type) - }) - commit('setMedia', media) - }, - setCurrentMedia ({ commit, state }, current) { - const index = state.media.indexOf(current) - commit('setCurrentMedia', index || 0) - }, - closeMediaViewer ({ commit }) { - commit('close') - } - } -} - -export default mediaViewer diff --git a/src/modules/notifications.js b/src/modules/notifications.js @@ -11,6 +11,8 @@ import { closeAllDesktopNotifications } from '../services/desktop_notification_utils/desktop_notification_utils.js' +import { useReportsStore } from 'src/stores/reports.js' + const emptyNotifications = () => ({ desktopNotificationSilence: true, maxId: 0, @@ -94,7 +96,7 @@ export const notifications = { validNotifications.forEach(notification => { if (notification.type === 'pleroma:report') { - dispatch('addReport', notification.report) + useReportsStore().addReport(notification.report) } if (notification.type === 'pleroma:emoji_reaction') { diff --git a/src/modules/polls.js b/src/modules/polls.js @@ -1,69 +0,0 @@ -import { merge } from 'lodash' - -const polls = { - state: { - // Contains key = id, value = number of trackers for this poll - trackedPolls: {}, - pollsObject: {} - }, - mutations: { - mergeOrAddPoll (state, poll) { - const existingPoll = state.pollsObject[poll.id] - // Make expired-state change trigger re-renders properly - poll.expired = Date.now() > Date.parse(poll.expires_at) - if (existingPoll) { - state.pollsObject[poll.id] = merge(existingPoll, poll) - } else { - state.pollsObject[poll.id] = poll - } - }, - trackPoll (state, pollId) { - const currentValue = state.trackedPolls[pollId] - if (currentValue) { - state.trackedPolls[pollId] = currentValue + 1 - } else { - state.trackedPolls[pollId] = 1 - } - }, - untrackPoll (state, pollId) { - const currentValue = state.trackedPolls[pollId] - if (currentValue) { - state.trackedPolls[pollId] = currentValue - 1 - } else { - state.trackedPolls[pollId] = 0 - } - } - }, - actions: { - mergeOrAddPoll ({ commit }, poll) { - commit('mergeOrAddPoll', poll) - }, - updateTrackedPoll ({ rootState, dispatch, commit }, pollId) { - rootState.api.backendInteractor.fetchPoll({ pollId }).then(poll => { - setTimeout(() => { - if (rootState.polls.trackedPolls[pollId]) { - dispatch('updateTrackedPoll', pollId) - } - }, 30 * 1000) - commit('mergeOrAddPoll', poll) - }) - }, - trackPoll ({ rootState, commit, dispatch }, pollId) { - if (!rootState.polls.trackedPolls[pollId]) { - setTimeout(() => dispatch('updateTrackedPoll', pollId), 30 * 1000) - } - commit('trackPoll', pollId) - }, - untrackPoll ({ commit }, pollId) { - commit('untrackPoll', pollId) - }, - votePoll ({ rootState, commit }, { id, pollId, choices }) { - return rootState.api.backendInteractor.vote({ pollId, choices }).then(poll => { - commit('mergeOrAddPoll', poll) - return poll - }) - } - } -} - -export default polls diff --git a/src/modules/postStatus.js b/src/modules/postStatus.js @@ -1,31 +0,0 @@ -const postStatus = { - state: { - params: null, - modalActivated: false - }, - mutations: { - openPostStatusModal (state, params) { - state.params = params - state.modalActivated = true - }, - closePostStatusModal (state) { - state.modalActivated = false - }, - resetPostStatusModal (state) { - state.params = null - } - }, - actions: { - openPostStatusModal ({ commit }, params) { - commit('openPostStatusModal', params) - }, - closePostStatusModal ({ commit }) { - commit('closePostStatusModal') - }, - resetPostStatusModal ({ commit }) { - commit('resetPostStatusModal') - } - } -} - -export default postStatus diff --git a/src/modules/reports.js b/src/modules/reports.js @@ -1,64 +0,0 @@ -import filter from 'lodash/filter' - -const reports = { - state: { - reportModal: { - userId: null, - statuses: [], - preTickedIds: [], - activated: false - }, - reports: {} - }, - mutations: { - openUserReportingModal (state, { userId, statuses, preTickedIds }) { - state.reportModal.userId = userId - state.reportModal.statuses = statuses - state.reportModal.preTickedIds = preTickedIds - state.reportModal.activated = true - }, - closeUserReportingModal (state) { - state.reportModal.activated = false - }, - setReportState (reportsState, { id, state }) { - reportsState.reports[id].state = state - }, - addReport (state, report) { - state.reports[report.id] = report - } - }, - actions: { - openUserReportingModal ({ rootState, commit }, { userId, statusIds = [] }) { - const preTickedStatuses = statusIds.map(id => rootState.statuses.allStatusesObject[id]) - const preTickedIds = statusIds - const statuses = preTickedStatuses.concat( - filter(rootState.statuses.allStatuses, - status => status.user.id === userId && !preTickedIds.includes(status.id) - ) - ) - commit('openUserReportingModal', { userId, statuses, preTickedIds }) - }, - closeUserReportingModal ({ commit }) { - commit('closeUserReportingModal') - }, - setReportState ({ commit, dispatch, rootState }, { id, state }) { - const oldState = rootState.reports.reports[id].state - commit('setReportState', { id, state }) - rootState.api.backendInteractor.setReportState({ id, state }).catch(e => { - console.error('Failed to set report state', e) - dispatch('pushGlobalNotice', { - level: 'error', - messageKey: 'general.generic_error_message', - messageArgs: [e.message], - timeout: 5000 - }) - commit('setReportState', { id, state: oldState }) - }) - }, - addReport ({ commit }, report) { - commit('addReport', report) - } - } -} - -export default reports diff --git a/src/modules/shout.js b/src/modules/shout.js @@ -1,46 +0,0 @@ -const shout = { - state: { - messages: [], - channel: { state: '' }, - joined: false - }, - mutations: { - setChannel (state, channel) { - state.channel = channel - }, - addMessage (state, message) { - state.messages.push(message) - state.messages = state.messages.slice(-19, 20) - }, - setMessages (state, messages) { - state.messages = messages.slice(-19, 20) - }, - setJoined (state, joined) { - state.joined = joined - } - }, - actions: { - initializeShout (store, socket) { - const channel = socket.channel('chat:public') - channel.joinPush.receive('ok', () => { - store.commit('setJoined', true) - }) - channel.onClose(() => { - store.commit('setJoined', false) - }) - channel.onError(() => { - store.commit('setJoined', false) - }) - channel.on('new_msg', (msg) => { - store.commit('addMessage', msg) - }) - channel.on('messages', ({ messages }) => { - store.commit('setMessages', messages) - }) - channel.join() - store.commit('setChannel', channel) - } - } -} - -export default shout diff --git a/src/modules/statusHistory.js b/src/modules/statusHistory.js @@ -1,25 +0,0 @@ -const statusHistory = { - state: { - params: {}, - modalActivated: false - }, - mutations: { - openStatusHistoryModal (state, params) { - state.params = params - state.modalActivated = true - }, - closeStatusHistoryModal (state) { - state.modalActivated = false - } - }, - actions: { - openStatusHistoryModal ({ commit }, params) { - commit('openStatusHistoryModal', params) - }, - closeStatusHistoryModal ({ commit }) { - commit('closeStatusHistoryModal') - } - } -} - -export default statusHistory diff --git a/src/modules/statuses.js b/src/modules/statuses.js @@ -13,6 +13,7 @@ import { omitBy } from 'lodash' import apiService from '../services/api/api.service.js' +import { useInterfaceStore } from 'src/stores/interface' const emptyTl = (userId = 0) => ({ statuses: [], @@ -510,7 +511,7 @@ const statuses = { commit('setDeleted', { status }) }) .catch((e) => { - dispatch('pushGlobalNotice', { + useInterfaceStore().pushGlobalNotice({ level: 'error', messageKey: 'status.delete_error', messageArgs: [e.message], diff --git 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/sw/sw.js' +import { useInterfaceStore } from 'src/stores/interface.js' // TODO: Unify with mergeOrAdd in statuses.js export const mergeOrAdd = (arr, obj, item) => { @@ -584,9 +585,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') }) }, @@ -611,7 +612,7 @@ const users = { dispatch('fetchEmoji') getNotificationPermission() - .then(permission => commit('setNotificationPermission', permission)) + .then(permission => useInterfaceStore().setNotificationPermission(permission)) // Set our new backend interactor commit('setBackendInteractor', backendInteractorService(accessToken)) @@ -658,8 +659,8 @@ const users = { // Get user mutes dispatch('fetchMutes') - dispatch('setLayoutWidth', windowWidth()) - 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/lists_fetcher/lists_fetcher.service.js b/src/services/lists_fetcher/lists_fetcher.service.js @@ -1,10 +1,11 @@ +import { useListsStore } from 'src/stores/lists.js' import apiService from '../api/api.service.js' import { promiseInterval } from '../promise_interval/promise_interval.js' const fetchAndUpdate = ({ store, credentials }) => { return apiService.fetchLists({ credentials }) .then(lists => { - store.commit('setLists', lists) + useListsStore().setLists(lists) }, () => {}) .catch(() => {}) } diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js @@ -1,5 +1,7 @@ import { muteWordHits } from '../status_parser/status_parser.js' import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js' +import { useI18nStore } from 'src/stores/i18n.js' +import { useAnnouncementsStore } from 'src/stores/announcements' import FaviconService from 'src/services/favicon_service/favicon_service.js' @@ -64,13 +66,12 @@ const isMutedNotification = (store, notification) => { export const maybeShowNotification = (store, notification) => { const rootState = store.rootState || store.state - const rootGetters = store.rootGetters || store.getters if (notification.seen) return if (!visibleTypes(store).includes(notification.type)) return if (notification.type === 'mention' && isMutedNotification(store, notification)) return - const notificationObject = prepareNotificationObject(notification, rootGetters.i18n) + const notificationObject = prepareNotificationObject(notification, useI18nStore().i18n) showDesktopNotification(rootState, notificationObject) } @@ -169,7 +170,7 @@ export const countExtraNotifications = (store) => { return [ mergedConfig.showChatsInExtraNotifications ? rootGetters.unreadChatCount : 0, - mergedConfig.showAnnouncementsInExtraNotifications ? rootGetters.unreadAnnouncementCount : 0, + mergedConfig.showAnnouncementsInExtraNotifications ? useAnnouncementsStore().unreadAnnouncementCount : 0, mergedConfig.showFollowRequestsInExtraNotifications ? rootGetters.followRequestCount : 0 ].reduce((a, c) => a + c, 0) } diff --git 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 'src/stores/interface.js' import apiService from '../api/api.service.js' import { promiseInterval } from '../promise_interval/promise_interval.js' @@ -78,7 +79,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/style_setter/style_setter.js b/src/services/style_setter/style_setter.js @@ -1,10 +1,12 @@ import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js' import { getCssRules } from '../theme_data/css_utils.js' -import { defaultState } from '../../modules/config.js' +import { defaultState } from 'src/modules/default_config_state.js' import { chunk } from 'lodash' import pako from 'pako' import localforage from 'localforage' +console.log('CONFIG', defaultState) + // On platforms where this is not supported, it will return undefined // Otherwise it will return an array const supportsAdoptedStyleSheets = !!document.adoptedStyleSheets @@ -212,6 +214,7 @@ const extractStyleConfig = ({ return result } +console.log(defaultState) const defaultStyleConfig = extractStyleConfig(defaultState) export const applyConfig = (input, i18n) => { diff --git 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 'src/stores/interface.js' const update = ({ store, statuses, timeline, showImmediately, userId, listId, pagination }) => { const ccTimeline = camelCase(timeline) @@ -73,7 +74,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/announcements.js b/src/stores/announcements.js @@ -0,0 +1,115 @@ +import { defineStore } from 'pinia' + +const FETCH_ANNOUNCEMENT_INTERVAL_MS = 1000 * 60 * 5 + +export const useAnnouncementsStore = defineStore('announcements', { + state: () => ({ + announcements: [], + supportsAnnouncements: true, + fetchAnnouncementsTimer: undefined + }), + getters: { + unreadAnnouncementCount () { + if (!window.vuex.state.users.currentUser) { + return 0 + } + + const unread = this.announcements.filter(announcement => !(announcement.inactive || announcement.read)) + return unread.length + } + }, + actions: { + fetchAnnouncements () { + if (!this.supportsAnnouncements) { + return Promise.resolve() + } + + const currentUser = window.vuex.state.users.currentUser + const isAdmin = currentUser && currentUser.privileges.includes('announcements_manage_announcements') + + const getAnnouncements = async () => { + if (!isAdmin) { + return window.vuex.state.api.backendInteractor.fetchAnnouncements() + } + + const all = await window.vuex.state.api.backendInteractor.adminFetchAnnouncements() + const visible = await window.vuex.state.api.backendInteractor.fetchAnnouncements() + const visibleObject = visible.reduce((a, c) => { + a[c.id] = c + return a + }, {}) + const getWithinVisible = announcement => visibleObject[announcement.id] + + all.forEach(announcement => { + const visibleAnnouncement = getWithinVisible(announcement) + if (!visibleAnnouncement) { + announcement.inactive = true + } else { + announcement.read = visibleAnnouncement.read + } + }) + + return all + } + + return getAnnouncements() + .then(announcements => { + this.announcements = announcements + }) + .catch(error => { + // If and only if backend does not support announcements, it would return 404. + // In this case, silently ignores it. + if (error && error.statusCode === 404) { + this.supportsAnnouncements = false + } else { + throw error + } + }) + }, + markAnnouncementAsRead (id) { + return window.vuex.state.api.backendInteractor.dismissAnnouncement({ id }) + .then(() => { + const index = this.announcements.findIndex(a => a.id === id) + + if (index < 0) { + return + } + + this.announcements[index].read = true + }) + }, + startFetchingAnnouncements () { + if (this.fetchAnnouncementsTimer) { + return + } + + const interval = setInterval(() => this.fetchAnnouncements(), FETCH_ANNOUNCEMENT_INTERVAL_MS) + this.fetchAnnouncementsTimer = interval + + return this.fetchAnnouncements() + }, + stopFetchingAnnouncements () { + const interval = this.fetchAnnouncementsTimer + this.fetchAnnouncementsTimer = undefined + clearInterval(interval) + }, + postAnnouncement ({ content, startsAt, endsAt, allDay }) { + return window.vuex.state.api.backendInteractor.postAnnouncement({ content, startsAt, endsAt, allDay }) + .then(() => { + return this.fetchAnnouncements() + }) + }, + editAnnouncement ({ id, content, startsAt, endsAt, allDay }) { + return window.vuex.state.api.backendInteractor.editAnnouncement({ id, content, startsAt, endsAt, allDay }) + .then(() => { + return this.fetchAnnouncements() + }) + }, + deleteAnnouncement (id) { + return window.vuex.state.api.backendInteractor.deleteAnnouncement({ id }) + .then(() => { + return this.fetchAnnouncements() + }) + } + } +}) diff --git a/src/stores/editStatus.js b/src/stores/editStatus.js @@ -0,0 +1,17 @@ +import { defineStore } from 'pinia' + +export const useEditStatusStore = defineStore('editStatus', { + state: () => ({ + params: null, + modalActivated: false + }), + actions: { + openEditStatusModal (params) { + this.params = params + this.modalActivated = true + }, + closeEditStatusModal () { + this.modalActivated = false + } + } +}) diff --git a/src/stores/i18n.js b/src/stores/i18n.js @@ -0,0 +1,14 @@ +import { defineStore } from 'pinia' + +export const useI18nStore = defineStore('i18n', { + state: () => ({ + i18n: null + }), + actions: { + setI18n (newI18n) { + this.$patch({ + i18n: newI18n.global + }) + } + } +}) diff --git a/src/stores/interface.js b/src/stores/interface.js @@ -0,0 +1,674 @@ +import { defineStore } from 'pinia' + +import { CURRENT_VERSION, generatePreset } from 'src/services/theme_data/theme_data.service.js' +import { getResourcesIndex, applyTheme, tryLoadCache } from '../services/style_setter/style_setter.js' +import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js' +import { deserialize } from '../services/theme_data/iss_deserializer.js' + +export const useInterfaceStore = defineStore('interface', { + state: () => ({ + localFonts: null, + themeApplied: false, + themeChangeInProgress: false, + themeVersion: 'v3', + styleNameUsed: null, + styleDataUsed: null, + useStylePalette: false, // hack for applying styles from appearance tab + paletteNameUsed: null, + paletteDataUsed: null, + themeNameUsed: null, + themeDataUsed: null, + temporaryChangesTimeoutId: null, // used for temporary options that revert after a timeout + temporaryChangesConfirm: () => {}, // used for applying temporary options + temporaryChangesRevert: () => {}, // used for reverting temporary options + settingsModalState: 'hidden', + settingsModalLoadedUser: false, + settingsModalLoadedAdmin: false, + settingsModalTargetTab: null, + settingsModalMode: 'user', + 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)') + ), + localFonts: typeof window.queryLocalFonts === 'function' + }, + 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 (value) { + this.settingsModalMode = value + this.settingsModalState = 'visible' + if (value === 'user') { + if (!this.settingsModalLoadedUser) { + this.settingsModalLoadedUser = true + } + } else if (value === 'admin') { + if (!this.settingsModalLoadedAdmin) { + this.settingsModalLoadedAdmin = 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, mode = 'user') { + this.settingsModalTargetTab = value + this.openSettingsModal(mode) + }, + 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 + } + }, + setFontsList (value) { + this.localFonts = [...(new Set(value.map(font => font.family))).values()] + }, + queryLocalFonts () { + if (this.localFonts !== null) return + this.setFontsList([]) + + if (!this.browserSupport.localFonts) { + return + } + window + .queryLocalFonts() + .then((fonts) => { + this.setFontsList(fonts) + }) + .catch((e) => { + this.pushGlobalNotice({ + messageKey: 'settings.style.themes3.font.font_list_unavailable', + messageArgs: { + error: e + }, + level: 'error' + }) + }) + }, + setLastTimeline (value) { + this.lastTimeline = value + }, + async fetchPalettesIndex () { + try { + const value = await getResourcesIndex('/static/palettes/index.json') + window.vuex.commit('setInstanceOption', { name: 'palettesIndex', value }) + return value + } catch (e) { + console.error('Could not fetch palettes index', e) + window.vuex.commit('setInstanceOption', { name: 'palettesIndex', value: { _error: e } }) + return Promise.resolve({}) + } + }, + setPalette (value) { + this.resetThemeV3Palette() + this.resetThemeV2() + + window.vuex.commit('setOption', { name: 'palette', value }) + + this.applyTheme({ recompile: true }) + }, + setPaletteCustom (value) { + this.resetThemeV3Palette() + this.resetThemeV2() + + window.vuex.commit('setOption', { name: 'paletteCustomData', value }) + + this.applyTheme({ recompile: true }) + }, + async fetchStylesIndex () { + try { + const value = await getResourcesIndex( + '/static/styles/index.json', + deserialize + ) + window.vuex.commit('setInstanceOption', { name: 'stylesIndex', value }) + return value + } catch (e) { + console.error('Could not fetch styles index', e) + window.vuex.commit('setInstanceOption', { name: 'stylesIndex', value: { _error: e } }) + return Promise.resolve({}) + } + }, + setStyle (value) { + this.resetThemeV3() + this.resetThemeV2() + this.resetThemeV3Palette() + + window.vuex.commit('setOption', { name: 'style', value }) + this.useStylePalette = true + + this.applyTheme({ recompile: true }).then(() => { + this.useStylePalette = false + }) + }, + setStyleCustom (value) { + this.resetThemeV3() + this.resetThemeV2() + this.resetThemeV3Palette() + + window.vuex.commit('setOption', { name: 'styleCustomData', value }) + + this.useStylePalette = true + this.applyTheme({ recompile: true }).then(() => { + this.useStylePalette = false + }) + }, + async fetchThemesIndex () { + try { + const value = await getResourcesIndex('/static/styles.json') + window.vuex.commit('setInstanceOption', { name: 'themesIndex', value }) + return value + } catch (e) { + console.error('Could not fetch themes index', e) + window.vuex.commit('setInstanceOption', { name: 'themesIndex', value: { _error: e } }) + return Promise.resolve({}) + } + }, + setTheme (value) { + this.resetThemeV3() + this.resetThemeV3Palette() + this.resetThemeV2() + + window.vuex.commit('setOption', { name: 'theme', value }) + + this.applyTheme({ recompile: true }) + }, + setThemeCustom (value) { + this.resetThemeV3() + this.resetThemeV3Palette() + this.resetThemeV2() + + window.vuex.commit('setOption', { name: 'customTheme', value }) + window.vuex.commit('setOption', { name: 'customThemeSource', value }) + + this.applyTheme({ recompile: true }) + }, + resetThemeV3 () { + window.vuex.commit('setOption', { name: 'style', value: null }) + window.vuex.commit('setOption', { name: 'styleCustomData', value: null }) + }, + resetThemeV3Palette () { + window.vuex.commit('setOption', { name: 'palette', value: null }) + window.vuex.commit('setOption', { name: 'paletteCustomData', value: null }) + }, + resetThemeV2 () { + window.vuex.commit('setOption', { name: 'theme', value: null }) + window.vuex.commit('setOption', { name: 'customTheme', value: null }) + window.vuex.commit('setOption', { name: 'customThemeSource', value: null }) + }, + async getThemeData () { + const getData = async (resource, index, customData, name) => { + const capitalizedResource = resource[0].toUpperCase() + resource.slice(1) + const result = {} + + if (customData) { + result.nameUsed = 'custom' // custom data overrides name + result.dataUsed = customData + } else { + result.nameUsed = name + + if (result.nameUsed == null) { + result.dataUsed = null + return result + } + + let fetchFunc = index[result.nameUsed] + // Fallbacks + if (!fetchFunc) { + if (resource === 'style' || resource === 'palette') { + return result + } + const newName = Object.keys(index)[0] + fetchFunc = index[newName] + console.warn(`${capitalizedResource} with id '${this.styleNameUsed}' not found, trying back to '${newName}'`) + if (!fetchFunc) { + console.warn(`${capitalizedResource} doesn't have a fallback, defaulting to stock.`) + fetchFunc = () => Promise.resolve(null) + } + } + result.dataUsed = await fetchFunc() + } + return result + } + + const { + style: instanceStyleName, + palette: instancePaletteName + } = window.vuex.state.instance + + let { + theme: instanceThemeV2Name, + themesIndex, + stylesIndex, + palettesIndex + } = window.vuex.state.instance + + const { + style: userStyleName, + styleCustomData: userStyleCustomData, + palette: userPaletteName, + paletteCustomData: userPaletteCustomData + } = window.vuex.state.config + + let { + theme: userThemeV2Name, + customTheme: userThemeV2Snapshot, + customThemeSource: userThemeV2Source + } = window.vuex.state.config + + let majorVersionUsed + + console.debug( + `User V3 palette: ${userPaletteName}, style: ${userStyleName} , custom: ${!!userStyleCustomData}` + ) + console.debug( + `User V2 name: ${userThemeV2Name}, source: ${!!userThemeV2Source}, snapshot: ${!!userThemeV2Snapshot}` + ) + + console.debug(`Instance V3 palette: ${instancePaletteName}, style: ${instanceStyleName}`) + console.debug('Instance V2 theme: ' + instanceThemeV2Name) + + if (userPaletteName || userPaletteCustomData || + userStyleName || userStyleCustomData || + ( + // User V2 overrides instance V3 + (instancePaletteName || + instanceStyleName) && + instanceThemeV2Name == null && + userThemeV2Name == null + ) + ) { + // Palette and/or style overrides V2 themes + instanceThemeV2Name = null + userThemeV2Name = null + userThemeV2Source = null + userThemeV2Snapshot = null + + majorVersionUsed = 'v3' + } else if ( + (userThemeV2Name || + userThemeV2Snapshot || + userThemeV2Source || + instanceThemeV2Name) + ) { + majorVersionUsed = 'v2' + } else { + // if all fails fallback to v3 + majorVersionUsed = 'v3' + } + + if (majorVersionUsed === 'v3') { + const result = await Promise.all([ + this.fetchPalettesIndex(), + this.fetchStylesIndex() + ]) + + palettesIndex = result[0] + stylesIndex = result[1] + } else { + // Promise.all just to be uniform with v3 + const result = await Promise.all([ + this.fetchThemesIndex() + ]) + + themesIndex = result[0] + } + + this.themeVersion = majorVersionUsed + + console.debug('Version used', majorVersionUsed) + + if (majorVersionUsed === 'v3') { + this.themeDataUsed = null + this.themeNameUsed = null + + const style = await getData( + 'style', + stylesIndex, + userStyleCustomData, + userStyleName || instanceStyleName + ) + this.styleNameUsed = style.nameUsed + this.styleDataUsed = style.dataUsed + + let firstStylePaletteName = null + style + .dataUsed + ?.filter(x => x.component === '@palette') + .map(x => { + const cleanDirectives = Object.fromEntries( + Object + .entries(x.directives) + .filter(([k, v]) => k) + ) + + return { name: x.variant, ...cleanDirectives } + }) + .forEach(palette => { + const key = 'style.' + palette.name.toLowerCase().replace(/ /g, '_') + if (!firstStylePaletteName) firstStylePaletteName = key + palettesIndex[key] = () => Promise.resolve(palette) + }) + + const palette = await getData( + 'palette', + palettesIndex, + userPaletteCustomData, + this.useStylePalette ? firstStylePaletteName : (userPaletteName || instancePaletteName) + ) + + if (this.useStylePalette) { + window.vuex.commit('setOption', { name: 'palette', value: firstStylePaletteName }) + } + + this.paletteNameUsed = palette.nameUsed + this.paletteDataUsed = palette.dataUsed + + if (this.paletteDataUsed) { + this.paletteDataUsed.link = this.paletteDataUsed.link || this.paletteDataUsed.accent + this.paletteDataUsed.accent = this.paletteDataUsed.accent || this.paletteDataUsed.link + } + if (Array.isArray(this.paletteDataUsed)) { + const [ + name, + bg, + fg, + text, + link, + cRed = '#FF0000', + cGreen = '#00FF00', + cBlue = '#0000FF', + cOrange = '#E3FF00' + ] = palette.dataUsed + this.paletteDataUsed = { + name, + bg, + fg, + text, + link, + accent: link, + cRed, + cBlue, + cGreen, + cOrange + } + } + console.debug('Palette data used', palette.dataUsed) + } else { + this.styleNameUsed = null + this.styleDataUsed = null + this.paletteNameUsed = null + this.paletteDataUsed = null + + const theme = await getData( + 'theme', + themesIndex, + userThemeV2Source || userThemeV2Snapshot, + userThemeV2Name || instanceThemeV2Name + ) + this.themeNameUsed = theme.nameUsed + this.themeDataUsed = theme.dataUsed + } + }, + async setThemeApplied () { + this.themeApplied = true + }, + async applyTheme ( + { recompile = false } = {} + ) { + const { + forceThemeRecompilation, + themeDebug, + theme3hacks + } = window.vuex.state.config + this.themeChangeInProgress = true + // If we're not not forced to recompile try using + // cache (tryLoadCache return true if load successful) + + const forceRecompile = forceThemeRecompilation || recompile + if (!forceRecompile && !themeDebug && await tryLoadCache()) { + this.themeChangeInProgress = false + return this.setThemeApplied() + } + window.splashUpdate('splash.theme') + await this.getThemeData() + + try { + const paletteIss = (() => { + if (!this.paletteDataUsed) return null + const result = { + component: 'Root', + directives: {} + } + + Object + .entries(this.paletteDataUsed) + .filter(([k]) => k !== 'name') + .forEach(([k, v]) => { + let issRootDirectiveName + switch (k) { + case 'background': + issRootDirectiveName = 'bg' + break + case 'foreground': + issRootDirectiveName = 'fg' + break + default: + issRootDirectiveName = k + } + result.directives['--' + issRootDirectiveName] = 'color | ' + v + }) + return result + })() + + const theme2ruleset = this.themeDataUsed && convertTheme2To3(normalizeThemeData(this.themeDataUsed)) + const hacks = [] + + Object.entries(theme3hacks).forEach(([key, value]) => { + switch (key) { + case 'fonts': { + Object.entries(theme3hacks.fonts).forEach(([fontKey, font]) => { + if (!font?.family) return + switch (fontKey) { + case 'interface': + hacks.push({ + component: 'Root', + directives: { + '--font': 'generic | ' + font.family + } + }) + break + case 'input': + hacks.push({ + component: 'Input', + directives: { + '--font': 'generic | ' + font.family + } + }) + break + case 'post': + hacks.push({ + component: 'RichContent', + directives: { + '--font': 'generic | ' + font.family + } + }) + break + case 'monospace': + hacks.push({ + component: 'Root', + directives: { + '--monoFont': 'generic | ' + font.family + } + }) + break + } + }) + break + } + case 'underlay': { + if (value !== 'none') { + const newRule = { + component: 'Underlay', + directives: {} + } + if (value === 'opaque') { + newRule.directives.opacity = 1 + newRule.directives.background = '--wallpaper' + } + if (value === 'transparent') { + newRule.directives.opacity = 0 + } + hacks.push(newRule) + } + break + } + } + }) + + const rulesetArray = [ + theme2ruleset, + this.styleDataUsed, + paletteIss, + hacks + ].filter(x => x) + + return applyTheme( + rulesetArray.flat(), + () => this.setThemeApplied(), + () => { + this.themeChangeInProgress = false + }, + themeDebug + ) + } catch (e) { + window.splashError(e) + } + } + } +}) + +export const normalizeThemeData = (input) => { + let themeData, themeSource + + if (input.themeFileVerison === 1) { + // this might not be even used at all, some leftover of unimplemented code in V2 editor + return generatePreset(input).theme + } else if ( + Object.prototype.hasOwnProperty.call(input, '_pleroma_theme_version') || + Object.prototype.hasOwnProperty.call(input, 'source') || + Object.prototype.hasOwnProperty.call(input, 'theme') + ) { + // We got passed a full theme file + themeData = input.theme + themeSource = input.source + } else if ( + Object.prototype.hasOwnProperty.call(input, 'themeEngineVersion') || + Object.prototype.hasOwnProperty.call(input, 'colors') + ) { + // We got passed a source/snapshot + themeData = input + themeSource = input + } + // New theme presets don't have 'theme' property, they use 'source' + + let out // shout, shout let it all out + if (themeSource && themeSource.themeEngineVersion === CURRENT_VERSION) { + // There are some themes in wild that have completely broken source + out = { ...(themeData || {}), ...themeSource } + } else { + out = themeData + } + + // generatePreset here basically creates/updates "snapshot", + // while also fixing the 2.2 -> 2.3 colors/shadows/etc + return generatePreset(out).theme +} diff --git a/src/stores/lists.js b/src/stores/lists.js @@ -0,0 +1,110 @@ +import { defineStore } from 'pinia' + +import { remove, find } from 'lodash' + +export const useListsStore = defineStore('lists', { + state: () => ({ + allLists: [], + allListsObject: {} + }), + getters: { + findListTitle (state) { + return (id) => { + if (!this.allListsObject[id]) return + return this.allListsObject[id].title + } + }, + findListAccounts (state) { + return (id) => [...this.allListsObject[id].accountIds] + } + }, + actions: { + setLists (value) { + this.allLists = value + }, + createList ({ title }) { + return window.vuex.state.api.backendInteractor.createList({ title }) + .then((list) => { + this.setList({ listId: list.id, title }) + return list + }) + }, + fetchList ({ listId }) { + return window.vuex.state.api.backendInteractor.getList({ listId }) + .then((list) => this.setList({ listId: list.id, title: list.title })) + }, + fetchListAccounts ({ listId }) { + return window.vuex.state.api.backendInteractor.getListAccounts({ listId }) + .then((accountIds) => { + if (!this.allListsObject[listId]) { + this.allListsObject[listId] = { accountIds: [] } + } + this.allListsObject[listId].accountIds = accountIds + }) + }, + setList ({ listId, title }) { + if (!this.allListsObject[listId]) { + this.allListsObject[listId] = { accountIds: [] } + } + this.allListsObject[listId].title = title + + const entry = find(this.allLists, { id: listId }) + if (!entry) { + this.allLists.push({ id: listId, title }) + } else { + entry.title = title + } + }, + setListAccounts ({ listId, accountIds }) { + const saved = this.allListsObject[listId]?.accountIds || [] + const added = accountIds.filter(id => !saved.includes(id)) + const removed = saved.filter(id => !accountIds.includes(id)) + if (!this.allListsObject[listId]) { + this.allListsObject[listId] = { accountIds: [] } + } + this.allListsObject[listId].accountIds = accountIds + if (added.length > 0) { + window.vuex.state.api.backendInteractor.addAccountsToList({ listId, accountIds: added }) + } + if (removed.length > 0) { + window.vuex.state.api.backendInteractor.removeAccountsFromList({ listId, accountIds: removed }) + } + }, + addListAccount ({ listId, accountId }) { + return window.vuex.state + .api + .backendInteractor + .addAccountsToList({ listId, accountIds: [accountId] }) + .then((result) => { + if (!this.allListsObject[listId]) { + this.allListsObject[listId] = { accountIds: [] } + } + this.allListsObject[listId].accountIds.push(accountId) + return result + }) + }, + removeListAccount ({ listId, accountId }) { + return window.vuex.state + .api + .backendInteractor + .removeAccountsFromList({ listId, accountIds: [accountId] }) + .then((result) => { + if (!this.allListsObject[listId]) { + this.allListsObject[listId] = { accountIds: [] } + } + const { accountIds } = this.allListsObject[listId] + const set = new Set(accountIds) + set.delete(accountId) + this.allListsObject[listId].accountIds = [...set] + + return result + }) + }, + deleteList ({ listId }) { + window.vuex.state.api.backendInteractor.deleteList({ listId }) + + delete this.allListsObject[listId] + remove(this.allLists, list => list.id === listId) + } + } +}) diff --git a/src/stores/media_viewer.js b/src/stores/media_viewer.js @@ -0,0 +1,30 @@ +import { defineStore } from 'pinia' +import fileTypeService from '../services/file_type/file_type.service.js' + +const supportedTypes = new Set(['image', 'video', 'audio', 'flash']) + +export const useMediaViewerStore = defineStore('mediaViewer', { + state: () => ({ + media: [], + currentIndex: 0, + activated: false + }), + actions: { + setMedia (attachments) { + const media = attachments.filter(attachment => { + const type = fileTypeService.fileType(attachment.mimetype) + return supportedTypes.has(type) + }) + + this.media = media + }, + setCurrentMedia (current) { + const index = this.media.indexOf(current) + this.activated = true + this.currentIndex = index + }, + closeMediaViewer () { + this.activated = false + } + } +}) diff --git a/src/stores/polls.js b/src/stores/polls.js @@ -0,0 +1,57 @@ +import { merge } from 'lodash' +import { defineStore } from 'pinia' + +export const usePollsStore = defineStore('polls', { + state: () => ({ + // Contains key = id, value = number of trackers for this poll + trackedPolls: {}, + pollsObject: {} + }), + actions: { + mergeOrAddPoll (poll) { + const existingPoll = this.pollsObject[poll.id] + // Make expired-state change trigger re-renders properly + poll.expired = Date.now() > Date.parse(poll.expires_at) + if (existingPoll) { + this.pollsObject[poll.id] = merge(existingPoll, poll) + } else { + this.pollsObject[poll.id] = poll + } + }, + updateTrackedPoll (pollId) { + window.vuex.state.api.backendInteractor.fetchPoll({ pollId }).then(poll => { + setTimeout(() => { + if (this.trackedPolls[pollId]) { + this.updateTrackedPoll(pollId) + } + }, 30 * 1000) + this.mergeOrAddPoll(poll) + }) + }, + trackPoll (pollId) { + if (!this.trackedPolls[pollId]) { + setTimeout(() => this.updateTrackedPoll(pollId), 30 * 1000) + } + const currentValue = this.trackedPolls[pollId] + if (currentValue) { + this.trackedPolls[pollId] = currentValue + 1 + } else { + this.trackedPolls[pollId] = 1 + } + }, + untrackPoll (pollId) { + const currentValue = this.trackedPolls[pollId] + if (currentValue) { + this.trackedPolls[pollId] = currentValue - 1 + } else { + this.trackedPolls[pollId] = 0 + } + }, + votePoll ({ id, pollId, choices }) { + return window.vuex.state.api.backendInteractor.vote({ pollId, choices }).then(poll => { + this.mergeOrAddPoll(poll) + return poll + }) + } + } +}) diff --git a/src/stores/postStatus.js b/src/stores/postStatus.js @@ -0,0 +1,17 @@ +import { defineStore } from 'pinia' + +export const usePostStatusStore = defineStore('postStatus', { + state: () => ({ + params: null, + modalActivated: false + }), + actions: { + openPostStatusModal (params) { + this.params = params + this.modalActivated = true + }, + closePostStatusModal () { + this.modalActivated = false + } + } +}) diff --git a/src/stores/reports.js b/src/stores/reports.js @@ -0,0 +1,52 @@ +import { defineStore } from 'pinia' + +import filter from 'lodash/filter' +import { useInterfaceStore } from 'src/stores/interface' + +export const useReportsStore = defineStore('reports', { + state: () => ({ + reportModal: { + userId: null, + statuses: [], + preTickedIds: [], + activated: false + }, + reports: {} + }), + actions: { + openUserReportingModal ({ userId, statusIds = [] }) { + const preTickedStatuses = statusIds.map(id => window.vuex.state.statuses.allStatusesObject[id]) + const preTickedIds = statusIds + const statuses = preTickedStatuses.concat( + filter(window.vuex.state.statuses.allStatuses, + status => status.user.id === userId && !preTickedIds.includes(status.id) + ) + ) + + this.reportModal.userId = userId + this.reportModal.statuses = statuses + this.reportModal.preTickedIds = preTickedIds + this.reportModal.activated = true + }, + closeUserReportingModal () { + this.reportModal.activated = false + }, + setReportState ({ id, state }) { + const oldState = window.vuex.state.reports.reports[id].state + this.reports[id].state = state + window.vuex.state.api.backendInteractor.setReportState({ id, state }).catch(e => { + console.error('Failed to set report state', e) + useInterfaceStore().pushGlobalNotice({ + level: 'error', + messageKey: 'general.generic_error_message', + messageArgs: [e.message], + timeout: 5000 + }) + this.reports[id].state = oldState + }) + }, + addReport (report) { + this.reports[report.id] = report + } + } +}) diff --git a/src/stores/shout.js b/src/stores/shout.js @@ -0,0 +1,32 @@ +import { defineStore } from 'pinia' + +export const useShoutStore = defineStore('shout', { + state: () => ({ + messages: [], + channel: { state: '' }, + joined: false + }), + actions: { + initializeShout (socket) { + const channel = socket.channel('chat:public') + channel.joinPush.receive('ok', () => { + this.joined = true + }) + channel.onClose(() => { + this.joined = false + }) + channel.onError(() => { + this.joined = false + }) + channel.on('new_msg', (msg) => { + this.messages.push(msg) + this.messages = this.messages.slice(-19, 20) + }) + channel.on('messages', ({ messages }) => { + this.messages = messages.slice(-19, 20) + }) + channel.join() + this.channel = channel + } + } +}) diff --git a/src/stores/statusHistory.js b/src/stores/statusHistory.js @@ -0,0 +1,17 @@ +import { defineStore } from 'pinia' + +export const useStatusHistoryStore = defineStore('statusHistory', { + state: () => ({ + params: {}, + modalActivated: false + }), + actions: { + openStatusHistoryModal (params) { + this.params = params + this.modalActivated = true + }, + closeStatusHistoryModal () { + this.modalActivated = false + } + } +}) diff --git a/test/unit/specs/modules/lists.spec.js b/test/unit/specs/modules/lists.spec.js @@ -1,83 +0,0 @@ -import { cloneDeep } from 'lodash' -import { defaultState, mutations, getters } from '../../../../src/modules/lists.js' - -describe('The lists module', () => { - describe('mutations', () => { - it('updates array of all lists', () => { - const state = cloneDeep(defaultState) - const list = { id: '1', title: 'testList' } - - mutations.setLists(state, [list]) - expect(state.allLists).to.have.length(1) - expect(state.allLists).to.eql([list]) - }) - - it('adds a new list with a title, updating the title for existing lists', () => { - const state = cloneDeep(defaultState) - const list = { id: '1', title: 'testList' } - const modList = { id: '1', title: 'anotherTestTitle' } - - mutations.setList(state, { listId: list.id, title: list.title }) - expect(state.allListsObject[list.id]).to.eql({ title: list.title, accountIds: [] }) - expect(state.allLists).to.have.length(1) - expect(state.allLists[0]).to.eql(list) - - mutations.setList(state, { listId: modList.id, title: modList.title }) - expect(state.allListsObject[modList.id]).to.eql({ title: modList.title, accountIds: [] }) - expect(state.allLists).to.have.length(1) - expect(state.allLists[0]).to.eql(modList) - }) - - it('adds a new list with an array of IDs, updating the IDs for existing lists', () => { - const state = cloneDeep(defaultState) - const list = { id: '1', accountIds: ['1', '2', '3'] } - const modList = { id: '1', accountIds: ['3', '4', '5'] } - - mutations.setListAccounts(state, { listId: list.id, accountIds: list.accountIds }) - expect(state.allListsObject[list.id]).to.eql({ accountIds: list.accountIds }) - - mutations.setListAccounts(state, { listId: modList.id, accountIds: modList.accountIds }) - expect(state.allListsObject[modList.id]).to.eql({ accountIds: modList.accountIds }) - }) - - it('deletes a list', () => { - const state = { - allLists: [{ id: '1', title: 'testList' }], - allListsObject: { - 1: { title: 'testList', accountIds: ['1', '2', '3'] } - } - } - const listId = '1' - - mutations.deleteList(state, { listId }) - expect(state.allLists).to.have.length(0) - expect(state.allListsObject).to.eql({}) - }) - }) - - describe('getters', () => { - it('returns list title', () => { - const state = { - allLists: [{ id: '1', title: 'testList' }], - allListsObject: { - 1: { title: 'testList', accountIds: ['1', '2', '3'] } - } - } - const id = '1' - - expect(getters.findListTitle(state)(id)).to.eql('testList') - }) - - it('returns list accounts', () => { - const state = { - allLists: [{ id: '1', title: 'testList' }], - allListsObject: { - 1: { title: 'testList', accountIds: ['1', '2', '3'] } - } - } - const id = '1' - - expect(getters.findListAccounts(state)(id)).to.eql(['1', '2', '3']) - }) - }) -}) diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js @@ -74,7 +74,7 @@ describe('The serverSideStorage module', () => { }) }) - it.only('should reset local timestamp to remote if contents are the same', () => { + it('should reset local timestamp to remote if contents are the same', () => { const state = { ...cloneDeep(defaultState), cache: null diff --git a/test/unit/specs/stores/lists.spec.js b/test/unit/specs/stores/lists.spec.js @@ -0,0 +1,93 @@ +import { createPinia, setActivePinia } from 'pinia' +import { useListsStore } from 'src/stores/lists.js' +import { createStore } from 'vuex' +import apiModule from 'src/modules/api.js' + +setActivePinia(createPinia()) +const store = useListsStore() +window.vuex = createStore({ + modules: { + api: apiModule + } +}) + +describe('The lists store', () => { + describe('actions', () => { + it('updates array of all lists', () => { + store.$reset() + const list = { id: '1', title: 'testList' } + + store.setLists([list]) + expect(store.allLists).to.have.length(1) + expect(store.allLists).to.eql([list]) + }) + + it('adds a new list with a title, updating the title for existing lists', () => { + store.$reset() + const list = { id: '1', title: 'testList' } + const modList = { id: '1', title: 'anotherTestTitle' } + + store.setList({ listId: list.id, title: list.title }) + expect(store.allListsObject[list.id]).to.eql({ title: list.title, accountIds: [] }) + expect(store.allLists).to.have.length(1) + expect(store.allLists[0]).to.eql(list) + + store.setList({ listId: modList.id, title: modList.title }) + expect(store.allListsObject[modList.id]).to.eql({ title: modList.title, accountIds: [] }) + expect(store.allLists).to.have.length(1) + expect(store.allLists[0]).to.eql(modList) + }) + + it('adds a new list with an array of IDs, updating the IDs for existing lists', () => { + store.$reset() + const list = { id: '1', accountIds: ['1', '2', '3'] } + const modList = { id: '1', accountIds: ['3', '4', '5'] } + + store.setListAccounts({ listId: list.id, accountIds: list.accountIds }) + expect(store.allListsObject[list.id].accountIds).to.eql(list.accountIds) + + store.setListAccounts({ listId: modList.id, accountIds: modList.accountIds }) + expect(store.allListsObject[modList.id].accountIds).to.eql(modList.accountIds) + }) + + it('deletes a list', () => { + store.$patch({ + allLists: [{ id: '1', title: 'testList' }], + allListsObject: { + 1: { title: 'testList', accountIds: ['1', '2', '3'] } + } + }) + const listId = '1' + + store.deleteList({ listId }) + expect(store.allLists).to.have.length(0) + expect(store.allListsObject).to.eql({}) + }) + }) + + describe('getters', () => { + it('returns list title', () => { + store.$patch({ + allLists: [{ id: '1', title: 'testList' }], + allListsObject: { + 1: { title: 'testList', accountIds: ['1', '2', '3'] } + } + }) + const id = '1' + + expect(store.findListTitle(id)).to.eql('testList') + }) + + it('returns list accounts', () => { + store.$patch({ + allLists: [{ id: '1', title: 'testList' }], + allListsObject: { + 1: { title: 'testList', accountIds: ['1', '2', '3'] } + } + }) + const id = '1' + + expect(store.findListAccounts(id)).to.eql(['1', '2', '3']) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock @@ -1521,7 +1521,7 @@ "@vue/compiler-dom" "3.5.13" "@vue/shared" "3.5.13" -"@vue/devtools-api@^6.0.0-beta.11", "@vue/devtools-api@^6.5.0", "@vue/devtools-api@^6.6.4": +"@vue/devtools-api@^6.0.0-beta.11", "@vue/devtools-api@^6.5.0", "@vue/devtools-api@^6.6.3", "@vue/devtools-api@^6.6.4": version "6.6.4" resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343" integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g== @@ -6609,6 +6609,14 @@ pify@^4.0.1: resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== +pinia@^2.0.33: + version "2.3.1" + resolved "https://registry.yarnpkg.com/pinia/-/pinia-2.3.1.tgz#54c476675b72f5abcfafa24a7582531ea8c23d94" + integrity sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug== + dependencies: + "@vue/devtools-api" "^6.6.3" + vue-demi "^0.14.10" + pirates@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" @@ -8505,6 +8513,11 @@ vue-demi@^0.13.11: resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.13.11.tgz#7d90369bdae8974d87b1973564ad390182410d99" integrity sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A== +vue-demi@^0.14.10: + version "0.14.10" + resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04" + integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg== + vue-eslint-parser@^9.4.3: version "9.4.3" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz#9b04b22c71401f1e8bca9be7c3e3416a4bde76a8"