aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorIlja <ilja@ilja.space>2022-03-20 09:53:57 +0100
committerIlja <ilja@ilja.space>2022-03-20 09:53:57 +0100
commitd0bfd9a80826a7cdb37e1a84cf70ab8a44c3579e (patch)
tree4a0fbf2367e7bb8f4d2e71cb2499f15416bf97ef /src
parentf9393b0dab22279e9bdb97a1cf52fecfe278a4a7 (diff)
parent6f1d953642473412c094e5b723eb479e2b19a542 (diff)
Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma-fe into feat/report-notification
Diffstat (limited to 'src')
-rw-r--r--src/_variables.scss2
-rw-r--r--src/boot/after_store.js1
-rw-r--r--src/components/attachment/attachment.vue1
-rw-r--r--src/components/conversation/conversation.js374
-rw-r--r--src/components/conversation/conversation.vue247
-rw-r--r--src/components/media_modal/media_modal.js68
-rw-r--r--src/components/media_modal/media_modal.vue147
-rw-r--r--src/components/mention_link/mention_link.scss1
-rw-r--r--src/components/pinch_zoom/pinch_zoom.js13
-rw-r--r--src/components/pinch_zoom/pinch_zoom.vue11
-rw-r--r--src/components/select/select.vue1
-rw-r--r--src/components/settings_modal/helpers/boolean_setting.js15
-rw-r--r--src/components/settings_modal/helpers/boolean_setting.vue4
-rw-r--r--src/components/settings_modal/helpers/choice_setting.js15
-rw-r--r--src/components/settings_modal/helpers/choice_setting.vue2
-rw-r--r--src/components/settings_modal/helpers/integer_setting.js41
-rw-r--r--src/components/settings_modal/helpers/integer_setting.vue23
-rw-r--r--src/components/settings_modal/helpers/server_side_indicator.vue51
-rw-r--r--src/components/settings_modal/helpers/shared_computed_object.js9
-rw-r--r--src/components/settings_modal/settings_modal.js11
-rw-r--r--src/components/settings_modal/settings_modal.scss7
-rw-r--r--src/components/settings_modal/settings_modal.vue6
-rw-r--r--src/components/settings_modal/tabs/filtering_tab.js4
-rw-r--r--src/components/settings_modal/tabs/filtering_tab.vue68
-rw-r--r--src/components/settings_modal/tabs/general_tab.js23
-rw-r--r--src/components/settings_modal/tabs/general_tab.vue269
-rw-r--r--src/components/settings_modal/tabs/notifications_tab.js8
-rw-r--r--src/components/settings_modal/tabs/notifications_tab.vue81
-rw-r--r--src/components/settings_modal/tabs/profile_tab.js23
-rw-r--r--src/components/settings_modal/tabs/profile_tab.vue121
-rw-r--r--src/components/status/status.js112
-rw-r--r--src/components/status/status.scss27
-rw-r--r--src/components/status/status.vue53
-rw-r--r--src/components/status_body/status_body.js16
-rw-r--r--src/components/status_body/status_body.vue4
-rw-r--r--src/components/status_content/status_content.js54
-rw-r--r--src/components/status_content/status_content.vue10
-rw-r--r--src/components/still-image/still-image.vue1
-rw-r--r--src/components/swipe_click/swipe_click.js84
-rw-r--r--src/components/swipe_click/swipe_click.vue14
-rw-r--r--src/components/thread_tree/thread_tree.js90
-rw-r--r--src/components/thread_tree/thread_tree.vue127
-rw-r--r--src/components/timeline/timeline_quick_settings.js7
-rw-r--r--src/components/timeline/timeline_quick_settings.vue9
-rw-r--r--src/components/user_avatar/user_avatar.js13
-rw-r--r--src/components/user_avatar/user_avatar.vue14
-rw-r--r--src/i18n/en.json38
-rw-r--r--src/main.js4
-rw-r--r--src/modules/config.js16
-rw-r--r--src/modules/instance.js7
-rw-r--r--src/modules/serverSideConfig.js137
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js1
-rw-r--r--src/services/gesture_service/gesture_service.js137
53 files changed, 2249 insertions, 373 deletions
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/attachment/attachment.vue b/src/components/attachment/attachment.vue
index c4399f30..2a89886d 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -12,6 +12,7 @@
:href="attachment.url"
:alt="attachment.description"
:title="attachment.description"
+ @click.prevent
>
<FAIcon :icon="placeholderIconClass" />
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }}
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,13 +70,51 @@ const conversation = {
}
},
computed: {
- hideStatus () {
+ 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.virtualHidden && this.$refs.statusComponent[0].suspendable
+ return this.$refs.statusComponent.every(s => s.suspendable)
} else {
- return this.virtualHidden
+ 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') }}
</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,19 +193,74 @@
@import '../../_variables.scss';
.Conversation {
- .conversation-status {
+ .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);
}
- &.-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.-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;
+ border-bottom-color: var(--border, $fallback--border);
+ border-radius: 0;
+ }
+
+ .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>
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 @@
<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;
- .arrow-icon {
+ .button-icon {
position: absolute;
- top: 35px;
- height: 30px;
- width: 32px;
+ height: $modal-view-button-icon-height;
+ width: $modal-view-button-icon-width;
font-size: 14px;
- line-height: 30px;
+ 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: 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>
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 @@
+<template>
+ <pinch-zoom
+ class="pinch-zoom-parent"
+ v-bind="$attrs"
+ v-on="$listeners"
+ >
+ <slot />
+ </pinch-zoom>
+</template>
+
+<script src="./pinch_zoom.js"></script>
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 @@
<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>
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 @@
<template>
<label
+ v-if="matchesExpertLevel"
class="ChoiceSetting"
>
<slot />
@@ -18,6 +19,7 @@
</option>
</Select>
<ModifiedIndicator :changed="isChanged" />
+ <ServerSideIndicator :server-side="isServerSide" />
</label>
</template>
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 @@
+<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>
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 @@
+<template>
+ <span
+ v-if="serverSide"
+ class="ServerSideIndicator"
+ >
+ <Popover
+ trigger="hover"
+ >
+ <template v-slot:trigger>
+ &nbsp;
+ <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>
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..fb466f2f 100644
--- a/src/components/settings_modal/settings_modal.scss
+++ b/src/components/settings_modal/settings_modal.scss
@@ -48,4 +48,11 @@
}
}
}
+
+ .settings-footer {
+ display: flex;
+ >* {
+ margin-right: 0.5em;
+ }
+ }
}
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
index 583c2ecc..1805c77f 100644
--- a/src/components/settings_modal/settings_modal.vue
+++ b/src/components/settings_modal/settings_modal.vue
@@ -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,10 @@
</div>
</template>
</Popover>
+
+ <Checkbox v-model="expertLevel">
+ {{ $t("settings.expert_mode") }}
+ </Checkbox>
</div>
</div>
</Modal>
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 @@
</li>
<li>
<BooleanSetting
+ v-if="user"
:disabled="hideFilteredStatuses"
path="hideMutedThreads"
>
@@ -29,6 +30,7 @@
</li>
<li>
<BooleanSetting
+ v-if="user"
:disabled="hideFilteredStatuses"
path="hideMutedPosts"
>
@@ -38,11 +40,22 @@
</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>
@@ -73,6 +86,14 @@
>
</li>
<li>
+ <IntegerSetting
+ path="maxThumbnails"
+ :min="0"
+ >
+ {{ $t('settings.max_thumbnails') }}
+ </IntegerSetting>
+ </li>
+ <li>
<BooleanSetting path="hideAttachments">
{{ $t('settings.hide_attachments_in_tl') }}
</BooleanSetting>
@@ -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>
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 @@
</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>
@@ -74,18 +90,79 @@
<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>
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 @@
<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>
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"
/>
</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>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index d8f94926..4c0ef3e0 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,12 @@ const Status = {
muteWordHits () {
return muteWordHits(this.status, this.muteWords)
},
+ botStatus () {
+ return this.status.user.bot
+ },
+ botIndicator () {
+ return this.botStatus && !this.hideBotIndication
+ },
mentionsLine () {
if (!this.headTailLinks) return []
const writtenSet = new Set(this.headTailLinks.writtenMentions.map(_ => _.url))
@@ -191,7 +256,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 +360,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 +377,12 @@ const Status = {
},
isSuspendable () {
return !this.replying && this.mediaPlaying.length === 0
+ },
+ inThreadForest () {
+ return !!this.controlledThreadDisplayStatus
+ },
+ threadShowing () {
+ return this.controlledThreadDisplayStatus === 'showing'
}
},
methods: {
@@ -326,7 +405,7 @@ const Status = {
this.error = undefined
},
toggleReplying () {
- this.replying = !this.replying
+ controlledOrUncontrolledToggle(this, 'replying')
},
gotoOriginal (id) {
if (this.inConversation) {
@@ -346,17 +425,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 +451,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..3f647b25 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 {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 3bb29db6..1679834e 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -77,6 +77,7 @@
<UserAvatar
v-if="retweet"
class="left-side repeater-avatar"
+ :bot="botIndicator"
:better-shadow="betterShadow"
:user="statusoid.user"
/>
@@ -124,6 +125,7 @@
@click.stop.prevent.capture.native="toggleUserExpanded"
>
<UserAvatar
+ :bot="botIndicator"
:compact="compact"
:better-shadow="betterShadow"
:user="status.user"
@@ -219,6 +221,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 +333,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 +348,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 +453,10 @@
class="gravestone"
>
<div class="left-side">
- <UserAvatar :compact="compact" />
+ <UserAvatar
+ :compact="compact"
+ :bot="botIndicator"
+ />
</div>
<div class="right-side">
<div class="deleted-text">
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 @@
<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>
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)"
>
<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;
diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue
index cca75fcb..e939b532 100644
--- a/src/components/still-image/still-image.vue
+++ b/src/components/still-image/still-image.vue
@@ -19,6 +19,7 @@
@load="onLoad"
@error="onError"
>
+ <slot />
</div>
</template>
diff --git a/src/components/swipe_click/swipe_click.js b/src/components/swipe_click/swipe_click.js
new file mode 100644
index 00000000..238e6df8
--- /dev/null
+++ b/src/components/swipe_click/swipe_click.js
@@ -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
diff --git a/src/components/swipe_click/swipe_click.vue b/src/components/swipe_click/swipe_click.vue
new file mode 100644
index 00000000..5372071d
--- /dev/null
+++ b/src/components/swipe_click/swipe_click.vue
@@ -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>
diff --git a/src/components/thread_tree/thread_tree.js b/src/components/thread_tree/thread_tree.js
new file mode 100644
index 00000000..71e63725
--- /dev/null
+++ b/src/components/thread_tree/thread_tree.js
@@ -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
diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue
new file mode 100644
index 00000000..e64455e0
--- /dev/null
+++ b/src/components/thread_tree/thread_tree.vue
@@ -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>
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
@@ -41,6 +41,15 @@
</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"
>
<span
diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js
index 94653004..33d9a258 100644
--- a/src/components/user_avatar/user_avatar.js
+++ b/src/components/user_avatar/user_avatar.js
@@ -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 {
diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue
index 4040e263..847d654b 100644
--- a/src/components/user_avatar/user_avatar.vue
+++ b/src/components/user_avatar/user_avatar.vue
@@ -7,7 +7,13 @@
:src="imgSrc(user.profile_image_url_original)"
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
:image-load-error="imageLoadError"
- />
+ >
+ <FAIcon
+ v-if="bot"
+ icon="robot"
+ class="bot-indicator"
+ />
+ </StillImage>
<div
v-else
class="Avatar -placeholder"
@@ -36,6 +42,12 @@
height: 100%;
}
+ & > .bot-indicator {
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ }
+
&.better-shadow {
box-shadow: var(--_avatarShadowInset);
filter: var(--_avatarShadowFilter);
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 90a9c19f..d2abac1d 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -120,7 +120,8 @@
"media_modal": {
"previous": "Previous",
"next": "Next",
- "counter": "{current} / {total}"
+ "counter": "{current} / {total}",
+ "hide": "Close media viewer"
},
"nav": {
"about": "About",
@@ -273,11 +274,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",
@@ -365,6 +369,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",
@@ -414,6 +420,7 @@
"name": "Label",
"value": "Content"
},
+ "account_privacy": "Privacy",
"use_contain_fit": "Don't crop the attachment in thumbnails",
"name": "Name",
"name_bio": "Name & bio",
@@ -431,6 +438,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",
@@ -444,7 +452,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",
@@ -479,13 +487,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.",
@@ -735,6 +751,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",
@@ -761,7 +778,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",
diff --git a/src/main.js b/src/main.js
index 3895da89..bdf8368b 100644
--- a/src/main.js
+++ b/src/main.js
@@ -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,
diff --git a/src/modules/config.js b/src/modules/config.js
index 825535cd..93487b99 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -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,
@@ -70,7 +74,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
@@ -80,9 +84,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
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 1abd784f..79c54096 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -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: [],
diff --git a/src/modules/serverSideConfig.js b/src/modules/serverSideConfig.js
new file mode 100644
index 00000000..5c1baedb
--- /dev/null
+++ b/src/modules/serverSideConfig.js
@@ -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
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index bf980669..0005e95a 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -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
diff --git a/src/services/gesture_service/gesture_service.js b/src/services/gesture_service/gesture_service.js
index 88a328f3..265a7f25 100644
--- a/src/services/gesture_service/gesture_service.js
+++ b/src/services/gesture_service/gesture_service.js
@@ -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