Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma-fe into feat/report-notification
This commit is contained in:
commit
d0bfd9a808
57 changed files with 3745 additions and 1467 deletions
|
@ -36,6 +36,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Attachments are truncated just like post contents
|
- Attachments are truncated just like post contents
|
||||||
- Media modal now also displays description and counter position in gallery (i.e. 1/5)
|
- Media modal now also displays description and counter position in gallery (i.e. 1/5)
|
||||||
- Ability to rearrange order of attachments when uploading
|
- Ability to rearrange order of attachments when uploading
|
||||||
|
- Enabled users to zoom and pan images in media viewer with mouse and touch
|
||||||
|
|
||||||
|
|
||||||
## [2.4.2] - 2022-01-09
|
## [2.4.2] - 2022-01-09
|
||||||
### Added
|
### Added
|
||||||
|
|
197
package.json
197
package.json
|
@ -16,107 +16,108 @@
|
||||||
"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.7.6",
|
"@babel/runtime": "7.17.7",
|
||||||
"@chenfengyuan/vue-qrcode": "^1.0.0",
|
"@chenfengyuan/vue-qrcode": "1.0.2",
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.15.1",
|
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||||
"@fortawesome/vue-fontawesome": "^2.0.0",
|
"@fortawesome/vue-fontawesome": "2.0.6",
|
||||||
"body-scroll-lock": "^2.6.4",
|
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
||||||
"chromatism": "^3.0.0",
|
"body-scroll-lock": "2.7.1",
|
||||||
"cropperjs": "^1.4.3",
|
"chromatism": "3.0.0",
|
||||||
"diff": "^3.0.1",
|
"cropperjs": "1.5.12",
|
||||||
"escape-html": "^1.0.3",
|
"diff": "3.5.0",
|
||||||
"localforage": "^1.5.0",
|
"escape-html": "1.0.3",
|
||||||
"parse-link-header": "^1.0.1",
|
"localforage": "1.7.3",
|
||||||
"phoenix": "^1.3.0",
|
"parse-link-header": "1.0.1",
|
||||||
"portal-vue": "^2.1.4",
|
"phoenix": "1.4.0",
|
||||||
"punycode.js": "^2.1.0",
|
"portal-vue": "2.1.7",
|
||||||
"ruffle-mirror": "^2021.4.10",
|
"punycode.js": "2.1.0",
|
||||||
"v-click-outside": "^2.1.1",
|
"ruffle-mirror": "2021.4.11",
|
||||||
"vue": "^2.6.11",
|
"v-click-outside": "2.1.5",
|
||||||
"vue-i18n": "^7.3.2",
|
"vue": "2.6.11",
|
||||||
"vue-router": "^3.0.1",
|
"vue-i18n": "7.8.1",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"vue-router": "3.0.2",
|
||||||
"vuelidate": "^0.7.4",
|
"vue-template-compiler": "2.6.11",
|
||||||
"vuex": "^3.0.1"
|
"vuelidate": "0.7.7",
|
||||||
|
"vuex": "3.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.7.5",
|
"@babel/core": "7.17.7",
|
||||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
"@babel/plugin-transform-runtime": "7.17.0",
|
||||||
"@babel/preset-env": "^7.7.6",
|
"@babel/preset-env": "7.16.11",
|
||||||
"@babel/register": "^7.7.4",
|
"@babel/register": "7.17.7",
|
||||||
"@ungap/event-target": "^0.1.0",
|
"@ungap/event-target": "0.2.3",
|
||||||
"@vue/babel-helper-vue-jsx-merge-props": "^1.2.1",
|
"@vue/babel-helper-vue-jsx-merge-props": "1.2.1",
|
||||||
"@vue/babel-preset-jsx": "^1.2.4",
|
"@vue/babel-preset-jsx": "1.2.4",
|
||||||
"@vue/test-utils": "^1.0.0-beta.26",
|
"@vue/test-utils": "1.0.0-beta.28",
|
||||||
"autoprefixer": "^6.4.0",
|
"autoprefixer": "6.7.7",
|
||||||
"babel-eslint": "^7.0.0",
|
"babel-eslint": "7.2.3",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "8.2.3",
|
||||||
"babel-plugin-lodash": "^3.3.4",
|
"babel-plugin-lodash": "3.3.4",
|
||||||
"chai": "^3.5.0",
|
"chai": "3.5.0",
|
||||||
"chalk": "^1.1.3",
|
"chalk": "1.1.3",
|
||||||
"chromedriver": "^87.0.1",
|
"chromedriver": "87.0.7",
|
||||||
"connect-history-api-fallback": "^1.1.0",
|
"connect-history-api-fallback": "1.6.0",
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"copy-webpack-plugin": "6.4.1",
|
||||||
"cross-spawn": "^4.0.2",
|
"cross-spawn": "4.0.2",
|
||||||
"css-loader": "^0.28.0",
|
"css-loader": "0.28.11",
|
||||||
"custom-event-polyfill": "^1.0.7",
|
"custom-event-polyfill": "1.0.7",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "5.16.0",
|
||||||
"eslint-config-standard": "^12.0.0",
|
"eslint-config-standard": "12.0.0",
|
||||||
"eslint-friendly-formatter": "^2.0.5",
|
"eslint-friendly-formatter": "2.0.7",
|
||||||
"eslint-loader": "^2.1.0",
|
"eslint-loader": "2.1.2",
|
||||||
"eslint-plugin-import": "^2.13.0",
|
"eslint-plugin-import": "2.17.2",
|
||||||
"eslint-plugin-node": "^7.0.0",
|
"eslint-plugin-node": "7.0.1",
|
||||||
"eslint-plugin-promise": "^4.0.0",
|
"eslint-plugin-promise": "4.1.1",
|
||||||
"eslint-plugin-standard": "^4.0.0",
|
"eslint-plugin-standard": "4.0.0",
|
||||||
"eslint-plugin-vue": "^5.2.2",
|
"eslint-plugin-vue": "5.2.3",
|
||||||
"eventsource-polyfill": "^0.9.6",
|
"eventsource-polyfill": "0.9.6",
|
||||||
"express": "^4.13.3",
|
"express": "4.16.4",
|
||||||
"file-loader": "^3.0.1",
|
"file-loader": "3.0.1",
|
||||||
"function-bind": "^1.0.2",
|
"function-bind": "1.1.1",
|
||||||
"html-webpack-plugin": "^3.0.0",
|
"html-webpack-plugin": "3.2.0",
|
||||||
"http-proxy-middleware": "^0.17.2",
|
"http-proxy-middleware": "0.17.4",
|
||||||
"inject-loader": "^2.0.1",
|
"inject-loader": "2.0.1",
|
||||||
"iso-639-1": "^2.0.3",
|
"iso-639-1": "2.0.3",
|
||||||
"isparta-loader": "^2.0.0",
|
"isparta-loader": "2.0.0",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "0.5.7",
|
||||||
"karma": "^3.0.0",
|
"karma": "3.1.4",
|
||||||
"karma-coverage": "^1.1.1",
|
"karma-coverage": "1.1.2",
|
||||||
"karma-firefox-launcher": "^1.1.0",
|
"karma-firefox-launcher": "1.1.0",
|
||||||
"karma-mocha": "^1.2.0",
|
"karma-mocha": "1.3.0",
|
||||||
"karma-mocha-reporter": "^2.2.1",
|
"karma-mocha-reporter": "2.2.5",
|
||||||
"karma-sinon-chai": "^2.0.2",
|
"karma-sinon-chai": "2.0.2",
|
||||||
"karma-sourcemap-loader": "^0.3.7",
|
"karma-sourcemap-loader": "0.3.8",
|
||||||
"karma-spec-reporter": "0.0.26",
|
"karma-spec-reporter": "0.0.33",
|
||||||
"karma-webpack": "^4.0.0-rc.3",
|
"karma-webpack": "4.0.2",
|
||||||
"lodash": "^4.16.4",
|
"lodash": "4.17.21",
|
||||||
"lolex": "^1.4.0",
|
"lolex": "1.6.0",
|
||||||
"mini-css-extract-plugin": "^0.5.0",
|
"mini-css-extract-plugin": "0.5.0",
|
||||||
"mocha": "^3.1.0",
|
"mocha": "3.5.3",
|
||||||
"nightwatch": "^0.9.8",
|
"nightwatch": "0.9.21",
|
||||||
"opn": "^4.0.2",
|
"opn": "4.0.2",
|
||||||
"ora": "^0.3.0",
|
"ora": "0.3.0",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "3.0.0",
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "0.5.1",
|
||||||
"sass": "^1.17.3",
|
"sass": "1.20.1",
|
||||||
"sass-loader": "git://github.com/webpack-contrib/sass-loader",
|
"sass-loader": "7.2.0",
|
||||||
"selenium-server": "2.53.1",
|
"selenium-server": "2.53.1",
|
||||||
"semver": "^5.3.0",
|
"semver": "5.6.0",
|
||||||
"serviceworker-webpack-plugin": "^1.0.0",
|
"serviceworker-webpack-plugin": "1.0.1",
|
||||||
"shelljs": "^0.8.4",
|
"shelljs": "0.8.5",
|
||||||
"sinon": "^2.1.0",
|
"sinon": "2.4.1",
|
||||||
"sinon-chai": "^2.8.0",
|
"sinon-chai": "2.14.0",
|
||||||
"stylelint": "^13.6.1",
|
"stylelint": "13.6.1",
|
||||||
"stylelint-config-standard": "^20.0.0",
|
"stylelint-config-standard": "20.0.0",
|
||||||
"stylelint-rscss": "^0.4.0",
|
"stylelint-rscss": "0.4.0",
|
||||||
"url-loader": "^1.1.2",
|
"url-loader": "1.1.2",
|
||||||
"vue-loader": "^14.0.0",
|
"vue-loader": "14.2.4",
|
||||||
"vue-style-loader": "^4.0.0",
|
"vue-style-loader": "4.1.2",
|
||||||
"webpack": "^4.44.0",
|
"webpack": "4.46.0",
|
||||||
"webpack-dev-middleware": "^3.6.0",
|
"webpack-dev-middleware": "3.7.3",
|
||||||
"webpack-hot-middleware": "^2.12.2",
|
"webpack-hot-middleware": "2.24.3",
|
||||||
"webpack-merge": "^0.14.1"
|
"webpack-merge": "0.14.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 4.0.0",
|
"node": ">= 4.0.0",
|
||||||
|
|
6
renovate.json
Normal file
6
renovate.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:base"
|
||||||
|
]
|
||||||
|
}
|
|
@ -30,3 +30,5 @@ $fallback--attachmentRadius: 10px;
|
||||||
$fallback--chatMessageRadius: 10px;
|
$fallback--chatMessageRadius: 10px;
|
||||||
|
|
||||||
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
|
||||||
|
|
||||||
|
$status-margin: 0.75em;
|
||||||
|
|
|
@ -115,6 +115,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
||||||
copyInstanceOption('nsfwCensorImage')
|
copyInstanceOption('nsfwCensorImage')
|
||||||
copyInstanceOption('background')
|
copyInstanceOption('background')
|
||||||
copyInstanceOption('hidePostStats')
|
copyInstanceOption('hidePostStats')
|
||||||
|
copyInstanceOption('hideBotIndication')
|
||||||
copyInstanceOption('hideUserStats')
|
copyInstanceOption('hideUserStats')
|
||||||
copyInstanceOption('hideFilteredStatuses')
|
copyInstanceOption('hideFilteredStatuses')
|
||||||
copyInstanceOption('logo')
|
copyInstanceOption('logo')
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
:href="attachment.url"
|
:href="attachment.url"
|
||||||
:alt="attachment.description"
|
:alt="attachment.description"
|
||||||
:title="attachment.description"
|
:title="attachment.description"
|
||||||
|
@click.prevent
|
||||||
>
|
>
|
||||||
<FAIcon :icon="placeholderIconClass" />
|
<FAIcon :icon="placeholderIconClass" />
|
||||||
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }}
|
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }}
|
||||||
|
|
|
@ -1,5 +1,19 @@
|
||||||
import { reduce, filter, findIndex, clone, get } from 'lodash'
|
import { reduce, filter, findIndex, clone, get } from 'lodash'
|
||||||
import Status from '../status/status.vue'
|
import Status from '../status/status.vue'
|
||||||
|
import ThreadTree from '../thread_tree/thread_tree.vue'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faAngleDoubleDown,
|
||||||
|
faAngleDoubleLeft,
|
||||||
|
faChevronLeft
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faAngleDoubleDown,
|
||||||
|
faAngleDoubleLeft,
|
||||||
|
faChevronLeft
|
||||||
|
)
|
||||||
|
|
||||||
const sortById = (a, b) => {
|
const sortById = (a, b) => {
|
||||||
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
||||||
|
@ -35,7 +49,10 @@ const conversation = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
highlight: null,
|
highlight: null,
|
||||||
expanded: false
|
expanded: false,
|
||||||
|
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
|
||||||
|
statusContentPropertiesObject: {},
|
||||||
|
inlineDivePosition: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
|
@ -53,12 +70,50 @@ const conversation = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hideStatus () {
|
maxDepthToShowByDefault () {
|
||||||
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
|
// maxDepthInThread = max number of depths that is *visible*
|
||||||
return this.virtualHidden && this.$refs.statusComponent[0].suspendable
|
// since our depth starts with 0 and "showing" means "showing children"
|
||||||
} else {
|
// there is a -2 here
|
||||||
return this.virtualHidden
|
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
|
||||||
|
return maxDepth >= 1 ? maxDepth : 1
|
||||||
|
},
|
||||||
|
displayStyle () {
|
||||||
|
return this.$store.getters.mergedConfig.conversationDisplay
|
||||||
|
},
|
||||||
|
isTreeView () {
|
||||||
|
return !this.isLinearView
|
||||||
|
},
|
||||||
|
treeViewIsSimple () {
|
||||||
|
return !this.$store.getters.mergedConfig.conversationTreeAdvanced
|
||||||
|
},
|
||||||
|
isLinearView () {
|
||||||
|
return this.displayStyle === 'linear'
|
||||||
|
},
|
||||||
|
shouldFadeAncestors () {
|
||||||
|
return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
|
||||||
|
},
|
||||||
|
otherRepliesButtonPosition () {
|
||||||
|
return this.$store.getters.mergedConfig.conversationOtherRepliesButton
|
||||||
|
},
|
||||||
|
showOtherRepliesButtonBelowStatus () {
|
||||||
|
return this.otherRepliesButtonPosition === 'below'
|
||||||
|
},
|
||||||
|
showOtherRepliesButtonInsideStatus () {
|
||||||
|
return this.otherRepliesButtonPosition === 'inside'
|
||||||
|
},
|
||||||
|
suspendable () {
|
||||||
|
if (this.isTreeView) {
|
||||||
|
return Object.entries(this.statusContentProperties)
|
||||||
|
.every(([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0)
|
||||||
}
|
}
|
||||||
|
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
|
||||||
|
return this.$refs.statusComponent.every(s => s.suspendable)
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hideStatus () {
|
||||||
|
return this.virtualHidden && this.suspendable
|
||||||
},
|
},
|
||||||
status () {
|
status () {
|
||||||
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
||||||
|
@ -90,6 +145,121 @@ const conversation = {
|
||||||
|
|
||||||
return sortAndFilterConversation(conversation, this.status)
|
return sortAndFilterConversation(conversation, this.status)
|
||||||
},
|
},
|
||||||
|
statusMap () {
|
||||||
|
return this.conversation.reduce((res, s) => {
|
||||||
|
res[s.id] = s
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
threadTree () {
|
||||||
|
const reverseLookupTable = this.conversation.reduce((table, status, index) => {
|
||||||
|
table[status.id] = index
|
||||||
|
return table
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const threads = this.conversation.reduce((a, cur) => {
|
||||||
|
const id = cur.id
|
||||||
|
a.forest[id] = this.getReplies(id)
|
||||||
|
.map(s => s.id)
|
||||||
|
|
||||||
|
return a
|
||||||
|
}, {
|
||||||
|
forest: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
|
||||||
|
if (processed[id]) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
processed[id] = true
|
||||||
|
return [{
|
||||||
|
status: this.conversation[reverseLookupTable[id]],
|
||||||
|
id,
|
||||||
|
depth
|
||||||
|
}, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
|
||||||
|
}).reduce((a, b) => a.concat(b), [])
|
||||||
|
|
||||||
|
const linearized = walk(threads.forest, this.topLevel.map(k => k.id))
|
||||||
|
|
||||||
|
return linearized
|
||||||
|
},
|
||||||
|
replyIds () {
|
||||||
|
return this.conversation.map(k => k.id)
|
||||||
|
.reduce((res, id) => {
|
||||||
|
res[id] = (this.replies[id] || []).map(k => k.id)
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
totalReplyCount () {
|
||||||
|
const sizes = {}
|
||||||
|
const subTreeSizeFor = (id) => {
|
||||||
|
if (sizes[id]) {
|
||||||
|
return sizes[id]
|
||||||
|
}
|
||||||
|
sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0)
|
||||||
|
return sizes[id]
|
||||||
|
}
|
||||||
|
this.conversation.map(k => k.id).map(subTreeSizeFor)
|
||||||
|
return Object.keys(sizes).reduce((res, id) => {
|
||||||
|
res[id] = sizes[id] - 1 // exclude itself
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
totalReplyDepth () {
|
||||||
|
const depths = {}
|
||||||
|
const subTreeDepthFor = (id) => {
|
||||||
|
if (depths[id]) {
|
||||||
|
return depths[id]
|
||||||
|
}
|
||||||
|
depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0)
|
||||||
|
return depths[id]
|
||||||
|
}
|
||||||
|
this.conversation.map(k => k.id).map(subTreeDepthFor)
|
||||||
|
return Object.keys(depths).reduce((res, id) => {
|
||||||
|
res[id] = depths[id] - 1 // exclude itself
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
depths () {
|
||||||
|
return this.threadTree.reduce((a, k) => {
|
||||||
|
a[k.id] = k.depth
|
||||||
|
return a
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
topLevel () {
|
||||||
|
const topLevel = this.conversation.reduce((tl, cur) =>
|
||||||
|
tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation)
|
||||||
|
return topLevel
|
||||||
|
},
|
||||||
|
otherTopLevelCount () {
|
||||||
|
return this.topLevel.length - 1
|
||||||
|
},
|
||||||
|
showingTopLevel () {
|
||||||
|
if (this.canDive && this.diveRoot) {
|
||||||
|
return [this.statusMap[this.diveRoot]]
|
||||||
|
}
|
||||||
|
return this.topLevel
|
||||||
|
},
|
||||||
|
diveRoot () {
|
||||||
|
const statusId = this.inlineDivePosition || this.statusId
|
||||||
|
const isTopLevel = !this.parentOf(statusId)
|
||||||
|
return isTopLevel ? null : statusId
|
||||||
|
},
|
||||||
|
diveDepth () {
|
||||||
|
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
|
||||||
|
},
|
||||||
|
diveMode () {
|
||||||
|
return this.canDive && !!this.diveRoot
|
||||||
|
},
|
||||||
|
shouldShowAllConversationButton () {
|
||||||
|
// The "show all conversation" button tells the user that there exist
|
||||||
|
// other toplevel statuses, so do not show it if there is only a single root
|
||||||
|
return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1
|
||||||
|
},
|
||||||
|
shouldShowAncestors () {
|
||||||
|
return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length
|
||||||
|
},
|
||||||
replies () {
|
replies () {
|
||||||
let i = 1
|
let i = 1
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
|
@ -109,15 +279,71 @@ const conversation = {
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
isExpanded () {
|
isExpanded () {
|
||||||
return this.expanded || this.isPage
|
return !!(this.expanded || this.isPage)
|
||||||
},
|
},
|
||||||
hiddenStyle () {
|
hiddenStyle () {
|
||||||
const height = (this.status && this.status.virtualHeight) || '120px'
|
const height = (this.status && this.status.virtualHeight) || '120px'
|
||||||
return this.virtualHidden ? { height } : {}
|
return this.virtualHidden ? { height } : {}
|
||||||
|
},
|
||||||
|
threadDisplayStatus () {
|
||||||
|
return this.conversation.reduce((a, k) => {
|
||||||
|
const id = k.id
|
||||||
|
const depth = this.depths[id]
|
||||||
|
const status = (() => {
|
||||||
|
if (this.threadDisplayStatusObject[id]) {
|
||||||
|
return this.threadDisplayStatusObject[id]
|
||||||
|
}
|
||||||
|
if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) {
|
||||||
|
return 'showing'
|
||||||
|
} else {
|
||||||
|
return 'hidden'
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
|
a[id] = status
|
||||||
|
return a
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
statusContentProperties () {
|
||||||
|
return this.conversation.reduce((a, k) => {
|
||||||
|
const id = k.id
|
||||||
|
const props = (() => {
|
||||||
|
const def = {
|
||||||
|
showingTall: false,
|
||||||
|
expandingSubject: false,
|
||||||
|
showingLongSubject: false,
|
||||||
|
isReplying: false,
|
||||||
|
mediaPlaying: []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.statusContentPropertiesObject[id]) {
|
||||||
|
return {
|
||||||
|
...def,
|
||||||
|
...this.statusContentPropertiesObject[id]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return def
|
||||||
|
})()
|
||||||
|
|
||||||
|
a[id] = props
|
||||||
|
return a
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
canDive () {
|
||||||
|
return this.isTreeView && this.isExpanded
|
||||||
|
},
|
||||||
|
focused () {
|
||||||
|
return (id) => {
|
||||||
|
return (this.isExpanded) && id === this.highlight
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maybeHighlight () {
|
||||||
|
return this.isExpanded ? this.highlight : null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Status
|
Status,
|
||||||
|
ThreadTree
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
statusId (newVal, oldVal) {
|
statusId (newVal, oldVal) {
|
||||||
|
@ -132,6 +358,8 @@ const conversation = {
|
||||||
expanded (value) {
|
expanded (value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
this.fetchConversation()
|
this.fetchConversation()
|
||||||
|
} else {
|
||||||
|
this.resetDisplayState()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
virtualHidden (value) {
|
virtualHidden (value) {
|
||||||
|
@ -161,8 +389,8 @@ const conversation = {
|
||||||
getReplies (id) {
|
getReplies (id) {
|
||||||
return this.replies[id] || []
|
return this.replies[id] || []
|
||||||
},
|
},
|
||||||
focused (id) {
|
getHighlight () {
|
||||||
return (this.isExpanded) && id === this.statusId
|
return this.isExpanded ? this.highlight : null
|
||||||
},
|
},
|
||||||
setHighlight (id) {
|
setHighlight (id) {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
|
@ -170,15 +398,139 @@ const conversation = {
|
||||||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||||
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
||||||
},
|
},
|
||||||
getHighlight () {
|
|
||||||
return this.isExpanded ? this.highlight : null
|
|
||||||
},
|
|
||||||
toggleExpanded () {
|
toggleExpanded () {
|
||||||
this.expanded = !this.expanded
|
this.expanded = !this.expanded
|
||||||
},
|
},
|
||||||
getConversationId (statusId) {
|
getConversationId (statusId) {
|
||||||
const status = this.$store.state.statuses.allStatusesObject[statusId]
|
const status = this.$store.state.statuses.allStatusesObject[statusId]
|
||||||
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
|
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
|
||||||
|
},
|
||||||
|
setThreadDisplay (id, nextStatus) {
|
||||||
|
this.threadDisplayStatusObject = {
|
||||||
|
...this.threadDisplayStatusObject,
|
||||||
|
[id]: nextStatus
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleThreadDisplay (id) {
|
||||||
|
const curStatus = this.threadDisplayStatus[id]
|
||||||
|
const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
|
||||||
|
this.setThreadDisplay(id, nextStatus)
|
||||||
|
},
|
||||||
|
setThreadDisplayRecursively (id, nextStatus) {
|
||||||
|
this.setThreadDisplay(id, nextStatus)
|
||||||
|
this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus))
|
||||||
|
},
|
||||||
|
showThreadRecursively (id) {
|
||||||
|
this.setThreadDisplayRecursively(id, 'showing')
|
||||||
|
},
|
||||||
|
setStatusContentProperty (id, name, value) {
|
||||||
|
this.statusContentPropertiesObject = {
|
||||||
|
...this.statusContentPropertiesObject,
|
||||||
|
[id]: {
|
||||||
|
...this.statusContentPropertiesObject[id],
|
||||||
|
[name]: value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleStatusContentProperty (id, name) {
|
||||||
|
this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
|
||||||
|
},
|
||||||
|
leastVisibleAncestor (id) {
|
||||||
|
let cur = id
|
||||||
|
let parent = this.parentOf(cur)
|
||||||
|
while (cur) {
|
||||||
|
// if the parent is showing it means cur is visible
|
||||||
|
if (this.threadDisplayStatus[parent] === 'showing') {
|
||||||
|
return cur
|
||||||
|
}
|
||||||
|
parent = this.parentOf(parent)
|
||||||
|
cur = this.parentOf(cur)
|
||||||
|
}
|
||||||
|
// nothing found, fall back to toplevel
|
||||||
|
return this.topLevel[0] ? this.topLevel[0].id : undefined
|
||||||
|
},
|
||||||
|
diveIntoStatus (id, preventScroll) {
|
||||||
|
this.tryScrollTo(id)
|
||||||
|
},
|
||||||
|
diveToTopLevel () {
|
||||||
|
this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id)
|
||||||
|
},
|
||||||
|
// only used when we are not on a page
|
||||||
|
undive () {
|
||||||
|
this.inlineDivePosition = null
|
||||||
|
this.setHighlight(this.statusId)
|
||||||
|
},
|
||||||
|
tryScrollTo (id) {
|
||||||
|
if (!id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.isPage) {
|
||||||
|
// set statusId
|
||||||
|
this.$router.push({ name: 'conversation', params: { id } })
|
||||||
|
} else {
|
||||||
|
this.inlineDivePosition = id
|
||||||
|
}
|
||||||
|
// Because the conversation can be unmounted when out of sight
|
||||||
|
// and mounted again when it comes into sight,
|
||||||
|
// the `mounted` or `created` function in `status` should not
|
||||||
|
// contain scrolling calls, as we do not want the page to jump
|
||||||
|
// when we scroll with an expanded conversation.
|
||||||
|
//
|
||||||
|
// Now the method is to rely solely on the `highlight` watcher
|
||||||
|
// in `status` components.
|
||||||
|
// In linear views, all statuses are rendered at all times, but
|
||||||
|
// in tree views, it is possible that a change in active status
|
||||||
|
// removes and adds status components (e.g. an originally child
|
||||||
|
// status becomes an ancestor status, and thus they will be
|
||||||
|
// different).
|
||||||
|
// Here, let the components be rendered first, in order to trigger
|
||||||
|
// the `highlight` watcher.
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.setHighlight(id)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
goToCurrent () {
|
||||||
|
this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
|
||||||
|
},
|
||||||
|
statusById (id) {
|
||||||
|
return this.statusMap[id]
|
||||||
|
},
|
||||||
|
parentOf (id) {
|
||||||
|
const status = this.statusById(id)
|
||||||
|
if (!status) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
const { in_reply_to_status_id: parentId } = status
|
||||||
|
if (!this.statusMap[parentId]) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return parentId
|
||||||
|
},
|
||||||
|
parentOrSelf (id) {
|
||||||
|
return this.parentOf(id) || id
|
||||||
|
},
|
||||||
|
// Ancestors of some status, from top to bottom
|
||||||
|
ancestorsOf (id) {
|
||||||
|
const ancestors = []
|
||||||
|
let cur = this.parentOf(id)
|
||||||
|
while (cur) {
|
||||||
|
ancestors.unshift(this.statusMap[cur])
|
||||||
|
cur = this.parentOf(cur)
|
||||||
|
}
|
||||||
|
return ancestors
|
||||||
|
},
|
||||||
|
topLevelAncestorOrSelfId (id) {
|
||||||
|
let cur = id
|
||||||
|
let parent = this.parentOf(id)
|
||||||
|
while (parent) {
|
||||||
|
cur = this.parentOf(cur)
|
||||||
|
parent = this.parentOf(parent)
|
||||||
|
}
|
||||||
|
return cur
|
||||||
|
},
|
||||||
|
resetDisplayState () {
|
||||||
|
this.undive()
|
||||||
|
this.threadDisplayStatusObject = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,138 @@
|
||||||
{{ $t('timeline.collapse') }}
|
{{ $t('timeline.collapse') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="conversation-body panel-body">
|
||||||
|
<div
|
||||||
|
v-if="isTreeView"
|
||||||
|
class="thread-body"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="shouldShowAllConversationButton"
|
||||||
|
class="conversation-dive-to-top-level-box"
|
||||||
|
>
|
||||||
|
<i18n
|
||||||
|
path="status.show_all_conversation_with_icon"
|
||||||
|
tag="button"
|
||||||
|
class="button-unstyled -link"
|
||||||
|
@click.prevent="diveToTopLevel"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
place="icon"
|
||||||
|
icon="angle-double-left"
|
||||||
|
/>
|
||||||
|
<span place="text">
|
||||||
|
{{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
|
||||||
|
</span>
|
||||||
|
</i18n>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="shouldShowAncestors"
|
||||||
|
class="thread-ancestors"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="status in ancestorsOf(diveRoot)"
|
||||||
|
:key="status.id"
|
||||||
|
class="thread-ancestor"
|
||||||
|
:class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}"
|
||||||
|
>
|
||||||
|
<status
|
||||||
|
ref="statusComponent"
|
||||||
|
:inline-expanded="collapsable && isExpanded"
|
||||||
|
:statusoid="status"
|
||||||
|
:expandable="!isExpanded"
|
||||||
|
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||||
|
:focused="focused(status.id)"
|
||||||
|
:in-conversation="isExpanded"
|
||||||
|
:highlight="getHighlight()"
|
||||||
|
:replies="getReplies(status.id)"
|
||||||
|
:in-profile="inProfile"
|
||||||
|
:profile-user-id="profileUserId"
|
||||||
|
class="conversation-status status-fadein panel-body"
|
||||||
|
|
||||||
|
:simple-tree="treeViewIsSimple"
|
||||||
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
|
:thread-display-status="threadDisplayStatus"
|
||||||
|
:show-thread-recursively="showThreadRecursively"
|
||||||
|
:total-reply-count="totalReplyCount"
|
||||||
|
:total-reply-depth="totalReplyDepth"
|
||||||
|
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
||||||
|
:dive="() => diveIntoStatus(status.id)"
|
||||||
|
|
||||||
|
:controlled-showing-tall="statusContentProperties[status.id].showingTall"
|
||||||
|
:controlled-expanding-subject="statusContentProperties[status.id].expandingSubject"
|
||||||
|
:controlled-showing-long-subject="statusContentProperties[status.id].showingLongSubject"
|
||||||
|
:controlled-replying="statusContentProperties[status.id].replying"
|
||||||
|
:controlled-media-playing="statusContentProperties[status.id].mediaPlaying"
|
||||||
|
:controlled-toggle-showing-tall="() => toggleStatusContentProperty(status.id, 'showingTall')"
|
||||||
|
:controlled-toggle-expanding-subject="() => toggleStatusContentProperty(status.id, 'expandingSubject')"
|
||||||
|
:controlled-toggle-showing-long-subject="() => toggleStatusContentProperty(status.id, 'showingLongSubject')"
|
||||||
|
:controlled-toggle-replying="() => toggleStatusContentProperty(status.id, 'replying')"
|
||||||
|
:controlled-set-media-playing="(newVal) => toggleStatusContentProperty(status.id, 'mediaPlaying', newVal)"
|
||||||
|
|
||||||
|
@goto="setHighlight"
|
||||||
|
@toggleExpanded="toggleExpanded"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
||||||
|
class="thread-ancestor-dive-box"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="thread-ancestor-dive-box-inner"
|
||||||
|
>
|
||||||
|
<i18n
|
||||||
|
tag="button"
|
||||||
|
path="status.ancestor_follow_with_icon"
|
||||||
|
class="button-unstyled -link thread-tree-show-replies-button"
|
||||||
|
@click.prevent="diveIntoStatus(status.id)"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
place="icon"
|
||||||
|
icon="angle-double-right"
|
||||||
|
/>
|
||||||
|
<span place="text">
|
||||||
|
{{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }}
|
||||||
|
</span>
|
||||||
|
</i18n>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<thread-tree
|
||||||
|
v-for="status in showingTopLevel"
|
||||||
|
:key="status.id"
|
||||||
|
ref="statusComponent"
|
||||||
|
:depth="0"
|
||||||
|
|
||||||
|
:status="status"
|
||||||
|
:in-profile="inProfile"
|
||||||
|
:conversation="conversation"
|
||||||
|
:collapsable="collapsable"
|
||||||
|
:is-expanded="isExpanded"
|
||||||
|
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||||
|
:profile-user-id="profileUserId"
|
||||||
|
|
||||||
|
:focused="focused"
|
||||||
|
:get-replies="getReplies"
|
||||||
|
:highlight="maybeHighlight"
|
||||||
|
:set-highlight="setHighlight"
|
||||||
|
:toggle-expanded="toggleExpanded"
|
||||||
|
|
||||||
|
:simple="treeViewIsSimple"
|
||||||
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
|
:thread-display-status="threadDisplayStatus"
|
||||||
|
:show-thread-recursively="showThreadRecursively"
|
||||||
|
:total-reply-count="totalReplyCount"
|
||||||
|
:total-reply-depth="totalReplyDepth"
|
||||||
|
:status-content-properties="statusContentProperties"
|
||||||
|
:set-status-content-property="setStatusContentProperty"
|
||||||
|
:toggle-status-content-property="toggleStatusContentProperty"
|
||||||
|
:dive="canDive ? diveIntoStatus : undefined"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isLinearView"
|
||||||
|
class="thread-body"
|
||||||
|
>
|
||||||
<status
|
<status
|
||||||
v-for="status in conversation"
|
v-for="status in conversation"
|
||||||
:key="status.id"
|
:key="status.id"
|
||||||
|
@ -33,10 +165,22 @@
|
||||||
:in-profile="inProfile"
|
:in-profile="inProfile"
|
||||||
:profile-user-id="profileUserId"
|
:profile-user-id="profileUserId"
|
||||||
class="conversation-status status-fadein panel-body"
|
class="conversation-status status-fadein panel-body"
|
||||||
|
|
||||||
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
|
:thread-display-status="threadDisplayStatus"
|
||||||
|
:show-thread-recursively="showThreadRecursively"
|
||||||
|
:total-reply-count="totalReplyCount"
|
||||||
|
:total-reply-depth="totalReplyDepth"
|
||||||
|
:status-content-properties="statusContentProperties"
|
||||||
|
:set-status-content-property="setStatusContentProperty"
|
||||||
|
:toggle-status-content-property="toggleStatusContentProperty"
|
||||||
|
|
||||||
@goto="setHighlight"
|
@goto="setHighlight"
|
||||||
@toggleExpanded="toggleExpanded"
|
@toggleExpanded="toggleExpanded"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
:style="hiddenStyle"
|
:style="hiddenStyle"
|
||||||
|
@ -49,6 +193,45 @@
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
.Conversation {
|
.Conversation {
|
||||||
|
.conversation-dive-to-top-level-box {
|
||||||
|
padding: var(--status-margin, $status-margin);
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-color: var(--border, $fallback--border);
|
||||||
|
border-radius: 0;
|
||||||
|
/* Make the button stretch along the whole row */
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-ancestors {
|
||||||
|
margin-left: var(--status-margin, $status-margin);
|
||||||
|
border-left: 2px solid var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-ancestor.-faded .StatusContent {
|
||||||
|
--link: var(--faintLink);
|
||||||
|
--text: var(--faint);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.thread-ancestor-dive-box {
|
||||||
|
padding-left: var(--status-margin, $status-margin);
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-bottom-color: var(--border, $fallback--border);
|
||||||
|
border-radius: 0;
|
||||||
|
/* Make the button stretch along the whole row */
|
||||||
|
&, &-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.thread-ancestor-dive-box-inner {
|
||||||
|
padding: var(--status-margin, $status-margin);
|
||||||
|
}
|
||||||
|
|
||||||
.conversation-status {
|
.conversation-status {
|
||||||
border-bottom-width: 1px;
|
border-bottom-width: 1px;
|
||||||
border-bottom-style: solid;
|
border-bottom-style: solid;
|
||||||
|
@ -56,12 +239,28 @@
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-expanded {
|
.thread-ancestor-has-other-replies .conversation-status,
|
||||||
.conversation-status:last-child {
|
.thread-ancestor:last-child .conversation-status,
|
||||||
|
.thread-ancestor:last-child .thread-ancestor-dive-box,
|
||||||
|
&.-expanded .thread-tree .conversation-status {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-ancestors + .thread-tree > .conversation-status {
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-top-style: solid;
|
||||||
|
border-top-color: var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* expanded conversation in timeline */
|
||||||
|
&.status-fadein.-expanded .thread-body {
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-left-style: solid;
|
||||||
|
border-left-color: $fallback--cRed;
|
||||||
|
border-left-color: var(--cRed, $fallback--cRed);
|
||||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||||
}
|
border-bottom: 1px solid var(--border, $fallback--border);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,32 +1,45 @@
|
||||||
import StillImage from '../still-image/still-image.vue'
|
import StillImage from '../still-image/still-image.vue'
|
||||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||||
import Modal from '../modal/modal.vue'
|
import Modal from '../modal/modal.vue'
|
||||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
import PinchZoom from '../pinch_zoom/pinch_zoom.vue'
|
||||||
|
import SwipeClick from '../swipe_click/swipe_click.vue'
|
||||||
import GestureService from '../../services/gesture_service/gesture_service'
|
import GestureService from '../../services/gesture_service/gesture_service'
|
||||||
import Flash from 'src/components/flash/flash.vue'
|
import Flash from 'src/components/flash/flash.vue'
|
||||||
|
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
faChevronRight,
|
faChevronRight,
|
||||||
faCircleNotch
|
faCircleNotch,
|
||||||
|
faTimes
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
faChevronLeft,
|
faChevronLeft,
|
||||||
faChevronRight,
|
faChevronRight,
|
||||||
faCircleNotch
|
faCircleNotch,
|
||||||
|
faTimes
|
||||||
)
|
)
|
||||||
|
|
||||||
const MediaModal = {
|
const MediaModal = {
|
||||||
components: {
|
components: {
|
||||||
StillImage,
|
StillImage,
|
||||||
VideoAttachment,
|
VideoAttachment,
|
||||||
|
PinchZoom,
|
||||||
|
SwipeClick,
|
||||||
Modal,
|
Modal,
|
||||||
Flash
|
Flash
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
loading: false
|
loading: false,
|
||||||
|
swipeDirection: GestureService.DIRECTION_LEFT,
|
||||||
|
swipeThreshold: () => {
|
||||||
|
const considerableMoveRatio = 1 / 4
|
||||||
|
return window.innerWidth * considerableMoveRatio
|
||||||
|
},
|
||||||
|
pinchZoomMinScale: 1,
|
||||||
|
pinchZoomScaleResetLimit: 1.2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -52,32 +65,26 @@ const MediaModal = {
|
||||||
return this.currentMedia ? this.getType(this.currentMedia) : null
|
return this.currentMedia ? this.getType(this.currentMedia) : null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
|
||||||
this.mediaSwipeGestureRight = GestureService.swipeGesture(
|
|
||||||
GestureService.DIRECTION_RIGHT,
|
|
||||||
this.goPrev,
|
|
||||||
50
|
|
||||||
)
|
|
||||||
this.mediaSwipeGestureLeft = GestureService.swipeGesture(
|
|
||||||
GestureService.DIRECTION_LEFT,
|
|
||||||
this.goNext,
|
|
||||||
50
|
|
||||||
)
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
getType (media) {
|
getType (media) {
|
||||||
return fileTypeService.fileType(media.mimetype)
|
return fileTypeService.fileType(media.mimetype)
|
||||||
},
|
},
|
||||||
mediaTouchStart (e) {
|
|
||||||
GestureService.beginSwipe(e, this.mediaSwipeGestureRight)
|
|
||||||
GestureService.beginSwipe(e, this.mediaSwipeGestureLeft)
|
|
||||||
},
|
|
||||||
mediaTouchMove (e) {
|
|
||||||
GestureService.updateSwipe(e, this.mediaSwipeGestureRight)
|
|
||||||
GestureService.updateSwipe(e, this.mediaSwipeGestureLeft)
|
|
||||||
},
|
|
||||||
hide () {
|
hide () {
|
||||||
|
// HACK: Closing immediately via a touch will cause the click
|
||||||
|
// to be processed on the content below the overlay
|
||||||
|
const transitionTime = 100 // ms
|
||||||
|
setTimeout(() => {
|
||||||
this.$store.dispatch('closeMediaViewer')
|
this.$store.dispatch('closeMediaViewer')
|
||||||
|
}, transitionTime)
|
||||||
|
},
|
||||||
|
hideIfNotSwiped (event) {
|
||||||
|
// If we have swiped over SwipeClick, do not trigger hide
|
||||||
|
const comp = this.$refs.swipeClick
|
||||||
|
if (!comp) {
|
||||||
|
this.hide()
|
||||||
|
} else {
|
||||||
|
comp.$gesture.click(event)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
goPrev () {
|
goPrev () {
|
||||||
if (this.canNavigate) {
|
if (this.canNavigate) {
|
||||||
|
@ -102,6 +109,17 @@ const MediaModal = {
|
||||||
onImageLoaded () {
|
onImageLoaded () {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
},
|
},
|
||||||
|
handleSwipePreview (offsets) {
|
||||||
|
this.$refs.pinchZoom.setTransform({ scale: 1, x: offsets[0], y: 0 })
|
||||||
|
},
|
||||||
|
handleSwipeEnd (sign) {
|
||||||
|
this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 })
|
||||||
|
if (sign > 0) {
|
||||||
|
this.goNext()
|
||||||
|
} else if (sign < 0) {
|
||||||
|
this.goPrev()
|
||||||
|
}
|
||||||
|
},
|
||||||
handleKeyupEvent (e) {
|
handleKeyupEvent (e) {
|
||||||
if (this.showing && e.keyCode === 27) { // escape
|
if (this.showing && e.keyCode === 27) { // escape
|
||||||
this.hide()
|
this.hide()
|
||||||
|
|
|
@ -2,20 +2,38 @@
|
||||||
<Modal
|
<Modal
|
||||||
v-if="showing"
|
v-if="showing"
|
||||||
class="media-modal-view"
|
class="media-modal-view"
|
||||||
@backdropClicked="hide"
|
@backdropClicked="hideIfNotSwiped"
|
||||||
|
>
|
||||||
|
<SwipeClick
|
||||||
|
v-if="type === 'image'"
|
||||||
|
ref="swipeClick"
|
||||||
|
class="modal-image-container"
|
||||||
|
:direction="swipeDirection"
|
||||||
|
:threshold="swipeThreshold"
|
||||||
|
@preview-requested="handleSwipePreview"
|
||||||
|
@swipe-finished="handleSwipeEnd"
|
||||||
|
@swipeless-clicked="hide"
|
||||||
|
>
|
||||||
|
<PinchZoom
|
||||||
|
ref="pinchZoom"
|
||||||
|
class="modal-image-container-inner"
|
||||||
|
selector=".modal-image"
|
||||||
|
reach-min-scale-strategy="reset"
|
||||||
|
stop-propagate-handled="stop-propgate-handled"
|
||||||
|
:allow-pan-min-scale="pinchZoomMinScale"
|
||||||
|
:min-scale="pinchZoomMinScale"
|
||||||
|
:reset-to-min-scale-limit="pinchZoomScaleResetLimit"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="type === 'image'"
|
|
||||||
:class="{ loading }"
|
:class="{ loading }"
|
||||||
class="modal-image"
|
class="modal-image"
|
||||||
:src="currentMedia.url"
|
:src="currentMedia.url"
|
||||||
:alt="currentMedia.description"
|
:alt="currentMedia.description"
|
||||||
:title="currentMedia.description"
|
:title="currentMedia.description"
|
||||||
@touchstart.stop="mediaTouchStart"
|
|
||||||
@touchmove.stop="mediaTouchMove"
|
|
||||||
@click="hide"
|
|
||||||
@load="onImageLoaded"
|
@load="onImageLoaded"
|
||||||
>
|
>
|
||||||
|
</PinchZoom>
|
||||||
|
</SwipeClick>
|
||||||
<VideoAttachment
|
<VideoAttachment
|
||||||
v-if="type === 'video'"
|
v-if="type === 'video'"
|
||||||
class="modal-image"
|
class="modal-image"
|
||||||
|
@ -40,25 +58,36 @@
|
||||||
<button
|
<button
|
||||||
v-if="canNavigate"
|
v-if="canNavigate"
|
||||||
:title="$t('media_modal.previous')"
|
:title="$t('media_modal.previous')"
|
||||||
class="modal-view-button-arrow modal-view-button-arrow--prev"
|
class="modal-view-button modal-view-button-arrow modal-view-button-arrow--prev"
|
||||||
@click.stop.prevent="goPrev"
|
@click.stop.prevent="goPrev"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
class="arrow-icon"
|
class="button-icon arrow-icon"
|
||||||
icon="chevron-left"
|
icon="chevron-left"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canNavigate"
|
v-if="canNavigate"
|
||||||
:title="$t('media_modal.next')"
|
:title="$t('media_modal.next')"
|
||||||
class="modal-view-button-arrow modal-view-button-arrow--next"
|
class="modal-view-button modal-view-button-arrow modal-view-button-arrow--next"
|
||||||
@click.stop.prevent="goNext"
|
@click.stop.prevent="goNext"
|
||||||
>
|
>
|
||||||
<FAIcon
|
<FAIcon
|
||||||
class="arrow-icon"
|
class="button-icon arrow-icon"
|
||||||
icon="chevron-right"
|
icon="chevron-right"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="modal-view-button modal-view-button-hide"
|
||||||
|
:title="$t('media_modal.hide')"
|
||||||
|
@click.stop.prevent="hide"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
class="button-icon"
|
||||||
|
icon="times"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
v-if="description"
|
v-if="description"
|
||||||
class="description"
|
class="description"
|
||||||
|
@ -86,11 +115,17 @@
|
||||||
<script src="./media_modal.js"></script>
|
<script src="./media_modal.js"></script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
$modal-view-button-icon-height: 3em;
|
||||||
|
$modal-view-button-icon-half-height: calc(#{$modal-view-button-icon-height} / 2);
|
||||||
|
$modal-view-button-icon-width: 3em;
|
||||||
|
$modal-view-button-icon-margin: 0.5em;
|
||||||
|
|
||||||
.modal-view.media-modal-view {
|
.modal-view.media-modal-view {
|
||||||
z-index: 1001;
|
z-index: 1001;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
.modal-view-button-arrow {
|
.modal-view-button-arrow,
|
||||||
|
.modal-view-button-hide {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
|
@ -103,6 +138,7 @@
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-modal-view {
|
.media-modal-view {
|
||||||
|
@ -115,6 +151,29 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-image-container {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&-inner {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.description,
|
.description,
|
||||||
.counter {
|
.counter {
|
||||||
/* Hardcoded since background is also hardcoded */
|
/* Hardcoded since background is also hardcoded */
|
||||||
|
@ -134,9 +193,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-image {
|
.modal-image {
|
||||||
max-width: 90%;
|
max-width: 100%;
|
||||||
max-height: 90%;
|
max-height: 100%;
|
||||||
box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5);
|
|
||||||
image-orientation: from-image; // NOTE: only FF supports this
|
image-orientation: from-image; // NOTE: only FF supports this
|
||||||
animation: 0.1s cubic-bezier(0.7, 0, 1, 0.6) media-fadein;
|
animation: 0.1s cubic-bezier(0.7, 0, 1, 0.6) media-fadein;
|
||||||
|
|
||||||
|
@ -159,13 +217,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-view-button-arrow {
|
.modal-view-button {
|
||||||
position: absolute;
|
|
||||||
display: block;
|
|
||||||
top: 50%;
|
|
||||||
margin-top: -50px;
|
|
||||||
width: 70px;
|
|
||||||
height: 100px;
|
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
@ -175,14 +227,33 @@
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity 333ms cubic-bezier(.4,0,.22,1);
|
transition: opacity 333ms cubic-bezier(.4,0,.22,1);
|
||||||
|
height: $modal-view-button-icon-height;
|
||||||
|
width: $modal-view-button-icon-width;
|
||||||
|
|
||||||
|
.button-icon {
|
||||||
|
position: absolute;
|
||||||
|
height: $modal-view-button-icon-height;
|
||||||
|
width: $modal-view-button-icon-width;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: $modal-view-button-icon-height;
|
||||||
|
color: #FFF;
|
||||||
|
text-align: center;
|
||||||
|
background-color: rgba(0,0,0,.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-view-button-arrow {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
top: 50%;
|
||||||
|
margin-top: $modal-view-button-icon-half-height;
|
||||||
|
width: $modal-view-button-icon-width;
|
||||||
|
height: $modal-view-button-icon-height;
|
||||||
|
|
||||||
.arrow-icon {
|
.arrow-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 35px;
|
top: 0;
|
||||||
height: 30px;
|
line-height: $modal-view-button-icon-height;
|
||||||
width: 32px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 30px;
|
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: rgba(0,0,0,.3);
|
background-color: rgba(0,0,0,.3);
|
||||||
|
@ -191,16 +262,26 @@
|
||||||
&--prev {
|
&--prev {
|
||||||
left: 0;
|
left: 0;
|
||||||
.arrow-icon {
|
.arrow-icon {
|
||||||
left: 6px;
|
left: $modal-view-button-icon-margin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--next {
|
&--next {
|
||||||
right: 0;
|
right: 0;
|
||||||
.arrow-icon {
|
.arrow-icon {
|
||||||
right: 6px;
|
right: $modal-view-button-icon-margin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-view-button-hide {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
.button-icon {
|
||||||
|
top: $modal-view-button-icon-margin;
|
||||||
|
right: $modal-view-button-icon-margin;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -65,7 +65,6 @@
|
||||||
color: var(--link);
|
color: var(--link);
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 50%;
|
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding: 0 0.1em;
|
padding: 0 0.1em;
|
||||||
vertical-align: -25%;
|
vertical-align: -25%;
|
||||||
|
|
13
src/components/pinch_zoom/pinch_zoom.js
Normal file
13
src/components/pinch_zoom/pinch_zoom.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import PinchZoom from '@kazvmoe-infra/pinch-zoom-element'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
setTransform ({ scale, x, y }) {
|
||||||
|
this.$el.setTransform({ scale, x, y })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
// Make lint happy
|
||||||
|
(() => PinchZoom)()
|
||||||
|
}
|
||||||
|
}
|
11
src/components/pinch_zoom/pinch_zoom.vue
Normal file
11
src/components/pinch_zoom/pinch_zoom.vue
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<template>
|
||||||
|
<pinch-zoom
|
||||||
|
class="pinch-zoom-parent"
|
||||||
|
v-bind="$attrs"
|
||||||
|
v-on="$listeners"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</pinch-zoom>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./pinch_zoom.js"></script>
|
|
@ -51,6 +51,7 @@
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
width: 0.875em;
|
||||||
color: $fallback--text;
|
color: $fallback--text;
|
||||||
color: var(--inputText, $fallback--text);
|
color: var(--inputText, $fallback--text);
|
||||||
line-height: 28px;
|
line-height: 28px;
|
||||||
|
|
|
@ -1,14 +1,17 @@
|
||||||
import { get, set } from 'lodash'
|
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 ModifiedIndicator from './modified_indicator.vue'
|
||||||
|
import ServerSideIndicator from './server_side_indicator.vue'
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Checkbox,
|
Checkbox,
|
||||||
ModifiedIndicator
|
ModifiedIndicator,
|
||||||
|
ServerSideIndicator
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
'path',
|
'path',
|
||||||
'disabled'
|
'disabled',
|
||||||
|
'expert'
|
||||||
],
|
],
|
||||||
computed: {
|
computed: {
|
||||||
pathDefault () {
|
pathDefault () {
|
||||||
|
@ -26,8 +29,14 @@ export default {
|
||||||
defaultState () {
|
defaultState () {
|
||||||
return get(this.$parent, this.pathDefault)
|
return get(this.$parent, this.pathDefault)
|
||||||
},
|
},
|
||||||
|
isServerSide () {
|
||||||
|
return this.path.startsWith('serverSide_')
|
||||||
|
},
|
||||||
isChanged () {
|
isChanged () {
|
||||||
return this.state !== this.defaultState
|
return !this.path.startsWith('serverSide_') && this.state !== this.defaultState
|
||||||
|
},
|
||||||
|
matchesExpertLevel () {
|
||||||
|
return (this.expert || 0) <= this.$parent.expertLevel
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<label
|
<label
|
||||||
|
v-if="matchesExpertLevel"
|
||||||
class="BooleanSetting"
|
class="BooleanSetting"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
@ -13,8 +14,7 @@
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</span>
|
</span>
|
||||||
<ModifiedIndicator :changed="isChanged" />
|
<ModifiedIndicator :changed="isChanged" /><ServerSideIndicator :server-side="isServerSide" /> </Checkbox>
|
||||||
</Checkbox>
|
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
import { get, set } from 'lodash'
|
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 ModifiedIndicator from './modified_indicator.vue'
|
||||||
|
import ServerSideIndicator from './server_side_indicator.vue'
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Select,
|
Select,
|
||||||
ModifiedIndicator
|
ModifiedIndicator,
|
||||||
|
ServerSideIndicator
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
'path',
|
'path',
|
||||||
'disabled',
|
'disabled',
|
||||||
'options'
|
'options',
|
||||||
|
'expert'
|
||||||
],
|
],
|
||||||
computed: {
|
computed: {
|
||||||
pathDefault () {
|
pathDefault () {
|
||||||
|
@ -27,8 +30,14 @@ export default {
|
||||||
defaultState () {
|
defaultState () {
|
||||||
return get(this.$parent, this.pathDefault)
|
return get(this.$parent, this.pathDefault)
|
||||||
},
|
},
|
||||||
|
isServerSide () {
|
||||||
|
return this.path.startsWith('serverSide_')
|
||||||
|
},
|
||||||
isChanged () {
|
isChanged () {
|
||||||
return this.state !== this.defaultState
|
return !this.path.startsWith('serverSide_') && this.state !== this.defaultState
|
||||||
|
},
|
||||||
|
matchesExpertLevel () {
|
||||||
|
return (this.expert || 0) <= this.$parent.expertLevel
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<label
|
<label
|
||||||
|
v-if="matchesExpertLevel"
|
||||||
class="ChoiceSetting"
|
class="ChoiceSetting"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
|
@ -18,6 +19,7 @@
|
||||||
</option>
|
</option>
|
||||||
</Select>
|
</Select>
|
||||||
<ModifiedIndicator :changed="isChanged" />
|
<ModifiedIndicator :changed="isChanged" />
|
||||||
|
<ServerSideIndicator :server-side="isServerSide" />
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
41
src/components/settings_modal/helpers/integer_setting.js
Normal file
41
src/components/settings_modal/helpers/integer_setting.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import { get, set } from 'lodash'
|
||||||
|
import ModifiedIndicator from './modified_indicator.vue'
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ModifiedIndicator
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
path: String,
|
||||||
|
disabled: Boolean,
|
||||||
|
min: Number,
|
||||||
|
expert: Number
|
||||||
|
},
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
src/components/settings_modal/helpers/integer_setting.vue
Normal file
23
src/components/settings_modal/helpers/integer_setting.vue
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
v-if="matchesExpertLevel"
|
||||||
|
class="IntegerSetting"
|
||||||
|
>
|
||||||
|
<label :for="path">
|
||||||
|
<slot />
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
:id="path"
|
||||||
|
class="number-input"
|
||||||
|
type="number"
|
||||||
|
step="1"
|
||||||
|
:disabled="disabled"
|
||||||
|
:min="min || 0"
|
||||||
|
:value="state"
|
||||||
|
@change="update"
|
||||||
|
>
|
||||||
|
<ModifiedIndicator :changed="isChanged" />
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./integer_setting.js"></script>
|
|
@ -0,0 +1,51 @@
|
||||||
|
<template>
|
||||||
|
<span
|
||||||
|
v-if="serverSide"
|
||||||
|
class="ServerSideIndicator"
|
||||||
|
>
|
||||||
|
<Popover
|
||||||
|
trigger="hover"
|
||||||
|
>
|
||||||
|
<template v-slot:trigger>
|
||||||
|
|
||||||
|
<FAIcon
|
||||||
|
icon="server"
|
||||||
|
:aria-label="$t('settings.setting_server_side')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-slot:content>
|
||||||
|
<div class="serverside-tooltip">
|
||||||
|
{{ $t('settings.setting_server_side') }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import { faServer } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faServer
|
||||||
|
)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { Popover },
|
||||||
|
props: ['serverSide']
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.ServerSideIndicator {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.serverside-tooltip {
|
||||||
|
margin: 0.5em 1em;
|
||||||
|
min-width: 10em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,4 +1,5 @@
|
||||||
import { defaultState as configDefaultState } from 'src/modules/config.js'
|
import { defaultState as configDefaultState } from 'src/modules/config.js'
|
||||||
|
import { defaultState as serverSideConfigDefaultState } from 'src/modules/serverSideConfig.js'
|
||||||
|
|
||||||
const SharedComputedObject = () => ({
|
const SharedComputedObject = () => ({
|
||||||
user () {
|
user () {
|
||||||
|
@ -22,6 +23,14 @@ const SharedComputedObject = () => ({
|
||||||
}
|
}
|
||||||
}])
|
}])
|
||||||
.reduce((acc, [key, value]) => ({ ...acc, [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)
|
// Special cases (need to transform values or perform actions first)
|
||||||
useStreamingApi: {
|
useStreamingApi: {
|
||||||
get () { return this.$store.getters.mergedConfig.useStreamingApi },
|
get () { return this.$store.getters.mergedConfig.useStreamingApi },
|
||||||
|
|
|
@ -3,6 +3,7 @@ import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
|
||||||
import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
|
import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue'
|
||||||
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
|
import getResettableAsyncComponent from 'src/services/resettable_async_component.js'
|
||||||
import Popover from '../popover/popover.vue'
|
import Popover from '../popover/popover.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 } from 'lodash'
|
||||||
import {
|
import {
|
||||||
|
@ -51,6 +52,7 @@ const SettingsModal = {
|
||||||
components: {
|
components: {
|
||||||
Modal,
|
Modal,
|
||||||
Popover,
|
Popover,
|
||||||
|
Checkbox,
|
||||||
SettingsModalContent: getResettableAsyncComponent(
|
SettingsModalContent: getResettableAsyncComponent(
|
||||||
() => import('./settings_modal_content.vue'),
|
() => import('./settings_modal_content.vue'),
|
||||||
{
|
{
|
||||||
|
@ -159,6 +161,15 @@ const SettingsModal = {
|
||||||
},
|
},
|
||||||
modalPeeked () {
|
modalPeeked () {
|
||||||
return this.$store.state.interface.settingsModalState === 'minimized'
|
return this.$store.state.interface.settingsModalState === 'minimized'
|
||||||
|
},
|
||||||
|
expertLevel: {
|
||||||
|
get () {
|
||||||
|
return this.$store.state.config.expertLevel > 0
|
||||||
|
},
|
||||||
|
set (value) {
|
||||||
|
console.log(value)
|
||||||
|
this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,4 +48,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-footer {
|
||||||
|
display: flex;
|
||||||
|
>* {
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<SettingsModalContent v-if="modalOpenedOnce" />
|
<SettingsModalContent v-if="modalOpenedOnce" />
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-footer">
|
<div class="panel-footer settings-footer">
|
||||||
<Popover
|
<Popover
|
||||||
class="export"
|
class="export"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
|
@ -108,6 +108,10 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
|
<Checkbox v-model="expertLevel">
|
||||||
|
{{ $t("settings.expert_mode") }}
|
||||||
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { filter, trim } from 'lodash'
|
import { filter, trim } from 'lodash'
|
||||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||||
|
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||||
|
|
||||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
|
|
||||||
|
@ -17,7 +18,8 @@ const FilteringTab = {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
BooleanSetting,
|
BooleanSetting,
|
||||||
ChoiceSetting
|
ChoiceSetting,
|
||||||
|
IntegerSetting
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...SharedComputedObject(),
|
...SharedComputedObject(),
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
|
v-if="user"
|
||||||
:disabled="hideFilteredStatuses"
|
:disabled="hideFilteredStatuses"
|
||||||
path="hideMutedThreads"
|
path="hideMutedThreads"
|
||||||
>
|
>
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
|
v-if="user"
|
||||||
:disabled="hideFilteredStatuses"
|
:disabled="hideFilteredStatuses"
|
||||||
path="hideMutedPosts"
|
path="hideMutedPosts"
|
||||||
>
|
>
|
||||||
|
@ -37,12 +39,23 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="muteBotStatuses">
|
||||||
|
{{ $t('settings.mute_bot_posts') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="hidePostStats">
|
<BooleanSetting path="hidePostStats">
|
||||||
{{ $t('settings.hide_post_stats') }}
|
{{ $t('settings.hide_post_stats') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="hideBotIndication">
|
||||||
|
{{ $t('settings.hide_bot_indication') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
<ChoiceSetting
|
<ChoiceSetting
|
||||||
|
v-if="user"
|
||||||
id="replyVisibility"
|
id="replyVisibility"
|
||||||
path="replyVisibility"
|
path="replyVisibility"
|
||||||
:options="replyVisibilityOptions"
|
:options="replyVisibilityOptions"
|
||||||
|
@ -59,7 +72,7 @@
|
||||||
<div>{{ $t('settings.filtering_explanation') }}</div>
|
<div>{{ $t('settings.filtering_explanation') }}</div>
|
||||||
</li>
|
</li>
|
||||||
<h3>{{ $t('settings.attachments') }}</h3>
|
<h3>{{ $t('settings.attachments') }}</h3>
|
||||||
<li>
|
<li v-if="expertLevel > 0">
|
||||||
<label for="maxThumbnails">
|
<label for="maxThumbnails">
|
||||||
{{ $t('settings.max_thumbnails') }}
|
{{ $t('settings.max_thumbnails') }}
|
||||||
</label>
|
</label>
|
||||||
|
@ -72,6 +85,14 @@
|
||||||
step="1"
|
step="1"
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
path="maxThumbnails"
|
||||||
|
:min="0"
|
||||||
|
>
|
||||||
|
{{ $t('settings.max_thumbnails') }}
|
||||||
|
</IntegerSetting>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="hideAttachments">
|
<BooleanSetting path="hideAttachments">
|
||||||
{{ $t('settings.hide_attachments_in_tl') }}
|
{{ $t('settings.hide_attachments_in_tl') }}
|
||||||
|
@ -84,7 +105,10 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div
|
||||||
|
v-if="expertLevel > 0"
|
||||||
|
class="setting-item"
|
||||||
|
>
|
||||||
<h2>{{ $t('settings.user_profiles') }}</h2>
|
<h2>{{ $t('settings.user_profiles') }}</h2>
|
||||||
<ul class="setting-list">
|
<ul class="setting-list">
|
||||||
<li>
|
<li>
|
||||||
|
@ -94,46 +118,6 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.notifications') }}</h2>
|
|
||||||
<ul class="setting-list">
|
|
||||||
<li class="select-multiple">
|
|
||||||
<span class="label">{{ $t('settings.notification_visibility') }}</span>
|
|
||||||
<ul class="option-list">
|
|
||||||
<li>
|
|
||||||
<BooleanSetting path="notificationVisibility.likes">
|
|
||||||
{{ $t('settings.notification_visibility_likes') }}
|
|
||||||
</BooleanSetting>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<BooleanSetting path="notificationVisibility.repeats">
|
|
||||||
{{ $t('settings.notification_visibility_repeats') }}
|
|
||||||
</BooleanSetting>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<BooleanSetting path="notificationVisibility.follows">
|
|
||||||
{{ $t('settings.notification_visibility_follows') }}
|
|
||||||
</BooleanSetting>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<BooleanSetting path="notificationVisibility.mentions">
|
|
||||||
{{ $t('settings.notification_visibility_mentions') }}
|
|
||||||
</BooleanSetting>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<BooleanSetting path="notificationVisibility.moves">
|
|
||||||
{{ $t('settings.notification_visibility_moves') }}
|
|
||||||
</BooleanSetting>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<BooleanSetting path="notificationVisibility.emojiReactions">
|
|
||||||
{{ $t('settings.notification_visibility_emoji_reactions') }}
|
|
||||||
</BooleanSetting>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script src="./filtering_tab.js"></script>
|
<script src="./filtering_tab.js"></script>
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
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 IntegerSetting from '../helpers/integer_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 { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faGlobe
|
faGlobe
|
||||||
|
@ -20,6 +23,16 @@ const GeneralTab = {
|
||||||
value: mode,
|
value: mode,
|
||||||
label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`)
|
label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`)
|
||||||
})),
|
})),
|
||||||
|
conversationDisplayOptions: ['tree', 'linear'].map(mode => ({
|
||||||
|
key: mode,
|
||||||
|
value: mode,
|
||||||
|
label: this.$t(`settings.conversation_display_${mode}`)
|
||||||
|
})),
|
||||||
|
conversationOtherRepliesButtonOptions: ['below', 'inside'].map(mode => ({
|
||||||
|
key: mode,
|
||||||
|
value: mode,
|
||||||
|
label: this.$t(`settings.conversation_other_replies_button_${mode}`)
|
||||||
|
})),
|
||||||
mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({
|
mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({
|
||||||
key: mode,
|
key: mode,
|
||||||
value: mode,
|
value: mode,
|
||||||
|
@ -37,7 +50,10 @@ const GeneralTab = {
|
||||||
components: {
|
components: {
|
||||||
BooleanSetting,
|
BooleanSetting,
|
||||||
ChoiceSetting,
|
ChoiceSetting,
|
||||||
InterfaceLanguageSwitcher
|
IntegerSetting,
|
||||||
|
InterfaceLanguageSwitcher,
|
||||||
|
ScopeSelector,
|
||||||
|
ServerSideIndicator
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
postFormats () {
|
postFormats () {
|
||||||
|
@ -57,6 +73,11 @@ const GeneralTab = {
|
||||||
},
|
},
|
||||||
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
|
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
|
||||||
...SharedComputedObject()
|
...SharedComputedObject()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeDefaultScope (value) {
|
||||||
|
this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,26 +45,42 @@
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="useStreamingApi">
|
<BooleanSetting
|
||||||
|
path="useStreamingApi"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.useStreamingApi') }}
|
{{ $t('settings.useStreamingApi') }}
|
||||||
<br>
|
|
||||||
<small>
|
|
||||||
{{ $t('settings.useStreamingApiWarning') }}
|
|
||||||
</small>
|
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="virtualScrolling">
|
<BooleanSetting
|
||||||
|
path="virtualScrolling"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.virtual_scrolling') }}
|
{{ $t('settings.virtual_scrolling') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="autohideFloatingPostButton">
|
<BooleanSetting
|
||||||
|
path="alwaysShowNewPostButton"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
|
{{ $t('settings.always_show_post_button') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path="autohideFloatingPostButton"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.autohide_floating_post_button') }}
|
{{ $t('settings.autohide_floating_post_button') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="instanceShoutboxPresent">
|
<li v-if="instanceShoutboxPresent">
|
||||||
<BooleanSetting path="hideShoutbox">
|
<BooleanSetting
|
||||||
|
path="hideShoutbox"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.hide_shoutbox') }}
|
{{ $t('settings.hide_shoutbox') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
|
@ -73,19 +89,80 @@
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{ $t('settings.post_look_feel') }}</h2>
|
<h2>{{ $t('settings.post_look_feel') }}</h2>
|
||||||
<ul class="setting-list">
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<ChoiceSetting
|
||||||
|
id="conversationDisplay"
|
||||||
|
path="conversationDisplay"
|
||||||
|
:options="conversationDisplayOptions"
|
||||||
|
>
|
||||||
|
{{ $t('settings.conversation_display') }}
|
||||||
|
</ChoiceSetting>
|
||||||
|
</li>
|
||||||
|
<ul
|
||||||
|
v-if="conversationDisplay !== 'linear'"
|
||||||
|
class="setting-list suboptions"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="conversationTreeAdvanced">
|
||||||
|
{{ $t('settings.tree_advanced') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path="conversationTreeFadeAncestors"
|
||||||
|
:expert="1"
|
||||||
|
>
|
||||||
|
{{ $t('settings.tree_fade_ancestors') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<IntegerSetting
|
||||||
|
path="maxDepthInThread"
|
||||||
|
:min="3"
|
||||||
|
:expert="1"
|
||||||
|
>
|
||||||
|
{{ $t('settings.max_depth_in_thread') }}
|
||||||
|
</IntegerSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ChoiceSetting
|
||||||
|
id="conversationOtherRepliesButton"
|
||||||
|
path="conversationOtherRepliesButton"
|
||||||
|
:options="conversationOtherRepliesButtonOptions"
|
||||||
|
:expert="1"
|
||||||
|
>
|
||||||
|
{{ $t('settings.conversation_other_replies_button') }}
|
||||||
|
</ChoiceSetting>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="collapseMessageWithSubject">
|
<BooleanSetting path="collapseMessageWithSubject">
|
||||||
{{ $t('settings.collapse_subject') }}
|
{{ $t('settings.collapse_subject') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="emojiReactionsOnTimeline">
|
<BooleanSetting
|
||||||
|
path="emojiReactionsOnTimeline"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.emoji_reactions_on_timeline') }}
|
{{ $t('settings.emoji_reactions_on_timeline') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
v-if="user"
|
||||||
|
path="serverSide_stripRichContent"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
|
{{ $t('settings.no_rich_text_description') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
<h3>{{ $t('settings.attachments') }}</h3>
|
<h3>{{ $t('settings.attachments') }}</h3>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="useContainFit">
|
<BooleanSetting
|
||||||
|
path="useContainFit"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.use_contain_fit') }}
|
{{ $t('settings.use_contain_fit') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
|
@ -98,6 +175,7 @@
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="preloadImage"
|
path="preloadImage"
|
||||||
|
expert="1"
|
||||||
:disabled="!hideNsfw"
|
:disabled="!hideNsfw"
|
||||||
>
|
>
|
||||||
{{ $t('settings.preload_images') }}
|
{{ $t('settings.preload_images') }}
|
||||||
|
@ -106,6 +184,7 @@
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="useOneClickNsfw"
|
path="useOneClickNsfw"
|
||||||
|
expert="1"
|
||||||
:disabled="!hideNsfw"
|
:disabled="!hideNsfw"
|
||||||
>
|
>
|
||||||
{{ $t('settings.use_one_click_nsfw') }}
|
{{ $t('settings.use_one_click_nsfw') }}
|
||||||
|
@ -113,7 +192,10 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="loopVideo">
|
<BooleanSetting
|
||||||
|
path="loopVideo"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.loop_video') }}
|
{{ $t('settings.loop_video') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
<ul
|
<ul
|
||||||
|
@ -123,6 +205,7 @@
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting
|
<BooleanSetting
|
||||||
path="loopVideoSilentOnly"
|
path="loopVideoSilentOnly"
|
||||||
|
expert="1"
|
||||||
:disabled="!loopVideo || !loopSilentAvailable"
|
:disabled="!loopVideo || !loopSilentAvailable"
|
||||||
>
|
>
|
||||||
{{ $t('settings.loop_video_silent_only') }}
|
{{ $t('settings.loop_video_silent_only') }}
|
||||||
|
@ -137,21 +220,14 @@
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="playVideosInModal">
|
<BooleanSetting
|
||||||
|
path="playVideosInModal"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.play_videos_in_modal') }}
|
{{ $t('settings.play_videos_in_modal') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<h3>{{ $t('settings.fun') }}</h3>
|
<h3>{{ $t('settings.mention_links') }}</h3>
|
||||||
<li>
|
|
||||||
<BooleanSetting path="greentext">
|
|
||||||
{{ $t('settings.greentext') }}
|
|
||||||
</BooleanSetting>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<BooleanSetting path="mentionLinkShowYous">
|
|
||||||
{{ $t('settings.show_yous') }}
|
|
||||||
</BooleanSetting>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<ChoiceSetting
|
<ChoiceSetting
|
||||||
id="mentionLinkDisplay"
|
id="mentionLinkDisplay"
|
||||||
|
@ -164,15 +240,20 @@
|
||||||
<ul
|
<ul
|
||||||
class="setting-list suboptions"
|
class="setting-list suboptions"
|
||||||
>
|
>
|
||||||
<li
|
<li v-if="mentionLinkDisplay === 'short'">
|
||||||
v-if="mentionLinkDisplay === 'short'"
|
<BooleanSetting
|
||||||
|
path="mentionLinkShowTooltip"
|
||||||
|
expert="1"
|
||||||
>
|
>
|
||||||
<BooleanSetting path="mentionLinkShowTooltip">
|
|
||||||
{{ $t('settings.mention_link_show_tooltip') }}
|
{{ $t('settings.mention_link_show_tooltip') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
|
</ul>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="useAtIcon">
|
<BooleanSetting
|
||||||
|
path="useAtIcon"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.use_at_icon') }}
|
{{ $t('settings.use_at_icon') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
|
@ -182,29 +263,80 @@
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="mentionLinkFadeDomain">
|
<BooleanSetting
|
||||||
|
path="mentionLinkFadeDomain"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.mention_link_fade_domain') }}
|
{{ $t('settings.mention_link_fade_domain') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li v-if="user">
|
||||||
<BooleanSetting path="mentionLinkBoldenYou">
|
<BooleanSetting
|
||||||
|
path="mentionLinkBoldenYou"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.mention_link_bolden_you') }}
|
{{ $t('settings.mention_link_bolden_you') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
<h3 v-if="expertLevel > 0">
|
||||||
|
{{ $t('settings.fun') }}
|
||||||
|
</h3>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path="greentext"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
|
{{ $t('settings.greentext') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li v-if="user">
|
||||||
|
<BooleanSetting
|
||||||
|
path="mentionLinkShowYous"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
|
{{ $t('settings.show_yous') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setting-item">
|
<div
|
||||||
|
v-if="user"
|
||||||
|
class="setting-item"
|
||||||
|
>
|
||||||
<h2>{{ $t('settings.composing') }}</h2>
|
<h2>{{ $t('settings.composing') }}</h2>
|
||||||
<ul class="setting-list">
|
<ul class="setting-list">
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="scopeCopy">
|
<label for="default-vis">
|
||||||
|
{{ $t('settings.default_vis') }} <ServerSideIndicator :server-side="true" />
|
||||||
|
<ScopeSelector
|
||||||
|
class="scope-selector"
|
||||||
|
:show-all="true"
|
||||||
|
:user-default="serverSide_defaultScope"
|
||||||
|
:initial-scope="serverSide_defaultScope"
|
||||||
|
:on-scope-change="changeDefaultScope"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<!-- <BooleanSetting path="serverSide_defaultNSFW"> -->
|
||||||
|
<BooleanSetting path="sensitiveByDefault">
|
||||||
|
{{ $t('settings.sensitive_by_default') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path="scopeCopy"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.scope_copy') }}
|
{{ $t('settings.scope_copy') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="alwaysShowSubjectInput">
|
<BooleanSetting
|
||||||
|
path="alwaysShowSubjectInput"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.subject_input_always_show') }}
|
{{ $t('settings.subject_input_always_show') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
|
@ -213,6 +345,7 @@
|
||||||
id="subjectLineBehavior"
|
id="subjectLineBehavior"
|
||||||
path="subjectLineBehavior"
|
path="subjectLineBehavior"
|
||||||
:options="subjectLineOptions"
|
:options="subjectLineOptions"
|
||||||
|
expert="1"
|
||||||
>
|
>
|
||||||
{{ $t('settings.subject_line_behavior') }}
|
{{ $t('settings.subject_line_behavior') }}
|
||||||
</ChoiceSetting>
|
</ChoiceSetting>
|
||||||
|
@ -227,43 +360,39 @@
|
||||||
</ChoiceSetting>
|
</ChoiceSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="minimalScopesMode">
|
<BooleanSetting
|
||||||
|
path="minimalScopesMode"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.minimal_scopes_mode') }}
|
{{ $t('settings.minimal_scopes_mode') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="sensitiveByDefault">
|
<BooleanSetting
|
||||||
{{ $t('settings.sensitive_by_default') }}
|
path="alwaysShowNewPostButton"
|
||||||
</BooleanSetting>
|
expert="1"
|
||||||
</li>
|
>
|
||||||
<li>
|
|
||||||
<BooleanSetting path="alwaysShowNewPostButton">
|
|
||||||
{{ $t('settings.always_show_post_button') }}
|
{{ $t('settings.always_show_post_button') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="autohideFloatingPostButton">
|
<BooleanSetting
|
||||||
|
path="autohideFloatingPostButton"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.autohide_floating_post_button') }}
|
{{ $t('settings.autohide_floating_post_button') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="padEmoji">
|
<BooleanSetting
|
||||||
|
path="padEmoji"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.pad_emoji') }}
|
{{ $t('settings.pad_emoji') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setting-item">
|
|
||||||
<h2>{{ $t('settings.notifications') }}</h2>
|
|
||||||
<ul class="setting-list">
|
|
||||||
<li>
|
|
||||||
<BooleanSetting path="webPushNotifications">
|
|
||||||
{{ $t('settings.enable_web_push_notifications') }}
|
|
||||||
</BooleanSetting>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||||
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
|
|
||||||
const NotificationsTab = {
|
const NotificationsTab = {
|
||||||
data () {
|
data () {
|
||||||
|
@ -9,12 +10,13 @@ const NotificationsTab = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Checkbox
|
BooleanSetting
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
user () {
|
user () {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
}
|
},
|
||||||
|
...SharedComputedObject()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateNotificationSettings () {
|
updateNotificationSettings () {
|
||||||
|
|
|
@ -2,30 +2,77 @@
|
||||||
<div :label="$t('settings.notifications')">
|
<div :label="$t('settings.notifications')">
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
|
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
|
||||||
<p>
|
<ul class="setting-list">
|
||||||
<Checkbox v-model="notificationSettings.block_from_strangers">
|
<li>
|
||||||
|
<BooleanSetting path="serverSide_blockNotificationsFromStrangers">
|
||||||
{{ $t('settings.notification_setting_block_from_strangers') }}
|
{{ $t('settings.notification_setting_block_from_strangers') }}
|
||||||
</Checkbox>
|
</BooleanSetting>
|
||||||
</p>
|
</li>
|
||||||
|
<li class="select-multiple">
|
||||||
|
<span class="label">{{ $t('settings.notification_visibility') }}</span>
|
||||||
|
<ul class="option-list">
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="notificationVisibility.likes">
|
||||||
|
{{ $t('settings.notification_visibility_likes') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="notificationVisibility.repeats">
|
||||||
|
{{ $t('settings.notification_visibility_repeats') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="notificationVisibility.follows">
|
||||||
|
{{ $t('settings.notification_visibility_follows') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="notificationVisibility.mentions">
|
||||||
|
{{ $t('settings.notification_visibility_mentions') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="notificationVisibility.moves">
|
||||||
|
{{ $t('settings.notification_visibility_moves') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="notificationVisibility.emojiReactions">
|
||||||
|
{{ $t('settings.notification_visibility_emoji_reactions') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="setting-item">
|
<div
|
||||||
|
v-if="expertLevel > 0"
|
||||||
|
class="setting-item"
|
||||||
|
>
|
||||||
<h2>{{ $t('settings.notification_setting_privacy') }}</h2>
|
<h2>{{ $t('settings.notification_setting_privacy') }}</h2>
|
||||||
<p>
|
<ul class="setting-list">
|
||||||
<Checkbox v-model="notificationSettings.hide_notification_contents">
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path="webPushNotifications"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
|
{{ $t('settings.enable_web_push_notifications') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path="serverSide_webPushHideContents"
|
||||||
|
expert="1"
|
||||||
|
>
|
||||||
{{ $t('settings.notification_setting_hide_notification_contents') }}
|
{{ $t('settings.notification_setting_hide_notification_contents') }}
|
||||||
</Checkbox>
|
</BooleanSetting>
|
||||||
</p>
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<p>{{ $t('settings.notification_mutes') }}</p>
|
<p>{{ $t('settings.notification_mutes') }}</p>
|
||||||
<p>{{ $t('settings.notification_blocks') }}</p>
|
<p>{{ $t('settings.notification_blocks') }}</p>
|
||||||
<button
|
|
||||||
class="btn button-default"
|
|
||||||
@click="updateNotificationSettings"
|
|
||||||
>
|
|
||||||
{{ $t('settings.save') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -8,6 +8,9 @@ import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
|
||||||
import suggestor from 'src/components/emoji_input/suggestor.js'
|
import suggestor from 'src/components/emoji_input/suggestor.js'
|
||||||
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
||||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||||
|
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||||
|
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
faTimes,
|
faTimes,
|
||||||
|
@ -27,18 +30,10 @@ const ProfileTab = {
|
||||||
newName: this.$store.state.users.currentUser.name_unescaped,
|
newName: this.$store.state.users.currentUser.name_unescaped,
|
||||||
newBio: unescape(this.$store.state.users.currentUser.description),
|
newBio: unescape(this.$store.state.users.currentUser.description),
|
||||||
newLocked: this.$store.state.users.currentUser.locked,
|
newLocked: this.$store.state.users.currentUser.locked,
|
||||||
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
|
|
||||||
newDefaultScope: this.$store.state.users.currentUser.default_scope,
|
|
||||||
newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })),
|
newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })),
|
||||||
hideFollows: this.$store.state.users.currentUser.hide_follows,
|
|
||||||
hideFollowers: this.$store.state.users.currentUser.hide_followers,
|
|
||||||
hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
|
|
||||||
hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count,
|
|
||||||
showRole: this.$store.state.users.currentUser.show_role,
|
showRole: this.$store.state.users.currentUser.show_role,
|
||||||
role: this.$store.state.users.currentUser.role,
|
role: this.$store.state.users.currentUser.role,
|
||||||
discoverable: this.$store.state.users.currentUser.discoverable,
|
|
||||||
bot: this.$store.state.users.currentUser.bot,
|
bot: this.$store.state.users.currentUser.bot,
|
||||||
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
|
||||||
pickAvatarBtnVisible: true,
|
pickAvatarBtnVisible: true,
|
||||||
bannerUploading: false,
|
bannerUploading: false,
|
||||||
backgroundUploading: false,
|
backgroundUploading: false,
|
||||||
|
@ -54,12 +49,14 @@ const ProfileTab = {
|
||||||
EmojiInput,
|
EmojiInput,
|
||||||
Autosuggest,
|
Autosuggest,
|
||||||
ProgressButton,
|
ProgressButton,
|
||||||
Checkbox
|
Checkbox,
|
||||||
|
BooleanSetting
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
user () {
|
user () {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
|
...SharedComputedObject(),
|
||||||
emojiUserSuggestor () {
|
emojiUserSuggestor () {
|
||||||
return suggestor({
|
return suggestor({
|
||||||
emoji: [
|
emoji: [
|
||||||
|
@ -123,15 +120,7 @@ const ProfileTab = {
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
display_name: this.newName,
|
display_name: this.newName,
|
||||||
fields_attributes: this.newFields.filter(el => el != null),
|
fields_attributes: this.newFields.filter(el => el != null),
|
||||||
default_scope: this.newDefaultScope,
|
|
||||||
no_rich_text: this.newNoRichText,
|
|
||||||
hide_follows: this.hideFollows,
|
|
||||||
hide_followers: this.hideFollowers,
|
|
||||||
discoverable: this.discoverable,
|
|
||||||
bot: this.bot,
|
bot: this.bot,
|
||||||
allow_following_move: this.allowFollowingMove,
|
|
||||||
hide_follows_count: this.hideFollowsCount,
|
|
||||||
hide_followers_count: this.hideFollowersCount,
|
|
||||||
show_role: this.showRole
|
show_role: this.showRole
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
} }).then((user) => {
|
} }).then((user) => {
|
||||||
|
|
|
@ -25,61 +25,6 @@
|
||||||
class="bio resize-height"
|
class="bio resize-height"
|
||||||
/>
|
/>
|
||||||
</EmojiInput>
|
</EmojiInput>
|
||||||
<p>
|
|
||||||
<Checkbox v-model="newLocked">
|
|
||||||
{{ $t('settings.lock_account_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<label for="default-vis">{{ $t('settings.default_vis') }}</label>
|
|
||||||
<div
|
|
||||||
id="default-vis"
|
|
||||||
class="visibility-tray"
|
|
||||||
>
|
|
||||||
<scope-selector
|
|
||||||
:show-all="true"
|
|
||||||
:user-default="newDefaultScope"
|
|
||||||
:initial-scope="newDefaultScope"
|
|
||||||
:on-scope-change="changeVis"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="newNoRichText">
|
|
||||||
{{ $t('settings.no_rich_text_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="hideFollows">
|
|
||||||
{{ $t('settings.hide_follows_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p class="setting-subitem">
|
|
||||||
<Checkbox
|
|
||||||
v-model="hideFollowsCount"
|
|
||||||
:disabled="!hideFollows"
|
|
||||||
>
|
|
||||||
{{ $t('settings.hide_follows_count_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="hideFollowers">
|
|
||||||
{{ $t('settings.hide_followers_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p class="setting-subitem">
|
|
||||||
<Checkbox
|
|
||||||
v-model="hideFollowersCount"
|
|
||||||
:disabled="!hideFollowers"
|
|
||||||
>
|
|
||||||
{{ $t('settings.hide_followers_count_description') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<Checkbox v-model="allowFollowingMove">
|
|
||||||
{{ $t('settings.allow_following_move') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<p v-if="role === 'admin' || role === 'moderator'">
|
<p v-if="role === 'admin' || role === 'moderator'">
|
||||||
<Checkbox v-model="showRole">
|
<Checkbox v-model="showRole">
|
||||||
<template v-if="role === 'admin'">
|
<template v-if="role === 'admin'">
|
||||||
|
@ -90,11 +35,6 @@
|
||||||
</template>
|
</template>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
<Checkbox v-model="discoverable">
|
|
||||||
{{ $t('settings.discoverable') }}
|
|
||||||
</Checkbox>
|
|
||||||
</p>
|
|
||||||
<div v-if="maxFields > 0">
|
<div v-if="maxFields > 0">
|
||||||
<p>{{ $t('settings.profile_fields.label') }}</p>
|
<p>{{ $t('settings.profile_fields.label') }}</p>
|
||||||
<div
|
<div
|
||||||
|
@ -269,6 +209,67 @@
|
||||||
{{ $t('settings.save') }}
|
{{ $t('settings.save') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{ $t('settings.account_privacy') }}</h2>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="serverSide_locked">
|
||||||
|
{{ $t('settings.lock_account_description') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="serverSide_discoverable">
|
||||||
|
{{ $t('settings.discoverable') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="serverSide_allowFollowingMove">
|
||||||
|
{{ $t('settings.allow_following_move') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="serverSide_hideFavorites">
|
||||||
|
{{ $t('settings.hide_favorites_description') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="serverSide_hideFollowers">
|
||||||
|
{{ $t('settings.hide_followers_description') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
<ul
|
||||||
|
class="setting-list suboptions"
|
||||||
|
:class="[{disabled: !serverSide_hideFollowers}]"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path="serverSide_hideFollowersCount"
|
||||||
|
:disabled="!serverSide_hideFollowers"
|
||||||
|
>
|
||||||
|
{{ $t('settings.hide_followers_count_description') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="serverSide_hideFollows">
|
||||||
|
{{ $t('settings.hide_follows_description') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
<ul
|
||||||
|
class="setting-list suboptions"
|
||||||
|
:class="[{disabled: !serverSide_hideFollows}]"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path="serverSide_hideFollowsCount"
|
||||||
|
:disabled="!serverSide_hideFollows"
|
||||||
|
>
|
||||||
|
{{ $t('settings.hide_follows_count_description') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,10 @@ import {
|
||||||
faStar,
|
faStar,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faEye,
|
faEye,
|
||||||
faThumbtack
|
faThumbtack,
|
||||||
|
faChevronUp,
|
||||||
|
faChevronDown,
|
||||||
|
faAngleDoubleRight
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
|
@ -52,9 +55,47 @@ library.add(
|
||||||
faEllipsisH,
|
faEllipsisH,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
faEye,
|
faEye,
|
||||||
faThumbtack
|
faThumbtack,
|
||||||
|
faChevronUp,
|
||||||
|
faChevronDown,
|
||||||
|
faAngleDoubleRight
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const camelCase = name => name.charAt(0).toUpperCase() + name.slice(1)
|
||||||
|
|
||||||
|
const controlledOrUncontrolledGetters = list => list.reduce((res, name) => {
|
||||||
|
const camelized = camelCase(name)
|
||||||
|
const toggle = `controlledToggle${camelized}`
|
||||||
|
const controlledName = `controlled${camelized}`
|
||||||
|
const uncontrolledName = `uncontrolled${camelized}`
|
||||||
|
res[name] = function () {
|
||||||
|
return this[toggle] ? this[controlledName] : this[uncontrolledName]
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const controlledOrUncontrolledToggle = (obj, name) => {
|
||||||
|
const camelized = camelCase(name)
|
||||||
|
const toggle = `controlledToggle${camelized}`
|
||||||
|
const uncontrolledName = `uncontrolled${camelized}`
|
||||||
|
if (obj[toggle]) {
|
||||||
|
obj[toggle]()
|
||||||
|
} else {
|
||||||
|
obj[uncontrolledName] = !obj[uncontrolledName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const controlledOrUncontrolledSet = (obj, name, val) => {
|
||||||
|
const camelized = camelCase(name)
|
||||||
|
const set = `controlledSet${camelized}`
|
||||||
|
const uncontrolledName = `uncontrolled${camelized}`
|
||||||
|
if (obj[set]) {
|
||||||
|
obj[set](val)
|
||||||
|
} else {
|
||||||
|
obj[uncontrolledName] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const Status = {
|
const Status = {
|
||||||
name: 'Status',
|
name: 'Status',
|
||||||
components: {
|
components: {
|
||||||
|
@ -89,20 +130,38 @@ const Status = {
|
||||||
'inlineExpanded',
|
'inlineExpanded',
|
||||||
'showPinned',
|
'showPinned',
|
||||||
'inProfile',
|
'inProfile',
|
||||||
'profileUserId'
|
'profileUserId',
|
||||||
|
|
||||||
|
'simpleTree',
|
||||||
|
'controlledThreadDisplayStatus',
|
||||||
|
'controlledToggleThreadDisplay',
|
||||||
|
'showOtherRepliesAsButton',
|
||||||
|
|
||||||
|
'controlledShowingTall',
|
||||||
|
'controlledToggleShowingTall',
|
||||||
|
'controlledExpandingSubject',
|
||||||
|
'controlledToggleExpandingSubject',
|
||||||
|
'controlledShowingLongSubject',
|
||||||
|
'controlledToggleShowingLongSubject',
|
||||||
|
'controlledReplying',
|
||||||
|
'controlledToggleReplying',
|
||||||
|
'controlledMediaPlaying',
|
||||||
|
'controlledSetMediaPlaying',
|
||||||
|
'dive'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
replying: false,
|
uncontrolledReplying: false,
|
||||||
unmuted: false,
|
unmuted: false,
|
||||||
userExpanded: false,
|
userExpanded: false,
|
||||||
mediaPlaying: [],
|
uncontrolledMediaPlaying: [],
|
||||||
suspendable: true,
|
suspendable: true,
|
||||||
error: null,
|
error: null,
|
||||||
headTailLinks: null
|
headTailLinks: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...controlledOrUncontrolledGetters(['replying', 'mediaPlaying']),
|
||||||
muteWords () {
|
muteWords () {
|
||||||
return this.mergedConfig.muteWords
|
return this.mergedConfig.muteWords
|
||||||
},
|
},
|
||||||
|
@ -166,6 +225,12 @@ const Status = {
|
||||||
muteWordHits () {
|
muteWordHits () {
|
||||||
return muteWordHits(this.status, this.muteWords)
|
return muteWordHits(this.status, this.muteWords)
|
||||||
},
|
},
|
||||||
|
botStatus () {
|
||||||
|
return this.status.user.bot
|
||||||
|
},
|
||||||
|
botIndicator () {
|
||||||
|
return this.botStatus && !this.hideBotIndication
|
||||||
|
},
|
||||||
mentionsLine () {
|
mentionsLine () {
|
||||||
if (!this.headTailLinks) return []
|
if (!this.headTailLinks) return []
|
||||||
const writtenSet = new Set(this.headTailLinks.writtenMentions.map(_ => _.url))
|
const writtenSet = new Set(this.headTailLinks.writtenMentions.map(_ => _.url))
|
||||||
|
@ -191,7 +256,9 @@ const Status = {
|
||||||
// Thread is muted
|
// Thread is muted
|
||||||
status.thread_muted ||
|
status.thread_muted ||
|
||||||
// Wordfiltered
|
// Wordfiltered
|
||||||
this.muteWordHits.length > 0
|
this.muteWordHits.length > 0 ||
|
||||||
|
// bot status
|
||||||
|
(this.muteBotStatuses && this.botStatus && !this.compact)
|
||||||
return !this.unmuted && !this.shouldNotMute && reasonsToMute
|
return !this.unmuted && !this.shouldNotMute && reasonsToMute
|
||||||
},
|
},
|
||||||
userIsMuted () {
|
userIsMuted () {
|
||||||
|
@ -293,6 +360,12 @@ const Status = {
|
||||||
hidePostStats () {
|
hidePostStats () {
|
||||||
return this.mergedConfig.hidePostStats
|
return this.mergedConfig.hidePostStats
|
||||||
},
|
},
|
||||||
|
muteBotStatuses () {
|
||||||
|
return this.mergedConfig.muteBotStatuses
|
||||||
|
},
|
||||||
|
hideBotIndication () {
|
||||||
|
return this.mergedConfig.hideBotIndication
|
||||||
|
},
|
||||||
currentUser () {
|
currentUser () {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
|
@ -304,6 +377,12 @@ const Status = {
|
||||||
},
|
},
|
||||||
isSuspendable () {
|
isSuspendable () {
|
||||||
return !this.replying && this.mediaPlaying.length === 0
|
return !this.replying && this.mediaPlaying.length === 0
|
||||||
|
},
|
||||||
|
inThreadForest () {
|
||||||
|
return !!this.controlledThreadDisplayStatus
|
||||||
|
},
|
||||||
|
threadShowing () {
|
||||||
|
return this.controlledThreadDisplayStatus === 'showing'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -326,7 +405,7 @@ const Status = {
|
||||||
this.error = undefined
|
this.error = undefined
|
||||||
},
|
},
|
||||||
toggleReplying () {
|
toggleReplying () {
|
||||||
this.replying = !this.replying
|
controlledOrUncontrolledToggle(this, 'replying')
|
||||||
},
|
},
|
||||||
gotoOriginal (id) {
|
gotoOriginal (id) {
|
||||||
if (this.inConversation) {
|
if (this.inConversation) {
|
||||||
|
@ -346,17 +425,19 @@ const Status = {
|
||||||
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||||
},
|
},
|
||||||
addMediaPlaying (id) {
|
addMediaPlaying (id) {
|
||||||
this.mediaPlaying.push(id)
|
controlledOrUncontrolledSet(this, 'mediaPlaying', this.mediaPlaying.concat(id))
|
||||||
},
|
},
|
||||||
removeMediaPlaying (id) {
|
removeMediaPlaying (id) {
|
||||||
this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id)
|
controlledOrUncontrolledSet(this, 'mediaPlaying', this.mediaPlaying.filter(mediaId => mediaId !== id))
|
||||||
},
|
},
|
||||||
setHeadTailLinks (headTailLinks) {
|
setHeadTailLinks (headTailLinks) {
|
||||||
this.headTailLinks = headTailLinks
|
this.headTailLinks = headTailLinks
|
||||||
}
|
|
||||||
},
|
},
|
||||||
watch: {
|
toggleThreadDisplay () {
|
||||||
'highlight': function (id) {
|
this.controlledToggleThreadDisplay()
|
||||||
|
},
|
||||||
|
scrollIfHighlighted (highlightId) {
|
||||||
|
const id = highlightId
|
||||||
if (this.status.id === id) {
|
if (this.status.id === id) {
|
||||||
let rect = this.$el.getBoundingClientRect()
|
let rect = this.$el.getBoundingClientRect()
|
||||||
if (rect.top < 100) {
|
if (rect.top < 100) {
|
||||||
|
@ -370,6 +451,11 @@ const Status = {
|
||||||
window.scrollBy(0, rect.bottom - window.innerHeight + 50)
|
window.scrollBy(0, rect.bottom - window.innerHeight + 50)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'highlight': function (id) {
|
||||||
|
this.scrollIfHighlighted(id)
|
||||||
},
|
},
|
||||||
'status.repeat_num': function (num) {
|
'status.repeat_num': function (num) {
|
||||||
// refetch repeats when repeat_num is changed in any way
|
// refetch repeats when repeat_num is changed in any way
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
$status-margin: 0.75em;
|
|
||||||
|
|
||||||
.Status {
|
.Status {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
@ -28,15 +26,8 @@ $status-margin: 0.75em;
|
||||||
--icon: var(--selectedPostIcon, $fallback--icon);
|
--icon: var(--selectedPostIcon, $fallback--icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-conversation {
|
|
||||||
border-left-width: 4px;
|
|
||||||
border-left-style: solid;
|
|
||||||
border-left-color: $fallback--cRed;
|
|
||||||
border-left-color: var(--cRed, $fallback--cRed);
|
|
||||||
}
|
|
||||||
|
|
||||||
.gravestone {
|
.gravestone {
|
||||||
padding: $status-margin;
|
padding: var(--status-margin, $status-margin);
|
||||||
color: $fallback--faint;
|
color: $fallback--faint;
|
||||||
color: var(--faint, $fallback--faint);
|
color: var(--faint, $fallback--faint);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -49,7 +40,7 @@ $status-margin: 0.75em;
|
||||||
|
|
||||||
.status-container {
|
.status-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: $status-margin;
|
padding: var(--status-margin, $status-margin);
|
||||||
|
|
||||||
&.-repeat {
|
&.-repeat {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
@ -57,7 +48,7 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pin {
|
.pin {
|
||||||
padding: $status-margin $status-margin 0;
|
padding: var(--status-margin, $status-margin) var(--status-margin, $status-margin) 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
@ -73,7 +64,7 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-side {
|
.left-side {
|
||||||
margin-right: $status-margin;
|
margin-right: var(--status-margin, $status-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-side {
|
.right-side {
|
||||||
|
@ -82,7 +73,7 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usercard {
|
.usercard {
|
||||||
margin-bottom: $status-margin;
|
margin-bottom: var(--status-margin, $status-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-username {
|
.status-username {
|
||||||
|
@ -248,7 +239,7 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repeat-info {
|
.repeat-info {
|
||||||
padding: 0.4em $status-margin;
|
padding: 0.4em var(--status-margin, $status-margin);
|
||||||
|
|
||||||
.repeat-icon {
|
.repeat-icon {
|
||||||
color: $fallback--cGreen;
|
color: $fallback--cGreen;
|
||||||
|
@ -294,7 +285,7 @@ $status-margin: 0.75em;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: $status-margin;
|
margin-top: var(--status-margin, $status-margin);
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
max-width: 4em;
|
max-width: 4em;
|
||||||
|
@ -362,7 +353,7 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.favs-repeated-users {
|
.favs-repeated-users {
|
||||||
margin-top: $status-margin;
|
margin-top: var(--status-margin, $status-margin);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats {
|
.stats {
|
||||||
|
@ -389,7 +380,7 @@ $status-margin: 0.75em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-count {
|
.stat-count {
|
||||||
margin-right: $status-margin;
|
margin-right: var(--status-margin, $status-margin);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
.stat-title {
|
.stat-title {
|
||||||
|
|
|
@ -77,6 +77,7 @@
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
v-if="retweet"
|
v-if="retweet"
|
||||||
class="left-side repeater-avatar"
|
class="left-side repeater-avatar"
|
||||||
|
:bot="botIndicator"
|
||||||
:better-shadow="betterShadow"
|
:better-shadow="betterShadow"
|
||||||
:user="statusoid.user"
|
:user="statusoid.user"
|
||||||
/>
|
/>
|
||||||
|
@ -124,6 +125,7 @@
|
||||||
@click.stop.prevent.capture.native="toggleUserExpanded"
|
@click.stop.prevent.capture.native="toggleUserExpanded"
|
||||||
>
|
>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
|
:bot="botIndicator"
|
||||||
:compact="compact"
|
:compact="compact"
|
||||||
:better-shadow="betterShadow"
|
:better-shadow="betterShadow"
|
||||||
:user="status.user"
|
:user="status.user"
|
||||||
|
@ -219,6 +221,31 @@
|
||||||
class="fa-scale-110"
|
class="fa-scale-110"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="inThreadForest && replies && replies.length && !simpleTree"
|
||||||
|
class="button-unstyled"
|
||||||
|
:title="threadShowing ? $t('status.thread_hide') : $t('status.thread_show')"
|
||||||
|
:aria-expanded="threadShowing ? 'true' : 'false'"
|
||||||
|
@click.prevent="toggleThreadDisplay"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110"
|
||||||
|
:icon="threadShowing ? 'chevron-up' : 'chevron-down'"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="dive && !simpleTree"
|
||||||
|
class="button-unstyled"
|
||||||
|
:title="$t('status.show_only_conversation_under_this')"
|
||||||
|
@click.prevent="dive"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110"
|
||||||
|
:icon="'angle-double-right'"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -306,6 +333,12 @@
|
||||||
:no-heading="noHeading"
|
:no-heading="noHeading"
|
||||||
:highlight="highlight"
|
:highlight="highlight"
|
||||||
:focused="isFocused"
|
:focused="isFocused"
|
||||||
|
:controlled-showing-tall="controlledShowingTall"
|
||||||
|
:controlled-expanding-subject="controlledExpandingSubject"
|
||||||
|
:controlled-showing-long-subject="controlledShowingLongSubject"
|
||||||
|
:controlled-toggle-showing-tall="controlledToggleShowingTall"
|
||||||
|
:controlled-toggle-expanding-subject="controlledToggleExpandingSubject"
|
||||||
|
:controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject"
|
||||||
@mediaplay="addMediaPlaying($event)"
|
@mediaplay="addMediaPlaying($event)"
|
||||||
@mediapause="removeMediaPlaying($event)"
|
@mediapause="removeMediaPlaying($event)"
|
||||||
@parseReady="setHeadTailLinks"
|
@parseReady="setHeadTailLinks"
|
||||||
|
@ -315,7 +348,20 @@
|
||||||
v-if="inConversation && !isPreview && replies && replies.length"
|
v-if="inConversation && !isPreview && replies && replies.length"
|
||||||
class="replies"
|
class="replies"
|
||||||
>
|
>
|
||||||
<span class="faint">{{ $t('status.replies_list') }}</span>
|
<button
|
||||||
|
v-if="showOtherRepliesAsButton && replies.length > 1"
|
||||||
|
class="button-unstyled -link faint"
|
||||||
|
:title="$tc('status.ancestor_follow', replies.length - 1, { numReplies: replies.length - 1 })"
|
||||||
|
@click.prevent="dive"
|
||||||
|
>
|
||||||
|
{{ $tc('status.replies_list_with_others', replies.length - 1, { numReplies: replies.length - 1 }) }}
|
||||||
|
</button>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="faint"
|
||||||
|
>
|
||||||
|
{{ $t('status.replies_list') }}
|
||||||
|
</span>
|
||||||
<StatusPopover
|
<StatusPopover
|
||||||
v-for="reply in replies"
|
v-for="reply in replies"
|
||||||
:key="reply.id"
|
:key="reply.id"
|
||||||
|
@ -407,7 +453,10 @@
|
||||||
class="gravestone"
|
class="gravestone"
|
||||||
>
|
>
|
||||||
<div class="left-side">
|
<div class="left-side">
|
||||||
<UserAvatar :compact="compact" />
|
<UserAvatar
|
||||||
|
:compact="compact"
|
||||||
|
:bot="botIndicator"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="right-side">
|
<div class="right-side">
|
||||||
<div class="deleted-text">
|
<div class="deleted-text">
|
||||||
|
|
|
@ -26,14 +26,16 @@ const StatusContent = {
|
||||||
'focused',
|
'focused',
|
||||||
'noHeading',
|
'noHeading',
|
||||||
'fullContent',
|
'fullContent',
|
||||||
'singleLine'
|
'singleLine',
|
||||||
|
'showingTall',
|
||||||
|
'expandingSubject',
|
||||||
|
'showingLongSubject',
|
||||||
|
'toggleShowingTall',
|
||||||
|
'toggleExpandingSubject',
|
||||||
|
'toggleShowingLongSubject'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
showingTall: this.fullContent || (this.inConversation && this.focused),
|
|
||||||
showingLongSubject: false,
|
|
||||||
// not as computed because it sets the initial state which will be changed later
|
|
||||||
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
|
|
||||||
postLength: this.status.text.length,
|
postLength: this.status.text.length,
|
||||||
parseReadyDone: false
|
parseReadyDone: false
|
||||||
}
|
}
|
||||||
|
@ -115,9 +117,9 @@ const StatusContent = {
|
||||||
},
|
},
|
||||||
toggleShowMore () {
|
toggleShowMore () {
|
||||||
if (this.mightHideBecauseTall) {
|
if (this.mightHideBecauseTall) {
|
||||||
this.showingTall = !this.showingTall
|
this.toggleShowingTall()
|
||||||
} else if (this.mightHideBecauseSubject) {
|
} else if (this.mightHideBecauseSubject) {
|
||||||
this.expandingSubject = !this.expandingSubject
|
this.toggleExpandingSubject()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
generateTagLink (tag) {
|
generateTagLink (tag) {
|
||||||
|
|
|
@ -17,14 +17,14 @@
|
||||||
<button
|
<button
|
||||||
v-if="longSubject && showingLongSubject"
|
v-if="longSubject && showingLongSubject"
|
||||||
class="button-unstyled -link tall-subject-hider"
|
class="button-unstyled -link tall-subject-hider"
|
||||||
@click.prevent="showingLongSubject=false"
|
@click.prevent="toggleShowingLongSubject"
|
||||||
>
|
>
|
||||||
{{ $t("status.hide_full_subject") }}
|
{{ $t("status.hide_full_subject") }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-else-if="longSubject"
|
v-else-if="longSubject"
|
||||||
class="button-unstyled -link tall-subject-hider"
|
class="button-unstyled -link tall-subject-hider"
|
||||||
@click.prevent="showingLongSubject=true"
|
@click.prevent="toggleShowingLongSubject"
|
||||||
>
|
>
|
||||||
{{ $t("status.show_full_subject") }}
|
{{ $t("status.show_full_subject") }}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -23,6 +23,30 @@ library.add(
|
||||||
faPollH
|
faPollH
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const camelCase = name => name.charAt(0).toUpperCase() + name.slice(1)
|
||||||
|
|
||||||
|
const controlledOrUncontrolledGetters = list => list.reduce((res, name) => {
|
||||||
|
const camelized = camelCase(name)
|
||||||
|
const toggle = `controlledToggle${camelized}`
|
||||||
|
const controlledName = `controlled${camelized}`
|
||||||
|
const uncontrolledName = `uncontrolled${camelized}`
|
||||||
|
res[name] = function () {
|
||||||
|
return this[toggle] ? this[controlledName] : this[uncontrolledName]
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const controlledOrUncontrolledToggle = (obj, name) => {
|
||||||
|
const camelized = camelCase(name)
|
||||||
|
const toggle = `controlledToggle${camelized}`
|
||||||
|
const uncontrolledName = `uncontrolled${camelized}`
|
||||||
|
if (obj[toggle]) {
|
||||||
|
obj[toggle]()
|
||||||
|
} else {
|
||||||
|
obj[uncontrolledName] = !obj[uncontrolledName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const StatusContent = {
|
const StatusContent = {
|
||||||
name: 'StatusContent',
|
name: 'StatusContent',
|
||||||
props: [
|
props: [
|
||||||
|
@ -31,9 +55,22 @@ const StatusContent = {
|
||||||
'focused',
|
'focused',
|
||||||
'noHeading',
|
'noHeading',
|
||||||
'fullContent',
|
'fullContent',
|
||||||
'singleLine'
|
'singleLine',
|
||||||
|
'controlledShowingTall',
|
||||||
|
'controlledExpandingSubject',
|
||||||
|
'controlledToggleShowingTall',
|
||||||
|
'controlledToggleExpandingSubject'
|
||||||
],
|
],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
uncontrolledShowingTall: this.fullContent || (this.inConversation && this.focused),
|
||||||
|
uncontrolledShowingLongSubject: false,
|
||||||
|
// not as computed because it sets the initial state which will be changed later
|
||||||
|
uncontrolledExpandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...controlledOrUncontrolledGetters(['showingTall', 'expandingSubject', 'showingLongSubject']),
|
||||||
hideAttachments () {
|
hideAttachments () {
|
||||||
return (this.mergedConfig.hideAttachments && !this.inConversation) ||
|
return (this.mergedConfig.hideAttachments && !this.inConversation) ||
|
||||||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)
|
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)
|
||||||
|
@ -71,6 +108,21 @@ const StatusContent = {
|
||||||
Gallery,
|
Gallery,
|
||||||
LinkPreview,
|
LinkPreview,
|
||||||
StatusBody
|
StatusBody
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleShowingTall () {
|
||||||
|
controlledOrUncontrolledToggle(this, 'showingTall')
|
||||||
|
},
|
||||||
|
toggleExpandingSubject () {
|
||||||
|
controlledOrUncontrolledToggle(this, 'expandingSubject')
|
||||||
|
},
|
||||||
|
toggleShowingLongSubject () {
|
||||||
|
controlledOrUncontrolledToggle(this, 'showingLongSubject')
|
||||||
|
},
|
||||||
|
setMedia () {
|
||||||
|
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
|
||||||
|
return () => this.$store.dispatch('setMedia', attachments)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,12 @@
|
||||||
:status="status"
|
:status="status"
|
||||||
:compact="compact"
|
:compact="compact"
|
||||||
:single-line="singleLine"
|
:single-line="singleLine"
|
||||||
|
:showing-tall="showingTall"
|
||||||
|
:expanding-subject="expandingSubject"
|
||||||
|
:showing-long-subject="showingLongSubject"
|
||||||
|
:toggle-showing-tall="toggleShowingTall"
|
||||||
|
:toggle-expanding-subject="toggleExpandingSubject"
|
||||||
|
:toggle-showing-long-subject="toggleShowingLongSubject"
|
||||||
@parseReady="$emit('parseReady', $event)"
|
@parseReady="$emit('parseReady', $event)"
|
||||||
>
|
>
|
||||||
<div v-if="status.poll && status.poll.options && !compact">
|
<div v-if="status.poll && status.poll.options && !compact">
|
||||||
|
@ -52,10 +58,6 @@
|
||||||
|
|
||||||
<script src="./status_content.js" ></script>
|
<script src="./status_content.js" ></script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
|
||||||
|
|
||||||
$status-margin: 0.75em;
|
|
||||||
|
|
||||||
.StatusContent {
|
.StatusContent {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
@load="onLoad"
|
@load="onLoad"
|
||||||
@error="onError"
|
@error="onError"
|
||||||
>
|
>
|
||||||
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
84
src/components/swipe_click/swipe_click.js
Normal file
84
src/components/swipe_click/swipe_click.js
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import GestureService from '../../services/gesture_service/gesture_service'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* props:
|
||||||
|
* direction: a vector that indicates the direction of the intended swipe
|
||||||
|
* threshold: the minimum distance in pixels the swipe has moved on `direction'
|
||||||
|
* for swipe-finished() to have a non-zero sign
|
||||||
|
* perpendicularTolerance: see gesture_service
|
||||||
|
*
|
||||||
|
* Events:
|
||||||
|
* preview-requested(offsets)
|
||||||
|
* Emitted when the pointer has moved.
|
||||||
|
* offsets: the offsets from the start of the swipe to the current cursor position
|
||||||
|
*
|
||||||
|
* swipe-canceled()
|
||||||
|
* Emitted when the swipe has been canceled due to a pointercancel event.
|
||||||
|
*
|
||||||
|
* swipe-finished(sign: 0|-1|1)
|
||||||
|
* Emitted when the swipe has finished.
|
||||||
|
* sign: if the swipe does not meet the threshold, 0
|
||||||
|
* if the swipe meets the threshold in the positive direction, 1
|
||||||
|
* if the swipe meets the threshold in the negative direction, -1
|
||||||
|
*
|
||||||
|
* swipeless-clicked()
|
||||||
|
* Emitted when there is a click without swipe.
|
||||||
|
* This and swipe-finished() cannot be emitted for the same pointerup event.
|
||||||
|
*/
|
||||||
|
const SwipeClick = {
|
||||||
|
props: {
|
||||||
|
direction: {
|
||||||
|
type: Array
|
||||||
|
},
|
||||||
|
threshold: {
|
||||||
|
type: Function,
|
||||||
|
default: () => 30
|
||||||
|
},
|
||||||
|
perpendicularTolerance: {
|
||||||
|
type: Number,
|
||||||
|
default: 1.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handlePointerDown (event) {
|
||||||
|
this.$gesture.start(event)
|
||||||
|
},
|
||||||
|
handlePointerMove (event) {
|
||||||
|
this.$gesture.move(event)
|
||||||
|
},
|
||||||
|
handlePointerUp (event) {
|
||||||
|
this.$gesture.end(event)
|
||||||
|
},
|
||||||
|
handlePointerCancel (event) {
|
||||||
|
this.$gesture.cancel(event)
|
||||||
|
},
|
||||||
|
handleNativeClick (event) {
|
||||||
|
this.$gesture.click(event)
|
||||||
|
},
|
||||||
|
preview (offsets) {
|
||||||
|
this.$emit('preview-requested', offsets)
|
||||||
|
},
|
||||||
|
end (sign) {
|
||||||
|
this.$emit('swipe-finished', sign)
|
||||||
|
},
|
||||||
|
click () {
|
||||||
|
this.$emit('swipeless-clicked')
|
||||||
|
},
|
||||||
|
cancel () {
|
||||||
|
this.$emit('swipe-canceled')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.$gesture = new GestureService.SwipeAndClickGesture({
|
||||||
|
direction: this.direction,
|
||||||
|
threshold: this.threshold,
|
||||||
|
perpendicularTolerance: this.perpendicularTolerance,
|
||||||
|
swipePreviewCallback: this.preview,
|
||||||
|
swipeEndCallback: this.end,
|
||||||
|
swipeCancelCallback: this.cancel,
|
||||||
|
swipelessClickCallback: this.click
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SwipeClick
|
14
src/components/swipe_click/swipe_click.vue
Normal file
14
src/components/swipe_click/swipe_click.vue
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-bind="$attrs"
|
||||||
|
@pointerdown="handlePointerDown"
|
||||||
|
@pointermove="handlePointerMove"
|
||||||
|
@pointerup="handlePointerUp"
|
||||||
|
@pointercancel="handlePointerCancel"
|
||||||
|
@click="handleNativeClick"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./swipe_click.js"></script>
|
90
src/components/thread_tree/thread_tree.js
Normal file
90
src/components/thread_tree/thread_tree.js
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import Status from '../status/status.vue'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faAngleDoubleDown,
|
||||||
|
faAngleDoubleRight
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faAngleDoubleDown,
|
||||||
|
faAngleDoubleRight
|
||||||
|
)
|
||||||
|
|
||||||
|
const ThreadTree = {
|
||||||
|
components: {
|
||||||
|
Status
|
||||||
|
},
|
||||||
|
name: 'ThreadTree',
|
||||||
|
props: {
|
||||||
|
depth: Number,
|
||||||
|
status: Object,
|
||||||
|
inProfile: Boolean,
|
||||||
|
conversation: Array,
|
||||||
|
collapsable: Boolean,
|
||||||
|
isExpanded: Boolean,
|
||||||
|
pinnedStatusIdsObject: Object,
|
||||||
|
profileUserId: String,
|
||||||
|
|
||||||
|
focused: Function,
|
||||||
|
highlight: String,
|
||||||
|
getReplies: Function,
|
||||||
|
setHighlight: Function,
|
||||||
|
toggleExpanded: Function,
|
||||||
|
|
||||||
|
simple: Boolean,
|
||||||
|
// to control display of the whole thread forest
|
||||||
|
toggleThreadDisplay: Function,
|
||||||
|
threadDisplayStatus: Object,
|
||||||
|
showThreadRecursively: Function,
|
||||||
|
totalReplyCount: Object,
|
||||||
|
totalReplyDepth: Object,
|
||||||
|
statusContentProperties: Object,
|
||||||
|
setStatusContentProperty: Function,
|
||||||
|
toggleStatusContentProperty: Function,
|
||||||
|
dive: Function
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
suspendable () {
|
||||||
|
const selfSuspendable = this.$refs.statusComponent ? this.$refs.statusComponent.suspendable : true
|
||||||
|
if (this.$refs.childComponent) {
|
||||||
|
return selfSuspendable && this.$refs.childComponent.every(s => s.suspendable)
|
||||||
|
}
|
||||||
|
return selfSuspendable
|
||||||
|
},
|
||||||
|
reverseLookupTable () {
|
||||||
|
return this.conversation.reduce((table, status, index) => {
|
||||||
|
table[status.id] = index
|
||||||
|
return table
|
||||||
|
}, {})
|
||||||
|
},
|
||||||
|
currentReplies () {
|
||||||
|
return this.getReplies(this.status.id).map(({ id }) => this.statusById(id))
|
||||||
|
},
|
||||||
|
threadShowing () {
|
||||||
|
return this.threadDisplayStatus[this.status.id] === 'showing'
|
||||||
|
},
|
||||||
|
currentProp () {
|
||||||
|
return this.statusContentProperties[this.status.id]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
statusById (id) {
|
||||||
|
return this.conversation[this.reverseLookupTable[id]]
|
||||||
|
},
|
||||||
|
collapseThread () {
|
||||||
|
},
|
||||||
|
showThread () {
|
||||||
|
},
|
||||||
|
showAllSubthreads () {
|
||||||
|
},
|
||||||
|
toggleCurrentProp (name) {
|
||||||
|
this.toggleStatusContentProperty(this.status.id, name)
|
||||||
|
},
|
||||||
|
setCurrentProp (name, newVal) {
|
||||||
|
this.setStatusContentProperty(this.status.id, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ThreadTree
|
127
src/components/thread_tree/thread_tree.vue
Normal file
127
src/components/thread_tree/thread_tree.vue
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
<template>
|
||||||
|
<div class="thread-tree panel-body">
|
||||||
|
<status
|
||||||
|
:key="status.id"
|
||||||
|
ref="statusComponent"
|
||||||
|
:inline-expanded="collapsable && isExpanded"
|
||||||
|
:statusoid="status"
|
||||||
|
:expandable="!isExpanded"
|
||||||
|
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||||
|
:focused="focused(status.id)"
|
||||||
|
:in-conversation="isExpanded"
|
||||||
|
:highlight="highlight"
|
||||||
|
:replies="getReplies(status.id)"
|
||||||
|
:in-profile="inProfile"
|
||||||
|
:profile-user-id="profileUserId"
|
||||||
|
class="conversation-status conversation-status-treeview status-fadein panel-body"
|
||||||
|
|
||||||
|
:simple-tree="simple"
|
||||||
|
:controlled-thread-display-status="threadDisplayStatus[status.id]"
|
||||||
|
:controlled-toggle-thread-display="() => toggleThreadDisplay(status.id)"
|
||||||
|
|
||||||
|
:controlled-showing-tall="currentProp.showingTall"
|
||||||
|
:controlled-expanding-subject="currentProp.expandingSubject"
|
||||||
|
:controlled-showing-long-subject="currentProp.showingLongSubject"
|
||||||
|
:controlled-replying="currentProp.replying"
|
||||||
|
:controlled-media-playing="currentProp.mediaPlaying"
|
||||||
|
:controlled-toggle-showing-tall="() => toggleCurrentProp('showingTall')"
|
||||||
|
:controlled-toggle-expanding-subject="() => toggleCurrentProp('expandingSubject')"
|
||||||
|
:controlled-toggle-showing-long-subject="() => toggleCurrentProp('showingLongSubject')"
|
||||||
|
:controlled-toggle-replying="() => toggleCurrentProp('replying')"
|
||||||
|
:controlled-set-media-playing="(newVal) => setCurrentProp('mediaPlaying', newVal)"
|
||||||
|
:dive="dive ? () => dive(status.id) : undefined"
|
||||||
|
|
||||||
|
@goto="setHighlight"
|
||||||
|
@toggleExpanded="toggleExpanded"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="currentReplies.length && threadShowing"
|
||||||
|
class="thread-tree-replies"
|
||||||
|
>
|
||||||
|
<thread-tree
|
||||||
|
v-for="replyStatus in currentReplies"
|
||||||
|
:key="replyStatus.id"
|
||||||
|
ref="childComponent"
|
||||||
|
:depth="depth + 1"
|
||||||
|
:status="replyStatus"
|
||||||
|
|
||||||
|
:in-profile="inProfile"
|
||||||
|
:conversation="conversation"
|
||||||
|
:collapsable="collapsable"
|
||||||
|
:is-expanded="isExpanded"
|
||||||
|
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||||
|
:profile-user-id="profileUserId"
|
||||||
|
|
||||||
|
:focused="focused"
|
||||||
|
:get-replies="getReplies"
|
||||||
|
:highlight="highlight"
|
||||||
|
:set-highlight="setHighlight"
|
||||||
|
:toggle-expanded="toggleExpanded"
|
||||||
|
|
||||||
|
:simple="simple"
|
||||||
|
:toggle-thread-display="toggleThreadDisplay"
|
||||||
|
:thread-display-status="threadDisplayStatus"
|
||||||
|
:show-thread-recursively="showThreadRecursively"
|
||||||
|
:total-reply-count="totalReplyCount"
|
||||||
|
:total-reply-depth="totalReplyDepth"
|
||||||
|
:status-content-properties="statusContentProperties"
|
||||||
|
:set-status-content-property="setStatusContentProperty"
|
||||||
|
:toggle-status-content-property="toggleStatusContentProperty"
|
||||||
|
:dive="dive"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="currentReplies.length && !threadShowing"
|
||||||
|
class="thread-tree-replies thread-tree-replies-hidden"
|
||||||
|
>
|
||||||
|
<i18n
|
||||||
|
v-if="simple"
|
||||||
|
tag="button"
|
||||||
|
path="status.thread_follow_with_icon"
|
||||||
|
class="button-unstyled -link thread-tree-show-replies-button"
|
||||||
|
@click.prevent="dive(status.id)"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
place="icon"
|
||||||
|
icon="angle-double-right"
|
||||||
|
/>
|
||||||
|
<span place="text">
|
||||||
|
{{ $tc('status.thread_follow', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id] }) }}
|
||||||
|
</span>
|
||||||
|
</i18n>
|
||||||
|
<i18n
|
||||||
|
v-else
|
||||||
|
tag="button"
|
||||||
|
path="status.thread_show_full_with_icon"
|
||||||
|
class="button-unstyled -link thread-tree-show-replies-button"
|
||||||
|
@click.prevent="showThreadRecursively(status.id)"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
place="icon"
|
||||||
|
icon="angle-double-down"
|
||||||
|
/>
|
||||||
|
<span place="text">
|
||||||
|
{{ $tc('status.thread_show_full', totalReplyCount[status.id], { numStatus: totalReplyCount[status.id], depth: totalReplyDepth[status.id] }) }}
|
||||||
|
</span>
|
||||||
|
</i18n>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./thread_tree.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
.thread-tree-replies {
|
||||||
|
margin-left: var(--status-margin, $status-margin);
|
||||||
|
border-left: 2px solid var(--border, $fallback--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread-tree-replies-hidden {
|
||||||
|
padding: var(--status-margin, $status-margin);
|
||||||
|
/* Make the button stretch along the whole row */
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -53,6 +53,13 @@ const TimelineQuickSettings = {
|
||||||
const value = !this.hideMutedPosts
|
const value = !this.hideMutedPosts
|
||||||
this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value })
|
this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value })
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
muteBotStatuses: {
|
||||||
|
get () { return this.mergedConfig.muteBotStatuses },
|
||||||
|
set () {
|
||||||
|
const value = !this.muteBotStatuses
|
||||||
|
this.$store.dispatch('setOption', { name: 'muteBotStatuses', value })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,15 @@
|
||||||
class="dropdown-divider"
|
class="dropdown-divider"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="muteBotStatuses = !muteBotStatuses"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox"
|
||||||
|
:class="{ 'menu-checkbox-checked': muteBotStatuses }"
|
||||||
|
/>{{ $t('settings.mute_bot_posts') }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
@click="hideMedia = !hideMedia"
|
@click="hideMedia = !hideMedia"
|
||||||
|
|
|
@ -1,10 +1,21 @@
|
||||||
import StillImage from '../still-image/still-image.vue'
|
import StillImage from '../still-image/still-image.vue'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
|
||||||
|
import {
|
||||||
|
faRobot
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faRobot
|
||||||
|
)
|
||||||
|
|
||||||
const UserAvatar = {
|
const UserAvatar = {
|
||||||
props: [
|
props: [
|
||||||
'user',
|
'user',
|
||||||
'betterShadow',
|
'betterShadow',
|
||||||
'compact'
|
'compact',
|
||||||
|
'bot'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -7,7 +7,13 @@
|
||||||
:src="imgSrc(user.profile_image_url_original)"
|
:src="imgSrc(user.profile_image_url_original)"
|
||||||
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
|
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
|
||||||
:image-load-error="imageLoadError"
|
:image-load-error="imageLoadError"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
v-if="bot"
|
||||||
|
icon="robot"
|
||||||
|
class="bot-indicator"
|
||||||
/>
|
/>
|
||||||
|
</StillImage>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="Avatar -placeholder"
|
class="Avatar -placeholder"
|
||||||
|
@ -36,6 +42,12 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& > .bot-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
&.better-shadow {
|
&.better-shadow {
|
||||||
box-shadow: var(--_avatarShadowInset);
|
box-shadow: var(--_avatarShadowInset);
|
||||||
filter: var(--_avatarShadowFilter);
|
filter: var(--_avatarShadowFilter);
|
||||||
|
|
|
@ -120,7 +120,8 @@
|
||||||
"media_modal": {
|
"media_modal": {
|
||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
"counter": "{current} / {total}"
|
"counter": "{current} / {total}",
|
||||||
|
"hide": "Close media viewer"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"about": "About",
|
"about": "About",
|
||||||
|
@ -273,11 +274,14 @@
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"app_name": "App name",
|
"app_name": "App name",
|
||||||
|
"expert_mode": "Show advanced",
|
||||||
"save": "Save changes",
|
"save": "Save changes",
|
||||||
"security": "Security",
|
"security": "Security",
|
||||||
"setting_changed": "Setting is different from default",
|
"setting_changed": "Setting is different from default",
|
||||||
|
"setting_server_side": "This setting is tied to your profile and affects all sessions and clients",
|
||||||
"enter_current_password_to_confirm": "Enter your current password to confirm your identity",
|
"enter_current_password_to_confirm": "Enter your current password to confirm your identity",
|
||||||
"post_look_feel": "Posts Look & Feel",
|
"post_look_feel": "Posts Look & Feel",
|
||||||
|
"mention_links": "Mention links",
|
||||||
"mfa": {
|
"mfa": {
|
||||||
"otp": "OTP",
|
"otp": "OTP",
|
||||||
"setup_otp": "Setup OTP",
|
"setup_otp": "Setup OTP",
|
||||||
|
@ -365,6 +369,8 @@
|
||||||
"hide_attachments_in_tl": "Hide attachments in timeline",
|
"hide_attachments_in_tl": "Hide attachments in timeline",
|
||||||
"hide_media_previews": "Hide media previews",
|
"hide_media_previews": "Hide media previews",
|
||||||
"hide_muted_posts": "Hide posts of muted users",
|
"hide_muted_posts": "Hide posts of muted users",
|
||||||
|
"mute_bot_posts": "Mute bot posts",
|
||||||
|
"hide_bot_indication": "Hide bot indication in posts",
|
||||||
"hide_all_muted_posts": "Hide muted posts",
|
"hide_all_muted_posts": "Hide muted posts",
|
||||||
"max_thumbnails": "Maximum amount of thumbnails per post (empty = no limit)",
|
"max_thumbnails": "Maximum amount of thumbnails per post (empty = no limit)",
|
||||||
"hide_isp": "Hide instance-specific panel",
|
"hide_isp": "Hide instance-specific panel",
|
||||||
|
@ -414,6 +420,7 @@
|
||||||
"name": "Label",
|
"name": "Label",
|
||||||
"value": "Content"
|
"value": "Content"
|
||||||
},
|
},
|
||||||
|
"account_privacy": "Privacy",
|
||||||
"use_contain_fit": "Don't crop the attachment in thumbnails",
|
"use_contain_fit": "Don't crop the attachment in thumbnails",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_bio": "Name & bio",
|
"name_bio": "Name & bio",
|
||||||
|
@ -431,6 +438,7 @@
|
||||||
"no_rich_text_description": "Strip rich text formatting from all posts",
|
"no_rich_text_description": "Strip rich text formatting from all posts",
|
||||||
"no_blocks": "No blocks",
|
"no_blocks": "No blocks",
|
||||||
"no_mutes": "No mutes",
|
"no_mutes": "No mutes",
|
||||||
|
"hide_favorites_description": "Don't show list of my favorites (people still get notified)",
|
||||||
"hide_follows_description": "Don't show who I'm following",
|
"hide_follows_description": "Don't show who I'm following",
|
||||||
"hide_followers_description": "Don't show who's following me",
|
"hide_followers_description": "Don't show who's following me",
|
||||||
"hide_follows_count_description": "Don't show follow count",
|
"hide_follows_count_description": "Don't show follow count",
|
||||||
|
@ -444,7 +452,7 @@
|
||||||
"valid_until": "Valid until",
|
"valid_until": "Valid until",
|
||||||
"revoke_token": "Revoke",
|
"revoke_token": "Revoke",
|
||||||
"panelRadius": "Panels",
|
"panelRadius": "Panels",
|
||||||
"pause_on_unfocused": "Pause streaming when tab is not focused",
|
"pause_on_unfocused": "Pause when tab is not focused",
|
||||||
"presets": "Presets",
|
"presets": "Presets",
|
||||||
"profile_background": "Profile background",
|
"profile_background": "Profile background",
|
||||||
"profile_banner": "Profile banner",
|
"profile_banner": "Profile banner",
|
||||||
|
@ -479,13 +487,21 @@
|
||||||
"subject_line_email": "Like email: \"re: subject\"",
|
"subject_line_email": "Like email: \"re: subject\"",
|
||||||
"subject_line_mastodon": "Like mastodon: copy as is",
|
"subject_line_mastodon": "Like mastodon: copy as is",
|
||||||
"subject_line_noop": "Do not copy",
|
"subject_line_noop": "Do not copy",
|
||||||
|
"conversation_display": "Conversation display style",
|
||||||
|
"conversation_display_tree": "Tree-style",
|
||||||
|
"tree_advanced": "Allow more flexible navigation in tree view",
|
||||||
|
"tree_fade_ancestors": "Display ancestors of the current status in faint text",
|
||||||
|
"conversation_display_linear": "Linear-style",
|
||||||
|
"conversation_other_replies_button": "Show the \"other replies\" button",
|
||||||
|
"conversation_other_replies_button_below": "Below statuses",
|
||||||
|
"conversation_other_replies_button_inside": "Inside statuses",
|
||||||
|
"max_depth_in_thread": "Maximum number of levels in thread to display by default",
|
||||||
"post_status_content_type": "Post status content type",
|
"post_status_content_type": "Post status content type",
|
||||||
"sensitive_by_default": "Mark posts as sensitive by default",
|
"sensitive_by_default": "Mark posts as sensitive by default",
|
||||||
"stop_gifs": "Pause animated images until you hover on them",
|
"stop_gifs": "Pause animated images until you hover on them",
|
||||||
"streaming": "Enable automatic streaming of new posts when scrolled to the top",
|
"streaming": "Automatically show new posts when scrolled to the top",
|
||||||
"user_mutes": "Users",
|
"user_mutes": "Users",
|
||||||
"useStreamingApi": "Receive posts and notifications real-time",
|
"useStreamingApi": "Receive posts and notifications real-time",
|
||||||
"useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
|
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"theme_help": "Use hex color codes (#rrggbb) to customize your color theme.",
|
"theme_help": "Use hex color codes (#rrggbb) to customize your color theme.",
|
||||||
|
@ -735,6 +751,7 @@
|
||||||
"reply_to": "Reply to",
|
"reply_to": "Reply to",
|
||||||
"mentions": "Mentions",
|
"mentions": "Mentions",
|
||||||
"replies_list": "Replies:",
|
"replies_list": "Replies:",
|
||||||
|
"replies_list_with_others": "Replies (+{numReplies} other): | Replies (+{numReplies} others):",
|
||||||
"mute_conversation": "Mute conversation",
|
"mute_conversation": "Mute conversation",
|
||||||
"unmute_conversation": "Unmute conversation",
|
"unmute_conversation": "Unmute conversation",
|
||||||
"status_unavailable": "Status unavailable",
|
"status_unavailable": "Status unavailable",
|
||||||
|
@ -761,7 +778,18 @@
|
||||||
"attachment_stop_flash": "Stop Flash player",
|
"attachment_stop_flash": "Stop Flash player",
|
||||||
"move_up": "Shift attachment left",
|
"move_up": "Shift attachment left",
|
||||||
"move_down": "Shift attachment right",
|
"move_down": "Shift attachment right",
|
||||||
"open_gallery": "Open gallery"
|
"open_gallery": "Open gallery",
|
||||||
|
"thread_hide": "Hide this thread",
|
||||||
|
"thread_show": "Show this thread",
|
||||||
|
"thread_show_full": "Show everything under this thread ({numStatus} status in total, max depth {depth}) | Show everything under this thread ({numStatus} statuses in total, max depth {depth})",
|
||||||
|
"thread_show_full_with_icon": "{icon} {text}",
|
||||||
|
"thread_follow": "See the remaining part of this thread ({numStatus} status in total) | See the remaining part of this thread ({numStatus} statuses in total)",
|
||||||
|
"thread_follow_with_icon": "{icon} {text}",
|
||||||
|
"ancestor_follow": "See {numReplies} other reply under this status | See {numReplies} other replies under this status",
|
||||||
|
"ancestor_follow_with_icon": "{icon} {text}",
|
||||||
|
"show_all_conversation_with_icon": "{icon} {text}",
|
||||||
|
"show_all_conversation": "Show full conversation ({numStatus} other status) | Show full conversation ({numStatus} other statuses)",
|
||||||
|
"show_only_conversation_under_this": "Only show replies to this status"
|
||||||
},
|
},
|
||||||
"user_card": {
|
"user_card": {
|
||||||
"approve": "Approve",
|
"approve": "Approve",
|
||||||
|
|
|
@ -11,6 +11,7 @@ import statusesModule from './modules/statuses.js'
|
||||||
import usersModule from './modules/users.js'
|
import usersModule from './modules/users.js'
|
||||||
import apiModule from './modules/api.js'
|
import apiModule from './modules/api.js'
|
||||||
import configModule from './modules/config.js'
|
import configModule from './modules/config.js'
|
||||||
|
import serverSideConfigModule from './modules/serverSideConfig.js'
|
||||||
import shoutModule from './modules/shout.js'
|
import shoutModule from './modules/shout.js'
|
||||||
import oauthModule from './modules/oauth.js'
|
import oauthModule from './modules/oauth.js'
|
||||||
import authFlowModule from './modules/auth_flow.js'
|
import authFlowModule from './modules/auth_flow.js'
|
||||||
|
@ -45,6 +46,8 @@ Vue.use(VueClickOutside)
|
||||||
Vue.use(PortalVue)
|
Vue.use(PortalVue)
|
||||||
Vue.use(VBodyScrollLock)
|
Vue.use(VBodyScrollLock)
|
||||||
|
|
||||||
|
Vue.config.ignoredElements = ['pinch-zoom']
|
||||||
|
|
||||||
Vue.component('FAIcon', FontAwesomeIcon)
|
Vue.component('FAIcon', FontAwesomeIcon)
|
||||||
Vue.component('FALayers', FontAwesomeLayers)
|
Vue.component('FALayers', FontAwesomeLayers)
|
||||||
|
|
||||||
|
@ -88,6 +91,7 @@ const persistedStateOptions = {
|
||||||
users: usersModule,
|
users: usersModule,
|
||||||
api: apiModule,
|
api: apiModule,
|
||||||
config: configModule,
|
config: configModule,
|
||||||
|
serverSideConfig: serverSideConfigModule,
|
||||||
shout: shoutModule,
|
shout: shoutModule,
|
||||||
oauth: oauthModule,
|
oauth: oauthModule,
|
||||||
authFlow: authFlowModule,
|
authFlow: authFlowModule,
|
||||||
|
|
|
@ -12,10 +12,13 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0]
|
||||||
export const multiChoiceProperties = [
|
export const multiChoiceProperties = [
|
||||||
'postContentType',
|
'postContentType',
|
||||||
'subjectLineBehavior',
|
'subjectLineBehavior',
|
||||||
|
'conversationDisplay', // tree | linear
|
||||||
|
'conversationOtherRepliesButton', // below | inside
|
||||||
'mentionLinkDisplay' // short | full_for_remote | full
|
'mentionLinkDisplay' // short | full_for_remote | full
|
||||||
]
|
]
|
||||||
|
|
||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
|
expertLevel: 0, // used to track which settings to show and hide
|
||||||
colors: {},
|
colors: {},
|
||||||
theme: undefined,
|
theme: undefined,
|
||||||
customTheme: undefined,
|
customTheme: undefined,
|
||||||
|
@ -27,6 +30,7 @@ export const defaultState = {
|
||||||
hideMutedPosts: undefined, // instance default
|
hideMutedPosts: undefined, // instance default
|
||||||
hideMutedThreads: undefined, // instance default
|
hideMutedThreads: undefined, // instance default
|
||||||
hideWordFilteredPosts: undefined, // instance default
|
hideWordFilteredPosts: undefined, // instance default
|
||||||
|
muteBotStatuses: undefined, // instance default
|
||||||
collapseMessageWithSubject: undefined, // instance default
|
collapseMessageWithSubject: undefined, // instance default
|
||||||
padEmoji: true,
|
padEmoji: true,
|
||||||
hideAttachments: false,
|
hideAttachments: false,
|
||||||
|
@ -41,7 +45,7 @@ export const defaultState = {
|
||||||
alwaysShowNewPostButton: false,
|
alwaysShowNewPostButton: false,
|
||||||
autohideFloatingPostButton: false,
|
autohideFloatingPostButton: false,
|
||||||
pauseOnUnfocused: true,
|
pauseOnUnfocused: true,
|
||||||
stopGifs: false,
|
stopGifs: true,
|
||||||
replyVisibility: 'all',
|
replyVisibility: 'all',
|
||||||
notificationVisibility: {
|
notificationVisibility: {
|
||||||
follows: true,
|
follows: true,
|
||||||
|
@ -70,7 +74,7 @@ export const defaultState = {
|
||||||
hideFilteredStatuses: undefined, // instance default
|
hideFilteredStatuses: undefined, // instance default
|
||||||
playVideosInModal: false,
|
playVideosInModal: false,
|
||||||
useOneClickNsfw: false,
|
useOneClickNsfw: false,
|
||||||
useContainFit: false,
|
useContainFit: true,
|
||||||
greentext: undefined, // instance default
|
greentext: undefined, // instance default
|
||||||
useAtIcon: undefined, // instance default
|
useAtIcon: undefined, // instance default
|
||||||
mentionLinkDisplay: undefined, // instance default
|
mentionLinkDisplay: undefined, // instance default
|
||||||
|
@ -80,9 +84,15 @@ export const defaultState = {
|
||||||
mentionLinkShowYous: undefined, // instance default
|
mentionLinkShowYous: undefined, // instance default
|
||||||
mentionLinkBoldenYou: undefined, // instance default
|
mentionLinkBoldenYou: undefined, // instance default
|
||||||
hidePostStats: undefined, // instance default
|
hidePostStats: undefined, // instance default
|
||||||
|
hideBotIndication: undefined, // instance default
|
||||||
hideUserStats: undefined, // instance default
|
hideUserStats: undefined, // instance default
|
||||||
virtualScrolling: undefined, // instance default
|
virtualScrolling: undefined, // instance default
|
||||||
sensitiveByDefault: undefined // instance default
|
sensitiveByDefault: undefined, // instance default
|
||||||
|
conversationDisplay: undefined, // instance default
|
||||||
|
conversationTreeAdvanced: undefined, // instance default
|
||||||
|
conversationOtherRepliesButton: undefined, // instance default
|
||||||
|
conversationTreeFadeAncestors: undefined, // instance default
|
||||||
|
maxDepthInThread: undefined // instance default
|
||||||
}
|
}
|
||||||
|
|
||||||
// caching the instance default properties
|
// caching the instance default properties
|
||||||
|
|
|
@ -33,8 +33,10 @@ const defaultState = {
|
||||||
hideMutedThreads: true,
|
hideMutedThreads: true,
|
||||||
hideWordFilteredPosts: false,
|
hideWordFilteredPosts: false,
|
||||||
hidePostStats: false,
|
hidePostStats: false,
|
||||||
|
hideBotIndication: false,
|
||||||
hideSitename: false,
|
hideSitename: false,
|
||||||
hideUserStats: false,
|
hideUserStats: false,
|
||||||
|
muteBotStatuses: false,
|
||||||
loginMethod: 'password',
|
loginMethod: 'password',
|
||||||
logo: '/static/logo.svg',
|
logo: '/static/logo.svg',
|
||||||
logoMargin: '.2em',
|
logoMargin: '.2em',
|
||||||
|
@ -53,6 +55,11 @@ const defaultState = {
|
||||||
theme: 'pleroma-dark',
|
theme: 'pleroma-dark',
|
||||||
virtualScrolling: true,
|
virtualScrolling: true,
|
||||||
sensitiveByDefault: false,
|
sensitiveByDefault: false,
|
||||||
|
conversationDisplay: 'linear',
|
||||||
|
conversationTreeAdvanced: false,
|
||||||
|
conversationOtherRepliesButton: 'below',
|
||||||
|
conversationTreeFadeAncestors: false,
|
||||||
|
maxDepthInThread: 6,
|
||||||
|
|
||||||
// Nasty stuff
|
// Nasty stuff
|
||||||
customEmoji: [],
|
customEmoji: [],
|
||||||
|
|
137
src/modules/serverSideConfig.js
Normal file
137
src/modules/serverSideConfig.js
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
import { get, set } from 'lodash'
|
||||||
|
|
||||||
|
const defaultApi = ({ rootState, commit }, { path, value }) => {
|
||||||
|
const params = {}
|
||||||
|
set(params, path, value)
|
||||||
|
return rootState
|
||||||
|
.api
|
||||||
|
.backendInteractor
|
||||||
|
.updateProfile({ params })
|
||||||
|
.then(result => {
|
||||||
|
commit('addNewUsers', [result])
|
||||||
|
commit('setCurrentUser', result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const notificationsApi = ({ rootState, commit }, { path, value, oldValue }) => {
|
||||||
|
const settings = {}
|
||||||
|
set(settings, path, value)
|
||||||
|
return rootState
|
||||||
|
.api
|
||||||
|
.backendInteractor
|
||||||
|
.updateNotificationSettings({ settings })
|
||||||
|
.then(result => {
|
||||||
|
if (result.status === 'success') {
|
||||||
|
commit('confirmServerSideOption', { name, value })
|
||||||
|
} else {
|
||||||
|
commit('confirmServerSideOption', { name, value: oldValue })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map that stores relation between path for reading (from user profile),
|
||||||
|
* for writing (into API) an what API to use.
|
||||||
|
*
|
||||||
|
* Shorthand - instead of { get, set, api? } object it's possible to use string
|
||||||
|
* in case default api is used and get = set
|
||||||
|
*
|
||||||
|
* If no api is specified, defaultApi is used (see above)
|
||||||
|
*/
|
||||||
|
export const settingsMap = {
|
||||||
|
'defaultScope': 'source.privacy',
|
||||||
|
'defaultNSFW': 'source.sensitive', // BROKEN: pleroma/pleroma#2837
|
||||||
|
'stripRichContent': {
|
||||||
|
get: 'source.pleroma.no_rich_text',
|
||||||
|
set: 'no_rich_text'
|
||||||
|
},
|
||||||
|
// Privacy
|
||||||
|
'locked': 'locked',
|
||||||
|
'acceptChatMessages': {
|
||||||
|
get: 'pleroma.accepts_chat_messages',
|
||||||
|
set: 'accepts_chat_messages'
|
||||||
|
},
|
||||||
|
'allowFollowingMove': {
|
||||||
|
get: 'pleroma.allow_following_move',
|
||||||
|
set: 'allow_following_move'
|
||||||
|
},
|
||||||
|
'discoverable': 'source.discoverable',
|
||||||
|
'hideFavorites': {
|
||||||
|
get: 'pleroma.hide_favorites',
|
||||||
|
set: 'hide_favorites'
|
||||||
|
},
|
||||||
|
'hideFollowers': {
|
||||||
|
get: 'pleroma.hide_followers',
|
||||||
|
set: 'hide_followers'
|
||||||
|
},
|
||||||
|
'hideFollows': {
|
||||||
|
get: 'pleroma.hide_follows',
|
||||||
|
set: 'hide_follows'
|
||||||
|
},
|
||||||
|
'hideFollowersCount': {
|
||||||
|
get: 'pleroma.hide_followers_count',
|
||||||
|
set: 'hide_followers_count'
|
||||||
|
},
|
||||||
|
'hideFollowsCount': {
|
||||||
|
get: 'pleroma.hide_follows_count',
|
||||||
|
set: 'hide_follows_count'
|
||||||
|
},
|
||||||
|
// NotificationSettingsAPIs
|
||||||
|
'webPushHideContents': {
|
||||||
|
get: 'pleroma.notification_settings.hide_notification_contents',
|
||||||
|
set: 'hide_notification_contents',
|
||||||
|
api: notificationsApi
|
||||||
|
},
|
||||||
|
'blockNotificationsFromStrangers': {
|
||||||
|
get: 'pleroma.notification_settings.block_from_strangers',
|
||||||
|
set: 'block_from_strangers',
|
||||||
|
api: notificationsApi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultState = Object.fromEntries(Object.keys(settingsMap).map(key => [key, null]))
|
||||||
|
|
||||||
|
const serverSideConfig = {
|
||||||
|
state: { ...defaultState },
|
||||||
|
mutations: {
|
||||||
|
confirmServerSideOption (state, { name, value }) {
|
||||||
|
set(state, name, value)
|
||||||
|
},
|
||||||
|
wipeServerSideOption (state, { name }) {
|
||||||
|
set(state, name, null)
|
||||||
|
},
|
||||||
|
wipeAllServerSideOptions (state) {
|
||||||
|
Object.keys(settingsMap).forEach(key => {
|
||||||
|
set(state, key, null)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// Set the settings based on their path location
|
||||||
|
setCurrentUser (state, user) {
|
||||||
|
Object.entries(settingsMap).forEach((map) => {
|
||||||
|
const [name, value] = map
|
||||||
|
const { get: path = value } = value
|
||||||
|
set(state, name, get(user._original, path))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setServerSideOption ({ rootState, state, commit, dispatch }, { name, value }) {
|
||||||
|
const oldValue = get(state, name)
|
||||||
|
const map = settingsMap[name]
|
||||||
|
if (!map) throw new Error('Invalid server-side setting')
|
||||||
|
const { set: path = map, api = defaultApi } = map
|
||||||
|
commit('wipeServerSideOption', { name })
|
||||||
|
|
||||||
|
api({ rootState, commit }, { path, value, oldValue })
|
||||||
|
.catch((e) => {
|
||||||
|
console.warn('Error setting server-side option:', e)
|
||||||
|
commit('confirmServerSideOption', { name, value: oldValue })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
logout ({ commit }) {
|
||||||
|
commit('wipeAllServerSideOptions')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default serverSideConfig
|
|
@ -44,6 +44,7 @@ export const parseUser = (data) => {
|
||||||
const mastoShort = masto && !data.hasOwnProperty('avatar')
|
const mastoShort = masto && !data.hasOwnProperty('avatar')
|
||||||
|
|
||||||
output.id = String(data.id)
|
output.id = String(data.id)
|
||||||
|
output._original = data // used for server-side settings
|
||||||
|
|
||||||
if (masto) {
|
if (masto) {
|
||||||
output.screen_name = data.acct
|
output.screen_name = data.acct
|
||||||
|
|
|
@ -4,9 +4,15 @@ const DIRECTION_RIGHT = [1, 0]
|
||||||
const DIRECTION_UP = [0, -1]
|
const DIRECTION_UP = [0, -1]
|
||||||
const DIRECTION_DOWN = [0, 1]
|
const DIRECTION_DOWN = [0, 1]
|
||||||
|
|
||||||
|
const BUTTON_LEFT = 0
|
||||||
|
|
||||||
const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
|
const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
|
||||||
|
|
||||||
const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY])
|
const touchCoord = touch => [touch.screenX, touch.screenY]
|
||||||
|
|
||||||
|
const touchEventCoord = e => touchCoord(e.touches[0])
|
||||||
|
|
||||||
|
const pointerEventCoord = e => [e.clientX, e.clientY]
|
||||||
|
|
||||||
const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1])
|
const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1])
|
||||||
|
|
||||||
|
@ -61,6 +67,132 @@ const updateSwipe = (event, gesture) => {
|
||||||
gesture._swiping = false
|
gesture._swiping = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SwipeAndClickGesture {
|
||||||
|
// swipePreviewCallback(offsets: Array[Number])
|
||||||
|
// offsets: the offset vector which the underlying component should move, from the starting position
|
||||||
|
// swipeEndCallback(sign: 0|-1|1)
|
||||||
|
// sign: if the swipe does not meet the threshold, 0
|
||||||
|
// if the swipe meets the threshold in the positive direction, 1
|
||||||
|
// if the swipe meets the threshold in the negative direction, -1
|
||||||
|
constructor ({
|
||||||
|
direction,
|
||||||
|
// swipeStartCallback
|
||||||
|
swipePreviewCallback,
|
||||||
|
swipeEndCallback,
|
||||||
|
swipeCancelCallback,
|
||||||
|
swipelessClickCallback,
|
||||||
|
threshold = 30,
|
||||||
|
perpendicularTolerance = 1.0,
|
||||||
|
disableClickThreshold = 1
|
||||||
|
}) {
|
||||||
|
const nop = () => {}
|
||||||
|
this.direction = direction
|
||||||
|
this.swipePreviewCallback = swipePreviewCallback || nop
|
||||||
|
this.swipeEndCallback = swipeEndCallback || nop
|
||||||
|
this.swipeCancelCallback = swipeCancelCallback || nop
|
||||||
|
this.swipelessClickCallback = swipelessClickCallback || nop
|
||||||
|
this.threshold = typeof threshold === 'function' ? threshold : () => threshold
|
||||||
|
this.disableClickThreshold = typeof disableClickThreshold === 'function' ? disableClickThreshold : () => disableClickThreshold
|
||||||
|
this.perpendicularTolerance = perpendicularTolerance
|
||||||
|
this._reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
_reset () {
|
||||||
|
this._startPos = [0, 0]
|
||||||
|
this._pointerId = -1
|
||||||
|
this._swiping = false
|
||||||
|
this._swiped = false
|
||||||
|
this._preventNextClick = false
|
||||||
|
}
|
||||||
|
|
||||||
|
start (event) {
|
||||||
|
// Only handle left click
|
||||||
|
if (event.button !== BUTTON_LEFT) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this._startPos = pointerEventCoord(event)
|
||||||
|
this._pointerId = event.pointerId
|
||||||
|
this._swiping = true
|
||||||
|
this._swiped = false
|
||||||
|
}
|
||||||
|
|
||||||
|
move (event) {
|
||||||
|
if (this._swiping && this._pointerId === event.pointerId) {
|
||||||
|
this._swiped = true
|
||||||
|
|
||||||
|
const coord = pointerEventCoord(event)
|
||||||
|
const delta = deltaCoord(this._startPos, coord)
|
||||||
|
|
||||||
|
this.swipePreviewCallback(delta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel (event) {
|
||||||
|
if (!this._swiping || this._pointerId !== event.pointerId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.swipeCancelCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
end (event) {
|
||||||
|
if (!this._swiping) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._pointerId !== event.pointerId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this._swiping = false
|
||||||
|
|
||||||
|
// movement too small
|
||||||
|
const coord = pointerEventCoord(event)
|
||||||
|
const delta = deltaCoord(this._startPos, coord)
|
||||||
|
|
||||||
|
const sign = (() => {
|
||||||
|
if (vectorLength(delta) < this.threshold()) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// movement is opposite from direction
|
||||||
|
const isPositive = dotProduct(delta, this.direction) > 0
|
||||||
|
|
||||||
|
// movement perpendicular to direction is too much
|
||||||
|
const towardsDir = project(delta, this.direction)
|
||||||
|
const perpendicularDir = perpendicular(this.direction)
|
||||||
|
const towardsPerpendicular = project(delta, perpendicularDir)
|
||||||
|
if (
|
||||||
|
vectorLength(towardsDir) * this.perpendicularTolerance <
|
||||||
|
vectorLength(towardsPerpendicular)
|
||||||
|
) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return isPositive ? 1 : -1
|
||||||
|
})()
|
||||||
|
|
||||||
|
if (this._swiped) {
|
||||||
|
this.swipeEndCallback(sign)
|
||||||
|
}
|
||||||
|
this._reset()
|
||||||
|
// Only a mouse will fire click event when
|
||||||
|
// the end point is far from the starting point
|
||||||
|
// so for other kinds of pointers do not check
|
||||||
|
// whether we have swiped
|
||||||
|
if (vectorLength(delta) >= this.disableClickThreshold() && event.pointerType === 'mouse') {
|
||||||
|
this._preventNextClick = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
click (event) {
|
||||||
|
if (!this._preventNextClick) {
|
||||||
|
this.swipelessClickCallback()
|
||||||
|
}
|
||||||
|
this._reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const GestureService = {
|
const GestureService = {
|
||||||
DIRECTION_LEFT,
|
DIRECTION_LEFT,
|
||||||
DIRECTION_RIGHT,
|
DIRECTION_RIGHT,
|
||||||
|
@ -68,7 +200,8 @@ const GestureService = {
|
||||||
DIRECTION_DOWN,
|
DIRECTION_DOWN,
|
||||||
swipeGesture,
|
swipeGesture,
|
||||||
beginSwipe,
|
beginSwipe,
|
||||||
updateSwipe
|
updateSwipe,
|
||||||
|
SwipeAndClickGesture
|
||||||
}
|
}
|
||||||
|
|
||||||
export default GestureService
|
export default GestureService
|
||||||
|
|
Loading…
Reference in a new issue