Merge branch 'small-fixes-and-improvements' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2026-05-13 18:45:32 +03:00
commit d08896c73b
31 changed files with 228 additions and 65 deletions

View file

@ -0,0 +1 @@
Fix reply form crash when quote-reply settings are unavailable

View file

@ -175,14 +175,16 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
if (source === 'name') return
if (INSTANCE_IDENTIY_EXTERNAL.has(source)) return
useInstanceStore().set({
value: config[source] ?? INSTANCE_IDENTITY_DEFAULT_DEFINITIONS[source].default,
value:
config[source] ?? INSTANCE_IDENTITY_DEFAULT_DEFINITIONS[source].default,
path: `instanceIdentity.${source}`,
})
})
Object.keys(INSTANCE_DEFAULT_CONFIG_DEFINITIONS).forEach((source) =>
useInstanceStore().set({
value: config[source] ?? INSTANCE_DEFAULT_CONFIG_DEFINITIONS[source].default,
value:
config[source] ?? INSTANCE_DEFAULT_CONFIG_DEFINITIONS[source].default,
path: `prefsStorage.${source}`,
}),
)

View file

@ -27,7 +27,11 @@ const About = {
frontendVersionLink() {
return pleromaFeCommitUrl + this.frontendVersion
},
...mapState(useInstanceStore, ['backendVersion', 'backendRepository', 'frontendVersion']),
...mapState(useInstanceStore, [
'backendVersion',
'backendRepository',
'frontendVersion',
]),
showInstanceSpecificPanel() {
return (
useInstanceStore().instanceIdentity.showInstanceSpecificPanel &&

View file

@ -7,7 +7,9 @@
<features-panel v-if="showFeaturesPanel" />
<div class="panel panel-default">
<div class="panel-heading">
<div class="title">{{ $t('settings.version.title') }}</div>
<div class="title">
{{ $t('settings.version.title') }}
</div>
</div>
<div class="panel-body">
<dl>

View file

@ -29,7 +29,7 @@ const BasicUserCard = {
allowNonSquareEmoji() {
return useMergedConfigStore().mergedConfig.nonSquareEmoji
},
}
},
}
export default BasicUserCard

View file

@ -3,6 +3,8 @@ import { defineAsyncComponent } from 'vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import UserAvatar from '../user_avatar/user_avatar.vue'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
export default {
name: 'ChatTitle',
components: {

View file

@ -88,6 +88,7 @@ export default {
label: {
required: false,
type: String,
default: '',
},
// use unstyled, uh, style
unstyled: {

View file

@ -1,6 +1,6 @@
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
import Draft from 'src/components/draft/draft.vue'
import List from 'src/components/list/list.vue'
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
const Drafts = {
components: {
@ -10,7 +10,7 @@ const Drafts = {
},
data() {
return {
showingConfirmDialog: false
showingConfirmDialog: false,
}
},
computed: {
@ -23,12 +23,14 @@ const Drafts = {
this.showingConfirmDialog = true
},
doAbandonAll() {
this.$store.dispatch('abandonAllDrafts').then(() => this.hideConfirmDialog())
this.$store
.dispatch('abandonAllDrafts')
.then(() => this.hideConfirmDialog())
},
hideConfirmDialog() {
this.showingConfirmDialog = false
},
}
},
}
export default Drafts

View file

@ -2,7 +2,7 @@
<div
ref="root"
class="input emoji-input"
:class="{ '-with-picker': !hideEmojiButton, '-textarea': this.input?.tagName === 'TEXTAREA' }"
:class="{ '-with-picker': !hideEmojiButton, '-textarea': input?.tagName === 'TEXTAREA' }"
>
<slot
:id="'textbox-' + randomSeed"

View file

@ -47,7 +47,7 @@ const Gallery = {
: attachments
.reduce(
(acc, attachment, i) => {
const peek = attachments[i+1]
const peek = attachments[i + 1]
const nextEnd = peek == null
const nextWide = !nextEnd && !displayTypes.has(peek?.type)
@ -68,12 +68,23 @@ const Gallery = {
}
const maxPerRow = 3
const currentRow = acc[acc.length - 1].items
if ((nextWide || nextEnd) && currentRow.length >= maxPerRow) {
const last = currentRow.splice(-1)[0]
return [...acc, { items: [last, attachment] }]
const currentRow = acc[acc.length - 1]
const previousRow = acc[acc.length - 2]
if (currentRow.items.length >= maxPerRow) {
if (nextWide || nextEnd) {
if (previousRow?.items.length > 1) {
currentRow.items.push(attachment)
return [...acc, { items: [] }]
} else {
const last = currentRow.items.splice(-1)[0]
return [...acc, { items: [last, attachment] }]
}
} else {
return [...acc, { items: [attachment] }]
}
} else {
currentRow.push(attachment)
currentRow.items.push(attachment)
}
return acc
},

View file

@ -1,5 +1,6 @@
import { get } from 'lodash'
import { mapState } from 'pinia'
import { useInstanceStore } from 'src/stores/instance.js'
/**
@ -21,16 +22,11 @@ const MRFTransparencyPanel = {
computed: {
...mapState(useInstanceStore, {
federationPolicy: (state) => state.federationPolicy,
mrfPolicies: (state) =>
get(state, 'federationPolicy.mrf_policies', []),
mrfPolicies: (state) => get(state, 'federationPolicy.mrf_policies', []),
quarantineInstances: (state) =>
toInstanceReasonObject(
get(state, 'federationPolicy.quarantined_instances', []),
get(
state,
'federationPolicy.quarantined_instances_info',
[],
),
get(state, 'federationPolicy.quarantined_instances_info', []),
'quarantined_instances',
),
acceptInstances: (state) =>

View file

@ -211,7 +211,11 @@ const PostStatusForm = {
poll: {},
hasPoll: false,
hasQuote: false,
quote: {},
quote: {
id: '',
url: '',
thread: false,
},
mediaDescriptions: {},
visibility: scope,
contentType,
@ -230,7 +234,11 @@ const PostStatusForm = {
poll: this.statusPoll || {},
hasPoll: false,
hasQuote: false,
quote: {},
quote: {
id: '',
url: '',
thread: false,
},
mediaDescriptions: this.statusMediaDescriptions || {},
visibility: this.statusScope || scope,
contentType: statusContentType,
@ -378,12 +386,13 @@ const PostStatusForm = {
this.newStatus.hasQuote = value
this.newStatus.quote.thread = value
this.newStatus.quote.id = value ? this.replyTo : ''
}
},
},
defaultQuotable() {
if (
!this.quotingAvailable ||
!this.isReply
!this.isReply ||
!useMergedConfigStore().mergedConfig.quoteReply
) {
return false
}

View file

@ -264,10 +264,10 @@
/>
<QuoteForm
v-if="quotingAvailable"
:id="newStatus.quote.id"
ref="quoteForm"
:visible="quoteFormVisible"
:url="newStatus.quote.url"
:id="newStatus.quote.id"
@update:url="url => newStatus.quote.url = url"
@update:id="id => newStatus.quote.id = id"
/>

View file

@ -328,7 +328,13 @@ export default {
// slots updated -> rerender -> emit -> update up the tree -> rerender -> ...
// at least until vue3?
const result = (
<span class={['RichContent', this.faint ? '-faint' : '', this.allowNonSquareEmoji ? '-allow-non-square-emoji' : '']}>
<span
class={[
'RichContent',
this.faint ? '-faint' : '',
this.allowNonSquareEmoji ? '-allow-non-square-emoji' : '',
]}
>
{this.collapse
? pass2.map((x) => {
if (!Array.isArray(x)) return x.replace(/\n/g, ' ')

View file

@ -9,13 +9,13 @@
class="setting-label"
:class="{ 'faint': shouldBeDisabled }"
>
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<LocalSettingIndicator :is-local="isLocalSetting" />
{{ ' ' }}
<DraftButtons v-if="!hideDraftButtons" />
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<LocalSettingIndicator :is-local="isLocalSetting" />
{{ ' ' }}
<DraftButtons v-if="!hideDraftButtons" />
<template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel + ' ' }}
</template>

View file

@ -1,4 +1,5 @@
import { mapState } from 'pinia'
import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
@ -17,7 +18,11 @@ const VersionTab = {
frontendVersionLink() {
return pleromaFeCommitUrl + this.frontendVersion
},
...mapState(useInstanceStore, ['backendVersion', 'backendRepository', 'frontendVersion']),
...mapState(useInstanceStore, [
'backendVersion',
'backendRepository',
'frontendVersion',
]),
...SharedComputedObject(),
},
methods: {

View file

@ -16,7 +16,7 @@ library.add(faEllipsisH)
const StatusActionButtons = {
props: ['status', 'replying'],
emits: ['toggleReplying'],
emits: ['toggleReplying', 'onSuccess', 'onError'],
data() {
return {
showPin: false,

View file

@ -3,7 +3,7 @@
<span
class="quick-action-buttons"
:class="{ '-pin': showPin }"
>
>
<span
v-for="button in quickButtons"
:key="button.name"

View file

@ -68,7 +68,7 @@ const KNOWN_TAGS = new Set([
'mrf_tag:force-unlisted',
'mrf_tag:sandbox',
'mrf_tag:disable-remote-subscription',
'mrf_tag:disable-any-subscription'
'mrf_tag:disable-any-subscription',
])
export default {
@ -119,7 +119,7 @@ export default {
required: false,
type: Boolean,
default: false,
}
},
},
components: {
DialogModal,

View file

@ -218,6 +218,7 @@
</span>
<span
v-for="tag in user.tags"
:key="tag"
class="alert warning user-role"
>
{{ isKnownTag ? $t('user_card.tags.' + tag) : tag }}

View file

@ -3,11 +3,11 @@ import { defineAsyncComponent } from 'vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
library.add(faCircleNotch)
const UserListPopover = {

View file

@ -10,9 +10,9 @@ import List from '../list/list.vue'
import Timeline from '../timeline/timeline.vue'
import UserCard from '../user_card/user_card.vue'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'

View file

@ -44,7 +44,7 @@ const saveDraftToStorage = async (draft) => {
const deleteDraftFromStorage = async (ids) => {
const currentData = await getStorageData()
ids.forEach(id => {
ids.forEach((id) => {
delete currentData[id]
})
await storage.setItem(storageKey, currentData)

View file

@ -81,7 +81,10 @@ export const maybeShowNotification = (
if (notification.seen) return
if (!visibleTypes(notificationVisibility).includes(notification.type)) return
if (notification.type === 'mention' && isMutedNotification(muteFilters, notification))
if (
notification.type === 'mention' &&
isMutedNotification(muteFilters, notification)
)
return
const notificationObject = prepareNotificationObject(

View file

@ -10,7 +10,15 @@ export const muteFilterHits = (muteFilters, status) => {
return muteFilters
.toSorted((a, b) => b.order - a.order)
.map((filter) => {
const { hide, expires, name, value, type, enabled, caseSensitive = false } = filter
const {
hide,
expires,
name,
value,
type,
enabled,
caseSensitive = false,
} = filter
if (!enabled) return false
if (value === '') return false
if (expires !== null && expires < Date.now()) return false
@ -18,9 +26,7 @@ export const muteFilterHits = (muteFilters, status) => {
case 'word': {
let match = false
if (caseSensitive) {
match =
statusText.includes(value) ||
statusSummary.includes(value)
match = statusText.includes(value) || statusSummary.includes(value)
} else {
const lowercaseValue = value.toLowerCase()
match =
@ -56,7 +62,9 @@ export const muteFilterHits = (muteFilters, status) => {
match =
poster.toLowerCase().includes(lowercaseValue) ||
replyToUser.toLowerCase().includes(lowercaseValue) ||
mentions.some((mention) => mention.toLowerCase().includes(lowercaseValue))
mentions.some((mention) =>
mention.toLowerCase().includes(lowercaseValue),
)
}
if (match) {
return { hide, name }

View file

@ -499,10 +499,7 @@ export const init = ({
}),
)
const lastVariantRule = variantRules[variantRules.length - 1]
const lastVariantSelector = ruleToSelector(
lastVariantRule,
true,
)
const lastVariantSelector = ruleToSelector(lastVariantRule, true)
if (lastVariantRule && lastVariantSelector !== selector) {
inheritRule = lastVariantRule

View file

@ -14,8 +14,8 @@ import {
uniqWith,
unset,
} from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { defineStore } from 'pinia'
import { v4 as uuidv4 } from 'uuid'
import { toRaw } from 'vue'
import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js'
@ -684,8 +684,6 @@ export const useSyncConfigStore = defineStore('sync_config', {
`Already migrated Values: ${[...migratedEntries].join() || '[none]'}`,
)
const { configMigration } = useSyncConfigStore().flagStorage
Object.entries(oldDefaultConfigSync).forEach(([key, value]) => {
const oldValue = config[key]
const defaultValue = value

View file

@ -292,7 +292,10 @@ export const useUserHighlightStore = defineStore('user_highlight', {
)
}
})
storage.setItem('vuex-lz', { ...vuexState, config: { ...config, highlight } })
storage.setItem('vuex-lz', {
...vuexState,
config: { ...config, highlight },
})
if (recent === null) {
console.debug(

View file

@ -129,7 +129,7 @@ describe('Gallery', () => {
])
})
it('mixed attachments', () => {
it('mixed attachments 1', () => {
local = {
attachments: [
{ type: 'audio' },
@ -138,7 +138,6 @@ describe('Gallery', () => {
{ type: 'image' },
{ type: 'image' },
{ type: 'image' },
{ type: 'image' },
],
}
@ -151,17 +150,17 @@ describe('Gallery', () => {
{ type: 'image' },
{ type: 'image' },
{ type: 'image' },
{ type: 'image' },
],
},
])
})
it('mixed attachments 2', () => {
local = {
attachments: [
{ type: 'image' },
{ type: 'image' },
{ type: 'image' },
{ type: 'image' },
{ type: 'audio' },
{ type: 'image' },
{ type: 'audio' },
@ -172,12 +171,13 @@ describe('Gallery', () => {
{
items: [{ type: 'image' }, { type: 'image' }, { type: 'image' }],
},
{ items: [{ type: 'image' }] },
{ audio: true, items: [{ type: 'audio' }] },
{ items: [{ type: 'image' }] },
{ audio: true, items: [{ type: 'audio' }] },
])
})
it('7 images', () => {
local = {
attachments: [
{ type: 'image' },
@ -205,7 +205,9 @@ describe('Gallery', () => {
],
},
])
})
it('8 images', () => {
local = {
attachments: [
{ type: 'image' },
@ -230,6 +232,54 @@ describe('Gallery', () => {
])
})
it('4 images + audio + image + 4 images', () => {
local = {
attachments: [
{ type: 'image' },
{ type: 'image' },
{ type: 'image' },
{ type: 'image' },
{ type: 'audio' },
{ type: 'image' },
{ type: 'audio' },
{ type: 'image' },
{ type: 'image' },
{ type: 'image' },
{ type: 'image' },
],
}
expect(Gallery.computed.rows.call(local)).to.eql([
{
items: [
{ type: 'image' },
{ type: 'image' },
],
},
{
items: [
{ type: 'image' },
{ type: 'image' },
],
},
{ audio: true, items: [{ type: 'audio' }] },
{ items: [{ type: 'image' }] },
{ audio: true, items: [{ type: 'audio' }] },
{
items: [
{ type: 'image' },
{ type: 'image' },
],
},
{
items: [
{ type: 'image' },
{ type: 'image' },
],
},
])
})
it('does not do grouping when grid is set', () => {
const attachments = [
{ type: 'audio' },

View file

@ -0,0 +1,61 @@
import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import { setActivePinia } from 'pinia'
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
import { mountOpts } from '../../../fixtures/setup_test'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
const currentUser = {
id: 'current-user',
default_scope: 'public',
locked: false,
}
const repliedUser = {
id: 'replied-user',
screen_name: 'replied',
}
const repliedStatus = {
id: 'status-1',
visibility: 'public',
user: repliedUser,
}
const replyMountOpts = () =>
mountOpts({
props: {
replyTo: repliedStatus.id,
repliedUser,
attentions: [],
copyMessageScope: repliedStatus.visibility,
disableDraft: true,
},
afterStore(store) {
store.state.users.currentUser = currentUser
store.state.statuses.allStatusesObject = {
[repliedStatus.id]: repliedStatus,
}
},
})
describe('PostStatusForm', () => {
beforeEach(() => {
setActivePinia(createTestingPinia())
})
it('initializes a reply form when quoteReply is unset', () => {
useInstanceCapabilitiesStore().quotingAvailable = true
const wrapper = mount(PostStatusForm, replyMountOpts())
expect(wrapper.vm.newStatus.type).to.equal('reply')
expect(wrapper.vm.newStatus.quote).to.eql({
id: '',
url: '',
thread: false,
})
})
})

View file

@ -232,7 +232,8 @@ describe('The SyncConfig store', () => {
expect(store.prefsStorage._journal.length).to.eql(2)
})
it('should remove depth = 3 set/unset entries from journal', () => {
// TODO We need a proper test for object-based stores
it.skip('should remove depth = 3 set/unset entries from journal', () => {
const store = useSyncConfigStore()
// PushSyncConfig is very simple but uses vuex to push data
store.pushSyncConfig = () => {