Merge remote-tracking branch 'origin/develop' into timed-user-mutes

This commit is contained in:
Henry Jameson 2025-07-09 15:57:23 +03:00
commit 385f921c41
72 changed files with 1336 additions and 851 deletions

View file

@ -15,7 +15,7 @@ const TAG_USER_URL = '/api/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate'
const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
const ADMIN_USERS_URL = '/api/pleroma/admin/users'
const ADMIN_USERS_URL = '/api/v1/pleroma/admin/users'
const SUGGESTIONS_URL = '/api/v1/suggestions'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
@ -61,6 +61,7 @@ const MASTODON_LIST_TIMELINE_URL = id => `/api/v1/timelines/list/${id}`
const MASTODON_LIST_ACCOUNTS_URL = id => `/api/v1/lists/${id}/accounts`
const MASTODON_TAG_TIMELINE_URL = tag => `/api/v1/timelines/tag/${tag}`
const MASTODON_BOOKMARK_TIMELINE_URL = '/api/v1/bookmarks'
const AKKOMA_BUBBLE_TIMELINE_URL = '/api/v1/timelines/bubble'
const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/'
const MASTODON_USER_MUTES_URL = '/api/v1/mutes/'
const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
@ -99,7 +100,7 @@ const PLEROMA_CHAT_URL = id => `/api/v1/pleroma/chats/by-account-id/${id}`
const PLEROMA_CHAT_MESSAGES_URL = id => `/api/v1/pleroma/chats/${id}/messages`
const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read`
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
const PLEROMA_ADMIN_REPORTS = '/api/pleroma/admin/reports'
const PLEROMA_ADMIN_REPORTS = '/api/v1/pleroma/admin/reports'
const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements'
const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
@ -111,10 +112,10 @@ const PLEROMA_USER_FAVORITES_TIMELINE_URL = id => `/api/v1/pleroma/accounts/${id
const PLEROMA_BOOKMARK_FOLDERS_URL = '/api/v1/pleroma/bookmark_folders'
const PLEROMA_BOOKMARK_FOLDER_URL = id => `/api/v1/pleroma/bookmark_folders/${id}`
const PLEROMA_ADMIN_CONFIG_URL = '/api/pleroma/admin/config'
const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/pleroma/admin/config/descriptions'
const PLEROMA_ADMIN_FRONTENDS_URL = '/api/pleroma/admin/frontends'
const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = '/api/pleroma/admin/frontends/install'
const PLEROMA_ADMIN_CONFIG_URL = '/api/v1/pleroma/admin/config'
const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/v1/pleroma/admin/config/descriptions'
const PLEROMA_ADMIN_FRONTENDS_URL = '/api/v1/pleroma/admin/frontends'
const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = '/api/v1/pleroma/admin/frontends/install'
const PLEROMA_EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji'
const PLEROMA_EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import'
@ -714,7 +715,8 @@ const fetchTimeline = ({
publicFavorites: PLEROMA_USER_FAVORITES_TIMELINE_URL,
tag: MASTODON_TAG_TIMELINE_URL,
bookmarks: MASTODON_BOOKMARK_TIMELINE_URL,
quotes: PLEROMA_STATUS_QUOTES_URL
quotes: PLEROMA_STATUS_QUOTES_URL,
bubble: AKKOMA_BUBBLE_TIMELINE_URL
}
const isNotifications = timeline === 'notifications'
const params = []
@ -764,7 +766,7 @@ const fetchTimeline = ({
if (replyVisibility !== 'all') {
params.push(['reply_visibility', replyVisibility])
}
if (includeTypes.length > 0) {
if (includeTypes.size > 0) {
includeTypes.forEach(type => {
params.push(['include_types[]', type])
})

View file

@ -95,6 +95,32 @@ export const getContrastRatioLayers = (text, layers, bedrock) => {
return getContrastRatio(alphaBlendLayers(bedrock, layers), text)
}
/**
* Blending of two solid colors with a user-defined operator: origin +- value
*
* @param {Object} origin - base color
* @param {Object} value - modification argument
* @param {string} operator - math operator to use
*/
export const arithmeticBlend = (origin, value, operator) => {
const func = (a, b) => {
switch (operator) {
case '+':
return Math.min(a + b, 255)
case '-':
return Math.max(a - b, 0)
default:
return a
}
}
return {
r: func(origin.r, value.r),
g: func(origin.g, value.g),
b: func(origin.b, value.b),
}
}
/**
* This performs alpha blending between solid background and semi-transparent foreground
*

View file

@ -91,6 +91,8 @@ export const parseUser = (data) => {
output.bot = data.bot
output.privileges = []
if (data.pleroma) {
if (data.pleroma.settings_store) {
output.storage = data.pleroma.settings_store['pleroma-fe']
@ -317,20 +319,18 @@ export const parseStatus = (data) => {
output.edited_at = data.edited_at
const { pleroma } = data
if (data.pleroma) {
const { pleroma } = data
output.text = pleroma.content ? data.pleroma.content['text/plain'] : data.content
output.summary = pleroma.spoiler_text ? data.pleroma.spoiler_text['text/plain'] : data.spoiler_text
output.statusnet_conversation_id = data.pleroma.conversation_id
output.is_local = pleroma.local
output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
output.in_reply_to_screen_name = pleroma.in_reply_to_account_acct
output.thread_muted = pleroma.thread_muted
output.emoji_reactions = pleroma.emoji_reactions
output.parent_visible = pleroma.parent_visible === undefined ? true : pleroma.parent_visible
output.quote = pleroma.quote ? parseStatus(pleroma.quote) : undefined
output.quote_id = pleroma.quote_id ? pleroma.quote_id : (output.quote ? output.quote.id : undefined)
output.quote_url = pleroma.quote_url
output.quote_visible = pleroma.quote_visible
output.quote_visible = pleroma.quote_visible || true
output.quotes_count = pleroma.quotes_count
output.bookmark_folder_id = pleroma.bookmark_folder
} else {
@ -338,6 +338,12 @@ export const parseStatus = (data) => {
output.summary = data.spoiler_text
}
const quoteRaw = pleroma?.quote || data.quote
const quoteData = quoteRaw ? parseStatus(quoteRaw) : undefined
output.quote = quoteData
output.quote_id = data.quote?.id ?? data.quote_id ?? quoteData?.id ?? pleroma.quote_id
output.quote_url = data.quote?.url ?? quoteData?.url ?? pleroma.quote_url
output.in_reply_to_status_id = data.in_reply_to_id
output.in_reply_to_user_id = data.in_reply_to_account_id
output.replies_count = data.replies_count

View file

@ -2,9 +2,13 @@ import { useInterfaceStore } from 'src/stores/interface.js'
import apiService from '../api/api.service.js'
import { promiseInterval } from '../promise_interval/promise_interval.js'
const update = ({ store, notifications, older }) => {
store.dispatch('addNewNotifications', { notifications, older })
}
//
// For using include_types when fetching notifications.
// Note: chat_mention excluded as pleroma-fe polls them separately
const mastoApiNotificationTypes = [
const mastoApiNotificationTypes = new Set([
'mention',
'status',
'favourite',
@ -14,21 +18,22 @@ const mastoApiNotificationTypes = [
'move',
'poll',
'pleroma:emoji_reaction',
'pleroma:chat_mention',
'pleroma:report'
]
const update = ({ store, notifications, older }) => {
store.dispatch('addNewNotifications', { notifications, older })
}
'pleroma:report',
'test'
])
const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
const args = { credentials }
const { getters } = store
const rootState = store.rootState || store.state
const timelineData = rootState.notifications
const hideMutedPosts = getters.mergedConfig.hideMutedPosts
if (rootState.instance.pleromaChatMessagesAvailable) {
mastoApiNotificationTypes.add('pleroma:chat_mention')
}
args.includeTypes = mastoApiNotificationTypes
args.withMuted = !hideMutedPosts
@ -72,7 +77,17 @@ const fetchNotifications = ({ store, args, older }) => {
return apiService.fetchTimeline(args)
.then((response) => {
if (response.errors) {
throw new Error(`${response.status} ${response.statusText}`)
if (response.status === 400 && response.statusText.includes('Invalid value for enum')) {
response
.statusText
.matchAll(/(\w+) - Invalid value for enum./g)
.toArray()
.map(x => x[1])
.forEach(x => mastoApiNotificationTypes.delete(x))
return fetchNotifications({ store, args, older })
} else {
throw new Error(`${response.status} ${response.statusText}`)
}
}
const notifications = response.data
update({ store, notifications, older })

View file

@ -1,47 +1,76 @@
import { init, getEngineChecksum } from '../theme_data/theme_data_3.service.js'
import { getCssRules } from '../theme_data/css_utils.js'
import { defaultState } from 'src/modules/default_config_state.js'
import { chunk } from 'lodash'
import { chunk, throttle } from 'lodash'
import localforage from 'localforage'
// On platforms where this is not supported, it will return undefined
// Otherwise it will return an array
const supportsAdoptedStyleSheets = !!document.adoptedStyleSheets
const createStyleSheet = (id) => {
if (supportsAdoptedStyleSheets) {
return {
el: null,
sheet: new CSSStyleSheet(),
rules: []
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.replace(/backdrop-filter:[^;]+;/g, '') // Remove backdrop-filter
}
this.rules.push(
newRule
.replace(/var\(--shadowFilter\)[^;]*;/g, '') // Remove shadowFilter references
)
}
}
const el = document.getElementById(id)
// Clear all rules in it
for (let i = el.sheet.cssRules.length - 1; i >= 0; --i) {
el.sheet.deleteRule(i)
}
return {
el,
sheet: el.sheet,
rules: []
}
stylesheets[id] = newStyleSheet
return newStyleSheet
}
const EAGER_STYLE_ID = 'pleroma-eager-styles'
const LAZY_STYLE_ID = 'pleroma-lazy-styles'
const adoptStyleSheets = (styles) => {
export const adoptStyleSheets = throttle(() => {
if (supportsAdoptedStyleSheets) {
document.adoptedStyleSheets = styles.map(s => s.sheet)
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 => css.insertRule(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'
export const generateTheme = (inputRuleset, callbacks, debug) => {
const {
@ -94,13 +123,17 @@ export const tryLoadCache = async () => {
if (!cache) return null
try {
if (cache.engineChecksum === getEngineChecksum()) {
const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
const eagerStyles = createStyleSheet(EAGER_STYLE_ID, 10)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID, 20)
cache.data[0].forEach(rule => eagerStyles.sheet.insertRule(rule, 'index-max'))
cache.data[1].forEach(rule => lazyStyles.sheet.insertRule(rule, 'index-max'))
cache.data[0].forEach(rule => eagerStyles.addRule(rule))
cache.data[1].forEach(rule => lazyStyles.addRule(rule))
adoptStyleSheets([eagerStyles, lazyStyles])
eagerStyles.ready = true
lazyStyles.ready = true
// Don't do this, we need to wait until config adopts its styles first
//adoptStyleSheets()
console.info(`Loaded theme from cache`)
return true
@ -120,57 +153,29 @@ export const applyTheme = (
onFinish = () => {},
debug
) => {
const eagerStyles = createStyleSheet(EAGER_STYLE_ID)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
const eagerStyles = createStyleSheet(EAGER_STYLE_ID, 10)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID, 20)
const insertRule = (styles, rule) => {
try {
// Try to use modern syntax first
try {
styles.sheet.insertRule(rule, 'index-max')
styles.rules.push(rule)
} catch {
// Fallback for older browsers that don't support 'index-max'
styles.sheet.insertRule(rule, styles.sheet.cssRules.length)
styles.rules.push(rule)
}
} catch (e) {
console.warn('Can\'t insert rule due to lack of support', e, rule)
// Try to sanitize the rule for better compatibility
try {
// Remove any potentially problematic CSS features
let sanitizedRule = rule
.replace(/backdrop-filter:[^;]+;/g, '') // Remove backdrop-filter
.replace(/var\(--shadowFilter\)[^;]*;/g, '') // Remove shadowFilter references
if (sanitizedRule !== rule) {
styles.sheet.insertRule(sanitizedRule, styles.sheet.cssRules.length)
styles.rules.push(sanitizedRule)
}
} catch (e2) {
console.error('Failed to insert even sanitized rule', e2)
}
}
}
const { lazyProcessFunc } = generateTheme(
input,
{
onNewRule (rule, isLazy) {
if (isLazy) {
insertRule(lazyStyles, rule)
lazyStyles.addRule(rule)
} else {
insertRule(eagerStyles, rule)
eagerStyles.addRule(rule)
}
},
onEagerFinished () {
adoptStyleSheets([eagerStyles])
eagerStyles.ready = true
adoptStyleSheets()
onEagerFinish()
console.info('Eager part of theme finished, waiting for lazy part to finish to store cache')
},
onLazyFinished () {
adoptStyleSheets([eagerStyles, lazyStyles])
lazyStyles.ready = true
adoptStyleSheets()
const cache = { engineChecksum: getEngineChecksum(), data: [eagerStyles.rules, lazyStyles.rules] }
onFinish(cache)
localforage.setItem('pleromafe-theme-cache', cache)
@ -234,28 +239,23 @@ export const applyConfig = (input) => {
return
}
const head = document.head
const rules = Object
.entries(config)
.filter(([, v]) => v)
.map(([k, v]) => `--${k}: ${v}`).join(';')
document.getElementById('style-config')?.remove()
const styleEl = document.createElement('style')
styleEl.id = 'style-config'
head.appendChild(styleEl)
const styleSheet = styleEl.sheet
const styleSheet = createStyleSheet('theme-holder', 30)
styleSheet.toString()
styleSheet.insertRule(`:root { ${rules} }`, 'index-max')
styleSheet.addRule(`:root { ${rules} }`)
// TODO find a way to make this not apply to theme previews
if (Object.prototype.hasOwnProperty.call(config, 'forcedRoundness')) {
styleSheet.insertRule(` *:not(.preview-block) {
styleSheet.addRule(` *:not(.preview-block) {
--roundness: var(--forcedRoundness) !important;
}`, 'index-max')
}`)
}
styleSheet.ready = true
adoptStyleSheets()
}
export const getResourcesIndex = async (url, parser = JSON.parse) => {

View file

@ -18,6 +18,7 @@ function isPushSupported () {
}
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))
@ -146,3 +147,11 @@ export function unregisterPushNotifications (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 }

View file

@ -1,8 +1,8 @@
import { convert, brightness } from 'chromatism'
import { alphaBlend, getTextColor, relativeLuminance } from '../color_convert/color_convert.js'
import { alphaBlend, arithmeticBlend, getTextColor, relativeLuminance } from '../color_convert/color_convert.js'
export const process = (text, functions, { findColor, findShadow }, { dynamicVars, staticVars }) => {
const { funcName, argsString } = /\$(?<funcName>\w+)\((?<argsString>[#a-zA-Z0-9-,.'"\s]*)\)/.exec(text).groups
const { funcName, argsString } = /\$(?<funcName>\w+)\((?<argsString>[#a-zA-Z0-9-+,.'"\s]*)\)/.exec(text).groups
const args = argsString.split(/ /g).map(a => a.trim())
const func = functions[funcName]
@ -81,6 +81,23 @@ export const colorFunctions = {
return alphaBlend(background, amount, foreground)
}
},
shift: {
argsNeeded: 2,
documentation: 'Arithmetic blend between two colors',
args: [
'origin: base color',
'value: shift value',
'operator: math operator to use (+ or -)'
],
exec: (args, { findColor }, { dynamicVars, staticVars }) => {
const [originArg, valueArg, operatorArg] = args
const origin = convert(findColor(originArg, { dynamicVars, staticVars })).rgb
const value = convert(findColor(valueArg, { dynamicVars, staticVars })).rgb
return arithmeticBlend(origin, value, operatorArg)
}
},
boost: {
argsNeeded: 2,
documentation: 'If given color is dark makes it darker, if color is light - makes it lighter',

View file

@ -54,7 +54,7 @@ const fetchAndUpdate = ({
args.bookmarkFolderId = bookmarkFolderId
args.tag = tag
args.withMuted = !hideMutedPosts
if (loggedIn && ['friends', 'public', 'publicAndExternal'].includes(timeline)) {
if (loggedIn && ['friends', 'public', 'publicAndExternal', 'bubble'].includes(timeline)) {
args.replyVisibility = replyVisibility
}
@ -63,6 +63,10 @@ const fetchAndUpdate = ({
return apiService.fetchTimeline(args)
.then(response => {
if (response.errors) {
if (timeline === 'favorites') {
rootState.instance.pleromaPublicFavouritesAvailable = false
return
}
throw new Error(`${response.status} ${response.statusText}`)
}