Merge branch 'chat-refactor' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2026-07-30 15:14:10 +03:00
commit e4429c57cf
20 changed files with 147 additions and 135 deletions

View file

@ -3,7 +3,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 2.11 ## 2.11.1
### Fixed
- User note not working
## 2.11.0
### Added ### Added
- Initial MFM rendering support - Initial MFM rendering support
- Button to remove all drafts - Button to remove all drafts

4
changelog.d/2-11-2.fix Normal file
View file

@ -0,0 +1,4 @@
Fixed crashing on Firefox 153
Fixed serif-fonts being used by default in some cases
Fixed font select component showing and using objects instead of strings
Fixed generic ('sans-serif', 'monospace' etc) fonts not working

View file

@ -0,0 +1 @@
Fix style of extra notifications

View file

@ -0,0 +1 @@
Fix MFA login and recovery code authentication.

View file

@ -0,0 +1 @@
Do not crash even on css rule insertion failure

View file

@ -1,6 +1,6 @@
{ {
"name": "pleroma_fe", "name": "pleroma_fe",
"version": "2.11.0", "version": "2.11.1",
"description": "Pleroma frontend, the default frontend of Pleroma social network server", "description": "Pleroma frontend, the default frontend of Pleroma social network server",
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/src/CONTRIBUTORS.md>", "author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/src/CONTRIBUTORS.md>",
"private": false, "private": false,

View file

@ -1,5 +1,3 @@
import { defineAsyncComponent } from 'vue'
import AuthForm from 'src/components/auth_form/auth_form.js' import AuthForm from 'src/components/auth_form/auth_form.js'
import BookmarkTimeline from 'src/components/bookmark_timeline/bookmark_timeline.vue' import BookmarkTimeline from 'src/components/bookmark_timeline/bookmark_timeline.vue'
import BubbleTimeline from 'src/components/bubble_timeline/bubble_timeline.vue' import BubbleTimeline from 'src/components/bubble_timeline/bubble_timeline.vue'
@ -68,9 +66,7 @@ export default (store) => {
{ {
name: 'conversation2', name: 'conversation2',
path: '/conversation/:statusId', path: '/conversation/:statusId',
component: defineAsyncComponent( component: () => import('src/components/chat_view/chat_view.vue'),
() => import('src/components/chat_view/chat_view.vue'),
),
props: true, props: true,
meta: { dontScroll: true }, meta: { dontScroll: true },
beforeEnter: validateAuthenticatedRoute, beforeEnter: validateAuthenticatedRoute,
@ -91,23 +87,18 @@ export default (store) => {
{ {
name: 'external-user-profile', name: 'external-user-profile',
path: '/users/$:id', path: '/users/$:id',
component: defineAsyncComponent( component: () => import('src/components/user_profile/user_profile.vue'),
() => import('src/components/user_profile/user_profile.vue'),
),
}, },
{ {
name: 'user-profile-admin-view', name: 'user-profile-admin-view',
path: '/users/$:id/admin_view', path: '/users/$:id/admin_view',
component: defineAsyncComponent( component: () =>
() => import('src/components/user_profile/user_profile_admin_view.vue'), import('src/components/user_profile/user_profile_admin_view.vue'),
),
}, },
{ {
name: 'interactions', name: 'interactions',
path: '/users/:username/interactions', path: '/users/:username/interactions',
component: defineAsyncComponent( component: () => import('src/components/interactions/interactions.vue'),
() => import('src/components/interactions/interactions.vue'),
),
beforeEnter: validateAuthenticatedRoute, beforeEnter: validateAuthenticatedRoute,
}, },
{ {
@ -119,39 +110,31 @@ export default (store) => {
{ {
name: 'registration', name: 'registration',
path: '/registration', path: '/registration',
component: defineAsyncComponent( component: () => import('src/components/registration/registration.vue'),
() => import('src/components/registration/registration.vue'),
),
}, },
{ {
name: 'password-reset', name: 'password-reset',
path: '/password-reset', path: '/password-reset',
component: defineAsyncComponent( component: () =>
() => import('src/components/password_reset/password_reset.vue'), import('src/components/password_reset/password_reset.vue'),
),
props: true, props: true,
}, },
{ {
name: 'registration-token', name: 'registration-token',
path: '/registration/:token', path: '/registration/:token',
component: defineAsyncComponent( component: () => import('src/components/registration/registration.vue'),
() => import('src/components/registration/registration.vue'),
),
}, },
{ {
name: 'friend-requests', name: 'friend-requests',
path: '/friend-requests', path: '/friend-requests',
component: defineAsyncComponent( component: () =>
() => import('src/components/follow_requests/follow_requests.vue'), import('src/components/follow_requests/follow_requests.vue'),
),
beforeEnter: validateAuthenticatedRoute, beforeEnter: validateAuthenticatedRoute,
}, },
{ {
name: 'notifications', name: 'notifications',
path: '/:username/notifications', path: '/:username/notifications',
component: defineAsyncComponent( component: () => import('src/components/notifications/notifications.vue'),
() => import('src/components/notifications/notifications.vue'),
),
props: () => ({ disableTeleport: true }), props: () => ({ disableTeleport: true }),
beforeEnter: validateAuthenticatedRoute, beforeEnter: validateAuthenticatedRoute,
}, },
@ -163,98 +146,74 @@ export default (store) => {
{ {
name: 'shout-panel', name: 'shout-panel',
path: '/shout-panel', path: '/shout-panel',
component: defineAsyncComponent( component: () => import('src/components/shout_panel/shout_panel.vue'),
() => import('src/components/shout_panel/shout_panel.vue'),
),
props: () => ({ floating: false }), props: () => ({ floating: false }),
}, },
{ {
name: 'oauth-callback', name: 'oauth-callback',
path: '/oauth-callback', path: '/oauth-callback',
component: defineAsyncComponent( component: () =>
() => import('src/components/oauth_callback/oauth_callback.vue'), import('src/components/oauth_callback/oauth_callback.vue'),
),
props: (route) => ({ code: route.query.code }), props: (route) => ({ code: route.query.code }),
}, },
{ {
name: 'search', name: 'search',
path: '/search', path: '/search',
component: defineAsyncComponent( component: () => import('src/components/search/search.vue'),
() => import('src/components/search/search.vue'),
),
props: (route) => ({ query: route.query.query }), props: (route) => ({ query: route.query.query }),
}, },
{ {
name: 'who-to-follow', name: 'who-to-follow',
path: '/who-to-follow', path: '/who-to-follow',
component: defineAsyncComponent( component: () => import('src/components/who_to_follow/who_to_follow.vue'),
() => import('src/components/who_to_follow/who_to_follow.vue'),
),
beforeEnter: validateAuthenticatedRoute, beforeEnter: validateAuthenticatedRoute,
}, },
{ {
name: 'about', name: 'about',
path: '/about', path: '/about',
component: defineAsyncComponent( component: () => import('src/components/about/about.vue'),
() => import('src/components/about/about.vue'),
),
}, },
{ {
name: 'announcements', name: 'announcements',
path: '/announcements', path: '/announcements',
component: defineAsyncComponent( component: () =>
() => import('src/components/announcements_page/announcements_page.vue'),
import('src/components/announcements_page/announcements_page.vue'),
),
}, },
{ {
name: 'drafts', name: 'drafts',
path: '/drafts', path: '/drafts',
component: defineAsyncComponent( component: () => import('src/components/drafts/drafts.vue'),
() => import('src/components/drafts/drafts.vue'),
),
}, },
{ {
name: 'user-profile', name: 'user-profile',
path: '/users/:name', path: '/users/:name',
component: defineAsyncComponent( component: () => import('src/components/user_profile/user_profile.vue'),
() => import('src/components/user_profile/user_profile.vue'),
),
}, },
{ {
name: 'legacy-user-profile', name: 'legacy-user-profile',
path: '/:name', path: '/:name',
component: defineAsyncComponent( component: () => import('src/components/user_profile/user_profile.vue'),
() => import('src/components/user_profile/user_profile.vue'),
),
}, },
{ {
name: 'lists', name: 'lists',
path: '/lists', path: '/lists',
component: defineAsyncComponent( component: () => import('src/components/lists/lists.vue'),
() => import('src/components/lists/lists.vue'),
),
}, },
{ {
name: 'lists-timeline', name: 'lists-timeline',
path: '/lists/:id', path: '/lists/:id',
component: defineAsyncComponent( component: () =>
() => import('src/components/lists_timeline/lists_timeline.vue'), import('src/components/lists_timeline/lists_timeline.vue'),
),
}, },
{ {
name: 'lists-edit', name: 'lists-edit',
path: '/lists/:id/edit', path: '/lists/:id/edit',
component: defineAsyncComponent( component: () => import('src/components/lists_edit/lists_edit.vue'),
() => import('src/components/lists_edit/lists_edit.vue'),
),
}, },
{ {
name: 'lists-new', name: 'lists-new',
path: '/lists/new', path: '/lists/new',
component: defineAsyncComponent( component: () => import('src/components/lists_edit/lists_edit.vue'),
() => import('src/components/lists_edit/lists_edit.vue'),
),
}, },
{ {
name: 'edit-navigation', name: 'edit-navigation',
@ -266,19 +225,14 @@ export default (store) => {
{ {
name: 'bookmark-folders', name: 'bookmark-folders',
path: '/bookmark_folders', path: '/bookmark_folders',
component: defineAsyncComponent( component: () =>
() => import('src/components/bookmark_folders/bookmark_folders.vue'), import('src/components/bookmark_folders/bookmark_folders.vue'),
),
}, },
{ {
name: 'bookmark-folder-new', name: 'bookmark-folder-new',
path: '/bookmarks/new-folder', path: '/bookmarks/new-folder',
component: defineAsyncComponent( component: () =>
() => import('src/components/bookmark_folder_edit/bookmark_folder_edit.vue'),
import(
'src/components/bookmark_folder_edit/bookmark_folder_edit.vue'
),
),
}, },
{ {
name: 'bookmark-folder', name: 'bookmark-folder',
@ -288,12 +242,8 @@ export default (store) => {
{ {
name: 'bookmark-folder-edit', name: 'bookmark-folder-edit',
path: '/bookmarks/:id/edit', path: '/bookmarks/:id/edit',
component: defineAsyncComponent( component: () =>
() => import('src/components/bookmark_folder_edit/bookmark_folder_edit.vue'),
import(
'src/components/bookmark_folder_edit/bookmark_folder_edit.vue'
),
),
}, },
] ]
@ -301,19 +251,16 @@ export default (store) => {
routes = routes.concat([ routes = routes.concat([
{ {
name: 'chat', name: 'chat',
path: '/users/:username/chats/:recipient_id', path: '/users/:username/chats/:chatUserId',
component: defineAsyncComponent( component: () => import('src/components/chat_view/chat_view.vue'),
() => import('src/components/chat_view/chat_view.vue'),
),
meta: { dontScroll: false }, meta: { dontScroll: false },
props: true,
beforeEnter: validateAuthenticatedRoute, beforeEnter: validateAuthenticatedRoute,
}, },
{ {
name: 'chats', name: 'chats',
path: '/users/:username/chats', path: '/users/:username/chats',
component: defineAsyncComponent( component: () => import('src/components/chat_list/chat_list.vue'),
() => import('src/components/chat_list/chat_list.vue'),
),
meta: { dontScroll: false }, meta: { dontScroll: false },
beforeEnter: validateAuthenticatedRoute, beforeEnter: validateAuthenticatedRoute,
}, },

View file

@ -76,9 +76,7 @@ const ChatMessage = {
return !!this.message.user return !!this.message.user
}, },
authorId() { authorId() {
return this.isStatus return this.isStatus ? this.message.user.id : this.message.account_id
? this.message.user.id
: this.message.account_id
}, },
author() { author() {
return this.$store.getters.findUser(this.authorId) return this.$store.getters.findUser(this.authorId)

View file

@ -27,8 +27,8 @@ import {
readChat, readChat,
sendChatMessage, sendChatMessage,
} from 'src/api/chats.js' } from 'src/api/chats.js'
import { WSConnectionStatus } from 'src/api/websocket.js'
import { fetchConversation, fetchStatus } from 'src/api/public.js' import { fetchConversation, fetchStatus } from 'src/api/public.js'
import { WSConnectionStatus } from 'src/api/websocket.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons' import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
@ -53,7 +53,14 @@ const Chat = {
PostStatusForm, PostStatusForm,
}, },
props: { props: {
statusId: String, statusId: {
type: String,
default: null,
},
chatUserId: {
type: String,
default: null,
},
testMode: Boolean, testMode: Boolean,
}, },
data() { data() {
@ -180,11 +187,20 @@ const Chat = {
}, },
$route: async function (newVal) { $route: async function (newVal) {
if (this.messagesIndex[newVal.params.statusId]) { if (this.messagesIndex[newVal.params.statusId]) {
const focused = document.getElementById(`chatmessage-${this.$route.params.statusId}`) const focused = document.getElementById(
`chatmessage-${this.$route.params.statusId}`,
)
if (focused?.getBoundingClientRect == null) return if (focused?.getBoundingClientRect == null) return
const bottomBoundary = window.innerHeight - this.$refs.footer.clientHeight const bottomBoundary =
const topBoundary = this.$refs.header.clientHeight + document.getElementById('nav').clientHeight window.innerHeight - this.$refs.footer.clientHeight
const margin = Number(window.getComputedStyle(this.$refs.messageList.$el).gap.replace('px','')) const topBoundary =
this.$refs.header.clientHeight +
document.getElementById('nav').clientHeight
const margin = Number(
window
.getComputedStyle(this.$refs.messageList.$el)
.gap.replace('px', ''),
)
const rect = focused.getBoundingClientRect() const rect = focused.getBoundingClientRect()
const scrollAmount = (() => { const scrollAmount = (() => {
@ -293,7 +309,9 @@ const Chat = {
if (this.isConversation) { if (this.isConversation) {
const [ const [
{ data: status }, { data: status },
{ data: { ancestors, descendants } } {
data: { ancestors, descendants },
},
] = await Promise.all([ ] = await Promise.all([
fetchStatus({ fetchStatus({
id: this.statusId, id: this.statusId,
@ -302,7 +320,7 @@ const Chat = {
fetchConversation({ fetchConversation({
id: this.statusId, id: this.statusId,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}) }),
]) ])
messages = [...ancestors, status, ...descendants] messages = [...ancestors, status, ...descendants]
} else { } else {
@ -337,17 +355,21 @@ const Chat = {
// full height of the scrollable container. // full height of the scrollable container.
// If this is the case, we want to fetch the messages until the scrollable container // If this is the case, we want to fetch the messages until the scrollable container
// is fully populated so that the user has the ability to scroll up and load the history. // is fully populated so that the user has the ability to scroll up and load the history.
if (!isScrollable() && messages.length > 0) { //
// Conversation fetching doesn't support pagination and spews out everything at once
// so we both can't and don't need to fetch previous posts
if (!this.isConversation && !isScrollable() && messages.length > 0) {
this.fetchChat({ this.fetchChat({
maxId: this.minId, maxId: this.minId,
}) })
} }
}, },
async startFetching() { async startFetching() {
console.log(this.statusId, this.chatUserId)
if (!this.isConversation) { if (!this.isConversation) {
try { try {
const { data } = await getOrCreateChat({ const { data } = await getOrCreateChat({
accountId: this.recipientId, accountId: this.chatUserId,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}) })
this.$store.commit('addNewUsers', [data.account]) this.$store.commit('addNewUsers', [data.account])
@ -378,7 +400,7 @@ const Chat = {
const message = newMessages[i] const message = newMessages[i]
// Sanity check // Sanity check
if (!this.isConversation && (message.chat_id !== this.chat.id)) { if (!this.isConversation && message.chat_id !== this.chat.id) {
console.warn( console.warn(
`Chat message doesn't belong to current chat (id: ${this.chat.id})!!`, `Chat message doesn't belong to current chat (id: ${this.chat.id})!!`,
message, message,
@ -516,7 +538,10 @@ const Chat = {
// Event handlers // Event handlers
onPosted(data) { onPosted(data) {
this.explicitReplyStatus = null this.explicitReplyStatus = null
this.$router.push({ name: 'conversation2', params: { statusId: data.id } }) this.$router.push({
name: 'conversation2',
params: { statusId: data.id },
})
}, },
handleVisibilityChange() { handleVisibilityChange() {
this.$nextTick(() => { this.$nextTick(() => {

View file

@ -67,7 +67,10 @@
</div> </div>
</span> </span>
</div> </div>
<div class="auto-reply-to-section"> <div
v-if="isConversation"
class="auto-reply-to-section"
>
<div class="reply-to-text"> <div class="reply-to-text">
{{ explicitReplyStatus ? $t('status.reply_to_selected') : $t('status.reply_to_last') }} {{ explicitReplyStatus ? $t('status.reply_to_selected') : $t('status.reply_to_last') }}
<button <button

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="ExtraNotifications"> <div class="ExtraNotifications panel-body">
<div <div
v-if="shouldShowChats" v-if="shouldShowChats"
class="notification unseen" class="notification unseen"
@ -87,6 +87,11 @@
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
&.panel-body::before {
content: '';
padding: 0;
}
.notification { .notification {
width: 100%; width: 100%;
border-bottom: 1px solid; border-bottom: 1px solid;

View file

@ -63,8 +63,8 @@ const LoginForm = {
}) })
}) })
.catch((error) => { .catch((error) => {
if (error.errorData?.error === 'mfa_required') { if (error.errorData === 'mfa_required') {
this.requireMFA({ settings: error }) this.requireMFA({ settings: error.error })
} else if (error.identifier === 'password_reset_required') { } else if (error.identifier === 'password_reset_required') {
this.$router.push({ this.$router.push({
name: 'password-reset', name: 'password-reset',

View file

@ -44,7 +44,7 @@ export default {
} }
verifyRecoveryCode(data) verifyRecoveryCode(data)
.then((result) => { .then(({ data: result }) => {
this.login(result).then(() => { this.login(result).then(() => {
this.$router.push({ name: 'friends' }) this.$router.push({ name: 'friends' })
}) })

View file

@ -690,6 +690,7 @@ const PostStatusForm = {
.then((data) => { .then((data) => {
this.abandonDraft() this.abandonDraft()
this.clearStatus() this.clearStatus()
this.updateIdempotencyKey()
this.$emit('posted', data) this.$emit('posted', data)
}) })
.catch((error) => { .catch((error) => {
@ -1007,6 +1008,7 @@ const PostStatusForm = {
this.saveable = false this.saveable = false
if (!this.shouldAutoSaveDraft) { if (!this.shouldAutoSaveDraft) {
this.clearStatus() this.clearStatus()
this.updateIdempotencyKey()
this.$emit('draft-done') this.$emit('draft-done')
} }
}) })
@ -1016,6 +1018,7 @@ const PostStatusForm = {
this.saveable = false this.saveable = false
if (!this.shouldAutoSaveDraft) { if (!this.shouldAutoSaveDraft) {
this.clearStatus() this.clearStatus()
this.updateIdempotencyKey()
this.$emit('draft-done') this.$emit('draft-done')
} }
}) })

View file

@ -82,13 +82,13 @@ const ScopeSelector = {
changeVis(scope) { changeVis(scope) {
this.currentScope = scope this.currentScope = scope
this.$emit('change', scope) this.$emit('change', scope)
} },
}, },
watch: { watch: {
originalScope(newVal) { originalScope(newVal) {
this.currentScope = newVal this.currentScope = newVal
} },
} },
} }
export default ScopeSelector export default ScopeSelector

View file

@ -68,7 +68,7 @@
:model-value="mergedConfig.fontInterface" :model-value="mergedConfig.fontInterface"
name="ui" name="ui"
:label="$t('settings.style.fonts.components_inline.interface')" :label="$t('settings.style.fonts.components_inline.interface')"
:fallback="{ family: 'sans-serif' }" fallback="sans-serif"
no-inherit="1" no-inherit="1"
@update:model-value="v => updateFont('fontInterface', v)" @update:model-value="v => updateFont('fontInterface', v)"
/> />
@ -77,7 +77,7 @@
<FontControl <FontControl
:model-value="mergedConfig.fontInput" :model-value="mergedConfig.fontInput"
name="input" name="input"
:fallback="{ family: 'inherit' }" fallback="inherit"
:label="$t('settings.style.fonts.components_inline.input')" :label="$t('settings.style.fonts.components_inline.input')"
@update:model-value="v => updateFont('fontInput', v)" @update:model-value="v => updateFont('fontInput', v)"
/> />

View file

@ -58,7 +58,7 @@
<FontControl <FontControl
:model-value="mergedConfig.fontPosts" :model-value="mergedConfig.fontPosts"
name="post" name="post"
:fallback="{ family: 'inherit' }" fallback="inherit"
:label="$t('settings.style.fonts.components.post')" :label="$t('settings.style.fonts.components.post')"
@update:model-value="v => updateFont('fontPosts', v)" @update:model-value="v => updateFont('fontPosts', v)"
/> />
@ -67,7 +67,7 @@
<FontControl <FontControl
:model-value="mergedConfig.fontMonospace" :model-value="mergedConfig.fontMonospace"
name="postCode" name="postCode"
:fallback="{ family: 'monospace' }" fallback="monospace"
:label="$t('settings.style.fonts.components.monospace')" :label="$t('settings.style.fonts.components.monospace')"
@update:model-value="v => updateFont('fontMonospace', v)" @update:model-value="v => updateFont('fontMonospace', v)"
/> />

View file

@ -17,21 +17,21 @@ import {
faCheck, faCheck,
faChevronDown, faChevronDown,
faChevronRight, faChevronRight,
faComments,
faExternalLinkAlt, faExternalLinkAlt,
faEye, faEye,
faEyeSlash, faEyeSlash,
faHistory, faHistory,
faMinus, faMinus,
faPencil,
faPlus, faPlus,
faReply, faReply,
faRetweet, faRetweet,
faShareAlt, faShareAlt,
faStar, faStar,
faThumbtack, faThumbtack,
faPencil,
faTimes, faTimes,
faWrench, faWrench,
faComments,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(

View file

@ -32,16 +32,17 @@ export const createStyleSheet = (id, priority = 1000) => {
newRule = newRule.replace(/backdrop-filter:[^;]+;/g, '') // Remove backdrop-filter newRule = newRule.replace(/backdrop-filter:[^;]+;/g, '') // Remove backdrop-filter
} }
// firefox doesn't like invalid selectors if (newRule.startsWith('::-webkit')) {
if ( if (typeof CSS.supports !== 'function') return
!CSS.supports?.('selector(::-webkit-scrollbar)') && // firefox doesn't like invalid selectors
!CSS.supports?.('selector(::-webkit-scrollbar-button)') && const fullWebkitScrollbarSupport =
!CSS.supports?.('selector(::-webkit-resizer)') && CSS.supports('selector(::-webkit-scrollbar)') &&
!CSS.supports?.('selector(::-webkit-scrollbar-thumb)') && CSS.supports('selector(::-webkit-scrollbar-button)') &&
newRule.startsWith('::-webkit') CSS.supports('selector(::-webkit-resizer)') &&
) { CSS.supports('selector(::-webkit-scrollbar-thumb)')
return if (!fullWebkitScrollbarSupport) return
} }
this.rules.push( this.rules.push(
newRule.replace(/var\(--shadowFilter\)[^;]*;/g, ''), // Remove shadowFilter references newRule.replace(/var\(--shadowFilter\)[^;]*;/g, ''), // Remove shadowFilter references
) )
@ -59,7 +60,13 @@ export const adoptStyleSheets = throttle(() => {
.sort((a, b) => a.priority - b.priority) .sort((a, b) => a.priority - b.priority)
.map((sheet) => { .map((sheet) => {
const css = new CSSStyleSheet() const css = new CSSStyleSheet()
sheet.rules.forEach((r) => css.insertRule(r)) sheet.rules.forEach((r) => {
try {
css.insertRule(r)
} catch (e) {
console.warn('Error inserting rule:', e, r)
}
})
return css return css
}) })
} else { } else {

View file

@ -17,6 +17,16 @@ import {
} from 'src/services/theme_data/theme_data.service.js' } from 'src/services/theme_data/theme_data.service.js'
import { convertTheme2To3 } from 'src/services/theme_data/theme2_to_theme3.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', { export const useInterfaceStore = defineStore('interface', {
state: () => ({ state: () => ({
localFonts: null, localFonts: null,
@ -246,7 +256,7 @@ export const useInterfaceStore = defineStore('interface', {
} }
}, },
setFontsList(value) { setFontsList(value) {
this.localFonts = [...new Set(value.map((font) => font)).values()] this.localFonts = [...new Set(value.map(({ family }) => family)).values()]
}, },
queryLocalFonts() { queryLocalFonts() {
if (this.localFonts !== null) return if (this.localFonts !== null) return
@ -706,11 +716,14 @@ export const useInterfaceStore = defineStore('interface', {
Object.entries(fontMap).forEach(([font, component]) => { Object.entries(fontMap).forEach(([font, component]) => {
const family = mergedConfig[`font${font}`] const family = mergedConfig[`font${font}`]
const variable = font === 'Monospace' ? '--monoFont' : '--font' const variable = font === 'Monospace' ? '--monoFont' : '--font'
if (family) { if (typeof family === 'string') {
const familyString = GENERIC_FONT_NAMES.has(family)
? family
: `"${family}"`
hacks.push({ hacks.push({
component, component,
directives: { directives: {
[variable]: `generic | "${family}"`, [variable]: `generic | ${familyString}`,
}, },
}) })
} }