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

This commit is contained in:
Henry Jameson 2026-05-21 14:56:31 +03:00
commit 9f09d91ec7
10 changed files with 219 additions and 217 deletions

View file

@ -12,6 +12,7 @@ import SharedComputedObject from '../helpers/shared_computed_object.js'
import StringSetting from '../helpers/string_setting.vue' import StringSetting from '../helpers/string_setting.vue'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useEmojiStore } from 'src/stores/emoji.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@ -174,51 +175,9 @@ const EmojiTab = {
this.sortPackFiles(packName) this.sortPackFiles(packName)
}, },
loadPacksPaginated(listFunction) {
const pageSize = 25
const allPacks = {}
return listFunction({
instance: this.remotePackInstance,
page: 1,
pageSize: 0,
})
.then((data) => data.json())
.then((data) => {
if (data.error !== undefined) {
return Promise.reject(data.error)
}
let resultingPromise = Promise.resolve({})
for (let i = 0; i < Math.ceil(data.count / pageSize); i++) {
resultingPromise = resultingPromise
.then(() =>
listFunction({
instance: this.remotePackInstance,
page: i,
pageSize,
}),
)
.then((data) => data.json())
.then((pageData) => {
if (pageData.error !== undefined) {
return Promise.reject(pageData.error)
}
assign(allPacks, pageData.packs)
})
}
return resultingPromise
})
.then(() => allPacks)
.catch((data) => {
this.displayError(data)
})
},
refreshPackList() { refreshPackList() {
this.loadPacksPaginated( useEmojiStore().getAdminPacks(
this.remotePackInstance,
this.$store.state.api.backendInteractor.listEmojiPacks, this.$store.state.api.backendInteractor.listEmojiPacks,
).then((allPacks) => { ).then((allPacks) => {
this.knownLocalPacks = allPacks this.knownLocalPacks = allPacks
@ -228,7 +187,8 @@ const EmojiTab = {
}) })
}, },
listRemotePacks() { listRemotePacks() {
this.loadPacksPaginated( useEmojiStore().getAdminPacks(
this.remotePackInstance,
this.$store.state.api.backendInteractor.listRemoteEmojiPacks, this.$store.state.api.backendInteractor.listRemoteEmojiPacks,
) )
.then((allPacks) => { .then((allPacks) => {

View file

@ -31,10 +31,10 @@
} }
.pin { .pin {
padding: var(--status-margin) var(--status-margin) 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
margin-right: 0.5em;
} }
._misclick-prevention & { ._misclick-prevention & {
@ -229,7 +229,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0.4em var(--status-margin); padding: 0.4em var(--status-margin);
border-bottom: 1px dotted var(--border);
.repeater-avatar { .repeater-avatar {
flex: 0 0 1.5em; flex: 0 0 1.5em;

View file

@ -46,16 +46,6 @@
</div> </div>
</template> </template>
<template v-else> <template v-else>
<div
v-if="showPinned"
class="pin"
>
<FAIcon
icon="thumbtack"
class="faint"
/>
<span class="faint">{{ $t('status.pinned') }}</span>
</div>
<div <div
v-if="retweet && !noHeading && !inConversation" v-if="retweet && !noHeading && !inConversation"
:class="[repeaterClass, { highlighted: repeaterStyle }]" :class="[repeaterClass, { highlighted: repeaterStyle }]"
@ -179,6 +169,13 @@
</div> </div>
<span class="heading-right"> <span class="heading-right">
<span class="pin" v-if="showPinned">
<FAIcon
icon="thumbtack"
class="faint"
/>
<span class="faint">{{ $t('status.pinned') }}</span>
</span>
<router-link <router-link
class="timeago faint" class="timeago faint"
:to="{ name: 'conversation', params: { id: status.id } }" :to="{ name: 'conversation', params: { id: status.id } }"

View file

@ -0,0 +1,72 @@
import Popover from 'components/popover/popover.vue'
import SelectComponent from 'components/select/select.vue'
import StillImage from './still-image.vue'
import { mapState } from 'pinia'
import { useInterfaceStore } from 'src/stores/interface'
import { useEmojiStore } from 'src/stores/emoji'
export default {
components: { StillImage, Popover, SelectComponent },
props: {
shortcode: {
type: String,
required: true,
},
isLocal: {
type: Boolean,
required: true,
},
},
data() {
return {
packName: '',
}
},
computed: {
isUserAdmin() {
return this.$store.state.users.currentUser?.rights.admin
},
...mapState(useEmojiStore, ['adminPacksLocal', 'adminPacksLocalLoading'])
},
methods: {
displayError(msg) {
useInterfaceStore().pushGlobalNotice({
messageKey: 'admin_dash.emoji.error',
messageArgs: [msg],
level: 'error',
})
},
copyToLocalPack() {
this.$store.state.api.backendInteractor
.addNewEmojiFile({
packName: this.packName,
file: this.$attrs.src,
shortcode: this.shortcode,
filename: '',
})
.then((resp) => resp.json())
.then((resp) => {
if (resp.error !== undefined) {
this.displayError(resp.error)
return
}
useInterfaceStore().pushGlobalNotice({
messageKey: 'admin_dash.emoji.copied_successfully',
messageArgs: [this.shortcode, this.packName],
level: 'success',
})
this.$refs.emojiPopover.hidePopover()
this.packName = ''
})
},
fetchEmojiPacksIfAdmin() {
useEmojiStore().getAdminPacksLocal().then(() => {
this.$refs.emojiPopover.updateStyles()
})
},
},
}

View file

@ -21,169 +21,53 @@
/> />
</div> </div>
<div v-if="isUserAdmin && !isLocal"> <template v-if="isUserAdmin && !isLocal">
<button <span
class="button button-default btn emoji-popover-button" v-if="adminPacksLocalLoading"
type="button" class="loading-spinner"
:disabled="packName == ''"
@click="copyToLocalPack"
> >
{{ $t('admin_dash.emoji.copy_to_pack') }} <FAIcon
</button> class="fa-old-padding"
spin
icon="circle-notch"
/>
</span>
<div v-else>
<button
class="button button-default btn emoji-popover-button"
type="button"
:disabled="packName == ''"
@click="copyToLocalPack"
>
{{ $t('admin_dash.emoji.copy_to_pack') }}
</button>
<SelectComponent <SelectComponent
v-model="packName" v-model="packName"
>
<option
value=""
disabled
hidden
> >
{{ $t('admin_dash.emoji.emoji_pack') }} <option
</option> value=""
<option disabled
v-for="(pack, listPackName) in knownLocalPacks" hidden
:key="listPackName" >
:label="listPackName" {{ $t('admin_dash.emoji.emoji_pack') }}
> </option>
{{ listPackName }} <option
</option> v-for="(pack, listPackName) in adminPacksLocal"
</SelectComponent> :key="listPackName"
</div> :label="listPackName"
>
{{ listPackName }}
</option>
</SelectComponent>
</div>
</template>
</div> </div>
</template> </template>
</Popover> </Popover>
</template> </template>
<script> <script src="./still-image-emoji-popover" />
import Popover from 'components/popover/popover.vue'
import SelectComponent from 'components/select/select.vue'
import { assign } from 'lodash'
import StillImage from './still-image.vue'
import { useInstanceStore } from 'src/stores/instance.js'
import { useInterfaceStore } from 'src/stores/interface'
export default {
components: { StillImage, Popover, SelectComponent },
props: {
shortcode: {
type: String,
required: true,
},
isLocal: {
type: Boolean,
required: true,
},
},
data() {
return {
knownLocalPacks: {},
packName: '',
}
},
computed: {
isUserAdmin() {
return this.$store.state.users.currentUser.rights.admin
},
},
methods: {
displayError(msg) {
useInterfaceStore().pushGlobalNotice({
messageKey: 'admin_dash.emoji.error',
messageArgs: [msg],
level: 'error',
})
},
copyToLocalPack() {
this.$store.state.api.backendInteractor
.addNewEmojiFile({
packName: this.packName,
file: this.$attrs.src,
shortcode: this.shortcode,
filename: '',
})
.then((resp) => resp.json())
.then((resp) => {
if (resp.error !== undefined) {
this.displayError(resp.error)
return
}
useInterfaceStore().pushGlobalNotice({
messageKey: 'admin_dash.emoji.copied_successfully',
messageArgs: [this.shortcode, this.packName],
level: 'success',
})
this.$refs.emojiPopover.hidePopover()
this.packName = ''
})
},
// Copied from emoji_tab.js
loadPacksPaginated(listFunction) {
const pageSize = 25
const allPacks = {}
return listFunction({
instance: useInstanceStore().server,
page: 1,
pageSize: 0,
})
.then((data) => data.json())
.then((data) => {
if (data.error !== undefined) {
return Promise.reject(data.error)
}
let resultingPromise = Promise.resolve({})
for (let i = 0; i < Math.ceil(data.count / pageSize); i++) {
resultingPromise = resultingPromise
.then(() =>
listFunction({
instance: useInstanceStore().server,
page: i,
pageSize,
}),
)
.then((data) => data.json())
.then((pageData) => {
if (pageData.error !== undefined) {
return Promise.reject(pageData.error)
}
assign(allPacks, pageData.packs)
})
}
return resultingPromise
})
.then(() => allPacks)
.catch((data) => {
this.displayError(data)
})
},
fetchEmojiPacksIfAdmin() {
if (!this.isUserAdmin) return
this.loadPacksPaginated(
this.$store.state.api.backendInteractor.listEmojiPacks,
).then((allPacks) => {
// Sort by key
const sorted = Object.keys(allPacks)
.sort()
.reduce((acc, key) => {
if (key.length === 0) return acc
acc[key] = allPacks[key]
return acc
}, {})
this.knownLocalPacks = sorted
})
},
},
}
</script>
<style> <style>
.emoji-popover { .emoji-popover {
@ -191,8 +75,8 @@ export default {
text-align: center; text-align: center;
.emoji { .emoji {
width: 4.6em; width: calc(var(--emoji-size) * 3);
height: 4.6em; height: calc(var(--emoji-size) * 3);
} }
.emoji-popover-centered { .emoji-popover-centered {

View file

@ -86,7 +86,7 @@
padding: 0.6em; padding: 0.6em;
&:not(:last-child) { &:not(:last-child) {
border-bottom: 1px solid var(--border); border-bottom: 1px dotted var(--border);
} }
.highlighter { .highlighter {

View file

@ -4,6 +4,9 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue' import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useInstanceStore } from 'src/stores/instance.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
@ -29,6 +32,15 @@ const UserListPopover = {
return useMergedConfigStore().mergedConfig.nonSquareEmoji return useMergedConfigStore().mergedConfig.nonSquareEmoji
}, },
}, },
methods: {
generateProfileLink(user) {
return generateProfileLink(
user.id,
user.screen_name,
useInstanceStore().restrictedNicknames,
)
},
}
} }
export default UserListPopover export default UserListPopover

View file

@ -10,8 +10,9 @@
<template #content> <template #content>
<div class="user-list-popover"> <div class="user-list-popover">
<template v-if="users.length"> <template v-if="users.length">
<div <router-link
v-for="(user) in usersCapped" v-for="(user) in usersCapped"
:to="generateProfileLink(user)"
:key="user.id" :key="user.id"
class="user-list-row" class="user-list-row"
> >
@ -32,7 +33,7 @@
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
<span class="user-list-screen-name">{{ user.screen_name_ui }}</span><UnicodeDomainIndicator :user="user" /> <span class="user-list-screen-name">{{ user.screen_name_ui }}</span><UnicodeDomainIndicator :user="user" />
</div> </div>
</div> </router-link>
</template> </template>
<template v-else> <template v-else>
<FAIcon <FAIcon
@ -58,6 +59,7 @@
padding: 0.25em; padding: 0.25em;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
color: var(--text);
.user-list-names { .user-list-names {
display: flex; display: flex;

View file

@ -2089,6 +2089,7 @@ const listEmojiPacks = ({ page, pageSize }) => {
} }
const listRemoteEmojiPacks = ({ instance, page, pageSize }) => { const listRemoteEmojiPacks = ({ instance, page, pageSize }) => {
console.log(instance)
if (!instance.startsWith('http')) { if (!instance.startsWith('http')) {
instance = 'https://' + instance instance = 'https://' + instance
} }

View file

@ -1,5 +1,6 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { merge } from 'lodash'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { ensureFinalFallback } from 'src/i18n/languages.js' import { ensureFinalFallback } from 'src/i18n/languages.js'
@ -10,6 +11,8 @@ const defaultState = {
// Custom emoji from server // Custom emoji from server
customEmoji: [], customEmoji: [],
customEmojiFetched: false, customEmojiFetched: false,
adminPacksLocal: null,
adminPacksLocalLoading: true,
// Unicode emoji from bundle // Unicode emoji from bundle
emoji: {}, emoji: {},
@ -178,6 +181,78 @@ export const useEmojiStore = defineStore('emoji', {
) )
}, },
async getAdminPacksLocal(refresh) {
if (!refresh && this.adminPacksLocal) return this.adminPacksLocal
const backendInteractor = window.vuex.state.api.backendInteractor
const listFunction = backendInteractor.listEmojiPacks
this.adminPacksLocalLoading = true
this.adminPacksLocal = await this.getAdminPacks(useInstanceStore().server, listFunction)
this.adminPacksLocalLoading = false
},
async getAdminPacks(instance, listFunction) {
const currentUser = window.vuex.state.users.currentUser
if (!currentUser.rights.admin) return
const pageSize = 25
const allPacks = {}
return await listFunction({
instance,
page: 1,
pageSize: 0,
})
.then((data) => data.json())
.then((data) => {
if (data.error !== undefined) {
return Promise.reject(data.error)
}
const promises = []
for (let i = 0; i < Math.ceil(data.count / pageSize); i++) {
promises.push(
listFunction({
instance,
page: i,
pageSize,
})
.then((data) => data.json())
.then((pageData) => {
if (pageData.error !== undefined) {
return Promise.reject(pageData.error)
}
return pageData.packs
})
)
}
return Promise
.all(promises)
.then((results) => {
return merge({}, ...results)
})
})
.then((allPacks) => {
// Sort by key
return Object
.keys(allPacks)
.sort()
.reduce((acc, key) => {
if (key.length === 0) return acc
acc[key] = allPacks[key]
return acc
}, {})
})
.catch((data) => {
this.displayError(data)
})
},
async getCustomEmoji() { async getCustomEmoji() {
try { try {
let res = await window.fetch('/api/v1/pleroma/emoji') let res = await window.fetch('/api/v1/pleroma/emoji')