Merge branch 'users-statuses-pinia' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2026-08-25 21:44:27 +03:00
commit c0bbb1a768
27 changed files with 227 additions and 97 deletions

View file

@ -1 +0,0 @@
If user avatars next to mentions are enabled it will show empty placeholder avatar next to label while user is being fetched, to avoid jumps

View file

@ -29,6 +29,7 @@ import {
import routes from './routes'
import { useAuthFlowStore } from 'src/stores/auth_flow'
import { useChatsStore } from 'src/stores/chats.js'
import { useEmojiStore } from 'src/stores/emoji.js'
import { useI18nStore } from 'src/stores/i18n'
import { useInstanceStore } from 'src/stores/instance.js'
@ -593,6 +594,7 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
useI18nStore().setI18n(i18n)
// Global WS handlers
useChatsStore().attachSocket()
useInterfaceStore().attachSocket()
useStatusesStore().attachSocket()

View file

@ -89,14 +89,15 @@ const Chat = {
fetcher: null,
socket: null,
streaming: false,
fetching: true,
errorLoadingChat: false,
messageRetriers: {},
idempotencyKeyIndex: {},
}
},
created() {
async created() {
if (this.testMode) return
this.activate()
await this.activate()
this.attachSocket()
},
mounted() {
@ -243,10 +244,11 @@ const Chat = {
accountId: this.chatUserId,
credentials: useOAuthStore().token,
})
useUsersStore().addNewUsers(result)
const { data } = result
useUsersStore().addNewUsers({ ...result, data: data.account })
data.account = useUsersStore().findUser(data.account.id)
this.chat = data
this.maxId = this.chat.lastMessage?.id
} catch (e) {
console.error('Error creating or getting a chat', e)
this.errorLoadingChat = true
@ -254,33 +256,38 @@ const Chat = {
}
if (this.isConversation || this.chat) {
this.startFetching('Chat activated', true)
this.$nextTick(() => {
this.scrollDown({ forceRead: true })
})
this.startFetching('Chat activated', true)
}
},
deactivate() {
this.clear()
if (!this.streaming) {
this.stopFetching()
if (this.fetching) {
this.stopFetching('Chat deactivated')
}
},
attachSocket() {
const et = new EventTarget()
const socket = { et }
const socket = {
name: 'chatview',
et,
}
et.addEventListener('update', this.onStreamMessage)
et.addEventListener('pleroma:chat_update', this.onChatUpdate)
et.addEventListener('open', this.onStreamConnect)
et.addEventListener('close', this.onStreamDisconnect)
useStreamingStore().addSubscriber(socket)
this.socket = socket
useStreamingStore().addSubscriber(this.socket)
},
detachSocket() {
const { et } = this.socket
et.removeEventListener('update', this.onStreamMessage)
et.removeEventListener('pleroma:chat_update', this.onChatUpdate)
et.removeEventListener('open', this.onStreamConnect)
et.removeEventListener('close', this.onStreamDisconnect)
@ -303,11 +310,13 @@ const Chat = {
5000,
)
this.fetchChat({ isFirstFetch })
this.fetching = true
},
stopFetching(reason) {
console.debug('[Chat View] Stopped fetching', 'Reason:', reason)
this.fetcher.stop()
this.fetcher = null
this.fetching = false
},
// Actions
@ -437,6 +446,10 @@ const Chat = {
)
this.addMessages({ messages })
},
onChatUpdate({ data: { chatUpdate } }) {
const messages = [chatUpdate.lastMessage]
this.addMessages({ messages })
},
addMessages({ messages: newMessages }) {
for (let i = 0; i < newMessages.length; i++) {
const message = newMessages[i]

View file

@ -155,9 +155,9 @@ const conversation = {
return this.otherRepliesButtonPosition === 'inside'
},
suspendable() {
return this.unsuspendibleIds.size > 0
return this.unsuspendibleIds.size === 0
},
hideStatus() {
hide() {
return this.virtualHidden && this.suspendable
},
originalStatusId() {
@ -365,7 +365,6 @@ const conversation = {
return !!(this.expanded || this.isPage)
},
hiddenStyle() {
if (this.isExpanded || !this.virtualHidden) return {}
return { height: this.virtualHeight + 'px' }
},
threadDisplayStatus() {
@ -621,6 +620,7 @@ const conversation = {
}
},
updateVirtualHeight() {
if (this.hide) return // no updates when not rendering
this.$nextTick(() => {
this.virtualHeight = this.$refs.body.getBoundingClientRect().height
this.$emit('update:virtualHeight', {

View file

@ -1,7 +1,6 @@
<template>
<div
v-if="!hideStatus"
:style="hiddenStyle"
v-if="!hide"
class="Conversation"
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
>

View file

@ -62,7 +62,7 @@ const EmojiReactions = {
},
async fetchEmojiReactionsIfMissing() {
const hasNoAccounts = this.status.emoji_reactions.find((r) => !r.accounts)
if (!hasNoAccounts) {
if (hasNoAccounts) {
return await useStatusesStore().fetchEmojiReactions(this.status.id)
}
},

View file

@ -8,20 +8,15 @@
:href="url"
class="original"
target="_blank"
><!-- eslint-enable vue/no-v-html -->
<UserAvatar
v-if="shouldShowAvatar"
class="mention-avatar"
:user-id="null"
/>
<span v-html="content" />
</a>
v-html="content"
/><!-- eslint-enable vue/no-v-html -->
<UserPopover
v-else
:user-id="user.id"
:disabled="!shouldShowTooltip"
>
<span
v-if="user"
class="new"
:style="style"
:class="classnames"

View file

@ -121,10 +121,10 @@ const Notifications = {
)
},
loading() {
return useNotificationsStore().fetcher.loading.value
return useNotificationsStore().fetcher.loading
},
bottomedOut() {
return useNotificationsStore().fetcher.bottomedOut.value
return useNotificationsStore().fetcher.bottomedOut
},
noHeading() {
const { layoutType } = useInterfaceStore()

View file

@ -119,7 +119,7 @@
>
<router-link
class="list-item hashtag"
:to="{ name: 'tag-timeline', params: { tag: hashtag.name } }"
:to="{ name: 'tag-timeline', params: { id: hashtag.name } }"
>
<span class="name">
#{{ hashtag.name }}

View file

@ -128,7 +128,6 @@ const Status = {
return {
replying: false,
unmuted: false,
userExpanded: false,
mediaPlaying: new Set(),
error: null,
headTailLinks: null,
@ -524,9 +523,6 @@ const Status = {
toggleMute() {
this.unmuted = !this.unmuted
},
toggleUserExpanded() {
this.userExpanded = !this.userExpanded
},
generateUserProfileLink(id, name) {
return generateProfileLink(
id,
@ -576,6 +572,12 @@ const Status = {
this.$emit('heightChange')
},
},
unmuted() {
this.$emit('heightChange')
},
error() {
this.$emit('heightChange')
},
replying() {
this.$emit('heightChange')
},
@ -604,7 +606,6 @@ const Status = {
},
isSuspendable: function (suspend) {
this.$emit('suspendableStateChange', { id: this.status.id, suspend })
this.$emit('heightChange')
},
},
}

View file

@ -549,6 +549,7 @@
@posted="closeReplyForm"
@draft-done="closeReplyForm"
@close-accepted="closeReplyForm"
@resize="$emit('heightChange')"
/>
</div>
</template>

View file

@ -239,7 +239,7 @@ const Timeline = {
const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -bodyBRect.y)
if (
!this.timeline.fetcher.loadingOlder.value &&
!this.timeline.fetcher.loadingOlder &&
window.innerHeight + window.pageYOffset >= height - 750
) {
this.fetchOlderStatuses()

View file

@ -12,7 +12,7 @@ const UserAvatar = {
props: {
// UserID of a user to show avatar of
userId: {
required: false, // You can pass null to just render a placeholder
required: true,
type: String,
},
// Use less space and use alternative roundness

View file

@ -70,7 +70,6 @@
&.-placeholder {
background-color: var(--background);
border: 1px solid var(--border)
}
}

View file

@ -30,8 +30,8 @@ function showWhoToFollow(panel, reply) {
})
}
function getWhoToFollow(panel) {
const credentials = panel.$useUsersStore().currentUser.credentials
function getWhoToFollow() {
const credentials = useOAuthStore().token
if (credentials) {
panel.usersToFollow.forEach((toFollow) => {
toFollow.name = 'Loading...'
@ -66,7 +66,7 @@ const WhoToFollowPanel = {
watch: {
user: function () {
if (this.suggestionsEnabled) {
getWhoToFollow(this)
getWhoToFollow()
}
},
},
@ -77,7 +77,7 @@ const WhoToFollowPanel = {
id: 0,
}))
if (this.suggestionsEnabled) {
getWhoToFollow(this)
getWhoToFollow()
}
},
}

View file

@ -11,13 +11,14 @@ export const maybeShowChatNotification = (chat) => {
title: chat.account.name,
icon: chat.account.profile_image_url,
body: chat.lastMessage.content,
type: 'chatMention',
}
if (chat.lastMessage.attachment?.type === 'image') {
opts.image = chat.lastMessage.attachment.preview_url
}
showDesktopNotification(window.vuex.state, opts)
showDesktopNotification(opts)
}
export const buildFakeMessage = ({

View file

@ -2,7 +2,7 @@ import { map } from 'lodash'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useTimelinesStore } from 'src/stores/timelines.js'
import { useUsersStore } from 'src/stores/users.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import {
editStatus as apiEditStatus,
@ -28,7 +28,7 @@ const postStatus = ({
const mediaIds = map(media, 'id')
return apiPostStatus({
credentials: useUsersStore().currentUser.credentials,
credentials: useOAuthStore().token,
status,
spoilerText,
visibility,
@ -71,7 +71,7 @@ const editStatus = ({
return apiEditStatus({
id: statusId,
credentials: useUsersStore().currentUser.credentials,
credentials: useOAuthStore().token,
status,
spoilerText,
sensitive,
@ -101,12 +101,12 @@ const editStatus = ({
}
const uploadMedia = ({ store, formData }) => {
const credentials = useUsersStore().currentUser.credentials
const credentials = useOAuthStore().token
return apiUploadMedia({ credentials, formData }).then(({ data }) => data)
}
const setMediaDescription = ({ store, id, description }) => {
const credentials = useUsersStore().currentUser.credentials
const credentials = useOAuthStore().token
return apiSetMediaDescription({ credentials, id, description }).then(
({ data }) => data,
)

View file

@ -37,7 +37,10 @@ export const useChatsStore = defineStore('chats', {
actions: {
attachSocket() {
const et = new EventTarget()
const socket = { et }
const socket = {
name: 'chats',
et,
}
et.addEventListener('pleroma:chat_update', this.updateChat)
@ -83,9 +86,6 @@ export const useChatsStore = defineStore('chats', {
chat.lastMessage = updatedChat.lastMessage
chat.unread = updatedChat.unread
chat.updated_at = updatedChat.updated_at
if (isNewMessage && chat.unread) {
maybeShowChatNotification(chat)
}
} else {
this.chatList.data.push(updatedChat)
this.chatList.idStore[updatedChat.id] = updatedChat
@ -98,16 +98,16 @@ export const useChatsStore = defineStore('chats', {
chat.unread = 0
}
},
updateChat({ chat: updatedChat }) {
updateChat({ data: { chatUpdate: updatedChat } }) {
const chat = getChatById(this, updatedChat.id)
if (chat) {
chat.lastMessage = updatedChat.lastMessage
chat.unread = updatedChat.unread
chat.updated_at = updatedChat.updated_at
}
if (!chat) {
} else {
this.chatList.data.unshift(updatedChat)
}
maybeShowChatNotification(chat)
this.chatList.idStore[updatedChat.id] = updatedChat
},
deleteChat(id) {

View file

@ -14,6 +14,7 @@ import {
import { useInterfaceStore } from 'src/stores/interface.js'
import { useUsersStore } from 'src/stores/users.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { fetchKnownDomains } from 'src/api/public.js'
@ -213,7 +214,7 @@ export const useInstanceStore = defineStore('instance', {
async getKnownDomains() {
try {
const { data } = await fetchKnownDomains({
credentials: useUsersStore().currentUser.credentials,
credentials: useOAuthStore().token
})
this.knownDomains = data
} catch (e) {

View file

@ -96,7 +96,10 @@ export const useInterfaceStore = defineStore('interface', {
actions: {
attachSocket() {
const et = new EventTarget()
const socket = { et }
const socket = {
name: 'interface',
et,
}
et.addEventListener('open', this.onStreamConnect)
et.addEventListener('close', this.onStreamDisconnect)
@ -854,7 +857,7 @@ export const useInterfaceStore = defineStore('interface', {
},
unregisterPushNotifications() {
const token = this.currentUser.credentials
const token = useOAuthStore().token
unregisterPushNotifications(token)
},

View file

@ -31,7 +31,9 @@ export const defaultState = () => ({
statusIdStore: new Set(),
socket: null,
streaming: false,
fetching: true,
fetcher: null,
paused: false,
})
export const useNotificationsStore = defineStore('notifications', {
@ -40,14 +42,29 @@ export const useNotificationsStore = defineStore('notifications', {
// Init
attachSocket() {
const et = new EventTarget()
const socket = { et }
const socket = {
name: 'notifications',
et,
}
et.addEventListener('notification', this.addNewNotifications)
et.addEventListener('open', this.onStreamConnect)
et.addEventListener('close', this.onStreamDisconnect)
useStreamingStore().addSubscriber(socket)
this.socket = socket
useStreamingStore().addSubscriber(this.socket)
},
pause() {
this.paused = true
if (this.fetcher && this.fetching) {
this.stopFetching('Notifications paused')
}
},
resume() {
this.paused = false
if (this.fetcher && this.fetching) {
this.startFetching('Notifications resumed')
}
},
activate() {
this.attachSocket()
@ -62,7 +79,7 @@ export const useNotificationsStore = defineStore('notifications', {
this.startFetching('Notifications activated')
},
deactivate() {
if (!this.streaming) {
if (this.fetching) {
this.stopFetching('Notifications deactivated')
}
@ -77,6 +94,7 @@ export const useNotificationsStore = defineStore('notifications', {
Object.keys(blankState).forEach((k) => {
this[k] = blankState[k]
})
console.log('[Notifications] Deactivated', this.fetcher)
},
// Poll & Push
@ -91,20 +109,30 @@ export const useNotificationsStore = defineStore('notifications', {
this.startFetching('Socket disconnected')
},
startFetching(reason) {
if (this.paused) {
console.debug(
'[Notificatiosn] NOT Starting notifications fetcher because it is paused',
'Original Reason:',
reason,
)
return
}
console.debug(
'[Notifications] Starting fetching notifications',
'[Notifications] Starting notifications fetcher',
'Reason:',
reason,
)
this.fetcher.startFetching()
this.fetching = true
},
stopFetching(reason) {
this.fetcher.stopFetching()
this.fetching = false
console.debug(
'[Notifications] Stopped fetching notifications',
'[Notifications] Stopped notifications fetcher',
'Reason:',
reason,
)
this.fetcher.stopFetching()
},
// Updates
@ -223,7 +251,7 @@ export const useNotificationsStore = defineStore('notifications', {
case 'follow_request':
break
default:
this.markSingleNotificationAsSeen({ id })
this.markSingleNotificationAsSeen(id)
}
}
},

View file

@ -19,7 +19,7 @@ export const useSearchStore = defineStore('search', {
credentials: useOAuthStore().token,
})
const { accounts, statuses } = data
const { accounts, statuses, hashtags } = data
useUsersStore().addNewUsers({
...rest,
@ -36,6 +36,7 @@ export const useSearchStore = defineStore('search', {
useStatusesStore().allStatuses.get(s.id),
)
output.accounts = accounts.map((s) => useUsersStore().findUser(s.id))
output.hashtags = hashtags ?? []
return output
},

View file

@ -49,6 +49,7 @@ export const useStatusesStore = defineStore('statuses', {
data.forEach((id) => this.setDeleted(id))
const socket = {
name: 'statuses',
et,
handlers: {
handleUpdate,
@ -64,15 +65,9 @@ export const useStatusesStore = defineStore('statuses', {
this.socket = socket
},
resetStatuses() {
this.socket.et.removeEventListener('update', this.socket.handleUpdate)
this.socket.et.removeEventListener(
'status.update',
this.socket.handleUpdate,
)
this.socket.et.removeEventListener('delete', this.socket.handleDelete)
const emptyState = defaultState()
Object.entries(emptyState).forEach(([key, value]) => {
if (key === 'socket') return
this[key] = value
})
},
@ -154,7 +149,7 @@ export const useStatusesStore = defineStore('statuses', {
const newStatus = {
...old,
...neu,
...Object.fromEntries(Object.entries(neu).filter(([, v]) => v !== undefined)),
user,
}

View file

@ -131,6 +131,7 @@ export const useStreamingStore = defineStore('streaming', {
},
getSubArgs(stream) {
if (stream === undefined) return undefined
const argumentKey = ARGUMENT_MAP[stream.name]
const args = argumentKey
? {

View file

@ -16,9 +16,11 @@ const emptyTl = (name, argument = null) => {
maxId: '',
minId: '',
streaming: false,
fetching: false,
reloadNeeded: false,
fetcher: null,
socket: null,
paused: false,
}
const property = ARGUMENT_MAP[name]
@ -105,16 +107,16 @@ export const useTimelinesStore = defineStore('timelines', {
const openHandler = () => this.onStreamConnect(timelineName, argument)
const closeHandler = () =>
this.onStreamDisconnect(timelineName, argument)
const messageHandler =
() =>
({ detail: message }) =>
this.onStreamMessage(timelineName, argument, message)
const messageHandler = (message) => {
this.onStreamMessage(timelineName, argument, message)
}
et.addEventListener('open', openHandler)
et.addEventListener('close', closeHandler)
et.addEventListener('update', messageHandler)
timeline.socket = {
name: 'timelines',
stream: {
name: streamName,
argument,
@ -133,7 +135,7 @@ export const useTimelinesStore = defineStore('timelines', {
deactivate(timelineName, persistent) {
const timeline = this[timelineName]
if (timeline.persistent && !persistent) return
if (!timeline.streaming) {
if (timeline.fetching) {
this.stopFetchingTimeline(timelineName, 'Timeline deactivation')
}
@ -177,6 +179,48 @@ export const useTimelinesStore = defineStore('timelines', {
})
},
// Pause
pause(name) {
const timeline = this[name]
timeline.paused = true
console.debug(
'[Timelines] Pausing timeline',
name,
)
if (timeline.fetcher && timeline.fetching) {
timeline.fetcher.stopFetching()
}
},
resume(name) {
const timeline = this[name]
timeline.paused = false
console.debug(
'[Timelines] Resuming timeline',
name,
)
if (timeline.fetcher && timeline.fetching) {
timeline.fetcher.startFetching()
}
},
pauseAll() {
TIMELINES.forEach((name) => {
try {
this.pause(name)
} catch (e) {
console.error(`[Timelines] Failed to pause timeline ${name}:`, e)
}
})
},
resumeAll() {
TIMELINES.forEach((name) => {
try {
this.resume(name)
} catch (e) {
console.error(`[Timelines] Failed to pause timeline ${name}:`, e)
}
})
},
// Update stuff
addStatusesToTimeline(
timelineName,
@ -231,7 +275,7 @@ export const useTimelinesStore = defineStore('timelines', {
},
onStreamMessage(timeline, argument, event) {
this.addStatusesToTimeline(timeline, argument, {
statuses: [event.data.status.id],
statuses: event.data.map(({ id }) => id),
})
},
@ -247,15 +291,28 @@ export const useTimelinesStore = defineStore('timelines', {
this.startFetchingTimeline(timeline, argument, 'Socket disconnected')
},
startFetchingTimeline(timelineName, argument, reason) {
const timeline = this[timelineName]
console.log('[Timelines]', toValue(timeline))
if (timeline.paused) {
console.debug(
'[Timelines] NOT Starting timeline fetcher because it is paused',
timelineName,
argument,
'Original Reason:',
reason,
)
return
}
console.debug(
'[Timelines] Starting fetching timeline',
'[Timelines] Starting timeline fetcher',
timelineName,
argument,
'Reason:',
reason,
)
const timeline = this[timelineName]
timeline.fetcher.startFetching()
timeline.fetching = true
},
stopFetchingTimeline(timelineName, reason) {
const timeline = this[timelineName]
@ -275,6 +332,7 @@ export const useTimelinesStore = defineStore('timelines', {
'Reason:',
reason,
)
timeline.fetching = false
}
},

View file

@ -2,6 +2,8 @@ import Cookies from 'js-cookie'
import { last } from 'lodash'
import { defineStore } from 'pinia'
import { WSConnectionStatus } from 'src/api/websocket.js'
import { useAnnouncementsStore } from 'src/stores/announcements.js'
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
import { useChatsStore } from 'src/stores/chats.js'
@ -615,12 +617,15 @@ export const useUsersStore = defineStore('users', {
user.domainMutes = new Set()
this.lastLoginName = user.screen_name
this.currentUser = user
useTimelinesStore().deactivateAll()
useStatusesStore().resetStatuses()
this.users = new Map()
this.usersByName = new Map()
this.usersByURL = new Map()
this.relationships = new Map()
this.currentUser = user
this.addNewUsers({ data: user, ...rest })
useInterfaceStore().onLogin()
useSyncConfigStore()
@ -634,7 +639,6 @@ export const useUsersStore = defineStore('users', {
})
useUserHighlightStore().initUserHighlight(user)
this.addNewUsers({ data: user, ...rest })
useEmojiStore().fetchEmoji()
@ -660,8 +664,8 @@ export const useUsersStore = defineStore('users', {
}
// DMs and Home
useNotificationsStore().activate()
useTimelinesStore().activatePersistents()
useNotificationsStore().activate()
if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) {
// Start fetching chats
@ -708,6 +712,16 @@ export const useUsersStore = defineStore('users', {
const store = window.vuex
const oauth = useOAuthStore()
// Pause fetching
useNotificationsStore().pause()
useTimelinesStore().pauseAll()
// Pause-less stores
useAnnouncementsStore().stopFetching()
useListsStore().stopFetching()
useBookmarkFoldersStore().stopFetching()
store?.dispatch('stopFetchingFollowRequests')
// NOTE: No need to verify the app still exists, because if it doesn't,
// the token will be invalid too
return oauth
@ -722,28 +736,53 @@ export const useUsersStore = defineStore('users', {
return revokeToken(params)
})
.then(() => {
oauth.clearToken()
this.currentUser = null
this.lastLoginName = null
useNotificationsStore().deactivate()
// Full reset on logout success
useTimelinesStore().deactivateAll()
useStatusesStore().resetStatuses()
useChatsStore().resetChats()
this.users = new Map()
this.usersByName = new Map()
this.usersByURL = new Map()
this.relationships = new Map()
useNotificationsStore().deactivate()
useAnnouncementsStore().stopFetching()
useListsStore().stopFetching()
useBookmarkFoldersStore().stopFetching()
store?.dispatch('stopFetchingFollowRequests')
useTimelinesStore().deactivateAll()
useStatusesStore().resetStatuses()
if (useMergedConfigStore().mergedConfig.useStreamingApi) {
// Socket is most likely already closed by server
if (
useMergedConfigStore().mergedConfig.useStreamingApi
&& useStreamingStore().state !== WSConnectionStatus.CLOSED
) {
useStreamingStore().stopSocket()
}
useChatsStore().resetChats()
oauth.clearToken()
Cookies.remove('__Host-pleroma_key', { path: '/' })
useInterfaceStore().onLogout()
})
.catch((e) => {
useInterfaceStore().pushGlobalNotice({
messageKey: 'user.logout_failure',
messageArgs: {
error: e,
},
level: 'error',
})
console.error('Logout error!', e)
useAnnouncementsStore().startFetching()
useListsStore().startFetching()
useBookmarkFoldersStore().startFetching()
store?.dispatch('startFetchingFollowRequests')
})
.finally(() => {
useNotificationsStore().resume()
useTimelinesStore().resumeAll()
})
},
},
persist: {

View file

@ -354,13 +354,11 @@ describe('RichContent', () => {
'<span class="MentionLink mention-link">',
'<a href="lol" class="original" target="_blank">',
'<span>',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</span>',
'</a>',
'</span>',
'</span>',
@ -420,25 +418,21 @@ describe('RichContent', () => {
'<span class="MentionLink mention-link">',
'<a href="lol" class="original" target="_blank">',
'<span>',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</span>',
'</a>',
'</span>',
'<span class="MentionLink mention-link">',
'<a href="lol" class="original" target="_blank">',
'<span>',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</span>',
'</a>',
'</span>',
'</span>',