diff --git a/CHANGELOG.md b/CHANGELOG.md
index ae54025a..d7cc6994 100644
--- a/CHANGELOG.md
+++ b/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
diff --git a/package.json b/package.json
index 57ef75bc..7cd94066 100644
--- a/package.json
+++ b/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"
},
diff --git a/src/_variables.scss b/src/_variables.scss
index 9004d551..099d3606 100644
--- a/src/_variables.scss
+++ b/src/_variables.scss
@@ -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;
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index cc0c7c5e..c4a0a800 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -115,6 +115,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('nsfwCensorImage')
copyInstanceOption('background')
copyInstanceOption('hidePostStats')
+ copyInstanceOption('hideBotIndication')
copyInstanceOption('hideUserStats')
copyInstanceOption('hideFilteredStatuses')
copyInstanceOption('logo')
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 069c0b40..2ef2977a 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -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 = {}
}
}
}
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 3fb26d92..7628ceaa 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -18,24 +18,168 @@
{{ $t('timeline.collapse') }}
-
+
+
+
+
+
+
+ {{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }}
+
+
+
+
+
+
+
+
+
+
+
+
.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);
}
}
diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js
index b8bce730..01a90377 100644
--- a/src/components/media_modal/media_modal.js
+++ b/src/components/media_modal/media_modal.js
@@ -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()
diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue
index 8680267b..708a43c6 100644
--- a/src/components/media_modal/media_modal.vue
+++ b/src/components/media_modal/media_modal.vue
@@ -2,20 +2,38 @@
-
+
+
+
+
+
+
diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss
index a4326296..1d856ff9 100644
--- a/src/components/mention_link/mention_link.scss
+++ b/src/components/mention_link/mention_link.scss
@@ -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%;
diff --git a/src/components/pinch_zoom/pinch_zoom.js b/src/components/pinch_zoom/pinch_zoom.js
new file mode 100644
index 00000000..82670ddf
--- /dev/null
+++ b/src/components/pinch_zoom/pinch_zoom.js
@@ -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)()
+ }
+}
diff --git a/src/components/pinch_zoom/pinch_zoom.vue b/src/components/pinch_zoom/pinch_zoom.vue
new file mode 100644
index 00000000..18d69719
--- /dev/null
+++ b/src/components/pinch_zoom/pinch_zoom.vue
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/src/components/select/select.vue b/src/components/select/select.vue
index 5ade1fa6..8d6528ff 100644
--- a/src/components/select/select.vue
+++ b/src/components/select/select.vue
@@ -51,6 +51,7 @@
bottom: 0;
right: 5px;
height: 100%;
+ width: 0.875em;
color: $fallback--text;
color: var(--inputText, $fallback--text);
line-height: 28px;
diff --git a/src/components/settings_modal/helpers/boolean_setting.js b/src/components/settings_modal/helpers/boolean_setting.js
index 5c52f697..353e551c 100644
--- a/src/components/settings_modal/helpers/boolean_setting.js
+++ b/src/components/settings_modal/helpers/boolean_setting.js
@@ -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: {
diff --git a/src/components/settings_modal/helpers/boolean_setting.vue b/src/components/settings_modal/helpers/boolean_setting.vue
index c3ee6583..e0d825f2 100644
--- a/src/components/settings_modal/helpers/boolean_setting.vue
+++ b/src/components/settings_modal/helpers/boolean_setting.vue
@@ -1,5 +1,6 @@
-
-
+
diff --git a/src/components/settings_modal/helpers/choice_setting.js b/src/components/settings_modal/helpers/choice_setting.js
index a15f6bac..4677d4c1 100644
--- a/src/components/settings_modal/helpers/choice_setting.js
+++ b/src/components/settings_modal/helpers/choice_setting.js
@@ -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: {
diff --git a/src/components/settings_modal/helpers/choice_setting.vue b/src/components/settings_modal/helpers/choice_setting.vue
index fa17661b..54f5d0a7 100644
--- a/src/components/settings_modal/helpers/choice_setting.vue
+++ b/src/components/settings_modal/helpers/choice_setting.vue
@@ -1,5 +1,6 @@
diff --git a/src/components/settings_modal/helpers/integer_setting.js b/src/components/settings_modal/helpers/integer_setting.js
new file mode 100644
index 00000000..4a19bd7c
--- /dev/null
+++ b/src/components/settings_modal/helpers/integer_setting.js
@@ -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))
+ }
+ }
+}
diff --git a/src/components/settings_modal/helpers/integer_setting.vue b/src/components/settings_modal/helpers/integer_setting.vue
new file mode 100644
index 00000000..408b0925
--- /dev/null
+++ b/src/components/settings_modal/helpers/integer_setting.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/helpers/server_side_indicator.vue b/src/components/settings_modal/helpers/server_side_indicator.vue
new file mode 100644
index 00000000..143a86a1
--- /dev/null
+++ b/src/components/settings_modal/helpers/server_side_indicator.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+ {{ $t('settings.setting_server_side') }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/settings_modal/helpers/shared_computed_object.js b/src/components/settings_modal/helpers/shared_computed_object.js
index 2c833c0c..12431dca 100644
--- a/src/components/settings_modal/helpers/shared_computed_object.js
+++ b/src/components/settings_modal/helpers/shared_computed_object.js
@@ -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 },
diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js
index 04043483..82ea410e 100644
--- a/src/components/settings_modal/settings_modal.js
+++ b/src/components/settings_modal/settings_modal.js
@@ -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 })
+ }
}
}
}
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
index 90446b36..2f7649a9 100644
--- a/src/components/settings_modal/settings_modal.scss
+++ b/src/components/settings_modal/settings_modal.scss
@@ -48,4 +48,16 @@
}
}
}
+
+ .settings-footer {
+ display: flex;
+ >* {
+ margin-right: 0.5em;
+ }
+
+ .extra-content {
+ display: flex;
+ flex-grow: 1;
+ }
+ }
}
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
index 583c2ecc..b8de7e7e 100644
--- a/src/components/settings_modal/settings_modal.vue
+++ b/src/components/settings_modal/settings_modal.vue
@@ -53,7 +53,7 @@
-
diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js
index 4eaf4217..73413b48 100644
--- a/src/components/settings_modal/tabs/filtering_tab.js
+++ b/src/components/settings_modal/tabs/filtering_tab.js
@@ -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(),
diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue
index 50ee20e0..dc48902f 100644
--- a/src/components/settings_modal/tabs/filtering_tab.vue
+++ b/src/components/settings_modal/tabs/filtering_tab.vue
@@ -21,6 +21,7 @@
@@ -29,6 +30,7 @@
@@ -37,12 +39,23 @@
+
+
+ {{ $t('settings.mute_bot_posts') }}
+
+
{{ $t('settings.hide_post_stats') }}
+
+
+ {{ $t('settings.hide_bot_indication') }}
+
+
{{ $t('settings.filtering_explanation') }}
{{ $t('settings.attachments') }}
-
+
@@ -72,6 +85,14 @@
step="1"
>
+
+
+ {{ $t('settings.max_thumbnails') }}
+
+
{{ $t('settings.hide_attachments_in_tl') }}
@@ -84,7 +105,10 @@
-
+
{{ $t('settings.user_profiles') }}
-
-
{{ $t('settings.notifications') }}
-
- -
- {{ $t('settings.notification_visibility') }}
-
- -
-
- {{ $t('settings.notification_visibility_likes') }}
-
-
- -
-
- {{ $t('settings.notification_visibility_repeats') }}
-
-
- -
-
- {{ $t('settings.notification_visibility_follows') }}
-
-
- -
-
- {{ $t('settings.notification_visibility_mentions') }}
-
-
- -
-
- {{ $t('settings.notification_visibility_moves') }}
-
-
- -
-
- {{ $t('settings.notification_visibility_emoji_reactions') }}
-
-
-
-
-
-
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index 952c328d..62d86176 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -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 })
+ }
}
}
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index eba3b268..a2c6bffa 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -45,26 +45,42 @@
-
+
{{ $t('settings.useStreamingApi') }}
-
-
- {{ $t('settings.useStreamingApiWarning') }}
-
-
+
{{ $t('settings.virtual_scrolling') }}
-
+
+ {{ $t('settings.always_show_post_button') }}
+
+
+
+
{{ $t('settings.autohide_floating_post_button') }}
-
+
{{ $t('settings.hide_shoutbox') }}
@@ -73,19 +89,80 @@
{{ $t('settings.post_look_feel') }}
+ -
+
+ {{ $t('settings.conversation_display') }}
+
+
+
+ -
+
+ {{ $t('settings.tree_advanced') }}
+
+
+ -
+
+ {{ $t('settings.tree_fade_ancestors') }}
+
+
+ -
+
+ {{ $t('settings.max_depth_in_thread') }}
+
+
+ -
+
+ {{ $t('settings.conversation_other_replies_button') }}
+
+
+
-
{{ $t('settings.collapse_subject') }}
-
-
+
{{ $t('settings.emoji_reactions_on_timeline') }}
+ -
+
+ {{ $t('settings.no_rich_text_description') }}
+
+
{{ $t('settings.attachments') }}
-
-
+
{{ $t('settings.use_contain_fit') }}
@@ -98,6 +175,7 @@
-
{{ $t('settings.preload_images') }}
@@ -106,6 +184,7 @@
-
{{ $t('settings.use_one_click_nsfw') }}
@@ -113,7 +192,10 @@
-
+
{{ $t('settings.loop_video') }}
{{ $t('settings.loop_video_silent_only') }}
@@ -137,21 +220,14 @@
-
+
{{ $t('settings.play_videos_in_modal') }}
-
{{ $t('settings.fun') }}
-
-
- {{ $t('settings.greentext') }}
-
-
-
-
- {{ $t('settings.show_yous') }}
-
-
+
{{ $t('settings.mention_links') }}
-
-
+
+
{{ $t('settings.mention_link_show_tooltip') }}
-
-
- {{ $t('settings.use_at_icon') }}
-
-
-
-
- {{ $t('settings.mention_link_show_avatar') }}
-
-
-
-
- {{ $t('settings.mention_link_fade_domain') }}
-
-
-
-
- {{ $t('settings.mention_link_bolden_you') }}
-
-
+
+
+ {{ $t('settings.use_at_icon') }}
+
+
+
+
+ {{ $t('settings.mention_link_show_avatar') }}
+
+
+
+
+ {{ $t('settings.mention_link_fade_domain') }}
+
+
+
+
+ {{ $t('settings.mention_link_bolden_you') }}
+
+
+
+ {{ $t('settings.fun') }}
+
+
+
+ {{ $t('settings.greentext') }}
+
+
+
+
+ {{ $t('settings.show_yous') }}
+
+
-
+
{{ $t('settings.composing') }}
-
-
+
+
+ -
+
+
+ {{ $t('settings.sensitive_by_default') }}
+
+
+ -
+
{{ $t('settings.scope_copy') }}
-
-
+
{{ $t('settings.subject_input_always_show') }}
@@ -213,6 +345,7 @@
id="subjectLineBehavior"
path="subjectLineBehavior"
:options="subjectLineOptions"
+ expert="1"
>
{{ $t('settings.subject_line_behavior') }}
@@ -227,43 +360,39 @@
-
-
+
{{ $t('settings.minimal_scopes_mode') }}
-
-
- {{ $t('settings.sensitive_by_default') }}
-
-
- -
-
+
{{ $t('settings.always_show_post_button') }}
-
-
+
{{ $t('settings.autohide_floating_post_button') }}
-
-
+
{{ $t('settings.pad_emoji') }}
-
-
-
{{ $t('settings.notifications') }}
-
- -
-
- {{ $t('settings.enable_web_push_notifications') }}
-
-
-
-
diff --git a/src/components/settings_modal/tabs/notifications_tab.js b/src/components/settings_modal/tabs/notifications_tab.js
index 3e44c95d..3c6ab87f 100644
--- a/src/components/settings_modal/tabs/notifications_tab.js
+++ b/src/components/settings_modal/tabs/notifications_tab.js
@@ -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 () {
diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue
index 7e0568ea..86be6095 100644
--- a/src/components/settings_modal/tabs/notifications_tab.vue
+++ b/src/components/settings_modal/tabs/notifications_tab.vue
@@ -2,30 +2,77 @@
{{ $t('settings.notification_setting_filters') }}
-
-
- {{ $t('settings.notification_setting_block_from_strangers') }}
-
-
+
+ -
+
+ {{ $t('settings.notification_setting_block_from_strangers') }}
+
+
+ -
+ {{ $t('settings.notification_visibility') }}
+
+ -
+
+ {{ $t('settings.notification_visibility_likes') }}
+
+
+ -
+
+ {{ $t('settings.notification_visibility_repeats') }}
+
+
+ -
+
+ {{ $t('settings.notification_visibility_follows') }}
+
+
+ -
+
+ {{ $t('settings.notification_visibility_mentions') }}
+
+
+ -
+
+ {{ $t('settings.notification_visibility_moves') }}
+
+
+ -
+
+ {{ $t('settings.notification_visibility_emoji_reactions') }}
+
+
+
+
+
-
+
{{ $t('settings.notification_setting_privacy') }}
-
-
- {{ $t('settings.notification_setting_hide_notification_contents') }}
-
-
+
+ -
+
+ {{ $t('settings.enable_web_push_notifications') }}
+
+
+ -
+
+ {{ $t('settings.notification_setting_hide_notification_contents') }}
+
+
+
{{ $t('settings.notification_mutes') }}
{{ $t('settings.notification_blocks') }}
-
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index 64079fcd..bee8a7bb 100644
--- a/src/components/settings_modal/tabs/profile_tab.js
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -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) => {
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index bb3c301d..e00f6e5b 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -25,61 +25,6 @@
class="bio resize-height"
/>
-
-
- {{ $t('settings.lock_account_description') }}
-
-
-
-
-
-
-
-
-
-
- {{ $t('settings.no_rich_text_description') }}
-
-
-
-
- {{ $t('settings.hide_follows_description') }}
-
-
-
-
- {{ $t('settings.hide_follows_count_description') }}
-
-
-
-
- {{ $t('settings.hide_followers_description') }}
-
-
-
-
- {{ $t('settings.hide_followers_count_description') }}
-
-
-
-
- {{ $t('settings.allow_following_move') }}
-
-
@@ -90,11 +35,6 @@
-
-
- {{ $t('settings.discoverable') }}
-
-
{{ $t('settings.profile_fields.label') }}
+
+
{{ $t('settings.account_privacy') }}
+
+ -
+
+ {{ $t('settings.lock_account_description') }}
+
+
+ -
+
+ {{ $t('settings.discoverable') }}
+
+
+ -
+
+ {{ $t('settings.allow_following_move') }}
+
+
+ -
+
+ {{ $t('settings.hide_favorites_description') }}
+
+
+ -
+
+ {{ $t('settings.hide_followers_description') }}
+
+
+ -
+
+ {{ $t('settings.hide_followers_count_description') }}
+
+
+
+
+ -
+
+ {{ $t('settings.hide_follows_description') }}
+
+
+ -
+
+ {{ $t('settings.hide_follows_count_description') }}
+
+
+
+
+
+
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
index 0b6669fc..6d140b6c 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
@@ -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: {
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
index 0db21537..21b49a32 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss
@@ -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;
+ }
+ }
+}
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
index c02986ed..c32f9353 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue
@@ -1016,21 +1016,26 @@
-
-
-
-
+
+
+
+
+
+
diff --git a/src/components/status/status.js b/src/components/status/status.js
index d8f94926..e54be241 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -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
diff --git a/src/components/status/status.scss b/src/components/status/status.scss
index 2028ade9..5ed43e0d 100644
--- a/src/components/status/status.scss
+++ b/src/components/status/status.scss
@@ -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;
}
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 3bb29db6..2387151e 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -77,6 +77,7 @@
@@ -124,6 +125,8 @@
@click.stop.prevent.capture.native="toggleUserExpanded"
>
+
+
-
{{ $t('status.replies_list') }}
+
+
+ {{ $t('status.replies_list') }}
+
-
+
diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js
index 91c33135..b8f6f9a0 100644
--- a/src/components/status_body/status_body.js
+++ b/src/components/status_body/status_body.js
@@ -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) {
diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue
index a088e6bc..24d842c2 100644
--- a/src/components/status_body/status_body.vue
+++ b/src/components/status_body/status_body.vue
@@ -17,14 +17,14 @@
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index dec8914a..cf72ccb8 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -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)
+ }
}
}
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 69635aad..9e7d7956 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -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)"
>
@@ -52,10 +58,6 @@
diff --git a/src/components/timeline/timeline_quick_settings.js b/src/components/timeline/timeline_quick_settings.js
index 7b4931ce..92d5ac14 100644
--- a/src/components/timeline/timeline_quick_settings.js
+++ b/src/components/timeline/timeline_quick_settings.js
@@ -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 })
+ }
}
}
}
diff --git a/src/components/timeline/timeline_quick_settings.vue b/src/components/timeline/timeline_quick_settings.vue
index 98996ebd..4d67e06b 100644
--- a/src/components/timeline/timeline_quick_settings.vue
+++ b/src/components/timeline/timeline_quick_settings.vue
@@ -39,6 +39,15 @@
class="dropdown-divider"
/>
+