Merge remote-tracking branch 'origin/develop' into from/develop/tusooa/sw-cache-assets

This commit is contained in:
Henry Jameson 2025-06-25 15:40:56 +03:00
commit a1f43234cd
235 changed files with 6354 additions and 4065 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'
@ -707,7 +708,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 = []
@ -1366,7 +1368,7 @@ const search2 = ({ credentials, q, resolve, limit, offset, following, type }) =>
}
if (type) {
params.push(['following', type])
params.push(['type', type])
}
params.push(['with_relationships', true])

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

@ -1,3 +1,19 @@
const checkCanvasExtractPermission = () => {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d');
if (!ctx) return false;
ctx.fillStyle = '#0f161e';
ctx.fillRect(0, 0, 1, 1);
const { data } = ctx.getImageData(0, 0, 1, 1);
return data.join(',') === '15,22,30,255';
};
const createFaviconService = () => {
const favicons = []
const faviconWidth = 128
@ -5,6 +21,8 @@ const createFaviconService = () => {
const badgeRadius = 32
const initFaviconService = () => {
if (!checkCanvasExtractPermission()) return;
const nodes = document.querySelectorAll('link[rel="icon"]')
nodes.forEach(favicon => {
if (favicon) {

View file

@ -1,12 +1,20 @@
import { reduce } from 'lodash'
import { StatusCodeError } from 'src/services/errors/errors.js'
const REDIRECT_URI = `${window.location.origin}/oauth-callback`
export const getOrCreateApp = ({ clientId, clientSecret, instance, commit }) => {
if (clientId && clientSecret) {
return Promise.resolve({ clientId, clientSecret })
export const getJsonOrError = async (response) => {
if (response.ok) {
return response.json()
.catch((error) => {
throw new StatusCodeError(response.status, error, {}, response)
})
} else {
throw new StatusCodeError(response.status, await response.text(), {}, response)
}
}
export const createApp = (instance) => {
const url = `${instance}/api/v1/apps`
const form = new window.FormData()
@ -19,9 +27,16 @@ export const getOrCreateApp = ({ clientId, clientSecret, instance, commit }) =>
method: 'POST',
body: form
})
.then((data) => data.json())
.then(getJsonOrError)
.then((app) => ({ clientId: app.client_id, clientSecret: app.client_secret }))
.then((app) => commit('setClientData', app) || app)
}
export const verifyAppToken = ({ instance, appToken }) => {
return window.fetch(`${instance}/api/v1/apps/verify_credentials`, {
method: 'GET',
headers: { Authorization: `Bearer ${appToken}` }
})
.then(getJsonOrError)
}
const login = ({ instance, clientId }) => {
@ -92,7 +107,7 @@ export const getClientToken = ({ clientId, clientSecret, instance }) => {
return window.fetch(url, {
method: 'POST',
body: form
}).then((data) => data.json())
}).then(getJsonOrError)
}
const verifyOTPCode = ({ app, instance, mfaToken, code }) => {
const url = `${instance}/oauth/mfa/challenge`
@ -144,7 +159,6 @@ const oauth = {
login,
getToken,
getTokenWithCredentials,
getOrCreateApp,
verifyOTPCode,
verifyRecoveryCode,
revokeToken

View file

@ -1,4 +1,4 @@
import { muteWordHits } from '../status_parser/status_parser.js'
import { muteFilterHits } from '../status_parser/status_parser.js'
import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js'
import { useI18nStore } from 'src/stores/i18n.js'
import { useAnnouncementsStore } from 'src/stores/announcements'
@ -58,10 +58,10 @@ const sortById = (a, b) => {
}
}
const isMutedNotification = (store, notification) => {
if (!notification.status) return
const rootGetters = store.rootGetters || store.getters
return notification.status.muted || muteWordHits(notification.status, rootGetters.mergedConfig.muteWords).length > 0
const isMutedNotification = (notification) => {
if (!notification.status) return false
if (notification.status.muted) return true
return muteFilterHits(notification.status).length > 0
}
export const maybeShowNotification = (store, notification) => {
@ -69,7 +69,7 @@ export const maybeShowNotification = (store, notification) => {
if (notification.seen) return
if (!visibleTypes(store).includes(notification.type)) return
if (notification.type === 'mention' && isMutedNotification(store, notification)) return
if (notification.type === 'mention' && isMutedNotification(notification)) return
const notificationObject = prepareNotificationObject(notification, useI18nStore().i18n)
showDesktopNotification(rootState, notificationObject)

View file

@ -1,11 +1,60 @@
import { filter } from 'lodash'
export const muteWordHits = (status, muteWords) => {
export const muteFilterHits = (muteFilters, status) => {
const statusText = status.text.toLowerCase()
const statusSummary = status.summary.toLowerCase()
const hits = filter(muteWords, (muteWord) => {
return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase())
})
const replyToUser = status.in_reply_to_screen_name?.toLowerCase()
const poster = status.user.screen_name.toLowerCase()
const mentions = (status.attentions || []).map(att => att.screen_name.toLowerCase())
return hits
return muteFilters.toSorted((a,b) => b.order - a.order).map(filter => {
const { hide, expires, name, value, type, enabled} = filter
if (!enabled) return false
if (value === '') return false
if (expires !== null && expires < Date.now()) return false
switch (type) {
case 'word': {
const lowercaseValue = value.toLowerCase()
if (statusText.toLowerCase().includes(lowercaseValue) || statusSummary.toLowerCase().includes(lowercaseValue)) {
return { hide, name }
}
break
}
case 'regexp': {
try {
const re = new RegExp(value, 'i')
if (re.test(statusText) || re.test(statusSummary)) {
return { hide, name }
}
return false
} catch {
return false
}
}
case 'user': {
if (
poster.includes(value) ||
replyToUser.includes(value) ||
mentions.some(mention => mention.includes(value))
) {
return { hide, name }
}
break
}
case 'user_regexp': {
try {
const re = new RegExp(value, 'i')
if (
re.test(poster) ||
re.test(replyToUser) ||
mentions.some(mention => re.test(mention))
) {
return { hide, name }
}
return false
} catch {
return false
}
}
}
}).filter(_ => _)
}

View file

@ -124,16 +124,33 @@ export const applyTheme = (
const lazyStyles = createStyleSheet(LAZY_STYLE_ID)
const insertRule = (styles, rule) => {
if (rule.indexOf('webkit') >= 0) {
try {
// Try to use modern syntax first
try {
styles.sheet.insertRule(rule, 'index-max')
styles.rules.push(rule)
} catch (e) {
console.warn('Can\'t insert rule due to lack of support', e)
} 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)
}
} else {
styles.sheet.insertRule(rule, 'index-max')
styles.rules.push(rule)
}
}
@ -170,6 +187,7 @@ const extractStyleConfig = ({
sidebarColumnWidth,
contentColumnWidth,
notifsColumnWidth,
themeEditorMinWidth,
emojiReactionsScale,
emojiSize,
navbarSize,
@ -181,6 +199,7 @@ const extractStyleConfig = ({
sidebarColumnWidth,
contentColumnWidth,
notifsColumnWidth,
themeEditorMinWidth: parseInt(themeEditorMinWidth) === 0 ? 'fit-content' : themeEditorMinWidth,
emojiReactionsScale,
emojiSize,
navbarSize,

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',
@ -112,7 +129,7 @@ export const colorFunctions = {
const color = convert(findColor(colorArg, { dynamicVars, staticVars })).rgb
const amount = Number(amountArg)
const effectiveBackground = dynamicVars.lowerLevelBackground
const effectiveBackground = dynamicVars.lowerLevelBackground ?? color
const isLightOnDark = relativeLuminance(convert(effectiveBackground).rgb) < 0.5
const mod = isLightOnDark ? 1 : -1
return brightness(amount * mod, color).rgb

View file

@ -78,23 +78,24 @@ export const findColor = (color, { dynamicVars, staticVars }) => {
targetColor = { r, g, b }
} else if (variableSlot.startsWith('parent')) {
if (variableSlot === 'parent') {
const { r, g, b } = dynamicVars.lowerLevelBackground
const { r, g, b } = dynamicVars.lowerLevelBackground ?? {}
targetColor = { r, g, b }
} else {
const virtualSlot = variableSlot.replace(/^parent/, '')
targetColor = convert(dynamicVars.lowerLevelVirtualDirectivesRaw[virtualSlot]).rgb
}
} else {
switch (variableSlot) {
case 'inheritedBackground':
targetColor = convert(dynamicVars.inheritedBackground).rgb
break
case 'background':
targetColor = convert(dynamicVars.background).rgb
break
default:
targetColor = convert(staticVars[variableSlot]).rgb
const staticVar = staticVars[variableSlot]
const dynamicVar = dynamicVars[variableSlot]
if (!staticVar && !dynamicVar) {
console.warn(dynamicVars, variableSlot, dynamicVars[variableSlot])
console.warn(`Couldn't find variable "${variableSlot}", falling back to magenta. Variables are:
Static:
${JSON.stringify(staticVars, null, 2)}
Dynamic:
${JSON.stringify(dynamicVars, null, 2)}`)
}
targetColor = convert(staticVar ?? dynamicVar ?? '#FF00FF').rgb
}
if (modifier) {
@ -109,7 +110,7 @@ export const findColor = (color, { dynamicVars, staticVars }) => {
try {
targetColor = process(color, colorFunctions, { findColor }, { dynamicVars, staticVars })
} catch (e) {
console.error('Failure executing color function', e)
console.error('Failure executing color function', e ,'\n Function: ' + color)
targetColor = '#FF00FF'
}
}
@ -120,7 +121,7 @@ export const findColor = (color, { dynamicVars, staticVars }) => {
Static:
${JSON.stringify(staticVars, null, 2)}
Dynamic:
${JSON.stringify(dynamicVars, null, 2)}`, e)
${JSON.stringify(dynamicVars, null, 2)}\nError: ${e}`)
}
}
@ -516,7 +517,7 @@ export const init = ({
.filter(c => virtualComponents.has(c) && !nonEditableComponents.has(c))
} else if (liteMode) {
validInnerComponents = (component.validInnerComponentsLite || component.validInnerComponents || [])
} else if (component.name === 'Root') {
} else if (component.name === 'Root' || component.states != null || component.background?.includes('--parent')) {
validInnerComponents = component.validInnerComponents || []
} else {
validInnerComponents = component

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}`)
}