Page MenuHomePhorge

No OneTemporary

Size
69 KB
Referenced Files
None
Subscribers
None
diff --git a/src/i18n/messages.js b/src/i18n/messages.js
index 76d6b53860..db80f7ac36 100644
--- a/src/i18n/messages.js
+++ b/src/i18n/messages.js
@@ -1,58 +1,58 @@
// When contributing, please sort JSON before committing so it would be easier to see what's missing and what's being added compared to English and other languages. It's not obligatory, but just an advice.
// To sort json use jq https://stedolan.github.io/jq and invoke it like `jq -S . xx.json > xx.sorted.json`, AFAIK, there's no inplace edit option like in sed
// Also, when adding a new language to "messages" variable, please do it alphabetically by language code so that users can search or check their custom language easily.
// For anyone contributing to old huge messages.js and in need to quickly convert it to JSON
// sed command for converting currently formatted JS to JSON:
// sed -i -e "s/'//gm" -e 's/"/\\"/gm' -re 's/^( +)(.+?): ((.+?))?(,?)(\{?)$/\1"\2": "\4"/gm' -e 's/\"\{\"/{/g' -e 's/,"$/",/g' file.json
// There's only problem that apostrophe character ' gets replaced by \\ so you have to fix it manually, sorry.
import { isEqual } from 'lodash'
import enMessages from './en.json'
import { langCodeToJsonName, languages } from './languages.js'
const ULTIMATE_FALLBACK_LOCALE = 'en'
const hasLanguageFile = (code) => languages.includes(code)
const languageFileMap = import.meta.glob(['./*.json', '!./en.json'])
const loadLanguageFile = (code) => {
const jsonName = langCodeToJsonName(code)
- if (jsonName === 'en') return Promise.resolve({ default: enMessages })
+ if (jsonName === 'en') return { default: enMessages }
return languageFileMap[`./${jsonName}.json`]()
}
const messages = {
languages,
default: {
en: enMessages,
},
setLanguage: async (i18n, language) => {
const languages = (Array.isArray(language) ? language : [language]).filter(
(k) => k,
)
if (!languages.includes(ULTIMATE_FALLBACK_LOCALE)) {
languages.push(ULTIMATE_FALLBACK_LOCALE)
}
const [first, ...rest] = languages
if (first === i18n.locale && isEqual(rest, i18n.fallbackLocale)) {
return
}
for (const lang of languages) {
if (hasLanguageFile(lang)) {
const messages = await loadLanguageFile(lang)
i18n.setLocaleMessage(lang, messages.default)
}
}
i18n.fallbackLocale = rest
i18n.locale = first
},
}
export default messages
diff --git a/src/modules/users.js b/src/modules/users.js
index 0988b45c56..c4af0fb36a 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -1,870 +1,870 @@
import Cookies from 'js-cookie'
import { compact, each, last, map, mergeWith } from 'lodash'
import {
registerPushNotifications,
unregisterPushNotifications,
} from '../services/sw/sw.js'
import {
windowHeight,
windowWidth,
} from '../services/window_utils/window_utils'
import { useAnnouncementsStore } from 'src/stores/announcements.js'
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
import { useChatsStore } from 'src/stores/chats.js'
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 { useListsStore } from 'src/stores/lists.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUserHighlightStore } from 'src/stores/user_highlight.js'
import { revokeToken } from 'src/api/oauth.js'
import {
fetchFollowers,
fetchFriends,
fetchUser,
fetchUserByName,
getCaptcha,
register,
searchUsers,
verifyCredentials,
} from 'src/api/public.js'
import {
blockUser as apiBlockUser,
editUserNote as apiEditUserNote,
muteUser as apiMuteUser,
unblockUser as apiUnblockUser,
unmuteUser as apiUnmuteUser,
fetchBlocks,
fetchDomainMutes,
fetchMutes,
fetchUserInLists,
fetchUserRelationship,
followUser,
} from 'src/api/user.js'
// TODO: Unify with mergeOrAdd in statuses.js
export const mergeOrAdd = (arr, obj, item) => {
if (!item) {
return false
}
const oldItem = obj[item.id]
if (oldItem) {
// We already have this, so only merge the new info.
mergeWith(oldItem, item, mergeArrayLength)
return { item: oldItem, new: false }
} else {
// This is a new item, prepare it
arr.push(item)
obj[item.id] = item
return { item, new: true }
}
}
const mergeArrayLength = (oldValue, newValue) => {
if (Array.isArray(oldValue) && Array.isArray(newValue)) {
oldValue.length = newValue.length
return mergeWith(oldValue, newValue, mergeArrayLength)
}
}
const getNotificationPermission = () => {
const Notification = window.Notification
- if (!Notification) return Promise.resolve(null)
+ if (!Notification) return null
if (Notification.permission === 'default')
return Notification.requestPermission()
- return Promise.resolve(Notification.permission)
+ return Notification.permission
}
const blockUser = (store, args) => {
const id = args.id
const expiresIn = typeof args === 'object' ? args.expiresIn : 0
const predictedRelationship = store.state.relationships[id] || { id }
store.commit('updateUserRelationship', [predictedRelationship])
store.commit('addBlockId', id)
return apiBlockUser({ id, expiresIn }).then(({ data: 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,
})
})
}
const unblockUser = (store, id) => {
return apiUnblockUser({ id }).then(({ data: relationship }) =>
store.commit('updateUserRelationship', [relationship]),
)
}
const removeUserFromFollowers = (store, id) => {
return removeUserFromFollowers({ id }).then((relationship) =>
store.commit('updateUserRelationship', [relationship]),
)
}
const editUserNote = (store, { id, comment }) => {
return apiEditUserNote({ id, comment }).then((relationship) =>
store.commit('updateUserRelationship', [relationship]),
)
}
const muteUser = (store, args) => {
const id = typeof args === 'object' ? args.id : args
const expiresIn = typeof args === 'object' ? args.expiresIn : 0
const predictedRelationship = store.state.relationships[id] || { id }
store.commit('updateUserRelationship', [predictedRelationship])
store.commit('addMuteId', id)
return apiMuteUser({
id,
expiresIn,
credentials: useOAuthStore().token,
}).then(({ data: relationship }) => {
store.commit('updateUserRelationship', [relationship])
store.commit('addMuteId', id)
})
}
const unmuteUser = (store, id) => {
const predictedRelationship = store.state.relationships[id] || { id }
predictedRelationship.muting = false
store.commit('updateUserRelationship', [predictedRelationship])
return apiUnmuteUser({ id }).then(({ data: relationship }) =>
store.commit('updateUserRelationship', [relationship]),
)
}
const hideReblogs = (store, userId) => {
return followUser({
id: userId,
reblogs: false,
credentials: useOAuthStore().token,
}).then(({ data: relationship }) =>
store.commit('updateUserRelationship', [relationship]),
)
}
const showReblogs = (store, userId) => {
return followUser({
id: userId,
reblogs: true,
credentials: useOAuthStore().token,
}).then(({ data: relationship }) =>
store.commit('updateUserRelationship', [relationship]),
)
}
const muteDomain = (store, domain) => {
return muteDomain({
domain,
credentials: useOAuthStore().token,
}).then(() => store.commit('addDomainMute', domain))
}
const unmuteDomain = (store, domain) => {
return unmuteDomain({
domain,
credentials: useOAuthStore().token,
}).then(() => store.commit('removeDomainMute', domain))
}
export const mutations = {
tagUser(state, { user: { id }, tag }) {
const user = state.usersObject[id]
user.tags.add(tag)
},
untagUser(state, { user: { id }, tag }) {
const user = state.usersObject[id]
user.tags.delete(tag)
},
updateRight(state, { user: { id }, right, value }) {
const user = state.usersObject[id]
const newRights = user.rights
newRights[right] = value
user.rights = newRights
},
updateUserAdminData(state, { user }) {
const { id } = user
const localUser = state.usersObject[id]
localUser.adminData = user
localUser.deactivated = !user.is_active
localUser.tags = new Set(user.tags)
},
setCurrentUser(state, user) {
state.lastLoginName = user.screen_name
state.currentUser = mergeWith(
state.currentUser || {},
user,
mergeArrayLength,
)
},
clearCurrentUser(state) {
state.currentUser = false
state.lastLoginName = false
},
beginLogin(state) {
state.loggingIn = true
},
endLogin(state) {
state.loggingIn = false
},
saveFriendIds(state, { id, friendIds }) {
const user = state.usersObject[id]
user.friendIds = [...new Set([...(user.friendIds || []), ...friendIds])]
},
saveFollowerIds(state, { id, followerIds }) {
const user = state.usersObject[id]
user.followerIds = [...new Set([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) {
const user = state.usersObject[userId]
if (user) {
user.friendIds = []
}
},
clearFollowers(state, userId) {
const user = state.usersObject[userId]
if (user) {
user.followerIds = []
}
},
addNewUsers(state, users) {
each(users, (user) => {
if (user.relationship) {
state.relationships[user.relationship.id] = user.relationship
}
const res = mergeOrAdd(state.users, state.usersObject, user)
const item = res.item
if (res.new && item.screen_name && !item.screen_name.includes('@')) {
state.usersByNameObject[item.screen_name.toLowerCase()] = item
}
})
},
updateUserRelationship(state, relationships) {
relationships.forEach((relationship) => {
state.relationships[relationship.id] = relationship
})
},
updateUserInLists(state, { id, inLists }) {
state.usersObject[id].inLists = inLists
},
saveBlockIds(state, blockIds) {
state.currentUser.blockIds = blockIds
},
addBlockId(state, blockId) {
if (state.currentUser.blockIds.includes(blockId)) {
state.currentUser.blockIds.push(blockId)
}
},
setBlockIdsMaxId(state, blockIdsMaxId) {
state.currentUser.blockIdsMaxId = blockIdsMaxId
},
saveMuteIds(state, muteIds) {
state.currentUser.muteIds = muteIds
},
setMuteIdsMaxId(state, muteIdsMaxId) {
state.currentUser.muteIdsMaxId = muteIdsMaxId
},
addMuteId(state, muteId) {
if (state.currentUser.muteIds.includes(muteId)) {
state.currentUser.muteIds.push(muteId)
}
},
saveDomainMutes(state, domainMutes) {
state.currentUser.domainMutes = domainMutes
},
addDomainMute(state, domain) {
if (state.currentUser.domainMutes.includes(domain)) {
state.currentUser.domainMutes.push(domain)
}
},
removeDomainMute(state, domain) {
const index = state.currentUser.domainMutes.indexOf(domain)
if (index !== -1) {
state.currentUser.domainMutes.splice(index, 1)
}
},
setPinnedToUser(state, status) {
const user = state.usersObject[status.user.id]
user.pinnedStatusIds = user.pinnedStatusIds || []
const index = user.pinnedStatusIds.indexOf(status.id)
if (status.pinned && index === -1) {
user.pinnedStatusIds.push(status.id)
} else if (!status.pinned && index !== -1) {
user.pinnedStatusIds.splice(index, 1)
}
},
setUserForStatus(state, status) {
status.user = state.usersObject[status.user.id]
},
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 }) {
const user = state.usersObject[id]
user.highlight = highlighted
},
signUpPending(state) {
state.signUpPending = true
state.signUpErrors = []
state.signUpNotice = {}
},
signUpSuccess(state) {
state.signUpPending = false
},
signUpFailure(state, errors) {
state.signUpPending = false
state.signUpErrors = errors
state.signUpNotice = {}
},
signUpNotice(state, notice) {
state.signUpPending = false
state.signUpErrors = []
state.signUpNotice = notice
},
}
export const getters = {
findUser: (state) => (query) => {
return state.usersObject[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(),
)
},
relationship: (state) => (id) => {
const rel = id && state.relationships[id]
return rel || { id, loading: true }
},
}
export const defaultState = {
loggingIn: false,
lastLoginName: false,
currentUser: false,
users: [],
usersObject: {},
usersByNameObject: {},
signUpPending: false,
signUpErrors: [],
signUpNotice: {},
relationships: {},
}
const users = {
state: defaultState,
mutations,
getters,
actions: {
fetchUserIfMissing(store, id) {
const user = store.getters.findUser(id)
if (!user) {
return store.dispatch('fetchUser', id)
} else {
- return Promise.resolve(user)
+ return user
}
},
updateUserAdminData(store, { userAdminData }) {
return store
.dispatch('fetchUserIfMissing', userAdminData.id)
.then((user) => {
user.adminData = userAdminData
store.commit('addNewUsers', [user])
return user
})
},
fetchUser(store, id) {
return fetchUser({
id,
credentials: useOAuthStore().token,
})
.then(({ data: user }) => {
store.commit('addNewUsers', [user])
return user
})
.catch((error) => {
if (error.statusCode === 404) {
console.warn(`User ${id} not found`)
} else {
throw error
}
})
},
fetchUserByName(store, name) {
return fetchUserByName({
name,
credentials: useOAuthStore().token,
}).then(({ data: user }) => {
store.commit('addNewUsers', [user])
return user
})
},
fetchUserRelationship(store, id) {
if (store.state.currentUser) {
fetchUserRelationship({
id,
credentials: useOAuthStore().token,
}).then(({ data: relationships }) =>
store.commit('updateUserRelationship', relationships),
)
}
},
fetchUserInLists(store, id) {
if (store.state.currentUser) {
fetchUserInLists({
id,
credentials: useOAuthStore().token,
}).then(({ data: inLists }) =>
store.commit('updateUserInLists', { id, inLists }),
)
}
},
fetchBlocks(store, args) {
const { reset } = args || {}
const maxId = store.state.currentUser.blockIdsMaxId
return fetchBlocks({
maxId,
credentials: useOAuthStore().token,
}).then(({ data: blocks }) => {
if (reset) {
store.commit('saveBlockIds', map(blocks, 'id'))
} else {
map(blocks, 'id').map((id) => store.commit('addBlockId', id))
}
if (blocks.length) {
store.commit('setBlockIdsMaxId', last(blocks).id)
}
store.commit('addNewUsers', blocks)
return blocks
})
},
blockUser(store, data) {
return blockUser(store, data)
},
unblockUser(store, data) {
return unblockUser(store, data)
},
removeUserFromFollowers(store, id) {
return removeUserFromFollowers(store, id)
},
blockUsers(store, data = []) {
return Promise.all(data.map((d) => blockUser(store, d)))
},
unblockUsers(store, data = []) {
return Promise.all(data.map((d) => unblockUser(store, d)))
},
editUserNote(store, args) {
return editUserNote(store, args)
},
fetchMutes(store, args) {
const { reset } = args || {}
const maxId = store.state.currentUser.muteIdsMaxId
return fetchMutes({
maxId,
credentials: useOAuthStore().token,
}).then(({ data: mutes }) => {
if (reset) {
store.commit('saveMuteIds', map(mutes, 'id'))
} else {
map(mutes, 'id').map((id) => store.commit('addMuteId', id))
}
if (mutes.length) {
store.commit('setMuteIdsMaxId', last(mutes).id)
}
store.commit('addNewUsers', mutes)
return mutes
})
},
muteUser(store, data) {
return muteUser(store, data)
},
unmuteUser(store, id) {
return unmuteUser(store, id)
},
hideReblogs(store, id) {
return hideReblogs(store, id)
},
showReblogs(store, id) {
return showReblogs(store, id)
},
muteUsers(store, data = []) {
return Promise.all(data.map((d) => muteUser(store, d)))
},
unmuteUsers(store, ids = []) {
return Promise.all(ids.map((d) => unmuteUser(store, d)))
},
fetchDomainMutes(store) {
return fetchDomainMutes({
credentials: useOAuthStore().token,
}).then(({ data: domainMutes }) => {
store.commit('saveDomainMutes', domainMutes)
return domainMutes
})
},
muteDomain(store, domain) {
return muteDomain(store, domain)
},
unmuteDomain(store, domain) {
return unmuteDomain(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)))
},
fetchFriends({ rootState, commit }, id) {
const user = rootState.users.usersObject[id]
const maxId = last(user.friendIds)
return fetchFriends({
id,
maxId,
credentials: useOAuthStore().token,
}).then(({ data: friends }) => {
commit('addNewUsers', friends)
commit('saveFriendIds', { id, friendIds: map(friends, 'id') })
return friends
})
},
fetchFollowers({ rootState, commit }, id) {
const user = rootState.users.usersObject[id]
const maxId = last(user.followerIds)
return fetchFollowers({
id,
maxId,
credentials: useOAuthStore().token,
}).then(({ data: followers }) => {
commit('addNewUsers', followers)
commit('saveFollowerIds', { id, followerIds: map(followers, 'id') })
return followers
})
},
clearFriends({ commit }, userId) {
commit('clearFriends', userId)
},
clearFollowers({ commit }, userId) {
commit('clearFollowers', userId)
},
subscribeUser({ rootState, commit }, id) {
return followUser({
id,
notify: true,
credentials: useOAuthStore().token,
}).then(({ data: relationship }) =>
commit('updateUserRelationship', [relationship]),
)
},
unsubscribeUser({ rootState, commit }, id) {
return followUser({
id,
notify: false,
credentials: useOAuthStore().token,
}).then(({ data: relationship }) =>
commit('updateUserRelationship', [relationship]),
)
},
registerPushNotifications(store) {
const token = store.state.currentUser.credentials
const vapidPublicKey = useInstanceStore().vapidPublicKey
const isEnabled = useMergedConfigStore().mergedConfig.webPushNotifications
const notificationVisibility =
useMergedConfigStore().mergedConfig.notificationVisibility
registerPushNotifications(
isEnabled,
vapidPublicKey,
token,
notificationVisibility,
)
},
unregisterPushNotifications(store) {
const token = store.state.currentUser.credentials
unregisterPushNotifications(token)
},
addNewUsers({ commit }, users) {
commit('addNewUsers', users)
},
addNewStatuses(store, { statuses }) {
const users = map(statuses, 'user')
const retweetedUsers = compact(map(statuses, 'retweeted_status.user'))
store.commit('addNewUsers', users)
store.commit('addNewUsers', retweetedUsers)
each(statuses, (status) => {
// Reconnect users to statuses
store.commit('setUserForStatus', status)
// Set pinned statuses to user
store.commit('setPinnedToUser', status)
})
each(compact(map(statuses, 'retweeted_status')), (status) => {
// Reconnect users to retweets
store.commit('setUserForStatus', status)
// Set pinned retweets to user
store.commit('setPinnedToUser', status)
})
},
addNewNotifications(store, { notifications }) {
const users = map(notifications, 'from_profile')
const targetUsers = map(notifications, 'target').filter(Boolean)
const notificationIds = notifications.map((_) => _.id)
store.commit('addNewUsers', users)
store.commit('addNewUsers', targetUsers)
const notificationsObject = store.rootState.notifications.idStore
const relevantNotifications = Object.entries(notificationsObject)
.filter(([k]) => notificationIds.includes(k))
.map(([, val]) => val)
// Reconnect users to notifications
each(relevantNotifications, (notification) => {
store.commit('setUserForNotification', notification)
})
},
searchUsers({ rootState, commit }, { query }) {
return searchUsers({
query,
credentials: useOAuthStore().token,
}).then(({ data: users }) => {
commit('addNewUsers', users)
return users
})
},
async signUp(store, userInfo) {
const oauthStore = useOAuthStore()
store.commit('signUpPending')
try {
const token = await oauthStore.ensureAppToken()
const { data } = await register({
credentials: token,
params: { ...userInfo },
})
if (data.access_token) {
store.commit('signUpSuccess')
oauthStore.setToken(data.access_token)
await store.dispatch('loginUser', data.access_token)
return 'ok'
} else {
// Request succeeded, but user cannot login yet.
store.commit('signUpNotice', data)
return 'request_sent'
}
} catch (e) {
const errors = e.message
store.commit('signUpFailure', errors)
throw e
}
},
getCaptcha(store) {
return getCaptcha({
credentials: useOAuthStore().token,
}).then(({ data }) => data)
},
logout(store) {
const oauth = useOAuthStore()
// NOTE: No need to verify the app still exists, because if it doesn't,
// the token will be invalid too
return oauth
.ensureApp()
.then((app) => {
const params = {
app,
instance: useInstanceStore().server,
token: oauth.userToken,
}
return revokeToken(params)
})
.then(() => {
store.commit('clearCurrentUser')
store.dispatch('disconnectFromSocket')
store.dispatch('stopFetchingTimeline', 'friends')
store.dispatch('stopFetchingNotifications')
useListsStore().stopFetching()
useBookmarkFoldersStore().stopFetching()
store.dispatch('stopFetchingFollowRequests')
store.commit('clearNotifications')
store.commit('resetStatuses')
useChatsStore().resetChats()
oauth.clearToken()
Cookies.remove('__Host-pleroma_key', { path: '/' })
useInterfaceStore().setLastTimeline('public-timeline')
useInterfaceStore().setLayoutWidth(windowWidth())
useInterfaceStore().setLayoutHeight(windowHeight())
})
},
loginUser(store, accessToken) {
return new Promise((resolve, reject) => {
const commit = store.commit
const dispatch = store.dispatch
commit('beginLogin')
verifyCredentials({
credentials: useOAuthStore().token,
})
.then(({ data: user }) => {
// user.credentials = userCredentials
user.credentials = accessToken
user.blockIds = []
user.muteIds = []
user.domainMutes = []
commit('setCurrentUser', user)
useSyncConfigStore()
.initSyncConfig(user)
.then(() => {
useInterfaceStore()
.applyTheme()
.catch((e) => {
console.error('Error setting theme', e)
})
})
useUserHighlightStore().initUserHighlight(user)
commit('addNewUsers', [user])
useEmojiStore().fetchEmoji()
getNotificationPermission().then((permission) =>
useInterfaceStore().setNotificationPermission(permission),
)
// Do server-side storage migrations
// Debug snippet to clean up storage and reset migrations
/*
// Reset wordfilter
Object.keys(
useSyncConfigStore().prefsStorage.simple.muteFilters
).forEach(key => {
useSyncConfigStore().unsetSimplePrefAndSave({ path: 'muteFilters.' + key, value: null })
})
// Reset flag to 0 to re-run migrations
useSyncConfigStore().setFlag({ flag: 'configMigration', value: 0 })
/**/
if (user.token) {
dispatch('setWsToken', user.token)
// Initialize the shout socket.
dispatch('initializeSocket')
}
const startPolling = () => {
// Start getting fresh posts.
dispatch('startFetchingTimeline', { timeline: 'friends' })
// Start fetching notifications
dispatch('startFetchingNotifications')
if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) {
// Start fetching chats
dispatch('startFetchingChats')
}
}
useListsStore().startFetching()
useBookmarkFoldersStore().startFetching()
if (user.locked) {
dispatch('startFetchingFollowRequests')
}
if (useMergedConfigStore().mergedConfig.useStreamingApi) {
dispatch('fetchTimeline', {
timeline: 'friends',
sinceId: null,
})
dispatch('fetchNotifications', { sinceId: null })
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()
}
// Start fetching things that don't need to block the UI
useAnnouncementsStore().startFetchingAnnouncements()
dispatch('fetchMutes')
dispatch('loadDrafts')
useInterfaceStore().setLayoutWidth(windowWidth())
useInterfaceStore().setLayoutHeight(windowHeight())
// Fetch our friends
fetchFriends({ id: user.id }).then(({ data: friends }) =>
commit('addNewUsers', friends),
)
commit('endLogin')
resolve()
})
.catch((error) => {
console.error(error)
// Authentication failed
commit('endLogin')
// remove authentication token on client/authentication errors
if ([400, 401, 403, 422].includes(error.statusCode)) {
useOAuthStore().clearToken()
}
commit('endLogin')
if (error.tatusCode === 401) {
throw new Error('Wrong username or password', error)
} else {
throw new Error('An error occurred, please try again', error)
}
})
})
},
},
}
export default users
diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js
index 68e0632bf3..a6f22e50b1 100644
--- a/src/services/style_setter/style_setter.js
+++ b/src/services/style_setter/style_setter.js
@@ -1,365 +1,365 @@
import sum from 'hash-sum'
import localforage from 'localforage'
import { chunk, throttle } from 'lodash'
import { getCssRules } from '../theme_data/css_utils.js'
import { getEngineChecksum, init } from '../theme_data/theme_data_3.service.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { promisedRequest } from 'src/api/helpers.js'
import { ROOT_CONFIG } from 'src/modules/default_config_state.js'
// On platforms where this is not supported, it will return undefined
// Otherwise it will return an array
const supportsAdoptedStyleSheets = !!document.adoptedStyleSheets
const stylesheets = {}
export const createStyleSheet = (id, priority = 1000) => {
if (stylesheets[id]) return stylesheets[id]
const newStyleSheet = {
rules: [],
ready: false,
priority,
clear() {
this.rules = []
},
addRule(rule) {
let newRule = rule
if (!CSS.supports?.('backdrop-filter', 'blur()')) {
newRule = newRule.replaceAll(/backdrop-filter:[^;]+;/g, '') // Remove backdrop-filter
}
if (newRule.startsWith('::-webkit')) {
if (typeof CSS.supports !== 'function') return
// firefox doesn't like invalid selectors
const fullWebkitScrollbarSupport =
CSS.supports('selector(::-webkit-scrollbar)') &&
CSS.supports('selector(::-webkit-scrollbar-button)') &&
CSS.supports('selector(::-webkit-resizer)') &&
CSS.supports('selector(::-webkit-scrollbar-thumb)')
if (!fullWebkitScrollbarSupport) return
}
this.rules.push(
newRule.replaceAll(/var\(--shadowFilter\)[^;]*;/g, ''), // Remove shadowFilter references
)
},
}
stylesheets[id] = newStyleSheet
return newStyleSheet
}
export const adoptStyleSheets = throttle(() => {
if (supportsAdoptedStyleSheets) {
document.adoptedStyleSheets = Object.values(stylesheets)
.filter((x) => x.ready)
.sort((a, b) => a.priority - b.priority)
.map((sheet) => {
const css = new CSSStyleSheet()
sheet.rules.forEach((r) => {
try {
css.insertRule(r)
} catch (e) {
console.warn('Error inserting rule:', e, r)
}
})
return css
})
} else {
const holder = document.getElementById('custom-styles-holder')
for (let i = holder.sheet.cssRules.length - 1; i >= 0; --i) {
holder.sheet.deleteRule(i)
}
Object.values(stylesheets)
.filter((x) => x.ready)
.sort((a, b) => a.priority - b.priority)
.forEach((sheet) => {
sheet.rules.forEach((r) => holder.sheet.insertRule(r))
})
}
// Some older browsers do not support document.adoptedStyleSheets.
// In this case, we use the <style> elements.
// Since the <style> elements we need are already in the DOM, there
// is nothing to do here.
}, 500)
const EAGER_STYLE_ID = 'pleroma-eager-styles'
const LAZY_STYLE_ID = 'pleroma-lazy-styles'
const generateTheme = (inputRuleset, callbacks, debug) => {
const {
onNewRule = () => {
/* no-op */
},
onLazyFinished = () => {
/* no-op */
},
onEagerFinished = () => {
/* no-op */
},
} = callbacks
const themes3 = init({
inputRuleset,
debug,
})
getCssRules(themes3.eager, debug).forEach((rule) => {
// Hacks to support multiple selectors on same component
onNewRule(rule, false)
})
onEagerFinished()
// Optimization - instead of processing all lazy rules in one go, process them in small chunks
// so that UI can do other things and be somewhat responsive while less important rules are being
// processed
let counter = 0
const chunks = chunk(themes3.lazy, 200)
// let t0 = performance.now()
const processChunk = () => {
const chunk = chunks[counter]
Promise.all(chunk.map((x) => x())).then((result) => {
getCssRules(result.filter(Boolean), debug).forEach((rule) => {
onNewRule(rule, true)
})
// const t1 = performance.now()
// console.debug('Chunk ' + counter + ' took ' + (t1 - t0) + 'ms')
// t0 = t1
counter += 1
if (counter < chunks.length) {
setTimeout(processChunk, 0)
} else {
onLazyFinished()
}
})
}
return { lazyProcessFunc: processChunk }
}
export const tryLoadCache = async () => {
console.info('Trying to load compiled theme data from cache')
const cache = await localforage.getItem('pleromafe-theme-cache')
if (!cache) return null
try {
if (
cache.engineChecksum === getEngineChecksum() &&
cache.checksum !== undefined &&
cache.checksum === useMergedConfigStore().mergedConfig.themeChecksum
) {
const eagerStyles = createStyleSheet(EAGER_STYLE_ID, 10)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID, 20)
cache.data[0].forEach((rule) => eagerStyles.addRule(rule))
cache.data[1].forEach((rule) => lazyStyles.addRule(rule))
eagerStyles.ready = true
lazyStyles.ready = true
console.info(`Loaded theme from cache`)
return true
} else {
console.warn("Checksums don't match, cache not usable, clearing")
localStorage.removeItem('pleroma-fe-theme-cache')
}
} catch (e) {
console.error('Failed to load theme cache:', e)
return false
}
}
export const applyTheme = (
input,
onEagerFinish = () => {
/* no-op */
},
onFinish = () => {
/* no-op */
},
debug,
) => {
const eagerStyles = createStyleSheet(EAGER_STYLE_ID, 10)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID, 20)
eagerStyles.clear()
lazyStyles.clear()
const { lazyProcessFunc } = generateTheme(
input,
{
onNewRule(rule, isLazy) {
if (isLazy) {
lazyStyles.addRule(rule)
} else {
eagerStyles.addRule(rule)
}
},
onEagerFinished() {
eagerStyles.ready = true
adoptStyleSheets()
onEagerFinish()
console.info(
'Eager part of theme finished, waiting for lazy part to finish to store cache',
)
},
onLazyFinished() {
lazyStyles.ready = true
adoptStyleSheets()
const data = [eagerStyles.rules, lazyStyles.rules]
const checksum = sum(data)
const cache = {
checksum,
engineChecksum: getEngineChecksum(),
data,
}
useSyncConfigStore().setSimplePrefAndSave({
path: 'themeChecksum',
value: checksum,
})
onFinish(cache)
localforage.setItem('pleromafe-theme-cache', cache)
console.info('Theme cache stored')
},
},
debug,
)
setTimeout(lazyProcessFunc, 0)
}
const extractStyleConfig = ({
sidebarColumnWidth,
contentColumnWidth,
notifsColumnWidth,
themeEditorMinWidth,
emojiReactionsScale,
emojiSize,
navbarSize,
panelHeaderSize,
textSize,
forcedRoundness,
}) => {
const result = {
sidebarColumnWidth,
contentColumnWidth,
notifsColumnWidth,
themeEditorMinWidth:
Number.parseInt(themeEditorMinWidth) === 0
? 'fit-content'
: themeEditorMinWidth,
emojiReactionsScale,
emojiSize,
navbarSize,
panelHeaderSize,
textSize,
}
switch (forcedRoundness) {
case 'disable':
break
case '0':
result.forcedRoundness = '0'
break
case '1':
result.forcedRoundness = '1px'
break
case '2':
result.forcedRoundness = '0.4rem'
break
default:
}
return result
}
const defaultStyleConfig = extractStyleConfig(ROOT_CONFIG)
export const applyStyleConfig = (input) => {
const config = extractStyleConfig(input)
if (config === defaultStyleConfig) {
return
}
const rules = Object.entries(config)
.filter(([, v]) => v)
.map(([k, v]) => `--${k}: ${v}`)
.join(';')
const styleSheet = createStyleSheet('theme-holder', 30)
styleSheet.clear()
styleSheet.addRule(`:root { ${rules} }`)
// TODO find a way to make this not apply to theme previews
if (Object.hasOwn(config, 'forcedRoundness')) {
styleSheet.addRule(` *:not(.preview-block) {
--roundness: var(--forcedRoundness) !important;
}`)
}
styleSheet.ready = true
adoptStyleSheets()
}
export const getResourcesIndex = async (url, parser = (x) => x) => {
const cache = 'no-store'
const customUrl = url.replace(/\.(\w+)$/, '.custom.$1')
let builtin
let custom
const resourceTransform = (resources) => {
return Object.entries(resources).map(([k, v]) => {
if (typeof v === 'object') {
- return [k, () => Promise.resolve(v)]
+ return [k, () => v]
} else if (typeof v === 'string') {
return [
k,
() =>
promisedRequest({
url: v,
cache,
})
.then(({ data: text }) => parser(text))
.catch((e) => {
console.error(e)
return null
}),
]
} else {
console.error(`Unknown resource format - ${k} is a ${typeof v}`)
return [k, null]
}
})
}
try {
const { data: builtinData } = await promisedRequest({ url, cache })
builtin = resourceTransform(builtinData)
} catch {
builtin = []
console.warn(`Builtin resources at ${url} unavailable`)
}
try {
const { data: customData } = await promisedRequest({
url: customUrl,
cache,
})
custom = resourceTransform(customData)
} catch {
custom = []
console.warn(`Custom resources at ${customUrl} unavailable`)
}
const total = [...custom, ...builtin]
if (total.length === 0) {
throw new Error(`Resource at ${url} and ${customUrl} completely unavailable. Panicking`)
}
- return Promise.resolve(Object.fromEntries(total))
+ return Object.fromEntries(total)
}
diff --git a/src/services/sw/sw.js b/src/services/sw/sw.js
index 30c95406ba..53c7b0cf2c 100644
--- a/src/services/sw/sw.js
+++ b/src/services/sw/sw.js
@@ -1,189 +1,189 @@
/* global process */
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding)
.replaceAll('-', '+')
.replaceAll('_', '/')
const rawData = window.atob(base64)
return Uint8Array.from([...rawData].map((char) => char.codePointAt(0)))
}
export function isSWSupported() {
return 'serviceWorker' in navigator
}
function isPushSupported() {
return 'PushManager' in window
}
function getOrCreateServiceWorker() {
if (!isSWSupported()) return
const swType = process.env.HAS_MODULE_SERVICE_WORKER ? 'module' : 'classic'
return navigator.serviceWorker
.register('/sw-pleroma.js', { type: swType })
.catch((err) =>
console.error('Unable to get or create a service worker.', err),
)
}
function subscribePush(registration, isEnabled, vapidPublicKey) {
if (!isEnabled)
throw new Error('Web Push is disabled in config')
if (!vapidPublicKey)
throw new Error('VAPID public key is not found')
const subscribeOptions = {
userVisibleOnly: false,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey),
}
return registration.pushManager.subscribe(subscribeOptions)
}
-function unsubscribePush(registration) {
+async function unsubscribePush(registration) {
return registration.pushManager.getSubscription().then((subscription) => {
if (subscription === null) {
- return Promise.resolve('No subscription')
+ return 'No subscription'
}
return subscription.unsubscribe()
})
}
function deleteSubscriptionFromBackEnd(token) {
return fetch('/api/v1/push/subscription/', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
}).then((response) => {
if (!response.ok) throw new Error('Bad status code from server.')
return response
})
}
function sendSubscriptionToBackEnd(
subscription,
token,
notificationVisibility,
) {
return window
.fetch('/api/v1/push/subscription/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
subscription,
data: {
alerts: {
follow: notificationVisibility.follows,
favourite: notificationVisibility.likes,
mention: notificationVisibility.mentions,
reblog: notificationVisibility.repeats,
move: notificationVisibility.moves,
},
},
}),
})
.then((response) => {
if (!response.ok) throw new Error('Bad status code from server.')
return response.json()
})
.then((responseData) => {
if (!responseData.id) throw new Error('Bad response from server.')
return responseData
})
}
export async function initServiceWorker(store) {
if (!isSWSupported()) return
await getOrCreateServiceWorker()
navigator.serviceWorker.addEventListener('message', (event) => {
const { dispatch } = store
const { type, ...rest } = event.data
switch (type) {
case 'notificationClicked':
dispatch('notificationClicked', { id: rest.id })
}
})
}
export async function showDesktopNotification(content) {
if (!isSWSupported) return
const { active: sw } =
(await window.navigator.serviceWorker.getRegistration()) || {}
if (!sw) return console.error('No serviceworker found!')
sw.postMessage({ type: 'desktopNotification', content })
}
export async function closeDesktopNotification({ id }) {
if (!isSWSupported) return
const { active: sw } =
(await window.navigator.serviceWorker.getRegistration()) || {}
if (!sw) return console.error('No serviceworker found!')
if (id >= 0) {
sw.postMessage({ type: 'desktopNotificationClose', content: { id } })
} else {
sw.postMessage({ type: 'desktopNotificationClose', content: { all: true } })
}
}
export async function updateFocus() {
if (!isSWSupported) return
const { active: sw } =
(await window.navigator.serviceWorker.getRegistration()) || {}
if (!sw) return console.error('No serviceworker found!')
sw.postMessage({ type: 'updateFocus' })
}
export function registerPushNotifications(
isEnabled,
vapidPublicKey,
token,
notificationVisibility,
) {
if (isPushSupported()) {
getOrCreateServiceWorker()
.then((registration) =>
subscribePush(registration, isEnabled, vapidPublicKey),
)
.then((subscription) =>
sendSubscriptionToBackEnd(subscription, token, notificationVisibility),
)
.catch((e) =>
console.warn(`Failed to setup Web Push Notifications: ${e.message}`),
)
}
}
export function unregisterPushNotifications(token) {
if (isPushSupported()) {
getOrCreateServiceWorker()
.then((registration) => {
return unsubscribePush(registration).then((result) => [
registration,
result,
])
})
.then(([, unsubResult]) => {
if (unsubResult === 'No subscription') return
if (!unsubResult) {
console.warn("Push subscription cancellation wasn't successful")
}
return deleteSubscriptionFromBackEnd(token)
})
.catch((e) => {
console.warn(`Failed to disable Web Push Notifications: ${e.message}`)
})
}
}
export const shouldCache = process.env.NODE_ENV === 'production'
export const cacheKey = 'pleroma-fe'
export const emojiCacheKey = 'pleroma-fe-emoji'
export const clearCache = (key) => caches.delete(key)
export { getOrCreateServiceWorker }
diff --git a/src/stores/interface.js b/src/stores/interface.js
index edc8796142..7be72a8d2a 100644
--- a/src/stores/interface.js
+++ b/src/stores/interface.js
@@ -1,803 +1,803 @@
import { defineStore } from 'pinia'
import {
applyTheme,
getResourcesIndex,
tryLoadCache,
} from '../services/style_setter/style_setter.js'
import { deserialize } from '../services/theme_data/iss_deserializer.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
import {
CURRENT_VERSION,
generatePreset,
} from 'src/services/theme_data/theme_data.service.js'
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.js'
const GENERIC_FONT_NAMES = new Set([
'serif',
'sans-serif',
'system-ui',
'cursive',
'fantasy',
'math',
'monospace',
])
export const useInterfaceStore = defineStore('interface', {
state: () => ({
localFonts: null,
themeApplied: false,
themeChangeInProgress: false,
themeVersion: 'v3',
styleNameUsed: null,
styleDataUsed: null,
useStylePalette: false, // hack for applying styles from appearance tab
paletteNameUsed: null,
paletteDataUsed: null,
themeNameUsed: null,
themeDataUsed: null,
temporaryChangesTimeoutId: null,
temporaryChangesCountdown: -1, // used for temporary options that revert after a timeout
temporaryChangesConfirm: () => {
/* no-op */
}, // used for applying temporary options
temporaryChangesRevert: () => {
/* no-op */
}, // used for reverting temporary options
settingsModalState: 'hidden',
settingsModalLoadedUser: false,
settingsModalLoadedAdmin: false,
settingsModalTargetTab: null,
settingsModalMode: 'user',
settings: {
currentSaveStateNotice: null,
noticeClearTimeout: null,
notificationPermission: null,
},
browserSupport: {
cssFilter:
window.CSS?.supports &&
(window.CSS.supports('filter', 'drop-shadow(0 0)') ||
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')),
localFonts: typeof window.queryLocalFonts === 'function',
},
layoutType: 'normal',
globalNotices: [],
globalError: null,
layoutHeight: 0,
lastTimeline: null,
foreignProfileBackground: null,
}),
actions: {
setTemporaryChanges({ confirm, revert }) {
this.temporaryChangesCountdown = 10
this.temporaryChangesConfirm = confirm
this.temporaryChangesRevert = revert
const countdownFunc = () => {
if (this.temporaryChangesCountdown <= 1) {
this.temporaryChangesRevert()
this.clearTemporaryChanges()
} else {
this.temporaryChangesCountdown--
this.temporaryChangesTimeoutId = setTimeout(countdownFunc, 1000)
}
}
this.temporaryChangesTimeoutId = setTimeout(countdownFunc, 1000)
},
clearTemporaryChanges() {
this.temporaryChangesTimeoutId ??
clearTimeout(this.temporaryChangesTimeoutId)
this.temporaryChangesTimeoutId = null
this.temporaryChangesCountdown = -1
this.temporaryChangesConfirm = () => {
/* no-op */
}
this.temporaryChangesRevert = () => {
/* no-op */
}
},
setPageTitle(option = '') {
try {
document.title = `${option} ${useInstanceStore().instanceIdentity.name}`
} catch (error) {
console.error(`${error}`)
}
},
setForeignProfileBackground(url) {
this.foreignProfileBackground = url
},
settingsSaved({ success, error }) {
if (success) {
if (this.noticeClearTimeout) {
clearTimeout(this.noticeClearTimeout)
}
this.settings.currentSaveStateNotice = { error: false, data: success }
this.settings.noticeClearTimeout = setTimeout(
() => delete this.settings.currentSaveStateNotice,
2000,
)
} else {
this.settings.currentSaveStateNotice = { error: true, errorData: error }
}
},
setNotificationPermission(permission) {
this.notificationPermission = permission
},
closeSettingsModal() {
this.settingsModalState = 'hidden'
},
openSettingsModal(value) {
this.settingsModalMode = value
this.settingsModalState = 'visible'
if (value === 'user') {
if (!this.settingsModalLoadedUser) {
this.settingsModalLoadedUser = true
}
} else if (value === 'admin') {
if (!this.settingsModalLoadedAdmin) {
this.settingsModalLoadedAdmin = true
}
}
},
setSettingsModalState(newState) {
const oldState = this.settingsModalState
const legal = (() => {
switch (oldState) {
case 'minimized':
return true
case 'visible':
return true
case 'hidden':
return newState === 'visible'
}
})()
if (legal) {
this.settingsModalState = newState
}
},
toggleMinimizeSettingsModal() {
switch (this.settingsModalState) {
case 'minimized':
this.settingsModalState = 'visible'
return
case 'visible':
this.settingsModalState = 'minimized'
return
case 'hidden':
return
default:
throw new Error(
`Illegal minimization state of settings modal: ${this.settingsModalState}`,
)
}
},
clearSettingsModalTargetTab() {
this.settingsModalTargetTab = null
},
openSettingsModalTab(value, mode = 'user') {
this.settingsModalTargetTab = value
this.openSettingsModal(mode)
},
removeGlobalNotice(notice) {
this.globalNotices = this.globalNotices.filter((n) => n !== notice)
},
setGlobalError({ error, instance, info }) {
switch (info) {
case 'https://vuejs.org/error-reference/#runtime-13': {
this.globalError = {
title: 'general.refresh_required',
content: 'general.refresh_required_content',
// `true` disables cache on Firefox (non-standard)
recover: () => window.location.reload(true),
recoverText: 'general.refresh_required_refresh',
error,
}
break
}
default: {
this.globalError = { error }
break
}
}
},
clearGlobalError() {
this.globalError = null
},
pushGlobalNotice({
messageKey,
messageArgs = {},
level = 'error',
timeout = 5000,
}) {
const notice = {
messageKey,
messageArgs,
level,
}
this.globalNotices.push(notice)
// Adding a new element to array wraps it in a Proxy, which breaks the comparison
// TODO: Generate UUID or something instead or relying on !== operator?
const newNotice = this.globalNotices[this.globalNotices.length - 1]
if (timeout > 0) {
setTimeout(() => this.removeGlobalNotice(newNotice), timeout)
}
return newNotice
},
setLayoutHeight(value) {
this.layoutHeight = value
},
setLayoutWidth(value) {
let width = value
if (value !== undefined) {
this.layoutWidth = value
} else {
width = this.layoutWidth
}
const mobileLayout = width <= 800
const normalOrMobile = mobileLayout ? 'mobile' : 'normal'
const { thirdColumnMode } = useMergedConfigStore().mergedConfig
if (thirdColumnMode === 'none' || !window.vuex.state.users.currentUser) {
this.layoutType = normalOrMobile
} else {
const wideLayout = width >= 1300
this.layoutType = wideLayout ? 'wide' : normalOrMobile
}
},
setFontsList(value) {
this.localFonts = [...new Set(value.map(({ family }) => family)).values()]
},
queryLocalFonts() {
if (this.localFonts !== null) return
this.setFontsList([])
if (!this.browserSupport.localFonts) {
return
}
window
.queryLocalFonts()
.then((fonts) => {
this.setFontsList(fonts)
})
.catch((e) => {
this.pushGlobalNotice({
messageKey: 'settings.style.themes3.font.font_list_unavailable',
messageArgs: {
error: e,
},
level: 'error',
})
})
},
setLastTimeline(value) {
this.lastTimeline = value
},
async fetchPalettesIndex() {
try {
const value = await getResourcesIndex('/static/palettes/index.json')
useInstanceStore().set({
path: 'palettesIndex',
value,
})
return value
} catch (e) {
console.error('Could not fetch palettes index', e)
useInstanceStore().set({
path: 'palettesIndex',
value: { _error: e },
})
- return Promise.resolve({})
+ return {}
}
},
setPalette(value) {
this.resetThemeV3Palette()
this.resetThemeV2()
useSyncConfigStore().setPreference({ path: 'simple.palette', value })
useSyncConfigStore().pushSyncConfig()
this.applyTheme({ recompile: true })
},
setPaletteCustom(value) {
this.resetThemeV3Palette()
this.resetThemeV2()
useSyncConfigStore().setPreference({
path: 'simple.paletteCustomData',
value,
})
useSyncConfigStore().pushSyncConfig()
this.applyTheme({ recompile: true })
},
async fetchStylesIndex() {
try {
const value = await getResourcesIndex(
'/static/styles/index.json',
deserialize,
)
useInstanceStore().set({ path: 'stylesIndex', value })
return value
} catch (e) {
console.error('Could not fetch styles index', e)
useInstanceStore().set({
path: 'simple.stylesIndex',
value: { _error: e },
})
- return Promise.resolve({})
+ return {}
}
},
setStyle(value) {
this.resetThemeV3()
this.resetThemeV2()
this.resetThemeV3Palette()
useSyncConfigStore().setPreference({ path: 'simple.style', value })
useSyncConfigStore().pushSyncConfig()
this.useStylePalette = true
this.applyTheme({ recompile: true }).then(() => {
this.useStylePalette = false
})
},
setStyleCustom(value) {
this.resetThemeV3()
this.resetThemeV2()
this.resetThemeV3Palette()
useSyncConfigStore().setPreference({
path: 'simple.styleCustomData',
value,
})
useSyncConfigStore().pushSyncConfig()
this.useStylePalette = true
this.applyTheme({ recompile: true }).then(() => {
this.useStylePalette = false
})
},
async fetchThemesIndex() {
try {
const value = await getResourcesIndex('/static/styles.json')
useInstanceStore().set({ path: 'themesIndex', value })
return value
} catch (e) {
console.error('Could not fetch themes index', e)
useInstanceStore().set({
path: 'themesIndex',
value: { _error: e },
})
- return Promise.resolve({})
+ return {}
}
},
setTheme(value) {
this.resetThemeV3()
this.resetThemeV3Palette()
this.resetThemeV2()
useSyncConfigStore().setPreference({ path: 'simple.theme', value })
useSyncConfigStore().pushSyncConfig()
this.applyTheme({ recompile: true })
},
setThemeCustom(value) {
this.resetThemeV3()
this.resetThemeV3Palette()
this.resetThemeV2()
useSyncConfigStore().setPreference({ path: 'simple.customTheme', value })
useSyncConfigStore().setPreference({
path: 'simple.customThemeSource',
value,
})
useSyncConfigStore().pushSyncConfig()
this.applyTheme({ recompile: true })
},
resetThemeV3() {
useSyncConfigStore().setPreference({ path: 'simple.style', value: null })
useSyncConfigStore().setPreference({
path: 'simple.styleCustomData',
value: null,
})
},
resetThemeV3Palette() {
useSyncConfigStore().setPreference({
path: 'simple.palette',
value: null,
})
useSyncConfigStore().setPreference({
path: 'simple.paletteCustomData',
value: null,
})
},
resetThemeV2() {
useSyncConfigStore().setPreference({ path: 'simple.theme', value: null })
useSyncConfigStore().setPreference({
path: 'simple.customTheme',
value: null,
})
useSyncConfigStore().setPreference({
path: 'simple.customThemeSource',
value: null,
})
},
async getThemeData() {
const getData = async (resource, index, customData, name) => {
const capitalizedResource =
resource[0].toUpperCase() + resource.slice(1)
const result = {}
if (customData) {
result.nameUsed = 'custom' // custom data overrides name
result.dataUsed = customData
} else {
result.nameUsed = name
if (result.nameUsed == null) {
result.dataUsed = null
return result
}
let fetchFunc = index[result.nameUsed]
// Fallbacks
if (!fetchFunc) {
if (resource === 'style' || resource === 'palette') {
return result
}
const newName = Object.keys(index)[0]
fetchFunc = index[newName]
console.warn(
`${capitalizedResource} with id '${this.styleNameUsed}' not found, trying back to '${newName}'`,
)
if (!fetchFunc) {
console.warn(
`${capitalizedResource} doesn't have a fallback, defaulting to stock.`,
)
fetchFunc = () => Promise.resolve(null)
}
}
result.dataUsed = await fetchFunc()
}
return result
}
let {
theme: instanceThemeName,
style: instanceStyleName,
palette: instancePaletteName,
} = useInstanceStore().instanceIdentity
let { themesIndex, stylesIndex, palettesIndex } = useInstanceStore()
const {
style: userStyleName,
styleCustomData: userStyleCustomData,
palette: userPaletteName,
paletteCustomData: userPaletteCustomData,
} = useMergedConfigStore().mergedConfig
let {
theme: userThemeV2Name,
customTheme: userThemeV2Snapshot,
customThemeSource: userThemeV2Source,
} = useMergedConfigStore().mergedConfig
let majorVersionUsed
console.debug(
`User V3 palette: ${userPaletteName}, style: ${userStyleName} , custom: ${!!userStyleCustomData}`,
)
console.debug(
`User V2 name: ${userThemeV2Name}, source: ${!!userThemeV2Source}, snapshot: ${!!userThemeV2Snapshot}`,
)
console.debug(
`Instance V3 palette: ${instancePaletteName}, style: ${instanceStyleName}`,
)
console.debug('Instance V2 theme: ' + instanceThemeName)
if (
userPaletteName ||
userPaletteCustomData ||
userStyleName ||
userStyleCustomData ||
// User V2 overrides instance V3
((instancePaletteName || instanceStyleName) &&
instanceThemeName == null &&
userThemeV2Name == null)
) {
// Palette and/or style overrides V2 themes
instanceThemeName = null
userThemeV2Name = null
userThemeV2Source = null
userThemeV2Snapshot = null
majorVersionUsed = 'v3'
} else if (
userThemeV2Name ||
userThemeV2Snapshot ||
userThemeV2Source ||
instanceThemeName
) {
majorVersionUsed = 'v2'
} else {
// if all fails fallback to v3
majorVersionUsed = 'v3'
}
if (majorVersionUsed === 'v3') {
const result = await Promise.all([
this.fetchPalettesIndex(),
this.fetchStylesIndex(),
])
palettesIndex = result[0]
stylesIndex = result[1]
} else {
// Promise.all just to be uniform with v3
const result = await Promise.all([this.fetchThemesIndex()])
themesIndex = result[0]
}
this.themeVersion = majorVersionUsed
console.debug('Version used', majorVersionUsed)
if (majorVersionUsed === 'v3') {
this.themeDataUsed = null
this.themeNameUsed = null
const style = await getData(
'style',
stylesIndex,
userStyleCustomData,
userStyleName || instanceStyleName,
)
this.styleNameUsed = style.nameUsed
this.styleDataUsed = style.dataUsed
let firstStylePaletteName = null
style.dataUsed
?.filter((x) => x.component === '@palette')
.map((x) => {
const cleanDirectives = Object.fromEntries(
Object.entries(x.directives).filter(([k]) => k),
)
return { name: x.variant, ...cleanDirectives }
})
.forEach((palette) => {
const key =
'style.' + palette.name.toLowerCase().replaceAll(' ', '_')
if (!firstStylePaletteName) firstStylePaletteName = key
palettesIndex[key] = () => Promise.resolve(palette)
})
const palette = await getData(
'palette',
palettesIndex,
userPaletteCustomData,
this.useStylePalette
? firstStylePaletteName
: userPaletteName || instancePaletteName,
)
if (this.useStylePalette) {
useSyncConfigStore().setPreference({
path: 'simple.palette',
value: firstStylePaletteName,
})
useSyncConfigStore().pushSyncConfig()
}
this.paletteNameUsed = palette.nameUsed
this.paletteDataUsed = palette.dataUsed
if (this.paletteDataUsed) {
this.paletteDataUsed.link =
this.paletteDataUsed.link || this.paletteDataUsed.accent
this.paletteDataUsed.accent =
this.paletteDataUsed.accent || this.paletteDataUsed.link
}
if (Array.isArray(this.paletteDataUsed)) {
const [
name,
bg,
fg,
text,
link,
cRed = '#FF0000',
cGreen = '#00FF00',
cBlue = '#0000FF',
cOrange = '#E3FF00',
] = palette.dataUsed
this.paletteDataUsed = {
name,
bg,
fg,
text,
link,
accent: link,
cRed,
cBlue,
cGreen,
cOrange,
}
}
console.debug('Palette data used', palette.dataUsed)
} else {
this.styleNameUsed = null
this.styleDataUsed = null
this.paletteNameUsed = null
this.paletteDataUsed = null
const theme = await getData(
'theme',
themesIndex,
userThemeV2Source || userThemeV2Snapshot,
userThemeV2Name || instanceThemeName,
)
this.themeNameUsed = theme.nameUsed
this.themeDataUsed = theme.dataUsed
}
},
async setThemeApplied() {
this.themeApplied = true
},
async applyTheme({ recompile = false } = {}) {
const { mergedConfig } = useMergedConfigStore()
const { forceThemeRecompilation, themeDebug } = mergedConfig
this.themeChangeInProgress = true
// If we're not forced to recompile try using
// cache (tryLoadCache return true if load successful)
const forceRecompile = forceThemeRecompilation || recompile
await this.getThemeData()
if (!forceRecompile && !themeDebug && (await tryLoadCache())) {
this.themeChangeInProgress = false
return this.setThemeApplied()
}
window.splashUpdate('splash.theme')
try {
const paletteIss = (() => {
if (!this.paletteDataUsed) return null
const result = {
component: 'Root',
directives: {},
}
Object.entries(this.paletteDataUsed)
.filter(([k]) => k !== 'name')
.forEach(([k, v]) => {
let issRootDirectiveName
switch (k) {
case 'background':
issRootDirectiveName = 'bg'
break
case 'foreground':
issRootDirectiveName = 'fg'
break
default:
issRootDirectiveName = k
}
result.directives['--' + issRootDirectiveName] = 'color | ' + v
})
return result
})()
const theme2ruleset =
this.themeDataUsed &&
convertTheme2To3(normalizeThemeData(this.themeDataUsed))
const hacks = []
const fontMap = {
Interface: 'Root',
Input: 'Input',
Posts: 'Post',
Monospace: 'Root',
}
Object.entries(fontMap).forEach(([font, component]) => {
const family = mergedConfig[`font${font}`]
const variable = font === 'Monospace' ? '--monoFont' : '--font'
if (typeof family === 'string') {
const familyString = GENERIC_FONT_NAMES.has(family)
? family
: `"${family}"`
hacks.push({
component,
directives: {
[variable]: `generic | ${familyString}`,
},
})
}
})
if (mergedConfig.underlay !== 'none') {
const newRule = {
component: 'Underlay',
directives: {},
}
if (mergedConfig.underlay === 'opaque') {
newRule.directives.opacity = 1
newRule.directives.background = '--wallpaper'
}
if (mergedConfig.underlay === 'transparent') {
newRule.directives.opacity = 0
}
hacks.push(newRule)
}
const rulesetArray = [
theme2ruleset,
this.styleDataUsed,
paletteIss,
hacks,
].filter(Boolean)
return applyTheme(
rulesetArray.flat(),
() => this.setThemeApplied(),
() => {
this.themeChangeInProgress = false
},
themeDebug,
)
} catch (e) {
console.error(e)
window.splashError(e)
}
},
},
})
export const normalizeThemeData = (input) => {
let themeData, themeSource
if (input.themeFileVerison === 1) {
// this might not be even used at all, some leftover of unimplemented code in V2 editor
return generatePreset(input).theme
} else if (
Object.hasOwn(input, '_pleroma_theme_version') ||
Object.hasOwn(input, 'source') ||
Object.hasOwn(input, 'theme')
) {
// We got passed a full theme file
themeData = input.theme
themeSource = input.source
} else if (
Object.hasOwn(input, 'themeEngineVersion') ||
Object.hasOwn(input, 'colors')
) {
// We got passed a source/snapshot
themeData = input
themeSource = input
}
// New theme presets don't have 'theme' property, they use 'source'
let out // shout, shout let it all out
if (themeSource?.themeEngineVersion === CURRENT_VERSION) {
// There are some themes in wild that have completely broken source
out = { ...(themeData || {}), ...themeSource }
} else {
out = themeData
}
// generatePreset here basically creates/updates "snapshot",
// while also fixing the 2.2 -> 2.3 colors/shadows/etc
return generatePreset(out).theme
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Aug 9, 1:45 AM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1724109
Default Alt Text
(69 KB)

Event Timeline