diff --git a/src/App.vue b/src/App.vue index e1c0f5dc..ca114c89 100644 --- a/src/App.vue +++ b/src/App.vue @@ -61,7 +61,6 @@ - diff --git a/src/boot/after_store.js b/src/boot/after_store.js index c12c70f1..9f0ff4d3 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -398,7 +398,6 @@ const afterStoreSetup = async ({ store, i18n }) => { store.dispatch('startFetchingAnnouncements') getTOS({ store }) getStickers({ store }) - store.dispatch('getSupportedTranslationlanguages') const router = createRouter({ history: createWebHistory(), diff --git a/src/components/settings_modal/helpers/choice_setting.js b/src/components/settings_modal/helpers/choice_setting.js index 4677d4c1..48cee348 100644 --- a/src/components/settings_modal/helpers/choice_setting.js +++ b/src/components/settings_modal/helpers/choice_setting.js @@ -12,7 +12,8 @@ export default { 'path', 'disabled', 'options', - 'expert' + 'expert', + 'hideDefaultLabel' ], computed: { pathDefault () { diff --git a/src/components/settings_modal/helpers/choice_setting.vue b/src/components/settings_modal/helpers/choice_setting.vue index 258c7422..8b1dd614 100644 --- a/src/components/settings_modal/helpers/choice_setting.vue +++ b/src/components/settings_modal/helpers/choice_setting.vue @@ -16,7 +16,11 @@ :value="option.value" > {{ option.label }} - {{ option.value === defaultState ? $t('settings.instance_default_simple') : '' }} + diff --git a/src/components/settings_modal/helpers/shared_computed_object.js b/src/components/settings_modal/helpers/shared_computed_object.js index 12431dca..3f40f6e5 100644 --- a/src/components/settings_modal/helpers/shared_computed_object.js +++ b/src/components/settings_modal/helpers/shared_computed_object.js @@ -19,7 +19,7 @@ const SharedComputedObject = () => ({ .map(key => [key, { get () { return this.$store.getters.mergedConfig[key] }, set (value) { - this.$store.dispatch('setOption', { name: key, value }) + this.$store.dispatch('setOption', { name: key, value, manual: true }) } }]) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), @@ -27,7 +27,7 @@ const SharedComputedObject = () => ({ .map(key => ['serverSide_' + key, { get () { return this.$store.state.serverSideConfig[key] }, set (value) { - this.$store.dispatch('setServerSideOption', { name: key, value }) + this.$store.dispatch('setServerSideOption', { name: key, value, manual: true }) } }]) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js index 9fe47eaf..935591dc 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -8,11 +8,12 @@ import SharedComputedObject from '../helpers/shared_computed_object.js' import ServerSideIndicator from '../helpers/server_side_indicator.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { - faGlobe + faGlobe, faSync } from '@fortawesome/free-solid-svg-icons' library.add( - faGlobe + faGlobe, + faSync ) const GeneralTab = { @@ -48,6 +49,8 @@ const GeneralTab = { value: tab, label: this.$t(`user_card.${tab}`) })), + profilesExpanded: false, + newProfileName: '', loopSilentAvailable: // Firefox Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || @@ -88,8 +91,22 @@ const GeneralTab = { this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) } }, + settingsProfiles () { + return (this.$store.state.instance.settingsProfiles || []) + }, + settingsProfile: { + get: function () { return this.$store.getters.mergedConfig.profile }, + set: function (val) { + this.$store.dispatch('setOption', { name: 'profile', value: val }) + this.$store.dispatch('getSettingsProfile') + } + }, + settingsVersion () { + return this.$store.getters.mergedConfig.profileVersion + }, translationLanguages () { - return (this.$store.getters.mergedConfig.supportedTranslationLanguages.target || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name })) + const langs = this.$store.state.instance.translationLanguages || [] + return (langs || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name })) }, translationLanguage: { get: function () { return this.$store.getters.mergedConfig.translationLanguage }, @@ -105,6 +122,30 @@ const GeneralTab = { }, setTranslationLanguage (value) { this.$store.dispatch('setOption', { name: 'translationLanguage', value }) + }, + toggleExpandedSettings () { + this.profilesExpanded = !this.profilesExpanded + }, + loadSettingsProfile (name) { + this.$store.commit('setOption', { name: 'profile', value: name }) + this.$store.dispatch('getSettingsProfile', true) + }, + createSettingsProfile () { + this.$store.dispatch('setOption', { name: 'profile', value: this.newProfileName }) + this.$store.dispatch('setOption', { name: 'profileVersion', value: 1 }) + this.$store.dispatch('syncSettings') + this.newProfileName = '' + }, + forceSync () { + this.$store.dispatch('getSettingsProfile') + }, + refreshProfiles () { + this.$store.dispatch('listSettingsProfiles') + }, + deleteSettingsProfile (name) { + if (confirm(this.$t('settings.settings_profile_delete_confirm'))) { + this.$store.dispatch('deleteSettingsProfile', name) + } } } } diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 608c73af..cc54bb2b 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -1,7 +1,6 @@ + diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js index 9e380f28..19cec6c6 100644 --- a/src/components/status_body/status_body.js +++ b/src/components/status_body/status_body.js @@ -83,7 +83,7 @@ const StatusContent = { return this.status.attachments.map(file => fileType.fileType(file.mimetype)) }, translationLanguages () { - return (this.$store.getters.mergedConfig.supportedTranslationLanguages.source || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name })) + return (this.$store.state.instance.supportedTranslationLanguages.source || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name })) }, ...mapGetters(['mergedConfig']) }, diff --git a/src/i18n/en.json b/src/i18n/en.json index 0b24b17c..9647b36e 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -693,6 +693,19 @@ "setting_changed": "Setting is different from default", "setting_server_side": "This setting is tied to your profile and affects all sessions and clients", "settings": "Settings", + "settings_profile": "Settings Profiles", + "settings_profile_currently": "Currently using {name} (version: {version})", + "settings_profiles_show": "Show all settings profiles", + "settings_profiles_unshow": "Hide all settings profiles", + "settings_profile_in_use": "In use", + "settings_profile_creation": "Create new profile", + "settings_profile_creation_submit": "Create", + "settings_profile_creation_new_name_label": "Name", + "settings_profile_use": "Use", + "settings_profile_delete": "Delete", + "settings_profile_delete_confirm": "Do you really want to delete this profile?", + "settings_profile_force_sync": "Synchronize", + "settings_profiles_refresh": "Reload settings profiles", "show_admin_badge": "Show \"Admin\" badge in my profile", "show_moderator_badge": "Show \"Moderator\" badge in my profile", "show_nav_shortcuts": "Show extra navigation shortcuts in top panel", @@ -892,6 +905,12 @@ "word_filter": "Word filter", "wordfilter": "Wordfilter" }, + "settings_profile": { + "synchronizing": "Synchronizing setting profile \"{profile}\"...", + "synchronized": "Synchronized settings!", + "synchronization_error": "Could not synchronize settings: {err}", + "creating": "Creating new setting profile \"{profile}\"..." + }, "status": { "ancestor_follow": "See {numReplies} other reply under this post | See {numReplies} other replies under this post", "ancestor_follow_with_icon": "{icon} {text}", diff --git a/src/modules/api.js b/src/modules/api.js index 1003a3f5..1c3423cb 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -273,10 +273,15 @@ const api = { getSupportedTranslationlanguages (store) { store.state.backendInteractor.getSupportedTranslationlanguages({ store }) .then((data) => { - store.dispatch('setOption', { name: 'supportedTranslationLanguages', value: data }) + store.dispatch('setInstanceOption', { name: 'supportedTranslationLanguages', value: data }) + }) + }, + listSettingsProfiles (store) { + store.state.backendInteractor.listSettingsProfiles({ store }) + .then((data) => { + store.commit('setInstanceOption', { name: 'settingsProfiles', value: data }) }) }, - // Pleroma websocket setWsToken (store, token) { store.commit('setWsToken', token) diff --git a/src/modules/config.js b/src/modules/config.js index 2934f2c5..8adc76b4 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -21,6 +21,8 @@ export const multiChoiceProperties = [ ] export const defaultState = { + profile: 'default', + profileVersion: 0, expertLevel: 0, // used to track which settings to show and hide colors: {}, theme: undefined, @@ -160,6 +162,42 @@ const config = { } }, actions: { + syncSettings: (store) => { + store.commit('setOption', { name: 'profileVersion', value: store.state.profileVersion + 1 }) + const notice = { + level: 'info', + messageKey: 'settings_profile.synchronizing', + messageArgs: { profile: store.state.profile }, + timeout: 5000 + } + store.dispatch('pushGlobalNotice', notice) + store.rootState.api.backendInteractor.saveSettingsProfile({ + settings: store.state, profileName: store.state.profile, version: store.state.profileVersion + }).then(() => { + store.dispatch('removeGlobalNotice', notice) + store.dispatch('pushGlobalNotice', { + level: 'success', + messageKey: 'settings_profile.synchronized', + messageArgs: { profile: store.state.profile }, + timeout: 2000 + }) + store.dispatch('listSettingsProfiles') + }).catch((err) => { + store.dispatch('removeGlobalNotice', notice) + store.dispatch('pushGlobalNotice', { + level: 'error', + messageKey: 'settings_profile.synchronization_error', + messageArgs: { error: err.message }, + timeout: 5000 + }) + console.error(err) + }) + }, + deleteSettingsProfile (store, name) { + store.rootState.api.backendInteractor.deleteSettingsProfile({ profileName: name }).then(() => { + store.dispatch('listSettingsProfiles') + }) + }, loadSettings ({ dispatch }, data) { const knownKeys = new Set(Object.keys(defaultState)) const presentKeys = new Set(Object.keys(data)) @@ -177,8 +215,11 @@ const config = { setHighlight ({ commit, dispatch }, { user, color, type }) { commit('setHighlight', { user, color, type }) }, - setOption ({ commit, dispatch }, { name, value }) { + setOption ({ commit, dispatch }, { name, value, manual }) { commit('setOption', { name, value }) + if (manual === true) { + dispatch('syncSettings') + } switch (name) { case 'theme': setPreset(value) @@ -196,6 +237,36 @@ const config = { dispatch('setLayoutWidth', undefined) break } + }, + getSettingsProfile (store, forceUpdate = false) { + const profile = store.state.profile + store.rootState.api.backendInteractor.getSettingsProfile({ store, profileName: profile }) + .then(({ settings, version }) => { + console.log('found settings version', version) + if (forceUpdate || (version > store.state.profileVersion)) { + store.commit('setOption', { name: 'profileVersion', value: version }) + Object.entries(settings).forEach(([name, value]) => { + if (store.state[name] !== value) { + store.dispatch('setOption', { name, value }) + } + }) + } else { + console.log('settings are up to date') + } + }) + .catch((err) => { + console.error(`could not fetch profile ${profile}`, err) + if (err.statusCode === 404) { + // create profile + store.dispatch('pushGlobalNotice', { + level: 'warning', + messageKey: 'settings_profile.creating', + messageArgs: { profile }, + timeout: 5000 + }) + store.dispatch('syncSettings') + } + }) } } } diff --git a/src/modules/users.js b/src/modules/users.js index 02a9e361..fb72a6a8 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -562,6 +562,8 @@ const users = { // Start fetching notifications store.dispatch('startFetchingNotifications') + + store.dispatch('startFetchingConfig') } if (store.getters.mergedConfig.useStreamingApi) { @@ -581,6 +583,9 @@ const users = { store.dispatch('setLayoutWidth', windowWidth()) store.dispatch('setLayoutHeight', windowHeight()) + store.dispatch('getSupportedTranslationlanguages') + store.dispatch('getSettingsProfile') + store.dispatch('listSettingsProfiles') // Fetch our friends store.rootState.api.backendInteractor.fetchFriends({ id: user.id }) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 8c7f80db..2ed0f1ac 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -102,6 +102,8 @@ const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements' const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements' const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}` const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}` +const AKKOMA_SETTING_PROFILE_URL = (name) => `/api/v1/akkoma/frontend_settings/pleroma-fe/${name}` +const AKKOMA_SETTING_PROFILE_LIST = `/api/v1/akkoma/frontend_settings/pleroma-fe` const oldfetch = window.fetch @@ -1451,6 +1453,40 @@ const deleteAnnouncement = ({ id, credentials }) => { }) } +const getSettingsProfile = ({ profileName, credentials }) => { + return promisedRequest({ + url: AKKOMA_SETTING_PROFILE_URL(profileName), + credentials + }) +} + +const saveSettingsProfile = ({ profileName, credentials, settings, version }) => { + return promisedRequest({ + url: AKKOMA_SETTING_PROFILE_URL(profileName), + method: 'PUT', + credentials, + payload: { + settings, + version + } + }) +} + +const deleteSettingsProfile = ({ profileName, credentials }) => { + return promisedRequest({ + url: AKKOMA_SETTING_PROFILE_URL(profileName), + method: 'DELETE', + credentials + }) +} + +const listSettingsProfiles = ({ credentials }) => { + return promisedRequest({ + url: AKKOMA_SETTING_PROFILE_LIST, + credentials + }) +} + export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => { return Object.entries({ ...(credentials @@ -1677,7 +1713,11 @@ const apiService = { deleteAnnouncement, adminFetchAnnouncements, translateStatus, - getSupportedTranslationlanguages + getSupportedTranslationlanguages, + getSettingsProfile, + saveSettingsProfile, + listSettingsProfiles, + deleteSettingsProfile } export default apiService diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index bedc51b2..596151d8 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -4,6 +4,7 @@ import notificationsFetcher from '../notifications_fetcher/notifications_fetcher import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service' import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js' import announcementsFetcher from '../../services/announcements_fetcher/announcements_fetcher.service.js' +import configFetcher from '../config_fetcher/config_fetcher.service.js' const backendInteractorService = credentials => ({ startFetchingTimeline ({ timeline, store, userId = false, listId = false, tag }) { @@ -18,6 +19,10 @@ const backendInteractorService = credentials => ({ return notificationsFetcher.startFetching({ store, credentials }) }, + startFetchingConfig ({ store }) { + return configFetcher.startFetching({ store, credentials }) + }, + fetchNotifications (args) { return notificationsFetcher.fetchAndUpdate({ ...args, credentials }) }, diff --git a/src/services/config_fetcher/config_fetcher.service.js b/src/services/config_fetcher/config_fetcher.service.js new file mode 100644 index 00000000..2c6d07ee --- /dev/null +++ b/src/services/config_fetcher/config_fetcher.service.js @@ -0,0 +1,13 @@ +import { promiseInterval } from '../promise_interval/promise_interval.js' + +const startFetching = ({ credentials, store }) => { + const boundFetchAndUpdate = () => store.dispatch('getSettingsProfile') + boundFetchAndUpdate() + return promiseInterval(boundFetchAndUpdate, 5 * 60000) +} + +const configFetcher = { + startFetching +} + +export default configFetcher