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] const warning = warnings[i]
console.warn(' ' + warning) console.warn(' ' + warning)
} }
console.warn()
process.exit(1) 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: () => ({ data: () => ({
mobileActivePanel: 'timeline', mobileActivePanel: 'timeline',
}), }),
provide() {
return {
allowNonSquareEmoji: useMergedConfigStore().mergedConfig.nonSquareEmoji,
}
},
watch: { watch: {
themeApplied() { themeApplied() {
this.removeSplash() this.removeSplash()

View file

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

View file

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

View file

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

View file

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

View file

@ -1,11 +1,47 @@
<template> <template>
<div class="column-inner"> <div class="About column-inner">
<instance-specific-panel v-if="showInstanceSpecificPanel" /> <instance-specific-panel v-if="showInstanceSpecificPanel" />
<staff-panel /> <staff-panel />
<terms-of-service-panel /> <terms-of-service-panel />
<MRFTransparencyPanel /> <MRFTransparencyPanel />
<features-panel v-if="showFeaturesPanel" /> <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> </div>
</template> </template>
<script src="./about.js"></script> <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 nsfwImage from '../../assets/nsfw.png'
import Flash from '../flash/flash.vue' import Flash from '../flash/flash.vue'
import Popover from '../popover/popover.vue'
import StillImage from '../still-image/still-image.vue' import StillImage from '../still-image/still-image.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue' import VideoAttachment from '../video_attachment/video_attachment.vue'
@ -65,13 +66,13 @@ const Attachment = {
modalOpen: false, modalOpen: false,
showHidden: false, showHidden: false,
flashLoaded: false, flashLoaded: false,
showDescription: false,
} }
}, },
components: { components: {
Flash, Flash,
StillImage, StillImage,
VideoAttachment, VideoAttachment,
Popover,
}, },
computed: { computed: {
classNames() { classNames() {
@ -180,9 +181,6 @@ const Attachment = {
setFlashLoaded(event) { setFlashLoaded(event) {
this.flashLoaded = event this.flashLoaded = event
}, },
toggleDescription() {
this.showDescription = !this.showDescription
},
toggleHidden(event) { toggleHidden(event) {
if ( if (
this.mergedConfig.useOneClickNsfw && this.mergedConfig.useOneClickNsfw &&

View file

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

View file

@ -4,6 +4,7 @@ import UserLink from '../user_link/user_link.vue'
import UserPopover from '../user_popover/user_popover.vue' import UserPopover from '../user_popover/user_popover.vue'
import { useInstanceStore } from 'src/stores/instance.js' 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' 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 export default BasicUserCard

View file

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

View file

@ -54,7 +54,7 @@ export default {
{ {
variant: 'danger', variant: 'danger',
directives: { 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 RichContent from 'src/components/rich_content/rich_content.jsx'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
export default { export default {
name: 'ChatTitle', name: 'ChatTitle',
components: { components: {
@ -20,5 +22,8 @@ export default {
htmlTitle() { htmlTitle() {
return this.user ? this.user.name_html : '' return this.user ? this.user.name_html : ''
}, },
allowNonSquareEmoji() {
return useMergedConfigStore().mergedConfig.nonSquareEmoji
},
}, },
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,54 +32,57 @@
> >
</router-link> </router-link>
<div class="item right actions"> <div class="item right actions">
<search-bar <SearchBar
v-if="currentUser || !privateMode" v-if="currentUser || !privateMode"
@toggled="onSearchBarToggled" @toggled="onSearchBarToggled"
@click.stop @click.stop
/> />
<button <template v-if="searchBarHidden">
class="button-unstyled nav-icon" <button
:title="$t('nav.preferences')" class="button-unstyled nav-icon"
@click.stop="openSettingsModal('user')" :title="$t('nav.preferences')"
> @click.stop="openSettingsModal('user')"
<FAIcon >
fixed-width <FAIcon
class="fa-scale-110 fa-old-padding" fixed-width
icon="cog" class="fa-scale-110 fa-old-padding"
/> icon="cog"
</button> />
<button </button>
v-if="currentUser && currentUser.role === 'admin'" <button
class="button-unstyled nav-icon" v-if="currentUser && currentUser.role === 'admin'"
target="_blank" class="button-unstyled nav-icon"
:title="$t('nav.administration')" target="_blank"
@click.stop="openSettingsModal('admin')" :title="$t('nav.administration')"
> @click.stop="openSettingsModal('admin')"
<FAIcon >
fixed-width <FAIcon
class="fa-scale-110 fa-old-padding" fixed-width
icon="tachometer-alt" class="fa-scale-110 fa-old-padding"
/> icon="tachometer-alt"
</button> />
<span class="spacer" /> </button>
<button <span class="spacer" />
v-if="currentUser" <button
class="button-unstyled nav-icon" v-if="currentUser"
:title="$t('login.logout')" class="button-unstyled nav-icon"
@click.stop.prevent="logout" :title="$t('login.logout')"
> @click.stop.prevent="logout"
<FAIcon >
fixed-width <FAIcon
class="fa-scale-110 fa-old-padding" fixed-width
icon="sign-out-alt" class="fa-scale-110 fa-old-padding"
/> icon="sign-out-alt"
</button> />
</button>
</template>
</div> </div>
</div> </div>
<teleport to="#modal"> <teleport to="#modal">
<confirm-modal <confirm-modal
v-if="showingConfirmLogout" v-if="showingConfirmLogout"
:title="$t('login.logout_confirm_title')" :title="$t('login.logout_confirm_title')"
:confirm-danger="true"
:confirm-text="$t('login.logout_confirm_accept_button')" :confirm-text="$t('login.logout_confirm_accept_button')"
:cancel-text="$t('login.logout_confirm_cancel_button')" :cancel-text="$t('login.logout_confirm_cancel_button')"
@accepted="doLogout" @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 Draft from 'src/components/draft/draft.vue'
import List from 'src/components/list/list.vue' import List from 'src/components/list/list.vue'
@ -5,12 +6,31 @@ const Drafts = {
components: { components: {
Draft, Draft,
List, List,
ConfirmModal,
},
data() {
return {
showingConfirmDialog: false,
}
}, },
computed: { computed: {
drafts() { drafts() {
return this.$store.getters.draftsArray 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 export default Drafts

View file

@ -13,36 +13,66 @@
> >
{{ $t('drafts.no_drafts') }} {{ $t('drafts.no_drafts') }}
</div> </div>
<List <template v-else>
v-else <List
:items="drafts" :items="drafts"
:non-interactive="true" :non-interactive="true"
> >
<template #item="{ item: draft }"> <template #item="{ item: draft }">
<Draft <Draft
class="draft" class="draft"
:draft="draft" :draft="draft"
/> />
</template> </template>
</List> </List>
<div class="remove-all">
<button
class="btn -danger button-default"
@click="abandonAll"
>
{{ $t('drafts.clean_drafts') }}
</button>
</div>
</template>
</div> </div>
</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> </div>
</template> </template>
<script src="./drafts.js"></script> <script src="./drafts.js"></script>
<style lang="scss"> <style lang="scss">
.draft { .Drafts {
margin: 1em 0; .draft {
} margin: 1em 0;
}
.empty-drafs-list-alert { .remove-all {
padding: 3em; margin: 1em;
font-size: 1.2em; display: flex;
display: flex; justify-content: center;
justify-content: center; }
color: var(--textFaint);
.empty-drafs-list-alert {
padding: 3em;
font-size: 1.2em;
display: flex;
justify-content: center;
color: var(--textFaint);
}
} }
</style> </style>

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@
trigger="click" trigger="click"
popover-class="emoji-picker popover-default" popover-class="emoji-picker popover-default"
:hide-trigger="true" :hide-trigger="true"
placement="bottom"
@show="onPopoverShown" @show="onPopoverShown"
@close="onPopoverClosed" @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 UserListPopover from '../user_list_popover/user_list_popover.vue'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCheck, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons' import { faCheck, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'
@ -48,6 +49,9 @@ const EmojiReactions = {
statusId: this.status.id, statusId: this.status.id,
}) })
}, },
allowNonSquareEmoji() {
return useMergedConfigStore().mergedConfig.nonSquareEmoji
},
}, },
methods: { methods: {
toggleShowAll() { toggleShowAll() {

View file

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

View file

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

View file

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

View file

@ -47,6 +47,11 @@ const Gallery = {
: attachments : attachments
.reduce( .reduce(
(acc, attachment, i) => { (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') { if (attachment.type === 'audio') {
return [ return [
...acc, ...acc,
@ -61,18 +66,27 @@ const Gallery = {
{ items: [] }, { items: [] },
] ]
} }
const maxPerRow = 3 const maxPerRow = 3
const attachmentsRemaining = this.attachments.length - i + 1 const currentRow = acc[acc.length - 1]
const currentRow = acc[acc.length - 1].items const previousRow = acc[acc.length - 2]
currentRow.push(attachment)
if ( if (currentRow.items.length >= maxPerRow) {
currentRow.length >= maxPerRow && if (nextWide || nextEnd) {
attachmentsRemaining > maxPerRow if (previousRow?.items.length > 1) {
) { currentRow.items.push(attachment)
return [...acc, { items: [] }] return [...acc, { items: [] }]
} else {
const last = currentRow.items.splice(-1)[0]
return [...acc, { items: [last, attachment] }]
}
} else {
return [...acc, { items: [attachment] }]
}
} else { } else {
return acc currentRow.items.push(attachment)
} }
return acc
}, },
[{ items: [] }], [{ items: [] }],
) )

View file

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

View file

@ -11,3 +11,9 @@
</template> </template>
<script src="./instance_specific_panel.js"></script> <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> </tab-switcher>
<Notifications <Notifications
ref="notifications" ref="notifications"
:no-heading="true"
:no-extra="true" :no-extra="true"
:minimal-mode="true" :minimal-mode="true"
:filter-mode="filterMode" :filter-mode="filterMode"

View file

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

View file

@ -89,13 +89,16 @@
/> />
</button> </button>
<span <details
v-if="description" v-if="description"
open
class="description" class="description"
> >
{{ description }} <summary>{{ $t('status.attachment_description') }}</summary>
</span> <span>{{ description }}</span>
</details>
<span <span
v-if="media.length > 1"
class="counter" class="counter"
> >
{{ $t('media_modal.counter', { current: currentIndex + 1, total: media.length }, currentIndex + 1) }} {{ $t('media_modal.counter', { current: currentIndex + 1, total: media.length }, currentIndex + 1) }}
@ -159,19 +162,43 @@ $modal-view-button-icon-margin: 0.5em;
.counter { .counter {
/* Hardcoded since background is also hardcoded */ /* Hardcoded since background is also hardcoded */
color: white; color: white;
margin-top: 1em; text-shadow: 0 0 1em black, 0 0 1em black, 0 0 1em black;
text-shadow: 0 0 10px black, 0 0 10px black; margin: 1em 2em;
padding: 0.2em 2em; overflow: hidden;
}
.description + .counter {
margin-top: 0;
} }
.description { .description {
flex: 0 0 auto; flex: 0 0 auto;
overflow-y: auto; max-width: 80ch;
min-height: 1em;
max-width: 35.8em;
max-height: 9.5em; 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 { .modal-image {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -304,10 +304,13 @@ const Popover = {
} }
this.scrollable.addEventListener('scroll', this.onScroll) this.scrollable.addEventListener('scroll', this.onScroll)
this.scrollable.addEventListener('resize', this.onResize) 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') if (wasHidden) this.$emit('show')
this.updateStyles() this.updateStyles()
}) }, 1)
}, },
hidePopover() { hidePopover() {
if (this.disabled) return if (this.disabled) return

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -112,6 +112,12 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
// Allow wide emoji (max 3:1 ratio)
allowNonSquareEmoji: {
required: false,
type: Boolean,
default: false,
},
}, },
// NEVER EVER TOUCH DATA INSIDE RENDER // NEVER EVER TOUCH DATA INSIDE RENDER
render() { render() {
@ -322,7 +328,13 @@ export default {
// slots updated -> rerender -> emit -> update up the tree -> rerender -> ... // slots updated -> rerender -> emit -> update up the tree -> rerender -> ...
// at least until vue3? // at least until vue3?
const result = ( const result = (
<span class={['RichContent', this.faint ? '-faint' : '']}> <span
class={[
'RichContent',
this.faint ? '-faint' : '',
this.allowNonSquareEmoji ? '-allow-non-square-emoji' : '',
]}
>
{this.collapse {this.collapse
? pass2.map((x) => { ? pass2.map((x) => {
if (!Array.isArray(x)) return x.replace(/\n/g, ' ') if (!Array.isArray(x)) return x.replace(/\n/g, ' ')

View file

@ -77,6 +77,14 @@
height: var(--emoji-size, 32px); 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, .img,
video { video {
max-width: 100%; max-width: 100%;

View file

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

View file

@ -80,10 +80,16 @@ const present = computed(() => props.modelValue[props.selectedId] != null)
const moveUp = async () => { const moveUp = async () => {
const newModel = [...props.modelValue] const newModel = [...props.modelValue]
const movable = newModel.splice(props.selectedId, 1)[0] const movableId = Number(props.selectedId)
newModel.splice(props.selectedId - 1, 0, movable)
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() await nextTick()
emit('update:selectedId', props.selectedId - 1) emit('update:selectedId', props.selectedId - 1)
} }
@ -94,12 +100,18 @@ const moveDnValid = computed(() => {
const moveDn = async () => { const moveDn = async () => {
const newModel = [...props.modelValue] const newModel = [...props.modelValue]
const movable = newModel.splice(props.selectedId.value, 1)[0] const movableId = Number(props.selectedId)
newModel.splice(props.selectedId + 1, 0, movable)
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() await nextTick()
emit('update:selectedId', props.selectedId + 1) emit('update:selectedId', movableId + 1)
} }
const add = async () => { 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 SharedComputedObject from '../helpers/shared_computed_object.js'
import StringSetting from '../helpers/string_setting.vue' import StringSetting from '../helpers/string_setting.vue'
import { useEmojiStore } from 'src/stores/emoji.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
@ -174,63 +175,25 @@ const EmojiTab = {
this.sortPackFiles(packName) this.sortPackFiles(packName)
}, },
loadPacksPaginated(listFunction) {
const pageSize = 25
const allPacks = {}
return listFunction({
instance: this.remotePackInstance,
page: 1,
pageSize: 0,
})
.then((data) => data.json())
.then((data) => {
if (data.error !== undefined) {
return Promise.reject(data.error)
}
let resultingPromise = Promise.resolve({})
for (let i = 0; i < Math.ceil(data.count / pageSize); i++) {
resultingPromise = resultingPromise
.then(() =>
listFunction({
instance: this.remotePackInstance,
page: i,
pageSize,
}),
)
.then((data) => data.json())
.then((pageData) => {
if (pageData.error !== undefined) {
return Promise.reject(pageData.error)
}
assign(allPacks, pageData.packs)
})
}
return resultingPromise
})
.then(() => allPacks)
.catch((data) => {
this.displayError(data)
})
},
refreshPackList() { refreshPackList() {
this.loadPacksPaginated( useEmojiStore()
this.$store.state.api.backendInteractor.listEmojiPacks, .getAdminPacks(
).then((allPacks) => { this.remotePackInstance,
this.knownLocalPacks = allPacks this.$store.state.api.backendInteractor.listEmojiPacks,
for (const name of Object.keys(this.knownLocalPacks)) { )
this.sortPackFiles(name) .then((allPacks) => {
} this.knownLocalPacks = allPacks
}) for (const name of Object.keys(this.knownLocalPacks)) {
this.sortPackFiles(name)
}
})
}, },
listRemotePacks() { listRemotePacks() {
this.loadPacksPaginated( useEmojiStore()
this.$store.state.api.backendInteractor.listRemoteEmojiPacks, .getAdminPacks(
) this.remotePackInstance,
this.$store.state.api.backendInteractor.listRemoteEmojiPacks,
)
.then((allPacks) => { .then((allPacks) => {
let inst = this.remotePackInstance let inst = this.remotePackInstance
if (!inst.startsWith('http')) { if (!inst.startsWith('http')) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -208,6 +208,23 @@
{{ $t('settings.reset') }} {{ $t('settings.reset') }}
</button> </button>
</div> </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 <button
v-if="!isDefaultBackground" v-if="!isDefaultBackground"
class="btn button-default reset-button" class="btn button-default reset-button"
@ -246,6 +263,11 @@
{{ $t('settings.hide_wallpaper') }} {{ $t('settings.hide_wallpaper') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<BooleanSetting path="compactProfiles">
{{ $t('settings.compact_profiles') }}
</BooleanSetting>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -50,6 +50,16 @@
</li> </li>
</ul> </ul>
</li> </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> </ul>
<h3>{{ $t('settings.attachments') }}</h3> <h3>{{ $t('settings.attachments') }}</h3>
<ul class="setting-list"> <ul class="setting-list">
@ -77,16 +87,6 @@
{{ $t('settings.hide_attachments_in_convo') }} {{ $t('settings.hide_attachments_in_convo') }}
</BooleanSetting> </BooleanSetting>
</li> </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> </ul>
</div> </div>
</div> </div>

View file

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

View file

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

View file

@ -51,7 +51,16 @@
text-align: right; 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 { .filter-field-value {
display: flex;
grid-column: 2 / span 2; grid-column: 2 / span 2;
} }
} }

View file

@ -254,6 +254,20 @@
:value="filter[1].value" :value="filter[1].value"
@input="updateFilter(filter[0], 'value', $event.target.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>
<div class="filter-expires filter-field"> <div class="filter-expires filter-field">
<label <label

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -23,21 +23,18 @@
.status-container { .status-container {
display: flex; display: flex;
padding: var(--status-margin); padding: var(--status-margin);
gap: var(--status-margin);
> * { > * {
min-width: 0; min-width: 0;
} }
&.-repeat {
padding-top: 0;
}
} }
.pin { .pin {
padding: var(--status-margin) var(--status-margin) 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
margin-right: 0.5em;
} }
._misclick-prevention & { ._misclick-prevention & {
@ -50,12 +47,11 @@
} }
.left-side { .left-side {
margin-right: var(--status-margin); flex: 0 0 auto;
} }
.right-side { .right-side {
flex: 1; flex: 1 1 auto;
min-width: 0;
} }
.usercard { .usercard {
@ -230,29 +226,47 @@
} }
.repeat-info { .repeat-info {
display: flex;
align-items: center;
padding: 0.4em var(--status-margin); padding: 0.4em var(--status-margin);
.repeat-icon { .repeater-avatar {
color: var(--cGreen); 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 { .right-side {
border-radius: var(--roundness); display: flex;
margin-left: 2em; // 3.5 (poster avatar size) - 1.5 (repeater avatar size) flex: 1 1 auto;
width: 1.5em; overflow-x: hidden;
height: 1.5em; text-overflow: ellipsis;
} margin-right: 0;
gap: 0.5em;
.repeater-name { .repeater-name {
text-overflow: ellipsis; flex: 0 1 auto;
margin-right: 0; margin: 0;
}
.emoji { .repeat-label {
width: 1em; white-space: nowrap;
height: 1em; flex: 0 0 auto;
vertical-align: middle;
object-fit: contain; .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; 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> </div>
</template> </template>
<template v-else> <template v-else>
<div
v-if="showPinned"
class="pin"
>
<FAIcon
icon="thumbtack"
class="faint"
/>
<span class="faint">{{ $t('status.pinned') }}</span>
</div>
<div <div
v-if="retweet && !noHeading && !inConversation" v-if="retweet && !noHeading && !inConversation"
:class="[repeaterClass, { highlighted: repeaterStyle }]" :class="[repeaterClass, { highlighted: repeaterStyle }]"
@ -80,6 +70,7 @@
<RichContent <RichContent
:html="retweeterHtml" :html="retweeterHtml"
:emoji="retweeterUser.emoji" :emoji="retweeterUser.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
:is-local="retweeterUser.is_local" :is-local="retweeterUser.is_local"
/> />
</router-link> </router-link>
@ -88,13 +79,14 @@
:to="retweeterProfileLink" :to="retweeterProfileLink"
>{{ retweeter }}</router-link> >{{ retweeter }}</router-link>
</bdi> </bdi>
{{ ' ' }} <div class="repeat-label">
<FAIcon <FAIcon
icon="retweet" icon="retweet"
class="repeat-icon" class="repeat-icon"
:title="$t('tool_tip.repeat')" :title="$t('tool_tip.repeat')"
/> />
{{ $t('timeline.repeated') }} {{ $t('timeline.repeated') }}
</div>
</div> </div>
</div> </div>
@ -152,6 +144,7 @@
<RichContent <RichContent
:html="status.user.name" :html="status.user.name"
:emoji="status.user.emoji" :emoji="status.user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
:is-local="status.user.is_local" :is-local="status.user.is_local"
/> />
</h4> </h4>
@ -176,6 +169,16 @@
</div> </div>
<span class="heading-right"> <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 <router-link
class="timeago faint" class="timeago faint"
:to="{ name: 'conversation', params: { id: status.id } }" :to="{ name: 'conversation', params: { id: status.id } }"
@ -519,7 +522,6 @@
:status="status" :status="status"
:replying="replying" :replying="replying"
@toggle-replying="toggleReplying" @toggle-replying="toggleReplying"
@interacted="e => $emit('interacted')"
/> />
</div> </div>
</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 { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.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 { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -13,8 +14,8 @@ import {
import { import {
faBookmark, faBookmark,
faCheck, faCheck,
faChevronDown,
faChevronRight, faChevronRight,
faChevronUp,
faExternalLinkAlt, faExternalLinkAlt,
faEyeSlash, faEyeSlash,
faHistory, faHistory,
@ -38,7 +39,7 @@ library.add(
faWrench, faWrench,
faChevronRight, faChevronRight,
faChevronUp, faChevronDown,
faReply, faReply,
faRetweet, faRetweet,
@ -67,7 +68,6 @@ export default {
'doAction', 'doAction',
'outerClose', 'outerClose',
], ],
emits: ['interacted'],
components: { components: {
StatusBookmarkFolderMenu, StatusBookmarkFolderMenu,
EmojiPicker, EmojiPicker,
@ -97,6 +97,9 @@ export default {
return !useInstanceCapabilitiesStore() return !useInstanceCapabilitiesStore()
.pleromaCustomEmojiReactionsAvailable .pleromaCustomEmojiReactionsAvailable
}, },
hidePostStats() {
return useMergedConfigStore().mergedConfig.hidePostStats
},
buttonInnerClass() { buttonInnerClass() {
return [ return [
this.button.name + '-button', this.button.name + '-button',
@ -128,6 +131,12 @@ export default {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
} }
}, },
onShowEmojiPicker() {
this.$emit('emojiPickerShown', true)
},
onHideEmojiPicker() {
this.$emit('emojiPickerShown', false)
},
doActionWrap( doActionWrap(
button, button,
close = () => { close = () => {
@ -138,9 +147,8 @@ export default {
this.button.interactive ? !this.button.interactive(this.funcArg) : false this.button.interactive ? !this.button.interactive(this.funcArg) : false
) )
return return
this.$emit('interacted')
if (button.name === 'emoji') { if (button.name === 'emoji') {
this.$refs.picker.showPicker() this.$refs.picker.togglePicker()
} else { } else {
this.animationState = true this.animationState = true
this.getComponent(button) === 'button' && this.doAction(button) this.getComponent(button) === 'button' && this.doAction(button)

View file

@ -6,19 +6,24 @@
display: flex; display: flex;
align-items: baseline; align-items: baseline;
align-items: center; align-items: center;
height: 1.5em; border: 2px solid transparent;
.chevron-popover {
.popover-trigger-button {
display: flex;
}
}
.action-counter { .action-counter {
overflow-x: hidden; overflow-x: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
margin-left: 1em;
} }
.action-button-inner, .action-button-inner,
.extra-button { .extra-button {
margin: -0.5em;
padding: 0.5em; padding: 0.5em;
z-index: 1;
} }
.separator { .separator {
@ -26,27 +31,13 @@
align-self: stretch; align-self: stretch;
width: 1px; width: 1px;
background-color: var(--icon); background-color: var(--icon);
margin-left: 0.75em; opacity: 0.75;
margin-right: 0.125em; margin: 0.5em 0;
}
&.-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;
}
} }
.action-button-inner { .action-button-inner {
display: grid; display: grid;
grid-gap: 1em; grid-gap: 0.125em;
grid-template-columns: max-content; grid-template-columns: max-content;
grid-auto-flow: column; grid-auto-flow: column;
grid-auto-columns: max-content; 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 { .action-button {
@ -106,7 +123,7 @@
&.-extra { &.-extra {
.action-counter { .action-counter {
justify-self: end; justify-self: end;
margin-right: 1em; margin-left: 1em;
} }
.chevron-icon { .chevron-icon {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,16 +3,19 @@
.StatusActionButtons { .StatusActionButtons {
.quick-action-buttons { .quick-action-buttons {
display: grid; 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-flow: row dense;
grid-auto-rows: 1fr; grid-auto-rows: 1fr;
grid-gap: 1.25em 0; grid-gap: 0.5em 0.1em;
margin-top: var(--status-margin); margin-top: var(--status-margin);
} }
.pin-action-button { .pin-action-button {
margin: -0.5em; display: flex;
z-index: 1;
padding: 0.5em; padding: 0.5em;
margin: 0;
} }
} }
// popover // popover

View file

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

View file

@ -43,6 +43,9 @@ const StatusBody = {
localCollapseSubjectDefault() { localCollapseSubjectDefault() {
return this.mergedConfig.collapseMessageWithSubject return this.mergedConfig.collapseMessageWithSubject
}, },
allowNonSquareEmoji() {
return this.mergedConfig.nonSquareEmoji
},
// This is a bit hacky, but we want to approximate post height before rendering // 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) // 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 // as well as approximate line count by counting characters and approximating ~80

View file

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

View file

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

View file

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