Merge remote-tracking branch 'origin/develop' into users-statuses-pinia

This commit is contained in:
Henry Jameson 2026-08-12 15:57:42 +03:00
commit 657f405ee0
27 changed files with 167 additions and 20 deletions

View file

@ -0,0 +1 @@
fixed tapping "Mute..." and "Change visiblity" (admin action) in extra status actions closing the dropdown

View file

@ -0,0 +1 @@
domain mute fixedw

View file

@ -0,0 +1 @@
Fix followers list showing followed users as non-followed

View file

@ -0,0 +1 @@
fix moderation tools button possibly not appearing for admins

View file

@ -0,0 +1 @@
Fixed moderation actions requiring confirmation closing user popover

View file

@ -0,0 +1 @@
Fixed server-side domain mutes rendering/api calls

View file

@ -0,0 +1 @@
Fix theme lists failing to load when custom resource indexes are unavailable

View file

@ -0,0 +1 @@
Fix Theme 2 fonts falling back to serif after upgrade

View file

@ -0,0 +1 @@
Fix Themes 2.0 applying incorrect fonts

1
changelog.d/themes3.fix Normal file
View file

@ -0,0 +1 @@
Fixed themes 3 not loading in appearance tab

View file

@ -0,0 +1 @@
Fixed status index approxmiation in timeline rendering for dynamically changing viewport geometries

View file

@ -67,6 +67,7 @@ export const promisedRequest = async ({
url,
payload,
formData,
forceContentType,
cache,
credentials,
headers = {},
@ -75,7 +76,7 @@ export const promisedRequest = async ({
method,
credentials: 'same-origin',
headers: {
Accept: 'application/json',
Accept: forceContentType ?? 'application/json',
...headers,
},
}
@ -110,7 +111,7 @@ export const promisedRequest = async ({
)
if (contentLength === 0) return null
switch (contentType) {
switch (forceContentType ?? contentType) {
case 'text/plain':
return await response.text()
case 'application/json':

View file

@ -117,9 +117,17 @@ export const fetchUserByName = ({ name, credentials }) =>
export const fetchFriends = ({ id, maxId, sinceId, limit = 20, credentials }) =>
promisedRequest({
url: MASTODON_FOLLOWING_URL(id, { maxId, sinceId, limit }),
url: MASTODON_FOLLOWING_URL(id, {
maxId,
sinceId,
limit,
withRelationships: true,
}),
credentials,
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
}).then(({ data, ...rest }) => ({
...rest,
data: data.map(parseUser),
}))
export const fetchFollowers = ({
id,
@ -136,7 +144,10 @@ export const fetchFollowers = ({
withRelationships: true,
}),
credentials,
}).then(({ data, ...rest }) => ({ ...rest, data: data.map(parseUser) }))
}).then(({ data, ...rest }) => ({
...rest,
data: data.map(parseUser),
}))
export const fetchConversation = ({ id, credentials }) =>
promisedRequest({

View file

@ -410,7 +410,6 @@ export const exportFriends = ({ id, credentials }) => {
id,
maxId,
credentials,
withRelationships: true,
})
friends = [...friends, ...users]
if (users.length === 0) {

View file

@ -517,8 +517,7 @@ const ModerationTools = {
setOpen(value) {
this.open = value
},
maybeShowConfirm(close, { group, name, action, value }) {
close()
maybeShowConfirm({ group, name, action, value }) {
this.confirmDialogName = name
this.confirmDialogGroup = group
this.confirmDialogAction = () => action()

View file

@ -9,7 +9,7 @@
@show="setOpen(true)"
@close="setOpen(false)"
>
<template #content="{close}">
<template #content>
<div class="dropdown-menu">
<template v-for="(entry, index) in entries">
<div
@ -26,7 +26,7 @@
>
<button
class="main-button"
@click="() => maybeShowConfirm(close, entry)"
@click="() => maybeShowConfirm(entry)"
>
<span
v-if="entry.checkbox"

View file

@ -78,8 +78,8 @@
v-model="expiryUnit"
unstyled="true"
class="expiry-unit"
@change="expiryAmountChange"
:aria-label="$t('polls.expiry_unit')"
@change="expiryAmountChange"
>
<option
v-for="unit in expiryUnits"

View file

@ -580,8 +580,11 @@ const PostStatusForm = {
}),
},
watch: {
isDirty(newVal, oldVal) {
this.statusChanged()
newStatus: {
deep: true,
handler() {
this.statusChanged()
},
},
saveable(val) {
// https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event#usage_notes

View file

@ -153,7 +153,6 @@
</div>
</template>
<template #item="{item}">
{{ item }}
<DomainMuteCard :domain="item" />
</template>
<template #empty>

View file

@ -169,7 +169,7 @@ export default {
setTimeout(() => {
this.animationState = false
}, 500)
close()
if (!this.button.dropdown) close()
}
},
},

View file

@ -213,7 +213,10 @@ const Timeline = {
// Start from approximating the index of some visible status by using the
// the center of the screen on the timeline.
let approxIndex = Math.floor(statuses.length * (centerOfScreen / height))
let approxIndex = Math.min(
Math.floor(statuses.length * (centerOfScreen / height)),
statuses.length - 1,
)
let err = statuses[approxIndex].getBoundingClientRect().y
// if we have a previous scroll index that can be used, test if it's

View file

@ -214,6 +214,7 @@ const api = {
if (state.mastoUserSocketStatus !== WSConnectionStatus.ERROR) {
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
useChatsStore().startFetchingChats()
useInterfaceStore().pushGlobalNotice({
level: 'error',
messageKey: 'timeline.socket_broke',
@ -235,6 +236,7 @@ const api = {
stopMastoUserSocket({ state, dispatch }) {
dispatch('startFetchingTimeline', { timeline: 'friends' })
dispatch('startFetchingNotifications')
useChatsStore().startFetchingChats()
state.mastoUserSocket.close()
},

View file

@ -92,6 +92,11 @@ export const adoptStyleSheets = throttle(() => {
const EAGER_STYLE_ID = 'pleroma-eager-styles'
const LAZY_STYLE_ID = 'pleroma-lazy-styles'
export const hasInvalidCachedThemeRules = (data) =>
data
.flat()
.some((rule) => /--(?:mono)?font:\s*\[object Object\](?:;|$)/i.test(rule))
const generateTheme = (inputRuleset, callbacks, debug) => {
const {
onNewRule = () => {
@ -151,7 +156,8 @@ export const tryLoadCache = async () => {
if (
cache.engineChecksum === getEngineChecksum() &&
cache.checksum !== undefined &&
cache.checksum === useMergedConfigStore().mergedConfig.themeChecksum
cache.checksum === useMergedConfigStore().mergedConfig.themeChecksum &&
!hasInvalidCachedThemeRules(cache.data)
) {
const eagerStyles = createStyleSheet(EAGER_STYLE_ID, 10)
const lazyStyles = createStyleSheet(LAZY_STYLE_ID, 20)
@ -307,7 +313,9 @@ export const applyStyleConfig = (input) => {
adoptStyleSheets()
}
export const getResourcesIndex = async (url, parser = (x) => x) => {
const noop = (x) => x
export const getResourcesIndex = async (url, parser = noop) => {
const cache = 'no-store'
const customUrl = url.replace(/\.(\w+)$/, '.custom.$1')
let builtin
@ -324,6 +332,7 @@ export const getResourcesIndex = async (url, parser = (x) => x) => {
promisedRequest({
url: v,
cache,
forceContentType: parser === noop ? null : 'text/plain',
})
.then(({ data: text }) => parser(text))
.catch((e) => {
@ -339,7 +348,11 @@ export const getResourcesIndex = async (url, parser = (x) => x) => {
}
try {
const { data: builtinData } = await promisedRequest({ url, cache })
const { data: builtinData } = await promisedRequest({
url,
cache,
forceContentType: 'application/json',
})
builtin = resourceTransform(builtinData)
} catch {
builtin = []
@ -350,6 +363,7 @@ export const getResourcesIndex = async (url, parser = (x) => x) => {
const { data: customData } = await promisedRequest({
url: customUrl,
cache,
forceContentType: 'application/json',
})
custom = resourceTransform(customData)
} catch {

View file

@ -266,7 +266,7 @@ export const convertTheme2To3 = (data) => {
Object.keys(data.fonts || {}).forEach((key) => {
if (!fontsKeys.has(key)) return
if (!data.fonts[key]) return
const originalFont = data.fonts[key]
const originalFont = data.fonts[key].family
const rule = { source: '2to3' }
switch (key) {

View file

@ -75,6 +75,11 @@ export const useNotificationsStore = defineStore('notifications', {
return true
})
commit(
'addNewUsers',
validNotifications.map((notification) => notification.from_profile),
)
const statusNotifications = validNotifications.filter(
(notification) =>
isStatusNotification(notification.type) && notification.status,

View file

@ -0,0 +1,58 @@
import {
getResourcesIndex,
hasInvalidCachedThemeRules,
} from 'src/services/style_setter/style_setter.js'
describe('resource index', () => {
afterEach(() => {
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
it('ignores an HTML fallback returned for a missing custom index', async () => {
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
vi.stubGlobal(
'fetch',
vi
.fn()
.mockResolvedValueOnce(
new Response(JSON.stringify({ builtin: { version: 1 } }), {
headers: { 'Content-Type': 'application/json' },
}),
)
.mockResolvedValueOnce(
new Response('<!doctype html><title>Pleroma</title>', {
headers: { 'Content-Type': 'text/html' },
}),
),
)
const resources = await getResourcesIndex('/static/styles.json')
expect(Object.keys(resources)).to.deep.equal(['builtin'])
expect(resources.builtin()).to.deep.equal({ version: 1 })
})
})
describe('style setter cache', () => {
it('rejects cached rules containing serialized objects', () => {
expect(
hasInvalidCachedThemeRules([
['html { --font: [object Object]; }'],
['.post { --font: sans-serif; }'],
]),
).to.equal(true)
})
it('accepts cached rules containing valid font families', () => {
expect(
hasInvalidCachedThemeRules([
['html { --font: sans-serif; }'],
[
'.post { --font: "Atkinson Hyperlegible"; }',
'.post::after { content: "[object Object]"; }',
],
]),
).to.equal(false)
})
})

View file

@ -0,0 +1,42 @@
import {
basePaletteKeys,
convertTheme2To3,
} from 'src/services/theme_data/theme2_to_theme3.js'
describe('Theme 2 to Theme 3 conversion', () => {
it('converts font descriptors to CSS font families', () => {
const colors = Object.fromEntries(
[...basePaletteKeys].map((key) => [key, '#000000']),
)
const rules = convertTheme2To3({
colors,
fonts: {
interface: { family: 'sans-serif' },
input: { family: 'Open Sans' },
post: { family: 'Atkinson Hyperlegible' },
postCode: { family: 'monospace' },
},
})
expect(rules).to.deep.include({
source: '2to3',
component: 'Root',
directives: { '--font': 'generic | sans-serif' },
})
expect(rules).to.deep.include({
source: '2to3',
component: 'Root',
directives: { '--monoFont': 'generic | monospace' },
})
expect(rules).to.deep.include({
source: '2to3',
component: 'Input',
directives: { '--font': 'generic | Open Sans' },
})
expect(rules).to.deep.include({
source: '2to3',
component: 'RichContent',
directives: { '--font': 'generic | Atkinson Hyperlegible' },
})
})
})