Compare commits

...

72 commits

Author SHA1 Message Date
Henry Jameson
478779121d Merge branch 'customizable-post-actions' into shigusegubu-themes3 2025-01-16 20:14:51 +02:00
Henry Jameson
68093b6276 abstracted mute confirmation dialog into its own component. mutes in status actions work now 2025-01-16 20:14:05 +02:00
Henry Jameson
41f54b687b Merge remote-tracking branch 'origin/develop' into customizable-post-actions 2025-01-16 18:12:29 +02:00
HJ
6eaebedebe Merge branch 'renovate/qrcode-1.x' into 'develop'
Update dependency qrcode to v1.5.4

See merge request pleroma/pleroma-fe!1982
2025-01-16 11:06:35 +00:00
HJ
3e401417df Merge branch 'renovate/babel-monorepo' into 'develop'
Update babel monorepo

See merge request pleroma/pleroma-fe!1984
2025-01-16 11:05:33 +00:00
HJ
9c571d6d17 Merge branch 'renovate/url-0.x' into 'develop'
Update dependency url to v0.11.4

See merge request pleroma/pleroma-fe!1983
2025-01-16 09:18:32 +00:00
Pleroma Renovate Bot
7c3d11d9df Update babel monorepo 2025-01-16 09:04:55 +00:00
Pleroma Renovate Bot
a7e484255d Update dependency url to v0.11.4 2025-01-16 09:04:41 +00:00
Henry Jameson
9deb8aaff6 remove old status actions stuff 2025-01-15 12:51:51 +02:00
Henry Jameson
cfa1a48bfb emoji react 2025-01-15 12:48:25 +02:00
Henry Jameson
edb704339f change to prevent so that popups close properly 2025-01-15 12:48:08 +02:00
Pleroma Renovate Bot
35087351e7 Update dependency qrcode to v1.5.4 2025-01-15 08:51:51 +00:00
HJ
2562e66ff4 Merge branch 'renovate/function-bind-1.x' into 'develop'
Update dependency function-bind to v1.1.2

See merge request pleroma/pleroma-fe!1899
2025-01-15 01:51:00 +00:00
HJ
6acf0e2f10 Merge branch 'renovate/postcss-scss-4.x-lockfile' into 'develop'
Update dependency postcss-scss to v4.0.9

See merge request pleroma/pleroma-fe!1979
2025-01-15 01:50:43 +00:00
HJ
b3fa273f09 Merge branch 'renovate/punycode.js-2.x' into 'develop'
Update dependency punycode.js to v2.3.1

See merge request pleroma/pleroma-fe!1980
2025-01-15 01:50:10 +00:00
Henry Jameson
6f9c7f1bbd fix emoji picker not opening when in extra-buttons 2025-01-15 02:33:03 +02:00
Henry Jameson
eafa378eb9 better indication and text for toggleable actions 2025-01-15 02:27:32 +02:00
Henry Jameson
692ee06477 small cleanup 2025-01-14 22:02:30 +02:00
Henry Jameson
2c9547f5ff better flow 2025-01-14 20:40:14 +02:00
Henry Jameson
6939405173 cleanup + brought back quick actions styles 2025-01-14 19:43:47 +02:00
Henry Jameson
7259817a84 pin button fix for extra-buttons 2025-01-14 18:11:42 +02:00
Pleroma Renovate Bot
25c9fa9eb3 Update dependency punycode.js to v2.3.1 2025-01-14 08:52:27 +00:00
Pleroma Renovate Bot
c36c133162 Update dependency postcss-scss to v4.0.9 2025-01-14 08:52:13 +00:00
Pleroma Renovate Bot
25200b7cca Update dependency function-bind to v1.1.2 2025-01-14 08:51:47 +00:00
Henry Jameson
bd99d3e9d5 fix bookmarks folders 2025-01-14 09:59:03 +02:00
HJ
582ec616b8 Merge branch 'fixes-batch2' into 'develop'
Fixes batch2

Closes #1351 and #1350

See merge request pleroma/pleroma-fe!1968
2025-01-14 07:52:11 +00:00
Henry Jameson
45d1a94153 don't clear status on autosave 2025-01-14 09:46:01 +02:00
HJ
4150ded11f Merge branch 'renovate/selenium-server-3.x' into 'develop'
Update dependency selenium-server to v3

See merge request pleroma/pleroma-fe!1740
2025-01-14 07:28:55 +00:00
HJ
f40dacaa22 Merge branch 'renovate/vue-babel-plugin-jsx-1.x' into 'develop'
Update dependency @vue/babel-plugin-jsx to v1.2.5

See merge request pleroma/pleroma-fe!1972
2025-01-14 07:28:38 +00:00
HJ
b1bd4da197 Merge branch 'renovate/karma-coverage-2.x' into 'develop'
Update dependency karma-coverage to v2.2.1

See merge request pleroma/pleroma-fe!1901
2025-01-14 07:27:02 +00:00
HJ
7887867f9a Merge branch 'renovate/express-4.x' into 'develop'
Update dependency express to v4.19.2

See merge request pleroma/pleroma-fe!1897
2025-01-14 07:26:29 +00:00
HJ
e72012ef35 Merge branch 'renovate/ruffle-rs-ruffle-0.x' into 'develop'
Update dependency @ruffle-rs/ruffle to v0.1.0-nightly.2025.1.13

See merge request pleroma/pleroma-fe!1975
2025-01-14 07:25:50 +00:00
HJ
774b018dc6 Merge branch 'vuex-devtools' into 'develop'
Enable store access in the vue devtools

See merge request pleroma/pleroma-fe!1967
2025-01-14 07:25:13 +00:00
HJ
f450979f46 Merge branch 'renovate/http-proxy-middleware-2.x' into 'develop'
Update dependency http-proxy-middleware to v2.0.7

See merge request pleroma/pleroma-fe!1976
2025-01-14 07:24:39 +00:00
HJ
3db1713616 Merge branch 'renovate/phoenix-1.x' into 'develop'
Update dependency phoenix to v1.7.18

See merge request pleroma/pleroma-fe!1977
2025-01-14 07:24:19 +00:00
HJ
fb5f873061 Merge branch 'missing-translation' into 'develop'
Add missing EN translation for muted users reason

See merge request pleroma/pleroma-fe!1978
2025-01-14 07:23:40 +00:00
Phantasm
0352dc9a11 Add missing EN translation for muted users reason 2025-01-14 07:23:40 +00:00
Henry Jameson
b831f34c06 abstraction, made popover optional, initial markup for better mute options 2025-01-14 01:42:36 +02:00
Henry Jameson
5a085d8e36 cleanup + fixes 2025-01-13 22:32:39 +02:00
Henry Jameson
4887d37110 make all dropdown menus consistent 2025-01-13 17:38:44 +02:00
Pleroma Renovate Bot
1bc0adb535 Update dependency phoenix to v1.7.18 2025-01-13 09:04:36 +00:00
Pleroma Renovate Bot
76a948c66f Update dependency @ruffle-rs/ruffle to v0.1.0-nightly.2025.1.13 2025-01-13 09:04:19 +00:00
Henry Jameson
7a3a4e81a1 combo menu styles 2025-01-12 22:32:30 +02:00
Henry Jameson
b3ce454203 more fixes for popover 2025-01-12 22:32:07 +02:00
Henry Jameson
af3c2bc6fc fix popover left/right placement 2025-01-12 22:19:33 +02:00
Henry Jameson
5222da7748 inner dropdowns work 2025-01-12 18:49:44 +02:00
Henry Jameson
17917932a0 disabled state + activation animation 2025-01-12 16:34:16 +02:00
Pleroma Renovate Bot
f38904ac8c Update dependency http-proxy-middleware to v2.0.7 2025-01-12 08:52:35 +00:00
Pleroma Renovate Bot
c516614bd4 Update dependency @vue/babel-plugin-jsx to v1.2.5 2025-01-12 08:52:18 +00:00
Henry Jameson
a89a21c3ef color+indicator for toggleable stuff in extra-buttons 2025-01-12 05:18:23 +02:00
Henry Jameson
1697b97e9d changelog 2025-01-12 05:13:54 +02:00
Henry Jameson
4e85003220 confirmation support 2025-01-12 05:13:09 +02:00
Henry Jameson
e78f82d674 proper toggle for pinning 2025-01-12 04:42:51 +02:00
Henry Jameson
96fd7f91c4 more work + dropdown items overhaul 2025-01-12 01:46:10 +02:00
Henry Jameson
eb7406c663 extraButtons implementation 2025-01-11 20:02:53 +02:00
Henry Jameson
08f8b975b6 use computed instead of methods when possible 2025-01-11 18:01:53 +02:00
HJ
ad0667ed3b Merge branch 'renovate/ruffle-rs-ruffle-0.x' into 'develop'
Update dependency @ruffle-rs/ruffle to v0.1.0-nightly.2025.1.11

See merge request pleroma/pleroma-fe!1971
2025-01-11 10:47:31 +00:00
HJ
8da89574fa Merge branch 'renovate/autoprefixer-10.x' into 'develop'
Update dependency autoprefixer to v10.4.20

See merge request pleroma/pleroma-fe!1973
2025-01-11 10:47:15 +00:00
HJ
7129c5a0c6 Merge branch 'renovate/cross-spawn-7.x' into 'develop'
Update dependency cross-spawn to v7.0.6

See merge request pleroma/pleroma-fe!1974
2025-01-11 10:46:51 +00:00
Pleroma Renovate Bot
e21fbeaa62 Update dependency @ruffle-rs/ruffle to v0.1.0-nightly.2025.1.11 2025-01-11 08:51:47 +00:00
Pleroma Renovate Bot
e4085fb457 Update dependency cross-spawn to v7.0.6 2025-01-10 09:04:44 +00:00
Pleroma Renovate Bot
1dcb641314 Update dependency autoprefixer to v10.4.20 2025-01-10 09:04:38 +00:00
Henry Jameson
fe84a52dcc initial work on quick actions 2025-01-09 17:43:48 +02:00
Pleroma Renovate Bot
6908ddeec1 Update dependency express to v4.21.2 2025-01-09 09:04:57 +00:00
Pleroma Renovate Bot
804bacb7ba Update dependency karma-coverage to v2.2.1 2025-01-09 09:04:37 +00:00
Henry Jameson
35409ad9eb initial buttons definitions 2025-01-09 00:01:32 +02:00
HJ
bb954482ee Merge branch 'tusooa/no-check-npm' into 'develop'
Do not check npm version

See merge request pleroma/pleroma-fe!1969
2025-01-08 11:28:33 +00:00
HJ
5bfe3e61a9 Merge branch 'denpmify-gitlab-ci' into 'develop'
Change npm run to yarn in the GitLab CI

See merge request pleroma/pleroma-fe!1970
2025-01-08 11:27:59 +00:00
Sean King
f6ec13b64d Change npm run to yarn in the GitLab CI
Signed-off-by: Sean King <seanking2919@protonmail.com>
2025-01-07 21:05:56 -07:00
tusooa
2ad5c3d3fe Do not check npm version
This project does not make use of npm at all. In addition, corepack's
npm will refuse to run in a project that defines packageManager in
package.json to be yarn. If we are using standalone yarn legacy,
it will just run fine. If using corepack, it will automatically
download (if needed) and use yarn v1.
2025-01-07 20:51:14 -05:00
Pleroma User
c04570b1e4 Enable store access in the vue devtools 2025-01-06 00:24:02 +00:00
Pleroma Renovate Bot
c7303598df Update dependency selenium-server to v3 2023-05-03 09:08:20 +00:00
59 changed files with 3192 additions and 3350 deletions

View file

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

View file

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

View file

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

View file

@ -9,60 +9,80 @@
<template #content> <template #content>
<div class="dropdown-menu"> <div class="dropdown-menu">
<template v-if="relationship.following"> <template v-if="relationship.following">
<button <div
v-if="relationship.showing_reblogs" v-if="relationship.showing_reblogs"
class="dropdown-item menu-item" class="menu-item dropdown-item"
@click="hideRepeats"
> >
{{ $t('user_card.hide_repeats') }} <button
</button> class="main-button"
<button @click="hideRepeats"
>
{{ $t('user_card.hide_repeats') }}
</button>
</div>
<div
v-if="!relationship.showing_reblogs" v-if="!relationship.showing_reblogs"
class="dropdown-item menu-item" class="menu-item dropdown-item"
@click="showRepeats"
> >
{{ $t('user_card.show_repeats') }} <button
</button> class="main-button"
@click="showRepeats"
>
{{ $t('user_card.show_repeats') }}
</button>
</div>
<div <div
role="separator" role="separator"
class="dropdown-divider" class="dropdown-divider"
/> />
</template> </template>
<UserListMenu :user="user" /> <UserListMenu :user="user" />
<button <div
v-if="relationship.followed_by" v-if="relationship.followed_by"
class="dropdown-item menu-item" class="menu-item dropdown-item"
@click="removeUserFromFollowers"
> >
{{ $t('user_card.remove_follower') }} <button
</button> class="main-button"
<button @click="removeUserFromFollowers"
v-if="relationship.blocking" >
class="dropdown-item menu-item" {{ $t('user_card.remove_follower') }}
@click="unblockUser" </button>
> </div>
{{ $t('user_card.unblock') }} <div class="menu-item dropdown-item">
</button> <button
<button v-if="relationship.blocking"
v-else class="main-button"
class="dropdown-item menu-item" @click="unblockUser"
@click="blockUser" >
> {{ $t('user_card.unblock') }}
{{ $t('user_card.block') }} </button>
</button> <button
<button v-else
class="dropdown-item menu-item" class="main-button"
@click="reportUser" @click="blockUser"
> >
{{ $t('user_card.report') }} {{ $t('user_card.block') }}
</button> </button>
<button </div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="reportUser"
>
{{ $t('user_card.report') }}
</button>
</div>
<div
v-if="pleromaChatMessagesAvailable" v-if="pleromaChatMessagesAvailable"
class="dropdown-item menu-item" class="menu-item dropdown-item"
@click="openChat"
> >
{{ $t('user_card.message') }} <button
</button> class="main-button"
@click="openChat"
>
{{ $t('user_card.message') }}
</button>
</div>
</div> </div>
</template> </template>
<template #trigger> <template #trigger>

View file

@ -51,12 +51,14 @@
> >
<template #content> <template #content>
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <div class="menu-item dropdown-item -icon">
class="menu-item dropdown-item dropdown-item-icon" <button
@click="deleteMessage" class="main-button"
> @click="deleteMessage"
<FAIcon icon="times" /> {{ $t("chats.delete") }} >
</button> <FAIcon icon="times" /> {{ $t("chats.delete") }}
</button>
</div>
</div> </div>
</template> </template>
<template #trigger> <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> <template #content>
<div class="dropdown-menu"> <div class="dropdown-menu">
<span v-if="canGrantRole"> <template v-if="canGrantRole">
<button <div class="menu-item dropdown-item -icon-space">
class="menu-item dropdown-item menu-item" <button
@click="toggleRight(&quot;admin&quot;)" 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> {{ $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" </div>
@click="toggleRight(&quot;moderator&quot;)" <div class="menu-item dropdown-item -icon-space">
> <button
{{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }} class="main-button"
</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 <div
v-if="canChangeActivationState || canDeleteAccount" v-if="canChangeActivationState || canDeleteAccount"
role="separator" role="separator"
class="dropdown-divider" class="dropdown-divider"
/> />
</span> </template>
<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>
<div <div
v-if="canUseTagPolicy" v-if="canChangeActivationState"
role="separator" class="menu-item dropdown-item -icon-space"
class="dropdown-divider" >
/>
<span v-if="canUseTagPolicy">
<button <button
class="menu-item dropdown-item menu-item" class="main-button"
@click="toggleTag(tags.FORCE_NSFW)" @click="toggleActivationStatus()"
> >
<span {{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"
/>
{{ $t('user_card.admin_menu.force_nsfw') }}
</button> </button>
</div>
<div
v-if="canDeleteAccount"
class="menu-item dropdown-item -icon-space"
>
<button <button
class="menu-item dropdown-item menu-item" class="main-button"
@click="toggleTag(tags.STRIP_MEDIA)" @click="deleteUserDialog(true)"
> >
<span {{ $t('user_card.admin_menu.delete_account') }}
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"
/>
{{ $t('user_card.admin_menu.strip_media') }}
</button> </button>
<button </div>
class="menu-item dropdown-item menu-item" <template v-if="canUseTagPolicy">
@click="toggleTag(tags.FORCE_UNLISTED)" <div
> role="separator"
<span class="dropdown-divider"
class="input menu-checkbox" />
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }" <div class="menu-item dropdown-item -icon">
/> <button
{{ $t('user_card.admin_menu.force_unlisted') }} class="main-button"
</button> @click="toggleTag(tags.FORCE_NSFW)"
<button >
class="menu-item dropdown-item menu-item" <span
@click="toggleTag(tags.SANDBOX)" class="input menu-checkbox"
> :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"
<span />
class="input menu-checkbox" {{ $t('user_card.admin_menu.force_nsfw') }}
:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }" </button>
/> </div>
{{ $t('user_card.admin_menu.sandbox') }} <div class="menu-item dropdown-item -icon">
</button> <button
<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" v-if="user.is_local"
class="menu-item dropdown-item menu-item" class="menu-item dropdown-item -icon"
@click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)"
> >
<span <button
class="input menu-checkbox" class="main-button"
:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }" @click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)"
/> >
{{ $t('user_card.admin_menu.disable_remote_subscription') }} <span
</button> class="input menu-checkbox"
<button :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" v-if="user.is_local"
class="menu-item dropdown-item menu-item" class="menu-item dropdown-item -icon"
@click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)"
> >
<span <button
class="input menu-checkbox" class="main-button"
:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }" @click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)"
/> >
{{ $t('user_card.admin_menu.disable_any_subscription') }} <span
</button> class="input menu-checkbox"
<button :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" v-if="user.is_local"
class="menu-item dropdown-item menu-item" class="menu-item dropdown-item -icon"
@click="toggleTag(tags.QUARANTINE)"
> >
<span <button
class="input menu-checkbox" class="main-button"
:class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }" @click="toggleTag(tags.QUARANTINE)"
/> >
{{ $t('user_card.admin_menu.quarantine') }} <span
</button> class="input menu-checkbox"
</span> :class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }"
/>
{{ $t('user_card.admin_menu.quarantine') }}
</button>
</div>
</template>
</div> </div>
</template> </template>
<template #trigger> <template #trigger>

View file

@ -7,78 +7,94 @@
> >
<template #content> <template #content>
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <div class="menu-item dropdown-item -icon">
class="menu-item dropdown-item" <button
@click="toggleNotificationFilter('likes')" class="main-button"
> @click="toggleNotificationFilter('likes')"
<span >
class="input menu-checkbox" <span
:class="{ 'menu-checkbox-checked': filters.likes }" class="input menu-checkbox"
/>{{ $t('settings.notification_visibility_likes') }} :class="{ 'menu-checkbox-checked': filters.likes }"
</button> />{{ $t('settings.notification_visibility_likes') }}
<button </button>
class="menu-item dropdown-item" </div>
@click="toggleNotificationFilter('repeats')" <div class="menu-item dropdown-item -icon">
> <button
<span class="main-button"
class="input menu-checkbox" @click="toggleNotificationFilter('repeats')"
:class="{ 'menu-checkbox-checked': filters.repeats }" >
/>{{ $t('settings.notification_visibility_repeats') }} <span
</button> class="input menu-checkbox"
<button :class="{ 'menu-checkbox-checked': filters.repeats }"
class="menu-item dropdown-item" />{{ $t('settings.notification_visibility_repeats') }}
@click="toggleNotificationFilter('follows')" </button>
> </div>
<span <div class="menu-item dropdown-item -icon">
class="input menu-checkbox" <button
:class="{ 'menu-checkbox-checked': filters.follows }" class="main-button"
/>{{ $t('settings.notification_visibility_follows') }} @click="toggleNotificationFilter('follows')"
</button> >
<button <span
class="menu-item dropdown-item" class="input menu-checkbox"
@click="toggleNotificationFilter('mentions')" :class="{ 'menu-checkbox-checked': filters.follows }"
> />{{ $t('settings.notification_visibility_follows') }}
<span </button>
class="input menu-checkbox" </div>
:class="{ 'menu-checkbox-checked': filters.mentions }" <div class="menu-item dropdown-item -icon">
/>{{ $t('settings.notification_visibility_mentions') }} <button
</button> class="main-button"
<button @click="toggleNotificationFilter('mentions')"
class="menu-item dropdown-item" >
@click="toggleNotificationFilter('statuses')" <span
> class="input menu-checkbox"
<span :class="{ 'menu-checkbox-checked': filters.mentions }"
class="input menu-checkbox" />{{ $t('settings.notification_visibility_mentions') }}
:class="{ 'menu-checkbox-checked': filters.statuses }" </button>
/>{{ $t('settings.notification_visibility_statuses') }} </div>
</button> <div class="menu-item dropdown-item -icon">
<button <button
class="menu-item dropdown-item" class="main-button"
@click="toggleNotificationFilter('emojiReactions')" @click="toggleNotificationFilter('statuses')"
> >
<span <span
class="input menu-checkbox" class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': filters.emojiReactions }" :class="{ 'menu-checkbox-checked': filters.statuses }"
/>{{ $t('settings.notification_visibility_emoji_reactions') }} />{{ $t('settings.notification_visibility_statuses') }}
</button> </button>
<button </div>
class="menu-item dropdown-item" <div class="menu-item dropdown-item -icon">
@click="toggleNotificationFilter('moves')" <button
> class="main-button"
<span @click="toggleNotificationFilter('emojiReactions')"
class="input menu-checkbox" >
:class="{ 'menu-checkbox-checked': filters.moves }" <span
/>{{ $t('settings.notification_visibility_moves') }} class="input menu-checkbox"
</button> :class="{ 'menu-checkbox-checked': filters.emojiReactions }"
<button />{{ $t('settings.notification_visibility_emoji_reactions') }}
class="menu-item dropdown-item" </button>
@click="toggleNotificationFilter('polls')" </div>
> <div class="menu-item dropdown-item -icon">
<span <button
class="input menu-checkbox" class="main-button"
:class="{ 'menu-checkbox-checked': filters.polls }" @click="toggleNotificationFilter('moves')"
/>{{ $t('settings.notification_visibility_polls') }} >
</button> <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> </div>
</template> </template>
<template #trigger> <template #trigger>

View file

@ -197,8 +197,8 @@ const Popover = {
// Default to whatever user wished with placement prop // Default to whatever user wished with placement prop
let usingTop = this.placement !== 'bottom' let usingTop = this.placement !== 'bottom'
// Handle special cases, first force to displaying on top if there's not space on bottom, // 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 not space on top, and // 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. // force to bottom, again regardless of what placement value was.
const topBoundary = origin.y - anchorHeight * 0.5 + (this.removePadding ? topPadding : 0) const topBoundary = origin.y - anchorHeight * 0.5 + (this.removePadding ? topPadding : 0)
const bottomBoundary = origin.y + anchorHeight * 0.5 - (this.removePadding ? bottomPadding : 0) const bottomBoundary = origin.y + anchorHeight * 0.5 - (this.removePadding ? bottomPadding : 0)
@ -214,20 +214,20 @@ const Popover = {
translateX = origin.x + horizOffset + xOffset translateX = origin.x + horizOffset + xOffset
} else { } else {
// Default to whatever user wished with placement prop // 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, // 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 not space on top, and // regardless of what placement value was. Then check if there's no space on right, and
// force to bottom, again regardless of what placement value was. // force to left, 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)
const leftBoundary = origin.x + anchorWidth * 0.5 - (this.removePadding ? leftPadding : 0) const rightBoundary = origin.x + anchorWidth * 0.5 - (this.removePadding ? rightPadding : 0)
if (leftBoundary + content.offsetWidth > xBounds.max) usingRight = true if (rightBoundary + content.offsetWidth > xBounds.max) usingLeft = true
if (rightBoundary - content.offsetWidth < xBounds.min) usingRight = false if (leftBoundary - content.offsetWidth < xBounds.min) usingLeft = false
const xOffset = (this.offset && this.offset.x) || 0 const xOffset = (this.offset && this.offset.x) || 0
translateX = usingRight translateX = usingLeft
? rightBoundary - xOffset - content.offsetWidth ? leftBoundary - xOffset - content.offsetWidth
: leftBoundary + xOffset : rightBoundary + xOffset
const yOffset = (this.offset && this.offset.y) || 0 const yOffset = (this.offset && this.offset.y) || 0
translateY = origin.y + vertOffset + yOffset 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> <template>
<span <span
class="popover-wrapper"
@mouseenter="onMouseenter" @mouseenter="onMouseenter"
@mouseleave="onMouseleave" @mouseleave="onMouseleave"
> >
@ -41,101 +42,4 @@
<script src="./popover.js" /> <script src="./popover.js" />
<style lang="scss"> <style src="./popover.scss" lang="scss"></style>
.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>

View file

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

View file

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

View file

@ -14,106 +14,126 @@
v-if="loggedIn" v-if="loggedIn"
role="group" 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" v-if="!conversation"
class="menu-item dropdown-item" class="menu-item dropdown-item -icon"
:aria-checked="replyVisibilityAll"
role="menuitemradio"
@click="replyVisibilityAll = true"
> >
<span <button
class="input menu-checkbox -radio" class="main-button"
:class="{ 'menu-checkbox-checked': replyVisibilityAll }" :aria-checked="replyVisibilityFollowing"
:aria-hidden="true" role="menuitemradio"
/>{{ $t('settings.reply_visibility_all') }} @click="replyVisibilityFollowing = true"
</button> >
<button <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" v-if="!conversation"
class="menu-item dropdown-item" class="menu-item dropdown-item -icon"
:aria-checked="replyVisibilityFollowing"
role="menuitemradio"
@click="replyVisibilityFollowing = true"
> >
<span <button
class="input menu-checkbox -radio" class="main-button"
:class="{ 'menu-checkbox-checked': replyVisibilityFollowing }" :aria-checked="replyVisibilitySelf"
:aria-hidden="true" role="menuitemradio"
/>{{ $t('settings.reply_visibility_following_short') }} @click="replyVisibilitySelf = true"
</button> >
<button <span
v-if="!conversation" class="input menu-checkbox -radio"
class="menu-item dropdown-item" :class="{ 'menu-checkbox-checked': replyVisibilitySelf }"
:aria-checked="replyVisibilitySelf" :aria-hidden="true"
role="menuitemradio" />{{ $t('settings.reply_visibility_self_short') }}
@click="replyVisibilitySelf = true" </button>
> </div>
<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" v-if="!conversation"
role="separator" role="separator"
class="dropdown-divider" class="dropdown-divider"
/> />
</div> </div>
<button <div class="menu-item dropdown-item -icon">
class="menu-item dropdown-item" <button
role="menuitemcheckbox" class="main-button"
:aria-checked="muteBotStatuses" role="menuitemcheckbox"
@click="muteBotStatuses = !muteBotStatuses" :aria-checked="muteBotStatuses"
> @click="muteBotStatuses = !muteBotStatuses"
<span >
class="input menu-checkbox" <span
:class="{ 'menu-checkbox-checked': muteBotStatuses }" class="input menu-checkbox"
:aria-hidden="true" :class="{ 'menu-checkbox-checked': muteBotStatuses }"
/>{{ $t('settings.mute_bot_posts') }} :aria-hidden="true"
</button> />{{ $t('settings.mute_bot_posts') }}
<button </button>
class="menu-item dropdown-item" </div>
role="menuitemcheckbox" <div class="menu-item dropdown-item -icon">
:aria-checked="muteSensitiveStatuses" <button
@click="muteSensitiveStatuses = !muteSensitiveStatuses" class="main-button"
> role="menuitemcheckbox"
<span :aria-checked="muteSensitiveStatuses"
class="input menu-checkbox" @click="muteSensitiveStatuses = !muteSensitiveStatuses"
:class="{ 'menu-checkbox-checked': muteSensitiveStatuses }" >
:aria-hidden="true" <span
/>{{ $t('settings.mute_sensitive_posts') }} class="input menu-checkbox"
</button> :class="{ 'menu-checkbox-checked': muteSensitiveStatuses }"
<button :aria-hidden="true"
class="menu-item dropdown-item" />{{ $t('settings.mute_sensitive_posts') }}
role="menuitemcheckbox" </button>
:aria-checked="hideMedia" </div>
@click="hideMedia = !hideMedia" <div class="menu-item dropdown-item -icon">
> <button
<span class="main-button"
class="input menu-checkbox" role="menuitemcheckbox"
:class="{ 'menu-checkbox-checked': hideMedia }" :aria-checked="hideMedia"
:aria-hidden="true" @click="hideMedia = !hideMedia"
/>{{ $t('settings.hide_media_previews') }} >
</button> <span
<button class="input menu-checkbox"
class="menu-item dropdown-item" :class="{ 'menu-checkbox-checked': hideMedia }"
role="menuitemcheckbox" :aria-hidden="true"
:aria-checked="hideMutedPosts" />{{ $t('settings.hide_media_previews') }}
@click="hideMutedPosts = !hideMutedPosts" </button>
> </div>
<span <div class="menu-item dropdown-item -icon">
class="input menu-checkbox" <button
:class="{ 'menu-checkbox-checked': hideMutedPosts }" class="main-button"
:aria-hidden="true" role="menuitemcheckbox"
/>{{ $t('settings.hide_all_muted_posts') }} :aria-checked="hideMutedPosts"
</button> @click="hideMutedPosts = !hideMutedPosts"
<button >
class="menu-item dropdown-item dropdown-item-icon" <span
role="menuitem" class="input menu-checkbox"
@click="openTab('filtering')" :class="{ 'menu-checkbox-checked': hideMutedPosts }"
> :aria-hidden="true"
<FAIcon icon="font" />{{ $t('settings.word_filter_and_more') }} />{{ $t('settings.hide_all_muted_posts') }}
</button> </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> </div>
</template> </template>
<template #trigger> <template #trigger>

View file

@ -11,86 +11,107 @@
role="menu" role="menu"
> >
<div role="group"> <div role="group">
<button <div class="menu-item dropdown-item -icon-double">
class="menu-item dropdown-item" <button
:aria-checked="conversationDisplay === 'tree'" class="main-button"
role="menuitemradio" :aria-checked="conversationDisplay === 'tree'"
@click="conversationDisplay = 'tree'" role="menuitemradio"
> @click="conversationDisplay = 'tree'"
<span >
class="input menu-checkbox -radio" <span
:aria-hidden="true" class="input menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }" :aria-hidden="true"
/><FAIcon :class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }"
icon="folder-tree" /><FAIcon
:aria-hidden="true" icon="folder-tree"
/> {{ $t('settings.conversation_display_tree_quick') }} :aria-hidden="true"
</button> fixed-width
<button /> {{ $t('settings.conversation_display_tree_quick') }}
class="menu-item dropdown-item" </button>
:aria-checked="conversationDisplay === 'linear'" </div>
role="menuitemradio" <div class="menu-item dropdown-item -icon-double">
@click="conversationDisplay = 'linear'" <button
> class="main-button"
<span :aria-checked="conversationDisplay === 'linear'"
class="input menu-checkbox -radio" role="menuitemradio"
:class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }" @click="conversationDisplay = 'linear'"
:aria-hidden="true" >
/><FAIcon <span
icon="list" class="input menu-checkbox -radio"
:aria-hidden="true" :class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }"
/> {{ $t('settings.conversation_display_linear_quick') }} :aria-hidden="true"
</button> /><FAIcon
icon="list"
:aria-hidden="true"
fixed-width
/> {{ $t('settings.conversation_display_linear_quick') }}
</button>
</div>
</div> </div>
<div <div
role="separator" role="separator"
class="dropdown-divider" class="dropdown-divider"
/> />
<button <div class="menu-item dropdown-item -icon">
class="menu-item dropdown-item" <button
role="menuitemcheckbox" class="main-button"
:aria-checked="showUserAvatars" role="menuitemcheckbox"
@click="showUserAvatars = !showUserAvatars" :aria-checked="showUserAvatars"
> @click="showUserAvatars = !showUserAvatars"
<span >
class="input menu-checkbox" <span
:class="{ 'menu-checkbox-checked': showUserAvatars }" class="main-button"
:aria-hidden="true" :class="{ 'menu-checkbox-checked': showUserAvatars }"
/>{{ $t('settings.mention_link_show_avatar_quick') }} :aria-hidden="true"
</button> />{{ $t('settings.mention_link_show_avatar_quick') }}
<button </button>
</div>
<div
v-if="!conversation" v-if="!conversation"
class="menu-item dropdown-item" class="menu-item dropdown-item -icon"
role="menuitemcheckbox"
:aria-checked="autoUpdate"
@click="autoUpdate = !autoUpdate"
> >
<span <button
class="input menu-checkbox" class="main-button"
:class="{ 'menu-checkbox-checked': autoUpdate }" role="menuitemcheckbox"
:aria-hidden="true" :aria-checked="autoUpdate"
/>{{ $t('settings.auto_update') }} @click="autoUpdate = !autoUpdate"
</button> >
<button <span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': autoUpdate }"
:aria-hidden="true"
/>{{ $t('settings.auto_update') }}
</button>
</div>
<div
v-if="!conversation" v-if="!conversation"
class="menu-item dropdown-item" class="menu-item dropdown-item -icon"
role="menuitemcheckbox"
:aria-checked="collapseWithSubjects"
@click="collapseWithSubjects = !collapseWithSubjects"
> >
<span <button
class="input menu-checkbox" class="main-button"
:class="{ 'menu-checkbox-checked': collapseWithSubjects }" role="menuitemcheckbox"
:aria-hidden="true" :aria-checked="collapseWithSubjects"
/>{{ $t('settings.collapse_subject') }} @click="collapseWithSubjects = !collapseWithSubjects"
</button> >
<button <span
class="menu-item dropdown-item dropdown-item-icon" class="input menu-checkbox"
role="menuitem" :class="{ 'menu-checkbox-checked': collapseWithSubjects }"
@click="openTab('general')" :aria-hidden="true"
> />{{ $t('settings.collapse_subject') }}
<FAIcon icon="wrench" />{{ $t('settings.more_settings') }} </button>
</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> </div>
</template> </template>
<template #trigger> <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}"> <template #content="{close}">
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <div
v-for="ref in frontend.refs" v-for="ref in frontend.refs"
:key="ref" :key="ref"
class="menu-item dropdown-item" class="menu-item dropdown-item"
@click.prevent="update(frontend, ref)"
@click="close"
> >
<i18n-t <button
keypath="admin_dash.frontend.install_version" class="main-button"
scope="global" @click.prevent="update(frontend, ref)"
@click="close"
> >
<template #version> <span>
<code>{{ ref }}</code> <i18n-t
</template> keypath="admin_dash.frontend.install_version"
</i18n-t> scope="global"
</button> >
<template #version>
<code>{{ ref }}</code>
</template>
</i18n-t>
</span>
</button>
</div>
</div> </div>
</template> </template>
<template #trigger> <template #trigger>
@ -175,22 +181,28 @@
> >
<template #content="{close}"> <template #content="{close}">
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <div
class="menu-item dropdown-item"
v-for="ref in frontend.installedRefs || frontend.refs" v-for="ref in frontend.installedRefs || frontend.refs"
:key="ref" :key="ref"
class="menu-item dropdown-item"
@click.prevent="setDefault(frontend, ref)"
@click="close"
> >
<i18n-t <button
keypath="admin_dash.frontend.set_default_version" class="main-button"
scope="global" @click.prevent="setDefault(frontend, ref)"
@click="close"
> >
<template #version> <span>
<code>{{ ref }}</code> <i18n-t
</template> keypath="admin_dash.frontend.set_default_version"
</i18n-t> scope="global"
</button> >
<template #version>
<code>{{ ref }}</code>
</template>
</i18n-t>
</span>
</button>
</div>
</div> </div>
</template> </template>
<template #trigger> <template #trigger>

View file

@ -69,36 +69,42 @@
</template> </template>
<template #content="{close}"> <template #content="{close}">
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <div class="menu-item dropdown-item -icon">
class="menu-item dropdown-item dropdown-item-icon" <button
@click.prevent="backup" class="main-button"
@click="close" @click.prevent="backup"
> @click="close"
<FAIcon >
icon="file-download" <FAIcon
fixed-width icon="file-download"
/><span>{{ $t("settings.file_export_import.backup_settings") }}</span> fixed-width
</button> /><span>{{ $t("settings.file_export_import.backup_settings") }}</span>
<button </button>
class="menu-item dropdown-item dropdown-item-icon" </div>
@click.prevent="backupWithTheme" <div class="menu-item dropdown-item -icon">
@click="close" <button
> class="main-button"
<FAIcon @click.prevent="backupWithTheme"
icon="file-download" @click="close"
fixed-width >
/><span>{{ $t("settings.file_export_import.backup_settings_theme") }}</span> <FAIcon
</button> icon="file-download"
<button fixed-width
class="menu-item dropdown-item dropdown-item-icon" /><span>{{ $t("settings.file_export_import.backup_settings_theme") }}</span>
@click.prevent="restore" </button>
@click="close" </div>
> <div class="menu-item dropdown-item -icon">
<FAIcon <button
icon="file-upload" class="main-button"
fixed-width @click.prevent="restore"
/><span>{{ $t("settings.file_export_import.restore_settings") }}</span> @click="close"
</button> >
<FAIcon
icon="file-upload"
fixed-width
/><span>{{ $t("settings.file_export_import.restore_settings") }}</span>
</button>
</div>
</div> </div>
</template> </template>
</Popover> </Popover>

View file

@ -116,6 +116,16 @@
{{ $t('settings.confirm_dialogs_mute') }} {{ $t('settings.confirm_dialogs_mute') }}
</BooleanSetting> </BooleanSetting>
</li> </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> <li>
<BooleanSetting path="modalOnDelete"> <BooleanSetting path="modalOnDelete">
{{ $t('settings.confirm_dialogs_delete') }} {{ $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 PostStatusForm from '../post_status_form/post_status_form.vue'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import AvatarList from '../avatar_list/avatar_list.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 UserLink from '../user_link/user_link.vue'
import MentionsLine from 'src/components/mentions_line/mentions_line.vue' import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
import MentionLink from 'src/components/mention_link/mention_link.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 generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { muteWordHits } from '../../services/status_parser/status_parser.js' import { muteWordHits } from '../../services/status_parser/status_parser.js'
@ -102,11 +98,6 @@ const controlledOrUncontrolledSet = (obj, name, val) => {
const Status = { const Status = {
name: 'Status', name: 'Status',
components: { components: {
ReplyButton,
FavoriteButton,
ReactButton,
RetweetButton,
ExtraButtons,
PostStatusForm, PostStatusForm,
UserAvatar, UserAvatar,
AvatarList, AvatarList,
@ -119,7 +110,8 @@ const Status = {
MentionLink, MentionLink,
MentionsLine, MentionsLine,
UserPopover, UserPopover,
UserLink UserLink,
StatusActionButtons
}, },
props: [ props: [
'statusoid', 'statusoid',

View file

@ -264,13 +264,11 @@
.status-actions { .status-actions {
position: relative; position: relative;
width: 100%; width: 100%;
display: flex; display: grid;
grid-template-columns: 1fr;
grid-auto-columns: 1fr;
grid-auto-flow: column;
margin-top: var(--status-margin); margin-top: var(--status-margin);
> * {
max-width: 4em;
flex: 1;
}
} }
.muted { .muted {

View file

@ -535,37 +535,12 @@
:status="status" :status="status"
/> />
<div <StatusActionButtons
v-if="!noHeading && !isPreview" v-if="!noHeading && !isPreview"
class="status-actions" :status="status"
> :replying="replying"
<reply-button @toggleReplying="toggleReplying"
: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>
</div> </div>
</div> </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> <template>
<div class="StatusBookmarkFolderMenu"> <div class="dropdown-menu">
<Popover <div
trigger="hover" v-for="folder in folders"
placement="left" :key="folder.id"
remove-padding class="menu-item dropdown-item -icon"
> >
<template #content> <button
<div class="dropdown-menu"> class="main-button"
<button @click="toggleFolder(folder.id)"
v-for="folder in folders" >
:key="folder.id" <span
class="menu-item dropdown-item" class="input menu-checkbox -radio"
@click="toggleFolder(folder.id)" :class="{ 'menu-checkbox-checked': status.bookmark_folder_id == folder.id }"
> />
<span {{ folder.name }}
class="input menu-checkbox -radio" </button>
:class="{ 'menu-checkbox-checked': status.bookmark_folder_id == folder.id }" </div>
/>
{{ 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>
</div> </div>
</template> </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 UserAvatar from '../user_avatar/user_avatar.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue' import RemoteFollow from '../remote_follow/remote_follow.vue'
import ProgressButton from '../progress_button/progress_button.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 Select from '../select/select.vue'
import UserLink from '../user_link/user_link.vue' import UserLink from '../user_link/user_link.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' 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 generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@ -48,7 +47,6 @@ export default {
data () { data () {
return { return {
followRequestInProgress: false, followRequestInProgress: false,
showingConfirmMute: false,
muteExpiryAmount: 0, muteExpiryAmount: 0,
muteExpiryUnit: 'minutes' muteExpiryUnit: 'minutes'
} }
@ -141,12 +139,6 @@ export default {
supportsNote () { supportsNote () {
return 'note' in this.relationship return 'note' in this.relationship
}, },
shouldConfirmMute () {
return this.mergedConfig.modalOnMute
},
muteExpiryUnits () {
return ['minutes', 'hours', 'days']
},
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
components: { components: {
@ -160,28 +152,11 @@ export default {
RichContent, RichContent,
UserLink, UserLink,
UserNote, UserNote,
ConfirmModal MuteConfirm
}, },
methods: { methods: {
showConfirmMute () {
this.showingConfirmMute = true
},
hideConfirmMute () {
this.showingConfirmMute = false
},
muteUser () { muteUser () {
if (!this.shouldConfirmMute) { this.$refs.confirmation.optionallyPrompt()
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()
}, },
unmuteUser () { unmuteUser () {
this.$store.dispatch('unmuteUser', this.user.id) this.$store.dispatch('unmuteUser', this.user.id)

View file

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

View file

@ -311,51 +311,11 @@
/> />
</div> </div>
<teleport to="#modal"> <teleport to="#modal">
<confirm-modal <mute-confirm
v-if="showingConfirmMute" type="user"
:title="$t('user_card.mute_confirm_title')" :user="this.user"
:confirm-text="$t('user_card.mute_confirm_accept_button')" ref="confirmation"
: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>
</teleport> </teleport>
</div> </div>
</template> </template>

View file

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

View file

@ -2,34 +2,39 @@
<div class="UserListMenu"> <div class="UserListMenu">
<Popover <Popover
trigger="hover" trigger="hover"
placement="left" placement="right"
:trigger-attrs="triggerAttrs"
remove-padding remove-padding
> >
<template #content> <template #content>
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <div
v-for="list in lists" v-for="list in lists"
:key="list.id" :key="list.id"
class="menu-item dropdown-item" class="menu-item dropdown-item -icon"
@click="toggleList(list.id)"
> >
<span <button
class="input menu-checkbox" class="main-button"
:class="{ 'menu-checkbox-checked': list.inList }" @click="toggleList(list.id)"
/> >
{{ list.title }} <span
</button> class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': list.inList }"
/>
{{ list.title }}
</button>
</div>
</div> </div>
</template> </template>
<template #trigger> <template #trigger>
<button class="menu-item dropdown-item -has-submenu"> <span class="main-button">
{{ $t('lists.manage_lists') }} {{ $t('lists.manage_lists') }}
<FAIcon <FAIcon
class="chevron-icon" class="chevron-icon"
size="lg" size="lg"
icon="chevron-right" icon="chevron-right"
/> />
</button> </span>
</template> </template>
</Popover> </Popover>
</div> </div>

View file

@ -490,6 +490,8 @@
"confirm_dialogs_unfollow": "unfollowing a user", "confirm_dialogs_unfollow": "unfollowing a user",
"confirm_dialogs_block": "blocking a user", "confirm_dialogs_block": "blocking a user",
"confirm_dialogs_mute": "muting 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_delete": "deleting a status",
"confirm_dialogs_logout": "logging out", "confirm_dialogs_logout": "logging out",
"confirm_dialogs_approve_follow": "approving a follower", "confirm_dialogs_approve_follow": "approving a follower",
@ -1241,6 +1243,11 @@
"mentions": "Mentions", "mentions": "Mentions",
"replies_list": "Replies:", "replies_list": "Replies:",
"replies_list_with_others": "Replies (+{numReplies} other): | Replies (+{numReplies} others):", "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", "mute_conversation": "Mute conversation",
"unmute_conversation": "Unmute conversation", "unmute_conversation": "Unmute conversation",
"status_unavailable": "Status unavailable", "status_unavailable": "Status unavailable",
@ -1248,6 +1255,7 @@
"external_source": "External source", "external_source": "External source",
"muted_words": "Wordfiltered: {word} | Wordfiltered: {word} and {numWordsMore} more words", "muted_words": "Wordfiltered: {word} | Wordfiltered: {word} and {numWordsMore} more words",
"multi_reason_mute": "{main} | {main} + one more reason | {main} + {numReasonsMore} more reasons", "multi_reason_mute": "{main} | {main} + one more reason | {main} + {numReasonsMore} more reasons",
"muted_user": "User muted",
"thread_muted": "Thread muted", "thread_muted": "Thread muted",
"thread_muted_and_words": ", has words:", "thread_muted_and_words": ", has words:",
"sensitive_muted": "Muting sensitive content", "sensitive_muted": "Muting sensitive content",
@ -1412,8 +1420,11 @@
"media_upload": "Upload media", "media_upload": "Upload media",
"mentions": "Mentions", "mentions": "Mentions",
"repeat": "Repeat", "repeat": "Repeat",
"unrepeat": "Unrepeat",
"reply": "Reply", "reply": "Reply",
"add_reaction": "Add reaction",
"favorite": "Favorite", "favorite": "Favorite",
"unfavorite": "Unfavorite",
"add_reaction": "Add Reaction", "add_reaction": "Add Reaction",
"user_settings": "User Settings", "user_settings": "User Settings",
"accept_follow_request": "Accept follow request", "accept_follow_request": "Accept follow request",

View file

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

View file

@ -137,6 +137,8 @@ export const defaultState = {
modalOnUnfollow: undefined, // instance default modalOnUnfollow: undefined, // instance default
modalOnBlock: undefined, // instance default modalOnBlock: undefined, // instance default
modalOnMute: undefined, // instance default modalOnMute: undefined, // instance default
modalOnMuteConversation: undefined, // instance default
modalOnMuteDomain: undefined, // instance default
modalOnDelete: undefined, // instance default modalOnDelete: undefined, // instance default
modalOnLogout: undefined, // instance default modalOnLogout: undefined, // instance default
modalOnApproveFollow: undefined, // instance default modalOnApproveFollow: undefined, // instance default

View file

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

View file

@ -1,5 +1,16 @@
import { toRaw } from 'vue' 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' import { CURRENT_UPDATE_COUNTER } from 'src/components/update_notification/update_notification.js'
export const VERSION = 1 export const VERSION = 1
@ -26,6 +37,7 @@ export const defaultState = {
collapseNav: false collapseNav: false
}, },
collections: { collections: {
pinnedStatusActions: ['reply', 'retweet', 'favorite', 'emoji'],
pinnedNavItems: ['home', 'dms', 'chats'] pinnedNavItems: ['home', 'dms', 'chats']
} }
}, },
@ -110,6 +122,21 @@ export const _getRecentData = (cache, live) => {
console.debug('Both sources are invalid, start from scratch') console.debug('Both sources are invalid, start from scratch')
result.needUpload = true 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 return result
} }
@ -292,7 +319,7 @@ export const mutations = {
cache = _doMigrations(cache) 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 userNew = userData.created_at > NEW_USER_DATE
const flagsTemplate = userNew ? newUserFlags : defaultState.flagStorage 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...') console.debug('Checking if data needs merging...')
// discarding timestamps and versions // discarding timestamps and versions
const { _timestamp: _0, _version: _1, ...recentData } = recent const { _timestamp: _0, _version: _1, ...recentData } = recent
@ -335,7 +362,7 @@ export const mutations = {
recent.flagStorage = { ...flagsTemplate, ...totalFlags } recent.flagStorage = { ...flagsTemplate, ...totalFlags }
recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs } recent.prefsStorage = { ...defaultState.prefsStorage, ...totalPrefs }
state.dirty = dirty || needsUpload state.dirty = dirty || needUpload
state.cache = recent state.cache = recent
// set local timestamp to smaller one if we don't have any changes // set local timestamp to smaller one if we don't have any changes
if (stale && recent && !state.dirty) { if (stale && recent && !state.dirty) {

2666
yarn.lock

File diff suppressed because it is too large Load diff