Merge pull request 'small-fixes-and-improvements' (#3492) from small-fixes-and-improvements into develop

Reviewed-on: https://git.pleroma.social/pleroma/pleroma-fe/pulls/3492
This commit is contained in:
HJ 2026-06-01 17:55:11 +00:00
commit 2b62f96889
129 changed files with 1448 additions and 814 deletions

View file

@ -36,7 +36,6 @@ export default function () {
const warning = warnings[i]
console.warn(' ' + warning)
}
console.warn()
process.exit(1)
}
}

6
changelog.d/minor.add Normal file
View file

@ -0,0 +1,6 @@
button to remove all drafts
option to remove forced aspect ratio for user profiles (requested)
showing user tags (mrf policies for user + custom if present)
version information now is also in about page
mention autosuggest now sorts by recent activity
non-square emoji support (toggleable by user)

7
changelog.d/minor.change Normal file
View file

@ -0,0 +1,7 @@
overall improved spacings in status action buttons and post form
logout confirm button is now dangerous
reply/quote now is a radio group and wraps, fixes overflow on languages where labels are too wide
personal note input is now bigger
moved "edit pinned" to the bottom for status action buttons.
dots status action button drops down instead of up to avoid hiding the action buttons
improved attachment description (alt text) input and display

12
changelog.d/minor.fix Normal file
View file

@ -0,0 +1,12 @@
navbar wide logo cropping search input
danger buttons being too bright
user background upload failure no longer breaks new uploads + displays an error
importing theme from old theme editor
removed duplicate federationpolicy entry in admin tab
repeater name overflowing content
reply popover is now shown if replied-to status is muted
second language input not having header
post form's bottom left buttons not showing their toggled state
some font overrides not working
popovers opening outside of window's boundaries
occasional blank page when showing new posts

View file

@ -0,0 +1,2 @@
settings synchronization
user highlight synchronization

View file

@ -67,6 +67,11 @@ export default {
data: () => ({
mobileActivePanel: 'timeline',
}),
provide() {
return {
allowNonSquareEmoji: useMergedConfigStore().mergedConfig.nonSquareEmoji,
}
},
watch: {
themeApplied() {
this.removeSplash()

View file

@ -50,7 +50,7 @@ body {
// have a cursor/pointer to operate them
@media (any-pointer: fine) {
* {
scrollbar-color: var(--fg) transparent;
scrollbar-color: var(--icon) transparent;
&::-webkit-scrollbar {
background: transparent;
@ -130,7 +130,7 @@ body {
}
// Body should have background to scrollbar otherwise it will use white (body color?)
html {
scrollbar-color: var(--fg) var(--wallpaper);
scrollbar-color: var(--icon) var(--wallpaper);
background: var(--wallpaper);
}
}
@ -787,6 +787,19 @@ option {
padding: 0 0.25em;
border-radius: var(--roundness);
border: 1px solid var(--border);
&.-dismissible {
display: flex;
padding-left: 0.5em;
margin: 0;
align-items: baseline;
line-height: 2;
span {
display: block;
flex: 1 0 auto;
}
}
}
.faint {
@ -802,9 +815,11 @@ option {
align-items: baseline;
line-height: 1.5;
p,
span {
display: block;
flex: 1 1 auto;
margin: 0;
}
.dismiss {

View file

@ -28,10 +28,10 @@
>
<user-panel />
<template v-if="layoutType !== 'mobile'">
<nav-panel />
<instance-specific-panel v-if="showInstanceSpecificPanel" />
<features-panel v-if="!currentUser && showFeaturesPanel" />
<who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
<NavPanel />
<InstanceSpecificPanel v-if="showInstanceSpecificPanel" />
<FeaturesPanel v-if="!currentUser && showFeaturesPanel" />
<WhoToFollowPanel v-if="currentUser && suggestionsEnabled" />
<div id="notifs-sidebar" />
</template>
</div>

View file

@ -152,8 +152,10 @@ const getStaticConfig = async () => {
throw res
}
} catch (error) {
console.warn('Failed to load static/config.json, continuing without it.')
console.warn(error)
console.warn(
'Failed to load static/config.json, continuing without it.',
error,
)
return {}
}
}
@ -175,14 +177,16 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
if (source === 'name') return
if (INSTANCE_IDENTIY_EXTERNAL.has(source)) return
useInstanceStore().set({
value: config[source],
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],
value:
config[source] ?? INSTANCE_DEFAULT_CONFIG_DEFINITIONS[source].default,
path: `prefsStorage.${source}`,
}),
)
@ -440,8 +444,7 @@ const getNodeInfo = async ({ store }) => {
throw res
}
} catch (e) {
console.warn('Could not load nodeinfo')
console.warn(e)
console.warn('Could not load nodeinfo', e)
}
}

View file

@ -1,3 +1,5 @@
import { mapState } from 'pinia'
import FeaturesPanel from '../features_panel/features_panel.vue'
import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
@ -7,6 +9,9 @@ import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_pane
import { useInstanceStore } from 'src/stores/instance.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
const pleromaFeCommitUrl =
'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
const About = {
components: {
InstanceSpecificPanel,
@ -19,6 +24,14 @@ const About = {
showFeaturesPanel() {
return useInstanceStore().instanceIdentity.showFeaturesPanel
},
frontendVersionLink() {
return pleromaFeCommitUrl + this.frontendVersion
},
...mapState(useInstanceStore, [
'backendVersion',
'backendRepository',
'frontendVersion',
]),
showInstanceSpecificPanel() {
return (
useInstanceStore().instanceIdentity.showInstanceSpecificPanel &&

View file

@ -1,11 +1,47 @@
<template>
<div class="column-inner">
<div class="About column-inner">
<instance-specific-panel v-if="showInstanceSpecificPanel" />
<staff-panel />
<terms-of-service-panel />
<MRFTransparencyPanel />
<features-panel v-if="showFeaturesPanel" />
<div class="panel panel-default">
<div class="panel-heading">
<div class="title">
{{ $t('settings.version.title') }}
</div>
</div>
<div class="panel-body">
<dl>
<dt>{{ $t('settings.version.backend_version') }}</dt>
<dd>
<a
:href="backendRepository"
target="_blank"
>
{{ backendVersion }}
</a>
</dd>
<dt>{{ $t('settings.version.frontend_version') }}</dt>
<dd>
<a
:href="frontendVersionLink"
target="_blank"
>
{{ frontendVersion }}
</a>
</dd>
</dl>
</div>
</div>
</div>
</template>
<script src="./about.js"></script>
<style>
.About {
dl {
padding-left: 1em;
}
}
</style>

View file

@ -2,6 +2,7 @@ import { mapState } from 'pinia'
import nsfwImage from '../../assets/nsfw.png'
import Flash from '../flash/flash.vue'
import Popover from '../popover/popover.vue'
import StillImage from '../still-image/still-image.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue'
@ -65,13 +66,13 @@ const Attachment = {
modalOpen: false,
showHidden: false,
flashLoaded: false,
showDescription: false,
}
},
components: {
Flash,
StillImage,
VideoAttachment,
Popover,
},
computed: {
classNames() {
@ -180,9 +181,6 @@ const Attachment = {
setFlashLoaded(event) {
this.flashLoaded = event
},
toggleDescription() {
this.showDescription = !this.showDescription
},
toggleHidden(event) {
if (
this.mergedConfig.useOneClickNsfw &&

View file

@ -134,7 +134,7 @@
width: 2em;
height: 2em;
margin-left: 0.5em;
font-size: 1.25em;
font-size: 1em;
}
}
@ -265,3 +265,27 @@
}
}
}
.description-popover {
padding: 1em;
width: 50ch;
max-width: 90vw;
overflow: hidden;
box-sizing: border-box;
summary {
display: inline-block;
margin-bottom: 0.5em;
font-weight: bold;
pointer-events: none;
}
span {
display: block;
overflow-y: auto;
max-height: 10.5em;
text-wrap: pretty;
line-height: 1.5;
white-space: pre-wrap;
}
}

View file

@ -30,21 +30,16 @@
</button>
</div>
<div
v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)"
v-if="size !== 'hide' && !hideDescription && edit"
class="description-container"
:class="{ '-static': !edit }"
>
<input
v-if="edit"
<textarea
v-model="localDescription"
type="text"
class="input description-field"
:placeholder="$t('post_status.media_description')"
@keydown.enter.prevent=""
>
<p v-else>
{{ localDescription }}
</p>
/>
</div>
</button>
<div
@ -87,14 +82,22 @@
>
<FAIcon icon="stop" />
</button>
<button
<Popover
v-if="attachment.description && size !== 'small' && !edit && attachment.type !== 'unknown'"
class="button-default attachment-button -transparent"
:title="$t('status.show_attachment_description')"
@click.prevent="toggleDescription"
trigger="click"
popover-class="popover popover-default description-popover"
:trigger-attrs="{ 'class': 'button-default attachment-button -transparent', 'title': $t('status.attachment_description') }"
>
<FAIcon icon="align-right" />
</button>
<template #trigger>
<FAIcon icon="align-right" />
</template>
<template #content>
<details open>
<summary>{{ $t('status.attachment_description') }}</summary>
<span>{{ localDescription }}</span>
</details>
</template>
</Popover>
<button
v-if="!useModal && attachment.type !== 'unknown'"
class="button-default attachment-button -transparent"
@ -244,21 +247,16 @@
</span>
</div>
<div
v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))"
v-if="size !== 'hide' && !hideDescription && edit"
class="description-container"
:class="{ '-static': !edit }"
>
<input
v-if="edit"
<textarea
v-model="localDescription"
type="text"
class="input description-field"
:placeholder="$t('post_status.media_description')"
@keydown.enter.prevent=""
>
<p v-else>
{{ localDescription }}
</p>
/>
</div>
</div>
</template>

View file

@ -4,6 +4,7 @@ import UserLink from '../user_link/user_link.vue'
import UserPopover from '../user_popover/user_popover.vue'
import { useInstanceStore } from 'src/stores/instance.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -24,6 +25,11 @@ const BasicUserCard = {
)
},
},
computed: {
allowNonSquareEmoji() {
return useMergedConfigStore().mergedConfig.nonSquareEmoji
},
},
}
export default BasicUserCard

View file

@ -27,6 +27,7 @@
class="basic-user-card-user-name-value"
:html="user.name"
:emoji="user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
/>
</div>
<div>

View file

@ -54,7 +54,7 @@ export default {
{
variant: 'danger',
directives: {
background: '--cRed',
background: '$blend(--cRed 0.25 --inheritedBackground)',
},
},
{

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: {
@ -20,5 +22,8 @@ export default {
htmlTitle() {
return this.user ? this.user.name_html : ''
},
allowNonSquareEmoji() {
return useMergedConfigStore().mergedConfig.nonSquareEmoji
},
},
}

View file

@ -19,6 +19,7 @@
:title="'@'+(user && user.screen_name_ui)"
:html="htmlTitle"
:emoji="user.emoji || []"
:allow-non-square-emoji="allowNonSquareEmoji"
:is-local="user.is_local"
/>
</div>

View file

@ -61,12 +61,14 @@ export default {
<style lang="scss">
.checkbox {
position: relative;
display: inline-block;
display: inline-flex;
min-height: 1.2em;
align-items: baseline;
gap: 0 0.5em;
&-indicator,
& .label {
vertical-align: middle;
align-self: center;
}
& > &-indicator {
@ -138,15 +140,5 @@ export default {
content: "";
}
}
& > .label {
&.-after {
margin-left: 0.5em;
}
&.-before {
margin-right: 0.5em;
}
}
}
</style>

View file

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

View file

@ -21,6 +21,9 @@ const ConfirmModal = {
confirmText: {
type: String,
},
confirmDanger: {
type: Boolean,
},
},
emits: ['cancelled', 'accepted'],
computed: {},

View file

@ -14,6 +14,7 @@
<slot name="footerLeft" />
<button
class="btn button-default"
:class="{ '-danger': confirmDanger }"
@click.prevent="onAccept"
v-text="confirmText"
/>

View file

@ -9,7 +9,7 @@
.inner-nav {
display: grid;
grid-template-rows: var(--navbar-height);
grid-template-columns: 2fr auto 2fr;
grid-template-columns: minmax(5em, 1fr) auto minmax(5em, 1fr);
grid-template-areas: "sitename logo actions";
box-sizing: border-box;
padding: 0 1.2em;
@ -31,7 +31,7 @@
}
&.-logoLeft .inner-nav {
grid-template-columns: auto 2fr 2fr;
grid-template-columns: auto minmax(5em, 1fr) minmax(5em, 1fr);
grid-template-areas: "logo sitename actions";
}
@ -92,23 +92,18 @@
.actions {
grid-area: actions;
justify-content: flex-end;
text-align: right;
z-index: 1;
}
.item {
flex: 1;
line-height: var(--navbar-height);
height: var(--navbar-height);
overflow: hidden;
display: flex;
flex-wrap: wrap;
&.right {
justify-content: flex-end;
text-align: right;
}
}
.spacer {
width: 1em;
min-width: 1em;
}
}

View file

@ -32,54 +32,57 @@
>
</router-link>
<div class="item right actions">
<search-bar
<SearchBar
v-if="currentUser || !privateMode"
@toggled="onSearchBarToggled"
@click.stop
/>
<button
class="button-unstyled nav-icon"
:title="$t('nav.preferences')"
@click.stop="openSettingsModal('user')"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="cog"
/>
</button>
<button
v-if="currentUser && currentUser.role === 'admin'"
class="button-unstyled nav-icon"
target="_blank"
:title="$t('nav.administration')"
@click.stop="openSettingsModal('admin')"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="tachometer-alt"
/>
</button>
<span class="spacer" />
<button
v-if="currentUser"
class="button-unstyled nav-icon"
:title="$t('login.logout')"
@click.stop.prevent="logout"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="sign-out-alt"
/>
</button>
<template v-if="searchBarHidden">
<button
class="button-unstyled nav-icon"
:title="$t('nav.preferences')"
@click.stop="openSettingsModal('user')"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="cog"
/>
</button>
<button
v-if="currentUser && currentUser.role === 'admin'"
class="button-unstyled nav-icon"
target="_blank"
:title="$t('nav.administration')"
@click.stop="openSettingsModal('admin')"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="tachometer-alt"
/>
</button>
<span class="spacer" />
<button
v-if="currentUser"
class="button-unstyled nav-icon"
:title="$t('login.logout')"
@click.stop.prevent="logout"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="sign-out-alt"
/>
</button>
</template>
</div>
</div>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmLogout"
:title="$t('login.logout_confirm_title')"
:confirm-danger="true"
:confirm-text="$t('login.logout_confirm_accept_button')"
:cancel-text="$t('login.logout_confirm_cancel_button')"
@accepted="doLogout"

View file

@ -1,3 +1,4 @@
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'
@ -5,12 +6,31 @@ const Drafts = {
components: {
Draft,
List,
ConfirmModal,
},
data() {
return {
showingConfirmDialog: false,
}
},
computed: {
drafts() {
return this.$store.getters.draftsArray
},
},
methods: {
abandonAll() {
this.showingConfirmDialog = true
},
doAbandonAll() {
this.$store
.dispatch('abandonAllDrafts')
.then(() => this.hideConfirmDialog())
},
hideConfirmDialog() {
this.showingConfirmDialog = false
},
},
}
export default Drafts

View file

@ -13,36 +13,66 @@
>
{{ $t('drafts.no_drafts') }}
</div>
<List
v-else
:items="drafts"
:non-interactive="true"
>
<template #item="{ item: draft }">
<Draft
class="draft"
:draft="draft"
/>
</template>
</List>
<template v-else>
<List
:items="drafts"
:non-interactive="true"
>
<template #item="{ item: draft }">
<Draft
class="draft"
:draft="draft"
/>
</template>
</List>
<div class="remove-all">
<button
class="btn -danger button-default"
@click="abandonAll"
>
{{ $t('drafts.clean_drafts') }}
</button>
</div>
</template>
</div>
</div>
<teleport to="#modal">
<ConfirmModal
v-if="showingConfirmDialog"
:confirm-danger="true"
:title="$t('drafts.abandon_confirm_title')"
:confirm-text="$t('drafts.abandon_confirm_accept_button')"
:cancel-text="$t('drafts.abandon_confirm_cancel_button')"
@accepted="doAbandonAll"
@cancelled="hideConfirmDialog"
>
{{ $t('drafts.abandon_all_confirm') }}
</ConfirmModal>
</teleport>
</div>
</template>
<script src="./drafts.js"></script>
<style lang="scss">
.draft {
margin: 1em 0;
}
.Drafts {
.draft {
margin: 1em 0;
}
.empty-drafs-list-alert {
padding: 3em;
font-size: 1.2em;
display: flex;
justify-content: center;
color: var(--textFaint);
.remove-all {
margin: 1em;
display: flex;
justify-content: center;
}
.empty-drafs-list-alert {
padding: 3em;
font-size: 1.2em;
display: flex;
justify-content: center;
color: var(--textFaint);
}
}
</style>

View file

@ -333,7 +333,6 @@ const EmojiInput = {
if (!this.pickerShown) {
this.scrollIntoView()
this.$refs.picker.showPicker()
this.$refs.picker.startEmojiLoad()
} else {
this.$refs.picker.hidePicker()
}
@ -590,7 +589,7 @@ const EmojiInput = {
setCaret({ target: { selectionStart } }) {
this.caret = selectionStart
this.$nextTick(() => {
this.$refs.suggestorPopover.updateStyles()
this.$refs.suggestorPopover?.updateStyles()
})
},
autoCompleteItemLabel(suggestion) {

View file

@ -2,7 +2,7 @@
<div
ref="root"
class="input emoji-input"
:class="{ 'with-picker': !hideEmojiButton }"
:class="{ '-with-picker': !hideEmojiButton, '-textarea': input?.tagName === 'TEXTAREA' }"
>
<slot
:id="'textbox-' + randomSeed"
@ -118,9 +118,10 @@
.emoji-picker-icon {
position: absolute;
top: 0;
bottom: 0;
right: 0;
margin: 0.2em 0.25em;
height: 100%;
padding: 0 0.2em;
font-size: 1.3em;
cursor: pointer;
line-height: 1.2em;
@ -130,6 +131,13 @@
}
}
&.-textarea {
.emoji-picker-icon {
height: auto;
padding: 0.2em;
}
}
.emoji-picker-panel {
position: absolute;
z-index: 20;
@ -151,8 +159,11 @@
outline: none;
}
&.with-picker input {
padding-right: 2em;
&.-with-picker {
textarea,
input {
padding-right: 2.4em;
}
}
.hidden-overlay {

View file

@ -131,10 +131,11 @@ export const suggestUsers = ({ dispatch, state }) => {
const diff = (bScore - aScore) * 10
// Then sort alphabetically
const activity = a.last_status_at < b.last_status_at ? 100 : -100
const nameAlphabetically = a.name > b.name ? 1 : -1
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
return diff + nameAlphabetically + screenNameAlphabetically
return diff + nameAlphabetically + screenNameAlphabetically + activity
})
.map((user) => ({
user,

View file

@ -129,6 +129,7 @@ const EmojiPicker = {
hideCustomEmojiInPicker: false,
// Lazy-load only after the first time `showing` becomes true.
contentLoaded: false,
popoverShown: false,
groupRefs: {},
emojiRefs: {},
filteredEmojiGroups: [],
@ -176,6 +177,13 @@ const EmojiPicker = {
const fullEmojiSize = emojiSizeReal + 2 * 0.2 * fontSizeMultiplier * 14
this.emojiSize = fullEmojiSize
},
togglePicker() {
if (this.popoverShown) {
this.hidePicker()
} else {
this.showPicker()
}
},
showPicker() {
this.$refs.popover.showPopover()
this.$nextTick(() => {
@ -194,10 +202,10 @@ const EmojiPicker = {
}
},
onPopoverShown() {
this.$emit('show')
this.popoverShown = true
},
onPopoverClosed() {
this.$emit('close')
this.popoverShown = false
},
onStickerUploaded(e) {
this.$emit('sticker-uploaded', e)

View file

@ -4,6 +4,7 @@
trigger="click"
popover-class="emoji-picker popover-default"
:hide-trigger="true"
placement="bottom"
@show="onPopoverShown"
@close="onPopoverClosed"
>

View file

@ -3,6 +3,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue'
import UserListPopover from '../user_list_popover/user_list_popover.vue'
import { useInstanceStore } from 'src/stores/instance.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCheck, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'
@ -48,6 +49,9 @@ const EmojiReactions = {
statusId: this.status.id,
})
},
allowNonSquareEmoji() {
return useMergedConfigStore().mergedConfig.nonSquareEmoji
},
},
methods: {
toggleShowAll() {

View file

@ -49,6 +49,12 @@
justify-content: center;
align-items: center;
&.-wide {
width: auto;
min-width: var(--emoji-size);
max-width: calc(var(--emoji-size) * 3);
}
--_still_image-label-scale: 0.3;
}
@ -62,6 +68,12 @@
font-size: calc(var(--emoji-size) * 0.8);
margin: 0;
&.-wide {
width: auto;
min-width: var(--emoji-size);
max-width: calc(var(--emoji-size) * 3);
}
img {
object-fit: contain;
}

View file

@ -17,11 +17,13 @@
>
<span
class="reaction-emoji"
:class="{ ['-wide']: allowNonSquareEmoji }"
>
<StillImage
v-if="reaction.url"
:src="reaction.url"
class="reaction-emoji-content"
:class="{ ['-wide']: allowNonSquareEmoji }"
/>
<span
v-else

View file

@ -23,7 +23,7 @@
</div>
{{ ' ' }}
<div
v-if="modelValue?.family"
v-if="modelValue"
class="font-input setting-item"
>
<label
@ -67,10 +67,10 @@
</button>
<input
:id="name"
:model-value="modelValue.family"
:model-value="modelValue"
class="input custom-font"
type="text"
@update:modelValue="$emit('update:modelValue', { ...(modelValue || {}), family: $event.target.value })"
@update:modelValue="$emit('update:modelValue', $event.target.value)"
>
</span>
<span
@ -89,9 +89,9 @@
</button>
<Select
:id="name + '-local-font-switcher'"
:model-value="modelValue?.family"
:model-value="modelValue"
class="custom-font"
@update:model-value="v => $emit('update:modelValue', { ...(modelValue || {}), family: v })"
@update:model-value="v => $emit('update:modelValue', v)"
>
<optgroup
:label="$t('settings.style.themes3.font.group-builtin')"

View file

@ -47,6 +47,11 @@ const Gallery = {
: attachments
.reduce(
(acc, attachment, i) => {
const peek = attachments[i + 1]
const nextEnd = peek == null
const nextWide = !nextEnd && !displayTypes.has(peek?.type)
// Inserting new row
if (attachment.type === 'audio') {
return [
...acc,
@ -61,18 +66,27 @@ const Gallery = {
{ items: [] },
]
}
const maxPerRow = 3
const attachmentsRemaining = this.attachments.length - i + 1
const currentRow = acc[acc.length - 1].items
currentRow.push(attachment)
if (
currentRow.length >= maxPerRow &&
attachmentsRemaining > maxPerRow
) {
return [...acc, { items: [] }]
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 {
return acc
currentRow.items.push(attachment)
}
return acc
},
[{ items: [] }],
)

View file

@ -129,7 +129,7 @@
.gallery-item {
margin: 0;
height: 15em;
height: 20em;
}
}
}

View file

@ -11,3 +11,9 @@
</template>
<script src="./instance_specific_panel.js"></script>
<style lang="scss">
.instance-specific-panel .panel-body {
border-radius: var(--roundness);
}
</style>

View file

@ -42,7 +42,6 @@
</tab-switcher>
<Notifications
ref="notifications"
:no-heading="true"
:no-extra="true"
:minimal-mode="true"
:filter-mode="filterMode"

View file

@ -48,6 +48,7 @@
img {
width: 100%;
height: 100%;
max-height: 10em;
object-fit: cover;
border-radius: var(--roundness);
}

View file

@ -89,13 +89,16 @@
/>
</button>
<span
<details
v-if="description"
open
class="description"
>
{{ description }}
</span>
<summary>{{ $t('status.attachment_description') }}</summary>
<span>{{ description }}</span>
</details>
<span
v-if="media.length > 1"
class="counter"
>
{{ $t('media_modal.counter', { current: currentIndex + 1, total: media.length }, currentIndex + 1) }}
@ -159,19 +162,43 @@ $modal-view-button-icon-margin: 0.5em;
.counter {
/* Hardcoded since background is also hardcoded */
color: white;
margin-top: 1em;
text-shadow: 0 0 10px black, 0 0 10px black;
padding: 0.2em 2em;
text-shadow: 0 0 1em black, 0 0 1em black, 0 0 1em black;
margin: 1em 2em;
overflow: hidden;
}
.description + .counter {
margin-top: 0;
}
.description {
flex: 0 0 auto;
overflow-y: auto;
min-height: 1em;
max-width: 35.8em;
max-width: 80ch;
max-height: 9.5em;
overflow-wrap: break-word;
text-wrap: pretty;
summary {
margin-bottom: 0.5em;
display: inline-block;
font-weight: bold;
pointer-events: none;
}
span {
display: block;
overflow-y: auto;
min-height: 1em;
text-wrap: pretty;
max-height: 10.5em;
white-space: pre-wrap;
line-height: 1.5;
scrollbar-color: white transparent;
&::-webkit-scrollbar-button,
&::-webkit-scrollbar-thumb {
background-color: white;
}
}
}
.modal-image {

View file

@ -109,6 +109,7 @@
<confirm-modal
v-if="showingConfirmLogout"
:title="$t('login.logout_confirm_title')"
:confirm-danger="true"
:confirm-text="$t('login.logout_confirm_accept_button')"
:cancel-text="$t('login.logout_confirm_cancel_button')"
@accepted="doLogout"

View file

@ -144,7 +144,6 @@ const Notification = {
}
},
doApprove() {
this.$emit('interacted')
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user)
this.$store.dispatch('markSingleNotificationAsSeen', {
@ -166,7 +165,6 @@ const Notification = {
}
},
doDeny() {
this.$emit('interacted')
this.$store.state.api.backendInteractor
.denyUser({ id: this.user.id })
.then(() => {
@ -212,6 +210,9 @@ const Notification = {
mergedConfig() {
return useMergedConfigStore().mergedConfig
},
allowNonSquareEmoji() {
return this.mergedConfig.nonSquareEmoji
},
shouldConfirmApprove() {
return this.mergedConfig.modalOnApproveFollow
},

View file

@ -7,7 +7,7 @@
class="Notification"
:compact="true"
:statusoid="notification.status"
@interacted="interacted"
@click="interacted"
/>
</article>
<article
@ -71,6 +71,7 @@
:title="'@'+notification.from_profile.screen_name_ui"
:html="notification.from_profile.name_html"
:emoji="notification.from_profile.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
:is-local="notification.from_profile.is_local"
/>
</bdi>
@ -136,6 +137,7 @@
:src="notification.emoji_url"
:alt="notification.emoji"
:title="notification.emoji"
:class="{ ['-wide']: allowNonSquareEmoji }"
>
<span
v-else

View file

@ -79,6 +79,7 @@ const Notifications = {
return unseenNotificationsFromStore(
this.$store,
useMergedConfigStore().mergedConfig.notificationVisibility,
useMergedConfigStore().mergedConfig.ignoreInactionableSeen,
)
},
filteredNotifications() {

View file

@ -119,6 +119,10 @@
max-width: 1.25em;
height: 1.25em;
width: auto;
&.-wide {
max-width: 3.75em;
}
}
.emoji-reaction-emoji-image {

View file

@ -55,6 +55,9 @@ export default {
return this.expired ? 'polls.expired' : 'polls.expires_in'
}
},
allowNonSquareEmoji() {
return useMergedConfigStore().mergedConfig.nonSquareEmoji
},
loggedIn() {
return this.$store.state.users.currentUser
},

View file

@ -24,6 +24,7 @@
:html="option.title_html"
:handle-links="false"
:emoji="emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
/>
</div>
<div

View file

@ -17,7 +17,6 @@
type="text"
:placeholder="$t('polls.option')"
:maxlength="maxLength"
@change="updatePollToParent"
@keydown.enter.stop.prevent="nextOption(index)"
>
</div>
@ -50,7 +49,6 @@
v-model="pollType"
class="poll-type-select"
unstyled="true"
@change="updatePollToParent"
>
<option value="single">
{{ $t('polls.single_choice') }}

View file

@ -304,10 +304,13 @@ const Popover = {
}
this.scrollable.addEventListener('scroll', this.onScroll)
this.scrollable.addEventListener('resize', this.onResize)
this.$nextTick(() => {
// My assumption is that upon showing popover initially has different size
// as its contents are getting populating, so logic uses those incorrect
// sizes as basis
setTimeout(() => {
if (wasHidden) this.$emit('show')
this.updateStyles()
})
}, 1)
},
hidePopover() {
if (this.disabled) return

View file

@ -152,9 +152,6 @@ const PostStatusForm = {
DraftCloser,
Popover,
},
created() {
this.initQuote()
},
mounted() {
this.updateIdempotencyKey()
this.resize(this.$refs.textarea)
@ -214,7 +211,11 @@ const PostStatusForm = {
poll: {},
hasPoll: false,
hasQuote: false,
quote: {},
quote: {
id: '',
url: '',
thread: false,
},
mediaDescriptions: {},
visibility: scope,
contentType,
@ -233,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,
@ -373,8 +378,15 @@ const PostStatusForm = {
quotable() {
return this.quotingAvailable && this.replyTo
},
quoteThreadToggled() {
return this.newStatus.hasQuote && this.newStatus.quote.thread
quoteThreadToggled: {
get() {
return this.newStatus.hasQuote && this.newStatus.quote.thread
},
set(value) {
this.newStatus.hasQuote = value
this.newStatus.quote.thread = value
this.newStatus.quote.id = value ? this.replyTo : ''
},
},
defaultQuotable() {
if (
@ -855,24 +867,6 @@ const PostStatusForm = {
this.$refs.pollForm.clear()
}
},
initQuote() {
const quote = this.newStatus.quote
if (Object.keys(quote).length > 0) {
return
}
const quotable = this.defaultQuotable
quote.id = quotable ? this.replyTo : ''
quote.url = ''
quote.thread = quotable
},
setQuoteThread(v) {
this.newStatus.hasQuote = v
this.newStatus.quote.thread = v
this.newStatus.quote.id = v ? this.replyTo : ''
},
clearQuoteForm() {
if (this.$refs.quoteForm) {
this.$refs.quoteForm.clear()
@ -880,6 +874,11 @@ const PostStatusForm = {
},
toggleQuoteForm() {
this.newStatus.hasQuote = !this.newStatus.hasQuote
this.newStatus.quote = {}
this.newStatus.quote.thread = false
this.newStatus.quote.id = null
this.newStatus.quote.url = ''
},
dismissScopeNotice() {
useSyncConfigStore().setSimplePrefAndSave({

View file

@ -42,6 +42,7 @@
.form-bottom-left {
display: flex;
gap: 1.5em;
margin-right: 1em;
button {
padding: 0.5em;
@ -52,13 +53,13 @@
.preview-heading {
display: flex;
flex-wrap: wrap;
margin: 0 0.5em;
}
.preview-toggle {
flex: 10 0 auto;
cursor: pointer;
user-select: none;
padding-left: 0.5em;
&:hover {
text-decoration: underline;
@ -89,9 +90,10 @@
}
.reply-or-quote-selector {
flex: 1 0 auto;
margin-bottom: 0.5em;
display: grid;
gap: 0 1em;
display: flex;
flex-wrap: wrap-reverse;
grid-template-columns: 1fr 1fr;
}
@ -105,7 +107,6 @@
display: flex;
justify-content: space-between;
align-items: baseline;
margin-left: -0.5em;
}
.visibility-notice {
@ -144,10 +145,7 @@
justify-content: right;
}
.media-upload-icon,
.poll-icon,
.quote-icon,
.emoji-icon {
.bottom-left-button {
font-size: 1.85em;
line-height: 1.1;
flex: 1;
@ -210,14 +208,14 @@
padding: 0;
}
.input.form-post-body {
textarea.input.form-post-body {
// TODO: make a resizable textarea component?
box-sizing: content-box; // needed for easier computation of dynamic size
overflow: hidden;
transition: min-height 200ms 100ms;
// stock padding + 1 line of text (for counter)
padding-bottom: calc(var(--_padding) + var(--post-line-height) * 1em);
padding-right: 1.5em;
padding-right: 0.5em;
// two lines of text
height: calc(var(--post-line-height) * 1em);
min-height: calc(var(--post-line-height) * 1em);
@ -241,9 +239,11 @@
.character-counter {
position: absolute;
bottom: 0;
right: 0;
right: 2.2em;
padding: 0;
margin: 0 0.5em;
margin: 0;
line-height: 2.2em;
height: 2.2em;
&.error {
color: var(--cRed);

View file

@ -88,7 +88,7 @@
</div>
<div
v-if="!disablePreview"
class="preview-heading faint"
class="preview-heading"
>
<a
class="preview-toggle faint"
@ -110,34 +110,24 @@
<div
v-if="quotable"
role="radiogroup"
class="btn-group reply-or-quote-selector"
class="reply-or-quote-selector"
>
<button
:id="`reply-or-quote-option-${randomSeed}-reply`"
class="btn button-default reply-or-quote-option"
:class="{ toggled: !quoteThreadToggled }"
tabindex="0"
role="radio"
<Checkbox
v-model="quoteThreadToggled"
:radio="true"
:disabled="quoteFormVisible"
:aria-labelledby="`reply-or-quote-option-${randomSeed}-reply`"
:aria-checked="!newStatus.quote.thread"
@click="setQuoteThread(false)"
>
{{ $t('post_status.reply_option') }}
</button>
<button
:id="`reply-or-quote-option-${randomSeed}-quote`"
class="btn button-default reply-or-quote-option"
:class="{ toggled: quoteThreadToggled }"
tabindex="0"
role="radio"
:disabled="quoteFormVisible"
:aria-labelledby="`reply-or-quote-option-${randomSeed}-quote`"
:aria-checked="newStatus.quote.thread"
@click="setQuoteThread(true)"
>
{{ $t('post_status.quote_option') }}
</button>
</Checkbox>
<Checkbox
role="radio"
:radio="true"
:model-value="!quoteThreadToggled"
:disabled="quoteFormVisible"
@update:model-value="e => quoteThreadToggled = !e"
>
{{ $t('post_status.reply_option') }}
</Checkbox>
</div>
</div>
<div
@ -266,18 +256,20 @@
</div>
</div>
</div>
<poll-form
<PollForm
v-if="pollsAvailable"
ref="pollForm"
:visible="pollFormVisible"
:params="newStatus.poll"
/>
<quote-form
<QuoteForm
v-if="quotingAvailable"
:id="newStatus.quote.id"
ref="quoteForm"
:visible="quoteFormVisible"
:reply="isReply"
:params="newStatus.quote"
:url="newStatus.quote.url"
@update:url="url => newStatus.quote.url = url"
@update:id="id => newStatus.quote.id = id"
/>
<span
v-if="!disableDraft && shouldAutoSaveDraft"
@ -292,7 +284,7 @@
<div class="form-bottom-left">
<media-upload
ref="mediaUpload"
class="media-upload-icon"
class="bottom-left-button media-upload-icon"
:drop-files="dropFiles"
:disabled="uploadFileLimitReached"
@uploading="startedUploadingFiles"
@ -302,8 +294,8 @@
/>
<button
v-if="pollsAvailable"
class="poll-icon button-unstyled"
:class="{ selected: pollFormVisible }"
class="bottom-left-button poll-icon button-unstyled"
:class="{ toggled: pollFormVisible }"
:title="$t('polls.add_poll')"
@click="togglePollForm"
>
@ -311,9 +303,9 @@
</button>
<button
v-if="quotingAvailable"
class="quote-icon button-unstyled"
:disabled="newStatus.quote.thread"
:class="{ selected: quoteFormVisible }"
class="bottom-left-button quote-icon button-unstyled"
:disabled="quoteThreadToggled"
:class="{ toggled: quoteFormVisible }"
:title="$t('tool_tip.add_quote')"
@click="toggleQuoteForm"
>
@ -389,9 +381,11 @@
</div>
<div
v-if="error"
class="alert error"
class="alert error -dismissible"
>
Error: {{ error }}
<span>
{{ error }}
</span>
<button
class="button-unstyled"
@click="clearError"

View file

@ -15,17 +15,20 @@ export default {
visible: {
type: Boolean,
},
reply: {
type: Boolean,
url: {
type: String,
required: false,
default: '',
},
params: {
type: Object,
required: true,
id: {
type: String,
required: false,
default: '',
},
},
data() {
return {
text: this.params.url,
text: this.url,
loading: false,
error: false,
debounceSetQuote: debounce((value) => {
@ -34,16 +37,15 @@ export default {
}
},
created() {
if (this.params.url && !this.params.id) {
this.fetchStatus(this.params.url)
} else if (this.params.id) {
if (this.url && !this.id) {
this.fetchStatus(this.url)
} else if (this.id) {
this.text =
window.location.protocol +
'//' +
this.instanceHost +
'/notice/' +
this.params.id
this.params.url = this.text
this.id
}
},
computed: {
@ -56,22 +58,23 @@ export default {
)
},
quoteVisible() {
return (!!this.params.id || this.loading) && !this.error
return (!!this.id || this.loading) && !this.error
},
},
watch: {
text(value) {
this.debounceSetQuote(value)
this.$emit('update:url', value)
},
visible(value) {
if (value && this.params.url) {
this.fetchStatus(this.params.url)
if (value && this.url) {
this.fetchStatus(this.url)
}
},
},
methods: {
clear() {
this.text = this.params.url
this.text = this.url
this.loading = false
this.error = false
},
@ -79,16 +82,15 @@ export default {
this.loading = value
},
handleError(error) {
this.params.id = null
this.id = null
this.error = !!error
},
fetchStatus(value) {
this.params.url = value
this.error = false
const notice = this.noticeRegex.exec(value)
if (notice && notice.length === 4) {
this.params.id = notice[3]
this.$emit('update:id', notice[3])
} else if (value) {
this.loading = true
this.$store
@ -101,7 +103,7 @@ export default {
})
.then((data) => {
if (data && data.statuses && data.statuses.length === 1) {
this.params.id = data.statuses[0].id
this.$emit('update:id', data.statuses[0].id)
} else {
this.handleError(true)
}
@ -111,7 +113,7 @@ export default {
this.loading = false
})
} else {
this.params.id = null
this.$emit('update:id', null)
}
},
},

View file

@ -13,8 +13,8 @@
>
</div>
<Quote
:status-id="params.id"
:status-url="params.url"
:status-id="id"
:status-url="url"
:status-visible="quoteVisible"
:initially-expanded="true"
:loading="loading"

View file

@ -112,6 +112,12 @@ export default {
type: Boolean,
default: true,
},
// Allow wide emoji (max 3:1 ratio)
allowNonSquareEmoji: {
required: false,
type: Boolean,
default: false,
},
},
// NEVER EVER TOUCH DATA INSIDE RENDER
render() {
@ -322,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' : '']}>
<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

@ -77,6 +77,14 @@
height: var(--emoji-size, 32px);
}
&.-allow-non-square-emoji {
.emoji {
width: auto;
max-width: calc(var(--emoji-size, 32px) * 3);
min-width: var(--emoji-size, 32px);
}
}
.img,
video {
max-width: 100%;

View file

@ -51,8 +51,6 @@
class="cancel-icon fa-scale-110 fa-old-padding"
/>
</button>
<span class="spacer" />
<span class="spacer" />
</template>
</div>
</template>
@ -61,18 +59,14 @@
<style lang="scss">
.SearchBar {
display: inline-flex;
display: flex;
align-items: baseline;
vertical-align: baseline;
justify-content: flex-end;
&.-expanded {
width: 100%;
}
.search-bar-input,
.search-button {
height: 29px;
height: 2em;
}
.search-bar-input {

View file

@ -80,10 +80,16 @@ const present = computed(() => props.modelValue[props.selectedId] != null)
const moveUp = async () => {
const newModel = [...props.modelValue]
const movable = newModel.splice(props.selectedId, 1)[0]
newModel.splice(props.selectedId - 1, 0, movable)
const movableId = Number(props.selectedId)
emit('update:modelValue', newModel)
const movable = newModel.slice(movableId, movableId + 1)[0]
const before = newModel.slice(0, movableId)
const after = newModel.slice(movableId + 1)
const newBefore = before.slice(0, -1)
const newAfter = [before.slice(-1)[0], ...after]
emit('update:modelValue', [...newBefore, movable, ...newAfter])
await nextTick()
emit('update:selectedId', props.selectedId - 1)
}
@ -94,12 +100,18 @@ const moveDnValid = computed(() => {
const moveDn = async () => {
const newModel = [...props.modelValue]
const movable = newModel.splice(props.selectedId.value, 1)[0]
newModel.splice(props.selectedId + 1, 0, movable)
const movableId = Number(props.selectedId)
emit('update:modelValue', newModel)
const movable = newModel.slice(movableId, movableId + 1)[0]
const before = newModel.slice(0, movableId)
const after = newModel.slice(movableId + 1)
const newBefore = [...before, after.slice(0, 1)[0]]
const newAfter = after.slice(1)
emit('update:modelValue', [...newBefore, movable, ...newAfter])
await nextTick()
emit('update:selectedId', props.selectedId + 1)
emit('update:selectedId', movableId + 1)
}
const add = async () => {

View file

@ -11,6 +11,7 @@ import ModifiedIndicator from '../helpers/modified_indicator.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import StringSetting from '../helpers/string_setting.vue'
import { useEmojiStore } from 'src/stores/emoji.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { useInterfaceStore } from 'src/stores/interface.js'
@ -174,63 +175,25 @@ const EmojiTab = {
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() {
this.loadPacksPaginated(
this.$store.state.api.backendInteractor.listEmojiPacks,
).then((allPacks) => {
this.knownLocalPacks = allPacks
for (const name of Object.keys(this.knownLocalPacks)) {
this.sortPackFiles(name)
}
})
useEmojiStore()
.getAdminPacks(
this.remotePackInstance,
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,
)
useEmojiStore()
.getAdminPacks(
this.remotePackInstance,
this.$store.state.api.backendInteractor.listRemoteEmojiPacks,
)
.then((allPacks) => {
let inst = this.remotePackInstance
if (!inst.startsWith('http')) {

View file

@ -24,9 +24,6 @@
</ul>
<h3>{{ $t('admin_dash.federation.activitypub') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting path=":pleroma.:instance.:allow_relay" />
</li>
<li>
<BooleanSetting path=":pleroma.:activitypub.:unfollow_blocked" />
</li>

View file

@ -5,7 +5,6 @@
:trigger-attrs="{ 'aria-label': $t('settings.setting_changed') }"
>
<template #trigger>
&nbsp;
<FAIcon icon="circle-question" />
</template>
<template #content>

View file

@ -7,7 +7,6 @@
trigger="hover"
>
<template #trigger>
&nbsp;
<FAIcon
icon="desktop"
:aria-label="$t('settings.setting_local_side')"

View file

@ -8,7 +8,6 @@
:trigger-attrs="{ 'aria-label': $t('settings.setting_changed') }"
>
<template #trigger>
&nbsp;
<FAIcon
icon="wrench"
/>

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

@ -41,6 +41,7 @@
.tab-slot-wrapper {
flex: 1 1 auto;
height: 100%;
padding: 0 1em;
overflow-y: auto;
display: grid;
grid-template-columns: minmax(1em, 1fr) minmax(min-content, 45em) minmax(1em, 1fr);
@ -65,6 +66,8 @@
.tab-content-wrapper {
flex: 1 1 auto;
height: 100%;
display: flex;
flex-direction: column;
&.-hidden {
display: none;

View file

@ -38,10 +38,7 @@
p {
line-height: 1.5;
}
.suboptions {
margin-left: 1em;
margin-left: 2em;
}
.sidenote {
@ -53,6 +50,7 @@
.setting-description {
margin-top: 0.2em;
margin-bottom: 0;
margin-left: 0;
font-size: 80%;
}
@ -66,6 +64,7 @@
column-gap: 0.5em;
align-items: baseline;
padding: 0.5em 0;
line-height: 1.5em;
.setting-label {
grid-area: label;
@ -96,6 +95,9 @@
.checkbox-indicator {
grid-area: control;
height: 1.5em;
line-height: 1.5em;
align-self: baseline;
}
.-mobile & {
@ -130,6 +132,13 @@
padding-left: 0;
margin: 0;
&.suboptions {
margin-left: 2em;
border-top: 1px dotted var(--border);
border-bottom: 1px dotted var(--border);
}
.btn:not(.dropdown-button) {
padding: 0 2em;
}
@ -207,6 +216,12 @@
}
}
li {
.sidenote {
margin-left: 1em;
}
}
/* stylelint-disable no-descending-specificity */
.setting-item {
grid-template-columns: 1fr min-content;
@ -221,9 +236,14 @@
.checkbox {
.label {
text-align: left;
margin-left: 0;
order: 2;
}
.checkbox-indicator {
order: 1;
}
}
}
ul {
@ -236,14 +256,16 @@
}
}
.setting-list:not(.suboptions),
.option-list {
&.two-column {
grid-template-columns: 1fr;
}
}
.UnitSetting {
padding-right: 0.5em;
}
}
&.peek {

View file

@ -67,6 +67,7 @@ const AppearanceTab = {
})),
backgroundUploading: false,
background: null,
backgroundError: null,
backgroundPreview: null,
}
},
@ -474,6 +475,9 @@ const AppearanceTab = {
resetUploadedBackground() {
this.backgroundPreview = null
},
clearBackgroundError() {
this.backgroundError = null
},
submitBackground(background) {
if (!this.backgroundPreview && background !== '') {
return
@ -486,8 +490,11 @@ const AppearanceTab = {
this.$store.commit('addNewUsers', [data])
this.$store.commit('setCurrentUser', data)
this.backgroundPreview = null
this.backgroundError = null
})
.catch((e) => {
this.backgroundError = e
})
.catch(this.displayUploadError)
.finally(() => {
this.backgroundUploading = false
})

View file

@ -208,6 +208,23 @@
{{ $t('settings.reset') }}
</button>
</div>
<div
v-if="backgroundError"
class="alert error -dismissible"
>
<span>
{{ backgroundError }}
</span>
<button
class="button-unstyled"
@click="clearBackgroundError"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="times"
/>
</button>
</div>
<button
v-if="!isDefaultBackground"
class="btn button-default reset-button"
@ -246,6 +263,11 @@
{{ $t('settings.hide_wallpaper') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="compactProfiles">
{{ $t('settings.compact_profiles') }}
</BooleanSetting>
</li>
</ul>
</div>
</div>

View file

@ -50,6 +50,16 @@
</li>
</ul>
</li>
<li>
<BooleanSetting path="userCardHidePersonalMarks">
{{ $t('settings.user_card_hide_personal_marks') }}
</BooleanSetting>
</li>
<li v-if="shoutAvailable">
<BooleanSetting path="hideShoutbox">
{{ $t('settings.hide_shoutbox') }}
</BooleanSetting>
</li>
</ul>
<h3>{{ $t('settings.attachments') }}</h3>
<ul class="setting-list">
@ -77,16 +87,6 @@
{{ $t('settings.hide_attachments_in_convo') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="userCardHidePersonalMarks">
{{ $t('settings.user_card_hide_personal_marks') }}
</BooleanSetting>
</li>
<li v-if="shoutAvailable">
<BooleanSetting path="hideShoutbox">
{{ $t('settings.hide_shoutbox') }}
</BooleanSetting>
</li>
</ul>
</div>
</div>

View file

@ -1,3 +1,5 @@
import { mapState } from 'pinia'
import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
@ -9,14 +11,6 @@ const pleromaFeCommitUrl =
'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
const VersionTab = {
data() {
const instance = useInstanceStore()
return {
backendVersion: instance.backendVersion,
backendRepository: instance.backendRepository,
frontendVersion: instance.frontendVersion,
}
},
components: {
BooleanSetting,
},
@ -24,6 +18,11 @@ const VersionTab = {
frontendVersionLink() {
return pleromaFeCommitUrl + this.frontendVersion
},
...mapState(useInstanceStore, [
'backendVersion',
'backendRepository',
'frontendVersion',
]),
...SharedComputedObject(),
},
methods: {

View file

@ -58,6 +58,7 @@ const FilteringTab = {
hide = false,
name = '',
value = '',
caseSensitive = false,
} = data
this.createFilter({
@ -66,6 +67,7 @@ const FilteringTab = {
hide,
name,
value,
caseSensitive,
})
},
onImportFailure(result) {

View file

@ -51,7 +51,16 @@
text-align: right;
}
> label.checkbox {
display: grid;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
grid-column: 1 / span 2;
text-align: right;
}
.filter-field-value {
display: flex;
grid-column: 2 / span 2;
}
}

View file

@ -254,6 +254,20 @@
:value="filter[1].value"
@input="updateFilter(filter[0], 'value', $event.target.value)"
>
{{ ' ' }}
</div>
<div class="filter-value filter-field">
<Checkbox
:id="'filterCaseSensitive' + filter[0]"
:model-value="filter[1].caseSensitive"
:name="'filterCaseSensitive' + filter[0]"
class="input-inset input-boolean case-sensitive"
@update:model-value="updateFilter(filter[0], 'caseSensitive', $event)"
>
<template #before>
{{ $t('settings.filter.case_sensitive') }}
</template>
</Checkbox>
</div>
<div class="filter-expires filter-field">
<label

View file

@ -9,6 +9,7 @@
class="lang-selector"
@update="val => language = val"
/>
<h5>{{ $t('settings.email_language') }}</h5>
<interface-language-switcher
v-model="emailLanguage"
class="lang-selector"

View file

@ -117,7 +117,6 @@
:key="column"
:local="true"
:path="column + 'ColumnWidth'"
:units="horizontalUnits"
expert="1"
>
{{ $t('settings.column_sizes_' + column) }}

View file

@ -509,22 +509,14 @@ export default {
}
},
setCustomTheme() {
useInterfaceStore().setThemeV2({
customTheme: {
ignore: true,
themeFileVersion: this.selectedVersion,
themeEngineVersion: CURRENT_VERSION,
...this.previewTheme,
},
customThemeSource: {
themeFileVersion: this.selectedVersion,
themeEngineVersion: CURRENT_VERSION,
shadows: this.shadowsLocal,
fonts: this.fontsLocal,
opacity: this.currentOpacity,
colors: this.currentColors,
radii: this.currentRadii,
},
useInterfaceStore().setTheme({
themeFileVersion: this.selectedVersion,
themeEngineVersion: CURRENT_VERSION,
shadows: this.shadowsLocal,
fonts: this.fontsLocal,
opacity: this.currentOpacity,
colors: this.currentColors,
radii: this.currentRadii,
})
},
updatePreviewColors() {

View file

@ -3,6 +3,7 @@ import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import UnitSetting from '../helpers/unit_setting.vue'
import { useLocalConfigStore } from 'src/stores/local_config.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
@ -62,6 +63,7 @@ const PostsTab = {
ChoiceSetting,
IntegerSetting,
FontControl,
UnitSetting,
},
computed: {
...SharedComputedObject(),

View file

@ -169,6 +169,11 @@
{{ $t('settings.stop_gifs') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="nonSquareEmoji">
{{ $t('settings.non_square_emoji') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
:local="true"

View file

@ -492,7 +492,7 @@ export default {
try {
return deserializeShadow(shadow)
} catch (e) {
console.warn(e)
console.warn('Failed to deserialize shadow', e)
return shadow
}
}
@ -652,7 +652,7 @@ export default {
return rgb2hex(computedColor)
}
} catch (e) {
console.warn(e)
console.warn('failed to get computed color', e)
}
return null
}

View file

@ -125,7 +125,7 @@ export default {
if (computedColor) return rgb2hex(computedColor)
return null
} catch (e) {
console.warn(e)
console.warn('Failed to get fallback color', e)
return null
}
} else {

View file

@ -76,6 +76,7 @@ const SideDrawer = {
return unseenNotificationsFromStore(
this.$store,
useMergedConfigStore().mergedConfig.notificationVisibility,
useMergedConfigStore().mergedConfig.ignoreInactionableSeen,
)
},
unseenNotificationsCount() {

View file

@ -152,6 +152,7 @@ const Status = {
'simpleTree',
'showOtherRepliesAsButton',
'dive',
'ignoreMute',
'controlledThreadDisplayStatus',
'controlledToggleThreadDisplay',
@ -166,7 +167,7 @@ const Status = {
'controlledMediaPlaying',
'controlledSetMediaPlaying',
],
emits: ['interacted', 'goto', 'toggleExpanded'],
emits: ['goto', 'toggleExpanded'],
data() {
return {
uncontrolledReplying: false,
@ -187,6 +188,9 @@ const Status = {
!this.inConversation
)
},
allowNonSquareEmoji() {
return this.mergedConfig.nonSquareEmoji
},
repeaterClass() {
const user = this.statusoid.user
return highlightClass(user)
@ -345,6 +349,7 @@ const Status = {
}
},
muted() {
if (this.ignoreMute) return false
if (this.statusoid.user.id === this.currentUser.id) return false
return !this.unmuted && !this.shouldNotMute && this.muteReasons.length > 0
},
@ -366,6 +371,7 @@ const Status = {
)
},
shouldNotMute() {
if (this.ignoreMute) return true
if (this.isFocused) return true
const { status } = this
const { reblog } = status
@ -557,11 +563,9 @@ const Status = {
this.error = error
},
clearError() {
this.$emit('interacted')
this.error = undefined
},
toggleReplying() {
this.$emit('interacted')
if (this.replying) {
this.$refs.postStatusForm.requestClose()
} else {

View file

@ -23,21 +23,18 @@
.status-container {
display: flex;
padding: var(--status-margin);
gap: var(--status-margin);
> * {
min-width: 0;
}
&.-repeat {
padding-top: 0;
}
}
.pin {
padding: var(--status-margin) var(--status-margin) 0;
display: flex;
align-items: center;
justify-content: flex-end;
margin-right: 0.5em;
}
._misclick-prevention & {
@ -50,12 +47,11 @@
}
.left-side {
margin-right: var(--status-margin);
flex: 0 0 auto;
}
.right-side {
flex: 1;
min-width: 0;
flex: 1 1 auto;
}
.usercard {
@ -230,29 +226,47 @@
}
.repeat-info {
display: flex;
align-items: center;
padding: 0.4em var(--status-margin);
.repeat-icon {
color: var(--cGreen);
.repeater-avatar {
flex: 0 0 1.5em;
border-radius: var(--roundness);
margin-left: 2em; // 3.5 (poster avatar size) - 1.5 (repeater avatar size)
width: 1.5em;
height: 1.5em;
}
}
.repeater-avatar {
border-radius: var(--roundness);
margin-left: 2em; // 3.5 (poster avatar size) - 1.5 (repeater avatar size)
width: 1.5em;
height: 1.5em;
}
.right-side {
display: flex;
flex: 1 1 auto;
overflow-x: hidden;
text-overflow: ellipsis;
margin-right: 0;
gap: 0.5em;
.repeater-name {
text-overflow: ellipsis;
margin-right: 0;
.repeater-name {
flex: 0 1 auto;
margin: 0;
}
.emoji {
width: 1em;
height: 1em;
vertical-align: middle;
object-fit: contain;
.repeat-label {
white-space: nowrap;
flex: 0 0 auto;
.repeat-icon {
vertical-align: middle;
color: var(--cGreen);
}
}
.emoji {
width: 1em;
height: 1em;
vertical-align: middle;
object-fit: contain;
}
}
}
@ -371,21 +385,4 @@
text-decoration: underline;
}
}
@media all and (width <= 800px) {
.repeater-avatar {
margin-left: 20px;
}
.post-avatar {
width: 40px;
height: 40px;
// TODO define those other way somehow?
&.-compact {
width: 32px;
height: 32px;
}
}
}
}

View file

@ -46,16 +46,6 @@
</div>
</template>
<template v-else>
<div
v-if="showPinned"
class="pin"
>
<FAIcon
icon="thumbtack"
class="faint"
/>
<span class="faint">{{ $t('status.pinned') }}</span>
</div>
<div
v-if="retweet && !noHeading && !inConversation"
:class="[repeaterClass, { highlighted: repeaterStyle }]"
@ -80,6 +70,7 @@
<RichContent
:html="retweeterHtml"
:emoji="retweeterUser.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
:is-local="retweeterUser.is_local"
/>
</router-link>
@ -88,13 +79,14 @@
:to="retweeterProfileLink"
>{{ retweeter }}</router-link>
</bdi>
{{ ' ' }}
<FAIcon
icon="retweet"
class="repeat-icon"
:title="$t('tool_tip.repeat')"
/>
{{ $t('timeline.repeated') }}
<div class="repeat-label">
<FAIcon
icon="retweet"
class="repeat-icon"
:title="$t('tool_tip.repeat')"
/>
{{ $t('timeline.repeated') }}
</div>
</div>
</div>
@ -152,6 +144,7 @@
<RichContent
:html="status.user.name"
:emoji="status.user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
:is-local="status.user.is_local"
/>
</h4>
@ -176,6 +169,16 @@
</div>
<span class="heading-right">
<span
v-if="showPinned"
class="pin"
>
<FAIcon
icon="thumbtack"
class="faint"
/>
<span class="faint">{{ $t('status.pinned') }}</span>
</span>
<router-link
class="timeago faint"
:to="{ name: 'conversation', params: { id: status.id } }"
@ -519,7 +522,6 @@
:status="status"
:replying="replying"
@toggle-replying="toggleReplying"
@interacted="e => $emit('interacted')"
/>
</div>
</div>

View file

@ -4,6 +4,7 @@ import StatusBookmarkFolderMenu from 'src/components/status_bookmark_folder_menu
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 {
@ -13,8 +14,8 @@ import {
import {
faBookmark,
faCheck,
faChevronDown,
faChevronRight,
faChevronUp,
faExternalLinkAlt,
faEyeSlash,
faHistory,
@ -38,7 +39,7 @@ library.add(
faWrench,
faChevronRight,
faChevronUp,
faChevronDown,
faReply,
faRetweet,
@ -67,7 +68,6 @@ export default {
'doAction',
'outerClose',
],
emits: ['interacted'],
components: {
StatusBookmarkFolderMenu,
EmojiPicker,
@ -97,6 +97,9 @@ export default {
return !useInstanceCapabilitiesStore()
.pleromaCustomEmojiReactionsAvailable
},
hidePostStats() {
return useMergedConfigStore().mergedConfig.hidePostStats
},
buttonInnerClass() {
return [
this.button.name + '-button',
@ -128,6 +131,12 @@ export default {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
}
},
onShowEmojiPicker() {
this.$emit('emojiPickerShown', true)
},
onHideEmojiPicker() {
this.$emit('emojiPickerShown', false)
},
doActionWrap(
button,
close = () => {
@ -138,9 +147,8 @@ export default {
this.button.interactive ? !this.button.interactive(this.funcArg) : false
)
return
this.$emit('interacted')
if (button.name === 'emoji') {
this.$refs.picker.showPicker()
this.$refs.picker.togglePicker()
} else {
this.animationState = true
this.getComponent(button) === 'button' && this.doAction(button)

View file

@ -6,19 +6,24 @@
display: flex;
align-items: baseline;
align-items: center;
height: 1.5em;
border: 2px solid transparent;
.chevron-popover {
.popover-trigger-button {
display: flex;
}
}
.action-counter {
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-left: 1em;
}
.action-button-inner,
.extra-button {
margin: -0.5em;
padding: 0.5em;
z-index: 1;
}
.separator {
@ -26,27 +31,13 @@
align-self: stretch;
width: 1px;
background-color: var(--icon);
margin-left: 0.75em;
margin-right: 0.125em;
}
&.-pin {
margin: calc(-2px - 0.25em);
padding: 0.25em;
border: 2px dashed var(--icon);
border-radius: var(--roundness);
grid-template-columns: minmax(max-content, 1fr) auto;
.chevron-icon,
.extra-button,
.separator {
display: none;
}
opacity: 0.75;
margin: 0.5em 0;
}
.action-button-inner {
display: grid;
grid-gap: 1em;
grid-gap: 0.125em;
grid-template-columns: max-content;
grid-auto-flow: column;
grid-auto-columns: max-content;
@ -72,6 +63,32 @@
}
}
}
&.-pin {
border: 2px dashed var(--icon);
border-radius: var(--roundness);
grid-template-columns: minmax(max-content, 1fr) auto;
.action-button-inner {
opacity: 0.8;
padding-right: 0;
pointer-events: none;
}
.chevron-icon,
.extra-button,
.separator {
display: none;
}
}
&.-with-extra {
.action-button-inner,
.extra-button {
padding-left: 0.25em;
padding-right: 0.25em;
}
}
}
.action-button {
@ -106,7 +123,7 @@
&.-extra {
.action-counter {
justify-self: end;
margin-right: 1em;
margin-left: 1em;
}
.chevron-icon {

View file

@ -9,6 +9,7 @@
:class="buttonInnerClass"
role="menuitem"
type="button"
placement="bottom"
:title="$t(button.label(funcArg))"
target="_blank"
:tabindex="0"
@ -26,21 +27,21 @@
/>
<template v-if="!buttonClass.disabled && (!button.interactive || button?.interactive(funcArg)) && button.toggleable?.(funcArg) && button.active">
<FAIcon
v-if="button.active(funcArg)"
v-if="button.active(funcArg) && button.activeIndicator?.() !== null"
class="active-marker"
transform="shrink-6 up-9 right-15"
transform="shrink-6 up-9 left-12"
:icon="button.activeIndicator?.(funcArg) || 'check'"
/>
<FAIcon
v-if="!button.active(funcArg)"
class="focus-marker"
transform="shrink-6 up-9 right-15"
transform="shrink-6 up-9 left-12"
:icon="button.openIndicator?.(funcArg) || 'plus'"
/>
<FAIcon
v-else
class="focus-marker"
transform="shrink-6 up-9 right-15"
transform="shrink-6 up-9 left-12"
:icon="button.closeIndicator?.(funcArg) || 'minus'"
/>
</template>
@ -54,13 +55,12 @@
<FAIcon
v-if="button.dropdown?.()"
class="chevron-icon"
size="lg"
:icon="extra ? 'chevron-right' : 'chevron-up'"
:icon="extra ? 'chevron-right' : 'chevron-down'"
fixed-width
/>
</component>
<span
v-if="button.counter?.(funcArg) > 0"
v-if="!hidePostStats && button.counter?.(funcArg) > 0"
class="action-counter"
>
{{ button.counter?.(funcArg) }}
@ -71,16 +71,16 @@
/>
<Popover
v-if="button.name === 'bookmark'"
class="chevron-popover"
:trigger="extra ? 'hover' : 'click'"
:placement="extra ? 'right' : 'top'"
:placement="extra ? 'right' : 'bottom'"
:offset="extra ? { x: 10 } : { y: 10 }"
:trigger-attrs="{ class: 'extra-button' }"
>
<template #trigger>
<FAIcon
class="chevron-icon"
size="lg"
:icon="extra ? 'chevron-right' : 'chevron-up'"
:icon="extra ? 'chevron-right' : 'chevron-down'"
fixed-width
/>
</template>
@ -100,6 +100,8 @@
:hide-custom-emoji="hideCustomEmoji"
class="emoji-picker-panel"
@emoji="addReaction"
@show="onShowEmojiPicker"
@close="onHideEmojiPicker"
/>
</div>
</template>

View file

@ -20,7 +20,7 @@ export default {
UserTimedFilterModal,
},
props: ['button', 'status'],
emits: ['interacted'],
emits: ['emojiPickerShown'],
mounted() {
if (this.button.name === 'mute') {
this.$store.dispatch('fetchDomainMutes')

View file

@ -4,7 +4,7 @@
v-if="button.dropdown?.()"
:trigger="$attrs.extra ? 'hover' : 'click'"
:offset="{ y: 5 }"
:placement="$attrs.extra ? 'right' : 'top'"
:placement="$attrs.extra ? 'right' : 'bottom'"
>
<template #trigger>
<ActionButton
@ -79,7 +79,7 @@
:button="button"
:status="status"
v-bind="$attrs"
@interacted="e => $emit('interacted')"
@emojiPickerShown="e => $emit('emojiPickerShown', e)"
/>
<teleport to="#modal">
<MuteConfirm

View file

@ -21,7 +21,7 @@ export const BUTTONS = [
anonLink: true,
toggleable: true,
closeIndicator: 'times',
activeIndicator: 'none',
activeIndicator: null,
action({ emit }) {
emit('toggleReplying')
return Promise.resolve()
@ -97,6 +97,9 @@ export const BUTTONS = [
name: 'emoji',
label: 'tool_tip.add_reaction',
icon: ['far', 'smile-beam'],
interactive: () => true,
active: ({ emojiPickerShown }) => emojiPickerShown,
toggleable: true,
anonLink: true,
},
{

View file

@ -16,7 +16,7 @@ library.add(faEllipsisH)
const StatusActionButtons = {
props: ['status', 'replying'],
emits: ['toggleReplying', 'interacted', 'onSuccess', 'onError'],
emits: ['toggleReplying', 'onSuccess', 'onError'],
data() {
return {
showPin: false,
@ -28,6 +28,7 @@ const StatusActionButtons = {
/* no-op */
},
randomSeed: genRandomSeed(),
emojiPickerShown: false,
}
},
components: {
@ -56,6 +57,7 @@ const StatusActionButtons = {
return {
status: this.status,
replying: this.replying,
emojiPickerShown: this.emojiPickerShown,
emit: this.$emit,
dispatch: this.$store.dispatch,
state: this.$store.state,
@ -107,6 +109,9 @@ const StatusActionButtons = {
onExtraClose() {
this.showPin = false
},
onEmojiPickerShown(state) {
this.emojiPickerShown = state
},
isPinned(button) {
return this.pinnedItems.has(button.name)
},

View file

@ -3,16 +3,19 @@
.StatusActionButtons {
.quick-action-buttons {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10%, 3em));
margin-left: -0.5em;
grid-template-columns: repeat(auto-fill, minmax(3.75em, 10%));
grid-auto-flow: row dense;
grid-auto-rows: 1fr;
grid-gap: 1.25em 0;
grid-gap: 0.5em 0.1em;
margin-top: var(--status-margin);
}
.pin-action-button {
margin: -0.5em;
display: flex;
z-index: 1;
padding: 0.5em;
margin: 0;
}
}
// popover

View file

@ -1,11 +1,14 @@
<template>
<div class="StatusActionButtons">
<span class="quick-action-buttons">
<span
class="quick-action-buttons"
:class="{ '-pin': showPin }"
>
<span
v-for="button in quickButtons"
:key="button.name"
class="quick-action"
:class="{ '-pin': showPin, '-toggle': button.dropdown?.() }"
:class="{ '-pin': showPin, '-toggle': button.dropdown?.(), '-with-extra': button.name === 'bookmark' }"
>
<ActionButtonContainer
:class="{ '-pin': showPin }"
@ -17,7 +20,7 @@
:get-component="getComponent"
:close="() => { /* no-op */ }"
:do-action="doAction"
@interacted="e => $emit('interacted')"
@emojiPickerShown="onEmojiPickerShown"
/>
<button
v-if="showPin && currentUser"
@ -30,7 +33,6 @@
<FAIcon
v-if="showPin && currentUser"
fixed-width
class="fa-scale-110"
icon="thumbtack"
/>
</button>
@ -38,15 +40,16 @@
<Popover
trigger="click"
:trigger-attrs="triggerAttrs"
class="quick-action"
:tabindex="0"
placement="top"
placement="bottom"
:offset="{ y: 5 }"
remove-padding
@close="onExtraClose"
>
<template #trigger>
<FAIcon
class="fa-scale-110 "
class="action-button-inner"
icon="ellipsis-h"
/>
</template>
@ -56,23 +59,6 @@
class="dropdown-menu extra-action-buttons"
role="menu"
>
<div
v-if="currentUser"
class="menu-item dropdown-item extra-action -icon"
>
<button
class="main-button"
role="menuitem"
:tabindex="0"
@click.stop="() => { resize(); showPin = !showPin }"
>
<FAIcon
class="fa-scale-110"
fixed-width
icon="wrench"
/><span>{{ $t('nav.edit_pinned') }}</span>
</button>
</div>
<div
v-for="button in extraButtons"
:key="button.name"
@ -89,7 +75,6 @@
:get-component="getComponent"
:outer-close="close"
:do-action="doAction"
@interacted="e => $emit('interacted')"
/>
<button
v-if="showPin && currentUser"
@ -108,6 +93,23 @@
/>
</button>
</div>
<div
v-if="currentUser"
class="menu-item dropdown-item extra-action -icon"
>
<button
class="main-button"
role="menuitem"
:tabindex="0"
@click.stop="() => { resize(); showPin = !showPin }"
>
<FAIcon
class="fa-scale-110"
fixed-width
icon="wrench"
/><span>{{ $t('nav.edit_pinned') }}</span>
</button>
</div>
</div>
</template>
</Popover>

View file

@ -43,6 +43,9 @@ const StatusBody = {
localCollapseSubjectDefault() {
return this.mergedConfig.collapseMessageWithSubject
},
allowNonSquareEmoji() {
return this.mergedConfig.nonSquareEmoji
},
// This is a bit hacky, but we want to approximate post height before rendering
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
// as well as approximate line count by counting characters and approximating ~80

View file

@ -138,8 +138,6 @@
align-items: start;
flex-direction: row;
--emoji-size: calc(var(--emojiSize, 32px) / 2);
& .body,
& .attachments {
max-height: 3.25em;

View file

@ -15,6 +15,7 @@
:html="status.summary_raw_html"
:emoji="status.emojis"
:is-local="status.isLocal"
:allow-non-square-emoji="allowNonSquareEmoji"
/>
<button
v-show="longSubject && showingLongSubject"
@ -47,6 +48,7 @@
:greentext="mergedConfig.greentext"
:attentions="status.attentions"
:is-local="status.is_local"
:allow-non-square-emoji="allowNonSquareEmoji"
@parse-ready="onParseReady"
/>
<div

View file

@ -16,6 +16,7 @@
:is-preview="true"
:statusoid="status"
:compact="true"
:ignore-mute="true"
/>
<div
v-else-if="error"

View file

@ -0,0 +1,74 @@
import Popover from 'components/popover/popover.vue'
import SelectComponent from 'components/select/select.vue'
import { mapState } from 'pinia'
import StillImage from './still-image.vue'
import { useEmojiStore } from 'src/stores/emoji'
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 {
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()
})
},
},
}

Some files were not shown because too many files have changed in this diff Show more