Merge branch 'customizable-post-actions' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2025-01-16 20:14:51 +02:00
commit 478779121d
59 changed files with 3192 additions and 3350 deletions

View file

@ -38,8 +38,8 @@ lint:
stage: lint
script:
- yarn
- npm run lint
- npm run stylelint
- yarn lint
- yarn stylelint
test:
stage: test
@ -62,7 +62,7 @@ build:
- himem
script:
- yarn
- npm run build
- yarn build
artifacts:
paths:
- dist/

View file

@ -11,11 +11,6 @@ var versionRequirements = [
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
},
{
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
}
]

View file

@ -0,0 +1 @@
Post actions can be customized

View file

View file

@ -0,0 +1 @@
Added missing EN translation key for status.muted_user

View file

View file

View file

@ -10,13 +10,13 @@
"unit": "karma start test/unit/karma.conf.js --single-run",
"unit:watch": "karma start test/unit/karma.conf.js --single-run=false",
"e2e": "node test/e2e/runner.js",
"test": "npm run unit && npm run e2e",
"stylelint": "npx stylelint '**/*.scss' '**/*.vue'",
"test": "yarn run unit && yarn run e2e",
"stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'",
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
},
"dependencies": {
"@babel/runtime": "7.21.5",
"@babel/runtime": "7.26.0",
"@chenfengyuan/vue-qrcode": "2.0.0",
"@fortawesome/fontawesome-svg-core": "6.4.0",
"@fortawesome/free-regular-svg-icons": "6.4.0",
@ -24,7 +24,7 @@
"@fortawesome/vue-fontawesome": "3.0.3",
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
"@kazvmoe-infra/unicode-emoji-json": "0.4.0",
"@ruffle-rs/ruffle": "0.1.0-nightly.2024.8.21",
"@ruffle-rs/ruffle": "0.1.0-nightly.2025.1.13",
"@vuelidate/core": "2.0.3",
"@vuelidate/validators": "2.0.4",
"body-scroll-lock": "3.1.5",
@ -37,11 +37,11 @@
"localforage": "1.10.0",
"pako": "^2.1.0",
"parse-link-header": "2.0.0",
"phoenix": "1.7.7",
"punycode.js": "2.3.0",
"qrcode": "1.5.3",
"phoenix": "1.7.18",
"punycode.js": "2.3.1",
"qrcode": "1.5.4",
"querystring-es3": "0.2.1",
"url": "0.11.0",
"url": "0.11.4",
"utf8": "3.0.0",
"vue": "3.2.45",
"vue-i18n": "10",
@ -51,18 +51,18 @@
"vuex": "4.1.0"
},
"devDependencies": {
"@babel/core": "7.21.8",
"@babel/eslint-parser": "7.21.8",
"@babel/plugin-transform-runtime": "7.21.4",
"@babel/preset-env": "7.21.5",
"@babel/register": "7.21.0",
"@babel/core": "7.26.0",
"@babel/eslint-parser": "7.26.5",
"@babel/plugin-transform-runtime": "7.25.9",
"@babel/preset-env": "7.26.0",
"@babel/register": "7.25.9",
"@intlify/vue-i18n-loader": "5.0.1",
"@ungap/event-target": "0.2.4",
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
"@vue/babel-plugin-jsx": "1.2.2",
"@vue/babel-plugin-jsx": "1.2.5",
"@vue/compiler-sfc": "3.2.45",
"@vue/test-utils": "2.2.8",
"autoprefixer": "10.4.19",
"autoprefixer": "10.4.20",
"babel-loader": "9.1.3",
"babel-plugin-lodash": "3.3.4",
"chai": "4.3.7",
@ -70,7 +70,7 @@
"chromedriver": "108.0.0",
"connect-history-api-fallback": "2.0.0",
"copy-webpack-plugin": "11.0.0",
"cross-spawn": "7.0.3",
"cross-spawn": "7.0.6",
"css-loader": "6.10.0",
"css-minimizer-webpack-plugin": "4.2.2",
"custom-event-polyfill": "1.0.7",
@ -83,14 +83,14 @@
"eslint-plugin-vue": "9.9.0",
"eslint-webpack-plugin": "3.2.0",
"eventsource-polyfill": "0.9.6",
"express": "4.18.2",
"function-bind": "1.1.1",
"express": "4.21.2",
"function-bind": "1.1.2",
"html-webpack-plugin": "5.5.1",
"http-proxy-middleware": "2.0.6",
"http-proxy-middleware": "2.0.7",
"iso-639-1": "2.1.15",
"json-loader": "0.5.7",
"karma": "6.4.4",
"karma-coverage": "2.2.0",
"karma-coverage": "2.2.1",
"karma-firefox-launcher": "2.1.3",
"karma-mocha": "2.0.1",
"karma-mocha-reporter": "2.2.5",
@ -110,7 +110,7 @@
"postcss-scss": "^4.0.6",
"sass": "1.60.0",
"sass-loader": "13.2.2",
"selenium-server": "2.53.1",
"selenium-server": "3.141.59",
"semver": "7.3.8",
"serviceworker-webpack5-plugin": "2.0.0",
"shelljs": "0.8.5",
@ -131,8 +131,7 @@
"webpack-merge": "0.20.0"
},
"engines": {
"node": ">= 16.0.0",
"npm": ">= 3.0.0"
"node": ">= 16.0.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View file

@ -339,6 +339,8 @@ nav {
grid-template-areas: "content";
padding: 0;
--_actionsColumnCount: 3;
.column {
padding-top: 0;
margin: var(--navbar-height) 0 0 0;

View file

@ -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>

View file

@ -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>

View file

@ -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()
}
}
}

View file

@ -0,0 +1,58 @@
<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 class="mute-expiry" v-if="type !== 'domain'">
<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>

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -10,119 +10,150 @@
>
<template #content>
<div class="dropdown-menu">
<span v-if="canGrantRole">
<button
class="menu-item dropdown-item menu-item"
@click="toggleRight(&quot;admin&quot;)"
>
{{ $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(&quot;moderator&quot;)"
>
{{ $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(&quot;admin&quot;)"
>
{{ $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(&quot;moderator&quot;)"
>
{{ $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
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)"
>
{{ $t('user_card.admin_menu.delete_account') }}
</button>
</template>
<div
v-if="canUseTagPolicy"
role="separator"
class="dropdown-divider"
/>
<span v-if="canUseTagPolicy">
v-if="canChangeActivationState"
class="menu-item dropdown-item -icon-space"
>
<button
class="menu-item dropdown-item menu-item"
@click="toggleTag(tags.FORCE_NSFW)"
class="main-button"
@click="toggleActivationStatus()"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"
/>
{{ $t('user_card.admin_menu.force_nsfw') }}
{{ $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.STRIP_MEDIA)"
class="main-button"
@click="deleteUserDialog(true)"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"
/>
{{ $t('user_card.admin_menu.strip_media') }}
{{ $t('user_card.admin_menu.delete_account') }}
</button>
<button
class="menu-item dropdown-item menu-item"
@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>
<button
class="menu-item dropdown-item menu-item"
@click="toggleTag(tags.SANDBOX)"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"
/>
{{ $t('user_card.admin_menu.sandbox') }}
</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>

View file

@ -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>

View file

@ -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

View file

@ -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;
}
}
}
}

View file

@ -1,5 +1,6 @@
<template>
<span
class="popover-wrapper"
@mouseenter="onMouseenter"
@mouseleave="onMouseleave"
>
@ -41,101 +42,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>

View file

@ -768,8 +768,8 @@ const PostStatusForm = {
this.newStatus.id = id
}
this.saveable = false
this.clearStatus()
if (!this.shouldAutoSaveDraft) {
this.clearStatus()
this.$emit('draft-done')
}
})
@ -778,8 +778,8 @@ const PostStatusForm = {
return this.abandonDraft()
.then(() => {
this.saveable = false
this.clearStatus()
if (!this.shouldAutoSaveDraft) {
this.clearStatus()
this.$emit('draft-done')
}
})
@ -789,7 +789,7 @@ const PostStatusForm = {
},
maybeAutoSaveDraft () {
if (this.shouldAutoSaveDraft) {
this.saveDraft()
this.saveDraft(false)
}
},
abandonDraft () {

View file

@ -335,8 +335,8 @@
role="menu"
>
<button
class="menu-item dropdown-item"
v-if="!hideDraft || !disableDraft"
class="menu-item dropdown-item dropdown-item-icon"
role="menu"
:disabled="!safeToSaveDraft && saveable"
:class="{ disabled: !safeToSaveDraft }"

View file

@ -14,106 +14,126 @@
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"
:aria-checked="replyVisibilityAll"
role="menuitemradio"
@click="replyVisibilityAll = true"
class="menu-item dropdown-item -icon"
>
<span
class="input menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': replyVisibilityAll }"
:aria-hidden="true"
/>{{ $t('settings.reply_visibility_all') }}
</button>
<button
<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="replyVisibilityFollowing"
role="menuitemradio"
@click="replyVisibilityFollowing = true"
class="menu-item dropdown-item -icon"
>
<span
class="input menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': replyVisibilityFollowing }"
:aria-hidden="true"
/>{{ $t('settings.reply_visibility_following_short') }}
</button>
<button
v-if="!conversation"
class="menu-item dropdown-item"
: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>
<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>
<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 class="menu-item dropdown-item -icon">
<button
class="main-button"
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>
</div>
<div class="menu-item dropdown-item -icon">
<button
class="main-button"
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>
</div>
<div class="menu-item dropdown-item -icon">
<button
class="main-button"
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>
</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>
</div>
</template>
<template #trigger>

View file

@ -11,86 +11,107 @@
role="menu"
>
<div role="group">
<button
class="menu-item dropdown-item"
: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"
/> {{ $t('settings.conversation_display_tree_quick') }}
</button>
<button
class="menu-item dropdown-item"
: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"
/> {{ $t('settings.conversation_display_linear_quick') }}
</button>
<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"
/>
<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
<div class="menu-item dropdown-item -icon">
<button
class="main-button"
role="menuitemcheckbox"
:aria-checked="showUserAvatars"
@click="showUserAvatars = !showUserAvatars"
>
<span
class="main-button"
:class="{ 'menu-checkbox-checked': showUserAvatars }"
:aria-hidden="true"
/>{{ $t('settings.mention_link_show_avatar_quick') }}
</button>
</div>
<div
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
<button
class="main-button"
role="menuitemcheckbox"
:aria-checked="autoUpdate"
@click="autoUpdate = !autoUpdate"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': autoUpdate }"
:aria-hidden="true"
/>{{ $t('settings.auto_update') }}
</button>
</div>
<div
v-if="!conversation"
class="menu-item dropdown-item"
role="menuitemcheckbox"
:aria-checked="collapseWithSubjects"
@click="collapseWithSubjects = !collapseWithSubjects"
class="menu-item dropdown-item -icon"
>
<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>

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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
class="menu-item dropdown-item"
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>

View file

@ -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>

View file

@ -116,6 +116,16 @@
{{ $t('settings.confirm_dialogs_mute') }}
</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') }}

View file

@ -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',

View file

@ -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 {

View file

@ -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

View file

@ -0,0 +1,123 @@
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',
'animationState',
'getClass',
'getComponent',
'doAction',
'close'
],
components: {
StatusBookmarkFolderMenu,
EmojiPicker,
Popover
},
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
}
]
}
},
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) {
if (button.name === 'emoji') {
this.$refs.picker.showPicker()
} else {
this.getComponent(button) === 'button' && this.doAction(button)
}
}
}
}

View file

@ -0,0 +1,92 @@
@import "../../mixins";
/* stylelint-disable declaration-no-important */
.quick-action {
display: grid;
grid-template-columns: max-content;
grid-gap: 0.25em;
&.-pin {
margin: calc(-2px - 0.25em);
padding: 0.25em;
border: 2px dashed var(--icon);
border-radius: var(--roundness);
grid-template-columns: 1fr auto;
}
.action-button-inner,
.extra-button {
margin: -0.5em;
padding: 0.5em;
}
.separator {
width: 0.5em;
&::before {
content: "";
display: block;
width: 1px;
height: 1.5em;
background-color: var(--icon);
}
}
.action-button-inner {
display: grid;
grid-gap: 1em;
grid-template-columns: max-content max-content;
grid-auto-flow: column;
grid-auto-columns: max-content;
align-items: center;
}
}
.action-button {
display: grid;
grid-auto-flow: column;
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;
}
}
}

View file

@ -0,0 +1,102 @@
<template>
<div
class="action-button"
:class="buttonClass"
>
<component
:is="getComponent(button)"
class="action-button-inner"
:class="buttonInnerClass"
role="menuitem"
:tabindex="0"
:disabled="buttonClass.disabled"
:href="getComponent(button) == 'a' ? button.link?.(funcArg) || getRemoteInteractionLink : undefined"
@click.prevent="doActionWrap(button)"
@click="button.name === 'emoji' ? () => {} : close()"
>
<FALayers>
<FAIcon
class="fa-scale-110"
:icon="button.icon(funcArg)"
:spin="!extra && button.animated?.() && animationState[button.name]"
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"
>
</span>
<Popover
trigger="hover"
:placement="extra ? 'right' : 'top'"
:trigger-attrs="{ class: 'extra-button' }"
v-if="button.name === 'bookmark'"
>
<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
ref="picker"
v-if="button.name === 'emoji'"
: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"/>

View file

@ -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()
}
}
}
}

View file

@ -0,0 +1,94 @@
<template>
<div>
<Popover
trigger="hover"
:placement="$attrs.extra ? 'right' : 'top'"
v-if="button.dropdown?.()"
>
<template #trigger>
{{ props }}
<ActionButton
:button="button"
:status="status"
v-bind.prop="$attrs"
/>
</template>
<template #content>
<div
v-if="button.name === 'mute'"
class="dropdown-menu"
:id="`popup-menu-${randomSeed}`"
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
type="conversation"
:status="this.status"
ref="confirmConversation"
/>
<mute-confirm
type="domain"
:user="this.user"
ref="confirmDomain"
/>
<mute-confirm
type="user"
:user="this.user"
ref="confirmUser"
/>
</teleport>
</div>
</template>
<script src="./action_button_container.js"/>

View file

@ -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
])
)
})

View file

@ -0,0 +1,143 @@
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 {
Popover,
animationState: {
retweet: false,
favorite: false
},
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) {
this.animationState[button.name] = true
button.action(this.funcArg)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
.finally(() => setTimeout(() => { this.animationState[button.name] = 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)
}
},
getRemoteInteractionLink () {
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
}
}
}
export default StatusActionButtons

View file

@ -0,0 +1,21 @@
@import "../../mixins";
.StatusActionButtons {
.quick-action-buttons {
display: grid;
grid-template-columns: repeat(var(--_actionsColumnCount, 6), 1fr);
grid-auto-flow: row dense;
grid-auto-rows: 1fr;
grid-gap: 1.25em 1em;
margin-top: var(--status-margin);
}
}
// popover
.extra-action-buttons {
.extra-action {
margin: 0;
padding-top: 0;
padding-bottom: 0;
padding-right: 0;
}
}

View file

@ -0,0 +1,132 @@
<template>
<div class="StatusActionButtons">
<span class="quick-action-buttons">
<span
class="quick-action"
:class="{ '-pin': showPin, '-toggle': button.dropdown?.() }"
v-for="button in quickButtons"
:key="button.name"
>
<ActionButtonContainer
:class="{ '-pin': showPin }"
:button="button"
:status="status"
:extra="false"
:funcArg="funcArg"
:get-class="getClass"
:get-component="getComponent"
:animation-state="animationState"
:close="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 }"
:bound-to="{ x: 'container' }"
remove-padding
@show="onShow"
@close="onClose"
>
<template #trigger>
<FAIcon
class="fa-scale-110 "
icon="ellipsis-h"
/>
</template>
<template #content="{close}">
<div
:id="`popup-menu-${randomSeed}`"
class="dropdown-menu extra-action-buttons"
role="menu"
>
<div class="menu-item dropdown-item extra-action -icon">
<button
class="main-button"
role="menuitem"
:tabindex="0"
@click.stop="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"
:funcArg="funcArg"
:get-class="getClass"
:get-component="getComponent"
:animation-state="animationState"
: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>

View file

@ -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>

View file

@ -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)

View file

@ -292,6 +292,10 @@
}
}
#sidebar {
--_actionsColumnCount: 4;
}
.sidebar .edit-profile-button {
display: none;
}
@ -321,8 +325,3 @@
text-decoration: none;
}
}
.mute-expiry {
display: flex;
flex-direction: row;
}

View file

@ -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
type="user"
:user="this.user"
ref="confirmation"
/>
</teleport>
</div>
</template>

View file

@ -34,6 +34,11 @@ const UserListMenu = {
...list,
inList: this.inListsSet.has(list.id)
}))
},
triggerAttrs () {
return {
class: 'menu-item dropdown-item -has-submenu'
}
}
},
methods: {

View file

@ -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>

View file

@ -490,6 +490,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",
@ -1241,6 +1243,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",
@ -1248,6 +1255,7 @@
"external_source": "External source",
"muted_words": "Wordfiltered: {word} | Wordfiltered: {word} and {numWordsMore} more words",
"multi_reason_mute": "{main} | {main} + one more reason | {main} + {numReasonsMore} more reasons",
"muted_user": "User muted",
"thread_muted": "Thread muted",
"thread_muted_and_words": ", has words:",
"sensitive_muted": "Muting sensitive content",
@ -1412,8 +1420,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",

View file

@ -131,6 +131,9 @@ const persistedStateOptions = {
bookmarkFolders: bookmarkFoldersModule
},
plugins,
options: {
devtools: process.env.NODE_ENV !== 'production'
},
strict: false // Socket modifies itself, let's ignore this for now.
// strict: process.env.NODE_ENV !== 'production'
})

View file

@ -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

View file

@ -77,6 +77,8 @@ const defaultState = {
modalOnUnfollow: false,
modalOnBlock: true,
modalOnMute: false,
modalOnMuteConversation: false,
modalOnMuteDomain: true,
modalOnDelete: true,
modalOnLogout: true,
modalOnApproveFollow: false,

View file

@ -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']
}
},
@ -110,6 +122,21 @@ export const _getRecentData = (cache, live) => {
console.debug('Both sources are invalid, start from scratch')
result.needUpload = true
}
const merge = (a, b) => ({
needUpload: b.needUpload ?? a.needUpload,
prefsStorage: {
...a.prefsStorage,
...b.prefsStorage
},
flagStorage: {
...a.flagStorage,
...b.flagStorage
}
})
result.recent = result.recent && merge(defaultState, result.recent)
result.stale = result.stale && merge(defaultState, result.stale)
return result
}
@ -292,7 +319,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 +333,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 +362,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) {

2666
yarn.lock

File diff suppressed because it is too large Load diff