setting-sync (#175)

Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk>
Reviewed-on: AkkomaGang/pleroma-fe#175
This commit is contained in:
floatingghost 2022-10-06 15:59:16 +00:00
parent eaf2bd05a0
commit 4f837f75ea
15 changed files with 314 additions and 16 deletions

View file

@ -61,7 +61,6 @@
<EditStatusModal v-if="editingAvailable" /> <EditStatusModal v-if="editingAvailable" />
<StatusHistoryModal v-if="editingAvailable" /> <StatusHistoryModal v-if="editingAvailable" />
<SettingsModal /> <SettingsModal />
<UpdateNotification />
<GlobalNoticeList /> <GlobalNoticeList />
</div> </div>
</template> </template>

View file

@ -398,7 +398,6 @@ const afterStoreSetup = async ({ store, i18n }) => {
store.dispatch('startFetchingAnnouncements') store.dispatch('startFetchingAnnouncements')
getTOS({ store }) getTOS({ store })
getStickers({ store }) getStickers({ store })
store.dispatch('getSupportedTranslationlanguages')
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),

View file

@ -12,7 +12,8 @@ export default {
'path', 'path',
'disabled', 'disabled',
'options', 'options',
'expert' 'expert',
'hideDefaultLabel'
], ],
computed: { computed: {
pathDefault () { pathDefault () {

View file

@ -16,7 +16,11 @@
:value="option.value" :value="option.value"
> >
{{ option.label }} {{ option.label }}
{{ option.value === defaultState ? $t('settings.instance_default_simple') : '' }} <template
v-if="hideDefaultLabel !== true"
>
{{ option.value === defaultState ? $t('settings.instance_default_simple') : '' }}
</template>
</option> </option>
</Select> </Select>
<ModifiedIndicator :changed="isChanged" /> <ModifiedIndicator :changed="isChanged" />

View file

@ -19,7 +19,7 @@ const SharedComputedObject = () => ({
.map(key => [key, { .map(key => [key, {
get () { return this.$store.getters.mergedConfig[key] }, get () { return this.$store.getters.mergedConfig[key] },
set (value) { 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 }), {}), .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
@ -27,7 +27,7 @@ const SharedComputedObject = () => ({
.map(key => ['serverSide_' + key, { .map(key => ['serverSide_' + key, {
get () { return this.$store.state.serverSideConfig[key] }, get () { return this.$store.state.serverSideConfig[key] },
set (value) { 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 }), {}), .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),

View file

@ -8,11 +8,12 @@ import SharedComputedObject from '../helpers/shared_computed_object.js'
import ServerSideIndicator from '../helpers/server_side_indicator.vue' import ServerSideIndicator from '../helpers/server_side_indicator.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faGlobe faGlobe, faSync
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
faGlobe faGlobe,
faSync
) )
const GeneralTab = { const GeneralTab = {
@ -48,6 +49,8 @@ const GeneralTab = {
value: tab, value: tab,
label: this.$t(`user_card.${tab}`) label: this.$t(`user_card.${tab}`)
})), })),
profilesExpanded: false,
newProfileName: '',
loopSilentAvailable: loopSilentAvailable:
// Firefox // Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
@ -88,8 +91,22 @@ const GeneralTab = {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) 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 () { 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: { translationLanguage: {
get: function () { return this.$store.getters.mergedConfig.translationLanguage }, get: function () { return this.$store.getters.mergedConfig.translationLanguage },
@ -105,6 +122,30 @@ const GeneralTab = {
}, },
setTranslationLanguage (value) { setTranslationLanguage (value) {
this.$store.dispatch('setOption', { name: 'translationLanguage', 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)
}
} }
} }
} }

View file

@ -1,7 +1,6 @@
<template> <template>
<div :label="$t('settings.general')"> <div :label="$t('settings.general')">
<div class="setting-item"> <div class="setting-item">
<h2>{{ $t('settings.interface') }}</h2>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<interface-language-switcher <interface-language-switcher
@ -10,6 +9,94 @@
:set-language="val => language = val" :set-language="val => language = val"
/> />
</li> </li>
<li
v-if="user && (settingsProfiles.length > 0)"
>
<h2>{{ $t('settings.settings_profile') }}</h2>
<p>
{{ $t('settings.settings_profile_currently', { name: settingsProfile, version: settingsVersion }) }}
<button
class="btn button-default"
@click="forceSync()"
>
{{ $t('settings.settings_profile_force_sync') }}
</button>
</p>
<div
@click="toggleExpandedSettings"
>
<template
v-if="profilesExpanded"
>
<button class="btn button-default">
{{ $t('settings.settings_profiles_unshow') }}
</button>
</template>
<template
v-else
>
<button class="btn button-default">
{{ $t('settings.settings_profiles_show') }}
</button>
</template>
</div>
<br>
<template
v-if="profilesExpanded"
>
<div
v-for="profile in settingsProfiles"
:key="profile.id"
class="settings-profile"
>
<h4>{{ profile.name }} ({{ profile.version }})</h4>
<template
v-if="settingsProfile === profile.name"
>
{{ $t('settings.settings_profile_in_use') }}
</template>
<template
v-else
>
<button
class="btn button-default"
@click="loadSettingsProfile(profile.name)"
>
{{ $t('settings.settings_profile_use') }}
</button>
<button
class="btn button-default"
@click="deleteSettingsProfile(profile.name)"
>
{{ $t('settings.settings_profile_delete') }}
</button>
</template>
</div>
<button class="btn button-default" @click="refreshProfiles()">
{{ $t('settings.settings_profiles_refresh') }}
<FAIcon icon="sync" @click="refreshProfiles()" />
</button>
<h3>{{ $t('settings.settings_profile_creation') }}</h3>
<label for="settings-profile-new-name">
{{ $t('settings.settings_profile_creation_new_name_label') }}
</label>
<input v-model="newProfileName" id="settings-profile-new-name">
<button
class="btn button-default"
@click="createSettingsProfile"
>
{{ $t('settings.settings_profile_creation_submit') }}
</button>
</template>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.interface') }}</h2>
<ul class="setting-list">
<li v-if="instanceSpecificPanelPresent"> <li v-if="instanceSpecificPanelPresent">
<BooleanSetting path="hideISP"> <BooleanSetting path="hideISP">
{{ $t('settings.hide_isp') }} {{ $t('settings.hide_isp') }}
@ -463,7 +550,6 @@
</BooleanSetting> </BooleanSetting>
</li> </li>
<li> <li>
<!-- <BooleanSetting path="serverSide_defaultNSFW"> -->
<BooleanSetting path="sensitiveByDefault"> <BooleanSetting path="sensitiveByDefault">
{{ $t('settings.sensitive_by_default') }} {{ $t('settings.sensitive_by_default') }}
</BooleanSetting> </BooleanSetting>
@ -546,3 +632,13 @@
</template> </template>
<script src="./general_tab.js"></script> <script src="./general_tab.js"></script>
<style lang="scss">
.settings-profile {
margin-bottom: 1em;
}
#settings-profile-new-name {
margin-left: 1em;
margin-right: 1em;
}
</style>

View file

@ -83,7 +83,7 @@ const StatusContent = {
return this.status.attachments.map(file => fileType.fileType(file.mimetype)) return this.status.attachments.map(file => fileType.fileType(file.mimetype))
}, },
translationLanguages () { 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']) ...mapGetters(['mergedConfig'])
}, },

View file

@ -693,6 +693,19 @@
"setting_changed": "Setting is different from default", "setting_changed": "Setting is different from default",
"setting_server_side": "This setting is tied to your profile and affects all sessions and clients", "setting_server_side": "This setting is tied to your profile and affects all sessions and clients",
"settings": "Settings", "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_admin_badge": "Show \"Admin\" badge in my profile",
"show_moderator_badge": "Show \"Moderator\" badge in my profile", "show_moderator_badge": "Show \"Moderator\" badge in my profile",
"show_nav_shortcuts": "Show extra navigation shortcuts in top panel", "show_nav_shortcuts": "Show extra navigation shortcuts in top panel",
@ -892,6 +905,12 @@
"word_filter": "Word filter", "word_filter": "Word filter",
"wordfilter": "Wordfilter" "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": { "status": {
"ancestor_follow": "See {numReplies} other reply under this post | See {numReplies} other replies under this post", "ancestor_follow": "See {numReplies} other reply under this post | See {numReplies} other replies under this post",
"ancestor_follow_with_icon": "{icon} {text}", "ancestor_follow_with_icon": "{icon} {text}",

View file

@ -273,10 +273,15 @@ const api = {
getSupportedTranslationlanguages (store) { getSupportedTranslationlanguages (store) {
store.state.backendInteractor.getSupportedTranslationlanguages({ store }) store.state.backendInteractor.getSupportedTranslationlanguages({ store })
.then((data) => { .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 // Pleroma websocket
setWsToken (store, token) { setWsToken (store, token) {
store.commit('setWsToken', token) store.commit('setWsToken', token)

View file

@ -21,6 +21,8 @@ export const multiChoiceProperties = [
] ]
export const defaultState = { export const defaultState = {
profile: 'default',
profileVersion: 0,
expertLevel: 0, // used to track which settings to show and hide expertLevel: 0, // used to track which settings to show and hide
colors: {}, colors: {},
theme: undefined, theme: undefined,
@ -160,6 +162,42 @@ const config = {
} }
}, },
actions: { 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) { loadSettings ({ dispatch }, data) {
const knownKeys = new Set(Object.keys(defaultState)) const knownKeys = new Set(Object.keys(defaultState))
const presentKeys = new Set(Object.keys(data)) const presentKeys = new Set(Object.keys(data))
@ -177,8 +215,11 @@ const config = {
setHighlight ({ commit, dispatch }, { user, color, type }) { setHighlight ({ commit, dispatch }, { user, color, type }) {
commit('setHighlight', { user, color, type }) commit('setHighlight', { user, color, type })
}, },
setOption ({ commit, dispatch }, { name, value }) { setOption ({ commit, dispatch }, { name, value, manual }) {
commit('setOption', { name, value }) commit('setOption', { name, value })
if (manual === true) {
dispatch('syncSettings')
}
switch (name) { switch (name) {
case 'theme': case 'theme':
setPreset(value) setPreset(value)
@ -196,6 +237,36 @@ const config = {
dispatch('setLayoutWidth', undefined) dispatch('setLayoutWidth', undefined)
break 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')
}
})
} }
} }
} }

View file

@ -562,6 +562,8 @@ const users = {
// Start fetching notifications // Start fetching notifications
store.dispatch('startFetchingNotifications') store.dispatch('startFetchingNotifications')
store.dispatch('startFetchingConfig')
} }
if (store.getters.mergedConfig.useStreamingApi) { if (store.getters.mergedConfig.useStreamingApi) {
@ -581,6 +583,9 @@ const users = {
store.dispatch('setLayoutWidth', windowWidth()) store.dispatch('setLayoutWidth', windowWidth())
store.dispatch('setLayoutHeight', windowHeight()) store.dispatch('setLayoutHeight', windowHeight())
store.dispatch('getSupportedTranslationlanguages')
store.dispatch('getSettingsProfile')
store.dispatch('listSettingsProfiles')
// Fetch our friends // Fetch our friends
store.rootState.api.backendInteractor.fetchFriends({ id: user.id }) store.rootState.api.backendInteractor.fetchFriends({ id: user.id })

View file

@ -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_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}` 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 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 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 = {} }) => { export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
return Object.entries({ return Object.entries({
...(credentials ...(credentials
@ -1677,7 +1713,11 @@ const apiService = {
deleteAnnouncement, deleteAnnouncement,
adminFetchAnnouncements, adminFetchAnnouncements,
translateStatus, translateStatus,
getSupportedTranslationlanguages getSupportedTranslationlanguages,
getSettingsProfile,
saveSettingsProfile,
listSettingsProfiles,
deleteSettingsProfile
} }
export default apiService export default apiService

View file

@ -4,6 +4,7 @@ import notificationsFetcher from '../notifications_fetcher/notifications_fetcher
import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service' import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js' import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js'
import announcementsFetcher from '../../services/announcements_fetcher/announcements_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 => ({ const backendInteractorService = credentials => ({
startFetchingTimeline ({ timeline, store, userId = false, listId = false, tag }) { startFetchingTimeline ({ timeline, store, userId = false, listId = false, tag }) {
@ -18,6 +19,10 @@ const backendInteractorService = credentials => ({
return notificationsFetcher.startFetching({ store, credentials }) return notificationsFetcher.startFetching({ store, credentials })
}, },
startFetchingConfig ({ store }) {
return configFetcher.startFetching({ store, credentials })
},
fetchNotifications (args) { fetchNotifications (args) {
return notificationsFetcher.fetchAndUpdate({ ...args, credentials }) return notificationsFetcher.fetchAndUpdate({ ...args, credentials })
}, },

View file

@ -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