Merge remote-tracking branch 'pleroma/develop' into improve_delete_user_confirmation_message
This commit is contained in:
commit
c34fdd46da
59 changed files with 4115 additions and 1601 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -36,19 +36,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Attachments are truncated just like post contents
|
||||
- Media modal now also displays description and counter position in gallery (i.e. 1/5)
|
||||
- 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
|
||||
### Added
|
||||
### Added
|
||||
- Added Apply and Reset buttons to the bottom of theme tab to minimize UI travel
|
||||
- Implemented user option to always show floating New Post button (normally mobile-only)
|
||||
- Display reasons for instance specific policies
|
||||
- Display reasons for instance specific policies
|
||||
- Added functionality to cancel follow request
|
||||
|
||||
### Fixed
|
||||
- Fixed link to external profile not working on user profiles
|
||||
- Fixed mobile shoutbox display
|
||||
- Fixed mobile shoutbox display
|
||||
- Fixed favicon badge not working in Chrome
|
||||
- Escape html more properly in subject/display name
|
||||
- Escape html more properly in subject/display name
|
||||
|
||||
|
||||
## [2.4.0] - 2021-08-08
|
||||
|
|
65
package.json
65
package.json
|
@ -16,43 +16,44 @@
|
|||
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.7.6",
|
||||
"@babel/runtime": "7.17.8",
|
||||
"@chenfengyuan/vue-qrcode": "1.0.2",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.32",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.1",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.1",
|
||||
"@fortawesome/vue-fontawesome": "2.0.0",
|
||||
"body-scroll-lock": "2.6.4",
|
||||
"@fortawesome/fontawesome-svg-core": "1.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "2.0.6",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
||||
"body-scroll-lock": "2.7.1",
|
||||
"chromatism": "3.0.0",
|
||||
"cropperjs": "1.4.3",
|
||||
"cropperjs": "1.5.12",
|
||||
"diff": "3.5.0",
|
||||
"escape-html": "1.0.3",
|
||||
"localforage": "1.7.3",
|
||||
"localforage": "1.10.0",
|
||||
"parse-link-header": "1.0.1",
|
||||
"phoenix": "1.4.0",
|
||||
"portal-vue": "2.1.4",
|
||||
"portal-vue": "2.1.7",
|
||||
"punycode.js": "2.1.0",
|
||||
"ruffle-mirror": "2021.4.11",
|
||||
"v-click-outside": "2.1.3",
|
||||
"ruffle-mirror": "2021.12.31",
|
||||
"v-click-outside": "2.1.5",
|
||||
"vue": "2.6.11",
|
||||
"vue-i18n": "7.8.1",
|
||||
"vue-router": "3.0.2",
|
||||
"vue-template-compiler": "2.6.11",
|
||||
"vuelidate": "0.7.4",
|
||||
"vuelidate": "0.7.7",
|
||||
"vuex": "3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.7.5",
|
||||
"@babel/plugin-transform-runtime": "7.7.6",
|
||||
"@babel/preset-env": "7.7.6",
|
||||
"@babel/register": "7.7.4",
|
||||
"@ungap/event-target": "0.1.0",
|
||||
"@babel/core": "7.17.8",
|
||||
"@babel/plugin-transform-runtime": "7.17.0",
|
||||
"@babel/preset-env": "7.16.11",
|
||||
"@babel/register": "7.17.7",
|
||||
"@ungap/event-target": "0.2.3",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "1.2.1",
|
||||
"@vue/babel-preset-jsx": "1.2.4",
|
||||
"@vue/test-utils": "1.0.0-beta.28",
|
||||
"autoprefixer": "6.7.7",
|
||||
"babel-eslint": "7.2.3",
|
||||
"babel-loader": "8.0.6",
|
||||
"babel-loader": "8.2.4",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"chai": "3.5.0",
|
||||
"chalk": "1.1.3",
|
||||
|
@ -65,46 +66,46 @@
|
|||
"eslint": "5.16.0",
|
||||
"eslint-config-standard": "12.0.0",
|
||||
"eslint-friendly-formatter": "2.0.7",
|
||||
"eslint-loader": "2.1.2",
|
||||
"eslint-plugin-import": "2.17.2",
|
||||
"eslint-loader": "2.2.1",
|
||||
"eslint-plugin-import": "2.25.4",
|
||||
"eslint-plugin-node": "7.0.1",
|
||||
"eslint-plugin-promise": "4.1.1",
|
||||
"eslint-plugin-standard": "4.0.0",
|
||||
"eslint-plugin-promise": "4.3.1",
|
||||
"eslint-plugin-standard": "4.1.0",
|
||||
"eslint-plugin-vue": "5.2.3",
|
||||
"eventsource-polyfill": "0.9.6",
|
||||
"express": "4.16.4",
|
||||
"express": "4.17.3",
|
||||
"file-loader": "3.0.1",
|
||||
"function-bind": "1.1.1",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"http-proxy-middleware": "0.17.4",
|
||||
"http-proxy-middleware": "0.21.0",
|
||||
"inject-loader": "2.0.1",
|
||||
"iso-639-1": "2.0.3",
|
||||
"iso-639-1": "2.1.13",
|
||||
"isparta-loader": "2.0.0",
|
||||
"json-loader": "0.5.7",
|
||||
"karma": "3.1.4",
|
||||
"karma-coverage": "1.1.2",
|
||||
"karma-firefox-launcher": "1.1.0",
|
||||
"karma-firefox-launcher": "1.3.0",
|
||||
"karma-mocha": "1.3.0",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"karma-sinon-chai": "2.0.2",
|
||||
"karma-sourcemap-loader": "0.3.8",
|
||||
"karma-spec-reporter": "0.0.33",
|
||||
"karma-webpack": "4.0.2",
|
||||
"lodash": "4.17.11",
|
||||
"lodash": "4.17.21",
|
||||
"lolex": "1.6.0",
|
||||
"mini-css-extract-plugin": "0.5.0",
|
||||
"mini-css-extract-plugin": "0.12.0",
|
||||
"mocha": "3.5.3",
|
||||
"nightwatch": "0.9.21",
|
||||
"opn": "4.0.2",
|
||||
"ora": "0.3.0",
|
||||
"ora": "0.4.1",
|
||||
"postcss-loader": "3.0.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"sass": "1.20.1",
|
||||
"sass-loader": "git://github.com/webpack-contrib/sass-loader",
|
||||
"sass-loader": "7.2.0",
|
||||
"selenium-server": "2.53.1",
|
||||
"semver": "5.6.0",
|
||||
"serviceworker-webpack-plugin": "1.0.1",
|
||||
"shelljs": "0.8.4",
|
||||
"shelljs": "0.8.5",
|
||||
"sinon": "2.4.1",
|
||||
"sinon-chai": "2.14.0",
|
||||
"stylelint": "13.6.1",
|
||||
|
@ -114,7 +115,7 @@
|
|||
"vue-loader": "14.2.4",
|
||||
"vue-style-loader": "4.1.2",
|
||||
"webpack": "4.46.0",
|
||||
"webpack-dev-middleware": "3.7.0",
|
||||
"webpack-dev-middleware": "3.7.3",
|
||||
"webpack-hot-middleware": "2.24.3",
|
||||
"webpack-merge": "0.14.1"
|
||||
},
|
||||
|
|
|
@ -30,3 +30,5 @@ $fallback--attachmentRadius: 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;
|
||||
|
||||
$status-margin: 0.75em;
|
||||
|
|
|
@ -115,6 +115,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
copyInstanceOption('nsfwCensorImage')
|
||||
copyInstanceOption('background')
|
||||
copyInstanceOption('hidePostStats')
|
||||
copyInstanceOption('hideBotIndication')
|
||||
copyInstanceOption('hideUserStats')
|
||||
copyInstanceOption('hideFilteredStatuses')
|
||||
copyInstanceOption('logo')
|
||||
|
|
|
@ -1,5 +1,19 @@
|
|||
import { reduce, filter, findIndex, clone, get } from 'lodash'
|
||||
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 idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
||||
|
@ -35,7 +49,10 @@ const conversation = {
|
|||
data () {
|
||||
return {
|
||||
highlight: null,
|
||||
expanded: false
|
||||
expanded: false,
|
||||
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
|
||||
statusContentPropertiesObject: {},
|
||||
inlineDivePosition: null
|
||||
}
|
||||
},
|
||||
props: [
|
||||
|
@ -53,12 +70,50 @@ const conversation = {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
hideStatus () {
|
||||
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
|
||||
return this.virtualHidden && this.$refs.statusComponent[0].suspendable
|
||||
} else {
|
||||
return this.virtualHidden
|
||||
maxDepthToShowByDefault () {
|
||||
// maxDepthInThread = max number of depths that is *visible*
|
||||
// since our depth starts with 0 and "showing" means "showing children"
|
||||
// there is a -2 here
|
||||
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 () {
|
||||
return this.$store.state.statuses.allStatusesObject[this.statusId]
|
||||
|
@ -90,6 +145,121 @@ const conversation = {
|
|||
|
||||
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 () {
|
||||
let i = 1
|
||||
// eslint-disable-next-line camelcase
|
||||
|
@ -109,15 +279,71 @@ const conversation = {
|
|||
}, {})
|
||||
},
|
||||
isExpanded () {
|
||||
return this.expanded || this.isPage
|
||||
return !!(this.expanded || this.isPage)
|
||||
},
|
||||
hiddenStyle () {
|
||||
const height = (this.status && this.status.virtualHeight) || '120px'
|
||||
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: {
|
||||
Status
|
||||
Status,
|
||||
ThreadTree
|
||||
},
|
||||
watch: {
|
||||
statusId (newVal, oldVal) {
|
||||
|
@ -132,6 +358,8 @@ const conversation = {
|
|||
expanded (value) {
|
||||
if (value) {
|
||||
this.fetchConversation()
|
||||
} else {
|
||||
this.resetDisplayState()
|
||||
}
|
||||
},
|
||||
virtualHidden (value) {
|
||||
|
@ -161,8 +389,8 @@ const conversation = {
|
|||
getReplies (id) {
|
||||
return this.replies[id] || []
|
||||
},
|
||||
focused (id) {
|
||||
return (this.isExpanded) && id === this.statusId
|
||||
getHighlight () {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
},
|
||||
setHighlight (id) {
|
||||
if (!id) return
|
||||
|
@ -170,15 +398,139 @@ const conversation = {
|
|||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
||||
},
|
||||
getHighlight () {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
},
|
||||
toggleExpanded () {
|
||||
this.expanded = !this.expanded
|
||||
},
|
||||
getConversationId (statusId) {
|
||||
const status = this.$store.state.statuses.allStatusesObject[statusId]
|
||||
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,24 +18,168 @@
|
|||
{{ $t('timeline.collapse') }}
|
||||
</button>
|
||||
</div>
|
||||
<status
|
||||
v-for="status in conversation"
|
||||
: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="getHighlight()"
|
||||
:replies="getReplies(status.id)"
|
||||
:in-profile="inProfile"
|
||||
:profile-user-id="profileUserId"
|
||||
class="conversation-status status-fadein panel-body"
|
||||
@goto="setHighlight"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
/>
|
||||
<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
|
||||
v-for="status in conversation"
|
||||
: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="getHighlight()"
|
||||
:replies="getReplies(status.id)"
|
||||
:in-profile="inProfile"
|
||||
:profile-user-id="profileUserId"
|
||||
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"
|
||||
@toggleExpanded="toggleExpanded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
|
@ -49,6 +193,45 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.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 {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
|
@ -56,12 +239,28 @@
|
|||
border-radius: 0;
|
||||
}
|
||||
|
||||
&.-expanded {
|
||||
.conversation-status:last-child {
|
||||
border-bottom: none;
|
||||
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
|
||||
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||
}
|
||||
.thread-ancestor-has-other-replies .conversation-status,
|
||||
.thread-ancestor:last-child .conversation-status,
|
||||
.thread-ancestor:last-child .thread-ancestor-dive-box,
|
||||
&.-expanded .thread-tree .conversation-status {
|
||||
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 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
|
||||
border-bottom: 1px solid var(--border, $fallback--border);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,32 +1,45 @@
|
|||
import StillImage from '../still-image/still-image.vue'
|
||||
import VideoAttachment from '../video_attachment/video_attachment.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 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 {
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faCircleNotch
|
||||
faCircleNotch,
|
||||
faTimes
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faChevronLeft,
|
||||
faChevronRight,
|
||||
faCircleNotch
|
||||
faCircleNotch,
|
||||
faTimes
|
||||
)
|
||||
|
||||
const MediaModal = {
|
||||
components: {
|
||||
StillImage,
|
||||
VideoAttachment,
|
||||
PinchZoom,
|
||||
SwipeClick,
|
||||
Modal,
|
||||
Flash
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false
|
||||
loading: false,
|
||||
swipeDirection: GestureService.DIRECTION_LEFT,
|
||||
swipeThreshold: () => {
|
||||
const considerableMoveRatio = 1 / 4
|
||||
return window.innerWidth * considerableMoveRatio
|
||||
},
|
||||
pinchZoomMinScale: 1,
|
||||
pinchZoomScaleResetLimit: 1.2
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -52,32 +65,26 @@ const MediaModal = {
|
|||
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: {
|
||||
getType (media) {
|
||||
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 () {
|
||||
this.$store.dispatch('closeMediaViewer')
|
||||
// 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')
|
||||
}, 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 () {
|
||||
if (this.canNavigate) {
|
||||
|
@ -102,6 +109,17 @@ const MediaModal = {
|
|||
onImageLoaded () {
|
||||
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) {
|
||||
if (this.showing && e.keyCode === 27) { // escape
|
||||
this.hide()
|
||||
|
|
|
@ -2,20 +2,38 @@
|
|||
<Modal
|
||||
v-if="showing"
|
||||
class="media-modal-view"
|
||||
@backdropClicked="hide"
|
||||
@backdropClicked="hideIfNotSwiped"
|
||||
>
|
||||
<img
|
||||
<SwipeClick
|
||||
v-if="type === 'image'"
|
||||
:class="{ loading }"
|
||||
class="modal-image"
|
||||
:src="currentMedia.url"
|
||||
:alt="currentMedia.description"
|
||||
:title="currentMedia.description"
|
||||
@touchstart.stop="mediaTouchStart"
|
||||
@touchmove.stop="mediaTouchMove"
|
||||
@click="hide"
|
||||
@load="onImageLoaded"
|
||||
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
|
||||
:class="{ loading }"
|
||||
class="modal-image"
|
||||
:src="currentMedia.url"
|
||||
:alt="currentMedia.description"
|
||||
:title="currentMedia.description"
|
||||
@load="onImageLoaded"
|
||||
>
|
||||
</PinchZoom>
|
||||
</SwipeClick>
|
||||
<VideoAttachment
|
||||
v-if="type === 'video'"
|
||||
class="modal-image"
|
||||
|
@ -40,25 +58,36 @@
|
|||
<button
|
||||
v-if="canNavigate"
|
||||
: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"
|
||||
>
|
||||
<FAIcon
|
||||
class="arrow-icon"
|
||||
class="button-icon arrow-icon"
|
||||
icon="chevron-left"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="canNavigate"
|
||||
: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"
|
||||
>
|
||||
<FAIcon
|
||||
class="arrow-icon"
|
||||
class="button-icon arrow-icon"
|
||||
icon="chevron-right"
|
||||
/>
|
||||
</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
|
||||
v-if="description"
|
||||
class="description"
|
||||
|
@ -86,11 +115,17 @@
|
|||
<script src="./media_modal.js"></script>
|
||||
|
||||
<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 {
|
||||
z-index: 1001;
|
||||
flex-direction: column;
|
||||
|
||||
.modal-view-button-arrow {
|
||||
.modal-view-button-arrow,
|
||||
.modal-view-button-hide {
|
||||
opacity: 0.75;
|
||||
|
||||
&:focus,
|
||||
|
@ -103,6 +138,7 @@
|
|||
opacity: 1;
|
||||
}
|
||||
}
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.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,
|
||||
.counter {
|
||||
/* Hardcoded since background is also hardcoded */
|
||||
|
@ -134,9 +193,8 @@
|
|||
}
|
||||
|
||||
.modal-image {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5);
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
image-orientation: from-image; // NOTE: only FF supports this
|
||||
animation: 0.1s cubic-bezier(0.7, 0, 1, 0.6) media-fadein;
|
||||
|
||||
|
@ -159,13 +217,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.modal-view-button-arrow {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 50%;
|
||||
margin-top: -50px;
|
||||
width: 70px;
|
||||
height: 100px;
|
||||
.modal-view-button {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
|
@ -175,14 +227,33 @@
|
|||
overflow: visible;
|
||||
cursor: pointer;
|
||||
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 {
|
||||
position: absolute;
|
||||
top: 35px;
|
||||
height: 30px;
|
||||
width: 32px;
|
||||
font-size: 14px;
|
||||
line-height: 30px;
|
||||
top: 0;
|
||||
line-height: $modal-view-button-icon-height;
|
||||
color: #FFF;
|
||||
text-align: center;
|
||||
background-color: rgba(0,0,0,.3);
|
||||
|
@ -191,16 +262,26 @@
|
|||
&--prev {
|
||||
left: 0;
|
||||
.arrow-icon {
|
||||
left: 6px;
|
||||
left: $modal-view-button-icon-margin;
|
||||
}
|
||||
}
|
||||
|
||||
&--next {
|
||||
right: 0;
|
||||
.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>
|
||||
|
|
|
@ -65,7 +65,6 @@
|
|||
color: var(--link);
|
||||
opacity: 0.8;
|
||||
display: inline-block;
|
||||
height: 50%;
|
||||
line-height: 1;
|
||||
padding: 0 0.1em;
|
||||
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;
|
||||
right: 5px;
|
||||
height: 100%;
|
||||
width: 0.875em;
|
||||
color: $fallback--text;
|
||||
color: var(--inputText, $fallback--text);
|
||||
line-height: 28px;
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
import { get, set } from 'lodash'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import ModifiedIndicator from './modified_indicator.vue'
|
||||
import ServerSideIndicator from './server_side_indicator.vue'
|
||||
export default {
|
||||
components: {
|
||||
Checkbox,
|
||||
ModifiedIndicator
|
||||
ModifiedIndicator,
|
||||
ServerSideIndicator
|
||||
},
|
||||
props: [
|
||||
'path',
|
||||
'disabled'
|
||||
'disabled',
|
||||
'expert'
|
||||
],
|
||||
computed: {
|
||||
pathDefault () {
|
||||
|
@ -26,8 +29,14 @@ export default {
|
|||
defaultState () {
|
||||
return get(this.$parent, this.pathDefault)
|
||||
},
|
||||
isServerSide () {
|
||||
return this.path.startsWith('serverSide_')
|
||||
},
|
||||
isChanged () {
|
||||
return this.state !== this.defaultState
|
||||
return !this.path.startsWith('serverSide_') && this.state !== this.defaultState
|
||||
},
|
||||
matchesExpertLevel () {
|
||||
return (this.expert || 0) <= this.$parent.expertLevel
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<label
|
||||
v-if="matchesExpertLevel"
|
||||
class="BooleanSetting"
|
||||
>
|
||||
<Checkbox
|
||||
|
@ -13,8 +14,7 @@
|
|||
>
|
||||
<slot />
|
||||
</span>
|
||||
<ModifiedIndicator :changed="isChanged" />
|
||||
</Checkbox>
|
||||
<ModifiedIndicator :changed="isChanged" /><ServerSideIndicator :server-side="isServerSide" /> </Checkbox>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
import { get, set } from 'lodash'
|
||||
import Select from 'src/components/select/select.vue'
|
||||
import ModifiedIndicator from './modified_indicator.vue'
|
||||
import ServerSideIndicator from './server_side_indicator.vue'
|
||||
export default {
|
||||
components: {
|
||||
Select,
|
||||
ModifiedIndicator
|
||||
ModifiedIndicator,
|
||||
ServerSideIndicator
|
||||
},
|
||||
props: [
|
||||
'path',
|
||||
'disabled',
|
||||
'options'
|
||||
'options',
|
||||
'expert'
|
||||
],
|
||||
computed: {
|
||||
pathDefault () {
|
||||
|
@ -27,8 +30,14 @@ export default {
|
|||
defaultState () {
|
||||
return get(this.$parent, this.pathDefault)
|
||||
},
|
||||
isServerSide () {
|
||||
return this.path.startsWith('serverSide_')
|
||||
},
|
||||
isChanged () {
|
||||
return this.state !== this.defaultState
|
||||
return !this.path.startsWith('serverSide_') && this.state !== this.defaultState
|
||||
},
|
||||
matchesExpertLevel () {
|
||||
return (this.expert || 0) <= this.$parent.expertLevel
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<label
|
||||
v-if="matchesExpertLevel"
|
||||
class="ChoiceSetting"
|
||||
>
|
||||
<slot />
|
||||
|
@ -18,6 +19,7 @@
|
|||
</option>
|
||||
</Select>
|
||||
<ModifiedIndicator :changed="isChanged" />
|
||||
<ServerSideIndicator :server-side="isServerSide" />
|
||||
</label>
|
||||
</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 serverSideConfigDefaultState } from 'src/modules/serverSideConfig.js'
|
||||
|
||||
const SharedComputedObject = () => ({
|
||||
user () {
|
||||
|
@ -22,6 +23,14 @@ const SharedComputedObject = () => ({
|
|||
}
|
||||
}])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
...Object.keys(serverSideConfigDefaultState)
|
||||
.map(key => ['serverSide_' + key, {
|
||||
get () { return this.$store.state.serverSideConfig[key] },
|
||||
set (value) {
|
||||
this.$store.dispatch('setServerSideOption', { name: key, value })
|
||||
}
|
||||
}])
|
||||
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
|
||||
// Special cases (need to transform values or perform actions first)
|
||||
useStreamingApi: {
|
||||
get () { return this.$store.getters.mergedConfig.useStreamingApi },
|
||||
|
|
|
@ -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 getResettableAsyncComponent from 'src/services/resettable_async_component.js'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import {
|
||||
|
@ -51,6 +52,7 @@ const SettingsModal = {
|
|||
components: {
|
||||
Modal,
|
||||
Popover,
|
||||
Checkbox,
|
||||
SettingsModalContent: getResettableAsyncComponent(
|
||||
() => import('./settings_modal_content.vue'),
|
||||
{
|
||||
|
@ -159,6 +161,15 @@ const SettingsModal = {
|
|||
},
|
||||
modalPeeked () {
|
||||
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,16 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-footer {
|
||||
display: flex;
|
||||
>* {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.extra-content {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
<div class="panel-body">
|
||||
<SettingsModalContent v-if="modalOpenedOnce" />
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<div class="panel-footer settings-footer">
|
||||
<Popover
|
||||
class="export"
|
||||
trigger="click"
|
||||
|
@ -108,6 +108,14 @@
|
|||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
|
||||
<Checkbox v-model="expertLevel">
|
||||
{{ $t("settings.expert_mode") }}
|
||||
</Checkbox>
|
||||
<portal-target
|
||||
class="extra-content"
|
||||
name="unscrolled-content"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { filter, trim } from 'lodash'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import ChoiceSetting from '../helpers/choice_setting.vue'
|
||||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
|
||||
|
@ -17,7 +18,8 @@ const FilteringTab = {
|
|||
},
|
||||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting
|
||||
ChoiceSetting,
|
||||
IntegerSetting
|
||||
},
|
||||
computed: {
|
||||
...SharedComputedObject(),
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
v-if="user"
|
||||
:disabled="hideFilteredStatuses"
|
||||
path="hideMutedThreads"
|
||||
>
|
||||
|
@ -29,6 +30,7 @@
|
|||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
v-if="user"
|
||||
:disabled="hideFilteredStatuses"
|
||||
path="hideMutedPosts"
|
||||
>
|
||||
|
@ -37,12 +39,23 @@
|
|||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="muteBotStatuses">
|
||||
{{ $t('settings.mute_bot_posts') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hidePostStats">
|
||||
{{ $t('settings.hide_post_stats') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideBotIndication">
|
||||
{{ $t('settings.hide_bot_indication') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<ChoiceSetting
|
||||
v-if="user"
|
||||
id="replyVisibility"
|
||||
path="replyVisibility"
|
||||
:options="replyVisibilityOptions"
|
||||
|
@ -59,7 +72,7 @@
|
|||
<div>{{ $t('settings.filtering_explanation') }}</div>
|
||||
</li>
|
||||
<h3>{{ $t('settings.attachments') }}</h3>
|
||||
<li>
|
||||
<li v-if="expertLevel > 0">
|
||||
<label for="maxThumbnails">
|
||||
{{ $t('settings.max_thumbnails') }}
|
||||
</label>
|
||||
|
@ -72,6 +85,14 @@
|
|||
step="1"
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<IntegerSetting
|
||||
path="maxThumbnails"
|
||||
:min="0"
|
||||
>
|
||||
{{ $t('settings.max_thumbnails') }}
|
||||
</IntegerSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="hideAttachments">
|
||||
{{ $t('settings.hide_attachments_in_tl') }}
|
||||
|
@ -84,7 +105,10 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div
|
||||
v-if="expertLevel > 0"
|
||||
class="setting-item"
|
||||
>
|
||||
<h2>{{ $t('settings.user_profiles') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
|
@ -94,46 +118,6 @@
|
|||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
</template>
|
||||
<script src="./filtering_tab.js"></script>
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import BooleanSetting from '../helpers/boolean_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 SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import ServerSideIndicator from '../helpers/server_side_indicator.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faGlobe
|
||||
|
@ -20,6 +23,16 @@ const GeneralTab = {
|
|||
value: 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 => ({
|
||||
key: mode,
|
||||
value: mode,
|
||||
|
@ -37,7 +50,10 @@ const GeneralTab = {
|
|||
components: {
|
||||
BooleanSetting,
|
||||
ChoiceSetting,
|
||||
InterfaceLanguageSwitcher
|
||||
IntegerSetting,
|
||||
InterfaceLanguageSwitcher,
|
||||
ScopeSelector,
|
||||
ServerSideIndicator
|
||||
},
|
||||
computed: {
|
||||
postFormats () {
|
||||
|
@ -57,6 +73,11 @@ const GeneralTab = {
|
|||
},
|
||||
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
|
||||
...SharedComputedObject()
|
||||
},
|
||||
methods: {
|
||||
changeDefaultScope (value) {
|
||||
this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -45,26 +45,42 @@
|
|||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="useStreamingApi">
|
||||
<BooleanSetting
|
||||
path="useStreamingApi"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.useStreamingApi') }}
|
||||
<br>
|
||||
<small>
|
||||
{{ $t('settings.useStreamingApiWarning') }}
|
||||
</small>
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="virtualScrolling">
|
||||
<BooleanSetting
|
||||
path="virtualScrolling"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.virtual_scrolling') }}
|
||||
</BooleanSetting>
|
||||
</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') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li v-if="instanceShoutboxPresent">
|
||||
<BooleanSetting path="hideShoutbox">
|
||||
<BooleanSetting
|
||||
path="hideShoutbox"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.hide_shoutbox') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
|
@ -73,19 +89,80 @@
|
|||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.post_look_feel') }}</h2>
|
||||
<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>
|
||||
<BooleanSetting path="collapseMessageWithSubject">
|
||||
{{ $t('settings.collapse_subject') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="emojiReactionsOnTimeline">
|
||||
<BooleanSetting
|
||||
path="emojiReactionsOnTimeline"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.emoji_reactions_on_timeline') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
v-if="user"
|
||||
path="serverSide_stripRichContent"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.no_rich_text_description') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<h3>{{ $t('settings.attachments') }}</h3>
|
||||
<li>
|
||||
<BooleanSetting path="useContainFit">
|
||||
<BooleanSetting
|
||||
path="useContainFit"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.use_contain_fit') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
|
@ -98,6 +175,7 @@
|
|||
<li>
|
||||
<BooleanSetting
|
||||
path="preloadImage"
|
||||
expert="1"
|
||||
:disabled="!hideNsfw"
|
||||
>
|
||||
{{ $t('settings.preload_images') }}
|
||||
|
@ -106,6 +184,7 @@
|
|||
<li>
|
||||
<BooleanSetting
|
||||
path="useOneClickNsfw"
|
||||
expert="1"
|
||||
:disabled="!hideNsfw"
|
||||
>
|
||||
{{ $t('settings.use_one_click_nsfw') }}
|
||||
|
@ -113,7 +192,10 @@
|
|||
</li>
|
||||
</ul>
|
||||
<li>
|
||||
<BooleanSetting path="loopVideo">
|
||||
<BooleanSetting
|
||||
path="loopVideo"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.loop_video') }}
|
||||
</BooleanSetting>
|
||||
<ul
|
||||
|
@ -123,6 +205,7 @@
|
|||
<li>
|
||||
<BooleanSetting
|
||||
path="loopVideoSilentOnly"
|
||||
expert="1"
|
||||
:disabled="!loopVideo || !loopSilentAvailable"
|
||||
>
|
||||
{{ $t('settings.loop_video_silent_only') }}
|
||||
|
@ -137,21 +220,14 @@
|
|||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="playVideosInModal">
|
||||
<BooleanSetting
|
||||
path="playVideosInModal"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.play_videos_in_modal') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<h3>{{ $t('settings.fun') }}</h3>
|
||||
<li>
|
||||
<BooleanSetting path="greentext">
|
||||
{{ $t('settings.greentext') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="mentionLinkShowYous">
|
||||
{{ $t('settings.show_yous') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<h3>{{ $t('settings.mention_links') }}</h3>
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
id="mentionLinkDisplay"
|
||||
|
@ -164,47 +240,103 @@
|
|||
<ul
|
||||
class="setting-list suboptions"
|
||||
>
|
||||
<li
|
||||
v-if="mentionLinkDisplay === 'short'"
|
||||
>
|
||||
<BooleanSetting path="mentionLinkShowTooltip">
|
||||
<li v-if="mentionLinkDisplay === 'short'">
|
||||
<BooleanSetting
|
||||
path="mentionLinkShowTooltip"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.mention_link_show_tooltip') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="useAtIcon">
|
||||
{{ $t('settings.use_at_icon') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="mentionLinkShowAvatar">
|
||||
{{ $t('settings.mention_link_show_avatar') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="mentionLinkFadeDomain">
|
||||
{{ $t('settings.mention_link_fade_domain') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="mentionLinkBoldenYou">
|
||||
{{ $t('settings.mention_link_bolden_you') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="useAtIcon"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.use_at_icon') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="mentionLinkShowAvatar">
|
||||
{{ $t('settings.mention_link_show_avatar') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="mentionLinkFadeDomain"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.mention_link_fade_domain') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li v-if="user">
|
||||
<BooleanSetting
|
||||
path="mentionLinkBoldenYou"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.mention_link_bolden_you') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div
|
||||
v-if="user"
|
||||
class="setting-item"
|
||||
>
|
||||
<h2>{{ $t('settings.composing') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<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') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="alwaysShowSubjectInput">
|
||||
<BooleanSetting
|
||||
path="alwaysShowSubjectInput"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.subject_input_always_show') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
|
@ -213,6 +345,7 @@
|
|||
id="subjectLineBehavior"
|
||||
path="subjectLineBehavior"
|
||||
:options="subjectLineOptions"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.subject_line_behavior') }}
|
||||
</ChoiceSetting>
|
||||
|
@ -227,43 +360,39 @@
|
|||
</ChoiceSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="minimalScopesMode">
|
||||
<BooleanSetting
|
||||
path="minimalScopesMode"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.minimal_scopes_mode') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="sensitiveByDefault">
|
||||
{{ $t('settings.sensitive_by_default') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="alwaysShowNewPostButton">
|
||||
<BooleanSetting
|
||||
path="alwaysShowNewPostButton"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.always_show_post_button') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="autohideFloatingPostButton">
|
||||
<BooleanSetting
|
||||
path="autohideFloatingPostButton"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.autohide_floating_post_button') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="padEmoji">
|
||||
<BooleanSetting
|
||||
path="padEmoji"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.pad_emoji') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
</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 = {
|
||||
data () {
|
||||
|
@ -9,12 +10,13 @@ const NotificationsTab = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
Checkbox
|
||||
BooleanSetting
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
}
|
||||
},
|
||||
...SharedComputedObject()
|
||||
},
|
||||
methods: {
|
||||
updateNotificationSettings () {
|
||||
|
|
|
@ -2,30 +2,77 @@
|
|||
<div :label="$t('settings.notifications')">
|
||||
<div class="setting-item">
|
||||
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
|
||||
<p>
|
||||
<Checkbox v-model="notificationSettings.block_from_strangers">
|
||||
{{ $t('settings.notification_setting_block_from_strangers') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<BooleanSetting path="serverSide_blockNotificationsFromStrangers">
|
||||
{{ $t('settings.notification_setting_block_from_strangers') }}
|
||||
</BooleanSetting>
|
||||
</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 class="setting-item">
|
||||
<div
|
||||
v-if="expertLevel > 0"
|
||||
class="setting-item"
|
||||
>
|
||||
<h2>{{ $t('settings.notification_setting_privacy') }}</h2>
|
||||
<p>
|
||||
<Checkbox v-model="notificationSettings.hide_notification_contents">
|
||||
{{ $t('settings.notification_setting_hide_notification_contents') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<ul class="setting-list">
|
||||
<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') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<p>{{ $t('settings.notification_mutes') }}</p>
|
||||
<p>{{ $t('settings.notification_blocks') }}</p>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="updateNotificationSettings"
|
||||
>
|
||||
{{ $t('settings.save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 Autosuggest from 'src/components/autosuggest/autosuggest.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 {
|
||||
faTimes,
|
||||
|
@ -27,18 +30,10 @@ const ProfileTab = {
|
|||
newName: this.$store.state.users.currentUser.name_unescaped,
|
||||
newBio: unescape(this.$store.state.users.currentUser.description),
|
||||
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 })),
|
||||
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,
|
||||
role: this.$store.state.users.currentUser.role,
|
||||
discoverable: this.$store.state.users.currentUser.discoverable,
|
||||
bot: this.$store.state.users.currentUser.bot,
|
||||
allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
|
||||
pickAvatarBtnVisible: true,
|
||||
bannerUploading: false,
|
||||
backgroundUploading: false,
|
||||
|
@ -54,12 +49,14 @@ const ProfileTab = {
|
|||
EmojiInput,
|
||||
Autosuggest,
|
||||
ProgressButton,
|
||||
Checkbox
|
||||
Checkbox,
|
||||
BooleanSetting
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
...SharedComputedObject(),
|
||||
emojiUserSuggestor () {
|
||||
return suggestor({
|
||||
emoji: [
|
||||
|
@ -123,15 +120,7 @@ const ProfileTab = {
|
|||
/* eslint-disable camelcase */
|
||||
display_name: this.newName,
|
||||
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,
|
||||
allow_following_move: this.allowFollowingMove,
|
||||
hide_follows_count: this.hideFollowsCount,
|
||||
hide_followers_count: this.hideFollowersCount,
|
||||
show_role: this.showRole
|
||||
/* eslint-enable camelcase */
|
||||
} }).then((user) => {
|
||||
|
|
|
@ -25,61 +25,6 @@
|
|||
class="bio resize-height"
|
||||
/>
|
||||
</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'">
|
||||
<Checkbox v-model="showRole">
|
||||
<template v-if="role === 'admin'">
|
||||
|
@ -90,11 +35,6 @@
|
|||
</template>
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<Checkbox v-model="discoverable">
|
||||
{{ $t('settings.discoverable') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<div v-if="maxFields > 0">
|
||||
<p>{{ $t('settings.profile_fields.label') }}</p>
|
||||
<div
|
||||
|
@ -269,6 +209,67 @@
|
|||
{{ $t('settings.save') }}
|
||||
</button>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -378,6 +378,10 @@ export default {
|
|||
// To separate from other random JSON files and possible future source formats
|
||||
_pleroma_theme_version: 2, theme, source
|
||||
}
|
||||
},
|
||||
isActive () {
|
||||
const tabSwitcher = this.$parent
|
||||
return tabSwitcher ? tabSwitcher.isActive('theme') : false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
|
@ -268,13 +268,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.apply-container {
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 5px;
|
||||
}
|
||||
|
||||
.radius-item,
|
||||
.color-item {
|
||||
min-width: 20em;
|
||||
|
@ -334,16 +327,25 @@
|
|||
padding: 20px;
|
||||
}
|
||||
|
||||
.apply-container {
|
||||
.btn {
|
||||
min-height: 28px;
|
||||
min-width: 10em;
|
||||
padding: 0 2em;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-left: .25em;
|
||||
margin-right: .25em;
|
||||
}
|
||||
}
|
||||
|
||||
.extra-content {
|
||||
.apply-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
flex-grow: 1;
|
||||
|
||||
.btn {
|
||||
flex-grow: 1;
|
||||
min-height: 28px;
|
||||
min-width: 0;
|
||||
max-width: 10em;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1016,21 +1016,26 @@
|
|||
</tab-switcher>
|
||||
</keep-alive>
|
||||
|
||||
<div class="apply-container">
|
||||
<button
|
||||
class="btn button-default submit"
|
||||
:disabled="!themeValid"
|
||||
@click="setCustomTheme"
|
||||
>
|
||||
{{ $t('general.apply') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="clearAll"
|
||||
>
|
||||
{{ $t('settings.style.switcher.reset') }}
|
||||
</button>
|
||||
</div>
|
||||
<portal
|
||||
v-if="isActive"
|
||||
to="unscrolled-content"
|
||||
>
|
||||
<div class="apply-container">
|
||||
<button
|
||||
class="btn button-default submit"
|
||||
:disabled="!themeValid"
|
||||
@click="setCustomTheme"
|
||||
>
|
||||
{{ $t('general.apply') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="clearAll"
|
||||
>
|
||||
{{ $t('settings.style.switcher.reset') }}
|
||||
</button>
|
||||
</div>
|
||||
</portal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -35,7 +35,10 @@ import {
|
|||
faStar,
|
||||
faEyeSlash,
|
||||
faEye,
|
||||
faThumbtack
|
||||
faThumbtack,
|
||||
faChevronUp,
|
||||
faChevronDown,
|
||||
faAngleDoubleRight
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
|
@ -52,9 +55,47 @@ library.add(
|
|||
faEllipsisH,
|
||||
faEyeSlash,
|
||||
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 = {
|
||||
name: 'Status',
|
||||
components: {
|
||||
|
@ -89,20 +130,38 @@ const Status = {
|
|||
'inlineExpanded',
|
||||
'showPinned',
|
||||
'inProfile',
|
||||
'profileUserId'
|
||||
'profileUserId',
|
||||
|
||||
'simpleTree',
|
||||
'controlledThreadDisplayStatus',
|
||||
'controlledToggleThreadDisplay',
|
||||
'showOtherRepliesAsButton',
|
||||
|
||||
'controlledShowingTall',
|
||||
'controlledToggleShowingTall',
|
||||
'controlledExpandingSubject',
|
||||
'controlledToggleExpandingSubject',
|
||||
'controlledShowingLongSubject',
|
||||
'controlledToggleShowingLongSubject',
|
||||
'controlledReplying',
|
||||
'controlledToggleReplying',
|
||||
'controlledMediaPlaying',
|
||||
'controlledSetMediaPlaying',
|
||||
'dive'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
replying: false,
|
||||
uncontrolledReplying: false,
|
||||
unmuted: false,
|
||||
userExpanded: false,
|
||||
mediaPlaying: [],
|
||||
uncontrolledMediaPlaying: [],
|
||||
suspendable: true,
|
||||
error: null,
|
||||
headTailLinks: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...controlledOrUncontrolledGetters(['replying', 'mediaPlaying']),
|
||||
muteWords () {
|
||||
return this.mergedConfig.muteWords
|
||||
},
|
||||
|
@ -166,6 +225,18 @@ const Status = {
|
|||
muteWordHits () {
|
||||
return muteWordHits(this.status, this.muteWords)
|
||||
},
|
||||
rtBotStatus () {
|
||||
return this.statusoid.user.bot
|
||||
},
|
||||
botStatus () {
|
||||
return this.status.user.bot
|
||||
},
|
||||
botIndicator () {
|
||||
return this.botStatus && !this.hideBotIndication
|
||||
},
|
||||
rtBotIndicator () {
|
||||
return this.rtBotStatus && !this.hideBotIndication
|
||||
},
|
||||
mentionsLine () {
|
||||
if (!this.headTailLinks) return []
|
||||
const writtenSet = new Set(this.headTailLinks.writtenMentions.map(_ => _.url))
|
||||
|
@ -191,7 +262,9 @@ const Status = {
|
|||
// Thread is muted
|
||||
status.thread_muted ||
|
||||
// Wordfiltered
|
||||
this.muteWordHits.length > 0
|
||||
this.muteWordHits.length > 0 ||
|
||||
// bot status
|
||||
(this.muteBotStatuses && this.botStatus && !this.compact)
|
||||
return !this.unmuted && !this.shouldNotMute && reasonsToMute
|
||||
},
|
||||
userIsMuted () {
|
||||
|
@ -293,6 +366,12 @@ const Status = {
|
|||
hidePostStats () {
|
||||
return this.mergedConfig.hidePostStats
|
||||
},
|
||||
muteBotStatuses () {
|
||||
return this.mergedConfig.muteBotStatuses
|
||||
},
|
||||
hideBotIndication () {
|
||||
return this.mergedConfig.hideBotIndication
|
||||
},
|
||||
currentUser () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
|
@ -304,6 +383,12 @@ const Status = {
|
|||
},
|
||||
isSuspendable () {
|
||||
return !this.replying && this.mediaPlaying.length === 0
|
||||
},
|
||||
inThreadForest () {
|
||||
return !!this.controlledThreadDisplayStatus
|
||||
},
|
||||
threadShowing () {
|
||||
return this.controlledThreadDisplayStatus === 'showing'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -326,7 +411,7 @@ const Status = {
|
|||
this.error = undefined
|
||||
},
|
||||
toggleReplying () {
|
||||
this.replying = !this.replying
|
||||
controlledOrUncontrolledToggle(this, 'replying')
|
||||
},
|
||||
gotoOriginal (id) {
|
||||
if (this.inConversation) {
|
||||
|
@ -346,17 +431,19 @@ const Status = {
|
|||
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||
},
|
||||
addMediaPlaying (id) {
|
||||
this.mediaPlaying.push(id)
|
||||
controlledOrUncontrolledSet(this, 'mediaPlaying', this.mediaPlaying.concat(id))
|
||||
},
|
||||
removeMediaPlaying (id) {
|
||||
this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id)
|
||||
controlledOrUncontrolledSet(this, 'mediaPlaying', this.mediaPlaying.filter(mediaId => mediaId !== id))
|
||||
},
|
||||
setHeadTailLinks (headTailLinks) {
|
||||
this.headTailLinks = headTailLinks
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'highlight': function (id) {
|
||||
},
|
||||
toggleThreadDisplay () {
|
||||
this.controlledToggleThreadDisplay()
|
||||
},
|
||||
scrollIfHighlighted (highlightId) {
|
||||
const id = highlightId
|
||||
if (this.status.id === id) {
|
||||
let rect = this.$el.getBoundingClientRect()
|
||||
if (rect.top < 100) {
|
||||
|
@ -370,6 +457,11 @@ const Status = {
|
|||
window.scrollBy(0, rect.bottom - window.innerHeight + 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'highlight': function (id) {
|
||||
this.scrollIfHighlighted(id)
|
||||
},
|
||||
'status.repeat_num': function (num) {
|
||||
// refetch repeats when repeat_num is changed in any way
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
$status-margin: 0.75em;
|
||||
|
||||
.Status {
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
|
@ -28,15 +26,8 @@ $status-margin: 0.75em;
|
|||
--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 {
|
||||
padding: $status-margin;
|
||||
padding: var(--status-margin, $status-margin);
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
display: flex;
|
||||
|
@ -49,7 +40,7 @@ $status-margin: 0.75em;
|
|||
|
||||
.status-container {
|
||||
display: flex;
|
||||
padding: $status-margin;
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
&.-repeat {
|
||||
padding-top: 0;
|
||||
|
@ -57,7 +48,7 @@ $status-margin: 0.75em;
|
|||
}
|
||||
|
||||
.pin {
|
||||
padding: $status-margin $status-margin 0;
|
||||
padding: var(--status-margin, $status-margin) var(--status-margin, $status-margin) 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
@ -73,7 +64,7 @@ $status-margin: 0.75em;
|
|||
}
|
||||
|
||||
.left-side {
|
||||
margin-right: $status-margin;
|
||||
margin-right: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
.right-side {
|
||||
|
@ -82,7 +73,7 @@ $status-margin: 0.75em;
|
|||
}
|
||||
|
||||
.usercard {
|
||||
margin-bottom: $status-margin;
|
||||
margin-bottom: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
.status-username {
|
||||
|
@ -248,7 +239,7 @@ $status-margin: 0.75em;
|
|||
}
|
||||
|
||||
.repeat-info {
|
||||
padding: 0.4em $status-margin;
|
||||
padding: 0.4em var(--status-margin, $status-margin);
|
||||
|
||||
.repeat-icon {
|
||||
color: $fallback--cGreen;
|
||||
|
@ -294,7 +285,7 @@ $status-margin: 0.75em;
|
|||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-top: $status-margin;
|
||||
margin-top: var(--status-margin, $status-margin);
|
||||
|
||||
> * {
|
||||
max-width: 4em;
|
||||
|
@ -362,7 +353,7 @@ $status-margin: 0.75em;
|
|||
}
|
||||
|
||||
.favs-repeated-users {
|
||||
margin-top: $status-margin;
|
||||
margin-top: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
.stats {
|
||||
|
@ -389,7 +380,7 @@ $status-margin: 0.75em;
|
|||
}
|
||||
|
||||
.stat-count {
|
||||
margin-right: $status-margin;
|
||||
margin-right: var(--status-margin, $status-margin);
|
||||
user-select: none;
|
||||
|
||||
.stat-title {
|
||||
|
@ -415,13 +406,13 @@ $status-margin: 0.75em;
|
|||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.avatar:not(.repeater-avatar) {
|
||||
.post-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
// TODO define those other way somehow?
|
||||
// stylelint-disable rscss/class-format
|
||||
&.avatar-compact {
|
||||
&.-compact {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@
|
|||
<UserAvatar
|
||||
v-if="retweet"
|
||||
class="left-side repeater-avatar"
|
||||
:bot="rtBotIndicator"
|
||||
:better-shadow="betterShadow"
|
||||
:user="statusoid.user"
|
||||
/>
|
||||
|
@ -124,6 +125,8 @@
|
|||
@click.stop.prevent.capture.native="toggleUserExpanded"
|
||||
>
|
||||
<UserAvatar
|
||||
class="post-avatar"
|
||||
:bot="botIndicator"
|
||||
:compact="compact"
|
||||
:better-shadow="betterShadow"
|
||||
:user="status.user"
|
||||
|
@ -219,6 +222,31 @@
|
|||
class="fa-scale-110"
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
<div
|
||||
|
@ -306,6 +334,12 @@
|
|||
:no-heading="noHeading"
|
||||
:highlight="highlight"
|
||||
: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)"
|
||||
@mediapause="removeMediaPlaying($event)"
|
||||
@parseReady="setHeadTailLinks"
|
||||
|
@ -315,7 +349,20 @@
|
|||
v-if="inConversation && !isPreview && replies && replies.length"
|
||||
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
|
||||
v-for="reply in replies"
|
||||
:key="reply.id"
|
||||
|
@ -407,7 +454,11 @@
|
|||
class="gravestone"
|
||||
>
|
||||
<div class="left-side">
|
||||
<UserAvatar :compact="compact" />
|
||||
<UserAvatar
|
||||
class="post-avatar"
|
||||
:compact="compact"
|
||||
:bot="botIndicator"
|
||||
/>
|
||||
</div>
|
||||
<div class="right-side">
|
||||
<div class="deleted-text">
|
||||
|
|
|
@ -26,14 +26,16 @@ const StatusContent = {
|
|||
'focused',
|
||||
'noHeading',
|
||||
'fullContent',
|
||||
'singleLine'
|
||||
'singleLine',
|
||||
'showingTall',
|
||||
'expandingSubject',
|
||||
'showingLongSubject',
|
||||
'toggleShowingTall',
|
||||
'toggleExpandingSubject',
|
||||
'toggleShowingLongSubject'
|
||||
],
|
||||
data () {
|
||||
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,
|
||||
parseReadyDone: false
|
||||
}
|
||||
|
@ -115,9 +117,9 @@ const StatusContent = {
|
|||
},
|
||||
toggleShowMore () {
|
||||
if (this.mightHideBecauseTall) {
|
||||
this.showingTall = !this.showingTall
|
||||
this.toggleShowingTall()
|
||||
} else if (this.mightHideBecauseSubject) {
|
||||
this.expandingSubject = !this.expandingSubject
|
||||
this.toggleExpandingSubject()
|
||||
}
|
||||
},
|
||||
generateTagLink (tag) {
|
||||
|
|
|
@ -17,14 +17,14 @@
|
|||
<button
|
||||
v-if="longSubject && showingLongSubject"
|
||||
class="button-unstyled -link tall-subject-hider"
|
||||
@click.prevent="showingLongSubject=false"
|
||||
@click.prevent="toggleShowingLongSubject"
|
||||
>
|
||||
{{ $t("status.hide_full_subject") }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="longSubject"
|
||||
class="button-unstyled -link tall-subject-hider"
|
||||
@click.prevent="showingLongSubject=true"
|
||||
@click.prevent="toggleShowingLongSubject"
|
||||
>
|
||||
{{ $t("status.show_full_subject") }}
|
||||
</button>
|
||||
|
|
|
@ -23,6 +23,30 @@ library.add(
|
|||
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 = {
|
||||
name: 'StatusContent',
|
||||
props: [
|
||||
|
@ -31,9 +55,22 @@ const StatusContent = {
|
|||
'focused',
|
||||
'noHeading',
|
||||
'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: {
|
||||
...controlledOrUncontrolledGetters(['showingTall', 'expandingSubject', 'showingLongSubject']),
|
||||
hideAttachments () {
|
||||
return (this.mergedConfig.hideAttachments && !this.inConversation) ||
|
||||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)
|
||||
|
@ -71,6 +108,21 @@ const StatusContent = {
|
|||
Gallery,
|
||||
LinkPreview,
|
||||
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"
|
||||
:compact="compact"
|
||||
: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)"
|
||||
>
|
||||
<div v-if="status.poll && status.poll.options && !compact">
|
||||
|
@ -52,10 +58,6 @@
|
|||
|
||||
<script src="./status_content.js" ></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
$status-margin: 0.75em;
|
||||
|
||||
.StatusContent {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
@load="onLoad"
|
||||
@error="onError"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</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>
|
|
@ -47,6 +47,12 @@ export default Vue.component('tab-switcher', {
|
|||
return this.active
|
||||
}
|
||||
},
|
||||
isActive () {
|
||||
return tabName => {
|
||||
const isWanted = slot => slot.data && slot.data.attrs['data-tab-name'] === tabName
|
||||
return this.$slots.default.findIndex(isWanted) === this.activeIndex
|
||||
}
|
||||
},
|
||||
settingsModalVisible () {
|
||||
return this.settingsModalState === 'visible'
|
||||
},
|
||||
|
|
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
|
||||
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"
|
||||
/>
|
||||
</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
|
||||
class="button-default dropdown-item"
|
||||
@click="hideMedia = !hideMedia"
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
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 = {
|
||||
props: [
|
||||
'user',
|
||||
'betterShadow',
|
||||
'compact'
|
||||
'compact',
|
||||
'bot'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
|
|
@ -1,18 +1,28 @@
|
|||
<template>
|
||||
<StillImage
|
||||
v-if="user"
|
||||
<span
|
||||
class="Avatar"
|
||||
:alt="user.screen_name_ui"
|
||||
:title="user.screen_name_ui"
|
||||
:src="imgSrc(user.profile_image_url_original)"
|
||||
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
|
||||
:image-load-error="imageLoadError"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="Avatar -placeholder"
|
||||
:class="{ 'avatar-compact': compact }"
|
||||
/>
|
||||
:class="{ '-compact': compact }"
|
||||
>
|
||||
<StillImage
|
||||
v-if="user"
|
||||
class="avatar"
|
||||
:alt="user.screen_name_ui"
|
||||
:title="user.screen_name_ui"
|
||||
:src="imgSrc(user.profile_image_url_original)"
|
||||
:image-load-error="imageLoadError"
|
||||
:class="{ '-compact': compact, '-better-shadow': betterShadow }"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="avatar -placeholder"
|
||||
:class="{ '-compact': compact }"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="bot"
|
||||
icon="robot"
|
||||
class="bot-indicator"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./user_avatar.js"></script>
|
||||
|
@ -25,36 +35,60 @@
|
|||
--_avatarShadowInset: var(--avatarStatusShadowInset);
|
||||
--_still-image-label-visibility: hidden;
|
||||
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
box-shadow: var(--_avatarShadowBox);
|
||||
border-radius: $fallback--avatarRadius;
|
||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.better-shadow {
|
||||
box-shadow: var(--_avatarShadowInset);
|
||||
filter: var(--_avatarShadowFilter);
|
||||
}
|
||||
|
||||
&.animated::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.avatar-compact {
|
||||
&.-compact {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: $fallback--avatarAltRadius;
|
||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||
}
|
||||
|
||||
&.-placeholder {
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--fg, $fallback--fg);
|
||||
.avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: var(--_avatarShadowBox);
|
||||
border-radius: $fallback--avatarRadius;
|
||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||
|
||||
&.-better-shadow {
|
||||
box-shadow: var(--_avatarShadowInset);
|
||||
filter: var(--_avatarShadowFilter);
|
||||
}
|
||||
|
||||
&.-animated::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.-compact {
|
||||
border-radius: $fallback--avatarAltRadius;
|
||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||
}
|
||||
|
||||
&.-placeholder {
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--fg, $fallback--fg);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.bot-indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: -0.2em;
|
||||
padding: 0.2em;
|
||||
background: rgba(127, 127, 127, 0.5);
|
||||
color: #fff;
|
||||
border-radius: var(--tooltipRadius);
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -119,7 +119,8 @@
|
|||
"media_modal": {
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"counter": "{current} / {total}"
|
||||
"counter": "{current} / {total}",
|
||||
"hide": "Close media viewer"
|
||||
},
|
||||
"nav": {
|
||||
"about": "About",
|
||||
|
@ -259,11 +260,14 @@
|
|||
},
|
||||
"settings": {
|
||||
"app_name": "App name",
|
||||
"expert_mode": "Show advanced",
|
||||
"save": "Save changes",
|
||||
"security": "Security",
|
||||
"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",
|
||||
"post_look_feel": "Posts Look & Feel",
|
||||
"mention_links": "Mention links",
|
||||
"mfa": {
|
||||
"otp": "OTP",
|
||||
"setup_otp": "Setup OTP",
|
||||
|
@ -351,6 +355,8 @@
|
|||
"hide_attachments_in_tl": "Hide attachments in timeline",
|
||||
"hide_media_previews": "Hide media previews",
|
||||
"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",
|
||||
"max_thumbnails": "Maximum amount of thumbnails per post (empty = no limit)",
|
||||
"hide_isp": "Hide instance-specific panel",
|
||||
|
@ -400,6 +406,7 @@
|
|||
"name": "Label",
|
||||
"value": "Content"
|
||||
},
|
||||
"account_privacy": "Privacy",
|
||||
"use_contain_fit": "Don't crop the attachment in thumbnails",
|
||||
"name": "Name",
|
||||
"name_bio": "Name & bio",
|
||||
|
@ -417,6 +424,7 @@
|
|||
"no_rich_text_description": "Strip rich text formatting from all posts",
|
||||
"no_blocks": "No blocks",
|
||||
"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_followers_description": "Don't show who's following me",
|
||||
"hide_follows_count_description": "Don't show follow count",
|
||||
|
@ -430,7 +438,7 @@
|
|||
"valid_until": "Valid until",
|
||||
"revoke_token": "Revoke",
|
||||
"panelRadius": "Panels",
|
||||
"pause_on_unfocused": "Pause streaming when tab is not focused",
|
||||
"pause_on_unfocused": "Pause when tab is not focused",
|
||||
"presets": "Presets",
|
||||
"profile_background": "Profile background",
|
||||
"profile_banner": "Profile banner",
|
||||
|
@ -465,13 +473,21 @@
|
|||
"subject_line_email": "Like email: \"re: subject\"",
|
||||
"subject_line_mastodon": "Like mastodon: copy as is",
|
||||
"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",
|
||||
"sensitive_by_default": "Mark posts as sensitive by default",
|
||||
"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",
|
||||
"useStreamingApi": "Receive posts and notifications real-time",
|
||||
"useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
|
||||
"text": "Text",
|
||||
"theme": "Theme",
|
||||
"theme_help": "Use hex color codes (#rrggbb) to customize your color theme.",
|
||||
|
@ -721,6 +737,7 @@
|
|||
"reply_to": "Reply to",
|
||||
"mentions": "Mentions",
|
||||
"replies_list": "Replies:",
|
||||
"replies_list_with_others": "Replies (+{numReplies} other): | Replies (+{numReplies} others):",
|
||||
"mute_conversation": "Mute conversation",
|
||||
"unmute_conversation": "Unmute conversation",
|
||||
"status_unavailable": "Status unavailable",
|
||||
|
@ -747,7 +764,18 @@
|
|||
"attachment_stop_flash": "Stop Flash player",
|
||||
"move_up": "Shift attachment left",
|
||||
"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": {
|
||||
"approve": "Approve",
|
||||
|
|
|
@ -11,6 +11,7 @@ import statusesModule from './modules/statuses.js'
|
|||
import usersModule from './modules/users.js'
|
||||
import apiModule from './modules/api.js'
|
||||
import configModule from './modules/config.js'
|
||||
import serverSideConfigModule from './modules/serverSideConfig.js'
|
||||
import shoutModule from './modules/shout.js'
|
||||
import oauthModule from './modules/oauth.js'
|
||||
import authFlowModule from './modules/auth_flow.js'
|
||||
|
@ -45,6 +46,8 @@ Vue.use(VueClickOutside)
|
|||
Vue.use(PortalVue)
|
||||
Vue.use(VBodyScrollLock)
|
||||
|
||||
Vue.config.ignoredElements = ['pinch-zoom']
|
||||
|
||||
Vue.component('FAIcon', FontAwesomeIcon)
|
||||
Vue.component('FALayers', FontAwesomeLayers)
|
||||
|
||||
|
@ -88,6 +91,7 @@ const persistedStateOptions = {
|
|||
users: usersModule,
|
||||
api: apiModule,
|
||||
config: configModule,
|
||||
serverSideConfig: serverSideConfigModule,
|
||||
shout: shoutModule,
|
||||
oauth: oauthModule,
|
||||
authFlow: authFlowModule,
|
||||
|
|
|
@ -12,10 +12,13 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0]
|
|||
export const multiChoiceProperties = [
|
||||
'postContentType',
|
||||
'subjectLineBehavior',
|
||||
'conversationDisplay', // tree | linear
|
||||
'conversationOtherRepliesButton', // below | inside
|
||||
'mentionLinkDisplay' // short | full_for_remote | full
|
||||
]
|
||||
|
||||
export const defaultState = {
|
||||
expertLevel: 0, // used to track which settings to show and hide
|
||||
colors: {},
|
||||
theme: undefined,
|
||||
customTheme: undefined,
|
||||
|
@ -27,6 +30,7 @@ export const defaultState = {
|
|||
hideMutedPosts: undefined, // instance default
|
||||
hideMutedThreads: undefined, // instance default
|
||||
hideWordFilteredPosts: undefined, // instance default
|
||||
muteBotStatuses: undefined, // instance default
|
||||
collapseMessageWithSubject: undefined, // instance default
|
||||
padEmoji: true,
|
||||
hideAttachments: false,
|
||||
|
@ -41,7 +45,7 @@ export const defaultState = {
|
|||
alwaysShowNewPostButton: false,
|
||||
autohideFloatingPostButton: false,
|
||||
pauseOnUnfocused: true,
|
||||
stopGifs: false,
|
||||
stopGifs: true,
|
||||
replyVisibility: 'all',
|
||||
notificationVisibility: {
|
||||
follows: true,
|
||||
|
@ -69,7 +73,7 @@ export const defaultState = {
|
|||
hideFilteredStatuses: undefined, // instance default
|
||||
playVideosInModal: false,
|
||||
useOneClickNsfw: false,
|
||||
useContainFit: false,
|
||||
useContainFit: true,
|
||||
greentext: undefined, // instance default
|
||||
useAtIcon: undefined, // instance default
|
||||
mentionLinkDisplay: undefined, // instance default
|
||||
|
@ -79,9 +83,15 @@ export const defaultState = {
|
|||
mentionLinkShowYous: undefined, // instance default
|
||||
mentionLinkBoldenYou: undefined, // instance default
|
||||
hidePostStats: undefined, // instance default
|
||||
hideBotIndication: undefined, // instance default
|
||||
hideUserStats: 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
|
||||
|
|
|
@ -33,8 +33,10 @@ const defaultState = {
|
|||
hideMutedThreads: true,
|
||||
hideWordFilteredPosts: false,
|
||||
hidePostStats: false,
|
||||
hideBotIndication: false,
|
||||
hideSitename: false,
|
||||
hideUserStats: false,
|
||||
muteBotStatuses: false,
|
||||
loginMethod: 'password',
|
||||
logo: '/static/logo.svg',
|
||||
logoMargin: '.2em',
|
||||
|
@ -53,6 +55,11 @@ const defaultState = {
|
|||
theme: 'pleroma-dark',
|
||||
virtualScrolling: true,
|
||||
sensitiveByDefault: false,
|
||||
conversationDisplay: 'linear',
|
||||
conversationTreeAdvanced: false,
|
||||
conversationOtherRepliesButton: 'below',
|
||||
conversationTreeFadeAncestors: false,
|
||||
maxDepthInThread: 6,
|
||||
|
||||
// Nasty stuff
|
||||
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')
|
||||
|
||||
output.id = String(data.id)
|
||||
output._original = data // used for server-side settings
|
||||
|
||||
if (masto) {
|
||||
output.screen_name = data.acct
|
||||
|
|
|
@ -4,9 +4,15 @@ const DIRECTION_RIGHT = [1, 0]
|
|||
const DIRECTION_UP = [0, -1]
|
||||
const DIRECTION_DOWN = [0, 1]
|
||||
|
||||
const BUTTON_LEFT = 0
|
||||
|
||||
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])
|
||||
|
||||
|
@ -61,6 +67,132 @@ const updateSwipe = (event, gesture) => {
|
|||
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 = {
|
||||
DIRECTION_LEFT,
|
||||
DIRECTION_RIGHT,
|
||||
|
@ -68,7 +200,8 @@ const GestureService = {
|
|||
DIRECTION_DOWN,
|
||||
swipeGesture,
|
||||
beginSwipe,
|
||||
updateSwipe
|
||||
updateSwipe,
|
||||
SwipeAndClickGesture
|
||||
}
|
||||
|
||||
export default GestureService
|
||||
|
|
Loading…
Reference in a new issue