commit: 943fee9fbc3768733417033191540773ac76c711
parent db33d58ba7a91da3461556a8a296991cad53afa8
Author: HJ <30-hj@users.noreply.git.pleroma.social>
Date: Tue, 5 Mar 2024 16:29:21 +0000
Merge branch 'admin-emoji-settings' into 'develop'
Admin emoji pack settings
See merge request pleroma/pleroma-fe!1886
Diffstat:
13 files changed, 994 insertions(+), 14 deletions(-)
diff --git a/changelog.d/admin-emoji-packs.add b/changelog.d/admin-emoji-packs.add
@@ -0,0 +1 @@
+Added emoji pack management to the admin panel
diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.js b/src/components/settings_modal/admin_tabs/emoji_tab.js
@@ -0,0 +1,257 @@
+import { clone, assign } from 'lodash'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
+import StringSetting from '../helpers/string_setting.vue'
+import Checkbox from 'components/checkbox/checkbox.vue'
+import StillImage from 'components/still-image/still-image.vue'
+import Select from 'components/select/select.vue'
+import Popover from 'components/popover/popover.vue'
+import ConfirmModal from 'components/confirm_modal/confirm_modal.vue'
+import ModifiedIndicator from '../helpers/modified_indicator.vue'
+import EmojiEditingPopover from '../helpers/emoji_editing_popover.vue'
+
+const EmojiTab = {
+ components: {
+ TabSwitcher,
+ StringSetting,
+ Checkbox,
+ StillImage,
+ Select,
+ Popover,
+ ConfirmModal,
+ ModifiedIndicator,
+ EmojiEditingPopover
+ },
+
+ data () {
+ return {
+ knownLocalPacks: { },
+ knownRemotePacks: { },
+ editedMetadata: { },
+ packName: '',
+ newPackName: '',
+ deleteModalVisible: false,
+ remotePackInstance: '',
+ remotePackDownloadAs: ''
+ }
+ },
+
+ provide () {
+ return { emojiAddr: this.emojiAddr }
+ },
+
+ computed: {
+ pack () {
+ return this.packName !== '' ? this.knownPacks[this.packName] : undefined
+ },
+ packMeta () {
+ if (this.editedMetadata[this.packName] === undefined) {
+ this.editedMetadata[this.packName] = clone(this.pack.pack)
+ }
+
+ return this.editedMetadata[this.packName]
+ },
+ knownPacks () {
+ // Copy the object itself but not the children, so they are still passed by reference and modified
+ const result = clone(this.knownLocalPacks)
+ for (const instName in this.knownRemotePacks) {
+ for (const instPack in this.knownRemotePacks[instName]) {
+ result[`${instPack}@${instName}`] = this.knownRemotePacks[instName][instPack]
+ }
+ }
+
+ return result
+ },
+ downloadWillReplaceLocal () {
+ return (this.remotePackDownloadAs.trim() === '' && this.pack.remote && this.pack.remote.baseName in this.knownLocalPacks) ||
+ (this.remotePackDownloadAs in this.knownLocalPacks)
+ }
+ },
+
+ methods: {
+ reloadEmoji () {
+ this.$store.state.api.backendInteractor.reloadEmoji()
+ },
+ importFromFS () {
+ this.$store.state.api.backendInteractor.importEmojiFromFS()
+ },
+ emojiAddr (name) {
+ if (this.pack.remote !== undefined) {
+ // Remote pack
+ return `${this.pack.remote.instance}/emoji/${encodeURIComponent(this.pack.remote.baseName)}/${name}`
+ } else {
+ return `${this.$store.state.instance.server}/emoji/${encodeURIComponent(this.packName)}/${name}`
+ }
+ },
+
+ createEmojiPack () {
+ this.$store.state.api.backendInteractor.createEmojiPack(
+ { name: this.newPackName }
+ ).then(resp => resp.json()).then(resp => {
+ if (resp === 'ok') {
+ return this.refreshPackList()
+ } else {
+ this.displayError(resp.error)
+ return Promise.reject(resp)
+ }
+ }).then(done => {
+ this.$refs.createPackPopover.hidePopover()
+
+ this.packName = this.newPackName
+ this.newPackName = ''
+ })
+ },
+ deleteEmojiPack () {
+ this.$store.state.api.backendInteractor.deleteEmojiPack(
+ { name: this.packName }
+ ).then(resp => resp.json()).then(resp => {
+ if (resp === 'ok') {
+ return this.refreshPackList()
+ } else {
+ this.displayError(resp.error)
+ return Promise.reject(resp)
+ }
+ }).then(done => {
+ delete this.editedMetadata[this.packName]
+
+ this.deleteModalVisible = false
+ this.packName = ''
+ })
+ },
+
+ metaEdited (prop) {
+ if (!this.pack) return
+
+ const def = this.pack.pack[prop] || ''
+ const edited = this.packMeta[prop] || ''
+ return edited !== def
+ },
+ savePackMetadata () {
+ this.$store.state.api.backendInteractor.saveEmojiPackMetadata({ name: this.packName, newData: this.packMeta }).then(
+ resp => resp.json()
+ ).then(resp => {
+ if (resp.error !== undefined) {
+ this.displayError(resp.error)
+ return
+ }
+
+ // Update actual pack data
+ this.pack.pack = resp
+ // Delete edited pack data, should auto-update itself
+ delete this.editedMetadata[this.packName]
+ })
+ },
+
+ updatePackFiles (newFiles) {
+ this.pack.files = newFiles
+ this.sortPackFiles(this.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(finished => allPacks)
+ .catch(data => {
+ this.displayError(data)
+ })
+ },
+
+ refreshPackList () {
+ this.loadPacksPaginated(this.$store.state.api.backendInteractor.listEmojiPacks)
+ .then(allPacks => {
+ this.knownLocalPacks = allPacks
+ for (const name of Object.keys(this.knownLocalPacks)) {
+ this.sortPackFiles(name)
+ }
+ })
+ },
+ listRemotePacks () {
+ this.loadPacksPaginated(this.$store.state.api.backendInteractor.listRemoteEmojiPacks)
+ .then(allPacks => {
+ let inst = this.remotePackInstance
+ if (!inst.startsWith('http')) { inst = 'https://' + inst }
+ const instUrl = new URL(inst)
+ inst = instUrl.host
+
+ for (const packName in allPacks) {
+ allPacks[packName].remote = {
+ baseName: packName,
+ instance: instUrl.origin
+ }
+ }
+
+ this.knownRemotePacks[inst] = allPacks
+ for (const pack in this.knownRemotePacks[inst]) {
+ this.sortPackFiles(`${pack}@${inst}`)
+ }
+
+ this.$refs.remotePackPopover.hidePopover()
+ })
+ .catch(data => {
+ this.displayError(data)
+ })
+ },
+ downloadRemotePack () {
+ if (this.remotePackDownloadAs.trim() === '') {
+ this.remotePackDownloadAs = this.pack.remote.baseName
+ }
+
+ this.$store.state.api.backendInteractor.downloadRemoteEmojiPack({
+ instance: this.pack.remote.instance, packName: this.pack.remote.baseName, as: this.remotePackDownloadAs
+ })
+ .then(data => data.json())
+ .then(resp => {
+ if (resp === 'ok') {
+ this.$refs.dlPackPopover.hidePopover()
+
+ return this.refreshPackList()
+ } else {
+ this.displayError(resp.error)
+ return Promise.reject(resp)
+ }
+ }).then(done => {
+ this.packName = this.remotePackDownloadAs
+ this.remotePackDownloadAs = ''
+ })
+ },
+ displayError (msg) {
+ this.$store.dispatch('pushGlobalNotice', {
+ messageKey: 'admin_dash.emoji.error',
+ messageArgs: [msg],
+ level: 'error'
+ })
+ },
+ sortPackFiles (nameOfPack) {
+ // Sort by key
+ const sorted = Object.keys(this.knownPacks[nameOfPack].files).sort().reduce((acc, key) => {
+ if (key.length === 0) return acc
+ acc[key] = this.knownPacks[nameOfPack].files[key]
+ return acc
+ }, {})
+ this.knownPacks[nameOfPack].files = sorted
+ }
+ },
+
+ mounted () {
+ this.refreshPackList()
+ }
+}
+
+export default EmojiTab
diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.scss b/src/components/settings_modal/admin_tabs/emoji_tab.scss
@@ -0,0 +1,61 @@
+@import "src/variables";
+
+.emoji-tab {
+ .btn-group .btn:not(:first-child) {
+ margin-left: 0.5em;
+ }
+
+ .pack-info-wrapper {
+ margin-top: 1em;
+ }
+
+ .emoji-info-input {
+ width: 100%;
+ }
+
+ .emoji-data-input {
+ width: 40%;
+ margin-left: 0.5em;
+ margin-right: 0.5em;
+ }
+
+ .emoji {
+ width: 32px;
+ height: 32px;
+ }
+
+ .emoji-unsaved {
+ box-shadow: 0 3px 5px var(--cBlue, $fallback--cBlue);
+ }
+
+ .emoji-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1em 1em;
+ }
+}
+
+.emoji-tab-popover-button:not(:first-child) {
+ margin-left: 0.5em;
+}
+
+.emoji-tab-popover-input {
+ margin-bottom: 0.5em;
+
+ label {
+ display: block;
+ margin-bottom: 0.5em;
+ }
+
+ input {
+ width: 20em;
+ }
+
+ .emoji-tab-popover-file {
+ padding-top: 3px;
+ }
+
+ .warning {
+ color: var(--cOrange, $fallback--cOrange);
+ }
+}
diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.vue b/src/components/settings_modal/admin_tabs/emoji_tab.vue
@@ -0,0 +1,278 @@
+<template>
+ <div
+ class="emoji-tab"
+ :label="$t('admin_dash.tabs.emoji')"
+ >
+ <div class="setting-item">
+ <h2>{{ $t('admin_dash.tabs.emoji') }}</h2>
+
+ <ul class="setting-list">
+ <h3>{{ $t('admin_dash.emoji.global_actions') }}</h3>
+
+ <li class="btn-group setting-item">
+ <button
+ class="button button-default btn"
+ type="button"
+ @click="reloadEmoji">
+ {{ $t('admin_dash.emoji.reload') }}
+ </button>
+ <button
+ class="button button-default btn"
+ type="button"
+ @click="importFromFS">
+ {{ $t('admin_dash.emoji.importFS') }}
+ </button>
+ </li>
+
+ <li class="btn-group setting-item">
+ <button
+ class="button button-default btn"
+ type="button"
+ @click="$refs.remotePackPopover.showPopover">
+ {{ $t('admin_dash.emoji.remote_packs') }}
+
+ <Popover
+ ref="remotePackPopover"
+ popover-class="emoji-tab-edit-popover popover-default"
+ trigger="click"
+ placement="bottom"
+ bound-to-selector=".emoji-tab"
+ :bound-to="{ x: 'container' }"
+ :offset="{ y: 5 }"
+ >
+ <template #content>
+ <div class="emoji-tab-popover-input">
+ <h3>{{ $t('admin_dash.emoji.remote_pack_instance') }}</h3>
+ <input v-model="remotePackInstance" :placeholder="$t('admin_dash.emoji.remote_pack_instance')">
+ <button
+ class="button button-default btn emoji-tab-popover-button"
+ type="button"
+ @click="listRemotePacks">
+ {{ $t('admin_dash.emoji.do_list') }}
+ </button>
+ </div>
+ </template>
+ </Popover>
+ </button>
+ </li>
+
+ <h3>{{ $t('admin_dash.emoji.emoji_packs') }}</h3>
+
+ <li>
+ <h4>{{ $t('admin_dash.emoji.edit_pack') }}</h4>
+
+ <Select class="form-control" v-model="packName">
+ <option value="" disabled hidden>{{ $t('admin_dash.emoji.emoji_pack') }}</option>
+ <option v-for="(pack, listPackName) in knownPacks" :label="listPackName" :key="listPackName">
+ {{ listPackName }}
+ </option>
+ </Select>
+
+ <button
+ class="button button-default btn emoji-tab-popover-button"
+ type="button"
+ @click="$refs.createPackPopover.showPopover">
+ {{ $t('admin_dash.emoji.create_pack') }}
+ </button>
+ <Popover
+ ref="createPackPopover"
+ popover-class="emoji-tab-edit-popover popover-default"
+ trigger="click"
+ placement="bottom"
+ bound-to-selector=".emoji-tab"
+ :bound-to="{ x: 'container' }"
+ :offset="{ y: 5 }"
+ >
+ <template #content>
+ <div class="emoji-tab-popover-input">
+ <h3>{{ $t('admin_dash.emoji.new_pack_name') }}</h3>
+ <input v-model="newPackName" :placeholder="$t('admin_dash.emoji.new_pack_name')">
+ <button
+ class="button button-default btn emoji-tab-popover-button"
+ type="button"
+ @click="createEmojiPack">
+ {{ $t('admin_dash.emoji.create') }}
+ </button>
+ </div>
+ </template>
+ </Popover>
+ </li>
+ </ul>
+
+ <div v-if="pack">
+ <div class="pack-info-wrapper">
+ <ul class="setting-list">
+ <li>
+ <label>
+ {{ $t('admin_dash.emoji.description') }}
+ <ModifiedIndicator :changed="metaEdited('description')" message-key="admin_dash.emoji.metadata_changed" />
+
+ <textarea
+ v-model="packMeta.description"
+ :disabled="pack.remote !== undefined"
+ class="bio resize-height" />
+ </label>
+ </li>
+ <li>
+ <label>
+ {{ $t('admin_dash.emoji.homepage') }}
+ <ModifiedIndicator :changed="metaEdited('homepage')" message-key="admin_dash.emoji.metadata_changed" />
+
+ <input
+ class="emoji-info-input" v-model="packMeta.homepage"
+ :disabled="pack.remote !== undefined">
+ </label>
+ </li>
+ <li>
+ <label>
+ {{ $t('admin_dash.emoji.fallback_src') }}
+ <ModifiedIndicator :changed="metaEdited('fallback-src')" message-key="admin_dash.emoji.metadata_changed" />
+
+ <input class="emoji-info-input" v-model="packMeta['fallback-src']" :disabled="pack.remote !== undefined">
+ </label>
+ </li>
+ <li>
+ <label>
+ {{ $t('admin_dash.emoji.fallback_sha256') }}
+
+ <input :disabled="true" class="emoji-info-input" v-model="packMeta['fallback-src-sha256']">
+ </label>
+ </li>
+ <li>
+ <Checkbox :disabled="pack.remote !== undefined" v-model="packMeta['share-files']">
+ {{ $t('admin_dash.emoji.share') }}
+ </Checkbox>
+
+ <ModifiedIndicator :changed="metaEdited('share-files')" message-key="admin_dash.emoji.metadata_changed" />
+ </li>
+ <li class="btn-group">
+ <button
+ class="button button-default btn"
+ type="button"
+ v-if="pack.remote === undefined"
+ @click="savePackMetadata">
+ {{ $t('admin_dash.emoji.save_meta') }}
+ </button>
+ <button
+ class="button button-default btn"
+ type="button"
+ v-if="pack.remote === undefined"
+ @click="savePackMetadata">
+ {{ $t('admin_dash.emoji.revert_meta') }}
+ </button>
+
+ <button
+ class="button button-default btn"
+ v-if="pack.remote === undefined"
+ type="button"
+ @click="deleteModalVisible = true">
+ {{ $t('admin_dash.emoji.delete_pack') }}
+
+ <ConfirmModal
+ v-if="deleteModalVisible"
+ :title="$t('admin_dash.emoji.delete_title')"
+ :cancel-text="$t('status.delete_confirm_cancel_button')"
+ :confirm-text="$t('status.delete_confirm_accept_button')"
+ @cancelled="deleteModalVisible = false"
+ @accepted="deleteEmojiPack" >
+ {{ $t('admin_dash.emoji.delete_confirm', [packName]) }}
+ </ConfirmModal>
+ </button>
+
+ <button
+ class="button button-default btn"
+ type="button"
+ v-if="pack.remote !== undefined"
+ @click="$refs.dlPackPopover.showPopover">
+ {{ $t('admin_dash.emoji.download_pack') }}
+
+ <Popover
+ ref="dlPackPopover"
+ trigger="click"
+ placement="bottom"
+ bound-to-selector=".emoji-tab"
+ popover-class="emoji-tab-edit-popover popover-default"
+ :bound-to="{ x: 'container' }"
+ :offset="{ y: 5 }"
+ >
+ <template #content>
+ <h3>{{ $t('admin_dash.emoji.downloading_pack', [packName]) }}</h3>
+ <div>
+ <div>
+ <div class="emoji-tab-popover-input">
+ <label>
+ {{ $t('admin_dash.emoji.download_as_name') }}
+ <input class="emoji-data-input"
+ v-model="remotePackDownloadAs"
+ :placeholder="$t('admin_dash.emoji.download_as_name_full')">
+ </label>
+
+ <div v-if="downloadWillReplaceLocal" class="warning">
+ <em>{{ $t('admin_dash.emoji.replace_warning') }}</em>
+ </div>
+ </div>
+
+ <button
+ class="button button-default btn"
+ type="button"
+ @click="downloadRemotePack">
+ {{ $t('admin_dash.emoji.download') }}
+ </button>
+ </div>
+ </div>
+ </template>
+ </Popover>
+ </button>
+ </li>
+ </ul>
+ </div>
+
+ <ul class="setting-list">
+ <h4>
+ {{ $t('admin_dash.emoji.files') }}
+
+ <ModifiedIndicator v-if="pack"
+ :changed="$refs.emojiPopovers && $refs.emojiPopovers.some(p => p.isEdited)"
+ message-key="admin_dash.emoji.emoji_changed"/>
+ </h4>
+
+ <div class="emoji-list" v-if="pack">
+ <EmojiEditingPopover
+ v-if="pack.remote === undefined"
+ placement="bottom" new-upload
+ :title="$t('admin_dash.emoji.adding_new')"
+ :packName="packName"
+ @updatePackFiles="updatePackFiles" @displayError="displayError"
+ >
+ <template #trigger>
+ <FAIcon icon="plus" size="2x" :title="$t('admin_dash.emoji.add_file')" />
+ </template>
+ </EmojiEditingPopover>
+
+ <EmojiEditingPopover
+ placement="top" ref="emojiPopovers"
+ v-for="(file, shortcode) in pack.files" :key="shortcode"
+ :title="$t('admin_dash.emoji.editing', [shortcode])"
+ :disabled="pack.remote !== undefined"
+ :shortcode="shortcode" :file="file" :packName="packName"
+ @updatePackFiles="updatePackFiles" @displayError="displayError"
+ >
+ <template #trigger>
+ <StillImage
+ class="emoji"
+ :src="emojiAddr(file)"
+ :title="`:${shortcode}:`"
+ :alt="`:${shortcode}:`"
+ />
+ </template>
+ </EmojiEditingPopover>
+ </div>
+ </ul>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script src="./emoji_tab.js"></script>
+
+<style lang="scss" src="./emoji_tab.scss"></style>
diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.js b/src/components/settings_modal/admin_tabs/frontends_tab.js
@@ -55,9 +55,13 @@ const FrontendsTab = {
return fe.refs.includes(frontend.ref)
},
getSuggestedRef (frontend) {
- const defaultFe = this.adminDraft[':pleroma'][':frontends'][':primary']
- if (defaultFe?.name === frontend.name && this.canInstall(defaultFe)) {
- return defaultFe.ref
+ if (this.adminDraft) {
+ const defaultFe = this.adminDraft[':pleroma'][':frontends'][':primary']
+ if (defaultFe?.name === frontend.name && this.canInstall(defaultFe)) {
+ return defaultFe.ref
+ } else {
+ return frontend.refs[0]
+ }
} else {
return frontend.refs[0]
}
diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.vue b/src/components/settings_modal/admin_tabs/frontends_tab.vue
@@ -6,7 +6,7 @@
<div class="setting-item">
<h2>{{ $t('admin_dash.tabs.frontends') }}</h2>
<p>{{ $t('admin_dash.frontend.wip_notice') }}</p>
- <ul class="setting-list">
+ <ul class="setting-list" v-if="adminDraft">
<li>
<h3>{{ $t('admin_dash.frontend.default_frontend') }}</h3>
<p>{{ $t('admin_dash.frontend.default_frontend_tip') }}</p>
@@ -23,6 +23,10 @@
</ul>
</li>
</ul>
+ <div v-else class="setting-list">
+ {{ $t('admin_dash.frontend.default_frontend_unavail') }}
+ </div>
+
<div class="setting-list relative">
<PanelLoading class="overlay" v-if="working"/>
<h3>{{ $t('admin_dash.frontend.available_frontends') }}</h3>
@@ -33,9 +37,9 @@
>
<strong>{{ frontend.name }}</strong>
{{ ' ' }}
- <span v-if="adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name">
+ <span v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name">
<i18n-t
- v-if="adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]"
+ v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]"
keypath="admin_dash.frontend.is_default"
/>
<i18n-t
@@ -43,7 +47,7 @@
keypath="admin_dash.frontend.is_default_custom"
>
<template #version>
- <code>{{ adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code>
+ <code>{{ adminDraft && adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code>
</template>
</i18n-t>
</span>
@@ -134,7 +138,7 @@
class="button button-default btn"
type="button"
:disabled="
- adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name &&
+ !adminDraft || adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name &&
adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]
"
@click="setDefault(frontend)"
diff --git a/src/components/settings_modal/helpers/emoji_editing_popover.vue b/src/components/settings_modal/helpers/emoji_editing_popover.vue
@@ -0,0 +1,208 @@
+<template>
+ <Popover
+ trigger="click"
+ :placement="placement"
+ bound-to-selector=".emoji-list"
+ popover-class="emoji-tab-edit-popover popover-default"
+ ref="emojiPopover"
+ :bound-to="{ x: 'container' }"
+ :offset="{ y: 5 }"
+ :disabled="disabled"
+ :class="{'emoji-unsaved': isEdited}"
+ >
+ <template #trigger>
+ <slot name="trigger" />
+ </template>
+ <template #content>
+ <h3>
+ {{ title }}
+ </h3>
+
+ <StillImage class="emoji" v-if="emojiPreview" :src="emojiPreview" />
+ <div v-else class="emoji"></div>
+
+ <div class="emoji-tab-popover-input" v-if="newUpload">
+ <input
+ type="file"
+ accept="image/*"
+ class="emoji-tab-popover-file"
+ @change="uploadFile = $event.target.files">
+ </div>
+ <div>
+ <div class="emoji-tab-popover-input">
+ <label>
+ {{ $t('admin_dash.emoji.shortcode') }}
+ <input class="emoji-data-input"
+ v-model="editedShortcode"
+ :placeholder="$t('admin_dash.emoji.new_shortcode')">
+ </label>
+ </div>
+
+ <div class="emoji-tab-popover-input">
+ <label>
+ {{ $t('admin_dash.emoji.filename') }}
+
+ <input class="emoji-data-input"
+ v-model="editedFile"
+ :placeholder="$t('admin_dash.emoji.new_filename')">
+ </label>
+ </div>
+
+ <button
+ class="button button-default btn"
+ type="button"
+ :disabled="newUpload ? uploadFile.length == 0 : !isEdited"
+ @click="newUpload ? uploadEmoji() : saveEditedEmoji()">
+ {{ $t('admin_dash.emoji.save') }}
+ </button>
+
+ <template v-if="!newUpload">
+ <button
+ class="button button-default btn emoji-tab-popover-button"
+ type="button"
+ @click="deleteModalVisible = true">
+ {{ $t('admin_dash.emoji.delete') }}
+ </button>
+ <button
+ class="button button-default btn emoji-tab-popover-button"
+ type="button"
+ @click="revertEmoji">
+ {{ $t('admin_dash.emoji.revert') }}
+ </button>
+ <ConfirmModal
+ v-if="deleteModalVisible"
+ :title="$t('admin_dash.emoji.delete_title')"
+ :cancel-text="$t('status.delete_confirm_cancel_button')"
+ :confirm-text="$t('status.delete_confirm_accept_button')"
+ @cancelled="deleteModalVisible = false"
+ @accepted="deleteEmoji" >
+ {{ $t('admin_dash.emoji.delete_confirm', [shortcode]) }}
+ </ConfirmModal>
+ </template>
+ </div>
+ </template>
+ </Popover>
+</template>
+
+<script>
+import Popover from 'components/popover/popover.vue'
+import ConfirmModal from 'components/confirm_modal/confirm_modal.vue'
+import StillImage from 'components/still-image/still-image.vue'
+
+export default {
+ components: { Popover, ConfirmModal, StillImage },
+ data () {
+ return {
+ uploadFile: [],
+ editedShortcode: this.shortcode,
+ editedFile: this.file,
+ deleteModalVisible: false
+ }
+ },
+ computed: {
+ emojiPreview () {
+ if (this.newUpload && this.uploadFile.length > 0) {
+ return URL.createObjectURL(this.uploadFile[0])
+ } else if (!this.newUpload) {
+ return this.emojiAddr(this.file)
+ }
+
+ return null
+ },
+ isEdited () {
+ return !this.newUpload && (this.editedShortcode !== this.shortcode || this.editedFile !== this.file)
+ }
+ },
+ inject: ['emojiAddr'],
+ methods: {
+ saveEditedEmoji () {
+ if (!this.isEdited) return
+
+ this.$store.state.api.backendInteractor.updateEmojiFile(
+ { packName: this.packName, shortcode: this.shortcode, newShortcode: this.editedShortcode, newFilename: this.editedFile, force: false }
+ ).then(resp => {
+ if (resp.error !== undefined) {
+ this.$emit('displayError', resp.error)
+ return Promise.reject(resp.error)
+ }
+
+ return resp.json()
+ }).then(resp => this.$emit('updatePackFiles', resp))
+ },
+ uploadEmoji () {
+ this.$store.state.api.backendInteractor.addNewEmojiFile({
+ packName: this.packName,
+ file: this.uploadFile[0],
+ shortcode: this.editedShortcode,
+ filename: this.editedFile
+ }).then(resp => resp.json()).then(resp => {
+ if (resp.error !== undefined) {
+ this.$emit('displayError', resp.error)
+ return
+ }
+
+ this.$emit('updatePackFiles', resp)
+ this.$refs.emojiPopover.hidePopover()
+
+ this.editedFile = ''
+ this.editedShortcode = ''
+ this.uploadFile = []
+ })
+ },
+ revertEmoji () {
+ this.editedFile = this.file
+ this.editedShortcode = this.shortcode
+ },
+ deleteEmoji () {
+ this.deleteModalVisible = false
+
+ this.$store.state.api.backendInteractor.deleteEmojiFile(
+ { packName: this.packName, shortcode: this.shortcode }
+ ).then(resp => resp.json()).then(resp => {
+ if (resp.error !== undefined) {
+ this.$emit('displayError', resp.error)
+ return
+ }
+
+ this.$emit('updatePackFiles', resp)
+ })
+ }
+ },
+ emits: ['updatePackFiles', 'displayError'],
+ props: {
+ placement: String,
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+
+ newUpload: Boolean,
+
+ title: String,
+ packName: String,
+ shortcode: {
+ type: String,
+ // Only exists when this is not a new upload
+ default: ''
+ },
+ file: {
+ type: String,
+ // Only exists when this is not a new upload
+ default: ''
+ }
+ }
+}
+</script>
+
+<style lang="scss">
+ .emoji-tab-edit-popover {
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+ padding-bottom: 0.5em;
+
+ .emoji {
+ width: 32px;
+ height: 32px;
+ }
+ }
+</style>
diff --git a/src/components/settings_modal/helpers/modified_indicator.vue b/src/components/settings_modal/helpers/modified_indicator.vue
@@ -15,7 +15,7 @@
</template>
<template #content>
<div class="modified-tooltip">
- {{ $t('settings.setting_changed') }}
+ {{ $t(messageKey) }}
</div>
</template>
</Popover>
@@ -33,7 +33,13 @@ library.add(
export default {
components: { Popover },
- props: ['changed']
+ props: {
+ changed: Boolean,
+ messageKey: {
+ type: String,
+ default: 'settings.setting_changed'
+ }
+ }
}
</script>
diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js
@@ -195,7 +195,8 @@ export default {
}
},
canHardReset () {
- return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> '))
+ return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths &&
+ this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> '))
},
matchesExpertLevel () {
return (this.expert || 0) <= this.$store.state.config.expertLevel > 0
diff --git a/src/components/settings_modal/settings_modal_admin_content.js b/src/components/settings_modal/settings_modal_admin_content.js
@@ -3,6 +3,7 @@ import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import InstanceTab from './admin_tabs/instance_tab.vue'
import LimitsTab from './admin_tabs/limits_tab.vue'
import FrontendsTab from './admin_tabs/frontends_tab.vue'
+import EmojiTab from './admin_tabs/emoji_tab.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@@ -33,7 +34,8 @@ const SettingsModalAdminContent = {
InstanceTab,
LimitsTab,
- FrontendsTab
+ FrontendsTab,
+ EmojiTab
},
computed: {
user () {
diff --git a/src/components/settings_modal/settings_modal_admin_content.vue b/src/components/settings_modal/settings_modal_admin_content.vue
@@ -60,6 +60,14 @@
>
<FrontendsTab />
</div>
+
+ <div
+ :label="$t('admin_dash.tabs.emoji')"
+ icon="face-smile-beam"
+ data-tab-name="emoji"
+ >
+ <EmojiTab />
+ </div>
</tab-switcher>
</template>
diff --git a/src/i18n/en.json b/src/i18n/en.json
@@ -882,7 +882,8 @@
"nodb": "No DB Config",
"instance": "Instance",
"limits": "Limits",
- "frontends": "Front-ends"
+ "frontends": "Front-ends",
+ "emoji": "Emoji"
},
"nodb": {
"heading": "Database config is disabled",
@@ -932,10 +933,55 @@
"wip_notice": "Please note that this section is a WIP and lacks certain features as backend implementation of front-end management is incomplete.",
"default_frontend": "Default front-end",
"default_frontend_tip": "Default front-end will be shown to all users. Currently there's no way to for a user to select personal front-end. If you switch away from PleromaFE you'll most likely have to use old and buggy AdminFE to do instance configuration until we replace it.",
+ "default_frontend_unavail": "Default frontend settings are not available, as this requires configuration in the database",
"available_frontends": "Available for install",
"failure_installing_frontend": "Failed to install frontend {version}: {reason}",
"success_installing_frontend": "Frontend {version} successfully installed"
},
+ "emoji": {
+ "global_actions": "Global actions",
+ "reload": "Reload emoji",
+ "importFS": "Import emoji from filesystem",
+ "error": "Error: {0}",
+ "create_pack": "Create pack",
+ "delete_pack": "Delete pack",
+ "new_pack_name": "New pack name",
+ "create": "Create",
+ "emoji_packs": "Emoji packs",
+ "remote_packs": "Remote packs",
+ "do_list": "List",
+ "remote_pack_instance": "Remote pack instance",
+ "emoji_pack": "Emoji pack",
+ "edit_pack": "Edit pack",
+ "description": "Description",
+ "homepage": "Homepage",
+ "fallback_src": "Fallback source",
+ "fallback_sha256": "Fallback SHA256",
+ "share": "Share",
+ "save": "Save",
+ "save_meta": "Save metadata",
+ "revert_meta": "Revert metadata",
+ "delete": "Delete",
+ "revert": "Revert",
+ "add_file": "Add file",
+ "adding_new": "Adding new emoji",
+ "shortcode": "Shortcode",
+ "filename": "Filename",
+ "new_shortcode": "Shortcode, leave blank to infer",
+ "new_filename": "Filename, leave blank to infer",
+ "delete_confirm": "Are you sure you want to delete {0}?",
+ "download_pack": "Download pack",
+ "downloading_pack": "Downloading {0}",
+ "download": "Download",
+ "download_as_name": "New name",
+ "download_as_name_full": "New name, leave blank to reuse",
+ "files": "Files",
+ "editing": "Editing {0}",
+ "delete_title": "Delete?",
+ "metadata_changed": "Metadata different from saved",
+ "emoji_changed": "Unsaved emoji file changes, check highlighted emoji",
+ "replace_warning": "This will REPLACE the local pack of the same name"
+ },
"temp_overrides": {
":pleroma": {
":instance": {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
@@ -115,6 +115,15 @@ const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/pleroma/admin/config/descriptions'
const PLEROMA_ADMIN_FRONTENDS_URL = '/api/pleroma/admin/frontends'
const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = '/api/pleroma/admin/frontends/install'
+const PLEROMA_EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji'
+const PLEROMA_EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import'
+const PLEROMA_EMOJI_PACKS_URL = (page, pageSize) => `/api/v1/pleroma/emoji/packs?page=${page}&page_size=${pageSize}`
+const PLEROMA_EMOJI_PACK_URL = (name) => `/api/v1/pleroma/emoji/pack?name=${name}`
+const PLEROMA_EMOJI_PACKS_DL_REMOTE_URL = '/api/v1/pleroma/emoji/packs/download'
+const PLEROMA_EMOJI_PACKS_LS_REMOTE_URL =
+ (url, page, pageSize) => `/api/v1/pleroma/emoji/packs/remote?url=${url}&page=${page}&page_size=${pageSize}`
+const PLEROMA_EMOJI_UPDATE_FILE_URL = (name) => `/api/v1/pleroma/emoji/packs/files?name=${name}`
+
const oldfetch = window.fetch
const fetch = (url, options) => {
@@ -1793,6 +1802,90 @@ const fetchScrobbles = ({ accountId, limit = 1 }) => {
})
}
+const deleteEmojiPack = ({ name }) => {
+ return fetch(PLEROMA_EMOJI_PACK_URL(name), { method: 'DELETE' })
+}
+
+const reloadEmoji = () => {
+ return fetch(PLEROMA_EMOJI_RELOAD_URL, { method: 'POST' })
+}
+
+const importEmojiFromFS = () => {
+ return fetch(PLEROMA_EMOJI_IMPORT_FS_URL)
+}
+
+const createEmojiPack = ({ name }) => {
+ return fetch(PLEROMA_EMOJI_PACK_URL(name), { method: 'POST' })
+}
+
+const listEmojiPacks = ({ page, pageSize }) => {
+ return fetch(PLEROMA_EMOJI_PACKS_URL(page, pageSize))
+}
+
+const listRemoteEmojiPacks = ({ instance, page, pageSize }) => {
+ if (!instance.startsWith('http')) {
+ instance = 'https://' + instance
+ }
+
+ return fetch(
+ PLEROMA_EMOJI_PACKS_LS_REMOTE_URL(instance, page, pageSize),
+ {
+ headers: { 'Content-Type': 'application/json' }
+ }
+ )
+}
+
+const downloadRemoteEmojiPack = ({ instance, packName, as }) => {
+ return fetch(
+ PLEROMA_EMOJI_PACKS_DL_REMOTE_URL,
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ url: instance, name: packName, as
+ })
+ }
+ )
+}
+
+const saveEmojiPackMetadata = ({ name, newData }) => {
+ return fetch(
+ PLEROMA_EMOJI_PACK_URL(name),
+ {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ metadata: newData })
+ }
+ )
+}
+
+const addNewEmojiFile = ({ packName, file, shortcode, filename }) => {
+ const data = new FormData()
+ if (filename.trim() !== '') { data.set('filename', filename) }
+ if (shortcode.trim() !== '') { data.set('shortcode', shortcode) }
+ data.set('file', file)
+
+ return fetch(
+ PLEROMA_EMOJI_UPDATE_FILE_URL(packName),
+ { method: 'POST', body: data }
+ )
+}
+
+const updateEmojiFile = ({ packName, shortcode, newShortcode, newFilename, force }) => {
+ return fetch(
+ PLEROMA_EMOJI_UPDATE_FILE_URL(packName),
+ {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ shortcode, new_shortcode: newShortcode, new_filename: newFilename, force })
+ }
+ )
+}
+
+const deleteEmojiFile = ({ packName, shortcode }) => {
+ return fetch(`${PLEROMA_EMOJI_UPDATE_FILE_URL(packName)}&shortcode=${shortcode}`, { method: 'DELETE' })
+}
+
const apiService = {
verifyCredentials,
fetchTimeline,
@@ -1912,7 +2005,18 @@ const apiService = {
fetchInstanceConfigDescriptions,
fetchAvailableFrontends,
pushInstanceDBConfig,
- installFrontend
+ installFrontend,
+ importEmojiFromFS,
+ reloadEmoji,
+ listEmojiPacks,
+ createEmojiPack,
+ deleteEmojiPack,
+ saveEmojiPackMetadata,
+ addNewEmojiFile,
+ updateEmojiFile,
+ deleteEmojiFile,
+ listRemoteEmojiPacks,
+ downloadRemoteEmojiPack
}
export default apiService