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/).
## 2.11
## 2.11.1
### Fixed
- User note not working
## 2.11.0
### Added
- Initial MFM rendering support
- 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",
"version": "2.11.0",
"version": "2.11.1",
"description": "Pleroma frontend, the default frontend of Pleroma social network server",
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/src/CONTRIBUTORS.md>",
"private": false,

View file

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

View file

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

View file

@ -27,8 +27,8 @@ import {
readChat,
sendChatMessage,
} from 'src/api/chats.js'
import { WSConnectionStatus } from 'src/api/websocket.js'
import { fetchConversation, fetchStatus } from 'src/api/public.js'
import { WSConnectionStatus } from 'src/api/websocket.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
@ -53,7 +53,14 @@ const Chat = {
PostStatusForm,
},
props: {
statusId: String,
statusId: {
type: String,
default: null,
},
chatUserId: {
type: String,
default: null,
},
testMode: Boolean,
},
data() {
@ -180,11 +187,20 @@ const Chat = {
},
$route: async function (newVal) {
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
const bottomBoundary = window.innerHeight - this.$refs.footer.clientHeight
const topBoundary = this.$refs.header.clientHeight + document.getElementById('nav').clientHeight
const margin = Number(window.getComputedStyle(this.$refs.messageList.$el).gap.replace('px',''))
const bottomBoundary =
window.innerHeight - this.$refs.footer.clientHeight
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 scrollAmount = (() => {
@ -293,7 +309,9 @@ const Chat = {
if (this.isConversation) {
const [
{ data: status },
{ data: { ancestors, descendants } }
{
data: { ancestors, descendants },
},
] = await Promise.all([
fetchStatus({
id: this.statusId,
@ -302,7 +320,7 @@ const Chat = {
fetchConversation({
id: this.statusId,
credentials: useOAuthStore().token,
})
}),
])
messages = [...ancestors, status, ...descendants]
} else {
@ -337,17 +355,21 @@ const Chat = {
// full height of 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.
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({
maxId: this.minId,
})
}
},
async startFetching() {
console.log(this.statusId, this.chatUserId)
if (!this.isConversation) {
try {
const { data } = await getOrCreateChat({
accountId: this.recipientId,
accountId: this.chatUserId,
credentials: useOAuthStore().token,
})
this.$store.commit('addNewUsers', [data.account])
@ -378,7 +400,7 @@ const Chat = {
const message = newMessages[i]
// Sanity check
if (!this.isConversation && (message.chat_id !== this.chat.id)) {
if (!this.isConversation && message.chat_id !== this.chat.id) {
console.warn(
`Chat message doesn't belong to current chat (id: ${this.chat.id})!!`,
message,
@ -516,7 +538,10 @@ const Chat = {
// Event handlers
onPosted(data) {
this.explicitReplyStatus = null
this.$router.push({ name: 'conversation2', params: { statusId: data.id } })
this.$router.push({
name: 'conversation2',
params: { statusId: data.id },
})
},
handleVisibilityChange() {
this.$nextTick(() => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,16 +32,17 @@ export const createStyleSheet = (id, priority = 1000) => {
newRule = newRule.replace(/backdrop-filter:[^;]+;/g, '') // Remove backdrop-filter
}
// firefox doesn't like invalid selectors
if (
!CSS.supports?.('selector(::-webkit-scrollbar)') &&
!CSS.supports?.('selector(::-webkit-scrollbar-button)') &&
!CSS.supports?.('selector(::-webkit-resizer)') &&
!CSS.supports?.('selector(::-webkit-scrollbar-thumb)') &&
newRule.startsWith('::-webkit')
) {
return
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.replace(/var\(--shadowFilter\)[^;]*;/g, ''), // Remove shadowFilter references
)
@ -59,7 +60,13 @@ export const adoptStyleSheets = throttle(() => {
.sort((a, b) => a.priority - b.priority)
.map((sheet) => {
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
})
} else {

View file

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