Merge remote-tracking branch 'origin/develop' into harden-parser
This commit is contained in:
commit
5e656cc0b4
|
@ -4,11 +4,36 @@
|
||||||
image: node:16
|
image: node:16
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
|
- check-changelog
|
||||||
- lint
|
- lint
|
||||||
- build
|
- build
|
||||||
- test
|
- test
|
||||||
- deploy
|
- deploy
|
||||||
|
|
||||||
|
# https://git.pleroma.social/help/ci/yaml/workflow.md#switch-between-branch-pipelines-and-merge-request-pipelines
|
||||||
|
workflow:
|
||||||
|
rules:
|
||||||
|
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||||
|
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
|
||||||
|
when: never
|
||||||
|
- if: $CI_COMMIT_BRANCH
|
||||||
|
|
||||||
|
check-changelog:
|
||||||
|
stage: check-changelog
|
||||||
|
image: alpine
|
||||||
|
rules:
|
||||||
|
- if: $CI_MERGE_REQUEST_SOURCE_PROJECT_PATH == 'pleroma/pleroma-fe' && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^renovate/
|
||||||
|
when: never
|
||||||
|
- if: $CI_MERGE_REQUEST_SOURCE_PROJECT_PATH == 'pleroma/pleroma-fe' && $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME == 'weblate'
|
||||||
|
when: never
|
||||||
|
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "develop"
|
||||||
|
before_script: ''
|
||||||
|
after_script: ''
|
||||||
|
cache: {}
|
||||||
|
script:
|
||||||
|
- apk add git
|
||||||
|
- sh ./tools/check-changelog
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
stage: lint
|
stage: lint
|
||||||
script:
|
script:
|
||||||
|
|
28
CHANGELOG.md
28
CHANGELOG.md
|
@ -3,6 +3,34 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
## 2.5.1
|
||||||
|
### Fixed
|
||||||
|
- Checkboxes in settings can now work with screenreaders
|
||||||
|
- Autocomplete in edit boxes can now work with screenreaders
|
||||||
|
- Status interact buttons now have focus indicator for anonymous users
|
||||||
|
- Top bar buttons now correctly have text labels
|
||||||
|
- It is now possible to register if the site admin requires birthday to register
|
||||||
|
- User cards from search results will correctly popup
|
||||||
|
- Fix notification attachment icon overflow
|
||||||
|
- Editing mute words is less laggy
|
||||||
|
- Repeater's name will no longer mess up with the directionality of the text sitting on the same line
|
||||||
|
- Unauthenticated access will give better error messages
|
||||||
|
- It is now easier to close the media viewer with a mouse when there is only one image
|
||||||
|
- Deleting profile fields can work properly
|
||||||
|
- Clicking the react button will correctly focus the search box
|
||||||
|
- Clicking buttons on the top-bar will no longer bring you to the top of the page
|
||||||
|
- Emoji picker is much faster to load
|
||||||
|
- `blockquote`s have a better display style
|
||||||
|
- Announcements posting and editing are now available to everyone with such a privilege, not just admins
|
||||||
|
- Adding or removing list members will actually work
|
||||||
|
- Emojis without a pack are now correctly displayed in emoji picker
|
||||||
|
- Changing notification settings will actually work
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- You can now set and see birthdays
|
||||||
|
- Optional confirmation dialogs when performing various actions
|
||||||
|
- You can now set fallback languages
|
||||||
|
|
||||||
## 2.5.0 - 23.12.2022
|
## 2.5.0 - 23.12.2022
|
||||||
### Fixed
|
### Fixed
|
||||||
- UI no longer lags when switching between mobile and desktop mode
|
- UI no longer lags when switching between mobile and desktop mode
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Implemented a very basic instance administration screen
|
54
package.json
54
package.json
|
@ -16,28 +16,28 @@
|
||||||
"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.20.7",
|
"@babel/runtime": "7.21.5",
|
||||||
"@chenfengyuan/vue-qrcode": "2.0.0",
|
"@chenfengyuan/vue-qrcode": "2.0.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.2.1",
|
"@fortawesome/fontawesome-svg-core": "6.4.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "6.2.1",
|
"@fortawesome/free-regular-svg-icons": "6.4.0",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.2.1",
|
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||||
"@fortawesome/vue-fontawesome": "3.0.2",
|
"@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.2022.7.12",
|
"@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12",
|
||||||
"@vuelidate/core": "2.0.0",
|
"@vuelidate/core": "2.0.2",
|
||||||
"@vuelidate/validators": "2.0.0",
|
"@vuelidate/validators": "2.0.0",
|
||||||
"body-scroll-lock": "3.1.5",
|
"body-scroll-lock": "3.1.5",
|
||||||
"chromatism": "3.0.0",
|
"chromatism": "3.0.0",
|
||||||
"click-outside-vue3": "4.0.1",
|
"click-outside-vue3": "4.0.1",
|
||||||
"cropperjs": "1.5.12",
|
"cropperjs": "1.5.13",
|
||||||
"escape-html": "1.0.3",
|
"escape-html": "1.0.3",
|
||||||
"js-cookie": "3.0.1",
|
"js-cookie": "3.0.1",
|
||||||
"localforage": "1.10.0",
|
"localforage": "1.10.0",
|
||||||
"parse-link-header": "2.0.0",
|
"parse-link-header": "2.0.0",
|
||||||
"phoenix": "1.6.2",
|
"phoenix": "1.6.2",
|
||||||
"punycode.js": "2.1.0",
|
"punycode.js": "2.3.0",
|
||||||
"qrcode": "1.5.0",
|
"qrcode": "1.5.1",
|
||||||
"querystring-es3": "0.2.1",
|
"querystring-es3": "0.2.1",
|
||||||
"url": "0.11.0",
|
"url": "0.11.0",
|
||||||
"utf8": "3.0.0",
|
"utf8": "3.0.0",
|
||||||
|
@ -49,19 +49,19 @@
|
||||||
"vuex": "4.1.0"
|
"vuex": "4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.20.7",
|
"@babel/core": "7.21.8",
|
||||||
"@babel/eslint-parser": "7.19.1",
|
"@babel/eslint-parser": "7.21.8",
|
||||||
"@babel/plugin-transform-runtime": "7.19.6",
|
"@babel/plugin-transform-runtime": "7.21.4",
|
||||||
"@babel/preset-env": "7.20.2",
|
"@babel/preset-env": "7.21.5",
|
||||||
"@babel/register": "7.18.9",
|
"@babel/register": "7.21.0",
|
||||||
"@intlify/vue-i18n-loader": "5.0.0",
|
"@intlify/vue-i18n-loader": "5.0.1",
|
||||||
"@ungap/event-target": "0.2.3",
|
"@ungap/event-target": "0.2.3",
|
||||||
"@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.1.1",
|
"@vue/babel-plugin-jsx": "1.1.1",
|
||||||
"@vue/compiler-sfc": "3.2.45",
|
"@vue/compiler-sfc": "3.2.45",
|
||||||
"@vue/test-utils": "2.2.7",
|
"@vue/test-utils": "2.2.8",
|
||||||
"autoprefixer": "10.4.13",
|
"autoprefixer": "10.4.14",
|
||||||
"babel-loader": "9.1.0",
|
"babel-loader": "9.1.2",
|
||||||
"babel-plugin-lodash": "3.3.4",
|
"babel-plugin-lodash": "3.3.4",
|
||||||
"chai": "4.3.7",
|
"chai": "4.3.7",
|
||||||
"chalk": "1.1.3",
|
"chalk": "1.1.3",
|
||||||
|
@ -72,7 +72,7 @@
|
||||||
"css-loader": "6.7.3",
|
"css-loader": "6.7.3",
|
||||||
"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",
|
||||||
"eslint": "8.32.0",
|
"eslint": "8.33.0",
|
||||||
"eslint-config-standard": "17.0.0",
|
"eslint-config-standard": "17.0.0",
|
||||||
"eslint-formatter-friendly": "7.0.0",
|
"eslint-formatter-friendly": "7.0.0",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.27.5",
|
||||||
|
@ -83,11 +83,11 @@
|
||||||
"eventsource-polyfill": "0.9.6",
|
"eventsource-polyfill": "0.9.6",
|
||||||
"express": "4.18.2",
|
"express": "4.18.2",
|
||||||
"function-bind": "1.1.1",
|
"function-bind": "1.1.1",
|
||||||
"html-webpack-plugin": "5.5.0",
|
"html-webpack-plugin": "5.5.1",
|
||||||
"http-proxy-middleware": "2.0.6",
|
"http-proxy-middleware": "2.0.6",
|
||||||
"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.1",
|
"karma": "6.4.2",
|
||||||
"karma-coverage": "2.2.0",
|
"karma-coverage": "2.2.0",
|
||||||
"karma-firefox-launcher": "2.1.2",
|
"karma-firefox-launcher": "2.1.2",
|
||||||
"karma-mocha": "2.0.1",
|
"karma-mocha": "2.0.1",
|
||||||
|
@ -97,22 +97,22 @@
|
||||||
"karma-spec-reporter": "0.0.36",
|
"karma-spec-reporter": "0.0.36",
|
||||||
"karma-webpack": "5.0.0",
|
"karma-webpack": "5.0.0",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"mini-css-extract-plugin": "2.7.2",
|
"mini-css-extract-plugin": "2.7.5",
|
||||||
"mocha": "10.2.0",
|
"mocha": "10.2.0",
|
||||||
"nightwatch": "2.6.10",
|
"nightwatch": "2.6.20",
|
||||||
"opn": "5.5.0",
|
"opn": "5.5.0",
|
||||||
"ora": "0.4.1",
|
"ora": "0.4.1",
|
||||||
"postcss": "8.4.20",
|
"postcss": "8.4.23",
|
||||||
"postcss-html": "^1.5.0",
|
"postcss-html": "^1.5.0",
|
||||||
"postcss-loader": "7.0.2",
|
"postcss-loader": "7.0.2",
|
||||||
"postcss-scss": "^4.0.6",
|
"postcss-scss": "^4.0.6",
|
||||||
"sass": "1.57.1",
|
"sass": "1.60.0",
|
||||||
"sass-loader": "13.2.0",
|
"sass-loader": "13.2.2",
|
||||||
"selenium-server": "2.53.1",
|
"selenium-server": "2.53.1",
|
||||||
"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",
|
||||||
"sinon": "15.0.1",
|
"sinon": "15.0.4",
|
||||||
"sinon-chai": "3.7.0",
|
"sinon-chai": "3.7.0",
|
||||||
"stylelint": "14.16.1",
|
"stylelint": "14.16.1",
|
||||||
"stylelint-config-html": "^1.1.0",
|
"stylelint-config-html": "^1.1.0",
|
||||||
|
|
37
src/App.scss
37
src/App.scss
|
@ -580,8 +580,6 @@ textarea,
|
||||||
}
|
}
|
||||||
|
|
||||||
&[type="checkbox"] {
|
&[type="checkbox"] {
|
||||||
display: none;
|
|
||||||
|
|
||||||
&:checked + label::before {
|
&:checked + label::before {
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--inputText, $fallback--text);
|
color: var(--inputText, $fallback--text);
|
||||||
|
@ -647,6 +645,20 @@ option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cards-list {
|
||||||
|
list-style: none;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: row dense;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
|
||||||
|
li {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--inputRadius);
|
||||||
|
padding: 0.5em;
|
||||||
|
margin: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.btn-block {
|
.btn-block {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -657,16 +669,19 @@ option {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
||||||
button {
|
button,
|
||||||
|
.button-dropdown {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child),
|
||||||
|
&:not(:last-child) .button-default {
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:first-child) {
|
&:not(:first-child),
|
||||||
|
&:not(:first-child) .button-default {
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
}
|
}
|
||||||
|
@ -887,3 +902,15 @@ option {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
/* stylelint-enable no-descending-specificity */
|
/* stylelint-enable no-descending-specificity */
|
||||||
|
|
||||||
|
.visible-for-screenreader-only {
|
||||||
|
display: block;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
visibility: visible;
|
||||||
|
clip: rect(0 0 0 0);
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
|
@ -253,6 +253,7 @@ const getNodeInfo = async ({ store }) => {
|
||||||
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
|
||||||
store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
|
store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
|
||||||
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
|
||||||
|
store.dispatch('setInstanceOption', { name: 'pleromaCustomEmojiReactionsAvailable', value: features.includes('pleroma_custom_emoji_reactions') })
|
||||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||||
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
|
||||||
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
|
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<label
|
<label
|
||||||
class="checkbox"
|
class="checkbox"
|
||||||
:class="{ disabled, indeterminate }"
|
:class="{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
class="visible-for-screenreader-only"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:checked="modelValue"
|
:checked="modelValue"
|
||||||
:indeterminate="indeterminate"
|
:indeterminate="indeterminate"
|
||||||
@change="$emit('update:modelValue', $event.target.checked)"
|
@change="$emit('update:modelValue', $event.target.checked)"
|
||||||
>
|
>
|
||||||
<i class="checkbox-indicator" />
|
<i
|
||||||
|
class="checkbox-indicator"
|
||||||
|
:aria-hidden="true"
|
||||||
|
@transitionend.capture="onTransitionEnd"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="!!$slots.default"
|
v-if="!!$slots.default"
|
||||||
class="label"
|
class="label"
|
||||||
|
@ -27,12 +32,30 @@ export default {
|
||||||
'indeterminate',
|
'indeterminate',
|
||||||
'disabled'
|
'disabled'
|
||||||
],
|
],
|
||||||
emits: ['update:modelValue']
|
emits: ['update:modelValue'],
|
||||||
|
data: (vm) => ({
|
||||||
|
indeterminateTransitionFix: vm.indeterminate
|
||||||
|
}),
|
||||||
|
watch: {
|
||||||
|
indeterminate (e) {
|
||||||
|
if (e) {
|
||||||
|
this.indeterminateTransitionFix = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onTransitionEnd (e) {
|
||||||
|
if (!this.indeterminate) {
|
||||||
|
this.indeterminateTransitionFix = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import "../../variables";
|
@import "../../variables";
|
||||||
|
@import "../../mixins";
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -81,8 +104,6 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"] {
|
input[type="checkbox"] {
|
||||||
display: none;
|
|
||||||
|
|
||||||
&:checked + .checkbox-indicator::before {
|
&:checked + .checkbox-indicator::before {
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--inputText, $fallback--text);
|
color: var(--inputText, $fallback--text);
|
||||||
|
@ -95,6 +116,12 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.indeterminate-fix {
|
||||||
|
input[type="checkbox"] + .checkbox-indicator::before {
|
||||||
|
content: "–";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& > span {
|
& > span {
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,7 +107,10 @@ export default {
|
||||||
this.searchBarHidden = hidden
|
this.searchBarHidden = hidden
|
||||||
},
|
},
|
||||||
openSettingsModal () {
|
openSettingsModal () {
|
||||||
this.$store.dispatch('openSettingsModal')
|
this.$store.dispatch('openSettingsModal', 'user')
|
||||||
|
},
|
||||||
|
openAdminModal () {
|
||||||
|
this.$store.dispatch('openSettingsModal', 'admin')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
class="logo"
|
class="logo"
|
||||||
:to="{ name: 'root' }"
|
:to="{ name: 'root' }"
|
||||||
:style="logoBgStyle"
|
:style="logoBgStyle"
|
||||||
|
:title="sitename"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mask"
|
class="mask"
|
||||||
|
@ -38,40 +39,39 @@
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="button-unstyled nav-icon"
|
class="button-unstyled nav-icon"
|
||||||
|
:title="$t('nav.preferences')"
|
||||||
@click.stop="openSettingsModal"
|
@click.stop="openSettingsModal"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
class="fa-scale-110 fa-old-padding"
|
class="fa-scale-110 fa-old-padding"
|
||||||
icon="cog"
|
icon="cog"
|
||||||
:title="$t('nav.preferences')"
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<a
|
<button
|
||||||
v-if="currentUser && currentUser.role === 'admin'"
|
v-if="currentUser && currentUser.role === 'admin'"
|
||||||
href="/pleroma/admin/#/login-pleroma"
|
class="button-unstyled nav-icon"
|
||||||
class="nav-icon"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@click.stop
|
:title="$t('nav.administration')"
|
||||||
|
@click.stop="openAdminModal"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
class="fa-scale-110 fa-old-padding"
|
class="fa-scale-110 fa-old-padding"
|
||||||
icon="tachometer-alt"
|
icon="tachometer-alt"
|
||||||
:title="$t('nav.administration')"
|
|
||||||
/>
|
/>
|
||||||
</a>
|
</button>
|
||||||
<span class="spacer" />
|
<span class="spacer" />
|
||||||
<button
|
<button
|
||||||
v-if="currentUser"
|
v-if="currentUser"
|
||||||
class="button-unstyled nav-icon"
|
class="button-unstyled nav-icon"
|
||||||
|
:title="$t('login.logout')"
|
||||||
@click.stop.prevent="logout"
|
@click.stop.prevent="logout"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
class="fa-scale-110 fa-old-padding"
|
class="fa-scale-110 fa-old-padding"
|
||||||
icon="sign-out-alt"
|
icon="sign-out-alt"
|
||||||
:title="$t('login.logout')"
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Completion from '../../services/completion/completion.js'
|
import Completion from '../../services/completion/completion.js'
|
||||||
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||||
import Popover from 'src/components/popover/popover.vue'
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
|
import ScreenReaderNotice from 'src/components/screen_reader_notice/screen_reader_notice.vue'
|
||||||
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
|
||||||
import { take } from 'lodash'
|
import { take } from 'lodash'
|
||||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||||
|
@ -109,9 +110,10 @@ const EmojiInput = {
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
randomSeed: `${Math.random()}`.replace('.', '-'),
|
||||||
input: undefined,
|
input: undefined,
|
||||||
caretEl: undefined,
|
caretEl: undefined,
|
||||||
highlighted: 0,
|
highlighted: -1,
|
||||||
caret: 0,
|
caret: 0,
|
||||||
focused: false,
|
focused: false,
|
||||||
blurTimeout: null,
|
blurTimeout: null,
|
||||||
|
@ -125,12 +127,16 @@ const EmojiInput = {
|
||||||
components: {
|
components: {
|
||||||
Popover,
|
Popover,
|
||||||
EmojiPicker,
|
EmojiPicker,
|
||||||
UnicodeDomainIndicator
|
UnicodeDomainIndicator,
|
||||||
|
ScreenReaderNotice
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
padEmoji () {
|
padEmoji () {
|
||||||
return this.$store.getters.mergedConfig.padEmoji
|
return this.$store.getters.mergedConfig.padEmoji
|
||||||
},
|
},
|
||||||
|
defaultCandidateIndex () {
|
||||||
|
return this.$store.getters.mergedConfig.autocompleteSelect ? 0 : -1
|
||||||
|
},
|
||||||
preText () {
|
preText () {
|
||||||
return this.modelValue.slice(0, this.caret)
|
return this.modelValue.slice(0, this.caret)
|
||||||
},
|
},
|
||||||
|
@ -203,6 +209,12 @@ const EmojiInput = {
|
||||||
top: this.input.scrollTop,
|
top: this.input.scrollTop,
|
||||||
left: this.input.scrollLeft
|
left: this.input.scrollLeft
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
suggestionListId () {
|
||||||
|
return `suggestions-${this.randomSeed}`
|
||||||
|
},
|
||||||
|
suggestionItemId () {
|
||||||
|
return (index) => `suggestion-item-${index}-${this.randomSeed}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
@ -278,6 +290,11 @@ const EmojiInput = {
|
||||||
...rest,
|
...rest,
|
||||||
img: imageUrl || ''
|
img: imageUrl || ''
|
||||||
}))
|
}))
|
||||||
|
this.highlighted = this.defaultCandidateIndex
|
||||||
|
this.$refs.screenReaderNotice.announce(
|
||||||
|
this.$tc('tool_tip.autocomplete_available',
|
||||||
|
this.suggestions.length,
|
||||||
|
{ number: this.suggestions.length }))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -374,26 +391,27 @@ const EmojiInput = {
|
||||||
},
|
},
|
||||||
cycleBackward (e) {
|
cycleBackward (e) {
|
||||||
const len = this.suggestions.length || 0
|
const len = this.suggestions.length || 0
|
||||||
if (len > 1) {
|
|
||||||
this.highlighted -= 1
|
this.highlighted -= 1
|
||||||
if (this.highlighted < 0) {
|
if (this.highlighted === -1) {
|
||||||
this.highlighted = this.suggestions.length - 1
|
this.input.focus()
|
||||||
}
|
} else if (this.highlighted < -1) {
|
||||||
|
this.highlighted = len - 1
|
||||||
|
}
|
||||||
|
if (len > 0) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
} else {
|
|
||||||
this.highlighted = 0
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cycleForward (e) {
|
cycleForward (e) {
|
||||||
const len = this.suggestions.length || 0
|
const len = this.suggestions.length || 0
|
||||||
if (len > 1) {
|
|
||||||
this.highlighted += 1
|
this.highlighted += 1
|
||||||
if (this.highlighted >= len) {
|
if (this.highlighted >= len) {
|
||||||
this.highlighted = 0
|
this.highlighted = -1
|
||||||
}
|
this.input.focus()
|
||||||
|
}
|
||||||
|
if (len > 0) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
} else {
|
|
||||||
this.highlighted = 0
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollIntoView () {
|
scrollIntoView () {
|
||||||
|
@ -540,6 +558,13 @@ const EmojiInput = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
resize () {
|
resize () {
|
||||||
|
},
|
||||||
|
autoCompleteItemLabel (suggestion) {
|
||||||
|
if (suggestion.user) {
|
||||||
|
return suggestion.displayText + ' ' + suggestion.detailText
|
||||||
|
} else {
|
||||||
|
return this.maybeLocalizedEmojiName(suggestion)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,19 @@
|
||||||
class="emoji-input"
|
class="emoji-input"
|
||||||
:class="{ 'with-picker': !hideEmojiButton }"
|
:class="{ 'with-picker': !hideEmojiButton }"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot
|
||||||
|
:id="'textbox-' + randomSeed"
|
||||||
|
:aria-owns="suggestionListId"
|
||||||
|
aria-autocomplete="both"
|
||||||
|
:aria-expanded="showSuggestions"
|
||||||
|
:aria-activedescendant="(!showSuggestions || highlighted === -1) ? '' : suggestionItemId(highlighted)"
|
||||||
|
/>
|
||||||
<!-- TODO: make the 'x' disappear if at the end maybe? -->
|
<!-- TODO: make the 'x' disappear if at the end maybe? -->
|
||||||
<div
|
<div
|
||||||
ref="hiddenOverlay"
|
ref="hiddenOverlay"
|
||||||
class="hidden-overlay"
|
class="hidden-overlay"
|
||||||
:style="overlayStyle"
|
:style="overlayStyle"
|
||||||
|
:aria-hidden="true"
|
||||||
>
|
>
|
||||||
<span>{{ preText }}</span>
|
<span>{{ preText }}</span>
|
||||||
<span
|
<span
|
||||||
|
@ -18,11 +25,16 @@
|
||||||
>x</span>
|
>x</span>
|
||||||
<span>{{ postText }}</span>
|
<span>{{ postText }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<screen-reader-notice
|
||||||
|
ref="screenReaderNotice"
|
||||||
|
aria-live="assertive"
|
||||||
|
/>
|
||||||
<template v-if="enableEmojiPicker">
|
<template v-if="enableEmojiPicker">
|
||||||
<button
|
<button
|
||||||
v-if="!hideEmojiButton"
|
v-if="!hideEmojiButton"
|
||||||
class="button-unstyled emoji-picker-icon"
|
class="button-unstyled emoji-picker-icon"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('emoji.add_emoji')"
|
||||||
@click.prevent="togglePicker"
|
@click.prevent="togglePicker"
|
||||||
>
|
>
|
||||||
<FAIcon :icon="['far', 'smile-beam']" />
|
<FAIcon :icon="['far', 'smile-beam']" />
|
||||||
|
@ -43,17 +55,24 @@
|
||||||
ref="suggestorPopover"
|
ref="suggestorPopover"
|
||||||
class="autocomplete-panel"
|
class="autocomplete-panel"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
|
:trigger-attrs="{ 'aria-hidden': true }"
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div
|
<div
|
||||||
|
:id="suggestionListId"
|
||||||
ref="panel-body"
|
ref="panel-body"
|
||||||
class="autocomplete-panel-body"
|
class="autocomplete-panel-body"
|
||||||
|
role="listbox"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(suggestion, index) in suggestions"
|
v-for="(suggestion, index) in suggestions"
|
||||||
|
:id="suggestionItemId(index)"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="autocomplete-item"
|
class="autocomplete-item"
|
||||||
|
role="option"
|
||||||
:class="{ highlighted: index === highlighted }"
|
:class="{ highlighted: index === highlighted }"
|
||||||
|
:aria-label="autoCompleteItemLabel(suggestion)"
|
||||||
|
:aria-selected="index === highlighted"
|
||||||
@click.stop.prevent="onClick($event, suggestion)"
|
@click.stop.prevent="onClick($event, suggestion)"
|
||||||
>
|
>
|
||||||
<span class="image">
|
<span class="image">
|
||||||
|
|
|
@ -94,8 +94,9 @@ export const suggestUsers = ({ dispatch, state }) => {
|
||||||
|
|
||||||
const newSuggestions = state.users.users.filter(
|
const newSuggestions = state.users.users.filter(
|
||||||
user =>
|
user =>
|
||||||
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
user.screen_name && user.name && (
|
||||||
user.name.toLowerCase().startsWith(noPrefix)
|
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||||
|
user.name.toLowerCase().startsWith(noPrefix))
|
||||||
).slice(0, 20).sort((a, b) => {
|
).slice(0, 20).sort((a, b) => {
|
||||||
let aScore = 0
|
let aScore = 0
|
||||||
let bScore = 0
|
let bScore = 0
|
||||||
|
|
|
@ -98,6 +98,11 @@ const EmojiPicker = {
|
||||||
required: false,
|
required: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
hideCustomEmoji: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
|
@ -280,6 +285,9 @@ const EmojiPicker = {
|
||||||
return 0
|
return 0
|
||||||
},
|
},
|
||||||
allCustomGroups () {
|
allCustomGroups () {
|
||||||
|
if (this.hideCustomEmoji) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
const emojis = this.$store.getters.groupedCustomEmojis
|
const emojis = this.$store.getters.groupedCustomEmojis
|
||||||
if (emojis.unpacked) {
|
if (emojis.unpacked) {
|
||||||
emojis.unpacked.text = this.$t('emoji.unpacked')
|
emojis.unpacked.text = this.$t('emoji.unpacked')
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
ref="popover"
|
ref="popover"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
popover-class="emoji-picker popover-default"
|
popover-class="emoji-picker popover-default"
|
||||||
|
:trigger-attrs="{ 'aria-hidden': true }"
|
||||||
@show="onPopoverShown"
|
@show="onPopoverShown"
|
||||||
@close="onPopoverClosed"
|
@close="onPopoverClosed"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,5 +1,17 @@
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
import UserListPopover from '../user_list_popover/user_list_popover.vue'
|
import UserListPopover from '../user_list_popover/user_list_popover.vue'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faPlus,
|
||||||
|
faMinus,
|
||||||
|
faCheck
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faPlus,
|
||||||
|
faMinus,
|
||||||
|
faCheck
|
||||||
|
)
|
||||||
|
|
||||||
const EMOJI_REACTION_COUNT_CUTOFF = 12
|
const EMOJI_REACTION_COUNT_CUTOFF = 12
|
||||||
|
|
||||||
|
@ -33,6 +45,9 @@ const EmojiReactions = {
|
||||||
},
|
},
|
||||||
loggedIn () {
|
loggedIn () {
|
||||||
return !!this.$store.state.users.currentUser
|
return !!this.$store.state.users.currentUser
|
||||||
|
},
|
||||||
|
remoteInteractionLink () {
|
||||||
|
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -62,6 +77,17 @@ const EmojiReactions = {
|
||||||
} else {
|
} else {
|
||||||
this.reactWith(emoji)
|
this.reactWith(emoji)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
counterTriggerAttrs (reaction) {
|
||||||
|
return {
|
||||||
|
class: [
|
||||||
|
'btn',
|
||||||
|
'button-default',
|
||||||
|
'emoji-reaction-count-button',
|
||||||
|
{ '-picked-reaction': this.reactedWith(reaction.name) }
|
||||||
|
],
|
||||||
|
'aria-label': this.$tc('status.reaction_count_label', reaction.count, { num: reaction.count })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,64 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="EmojiReactions">
|
<div class="EmojiReactions">
|
||||||
<UserListPopover
|
<span
|
||||||
v-for="(reaction) in emojiReactions"
|
v-for="(reaction) in emojiReactions"
|
||||||
:key="reaction.name"
|
:key="reaction.url || reaction.name"
|
||||||
:users="accountsForEmoji[reaction.name]"
|
class="emoji-reaction-container btn-group"
|
||||||
>
|
>
|
||||||
<button
|
<component
|
||||||
|
:is="loggedIn ? 'button' : 'a'"
|
||||||
|
v-bind="!loggedIn ? { href: remoteInteractionLink } : {}"
|
||||||
|
role="button"
|
||||||
class="emoji-reaction btn button-default"
|
class="emoji-reaction btn button-default"
|
||||||
:class="{ '-picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
|
:class="{ '-picked-reaction': reactedWith(reaction.name) }"
|
||||||
|
:title="reaction.url ? reaction.name : undefined"
|
||||||
|
:aria-pressed="reactedWith(reaction.name)"
|
||||||
@click="emojiOnClick(reaction.name, $event)"
|
@click="emojiOnClick(reaction.name, $event)"
|
||||||
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
|
||||||
>
|
>
|
||||||
<span class="reaction-emoji">{{ reaction.name }}</span>
|
<span
|
||||||
<span>{{ reaction.count }}</span>
|
class="reaction-emoji"
|
||||||
</button>
|
>
|
||||||
</UserListPopover>
|
<img
|
||||||
|
v-if="reaction.url"
|
||||||
|
:src="reaction.url"
|
||||||
|
class="reaction-emoji-content"
|
||||||
|
width="1em"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="reaction-emoji reaction-emoji-content"
|
||||||
|
>{{ reaction.name }}</span>
|
||||||
|
</span>
|
||||||
|
<FALayers>
|
||||||
|
<FAIcon
|
||||||
|
v-if="reactedWith(reaction.name)"
|
||||||
|
class="active-marker"
|
||||||
|
transform="shrink-6 up-9"
|
||||||
|
icon="check"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-if="!reactedWith(reaction.name)"
|
||||||
|
class="focus-marker"
|
||||||
|
transform="shrink-6 up-9"
|
||||||
|
icon="plus"
|
||||||
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-else
|
||||||
|
class="focus-marker"
|
||||||
|
transform="shrink-6 up-9"
|
||||||
|
icon="minus"
|
||||||
|
/>
|
||||||
|
</FALayers>
|
||||||
|
</component>
|
||||||
|
<UserListPopover
|
||||||
|
:users="accountsForEmoji[reaction.name]"
|
||||||
|
class="emoji-reaction-popover"
|
||||||
|
:trigger-attrs="counterTriggerAttrs(reaction)"
|
||||||
|
@show="fetchEmojiReactionsByIfMissing()"
|
||||||
|
>
|
||||||
|
<span class="emoji-reaction-counts">{{ reaction.count }}</span>
|
||||||
|
</UserListPopover>
|
||||||
|
</span>
|
||||||
<a
|
<a
|
||||||
v-if="tooManyReactions"
|
v-if="tooManyReactions"
|
||||||
class="emoji-reaction-expand faint"
|
class="emoji-reaction-expand faint"
|
||||||
|
@ -29,43 +73,118 @@
|
||||||
<script src="./emoji_reactions.js"></script>
|
<script src="./emoji_reactions.js"></script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import "../../variables";
|
@import "../../variables";
|
||||||
|
@import "../../mixins";
|
||||||
|
|
||||||
.EmojiReactions {
|
.EmojiReactions {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 0.25em;
|
margin-top: 0.25em;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.emoji-reaction {
|
--emoji-size: calc(1.25em * var(--emojiReactionsScale, 1));
|
||||||
padding: 0 0.5em;
|
|
||||||
margin-right: 0.5em;
|
.emoji-reaction-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
|
||||||
|
.emoji-reaction-popover {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.emoji-reaction-count-button {
|
||||||
|
background-color: var(--btn);
|
||||||
|
height: 100%;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 2em;
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: $fallback--text;
|
||||||
|
color: var(--btnText, $fallback--text);
|
||||||
|
|
||||||
|
&.-picked-reaction {
|
||||||
|
border: 1px solid var(--accent, $fallback--link);
|
||||||
|
margin-right: -1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-reaction {
|
||||||
|
padding-left: 0.5em;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
|
||||||
.reaction-emoji {
|
.reaction-emoji {
|
||||||
width: 1.25em;
|
width: var(--emoji-size);
|
||||||
|
height: var(--emoji-size);
|
||||||
margin-right: 0.25em;
|
margin-right: 0.25em;
|
||||||
|
line-height: var(--emoji-size);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reaction-emoji-content {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
line-height: inherit;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: calc(var(--emoji-size) * 0.8);
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.not-clickable {
|
.svg-inline--fa {
|
||||||
cursor: default;
|
color: $fallback--text;
|
||||||
|
color: var(--btnText, $fallback--text);
|
||||||
&:hover {
|
|
||||||
box-shadow: $fallback--buttonShadow;
|
|
||||||
box-shadow: var(--buttonShadow);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-picked-reaction {
|
&.-picked-reaction {
|
||||||
border: 1px solid var(--accent, $fallback--link);
|
border: 1px solid var(--accent, $fallback--link);
|
||||||
margin-left: -1px; // offset the border, can't use inset shadows either
|
margin-left: -1px; // offset the border, can't use inset shadows either
|
||||||
margin-right: calc(0.5em - 1px);
|
margin-right: -1px;
|
||||||
|
|
||||||
|
.svg-inline--fa {
|
||||||
|
color: $fallback--link;
|
||||||
|
color: var(--accent, $fallback--link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include unfocused-style {
|
||||||
|
.focus-marker {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-marker {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include focused-style {
|
||||||
|
.svg-inline--fa {
|
||||||
|
color: $fallback--link;
|
||||||
|
color: var(--accent, $fallback--link);
|
||||||
|
}
|
||||||
|
|
||||||
|
.focus-marker {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-marker {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,13 +38,20 @@
|
||||||
class="button-unstyled interactive"
|
class="button-unstyled interactive"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
role="button"
|
role="button"
|
||||||
|
:title="$t('tool_tip.favorite')"
|
||||||
:href="remoteInteractionLink"
|
:href="remoteInteractionLink"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FALayers class="fa-scale-110 fa-old-padding-layer">
|
||||||
class="fa-scale-110 fa-old-padding"
|
<FAIcon
|
||||||
:title="$t('tool_tip.favorite')"
|
class="fa-scale-110"
|
||||||
:icon="['far', 'star']"
|
:icon="['far', 'star']"
|
||||||
/>
|
/>
|
||||||
|
<FAIcon
|
||||||
|
class="focus-marker"
|
||||||
|
transform="shrink-6 up-9 right-12"
|
||||||
|
icon="plus"
|
||||||
|
/>
|
||||||
|
</FALayers>
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
v-if="!mergedConfig.hidePostStats && status.fave_num > 0"
|
v-if="!mergedConfig.hidePostStats && status.fave_num > 0"
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
:class="{ custom: isCustom }"
|
:class="{ custom: isCustom }"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
|
:id="name + '-label'"
|
||||||
:for="preset === 'custom' ? name : name + '-font-switcher'"
|
:for="preset === 'custom' ? name : name + '-font-switcher'"
|
||||||
class="label"
|
class="label"
|
||||||
>
|
>
|
||||||
|
@ -12,7 +13,8 @@
|
||||||
<input
|
<input
|
||||||
v-if="typeof fallback !== 'undefined'"
|
v-if="typeof fallback !== 'undefined'"
|
||||||
:id="name + '-o'"
|
:id="name + '-o'"
|
||||||
class="opt exlcude-disabled"
|
:aria-labelledby="name + '-label'"
|
||||||
|
class="opt exlcude-disabled visible-for-screenreader-only"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="present"
|
:checked="present"
|
||||||
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
|
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
|
||||||
|
@ -21,6 +23,7 @@
|
||||||
v-if="typeof fallback !== 'undefined'"
|
v-if="typeof fallback !== 'undefined'"
|
||||||
class="opt-l"
|
class="opt-l"
|
||||||
:for="name + '-o'"
|
:for="name + '-o'"
|
||||||
|
:aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
{{ ' ' }}
|
{{ ' ' }}
|
||||||
<Select
|
<Select
|
||||||
|
|
|
@ -36,7 +36,9 @@
|
||||||
<button
|
<button
|
||||||
class="button-default btn"
|
class="button-default btn"
|
||||||
@click="addLanguage"
|
@click="addLanguage"
|
||||||
>{{ $t('settings.add_language') }}</button>
|
>
|
||||||
|
{{ $t('settings.add_language') }}
|
||||||
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -102,7 +104,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import "../../variables";
|
||||||
|
|
||||||
.interface-language-switcher {
|
.interface-language-switcher {
|
||||||
.language-select {
|
.language-select {
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="list">
|
<div
|
||||||
|
class="list"
|
||||||
|
role="list"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="item in items"
|
v-for="item in items"
|
||||||
:key="getKey(item)"
|
:key="getKey(item)"
|
||||||
class="list-item"
|
class="list-item"
|
||||||
|
role="listitem"
|
||||||
>
|
>
|
||||||
<slot
|
<slot
|
||||||
name="item"
|
name="item"
|
||||||
|
|
|
@ -23,6 +23,11 @@ const mediaUpload = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onClick () {
|
||||||
|
if (this.uploadReady) {
|
||||||
|
this.$refs.input.click()
|
||||||
|
}
|
||||||
|
},
|
||||||
uploadFile (file) {
|
uploadFile (file) {
|
||||||
const self = this
|
const self = this
|
||||||
const store = this.$store
|
const store = this.$store
|
||||||
|
@ -69,10 +74,15 @@ const mediaUpload = {
|
||||||
this.multiUpload(target.files)
|
this.multiUpload(target.files)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: [
|
props: {
|
||||||
'dropFiles',
|
dropFiles: Object,
|
||||||
'disabled'
|
disabled: Boolean,
|
||||||
],
|
normalButton: Boolean,
|
||||||
|
acceptTypes: {
|
||||||
|
type: String,
|
||||||
|
default: '*/*'
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
dropFiles: function (fileInfos) {
|
dropFiles: function (fileInfos) {
|
||||||
if (!this.uploading) {
|
if (!this.uploading) {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<label
|
<button
|
||||||
class="media-upload"
|
class="media-upload"
|
||||||
:class="{ disabled: disabled }"
|
:class="[normalButton ? 'button-default btn' : 'button-unstyled', { disabled }]"
|
||||||
:title="$t('tool_tip.media_upload')"
|
:title="$t('tool_tip.media_upload')"
|
||||||
|
@click="onClick"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
v-if="uploading"
|
v-if="uploading"
|
||||||
|
@ -15,15 +16,21 @@
|
||||||
class="new-icon"
|
class="new-icon"
|
||||||
icon="upload"
|
icon="upload"
|
||||||
/>
|
/>
|
||||||
|
<template v-if="normalButton">
|
||||||
|
{{ ' ' }}
|
||||||
|
{{ uploading ? $t('general.loading') : $t('tool_tip.media_upload') }}
|
||||||
|
</template>
|
||||||
<input
|
<input
|
||||||
v-if="uploadReady"
|
v-if="uploadReady"
|
||||||
|
ref="input"
|
||||||
class="hidden-input-file"
|
class="hidden-input-file"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
type="file"
|
type="file"
|
||||||
multiple="true"
|
multiple="true"
|
||||||
|
:accept="acceptTypes"
|
||||||
@change="change"
|
@change="change"
|
||||||
>
|
>
|
||||||
</label>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./media_upload.js"></script>
|
<script src="./media_upload.js"></script>
|
||||||
|
@ -32,10 +39,12 @@
|
||||||
@import "../../variables";
|
@import "../../variables";
|
||||||
|
|
||||||
.media-upload {
|
.media-upload {
|
||||||
cursor: pointer; // We use <label> for interactivity... i wonder if it's fine
|
|
||||||
|
|
||||||
.hidden-input-file {
|
.hidden-input-file {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
label.media-upload {
|
||||||
|
cursor: pointer; // We use <label> for interactivity... i wonder if it's fine
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -80,3 +80,21 @@ export const ROOT_ITEMS = {
|
||||||
criteria: ['announcements']
|
criteria: ['announcements']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function routeTo (item, currentUser) {
|
||||||
|
if (!item.route && !item.routeObject) return null
|
||||||
|
|
||||||
|
let route
|
||||||
|
|
||||||
|
if (item.routeObject) {
|
||||||
|
route = item.routeObject
|
||||||
|
} else {
|
||||||
|
route = { name: (item.anon || currentUser) ? item.route : item.anonRoute }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (USERNAME_ROUTES.has(route.name)) {
|
||||||
|
route.params = { username: currentUser.screen_name, name: currentUser.screen_name }
|
||||||
|
}
|
||||||
|
|
||||||
|
return route
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js'
|
import { routeTo } from 'src/components/navigation/navigation.js'
|
||||||
import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue'
|
import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faThumbtack } from '@fortawesome/free-solid-svg-icons'
|
import { faThumbtack } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
@ -26,17 +26,7 @@ const NavigationEntry = {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
routeTo () {
|
routeTo () {
|
||||||
if (!this.item.route && !this.item.routeObject) return null
|
return routeTo(this.item, this.currentUser)
|
||||||
let route
|
|
||||||
if (this.item.routeObject) {
|
|
||||||
route = this.item.routeObject
|
|
||||||
} else {
|
|
||||||
route = { name: (this.item.anon || this.currentUser) ? this.item.route : this.item.anonRoute }
|
|
||||||
}
|
|
||||||
if (USERNAME_ROUTES.has(route.name)) {
|
|
||||||
route.params = { username: this.currentUser.screen_name, name: this.currentUser.screen_name }
|
|
||||||
}
|
|
||||||
return route
|
|
||||||
},
|
},
|
||||||
getters () {
|
getters () {
|
||||||
return this.$store.getters
|
return this.$store.getters
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { mapState } from 'vuex'
|
import { mapState } from 'vuex'
|
||||||
import { TIMELINES, ROOT_ITEMS, USERNAME_ROUTES } from 'src/components/navigation/navigation.js'
|
import { TIMELINES, ROOT_ITEMS, routeTo } from 'src/components/navigation/navigation.js'
|
||||||
import { getListEntries, filterNavigation } from 'src/components/navigation/filter.js'
|
import { getListEntries, filterNavigation } from 'src/components/navigation/filter.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
@ -31,14 +31,7 @@ const NavPanel = {
|
||||||
props: ['limit'],
|
props: ['limit'],
|
||||||
methods: {
|
methods: {
|
||||||
getRouteTo (item) {
|
getRouteTo (item) {
|
||||||
if (item.routeObject) {
|
return routeTo(item, this.currentUser)
|
||||||
return item.routeObject
|
|
||||||
}
|
|
||||||
const route = { name: (item.anon || this.currentUser) ? item.route : item.anonRoute }
|
|
||||||
if (USERNAME_ROUTES.has(route.name)) {
|
|
||||||
route.params = { username: this.currentUser.screen_name }
|
|
||||||
}
|
|
||||||
return route
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -52,6 +45,7 @@ const NavPanel = {
|
||||||
privateMode: state => state.instance.private,
|
privateMode: state => state.instance.private,
|
||||||
federating: state => state.instance.federating,
|
federating: state => state.instance.federating,
|
||||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
||||||
|
supportsAnnouncements: state => state.announcements.supportsAnnouncements,
|
||||||
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems)
|
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems)
|
||||||
}),
|
}),
|
||||||
pinnedList () {
|
pinnedList () {
|
||||||
|
@ -63,6 +57,7 @@ const NavPanel = {
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
hasChats: this.pleromaChatMessagesAvailable,
|
hasChats: this.pleromaChatMessagesAvailable,
|
||||||
|
hasAnnouncements: this.supportsAnnouncements,
|
||||||
isFederating: this.federating,
|
isFederating: this.federating,
|
||||||
isPrivate: this.privateMode,
|
isPrivate: this.privateMode,
|
||||||
currentUser: this.currentUser
|
currentUser: this.currentUser
|
||||||
|
@ -82,6 +77,7 @@ const NavPanel = {
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
hasChats: this.pleromaChatMessagesAvailable,
|
hasChats: this.pleromaChatMessagesAvailable,
|
||||||
|
hasAnnouncements: this.supportsAnnouncements,
|
||||||
isFederating: this.federating,
|
isFederating: this.federating,
|
||||||
isPrivate: this.privateMode,
|
isPrivate: this.privateMode,
|
||||||
currentUser: this.currentUser
|
currentUser: this.currentUser
|
||||||
|
|
|
@ -121,7 +121,17 @@
|
||||||
scope="global"
|
scope="global"
|
||||||
keypath="notifications.reacted_with"
|
keypath="notifications.reacted_with"
|
||||||
>
|
>
|
||||||
<span class="emoji-reaction-emoji">{{ notification.emoji }}</span>
|
<img
|
||||||
|
v-if="notification.emoji_url"
|
||||||
|
class="emoji-reaction-emoji emoji-reaction-emoji-image"
|
||||||
|
:src="notification.emoji_url"
|
||||||
|
:alt="notification.emoji"
|
||||||
|
:title="notification.emoji"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="emoji-reaction-emoji"
|
||||||
|
>{{ notification.emoji }}</span>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</small>
|
</small>
|
||||||
</span>
|
</span>
|
||||||
|
@ -153,9 +163,9 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
<button
|
<button
|
||||||
class="button-unstyled expand-icon"
|
class="button-unstyled expand-icon"
|
||||||
@click.prevent="toggleStatusExpanded"
|
|
||||||
:title="$t('tool_tip.toggle_expand')"
|
:title="$t('tool_tip.toggle_expand')"
|
||||||
:aria-expanded="statusExpanded"
|
:aria-expanded="statusExpanded"
|
||||||
|
@click.prevent="toggleStatusExpanded"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
class="fa-scale-110"
|
class="fa-scale-110"
|
||||||
|
|
|
@ -129,6 +129,13 @@
|
||||||
|
|
||||||
.emoji-reaction-emoji {
|
.emoji-reaction-emoji {
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
|
max-width: 1.25em;
|
||||||
|
height: 1.25em;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-reaction-emoji-image {
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-details {
|
.notification-details {
|
||||||
|
|
|
@ -12,7 +12,8 @@ export default {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
choices: []
|
choices: [],
|
||||||
|
randomSeed: `${Math.random()}`.replace('.', '-')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
|
|
@ -4,53 +4,63 @@
|
||||||
:class="containerClass"
|
:class="containerClass"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(option, index) in options"
|
:role="showResults ? 'section' : (poll.multiple ? 'group' : 'radiogroup')"
|
||||||
:key="index"
|
|
||||||
class="poll-option"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="showResults"
|
v-for="(option, index) in options"
|
||||||
:title="resultTitle(option)"
|
:key="index"
|
||||||
class="option-result"
|
class="poll-option"
|
||||||
>
|
>
|
||||||
<div class="option-result-label">
|
<div
|
||||||
<span class="result-percentage">
|
v-if="showResults"
|
||||||
{{ percentageForOption(option.votes_count) }}%
|
:title="resultTitle(option)"
|
||||||
</span>
|
class="option-result"
|
||||||
<RichContent
|
>
|
||||||
:html="option.title_html"
|
<div class="option-result-label">
|
||||||
:handle-links="false"
|
<span class="result-percentage">
|
||||||
:emoji="emoji"
|
{{ percentageForOption(option.votes_count) }}%
|
||||||
|
</span>
|
||||||
|
<RichContent
|
||||||
|
:html="option.title_html"
|
||||||
|
:handle-links="false"
|
||||||
|
:emoji="emoji"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="result-fill"
|
||||||
|
:style="{ 'width': `${percentageForOption(option.votes_count)}%` }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="result-fill"
|
|
||||||
:style="{ 'width': `${percentageForOption(option.votes_count)}%` }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
@click="activateOption(index)"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-if="poll.multiple"
|
|
||||||
type="checkbox"
|
|
||||||
:disabled="loading"
|
|
||||||
:value="index"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-else
|
v-else
|
||||||
type="radio"
|
tabindex="0"
|
||||||
:disabled="loading"
|
:role="poll.multiple ? 'checkbox' : 'radio'"
|
||||||
:value="index"
|
:aria-labelledby="`option-vote-${randomSeed}-${index}`"
|
||||||
|
:aria-checked="choices[index]"
|
||||||
|
@click="activateOption(index)"
|
||||||
>
|
>
|
||||||
<label class="option-vote">
|
<input
|
||||||
<RichContent
|
v-if="poll.multiple"
|
||||||
:html="option.title_html"
|
type="checkbox"
|
||||||
:handle-links="false"
|
class="poll-checkbox"
|
||||||
:emoji="emoji"
|
:disabled="loading"
|
||||||
/>
|
:value="index"
|
||||||
</label>
|
>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
type="radio"
|
||||||
|
:disabled="loading"
|
||||||
|
:value="index"
|
||||||
|
>
|
||||||
|
<label class="option-vote">
|
||||||
|
<RichContent
|
||||||
|
:id="`option-vote-${randomSeed}-${index}`"
|
||||||
|
:html="option.title_html"
|
||||||
|
:handle-links="false"
|
||||||
|
:emoji="emoji"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer faint">
|
<div class="footer faint">
|
||||||
|
@ -161,5 +171,9 @@
|
||||||
padding: 0 0.5em;
|
padding: 0 0.5em;
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.poll-checkbox {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -45,6 +45,9 @@ const Popover = {
|
||||||
// Lets hover popover stay when clicking inside of it
|
// Lets hover popover stay when clicking inside of it
|
||||||
stayOnClick: Boolean,
|
stayOnClick: Boolean,
|
||||||
|
|
||||||
|
// Use styled button (to avoid nested buttons)
|
||||||
|
normalButton: Boolean,
|
||||||
|
|
||||||
triggerAttrs: {
|
triggerAttrs: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: {}
|
default: {}
|
||||||
|
|
|
@ -5,7 +5,8 @@
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
ref="trigger"
|
ref="trigger"
|
||||||
class="button-unstyled popover-trigger-button"
|
class="popover-trigger-button"
|
||||||
|
:class="normalButton ? 'button-default btn' : 'button-unstyled'"
|
||||||
type="button"
|
type="button"
|
||||||
v-bind="triggerAttrs"
|
v-bind="triggerAttrs"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
|
|
|
@ -8,6 +8,7 @@ import Gallery from 'src/components/gallery/gallery.vue'
|
||||||
import StatusContent from '../status_content/status_content.vue'
|
import StatusContent from '../status_content/status_content.vue'
|
||||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||||
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
|
||||||
|
import { propsToNative } from '../../services/attributes_helper/attributes_helper.service.js'
|
||||||
import { reject, map, uniqBy, debounce } from 'lodash'
|
import { reject, map, uniqBy, debounce } from 'lodash'
|
||||||
import suggestor from '../emoji_input/suggestor.js'
|
import suggestor from '../emoji_input/suggestor.js'
|
||||||
import { mapGetters, mapState } from 'vuex'
|
import { mapGetters, mapState } from 'vuex'
|
||||||
|
@ -629,6 +630,9 @@ const PostStatusForm = {
|
||||||
},
|
},
|
||||||
openProfileTab () {
|
openProfileTab () {
|
||||||
this.$store.dispatch('openSettingsModalTab', 'profile')
|
this.$store.dispatch('openSettingsModalTab', 'profile')
|
||||||
|
},
|
||||||
|
propsToNative (props) {
|
||||||
|
return propsToNative(props)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,9 @@
|
||||||
<span>{{ $t('post_status.scope_notice.public') }}</span>
|
<span>{{ $t('post_status.scope_notice.public') }}</span>
|
||||||
<a
|
<a
|
||||||
class="fa-scale-110 fa-old-padding dismiss"
|
class="fa-scale-110 fa-old-padding dismiss"
|
||||||
|
:title="$t('post_status.scope_notice_dismiss')"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
@click.prevent="dismissScopeNotice()"
|
@click.prevent="dismissScopeNotice()"
|
||||||
>
|
>
|
||||||
<FAIcon icon="times" />
|
<FAIcon icon="times" />
|
||||||
|
@ -42,6 +45,9 @@
|
||||||
<span>{{ $t('post_status.scope_notice.unlisted') }}</span>
|
<span>{{ $t('post_status.scope_notice.unlisted') }}</span>
|
||||||
<a
|
<a
|
||||||
class="fa-scale-110 fa-old-padding dismiss"
|
class="fa-scale-110 fa-old-padding dismiss"
|
||||||
|
:title="$t('post_status.scope_notice_dismiss')"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
@click.prevent="dismissScopeNotice()"
|
@click.prevent="dismissScopeNotice()"
|
||||||
>
|
>
|
||||||
<FAIcon icon="times" />
|
<FAIcon icon="times" />
|
||||||
|
@ -54,6 +60,9 @@
|
||||||
<span>{{ $t('post_status.scope_notice.private') }}</span>
|
<span>{{ $t('post_status.scope_notice.private') }}</span>
|
||||||
<a
|
<a
|
||||||
class="fa-scale-110 fa-old-padding dismiss"
|
class="fa-scale-110 fa-old-padding dismiss"
|
||||||
|
:title="$t('post_status.scope_notice_dismiss')"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
@click.prevent="dismissScopeNotice()"
|
@click.prevent="dismissScopeNotice()"
|
||||||
>
|
>
|
||||||
<FAIcon icon="times" />
|
<FAIcon icon="times" />
|
||||||
|
@ -124,14 +133,17 @@
|
||||||
:suggest="emojiSuggestor"
|
:suggest="emojiSuggestor"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
>
|
>
|
||||||
<input
|
<template #default="inputProps">
|
||||||
v-model="newStatus.spoilerText"
|
<input
|
||||||
type="text"
|
v-model="newStatus.spoilerText"
|
||||||
:placeholder="$t('post_status.content_warning')"
|
type="text"
|
||||||
:disabled="posting && !optimisticPosting"
|
:placeholder="$t('post_status.content_warning')"
|
||||||
size="1"
|
:disabled="posting && !optimisticPosting"
|
||||||
class="form-post-subject"
|
v-bind="propsToNative(inputProps)"
|
||||||
>
|
size="1"
|
||||||
|
class="form-post-subject"
|
||||||
|
>
|
||||||
|
</template>
|
||||||
</EmojiInput>
|
</EmojiInput>
|
||||||
<EmojiInput
|
<EmojiInput
|
||||||
ref="emoji-input"
|
ref="emoji-input"
|
||||||
|
@ -148,29 +160,32 @@
|
||||||
@sticker-upload-failed="uploadFailed"
|
@sticker-upload-failed="uploadFailed"
|
||||||
@shown="handleEmojiInputShow"
|
@shown="handleEmojiInputShow"
|
||||||
>
|
>
|
||||||
<textarea
|
<template #default="inputProps">
|
||||||
ref="textarea"
|
<textarea
|
||||||
v-model="newStatus.status"
|
ref="textarea"
|
||||||
:placeholder="placeholder || $t('post_status.default')"
|
v-model="newStatus.status"
|
||||||
rows="1"
|
:placeholder="placeholder || $t('post_status.default')"
|
||||||
cols="1"
|
rows="1"
|
||||||
:disabled="posting && !optimisticPosting"
|
cols="1"
|
||||||
class="form-post-body"
|
:disabled="posting && !optimisticPosting"
|
||||||
:class="{ 'scrollable-form': !!maxHeight }"
|
class="form-post-body"
|
||||||
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
|
:class="{ 'scrollable-form': !!maxHeight }"
|
||||||
@keydown.meta.enter="postStatus($event, newStatus)"
|
v-bind="propsToNative(inputProps)"
|
||||||
@keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
|
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
|
||||||
@input="resize"
|
@keydown.meta.enter="postStatus($event, newStatus)"
|
||||||
@compositionupdate="resize"
|
@keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
|
||||||
@paste="paste"
|
@input="resize"
|
||||||
/>
|
@compositionupdate="resize"
|
||||||
<p
|
@paste="paste"
|
||||||
v-if="hasStatusLengthLimit"
|
/>
|
||||||
class="character-counter faint"
|
<p
|
||||||
:class="{ error: isOverLengthLimit }"
|
v-if="hasStatusLengthLimit"
|
||||||
>
|
class="character-counter faint"
|
||||||
{{ charactersLeft }}
|
:class="{ error: isOverLengthLimit }"
|
||||||
</p>
|
>
|
||||||
|
{{ charactersLeft }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
</EmojiInput>
|
</EmojiInput>
|
||||||
<div
|
<div
|
||||||
v-if="!disableScopeSelector"
|
v-if="!disableScopeSelector"
|
||||||
|
@ -193,6 +208,7 @@
|
||||||
id="post-content-type"
|
id="post-content-type"
|
||||||
v-model="newStatus.contentType"
|
v-model="newStatus.contentType"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
|
:attrs="{ 'aria-label': $t('post_status.content_type_selection') }"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="postFormat in postFormats"
|
v-for="postFormat in postFormats"
|
||||||
|
@ -265,12 +281,10 @@
|
||||||
>
|
>
|
||||||
{{ $t('post_status.post') }}
|
{{ $t('post_status.post') }}
|
||||||
</button>
|
</button>
|
||||||
<!-- touchstart is used to keep the OSK at the same position after a message send -->
|
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
:disabled="uploadingFiles || disableSubmit"
|
:disabled="uploadingFiles || disableSubmit"
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
@touchstart.stop.prevent="postStatus($event, newStatus)"
|
|
||||||
@click.stop.prevent="postStatus($event, newStatus)"
|
@click.stop.prevent="postStatus($event, newStatus)"
|
||||||
>
|
>
|
||||||
{{ $t('post_status.post') }}
|
{{ $t('post_status.post') }}
|
||||||
|
|
|
@ -6,36 +6,51 @@
|
||||||
:trigger-attrs="{ title: $t('timeline.quick_filter_settings') }"
|
:trigger-attrs="{ title: $t('timeline.quick_filter_settings') }"
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="dropdown-menu">
|
<div
|
||||||
<div v-if="loggedIn">
|
class="dropdown-menu"
|
||||||
|
role="menu"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="loggedIn"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
v-if="!conversation"
|
v-if="!conversation"
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
|
:aria-checked="replyVisibilityAll"
|
||||||
|
role="menuitemradio"
|
||||||
@click="replyVisibilityAll = true"
|
@click="replyVisibilityAll = true"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="menu-checkbox -radio"
|
class="menu-checkbox -radio"
|
||||||
:class="{ 'menu-checkbox-checked': replyVisibilityAll }"
|
:class="{ 'menu-checkbox-checked': replyVisibilityAll }"
|
||||||
|
:aria-hidden="true"
|
||||||
/>{{ $t('settings.reply_visibility_all') }}
|
/>{{ $t('settings.reply_visibility_all') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!conversation"
|
v-if="!conversation"
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
|
:aria-checked="replyVisibilityFollowing"
|
||||||
|
role="menuitemradio"
|
||||||
@click="replyVisibilityFollowing = true"
|
@click="replyVisibilityFollowing = true"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="menu-checkbox -radio"
|
class="menu-checkbox -radio"
|
||||||
:class="{ 'menu-checkbox-checked': replyVisibilityFollowing }"
|
:class="{ 'menu-checkbox-checked': replyVisibilityFollowing }"
|
||||||
|
:aria-hidden="true"
|
||||||
/>{{ $t('settings.reply_visibility_following_short') }}
|
/>{{ $t('settings.reply_visibility_following_short') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!conversation"
|
v-if="!conversation"
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
|
:aria-checked="replyVisibilitySelf"
|
||||||
|
role="menuitemradio"
|
||||||
@click="replyVisibilitySelf = true"
|
@click="replyVisibilitySelf = true"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="menu-checkbox -radio"
|
class="menu-checkbox -radio"
|
||||||
:class="{ 'menu-checkbox-checked': replyVisibilitySelf }"
|
:class="{ 'menu-checkbox-checked': replyVisibilitySelf }"
|
||||||
|
:aria-hidden="true"
|
||||||
/>{{ $t('settings.reply_visibility_self_short') }}
|
/>{{ $t('settings.reply_visibility_self_short') }}
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
|
@ -46,33 +61,43 @@
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
|
role="menuitemcheckbox"
|
||||||
|
:aria-checked="muteBotStatuses"
|
||||||
@click="muteBotStatuses = !muteBotStatuses"
|
@click="muteBotStatuses = !muteBotStatuses"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="menu-checkbox"
|
class="menu-checkbox"
|
||||||
:class="{ 'menu-checkbox-checked': muteBotStatuses }"
|
:class="{ 'menu-checkbox-checked': muteBotStatuses }"
|
||||||
|
:aria-hidden="true"
|
||||||
/>{{ $t('settings.mute_bot_posts') }}
|
/>{{ $t('settings.mute_bot_posts') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
|
role="menuitemcheckbox"
|
||||||
|
:aria-checked="hideMedia"
|
||||||
@click="hideMedia = !hideMedia"
|
@click="hideMedia = !hideMedia"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="menu-checkbox"
|
class="menu-checkbox"
|
||||||
:class="{ 'menu-checkbox-checked': hideMedia }"
|
:class="{ 'menu-checkbox-checked': hideMedia }"
|
||||||
|
:aria-hidden="true"
|
||||||
/>{{ $t('settings.hide_media_previews') }}
|
/>{{ $t('settings.hide_media_previews') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
|
role="menuitemcheckbox"
|
||||||
|
:aria-checked="hideMutedPosts"
|
||||||
@click="hideMutedPosts = !hideMutedPosts"
|
@click="hideMutedPosts = !hideMutedPosts"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="menu-checkbox"
|
class="menu-checkbox"
|
||||||
:class="{ 'menu-checkbox-checked': hideMutedPosts }"
|
:class="{ 'menu-checkbox-checked': hideMutedPosts }"
|
||||||
|
:aria-hidden="true"
|
||||||
/>{{ $t('settings.hide_all_muted_posts') }}
|
/>{{ $t('settings.hide_all_muted_posts') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button-default dropdown-item dropdown-item-icon"
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
|
role="menuitem"
|
||||||
@click="openTab('filtering')"
|
@click="openTab('filtering')"
|
||||||
>
|
>
|
||||||
<FAIcon icon="font" />{{ $t('settings.word_filter_and_more') }}
|
<FAIcon icon="font" />{{ $t('settings.word_filter_and_more') }}
|
||||||
|
|
|
@ -6,60 +6,87 @@
|
||||||
:trigger-attrs="{ title: $t('timeline.quick_view_settings') }"
|
:trigger-attrs="{ title: $t('timeline.quick_view_settings') }"
|
||||||
>
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="dropdown-menu">
|
<div
|
||||||
<button
|
class="dropdown-menu"
|
||||||
class="button-default dropdown-item"
|
role="menu"
|
||||||
@click="conversationDisplay = 'tree'"
|
>
|
||||||
>
|
<div role="group">
|
||||||
<span
|
<button
|
||||||
class="menu-checkbox -radio"
|
class="button-default dropdown-item"
|
||||||
:class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }"
|
:aria-checked="conversationDisplay === 'tree'"
|
||||||
/><FAIcon icon="folder-tree" /> {{ $t('settings.conversation_display_tree_quick') }}
|
role="menuitemradio"
|
||||||
</button>
|
@click="conversationDisplay = 'tree'"
|
||||||
<button
|
>
|
||||||
class="button-default dropdown-item"
|
<span
|
||||||
@click="conversationDisplay = 'linear'"
|
class="menu-checkbox -radio"
|
||||||
>
|
:aria-hidden="true"
|
||||||
<span
|
:class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }"
|
||||||
class="menu-checkbox -radio"
|
/><FAIcon
|
||||||
:class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }"
|
icon="folder-tree"
|
||||||
/><FAIcon icon="list" /> {{ $t('settings.conversation_display_linear_quick') }}
|
:aria-hidden="true"
|
||||||
</button>
|
/> {{ $t('settings.conversation_display_tree_quick') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
:aria-checked="conversationDisplay === 'linear'"
|
||||||
|
role="menuitemradio"
|
||||||
|
@click="conversationDisplay = 'linear'"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox -radio"
|
||||||
|
:class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }"
|
||||||
|
:aria-hidden="true"
|
||||||
|
/><FAIcon
|
||||||
|
icon="list"
|
||||||
|
:aria-hidden="true"
|
||||||
|
/> {{ $t('settings.conversation_display_linear_quick') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
role="separator"
|
role="separator"
|
||||||
class="dropdown-divider"
|
class="dropdown-divider"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
|
role="menuitemcheckbox"
|
||||||
|
:aria-checked="showUserAvatars"
|
||||||
@click="showUserAvatars = !showUserAvatars"
|
@click="showUserAvatars = !showUserAvatars"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="menu-checkbox"
|
class="menu-checkbox"
|
||||||
:class="{ 'menu-checkbox-checked': showUserAvatars }"
|
:class="{ 'menu-checkbox-checked': showUserAvatars }"
|
||||||
|
:aria-hidden="true"
|
||||||
/>{{ $t('settings.mention_link_show_avatar_quick') }}
|
/>{{ $t('settings.mention_link_show_avatar_quick') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!conversation"
|
v-if="!conversation"
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
|
role="menuitemcheckbox"
|
||||||
|
:aria-checked="autoUpdate"
|
||||||
@click="autoUpdate = !autoUpdate"
|
@click="autoUpdate = !autoUpdate"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="menu-checkbox"
|
class="menu-checkbox"
|
||||||
:class="{ 'menu-checkbox-checked': autoUpdate }"
|
:class="{ 'menu-checkbox-checked': autoUpdate }"
|
||||||
|
:aria-hidden="true"
|
||||||
/>{{ $t('settings.auto_update') }}
|
/>{{ $t('settings.auto_update') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!conversation"
|
v-if="!conversation"
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
|
role="menuitemcheckbox"
|
||||||
|
:aria-checked="collapseWithSubjects"
|
||||||
@click="collapseWithSubjects = !collapseWithSubjects"
|
@click="collapseWithSubjects = !collapseWithSubjects"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="menu-checkbox"
|
class="menu-checkbox"
|
||||||
:class="{ 'menu-checkbox-checked': collapseWithSubjects }"
|
:class="{ 'menu-checkbox-checked': collapseWithSubjects }"
|
||||||
|
:aria-hidden="true"
|
||||||
/>{{ $t('settings.collapse_subject') }}
|
/>{{ $t('settings.collapse_subject') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button-default dropdown-item dropdown-item-icon"
|
class="button-default dropdown-item dropdown-item-icon"
|
||||||
|
role="menuitem"
|
||||||
@click="openTab('general')"
|
@click="openTab('general')"
|
||||||
>
|
>
|
||||||
<FAIcon icon="wrench" />{{ $t('settings.more_settings') }}
|
<FAIcon icon="wrench" />{{ $t('settings.more_settings') }}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
:class="{ disabled: !present || disabled }"
|
:class="{ disabled: !present || disabled }"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
|
:id="name + '-label'"
|
||||||
:for="name"
|
:for="name"
|
||||||
class="label"
|
class="label"
|
||||||
>
|
>
|
||||||
|
@ -12,7 +13,8 @@
|
||||||
<input
|
<input
|
||||||
v-if="typeof fallback !== 'undefined'"
|
v-if="typeof fallback !== 'undefined'"
|
||||||
:id="name + '-o'"
|
:id="name + '-o'"
|
||||||
class="opt"
|
:aria-labelledby="name + '-label'"
|
||||||
|
class="opt visible-for-screenreader-only"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="present"
|
:checked="present"
|
||||||
@change="$emit('update:modelValue', !present ? fallback : undefined)"
|
@change="$emit('update:modelValue', !present ? fallback : undefined)"
|
||||||
|
@ -21,6 +23,7 @@
|
||||||
v-if="typeof fallback !== 'undefined'"
|
v-if="typeof fallback !== 'undefined'"
|
||||||
class="opt-l"
|
class="opt-l"
|
||||||
:for="name + '-o'"
|
:for="name + '-o'"
|
||||||
|
:aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
:id="name"
|
:id="name"
|
||||||
|
@ -34,9 +37,10 @@
|
||||||
@input="$emit('update:modelValue', $event.target.value)"
|
@input="$emit('update:modelValue', $event.target.value)"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
:id="name"
|
:id="name + '-numeric'"
|
||||||
class="input-number"
|
class="input-number"
|
||||||
type="number"
|
type="number"
|
||||||
|
:aria-labelledby="name + '-label'"
|
||||||
:value="modelValue || fallback"
|
:value="modelValue || fallback"
|
||||||
:disabled="!present || disabled"
|
:disabled="!present || disabled"
|
||||||
:max="hardMax"
|
:max="hardMax"
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import Popover from '../popover/popover.vue'
|
import Popover from '../popover/popover.vue'
|
||||||
import { ensureFinalFallback } from '../../i18n/languages.js'
|
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons'
|
import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
|
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
|
||||||
import { trim } from 'lodash'
|
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faPlus,
|
faPlus,
|
||||||
|
@ -20,105 +19,34 @@ const ReactButton = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Popover
|
Popover,
|
||||||
|
EmojiPicker
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addReaction (event, emoji, close) {
|
addReaction (event) {
|
||||||
|
const emoji = event.insertion
|
||||||
const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
|
const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
|
||||||
if (existingReaction && existingReaction.me) {
|
if (existingReaction && existingReaction.me) {
|
||||||
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
||||||
} else {
|
} else {
|
||||||
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
||||||
}
|
}
|
||||||
close()
|
},
|
||||||
|
show () {
|
||||||
|
if (!this.expanded) {
|
||||||
|
this.$refs.picker.showPicker()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onShow () {
|
onShow () {
|
||||||
this.expanded = true
|
this.expanded = true
|
||||||
this.focusInput()
|
|
||||||
},
|
},
|
||||||
onClose () {
|
onClose () {
|
||||||
this.expanded = false
|
this.expanded = false
|
||||||
},
|
|
||||||
focusInput () {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
const input = document.querySelector('.reaction-picker-filter > input')
|
|
||||||
if (input) input.focus()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// Vaguely adjusted copypaste from emoji_input and emoji_picker!
|
|
||||||
maybeLocalizedEmojiNamesAndKeywords (emoji) {
|
|
||||||
const names = [emoji.displayText]
|
|
||||||
const keywords = []
|
|
||||||
|
|
||||||
if (emoji.displayTextI18n) {
|
|
||||||
names.push(this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emoji.annotations) {
|
|
||||||
this.languages.forEach(lang => {
|
|
||||||
names.push(emoji.annotations[lang]?.name)
|
|
||||||
|
|
||||||
keywords.push(...(emoji.annotations[lang]?.keywords || []))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
names: names.filter(k => k),
|
|
||||||
keywords: keywords.filter(k => k)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
maybeLocalizedEmojiName (emoji) {
|
|
||||||
if (!emoji.annotations) {
|
|
||||||
return emoji.displayText
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emoji.displayTextI18n) {
|
|
||||||
return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const lang of this.languages) {
|
|
||||||
if (emoji.annotations[lang]?.name) {
|
|
||||||
return emoji.annotations[lang].name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return emoji.displayText
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
commonEmojis () {
|
hideCustomEmoji () {
|
||||||
const hardcodedSet = new Set(['👍', '😠', '👀', '😂', '🔥'])
|
return !this.$store.state.instance.pleromaCustomEmojiReactionsAvailable
|
||||||
return this.$store.getters.standardEmojiList.filter(emoji => hardcodedSet.has(emoji.replacement))
|
|
||||||
},
|
|
||||||
languages () {
|
|
||||||
return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
|
|
||||||
},
|
|
||||||
emojis () {
|
|
||||||
if (this.filterWord !== '') {
|
|
||||||
const keywordLowercase = trim(this.filterWord.toLowerCase())
|
|
||||||
|
|
||||||
const orderedEmojiList = []
|
|
||||||
for (const emoji of this.$store.getters.standardEmojiList) {
|
|
||||||
const indices = this.maybeLocalizedEmojiNamesAndKeywords(emoji)
|
|
||||||
.keywords
|
|
||||||
.map(k => k.toLowerCase().indexOf(keywordLowercase))
|
|
||||||
.filter(k => k > -1)
|
|
||||||
|
|
||||||
const indexOfKeyword = indices.length ? Math.min(...indices) : -1
|
|
||||||
|
|
||||||
if (indexOfKeyword > -1) {
|
|
||||||
if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
|
|
||||||
orderedEmojiList[indexOfKeyword] = []
|
|
||||||
}
|
|
||||||
orderedEmojiList[indexOfKeyword].push(emoji)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return orderedEmojiList.flat()
|
|
||||||
}
|
|
||||||
return this.$store.getters.standardEmojiList || []
|
|
||||||
},
|
|
||||||
mergedConfig () {
|
|
||||||
return this.$store.getters.mergedConfig
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,73 +1,39 @@
|
||||||
<template>
|
<template>
|
||||||
<Popover
|
<span class="ReactButton">
|
||||||
trigger="click"
|
<EmojiPicker
|
||||||
class="ReactButton"
|
ref="picker"
|
||||||
placement="top"
|
:enable-sticker-picker="enableStickerPicker"
|
||||||
:offset="{ y: 5 }"
|
:hide-custom-emoji="hideCustomEmoji"
|
||||||
:bound-to="{ x: 'container' }"
|
class="emoji-picker-panel"
|
||||||
remove-padding
|
@emoji="addReaction"
|
||||||
popover-class="ReactButton popover-default"
|
@show="onShow"
|
||||||
@show="onShow"
|
@close="onClose"
|
||||||
@close="onClose"
|
/>
|
||||||
>
|
<span
|
||||||
<template #content="{close}">
|
class="button-unstyled popover-trigger"
|
||||||
<div class="reaction-picker-filter">
|
:title="$t('tool_tip.add_reaction')"
|
||||||
<input
|
@click.stop.prevent="show"
|
||||||
v-model="filterWord"
|
>
|
||||||
size="1"
|
<FALayers>
|
||||||
:placeholder="$t('emoji.search_emoji')"
|
<FAIcon
|
||||||
@input="$event.target.composing = false"
|
class="fa-scale-110 fa-old-padding"
|
||||||
>
|
:icon="['far', 'smile-beam']"
|
||||||
</div>
|
/>
|
||||||
<div class="reaction-picker">
|
<FAIcon
|
||||||
<span
|
v-show="!expanded"
|
||||||
v-for="emoji in commonEmojis"
|
class="focus-marker"
|
||||||
:key="emoji.replacement"
|
transform="shrink-6 up-9 right-17"
|
||||||
class="emoji-button"
|
icon="plus"
|
||||||
:title="maybeLocalizedEmojiName(emoji)"
|
/>
|
||||||
@click="addReaction($event, emoji.replacement, close)"
|
<FAIcon
|
||||||
>
|
v-show="expanded"
|
||||||
{{ emoji.replacement }}
|
class="focus-marker"
|
||||||
</span>
|
transform="shrink-6 up-9 right-17"
|
||||||
<div class="reaction-picker-divider" />
|
icon="times"
|
||||||
<span
|
/>
|
||||||
v-for="(emoji, key) in emojis"
|
</FALayers>
|
||||||
:key="key"
|
</span>
|
||||||
class="emoji-button"
|
</span>
|
||||||
:title="maybeLocalizedEmojiName(emoji)"
|
|
||||||
@click="addReaction($event, emoji.replacement, close)"
|
|
||||||
>
|
|
||||||
{{ emoji.replacement }}
|
|
||||||
</span>
|
|
||||||
<div class="reaction-bottom-fader" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #trigger>
|
|
||||||
<span
|
|
||||||
class="button-unstyled popover-trigger"
|
|
||||||
:title="$t('tool_tip.add_reaction')"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</template>
|
|
||||||
</Popover>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./react_button.js"></script>
|
<script src="./react_button.js"></script>
|
||||||
|
@ -135,11 +101,6 @@
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--text, $fallback--text);
|
color: var(--text, $fallback--text);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.popover-trigger-button {
|
|
||||||
/* override of popover internal stuff */
|
|
||||||
width: auto;
|
|
||||||
|
|
||||||
@include unfocused-style {
|
@include unfocused-style {
|
||||||
.focus-marker {
|
.focus-marker {
|
||||||
|
|
|
@ -16,7 +16,7 @@ const registration = {
|
||||||
confirm: '',
|
confirm: '',
|
||||||
birthday: '',
|
birthday: '',
|
||||||
reason: '',
|
reason: '',
|
||||||
language: ''
|
language: ['']
|
||||||
},
|
},
|
||||||
captcha: {}
|
captcha: {}
|
||||||
}),
|
}),
|
||||||
|
@ -100,7 +100,7 @@ const registration = {
|
||||||
this.user.captcha_token = this.captcha.token
|
this.user.captcha_token = this.captcha.token
|
||||||
this.user.captcha_answer_data = this.captcha.answer_data
|
this.user.captcha_answer_data = this.captcha.answer_data
|
||||||
if (this.user.language) {
|
if (this.user.language) {
|
||||||
this.user.language = localeService.internalToBackendLocale(this.user.language)
|
this.user.language = localeService.internalToBackendLocaleMulti(this.user.language.filter(k => k))
|
||||||
}
|
}
|
||||||
|
|
||||||
this.v$.$touch()
|
this.v$.$touch()
|
||||||
|
|
|
@ -210,6 +210,7 @@
|
||||||
:prompt-text="$t('registration.email_language')"
|
:prompt-text="$t('registration.email_language')"
|
||||||
:language="v$.user.language.$model"
|
:language="v$.user.language.$model"
|
||||||
:set-language="val => v$.user.language.$model = val"
|
:set-language="val => v$.user.language.$model = val"
|
||||||
|
@click.stop.prevent
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -32,12 +32,20 @@
|
||||||
target="_blank"
|
target="_blank"
|
||||||
role="button"
|
role="button"
|
||||||
:href="remoteInteractionLink"
|
:href="remoteInteractionLink"
|
||||||
|
:title="$t('tool_tip.reply')"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FALayers class="fa-old-padding-layer">
|
||||||
icon="reply"
|
<FAIcon
|
||||||
class="fa-scale-110 fa-old-padding"
|
class="fa-scale-110"
|
||||||
:title="$t('tool_tip.reply')"
|
icon="reply"
|
||||||
/>
|
/>
|
||||||
|
<FAIcon
|
||||||
|
v-if="!replying"
|
||||||
|
class="focus-marker"
|
||||||
|
transform="shrink-6 up-8 right-16"
|
||||||
|
icon="plus"
|
||||||
|
/>
|
||||||
|
</FALayers>
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
v-if="status.replies_count > 0"
|
v-if="status.replies_count > 0"
|
||||||
|
|
|
@ -45,13 +45,20 @@
|
||||||
class="button-unstyled interactive"
|
class="button-unstyled interactive"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
role="button"
|
role="button"
|
||||||
|
:title="$t('tool_tip.repeat')"
|
||||||
:href="remoteInteractionLink"
|
:href="remoteInteractionLink"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FALayers class="fa-old-padding-layer">
|
||||||
class="fa-scale-110 fa-old-padding"
|
<FAIcon
|
||||||
icon="retweet"
|
class="fa-scale-110"
|
||||||
:title="$t('tool_tip.repeat')"
|
icon="retweet"
|
||||||
/>
|
/>
|
||||||
|
<FAIcon
|
||||||
|
class="focus-marker"
|
||||||
|
transform="shrink-6 up-9 right-12"
|
||||||
|
icon="plus"
|
||||||
|
/>
|
||||||
|
</FALayers>
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
v-if="!mergedConfig.hidePostStats && status.repeat_num > 0"
|
v-if="!mergedConfig.hidePostStats && status.repeat_num > 0"
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
const ScreenReaderNotice = {
|
||||||
|
props: {
|
||||||
|
ariaLive: {
|
||||||
|
type: String,
|
||||||
|
defualt: 'assertive'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
currentText: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
announce (text) {
|
||||||
|
this.currentText = text
|
||||||
|
setTimeout(() => { this.currentText = '' }, 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScreenReaderNotice
|
|
@ -0,0 +1,10 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="visible-for-screenreader-only"
|
||||||
|
:aria-live="ariaLive"
|
||||||
|
>
|
||||||
|
{{ currentText }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./screen_reader_notice.js"></script>
|
|
@ -8,6 +8,7 @@
|
||||||
class="button-unstyled nav-icon"
|
class="button-unstyled nav-icon"
|
||||||
:title="$t('nav.search')"
|
:title="$t('nav.search')"
|
||||||
type="button"
|
type="button"
|
||||||
|
:aria-expanded="!hidden"
|
||||||
@click.prevent.stop="toggleHidden"
|
@click.prevent.stop="toggleHidden"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
<button
|
<button
|
||||||
class="button-default search-button"
|
class="button-default search-button"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
:title="$t('nav.search')"
|
||||||
@click="find(searchTerm)"
|
@click="find(searchTerm)"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
|
@ -39,6 +41,8 @@
|
||||||
<button
|
<button
|
||||||
class="button-unstyled cancel-search"
|
class="button-unstyled cancel-search"
|
||||||
type="button"
|
type="button"
|
||||||
|
:title="$t('nav.search_close')"
|
||||||
|
:aria-expanded="!hidden"
|
||||||
@click.prevent.stop="toggleHidden"
|
@click.prevent.stop="toggleHidden"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
|
|
|
@ -13,6 +13,7 @@ export default {
|
||||||
'modelValue',
|
'modelValue',
|
||||||
'disabled',
|
'disabled',
|
||||||
'unstyled',
|
'unstyled',
|
||||||
'kind'
|
'kind',
|
||||||
|
'attrs'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
<select
|
<select
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
|
v-bind="attrs"
|
||||||
@change="$emit('update:modelValue', $event.target.value)"
|
@change="$emit('update:modelValue', $event.target.value)"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||||
|
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||||
|
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||||
|
import StringSetting from '../helpers/string_setting.vue'
|
||||||
|
import GroupSetting from '../helpers/group_setting.vue'
|
||||||
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
|
|
||||||
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faGlobe
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faGlobe
|
||||||
|
)
|
||||||
|
|
||||||
|
const FrontendsTab = {
|
||||||
|
provide () {
|
||||||
|
return {
|
||||||
|
defaultDraftMode: true,
|
||||||
|
defaultSource: 'admin'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
BooleanSetting,
|
||||||
|
ChoiceSetting,
|
||||||
|
IntegerSetting,
|
||||||
|
StringSetting,
|
||||||
|
GroupSetting,
|
||||||
|
Popover
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
if (this.user.rights.admin) {
|
||||||
|
this.$store.dispatch('loadFrontendsStuff')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
frontends () {
|
||||||
|
return this.$store.state.adminSettings.frontends
|
||||||
|
},
|
||||||
|
...SharedComputedObject()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
update (frontend, suggestRef) {
|
||||||
|
const ref = suggestRef || frontend.refs[0]
|
||||||
|
const { name } = frontend
|
||||||
|
const payload = { name, ref }
|
||||||
|
|
||||||
|
this.$store.state.api.backendInteractor.installFrontend({ payload })
|
||||||
|
.then((externalUser) => {
|
||||||
|
this.$store.dispatch('loadFrontendsStuff')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setDefault (frontend, suggestRef) {
|
||||||
|
const ref = suggestRef || frontend.refs[0]
|
||||||
|
const { name } = frontend
|
||||||
|
|
||||||
|
this.$store.commit('updateAdminDraft', { path: [':pleroma', ':frontends', ':primary'], value: { name, ref } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FrontendsTab
|
|
@ -0,0 +1,13 @@
|
||||||
|
.frontends-tab {
|
||||||
|
.cards-list {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
word-wrap: nowrap;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-width: 10em;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,184 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="frontends-tab"
|
||||||
|
:label="$t('admin_dash.tabs.frontends')"
|
||||||
|
>
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('admin_dash.tabs.frontends') }}</h2>
|
||||||
|
<p>{{ $t('admin_dash.frontend.wip_notice') }}</p>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<h3>{{ $t('admin_dash.frontend.default_frontend') }}</h3>
|
||||||
|
<p>{{ $t('admin_dash.frontend.default_frontend_tip') }}</p>
|
||||||
|
<p>{{ $t('admin_dash.frontend.default_frontend_tip2') }}</p>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<StringSetting path=":pleroma.:frontends.:primary.name" />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<StringSetting path=":pleroma.:frontends.:primary.ref" />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<GroupSetting path=":pleroma.:frontends.:primary" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="setting-list">
|
||||||
|
<h3>{{ $t('admin_dash.frontend.available_frontends') }}</h3>
|
||||||
|
<ul class="cards-list">
|
||||||
|
<li
|
||||||
|
v-for="frontend in frontends"
|
||||||
|
:key="frontend.name"
|
||||||
|
>
|
||||||
|
<strong>{{ frontend.name }}</strong>
|
||||||
|
{{ ' ' }}
|
||||||
|
<span v-if="adminDraft[':pleroma'][':frontends'][':primary'].name === frontend.name">
|
||||||
|
<i18n-t
|
||||||
|
v-if="adminDraft[':pleroma'][':frontends'][':primary'].ref === frontend.refs[0]"
|
||||||
|
keypath="admin_dash.frontend.is_default"
|
||||||
|
/>
|
||||||
|
<i18n-t
|
||||||
|
v-else
|
||||||
|
keypath="admin_dash.frontend.is_default_custom"
|
||||||
|
>
|
||||||
|
<template #version>
|
||||||
|
<code>{{ adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</span>
|
||||||
|
<dl>
|
||||||
|
<dt>{{ $t('admin_dash.frontend.repository') }}</dt>
|
||||||
|
<dd>
|
||||||
|
<a
|
||||||
|
:href="frontend.git"
|
||||||
|
target="_blank"
|
||||||
|
>{{ frontend.git }}</a>
|
||||||
|
</dd>
|
||||||
|
<template v-if="expertLevel">
|
||||||
|
<dt>{{ $t('admin_dash.frontend.versions') }}</dt>
|
||||||
|
<dd
|
||||||
|
v-for="ref in frontend.refs"
|
||||||
|
:key="ref"
|
||||||
|
>
|
||||||
|
<code>{{ ref }}</code>
|
||||||
|
</dd>
|
||||||
|
</template>
|
||||||
|
<dt v-if="expertLevel">
|
||||||
|
{{ $t('admin_dash.frontend.build_url') }}
|
||||||
|
</dt>
|
||||||
|
<dd v-if="expertLevel">
|
||||||
|
<a
|
||||||
|
:href="frontend.build_url"
|
||||||
|
target="_blank"
|
||||||
|
>{{ frontend.build_url }}</a>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
<div>
|
||||||
|
<span class="btn-group">
|
||||||
|
<button
|
||||||
|
class="button button-default btn"
|
||||||
|
type="button"
|
||||||
|
@click="update(frontend)"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
frontend.installed
|
||||||
|
? $t('admin_dash.frontend.reinstall')
|
||||||
|
: $t('admin_dash.frontend.install')
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
<Popover
|
||||||
|
v-if="frontend.refs.length > 1"
|
||||||
|
trigger="click"
|
||||||
|
class="button-dropdown"
|
||||||
|
placement="bottom"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<button
|
||||||
|
v-for="ref in frontend.refs"
|
||||||
|
:key="ref"
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="update(frontend, ref)"
|
||||||
|
>
|
||||||
|
<i18n-t keypath="admin_dash.frontend.install_version">
|
||||||
|
<template #version>
|
||||||
|
<code>{{ ref }}</code>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #trigger>
|
||||||
|
<button
|
||||||
|
class="button button-default btn dropdown-button"
|
||||||
|
type="button"
|
||||||
|
:title="$t('admin_dash.frontend.more_install_options')"
|
||||||
|
>
|
||||||
|
<FAIcon icon="chevron-down" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="frontend.installed && frontend.name !== 'admin-fe'"
|
||||||
|
class="btn-group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button button-default btn"
|
||||||
|
type="button"
|
||||||
|
:disabled="
|
||||||
|
adminDraft[':pleroma'][':frontends'][':primary'].name === frontend.name &&
|
||||||
|
adminDraft[':pleroma'][':frontends'][':primary'].ref === frontend.refs[0]
|
||||||
|
"
|
||||||
|
@click="setDefault(frontend)"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
$t('admin_dash.frontend.set_default')
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
{{ ' ' }}
|
||||||
|
<Popover
|
||||||
|
v-if="frontend.refs.length > 1"
|
||||||
|
trigger="click"
|
||||||
|
class="button-dropdown"
|
||||||
|
placement="bottom"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<button
|
||||||
|
v-for="ref in frontend.refs.slice(1)"
|
||||||
|
:key="ref"
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="setDefault(frontend, ref)"
|
||||||
|
>
|
||||||
|
<i18n-t keypath="admin_dash.frontend.set_default_version">
|
||||||
|
<template #version>
|
||||||
|
<code>{{ ref }}</code>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #trigger>
|
||||||
|
<button
|
||||||
|
class="button button-default btn dropdown-button"
|
||||||
|
type="button"
|
||||||
|
:title="$t('admin_dash.frontend.more_default_options')"
|
||||||
|
>
|
||||||
|
<FAIcon icon="chevron-down" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./frontends_tab.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss" src="./frontends_tab.scss"></style>
|
|
@ -0,0 +1,38 @@
|
||||||
|
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||||
|
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||||
|
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||||
|
import StringSetting from '../helpers/string_setting.vue'
|
||||||
|
import GroupSetting from '../helpers/group_setting.vue'
|
||||||
|
import AttachmentSetting from '../helpers/attachment_setting.vue'
|
||||||
|
|
||||||
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faGlobe
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faGlobe
|
||||||
|
)
|
||||||
|
|
||||||
|
const InstanceTab = {
|
||||||
|
provide () {
|
||||||
|
return {
|
||||||
|
defaultDraftMode: true,
|
||||||
|
defaultSource: 'admin'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
BooleanSetting,
|
||||||
|
ChoiceSetting,
|
||||||
|
IntegerSetting,
|
||||||
|
StringSetting,
|
||||||
|
AttachmentSetting,
|
||||||
|
GroupSetting
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...SharedComputedObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InstanceTab
|
|
@ -0,0 +1,196 @@
|
||||||
|
<template>
|
||||||
|
<div :label="$t('admin_dash.tabs.instance')">
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('admin_dash.instance.instance') }}</h2>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<StringSetting path=":pleroma.:instance.:name" />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<StringSetting path=":pleroma.:instance.:email" />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<StringSetting path=":pleroma.:instance.:description" />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<StringSetting path=":pleroma.:instance.:short_description" />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<AttachmentSetting path=":pleroma.:instance.:instance_thumbnail" />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<AttachmentSetting path=":pleroma.:instance.:background_image" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('admin_dash.instance.registrations') }}</h2>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path=":pleroma.:instance.:registrations_open" />
|
||||||
|
<ul class="setting-list suboptions">
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path=":pleroma.:instance.:invites_enabled"
|
||||||
|
parent-path=":pleroma.:instance.:registrations_open"
|
||||||
|
parent-invert
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path=":pleroma.:instance.:birthday_required" />
|
||||||
|
<ul class="setting-list suboptions">
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
path=":pleroma.:instance.:birthday_min_age"
|
||||||
|
parent-path=":pleroma.:instance.:birthday_required"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path=":pleroma.:instance.:account_activation_required" />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path=":pleroma.:instance.:account_approval_required" />
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h3>{{ $t('admin_dash.instance.captcha_header') }}</h3>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<BooleanSetting :path="[':pleroma', 'Pleroma.Captcha', ':enabled']" />
|
||||||
|
<ul class="setting-list suboptions">
|
||||||
|
<li>
|
||||||
|
<ChoiceSetting
|
||||||
|
:path="[':pleroma', 'Pleroma.Captcha', ':method']"
|
||||||
|
:parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']"
|
||||||
|
:option-label-map="{
|
||||||
|
'Pleroma.Captcha.Native': $t('admin_dash.captcha.native'),
|
||||||
|
'Pleroma.Captcha.Kocaptcha': $t('admin_dash.captcha.kocaptcha')
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
<IntegerSetting
|
||||||
|
:path="[':pleroma', 'Pleroma.Captcha', ':seconds_valid']"
|
||||||
|
:parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="adminDraft[':pleroma']['Pleroma.Captcha'][':enabled'] && adminDraft[':pleroma']['Pleroma.Captcha'][':method'] === 'Pleroma.Captcha.Kocaptcha'"
|
||||||
|
>
|
||||||
|
<h4>{{ $t('admin_dash.instance.kocaptcha') }}</h4>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<StringSetting :path="[':pleroma', 'Pleroma.Captcha.Kocaptcha', ':endpoint']" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('admin_dash.instance.access') }}</h2>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
override-backend-description
|
||||||
|
override-backend-description-label
|
||||||
|
path=":pleroma.:instance.:public"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ChoiceSetting
|
||||||
|
override-backend-description
|
||||||
|
override-backend-description-label
|
||||||
|
path=":pleroma.:instance.:limit_to_local_content"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li v-if="expertLevel">
|
||||||
|
<h3>{{ $t('admin_dash.instance.restrict.header') }}</h3>
|
||||||
|
<p>
|
||||||
|
{{ $t('admin_dash.instance.restrict.description') }}
|
||||||
|
</p>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<h4>{{ $t('admin_dash.instance.restrict.timelines') }}</h4>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path=":pleroma.:restrict_unauthenticated.:timelines.:local"
|
||||||
|
indeterminate-state=":if_instance_is_private"
|
||||||
|
swap-description-and-label
|
||||||
|
hide-description
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path=":pleroma.:restrict_unauthenticated.:timelines.:federated"
|
||||||
|
indeterminate-state=":if_instance_is_private"
|
||||||
|
swap-description-and-label
|
||||||
|
hide-description
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<GroupSetting path=":pleroma.:restrict_unauthenticated.:timelines" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h4>{{ $t('admin_dash.instance.restrict.profiles') }}</h4>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path=":pleroma.:restrict_unauthenticated.:profiles.:local"
|
||||||
|
indeterminate-state=":if_instance_is_private"
|
||||||
|
swap-description-and-label
|
||||||
|
hide-description
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path=":pleroma.:restrict_unauthenticated.:profiles.:remote"
|
||||||
|
indeterminate-state=":if_instance_is_private"
|
||||||
|
swap-description-and-label
|
||||||
|
hide-description
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<GroupSetting path=":pleroma.:restrict_unauthenticated.:profiles" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h4>{{ $t('admin_dash.instance.restrict.activities') }}</h4>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path=":pleroma.:restrict_unauthenticated.:activities.:local"
|
||||||
|
indeterminate-state=":if_instance_is_private"
|
||||||
|
swap-description-and-label
|
||||||
|
hide-description
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path=":pleroma.:restrict_unauthenticated.:activities.:remote"
|
||||||
|
indeterminate-state=":if_instance_is_private"
|
||||||
|
swap-description-and-label
|
||||||
|
hide-description
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<GroupSetting path=":pleroma.:restrict_unauthenticated.:activities" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./instance_tab.js"></script>
|
|
@ -0,0 +1,29 @@
|
||||||
|
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||||
|
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||||
|
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||||
|
import StringSetting from '../helpers/string_setting.vue'
|
||||||
|
|
||||||
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faGlobe
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faGlobe
|
||||||
|
)
|
||||||
|
|
||||||
|
const LimitsTab = {
|
||||||
|
data () {},
|
||||||
|
components: {
|
||||||
|
BooleanSetting,
|
||||||
|
ChoiceSetting,
|
||||||
|
IntegerSetting,
|
||||||
|
StringSetting
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...SharedComputedObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LimitsTab
|
|
@ -0,0 +1,136 @@
|
||||||
|
<template>
|
||||||
|
<div :label="$t('admin_dash.tabs.limits')">
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('admin_dash.limits.arbitrary_limits') }}</h2>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<h3>{{ $t('admin_dash.limits.posts') }}</h3>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
source="admin"
|
||||||
|
path=":pleroma.:instance.:limit"
|
||||||
|
draft-mode
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
source="admin"
|
||||||
|
path=":pleroma.:instance.:remote_limit"
|
||||||
|
expert="1"
|
||||||
|
draft-mode
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h3>{{ $t('admin_dash.limits.uploads') }}</h3>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
source="admin"
|
||||||
|
path=":pleroma.:instance.:description_limit"
|
||||||
|
draft-mode
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
source="admin"
|
||||||
|
path=":pleroma.:instance.:upload_limit"
|
||||||
|
draft-mode
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
source="admin"
|
||||||
|
path=":pleroma.:instance.:max_media_attachments"
|
||||||
|
draft-mode
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h3>{{ $t('admin_dash.limits.users') }}</h3>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
source="admin"
|
||||||
|
path=":pleroma.:instance.:max_pinned_statuses"
|
||||||
|
draft-mode
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
source="admin"
|
||||||
|
path=":pleroma.:instance.:user_bio_length"
|
||||||
|
draft-mode
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
source="admin"
|
||||||
|
path=":pleroma.:instance.:user_name_length"
|
||||||
|
draft-mode
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h4>{{ $t('admin_dash.limits.profile_fields') }}</h4>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
source="admin"
|
||||||
|
path=":pleroma.:instance.:max_account_fields"
|
||||||
|
draft-mode
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
source="admin"
|
||||||
|
path=":pleroma.:instance.:max_remote_account_fields"
|
||||||
|
draft-mode
|
||||||
|
expert="1"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
source="admin"
|
||||||
|
path=":pleroma.:instance.:account_field_name_length"
|
||||||
|
draft-mode
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
source="admin"
|
||||||
|
path=":pleroma.:instance.:account_field_value_length"
|
||||||
|
draft-mode
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h4>{{ $t('admin_dash.limits.user_uploads') }}</h4>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
source="admin"
|
||||||
|
path=":pleroma.:instance.:avatar_upload_limit"
|
||||||
|
draft-mode
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
source="admin"
|
||||||
|
path=":pleroma.:instance.:banner_upload_limit"
|
||||||
|
draft-mode
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./limits_tab.js"></script>
|
|
@ -0,0 +1,43 @@
|
||||||
|
import Setting from './setting.js'
|
||||||
|
import { fileTypeExt } from 'src/services/file_type/file_type.service.js'
|
||||||
|
import MediaUpload from 'src/components/media_upload/media_upload.vue'
|
||||||
|
import Attachment from 'src/components/attachment/attachment.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...Setting,
|
||||||
|
props: {
|
||||||
|
...Setting.props,
|
||||||
|
acceptTypes: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'image/*'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
...Setting.components,
|
||||||
|
MediaUpload,
|
||||||
|
Attachment
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...Setting.computed,
|
||||||
|
attachment () {
|
||||||
|
const path = this.realDraftMode ? this.draft : this.state
|
||||||
|
// The "server" part is primarily for local dev, but could be useful for alt-domain or multiuser usage.
|
||||||
|
const url = path.includes('://') ? path : this.$store.state.instance.server + path
|
||||||
|
return {
|
||||||
|
mimetype: fileTypeExt(url),
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...Setting.methods,
|
||||||
|
setMediaFile (fileInfo) {
|
||||||
|
if (this.realDraftMode) {
|
||||||
|
this.draft = fileInfo.url
|
||||||
|
} else {
|
||||||
|
this.configSink(this.path, fileInfo.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
v-if="matchesExpertLevel"
|
||||||
|
class="AttachmentSetting"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
:for="path"
|
||||||
|
:class="{ 'faint': shouldBeDisabled }"
|
||||||
|
>
|
||||||
|
<template v-if="backendDescriptionLabel">
|
||||||
|
{{ backendDescriptionLabel + ' ' }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="source === 'admin'">
|
||||||
|
MISSING LABEL FOR {{ path }}
|
||||||
|
</template>
|
||||||
|
<slot v-else />
|
||||||
|
|
||||||
|
</label>
|
||||||
|
<p
|
||||||
|
v-if="backendDescriptionDescription"
|
||||||
|
class="setting-description"
|
||||||
|
:class="{ 'faint': shouldBeDisabled }"
|
||||||
|
>
|
||||||
|
{{ backendDescriptionDescription + ' ' }}
|
||||||
|
</p>
|
||||||
|
<div class="attachment-input">
|
||||||
|
<div>{{ $t('settings.url') }}</div>
|
||||||
|
<div class="controls">
|
||||||
|
<input
|
||||||
|
:id="path"
|
||||||
|
class="string-input"
|
||||||
|
:disabled="shouldBeDisabled"
|
||||||
|
:value="realDraftMode ? draft : state"
|
||||||
|
@change="update"
|
||||||
|
>
|
||||||
|
{{ ' ' }}
|
||||||
|
<ModifiedIndicator
|
||||||
|
:changed="isChanged"
|
||||||
|
:onclick="reset"
|
||||||
|
/>
|
||||||
|
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||||
|
</div>
|
||||||
|
<div>{{ $t('settings.preview') }}</div>
|
||||||
|
<Attachment
|
||||||
|
class="attachment"
|
||||||
|
:compact="compact"
|
||||||
|
:attachment="attachment"
|
||||||
|
size="small"
|
||||||
|
hide-description
|
||||||
|
@setMedia="onMedia"
|
||||||
|
@naturalSizeLoad="onNaturalSizeLoad"
|
||||||
|
/>
|
||||||
|
<div class="controls">
|
||||||
|
<MediaUpload
|
||||||
|
ref="mediaUpload"
|
||||||
|
class="media-upload-icon"
|
||||||
|
:drop-files="dropFiles"
|
||||||
|
normal-button
|
||||||
|
:accept-types="acceptTypes"
|
||||||
|
@uploaded="setMediaFile"
|
||||||
|
@upload-failed="uploadFailed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DraftButtons />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./attachment_setting.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.AttachmentSetting {
|
||||||
|
.attachment {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 15em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-input {
|
||||||
|
margin-left: 1em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
|
input,
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,56 +1,31 @@
|
||||||
import { get, set } from 'lodash'
|
|
||||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
import ModifiedIndicator from './modified_indicator.vue'
|
import Setting from './setting.js'
|
||||||
import ServerSideIndicator from './server_side_indicator.vue'
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
...Setting,
|
||||||
Checkbox,
|
props: {
|
||||||
ModifiedIndicator,
|
...Setting.props,
|
||||||
ServerSideIndicator
|
indeterminateState: [String, Object]
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
...Setting.components,
|
||||||
|
Checkbox
|
||||||
},
|
},
|
||||||
props: [
|
|
||||||
'path',
|
|
||||||
'disabled',
|
|
||||||
'expert'
|
|
||||||
],
|
|
||||||
computed: {
|
computed: {
|
||||||
pathDefault () {
|
...Setting.computed,
|
||||||
const [firstSegment, ...rest] = this.path.split('.')
|
isIndeterminate () {
|
||||||
return [firstSegment + 'DefaultValue', ...rest].join('.')
|
return this.visibleState === this.indeterminateState
|
||||||
},
|
|
||||||
state () {
|
|
||||||
const value = get(this.$parent, this.path)
|
|
||||||
if (value === undefined) {
|
|
||||||
return this.defaultState
|
|
||||||
} else {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
defaultState () {
|
|
||||||
return get(this.$parent, this.pathDefault)
|
|
||||||
},
|
|
||||||
isServerSide () {
|
|
||||||
return this.path.startsWith('serverSide_')
|
|
||||||
},
|
|
||||||
isChanged () {
|
|
||||||
return !this.path.startsWith('serverSide_') && this.state !== this.defaultState
|
|
||||||
},
|
|
||||||
matchesExpertLevel () {
|
|
||||||
return (this.expert || 0) <= this.$parent.expertLevel
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
update (e) {
|
...Setting.methods,
|
||||||
const [firstSegment, ...rest] = this.path.split('.')
|
getValue (e) {
|
||||||
set(this.$parent, this.path, e)
|
// Basic tri-state toggle implementation
|
||||||
// Updating nested properties does not trigger update on its parent.
|
if (!!this.indeterminateState && !e && this.visibleState === true) {
|
||||||
// probably still not as reliable, but works for depth=1 at least
|
// If we have indeterminate state, switching from true to false first goes through indeterminate
|
||||||
if (rest.length > 0) {
|
return this.indeterminateState
|
||||||
set(this.$parent, firstSegment, { ...get(this.$parent, firstSegment) })
|
|
||||||
}
|
}
|
||||||
},
|
return e
|
||||||
reset () {
|
|
||||||
set(this.$parent, this.path, this.defaultState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,23 +4,37 @@
|
||||||
class="BooleanSetting"
|
class="BooleanSetting"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="state"
|
:model-value="visibleState"
|
||||||
:disabled="disabled"
|
:disabled="shouldBeDisabled"
|
||||||
|
:indeterminate="isIndeterminate"
|
||||||
@update:modelValue="update"
|
@update:modelValue="update"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="!!$slots.default"
|
|
||||||
class="label"
|
class="label"
|
||||||
|
:class="{ 'faint': shouldBeDisabled }"
|
||||||
>
|
>
|
||||||
<slot />
|
<template v-if="backendDescriptionLabel">
|
||||||
|
{{ backendDescriptionLabel }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="source === 'admin'">
|
||||||
|
MISSING LABEL FOR {{ path }}
|
||||||
|
</template>
|
||||||
|
<slot v-else />
|
||||||
</span>
|
</span>
|
||||||
{{ ' ' }}
|
|
||||||
<ModifiedIndicator
|
|
||||||
:changed="isChanged"
|
|
||||||
:onclick="reset"
|
|
||||||
/>
|
|
||||||
<ServerSideIndicator :server-side="isServerSide" />
|
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
<ModifiedIndicator
|
||||||
|
:changed="isChanged"
|
||||||
|
:onclick="reset"
|
||||||
|
/>
|
||||||
|
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||||
|
<DraftButtons />
|
||||||
|
<p
|
||||||
|
v-if="backendDescriptionDescription"
|
||||||
|
class="setting-description"
|
||||||
|
:class="{ 'faint': shouldBeDisabled }"
|
||||||
|
>
|
||||||
|
{{ backendDescriptionDescription + ' ' }}
|
||||||
|
</p>
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,51 +1,41 @@
|
||||||
import { get, set } from 'lodash'
|
|
||||||
import Select from 'src/components/select/select.vue'
|
import Select from 'src/components/select/select.vue'
|
||||||
import ModifiedIndicator from './modified_indicator.vue'
|
import Setting from './setting.js'
|
||||||
import ServerSideIndicator from './server_side_indicator.vue'
|
|
||||||
export default {
|
export default {
|
||||||
|
...Setting,
|
||||||
components: {
|
components: {
|
||||||
Select,
|
...Setting.components,
|
||||||
ModifiedIndicator,
|
Select
|
||||||
ServerSideIndicator
|
},
|
||||||
|
props: {
|
||||||
|
...Setting.props,
|
||||||
|
options: {
|
||||||
|
type: Array,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
optionLabelMap: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: {}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
props: [
|
|
||||||
'path',
|
|
||||||
'disabled',
|
|
||||||
'options',
|
|
||||||
'expert'
|
|
||||||
],
|
|
||||||
computed: {
|
computed: {
|
||||||
pathDefault () {
|
...Setting.computed,
|
||||||
const [firstSegment, ...rest] = this.path.split('.')
|
realOptions () {
|
||||||
return [firstSegment + 'DefaultValue', ...rest].join('.')
|
if (this.realSource === 'admin') {
|
||||||
},
|
return this.backendDescriptionSuggestions.map(x => ({
|
||||||
state () {
|
key: x,
|
||||||
const value = get(this.$parent, this.path)
|
value: x,
|
||||||
if (value === undefined) {
|
label: this.optionLabelMap[x] || x
|
||||||
return this.defaultState
|
}))
|
||||||
} else {
|
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
},
|
return this.options
|
||||||
defaultState () {
|
|
||||||
return get(this.$parent, this.pathDefault)
|
|
||||||
},
|
|
||||||
isServerSide () {
|
|
||||||
return this.path.startsWith('serverSide_')
|
|
||||||
},
|
|
||||||
isChanged () {
|
|
||||||
return !this.path.startsWith('serverSide_') && this.state !== this.defaultState
|
|
||||||
},
|
|
||||||
matchesExpertLevel () {
|
|
||||||
return (this.expert || 0) <= this.$parent.expertLevel
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
update (e) {
|
...Setting.methods,
|
||||||
set(this.$parent, this.path, e)
|
getValue (e) {
|
||||||
},
|
return e
|
||||||
reset () {
|
|
||||||
set(this.$parent, this.path, this.defaultState)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,20 @@
|
||||||
v-if="matchesExpertLevel"
|
v-if="matchesExpertLevel"
|
||||||
class="ChoiceSetting"
|
class="ChoiceSetting"
|
||||||
>
|
>
|
||||||
<slot />
|
<template v-if="backendDescriptionLabel">
|
||||||
|
{{ backendDescriptionLabel }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
{{ ' ' }}
|
{{ ' ' }}
|
||||||
<Select
|
<Select
|
||||||
:model-value="state"
|
:model-value="realDraftMode ? draft :state"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@update:modelValue="update"
|
@update:modelValue="update"
|
||||||
>
|
>
|
||||||
<option
|
<option
|
||||||
v-for="option in options"
|
v-for="option in realOptions"
|
||||||
:key="option.key"
|
:key="option.key"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
>
|
>
|
||||||
|
@ -23,7 +28,14 @@
|
||||||
:changed="isChanged"
|
:changed="isChanged"
|
||||||
:onclick="reset"
|
:onclick="reset"
|
||||||
/>
|
/>
|
||||||
<ServerSideIndicator :server-side="isServerSide" />
|
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||||
|
<DraftButtons />
|
||||||
|
<p
|
||||||
|
v-if="backendDescriptionDescription"
|
||||||
|
class="setting-description"
|
||||||
|
>
|
||||||
|
{{ backendDescriptionDescription + ' ' }}
|
||||||
|
</p>
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
<!-- this is a helper exclusive to Setting components -->
|
||||||
|
<!-- TODO make it reusable -->
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
class="DraftButtons"
|
||||||
|
>
|
||||||
|
<Popover
|
||||||
|
v-if="$parent.isDirty"
|
||||||
|
trigger="hover"
|
||||||
|
normal-button
|
||||||
|
:trigger-attrs="{ 'aria-label': $t('settings.commit_value_tooltip') }"
|
||||||
|
@click="$parent.commitDraft"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
{{ $t('settings.commit_value') }}
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="modified-tooltip">
|
||||||
|
{{ $t('settings.commit_value_tooltip') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
<Popover
|
||||||
|
v-if="$parent.isDirty"
|
||||||
|
trigger="hover"
|
||||||
|
normal-button
|
||||||
|
:trigger-attrs="{ 'aria-label': $t('settings.reset_value_tooltip') }"
|
||||||
|
@click="$parent.reset"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
{{ $t('settings.reset_value') }}
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="modified-tooltip">
|
||||||
|
{{ $t('settings.reset_value_tooltip') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
<Popover
|
||||||
|
v-if="$parent.canHardReset"
|
||||||
|
trigger="hover"
|
||||||
|
normal-button
|
||||||
|
:trigger-attrs="{ 'aria-label': $t('settings.hard_reset_value_tooltip') }"
|
||||||
|
@click="$parent.hardReset"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
{{ $t('settings.hard_reset_value') }}
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<div class="modified-tooltip">
|
||||||
|
{{ $t('settings.hard_reset_value_tooltip') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faWrench } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faWrench
|
||||||
|
)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { Popover },
|
||||||
|
props: ['changed']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.DraftButtons {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.button-default {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-tooltip {
|
||||||
|
margin: 0.5em 1em;
|
||||||
|
min-width: 10em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,16 @@
|
||||||
|
<template>
|
||||||
|
<NumberSetting
|
||||||
|
v-bind="$attrs"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</NumberSetting>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import NumberSetting from './number_setting.vue'
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
NumberSetting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { isEqual } from 'lodash'
|
||||||
|
|
||||||
|
import Setting from './setting.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...Setting,
|
||||||
|
computed: {
|
||||||
|
...Setting.computed,
|
||||||
|
isDirty () {
|
||||||
|
return !isEqual(this.state, this.draft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
v-if="matchesExpertLevel"
|
||||||
|
class="GroupSetting"
|
||||||
|
>
|
||||||
|
<ModifiedIndicator
|
||||||
|
:changed="isChanged"
|
||||||
|
:onclick="reset"
|
||||||
|
/>
|
||||||
|
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||||
|
<DraftButtons />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./group_setting.js"></script>
|
|
@ -1,44 +0,0 @@
|
||||||
import { get, set } from 'lodash'
|
|
||||||
import ModifiedIndicator from './modified_indicator.vue'
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
ModifiedIndicator
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
path: String,
|
|
||||||
disabled: Boolean,
|
|
||||||
min: Number,
|
|
||||||
expert: [Number, String]
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
pathDefault () {
|
|
||||||
const [firstSegment, ...rest] = this.path.split('.')
|
|
||||||
return [firstSegment + 'DefaultValue', ...rest].join('.')
|
|
||||||
},
|
|
||||||
state () {
|
|
||||||
const value = get(this.$parent, this.path)
|
|
||||||
if (value === undefined) {
|
|
||||||
return this.defaultState
|
|
||||||
} else {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
defaultState () {
|
|
||||||
return get(this.$parent, this.pathDefault)
|
|
||||||
},
|
|
||||||
isChanged () {
|
|
||||||
return this.state !== this.defaultState
|
|
||||||
},
|
|
||||||
matchesExpertLevel () {
|
|
||||||
return (this.expert || 0) <= this.$parent.expertLevel
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
update (e) {
|
|
||||||
set(this.$parent, this.path, parseInt(e.target.value))
|
|
||||||
},
|
|
||||||
reset () {
|
|
||||||
set(this.$parent, this.path, this.defaultState)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<span
|
<NumberSetting
|
||||||
v-if="matchesExpertLevel"
|
v-bind="$attrs"
|
||||||
class="IntegerSetting"
|
truncate="1"
|
||||||
>
|
>
|
||||||
<label :for="path">
|
<slot />
|
||||||
<slot />
|
</NumberSetting>
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
:id="path"
|
|
||||||
class="number-input"
|
|
||||||
type="number"
|
|
||||||
step="1"
|
|
||||||
:disabled="disabled"
|
|
||||||
:min="min || 0"
|
|
||||||
:value="state"
|
|
||||||
@change="update"
|
|
||||||
>
|
|
||||||
{{ ' ' }}
|
|
||||||
<ModifiedIndicator
|
|
||||||
:changed="isChanged"
|
|
||||||
:onclick="reset"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./integer_setting.js"></script>
|
<script>
|
||||||
|
import NumberSetting from './number_setting.vue'
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
NumberSetting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
>
|
>
|
||||||
<Popover
|
<Popover
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
|
:trigger-attrs="{ 'aria-label': $t('settings.setting_changed') }"
|
||||||
>
|
>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
|
|
||||||
<FAIcon
|
<FAIcon
|
||||||
icon="wrench"
|
icon="wrench"
|
||||||
:aria-label="$t('settings.setting_changed')"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import Setting from './setting.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...Setting,
|
||||||
|
props: {
|
||||||
|
...Setting.props,
|
||||||
|
truncate: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
default: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...Setting.methods,
|
||||||
|
getValue (e) {
|
||||||
|
if (!this.truncate === 1) {
|
||||||
|
return parseInt(e.target.value)
|
||||||
|
} else if (this.truncate > 1) {
|
||||||
|
return Math.trunc(e.target.value / this.truncate) * this.truncate
|
||||||
|
}
|
||||||
|
return parseFloat(e.target.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
v-if="matchesExpertLevel"
|
||||||
|
class="NumberSetting"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
:for="path"
|
||||||
|
:class="{ 'faint': shouldBeDisabled }"
|
||||||
|
>
|
||||||
|
<template v-if="backendDescriptionLabel">
|
||||||
|
{{ backendDescriptionLabel + ' ' }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="source === 'admin'">
|
||||||
|
MISSING LABEL FOR {{ path }}
|
||||||
|
</template>
|
||||||
|
<slot v-else />
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
:id="path"
|
||||||
|
class="number-input"
|
||||||
|
type="number"
|
||||||
|
:step="step || 1"
|
||||||
|
:disabled="shouldBeDisabled"
|
||||||
|
:min="min || 0"
|
||||||
|
:value="realDraftMode ? draft :state"
|
||||||
|
@change="update"
|
||||||
|
>
|
||||||
|
{{ ' ' }}
|
||||||
|
<ModifiedIndicator
|
||||||
|
:changed="isChanged"
|
||||||
|
:onclick="reset"
|
||||||
|
/>
|
||||||
|
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||||
|
<DraftButtons />
|
||||||
|
<p
|
||||||
|
v-if="backendDescriptionDescription"
|
||||||
|
class="setting-description"
|
||||||
|
:class="{ 'faint': shouldBeDisabled }"
|
||||||
|
>
|
||||||
|
{{ backendDescriptionDescription + ' ' }}
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./number_setting.js"></script>
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<span
|
<span
|
||||||
v-if="serverSide"
|
v-if="isProfile"
|
||||||
class="ServerSideIndicator"
|
class="ProfileSettingIndicator"
|
||||||
>
|
>
|
||||||
<Popover
|
<Popover
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<div class="serverside-tooltip">
|
<div class="profilesetting-tooltip">
|
||||||
{{ $t('settings.setting_server_side') }}
|
{{ $t('settings.setting_server_side') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -33,17 +33,17 @@ library.add(
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { Popover },
|
components: { Popover },
|
||||||
props: ['serverSide']
|
props: ['isProfile']
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.ServerSideIndicator {
|
.ProfileSettingIndicator {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.serverside-tooltip {
|
.profilesetting-tooltip {
|
||||||
margin: 0.5em 1em;
|
margin: 0.5em 1em;
|
||||||
min-width: 10em;
|
min-width: 10em;
|
||||||
text-align: center;
|
text-align: center;
|
|
@ -0,0 +1,237 @@
|
||||||
|
import ModifiedIndicator from './modified_indicator.vue'
|
||||||
|
import ProfileSettingIndicator from './profile_setting_indicator.vue'
|
||||||
|
import DraftButtons from './draft_buttons.vue'
|
||||||
|
import { get, set, cloneDeep } from 'lodash'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ModifiedIndicator,
|
||||||
|
DraftButtons,
|
||||||
|
ProfileSettingIndicator
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
path: {
|
||||||
|
type: [String, Array],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
parentPath: {
|
||||||
|
type: [String, Array]
|
||||||
|
},
|
||||||
|
parentInvert: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
expert: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
source: {
|
||||||
|
type: String,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
hideDescription: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
swapDescriptionAndLabel: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
overrideBackendDescription: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
overrideBackendDescriptionLabel: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
draftMode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inject: {
|
||||||
|
defaultSource: {
|
||||||
|
default: 'default'
|
||||||
|
},
|
||||||
|
defaultDraftMode: {
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
localDraft: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
if (this.realDraftMode && this.realSource !== 'admin') {
|
||||||
|
this.draft = this.state
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
draft: {
|
||||||
|
// TODO allow passing shared draft object?
|
||||||
|
get () {
|
||||||
|
if (this.realSource === 'admin') {
|
||||||
|
return get(this.$store.state.adminSettings.draft, this.canonPath)
|
||||||
|
} else {
|
||||||
|
return this.localDraft
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
if (this.realSource === 'admin') {
|
||||||
|
this.$store.commit('updateAdminDraft', { path: this.canonPath, value })
|
||||||
|
} else {
|
||||||
|
this.localDraft = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state () {
|
||||||
|
const value = get(this.configSource, this.canonPath)
|
||||||
|
if (value === undefined) {
|
||||||
|
return this.defaultState
|
||||||
|
} else {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
visibleState () {
|
||||||
|
return this.realDraftMode ? this.draft : this.state
|
||||||
|
},
|
||||||
|
realSource () {
|
||||||
|
return this.source || this.defaultSource
|
||||||
|
},
|
||||||
|
realDraftMode () {
|
||||||
|
return typeof this.draftMode === 'undefined' ? this.defaultDraftMode : this.draftMode
|
||||||
|
},
|
||||||
|
backendDescription () {
|
||||||
|
return get(this.$store.state.adminSettings.descriptions, this.path)
|
||||||
|
},
|
||||||
|
backendDescriptionLabel () {
|
||||||
|
if (this.realSource !== 'admin') return ''
|
||||||
|
if (!this.backendDescription || this.overrideBackendDescriptionLabel) {
|
||||||
|
return this.$t([
|
||||||
|
'admin_dash',
|
||||||
|
'temp_overrides',
|
||||||
|
...this.canonPath.map(p => p.replace(/\./g, '_DOT_')),
|
||||||
|
'label'
|
||||||
|
].join('.'))
|
||||||
|
} else {
|
||||||
|
return this.swapDescriptionAndLabel
|
||||||
|
? this.backendDescription?.description
|
||||||
|
: this.backendDescription?.label
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backendDescriptionDescription () {
|
||||||
|
if (this.realSource !== 'admin') return ''
|
||||||
|
if (this.hideDescription) return null
|
||||||
|
if (!this.backendDescription || this.overrideBackendDescription) {
|
||||||
|
return this.$t([
|
||||||
|
'admin_dash',
|
||||||
|
'temp_overrides',
|
||||||
|
...this.canonPath.map(p => p.replace(/\./g, '_DOT_')),
|
||||||
|
'description'
|
||||||
|
].join('.'))
|
||||||
|
} else {
|
||||||
|
return this.swapDescriptionAndLabel
|
||||||
|
? this.backendDescription?.label
|
||||||
|
: this.backendDescription?.description
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backendDescriptionSuggestions () {
|
||||||
|
return this.backendDescription?.suggestions
|
||||||
|
},
|
||||||
|
shouldBeDisabled () {
|
||||||
|
const parentValue = this.parentPath !== undefined ? get(this.configSource, this.parentPath) : null
|
||||||
|
return this.disabled || (parentValue !== null ? (this.parentInvert ? parentValue : !parentValue) : false)
|
||||||
|
},
|
||||||
|
configSource () {
|
||||||
|
switch (this.realSource) {
|
||||||
|
case 'profile':
|
||||||
|
return this.$store.state.profileConfig
|
||||||
|
case 'admin':
|
||||||
|
return this.$store.state.adminSettings.config
|
||||||
|
default:
|
||||||
|
return this.$store.getters.mergedConfig
|
||||||
|
}
|
||||||
|
},
|
||||||
|
configSink () {
|
||||||
|
switch (this.realSource) {
|
||||||
|
case 'profile':
|
||||||
|
return (k, v) => this.$store.dispatch('setProfileOption', { name: k, value: v })
|
||||||
|
case 'admin':
|
||||||
|
return (k, v) => this.$store.dispatch('pushAdminSetting', { path: k, value: v })
|
||||||
|
default:
|
||||||
|
return (k, v) => this.$store.dispatch('setOption', { name: k, value: v })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultState () {
|
||||||
|
switch (this.realSource) {
|
||||||
|
case 'profile':
|
||||||
|
return {}
|
||||||
|
default:
|
||||||
|
return get(this.$store.getters.defaultConfig, this.path)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isProfileSetting () {
|
||||||
|
return this.realSource === 'profile'
|
||||||
|
},
|
||||||
|
isChanged () {
|
||||||
|
switch (this.realSource) {
|
||||||
|
case 'profile':
|
||||||
|
case 'admin':
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return this.state !== this.defaultState
|
||||||
|
}
|
||||||
|
},
|
||||||
|
canonPath () {
|
||||||
|
return Array.isArray(this.path) ? this.path : this.path.split('.')
|
||||||
|
},
|
||||||
|
isDirty () {
|
||||||
|
if (this.realSource === 'admin' && this.canonPath.length > 3) {
|
||||||
|
return false // should not show draft buttons for "grouped" values
|
||||||
|
} else {
|
||||||
|
return this.realDraftMode && this.draft !== this.state
|
||||||
|
}
|
||||||
|
},
|
||||||
|
canHardReset () {
|
||||||
|
return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> '))
|
||||||
|
},
|
||||||
|
matchesExpertLevel () {
|
||||||
|
return (this.expert || 0) <= this.$store.state.config.expertLevel > 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getValue (e) {
|
||||||
|
return e.target.value
|
||||||
|
},
|
||||||
|
update (e) {
|
||||||
|
if (this.realDraftMode) {
|
||||||
|
this.draft = this.getValue(e)
|
||||||
|
} else {
|
||||||
|
this.configSink(this.path, this.getValue(e))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
commitDraft () {
|
||||||
|
if (this.realDraftMode) {
|
||||||
|
this.configSink(this.path, this.draft)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reset () {
|
||||||
|
if (this.realDraftMode) {
|
||||||
|
this.draft = cloneDeep(this.state)
|
||||||
|
} else {
|
||||||
|
set(this.$store.getters.mergedConfig, this.path, cloneDeep(this.defaultState))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hardReset () {
|
||||||
|
switch (this.realSource) {
|
||||||
|
case 'admin':
|
||||||
|
return this.$store.dispatch('resetAdminSetting', { path: this.path })
|
||||||
|
.then(() => { this.draft = this.state })
|
||||||
|
default:
|
||||||
|
console.warn('Hard reset not implemented yet!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,52 +1,18 @@
|
||||||
import { defaultState as configDefaultState } from 'src/modules/config.js'
|
|
||||||
import { defaultState as serverSideConfigDefaultState } from 'src/modules/serverSideConfig.js'
|
|
||||||
|
|
||||||
const SharedComputedObject = () => ({
|
const SharedComputedObject = () => ({
|
||||||
user () {
|
user () {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
// Getting values for default properties
|
expertLevel () {
|
||||||
...Object.keys(configDefaultState)
|
return this.$store.getters.mergedConfig.expertLevel > 0
|
||||||
.map(key => [
|
},
|
||||||
key + 'DefaultValue',
|
mergedConfig () {
|
||||||
function () {
|
return this.$store.getters.mergedConfig
|
||||||
return this.$store.getters.defaultConfig[key]
|
},
|
||||||
}
|
adminConfig () {
|
||||||
])
|
return this.$store.state.adminSettings.config
|
||||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
},
|
||||||
// Generating computed values for vuex properties
|
adminDraft () {
|
||||||
...Object.keys(configDefaultState)
|
return this.$store.state.adminSettings.draft
|
||||||
.map(key => [key, {
|
|
||||||
get () { return this.$store.getters.mergedConfig[key] },
|
|
||||||
set (value) {
|
|
||||||
this.$store.dispatch('setOption', { name: key, value })
|
|
||||||
}
|
|
||||||
}])
|
|
||||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
|
||||||
...Object.keys(serverSideConfigDefaultState)
|
|
||||||
.map(key => ['serverSide_' + key, {
|
|
||||||
get () { return this.$store.state.serverSideConfig[key] },
|
|
||||||
set (value) {
|
|
||||||
this.$store.dispatch('setServerSideOption', { name: key, value })
|
|
||||||
}
|
|
||||||
}])
|
|
||||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
|
||||||
// Special cases (need to transform values or perform actions first)
|
|
||||||
useStreamingApi: {
|
|
||||||
get () { return this.$store.getters.mergedConfig.useStreamingApi },
|
|
||||||
set (value) {
|
|
||||||
const promise = value
|
|
||||||
? this.$store.dispatch('enableMastoSockets')
|
|
||||||
: this.$store.dispatch('disableMastoSockets')
|
|
||||||
|
|
||||||
promise.then(() => {
|
|
||||||
this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
|
|
||||||
}).catch((e) => {
|
|
||||||
console.error('Failed starting MastoAPI Streaming socket', e)
|
|
||||||
this.$store.dispatch('disableMastoSockets')
|
|
||||||
this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,67 +1,40 @@
|
||||||
import { get, set } from 'lodash'
|
|
||||||
import ModifiedIndicator from './modified_indicator.vue'
|
|
||||||
import Select from 'src/components/select/select.vue'
|
import Select from 'src/components/select/select.vue'
|
||||||
|
import Setting from './setting.js'
|
||||||
|
|
||||||
export const allCssUnits = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%']
|
export const allCssUnits = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%']
|
||||||
export const defaultHorizontalUnits = ['px', 'rem', 'vw']
|
export const defaultHorizontalUnits = ['px', 'rem', 'vw']
|
||||||
export const defaultVerticalUnits = ['px', 'rem', 'vh']
|
export const defaultVerticalUnits = ['px', 'rem', 'vh']
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
...Setting,
|
||||||
components: {
|
components: {
|
||||||
ModifiedIndicator,
|
...Setting.components,
|
||||||
Select
|
Select
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
path: String,
|
...Setting.props,
|
||||||
disabled: Boolean,
|
|
||||||
min: Number,
|
min: Number,
|
||||||
units: {
|
units: {
|
||||||
type: [String],
|
type: Array,
|
||||||
default: () => allCssUnits
|
default: () => allCssUnits
|
||||||
},
|
}
|
||||||
expert: [Number, String]
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
pathDefault () {
|
...Setting.computed,
|
||||||
const [firstSegment, ...rest] = this.path.split('.')
|
|
||||||
return [firstSegment + 'DefaultValue', ...rest].join('.')
|
|
||||||
},
|
|
||||||
stateUnit () {
|
stateUnit () {
|
||||||
return (this.state || '').replace(/\d+/, '')
|
return this.state.replace(/\d+/, '')
|
||||||
},
|
},
|
||||||
stateValue () {
|
stateValue () {
|
||||||
return (this.state || '').replace(/\D+/, '')
|
return this.state.replace(/\D+/, '')
|
||||||
},
|
|
||||||
state () {
|
|
||||||
const value = get(this.$parent, this.path)
|
|
||||||
if (value === undefined) {
|
|
||||||
return this.defaultState
|
|
||||||
} else {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
},
|
|
||||||
defaultState () {
|
|
||||||
return get(this.$parent, this.pathDefault)
|
|
||||||
},
|
|
||||||
isChanged () {
|
|
||||||
return this.state !== this.defaultState
|
|
||||||
},
|
|
||||||
matchesExpertLevel () {
|
|
||||||
return (this.expert || 0) <= this.$parent.expertLevel
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
update (e) {
|
...Setting.methods,
|
||||||
set(this.$parent, this.path, e)
|
|
||||||
},
|
|
||||||
reset () {
|
|
||||||
set(this.$parent, this.path, this.defaultState)
|
|
||||||
},
|
|
||||||
updateValue (e) {
|
updateValue (e) {
|
||||||
set(this.$parent, this.path, parseInt(e.target.value) + this.stateUnit)
|
this.configSink(this.path, parseInt(e.target.value) + this.stateUnit)
|
||||||
},
|
},
|
||||||
updateUnit (e) {
|
updateUnit (e) {
|
||||||
set(this.$parent, this.path, this.stateValue + e.target.value)
|
this.configSink(this.path, this.stateValue + e.target.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,11 +45,18 @@
|
||||||
<script src="./size_setting.js"></script>
|
<script src="./size_setting.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.css-unit-input,
|
.SizeSetting {
|
||||||
.css-unit-input select {
|
.number-input {
|
||||||
margin-left: 0.5em;
|
max-width: 6.5em;
|
||||||
width: 4em;
|
}
|
||||||
max-width: 4em;
|
|
||||||
min-width: 4em;
|
.css-unit-input,
|
||||||
|
.css-unit-input select {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
width: 4em;
|
||||||
|
max-width: 4em;
|
||||||
|
min-width: 4em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Setting from './setting.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
...Setting
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<label
|
||||||
|
v-if="matchesExpertLevel"
|
||||||
|
class="StringSetting"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
:for="path"
|
||||||
|
:class="{ 'faint': shouldBeDisabled }"
|
||||||
|
>
|
||||||
|
<template v-if="backendDescriptionLabel">
|
||||||
|
{{ backendDescriptionLabel + ' ' }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="source === 'admin'">
|
||||||
|
MISSING LABEL FOR {{ path }}
|
||||||
|
</template>
|
||||||
|
<slot v-else />
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
:id="path"
|
||||||
|
class="string-input"
|
||||||
|
:disabled="shouldBeDisabled"
|
||||||
|
:value="realDraftMode ? draft : state"
|
||||||
|
@change="update"
|
||||||
|
>
|
||||||
|
{{ ' ' }}
|
||||||
|
<ModifiedIndicator
|
||||||
|
:changed="isChanged"
|
||||||
|
:onclick="reset"
|
||||||
|
/>
|
||||||
|
<ProfileSettingIndicator :is-profile="isProfileSetting" />
|
||||||
|
<DraftButtons />
|
||||||
|
<p
|
||||||
|
v-if="backendDescriptionDescription"
|
||||||
|
class="setting-description"
|
||||||
|
:class="{ 'faint': shouldBeDisabled }"
|
||||||
|
>
|
||||||
|
{{ backendDescriptionDescription + ' ' }}
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./string_setting.js"></script>
|
|
@ -5,7 +5,7 @@ import getResettableAsyncComponent from 'src/services/resettable_async_component
|
||||||
import Popover from '../popover/popover.vue'
|
import Popover from '../popover/popover.vue'
|
||||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep, isEqual } from 'lodash'
|
||||||
import {
|
import {
|
||||||
newImporter,
|
newImporter,
|
||||||
newExporter
|
newExporter
|
||||||
|
@ -53,8 +53,16 @@ const SettingsModal = {
|
||||||
Modal,
|
Modal,
|
||||||
Popover,
|
Popover,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
SettingsModalContent: getResettableAsyncComponent(
|
SettingsModalUserContent: getResettableAsyncComponent(
|
||||||
() => import('./settings_modal_content.vue'),
|
() => import('./settings_modal_user_content.vue'),
|
||||||
|
{
|
||||||
|
loadingComponent: PanelLoading,
|
||||||
|
errorComponent: AsyncComponentError,
|
||||||
|
delay: 0
|
||||||
|
}
|
||||||
|
),
|
||||||
|
SettingsModalAdminContent: getResettableAsyncComponent(
|
||||||
|
() => import('./settings_modal_admin_content.vue'),
|
||||||
{
|
{
|
||||||
loadingComponent: PanelLoading,
|
loadingComponent: PanelLoading,
|
||||||
errorComponent: AsyncComponentError,
|
errorComponent: AsyncComponentError,
|
||||||
|
@ -147,6 +155,12 @@ const SettingsModal = {
|
||||||
PLEROMAFE_SETTINGS_MINOR_VERSION
|
PLEROMAFE_SETTINGS_MINOR_VERSION
|
||||||
]
|
]
|
||||||
return clone
|
return clone
|
||||||
|
},
|
||||||
|
resetAdminDraft () {
|
||||||
|
this.$store.commit('resetAdminDraft')
|
||||||
|
},
|
||||||
|
pushAdminDraft () {
|
||||||
|
this.$store.dispatch('pushAdminDraft')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -156,8 +170,14 @@ const SettingsModal = {
|
||||||
modalActivated () {
|
modalActivated () {
|
||||||
return this.$store.state.interface.settingsModalState !== 'hidden'
|
return this.$store.state.interface.settingsModalState !== 'hidden'
|
||||||
},
|
},
|
||||||
modalOpenedOnce () {
|
modalMode () {
|
||||||
return this.$store.state.interface.settingsModalLoaded
|
return this.$store.state.interface.settingsModalMode
|
||||||
|
},
|
||||||
|
modalOpenedOnceUser () {
|
||||||
|
return this.$store.state.interface.settingsModalLoadedUser
|
||||||
|
},
|
||||||
|
modalOpenedOnceAdmin () {
|
||||||
|
return this.$store.state.interface.settingsModalLoadedAdmin
|
||||||
},
|
},
|
||||||
modalPeeked () {
|
modalPeeked () {
|
||||||
return this.$store.state.interface.settingsModalState === 'minimized'
|
return this.$store.state.interface.settingsModalState === 'minimized'
|
||||||
|
@ -167,9 +187,14 @@ const SettingsModal = {
|
||||||
return this.$store.state.config.expertLevel > 0
|
return this.$store.state.config.expertLevel > 0
|
||||||
},
|
},
|
||||||
set (value) {
|
set (value) {
|
||||||
console.log(value)
|
|
||||||
this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 })
|
this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 })
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
adminDraftAny () {
|
||||||
|
return !isEqual(
|
||||||
|
this.$store.state.adminSettings.config,
|
||||||
|
this.$store.state.adminSettings.draft
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setting-description {
|
||||||
|
margin-top: 0.2em;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
font-size: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-modal-panel {
|
.settings-modal-panel {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: transform;
|
transition: transform;
|
||||||
|
@ -37,7 +43,9 @@
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
min-height: 2em;
|
min-height: 2em;
|
||||||
min-width: 10em;
|
}
|
||||||
|
|
||||||
|
.btn:not(.dropdown-button) {
|
||||||
padding: 0 2em;
|
padding: 0 2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,6 +53,8 @@
|
||||||
|
|
||||||
.settings-footer {
|
.settings-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
line-height: 2;
|
||||||
|
|
||||||
>* {
|
>* {
|
||||||
margin-right: 0.5em;
|
margin-right: 0.5em;
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<div class="settings-modal-panel panel">
|
<div class="settings-modal-panel panel">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<span class="title">
|
<span class="title">
|
||||||
{{ $t('settings.settings') }}
|
{{ modalMode === 'user' ? $t('settings.settings') : $t('admin_dash.window_title') }}
|
||||||
</span>
|
</span>
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div
|
<div
|
||||||
|
@ -42,10 +42,12 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<SettingsModalContent v-if="modalOpenedOnce" />
|
<SettingsModalUserContent v-if="modalMode === 'user' && modalOpenedOnceUser" />
|
||||||
|
<SettingsModalAdminContent v-if="modalMode === 'admin' && modalOpenedOnceAdmin" />
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-footer settings-footer">
|
<div class="panel-footer settings-footer -flexible-height">
|
||||||
<Popover
|
<Popover
|
||||||
|
v-if="modalMode === 'user'"
|
||||||
class="export"
|
class="export"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
placement="top"
|
placement="top"
|
||||||
|
@ -107,10 +109,42 @@
|
||||||
>
|
>
|
||||||
{{ $t("settings.expert_mode") }}
|
{{ $t("settings.expert_mode") }}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
<span v-if="modalMode === 'admin'">
|
||||||
|
<i18n-t keypath="admin_dash.wip_notice">
|
||||||
|
<template #adminFeLink>
|
||||||
|
<a
|
||||||
|
href="/pleroma/admin/#/login-pleroma"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ $t("admin_dash.old_ui_link") }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
id="unscrolled-content"
|
id="unscrolled-content"
|
||||||
class="extra-content"
|
class="extra-content"
|
||||||
/>
|
/>
|
||||||
|
<span
|
||||||
|
v-if="modalMode === 'admin'"
|
||||||
|
class="admin-buttons"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="button-default btn"
|
||||||
|
:disabled="!adminDraftAny"
|
||||||
|
@click="resetAdminDraft"
|
||||||
|
>
|
||||||
|
{{ $t("admin_dash.reset_all") }}
|
||||||
|
</button>
|
||||||
|
{{ ' ' }}
|
||||||
|
<button
|
||||||
|
class="button-default btn"
|
||||||
|
:disabled="!adminDraftAny"
|
||||||
|
@click="pushAdminDraft"
|
||||||
|
>
|
||||||
|
{{ $t("admin_dash.commit_all") }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||||
|
|
||||||
|
import InstanceTab from './admin_tabs/instance_tab.vue'
|
||||||
|
import LimitsTab from './admin_tabs/limits_tab.vue'
|
||||||
|
import FrontendsTab from './admin_tabs/frontends_tab.vue'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faWrench,
|
||||||
|
faHand,
|
||||||
|
faLaptopCode,
|
||||||
|
faPaintBrush,
|
||||||
|
faBell,
|
||||||
|
faDownload,
|
||||||
|
faEyeSlash,
|
||||||
|
faInfo
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faWrench,
|
||||||
|
faHand,
|
||||||
|
faLaptopCode,
|
||||||
|
faPaintBrush,
|
||||||
|
faBell,
|
||||||
|
faDownload,
|
||||||
|
faEyeSlash,
|
||||||
|
faInfo
|
||||||
|
)
|
||||||
|
|
||||||
|
const SettingsModalAdminContent = {
|
||||||
|
components: {
|
||||||
|
TabSwitcher,
|
||||||
|
|
||||||
|
InstanceTab,
|
||||||
|
LimitsTab,
|
||||||
|
FrontendsTab
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
user () {
|
||||||
|
return this.$store.state.users.currentUser
|
||||||
|
},
|
||||||
|
isLoggedIn () {
|
||||||
|
return !!this.$store.state.users.currentUser
|
||||||
|
},
|
||||||
|
open () {
|
||||||
|
return this.$store.state.interface.settingsModalState !== 'hidden'
|
||||||
|
},
|
||||||
|
bodyLock () {
|
||||||
|
return this.$store.state.interface.settingsModalState === 'visible'
|
||||||
|
},
|
||||||
|
adminDbLoaded () {
|
||||||
|
return this.$store.state.adminSettings.loaded
|
||||||
|
},
|
||||||
|
adminDescriptionsLoaded () {
|
||||||
|
return this.$store.state.adminSettings.descriptions !== null
|
||||||
|
},
|
||||||
|
noDb () {
|
||||||
|
return this.$store.state.adminSettings.dbConfigEnabled === false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
if (this.user.rights.admin) {
|
||||||
|
this.$store.dispatch('loadAdminStuff')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onOpen () {
|
||||||
|
const targetTab = this.$store.state.interface.settingsModalTargetTab
|
||||||
|
// We're being told to open in specific tab
|
||||||
|
if (targetTab) {
|
||||||
|
const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => {
|
||||||
|
return elm.props && elm.props['data-tab-name'] === targetTab
|
||||||
|
})
|
||||||
|
if (tabIndex >= 0) {
|
||||||
|
this.$refs.tabSwitcher.setTab(tabIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear the state of target tab, so that next time settings is opened
|
||||||
|
// it doesn't force it.
|
||||||
|
this.$store.dispatch('clearSettingsModalTargetTab')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.onOpen()
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
open: function (value) {
|
||||||
|
if (value) this.onOpen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsModalAdminContent
|
|
@ -48,9 +48,5 @@
|
||||||
color: var(--cRed, $fallback--cRed);
|
color: var(--cRed, $fallback--cRed);
|
||||||
color: $fallback--cRed;
|
color: $fallback--cRed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.number-input {
|
|
||||||
max-width: 6em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
<template>
|
||||||
|
<tab-switcher
|
||||||
|
v-if="adminDescriptionsLoaded && (noDb || adminDbLoaded)"
|
||||||
|
ref="tabSwitcher"
|
||||||
|
class="settings_tab-switcher"
|
||||||
|
:side-tab-bar="true"
|
||||||
|
:scrollable-tabs="true"
|
||||||
|
:render-only-focused="true"
|
||||||
|
:body-scroll-lock="bodyLock"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="noDb"
|
||||||
|
:label="$t('admin_dash.tabs.nodb')"
|
||||||
|
icon="exclamation-triangle"
|
||||||
|
data-tab-name="nodb-notice"
|
||||||
|
>
|
||||||
|
<div :label="$t('admin_dash.tabs.nodb')">
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('admin_dash.nodb.heading') }}</h2>
|
||||||
|
<i18n-t keypath="admin_dash.nodb.text">
|
||||||
|
<template #documentation>
|
||||||
|
<a
|
||||||
|
href="https://docs-develop.pleroma.social/backend/configuration/howto_database_config/"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ $t("admin_dash.nodb.documentation") }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<template #property>
|
||||||
|
<code>config :pleroma, configurable_from_database</code>
|
||||||
|
</template>
|
||||||
|
<template #value>
|
||||||
|
<code>true</code>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
<p>{{ $t('admin_dash.nodb.text2') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="adminDbLoaded"
|
||||||
|
:label="$t('admin_dash.tabs.instance')"
|
||||||
|
icon="wrench"
|
||||||
|
data-tab-name="general"
|
||||||
|
>
|
||||||
|
<InstanceTab />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="adminDbLoaded"
|
||||||
|
:label="$t('admin_dash.tabs.limits')"
|
||||||
|
icon="hand"
|
||||||
|
data-tab-name="limits"
|
||||||
|
>
|
||||||
|
<LimitsTab />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:label="$t('admin_dash.tabs.frontends')"
|
||||||
|
icon="laptop-code"
|
||||||
|
data-tab-name="frontends"
|
||||||
|
>
|
||||||
|
<FrontendsTab />
|
||||||
|
</div>
|
||||||
|
</tab-switcher>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./settings_modal_admin_content.js"></script>
|
||||||
|
|
||||||
|
<style src="./settings_modal_admin_content.scss" lang="scss"></style>
|
|
@ -0,0 +1,52 @@
|
||||||
|
@import "src/variables";
|
||||||
|
|
||||||
|
.settings_tab-switcher {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
border-bottom: 2px solid var(--fg, $fallback--fg);
|
||||||
|
margin: 1em 1em 1.4em;
|
||||||
|
padding-bottom: 1.4em;
|
||||||
|
|
||||||
|
> div,
|
||||||
|
> label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-multiple {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.option-list {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
min-width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unavailable,
|
||||||
|
.unavailable svg {
|
||||||
|
color: var(--cRed, $fallback--cRed);
|
||||||
|
color: $fallback--cRed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,6 +78,6 @@
|
||||||
</tab-switcher>
|
</tab-switcher>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./settings_modal_content.js"></script>
|
<script src="./settings_modal_user_content.js"></script>
|
||||||
|
|
||||||
<style src="./settings_modal_content.scss" lang="scss"></style>
|
<style src="./settings_modal_user_content.scss" lang="scss"></style>
|
|
@ -7,13 +7,11 @@
|
||||||
<BooleanSetting path="hideFilteredStatuses">
|
<BooleanSetting path="hideFilteredStatuses">
|
||||||
{{ $t('settings.hide_filtered_statuses') }}
|
{{ $t('settings.hide_filtered_statuses') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
<ul
|
<ul class="setting-list suboptions">
|
||||||
class="setting-list suboptions"
|
|
||||||
:class="[{disabled: !streaming}]"
|
|
||||||
>
|
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
:disabled="hideFilteredStatuses"
|
parent-path="hideFilteredStatuses"
|
||||||
|
:parent-invert="true"
|
||||||
path="hideWordFilteredPosts"
|
path="hideWordFilteredPosts"
|
||||||
>
|
>
|
||||||
{{ $t('settings.hide_wordfiltered_statuses') }}
|
{{ $t('settings.hide_wordfiltered_statuses') }}
|
||||||
|
@ -22,7 +20,8 @@
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
v-if="user"
|
v-if="user"
|
||||||
:disabled="hideFilteredStatuses"
|
parent-path="hideFilteredStatuses"
|
||||||
|
:parent-invert="true"
|
||||||
path="hideMutedThreads"
|
path="hideMutedThreads"
|
||||||
>
|
>
|
||||||
{{ $t('settings.hide_muted_threads') }}
|
{{ $t('settings.hide_muted_threads') }}
|
||||||
|
@ -31,7 +30,8 @@
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
v-if="user"
|
v-if="user"
|
||||||
:disabled="hideFilteredStatuses"
|
parent-path="hideFilteredStatuses"
|
||||||
|
:parent-invert="true"
|
||||||
path="hideMutedPosts"
|
path="hideMutedPosts"
|
||||||
>
|
>
|
||||||
{{ $t('settings.hide_muted_posts') }}
|
{{ $t('settings.hide_muted_posts') }}
|
||||||
|
|
|
@ -2,11 +2,12 @@ import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||||
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
||||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||||
|
import FloatSetting from '../helpers/float_setting.vue'
|
||||||
import SizeSetting, { defaultHorizontalUnits } from '../helpers/size_setting.vue'
|
import SizeSetting, { defaultHorizontalUnits } from '../helpers/size_setting.vue'
|
||||||
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
||||||
|
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
import ServerSideIndicator from '../helpers/server_side_indicator.vue'
|
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faGlobe
|
faGlobe
|
||||||
|
@ -62,10 +63,11 @@ const GeneralTab = {
|
||||||
BooleanSetting,
|
BooleanSetting,
|
||||||
ChoiceSetting,
|
ChoiceSetting,
|
||||||
IntegerSetting,
|
IntegerSetting,
|
||||||
|
FloatSetting,
|
||||||
SizeSetting,
|
SizeSetting,
|
||||||
InterfaceLanguageSwitcher,
|
InterfaceLanguageSwitcher,
|
||||||
ScopeSelector,
|
ScopeSelector,
|
||||||
ServerSideIndicator
|
ProfileSettingIndicator
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
horizontalUnits () {
|
horizontalUnits () {
|
||||||
|
@ -108,7 +110,7 @@ const GeneralTab = {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
changeDefaultScope (value) {
|
changeDefaultScope (value) {
|
||||||
this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value })
|
this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,14 +29,11 @@
|
||||||
<BooleanSetting path="streaming">
|
<BooleanSetting path="streaming">
|
||||||
{{ $t('settings.streaming') }}
|
{{ $t('settings.streaming') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
<ul
|
<ul class="setting-list suboptions">
|
||||||
class="setting-list suboptions"
|
|
||||||
:class="[{disabled: !streaming}]"
|
|
||||||
>
|
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="pauseOnUnfocused"
|
path="pauseOnUnfocused"
|
||||||
:disabled="!streaming"
|
parent-path="streaming"
|
||||||
>
|
>
|
||||||
{{ $t('settings.pause_on_unfocused') }}
|
{{ $t('settings.pause_on_unfocused') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
|
@ -213,7 +210,7 @@
|
||||||
</ChoiceSetting>
|
</ChoiceSetting>
|
||||||
</li>
|
</li>
|
||||||
<ul
|
<ul
|
||||||
v-if="conversationDisplay !== 'linear'"
|
v-if="mergedConfig.conversationDisplay !== 'linear'"
|
||||||
class="setting-list suboptions"
|
class="setting-list suboptions"
|
||||||
>
|
>
|
||||||
<li>
|
<li>
|
||||||
|
@ -265,12 +262,22 @@
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
v-if="user"
|
v-if="user"
|
||||||
path="serverSide_stripRichContent"
|
source="profile"
|
||||||
|
path="stripRichContent"
|
||||||
expert="1"
|
expert="1"
|
||||||
>
|
>
|
||||||
{{ $t('settings.no_rich_text_description') }}
|
{{ $t('settings.no_rich_text_description') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<FloatSetting
|
||||||
|
v-if="user"
|
||||||
|
path="emojiReactionsScale"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
|
{{ $t('settings.emoji_reactions_scale') }}
|
||||||
|
</FloatSetting>
|
||||||
|
</li>
|
||||||
<h3>{{ $t('settings.attachments') }}</h3>
|
<h3>{{ $t('settings.attachments') }}</h3>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
|
@ -290,7 +297,7 @@
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="preloadImage"
|
path="preloadImage"
|
||||||
expert="1"
|
expert="1"
|
||||||
:disabled="!hideNsfw"
|
parent-path="hideNsfw"
|
||||||
>
|
>
|
||||||
{{ $t('settings.preload_images') }}
|
{{ $t('settings.preload_images') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
|
@ -299,7 +306,7 @@
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="useOneClickNsfw"
|
path="useOneClickNsfw"
|
||||||
expert="1"
|
expert="1"
|
||||||
:disabled="!hideNsfw"
|
parent-path="hideNsfw"
|
||||||
>
|
>
|
||||||
{{ $t('settings.use_one_click_nsfw') }}
|
{{ $t('settings.use_one_click_nsfw') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
|
@ -312,15 +319,13 @@
|
||||||
>
|
>
|
||||||
{{ $t('settings.loop_video') }}
|
{{ $t('settings.loop_video') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
<ul
|
<ul class="setting-list suboptions">
|
||||||
class="setting-list suboptions"
|
|
||||||
:class="[{disabled: !streaming}]"
|
|
||||||
>
|
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="loopVideoSilentOnly"
|
path="loopVideoSilentOnly"
|
||||||
expert="1"
|
expert="1"
|
||||||
:disabled="!loopVideo || !loopSilentAvailable"
|
parent-path="loopVideo"
|
||||||
|
:disabled="!loopSilentAvailable"
|
||||||
>
|
>
|
||||||
{{ $t('settings.loop_video_silent_only') }}
|
{{ $t('settings.loop_video_silent_only') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
|
@ -418,18 +423,18 @@
|
||||||
<ul class="setting-list">
|
<ul class="setting-list">
|
||||||
<li>
|
<li>
|
||||||
<label for="default-vis">
|
<label for="default-vis">
|
||||||
{{ $t('settings.default_vis') }} <ServerSideIndicator :server-side="true" />
|
{{ $t('settings.default_vis') }} <ProfileSettingIndicator :is-profile="true" />
|
||||||
<ScopeSelector
|
<ScopeSelector
|
||||||
class="scope-selector"
|
class="scope-selector"
|
||||||
:show-all="true"
|
:show-all="true"
|
||||||
:user-default="serverSide_defaultScope"
|
:user-default="$store.state.profileConfig.defaultScope"
|
||||||
:initial-scope="serverSide_defaultScope"
|
:initial-scope="$store.state.profileConfig.defaultScope"
|
||||||
:on-scope-change="changeDefaultScope"
|
:on-scope-change="changeDefaultScope"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<!-- <BooleanSetting path="serverSide_defaultNSFW"> -->
|
<!-- <BooleanSetting source="profile" path="defaultNSFW"> -->
|
||||||
<BooleanSetting path="sensitiveByDefault">
|
<BooleanSetting path="sensitiveByDefault">
|
||||||
{{ $t('settings.sensitive_by_default') }}
|
{{ $t('settings.sensitive_by_default') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
|
@ -501,6 +506,14 @@
|
||||||
{{ $t('settings.pad_emoji') }}
|
{{ $t('settings.pad_emoji') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path="autocompleteSelect"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
|
{{ $t('settings.autocomplete_select_first') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,17 +9,20 @@ import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue
|
||||||
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
|
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
|
||||||
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
import ProgressButton from 'src/components/progress_button/progress_button.vue'
|
||||||
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
|
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
|
||||||
|
import withLoadMore from 'src/components/../hocs/with_load_more/with_load_more'
|
||||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
|
||||||
const BlockList = withSubscription({
|
const BlockList = withLoadMore({
|
||||||
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
||||||
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
|
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
|
||||||
|
destroy: () => {},
|
||||||
childPropName: 'items'
|
childPropName: 'items'
|
||||||
})(SelectableList)
|
})(SelectableList)
|
||||||
|
|
||||||
const MuteList = withSubscription({
|
const MuteList = withLoadMore({
|
||||||
fetch: (props, $store) => $store.dispatch('fetchMutes'),
|
fetch: (props, $store) => $store.dispatch('fetchMutes'),
|
||||||
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
|
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
|
||||||
|
destroy: () => {},
|
||||||
childPropName: 'items'
|
childPropName: 'items'
|
||||||
})(SelectableList)
|
})(SelectableList)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,10 @@
|
||||||
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
|
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
|
||||||
<ul class="setting-list">
|
<ul class="setting-list">
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="serverSide_blockNotificationsFromStrangers">
|
<BooleanSetting
|
||||||
|
source="profile"
|
||||||
|
path="blockNotificationsFromStrangers"
|
||||||
|
>
|
||||||
{{ $t('settings.notification_setting_block_from_strangers') }}
|
{{ $t('settings.notification_setting_block_from_strangers') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
|
@ -67,7 +70,8 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="serverSide_webPushHideContents"
|
source="profile"
|
||||||
|
path="webPushHideContents"
|
||||||
expert="1"
|
expert="1"
|
||||||
>
|
>
|
||||||
{{ $t('settings.notification_setting_hide_notification_contents') }}
|
{{ $t('settings.notification_setting_hide_notification_contents') }}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import InterfaceLanguageSwitcher from 'src/components/interface_language_switche
|
||||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
import localeService from 'src/services/locale/locale.service.js'
|
import localeService from 'src/services/locale/locale.service.js'
|
||||||
|
import { propsToNative } from 'src/services/attributes_helper/attributes_helper.service.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
|
@ -261,6 +262,9 @@ const ProfileTab = {
|
||||||
messageArgs: [error.message],
|
messageArgs: [error.message],
|
||||||
level: 'error'
|
level: 'error'
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
propsToNative (props) {
|
||||||
|
return propsToNative(props)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,14 @@
|
||||||
enable-emoji-picker
|
enable-emoji-picker
|
||||||
:suggest="emojiSuggestor"
|
:suggest="emojiSuggestor"
|
||||||
>
|
>
|
||||||
<input
|
<template #default="inputProps">
|
||||||
id="username"
|
<input
|
||||||
v-model="newName"
|
id="username"
|
||||||
class="name-changer"
|
v-model="newName"
|
||||||
>
|
class="name-changer"
|
||||||
|
v-bind="propsToNative(inputProps)"
|
||||||
|
>
|
||||||
|
</template>
|
||||||
</EmojiInput>
|
</EmojiInput>
|
||||||
<p>{{ $t('settings.bio') }}</p>
|
<p>{{ $t('settings.bio') }}</p>
|
||||||
<EmojiInput
|
<EmojiInput
|
||||||
|
@ -20,10 +23,13 @@
|
||||||
enable-emoji-picker
|
enable-emoji-picker
|
||||||
:suggest="emojiUserSuggestor"
|
:suggest="emojiUserSuggestor"
|
||||||
>
|
>
|
||||||
<textarea
|
<template #default="inputProps">
|
||||||
v-model="newBio"
|
<textarea
|
||||||
class="bio resize-height"
|
v-model="newBio"
|
||||||
/>
|
class="bio resize-height"
|
||||||
|
v-bind="propsToNative(inputProps)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</EmojiInput>
|
</EmojiInput>
|
||||||
<p v-if="role === 'admin' || role === 'moderator'">
|
<p v-if="role === 'admin' || role === 'moderator'">
|
||||||
<Checkbox v-model="showRole">
|
<Checkbox v-model="showRole">
|
||||||
|
@ -60,10 +66,13 @@
|
||||||
hide-emoji-button
|
hide-emoji-button
|
||||||
:suggest="userSuggestor"
|
:suggest="userSuggestor"
|
||||||
>
|
>
|
||||||
<input
|
<template #default="inputProps">
|
||||||
v-model="newFields[i].name"
|
<input
|
||||||
:placeholder="$t('settings.profile_fields.name')"
|
v-model="newFields[i].name"
|
||||||
>
|
:placeholder="$t('settings.profile_fields.name')"
|
||||||
|
v-bind="propsToNative(inputProps)"
|
||||||
|
>
|
||||||
|
</template>
|
||||||
</EmojiInput>
|
</EmojiInput>
|
||||||
<EmojiInput
|
<EmojiInput
|
||||||
v-model="newFields[i].value"
|
v-model="newFields[i].value"
|
||||||
|
@ -71,10 +80,13 @@
|
||||||
hide-emoji-button
|
hide-emoji-button
|
||||||
:suggest="userSuggestor"
|
:suggest="userSuggestor"
|
||||||
>
|
>
|
||||||
<input
|
<template #default="inputProps">
|
||||||
v-model="newFields[i].value"
|
<input
|
||||||
:placeholder="$t('settings.profile_fields.value')"
|
v-model="newFields[i].value"
|
||||||
>
|
:placeholder="$t('settings.profile_fields.value')"
|
||||||
|
v-bind="propsToNative(inputProps)"
|
||||||
|
>
|
||||||
|
</template>
|
||||||
</EmojiInput>
|
</EmojiInput>
|
||||||
<button
|
<button
|
||||||
class="delete-field button-unstyled -hover-highlight"
|
class="delete-field button-unstyled -hover-highlight"
|
||||||
|
@ -242,37 +254,50 @@
|
||||||
<h2>{{ $t('settings.account_privacy') }}</h2>
|
<h2>{{ $t('settings.account_privacy') }}</h2>
|
||||||
<ul class="setting-list">
|
<ul class="setting-list">
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="serverSide_locked">
|
<BooleanSetting
|
||||||
|
source="profile"
|
||||||
|
path="locked"
|
||||||
|
>
|
||||||
{{ $t('settings.lock_account_description') }}
|
{{ $t('settings.lock_account_description') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="serverSide_discoverable">
|
<BooleanSetting
|
||||||
|
source="profile"
|
||||||
|
path="discoverable"
|
||||||
|
>
|
||||||
{{ $t('settings.discoverable') }}
|
{{ $t('settings.discoverable') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="serverSide_allowFollowingMove">
|
<BooleanSetting
|
||||||
|
source="profile"
|
||||||
|
path="allowFollowingMove"
|
||||||
|
>
|
||||||
{{ $t('settings.allow_following_move') }}
|
{{ $t('settings.allow_following_move') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="serverSide_hideFavorites">
|
<BooleanSetting
|
||||||
|
source="profile"
|
||||||
|
path="hideFavorites"
|
||||||
|
>
|
||||||
{{ $t('settings.hide_favorites_description') }}
|
{{ $t('settings.hide_favorites_description') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="serverSide_hideFollowers">
|
<BooleanSetting
|
||||||
|
source="profile"
|
||||||
|
path="hideFollowers"
|
||||||
|
>
|
||||||
{{ $t('settings.hide_followers_description') }}
|
{{ $t('settings.hide_followers_description') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
<ul
|
<ul class="setting-list suboptions">
|
||||||
class="setting-list suboptions"
|
|
||||||
:class="[{disabled: !serverSide_hideFollowers}]"
|
|
||||||
>
|
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="serverSide_hideFollowersCount"
|
source="profile"
|
||||||
:disabled="!serverSide_hideFollowers"
|
path="hideFollowersCount"
|
||||||
|
parent-path="hideFollowers"
|
||||||
>
|
>
|
||||||
{{ $t('settings.hide_followers_count_description') }}
|
{{ $t('settings.hide_followers_count_description') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
|
@ -280,17 +305,18 @@
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="serverSide_hideFollows">
|
<BooleanSetting
|
||||||
|
source="profile"
|
||||||
|
path="hideFollows"
|
||||||
|
>
|
||||||
{{ $t('settings.hide_follows_description') }}
|
{{ $t('settings.hide_follows_description') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
<ul
|
<ul class="setting-list suboptions">
|
||||||
class="setting-list suboptions"
|
|
||||||
:class="[{disabled: !serverSide_hideFollows}]"
|
|
||||||
>
|
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="serverSide_hideFollowsCount"
|
source="profile"
|
||||||
:disabled="!serverSide_hideFollows"
|
path="hideFollowsCount"
|
||||||
|
parent-path="hideFollows"
|
||||||
>
|
>
|
||||||
{{ $t('settings.hide_follows_count_description') }}
|
{{ $t('settings.hide_follows_count_description') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
|
|
|
@ -143,8 +143,8 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<i18n
|
<i18n-t
|
||||||
path="settings.new_alias_target"
|
keypath="settings.new_alias_target"
|
||||||
tag="p"
|
tag="p"
|
||||||
>
|
>
|
||||||
<code
|
<code
|
||||||
|
@ -152,7 +152,7 @@
|
||||||
>
|
>
|
||||||
foo@example.org
|
foo@example.org
|
||||||
</code>
|
</code>
|
||||||
</i18n>
|
</i18n-t>
|
||||||
<input
|
<input
|
||||||
v-model="addAliasTarget"
|
v-model="addAliasTarget"
|
||||||
>
|
>
|
||||||
|
@ -175,16 +175,16 @@
|
||||||
<h2>{{ $t('settings.move_account') }}</h2>
|
<h2>{{ $t('settings.move_account') }}</h2>
|
||||||
<p>{{ $t('settings.move_account_notes') }}</p>
|
<p>{{ $t('settings.move_account_notes') }}</p>
|
||||||
<div>
|
<div>
|
||||||
<i18n
|
<i18n-t
|
||||||
path="settings.move_account_target"
|
keypath="settings.move_account_target"
|
||||||
tag="p"
|
tag="p"
|
||||||
>
|
>
|
||||||
<code
|
<template #example>
|
||||||
place="example"
|
<code>
|
||||||
>
|
foo@example.org
|
||||||
foo@example.org
|
</code>
|
||||||
</code>
|
</template>
|
||||||
</i18n>
|
</i18n-t>
|
||||||
<input
|
<input
|
||||||
v-model="moveAccountTarget"
|
v-model="moveAccountTarget"
|
||||||
>
|
>
|
||||||
|
|
|
@ -129,12 +129,13 @@
|
||||||
v-model="selected.inset"
|
v-model="selected.inset"
|
||||||
:disabled="!present"
|
:disabled="!present"
|
||||||
name="inset"
|
name="inset"
|
||||||
class="input-inset"
|
class="input-inset visible-for-screenreader-only"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
class="checkbox-label"
|
class="checkbox-label"
|
||||||
for="inset"
|
for="inset"
|
||||||
|
:aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -115,7 +115,10 @@ const SideDrawer = {
|
||||||
GestureService.updateSwipe(e, this.closeGesture)
|
GestureService.updateSwipe(e, this.closeGesture)
|
||||||
},
|
},
|
||||||
openSettingsModal () {
|
openSettingsModal () {
|
||||||
this.$store.dispatch('openSettingsModal')
|
this.$store.dispatch('openSettingsModal', 'user')
|
||||||
|
},
|
||||||
|
openAdminModal () {
|
||||||
|
this.$store.dispatch('openSettingsModal', 'admin')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,16 +180,16 @@
|
||||||
v-if="currentUser && currentUser.role === 'admin'"
|
v-if="currentUser && currentUser.role === 'admin'"
|
||||||
@click="toggleDrawer"
|
@click="toggleDrawer"
|
||||||
>
|
>
|
||||||
<a
|
<button
|
||||||
href="/pleroma/admin/#/login-pleroma"
|
class="button-unstyled -link -fullwidth"
|
||||||
target="_blank"
|
@click.stop="openAdminModal"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
fixed-width
|
fixed-width
|
||||||
class="fa-scale-110 fa-old-padding"
|
class="fa-scale-110 fa-old-padding"
|
||||||
icon="tachometer-alt"
|
icon="tachometer-alt"
|
||||||
/> {{ $t("nav.administration") }}
|
/> {{ $t("nav.administration") }}
|
||||||
</a>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="currentUser && supportsAnnouncements"
|
v-if="currentUser && supportsAnnouncements"
|
||||||
|
|
|
@ -60,13 +60,7 @@ export default {
|
||||||
const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName
|
const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName
|
||||||
return this.$slots.default().findIndex(isWanted) === this.activeIndex
|
return this.$slots.default().findIndex(isWanted) === this.activeIndex
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
settingsModalVisible () {
|
|
||||||
return this.settingsModalState === 'visible'
|
|
||||||
},
|
|
||||||
...mapState({
|
|
||||||
settingsModalState: state => state.interface.settingsModalState
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
beforeUpdate () {
|
beforeUpdate () {
|
||||||
const currentSlot = this.slots()[this.active]
|
const currentSlot = this.slots()[this.active]
|
||||||
|
@ -117,6 +111,7 @@ export default {
|
||||||
onClick={this.clickTab(index)}
|
onClick={this.clickTab(index)}
|
||||||
class={classesTab.join(' ')}
|
class={classesTab.join(' ')}
|
||||||
type="button"
|
type="button"
|
||||||
|
role="tab"
|
||||||
>
|
>
|
||||||
<img src={props.image} title={props['image-tooltip']}/>
|
<img src={props.image} title={props['image-tooltip']}/>
|
||||||
{props.label ? '' : props.label}
|
{props.label ? '' : props.label}
|
||||||
|
@ -131,6 +126,7 @@ export default {
|
||||||
onClick={this.clickTab(index)}
|
onClick={this.clickTab(index)}
|
||||||
class={classesTab.join(' ')}
|
class={classesTab.join(' ')}
|
||||||
type="button"
|
type="button"
|
||||||
|
role="tab"
|
||||||
>
|
>
|
||||||
{!props.icon ? '' : (<FAIcon class="tab-icon" size="2x" fixed-width icon={props.icon}/>)}
|
{!props.icon ? '' : (<FAIcon class="tab-icon" size="2x" fixed-width icon={props.icon}/>)}
|
||||||
<span class="text">
|
<span class="text">
|
||||||
|
@ -167,11 +163,15 @@ export default {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}>
|
<div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}>
|
||||||
<div class="tabs">
|
<div
|
||||||
|
class="tabs"
|
||||||
|
role="tablist"
|
||||||
|
>
|
||||||
{tabs}
|
{tabs}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
ref="contents"
|
ref="contents"
|
||||||
|
role="tabpanel"
|
||||||
class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
|
class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
|
||||||
v-body-scroll-lock={this.bodyScrollLock}
|
v-body-scroll-lock={this.bodyScrollLock}
|
||||||
>
|
>
|
||||||
|
|
|
@ -98,7 +98,7 @@ const withLoadMore = ({
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
{!this.error && this.loading && <FAIcon spin icon="circle-notch"/>}
|
{!this.error && this.loading && <FAIcon spin icon="circle-notch"/>}
|
||||||
{!this.error && !this.loading && !this.bottomedOut && <a onClick={this.fetchEntries}>{this.$t('general.more')}</a>}
|
{!this.error && !this.loading && !this.bottomedOut && <a onClick={this.fetchEntries} role="button" tabindex="0">{this.$t('general.more')}</a>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
380
src/i18n/ar.json
380
src/i18n/ar.json
|
@ -9,7 +9,8 @@
|
||||||
"scope_options": "",
|
"scope_options": "",
|
||||||
"text_limit": "الحد الأقصى للنص",
|
"text_limit": "الحد الأقصى للنص",
|
||||||
"title": "الميّزات",
|
"title": "الميّزات",
|
||||||
"who_to_follow": "للمتابعة"
|
"who_to_follow": "للمتابعة",
|
||||||
|
"upload_limit": "حد الرفع"
|
||||||
},
|
},
|
||||||
"finder": {
|
"finder": {
|
||||||
"error_fetching_user": "خطأ أثناء جلب صفحة المستخدم",
|
"error_fetching_user": "خطأ أثناء جلب صفحة المستخدم",
|
||||||
|
@ -17,7 +18,35 @@
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"apply": "تطبيق",
|
"apply": "تطبيق",
|
||||||
"submit": "إرسال"
|
"submit": "إرسال",
|
||||||
|
"error_retry": "حاول مجددًا",
|
||||||
|
"retry": "حاول مجدداً",
|
||||||
|
"optional": "اختياري",
|
||||||
|
"show_more": "اعرض المزيد",
|
||||||
|
"show_less": "اعرض أقل",
|
||||||
|
"cancel": "ألغ",
|
||||||
|
"disable": "عطّل",
|
||||||
|
"enable": "فعّل",
|
||||||
|
"confirm": "تأكيد",
|
||||||
|
"close": "أغلق",
|
||||||
|
"role": {
|
||||||
|
"admin": "مدير",
|
||||||
|
"moderator": "مشرف"
|
||||||
|
},
|
||||||
|
"generic_error_message": "حدث خطأ: {0}",
|
||||||
|
"never_show_again": "لا تظهره مجددًا",
|
||||||
|
"yes": "نعم",
|
||||||
|
"no": "لا",
|
||||||
|
"unpin": "ألغ تثبيت العنصر",
|
||||||
|
"undo": "تراجع",
|
||||||
|
"more": "المزيد",
|
||||||
|
"loading": "يحمل…",
|
||||||
|
"generic_error": "حدث خطأ",
|
||||||
|
"scope_in_timeline": {
|
||||||
|
"private": "المتابِعون فقط"
|
||||||
|
},
|
||||||
|
"scroll_to_top": "مرر لأعلى",
|
||||||
|
"pin": "ثبت العنصر"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"login": "تسجيل الدخول",
|
"login": "تسجيل الدخول",
|
||||||
|
@ -25,7 +54,19 @@
|
||||||
"password": "الكلمة السرية",
|
"password": "الكلمة السرية",
|
||||||
"placeholder": "مثال lain",
|
"placeholder": "مثال lain",
|
||||||
"register": "انشاء حساب",
|
"register": "انشاء حساب",
|
||||||
"username": "إسم المستخدم"
|
"username": "إسم المستخدم",
|
||||||
|
"logout_confirm_title": "تأكيد الخروج",
|
||||||
|
"logout_confirm": "أتريد الخروج؟",
|
||||||
|
"logout_confirm_accept_button": "خروج",
|
||||||
|
"logout_confirm_cancel_button": "لا تخرج",
|
||||||
|
"hint": "لِج للانضمام للمناقشة",
|
||||||
|
"authentication_code": "رمز الاستيثاق",
|
||||||
|
"enter_recovery_code": "أدخل رمز التأكيد",
|
||||||
|
"enter_two_factor_code": "أدخل رمز الاستيثاق بعاملين",
|
||||||
|
"recovery_code": "رمز الاستعادة",
|
||||||
|
"heading": {
|
||||||
|
"totp": "الاستيثاق بعاملين"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"chat": "الدردشة المحلية",
|
"chat": "الدردشة المحلية",
|
||||||
|
@ -33,23 +74,48 @@
|
||||||
"mentions": "الإشارات",
|
"mentions": "الإشارات",
|
||||||
"public_tl": "الخيط الزمني العام",
|
"public_tl": "الخيط الزمني العام",
|
||||||
"timeline": "الخيط الزمني",
|
"timeline": "الخيط الزمني",
|
||||||
"twkn": "كافة الشبكة المعروفة"
|
"twkn": "كافة الشبكة المعروفة",
|
||||||
|
"search_close": "أغلق شربط البحث",
|
||||||
|
"back": "للخلف",
|
||||||
|
"administration": "الإدارة",
|
||||||
|
"preferences": "التفضيلات",
|
||||||
|
"chats": "المحادثات",
|
||||||
|
"lists": "القوائم",
|
||||||
|
"edit_nav_mobile": "خصص شريط التنقل",
|
||||||
|
"edit_pinned": "حرر العناصر المثبتة",
|
||||||
|
"mobile_notifications_close": "أغلق الاشعارات",
|
||||||
|
"announcements": "إعلانات",
|
||||||
|
"home_timeline": "الخط الزمني الرئيس",
|
||||||
|
"search": "بحث",
|
||||||
|
"who_to_follow": "للمتابعة",
|
||||||
|
"dms": "رسالة شخصية",
|
||||||
|
"edit_finish": "تم التحرير",
|
||||||
|
"timelines": "الخيوط الزمنية",
|
||||||
|
"mobile_notifications": "افتح الإشعارات (تتواجد اشعارات غير مقروءة)"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"broken_favorite": "منشور مجهول، جارٍ البحث عنه…",
|
"broken_favorite": "منشور مجهول، جارٍ البحث عنه…",
|
||||||
"favorited_you": "أعجِب بمنشورك",
|
"favorited_you": "أعجِب بمنشورك",
|
||||||
"followed_you": "يُتابعك",
|
"followed_you": "يُتابعك",
|
||||||
"load_older": "تحميل الإشعارات الأقدم",
|
"load_older": "تحميل الإشعارات الأقدم",
|
||||||
"notifications": "الإخطارات",
|
"notifications": "الاشعارات",
|
||||||
"read": "مقروء!",
|
"read": "مقروء!",
|
||||||
"repeated_you": "شارَك منشورك"
|
"repeated_you": "شارَك منشورك",
|
||||||
|
"error": "خطأ أثناء جلب الاشعارات: {0}",
|
||||||
|
"follow_request": "يريد متابعتك",
|
||||||
|
"poll_ended": "انتهى الاستطلاع",
|
||||||
|
"no_more_notifications": "لا مزيد من الإشعارات",
|
||||||
|
"reacted_with": "تفاعل بـ{0}",
|
||||||
|
"submitted_report": "أرسل بلاغًا"
|
||||||
},
|
},
|
||||||
"post_status": {
|
"post_status": {
|
||||||
"account_not_locked_warning": "",
|
"account_not_locked_warning": "",
|
||||||
"account_not_locked_warning_link": "مقفل",
|
"account_not_locked_warning_link": "مقفل",
|
||||||
"attachments_sensitive": "اعتبر المرفقات كلها كمحتوى حساس",
|
"attachments_sensitive": "اعتبر المرفقات كلها كمحتوى حساس",
|
||||||
"content_type": {
|
"content_type": {
|
||||||
"text/plain": "نص صافٍ"
|
"text/plain": "نص صِرف",
|
||||||
|
"text/html": "HTML",
|
||||||
|
"text/markdown": "ماركداون"
|
||||||
},
|
},
|
||||||
"content_warning": "الموضوع (اختياري)",
|
"content_warning": "الموضوع (اختياري)",
|
||||||
"default": "وصلت للتوّ إلى لوس أنجلس.",
|
"default": "وصلت للتوّ إلى لوس أنجلس.",
|
||||||
|
@ -60,15 +126,47 @@
|
||||||
"private": "",
|
"private": "",
|
||||||
"public": "علني - يُنشر على الخيوط الزمنية العمومية",
|
"public": "علني - يُنشر على الخيوط الزمنية العمومية",
|
||||||
"unlisted": "غير مُدرَج - لا يُنشَر على الخيوط الزمنية العمومية"
|
"unlisted": "غير مُدرَج - لا يُنشَر على الخيوط الزمنية العمومية"
|
||||||
}
|
},
|
||||||
|
"media_description": "وصف الوسائط",
|
||||||
|
"direct_warning_to_all": "سيكون عذا المنشور مرئيًا لكل المستخدمين المذكورين.",
|
||||||
|
"post": "انشر",
|
||||||
|
"preview": "معاينة",
|
||||||
|
"preview_empty": "فارغ",
|
||||||
|
"scope_notice": {
|
||||||
|
"public": "سيكون هذا المنشور مرئيًا للجميع",
|
||||||
|
"private": "سيكون هذا المنشور مرئيا لمتابِعيك فقط"
|
||||||
|
},
|
||||||
|
"direct_warning_to_first_only": "سيكون عذا المنشور مرئيًا للمستخدمين المذكورين في أول الرسالة.",
|
||||||
|
"edit_unsupported_warning": "بليروما لا يدعم تعديل الذكر والاستطلاع.",
|
||||||
|
"empty_status_error": "يتعذر نشر منشور فارغ دون ملفات"
|
||||||
},
|
},
|
||||||
"registration": {
|
"registration": {
|
||||||
"bio": "السيرة الذاتية",
|
"bio": "السيرة الذاتية",
|
||||||
"email": "عنوان البريد الإلكتروني",
|
"email": "عنوان البريد الإلكتروني",
|
||||||
"fullname": "الإسم المعروض",
|
"fullname": "الاسم العلني",
|
||||||
"password_confirm": "تأكيد الكلمة السرية",
|
"password_confirm": "تأكيد الكلمة السرية",
|
||||||
"registration": "التسجيل",
|
"registration": "التسجيل",
|
||||||
"token": "رمز الدعوة"
|
"token": "رمز الدعوة",
|
||||||
|
"bio_optional": "سيرة (اختيارية)",
|
||||||
|
"email_optional": "بيرد إلكتروني (اختياري)",
|
||||||
|
"username_placeholder": "مثل lain",
|
||||||
|
"reason": "سبب التسجيل",
|
||||||
|
"register": "سجل",
|
||||||
|
"validations": {
|
||||||
|
"username_required": "لايمكن تركه فارغًا",
|
||||||
|
"email_required": "لايمكن تركه فارغًا",
|
||||||
|
"password_required": "لايمكن تركه فارغًا",
|
||||||
|
"password_confirmation_required": "لايمكن تركه فارغًا",
|
||||||
|
"fullname_required": "لايمكن تركه فارغًا",
|
||||||
|
"password_confirmation_match": "يلزم أن يطابق كلمة السر",
|
||||||
|
"birthday_required": "لايمكن تركه فارغًا",
|
||||||
|
"birthday_min_age": "يلزم أن يكون في {date} أو قبله"
|
||||||
|
},
|
||||||
|
"fullname_placeholder": "مثل Lain Iwakura",
|
||||||
|
"reason_placeholder": "قبول التسجيل في هذا المثيل يستلزم موافقة المدير\nلهذا يجب عليك إعلامه بسبب التسجيل.",
|
||||||
|
"birthday_optional": "تاريخ الميلاد (اختياري):",
|
||||||
|
"email_language": "بأي لغة تريد استلام رسائل البريد الإلكتروني؟",
|
||||||
|
"birthday": "تاريخ الميلاد:"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"attachmentRadius": "المُرفَقات",
|
"attachmentRadius": "المُرفَقات",
|
||||||
|
@ -83,9 +181,9 @@
|
||||||
"cGreen": "أخضر (إعادة النشر)",
|
"cGreen": "أخضر (إعادة النشر)",
|
||||||
"cOrange": "برتقالي (مفضلة)",
|
"cOrange": "برتقالي (مفضلة)",
|
||||||
"cRed": "أحمر (إلغاء)",
|
"cRed": "أحمر (إلغاء)",
|
||||||
"change_password": "تغيير كلمة السر",
|
"change_password": "غيّر كلمة السر",
|
||||||
"change_password_error": "وقع هناك خلل أثناء تعديل كلمتك السرية.",
|
"change_password_error": "حدث خلل أثناء تعديل كلمتك السرية.",
|
||||||
"changed_password": "تم تغيير كلمة المرور بنجاح!",
|
"changed_password": "نجح تغيير كلمة السر!",
|
||||||
"collapse_subject": "",
|
"collapse_subject": "",
|
||||||
"confirm_new_password": "تأكيد كلمة السر الجديدة",
|
"confirm_new_password": "تأكيد كلمة السر الجديدة",
|
||||||
"current_avatar": "صورتك الرمزية الحالية",
|
"current_avatar": "صورتك الرمزية الحالية",
|
||||||
|
@ -94,11 +192,11 @@
|
||||||
"data_import_export_tab": "تصدير واستيراد البيانات",
|
"data_import_export_tab": "تصدير واستيراد البيانات",
|
||||||
"default_vis": "أسلوب العرض الافتراضي",
|
"default_vis": "أسلوب العرض الافتراضي",
|
||||||
"delete_account": "حذف الحساب",
|
"delete_account": "حذف الحساب",
|
||||||
"delete_account_description": "حذف حسابك و كافة منشوراتك نهائيًا.",
|
"delete_account_description": "حذف حسابك و كافة بياناتك نهائيًا.",
|
||||||
"delete_account_error": "",
|
"delete_account_error": "حدثة مشكلة اثناء حذف حسابك، إذا استمرت تواصل مع مدير المثيل.",
|
||||||
"delete_account_instructions": "يُرجى إدخال كلمتك السرية أدناه لتأكيد عملية حذف الحساب.",
|
"delete_account_instructions": "يُرجى إدخال كلمتك السرية أدناه لتأكيد عملية حذف الحساب.",
|
||||||
"export_theme": "حفظ النموذج",
|
"export_theme": "حفظ النموذج",
|
||||||
"filtering": "التصفية",
|
"filtering": "الترشيح",
|
||||||
"filtering_explanation": "سيتم إخفاء كافة المنشورات التي تحتوي على هذه الكلمات، كلمة واحدة في كل سطر",
|
"filtering_explanation": "سيتم إخفاء كافة المنشورات التي تحتوي على هذه الكلمات، كلمة واحدة في كل سطر",
|
||||||
"follow_export": "تصدير الاشتراكات",
|
"follow_export": "تصدير الاشتراكات",
|
||||||
"follow_export_button": "تصدير الاشتراكات كملف csv",
|
"follow_export_button": "تصدير الاشتراكات كملف csv",
|
||||||
|
@ -108,30 +206,30 @@
|
||||||
"follows_imported": "",
|
"follows_imported": "",
|
||||||
"foreground": "الأمامية",
|
"foreground": "الأمامية",
|
||||||
"general": "الإعدادات العامة",
|
"general": "الإعدادات العامة",
|
||||||
"hide_attachments_in_convo": "إخفاء المرفقات على المحادثات",
|
"hide_attachments_in_convo": "اخف المرفقات من المحادثات",
|
||||||
"hide_attachments_in_tl": "إخفاء المرفقات على الخيط الزمني",
|
"hide_attachments_in_tl": "اخف المرفقات من الخيط الزمني",
|
||||||
"hide_post_stats": "",
|
"hide_post_stats": "اخف احصائيات المنشور (مثل عدد التفضيلات)",
|
||||||
"hide_user_stats": "",
|
"hide_user_stats": "اخف احصائيات المستخدم (مثل عدد المتابِعين)",
|
||||||
"import_followers_from_a_csv_file": "",
|
"import_followers_from_a_csv_file": "استورد المتابِعين من ملف csv",
|
||||||
"import_theme": "تحميل نموذج",
|
"import_theme": "تحميل نموذج",
|
||||||
"inputRadius": "",
|
"inputRadius": "",
|
||||||
"instance_default": "",
|
"instance_default": "(الافتراضي: {value})",
|
||||||
"interfaceLanguage": "لغة الواجهة",
|
"interfaceLanguage": "لغة الواجهة",
|
||||||
"invalid_theme_imported": "",
|
"invalid_theme_imported": "الملف المختار ليس سمة تدعمها بليروما.لن تطرأ تغييرات على سمتك.",
|
||||||
"limited_availability": "غير متوفر على متصفحك",
|
"limited_availability": "غير متوفر على متصفحك",
|
||||||
"links": "الروابط",
|
"links": "الروابط",
|
||||||
"lock_account_description": "",
|
"lock_account_description": "",
|
||||||
"loop_video": "",
|
"loop_video": "كرر الفيديوهات",
|
||||||
"loop_video_silent_only": "",
|
"loop_video_silent_only": "كرر فيديوهات بدون صوت (مثل gif في ماستودون)",
|
||||||
"name": "الاسم",
|
"name": "الاسم",
|
||||||
"name_bio": "الاسم والسيرة الذاتية",
|
"name_bio": "الاسم والسيرة الذاتية",
|
||||||
"new_password": "كلمة السر الجديدة",
|
"new_password": "كلمة السر الجديدة",
|
||||||
"no_rich_text_description": "",
|
"no_rich_text_description": "",
|
||||||
"notification_visibility": "نوع الإشعارات التي تريد عرضها",
|
"notification_visibility": "نوع الإشعارات التي تريد عرضها",
|
||||||
"notification_visibility_follows": "يتابع",
|
"notification_visibility_follows": "يتابع",
|
||||||
"notification_visibility_likes": "الإعجابات",
|
"notification_visibility_likes": "المفضلة",
|
||||||
"notification_visibility_mentions": "الإشارات",
|
"notification_visibility_mentions": "ذِكر",
|
||||||
"notification_visibility_repeats": "",
|
"notification_visibility_repeats": "مشاركات",
|
||||||
"nsfw_clickthrough": "",
|
"nsfw_clickthrough": "",
|
||||||
"oauth_tokens": "رموز OAuth",
|
"oauth_tokens": "رموز OAuth",
|
||||||
"token": "رمز",
|
"token": "رمز",
|
||||||
|
@ -141,16 +239,16 @@
|
||||||
"panelRadius": "",
|
"panelRadius": "",
|
||||||
"pause_on_unfocused": "",
|
"pause_on_unfocused": "",
|
||||||
"presets": "النماذج",
|
"presets": "النماذج",
|
||||||
"profile_background": "خلفية الصفحة الشخصية",
|
"profile_background": "خلفية الملف التعريفي",
|
||||||
"profile_banner": "رأسية الصفحة الشخصية",
|
"profile_banner": "رأسية الصفحة الشخصية",
|
||||||
"profile_tab": "الملف الشخصي",
|
"profile_tab": "الملف التعريفي",
|
||||||
"radii_help": "",
|
"radii_help": "",
|
||||||
"replies_in_timeline": "الردود على الخيط الزمني",
|
"replies_in_timeline": "المشاركات في الخيط الزمني",
|
||||||
"reply_visibility_all": "عرض كافة الردود",
|
"reply_visibility_all": "أظهر كل المشاركات",
|
||||||
"reply_visibility_following": "",
|
"reply_visibility_following": "",
|
||||||
"reply_visibility_self": "",
|
"reply_visibility_self": "",
|
||||||
"saving_err": "خطأ أثناء حفظ الإعدادات",
|
"saving_err": "خطأ أثناء حفظ الإعدادات",
|
||||||
"saving_ok": "تم حفظ الإعدادات",
|
"saving_ok": "حُفظت الإعدادات",
|
||||||
"security_tab": "الأمان",
|
"security_tab": "الأمان",
|
||||||
"set_new_avatar": "اختيار صورة رمزية جديدة",
|
"set_new_avatar": "اختيار صورة رمزية جديدة",
|
||||||
"set_new_profile_background": "اختيار خلفية جديدة للملف الشخصي",
|
"set_new_profile_background": "اختيار خلفية جديدة للملف الشخصي",
|
||||||
|
@ -166,7 +264,121 @@
|
||||||
"values": {
|
"values": {
|
||||||
"false": "لا",
|
"false": "لا",
|
||||||
"true": "نعم"
|
"true": "نعم"
|
||||||
}
|
},
|
||||||
|
"emoji_reactions_scale": "معامل تحجيم التفاعلات",
|
||||||
|
"app_name": "اسم تطبيق",
|
||||||
|
"security": "الأمن",
|
||||||
|
"enter_current_password_to_confirm": "أدخل كلمة السر الحالية لتيقن من هويتك",
|
||||||
|
"mfa": {
|
||||||
|
"title": "الاستيثاق بعاملين",
|
||||||
|
"generate_new_recovery_codes": "ولّد رموز استعادة جديدة",
|
||||||
|
"warning_of_generate_new_codes": "عند توليد رموز استعادة جديدة ستزال القديمة.",
|
||||||
|
"recovery_codes": "رموز الاستعادة.",
|
||||||
|
"recovery_codes_warning": "خزن هذه الرموز في مكان آمن. إذا فقدت هذه الرموز وتعذر عليك الوصول إلى تطبيق الاستيثاق بعاملين، لن تتمكن من الوصول لحسابك.",
|
||||||
|
"authentication_methods": "طرق الاستيثاق",
|
||||||
|
"scan": {
|
||||||
|
"title": "مسح",
|
||||||
|
"desc": "امسح رمز الاستجابة السريعة QR من تطبيق الاستيثاق أو أدخل المفتاح:",
|
||||||
|
"secret_code": "مفتاح"
|
||||||
|
},
|
||||||
|
"verify": {
|
||||||
|
"desc": "لتفعيل الاستيثاق بعاملين أدخل الرمز من تطبيق الاستيثاق:"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"block_import": "استيراد المحجوبين",
|
||||||
|
"import_mutes_from_a_csv_file": "استورد قائمة المكتومين من ملف csv",
|
||||||
|
"account_backup": "نسخ احتياطي للحساب",
|
||||||
|
"download_backup": "نزّل",
|
||||||
|
"account_backup_table_head": "نسخ احتياطي",
|
||||||
|
"backup_not_ready": "هذا النسخ الاحتياطي ليس جاهزًا.",
|
||||||
|
"backup_failed": "فشل النسخ الاحتياطي.",
|
||||||
|
"remove_backup": "أزل",
|
||||||
|
"list_backups_error": "خطأ أثناء حلب قائمة النُسخ الاحتياطية: {error}",
|
||||||
|
"added_backup": "أُضيفت نسخة احتياطية جديدة.",
|
||||||
|
"blocks_tab": "المحجوبون",
|
||||||
|
"confirm_dialogs_block": "حجب مستخدم",
|
||||||
|
"confirm_dialogs_mute": "كتم مستخدم",
|
||||||
|
"confirm_dialogs_delete": "حذف حالة",
|
||||||
|
"confirm_dialogs_logout": "خروج",
|
||||||
|
"confirm_dialogs_approve_follow": "قبول متابِع",
|
||||||
|
"confirm_dialogs_deny_follow": "رفض متابِع",
|
||||||
|
"list_aliases_error": "خطأ أثناء جلب الكنيات: {error}",
|
||||||
|
"hide_list_aliases_error_action": "أغلق",
|
||||||
|
"remove_alias": "أزل هذه الكنية",
|
||||||
|
"add_alias_error": "حدث خطأ أثناء إضافة الكنية: {error}",
|
||||||
|
"confirm_dialogs": "أطلب تأكيدًا عند",
|
||||||
|
"confirm_dialogs_repeat": "مشاركة حالة",
|
||||||
|
"mutes_and_blocks": "المكتومون والمحجوبون",
|
||||||
|
"move_account_target": "الحساب المستهدف (مثل {example})",
|
||||||
|
"wordfilter": "ترشيح الكلمات",
|
||||||
|
"always_show_post_button": "أظهر الزر العائم لإنشاء منشور جديد دائمًا",
|
||||||
|
"hide_wallpaper": "اخف خلفية المثيل",
|
||||||
|
"save": "احفظ التعديلات",
|
||||||
|
"lists_navigation": "أظهر القوائم في شريط التنقل",
|
||||||
|
"mute_export_button": "صدّر قائمة المكتومين إلى ملف csv",
|
||||||
|
"blocks_imported": "اُستورد المحجوبون! معالجة القائمة ستستغرق وقتًا.",
|
||||||
|
"mute_export": "تصدير المكتومين",
|
||||||
|
"mute_import": "استيراد المكتومين",
|
||||||
|
"mute_import_error": "خطأ أثناء استيراد المكتومين",
|
||||||
|
"change_email_error": "حدثت خلل أثناء تغيير بريدك الإلكتروني.",
|
||||||
|
"change_email": "غيّر البريد الإلكتروني",
|
||||||
|
"changed_email": "نجح تغيير البريد الإلكتروني!",
|
||||||
|
"account_alias_table_head": "الكنية",
|
||||||
|
"account_alias": "كنيات الحساب",
|
||||||
|
"move_account": "أنقل الحساب",
|
||||||
|
"moved_account": "نُقل الحساب.",
|
||||||
|
"hide_media_previews": "اخف معاينات الوسائط",
|
||||||
|
"hide_muted_posts": "اخف منشورات المستخدمين المكتومين",
|
||||||
|
"confirm_dialogs_unfollow": "الغاء متابعة مستخدم",
|
||||||
|
"confirm_dialogs_remove_follower": "إزالة متابع",
|
||||||
|
"new_alias_target": "أضف كنية جديدة (مثل {example})",
|
||||||
|
"added_alias": "أُضيفت الكنية.",
|
||||||
|
"move_account_error": "خطأ أثناء نقل الحساب: {error}",
|
||||||
|
"emoji_reactions_on_timeline": "أظهر التفاعلات في الخط الزمني",
|
||||||
|
"mutes_imported": "اُستورد المكتومون! معالجة القئمة ستستغرق وقتًا.",
|
||||||
|
"remove_language": "أزل",
|
||||||
|
"primary_language": "اللغة الرئيسية:",
|
||||||
|
"expert_mode": "أظهر الإعدادات المتقدمة",
|
||||||
|
"block_import_error": "خطأ أثناء استيراد قائمة المحجوبين",
|
||||||
|
"add_backup": "أنشئ نسخة احتياطية جديدة",
|
||||||
|
"add_backup_error": "خطأ أثناء إضافة نسخ احتياطي جديد: {error}",
|
||||||
|
"move_account_notes": "إذا أردت نقل حسابك عليك إضافة كنية تشير إلى هنا في الحساب المستهدف.",
|
||||||
|
"avatar_size_instruction": "أدنى حجم مستحسن للصورة الرمزية هو 150x150 بيكسل.",
|
||||||
|
"word_filter_and_more": "مرشح الكلمات والمزيد...",
|
||||||
|
"hide_all_muted_posts": "اخف المنشورات المكتومة",
|
||||||
|
"max_thumbnails": "أقصى عدد للصور المصغرة لكل منشور (فارغ = غير محدود)",
|
||||||
|
"block_export_button": "صدّر قائمة المحجوبين إلى ملف csv",
|
||||||
|
"block_export": "تصدير المحجوبين",
|
||||||
|
"use_one_click_nsfw": "افتح المرفقات ذات المحتوى الحساس NSFW بنقرة واحدة",
|
||||||
|
"account_privacy": "خصوصية",
|
||||||
|
"use_contain_fit": "لا تقتص الصور المصغرة للمرفقات",
|
||||||
|
"import_blocks_from_a_csv_file": "استورد المحجوبين من ملف csv",
|
||||||
|
"instance_default_simple": "(افتراضي)",
|
||||||
|
"interface": "واجهة",
|
||||||
|
"birthday": {
|
||||||
|
"label": "تاريخ الميلاد",
|
||||||
|
"show_birthday": "اظهر تاريخ ميلادي"
|
||||||
|
},
|
||||||
|
"profile_fields": {
|
||||||
|
"add_field": "أضف حقل",
|
||||||
|
"value": "محتوى"
|
||||||
|
},
|
||||||
|
"posts": "منشورات",
|
||||||
|
"user_profiles": "ملفات تعريفية للمستخدمين",
|
||||||
|
"notification_visibility_emoji_reactions": "تفاعلات",
|
||||||
|
"notification_visibility_polls": "انتهاء استطلاعات اشتركت بها",
|
||||||
|
"file_export_import": {
|
||||||
|
"restore_settings": "استرجع الإعدادات من ملف",
|
||||||
|
"backup_restore": "نسخ احتياطي للإعدادات"
|
||||||
|
},
|
||||||
|
"mutes_tab": "مكتومون",
|
||||||
|
"no_mutes": "لا يوجد مكتومون",
|
||||||
|
"hide_followers_count_description": "لا تظهر عدد المتابِعين",
|
||||||
|
"show_moderator_badge": "أظهر شارة \"مشرف\" في ملفي التعريفي",
|
||||||
|
"hide_follows_count_description": "لا تظهر عدد المتابَعين",
|
||||||
|
"hide_muted_threads": "اخف النقاشات المكتومة",
|
||||||
|
"no_blocks": "لا يوجد محجوبون",
|
||||||
|
"show_admin_badge": "أظهر شارة \"مدير\" في ملفي التعريفي"
|
||||||
},
|
},
|
||||||
"timeline": {
|
"timeline": {
|
||||||
"collapse": "",
|
"collapse": "",
|
||||||
|
@ -211,11 +423,109 @@
|
||||||
"keyword_policies": "سياسة الكلمات الدلالية"
|
"keyword_policies": "سياسة الكلمات الدلالية"
|
||||||
},
|
},
|
||||||
"simple": {
|
"simple": {
|
||||||
"simple_policies": "سياسات الخادم"
|
"simple_policies": "سياسات الخادم",
|
||||||
|
"instance": "مثيل",
|
||||||
|
"reason": "السبب",
|
||||||
|
"accept": "قبول",
|
||||||
|
"reject": "رفض"
|
||||||
},
|
},
|
||||||
"federation": "الاتحاد",
|
"federation": "الاتحاد",
|
||||||
"mrf_policies": "تفعيل سياسات إعادة كتابة المنشور",
|
"mrf_policies": "تفعيل سياسات إعادة كتابة المنشور",
|
||||||
"mrf_policies_desc": "خاصية إعادة كتابة المناشير تقوم بتعديل تفاعل الاتحاد مع هذا الخادم. السياسات التالية مفعّلة:"
|
"mrf_policies_desc": "خاصية إعادة كتابة المناشير تقوم بتعديل تفاعل الاتحاد مع هذا الخادم. السياسات التالية مفعّلة:"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"announcements": {
|
||||||
|
"page_header": "إعلانات",
|
||||||
|
"title": "إعلان",
|
||||||
|
"mark_as_read_action": "علّمه كمقروء",
|
||||||
|
"post_form_header": "انشر إعلانًا",
|
||||||
|
"post_placeholder": "اكتب محتوى الاعلان هنا...",
|
||||||
|
"post_action": "انشر",
|
||||||
|
"post_error": "خطأ: {error}",
|
||||||
|
"close_error": "أغلاق",
|
||||||
|
"delete_action": "احذف",
|
||||||
|
"start_time_prompt": "وقت البدأ: ",
|
||||||
|
"end_time_prompt": "وقت النهاية: ",
|
||||||
|
"all_day_prompt": "هذا حدث يوم كامل",
|
||||||
|
"start_time_display": "يبدأ في {time}",
|
||||||
|
"end_time_display": "ينتهي في {time}",
|
||||||
|
"edit_action": "حرر",
|
||||||
|
"submit_edit_action": "أرسل",
|
||||||
|
"cancel_edit_action": "ألغِ",
|
||||||
|
"inactive_message": "هذا الاعلان غير نشط",
|
||||||
|
"published_time_display": "نُشر في {time}"
|
||||||
|
},
|
||||||
|
"polls": {
|
||||||
|
"votes": "أصوات",
|
||||||
|
"vote": "صوّت",
|
||||||
|
"type": "نوع الاستطلاع",
|
||||||
|
"single_choice": "خيار واحد",
|
||||||
|
"multiple_choices": "متعدد الخيارات",
|
||||||
|
"expiry": "عمر الاستطلاع",
|
||||||
|
"expires_in": "ينتهي الاستطلاع في {0}",
|
||||||
|
"expired": "انتهى الاستطلاع منذ {0}",
|
||||||
|
"add_poll": "أضف استطلاعًا",
|
||||||
|
"add_option": "أضف خيارًا",
|
||||||
|
"option": "خيار"
|
||||||
|
},
|
||||||
|
"emoji": {
|
||||||
|
"stickers": "ملصقات",
|
||||||
|
"emoji": "إيموجي",
|
||||||
|
"search_emoji": "ابحث عن إيموجي",
|
||||||
|
"unicode_groups": {
|
||||||
|
"animals-and-nature": "حيوانات وطبيعة",
|
||||||
|
"food-and-drink": "أطعمة ومشروبات",
|
||||||
|
"symbols": "رموز",
|
||||||
|
"activities": "نشاطات",
|
||||||
|
"flags": "أعلام"
|
||||||
|
},
|
||||||
|
"add_emoji": "أدخل إيموجي",
|
||||||
|
"custom": "إيموجي مخصص"
|
||||||
|
},
|
||||||
|
"interactions": {
|
||||||
|
"emoji_reactions": "تفاعلات بالإيموجي",
|
||||||
|
"reports": "البلاغات",
|
||||||
|
"follows": "المتابعات الجديدة"
|
||||||
|
},
|
||||||
|
"report": {
|
||||||
|
"state_closed": "مغلق",
|
||||||
|
"state_resolved": "عولج",
|
||||||
|
"reported_statuses": "الحالة المبلغة عنها:",
|
||||||
|
"state_open": "مفتوح",
|
||||||
|
"notes": "ملاحظة:",
|
||||||
|
"state": "الحالة:",
|
||||||
|
"reporter": "المبلِّغ:",
|
||||||
|
"reported_user": "المُبلغ عنه:"
|
||||||
|
},
|
||||||
|
"selectable_list": {
|
||||||
|
"select_all": "اختر الكل"
|
||||||
|
},
|
||||||
|
"image_cropper": {
|
||||||
|
"save": "احفظ",
|
||||||
|
"cancel": "ألغ"
|
||||||
|
},
|
||||||
|
"importer": {
|
||||||
|
"submit": "أرسل",
|
||||||
|
"success": "نجح الاستيراد.",
|
||||||
|
"error": "حدث خطأ أثناء الاستيراد."
|
||||||
|
},
|
||||||
|
"domain_mute_card": {
|
||||||
|
"mute": "اكتم",
|
||||||
|
"mute_progress": "يكتم…",
|
||||||
|
"unmute": "ارفع الكتم",
|
||||||
|
"unmute_progress": "يرفع الكتم…"
|
||||||
|
},
|
||||||
|
"exporter": {
|
||||||
|
"export": "صدر",
|
||||||
|
"processing": "يُعالج. سيُطلب منك تنزيل الملف قريباً"
|
||||||
|
},
|
||||||
|
"media_modal": {
|
||||||
|
"previous": "السابق",
|
||||||
|
"next": "التالي",
|
||||||
|
"hide": "أغلق عارض الوسائط"
|
||||||
|
},
|
||||||
|
"remote_user_resolver": {
|
||||||
|
"searching_for": "يبحث عن",
|
||||||
|
"error": "لم يُعثر عليه."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue