Merge remote-tracking branch 'origin/develop' into admin-users

This commit is contained in:
Henry Jameson 2026-06-08 00:57:42 +03:00
commit 43936a8725
628 changed files with 72639 additions and 24537 deletions

View file

@ -1,4 +1,4 @@
import { set, get, cloneDeep, differenceWith, isEqual, flatten } from 'lodash'
import { cloneDeep, differenceWith, flatten, get, isEqual, set } from 'lodash'
export const defaultState = {
frontends: [],
@ -8,24 +8,24 @@ export const defaultState = {
modifiedPaths: null,
descriptions: null,
draft: null,
dbConfigEnabled: null
dbConfigEnabled: null,
}
export const newUserFlags = {
...defaultState.flagStorage
...defaultState.flagStorage,
}
const adminSettingsStorage = {
state: {
...cloneDeep(defaultState)
...cloneDeep(defaultState),
},
mutations: {
setInstanceAdminNoDbConfig (state) {
setInstanceAdminNoDbConfig(state) {
state.loaded = false
state.dbConfigEnabled = false
},
setAvailableFrontends (state, { frontends }) {
state.frontends = frontends.map(f => {
setAvailableFrontends(state, { frontends }) {
state.frontends = frontends.map((f) => {
f.installedRefs = f.installed_refs
if (f.name === 'pleroma-fe') {
f.refs = ['master', 'develop']
@ -35,16 +35,16 @@ const adminSettingsStorage = {
return f
})
},
updateAdminSettings (state, { config, modifiedPaths }) {
updateAdminSettings(state, { config, modifiedPaths }) {
state.loaded = true
state.dbConfigEnabled = true
state.config = config
state.modifiedPaths = modifiedPaths
},
updateAdminDescriptions (state, { descriptions }) {
updateAdminDescriptions(state, { descriptions }) {
state.descriptions = descriptions
},
updateAdminDraft (state, { path, value }) {
updateAdminDraft(state, { path, value }) {
const [group, key, subkey] = path
const parent = [group, key, subkey]
@ -55,84 +55,132 @@ const adminSettingsStorage = {
set(state.draft, parent, cloneDeep(get(state.draft, parent)))
}
},
resetAdminDraft (state) {
resetAdminDraft(state) {
state.draft = cloneDeep(state.config)
}
},
},
actions: {
async fetchAdminUsers (store, opts) {
const users = await store.rootState.api.backendInteractor.adminListUsers({opts})
users.forEach(user => store.dispatch('fetchUserIfMissing', user.id))
return users
async fetchAdminUsers(store, opts) {
const users = await store.rootState.api.backendInteractor.adminListUsers({
opts,
})
users.forEach((user) => store.dispatch('fetchUserIfMissing', user.id))
return users
},
adminAddUserToAdminGroup (store, user) {
store.rootState.api.backendInteractor.adminAddUserToAdminGroup({ user })
.then(res => store.commit('updateRight', { user, right: 'admin', value: res.is_admin }))
adminAddUserToAdminGroup(store, user) {
store.rootState.api.backendInteractor
.adminAddUserToAdminGroup({ user })
.then((res) =>
store.commit('updateRight', {
user,
right: 'admin',
value: res.is_admin,
}),
)
},
adminRemoveUserFromAdminGroup (store, user) {
// prevent revokation of own rights
if (user.id !== store.rootState.users.currentUser.id) {
return store.rootState.api.backendInteractor.adminRemoveUserFromAdminGroup({ user })
.then(res => store.commit('updateRight', { user, right: 'admin', value: res.is_admin }))
}
adminRemoveUserFromAdminGroup(store, user) {
// prevent revokation of own rights
if (user.id !== store.rootState.users.currentUser.id) {
return store.rootState.api.backendInteractor
.adminRemoveUserFromAdminGroup({ user })
.then((res) =>
store.commit('updateRight', {
user,
right: 'admin',
value: res.is_admin,
}),
)
}
},
adminAddUserToModeratorGroup (store, user) {
return store.rootState.api.backendInteractor.adminAddUserToModeratorGroup({ user })
.then(res => store.commit('updateRight', { user, right: 'moderator', value: res.is_moderator }))
adminAddUserToModeratorGroup(store, user) {
return store.rootState.api.backendInteractor
.adminAddUserToModeratorGroup({ user })
.then((res) =>
store.commit('updateRight', {
user,
right: 'moderator',
value: res.is_moderator,
}),
)
},
adminRemoveUserFromModeratorGroup (store, user) {
// prevent revokation of own rights
if (user.id !== store.state.users.currentUser.id) {
return store.rootState.api.backendInteractor.adminRemoveUserFromModeratorGroup({ user })
.then(res => store.commit('updateRight', { user, right: 'moderator', value: res.is_moderator }))
}
adminRemoveUserFromModeratorGroup(store, user) {
// prevent revokation of own rights
if (user.id !== store.state.users.currentUser.id) {
return store.rootState.api.backendInteractor
.adminRemoveUserFromModeratorGroup({ user })
.then((res) =>
store.commit('updateRight', {
user,
right: 'moderator',
value: res.is_moderator,
}),
)
}
},
adminActivateUser (store, user) {
return store.rootState.api.backendInteractor.activateUser({ user })
.then(res => { const deactivated = !res.is_active; store.commit('updateActivationStatus', { user, deactivated })})
adminActivateUser(store, user) {
return store.rootState.api.backendInteractor
.activateUser({ user })
.then((res) => {
const deactivated = !res.is_active
store.commit('updateActivationStatus', { user, deactivated })
})
},
adminDeactivateUser (store, user) {
return store.rootState.api.backendInteractor.deactivateUser({ user })
.then(res => { const deactivated = !res.is_active; store.commit('updateActivationStatus', { user, deactivated })})
adminDeactivateUser(store, user) {
return store.rootState.api.backendInteractor
.deactivateUser({ user })
.then((res) => {
const deactivated = !res.is_active
store.commit('updateActivationStatus', { user, deactivated })
})
},
adminDeleteUser (store, user) {
return store.rootState.api.backendInteractor.deleteUser({ user })
adminDeleteUser(store, user) {
return store.rootState.api.backendInteractor.deleteUser({ user })
},
adminConfirmUser (store, user) {
return store.rootState.api.backendInteractor.adminConfirmUser({ user })
.then(() => store.dispatch('fetchUser', user.id))
adminConfirmUser(store, user) {
return store.rootState.api.backendInteractor
.adminConfirmUser({ user })
.then(() => store.dispatch('fetchUser', user.id))
},
adminResendConfirmationEmail (store, user) {
return store.rootState.api.backendInteractor.adminResendConfirmationEmail({ user })
adminResendConfirmationEmail(store, user) {
return store.rootState.api.backendInteractor.adminResendConfirmationEmail(
{ user },
)
},
adminApproveUser (store, user) {
return store.rootState.api.backendInteractor.adminApproveUser({ user })
adminApproveUser(store, user) {
return store.rootState.api.backendInteractor.adminApproveUser({ user })
},
adminListStatuses (store, { user, opts }) {
return store.rootState.api.backendInteractor.adminListStatuses({ user, opts })
adminListStatuses(store, { user, opts }) {
return store.rootState.api.backendInteractor.adminListStatuses({
user,
opts,
})
},
adminChangeStatusScope (store, { opts }) {
return store.rootState.api.backendInteractor.adminChangeStatusScope({ opts })
adminChangeStatusScope(store, { opts }) {
return store.rootState.api.backendInteractor.adminChangeStatusScope({
opts,
})
},
adminDisableMFA (store, user) {
adminDisableMFA(store, user) {
return store.rootState.api.backendInteractor.adminDisableMFA({ user })
},
adminTagUser (store, { user, tag }) {
adminTagUser(store, { user, tag }) {
return store.rootState.api.backendInteractor.tagUser({ user, tag })
},
adminUntagUser (store, { user, tag }) {
adminUntagUser(store, { user, tag }) {
return store.rootState.api.backendInteractor.untagUser({ user, tag })
},
loadFrontendsStuff ({ rootState, commit }) {
rootState.api.backendInteractor.fetchAvailableFrontends()
.then(frontends => commit('setAvailableFrontends', { frontends }))
loadFrontendsStuff({ rootState, commit }) {
rootState.api.backendInteractor
.fetchAvailableFrontends()
.then((frontends) => commit('setAvailableFrontends', { frontends }))
},
loadAdminStuff ({ state, rootState, dispatch, commit }) {
rootState.api.backendInteractor.fetchInstanceDBConfig()
.then(backendDbConfig => {
loadAdminStuff({ state, rootState, dispatch, commit }) {
rootState.api.backendInteractor
.fetchInstanceDBConfig()
.then((backendDbConfig) => {
if (backendDbConfig.error) {
if (backendDbConfig.error.status === 400) {
backendDbConfig.error.json().then(errorJson => {
backendDbConfig.error.json().then((errorJson) => {
if (/configurable_from_database/.test(errorJson.error)) {
commit('setInstanceAdminNoDbConfig')
}
@ -143,40 +191,83 @@ const adminSettingsStorage = {
}
})
if (state.descriptions === null) {
rootState.api.backendInteractor.fetchInstanceConfigDescriptions()
.then(backendDescriptions => dispatch('setInstanceAdminDescriptions', { backendDescriptions }))
rootState.api.backendInteractor
.fetchInstanceConfigDescriptions()
.then((backendDescriptions) =>
dispatch('setInstanceAdminDescriptions', { backendDescriptions }),
)
}
},
setInstanceAdminSettings ({ state, commit }, { backendDbConfig }) {
setInstanceAdminSettings({ state, commit }, { backendDbConfig }) {
const config = state.config || {}
const modifiedPaths = new Set()
backendDbConfig.configs.forEach(c => {
backendDbConfig.configs.forEach((c) => {
const path = [c.group, c.key]
if (c.db) {
// Path elements can contain dot, therefore we use ' -> ' as a separator instead
// Using strings for modified paths for easier searching
c.db.forEach(x => modifiedPaths.add([...path, x].join(' -> ')))
c.db.forEach((x) => modifiedPaths.add([...path, x].join(' -> ')))
}
const convert = (value) => {
// we need to preserve tuples on second level only, possibly third
// but it's not a case right now.
const convert = (value, preserveTuples, preserveTuplesLv2) => {
if (Array.isArray(value) && value.length > 0 && value[0].tuple) {
return value.reduce((acc, c) => {
return { ...acc, [c.tuple[0]]: convert(c.tuple[1]) }
}, {})
if (!preserveTuples) {
return value.reduce((acc, c) => {
if (c.tuple == null) {
return {
...acc,
[c]: c,
}
}
return {
...acc,
[c.tuple[0]]: convert(c.tuple[1], preserveTuplesLv2),
}
}, {})
} else {
return value.map((x) => x.tuple)
}
} else {
return value
if (!preserveTuples) {
return value
} else {
return value.tuple
}
}
}
set(config, path, convert(c.value))
// for most stuff we want maps since those are more convenient
// however this doesn't allow for multiple values per same key
// so for those cases we want to preserve tuples as-is
// right now it's made exclusively for :pleroma.:rate_limit
// so it might not work properly elsewhere
const preserveTuples = path.find((x) => x === ':rate_limit')
set(config, path, convert(c.value, false, preserveTuples))
})
// patching http adapter config to be easier to handle
const adapter = config[':pleroma'][':http'][':adapter']
if (Array.isArray(adapter)) {
config[':pleroma'][':http'][':adapter'] = {
[':ssl_options']: {
[':versions']: [],
},
}
}
commit('updateAdminSettings', { config, modifiedPaths })
commit('resetAdminDraft')
},
setInstanceAdminDescriptions ({ commit }, { backendDescriptions }) {
const convert = ({ children, description, label, key = '<ROOT>', group, suggestions }, path, acc) => {
setInstanceAdminDescriptions({ commit }, { backendDescriptions }) {
const convert = (
{ children, description, label, key = '<ROOT>', group, suggestions },
path,
acc,
) => {
const newPath = group ? [group, key] : [key]
const obj = { description, label, suggestions }
if (Array.isArray(children)) {
children.forEach(c => {
children.forEach((c) => {
convert(c, newPath, obj)
})
}
@ -184,13 +275,14 @@ const adminSettingsStorage = {
}
const descriptions = {}
backendDescriptions.forEach(d => convert(d, '', descriptions))
backendDescriptions.forEach((d) => convert(d, '', descriptions))
commit('updateAdminDescriptions', { descriptions })
},
// This action takes draft state, diffs it with live config state and then pushes
// only differences between the two. Difference detection only work up to subkey (third) level.
pushAdminDraft ({ rootState, state, dispatch }) {
pushAdminDraft({ rootState, state, dispatch }) {
// TODO cleanup paths in modifiedPaths
const convert = (value) => {
if (typeof value !== 'object') {
@ -204,13 +296,9 @@ const adminSettingsStorage = {
// Getting all group-keys used in config
const allGroupKeys = flatten(
Object
.entries(state.config)
.map(
([group, lv1data]) => Object
.keys(lv1data)
.map((key) => ({ group, key }))
)
Object.entries(state.config).map(([group, lv1data]) =>
Object.keys(lv1data).map((key) => ({ group, key })),
),
)
// Only using group-keys where there are changes detected
@ -229,19 +317,30 @@ const adminSettingsStorage = {
// Then those entries array we diff so only changed subkey entries remain
// We use the diffed array to reconstruct the object and then shove it into convert()
return ({ group, key, value: convert(Object.fromEntries(differenceWith(eDraft, eConfig, isEqual))) })
})
rootState.api.backendInteractor.pushInstanceDBConfig({
payload: {
configs: changed
return {
group,
key,
value: convert(
Object.fromEntries(differenceWith(eDraft, eConfig, isEqual)),
),
}
})
rootState.api.backendInteractor
.pushInstanceDBConfig({
payload: {
configs: changed,
},
})
.then(() => rootState.api.backendInteractor.fetchInstanceDBConfig())
.then(backendDbConfig => dispatch('setInstanceAdminSettings', { backendDbConfig }))
.then((backendDbConfig) =>
dispatch('setInstanceAdminSettings', { backendDbConfig }),
)
},
pushAdminSetting ({ rootState, dispatch }, { path, value }) {
const [group, key, ...rest] = Array.isArray(path) ? path : path.split(/\./g)
pushAdminSetting({ rootState, dispatch }, { path, value }) {
const [group, key, ...rest] = Array.isArray(path)
? path
: path.split(/\./g)
const clone = {} // not actually cloning the entire thing to avoid excessive writes
set(clone, rest, value)
@ -256,37 +355,49 @@ const adminSettingsStorage = {
}
}
rootState.api.backendInteractor.pushInstanceDBConfig({
payload: {
configs: [{
group,
key,
value: convert(clone)
}]
}
})
rootState.api.backendInteractor
.pushInstanceDBConfig({
payload: {
configs: [
{
group,
key,
value: convert(clone),
},
],
},
})
.then(() => rootState.api.backendInteractor.fetchInstanceDBConfig())
.then(backendDbConfig => dispatch('setInstanceAdminSettings', { backendDbConfig }))
.then((backendDbConfig) =>
dispatch('setInstanceAdminSettings', { backendDbConfig }),
)
},
resetAdminSetting ({ rootState, state, dispatch }, { path }) {
const [group, key, subkey] = path.split(/\./g)
resetAdminSetting({ rootState, state, dispatch }, { path }) {
const [group, key, subkey] = Array.isArray(path)
? path
: path.split(/\./g)
state.modifiedPaths.delete(path)
return rootState.api.backendInteractor.pushInstanceDBConfig({
payload: {
configs: [{
group,
key,
delete: true,
subkeys: [subkey]
}]
}
})
return rootState.api.backendInteractor
.pushInstanceDBConfig({
payload: {
configs: [
{
group,
key,
delete: true,
subkeys: [subkey],
},
],
},
})
.then(() => rootState.api.backendInteractor.fetchInstanceDBConfig())
.then(backendDbConfig => dispatch('setInstanceAdminSettings', { backendDbConfig }))
}
}
.then((backendDbConfig) =>
dispatch('setInstanceAdminSettings', { backendDbConfig }),
)
},
},
}
export default adminSettingsStorage

View file

@ -1,9 +1,13 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
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 { WSConnectionStatus } from '../services/api/api.service.js'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useShoutStore } from 'src/stores/shout.js'
const retryTimeout = (multiplier) => 1000 * multiplier
@ -15,40 +19,40 @@ const api = {
socket: null,
mastoUserSocket: null,
mastoUserSocketStatus: null,
followRequests: []
followRequests: [],
},
getters: {
followRequestCount: state => state.followRequests.length
followRequestCount: (state) => state.followRequests.length,
},
mutations: {
setBackendInteractor (state, backendInteractor) {
setBackendInteractor(state, backendInteractor) {
state.backendInteractor = backendInteractor
},
addFetcher (state, { fetcherName, fetcher }) {
addFetcher(state, { fetcherName, fetcher }) {
state.fetchers[fetcherName] = fetcher
},
removeFetcher (state, { fetcherName }) {
removeFetcher(state, { fetcherName }) {
state.fetchers[fetcherName].stop()
delete state.fetchers[fetcherName]
},
setWsToken (state, token) {
setWsToken(state, token) {
state.wsToken = token
},
setSocket (state, socket) {
setSocket(state, socket) {
state.socket = socket
},
setFollowRequests (state, value) {
setFollowRequests(state, value) {
state.followRequests = value
},
setMastoUserSocketStatus (state, value) {
setMastoUserSocketStatus(state, value) {
state.mastoUserSocketStatus = value
},
incrementRetryMultiplier (state) {
incrementRetryMultiplier(state) {
state.retryMultiplier = Math.max(++state.retryMultiplier, 3)
},
resetRetryMultiplier (state) {
resetRetryMultiplier(state) {
state.retryMultiplier = 1
}
},
},
actions: {
/**
@ -56,15 +60,14 @@ const api = {
*
* @param {Boolean} [initial] - whether this enabling happened at boot time or not
*/
enableMastoSockets (store, initial) {
enableMastoSockets(store, initial) {
const { state, dispatch, commit } = store
// Do not initialize unless nonexistent or closed
if (
state.mastoUserSocket &&
![
WebSocket.CLOSED,
WebSocket.CLOSING
].includes(state.mastoUserSocket.getState())
![WebSocket.CLOSED, WebSocket.CLOSING].includes(
state.mastoUserSocket.getState(),
)
) {
return
}
@ -75,7 +78,7 @@ const api = {
}
return dispatch('startMastoUserSocket')
},
disableMastoSockets (store) {
disableMastoSockets(store) {
const { state, dispatch, commit } = store
if (!state.mastoUserSocket) return
commit('setMastoUserSocketStatus', WSConnectionStatus.DISABLED)
@ -83,15 +86,20 @@ const api = {
},
// MastoAPI 'User' sockets
startMastoUserSocket (store) {
startMastoUserSocket(store) {
return new Promise((resolve, reject) => {
try {
const { state, commit, dispatch, rootState } = store
const timelineData = rootState.statuses.timelines.friends
state.mastoUserSocket = state.backendInteractor.startUserSocket({ store })
state.mastoUserSocket.addEventListener('pleroma:authenticated', () => {
state.mastoUserSocket.subscribe('user')
state.mastoUserSocket = state.backendInteractor.startUserSocket({
store,
})
state.mastoUserSocket.addEventListener(
'pleroma:authenticated',
() => {
state.mastoUserSocket.subscribe('user')
},
)
state.mastoUserSocket.addEventListener(
'message',
({ detail: message }) => {
@ -99,21 +107,22 @@ const api = {
if (message.event === 'notification') {
dispatch('addNewNotifications', {
notifications: [message.notification],
older: false
older: false,
})
} else if (message.event === 'update') {
dispatch('addNewStatuses', {
statuses: [message.status],
userId: false,
showImmediately: timelineData.visibleStatuses.length === 0,
timeline: 'friends'
timeline: 'friends',
})
} else if (message.event === 'status.update') {
dispatch('addNewStatuses', {
statuses: [message.status],
userId: false,
showImmediately: message.status.id in timelineData.visibleStatusesObject,
timeline: 'friends'
showImmediately:
message.status.id in timelineData.visibleStatusesObject,
timeline: 'friends',
})
} else if (message.event === 'delete') {
dispatch('deleteStatusById', message.id)
@ -125,28 +134,33 @@ const api = {
setTimeout(() => {
dispatch('addChatMessages', {
chatId: message.chatUpdate.id,
messages: [message.chatUpdate.lastMessage]
messages: [message.chatUpdate.lastMessage],
})
dispatch('updateChat', { chat: message.chatUpdate })
maybeShowChatNotification(store, message.chatUpdate)
}, 100)
}
}
},
)
state.mastoUserSocket.addEventListener('open', () => {
// Do not show notification when we just opened up the page
if (state.mastoUserSocketStatus !== WSConnectionStatus.STARTING_INITIAL) {
if (
state.mastoUserSocketStatus !==
WSConnectionStatus.STARTING_INITIAL
) {
useInterfaceStore().pushGlobalNotice({
level: 'success',
messageKey: 'timeline.socket_reconnected',
timeout: 5000
timeout: 5000,
})
}
// Stop polling if we were errored or disabled
if (new Set([
WSConnectionStatus.ERROR,
WSConnectionStatus.DISABLED
]).has(state.mastoUserSocketStatus)) {
if (
new Set([
WSConnectionStatus.ERROR,
WSConnectionStatus.DISABLED,
]).has(state.mastoUserSocketStatus)
) {
dispatch('stopFetchingTimeline', { timeline: 'friends' })
dispatch('stopFetchingNotifications')
dispatch('stopFetchingChats')
@ -154,48 +168,58 @@ const api = {
commit('resetRetryMultiplier')
commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED)
})
state.mastoUserSocket.addEventListener('error', ({ detail: error }) => {
console.error('Error in MastoAPI websocket:', error)
// TODO is this needed?
dispatch('clearOpenedChats')
})
state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => {
const ignoreCodes = new Set([
1000, // Normal (intended) closure
1001 // Going away
])
const { code } = closeEvent
if (ignoreCodes.has(code)) {
console.debug(`Not restarting socket becasue of closure code ${code} is in ignore list`)
commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED)
} else {
console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`)
setTimeout(() => {
dispatch('startMastoUserSocket')
}, retryTimeout(state.retryMultiplier))
commit('incrementRetryMultiplier')
if (state.mastoUserSocketStatus !== WSConnectionStatus.ERROR) {
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
dispatch('startFetchingChats')
useInterfaceStore().pushGlobalNotice({
level: 'error',
messageKey: 'timeline.socket_broke',
messageArgs: [code],
timeout: 5000
})
state.mastoUserSocket.addEventListener(
'error',
({ detail: error }) => {
console.error('Error in MastoAPI websocket:', error)
// TODO is this needed?
dispatch('clearOpenedChats')
},
)
state.mastoUserSocket.addEventListener(
'close',
({ detail: closeEvent }) => {
const ignoreCodes = new Set([
1000, // Normal (intended) closure
1001, // Going away
])
const { code } = closeEvent
if (ignoreCodes.has(code)) {
console.debug(
`Not restarting socket becasue of closure code ${code} is in ignore list`,
)
commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED)
} else {
console.warn(
`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`,
)
setTimeout(() => {
dispatch('startMastoUserSocket')
}, retryTimeout(state.retryMultiplier))
commit('incrementRetryMultiplier')
if (state.mastoUserSocketStatus !== WSConnectionStatus.ERROR) {
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
dispatch('startFetchingChats')
useInterfaceStore().pushGlobalNotice({
level: 'error',
messageKey: 'timeline.socket_broke',
messageArgs: [code],
timeout: 5000,
})
}
commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR)
}
commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR)
}
dispatch('clearOpenedChats')
})
dispatch('clearOpenedChats')
},
)
resolve()
} catch (e) {
reject(e)
}
})
},
stopMastoUserSocket ({ state, dispatch }) {
stopMastoUserSocket({ state, dispatch }) {
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
dispatch('startFetchingChats')
@ -203,103 +227,128 @@ const api = {
},
// Timelines
startFetchingTimeline (store, {
timeline = 'friends',
tag = false,
userId = false,
listId = false,
statusId = false,
bookmarkFolderId = false
}) {
if (timeline === 'favourites' && !store.rootState.instance.pleromaPublicFavouritesAvailable) return
startFetchingTimeline(
store,
{
timeline = 'friends',
tag = false,
userId = false,
listId = false,
statusId = false,
bookmarkFolderId = false,
},
) {
if (
timeline === 'favourites' &&
!useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable
)
return
if (store.state.fetchers[timeline]) return
const fetcher = store.state.backendInteractor.startFetchingTimeline({
timeline, store, userId, listId, statusId, bookmarkFolderId, tag
timeline,
store,
userId,
listId,
statusId,
bookmarkFolderId,
tag,
})
store.commit('addFetcher', { fetcherName: timeline, fetcher })
},
stopFetchingTimeline (store, timeline) {
stopFetchingTimeline(store, timeline) {
const fetcher = store.state.fetchers[timeline]
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: timeline, fetcher })
},
fetchTimeline (store, { timeline, ...rest }) {
fetchTimeline(store, { timeline, ...rest }) {
store.state.backendInteractor.fetchTimeline({
store,
timeline,
...rest
...rest,
})
},
// Notifications
startFetchingNotifications (store) {
startFetchingNotifications(store) {
if (store.state.fetchers.notifications) return
const fetcher = store.state.backendInteractor.startFetchingNotifications({ store })
const fetcher = store.state.backendInteractor.startFetchingNotifications({
store,
})
store.commit('addFetcher', { fetcherName: 'notifications', fetcher })
},
stopFetchingNotifications (store) {
stopFetchingNotifications(store) {
const fetcher = store.state.fetchers.notifications
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'notifications', fetcher })
},
fetchNotifications (store, { ...rest }) {
fetchNotifications(store, { ...rest }) {
store.state.backendInteractor.fetchNotifications({
store,
...rest
...rest,
})
},
// Follow requests
startFetchingFollowRequests (store) {
startFetchingFollowRequests(store) {
if (store.state.fetchers.followRequests) return
const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
const fetcher = store.state.backendInteractor.startFetchingFollowRequests(
{ store },
)
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
},
stopFetchingFollowRequests (store) {
stopFetchingFollowRequests(store) {
const fetcher = store.state.fetchers.followRequests
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'followRequests', fetcher })
},
removeFollowRequest (store, request) {
removeFollowRequest(store, request) {
const requests = store.state.followRequests.filter((it) => it !== request)
store.commit('setFollowRequests', requests)
},
// Lists
startFetchingLists (store) {
startFetchingLists(store) {
if (store.state.fetchers.lists) return
const fetcher = store.state.backendInteractor.startFetchingLists({ store })
const fetcher = store.state.backendInteractor.startFetchingLists({
store,
})
store.commit('addFetcher', { fetcherName: 'lists', fetcher })
},
stopFetchingLists (store) {
stopFetchingLists(store) {
const fetcher = store.state.fetchers.lists
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'lists', fetcher })
},
// Bookmark folders
startFetchingBookmarkFolders (store) {
startFetchingBookmarkFolders(store) {
if (store.state.fetchers.bookmarkFolders) return
if (!store.rootState.instance.pleromaBookmarkFoldersAvailable) return
const fetcher = store.state.backendInteractor.startFetchingBookmarkFolders({ store })
if (!useInstanceCapabilitiesStore().pleromaBookmarkFoldersAvailable)
return
const fetcher =
store.state.backendInteractor.startFetchingBookmarkFolders({ store })
store.commit('addFetcher', { fetcherName: 'bookmarkFolders', fetcher })
},
stopFetchingBookmarkFolders (store) {
stopFetchingBookmarkFolders(store) {
const fetcher = store.state.fetchers.bookmarkFolders
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'bookmarkFolders', fetcher })
},
// Pleroma websocket
setWsToken (store, token) {
setWsToken(store, token) {
store.commit('setWsToken', token)
},
initializeSocket ({ commit, state, rootState }) {
initializeSocket({ commit, state, rootState }) {
// Set up websocket connection
const token = state.wsToken
if (rootState.instance.shoutAvailable && typeof token !== 'undefined' && state.socket === null) {
if (
useInstanceCapabilitiesStore().shoutAvailable &&
typeof token !== 'undefined' &&
state.socket === null
) {
const socket = new Socket('/socket', { params: { token } })
socket.connect()
@ -307,11 +356,11 @@ const api = {
useShoutStore().initializeShout(socket)
}
},
disconnectFromSocket ({ commit, state }) {
disconnectFromSocket({ commit, state }) {
state.socket && state.socket.disconnect()
commit('setSocket', null)
}
}
},
},
}
export default api

View file

@ -1,13 +1,17 @@
import { reactive } from 'vue'
import { find, omitBy, orderBy, sumBy } from 'lodash'
import { reactive } from 'vue'
import chatService from '../services/chat_service/chat_service.js'
import { parseChat, parseChatMessage } from '../services/entity_normalizer/entity_normalizer.service.js'
import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js'
import {
parseChat,
parseChatMessage,
} from '../services/entity_normalizer/entity_normalizer.service.js'
import { promiseInterval } from '../services/promise_interval/promise_interval.js'
const emptyChatList = () => ({
data: [],
idStore: {}
idStore: {},
})
const defaultState = {
@ -17,7 +21,7 @@ const defaultState = {
openedChatMessageServices: reactive({}),
fetcher: undefined,
currentChatId: null,
lastReadMessageId: null
lastReadMessageId: null,
}
const getChatById = (state, id) => {
@ -35,65 +39,74 @@ const unreadChatCount = (state) => {
const chats = {
state: { ...defaultState },
getters: {
currentChat: state => state.openedChats[state.currentChatId],
currentChatMessageService: state => state.openedChatMessageServices[state.currentChatId],
findOpenedChatByRecipientId: state => recipientId => find(state.openedChats, c => c.account.id === recipientId),
currentChat: (state) => state.openedChats[state.currentChatId],
currentChatMessageService: (state) =>
state.openedChatMessageServices[state.currentChatId],
findOpenedChatByRecipientId: (state) => (recipientId) =>
find(state.openedChats, (c) => c.account.id === recipientId),
sortedChatList,
unreadChatCount
unreadChatCount,
},
actions: {
// Chat list
startFetchingChats ({ dispatch, commit }) {
startFetchingChats({ dispatch, commit }) {
const fetcher = () => dispatch('fetchChats', { latest: true })
fetcher()
commit('setChatListFetcher', {
fetcher: () => promiseInterval(fetcher, 5000)
fetcher: () => promiseInterval(fetcher, 5000),
})
},
stopFetchingChats ({ commit }) {
stopFetchingChats({ commit }) {
commit('setChatListFetcher', { fetcher: undefined })
},
fetchChats ({ dispatch, rootState }) {
return rootState.api.backendInteractor.chats()
.then(({ chats }) => {
dispatch('addNewChats', { chats })
return chats
})
fetchChats({ dispatch, rootState }) {
return rootState.api.backendInteractor.chats().then(({ chats }) => {
dispatch('addNewChats', { chats })
return chats
})
},
addNewChats (store, { chats }) {
addNewChats(store, { chats }) {
const { commit, dispatch, rootGetters } = store
const newChatMessageSideEffects = (chat) => {
maybeShowChatNotification(store, chat)
}
commit('addNewUsers', chats.map(k => k.account).filter(k => k))
commit('addNewChats', { dispatch, chats, rootGetters, newChatMessageSideEffects })
commit(
'addNewUsers',
chats.map((k) => k.account).filter((k) => k),
)
commit('addNewChats', {
dispatch,
chats,
rootGetters,
newChatMessageSideEffects,
})
},
updateChat ({ commit }, { chat }) {
updateChat({ commit }, { chat }) {
commit('updateChat', { chat })
},
// Opened Chats
startFetchingCurrentChat ({ dispatch }, { fetcher }) {
startFetchingCurrentChat({ dispatch }, { fetcher }) {
dispatch('setCurrentChatFetcher', { fetcher })
},
setCurrentChatFetcher ({ commit }, { fetcher }) {
setCurrentChatFetcher({ commit }, { fetcher }) {
commit('setCurrentChatFetcher', { fetcher })
},
addOpenedChat ({ commit, dispatch }, { chat }) {
addOpenedChat({ commit, dispatch }, { chat }) {
commit('addOpenedChat', { dispatch, chat: parseChat(chat) })
dispatch('addNewUsers', [chat.account])
},
addChatMessages ({ commit }, value) {
addChatMessages({ commit }, value) {
commit('addChatMessages', { commit, ...value })
},
resetChatNewMessageCount ({ commit }, value) {
resetChatNewMessageCount({ commit }, value) {
commit('resetChatNewMessageCount', value)
},
clearCurrentChat ({ commit }) {
clearCurrentChat({ commit }) {
commit('setCurrentChatId', { chatId: undefined })
commit('setCurrentChatFetcher', { fetcher: undefined })
},
readChat ({ rootState, commit, dispatch }, { id, lastReadId }) {
readChat({ rootState, commit, dispatch }, { id, lastReadId }) {
const isNewMessage = rootState.chats.lastReadMessageId !== lastReadId
dispatch('resetChatNewMessageCount')
@ -103,40 +116,40 @@ const chats = {
rootState.api.backendInteractor.readChat({ id, lastReadId })
}
},
deleteChatMessage ({ rootState, commit }, value) {
deleteChatMessage({ rootState, commit }, value) {
rootState.api.backendInteractor.deleteChatMessage(value)
commit('deleteChatMessage', { commit, ...value })
},
resetChats ({ commit, dispatch }) {
resetChats({ commit, dispatch }) {
dispatch('clearCurrentChat')
commit('resetChats', { commit })
},
clearOpenedChats ({ commit }) {
clearOpenedChats({ commit }) {
commit('clearOpenedChats', { commit })
},
handleMessageError ({ commit }, value) {
handleMessageError({ commit }, value) {
commit('handleMessageError', { commit, ...value })
},
cullOlderMessages ({ commit }, chatId) {
cullOlderMessages({ commit }, chatId) {
commit('cullOlderMessages', chatId)
}
},
},
mutations: {
setChatListFetcher (state, { fetcher }) {
setChatListFetcher(state, { fetcher }) {
const prevFetcher = state.chatListFetcher
if (prevFetcher) {
prevFetcher.stop()
}
state.chatListFetcher = fetcher && fetcher()
},
setCurrentChatFetcher (state, { fetcher }) {
setCurrentChatFetcher(state, { fetcher }) {
const prevFetcher = state.fetcher
if (prevFetcher) {
prevFetcher.stop()
}
state.fetcher = fetcher && fetcher()
},
addOpenedChat (state, { chat }) {
addOpenedChat(state, { chat }) {
state.currentChatId = chat.id
state.openedChats[chat.id] = chat
@ -144,15 +157,17 @@ const chats = {
state.openedChatMessageServices[chat.id] = chatService.empty(chat.id)
}
},
setCurrentChatId (state, { chatId }) {
setCurrentChatId(state, { chatId }) {
state.currentChatId = chatId
},
addNewChats (state, { chats, newChatMessageSideEffects }) {
addNewChats(state, { chats, newChatMessageSideEffects }) {
chats.forEach((updatedChat) => {
const chat = getChatById(state, updatedChat.id)
if (chat) {
const isNewMessage = (chat.lastMessage && chat.lastMessage.id) !== (updatedChat.lastMessage && updatedChat.lastMessage.id)
const isNewMessage =
(chat.lastMessage && chat.lastMessage.id) !==
(updatedChat.lastMessage && updatedChat.lastMessage.id)
chat.lastMessage = updatedChat.lastMessage
chat.unread = updatedChat.unread
chat.updated_at = updatedChat.updated_at
@ -165,23 +180,28 @@ const chats = {
}
})
},
updateChat (state, { chat: updatedChat }) {
updateChat(state, { chat: updatedChat }) {
const chat = getChatById(state, updatedChat.id)
if (chat) {
chat.lastMessage = updatedChat.lastMessage
chat.unread = updatedChat.unread
chat.updated_at = updatedChat.updated_at
}
if (!chat) { state.chatList.data.unshift(updatedChat) }
if (!chat) {
state.chatList.data.unshift(updatedChat)
}
state.chatList.idStore[updatedChat.id] = updatedChat
},
deleteChat (state, { id }) {
state.chats.data = state.chats.data.filter(conversation =>
conversation.last_status.id !== id
deleteChat(state, { id }) {
state.chats.data = state.chats.data.filter(
(conversation) => conversation.last_status.id !== id,
)
state.chats.idStore = omitBy(
state.chats.idStore,
(conversation) => conversation.last_status.id === id,
)
state.chats.idStore = omitBy(state.chats.idStore, conversation => conversation.last_status.id === id)
},
resetChats (state, { commit }) {
resetChats(state, { commit }) {
state.chatList = emptyChatList()
state.currentChatId = null
commit('setChatListFetcher', { fetcher: undefined })
@ -191,27 +211,31 @@ const chats = {
delete state.openedChatMessageServices[chatId]
}
},
setChatsLoading (state, { value }) {
setChatsLoading(state, { value }) {
state.chats.loading = value
},
addChatMessages (state, { chatId, messages, updateMaxId }) {
addChatMessages(state, { chatId, messages, updateMaxId }) {
const chatMessageService = state.openedChatMessageServices[chatId]
if (chatMessageService) {
chatService.add(chatMessageService, { messages: messages.map(parseChatMessage), updateMaxId })
chatService.add(chatMessageService, {
messages: messages.map(parseChatMessage),
updateMaxId,
})
}
},
deleteChatMessage (state, { chatId, messageId }) {
deleteChatMessage(state, { chatId, messageId }) {
const chatMessageService = state.openedChatMessageServices[chatId]
if (chatMessageService) {
chatService.deleteMessage(chatMessageService, messageId)
}
},
resetChatNewMessageCount (state) {
const chatMessageService = state.openedChatMessageServices[state.currentChatId]
resetChatNewMessageCount(state) {
const chatMessageService =
state.openedChatMessageServices[state.currentChatId]
chatService.resetNewMessageCount(chatMessageService)
},
// Used when a connection loss occurs
clearOpenedChats (state) {
clearOpenedChats(state) {
const currentChatId = state.currentChatId
for (const chatId in state.openedChats) {
if (currentChatId !== chatId) {
@ -221,21 +245,21 @@ const chats = {
}
}
},
readChat (state, { id, lastReadId }) {
readChat(state, { id, lastReadId }) {
state.lastReadMessageId = lastReadId
const chat = getChatById(state, id)
if (chat) {
chat.unread = 0
}
},
handleMessageError (state, { chatId, fakeId, isRetry }) {
handleMessageError(state, { chatId, fakeId, isRetry }) {
const chatMessageService = state.openedChatMessageServices[chatId]
chatService.handleMessageError(chatMessageService, fakeId, isRetry)
},
cullOlderMessages (state, chatId) {
cullOlderMessages(state, chatId) {
chatService.cullOlderMessages(state.openedChatMessageServices[chatId])
}
}
},
},
}
export default chats

View file

@ -1,186 +0,0 @@
import Cookies from 'js-cookie'
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([
'sidebarColumnWidth',
'contentColumnWidth',
'notifsColumnWidth',
'themeEditorMinWidth',
'textSize',
'navbarSize',
'panelHeaderSize',
'forcedRoundness',
'emojiSize',
'emojiReactionsScale'
])
/* 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
* in general.
*/
export const multiChoiceProperties = [
'postContentType',
'subjectLineBehavior',
'conversationDisplay', // tree | linear
'conversationOtherRepliesButton', // below | inside
'mentionLinkDisplay', // short | full_for_remote | full
'userPopoverAvatarAction', // close | zoom | open
'unsavedPostAction' // save | discard | confirm
]
// caching the instance default properties
export const instanceDefaultProperties = Object.entries(defaultState)
.filter(([, value]) => value === undefined)
.map(([key]) => key)
const config = {
state: { ...defaultState },
getters: {
defaultConfig (state, getters, rootState) {
const { instance } = rootState
return {
...defaultState,
...Object.fromEntries(
instanceDefaultProperties.map(key => [key, instance[key]])
)
}
},
mergedConfig (state, getters, rootState, rootGetters) {
const { defaultConfig } = rootGetters
return {
...defaultConfig,
// Do not override with undefined
...Object.fromEntries(Object.entries(state).filter(([, v]) => v !== undefined))
}
}
},
mutations: {
setOptionTemporarily (state, { name, value }) {
set(state, name, value)
applyConfig(state)
},
setOption (state, { name, value }) {
set(state, name, value)
},
setHighlight (state, { user, color, type }) {
const data = this.state.config.highlight[user]
if (color || type) {
state.highlight[user] = { color: color || data.color, type: type || data.type }
} else {
delete state.highlight[user]
}
}
},
actions: {
loadSettings ({ dispatch }, data) {
const knownKeys = new Set(Object.keys(defaultState))
const presentKeys = new Set(Object.keys(data))
const intersection = new Set()
for (const elem of presentKeys) {
if (knownKeys.has(elem)) {
intersection.add(elem)
}
}
intersection.forEach(
name => dispatch('setOption', { name, value: data[name] })
)
},
setHighlight ({ commit }, { user, color, type }) {
commit('setHighlight', { user, color, type })
},
setOptionTemporarily ({ commit, dispatch, state }, { name, value }) {
if (useInterfaceStore().temporaryChangesTimeoutId !== null) {
console.warn('Can\'t track more than one temporary change')
return
}
const oldValue = state[name]
commit('setOptionTemporarily', { name, value })
const confirm = () => {
dispatch('setOption', { name, value })
useInterfaceStore().clearTemporaryChanges()
}
const revert = () => {
commit('setOptionTemporarily', { name, value: oldValue })
useInterfaceStore().clearTemporaryChanges()
}
useInterfaceStore().setTemporaryChanges({
timeoutId: setTimeout(revert, 10000),
confirm,
revert
})
},
setThemeV2 ({ commit, dispatch }, { customTheme, customThemeSource }) {
commit('setOption', { name: 'theme', value: 'custom' })
commit('setOption', { name: 'customTheme', value: customTheme })
commit('setOption', { name: 'customThemeSource', value: customThemeSource })
dispatch('setTheme', { themeData: customThemeSource, recompile: true })
},
setOption ({ commit, dispatch, state }, { name, value }) {
const exceptions = new Set([
'useStreamingApi'
])
if (exceptions.has(name)) {
switch (name) {
case 'useStreamingApi': {
const action = value ? 'enableMastoSockets' : 'disableMastoSockets'
dispatch(action).then(() => {
commit('setOption', { name: 'useStreamingApi', value })
}).catch((e) => {
console.error('Failed starting MastoAPI Streaming socket', e)
dispatch('disableMastoSockets')
dispatch('setOption', { name: 'useStreamingApi', value: false })
})
break
}
}
} else {
commit('setOption', { name, value })
if (APPEARANCE_SETTINGS_KEYS.has(name)) {
applyConfig(state)
}
if (name.startsWith('theme3hacks')) {
dispatch('applyTheme', { recompile: true })
}
switch (name) {
case 'theme':
if (value === 'custom') break
dispatch('setTheme', { themeName: value, recompile: true, saveData: true })
break
case 'themeDebug': {
dispatch('setTheme', { recompile: true })
break
}
case 'interfaceLanguage':
messages.setLanguage(useI18nStore().i18n, value)
dispatch('loadUnicodeEmojiData', value)
Cookies.set(
BACKEND_LANGUAGE_COOKIE_NAME,
localeService.internalToBackendLocaleMulti(value)
)
break
case 'thirdColumnMode':
useInterfaceStore().setLayoutWidth(undefined)
break
}
}
}
}
}
export default config

View file

@ -1,42 +0,0 @@
export const CONFIG_MIGRATION = 1
import { v4 as uuidv4 } from 'uuid';
// for future use
/*
const simpleDeclaration = {
store: 'server-side',
migrationFlag: 'configMigration',
migration(serverside, rootState) {
serverside.setPreference({ path: 'simple.' + field, value: rootState.config[oldField ?? field] })
}
}
*/
export const declarations = [
{
field: 'muteFilters',
store: 'server-side',
migrationFlag: 'configMigration',
migrationNum: 1,
description: 'Mute filters, wordfilter/regexp/etc',
valueType: 'complex',
migration (serverside, rootState) {
rootState.config.muteWords.forEach((word, order) => {
const uniqueId = uuidv4()
serverside.setPreference({
path: 'simple.muteFilters.' + uniqueId,
value: {
type: 'word',
value: word,
name: word,
enabled: true,
expires: null,
hide: false,
order
}
})
})
}
}
]

View file

@ -1,175 +1,827 @@
const browserLocale = (window.navigator.language || 'en').split('-')[0]
import { get, set } from 'lodash'
export const defaultState = {
expertLevel: 0, // used to track which settings to show and hide
const browserLocale = (navigator.language || 'en').split('-')[0]
// Theme stuff
theme: undefined, // Very old theme store, stores preset name, still in use
export const convertDefinitions = (definitions) =>
Object.fromEntries(
Object.entries(definitions).map(([k, v]) => {
const defaultValue = v.default ?? null
return [k, defaultValue]
}),
)
// 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
}
/// Instance config entries provided by static config or pleroma api
/// Put settings here only if it does not make sense for a normal user
/// to override it.
export const INSTANCE_IDENTITY_DEFAULT_DEFINITIONS = {
style: {
description: 'Instance default style name',
type: 'string',
required: false,
},
palette: {
description: 'Instance default palette name',
type: 'string',
required: false,
},
theme: {
description: 'Instance default theme name',
type: 'string',
required: false,
},
defaultAvatar: {
description: "Default avatar image to use when user doesn't have one set",
type: 'string',
default: '/images/avi.png',
},
defaultBanner: {
description: "Default banner image to use when user doesn't have one set",
type: 'string',
default: '/images/banner.png',
},
background: {
description: 'Instance background/wallpaper',
type: 'string',
default: '/static/aurora_borealis.jpg',
},
embeddedToS: {
description: 'Whether to show Terms of Service title bar',
type: 'boolean',
default: true,
},
logo: {
description: 'Instance logo',
type: 'string',
default: '/static/logo.svg',
},
logoMargin: {
description: 'Margin for logo (spacing above/below)',
type: 'string',
default: '.2em',
},
logoMask: {
description:
'Use logo as a mask (works well for monochrome/transparent logos)',
type: 'boolean',
default: true,
},
logoLeft: {
description: 'Show logo on the left side of navbar',
type: 'boolean',
default: false,
},
redirectRootLogin: {
description: 'Where to redirect user after login',
type: 'string',
default: '/main/friends',
},
redirectRootNoLogin: {
description: 'Where to redirect anonymous visitors',
type: 'string',
default: '/main/all',
},
hideSitename: {
description: 'Hide the instance name in navbar',
type: 'boolean',
default: false,
},
nsfwCensorImage: {
description: 'Default NSFW censor image',
type: 'string',
required: false,
},
showFeaturesPanel: {
description: 'Show features panel to anonymous visitors',
type: 'boolean',
default: true,
},
showInstanceSpecificPanel: {
description: 'Show instance-specific panel',
type: 'boolean',
default: false,
},
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',
// Html stuff
instanceSpecificPanelContent: {
description: 'HTML of Instance-specific panel',
type: 'string',
required: false,
},
tos: {
description: 'HTML of Terms of Service panel',
type: 'string',
required: false,
},
name: {
description: 'Instance Name',
type: 'string',
required: false,
},
}
export const INSTANCE_IDENTITY_DEFAULT = convertDefinitions(
INSTANCE_IDENTITY_DEFAULT_DEFINITIONS,
)
export const INSTANCE_IDENTIY_EXTERNAL = new Set([
'tos',
'instanceSpecificPanelContent',
])
/// This object contains setting entries that makes sense
/// at the user level. The defaults can also be overriden by
/// instance admins in the frontend_configuration endpoint or static config.
export const INSTANCE_DEFAULT_CONFIG_DEFINITIONS = {
expertLevel: {
description:
'Used to track which settings to show and hide in settings modal',
type: 'number', // not a boolean so we could potentially make multiple levels of expert-ness
default: 0,
},
hideISP: {
description: 'Hide Instance-specific panel',
default: false,
},
allowForeignUserBackground: {
description: "Allow other user's profiles to override wallpaper",
default: true,
},
hideInstanceWallpaper: {
description: 'Hide Instance default background',
default: false,
},
hideShoutbox: {
description: 'Hide shoutbox if present',
default: false,
},
hideMutedPosts: {
// bad name
description: 'Hide posts of muted users entirely',
default: false,
},
hideMutedThreads: {
description: 'Hide muted threads entirely',
default: true,
},
hideWordFilteredPosts: {
description: 'Hide wordfiltered posts entirely',
default: false,
},
muteBotStatuses: {
description: 'Mute posts made by bots',
default: false,
},
muteSensitiveStatuses: {
description: 'Mute posts marked as NSFW',
default: false,
},
collapseMessageWithSubject: {
description: 'Collapse posts with subject',
default: false,
},
padEmoji: {
description: 'Pad emoji with spaces when using emoji picker',
default: true,
},
hideAttachmentsInConv: {
description: 'Hide attachments',
default: false,
},
hideScrobbles: {
description: 'Hide scrobbles',
default: false,
},
hideScrobblesAfter: {
description: 'Hide scrobbles older than',
default: '2d',
},
maxThumbnails: {
description: 'Maximum attachments to show',
default: 16,
},
loopVideo: {
description: 'Loop videos',
default: true,
},
loopVideoSilentOnly: {
description: 'Loop only videos without sound',
default: true,
},
/// This is not the streaming API configuration, but rather an option
/// for automatically loading new posts into the timeline without
/// the user clicking the Show New button.
streaming: {
description: 'Automatically show new posts',
default: false,
},
pauseOnUnfocused: {
description: 'Pause showing new posts when tab is unfocused',
default: true,
},
emojiReactionsOnTimeline: {
description: 'Show emoji reactions on timeline',
default: true,
},
alwaysShowNewPostButton: {
description: 'Always show mobile "new post" button, even in desktop mode',
default: false,
},
autohideFloatingPostButton: {
description:
'Automatically hide mobile "new post" button when scrolling down',
default: false,
},
stopGifs: {
description: 'Play animated gifs on hover only',
default: true,
},
nonSquareEmoji: {
description: 'Allow emoji to be non-square (max 3:1 aspect)',
default: true,
},
replyVisibility: {
description: 'Type of replies to show',
default: 'all',
},
thirdColumnMode: {
description: 'What to display in third column',
default: 'notifications',
},
notificationVisibility: {
follows: true,
mentions: true,
statuses: true,
likes: true,
repeats: true,
moves: true,
emojiReactions: true,
followRequest: true,
reports: true,
chatMention: true,
polls: true
description: 'What types of notifications to show',
default: {
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
description: 'What type of notifications to show desktop notification for',
default: {
follows: true,
mentions: true,
statuses: true,
likes: false,
repeats: false,
moves: false,
emojiReactions: false,
followRequest: true,
reports: true,
chatMention: true,
polls: true,
},
},
webPushNotifications: {
description: 'Use WebPush',
default: false,
},
webPushAlwaysShowNotifications: {
description: 'Ignore filter when using WebPush',
default: false,
},
interfaceLanguage: {
description: 'UI language',
default: [browserLocale],
},
hideScopeNotice: {
description: 'Hide scope notification',
default: false,
},
scopeCopy: {
description: 'Copy scope like mastodon does',
default: true,
},
subjectLineBehavior: {
description: 'How to treat subject line',
default: 'email',
},
alwaysShowSubjectInput: {
description: 'Always show subject line field',
default: true,
},
minimalScopesMode: {
description: 'Minimize amount of options shown in scope selector',
default: false,
},
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
hideFilteredStatuses: {
description: 'Hide wordfiltered entirely',
default: false,
},
// Confirmations
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
modalOnRepeat: {
description: 'Show confirmation modal for repeat',
default: false,
},
modalOnUnfollow: {
description: 'Show confirmation modal for unfollow',
default: false,
},
modalOnBlock: {
description: 'Show confirmation modal for block',
default: true,
},
modalOnMute: {
description: 'Show confirmation modal for mute',
default: false,
},
modalOnMuteConversation: {
description: 'Show confirmation modal for mute conversation',
default: false,
},
modalOnMuteDomain: {
description: 'Show confirmation modal for mute domain',
default: true,
},
modalOnDelete: {
description: 'Show confirmation modal for delete',
default: true,
},
modalOnLogout: {
description: 'Show confirmation modal for logout',
default: true,
},
modalOnApproveFollow: {
description: 'Show confirmation modal for approve follow',
default: false,
},
modalOnDenyFollow: {
description: 'Show confirmation modal for deny follow',
default: false,
},
modalOnRemoveUserFromFollowers: {
description: 'Show confirmation modal for follower removal',
default: false,
},
// Expiry confirmations/default actions
onMuteDefaultAction: 'ask',
onBlockDefaultAction: 'ask',
onMuteDefaultAction: {
description: 'Default action when muting user',
default: 'ask',
},
onBlockDefaultAction: {
description: 'Default action when blocking user',
default: 'ask',
},
modalMobileCenter: undefined,
playVideosInModal: false,
useOneClickNsfw: false,
useContainFit: true,
disableStickyHeaders: false,
showScrollbars: false,
userPopoverAvatarAction: 'open',
userPopoverOverlay: false,
userCardLeftJustify: false,
userCardHidePersonalMarks: false,
sidebarColumnWidth: '25rem',
contentColumnWidth: '45rem',
notifsColumnWidth: '25rem',
themeEditorMinWidth: undefined, // instance default
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
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
modalMobileCenter: {
description: 'Center mobile dialogs vertically',
default: false,
},
playVideosInModal: {
description: 'Play videos in gallery view',
default: false,
},
useContainFit: {
description: 'Use object-fit: contain for attachments',
default: true,
},
disableStickyHeaders: {
description: 'Disable sticky headers',
default: false,
},
showScrollbars: {
description: 'Always show scrollbars',
default: false,
},
userPopoverAvatarAction: {
description: 'What to do when clicking popover avatar',
default: 'open',
},
userPopoverOverlay: {
description: 'Overlay user popover with centering on avatar',
default: false,
},
userCardLeftJustify: {
description: 'Justify user bio to the left',
default: false,
},
userCardHidePersonalMarks: {
description: 'Hide highlight/personal note in user view',
default: false,
},
forcedRoundness: {
description: 'Force roundness of the theme',
default: -1,
},
greentext: {
description: 'Highlight plaintext >quotes',
default: false,
},
mentionLinkShowTooltip: {
description: 'Show tooltips for mention links',
default: true,
},
mentionLinkShowAvatar: {
description: 'Show avatar next to mention link',
default: false,
},
mentionLinkFadeDomain: {
description:
'Mute (fade) domain name in mention links if configured to show it',
default: true,
},
mentionLinkShowYous: {
description: 'Show (you)s when you are mentioned',
default: false,
},
mentionLinkBoldenYou: {
description: 'Boldern mentionlink of you',
default: true,
},
hidePostStats: {
description: 'Hide post stats (rt, favs)',
default: false,
},
hideBotIndication: {
description: 'Hide bot indicator',
default: false,
},
hideUserStats: {
description: 'Hide user stats (followers etc)',
default: false,
},
virtualScrolling: {
description: 'Timeline virtual scrolling',
default: true,
},
sensitiveByDefault: {
description: 'Assume attachments are NSFW by default',
default: false,
},
conversationDisplay: {
description: 'Style of conversation display',
default: 'linear',
},
conversationTreeAdvanced: {
description: 'Advanced features of tree view conversation',
default: false,
},
conversationOtherRepliesButton: {
description: 'Where to show "other replies" in tree conversation view',
default: 'below',
},
conversationTreeFadeAncestors: {
description: 'Fade ancestors in tree conversation view',
default: false,
},
showExtraNotifications: {
description:
'Show extra notifications (chats, announcements etc) in notification panel',
default: true,
},
showExtraNotificationsTip: {
description: 'Show tip for extra notifications (that user can remove them)',
default: true,
},
showChatsInExtraNotifications: {
description: 'Show chat messages in notifications',
default: true,
},
showAnnouncementsInExtraNotifications: {
description: 'Show announcements in notifications',
default: true,
},
showFollowRequestsInExtraNotifications: {
description: 'Show follow requests in notifications',
default: true,
},
maxDepthInThread: {
description: 'Maximum depth in tree conversation view',
default: 6,
},
autocompleteSelect: {
description: '',
default: false,
},
closingDrawerMarksAsSeen: {
description: 'Closing mobile notification pane marks everything as seen',
default: true,
},
unseenAtTop: {
description: 'Show unseen notifications above others',
default: false,
},
ignoreInactionableSeen: {
description: 'Treat inactionable (fav, rt etc) notifications as "seen"',
default: false,
},
unsavedPostAction: {
description: 'What to do if post is aborted',
default: 'confirm',
},
autoSaveDraft: {
description: 'Save drafts automatically',
default: false,
},
useAbsoluteTimeFormat: {
description: 'Use absolute time format',
default: false,
},
absoluteTimeFormatMinAge: {
description: 'Show absolute time format only after this post age',
default: '0d',
},
absoluteTime12h: {
description: 'Use 24h time format',
default: '24h',
},
themeChecksum: {
description: 'Checksum of theme used',
type: 'string',
required: false,
},
highlights: {
description: 'User highlights',
type: 'object',
required: false,
default: {},
},
underlay: {
description: 'Underlay override',
required: true,
default: 'none',
},
compactProfiles: {
description: 'Reduce profile height on user pages',
default: false,
},
}
export const INSTANCE_DEFAULT_CONFIG = convertDefinitions(
INSTANCE_DEFAULT_CONFIG_DEFINITIONS,
)
export const LOCAL_DEFAULT_CONFIG_DEFINITIONS = {
// TODO these two used to be separate but since separation feature got broken it doesn't matter
hideAttachments: {
description: 'Hide attachments in timeline',
default: false,
},
hideAttachmentsInConv: {
description: 'Hide attachments in coversation',
default: false,
},
hideNsfw: {
description: 'Hide nsfw posts',
default: true,
},
useOneClickNsfw: {
description: 'Open NSFW images directly in media modal',
default: false,
},
preloadImage: {
description: 'Preload images for NSFW',
default: true,
},
postContentType: {
description: 'Default post content type',
default: 'text/plain',
},
sidebarRight: {
description: 'Reverse order of columns',
default: false,
},
sidebarColumnWidth: {
description: 'Sidebar column width',
default: '25rem',
},
contentColumnWidth: {
description: 'Middle column width',
default: '45rem',
},
notifsColumnWidth: {
description: 'Notifications column width',
default: '25rem',
},
themeEditorMinWidth: {
description: 'Hack for theme editor on mobile',
default: '0rem',
},
emojiReactionsScale: {
description: 'Emoji reactions scale factor',
default: 0.5,
},
textSize: {
description: 'Font size',
default: '1rem',
},
emojiSize: {
description: 'Emoji size',
default: '2.2rem',
},
navbarSize: {
description: 'Navbar size',
default: '3.5rem',
},
panelHeaderSize: {
description: 'Panel header size',
default: '3.2rem',
},
navbarColumnStretch: {
description: 'Stretch navbar to match columns width',
default: false,
},
mentionLinkDisplay: {
description: 'How to display mention links',
default: 'short',
},
imageCompression: {
description: 'Image compression (WebP/JPEG)',
default: true,
},
alwaysUseJpeg: {
description: 'Compress images using JPEG only',
default: false,
},
useStreamingApi: {
description: 'Streaming API (WebSocket)',
default: false,
},
fontInterface: {
description: 'Interface font override',
type: 'string',
default: null,
},
fontInput: {
description: 'Input font override',
type: 'string',
default: null,
},
fontPosts: {
description: 'Post font override',
type: 'string',
default: null,
},
fontMonospace: {
description: 'Monospace font override',
type: 'string',
default: null,
},
themeDebug: {
description:
'Debug mode that uses computed backgrounds instead of real ones to debug contrast functions',
default: false,
},
forceThemeRecompilation: {
description: 'Flag that forces recompilation on boot even if cache exists',
default: false,
},
}
export const LOCAL_DEFAULT_CONFIG = convertDefinitions(
LOCAL_DEFAULT_CONFIG_DEFINITIONS,
)
export const LOCAL_ONLY_KEYS = new Set(Object.keys(LOCAL_DEFAULT_CONFIG))
export const SYNC_DEFAULT_CONFIG_DEFINITIONS = {
dontShowUpdateNotifs: {
description: 'Never show update notification (pleroma-tan)',
default: false,
},
collapseNav: {
description: 'Collapse navigation panel to header only',
default: false,
},
muteFilters: {
description: 'Object containing mute filters',
type: 'object',
default: {},
},
}
export const SYNC_DEFAULT_CONFIG = convertDefinitions(
SYNC_DEFAULT_CONFIG_DEFINITIONS,
)
export const SYNC_ONLY_KEYS = new Set(Object.keys(SYNC_DEFAULT_CONFIG))
export const THEME_CONFIG_DEFINITIONS = {
theme: {
description: 'Very old theme store, stores preset name, still in use',
default: null,
},
colors: {
description:
'VERY old theme store, just colors of V1, probably not even used anymore',
default: {},
},
// V2
customTheme: {
description:
'"Snapshot", previously was used as actual theme store for V2 so it\'s still used in case of PleromaFE downgrade event.',
default: null,
},
customThemeSource: {
description: '"Source", stores original theme data',
default: null,
},
// V3
style: {
description: 'Style name for builtins',
default: null,
},
styleCustomData: {
description: 'Custom style data (i.e. not builtin)',
default: null,
},
palette: {
description: 'Palette name for builtins',
default: null,
},
paletteCustomData: {
description: 'Custom palette data (i.e. not builtin)',
default: null,
},
}
export const THEME_CONFIG = convertDefinitions(THEME_CONFIG_DEFINITIONS)
export const makeUndefined = (c) =>
Object.fromEntries(Object.keys(c).map((key) => [key, undefined]))
/// For properties with special processing or properties that does not
/// make sense to be overriden on a instance-wide level.
export const ROOT_CONFIG = {
// Set these to undefined so it does not interfere with default settings check
...INSTANCE_DEFAULT_CONFIG,
...LOCAL_DEFAULT_CONFIG,
...SYNC_DEFAULT_CONFIG,
...THEME_CONFIG,
}
export const ROOT_CONFIG_DEFINITIONS = {
...INSTANCE_DEFAULT_CONFIG_DEFINITIONS,
...LOCAL_DEFAULT_CONFIG_DEFINITIONS,
...SYNC_DEFAULT_CONFIG_DEFINITIONS,
...THEME_CONFIG_DEFINITIONS,
}
export const validateSetting = ({
value,
path: fullPath,
definition,
throwError,
defaultState,
validateObjects = true,
}) => {
if (value === undefined) return undefined // only null is allowed as missing value
if (definition === undefined) return undefined // invalid definition
const path = fullPath.replace(/^simple./, '')
const depth = path.split('.')
if (
validateObjects &&
definition.type === 'object' &&
path.split('.').length <= 1
) {
console.error(
`attempt to set object ${fullPath} instead of its children. ignoring.`,
)
return undefined
}
if (get(defaultState, path.split('.')[0]) === undefined) {
const string = `Unknown option ${fullPath}, value: ${value}`
if (throwError) {
throw new Error(string)
} else {
console.error(string)
return undefined
}
}
let { required, type, default: defaultValue } = definition
if (type == null && defaultValue != null) {
type = typeof defaultValue
}
if (required && value == null) {
const string = `Value required for setting ${path} but was provided nullish; defaulting`
if (throwError) {
throw new Error(string)
} else {
console.error(string)
return defaultValue
}
}
if (depth > 2 && value !== null && type != null && typeof value !== type) {
const string = `Invalid type for setting ${path}: expected type ${type}, got ${typeof value}, value ${value}; defaulting`
if (throwError) {
throw new Error(string)
} else {
console.error(string)
return defaultValue
}
}
return value
}

View file

@ -1,19 +1,19 @@
import { storage } from 'src/lib/storage.js'
export const defaultState = {
drafts: {}
drafts: {},
}
export const mutations = {
addOrSaveDraft (state, { draft }) {
addOrSaveDraft(state, { draft }) {
state.drafts[draft.id] = draft
},
abandonDraft (state, { id }) {
abandonDraft(state, { id }) {
delete state.drafts[id]
},
loadDrafts (state, data) {
loadDrafts(state, data) {
state.drafts = data
}
},
}
const storageKey = 'pleroma-fe-drafts'
@ -30,7 +30,11 @@ const storageKey = 'pleroma-fe-drafts'
* different keys, which will just pollute the whole storage.
* It is indeed best to have backend support for this.
*/
const getStorageData = async () => ((await storage.getItem(storageKey)) || {})
const getStorageData = async () =>
(await storage.getItem(storageKey)) ||
{
/* no-op */
}
const saveDraftToStorage = async (draft) => {
const currentData = await getStorageData()
@ -38,49 +42,58 @@ const saveDraftToStorage = async (draft) => {
await storage.setItem(storageKey, currentData)
}
const deleteDraftFromStorage = async (id) => {
const deleteDraftFromStorage = async (ids) => {
const currentData = await getStorageData()
delete currentData[id]
ids.forEach((id) => {
delete currentData[id]
})
await storage.setItem(storageKey, currentData)
}
export const actions = {
async addOrSaveDraft (store, { draft }) {
const id = draft.id || (new Date().getTime()).toString()
async addOrSaveDraft(store, { draft }) {
const id = draft.id || new Date().getTime().toString()
const draftWithId = { ...draft, id }
store.commit('addOrSaveDraft', { draft: draftWithId })
await saveDraftToStorage(draftWithId)
return id
},
async abandonDraft (store, { id }) {
async abandonDraft(store, { id }) {
store.commit('abandonDraft', { id })
await deleteDraftFromStorage(id)
await deleteDraftFromStorage([id])
},
async loadDrafts (store) {
async abandonAllDrafts(store) {
const ids = Object.keys(store.state.drafts)
ids.forEach((id) => store.commit('abandonDraft', { id }))
await deleteDraftFromStorage(ids)
},
async loadDrafts(store) {
const currentData = await getStorageData()
store.commit('loadDrafts', currentData)
}
},
}
export const getters = {
draftsByTypeAndRefId (state) {
draftsByTypeAndRefId(state) {
return (type, refId) => {
return Object.values(state.drafts).filter(draft => draft.type === type && draft.refId === refId)
return Object.values(state.drafts).filter(
(draft) => draft.type === type && draft.refId === refId,
)
}
},
draftsArray (state) {
draftsArray(state) {
return Object.values(state.drafts)
},
draftCount (state) {
draftCount(state) {
return Object.values(state.drafts).length
}
},
}
const drafts = {
state: defaultState,
mutations,
getters,
actions
actions,
}
export default drafts

View file

@ -1,23 +1,19 @@
import instance from './instance.js'
import statuses from './statuses.js'
import notifications from './notifications.js'
import users from './users.js'
import api from './api.js'
import config from './config.js'
import profileConfig from './profileConfig.js'
import adminSettings from './adminSettings.js'
import drafts from './drafts.js'
import api from './api.js'
import chats from './chats.js'
import drafts from './drafts.js'
import notifications from './notifications.js'
import profileConfig from './profileConfig.js'
import statuses from './statuses.js'
import users from './users.js'
export default {
instance,
statuses,
notifications,
users,
api,
config,
profileConfig,
adminSettings,
drafts,
chats
chats,
}

View file

@ -1,420 +0,0 @@
import apiService from '../services/api/api.service.js'
import { instanceDefaultProperties } from './config.js'
import { ensureFinalFallback } from '../i18n/languages.js'
import { useInterfaceStore } from 'src/stores/interface.js'
// See build/emojis_plugin for more details
import { annotationsLoader } from 'virtual:pleroma-fe/emoji-annotations'
const SORTED_EMOJI_GROUP_IDS = [
'smileys-and-emotion',
'people-and-body',
'animals-and-nature',
'food-and-drink',
'travel-and-places',
'activities',
'objects',
'symbols',
'flags'
]
const REGIONAL_INDICATORS = (() => {
const start = 0x1F1E6
const end = 0x1F1FF
const A = 'A'.codePointAt(0)
const res = new Array(end - start + 1)
for (let i = start; i <= end; ++i) {
const letter = String.fromCodePoint(A + i - start)
res[i - start] = {
replacement: String.fromCodePoint(i),
imageUrl: false,
displayText: 'regional_indicator_' + letter,
displayTextI18n: {
key: 'emoji.regional_indicator',
args: { letter }
}
}
}
return res
})()
const REMOTE_INTERACTION_URL = '/main/ostatus'
const defaultState = {
// Stuff from apiConfig
name: 'Pleroma FE',
registrationOpen: true,
server: 'http://localhost:4040/',
textlimit: 5000,
themesIndex: undefined,
stylesIndex: undefined,
palettesIndex: undefined,
themeData: undefined, // used for theme editor v2
vapidPublicKey: undefined,
// Stuff from static/config.json
alwaysShowSubjectInput: true,
defaultAvatar: '/images/avi.png',
defaultBanner: '/images/banner.png',
background: '/static/aurora_borealis.jpg',
embeddedToS: true,
collapseMessageWithSubject: false,
greentext: false,
mentionLinkDisplay: 'short',
mentionLinkShowTooltip: true,
mentionLinkShowAvatar: false,
mentionLinkFadeDomain: true,
mentionLinkShowYous: false,
mentionLinkBoldenYou: true,
hideFilteredStatuses: false,
// bad name: actually hides posts of muted USERS
hideMutedPosts: false,
hideMutedThreads: true,
hideWordFilteredPosts: false,
hidePostStats: false,
hideBotIndication: false,
hideSitename: false,
hideUserStats: false,
muteBotStatuses: false,
muteSensitiveStatuses: false,
modalOnRepeat: false,
modalOnUnfollow: false,
modalOnBlock: true,
modalOnMute: false,
modalOnMuteConversation: false,
modalOnMuteDomain: true,
modalOnDelete: true,
modalOnLogout: true,
modalOnApproveFollow: false,
modalOnDenyFollow: false,
modalOnRemoveUserFromFollowers: false,
modalMobileCenter: false,
loginMethod: 'password',
logo: '/static/logo.svg',
logoMargin: '.2em',
logoMask: true,
logoLeft: false,
disableUpdateNotification: false,
minimalScopesMode: false,
nsfwCensorImage: undefined,
postContentType: 'text/plain',
redirectRootLogin: '/main/friends',
redirectRootNoLogin: '/main/all',
scopeCopy: true,
showFeaturesPanel: true,
showInstanceSpecificPanel: false,
sidebarRight: false,
subjectLineBehavior: 'email',
theme: 'pleroma-dark',
palette: null,
style: null,
emojiReactionsScale: 0.5,
textSize: '1rem',
emojiSize: '2.2rem',
navbarSize: '3.5rem',
panelHeaderSize: '3.2rem',
themeEditorMinWidth: '0rem',
forcedRoundness: -1,
fontsOverride: {},
virtualScrolling: true,
sensitiveByDefault: false,
conversationDisplay: 'linear',
conversationTreeAdvanced: false,
conversationOtherRepliesButton: 'below',
conversationTreeFadeAncestors: false,
showExtraNotifications: true,
showExtraNotificationsTip: true,
showChatsInExtraNotifications: true,
showAnnouncementsInExtraNotifications: true,
showFollowRequestsInExtraNotifications: true,
maxDepthInThread: 6,
autocompleteSelect: false,
closingDrawerMarksAsSeen: true,
unseenAtTop: false,
ignoreInactionableSeen: false,
unsavedPostAction: 'confirm',
autoSaveDraft: false,
useAbsoluteTimeFormat: false,
absoluteTimeFormatMinAge: '0d',
absoluteTime12h: '24h',
// Nasty stuff
customEmoji: [],
customEmojiFetched: false,
emoji: {},
emojiFetched: false,
unicodeEmojiAnnotations: {},
pleromaExtensionsAvailable: true,
postFormats: [],
restrictedNicknames: [],
safeDM: true,
knownDomains: [],
birthdayRequired: false,
birthdayMinAge: 0,
// Feature-set, apparently, not everything here is reported...
shoutAvailable: false,
pleromaChatMessagesAvailable: false,
pleromaCustomEmojiReactionsAvailable: false,
pleromaBookmarkFoldersAvailable: false,
pleromaPublicFavouritesAvailable: true,
statusNotificationTypeAvailable: true,
gopherAvailable: false,
mediaProxyAvailable: false,
suggestionsEnabled: false,
suggestionsWeb: '',
quotingAvailable: false,
groupActorAvailable: false,
blockExpiration: false,
localBubbleInstances: [], // Akkoma
// Html stuff
instanceSpecificPanelContent: '',
tos: '',
// Version Information
backendVersion: '',
backendRepository: '',
frontendVersion: '',
pollsAvailable: false,
pollLimits: {
max_options: 4,
max_option_chars: 255,
min_expiration: 60,
max_expiration: 60 * 60 * 24
}
}
const loadAnnotations = (lang) => {
return annotationsLoader[lang]()
.then(k => k.default)
}
const injectAnnotations = (emoji, annotations) => {
const availableLangs = Object.keys(annotations)
return {
...emoji,
annotations: availableLangs.reduce((acc, cur) => {
acc[cur] = annotations[cur][emoji.replacement]
return acc
}, {})
}
}
const injectRegionalIndicators = groups => {
groups.symbols.push(...REGIONAL_INDICATORS)
return groups
}
const instance = {
state: defaultState,
mutations: {
setInstanceOption (state, { name, value }) {
if (typeof value !== 'undefined') {
state[name] = value
}
},
setKnownDomains (state, domains) {
state.knownDomains = domains
},
setUnicodeEmojiAnnotations (state, { lang, annotations }) {
state.unicodeEmojiAnnotations[lang] = annotations
}
},
getters: {
instanceDefaultConfig (state) {
return instanceDefaultProperties
.map(key => [key, state[key]])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
},
groupedCustomEmojis (state) {
const packsOf = emoji => {
const packs = emoji.tags
.filter(k => k.startsWith('pack:'))
.map(k => {
const packName = k.slice(5) // remove 'pack:' prefix
return {
id: `custom-${packName}`,
text: packName
}
})
if (!packs.length) {
return [{
id: 'unpacked'
}]
} else {
return packs
}
}
return state.customEmoji
.reduce((res, emoji) => {
packsOf(emoji).forEach(({ id: packId, text: packName }) => {
if (!res[packId]) {
res[packId] = ({
id: packId,
text: packName,
image: emoji.imageUrl,
emojis: []
})
}
res[packId].emojis.push(emoji)
})
return res
}, {})
},
standardEmojiList (state) {
return SORTED_EMOJI_GROUP_IDS
.map(groupId => (state.emoji[groupId] || []).map(k => injectAnnotations(k, state.unicodeEmojiAnnotations)))
.reduce((a, b) => a.concat(b), [])
},
standardEmojiGroupList (state) {
return SORTED_EMOJI_GROUP_IDS.map(groupId => ({
id: groupId,
emojis: (state.emoji[groupId] || []).map(k => injectAnnotations(k, state.unicodeEmojiAnnotations))
}))
},
instanceDomain (state) {
return new URL(state.server).hostname
},
remoteInteractionLink (state) {
const server = state.server.endsWith('/') ? state.server.slice(0, -1) : state.server
const link = server + REMOTE_INTERACTION_URL
return ({ statusId, nickname }) => {
if (statusId) {
return `${link}?status_id=${statusId}`
} else {
return `${link}?nickname=${nickname}`
}
}
}
},
actions: {
setInstanceOption ({ commit, dispatch }, { name, value }) {
commit('setInstanceOption', { name, value })
switch (name) {
case 'name':
useInterfaceStore().setPageTitle()
break
case 'shoutAvailable':
if (value) {
dispatch('initializeSocket')
}
break
}
},
async getStaticEmoji ({ commit }) {
try {
const values = (await import('/src/assets/emoji.json')).default
const emoji = Object.keys(values).reduce((res, groupId) => {
res[groupId] = values[groupId].map(e => ({
displayText: e.slug,
imageUrl: false,
replacement: e.emoji
}))
return res
}, {})
commit('setInstanceOption', { name: 'emoji', value: injectRegionalIndicators(emoji) })
} catch (e) {
console.warn("Can't load static emoji\n", e)
}
},
loadUnicodeEmojiData ({ commit, state }, language) {
const langList = ensureFinalFallback(language)
return Promise.all(
langList
.map(async lang => {
if (!state.unicodeEmojiAnnotations[lang]) {
try {
const annotations = await loadAnnotations(lang)
commit('setUnicodeEmojiAnnotations', { lang, annotations })
} catch (e) {
console.warn(`Error loading unicode emoji annotations for ${lang}: `, e)
// ignore
}
}
}))
},
async getCustomEmoji ({ commit, state }) {
try {
let res = await window.fetch('/api/v1/pleroma/emoji')
if (!res.ok) {
res = await window.fetch('/api/pleroma/emoji.json')
}
if (res.ok) {
const result = await res.json()
const values = Array.isArray(result) ? Object.assign({}, ...result) : result
const caseInsensitiveStrCmp = (a, b) => {
const la = a.toLowerCase()
const lb = b.toLowerCase()
return la > lb ? 1 : (la < lb ? -1 : 0)
}
const noPackLast = (a, b) => {
const aNull = a === ''
const bNull = b === ''
if (aNull === bNull) {
return 0
} else if (aNull && !bNull) {
return 1
} else {
return -1
}
}
const byPackThenByName = (a, b) => {
const packOf = emoji => (emoji.tags.filter(k => k.startsWith('pack:'))[0] || '').slice(5)
const packOfA = packOf(a)
const packOfB = packOf(b)
return noPackLast(packOfA, packOfB) || caseInsensitiveStrCmp(packOfA, packOfB) || caseInsensitiveStrCmp(a.displayText, b.displayText)
}
const emoji = Object.entries(values).map(([key, value]) => {
const imageUrl = value.image_url
return {
displayText: key,
imageUrl: imageUrl ? state.server + imageUrl : value,
tags: imageUrl ? value.tags.sort((a, b) => a > b ? 1 : 0) : ['utf'],
replacement: `:${key}: `
}
// Technically could use tags but those are kinda useless right now,
// should have been "pack" field, that would be more useful
}).sort(byPackThenByName)
commit('setInstanceOption', { name: 'customEmoji', value: emoji })
} else {
throw (res)
}
} catch (e) {
console.warn("Can't load custom emojis\n", e)
}
},
fetchEmoji ({ dispatch, state }) {
if (!state.customEmojiFetched) {
state.customEmojiFetched = true
dispatch('getCustomEmoji')
}
if (!state.emojiFetched) {
state.emojiFetched = true
dispatch('getStaticEmoji')
}
},
async getKnownDomains ({ commit, rootState }) {
try {
const result = await apiService.fetchKnownDomains({
credentials: rootState.users.currentUser.credentials
})
commit('setKnownDomains', result)
} catch (e) {
console.warn("Can't load known domains\n", e)
}
}
}
}
export default instance

View file

@ -1,18 +1,18 @@
import apiService from '../services/api/api.service.js'
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
import {
closeAllDesktopNotifications,
closeDesktopNotification,
} from '../services/desktop_notification_utils/desktop_notification_utils.js'
import {
isStatusNotification,
isValidNotification,
maybeShowNotification
maybeShowNotification,
} from '../services/notification_utils/notification_utils.js'
import {
closeDesktopNotification,
closeAllDesktopNotifications
} from '../services/desktop_notification_utils/desktop_notification_utils.js'
import { useI18nStore } from 'src/stores/i18n.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useReportsStore } from 'src/stores/reports.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
const emptyNotifications = () => ({
desktopNotificationSilence: true,
@ -20,58 +20,58 @@ const emptyNotifications = () => ({
minId: Number.POSITIVE_INFINITY,
data: [],
idStore: {},
loading: false
loading: false,
})
export const defaultState = () => ({
...emptyNotifications()
...emptyNotifications(),
})
export const notifications = {
state: defaultState(),
mutations: {
addNewNotifications (state, { notifications }) {
notifications.forEach(notification => {
addNewNotifications(state, { notifications }) {
notifications.forEach((notification) => {
state.data.push(notification)
state.idStore[notification.id] = notification
})
},
clearNotifications (state) {
clearNotifications(state) {
const blankState = defaultState()
Object.keys(state).forEach(k => {
Object.keys(state).forEach((k) => {
state[k] = blankState[k]
})
},
updateNotificationsMinMaxId (state, id) {
updateNotificationsMinMaxId(state, id) {
state.maxId = id > state.maxId ? id : state.maxId
state.minId = id < state.minId ? id : state.minId
},
setNotificationsLoading (state, { value }) {
setNotificationsLoading(state, { value }) {
state.loading = value
},
setNotificationsSilence (state, { value }) {
setNotificationsSilence(state, { value }) {
state.desktopNotificationSilence = value
},
markNotificationsAsSeen (state) {
markNotificationsAsSeen(state) {
state.data.forEach((notification) => {
notification.seen = true
})
},
markSingleNotificationAsSeen (state, { id }) {
markSingleNotificationAsSeen(state, { id }) {
const notification = state.idStore[id]
if (notification) notification.seen = true
},
dismissNotification (state, { id }) {
state.data = state.data.filter(n => n.id !== id)
dismissNotification(state, { id }) {
state.data = state.data.filter((n) => n.id !== id)
delete state.idStore[id]
},
updateNotification (state, { id, updater }) {
updateNotification(state, { id, updater }) {
const notification = state.idStore[id]
notification && updater(notification)
}
},
},
actions: {
addNewNotifications (store, { notifications }) {
addNewNotifications(store, { notifications }) {
const { commit, dispatch, state, rootState } = store
const validNotifications = notifications.filter((notification) => {
// If invalid notification, update ids but don't add it to store
@ -83,13 +83,20 @@ export const notifications = {
return true
})
const statusNotifications = validNotifications.filter(notification => isStatusNotification(notification.type) && notification.status)
const statusNotifications = validNotifications.filter(
(notification) =>
isStatusNotification(notification.type) && notification.status,
)
// Synchronous commit to add all the statuses
commit('addNewStatuses', { statuses: statusNotifications.map(notification => notification.status) })
commit('addNewStatuses', {
statuses: statusNotifications.map(
(notification) => notification.status,
),
})
// Update references to statuses in notifications to ones in the store
statusNotifications.forEach(notification => {
statusNotifications.forEach((notification) => {
const id = notification.status.id
const referenceStatus = rootState.statuses.allStatusesObject[id]
@ -98,7 +105,7 @@ export const notifications = {
}
})
validNotifications.forEach(notification => {
validNotifications.forEach((notification) => {
if (notification.type === 'pleroma:report') {
useReportsStore().addReport(notification.report)
}
@ -108,22 +115,23 @@ export const notifications = {
}
// Only add a new notification if we don't have one for the same action
// eslint-disable-next-line no-prototype-builtins
if (!state.idStore.hasOwnProperty(notification.id)) {
if (!Object.hasOwn(state.idStore, notification.id)) {
commit('updateNotificationsMinMaxId', notification.id)
commit('addNewNotifications', { notifications: [notification] })
maybeShowNotification(
store,
Object.values(useServerSideStorageStore().prefsStorage.simple.muteFilters),
notification
useMergedConfigStore().mergedConfig.notificationVisibility,
Object.values(useSyncConfigStore().prefsStorage.simple.muteFilters),
notification,
useI18nStore().i18n,
)
} else if (notification.seen) {
state.idStore[notification.id].seen = true
}
})
},
notificationClicked ({ state, dispatch }, { id }) {
notificationClicked({ state, dispatch }, { id }) {
const notification = state.idStore[id]
const { type, seen } = notification
@ -138,42 +146,46 @@ export const notifications = {
}
}
},
setNotificationsLoading ({ commit }, { value }) {
setNotificationsLoading({ commit }, { value }) {
commit('setNotificationsLoading', { value })
},
setNotificationsSilence ({ commit }, { value }) {
setNotificationsSilence({ commit }, { value }) {
commit('setNotificationsSilence', { value })
},
markNotificationsAsSeen ({ rootState, state, commit }) {
markNotificationsAsSeen({ rootState, state, commit }) {
commit('markNotificationsAsSeen')
apiService.markNotificationsAsSeen({
id: state.maxId,
credentials: rootState.users.currentUser.credentials
}).then(() => {
closeAllDesktopNotifications(rootState)
})
apiService
.markNotificationsAsSeen({
id: state.maxId,
credentials: rootState.users.currentUser.credentials,
})
.then(() => {
closeAllDesktopNotifications(rootState)
})
},
markSingleNotificationAsSeen ({ rootState, commit }, { id }) {
markSingleNotificationAsSeen({ rootState, commit }, { id }) {
commit('markSingleNotificationAsSeen', { id })
apiService.markNotificationsAsSeen({
single: true,
id,
credentials: rootState.users.currentUser.credentials
}).then(() => {
closeDesktopNotification(rootState, { id })
})
apiService
.markNotificationsAsSeen({
single: true,
id,
credentials: rootState.users.currentUser.credentials,
})
.then(() => {
closeDesktopNotification(rootState, { id })
})
},
dismissNotificationLocal ({ commit }, { id }) {
dismissNotificationLocal({ commit }, { id }) {
commit('dismissNotification', { id })
},
dismissNotification ({ rootState, commit }, { id }) {
dismissNotification({ rootState, commit }, { id }) {
commit('dismissNotification', { id })
rootState.api.backendInteractor.dismissNotification({ id })
},
updateNotification ({ commit }, { id, updater }) {
updateNotification({ commit }, { id, updater }) {
commit('updateNotification', { id, updater })
}
}
},
},
}
export default notifications

View file

@ -0,0 +1,185 @@
// this is a snapshot of config keys used prior to sync config.
// used to migrate from old config.
// commented entries are unsynced stuff
export const oldDefaultConfigSync = {
expertLevel: 0, // used to track which settings to show and hide
hideISP: false,
hideInstanceWallpaper: false,
hideShoutbox: false,
// bad name: actually hides posts of muted USERS
hideMutedPosts: false,
hideMutedThreads: true,
hideWordFilteredPosts: false,
muteBotStatuses: false,
muteSensitiveStatuses: false,
collapseMessageWithSubject: false,
padEmoji: true,
hideScrobbles: false,
hideScrobblesAfter: '2d',
maxThumbnails: 16,
loopVideo: true,
loopVideoSilentOnly: true,
/// This is not the streaming API configuration, but rather an option
/// for automatically loading new posts into the timeline without
/// the user clicking the Show New button.
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,
//interfaceLanguage: '',
hideScopeNotice: false,
useStreamingApi: false,
sidebarRight: false,
scopeCopy: true,
subjectLineBehavior: 'email',
alwaysShowSubjectInput: true,
postContentType: 'text/plain',
minimalScopesMode: false,
// This hides statuses filtered via a word filter
hideFilteredStatuses: false,
// Confirmations
modalOnRepeat: false,
modalOnUnfollow: false,
modalOnBlock: true,
modalOnMute: false,
modalOnMuteConversation: false,
modalOnMuteDomain: true,
modalOnDelete: true,
modalOnLogout: true,
modalOnApproveFollow: false,
modalOnDenyFollow: false,
modalOnRemoveUserFromFollowers: false,
// Expiry confirmations/default actions
onMuteDefaultAction: 'ask',
onBlockDefaultAction: 'ask',
modalMobileCenter: false,
playVideosInModal: false,
useContainFit: true,
disableStickyHeaders: false,
showScrollbars: false,
userPopoverAvatarAction: 'open',
userPopoverOverlay: false,
userCardLeftJustify: false,
userCardHidePersonalMarks: false,
forcedRoundness: -1,
greentext: false,
mentionLinkShowTooltip: true,
mentionLinkShowAvatar: false,
mentionLinkFadeDomain: true,
mentionLinkShowYous: false,
mentionLinkBoldenYou: true,
hidePostStats: false,
hideBotIndication: false,
hideUserStats: false,
virtualScrolling: true,
sensitiveByDefault: false,
conversationDisplay: 'linear',
conversationTreeAdvanced: false,
conversationOtherRepliesButton: 'below',
conversationTreeFadeAncestors: false,
showExtraNotifications: true,
showExtraNotificationsTip: true,
showChatsInExtraNotifications: true,
showAnnouncementsInExtraNotifications: true,
showFollowRequestsInExtraNotifications: true,
maxDepthInThread: 6,
autocompleteSelect: false,
closingDrawerMarksAsSeen: true,
unseenAtTop: false,
ignoreInactionableSeen: false,
unsavedPostAction: 'confirm',
autoSaveDraft: false,
useAbsoluteTimeFormat: false,
absoluteTimeFormatMinAge: '0d',
absoluteTime12h: '24h',
theme3hacks: {
// Hacks, user overrides that are independent of theme used
underlay: 'none',
fonts: {
interface: undefined,
input: undefined,
post: undefined,
monospace: undefined,
},
},
// Special processing
// 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
// Those are handled outside config now
// muteWords: [],
// highlight: {},
}
export const defaultConfigLocal = {
hideAttachments: false,
hideAttachmentsInConv: false,
hideNsfw: true,
useOneClickNsfw: false,
preloadImage: true,
sidebarColumnWidth: '25rem',
contentColumnWidth: '45rem',
notifsColumnWidth: '25rem',
themeEditorMinWidth: '0rem',
emojiReactionsScale: 0.5,
textSize: '1rem',
emojiSize: '2.2rem',
navbarSize: '3.5rem',
panelHeaderSize: '3.2rem',
navbarColumnStretch: false,
mentionLinkDisplay: 'short',
imageCompression: true,
alwaysUseJpeg: false,
}

View file

@ -3,11 +3,9 @@ import { get, set } from 'lodash'
const defaultApi = ({ rootState, commit }, { path, value }) => {
const params = {}
set(params, path, value)
return rootState
.api
.backendInteractor
return rootState.api.backendInteractor
.updateProfile({ params })
.then(result => {
.then((result) => {
commit('addNewUsers', [result])
commit('setCurrentUser', result)
})
@ -16,11 +14,9 @@ const defaultApi = ({ rootState, commit }, { path, value }) => {
const notificationsApi = ({ rootState, commit }, { path, value, oldValue }) => {
const settings = {}
set(settings, path, value)
return rootState
.api
.backendInteractor
return rootState.api.backendInteractor
.updateNotificationSettings({ settings })
.then(result => {
.then((result) => {
if (result.status === 'success') {
commit('confirmProfileOption', { name, value })
} else {
@ -43,98 +39,99 @@ export const settingsMap = {
defaultNSFW: 'source.sensitive', // BROKEN: pleroma/pleroma#2837
stripRichContent: {
get: 'source.pleroma.no_rich_text',
set: 'no_rich_text'
set: 'no_rich_text',
},
// Privacy
locked: 'locked',
acceptChatMessages: {
get: 'pleroma.accepts_chat_messages',
set: 'accepts_chat_messages'
set: 'accepts_chat_messages',
},
allowFollowingMove: {
get: 'pleroma.allow_following_move',
set: 'allow_following_move'
set: 'allow_following_move',
},
discoverable: {
get: 'source.pleroma.discoverable',
set: 'discoverable'
set: 'discoverable',
},
hideFavorites: {
get: 'pleroma.hide_favorites',
set: 'hide_favorites'
set: 'hide_favorites',
},
hideFollowers: {
get: 'pleroma.hide_followers',
set: 'hide_followers'
set: 'hide_followers',
},
hideFollows: {
get: 'pleroma.hide_follows',
set: 'hide_follows'
set: 'hide_follows',
},
hideFollowersCount: {
get: 'pleroma.hide_followers_count',
set: 'hide_followers_count'
set: 'hide_followers_count',
},
hideFollowsCount: {
get: 'pleroma.hide_follows_count',
set: 'hide_follows_count'
set: 'hide_follows_count',
},
// NotificationSettingsAPIs
webPushHideContents: {
get: 'pleroma.notification_settings.hide_notification_contents',
set: 'hide_notification_contents',
api: notificationsApi
api: notificationsApi,
},
blockNotificationsFromStrangers: {
get: 'pleroma.notification_settings.block_from_strangers',
set: 'block_from_strangers',
api: notificationsApi
}
api: notificationsApi,
},
}
export const defaultState = Object.fromEntries(Object.keys(settingsMap).map(key => [key, null]))
export const defaultState = Object.fromEntries(
Object.keys(settingsMap).map((key) => [key, null]),
)
const profileConfig = {
state: { ...defaultState },
mutations: {
confirmProfileOption (state, { name, value }) {
confirmProfileOption(state, { name, value }) {
set(state, name, value)
},
wipeProfileOption (state, { name }) {
wipeProfileOption(state, { name }) {
set(state, name, null)
},
wipeAllProfileOptions (state) {
Object.keys(settingsMap).forEach(key => {
wipeAllProfileOptions(state) {
Object.keys(settingsMap).forEach((key) => {
set(state, key, null)
})
},
// Set the settings based on their path location
setCurrentUser (state, user) {
setCurrentUser(state, user) {
Object.entries(settingsMap).forEach((map) => {
const [name, value] = map
const { get: path = value } = value
set(state, name, get(user._original, path))
})
}
},
},
actions: {
setProfileOption ({ rootState, state, commit }, { name, value }) {
setProfileOption({ rootState, state, commit }, { name, value }) {
const oldValue = get(state, name)
const map = settingsMap[name]
if (!map) throw new Error('Invalid server-side setting')
const { set: path = map, api = defaultApi } = map
commit('wipeProfileOption', { name })
api({ rootState, commit }, { path, value, oldValue })
.catch((e) => {
console.warn('Error setting server-side option:', e)
commit('confirmProfileOption', { name, value: oldValue })
})
api({ rootState, commit }, { path, value, oldValue }).catch((e) => {
console.warn('Error setting server-side option:', e)
commit('confirmProfileOption', { name, value: oldValue })
})
},
logout ({ commit }) {
logout({ commit }) {
commit('wipeAllProfileOptions')
}
}
},
},
}
export default profileConfig

View file

@ -1,19 +1,22 @@
import {
each,
find,
findIndex,
first,
isArray,
last,
maxBy,
merge,
minBy,
omitBy,
remove,
slice,
each,
findIndex,
find,
maxBy,
minBy,
merge,
first,
last,
isArray,
omitBy
} from 'lodash'
import apiService from '../services/api/api.service.js'
import { useInterfaceStore } from 'src/stores/interface'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js'
const emptyTl = (userId = 0) => ({
statuses: [],
@ -29,7 +32,7 @@ const emptyTl = (userId = 0) => ({
followers: [],
friends: [],
userId,
flushMarker: 0
flushMarker: 0,
})
export const defaultState = () => ({
@ -39,7 +42,6 @@ export const defaultState = () => ({
conversationsObject: {},
maxId: 0,
favorites: new Set(),
pleromaScrobblesAvailable: true, // not reported in nodeinfo
timelines: {
mentions: emptyTl(),
public: emptyTl(),
@ -52,8 +54,8 @@ export const defaultState = () => ({
dms: emptyTl(),
bookmarks: emptyTl(),
list: emptyTl(),
bubble: emptyTl()
}
bubble: emptyTl(),
},
})
export const prepareStatus = (status) => {
@ -73,7 +75,10 @@ const mergeOrAdd = (arr, obj, item) => {
// We already have this, so only merge the new info.
// We ignore null values to avoid overwriting existing properties with missing data
// we also skip 'user' because that is handled by users module
merge(oldItem, omitBy(item, (v, k) => v === null || k === 'user'))
merge(
oldItem,
omitBy(item, (v, k) => v === null || k === 'user'),
)
// Reactivity fix.
oldItem.attachments.splice(oldItem.attachments.length)
return { item: oldItem, new: false }
@ -110,29 +115,39 @@ const sortTimeline = (timeline) => {
}
const getLatestScrobble = (state, user) => {
const scrobblesSupport = state.pleromaScrobblesAvailable
if (!scrobblesSupport) return
const scrobblesSupport =
useInstanceCapabilitiesStore().pleromaScrobblesAvailable
if (state.scrobblesNextFetch[user.id] && state.scrobblesNextFetch[user.id] > Date.now()) {
if (!scrobblesSupport || !user.name || user.id === 'undefined') {
return
}
if (
state.scrobblesNextFetch[user.id] &&
state.scrobblesNextFetch[user.id] > Date.now()
) {
return
}
state.scrobblesNextFetch[user.id] = Date.now() + 24 * 60 * 60 * 1000
if (!scrobblesSupport) return
apiService.fetchScrobbles({ accountId: user.id }).then((scrobbles) => {
if (scrobbles?.error) {
state.pleromaScrobblesAvailable = false
return
}
apiService
.fetchScrobbles({ accountId: user.id })
.then((scrobbles) => {
if (scrobbles?.error) {
useInstanceCapabilitiesStore().set('pleromaScrobblesAvailable', false)
return
}
if (scrobbles.length > 0) {
user.latestScrobble = scrobbles[0]
if (scrobbles.length > 0) {
user.latestScrobble = scrobbles[0]
state.scrobblesNextFetch[user.id] = Date.now() + 60 * 1000
}
}).catch(e => {
console.warn('cannot fetch scrobbles', e)
})
state.scrobblesNextFetch[user.id] = Date.now() + 60 * 1000
}
})
.catch((e) => {
console.warn('cannot fetch scrobbles', e)
})
}
// Add status to the global storages (arrays and objects maintaining statuses) except timelines
@ -153,7 +168,18 @@ const addStatusToGlobalStorage = (state, data) => {
return result
}
const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId, pagination = {} }) => {
const addNewStatuses = (
state,
{
statuses,
showImmediately = false,
timeline,
user = {},
noIdUpdate = false,
userId,
pagination = {},
},
) => {
// Sanity check
if (!isArray(statuses)) {
return false
@ -166,11 +192,19 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
// pagination.maxId is the oldest of the returned statuses when fetching older,
// and pagination.minId is the newest when fetching newer. The names come directly
// from the arguments they're supposed to be passed as for the next fetch.
const minNew = pagination.maxId || (statuses.length > 0 ? minBy(statuses, 'id').id : 0)
const maxNew = pagination.minId || (statuses.length > 0 ? maxBy(statuses, 'id').id : 0)
const minNew =
pagination.maxId || (statuses.length > 0 ? minBy(statuses, 'id').id : 0)
const maxNew =
pagination.minId || (statuses.length > 0 ? maxBy(statuses, 'id').id : 0)
const newer = timeline && (maxNew > timelineObject.maxId || timelineObject.maxId === 0) && statuses.length > 0
const older = timeline && (minNew < timelineObject.minId || timelineObject.minId === 0) && statuses.length > 0
const newer =
timeline &&
(maxNew > timelineObject.maxId || timelineObject.maxId === 0) &&
statuses.length > 0
const older =
timeline &&
(minNew < timelineObject.minId || timelineObject.minId === 0) &&
statuses.length > 0
if (!noIdUpdate && newer) {
timelineObject.maxId = maxNew
@ -182,7 +216,10 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
// This makes sure that user timeline won't get data meant for other
// user. I.e. opening different user profiles makes request which could
// return data late after user already viewing different user profile
if ((timeline === 'user' || timeline === 'media') && timelineObject.userId !== userId) {
if (
(timeline === 'user' || timeline === 'media') &&
timelineObject.userId !== userId
) {
return
}
@ -192,7 +229,10 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
if (result.new) {
// We are mentioned in a post
if (status.type === 'status' && find(status.attentions, { id: user.id })) {
if (
status.type === 'status' &&
find(status.attentions, { id: user.id })
) {
const mentions = state.timelines.mentions
// Add the mention to the mentions timeline
@ -217,20 +257,32 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
let resultForCurrentTimeline
// Some statuses should only be added to the global status repository.
if (timeline && addToTimeline) {
resultForCurrentTimeline = mergeOrAdd(timelineObject.statuses, timelineObject.statusesObject, status)
resultForCurrentTimeline = mergeOrAdd(
timelineObject.statuses,
timelineObject.statusesObject,
status,
)
}
if (timeline && showImmediately) {
// Add it directly to the visibleStatuses, don't change
// newStatusCount
mergeOrAdd(timelineObject.visibleStatuses, timelineObject.visibleStatusesObject, status)
mergeOrAdd(
timelineObject.visibleStatuses,
timelineObject.visibleStatusesObject,
status,
)
} else if (timeline && addToTimeline && resultForCurrentTimeline.new) {
// Just change newStatuscount
timelineObject.newStatusCount += 1
}
if (status.quote) {
addStatus(status.quote, /* showImmediately = */ false, /* addToTimeline = */ false)
addStatus(
status.quote,
/* showImmediately = */ false,
/* addToTimeline = */ false,
)
}
return status
@ -263,13 +315,19 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
let retweet
// If the retweeted status is already there, don't add the retweet
// to the timeline.
if (timeline && find(timelineObject.statuses, (s) => {
if (s.retweeted_status) {
return s.id === retweetedStatus.id || s.retweeted_status.id === retweetedStatus.id
} else {
return s.id === retweetedStatus.id
}
})) {
if (
timeline &&
find(timelineObject.statuses, (s) => {
if (s.retweeted_status) {
return (
s.id === retweetedStatus.id ||
s.retweeted_status.id === retweetedStatus.id
)
} else {
return s.id === retweetedStatus.id
}
})
) {
// Already have it visible (either as the original or another RT), don't add to timeline, don't show.
retweet = addStatus(status, false, false)
} else {
@ -290,9 +348,8 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
// NOOP, it is known status but we don't do anything about it for now
},
default: (unknown) => {
console.warn('unknown status type')
console.warn(unknown)
}
console.warn('unknown status type', unknown)
},
}
each(statuses, (status) => {
@ -312,35 +369,41 @@ const removeStatus = (state, { timeline, userId }) => {
if (userId) {
remove(timelineObject.statuses, { user: { id: userId } })
remove(timelineObject.visibleStatuses, { user: { id: userId } })
timelineObject.minVisibleId = timelineObject.visibleStatuses.length > 0 ? last(timelineObject.visibleStatuses).id : 0
timelineObject.maxId = timelineObject.statuses.length > 0 ? first(timelineObject.statuses).id : 0
timelineObject.minVisibleId =
timelineObject.visibleStatuses.length > 0
? last(timelineObject.visibleStatuses).id
: 0
timelineObject.maxId =
timelineObject.statuses.length > 0 ? first(timelineObject.statuses).id : 0
}
}
export const mutations = {
addNewStatuses,
removeStatus,
showNewStatuses (state, { timeline }) {
const oldTimeline = (state.timelines[timeline])
showNewStatuses(state, { timeline }) {
const oldTimeline = state.timelines[timeline]
oldTimeline.newStatusCount = 0
oldTimeline.visibleStatuses = slice(oldTimeline.statuses, 0, 50)
oldTimeline.minVisibleId = last(oldTimeline.visibleStatuses).id
oldTimeline.minId = oldTimeline.minVisibleId
oldTimeline.visibleStatusesObject = {}
each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
each(oldTimeline.visibleStatuses, (status) => {
oldTimeline.visibleStatusesObject[status.id] = status
})
},
resetStatuses (state) {
resetStatuses(state) {
const emptyState = defaultState()
Object.entries(emptyState).forEach(([key, value]) => {
state[key] = value
})
},
clearTimeline (state, { timeline, excludeUserId = false }) {
clearTimeline(state, { timeline, excludeUserId = false }) {
const userId = excludeUserId ? state.timelines[timeline].userId : undefined
state.timelines[timeline] = emptyTl(userId)
},
setFavorited (state, { status, value }) {
setFavorited(state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
if (newStatus.favorited !== value) {
@ -353,7 +416,7 @@ export const mutations = {
newStatus.favorited = value
},
setFavoritedConfirm (state, { status, user }) {
setFavoritedConfirm(state, { status, user }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.favorited = status.favorited
newStatus.fave_num = status.fave_num
@ -364,15 +427,19 @@ export const mutations = {
newStatus.favoritedBy.push(user)
}
},
setMutedStatus (state, status) {
setMutedStatus(state, status) {
const newStatus = state.allStatusesObject[status.id]
newStatus.thread_muted = status.thread_muted
if (newStatus.thread_muted !== undefined) {
state.conversationsObject[newStatus.statusnet_conversation_id].forEach(status => { status.thread_muted = newStatus.thread_muted })
state.conversationsObject[newStatus.statusnet_conversation_id].forEach(
(status) => {
status.thread_muted = newStatus.thread_muted
},
)
}
},
setRetweeted (state, { status, value }) {
setRetweeted(state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
if (newStatus.repeated !== value) {
@ -385,7 +452,7 @@ export const mutations = {
newStatus.repeated = value
},
setRetweetedConfirm (state, { status, user }) {
setRetweetedConfirm(state, { status, user }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.repeated = status.repeated
newStatus.repeat_num = status.repeat_num
@ -396,73 +463,79 @@ export const mutations = {
newStatus.rebloggedBy.push(user)
}
},
setBookmarked (state, { status, value }) {
setBookmarked(state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.bookmarked = value
newStatus.bookmark_folder_id = status.bookmark_folder_id
},
setBookmarkedConfirm (state, { status }) {
setBookmarkedConfirm(state, { status }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.bookmarked = status.bookmarked
if (status.pleroma) newStatus.bookmark_folder_id = status.pleroma.bookmark_folder
if (status.pleroma)
newStatus.bookmark_folder_id = status.pleroma.bookmark_folder
},
setDeleted (state, { status }) {
setDeleted(state, { status }) {
const newStatus = state.allStatusesObject[status.id]
if (newStatus) newStatus.deleted = true
},
setManyDeleted (state, condition) {
Object.values(state.allStatusesObject).forEach(status => {
setManyDeleted(state, condition) {
Object.values(state.allStatusesObject).forEach((status) => {
if (condition(status)) {
status.deleted = true
}
})
},
setLoading (state, { timeline, value }) {
setLoading(state, { timeline, value }) {
state.timelines[timeline].loading = value
},
setNsfw (state, { id, nsfw }) {
setNsfw(state, { id, nsfw }) {
const newStatus = state.allStatusesObject[id]
newStatus.nsfw = nsfw
},
queueFlush (state, { timeline, id }) {
queueFlush(state, { timeline, id }) {
state.timelines[timeline].flushMarker = id
},
queueFlushAll (state) {
queueFlushAll(state) {
Object.keys(state.timelines).forEach((timeline) => {
state.timelines[timeline].flushMarker = state.timelines[timeline].maxId
})
},
addRepeats (state, { id, rebloggedByUsers, currentUser }) {
addRepeats(state, { id, rebloggedByUsers, currentUser }) {
const newStatus = state.allStatusesObject[id]
newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _)
newStatus.rebloggedBy = rebloggedByUsers.filter((_) => _)
// repeats stats can be incorrect based on polling condition, let's update them using the most recent data
newStatus.repeat_num = newStatus.rebloggedBy.length
newStatus.repeated = !!newStatus.rebloggedBy.find(({ id }) => currentUser.id === id)
newStatus.repeated = !!newStatus.rebloggedBy.find(
({ id }) => currentUser.id === id,
)
},
addFavs (state, { id, favoritedByUsers, currentUser }) {
addFavs(state, { id, favoritedByUsers, currentUser }) {
const newStatus = state.allStatusesObject[id]
newStatus.favoritedBy = favoritedByUsers.filter(_ => _)
newStatus.favoritedBy = favoritedByUsers.filter((_) => _)
// favorites stats can be incorrect based on polling condition, let's update them using the most recent data
newStatus.fave_num = newStatus.favoritedBy.length
newStatus.favorited = !!newStatus.favoritedBy.find(({ id }) => currentUser.id === id)
newStatus.favorited = !!newStatus.favoritedBy.find(
({ id }) => currentUser.id === id,
)
},
addEmojiReactionsBy (state, { id, emojiReactions }) {
addEmojiReactionsBy(state, { id, emojiReactions }) {
const status = state.allStatusesObject[id]
status.emoji_reactions = emojiReactions
},
addOwnReaction (state, { id, emoji, currentUser }) {
addOwnReaction(state, { id, emoji, currentUser }) {
const status = state.allStatusesObject[id]
const reactionIndex = findIndex(status.emoji_reactions, { name: emoji })
const reaction = status.emoji_reactions[reactionIndex] || { name: emoji, count: 0, accounts: [] }
const reaction = status.emoji_reactions[reactionIndex] || {
name: emoji,
count: 0,
accounts: [],
}
const newReaction = {
...reaction,
count: reaction.count + 1,
me: true,
accounts: [
...reaction.accounts,
currentUser
]
accounts: [...reaction.accounts, currentUser],
}
// Update count of existing reaction if it exists, otherwise append at the end
@ -472,7 +545,7 @@ export const mutations = {
status.emoji_reactions = [...status.emoji_reactions, newReaction]
}
},
removeOwnReaction (state, { id, emoji, currentUser }) {
removeOwnReaction(state, { id, emoji, currentUser }) {
const status = state.allStatusesObject[id]
const reactionIndex = findIndex(status.emoji_reactions, { name: emoji })
if (reactionIndex < 0) return
@ -484,42 +557,70 @@ export const mutations = {
...reaction,
count: reaction.count - 1,
me: false,
accounts: accounts.filter(acc => acc.id !== currentUser.id)
accounts: accounts.filter((acc) => acc.id !== currentUser.id),
}
if (newReaction.count > 0) {
status.emoji_reactions[reactionIndex] = newReaction
} else {
status.emoji_reactions = status.emoji_reactions.filter(r => r.name !== emoji)
status.emoji_reactions = status.emoji_reactions.filter(
(r) => r.name !== emoji,
)
}
},
updateStatusWithPoll (state, { id, poll }) {
updateStatusWithPoll(state, { id, poll }) {
const status = state.allStatusesObject[id]
status.poll = poll
},
setVirtualHeight (state, { statusId, height }) {
setVirtualHeight(state, { statusId, height }) {
state.allStatusesObject[statusId].virtualHeight = height
}
},
}
const statuses = {
state: defaultState(),
actions: {
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) {
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination })
addNewStatuses(
{ rootState, commit },
{
statuses,
showImmediately = false,
timeline = false,
noIdUpdate = false,
userId,
pagination,
},
) {
commit('addNewStatuses', {
statuses,
showImmediately,
timeline,
noIdUpdate,
user: rootState.users.currentUser,
userId,
pagination,
})
},
fetchStatus ({ rootState, dispatch }, id) {
return rootState.api.backendInteractor.fetchStatus({ id })
fetchStatus({ rootState, dispatch }, id) {
return rootState.api.backendInteractor
.fetchStatus({ id })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
fetchStatusSource ({ rootState }, status) {
return apiService.fetchStatusSource({ id: status.id, credentials: rootState.users.currentUser.credentials })
fetchStatusSource({ rootState }, status) {
return apiService.fetchStatusSource({
id: status.id,
credentials: rootState.users.currentUser.credentials,
})
},
fetchStatusHistory (_, status) {
fetchStatusHistory(_, status) {
return apiService.fetchStatusHistory({ status })
},
deleteStatus ({ rootState, commit }, status) {
apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
deleteStatus({ rootState, commit }, status) {
apiService
.deleteStatus({
id: status.id,
credentials: rootState.users.currentUser.credentials,
})
.then(() => {
commit('setDeleted', { status })
})
@ -528,141 +629,208 @@ const statuses = {
level: 'error',
messageKey: 'status.delete_error',
messageArgs: [e.message],
timeout: 5000
timeout: 5000,
})
})
},
deleteStatusById ({ rootState, commit }, id) {
deleteStatusById({ rootState, commit }, id) {
const status = rootState.statuses.allStatusesObject[id]
commit('setDeleted', { status })
},
markStatusesAsDeleted ({ commit }, condition) {
markStatusesAsDeleted({ commit }, condition) {
commit('setManyDeleted', condition)
},
favorite ({ rootState, commit }, status) {
favorite({ rootState, commit }, status) {
// Optimistic favoriting...
commit('setFavorited', { status, value: true })
rootState.api.backendInteractor.favorite({ id: status.id })
.then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
rootState.api.backendInteractor
.favorite({ id: status.id })
.then((status) =>
commit('setFavoritedConfirm', {
status,
user: rootState.users.currentUser,
}),
)
},
unfavorite ({ rootState, commit }, status) {
unfavorite({ rootState, commit }, status) {
// Optimistic unfavoriting...
commit('setFavorited', { status, value: false })
rootState.api.backendInteractor.unfavorite({ id: status.id })
.then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
rootState.api.backendInteractor
.unfavorite({ id: status.id })
.then((status) =>
commit('setFavoritedConfirm', {
status,
user: rootState.users.currentUser,
}),
)
},
fetchPinnedStatuses ({ rootState, dispatch }, userId) {
rootState.api.backendInteractor.fetchPinnedStatuses({ id: userId })
.then(statuses => dispatch('addNewStatuses', { statuses, timeline: 'user', userId, showImmediately: true, noIdUpdate: true }))
fetchPinnedStatuses({ rootState, dispatch }, userId) {
rootState.api.backendInteractor
.fetchPinnedStatuses({ id: userId })
.then((statuses) =>
dispatch('addNewStatuses', {
statuses,
timeline: 'user',
userId,
showImmediately: true,
noIdUpdate: true,
}),
)
},
pinStatus ({ rootState, dispatch }, statusId) {
return rootState.api.backendInteractor.pinOwnStatus({ id: statusId })
pinStatus({ rootState, dispatch }, statusId) {
return rootState.api.backendInteractor
.pinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
unpinStatus ({ rootState, dispatch }, statusId) {
rootState.api.backendInteractor.unpinOwnStatus({ id: statusId })
unpinStatus({ rootState, dispatch }, statusId) {
rootState.api.backendInteractor
.unpinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
muteConversation ({ rootState, commit }, { id: statusId }) {
return rootState.api.backendInteractor.muteConversation({ id: statusId })
muteConversation({ rootState, commit }, { id: statusId }) {
return rootState.api.backendInteractor
.muteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status))
},
unmuteConversation ({ rootState, commit }, { id: statusId }) {
return rootState.api.backendInteractor.unmuteConversation({ id: statusId })
unmuteConversation({ rootState, commit }, { id: statusId }) {
return rootState.api.backendInteractor
.unmuteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status))
},
retweet ({ rootState, commit }, status) {
retweet({ rootState, commit }, status) {
// Optimistic retweeting...
commit('setRetweeted', { status, value: true })
rootState.api.backendInteractor.retweet({ id: status.id })
.then(status => commit('setRetweetedConfirm', { status: status.retweeted_status, user: rootState.users.currentUser }))
rootState.api.backendInteractor
.retweet({ id: status.id })
.then((status) =>
commit('setRetweetedConfirm', {
status: status.retweeted_status,
user: rootState.users.currentUser,
}),
)
},
unretweet ({ rootState, commit }, status) {
unretweet({ rootState, commit }, status) {
// Optimistic unretweeting...
commit('setRetweeted', { status, value: false })
rootState.api.backendInteractor.unretweet({ id: status.id })
.then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser }))
rootState.api.backendInteractor
.unretweet({ id: status.id })
.then((status) =>
commit('setRetweetedConfirm', {
status,
user: rootState.users.currentUser,
}),
)
},
bookmark ({ rootState, commit }, status) {
bookmark({ rootState, commit }, status) {
commit('setBookmarked', { status, value: true })
rootState.api.backendInteractor.bookmarkStatus({ id: status.id, folder_id: status.bookmark_folder_id })
.then(status => {
rootState.api.backendInteractor
.bookmarkStatus({ id: status.id, folder_id: status.bookmark_folder_id })
.then((status) => {
commit('setBookmarkedConfirm', { status })
})
},
unbookmark ({ rootState, commit }, status) {
unbookmark({ rootState, commit }, status) {
commit('setBookmarked', { status, value: false })
rootState.api.backendInteractor.unbookmarkStatus({ id: status.id })
.then(status => {
rootState.api.backendInteractor
.unbookmarkStatus({ id: status.id })
.then((status) => {
commit('setBookmarkedConfirm', { status })
})
},
queueFlush ({ commit }, { timeline, id }) {
queueFlush({ commit }, { timeline, id }) {
commit('queueFlush', { timeline, id })
},
queueFlushAll ({ commit }) {
queueFlushAll({ commit }) {
commit('queueFlushAll')
},
fetchFavsAndRepeats ({ rootState, commit }, id) {
fetchFavsAndRepeats({ rootState, commit }, id) {
Promise.all([
rootState.api.backendInteractor.fetchFavoritedByUsers({ id }),
rootState.api.backendInteractor.fetchRebloggedByUsers({ id })
rootState.api.backendInteractor.fetchRebloggedByUsers({ id }),
]).then(([favoritedByUsers, rebloggedByUsers]) => {
commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser })
commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser })
commit('addFavs', {
id,
favoritedByUsers,
currentUser: rootState.users.currentUser,
})
commit('addRepeats', {
id,
rebloggedByUsers,
currentUser: rootState.users.currentUser,
})
})
},
reactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
reactWithEmoji({ rootState, dispatch, commit }, { id, emoji }) {
const currentUser = rootState.users.currentUser
if (!currentUser) return
commit('addOwnReaction', { id, emoji, currentUser })
rootState.api.backendInteractor.reactWithEmoji({ id, emoji }).then(
() => {
dispatch('fetchEmojiReactionsBy', id)
}
)
rootState.api.backendInteractor.reactWithEmoji({ id, emoji }).then(() => {
dispatch('fetchEmojiReactionsBy', id)
})
},
unreactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
unreactWithEmoji({ rootState, dispatch, commit }, { id, emoji }) {
const currentUser = rootState.users.currentUser
if (!currentUser) return
commit('removeOwnReaction', { id, emoji, currentUser })
rootState.api.backendInteractor.unreactWithEmoji({ id, emoji }).then(
() => {
rootState.api.backendInteractor
.unreactWithEmoji({ id, emoji })
.then(() => {
dispatch('fetchEmojiReactionsBy', id)
}
)
})
},
fetchEmojiReactionsBy ({ rootState, commit }, id) {
return rootState.api.backendInteractor.fetchEmojiReactions({ id }).then(
emojiReactions => {
commit('addEmojiReactionsBy', { id, emojiReactions, currentUser: rootState.users.currentUser })
}
)
fetchEmojiReactionsBy({ rootState, commit }, id) {
return rootState.api.backendInteractor
.fetchEmojiReactions({ id })
.then((emojiReactions) => {
commit('addEmojiReactionsBy', {
id,
emojiReactions,
currentUser: rootState.users.currentUser,
})
})
},
fetchFavs ({ rootState, commit }, id) {
rootState.api.backendInteractor.fetchFavoritedByUsers({ id })
.then(favoritedByUsers => commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser }))
fetchFavs({ rootState, commit }, id) {
rootState.api.backendInteractor
.fetchFavoritedByUsers({ id })
.then((favoritedByUsers) =>
commit('addFavs', {
id,
favoritedByUsers,
currentUser: rootState.users.currentUser,
}),
)
},
fetchRepeats ({ rootState, commit }, id) {
rootState.api.backendInteractor.fetchRebloggedByUsers({ id })
.then(rebloggedByUsers => commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser }))
fetchRepeats({ rootState, commit }, id) {
rootState.api.backendInteractor
.fetchRebloggedByUsers({ id })
.then((rebloggedByUsers) =>
commit('addRepeats', {
id,
rebloggedByUsers,
currentUser: rootState.users.currentUser,
}),
)
},
search (store, { q, resolve, limit, offset, following, type }) {
return store.rootState.api.backendInteractor.search2({ q, resolve, limit, offset, following, type })
search(store, { q, resolve, limit, offset, following, type }) {
return store.rootState.api.backendInteractor
.search2({ q, resolve, limit, offset, following, type })
.then((data) => {
store.commit('addNewUsers', data.accounts)
store.commit('addNewUsers', data.statuses.map(s => s.user).filter(u => u))
store.commit(
'addNewUsers',
data.statuses.map((s) => s.user).filter((u) => u),
)
store.commit('addNewStatuses', { statuses: data.statuses })
return data
})
},
setVirtualHeight ({ commit }, { statusId, height }) {
setVirtualHeight({ commit }, { statusId, height }) {
commit('setVirtualHeight', { statusId, height })
}
},
},
mutations
mutations,
}
export default statuses

View file

@ -1,20 +1,40 @@
import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
import {
compact,
concat,
each,
isArray,
last,
map,
mergeWith,
uniq,
} from 'lodash'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
import apiService from '../services/api/api.service.js'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import oauthApi from '../services/new_api/oauth.js'
import { registerPushNotifications, unregisterPushNotifications } from '../services/sw/sw.js'
import {
registerPushNotifications,
unregisterPushNotifications,
} from '../services/sw/sw.js'
import {
windowHeight,
windowWidth,
} from '../services/window_utils/window_utils'
import { useEmojiStore } from 'src/stores/emoji.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useServerSideStorageStore } from 'src/stores/serverSideStorage'
import { declarations } from 'src/modules/config_declaration'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUserHighlightStore } from 'src/stores/user_highlight.js'
// TODO: Unify with mergeOrAdd in statuses.js
export const mergeOrAdd = (arr, obj, item) => {
if (!item) { return false }
if (!item) {
return false
}
const oldItem = obj[item.id]
if (oldItem) {
// We already have this, so only merge the new info.
@ -39,7 +59,8 @@ const getNotificationPermission = () => {
const Notification = window.Notification
if (!Notification) return Promise.resolve(null)
if (Notification.permission === 'default') return Notification.requestPermission()
if (Notification.permission === 'default')
return Notification.requestPermission()
return Promise.resolve(Notification.permission)
}
@ -51,30 +72,43 @@ const blockUser = (store, args) => {
store.commit('updateUserRelationship', [predictedRelationship])
store.commit('addBlockId', id)
return store.rootState.api.backendInteractor.blockUser({ id, expiresIn })
return store.rootState.api.backendInteractor
.blockUser({ id, expiresIn })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
store.commit('addBlockId', id)
store.commit('removeStatus', { timeline: 'friends', userId: id })
store.commit('removeStatus', { timeline: 'public', userId: id })
store.commit('removeStatus', { timeline: 'publicAndExternal', userId: id })
store.commit('removeStatus', {
timeline: 'publicAndExternal',
userId: id,
})
})
}
const unblockUser = (store, id) => {
return store.rootState.api.backendInteractor.unblockUser({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
return store.rootState.api.backendInteractor
.unblockUser({ id })
.then((relationship) =>
store.commit('updateUserRelationship', [relationship]),
)
}
const removeUserFromFollowers = (store, id) => {
return store.rootState.api.backendInteractor.removeUserFromFollowers({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
return store.rootState.api.backendInteractor
.removeUserFromFollowers({ id })
.then((relationship) =>
store.commit('updateUserRelationship', [relationship]),
)
}
const editUserNote = (store, { id, comment }) => {
return store.rootState.api.backendInteractor.editUserNote({ id, comment })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
return store.rootState.api.backendInteractor
.editUserNote({ id, comment })
.then((relationship) =>
store.commit('updateUserRelationship', [relationship]),
)
}
const muteUser = (store, args) => {
@ -85,7 +119,8 @@ const muteUser = (store, args) => {
store.commit('updateUserRelationship', [predictedRelationship])
store.commit('addMuteId', id)
return store.rootState.api.backendInteractor.muteUser({ id, expiresIn })
return store.rootState.api.backendInteractor
.muteUser({ id, expiresIn })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
store.commit('addMuteId', id)
@ -97,92 +132,105 @@ const unmuteUser = (store, id) => {
predictedRelationship.muting = false
store.commit('updateUserRelationship', [predictedRelationship])
return store.rootState.api.backendInteractor.unmuteUser({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
return store.rootState.api.backendInteractor
.unmuteUser({ id })
.then((relationship) =>
store.commit('updateUserRelationship', [relationship]),
)
}
const hideReblogs = (store, userId) => {
return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: false })
return store.rootState.api.backendInteractor
.followUser({ id: userId, reblogs: false })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
})
}
const showReblogs = (store, userId) => {
return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: true })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
return store.rootState.api.backendInteractor
.followUser({ id: userId, reblogs: true })
.then((relationship) =>
store.commit('updateUserRelationship', [relationship]),
)
}
const muteDomain = (store, domain) => {
return store.rootState.api.backendInteractor.muteDomain({ domain })
return store.rootState.api.backendInteractor
.muteDomain({ domain })
.then(() => store.commit('addDomainMute', domain))
}
const unmuteDomain = (store, domain) => {
return store.rootState.api.backendInteractor.unmuteDomain({ domain })
return store.rootState.api.backendInteractor
.unmuteDomain({ domain })
.then(() => store.commit('removeDomainMute', domain))
}
export const mutations = {
tagUser (state, { user: { id }, tag }) {
tagUser(state, { user: { id }, tag }) {
const user = state.usersObject[id]
const tags = user.tags || []
const newTags = tags.concat([tag])
user.tags = newTags
},
untagUser (state, { user: { id }, tag }) {
untagUser(state, { user: { id }, tag }) {
const user = state.usersObject[id]
const tags = user.tags || []
const newTags = tags.filter(t => t !== tag)
const newTags = tags.filter((t) => t !== tag)
user.tags = newTags
},
updateRight (state, { user: { id }, right, value }) {
updateRight(state, { user: { id }, right, value }) {
const user = state.usersObject[id]
const newRights = user.rights
newRights[right] = value
user.rights = newRights
},
updateActivationStatus (state, { user: { id }, deactivated }) {
updateActivationStatus(state, { user: { id }, deactivated }) {
const user = state.usersObject[id]
user.deactivated = deactivated
},
setCurrentUser (state, user) {
setCurrentUser(state, user) {
state.lastLoginName = user.screen_name
state.currentUser = mergeWith(state.currentUser || {}, user, mergeArrayLength)
state.currentUser = mergeWith(
state.currentUser || {},
user,
mergeArrayLength,
)
},
clearCurrentUser (state) {
clearCurrentUser(state) {
state.currentUser = false
state.lastLoginName = false
},
beginLogin (state) {
beginLogin(state) {
state.loggingIn = true
},
endLogin (state) {
endLogin(state) {
state.loggingIn = false
},
saveFriendIds (state, { id, friendIds }) {
saveFriendIds(state, { id, friendIds }) {
const user = state.usersObject[id]
user.friendIds = uniq(concat(user.friendIds || [], friendIds))
},
saveFollowerIds (state, { id, followerIds }) {
saveFollowerIds(state, { id, followerIds }) {
const user = state.usersObject[id]
user.followerIds = uniq(concat(user.followerIds || [], followerIds))
},
// Because frontend doesn't have a reason to keep these stuff in memory
// outside of viewing someones user profile.
clearFriends (state, userId) {
clearFriends(state, userId) {
const user = state.usersObject[userId]
if (user) {
user.friendIds = []
}
},
clearFollowers (state, userId) {
clearFollowers(state, userId) {
const user = state.usersObject[userId]
if (user) {
user.followerIds = []
}
},
addNewUsers (state, users) {
addNewUsers(state, users) {
each(users, (user) => {
if (user.relationship) {
state.relationships[user.relationship.id] = user.relationship
@ -194,51 +242,51 @@ export const mutations = {
}
})
},
updateUserRelationship (state, relationships) {
updateUserRelationship(state, relationships) {
relationships.forEach((relationship) => {
state.relationships[relationship.id] = relationship
})
},
updateUserInLists (state, { id, inLists }) {
updateUserInLists(state, { id, inLists }) {
state.usersObject[id].inLists = inLists
},
saveBlockIds (state, blockIds) {
saveBlockIds(state, blockIds) {
state.currentUser.blockIds = blockIds
},
addBlockId (state, blockId) {
addBlockId(state, blockId) {
if (state.currentUser.blockIds.indexOf(blockId) === -1) {
state.currentUser.blockIds.push(blockId)
}
},
setBlockIdsMaxId (state, blockIdsMaxId) {
setBlockIdsMaxId(state, blockIdsMaxId) {
state.currentUser.blockIdsMaxId = blockIdsMaxId
},
saveMuteIds (state, muteIds) {
saveMuteIds(state, muteIds) {
state.currentUser.muteIds = muteIds
},
setMuteIdsMaxId (state, muteIdsMaxId) {
setMuteIdsMaxId(state, muteIdsMaxId) {
state.currentUser.muteIdsMaxId = muteIdsMaxId
},
addMuteId (state, muteId) {
addMuteId(state, muteId) {
if (state.currentUser.muteIds.indexOf(muteId) === -1) {
state.currentUser.muteIds.push(muteId)
}
},
saveDomainMutes (state, domainMutes) {
saveDomainMutes(state, domainMutes) {
state.currentUser.domainMutes = domainMutes
},
addDomainMute (state, domain) {
addDomainMute(state, domain) {
if (state.currentUser.domainMutes.indexOf(domain) === -1) {
state.currentUser.domainMutes.push(domain)
}
},
removeDomainMute (state, domain) {
removeDomainMute(state, domain) {
const index = state.currentUser.domainMutes.indexOf(domain)
if (index !== -1) {
state.currentUser.domainMutes.splice(index, 1)
}
},
setPinnedToUser (state, status) {
setPinnedToUser(state, status) {
const user = state.usersObject[status.user.id]
user.pinnedStatusIds = user.pinnedStatusIds || []
const index = user.pinnedStatusIds.indexOf(status.id)
@ -249,55 +297,57 @@ export const mutations = {
user.pinnedStatusIds.splice(index, 1)
}
},
setUserForStatus (state, status) {
setUserForStatus(state, status) {
status.user = state.usersObject[status.user.id]
},
setUserForNotification (state, notification) {
setUserForNotification(state, notification) {
if (notification.type !== 'follow') {
notification.action.user = state.usersObject[notification.action.user.id]
}
notification.from_profile = state.usersObject[notification.from_profile.id]
},
setColor (state, { user: { id }, highlighted }) {
setColor(state, { user: { id }, highlighted }) {
const user = state.usersObject[id]
user.highlight = highlighted
},
signUpPending (state) {
signUpPending(state) {
state.signUpPending = true
state.signUpErrors = []
state.signUpNotice = {}
},
signUpSuccess (state) {
signUpSuccess(state) {
state.signUpPending = false
},
signUpFailure (state, errors) {
signUpFailure(state, errors) {
state.signUpPending = false
state.signUpErrors = errors
state.signUpNotice = {}
},
signUpNotice (state, notice) {
signUpNotice(state, notice) {
state.signUpPending = false
state.signUpErrors = []
state.signUpNotice = notice
}
},
}
export const getters = {
findUser: state => query => {
findUser: (state) => (query) => {
return state.usersObject[query]
},
findUserByName: state => query => {
findUserByName: (state) => (query) => {
return state.usersByNameObject[query.toLowerCase()]
},
findUserByUrl: state => query => {
return state.users
.find(u => u.statusnet_profile_url &&
u.statusnet_profile_url.toLowerCase() === query.toLowerCase())
findUserByUrl: (state) => (query) => {
return state.users.find(
(u) =>
u.statusnet_profile_url &&
u.statusnet_profile_url.toLowerCase() === query.toLowerCase(),
)
},
relationship: state => id => {
relationship: (state) => (id) => {
const rel = id && state.relationships[id]
return rel || { id, loading: true }
}
},
}
export const defaultState = {
@ -310,7 +360,7 @@ export const defaultState = {
signUpPending: false,
signUpErrors: [],
signUpNotice: {},
relationships: {}
relationships: {},
}
const users = {
@ -318,47 +368,54 @@ const users = {
mutations,
getters,
actions: {
fetchUserIfMissing (store, id) {
fetchUserIfMissing(store, id) {
if (!store.getters.findUser(id)) {
store.dispatch('fetchUser', id)
}
},
fetchUser (store, id) {
return store.rootState.api.backendInteractor.fetchUser({ id })
fetchUser(store, id) {
return store.rootState.api.backendInteractor
.fetchUser({ id })
.then((user) => {
store.commit('addNewUsers', [user])
return user
})
},
fetchUserByName (store, name) {
return store.rootState.api.backendInteractor.fetchUserByName({ name })
fetchUserByName(store, name) {
return store.rootState.api.backendInteractor
.fetchUserByName({ name })
.then((user) => {
store.commit('addNewUsers', [user])
return user
})
},
fetchUserRelationship (store, id) {
fetchUserRelationship(store, id) {
if (store.state.currentUser) {
store.rootState.api.backendInteractor.fetchUserRelationship({ id })
.then((relationships) => store.commit('updateUserRelationship', relationships))
store.rootState.api.backendInteractor
.fetchUserRelationship({ id })
.then((relationships) =>
store.commit('updateUserRelationship', relationships),
)
}
},
fetchUserInLists (store, id) {
fetchUserInLists(store, id) {
if (store.state.currentUser) {
store.rootState.api.backendInteractor.fetchUserInLists({ id })
store.rootState.api.backendInteractor
.fetchUserInLists({ id })
.then((inLists) => store.commit('updateUserInLists', { id, inLists }))
}
},
fetchBlocks (store, args) {
fetchBlocks(store, args) {
const { reset } = args || {}
const maxId = store.state.currentUser.blockIdsMaxId
return store.rootState.api.backendInteractor.fetchBlocks({ maxId })
return store.rootState.api.backendInteractor
.fetchBlocks({ maxId })
.then((blocks) => {
if (reset) {
store.commit('saveBlockIds', map(blocks, 'id'))
} else {
map(blocks, 'id').map(id => store.commit('addBlockId', id))
map(blocks, 'id').map((id) => store.commit('addBlockId', id))
}
if (blocks.length) {
store.commit('setBlockIdsMaxId', last(blocks).id)
@ -367,34 +424,35 @@ const users = {
return blocks
})
},
blockUser (store, data) {
blockUser(store, data) {
return blockUser(store, data)
},
unblockUser (store, data) {
unblockUser(store, data) {
return unblockUser(store, data)
},
removeUserFromFollowers (store, id) {
removeUserFromFollowers(store, id) {
return removeUserFromFollowers(store, id)
},
blockUsers (store, data = []) {
return Promise.all(data.map(d => blockUser(store, d)))
blockUsers(store, data = []) {
return Promise.all(data.map((d) => blockUser(store, d)))
},
unblockUsers (store, data = []) {
return Promise.all(data.map(d => unblockUser(store, d)))
unblockUsers(store, data = []) {
return Promise.all(data.map((d) => unblockUser(store, d)))
},
editUserNote (store, args) {
editUserNote(store, args) {
return editUserNote(store, args)
},
fetchMutes (store, args) {
fetchMutes(store, args) {
const { reset } = args || {}
const maxId = store.state.currentUser.muteIdsMaxId
return store.rootState.api.backendInteractor.fetchMutes({ maxId })
return store.rootState.api.backendInteractor
.fetchMutes({ maxId })
.then((mutes) => {
if (reset) {
store.commit('saveMuteIds', map(mutes, 'id'))
} else {
map(mutes, 'id').map(id => store.commit('addMuteId', id))
map(mutes, 'id').map((id) => store.commit('addMuteId', id))
}
if (mutes.length) {
store.commit('setMuteIdsMaxId', last(mutes).id)
@ -403,99 +461,118 @@ const users = {
return mutes
})
},
muteUser (store, data) {
muteUser(store, data) {
return muteUser(store, data)
},
unmuteUser (store, id) {
unmuteUser(store, id) {
return unmuteUser(store, id)
},
hideReblogs (store, id) {
hideReblogs(store, id) {
return hideReblogs(store, id)
},
showReblogs (store, id) {
showReblogs(store, id) {
return showReblogs(store, id)
},
muteUsers (store, data = []) {
return Promise.all(data.map(d => muteUser(store, d)))
muteUsers(store, data = []) {
return Promise.all(data.map((d) => muteUser(store, d)))
},
unmuteUsers (store, ids = []) {
return Promise.all(ids.map(d => unmuteUser(store, d)))
unmuteUsers(store, ids = []) {
return Promise.all(ids.map((d) => unmuteUser(store, d)))
},
fetchDomainMutes (store) {
return store.rootState.api.backendInteractor.fetchDomainMutes()
fetchDomainMutes(store) {
return store.rootState.api.backendInteractor
.fetchDomainMutes()
.then((domainMutes) => {
store.commit('saveDomainMutes', domainMutes)
return domainMutes
})
},
muteDomain (store, domain) {
muteDomain(store, domain) {
return muteDomain(store, domain)
},
unmuteDomain (store, domain) {
unmuteDomain(store, domain) {
return unmuteDomain(store, domain)
},
muteDomains (store, domains = []) {
return Promise.all(domains.map(domain => muteDomain(store, domain)))
muteDomains(store, domains = []) {
return Promise.all(domains.map((domain) => muteDomain(store, domain)))
},
unmuteDomains (store, domain = []) {
return Promise.all(domain.map(domain => unmuteDomain(store, domain)))
unmuteDomains(store, domain = []) {
return Promise.all(domain.map((domain) => unmuteDomain(store, domain)))
},
fetchFriends ({ rootState, commit }, id) {
fetchFriends({ rootState, commit }, id) {
const user = rootState.users.usersObject[id]
const maxId = last(user.friendIds)
return rootState.api.backendInteractor.fetchFriends({ id, maxId })
return rootState.api.backendInteractor
.fetchFriends({ id, maxId })
.then((friends) => {
commit('addNewUsers', friends)
commit('saveFriendIds', { id, friendIds: map(friends, 'id') })
return friends
})
},
fetchFollowers ({ rootState, commit }, id) {
fetchFollowers({ rootState, commit }, id) {
const user = rootState.users.usersObject[id]
const maxId = last(user.followerIds)
return rootState.api.backendInteractor.fetchFollowers({ id, maxId })
return rootState.api.backendInteractor
.fetchFollowers({ id, maxId })
.then((followers) => {
commit('addNewUsers', followers)
commit('saveFollowerIds', { id, followerIds: map(followers, 'id') })
return followers
})
},
clearFriends ({ commit }, userId) {
clearFriends({ commit }, userId) {
commit('clearFriends', userId)
},
clearFollowers ({ commit }, userId) {
clearFollowers({ commit }, userId) {
commit('clearFollowers', userId)
},
subscribeUser ({ rootState, commit }, id) {
return rootState.api.backendInteractor.followUser({ id, notify: true })
.then((relationship) => commit('updateUserRelationship', [relationship]))
subscribeUser({ rootState, commit }, id) {
return rootState.api.backendInteractor
.followUser({ id, notify: true })
.then((relationship) =>
commit('updateUserRelationship', [relationship]),
)
},
unsubscribeUser ({ rootState, commit }, id) {
return rootState.api.backendInteractor.followUser({ id, notify: false })
.then((relationship) => commit('updateUserRelationship', [relationship]))
unsubscribeUser({ rootState, commit }, id) {
return rootState.api.backendInteractor
.followUser({ id, notify: false })
.then((relationship) =>
commit('updateUserRelationship', [relationship]),
)
},
toggleActivationStatus ({ rootState, commit }, { user }) {
const api = user.deactivated ? rootState.api.backendInteractor.activateUser : rootState.api.backendInteractor.deactivateUser
api({ user })
.then((user) => { const deactivated = !user.is_active; commit('updateActivationStatus', { user, deactivated }) })
toggleActivationStatus({ rootState, commit }, { user }) {
const api = user.deactivated
? rootState.api.backendInteractor.activateUser
: rootState.api.backendInteractor.deactivateUser
api({ user }).then((user) => {
const deactivated = !user.is_active
commit('updateActivationStatus', { user, deactivated })
})
},
registerPushNotifications (store) {
registerPushNotifications(store) {
const token = store.state.currentUser.credentials
const vapidPublicKey = store.rootState.instance.vapidPublicKey
const isEnabled = store.rootState.config.webPushNotifications
const notificationVisibility = store.rootState.config.notificationVisibility
const vapidPublicKey = useInstanceStore().vapidPublicKey
const isEnabled = useMergedConfigStore().mergedConfig.webPushNotifications
const notificationVisibility =
useMergedConfigStore().mergedConfig.notificationVisibility
registerPushNotifications(isEnabled, vapidPublicKey, token, notificationVisibility)
registerPushNotifications(
isEnabled,
vapidPublicKey,
token,
notificationVisibility,
)
},
unregisterPushNotifications (store) {
unregisterPushNotifications(store) {
const token = store.state.currentUser.credentials
unregisterPushNotifications(token)
},
addNewUsers ({ commit }, users) {
addNewUsers({ commit }, users) {
commit('addNewUsers', users)
},
addNewStatuses (store, { statuses }) {
addNewStatuses(store, { statuses }) {
const users = map(statuses, 'user')
const retweetedUsers = compact(map(statuses, 'retweeted_status.user'))
store.commit('addNewUsers', users)
@ -514,10 +591,10 @@ const users = {
store.commit('setPinnedToUser', status)
})
},
addNewNotifications (store, { notifications }) {
addNewNotifications(store, { notifications }) {
const users = map(notifications, 'from_profile')
const targetUsers = map(notifications, 'target').filter(_ => _)
const notificationIds = notifications.map(_ => _.id)
const targetUsers = map(notifications, 'target').filter((_) => _)
const notificationIds = notifications.map((_) => _.id)
store.commit('addNewUsers', users)
store.commit('addNewUsers', targetUsers)
@ -531,29 +608,32 @@ const users = {
store.commit('setUserForNotification', notification)
})
},
searchUsers ({ rootState, commit }, { query }) {
return rootState.api.backendInteractor.searchUsers({ query })
searchUsers({ rootState, commit }, { query }) {
return rootState.api.backendInteractor
.searchUsers({ query })
.then((users) => {
commit('addNewUsers', users)
return users
})
},
async signUp (store, userInfo) {
async signUp(store, userInfo) {
const oauthStore = useOAuthStore()
store.commit('signUpPending')
try {
const token = await oauthStore.ensureAppToken()
const data = await apiService.register(
{ credentials: token, params: { ...userInfo } }
)
const data = await apiService.register({
credentials: token,
params: { ...userInfo },
})
if (data.access_token) {
store.commit('signUpSuccess')
oauthStore.setToken(data.access_token)
store.dispatch('loginUser', data.access_token)
await store.dispatch('loginUser', data.access_token)
return 'ok'
} else { // Request succeeded, but user cannot login yet.
} else {
// Request succeeded, but user cannot login yet.
store.commit('signUpNotice', data)
return 'request_sent'
}
@ -563,22 +643,22 @@ const users = {
throw e
}
},
async getCaptcha (store) {
async getCaptcha(store) {
return store.rootState.api.backendInteractor.getCaptcha()
},
logout (store) {
logout(store) {
const oauth = useOAuthStore()
const { instance } = store.rootState
// NOTE: No need to verify the app still exists, because if it doesn't,
// the token will be invalid too
return oauth.ensureApp()
return oauth
.ensureApp()
.then((app) => {
const params = {
app,
instance: instance.server,
token: oauth.userToken
instance: useInstanceStore().server,
token: oauth.userToken,
}
return oauthApi.revokeToken(params)
@ -588,7 +668,10 @@ const users = {
store.dispatch('disconnectFromSocket')
oauth.clearToken()
store.dispatch('stopFetchingTimeline', 'friends')
store.commit('setBackendInteractor', backendInteractorService(oauth.getToken))
store.commit(
'setBackendInteractor',
backendInteractorService(oauth.getToken),
)
store.dispatch('stopFetchingNotifications')
store.dispatch('stopFetchingLists')
store.dispatch('stopFetchingBookmarkFolders')
@ -599,16 +682,15 @@ const users = {
useInterfaceStore().setLastTimeline('public-timeline')
useInterfaceStore().setLayoutWidth(windowWidth())
useInterfaceStore().setLayoutHeight(windowHeight())
store.commit('clearServerSideStorage')
})
},
loginUser (store, accessToken) {
loginUser(store, accessToken) {
return new Promise((resolve, reject) => {
const commit = store.commit
const dispatch = store.dispatch
const rootState = store.rootState
commit('beginLogin')
store.rootState.api.backendInteractor.verifyCredentials(accessToken)
store.rootState.api.backendInteractor
.verifyCredentials(accessToken)
.then((data) => {
if (!data.error) {
const user = data
@ -619,16 +701,29 @@ const users = {
user.domainMutes = []
commit('setCurrentUser', user)
useServerSideStorageStore().setServerSideStorage(user)
useSyncConfigStore()
.initSyncConfig(user)
.then(() => {
useInterfaceStore()
.applyTheme()
.catch((e) => {
console.error('Error setting theme', e)
})
})
useUserHighlightStore().initUserHighlight(user)
commit('addNewUsers', [user])
dispatch('fetchEmoji')
useEmojiStore().fetchEmoji()
getNotificationPermission()
.then(permission => useInterfaceStore().setNotificationPermission(permission))
getNotificationPermission().then((permission) =>
useInterfaceStore().setNotificationPermission(permission),
)
// Set our new backend interactor
commit('setBackendInteractor', backendInteractorService(accessToken))
commit(
'setBackendInteractor',
backendInteractorService(accessToken),
)
// Do server-side storage migrations
@ -636,29 +731,15 @@ const users = {
/*
// Reset wordfilter
Object.keys(
useServerSideStorageStore().prefsStorage.simple.muteFilters
useSyncConfigStore().prefsStorage.simple.muteFilters
).forEach(key => {
useServerSideStorageStore().unsetPreference({ path: 'simple.muteFilters.' + key, value: null })
useSyncConfigStore().unsetSimplePrefAndSave({ path: 'muteFilters.' + key, value: null })
})
// Reset flag to 0 to re-run migrations
useServerSideStorageStore().setFlag({ flag: 'configMigration', value: 0 })
useSyncConfigStore().setFlag({ flag: 'configMigration', value: 0 })
/**/
const { configMigration } = useServerSideStorageStore().flagStorage
declarations
.filter(x => {
return x.store === 'server-side' &&
x.migrationNum > 0 &&
x.migrationNum > configMigration
})
.toSorted((a, b) => a.configMigration - b.configMigration)
.forEach(value => {
value.migration(useServerSideStorageStore(), store.rootState)
useServerSideStorageStore().setFlag({ flag: 'configMigration', value: value.migrationNum })
useServerSideStorageStore().pushServerSideStorage()
})
if (user.token) {
dispatch('setWsToken', user.token)
@ -673,7 +754,9 @@ const users = {
// Start fetching notifications
dispatch('startFetchingNotifications')
if (rootState.instance.pleromaChatMessagesAvailable) {
if (
useInstanceCapabilitiesStore().pleromaChatMessagesAvailable
) {
// Start fetching chats
dispatch('startFetchingChats')
}
@ -686,15 +769,23 @@ const users = {
dispatch('startFetchingFollowRequests')
}
if (store.getters.mergedConfig.useStreamingApi) {
if (useMergedConfigStore().mergedConfig.useStreamingApi) {
dispatch('fetchTimeline', { timeline: 'friends', since: null })
dispatch('fetchNotifications', { since: null })
dispatch('enableMastoSockets', true).catch((error) => {
console.error('Failed initializing MastoAPI Streaming socket', error)
}).then(() => {
dispatch('fetchChats', { latest: true })
setTimeout(() => dispatch('setNotificationsSilence', false), 10000)
})
dispatch('enableMastoSockets', true)
.catch((error) => {
console.error(
'Failed initializing MastoAPI Streaming socket',
error,
)
})
.then(() => {
dispatch('fetchChats', { latest: true })
setTimeout(
() => dispatch('setNotificationsSilence', false),
10000,
)
})
} else {
startPolling()
}
@ -706,7 +797,8 @@ const users = {
useInterfaceStore().setLayoutHeight(windowHeight())
// Fetch our friends
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
store.rootState.api.backendInteractor
.fetchFriends({ id: user.id })
.then((friends) => commit('addNewUsers', friends))
} else {
const response = data.error
@ -733,8 +825,8 @@ const users = {
reject(new Error('Failed to connect to server, try again'))
})
})
}
}
},
},
}
export default users