commit: ebd3b7d9f569186a612b5f019deaa895c5cf3417
parent eea173cf7e0651bedda62aac5ba6d77cbbf0ef1e
Author: HJ <30-hj@users.noreply.git.pleroma.social>
Date: Tue, 21 Jan 2025 09:42:36 +0000
Merge branch 'customizable-post-actions' into 'develop'
Customizable post actions
See merge request pleroma/pleroma-fe!1985
Diffstat:
56 files changed, 2174 insertions(+), 1839 deletions(-)
diff --git a/changelog.d/customizable-actions.add b/changelog.d/customizable-actions.add
@@ -0,0 +1 @@
+Post actions can be customized
diff --git a/src/App.scss b/src/App.scss
@@ -408,32 +408,11 @@ nav {
}
}
-.menu-item,
.list-item {
- display: block;
- box-sizing: border-box;
- border: none;
- outline: none;
- text-align: initial;
- color: inherit;
- clear: both;
- position: relative;
- white-space: nowrap;
border-color: var(--border);
border-style: solid;
border-width: 0;
border-top-width: 1px;
- width: 100%;
- padding: var(--__vertical-gap) var(--__horizontal-gap);
- background: transparent;
-
- --__line-height: 1.5em;
- --__horizontal-gap: 0.75em;
- --__vertical-gap: 0.5em;
-
- &.-non-interactive {
- cursor: auto;
- }
&.-active,
&:hover {
@@ -455,18 +434,6 @@ nav {
border-bottom-width: 1px;
}
- a,
- button:not(.button-default) {
- text-align: initial;
- padding: 0;
- background: none;
- border: none;
- outline: none;
- display: inline;
- font-family: inherit;
- line-height: unset;
- }
-
&:first-child {
border-top-right-radius: var(--roundness);
border-top-left-radius: var(--roundness);
@@ -480,6 +447,42 @@ nav {
}
}
+.menu-item,
+.list-item {
+ display: block;
+ box-sizing: border-box;
+ border: none;
+ outline: none;
+ text-align: initial;
+ color: inherit;
+ clear: both;
+ position: relative;
+ white-space: nowrap;
+ width: 100%;
+ padding: var(--__vertical-gap) var(--__horizontal-gap);
+ background: transparent;
+
+ --__line-height: 1.5em;
+ --__horizontal-gap: 0.75em;
+ --__vertical-gap: 0.5em;
+
+ &.-non-interactive {
+ cursor: auto;
+ }
+
+ a,
+ button:not(.button-default) {
+ text-align: initial;
+ padding: 0;
+ background: none;
+ border: none;
+ outline: none;
+ display: inline;
+ font-family: inherit;
+ line-height: unset;
+ }
+}
+
.button-unstyled {
border: none;
outline: none;
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
@@ -9,60 +9,80 @@
<template #content>
<div class="dropdown-menu">
<template v-if="relationship.following">
- <button
+ <div
v-if="relationship.showing_reblogs"
- class="dropdown-item menu-item"
- @click="hideRepeats"
+ class="menu-item dropdown-item"
>
- {{ $t('user_card.hide_repeats') }}
- </button>
- <button
+ <button
+ class="main-button"
+ @click="hideRepeats"
+ >
+ {{ $t('user_card.hide_repeats') }}
+ </button>
+ </div>
+ <div
v-if="!relationship.showing_reblogs"
- class="dropdown-item menu-item"
- @click="showRepeats"
+ class="menu-item dropdown-item"
>
- {{ $t('user_card.show_repeats') }}
- </button>
+ <button
+ class="main-button"
+ @click="showRepeats"
+ >
+ {{ $t('user_card.show_repeats') }}
+ </button>
+ </div>
<div
role="separator"
class="dropdown-divider"
/>
</template>
<UserListMenu :user="user" />
- <button
+ <div
v-if="relationship.followed_by"
- class="dropdown-item menu-item"
- @click="removeUserFromFollowers"
+ class="menu-item dropdown-item"
>
- {{ $t('user_card.remove_follower') }}
- </button>
- <button
- v-if="relationship.blocking"
- class="dropdown-item menu-item"
- @click="unblockUser"
- >
- {{ $t('user_card.unblock') }}
- </button>
- <button
- v-else
- class="dropdown-item menu-item"
- @click="blockUser"
- >
- {{ $t('user_card.block') }}
- </button>
- <button
- class="dropdown-item menu-item"
- @click="reportUser"
- >
- {{ $t('user_card.report') }}
- </button>
- <button
+ <button
+ class="main-button"
+ @click="removeUserFromFollowers"
+ >
+ {{ $t('user_card.remove_follower') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item">
+ <button
+ v-if="relationship.blocking"
+ class="main-button"
+ @click="unblockUser"
+ >
+ {{ $t('user_card.unblock') }}
+ </button>
+ <button
+ v-else
+ class="main-button"
+ @click="blockUser"
+ >
+ {{ $t('user_card.block') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item">
+ <button
+ class="main-button"
+ @click="reportUser"
+ >
+ {{ $t('user_card.report') }}
+ </button>
+ </div>
+ <div
v-if="pleromaChatMessagesAvailable"
- class="dropdown-item menu-item"
- @click="openChat"
+ class="menu-item dropdown-item"
>
- {{ $t('user_card.message') }}
- </button>
+ <button
+ class="main-button"
+ @click="openChat"
+ >
+ {{ $t('user_card.message') }}
+ </button>
+ </div>
</div>
</template>
<template #trigger>
diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue
@@ -51,12 +51,14 @@
>
<template #content>
<div class="dropdown-menu">
- <button
- class="menu-item dropdown-item dropdown-item-icon"
- @click="deleteMessage"
- >
- <FAIcon icon="times" /> {{ $t("chats.delete") }}
- </button>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ @click="deleteMessage"
+ >
+ <FAIcon icon="times" /> {{ $t("chats.delete") }}
+ </button>
+ </div>
</div>
</template>
<template #trigger>
diff --git a/src/components/confirm_modal/mute_confirm.js b/src/components/confirm_modal/mute_confirm.js
@@ -0,0 +1,111 @@
+import { unitToSeconds } from 'src/services/date_utils/date_utils.js'
+import { mapGetters } from 'vuex'
+
+import ConfirmModal from './confirm_modal.vue'
+import Select from 'src/components/select/select.vue'
+
+export default {
+ props: ['type', 'user'],
+ emits: ['hide', 'show', 'muted'],
+ data: () => ({
+ showing: false,
+ muteExpiryAmount: 2,
+ muteExpiryUnit: 'hours'
+ }),
+ components: {
+ ConfirmModal,
+ Select
+ },
+ computed: {
+ muteExpiryValue () {
+ unitToSeconds(this.muteExpiryUnit, this.muteExpiryAmount)
+ },
+ muteExpiryUnits () {
+ return ['minutes', 'hours', 'days']
+ },
+ domain () {
+ return this.user.fqn.split('@')[1]
+ },
+ keypath () {
+ if (this.type === 'domain') {
+ return 'status.mute_domain_confirm'
+ } else if (this.type === 'conversation') {
+ return 'status.mute_conversation_confirm'
+ } else {
+ return 'user_card.mute_confirm'
+ }
+ },
+ userIsMuted () {
+ return this.$store.getters.relationship(this.user.id).muting
+ },
+ conversationIsMuted () {
+ return this.status.conversation_muted
+ },
+ domainIsMuted () {
+ return new Set(this.$store.state.users.currentUser.domainMutes).has(this.domain)
+ },
+ shouldConfirm () {
+ switch (this.type) {
+ case 'domain': {
+ return this.mergedConfig.modalOnMuteDomain
+ }
+ case 'conversation': {
+ return this.mergedConfig.modalOnMuteConversation
+ }
+ default: {
+ return this.mergedConfig.modalOnMute
+ }
+ }
+ },
+ ...mapGetters(['mergedConfig'])
+ },
+ methods: {
+ optionallyPrompt () {
+ console.log('Triggered')
+ if (this.shouldConfirm) {
+ console.log('SHAWN!!')
+ this.show()
+ } else {
+ this.doMute()
+ }
+ },
+ show () {
+ this.showing = true
+ this.$emit('show')
+ },
+ hide () {
+ this.showing = false
+ this.$emit('hide')
+ },
+ doMute () {
+ switch (this.type) {
+ case 'domain': {
+ if (!this.domainIsMuted) {
+ this.$store.dispatch('muteDomain', { id: this.domain, expiresIn: this.muteExpiryValue })
+ } else {
+ this.$store.dispatch('unmuteDomain', { id: this.domain })
+ }
+ break
+ }
+ case 'conversation': {
+ if (!this.conversationIsMuted) {
+ this.$store.dispatch('muteConversation', { id: this.status.id, expiresIn: this.muteExpiryValue })
+ } else {
+ this.$store.dispatch('unmuteConversation', { id: this.status.id })
+ }
+ break
+ }
+ default: {
+ if (!this.userIsMuted) {
+ this.$store.dispatch('muteUser', { id: this.user.id, expiresIn: this.muteExpiryValue })
+ } else {
+ this.$store.dispatch('unmuteUser', { id: this.user.id })
+ }
+ break
+ }
+ }
+ this.$emit('muted')
+ this.hide()
+ }
+ }
+}
diff --git a/src/components/confirm_modal/mute_confirm.vue b/src/components/confirm_modal/mute_confirm.vue
@@ -0,0 +1,61 @@
+<template>
+ <confirm-modal
+ v-if="showing"
+ :title="$t('user_card.mute_confirm_title')"
+ :confirm-text="$t('user_card.mute_confirm_accept_button')"
+ :cancel-text="$t('user_card.mute_confirm_cancel_button')"
+ @accepted="doMute"
+ @cancelled="hide"
+ >
+ <i18n-t
+ :keypath="keypath"
+ tag="div"
+ >
+ <template #domain>
+ <span v-text="domain" />
+ </template>
+ <template #user>
+ <span v-text="user.screen_name_ui" />
+ </template>
+ </i18n-t>
+ <div
+ v-if="type !== 'domain'"
+ class="mute-expiry"
+ >
+ <p>
+ <label>
+ {{ $t('user_card.mute_duration_prompt') }}
+ </label>
+ <input
+ v-model="muteExpiryAmount"
+ type="number"
+ class="input expiry-amount hide-number-spinner"
+ :min="0"
+ >
+ {{ ' ' }}
+ <Select
+ v-model="muteExpiryUnit"
+ unstyled="true"
+ class="expiry-unit"
+ >
+ <option
+ v-for="unit in muteExpiryUnits"
+ :key="unit"
+ :value="unit"
+ >
+ {{ $t(`time.unit.${unit}_short`, ['']) }}
+ </option>
+ </Select>
+ </p>
+ </div>
+ </confirm-modal>
+</template>
+
+<script src="./mute_confirm.js" />
+
+<style lang="scss">
+.expiry-amount {
+ width: 4em;
+ text-align: right;
+}
+</style>
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
@@ -350,6 +350,7 @@ const conversation = {
},
...mapGetters(['mergedConfig']),
...mapState({
+ mobileLayout: state => state.interface.layoutType === 'mobile',
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus
})
},
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
@@ -20,7 +20,7 @@
{{ $t('timeline.collapse') }}
</button>
<QuickFilterSettings
- v-if="!collapsable"
+ v-if="!collapsable && mobileLayout"
:conversation="true"
class="rightside-button"
/>
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
@@ -1,175 +0,0 @@
-import Popover from '../popover/popover.vue'
-import genRandomSeed from '../../services/random_seed/random_seed.service.js'
-import ConfirmModal from '../confirm_modal/confirm_modal.vue'
-import StatusBookmarkFolderMenu from '../status_bookmark_folder_menu/status_bookmark_folder_menu.vue'
-import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faEllipsisH,
- faBookmark,
- faEyeSlash,
- faThumbtack,
- faShareAlt,
- faExternalLinkAlt,
- faHistory,
- faPlus,
- faTimes
-} from '@fortawesome/free-solid-svg-icons'
-import {
- faBookmark as faBookmarkReg,
- faFlag
-} from '@fortawesome/free-regular-svg-icons'
-
-library.add(
- faEllipsisH,
- faBookmark,
- faBookmarkReg,
- faEyeSlash,
- faThumbtack,
- faShareAlt,
- faExternalLinkAlt,
- faFlag,
- faHistory,
- faPlus,
- faTimes
-)
-
-const ExtraButtons = {
- props: ['status'],
- components: {
- Popover,
- ConfirmModal,
- StatusBookmarkFolderMenu
- },
- data () {
- return {
- expanded: false,
- showingDeleteDialog: false,
- randomSeed: genRandomSeed()
- }
- },
- methods: {
- onShow () {
- this.expanded = true
- },
- onClose () {
- this.expanded = false
- },
- deleteStatus () {
- if (this.shouldConfirmDelete) {
- this.showDeleteStatusConfirmDialog()
- } else {
- this.doDeleteStatus()
- }
- },
- doDeleteStatus () {
- this.$store.dispatch('deleteStatus', { id: this.status.id })
- this.hideDeleteStatusConfirmDialog()
- },
- showDeleteStatusConfirmDialog () {
- this.showingDeleteDialog = true
- },
- hideDeleteStatusConfirmDialog () {
- this.showingDeleteDialog = false
- },
- pinStatus () {
- this.$store.dispatch('pinStatus', this.status.id)
- .then(() => this.$emit('onSuccess'))
- .catch(err => this.$emit('onError', err.error.error))
- },
- unpinStatus () {
- this.$store.dispatch('unpinStatus', this.status.id)
- .then(() => this.$emit('onSuccess'))
- .catch(err => this.$emit('onError', err.error.error))
- },
- muteConversation () {
- this.$store.dispatch('muteConversation', this.status.id)
- .then(() => this.$emit('onSuccess'))
- .catch(err => this.$emit('onError', err.error.error))
- },
- unmuteConversation () {
- this.$store.dispatch('unmuteConversation', this.status.id)
- .then(() => this.$emit('onSuccess'))
- .catch(err => this.$emit('onError', err.error.error))
- },
- copyLink () {
- navigator.clipboard.writeText(this.statusLink)
- .then(() => this.$emit('onSuccess'))
- .catch(err => this.$emit('onError', err.error.error))
- },
- bookmarkStatus () {
- this.$store.dispatch('bookmark', { id: this.status.id })
- .then(() => this.$emit('onSuccess'))
- .catch(err => this.$emit('onError', err.error.error))
- },
- unbookmarkStatus () {
- this.$store.dispatch('unbookmark', { id: this.status.id })
- .then(() => this.$emit('onSuccess'))
- .catch(err => this.$emit('onError', err.error.error))
- },
- reportStatus () {
- this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
- },
- editStatus () {
- this.$store.dispatch('fetchStatusSource', { id: this.status.id })
- .then(data => this.$store.dispatch('openEditStatusModal', {
- statusId: this.status.id,
- subject: data.spoiler_text,
- statusText: data.text,
- statusIsSensitive: this.status.nsfw,
- statusPoll: this.status.poll,
- statusFiles: [...this.status.attachments],
- visibility: this.status.visibility,
- statusContentType: data.content_type
- }))
- },
- showStatusHistory () {
- const originalStatus = { ...this.status }
- const stripFieldsList = ['attachments', 'created_at', 'emojis', 'text', 'raw_html', 'nsfw', 'poll', 'summary', 'summary_raw_html']
- stripFieldsList.forEach(p => delete originalStatus[p])
- this.$store.dispatch('openStatusHistoryModal', originalStatus)
- }
- },
- computed: {
- currentUser () { return this.$store.state.users.currentUser },
- canDelete () {
- if (!this.currentUser) { return }
- return this.currentUser.privileges.includes('messages_delete') || this.status.user.id === this.currentUser.id
- },
- ownStatus () {
- return this.status.user.id === this.currentUser.id
- },
- canPin () {
- return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted')
- },
- canMute () {
- return !!this.currentUser
- },
- canBookmark () {
- return !!this.currentUser
- },
- bookmarkFolders () {
- return this.$store.state.instance.pleromaBookmarkFoldersAvailable
- },
- statusLink () {
- return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
- },
- isEdited () {
- return this.status.edited_at !== null
- },
- editingAvailable () { return this.$store.state.instance.editingAvailable },
- shouldConfirmDelete () {
- return this.$store.getters.mergedConfig.modalOnDelete
- },
- triggerAttrs () {
- return {
- title: this.$t('status.more_actions'),
- id: `popup-trigger-${this.randomSeed}`,
- 'aria-controls': `popup-menu-${this.randomSeed}`,
- 'aria-expanded': this.expanded,
- 'aria-haspopup': 'menu'
- }
- }
- }
-}
-
-export default ExtraButtons
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
@@ -1,238 +0,0 @@
-<template>
- <Popover
- class="ExtraButtons"
- trigger="click"
- :trigger-attrs="triggerAttrs"
- placement="top"
- :offset="{ y: 5 }"
- :bound-to="{ x: 'container' }"
- remove-padding
- @show="onShow"
- @close="onClose"
- >
- <template #content="{close}">
- <div
- :id="`popup-menu-${randomSeed}`"
- class="dropdown-menu"
- role="menu"
- >
- <button
- v-if="canMute && !status.thread_muted"
- class="menu-item dropdown-item dropdown-item-icon"
- role="menuitem"
- @click.prevent="muteConversation"
- >
- <FAIcon
- fixed-width
- icon="eye-slash"
- /><span>{{ $t("status.mute_conversation") }}</span>
- </button>
- <button
- v-if="canMute && status.thread_muted"
- class="menu-item dropdown-item dropdown-item-icon"
- role="menuitem"
- @click.prevent="unmuteConversation"
- >
- <FAIcon
- fixed-width
- icon="eye-slash"
- /><span>{{ $t("status.unmute_conversation") }}</span>
- </button>
- <button
- v-if="!status.pinned && canPin"
- class="menu-item dropdown-item dropdown-item-icon"
- role="menuitem"
- @click.prevent="pinStatus"
- @click="close"
- >
- <FAIcon
- fixed-width
- icon="thumbtack"
- /><span>{{ $t("status.pin") }}</span>
- </button>
- <button
- v-if="status.pinned && canPin"
- class="menu-item dropdown-item dropdown-item-icon"
- role="menuitem"
- @click.prevent="unpinStatus"
- @click="close"
- >
- <FAIcon
- fixed-width
- icon="thumbtack"
- /><span>{{ $t("status.unpin") }}</span>
- </button>
- <template v-if="canBookmark">
- <button
- v-if="!status.bookmarked"
- class="menu-item dropdown-item dropdown-item-icon"
- role="menuitem"
- @click.prevent="bookmarkStatus"
- @click="close"
- >
- <FAIcon
- fixed-width
- :icon="['far', 'bookmark']"
- /><span>{{ $t("status.bookmark") }}</span>
- </button>
- <button
- v-if="status.bookmarked"
- class="menu-item dropdown-item dropdown-item-icon"
- role="menuitem"
- @click.prevent="unbookmarkStatus"
- @click="close"
- >
- <FAIcon
- fixed-width
- icon="bookmark"
- /><span>{{ $t("status.unbookmark") }}</span>
- </button>
- <StatusBookmarkFolderMenu
- v-if="status.bookmarked && bookmarkFolders"
- :status="status"
- />
- </template>
- <button
- v-if="ownStatus && editingAvailable"
- class="menu-item dropdown-item dropdown-item-icon"
- role="menuitem"
- @click.prevent="editStatus"
- @click="close"
- >
- <FAIcon
- fixed-width
- icon="pen"
- /><span>{{ $t("status.edit") }}</span>
- </button>
- <button
- v-if="isEdited && editingAvailable"
- class="menu-item dropdown-item dropdown-item-icon"
- role="menuitem"
- @click.prevent="showStatusHistory"
- @click="close"
- >
- <FAIcon
- fixed-width
- icon="history"
- /><span>{{ $t("status.status_history") }}</span>
- </button>
- <button
- v-if="canDelete"
- class="menu-item dropdown-item dropdown-item-icon"
- role="menuitem"
- @click.prevent="deleteStatus"
- @click="close"
- >
- <FAIcon
- fixed-width
- icon="times"
- /><span>{{ $t("status.delete") }}</span>
- </button>
- <button
- class="menu-item dropdown-item dropdown-item-icon"
- role="menuitem"
- @click.prevent="copyLink"
- @click="close"
- >
- <FAIcon
- fixed-width
- icon="share-alt"
- /><span>{{ $t("status.copy_link") }}</span>
- </button>
- <a
- v-if="!status.is_local"
- class="menu-item dropdown-item dropdown-item-icon"
- role="menuitem"
- title="Source"
- :href="status.external_url"
- target="_blank"
- >
- <FAIcon
- fixed-width
- icon="external-link-alt"
- /><span>{{ $t("status.external_source") }}</span>
- </a>
- <button
- class="menu-item dropdown-item dropdown-item-icon"
- role="menuitem"
- @click.prevent="reportStatus"
- @click="close"
- >
- <FAIcon
- fixed-width
- :icon="['far', 'flag']"
- /><span>{{ $t("user_card.report") }}</span>
- </button>
- </div>
- </template>
- <template #trigger>
- <span class="button-unstyled popover-trigger">
- <FALayers class="fa-old-padding-layer">
- <FAIcon
- class="fa-scale-110 "
- icon="ellipsis-h"
- />
- <FAIcon
- v-show="!expanded"
- class="focus-marker"
- transform="shrink-6 up-8 right-16"
- icon="plus"
- />
- <FAIcon
- v-show="expanded"
- class="focus-marker"
- transform="shrink-6 up-8 right-16"
- icon="times"
- />
- </FALayers>
- </span>
- <teleport to="#modal">
- <ConfirmModal
- v-if="showingDeleteDialog"
- :title="$t('status.delete_confirm_title')"
- :cancel-text="$t('status.delete_confirm_cancel_button')"
- :confirm-text="$t('status.delete_confirm_accept_button')"
- @cancelled="hideDeleteStatusConfirmDialog"
- @accepted="doDeleteStatus"
- >
- {{ $t('status.delete_confirm') }}
- </ConfirmModal>
- </teleport>
- </template>
- </Popover>
-</template>
-
-<script src="./extra_buttons.js"></script>
-
-<style lang="scss">
-@import "../../mixins";
-
-.ExtraButtons {
- .popover-trigger {
- position: static;
- padding: 10px;
- margin: -10px;
-
- &:hover .svg-inline--fa {
- color: var(--text);
- }
- }
-
- .popover-trigger-button {
- /* override of popover internal stuff */
- width: auto;
-
- @include unfocused-style {
- .focus-marker {
- visibility: hidden;
- }
- }
-
- @include focused-style {
- .focus-marker {
- visibility: visible;
- }
- }
- }
-}
-</style>
diff --git a/src/components/favorite_button/favorite_button.js b/src/components/favorite_button/favorite_button.js
@@ -1,49 +0,0 @@
-import { mapGetters } from 'vuex'
-import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faStar,
- faPlus,
- faMinus,
- faCheck
-} from '@fortawesome/free-solid-svg-icons'
-import {
- faStar as faStarRegular
-} from '@fortawesome/free-regular-svg-icons'
-
-library.add(
- faStar,
- faStarRegular,
- faPlus,
- faMinus,
- faCheck
-)
-
-const FavoriteButton = {
- props: ['status', 'loggedIn'],
- data () {
- return {
- animated: false
- }
- },
- methods: {
- favorite () {
- if (!this.status.favorited) {
- this.$store.dispatch('favorite', { id: this.status.id })
- } else {
- this.$store.dispatch('unfavorite', { id: this.status.id })
- }
- this.animated = true
- setTimeout(() => {
- this.animated = false
- }, 500)
- }
- },
- computed: {
- ...mapGetters(['mergedConfig']),
- remoteInteractionLink () {
- return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
- }
- }
-}
-
-export default FavoriteButton
diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue
@@ -1,114 +0,0 @@
-<template>
- <div class="FavoriteButton">
- <button
- v-if="loggedIn"
- class="button-unstyled interactive"
- :class="status.favorited && '-favorited'"
- :title="$t('tool_tip.favorite')"
- @click.prevent="favorite()"
- >
- <FALayers class="fa-scale-110 fa-old-padding-layer">
- <FAIcon
- class="fa-scale-110"
- :icon="[status.favorited ? 'fas' : 'far', 'star']"
- :spin="animated"
- />
- <FAIcon
- v-if="status.favorited"
- class="active-marker"
- transform="shrink-6 up-9 right-12"
- icon="check"
- />
- <FAIcon
- v-if="!status.favorited"
- class="focus-marker"
- transform="shrink-6 up-9 right-12"
- icon="plus"
- />
- <FAIcon
- v-else
- class="focus-marker"
- transform="shrink-6 up-9 right-12"
- icon="minus"
- />
- </FALayers>
- </button>
- <a
- v-else
- class="button-unstyled interactive"
- target="_blank"
- role="button"
- :title="$t('tool_tip.favorite')"
- :href="remoteInteractionLink"
- >
- <FALayers class="fa-scale-110 fa-old-padding-layer">
- <FAIcon
- class="fa-scale-110"
- :icon="['far', 'star']"
- />
- <FAIcon
- class="focus-marker"
- transform="shrink-6 up-9 right-12"
- icon="plus"
- />
- </FALayers>
- </a>
- <span
- v-if="!mergedConfig.hidePostStats && status.fave_num > 0"
- class="action-counter"
- >
- {{ status.fave_num }}
- </span>
- </div>
-</template>
-
-<script src="./favorite_button.js"></script>
-
-<style lang="scss">
-@import "../../mixins";
-
-.FavoriteButton {
- display: flex;
-
- > :first-child {
- padding: 10px;
- margin: -10px -8px -10px -10px;
- }
-
- .action-counter {
- pointer-events: none;
- user-select: none;
- }
-
- .interactive {
- .svg-inline--fa {
- animation-duration: 0.6s;
- }
-
- &:hover .svg-inline--fa,
- &.-favorited .svg-inline--fa {
- color: var(--cOrange);
- }
-
- @include unfocused-style {
- .focus-marker {
- visibility: hidden;
- }
-
- .active-marker {
- visibility: visible;
- }
- }
-
- @include focused-style {
- .focus-marker {
- visibility: visible;
- }
-
- .active-marker {
- visibility: hidden;
- }
- }
- }
-}
-</style>
diff --git a/src/components/input.style.js b/src/components/input.style.js
@@ -18,7 +18,7 @@ export default {
{
component: 'Root',
directives: {
- '--defaultInputBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2), $borderSide(#000000 top 0.2), inset 0 0 2 #000000 / 0.15',
+ '--defaultInputBevel': 'shadow | $borderSide(#FFFFFF bottom 0.2), $borderSide(#000000 top 0.2), inset 0 0 2 #000000 / 0.15, 1 0 1 1 --text / 0.15, -1 0 1 1 --text / 0.15',
'--defaultInputHoverGlow': 'shadow | 0 0 4 --text / 0.5',
'--defaultInputFocusGlow': 'shadow | 0 0 4 4 --link / 0.5'
}
diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue
@@ -10,119 +10,150 @@
>
<template #content>
<div class="dropdown-menu">
- <span v-if="canGrantRole">
- <button
- class="menu-item dropdown-item menu-item"
- @click="toggleRight("admin")"
- >
- {{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }}
- </button>
- <button
- class="menu-item dropdown-item menu-item"
- @click="toggleRight("moderator")"
- >
- {{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
- </button>
+ <template v-if="canGrantRole">
+ <div class="menu-item dropdown-item -icon-space">
+ <button
+ class="main-button"
+ @click="toggleRight("admin")"
+ >
+ {{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon-space">
+ <button
+ class="main-button"
+ @click="toggleRight("moderator")"
+ >
+ {{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
+ </button>
+ </div>
<div
v-if="canChangeActivationState || canDeleteAccount"
role="separator"
class="dropdown-divider"
/>
- </span>
- <button
+ </template>
+ <div
v-if="canChangeActivationState"
- class="menu-item dropdown-item menu-item"
- @click="toggleActivationStatus()"
- >
- {{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
- </button>
- <button
- v-if="canDeleteAccount"
- class="menu-item dropdown-item menu-item"
- @click="deleteUserDialog(true)"
+ class="menu-item dropdown-item -icon-space"
>
- {{ $t('user_card.admin_menu.delete_account') }}
- </button>
- <div
- v-if="canUseTagPolicy"
- role="separator"
- class="dropdown-divider"
- />
- <span v-if="canUseTagPolicy">
- <button
- class="menu-item dropdown-item menu-item"
- @click="toggleTag(tags.FORCE_NSFW)"
- >
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"
- />
- {{ $t('user_card.admin_menu.force_nsfw') }}
- </button>
- <button
- class="menu-item dropdown-item menu-item"
- @click="toggleTag(tags.STRIP_MEDIA)"
- >
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"
- />
- {{ $t('user_card.admin_menu.strip_media') }}
- </button>
<button
- class="menu-item dropdown-item menu-item"
- @click="toggleTag(tags.FORCE_UNLISTED)"
+ class="main-button"
+ @click="toggleActivationStatus()"
>
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"
- />
- {{ $t('user_card.admin_menu.force_unlisted') }}
+ {{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
</button>
+ </div>
+ <div
+ v-if="canDeleteAccount"
+ class="menu-item dropdown-item -icon-space"
+ >
<button
- class="menu-item dropdown-item menu-item"
- @click="toggleTag(tags.SANDBOX)"
+ class="main-button"
+ @click="deleteUserDialog(true)"
>
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"
- />
- {{ $t('user_card.admin_menu.sandbox') }}
+ {{ $t('user_card.admin_menu.delete_account') }}
</button>
- <button
+ </div>
+ <template v-if="canUseTagPolicy">
+ <div
+ role="separator"
+ class="dropdown-divider"
+ />
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ @click="toggleTag(tags.FORCE_NSFW)"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"
+ />
+ {{ $t('user_card.admin_menu.force_nsfw') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ @click="toggleTag(tags.STRIP_MEDIA)"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"
+ />
+ {{ $t('user_card.admin_menu.strip_media') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ @click="toggleTag(tags.FORCE_UNLISTED)"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"
+ />
+ {{ $t('user_card.admin_menu.force_unlisted') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ @click="toggleTag(tags.SANDBOX)"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"
+ />
+ {{ $t('user_card.admin_menu.sandbox') }}
+ </button>
+ </div>
+ <div
v-if="user.is_local"
- class="menu-item dropdown-item menu-item"
- @click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)"
+ class="menu-item dropdown-item -icon"
>
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }"
- />
- {{ $t('user_card.admin_menu.disable_remote_subscription') }}
- </button>
- <button
+ <button
+ class="main-button"
+ @click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }"
+ />
+ {{ $t('user_card.admin_menu.disable_remote_subscription') }}
+ </button>
+ </div>
+ <div
v-if="user.is_local"
- class="menu-item dropdown-item menu-item"
- @click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)"
+ class="menu-item dropdown-item -icon"
>
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }"
- />
- {{ $t('user_card.admin_menu.disable_any_subscription') }}
- </button>
- <button
+ <button
+ class="main-button"
+ @click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }"
+ />
+ {{ $t('user_card.admin_menu.disable_any_subscription') }}
+ </button>
+ </div>
+ <div
v-if="user.is_local"
- class="menu-item dropdown-item menu-item"
- @click="toggleTag(tags.QUARANTINE)"
+ class="menu-item dropdown-item -icon"
>
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }"
- />
- {{ $t('user_card.admin_menu.quarantine') }}
- </button>
- </span>
+ <button
+ class="main-button"
+ @click="toggleTag(tags.QUARANTINE)"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }"
+ />
+ {{ $t('user_card.admin_menu.quarantine') }}
+ </button>
+ </div>
+ </template>
</div>
</template>
<template #trigger>
diff --git a/src/components/notifications/notification_filters.vue b/src/components/notifications/notification_filters.vue
@@ -7,78 +7,94 @@
>
<template #content>
<div class="dropdown-menu">
- <button
- class="menu-item dropdown-item"
- @click="toggleNotificationFilter('likes')"
- >
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': filters.likes }"
- />{{ $t('settings.notification_visibility_likes') }}
- </button>
- <button
- class="menu-item dropdown-item"
- @click="toggleNotificationFilter('repeats')"
- >
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': filters.repeats }"
- />{{ $t('settings.notification_visibility_repeats') }}
- </button>
- <button
- class="menu-item dropdown-item"
- @click="toggleNotificationFilter('follows')"
- >
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': filters.follows }"
- />{{ $t('settings.notification_visibility_follows') }}
- </button>
- <button
- class="menu-item dropdown-item"
- @click="toggleNotificationFilter('mentions')"
- >
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': filters.mentions }"
- />{{ $t('settings.notification_visibility_mentions') }}
- </button>
- <button
- class="menu-item dropdown-item"
- @click="toggleNotificationFilter('statuses')"
- >
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': filters.statuses }"
- />{{ $t('settings.notification_visibility_statuses') }}
- </button>
- <button
- class="menu-item dropdown-item"
- @click="toggleNotificationFilter('emojiReactions')"
- >
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': filters.emojiReactions }"
- />{{ $t('settings.notification_visibility_emoji_reactions') }}
- </button>
- <button
- class="menu-item dropdown-item"
- @click="toggleNotificationFilter('moves')"
- >
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': filters.moves }"
- />{{ $t('settings.notification_visibility_moves') }}
- </button>
- <button
- class="menu-item dropdown-item"
- @click="toggleNotificationFilter('polls')"
- >
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': filters.polls }"
- />{{ $t('settings.notification_visibility_polls') }}
- </button>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ @click="toggleNotificationFilter('likes')"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': filters.likes }"
+ />{{ $t('settings.notification_visibility_likes') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ @click="toggleNotificationFilter('repeats')"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': filters.repeats }"
+ />{{ $t('settings.notification_visibility_repeats') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ @click="toggleNotificationFilter('follows')"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': filters.follows }"
+ />{{ $t('settings.notification_visibility_follows') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ @click="toggleNotificationFilter('mentions')"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': filters.mentions }"
+ />{{ $t('settings.notification_visibility_mentions') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ @click="toggleNotificationFilter('statuses')"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': filters.statuses }"
+ />{{ $t('settings.notification_visibility_statuses') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ @click="toggleNotificationFilter('emojiReactions')"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': filters.emojiReactions }"
+ />{{ $t('settings.notification_visibility_emoji_reactions') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ @click="toggleNotificationFilter('moves')"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': filters.moves }"
+ />{{ $t('settings.notification_visibility_moves') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ @click="toggleNotificationFilter('polls')"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': filters.polls }"
+ />{{ $t('settings.notification_visibility_polls') }}
+ </button>
+ </div>
</div>
</template>
<template #trigger>
diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js
@@ -197,8 +197,8 @@ const Popover = {
// Default to whatever user wished with placement prop
let usingTop = this.placement !== 'bottom'
- // Handle special cases, first force to displaying on top if there's not space on bottom,
- // regardless of what placement value was. Then check if there's not space on top, and
+ // Handle special cases, first force to displaying on top if there's no space on bottom,
+ // regardless of what placement value was. Then check if there's no space on top, and
// force to bottom, again regardless of what placement value was.
const topBoundary = origin.y - anchorHeight * 0.5 + (this.removePadding ? topPadding : 0)
const bottomBoundary = origin.y + anchorHeight * 0.5 - (this.removePadding ? bottomPadding : 0)
@@ -214,20 +214,20 @@ const Popover = {
translateX = origin.x + horizOffset + xOffset
} else {
// Default to whatever user wished with placement prop
- let usingRight = this.placement !== 'left'
+ let usingLeft = this.placement !== 'right'
- // Handle special cases, first force to displaying on top if there's not space on bottom,
- // regardless of what placement value was. Then check if there's not space on top, and
- // force to bottom, again regardless of what placement value was.
- const rightBoundary = origin.x - anchorWidth * 0.5 + (this.removePadding ? rightPadding : 0)
- const leftBoundary = origin.x + anchorWidth * 0.5 - (this.removePadding ? leftPadding : 0)
- if (leftBoundary + content.offsetWidth > xBounds.max) usingRight = true
- if (rightBoundary - content.offsetWidth < xBounds.min) usingRight = false
+ // Handle special cases, first force to displaying on left if there's no space on right,
+ // regardless of what placement value was. Then check if there's no space on right, and
+ // force to left, again regardless of what placement value was.
+ const leftBoundary = origin.x - anchorWidth * 0.5 + (this.removePadding ? leftPadding : 0)
+ const rightBoundary = origin.x + anchorWidth * 0.5 - (this.removePadding ? rightPadding : 0)
+ if (rightBoundary + content.offsetWidth > xBounds.max) usingLeft = true
+ if (leftBoundary - content.offsetWidth < xBounds.min) usingLeft = false
const xOffset = (this.offset && this.offset.x) || 0
- translateX = usingRight
- ? rightBoundary - xOffset - content.offsetWidth
- : leftBoundary + xOffset
+ translateX = usingLeft
+ ? leftBoundary - xOffset - content.offsetWidth
+ : rightBoundary + xOffset
const yOffset = (this.offset && this.offset.y) || 0
translateY = origin.y + vertOffset + yOffset
@@ -275,6 +275,11 @@ const Popover = {
this.scrollable.removeEventListener('scroll', this.onScroll)
this.scrollable.removeEventListener('resize', this.onResize)
},
+ resizePopover () {
+ setTimeout(() => {
+ this.updateStyles()
+ }, 1)
+ },
onMouseenter (e) {
if (this.trigger === 'hover') {
this.lockReEntry = false
@@ -323,7 +328,12 @@ const Popover = {
this.updateStyles()
},
onResize (e) {
- this.updateStyles()
+ const content = this.$refs.content
+ if (!content) return
+ if (this.oldSize.width !== content.offsetWidth || this.oldSize.height !== content.offsetHeight) {
+ this.updateStyles()
+ this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
+ }
},
onChildPopoverState (childRef, state) {
if (state) {
@@ -337,12 +347,7 @@ const Popover = {
// Monitor changes to content size, update styles only when content sizes have changed,
// that should be the only time we need to move the popover box if we don't care about scroll
// or resize
- const content = this.$refs.content
- if (!content) return
- if (this.oldSize.width !== content.offsetWidth || this.oldSize.height !== content.offsetHeight) {
- this.updateStyles()
- this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
- }
+ this.onResize()
},
mounted () {
this.teleport = true
diff --git a/src/components/popover/popover.scss b/src/components/popover/popover.scss
@@ -0,0 +1,142 @@
+.popover-trigger-button {
+ display: inline-block;
+}
+
+.popover {
+ z-index: var(--ZI_popover_override, var(--ZI_popovers));
+ position: fixed;
+ min-width: 0;
+ max-width: calc(100vw - 20px);
+ box-shadow: var(--shadow);
+}
+
+.popover-default {
+ &::after {
+ content: "";
+ position: absolute;
+ top: -1px;
+ bottom: -1px;
+ left: -1px;
+ right: -1px;
+ z-index: -1px;
+ box-shadow: var(--shadow);
+ pointer-events: none;
+ }
+
+ border-radius: var(--roundness);
+ border-color: var(--border);
+ border-style: solid;
+ border-width: 1px;
+ background-color: var(--background);
+}
+
+.dropdown-menu {
+ display: block;
+ padding: 0;
+ font-size: 1em;
+ text-align: left;
+ list-style: none;
+ max-width: 100vw;
+ z-index: var(--ZI_popover_override, var(--ZI_popovers));
+ white-space: nowrap;
+ background-color: var(--background);
+
+ .dropdown-divider {
+ height: 0;
+ margin: 0.5rem 0;
+ overflow: hidden;
+ border-top: 1px solid var(--border);
+ }
+
+ .dropdown-item {
+ padding: 0;
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-auto-flow: column;
+ grid-auto-columns: auto;
+
+ .popover-wrapper {
+ box-sizing: border-box;
+ display: grid;
+ grid-template-columns: 1fr;
+ }
+
+ .extra-button {
+ border-left: 1px solid var(--icon);
+ padding-left: calc(var(--__horizontal-gap) - 1px);
+ border-right: var(--__horizontal-gap) solid transparent;
+ border-top: var(--__horizontal-gap) solid transparent;
+ border-bottom: var(--__horizontal-gap) solid transparent;
+ }
+
+ .main-button {
+ width: 100%;
+ padding: var(--__horizontal-gap) var(--__horizontal-gap);
+ grid-gap: var(--__horizontal-gap);
+ grid-template-columns: 1fr var(--__line-height);
+ grid-auto-flow: column;
+ grid-auto-columns: auto;
+
+ .menu-checkbox {
+ display: inline-block;
+ vertical-align: middle;
+ min-width: calc(var(--__line-height) + 1px);
+ max-width: calc(var(--__line-height) + 1px);
+ min-height: calc(var(--__line-height) + 1px);
+ max-height: calc(var(--__line-height) + 1px);
+ line-height: var(--__line-height);
+ text-align: center;
+ border-radius: 0;
+ box-shadow: var(--shadow);
+ margin-right: var(--__horizontal-gap);
+
+ &.menu-checkbox-checked::after {
+ font-size: 1.25em;
+ content: "✓";
+ }
+
+ &.-radio {
+ border-radius: 9999px;
+
+ &.menu-checkbox-checked::after {
+ font-size: 2em;
+ content: "•";
+ }
+ }
+ }
+ }
+
+ .main-button,
+ .extra-button {
+ display: grid;
+ box-sizing: border-box;
+ align-items: center;
+
+ &.disabled {
+ cursor: not-allowed;
+ }
+
+ &:not(.disabled) {
+ cursor: pointer;
+ }
+ }
+
+ &.-icon {
+ .main-button {
+ grid-template-columns: var(--__line-height) 1fr;
+ }
+ }
+
+ &.-icon-space {
+ .main-button {
+ padding-left: calc(var(--__line-height) + var(--__horizontal-gap) * 2);
+ }
+ }
+
+ &.-icon-double {
+ .main-button {
+ grid-template-columns: var(--__line-height) var(--__line-height) 1fr;
+ }
+ }
+ }
+}
diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue
@@ -1,5 +1,6 @@
<template>
<span
+ class="popover-wrapper"
@mouseenter="onMouseenter"
@mouseleave="onMouseleave"
>
@@ -32,6 +33,7 @@
name="content"
class="popover-inner"
:close="hidePopover"
+ :resize="resizePopover"
/>
</div>
</transition>
@@ -41,101 +43,4 @@
<script src="./popover.js" />
-<style lang="scss">
-.popover-trigger-button {
- display: inline-block;
-}
-
-.popover {
- z-index: var(--ZI_popover_override, var(--ZI_popovers));
- position: fixed;
- min-width: 0;
- max-width: calc(100vw - 20px);
- box-shadow: var(--shadow);
-}
-
-.popover-default {
- &::after {
- content: "";
- position: absolute;
- top: -1px;
- bottom: -1px;
- left: -1px;
- right: -1px;
- z-index: -1px;
- box-shadow: var(--shadow);
- pointer-events: none;
- }
-
- border-radius: var(--roundness);
- border-color: var(--border);
- border-style: solid;
- border-width: 1px;
- background-color: var(--background);
-}
-
-.dropdown-menu {
- display: block;
- padding: 0;
- font-size: 1em;
- text-align: left;
- list-style: none;
- max-width: 100vw;
- z-index: var(--ZI_popover_override, var(--ZI_popovers));
- white-space: nowrap;
- background-color: var(--background);
-
- .dropdown-divider {
- height: 0;
- margin: 0.5rem 0;
- overflow: hidden;
- border-top: 1px solid var(--border);
- }
-
- .dropdown-item {
- border: none;
-
- &-icon {
- svg {
- width: var(--__line-height);
- margin-right: var(--__horizontal-gap);
- }
- }
-
- &.-has-submenu {
- .chevron-icon {
- margin-right: 0.25rem;
- margin-left: 2rem;
- }
- }
-
- .menu-checkbox {
- display: inline-block;
- vertical-align: middle;
- min-width: calc(var(--__line-height) + 1px);
- max-width: calc(var(--__line-height) + 1px);
- min-height: calc(var(--__line-height) + 1px);
- max-height: calc(var(--__line-height) + 1px);
- line-height: var(--__line-height);
- text-align: center;
- border-radius: 0;
- box-shadow: var(--shadow);
- margin-right: var(--__horizontal-gap);
-
- &.menu-checkbox-checked::after {
- font-size: 1.25em;
- content: "✓";
- }
-
- &.-radio {
- border-radius: 9999px;
-
- &.menu-checkbox-checked::after {
- font-size: 2em;
- content: "•";
- }
- }
- }
- }
-}
-</style>
+<style src="./popover.scss" lang="scss"></style>
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
@@ -336,7 +336,7 @@
>
<button
v-if="!hideDraft || !disableDraft"
- class="menu-item dropdown-item dropdown-item-icon"
+ class="menu-item dropdown-item"
role="menu"
:disabled="!safeToSaveDraft && saveable"
:class="{ disabled: !safeToSaveDraft }"
diff --git a/src/components/quick_filter_settings/quick_filter_settings.js b/src/components/quick_filter_settings/quick_filter_settings.js
@@ -1,5 +1,5 @@
import Popover from '../popover/popover.vue'
-import { mapGetters } from 'vuex'
+import { mapGetters, mapState } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faFilter, faFont, faWrench } from '@fortawesome/free-solid-svg-icons'
@@ -11,7 +11,8 @@ library.add(
const QuickFilterSettings = {
props: {
- conversation: Boolean
+ conversation: Boolean,
+ nested: Boolean
},
components: {
Popover
@@ -27,6 +28,25 @@ const QuickFilterSettings = {
},
computed: {
...mapGetters(['mergedConfig']),
+ ...mapState({
+ mobileLayout: state => state.interface.layoutType === 'mobile'
+ }),
+ triggerAttrs () {
+ if (this.mobileLayout) {
+ return {}
+ } else {
+ return {
+ title: this.$t('timeline.quick_filter_settings')
+ }
+ }
+ },
+ mainClass () {
+ if (this.mobileLayout) {
+ return 'main-button'
+ } else {
+ return 'dropdown-item'
+ }
+ },
loggedIn () {
return !!this.$store.state.users.currentUser
},
diff --git a/src/components/quick_filter_settings/quick_filter_settings.vue b/src/components/quick_filter_settings/quick_filter_settings.vue
@@ -1,9 +1,10 @@
<template>
<Popover
- trigger="click"
+ :trigger="nested ? 'hover' : 'click'"
class="QuickFilterSettings"
:bound-to="{ x: 'container' }"
- :trigger-attrs="{ title: $t('timeline.quick_filter_settings') }"
+ :position="nested ? 'right' : 'top'"
+ :trigger-attrs="triggerAttrs"
>
<template #content>
<div
@@ -14,110 +15,148 @@
v-if="loggedIn"
role="group"
>
- <button
+ <div class="menu-item dropdown-item -icon">
+ <button
+ v-if="!conversation"
+ class="main-button"
+ :aria-checked="replyVisibilityAll"
+ role="menuitemradio"
+ @click="replyVisibilityAll = true"
+ >
+ <span
+ class="input menu-checkbox -radio"
+ :class="{ 'menu-checkbox-checked': replyVisibilityAll }"
+ :aria-hidden="true"
+ />{{ $t('settings.reply_visibility_all') }}
+ </button>
+ </div>
+ <div
+ v-if="!conversation"
+ class="menu-item dropdown-item -icon"
+ >
+ <button
+ class="main-button"
+ :aria-checked="replyVisibilityFollowing"
+ role="menuitemradio"
+ @click="replyVisibilityFollowing = true"
+ >
+ <span
+ class="input menu-checkbox -radio"
+ :class="{ 'menu-checkbox-checked': replyVisibilityFollowing }"
+ :aria-hidden="true"
+ />{{ $t('settings.reply_visibility_following_short') }}
+ </button>
+ </div>
+ <div
v-if="!conversation"
- class="menu-item dropdown-item"
- :aria-checked="replyVisibilityAll"
- role="menuitemradio"
- @click="replyVisibilityAll = true"
+ class="menu-item dropdown-item -icon"
+ >
+ <button
+ class="main-button"
+ :aria-checked="replyVisibilitySelf"
+ role="menuitemradio"
+ @click="replyVisibilitySelf = true"
+ >
+ <span
+ class="input menu-checkbox -radio"
+ :class="{ 'menu-checkbox-checked': replyVisibilitySelf }"
+ :aria-hidden="true"
+ />{{ $t('settings.reply_visibility_self_short') }}
+ </button>
+ </div>
+ <div
+ v-if="!conversation"
+ role="separator"
+ class="dropdown-divider"
+ />
+ </div>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ role="menuitemcheckbox"
+ :aria-checked="muteBotStatuses"
+ @click="muteBotStatuses = !muteBotStatuses"
>
<span
- class="input menu-checkbox -radio"
- :class="{ 'menu-checkbox-checked': replyVisibilityAll }"
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': muteBotStatuses }"
:aria-hidden="true"
- />{{ $t('settings.reply_visibility_all') }}
+ />{{ $t('settings.mute_bot_posts') }}
</button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
<button
- v-if="!conversation"
- class="menu-item dropdown-item"
- :aria-checked="replyVisibilityFollowing"
- role="menuitemradio"
- @click="replyVisibilityFollowing = true"
+ class="main-button"
+ role="menuitemcheckbox"
+ :aria-checked="muteSensitiveStatuses"
+ @click="muteSensitiveStatuses = !muteSensitiveStatuses"
>
<span
- class="input menu-checkbox -radio"
- :class="{ 'menu-checkbox-checked': replyVisibilityFollowing }"
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': muteSensitiveStatuses }"
:aria-hidden="true"
- />{{ $t('settings.reply_visibility_following_short') }}
+ />{{ $t('settings.mute_sensitive_posts') }}
</button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
<button
- v-if="!conversation"
- class="menu-item dropdown-item"
- :aria-checked="replyVisibilitySelf"
- role="menuitemradio"
- @click="replyVisibilitySelf = true"
+ class="main-button"
+ role="menuitemcheckbox"
+ :aria-checked="hideMedia"
+ @click="hideMedia = !hideMedia"
>
<span
- class="input menu-checkbox -radio"
- :class="{ 'menu-checkbox-checked': replyVisibilitySelf }"
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': hideMedia }"
:aria-hidden="true"
- />{{ $t('settings.reply_visibility_self_short') }}
+ />{{ $t('settings.hide_media_previews') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ role="menuitemcheckbox"
+ :aria-checked="hideMutedPosts"
+ @click="hideMutedPosts = !hideMutedPosts"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': hideMutedPosts }"
+ :aria-hidden="true"
+ />{{ $t('settings.hide_all_muted_posts') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ role="menuitem"
+ @click="openTab('filtering')"
+ >
+ <FAIcon
+ fixed-width
+ icon="font"
+ />{{ $t('settings.word_filter_and_more') }}
</button>
- <div
- v-if="!conversation"
- role="separator"
- class="dropdown-divider"
- />
</div>
- <button
- class="menu-item dropdown-item"
- role="menuitemcheckbox"
- :aria-checked="muteBotStatuses"
- @click="muteBotStatuses = !muteBotStatuses"
- >
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': muteBotStatuses }"
- :aria-hidden="true"
- />{{ $t('settings.mute_bot_posts') }}
- </button>
- <button
- class="menu-item dropdown-item"
- role="menuitemcheckbox"
- :aria-checked="muteSensitiveStatuses"
- @click="muteSensitiveStatuses = !muteSensitiveStatuses"
- >
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': muteSensitiveStatuses }"
- :aria-hidden="true"
- />{{ $t('settings.mute_sensitive_posts') }}
- </button>
- <button
- class="menu-item dropdown-item"
- role="menuitemcheckbox"
- :aria-checked="hideMedia"
- @click="hideMedia = !hideMedia"
- >
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': hideMedia }"
- :aria-hidden="true"
- />{{ $t('settings.hide_media_previews') }}
- </button>
- <button
- class="menu-item dropdown-item"
- role="menuitemcheckbox"
- :aria-checked="hideMutedPosts"
- @click="hideMutedPosts = !hideMutedPosts"
- >
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': hideMutedPosts }"
- :aria-hidden="true"
- />{{ $t('settings.hide_all_muted_posts') }}
- </button>
- <button
- class="menu-item dropdown-item dropdown-item-icon"
- role="menuitem"
- @click="openTab('filtering')"
- >
- <FAIcon icon="font" />{{ $t('settings.word_filter_and_more') }}
- </button>
</div>
</template>
<template #trigger>
- <FAIcon icon="filter" />
+ <div :class="mobileLayout ? 'main-button' : ''">
+ <FAIcon
+ icon="filter"
+ :fixed-width="nested"
+ />
+ <template v-if="nested">
+ {{ $t('timeline.filter_settings') }}
+ </template>
+ <FAIcon
+ v-if="nested"
+ class="chevron-icon"
+ size="lg"
+ icon="chevron-right"
+ fixed-width
+ />
+ </div>
</template>
</Popover>
</template>
diff --git a/src/components/quick_view_settings/quick_view_settings.js b/src/components/quick_view_settings/quick_view_settings.js
@@ -1,5 +1,6 @@
-import Popover from '../popover/popover.vue'
-import { mapGetters } from 'vuex'
+import Popover from 'src/components/popover/popover.vue'
+import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filter_settings.vue'
+import { mapGetters, mapState } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faList, faFolderTree, faBars, faWrench } from '@fortawesome/free-solid-svg-icons'
@@ -15,7 +16,8 @@ const QuickViewSettings = {
conversation: Boolean
},
components: {
- Popover
+ Popover,
+ QuickFilterSettings
},
methods: {
setConversationDisplay (visibility) {
@@ -27,6 +29,9 @@ const QuickViewSettings = {
},
computed: {
...mapGetters(['mergedConfig']),
+ ...mapState({
+ mobileLayout: state => state.interface.layoutType === 'mobile'
+ }),
loggedIn () {
return !!this.$store.state.users.currentUser
},
diff --git a/src/components/quick_view_settings/quick_view_settings.vue b/src/components/quick_view_settings/quick_view_settings.vue
@@ -3,94 +3,126 @@
trigger="click"
class="QuickViewSettings"
:bound-to="{ x: 'container' }"
- :trigger-attrs="{ title: $t('timeline.quick_view_settings') }"
+ :trigger-attrs="triggerAttrs"
>
<template #content>
<div
class="dropdown-menu"
role="menu"
>
+ <div
+ v-if="mobileLayout"
+ class="menu-item dropdown-item -icon"
+ >
+ <QuickFilterSettings :nested="true" />
+ </div>
+ <div
+ v-if="mobileLayout"
+ role="separator"
+ class="dropdown-divider"
+ />
<div role="group">
+ <div class="menu-item dropdown-item -icon-double">
+ <button
+ class="main-button"
+ :aria-checked="conversationDisplay === 'tree'"
+ role="menuitemradio"
+ @click="conversationDisplay = 'tree'"
+ >
+ <span
+ class="input menu-checkbox -radio"
+ :aria-hidden="true"
+ :class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }"
+ /><FAIcon
+ icon="folder-tree"
+ :aria-hidden="true"
+ fixed-width
+ /> {{ $t('settings.conversation_display_tree_quick') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon-double">
+ <button
+ class="main-button"
+ :aria-checked="conversationDisplay === 'linear'"
+ role="menuitemradio"
+ @click="conversationDisplay = 'linear'"
+ >
+ <span
+ class="input menu-checkbox -radio"
+ :class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }"
+ :aria-hidden="true"
+ /><FAIcon
+ icon="list"
+ :aria-hidden="true"
+ fixed-width
+ /> {{ $t('settings.conversation_display_linear_quick') }}
+ </button>
+ </div>
+ </div>
+ <div
+ role="separator"
+ class="dropdown-divider"
+ />
+ <div class="menu-item dropdown-item -icon">
<button
- class="menu-item dropdown-item"
- :aria-checked="conversationDisplay === 'tree'"
- role="menuitemradio"
- @click="conversationDisplay = 'tree'"
+ class="main-button"
+ role="menuitemcheckbox"
+ :aria-checked="showUserAvatars"
+ @click="showUserAvatars = !showUserAvatars"
>
<span
- class="input menu-checkbox -radio"
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': showUserAvatars }"
:aria-hidden="true"
- :class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }"
- /><FAIcon
- icon="folder-tree"
- :aria-hidden="true"
- /> {{ $t('settings.conversation_display_tree_quick') }}
+ />{{ $t('settings.mention_link_show_avatar_quick') }}
</button>
+ </div>
+ <div
+ v-if="!conversation"
+ class="menu-item dropdown-item -icon"
+ >
<button
- class="menu-item dropdown-item"
- :aria-checked="conversationDisplay === 'linear'"
- role="menuitemradio"
- @click="conversationDisplay = 'linear'"
+ class="main-button"
+ role="menuitemcheckbox"
+ :aria-checked="autoUpdate"
+ @click="autoUpdate = !autoUpdate"
>
<span
- class="input menu-checkbox -radio"
- :class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }"
- :aria-hidden="true"
- /><FAIcon
- icon="list"
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': autoUpdate }"
:aria-hidden="true"
- /> {{ $t('settings.conversation_display_linear_quick') }}
+ />{{ $t('settings.auto_update') }}
</button>
</div>
<div
- role="separator"
- class="dropdown-divider"
- />
- <button
- class="menu-item dropdown-item"
- role="menuitemcheckbox"
- :aria-checked="showUserAvatars"
- @click="showUserAvatars = !showUserAvatars"
- >
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': showUserAvatars }"
- :aria-hidden="true"
- />{{ $t('settings.mention_link_show_avatar_quick') }}
- </button>
- <button
v-if="!conversation"
- class="menu-item dropdown-item"
- role="menuitemcheckbox"
- :aria-checked="autoUpdate"
- @click="autoUpdate = !autoUpdate"
+ class="menu-item dropdown-item -icon"
>
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': autoUpdate }"
- :aria-hidden="true"
- />{{ $t('settings.auto_update') }}
- </button>
- <button
- v-if="!conversation"
- class="menu-item dropdown-item"
- role="menuitemcheckbox"
- :aria-checked="collapseWithSubjects"
- @click="collapseWithSubjects = !collapseWithSubjects"
- >
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': collapseWithSubjects }"
- :aria-hidden="true"
- />{{ $t('settings.collapse_subject') }}
- </button>
- <button
- class="menu-item dropdown-item dropdown-item-icon"
- role="menuitem"
- @click="openTab('general')"
- >
- <FAIcon icon="wrench" />{{ $t('settings.more_settings') }}
- </button>
+ <button
+ class="main-button"
+ role="menuitemcheckbox"
+ :aria-checked="collapseWithSubjects"
+ @click="collapseWithSubjects = !collapseWithSubjects"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': collapseWithSubjects }"
+ :aria-hidden="true"
+ />{{ $t('settings.collapse_subject') }}
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ role="menuitem"
+ @click="openTab('general')"
+ >
+ <FAIcon
+ icon="wrench"
+ fixed-width
+ />{{ $t('settings.more_settings') }}
+ </button>
+ </div>
</div>
</template>
<template #trigger>
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
@@ -1,54 +0,0 @@
-import Popover from '../popover/popover.vue'
-import EmojiPicker from '../emoji_picker/emoji_picker.vue'
-import { library } from '@fortawesome/fontawesome-svg-core'
-import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons'
-import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
-
-library.add(
- faPlus,
- faTimes,
- faSmileBeam
-)
-
-const ReactButton = {
- props: ['status'],
- data () {
- return {
- filterWord: '',
- expanded: false
- }
- },
- components: {
- Popover,
- EmojiPicker
- },
- methods: {
- addReaction (event) {
- const emoji = event.insertion
- const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
- if (existingReaction && existingReaction.me) {
- this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
- } else {
- this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
- }
- },
- show () {
- if (!this.expanded) {
- this.$refs.picker.showPicker()
- }
- },
- onShow () {
- this.expanded = true
- },
- onClose () {
- this.expanded = false
- }
- },
- computed: {
- hideCustomEmoji () {
- return !this.$store.state.instance.pleromaCustomEmojiReactionsAvailable
- }
- }
-}
-
-export default ReactButton
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
@@ -1,115 +0,0 @@
-<template>
- <span class="ReactButton">
- <EmojiPicker
- ref="picker"
- :enable-sticker-picker="false"
- :hide-custom-emoji="hideCustomEmoji"
- class="emoji-picker-panel"
- @emoji="addReaction"
- @show="onShow"
- @close="onClose"
- />
- <span
- class="button-unstyled popover-trigger"
- role="button"
- :tabindex="0"
- :title="$t('tool_tip.add_reaction')"
- @click.stop.prevent="show"
- >
- <FALayers>
- <FAIcon
- class="fa-scale-110 fa-old-padding"
- :icon="['far', 'smile-beam']"
- />
- <FAIcon
- v-show="!expanded"
- class="focus-marker"
- transform="shrink-6 up-9 right-17"
- icon="plus"
- />
- <FAIcon
- v-show="expanded"
- class="focus-marker"
- transform="shrink-6 up-9 right-17"
- icon="times"
- />
- </FALayers>
- </span>
- </span>
-</template>
-
-<script src="./react_button.js"></script>
-
-<style lang="scss">
-@import "../../mixins";
-
-.ReactButton {
- .reaction-picker-filter {
- padding: 0.5em;
- display: flex;
-
- input {
- flex: 1;
- }
- }
-
- .reaction-picker-divider {
- height: 1px;
- width: 100%;
- margin: 0.5em;
- background-color: var(--border);
- }
-
- .reaction-picker {
- width: 10em;
- height: 9em;
- font-size: 1.5em;
- overflow-y: scroll;
- display: flex;
- flex-wrap: wrap;
- padding: 0.5em;
- text-align: center;
- align-content: flex-start;
- user-select: none;
- mask:
- linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
- linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
- linear-gradient(to top, white, white);
- transition: mask-size 150ms;
- mask-size: 100% 20px, 100% 20px, auto;
-
- /* Autoprefixed seem to ignore this one, and also syntax is different */
- mask-composite: xor;
- mask-composite: exclude;
-
- .emoji-button {
- cursor: pointer;
- flex-basis: 20%;
- line-height: 1.5;
- align-content: center;
-
- &:hover {
- transform: scale(1.25);
- }
- }
- }
-
- .popover-trigger {
- padding: 10px;
- margin: -10px;
-
- @include unfocused-style {
- .focus-marker {
- visibility: hidden;
- }
- }
-
- @include focused-style {
- .focus-marker {
- visibility: visible;
- }
- }
- }
-}
-
-</style>
diff --git a/src/components/reply_button/reply_button.js b/src/components/reply_button/reply_button.js
@@ -1,27 +0,0 @@
-import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faReply,
- faPlus,
- faTimes
-} from '@fortawesome/free-solid-svg-icons'
-
-library.add(
- faReply,
- faPlus,
- faTimes
-)
-
-const ReplyButton = {
- name: 'ReplyButton',
- props: ['status', 'replying'],
- computed: {
- loggedIn () {
- return !!this.$store.state.users.currentUser
- },
- remoteInteractionLink () {
- return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
- }
- }
-}
-
-export default ReplyButton
diff --git a/src/components/reply_button/reply_button.vue b/src/components/reply_button/reply_button.vue
@@ -1,96 +0,0 @@
-<template>
- <div class="ReplyButton">
- <button
- v-if="loggedIn"
- class="button-unstyled interactive"
- :class="{'-active': replying}"
- :title="$t('tool_tip.reply')"
- @click.prevent="$emit('toggle')"
- >
- <FALayers class="fa-old-padding-layer">
- <FAIcon
- class="fa-scale-110"
- icon="reply"
- />
- <FAIcon
- v-if="!replying"
- class="focus-marker"
- transform="shrink-6 up-8 right-11"
- icon="plus"
- />
- <FAIcon
- v-else
- class="focus-marker"
- transform="shrink-6 up-8 right-11"
- icon="times"
- />
- </FALayers>
- </button>
- <a
- v-else
- class="button-unstyled interactive"
- target="_blank"
- role="button"
- :href="remoteInteractionLink"
- :title="$t('tool_tip.reply')"
- >
- <FALayers class="fa-old-padding-layer">
- <FAIcon
- class="fa-scale-110"
- icon="reply"
- />
- <FAIcon
- v-if="!replying"
- class="focus-marker"
- transform="shrink-6 up-8 right-16"
- icon="plus"
- />
- </FALayers>
- </a>
- <span
- v-if="status.replies_count > 0"
- class="action-counter"
- >
- {{ status.replies_count }}
- </span>
- </div>
-</template>
-
-<script src="./reply_button.js"></script>
-
-<style lang="scss">
-@import "../../mixins";
-
-.ReplyButton {
- display: flex;
-
- > :first-child {
- padding: 10px;
- margin: -10px -8px -10px -10px;
- }
-
- .action-counter {
- pointer-events: none;
- user-select: none;
- }
-
- .interactive {
- &:hover .svg-inline--fa,
- &.-active .svg-inline--fa {
- color: var(--cBlue);
- }
-
- @include unfocused-style {
- .focus-marker {
- visibility: hidden;
- }
- }
-
- @include focused-style {
- .focus-marker {
- visibility: visible;
- }
- }
- }
-}
-</style>
diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js
@@ -1,68 +0,0 @@
-import ConfirmModal from '../confirm_modal/confirm_modal.vue'
-import { library } from '@fortawesome/fontawesome-svg-core'
-import {
- faRetweet,
- faPlus,
- faMinus,
- faCheck
-} from '@fortawesome/free-solid-svg-icons'
-
-library.add(
- faRetweet,
- faPlus,
- faMinus,
- faCheck
-)
-
-const RetweetButton = {
- props: ['status', 'loggedIn', 'visibility'],
- components: {
- ConfirmModal
- },
- data () {
- return {
- animated: false,
- showingConfirmDialog: false
- }
- },
- methods: {
- retweet () {
- if (!this.status.repeated && this.shouldConfirmRepeat) {
- this.showConfirmDialog()
- } else {
- this.doRetweet()
- }
- },
- doRetweet () {
- if (!this.status.repeated) {
- this.$store.dispatch('retweet', { id: this.status.id })
- } else {
- this.$store.dispatch('unretweet', { id: this.status.id })
- }
- this.animated = true
- setTimeout(() => {
- this.animated = false
- }, 500)
- this.hideConfirmDialog()
- },
- showConfirmDialog () {
- this.showingConfirmDialog = true
- },
- hideConfirmDialog () {
- this.showingConfirmDialog = false
- }
- },
- computed: {
- mergedConfig () {
- return this.$store.getters.mergedConfig
- },
- remoteInteractionLink () {
- return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
- },
- shouldConfirmRepeat () {
- return this.mergedConfig.modalOnRepeat
- }
- }
-}
-
-export default RetweetButton
diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue
@@ -1,133 +0,0 @@
-<template>
- <div class="RetweetButton">
- <button
- v-if="visibility !== 'private' && visibility !== 'direct' && loggedIn"
- class="button-unstyled interactive"
- :class="status.repeated && '-repeated'"
- :title="$t('tool_tip.repeat')"
- @click.prevent="retweet()"
- >
- <FALayers class="fa-old-padding-layer">
- <FAIcon
- class="fa-scale-110"
- icon="retweet"
- :spin="animated"
- />
- <FAIcon
- v-if="status.repeated"
- class="active-marker"
- transform="shrink-6 up-9 right-12"
- icon="check"
- />
- <FAIcon
- v-if="!status.repeated"
- class="focus-marker"
- transform="shrink-6 up-9 right-12"
- icon="plus"
- />
- <FAIcon
- v-else
- class="focus-marker"
- transform="shrink-6 up-9 right-12"
- icon="minus"
- />
- </FALayers>
- </button>
- <span v-else-if="loggedIn">
- <FAIcon
- class="fa-scale-110 fa-old-padding"
- icon="lock"
- :title="$t('timeline.no_retweet_hint')"
- />
- </span>
- <a
- v-else
- class="button-unstyled interactive"
- target="_blank"
- role="button"
- :title="$t('tool_tip.repeat')"
- :href="remoteInteractionLink"
- >
- <FALayers class="fa-old-padding-layer">
- <FAIcon
- class="fa-scale-110"
- icon="retweet"
- />
- <FAIcon
- class="focus-marker"
- transform="shrink-6 up-9 right-12"
- icon="plus"
- />
- </FALayers>
- </a>
- <span
- v-if="!mergedConfig.hidePostStats && status.repeat_num > 0"
- class="no-event"
- >
- {{ status.repeat_num }}
- </span>
- <teleport to="#modal">
- <confirm-modal
- v-if="showingConfirmDialog"
- :title="$t('status.repeat_confirm_title')"
- :confirm-text="$t('status.repeat_confirm_accept_button')"
- :cancel-text="$t('status.repeat_confirm_cancel_button')"
- @accepted="doRetweet"
- @cancelled="hideConfirmDialog"
- >
- {{ $t('status.repeat_confirm') }}
- </confirm-modal>
- </teleport>
- </div>
-</template>
-
-<script src="./retweet_button.js"></script>
-
-<style lang="scss">
-@import "../../mixins";
-
-.RetweetButton {
- display: flex;
-
- > :first-child {
- padding: 10px;
- margin: -10px -8px -10px -10px;
- }
-
- .action-counter {
- pointer-events: none;
- user-select: none;
- }
-
- .interactive {
- .svg-inline--fa {
- animation-duration: 0.6s;
- }
-
- &:hover .svg-inline--fa,
- &.-repeated .svg-inline--fa {
- color: var(--cGreen);
- }
-
- @include unfocused-style {
- .focus-marker {
- visibility: hidden;
- }
-
- .active-marker {
- visibility: visible;
- }
- }
-
- @include focused-style {
- .focus-marker {
- visibility: visible;
- }
-
- .active-marker {
- visibility: hidden;
- }
- }
- }
-}
-</style>
diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.vue b/src/components/settings_modal/admin_tabs/frontends_tab.vue
@@ -115,22 +115,28 @@
>
<template #content="{close}">
<div class="dropdown-menu">
- <button
+ <div
v-for="ref in frontend.refs"
:key="ref"
class="menu-item dropdown-item"
- @click.prevent="update(frontend, ref)"
- @click="close"
>
- <i18n-t
- keypath="admin_dash.frontend.install_version"
- scope="global"
+ <button
+ class="main-button"
+ @click.prevent="update(frontend, ref)"
+ @click="close"
>
- <template #version>
- <code>{{ ref }}</code>
- </template>
- </i18n-t>
- </button>
+ <span>
+ <i18n-t
+ keypath="admin_dash.frontend.install_version"
+ scope="global"
+ >
+ <template #version>
+ <code>{{ ref }}</code>
+ </template>
+ </i18n-t>
+ </span>
+ </button>
+ </div>
</div>
</template>
<template #trigger>
@@ -175,22 +181,28 @@
>
<template #content="{close}">
<div class="dropdown-menu">
- <button
+ <div
v-for="ref in frontend.installedRefs || frontend.refs"
:key="ref"
class="menu-item dropdown-item"
- @click.prevent="setDefault(frontend, ref)"
- @click="close"
>
- <i18n-t
- keypath="admin_dash.frontend.set_default_version"
- scope="global"
+ <button
+ class="main-button"
+ @click.prevent="setDefault(frontend, ref)"
+ @click="close"
>
- <template #version>
- <code>{{ ref }}</code>
- </template>
- </i18n-t>
- </button>
+ <span>
+ <i18n-t
+ keypath="admin_dash.frontend.set_default_version"
+ scope="global"
+ >
+ <template #version>
+ <code>{{ ref }}</code>
+ </template>
+ </i18n-t>
+ </span>
+ </button>
+ </div>
</div>
</template>
<template #trigger>
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
@@ -69,36 +69,42 @@
</template>
<template #content="{close}">
<div class="dropdown-menu">
- <button
- class="menu-item dropdown-item dropdown-item-icon"
- @click.prevent="backup"
- @click="close"
- >
- <FAIcon
- icon="file-download"
- fixed-width
- /><span>{{ $t("settings.file_export_import.backup_settings") }}</span>
- </button>
- <button
- class="menu-item dropdown-item dropdown-item-icon"
- @click.prevent="backupWithTheme"
- @click="close"
- >
- <FAIcon
- icon="file-download"
- fixed-width
- /><span>{{ $t("settings.file_export_import.backup_settings_theme") }}</span>
- </button>
- <button
- class="menu-item dropdown-item dropdown-item-icon"
- @click.prevent="restore"
- @click="close"
- >
- <FAIcon
- icon="file-upload"
- fixed-width
- /><span>{{ $t("settings.file_export_import.restore_settings") }}</span>
- </button>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ @click.prevent="backup"
+ @click="close"
+ >
+ <FAIcon
+ icon="file-download"
+ fixed-width
+ /><span>{{ $t("settings.file_export_import.backup_settings") }}</span>
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ @click.prevent="backupWithTheme"
+ @click="close"
+ >
+ <FAIcon
+ icon="file-download"
+ fixed-width
+ /><span>{{ $t("settings.file_export_import.backup_settings_theme") }}</span>
+ </button>
+ </div>
+ <div class="menu-item dropdown-item -icon">
+ <button
+ class="main-button"
+ @click.prevent="restore"
+ @click="close"
+ >
+ <FAIcon
+ icon="file-upload"
+ fixed-width
+ /><span>{{ $t("settings.file_export_import.restore_settings") }}</span>
+ </button>
+ </div>
</div>
</template>
</Popover>
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
@@ -117,6 +117,16 @@
</BooleanSetting>
</li>
<li>
+ <BooleanSetting path="modalOnMuteConversation">
+ {{ $t('settings.confirm_dialogs_mute_conversation') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="modalOnMuteDomain">
+ {{ $t('settings.confirm_dialogs_mute_domain') }}
+ </BooleanSetting>
+ </li>
+ <li>
<BooleanSetting path="modalOnDelete">
{{ $t('settings.confirm_dialogs_delete') }}
</BooleanSetting>
diff --git a/src/components/status/status.js b/src/components/status/status.js
@@ -1,8 +1,3 @@
-import ReplyButton from '../reply_button/reply_button.vue'
-import FavoriteButton from '../favorite_button/favorite_button.vue'
-import ReactButton from '../react_button/react_button.vue'
-import RetweetButton from '../retweet_button/retweet_button.vue'
-import ExtraButtons from '../extra_buttons/extra_buttons.vue'
import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import AvatarList from '../avatar_list/avatar_list.vue'
@@ -16,6 +11,7 @@ import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
import UserLink from '../user_link/user_link.vue'
import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
import MentionLink from 'src/components/mention_link/mention_link.vue'
+import StatusActionButtons from 'src/components/status_action_buttons/status_action_buttons.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { muteWordHits } from '../../services/status_parser/status_parser.js'
@@ -102,11 +98,6 @@ const controlledOrUncontrolledSet = (obj, name, val) => {
const Status = {
name: 'Status',
components: {
- ReplyButton,
- FavoriteButton,
- ReactButton,
- RetweetButton,
- ExtraButtons,
PostStatusForm,
UserAvatar,
AvatarList,
@@ -119,7 +110,8 @@ const Status = {
MentionLink,
MentionsLine,
UserPopover,
- UserLink
+ UserLink,
+ StatusActionButtons
},
props: [
'statusoid',
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
@@ -264,13 +264,11 @@
.status-actions {
position: relative;
width: 100%;
- display: flex;
+ display: grid;
+ grid-template-columns: 1fr;
+ grid-auto-columns: 1fr;
+ grid-auto-flow: column;
margin-top: var(--status-margin);
-
- > * {
- max-width: 4em;
- flex: 1;
- }
}
.muted {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
@@ -535,37 +535,12 @@
:status="status"
/>
- <div
+ <StatusActionButtons
v-if="!noHeading && !isPreview"
- class="status-actions"
- >
- <reply-button
- :replying="replying"
- :status="status"
- @toggle="toggleReplying"
- />
- <retweet-button
- :visibility="status.visibility"
- :logged-in="loggedIn"
- :status="status"
- @click="$emit('interacted')"
- />
- <favorite-button
- :logged-in="loggedIn"
- :status="status"
- @click="$emit('interacted')"
- />
- <ReactButton
- v-if="loggedIn"
- :status="status"
- @click="$emit('interacted')"
- />
- <extra-buttons
- :status="status"
- @onError="showError"
- @onSuccess="clearError"
- />
- </div>
+ :status="status"
+ :replying="replying"
+ @toggleReplying="toggleReplying"
+ />
</div>
</div>
<div
@@ -583,12 +558,6 @@
<div class="deleted-text">
{{ $t('status.status_deleted') }}
</div>
- <reply-button
- v-if="replying"
- :replying="replying"
- :status="status"
- @toggle="toggleReplying"
- />
</div>
</div>
<div
diff --git a/src/components/status_action_buttons/action_button.js b/src/components/status_action_buttons/action_button.js
@@ -0,0 +1,133 @@
+import StatusBookmarkFolderMenu from 'src/components/status_bookmark_folder_menu/status_bookmark_folder_menu.vue'
+import EmojiPicker from 'src/components/emoji_picker/emoji_picker.vue'
+import Popover from 'src/components/popover/popover.vue'
+
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faPlus,
+ faMinus,
+ faCheck,
+ faTimes,
+ faWrench,
+
+ faChevronRight,
+ faChevronUp,
+
+ faReply,
+ faRetweet,
+ faStar,
+ faSmileBeam,
+
+ faBookmark,
+ faEyeSlash,
+ faThumbtack,
+ faShareAlt,
+ faExternalLinkAlt,
+ faHistory
+} from '@fortawesome/free-solid-svg-icons'
+import {
+ faStar as faStarRegular,
+ faBookmark as faBookmarkRegular
+} from '@fortawesome/free-regular-svg-icons'
+
+library.add(
+ faPlus,
+ faMinus,
+ faCheck,
+ faTimes,
+ faWrench,
+
+ faChevronRight,
+ faChevronUp,
+
+ faReply,
+ faRetweet,
+ faStar,
+ faStarRegular,
+ faSmileBeam,
+
+ faBookmark,
+ faBookmarkRegular,
+ faEyeSlash,
+ faThumbtack,
+ faShareAlt,
+ faExternalLinkAlt,
+ faHistory
+)
+
+export default {
+ props: [
+ 'button',
+ 'status',
+ 'extra',
+ 'status',
+ 'funcArg',
+ 'getClass',
+ 'getComponent',
+ 'doAction',
+ 'close'
+ ],
+ components: {
+ StatusBookmarkFolderMenu,
+ EmojiPicker,
+ Popover
+ },
+ data: () => ({
+ animationState: false
+ }),
+ computed: {
+ buttonClass () {
+ return [
+ this.button.name + '-button',
+ {
+ '-with-extra': this.button.name === 'bookmark',
+ '-extra': this.extra,
+ '-quick': !this.extra
+ }
+ ]
+ },
+ userIsMuted () {
+ return this.$store.getters.relationship(this.status.user.id).muting
+ },
+ threadIsMuted () {
+ return this.status.thread_muted
+ },
+ buttonInnerClass () {
+ return [
+ this.button.name + '-button',
+ {
+ 'main-button': this.extra,
+ 'button-unstyled': !this.extra,
+ '-active': this.button.active?.(this.funcArg),
+ disabled: this.button.interactive ? !this.button.interactive(this.funcArg) : false
+ }
+ ]
+ },
+ remoteInteractionLink () {
+ return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
+ }
+ },
+ methods: {
+ addReaction (event) {
+ const emoji = event.insertion
+ const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
+ if (existingReaction && existingReaction.me) {
+ this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
+ } else {
+ this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
+ }
+ },
+ doActionWrap (button, close) {
+ if (button.name === 'emoji') {
+ this.$refs.picker.showPicker()
+ } else {
+ this.animationState = true
+ this.getComponent(button) === 'button' && this.doAction(button)
+ setTimeout(() => {
+ this.animationState = false
+ }, 500)
+ close()
+ }
+ }
+ }
+}
diff --git a/src/components/status_action_buttons/action_button.scss b/src/components/status_action_buttons/action_button.scss
@@ -0,0 +1,102 @@
+@import "../../mixins";
+/* stylelint-disable declaration-no-important */
+
+.quick-action {
+ display: grid;
+ grid-template-columns: minmax(max-content, 1fr);
+ grid-gap: 0.25em;
+ align-items: center;
+ height: 1.5em;
+
+ .action-counter {
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .action-button-inner,
+ .extra-button {
+ margin: -0.5em;
+ padding: 0.5em;
+ }
+
+ .separator {
+ display: block;
+ align-self: stretch;
+ width: 1px;
+ background-color: var(--icon);
+ margin-right: 0.5em;
+ }
+
+ &.-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;
+
+ .extra-button,
+ .separator {
+ display: none;
+ }
+ }
+
+ .action-button-inner {
+ display: grid;
+ grid-gap: 1em;
+ grid-template-columns: max-content 1fr;
+ grid-auto-flow: column;
+ grid-auto-columns: max-content;
+ align-items: center;
+ }
+}
+
+.action-button {
+ display: grid;
+ grid-auto-flow: column;
+ align-items: center;
+ padding: 0;
+
+ .action-button-inner {
+ &:hover,
+ &.-active {
+ &.reply-button:not(.disabled) {
+ .svg-inline--fa {
+ color: var(--cBlue);
+ }
+ }
+
+ &.retweet-button:not(.disabled) {
+ .svg-inline--fa {
+ color: var(--cGreen);
+ }
+ }
+
+ &.favorite-button:not(.disabled) {
+ .svg-inline--fa {
+ color: var(--cOrange);
+ }
+ }
+ }
+ }
+
+ @include unfocused-style {
+ .focus-marker {
+ visibility: hidden;
+ }
+
+ .active-marker {
+ visibility: visible;
+ }
+ }
+
+ @include focused-style {
+ .focus-marker {
+ visibility: visible;
+ }
+
+ .active-marker {
+ visibility: hidden;
+ }
+ }
+}
diff --git a/src/components/status_action_buttons/action_button.vue b/src/components/status_action_buttons/action_button.vue
@@ -0,0 +1,107 @@
+<template>
+ <div
+ class="action-button"
+ :class="buttonClass"
+ >
+ <component
+ :is="getComponent(button)"
+ class="action-button-inner"
+ :class="buttonInnerClass"
+ role="menuitem"
+ type="button"
+ target="_blank"
+ :tabindex="0"
+ :disabled="buttonClass.disabled"
+ :href="getComponent(button) == 'a' ? button.link?.(funcArg) || remoteInteractionLink : undefined"
+ @click="doActionWrap(button, close)"
+ >
+ <FALayers>
+ <FAIcon
+ class="fa-scale-110"
+ :icon="button.icon(funcArg)"
+ :spin="!extra && getComponent(button) == 'button' && button.animated?.() && animationState"
+ :style="{ '--fa-animation-duration': '750ms' }"
+ fixed-width
+ />
+ <template v-if="!buttonClass.disabled && button.toggleable?.(funcArg) && button.active">
+ <FAIcon
+ v-if="button.active(funcArg)"
+ class="active-marker"
+ transform="shrink-6 up-9 right-15"
+ :icon="button.activeIndicator?.(funcArg) || 'check'"
+ />
+ <FAIcon
+ v-if="!button.active(funcArg)"
+ class="focus-marker"
+ transform="shrink-6 up-9 right-15"
+ :icon="button.openIndicator?.(funcArg) || 'plus'"
+ />
+ <FAIcon
+ v-else
+ class="focus-marker"
+ transform="shrink-6 up-9 right-15"
+ :icon="button.closeIndicator?.(funcArg) || 'minus'"
+ />
+ </template>
+ </FALayers>
+ <span
+ v-if="extra"
+ class="action-label"
+ >
+ {{ $t(button.label(funcArg)) }}
+ </span>
+ <span
+ v-if="!extra && button.counter?.(funcArg) > 0"
+ class="action-counter"
+ >
+ {{ button.counter?.(funcArg) }}
+ </span>
+ <FAIcon
+ v-if="button.dropdown?.()"
+ class="chevron-icon"
+ size="lg"
+ :icon="extra ? 'chevron-right' : 'chevron-up'"
+ fixed-width
+ />
+ </component>
+ <span
+ v-if="!extra && button.name === 'bookmark'"
+ class="separator"
+ />
+ <Popover
+ v-if="button.name === 'bookmark'"
+ trigger="hover"
+ :placement="extra ? 'right' : 'top'"
+ :offset="{ y: 5 }"
+ :trigger-attrs="{ class: 'extra-button' }"
+ >
+ <template #trigger>
+ <FAIcon
+ class="chevron-icon"
+ size="lg"
+ :icon="extra ? 'chevron-right' : 'chevron-up'"
+ fixed-width
+ />
+ </template>
+ <template #content>
+ <StatusBookmarkFolderMenu
+ v-if="button.name === 'bookmark'"
+ :status="status"
+ />
+ </template>
+ </Popover>
+
+ <EmojiPicker
+ v-if="button.name === 'emoji'"
+ ref="picker"
+ :enable-sticker-picker="false"
+ :hide-custom-emoji="hideCustomEmoji"
+ class="emoji-picker-panel"
+ @emoji="addReaction"
+ />
+ </div>
+</template>
+
+<script src="./action_button.js" />
+
+<style lang="scss" src="./action_button.scss" />
diff --git a/src/components/status_action_buttons/action_button_container.js b/src/components/status_action_buttons/action_button_container.js
@@ -0,0 +1,89 @@
+import ActionButton from './action_button.vue'
+import Popover from 'src/components/popover/popover.vue'
+import MuteConfirm from 'src/components/confirm_modal/mute_confirm.vue'
+
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faUser,
+ faGlobe,
+ faFolderTree
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faUser,
+ faGlobe,
+ faFolderTree
+)
+
+export default {
+ components: {
+ ActionButton,
+ Popover,
+ MuteConfirm
+ },
+ props: ['button', 'status'],
+ mounted () {
+ if (this.button.name === 'mute') {
+ this.$store.dispatch('fetchDomainMutes')
+ }
+ },
+ computed: {
+ buttonClass () {
+ return [
+ this.button.name + '-button',
+ {
+ '-with-extra': this.button.name === 'bookmark',
+ '-extra': this.extra,
+ '-quick': !this.extra
+ }
+ ]
+ },
+ user () {
+ return this.status.user
+ },
+ userIsMuted () {
+ return this.$store.getters.relationship(this.user.id).muting
+ },
+ conversationIsMuted () {
+ return this.status.thread_muted
+ },
+ domain () {
+ return this.user.fqn.split('@')[1]
+ },
+ domainIsMuted () {
+ return new Set(this.$store.state.users.currentUser.domainMutes).has(this.domain)
+ }
+ },
+ methods: {
+ unmuteUser () {
+ return this.$store.dispatch('unmuteUser', this.user.id)
+ },
+ unmuteThread () {
+ return this.$store.dispatch('unmuteConversation', this.user.id)
+ },
+ unmuteDomain () {
+ return this.$store.dispatch('unmuteDomain', this.user.id)
+ },
+ toggleUserMute () {
+ if (this.userIsMuted) {
+ this.unmuteUser()
+ } else {
+ this.$refs.confirmUser.optionallyPrompt()
+ }
+ },
+ toggleConversationMute () {
+ if (this.conversationIsMuted) {
+ this.unmuteConversation()
+ } else {
+ this.$refs.confirmConversation.optionallyPrompt()
+ }
+ },
+ toggleDomainMute () {
+ if (this.domainIsMuted) {
+ this.unmuteDomain()
+ } else {
+ this.$refs.confirmDomain.optionallyPrompt()
+ }
+ }
+ }
+}
diff --git a/src/components/status_action_buttons/action_button_container.vue b/src/components/status_action_buttons/action_button_container.vue
@@ -0,0 +1,103 @@
+<template>
+ <div>
+ <Popover
+ v-if="button.dropdown?.()"
+ trigger="hover"
+ :offset="{ y: 5 }"
+ :placement="$attrs.extra ? 'right' : 'top'"
+ >
+ <template #trigger>
+ <ActionButton
+ :button="button"
+ :status="status"
+ v-bind.prop="$attrs"
+ />
+ </template>
+ <template #content>
+ <div
+ v-if="button.name === 'mute'"
+ :id="`popup-menu-${randomSeed}`"
+ class="dropdown-menu"
+ role="menu"
+ >
+ <div class="menu-item dropdown-item extra-action -icon">
+ <button
+ class="main-button"
+ @click="toggleUserMute"
+ >
+ <FAIcon
+ icon="user"
+ fixed-width
+ />
+ <template v-if="userIsMuted">
+ {{ $t('status.unmute_user') }}
+ </template>
+ <template v-else>
+ {{ $t('status.mute_user') }}
+ </template>
+ </button>
+ </div>
+ <div class="menu-item dropdown-item extra-action -icon">
+ <button
+ class="main-button"
+ @click="toggleUserMute"
+ >
+ <FAIcon
+ icon="folder-tree"
+ fixed-width
+ />
+ <template v-if="threadIsMuted">
+ {{ $t('status.unmute_conversation') }}
+ </template>
+ <template v-else>
+ {{ $t('status.mute_conversation') }}
+ </template>
+ </button>
+ </div>
+ <div class="menu-item dropdown-item extra-action -icon">
+ <button
+ class="main-button"
+ @click="toggleDomainMute"
+ >
+ <FAIcon
+ icon="globe"
+ fixed-width
+ />
+ <template v-if="domainIsMuted">
+ {{ $t('status.unmute_domain') }}
+ </template>
+ <template v-else>
+ {{ $t('status.mute_domain') }}
+ </template>
+ </button>
+ </div>
+ </div>
+ </template>
+ </Popover>
+ <ActionButton
+ v-else
+ :button="button"
+ :status="status"
+ v-bind="$attrs"
+ />
+ <teleport to="#modal">
+ <mute-confirm
+ ref="confirmConversation"
+ type="conversation"
+ :status="status"
+ />
+ <mute-confirm
+ ref="confirmDomain"
+ type="domain"
+ :user="user"
+ />
+ <mute-confirm
+ ref="confirmUser"
+ type="user"
+ :user="user"
+ />
+ </teleport>
+ </div>
+</template>
+
+<script src="./action_button_container.js" />
diff --git a/src/components/status_action_buttons/buttons_definitions.js b/src/components/status_action_buttons/buttons_definitions.js
@@ -0,0 +1,228 @@
+const PRIVATE_SCOPES = new Set(['private', 'direct'])
+const PUBLIC_SCOPES = new Set(['public', 'unlisted'])
+export const BUTTONS = [{
+ // =========
+ // REPLY
+ // =========
+ name: 'reply',
+ label: 'tool_tip.reply',
+ icon: 'reply',
+ active: ({ replying }) => replying,
+ counter: ({ status }) => status.replies_count,
+ anon: true,
+ anonLink: true,
+ toggleable: true,
+ closeIndicator: 'times',
+ activeIndicator: 'none',
+ action ({ emit }) {
+ emit('toggleReplying')
+ return Promise.resolve()
+ }
+}, {
+ // =========
+ // REPEAT
+ // =========
+ name: 'retweet',
+ label: ({ status }) => status.repeated
+ ? 'tool_tip.unrepeat'
+ : 'tool_tip.repeat',
+ icon ({ status }) {
+ if (PRIVATE_SCOPES.has(status.visibility)) {
+ return 'lock'
+ }
+ return 'retweet'
+ },
+ animated: true,
+ active: ({ status }) => status.repeated,
+ counter: ({ status }) => status.repeat_num,
+ anonLink: true,
+ interactive: ({ status, loggedIn }) => loggedIn && !PRIVATE_SCOPES.has(status.visibility),
+ toggleable: true,
+ confirm: ({ status, getters }) => !status.repeated && getters.mergedConfig.modalOnRepeat,
+ confirmStrings: {
+ title: 'status.repeat_confirm_title',
+ body: 'status.repeat_confirm',
+ confirm: 'status.repeat_confirm_accept_button',
+ cancel: 'status.repeat_confirm_cancel_button'
+ },
+ action ({ status, dispatch }) {
+ if (!status.repeated) {
+ return dispatch('retweet', { id: status.id })
+ } else {
+ return dispatch('unretweet', { id: status.id })
+ }
+ }
+}, {
+ // =========
+ // FAVORITE
+ // =========
+ name: 'favorite',
+ label: ({ status }) => status.favorited
+ ? 'tool_tip.unfavorite'
+ : 'tool_tip.favorite',
+ icon: ({ status }) => status.favorited
+ ? ['fas', 'star']
+ : ['far', 'star'],
+ animated: true,
+ active: ({ status }) => status.favorited,
+ counter: ({ status }) => status.fave_num,
+ anonLink: true,
+ toggleable: true,
+ action ({ status, dispatch }) {
+ if (!status.favorited) {
+ return dispatch('favorite', { id: status.id })
+ } else {
+ return dispatch('unfavorite', { id: status.id })
+ }
+ }
+}, {
+ // =========
+ // EMOJI REACTIONS
+ // =========
+ name: 'emoji',
+ label: 'tool_tip.add_reaction',
+ icon: ['far', 'smile-beam'],
+ anonLink: true
+}, {
+ // =========
+ // MUTE
+ // =========
+ name: 'mute',
+ icon: 'eye-slash',
+ label: 'status.mute_ellipsis',
+ if: ({ loggedIn }) => loggedIn,
+ toggleable: true,
+ dropdown: true
+ // action ({ status, dispatch, emit }) {
+ // }
+}, {
+ // =========
+ // PIN STATUS
+ // =========
+ name: 'pin',
+ icon: 'thumbtack',
+ label: ({ status }) => status.pinned
+ ? 'status.unpin'
+ : 'status.pin',
+ if ({ status, loggedIn, currentUser }) {
+ return loggedIn &&
+ status.user.id === currentUser.id &&
+ PUBLIC_SCOPES.has(status.visibility)
+ },
+ action ({ status, dispatch, emit }) {
+ if (status.pinned) {
+ return dispatch('unpinStatus', { id: status.id })
+ } else {
+ return dispatch('pinStatus', { id: status.id })
+ }
+ }
+}, {
+ // =========
+ // BOOKMARK
+ // =========
+ name: 'bookmark',
+ icon: ({ status }) => status.bookmarked
+ ? ['fas', 'bookmark']
+ : ['far', 'bookmark'],
+ toggleable: true,
+ active: ({ status }) => status.bookmarked,
+ label: ({ status }) => status.bookmarked
+ ? 'status.unbookmark'
+ : 'status.bookmark',
+ if: ({ loggedIn }) => loggedIn,
+ action ({ status, dispatch, emit }) {
+ if (status.bookmarked) {
+ return dispatch('unbookmark', { id: status.id })
+ } else {
+ return dispatch('bookmark', { id: status.id })
+ }
+ }
+}, {
+ // =========
+ // EDIT
+ // =========
+ name: 'edit',
+ icon: 'pen',
+ label: 'status.edit',
+ if ({ status, loggedIn, currentUser, state }) {
+ return loggedIn &&
+ state.instance.editingAvailable &&
+ status.user.id === currentUser.id
+ },
+ action ({ dispatch, status }) {
+ return dispatch('fetchStatusSource', { id: status.id })
+ .then(data => dispatch('openEditStatusModal', {
+ statusId: status.id,
+ subject: data.spoiler_text,
+ statusText: data.text,
+ statusIsSensitive: status.nsfw,
+ statusPoll: status.poll,
+ statusFiles: [...status.attachments],
+ visibility: status.visibility,
+ statusContentType: data.content_type
+ }))
+ }
+}, {
+ // =========
+ // DELETE
+ // =========
+ name: 'delete',
+ icon: 'times',
+ label: 'status.delete',
+ if ({ status, loggedIn, currentUser }) {
+ return loggedIn && (
+ status.user.id === currentUser.id ||
+ currentUser.privileges.includes('messages_delete')
+ )
+ },
+ confirm: ({ status, getters }) => getters.mergedConfig.modalOnDelete,
+ confirmStrings: {
+ title: 'status.delete_confirm_title',
+ body: 'status.delete_confirm',
+ confirm: 'status.delete_confirm_accept_button',
+ cancel: 'status.delete_confirm_cancel_button'
+ },
+ action ({ dispatch, status }) {
+ return dispatch('deleteStatus', { id: status.id })
+ }
+}, {
+ // =========
+ // SHARE/COPY
+ // =========
+ name: 'share',
+ icon: 'share-alt',
+ label: 'status.copy_link',
+ action ({ state, status, router }) {
+ navigator.clipboard.writeText([
+ state.instance.server,
+ router.resolve({ name: 'conversation', params: { id: status.id } }).href
+ ].join(''))
+ return Promise.resolve()
+ }
+}, {
+ // =========
+ // EXTERNAL
+ // =========
+ name: 'external',
+ icon: 'external-link-alt',
+ label: 'status.external_source',
+ link: ({ status }) => status.external_url
+}, {
+ // =========
+ // REPORT
+ // =========
+ name: 'report',
+ icon: 'flag',
+ label: 'user_card.report',
+ if: ({ loggedIn }) => loggedIn,
+ action ({ dispatch, status }) {
+ dispatch('openUserReportingModal', { userId: status.user.id, statusIds: [status.id] })
+ }
+}].map(button => {
+ return Object.fromEntries(
+ Object.entries(button).map(([k, v]) => [
+ k,
+ (typeof v === 'function' || k === 'name') ? v : () => v
+ ])
+ )
+})
diff --git a/src/components/status_action_buttons/status_action_buttons.js b/src/components/status_action_buttons/status_action_buttons.js
@@ -0,0 +1,136 @@
+import { mapState } from 'vuex'
+
+import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
+import ActionButtonContainer from './action_button_container.vue'
+import Popover from 'src/components/popover/popover.vue'
+import genRandomSeed from 'src/services/random_seed/random_seed.service.js'
+
+import { BUTTONS } from './buttons_definitions.js'
+
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faEllipsisH
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faEllipsisH
+)
+
+const StatusActionButtons = {
+ props: ['status', 'replying'],
+ emits: ['toggleReplying'],
+ data () {
+ return {
+ showPin: false,
+ showingConfirmDialog: false,
+ currentConfirmTitle: '',
+ currentConfirmOkText: '',
+ currentConfirmCancelText: '',
+ currentConfirmAction: () => {},
+ randomSeed: genRandomSeed()
+ }
+ },
+ components: {
+ Popover,
+ ConfirmModal,
+ ActionButtonContainer
+ },
+ computed: {
+ ...mapState({
+ pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedStatusActions)
+ }),
+ buttons () {
+ return BUTTONS.filter(x => x.if ? x.if(this.funcArg) : true)
+ },
+ quickButtons () {
+ return this.buttons.filter(x => this.pinnedItems.has(x.name))
+ },
+ extraButtons () {
+ return this.buttons.filter(x => !this.pinnedItems.has(x.name))
+ },
+ currentUser () {
+ return this.$store.state.users.currentUser
+ },
+ hideCustomEmoji () {
+ return !this.$store.state.instance.pleromaCustomEmojiReactionsAvailable
+ },
+ funcArg () {
+ return {
+ status: this.status,
+ replying: this.replying,
+ emit: this.$emit,
+ dispatch: this.$store.dispatch,
+ state: this.$store.state,
+ getters: this.$store.getters,
+ router: this.$router,
+ currentUser: this.currentUser,
+ loggedIn: !!this.currentUser
+ }
+ },
+ triggerAttrs () {
+ return {
+ title: this.$t('status.more_actions'),
+ 'aria-controls': `popup-menu-${this.randomSeed}`,
+ 'aria-expanded': this.expanded,
+ 'aria-haspopup': 'menu'
+ }
+ }
+ },
+ methods: {
+ doAction (button) {
+ if (button.confirm?.(this.funcArg)) {
+ // TODO move to action_button
+ this.currentConfirmTitle = this.$t(button.confirmStrings(this.funcArg).title)
+ this.currentConfirmOkText = this.$t(button.confirmStrings(this.funcArg).confirm)
+ this.currentConfirmCancelText = this.$t(button.confirmStrings(this.funcArg).cancel)
+ this.currentConfirmBody = this.$t(button.confirmStrings(this.funcArg).body)
+ this.currentConfirmAction = () => {
+ this.showingConfirmDialog = false
+ this.doActionReal(button)
+ }
+ this.showingConfirmDialog = true
+ } else {
+ this.doActionReal(button)
+ }
+ },
+ doActionReal (button) {
+ button.action(this.funcArg)
+ .then(() => this.$emit('onSuccess'))
+ .catch(err => this.$emit('onError', err.error.error))
+ },
+ onExtraClose () {
+ this.showPin = false
+ },
+ isPinned (button) {
+ return this.pinnedItems.has(button.name)
+ },
+ unpin (button) {
+ this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedStatusActions', value: button.name })
+ this.$store.dispatch('pushServerSideStorage')
+ },
+ pin (button) {
+ this.$store.commit('addCollectionPreference', { path: 'collections.pinnedStatusActions', value: button.name })
+ this.$store.dispatch('pushServerSideStorage')
+ },
+ getComponent (button) {
+ if (!this.$store.state.users.currentUser && button.anonLink) {
+ return 'a'
+ } else if (button.action == null && button.link != null) {
+ return 'a'
+ } else {
+ return 'button'
+ }
+ },
+ getClass (button) {
+ return {
+ [button.name + '-button']: true,
+ disabled: button.interactive ? !button.interactive(this.funcArg) : false,
+ '-pin-edit': this.showPin,
+ '-dropdown': button.dropdown?.(),
+ '-active': button.active?.(this.funcArg)
+ }
+ }
+ }
+}
+
+export default StatusActionButtons
diff --git a/src/components/status_action_buttons/status_action_buttons.scss b/src/components/status_action_buttons/status_action_buttons.scss
@@ -0,0 +1,27 @@
+@import "../../mixins";
+
+.StatusActionButtons {
+ .quick-action-buttons {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, 5em);
+ grid-auto-flow: row dense;
+ grid-auto-rows: 1fr;
+ grid-gap: 1.25em 1em;
+ margin-top: var(--status-margin);
+ align-items: baseline;
+ }
+
+ .pin-action-button {
+ margin: -0.5em;
+ padding: 0.5em;
+ }
+}
+// popover
+.extra-action-buttons {
+ .extra-action {
+ margin: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ padding-right: 0;
+ }
+}
diff --git a/src/components/status_action_buttons/status_action_buttons.vue b/src/components/status_action_buttons/status_action_buttons.vue
@@ -0,0 +1,131 @@
+<template>
+ <div class="StatusActionButtons">
+ <span class="quick-action-buttons">
+ <span
+ v-for="button in quickButtons"
+ :key="button.name"
+ class="quick-action"
+ :class="{ '-pin': showPin, '-toggle': button.dropdown?.() }"
+ >
+ <ActionButtonContainer
+ :class="{ '-pin': showPin }"
+ :button="button"
+ :status="status"
+ :extra="false"
+ :func-arg="funcArg"
+ :get-class="getClass"
+ :get-component="getComponent"
+ :close="() => {}"
+ :do-action="doAction"
+ />
+ <button
+ v-if="showPin && currentUser"
+ type="button"
+ class="button-unstyled pin-action-button"
+ :title="$t('general.unpin')"
+ :aria-pressed="true"
+ @click.stop.prevent="unpin(button)"
+ >
+ <FAIcon
+ v-if="showPin && currentUser"
+ fixed-width
+ class="fa-scale-110"
+ icon="thumbtack"
+ />
+ </button>
+ </span>
+ <Popover
+ trigger="click"
+ :trigger-attrs="triggerAttrs"
+ :tabindex="0"
+ placement="top"
+ :offset="{ y: 5 }"
+ remove-padding
+ @close="onExtraClose"
+ >
+ <template #trigger>
+ <FAIcon
+ class="fa-scale-110 "
+ icon="ellipsis-h"
+ />
+ </template>
+ <template #content="{close, resize}">
+ <div
+ :id="`popup-menu-${randomSeed}`"
+ class="dropdown-menu extra-action-buttons"
+ role="menu"
+ >
+ <div
+ v-if="currentUser"
+ class="menu-item dropdown-item extra-action -icon"
+ >
+ <button
+ class="main-button"
+ role="menuitem"
+ :tabindex="0"
+ @click.stop="() => { resize(); showPin = !showPin }"
+ >
+ <FAIcon
+ class="fa-scale-110"
+ fixed-width
+ icon="wrench"
+ /><span>{{ $t('nav.edit_pinned') }}</span>
+ </button>
+ </div>
+ <div
+ v-for="button in extraButtons"
+ :key="button.name"
+ class="menu-item dropdown-item extra-action -icon"
+ :disabled="getClass(button).disabled"
+ :class="{ disabled: getClass(button).disabled }"
+ >
+ <ActionButtonContainer
+ :button="button"
+ :status="status"
+ :extra="true"
+ :func-arg="funcArg"
+ :get-class="getClass"
+ :get-component="getComponent"
+ :close="close"
+ :do-action="doAction"
+ />
+ <button
+ v-if="showPin && currentUser"
+ type="button"
+ class="button-unstyled pin-action-button extra-button"
+ :title="$t('general.pin')"
+ :aria-pressed="false"
+ @click.stop.prevent="pin(button)"
+ >
+ <FAIcon
+ v-if="showPin && currentUser"
+ fixed-width
+ class="fa-scale-110"
+ transform="rotate-45"
+ icon="thumbtack"
+ />
+ </button>
+ </div>
+ </div>
+ </template>
+ </Popover>
+ </span>
+
+ <teleport to="#modal">
+ <confirm-modal
+ v-if="showingConfirmDialog"
+ :title="currentConfirmTitle"
+ :confirm-text="currentConfirmOkText"
+ :cancel-text="currentConfirmCancelText"
+ @accepted="currentConfirmAction"
+ @cancelled="showingConfirmDialog = false"
+ >
+ {{ currentConfirmBody }}
+ </confirm-modal>
+ </teleport>
+ </div>
+</template>
+
+<script src="./status_action_buttons.js"></script>
+
+<style lang="scss" src="./status_action_buttons.scss"></style>
diff --git a/src/components/status_bookmark_folder_menu/status_bookmark_folder_menu.vue b/src/components/status_bookmark_folder_menu/status_bookmark_folder_menu.vue
@@ -1,39 +1,21 @@
<template>
- <div class="StatusBookmarkFolderMenu">
- <Popover
- trigger="hover"
- placement="left"
- remove-padding
+ <div class="dropdown-menu">
+ <div
+ v-for="folder in folders"
+ :key="folder.id"
+ class="menu-item dropdown-item -icon"
>
- <template #content>
- <div class="dropdown-menu">
- <button
- v-for="folder in folders"
- :key="folder.id"
- class="menu-item dropdown-item"
- @click="toggleFolder(folder.id)"
- >
- <span
- class="input menu-checkbox -radio"
- :class="{ 'menu-checkbox-checked': status.bookmark_folder_id == folder.id }"
- />
- {{ folder.name }}
- </button>
- </div>
- </template>
- <template #trigger>
- <button class="menu-item dropdown-item dropdown-item-icon -has-submenu">
- <FAIcon
- fixed-width
- icon="folder"
- />{{ $t('bookmark_folders.select_folder') }}<FAIcon
- class="chevron-icon"
- size="lg"
- icon="chevron-right"
- />
- </button>
- </template>
- </Popover>
+ <button
+ class="main-button"
+ @click="toggleFolder(folder.id)"
+ >
+ <span
+ class="input menu-checkbox -radio"
+ :class="{ 'menu-checkbox-checked': status.bookmark_folder_id == folder.id }"
+ />
+ {{ folder.name }}
+ </button>
+ </div>
</div>
</template>
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
@@ -76,6 +76,7 @@
</div>
</template>
<QuickFilterSettings
+ v-if="!mobileLayout"
class="rightside-button"
/>
<QuickViewSettings
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
@@ -1,4 +1,3 @@
-import { unitToSeconds } from 'src/services/date_utils/date_utils.js'
import UserAvatar from '../user_avatar/user_avatar.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue'
import ProgressButton from '../progress_button/progress_button.vue'
@@ -9,7 +8,7 @@ import UserNote from '../user_note/user_note.vue'
import Select from '../select/select.vue'
import UserLink from '../user_link/user_link.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
-import ConfirmModal from '../confirm_modal/confirm_modal.vue'
+import MuteConfirm from '../confirm_modal/mute_confirm.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
@@ -48,7 +47,6 @@ export default {
data () {
return {
followRequestInProgress: false,
- showingConfirmMute: false,
muteExpiryAmount: 0,
muteExpiryUnit: 'minutes'
}
@@ -141,12 +139,6 @@ export default {
supportsNote () {
return 'note' in this.relationship
},
- shouldConfirmMute () {
- return this.mergedConfig.modalOnMute
- },
- muteExpiryUnits () {
- return ['minutes', 'hours', 'days']
- },
...mapGetters(['mergedConfig'])
},
components: {
@@ -160,28 +152,11 @@ export default {
RichContent,
UserLink,
UserNote,
- ConfirmModal
+ MuteConfirm
},
methods: {
- showConfirmMute () {
- this.showingConfirmMute = true
- },
- hideConfirmMute () {
- this.showingConfirmMute = false
- },
muteUser () {
- if (!this.shouldConfirmMute) {
- this.doMuteUser()
- } else {
- this.showConfirmMute()
- }
- },
- doMuteUser () {
- this.$store.dispatch('muteUser', {
- id: this.user.id,
- expiresIn: this.shouldConfirmMute ? unitToSeconds(this.muteExpiryUnit, this.muteExpiryAmount) : 0
- })
- this.hideConfirmMute()
+ this.$refs.confirmation.optionallyPrompt()
},
unmuteUser () {
this.$store.dispatch('unmuteUser', this.user.id)
diff --git a/src/components/user_card/user_card.scss b/src/components/user_card/user_card.scss
@@ -321,8 +321,3 @@
text-decoration: none;
}
}
-
-.mute-expiry {
- display: flex;
- flex-direction: row;
-}
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
@@ -311,51 +311,11 @@
/>
</div>
<teleport to="#modal">
- <confirm-modal
- v-if="showingConfirmMute"
- :title="$t('user_card.mute_confirm_title')"
- :confirm-text="$t('user_card.mute_confirm_accept_button')"
- :cancel-text="$t('user_card.mute_confirm_cancel_button')"
- @accepted="doMuteUser"
- @cancelled="hideConfirmMute"
- >
- <i18n-t
- keypath="user_card.mute_confirm"
- tag="div"
- >
- <template #user>
- <span
- v-text="user.screen_name_ui"
- />
- </template>
- </i18n-t>
- <div
- class="mute-expiry"
- >
- <label>
- {{ $t('user_card.mute_duration_prompt') }}
- </label>
- <input
- v-model="muteExpiryAmount"
- type="number"
- class="expiry-amount hide-number-spinner"
- :min="0"
- >
- <Select
- v-model="muteExpiryUnit"
- unstyled="true"
- class="expiry-unit"
- >
- <option
- v-for="unit in muteExpiryUnits"
- :key="unit"
- :value="unit"
- >
- {{ $t(`time.${unit}_short`, ['']) }}
- </option>
- </Select>
- </div>
- </confirm-modal>
+ <mute-confirm
+ ref="confirmation"
+ type="user"
+ :user="user"
+ />
</teleport>
</div>
</template>
diff --git a/src/components/user_list_menu/user_list_menu.js b/src/components/user_list_menu/user_list_menu.js
@@ -34,6 +34,11 @@ const UserListMenu = {
...list,
inList: this.inListsSet.has(list.id)
}))
+ },
+ triggerAttrs () {
+ return {
+ class: 'menu-item dropdown-item -has-submenu'
+ }
}
},
methods: {
diff --git a/src/components/user_list_menu/user_list_menu.vue b/src/components/user_list_menu/user_list_menu.vue
@@ -2,34 +2,39 @@
<div class="UserListMenu">
<Popover
trigger="hover"
- placement="left"
+ placement="right"
+ :trigger-attrs="triggerAttrs"
remove-padding
>
<template #content>
<div class="dropdown-menu">
- <button
+ <div
v-for="list in lists"
:key="list.id"
- class="menu-item dropdown-item"
- @click="toggleList(list.id)"
+ class="menu-item dropdown-item -icon"
>
- <span
- class="input menu-checkbox"
- :class="{ 'menu-checkbox-checked': list.inList }"
- />
- {{ list.title }}
- </button>
+ <button
+ class="main-button"
+ @click="toggleList(list.id)"
+ >
+ <span
+ class="input menu-checkbox"
+ :class="{ 'menu-checkbox-checked': list.inList }"
+ />
+ {{ list.title }}
+ </button>
+ </div>
</div>
</template>
<template #trigger>
- <button class="menu-item dropdown-item -has-submenu">
+ <span class="main-button">
{{ $t('lists.manage_lists') }}
<FAIcon
class="chevron-icon"
size="lg"
icon="chevron-right"
/>
- </button>
+ </span>
</template>
</Popover>
</div>
diff --git a/src/i18n/en.json b/src/i18n/en.json
@@ -491,6 +491,8 @@
"confirm_dialogs_unfollow": "unfollowing a user",
"confirm_dialogs_block": "blocking a user",
"confirm_dialogs_mute": "muting a user",
+ "confirm_dialogs_mute_domain": "muting domains",
+ "confirm_dialogs_mute_conversation": "muting conversations",
"confirm_dialogs_delete": "deleting a status",
"confirm_dialogs_logout": "logging out",
"confirm_dialogs_approve_follow": "approving a follower",
@@ -1213,7 +1215,8 @@
"socket_reconnected": "Realtime connection established",
"socket_broke": "Realtime connection lost: CloseEvent code {0}",
"quick_view_settings": "Quick view settings",
- "quick_filter_settings": "Quick filter settings"
+ "quick_filter_settings": "Quick filter settings",
+ "filter_settings": "Filter"
},
"status": {
"favorites": "Favorites",
@@ -1242,6 +1245,11 @@
"mentions": "Mentions",
"replies_list": "Replies:",
"replies_list_with_others": "Replies (+{numReplies} other): | Replies (+{numReplies} others):",
+ "mute_ellipsis": "Mute…",
+ "mute_user": "Mute user",
+ "unmute_user": "Unmute user",
+ "mute_domain": "Mute domain",
+ "unmute_domain": "Unmute domain",
"mute_conversation": "Mute conversation",
"unmute_conversation": "Unmute conversation",
"status_unavailable": "Status unavailable",
@@ -1414,8 +1422,11 @@
"media_upload": "Upload media",
"mentions": "Mentions",
"repeat": "Repeat",
+ "unrepeat": "Unrepeat",
"reply": "Reply",
+ "add_reaction": "Add reaction",
"favorite": "Favorite",
+ "unfavorite": "Unfavorite",
"add_reaction": "Add Reaction",
"user_settings": "User Settings",
"accept_follow_request": "Accept follow request",
diff --git a/src/modules/config.js b/src/modules/config.js
@@ -137,6 +137,8 @@ export const defaultState = {
modalOnUnfollow: undefined, // instance default
modalOnBlock: undefined, // instance default
modalOnMute: undefined, // instance default
+ modalOnMuteConversation: undefined, // instance default
+ modalOnMuteDomain: undefined, // instance default
modalOnDelete: undefined, // instance default
modalOnLogout: undefined, // instance default
modalOnApproveFollow: undefined, // instance default
diff --git a/src/modules/instance.js b/src/modules/instance.js
@@ -77,6 +77,8 @@ const defaultState = {
modalOnUnfollow: false,
modalOnBlock: true,
modalOnMute: false,
+ modalOnMuteConversation: false,
+ modalOnMuteDomain: true,
modalOnDelete: true,
modalOnLogout: true,
modalOnApproveFollow: false,
diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js
@@ -1,5 +1,16 @@
import { toRaw } from 'vue'
-import { isEqual, cloneDeep, set, get, clamp, flatten, groupBy, findLastIndex, takeRight, uniqWith } from 'lodash'
+import {
+ isEqual,
+ cloneDeep,
+ set,
+ get,
+ clamp,
+ flatten,
+ groupBy,
+ findLastIndex,
+ takeRight,
+ uniqWith
+} from 'lodash'
import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js'
export const VERSION = 1
@@ -26,6 +37,7 @@ export const defaultState = {
collapseNav: false
},
collections: {
+ pinnedStatusActions: ['reply', 'retweet', 'favorite', 'emoji'],
pinnedNavItems: ['home', 'dms', 'chats']
}
},
@@ -77,7 +89,7 @@ const _verifyPrefs = (state) => {
})
}
-export const _getRecentData = (cache, live) => {
+export const _getRecentData = (cache, live, isTest) => {
const result = { recent: null, stale: null, needUpload: false }
const cacheValid = _checkValidity(cache || {})
const liveValid = _checkValidity(live || {})
@@ -110,6 +122,23 @@ export const _getRecentData = (cache, live) => {
console.debug('Both sources are invalid, start from scratch')
result.needUpload = true
}
+
+ const merge = (a, b) => ({
+ _version: a._version ?? b._version,
+ _timestamp: a._timestamp ?? b._timestamp,
+ needUpload: b.needUpload ?? a.needUpload,
+ prefsStorage: {
+ ...a.prefsStorage,
+ ...b.prefsStorage
+ },
+ flagStorage: {
+ ...a.flagStorage,
+ ...b.flagStorage
+ }
+ })
+ result.recent = isTest ? result.recent : (result.recent && merge(defaultState, result.recent))
+ result.stale = isTest ? result.stale : (result.stale && merge(defaultState, result.stale))
+
return result
}
@@ -281,7 +310,7 @@ export const mutations = {
clearServerSideStorage (state, userData) {
state = { ...cloneDeep(defaultState) }
},
- setServerSideStorage (state, userData) {
+ setServerSideStorage (state, userData, test) {
const live = userData.storage
state.raw = live
let cache = state.cache
@@ -292,7 +321,7 @@ export const mutations = {
cache = _doMigrations(cache)
- let { recent, stale, needsUpload } = _getRecentData(cache, live)
+ let { recent, stale, needUpload } = _getRecentData(cache, live)
const userNew = userData.created_at > NEW_USER_DATE
const flagsTemplate = userNew ? newUserFlags : defaultState.flagStorage
@@ -306,7 +335,7 @@ export const mutations = {
})
}
- if (!needsUpload && recent && stale) {
+ if (!needUpload && recent && stale) {
console.debug('Checking if data needs merging...')
// discarding timestamps and versions
const { _timestamp: _0, _version: _1, ...recentData } = recent
@@ -335,7 +364,7 @@ export const mutations = {
recent.flagStorage = { ...flagsTemplate, ...totalFlags }
recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs }
- state.dirty = dirty || needsUpload
+ state.dirty = dirty || needUpload
state.cache = recent
// set local timestamp to smaller one if we don't have any changes
if (stale && recent && !state.dirty) {
diff --git a/test/unit/specs/modules/serverSideStorage.spec.js b/test/unit/specs/modules/serverSideStorage.spec.js
@@ -74,7 +74,7 @@ describe('The serverSideStorage module', () => {
})
})
- it('should reset local timestamp to remote if contents are the same', () => {
+ it.only('should reset local timestamp to remote if contents are the same', () => {
const state = {
...cloneDeep(defaultState),
cache: null
@@ -176,33 +176,33 @@ describe('The serverSideStorage module', () => {
})
describe('_getRecentData', () => {
it('should handle nulls correctly', () => {
- expect(_getRecentData(null, null)).to.eql({ recent: null, stale: null, needUpload: true })
+ expect(_getRecentData(null, null, true)).to.eql({ recent: null, stale: null, needUpload: true })
})
it('doesn\'t choke on invalid data', () => {
- expect(_getRecentData({ a: 1 }, { b: 2 })).to.eql({ recent: null, stale: null, needUpload: true })
+ expect(_getRecentData({ a: 1 }, { b: 2 }, true)).to.eql({ recent: null, stale: null, needUpload: true })
})
it('should prefer the valid non-null correctly, needUpload works properly', () => {
const nonNull = { _version: VERSION, _timestamp: 1 }
- expect(_getRecentData(nonNull, null)).to.eql({ recent: nonNull, stale: null, needUpload: true })
- expect(_getRecentData(null, nonNull)).to.eql({ recent: nonNull, stale: null, needUpload: false })
+ expect(_getRecentData(nonNull, null, true)).to.eql({ recent: nonNull, stale: null, needUpload: true })
+ expect(_getRecentData(null, nonNull, true)).to.eql({ recent: nonNull, stale: null, needUpload: false })
})
it('should prefer the one with higher timestamp', () => {
const a = { _version: VERSION, _timestamp: 1 }
const b = { _version: VERSION, _timestamp: 2 }
- expect(_getRecentData(a, b)).to.eql({ recent: b, stale: a, needUpload: false })
- expect(_getRecentData(b, a)).to.eql({ recent: b, stale: a, needUpload: false })
+ expect(_getRecentData(a, b, true)).to.eql({ recent: b, stale: a, needUpload: false })
+ expect(_getRecentData(b, a, true)).to.eql({ recent: b, stale: a, needUpload: false })
})
it('case where both are same', () => {
const a = { _version: VERSION, _timestamp: 3 }
const b = { _version: VERSION, _timestamp: 3 }
- expect(_getRecentData(a, b)).to.eql({ recent: b, stale: a, needUpload: false })
- expect(_getRecentData(b, a)).to.eql({ recent: b, stale: a, needUpload: false })
+ expect(_getRecentData(a, b, true)).to.eql({ recent: b, stale: a, needUpload: false })
+ expect(_getRecentData(b, a, true)).to.eql({ recent: b, stale: a, needUpload: false })
})
})