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/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 069c0b40..2ef2977a 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -1,5 +1,19 @@
import { reduce, filter, findIndex, clone, get } from 'lodash'
import Status from '../status/status.vue'
+import ThreadTree from '../thread_tree/thread_tree.vue'
+
+import { library } from '@fortawesome/fontawesome-svg-core'
+import {
+ faAngleDoubleDown,
+ faAngleDoubleLeft,
+ faChevronLeft
+} from '@fortawesome/free-solid-svg-icons'
+
+library.add(
+ faAngleDoubleDown,
+ faAngleDoubleLeft,
+ faChevronLeft
+)
const sortById = (a, b) => {
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
@@ -35,7 +49,10 @@ const conversation = {
data () {
return {
highlight: null,
- expanded: false
+ expanded: false,
+ threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
+ statusContentPropertiesObject: {},
+ inlineDivePosition: null
}
},
props: [
@@ -53,12 +70,50 @@ const conversation = {
}
},
computed: {
- hideStatus () {
- if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
- return this.virtualHidden && this.$refs.statusComponent[0].suspendable
- } else {
- return this.virtualHidden
+ maxDepthToShowByDefault () {
+ // maxDepthInThread = max number of depths that is *visible*
+ // since our depth starts with 0 and "showing" means "showing children"
+ // there is a -2 here
+ const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
+ return maxDepth >= 1 ? maxDepth : 1
+ },
+ displayStyle () {
+ return this.$store.getters.mergedConfig.conversationDisplay
+ },
+ isTreeView () {
+ return !this.isLinearView
+ },
+ treeViewIsSimple () {
+ return !this.$store.getters.mergedConfig.conversationTreeAdvanced
+ },
+ isLinearView () {
+ return this.displayStyle === 'linear'
+ },
+ shouldFadeAncestors () {
+ return this.$store.getters.mergedConfig.conversationTreeFadeAncestors
+ },
+ otherRepliesButtonPosition () {
+ return this.$store.getters.mergedConfig.conversationOtherRepliesButton
+ },
+ showOtherRepliesButtonBelowStatus () {
+ return this.otherRepliesButtonPosition === 'below'
+ },
+ showOtherRepliesButtonInsideStatus () {
+ return this.otherRepliesButtonPosition === 'inside'
+ },
+ suspendable () {
+ if (this.isTreeView) {
+ return Object.entries(this.statusContentProperties)
+ .every(([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0)
}
+ if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
+ return this.$refs.statusComponent.every(s => s.suspendable)
+ } else {
+ return true
+ }
+ },
+ hideStatus () {
+ return this.virtualHidden && this.suspendable
},
status () {
return this.$store.state.statuses.allStatusesObject[this.statusId]
@@ -90,6 +145,121 @@ const conversation = {
return sortAndFilterConversation(conversation, this.status)
},
+ statusMap () {
+ return this.conversation.reduce((res, s) => {
+ res[s.id] = s
+ return res
+ }, {})
+ },
+ threadTree () {
+ const reverseLookupTable = this.conversation.reduce((table, status, index) => {
+ table[status.id] = index
+ return table
+ }, {})
+
+ const threads = this.conversation.reduce((a, cur) => {
+ const id = cur.id
+ a.forest[id] = this.getReplies(id)
+ .map(s => s.id)
+
+ return a
+ }, {
+ forest: {}
+ })
+
+ const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
+ if (processed[id]) {
+ return []
+ }
+
+ processed[id] = true
+ return [{
+ status: this.conversation[reverseLookupTable[id]],
+ id,
+ depth
+ }, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
+ }).reduce((a, b) => a.concat(b), [])
+
+ const linearized = walk(threads.forest, this.topLevel.map(k => k.id))
+
+ return linearized
+ },
+ replyIds () {
+ return this.conversation.map(k => k.id)
+ .reduce((res, id) => {
+ res[id] = (this.replies[id] || []).map(k => k.id)
+ return res
+ }, {})
+ },
+ totalReplyCount () {
+ const sizes = {}
+ const subTreeSizeFor = (id) => {
+ if (sizes[id]) {
+ return sizes[id]
+ }
+ sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0)
+ return sizes[id]
+ }
+ this.conversation.map(k => k.id).map(subTreeSizeFor)
+ return Object.keys(sizes).reduce((res, id) => {
+ res[id] = sizes[id] - 1 // exclude itself
+ return res
+ }, {})
+ },
+ totalReplyDepth () {
+ const depths = {}
+ const subTreeDepthFor = (id) => {
+ if (depths[id]) {
+ return depths[id]
+ }
+ depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0)
+ return depths[id]
+ }
+ this.conversation.map(k => k.id).map(subTreeDepthFor)
+ return Object.keys(depths).reduce((res, id) => {
+ res[id] = depths[id] - 1 // exclude itself
+ return res
+ }, {})
+ },
+ depths () {
+ return this.threadTree.reduce((a, k) => {
+ a[k.id] = k.depth
+ return a
+ }, {})
+ },
+ topLevel () {
+ const topLevel = this.conversation.reduce((tl, cur) =>
+ tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation)
+ return topLevel
+ },
+ otherTopLevelCount () {
+ return this.topLevel.length - 1
+ },
+ showingTopLevel () {
+ if (this.canDive && this.diveRoot) {
+ return [this.statusMap[this.diveRoot]]
+ }
+ return this.topLevel
+ },
+ diveRoot () {
+ const statusId = this.inlineDivePosition || this.statusId
+ const isTopLevel = !this.parentOf(statusId)
+ return isTopLevel ? null : statusId
+ },
+ diveDepth () {
+ return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
+ },
+ diveMode () {
+ return this.canDive && !!this.diveRoot
+ },
+ shouldShowAllConversationButton () {
+ // The "show all conversation" button tells the user that there exist
+ // other toplevel statuses, so do not show it if there is only a single root
+ return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1
+ },
+ shouldShowAncestors () {
+ return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length
+ },
replies () {
let i = 1
// eslint-disable-next-line camelcase
@@ -109,15 +279,71 @@ const conversation = {
}, {})
},
isExpanded () {
- return this.expanded || this.isPage
+ return !!(this.expanded || this.isPage)
},
hiddenStyle () {
const height = (this.status && this.status.virtualHeight) || '120px'
return this.virtualHidden ? { height } : {}
+ },
+ threadDisplayStatus () {
+ return this.conversation.reduce((a, k) => {
+ const id = k.id
+ const depth = this.depths[id]
+ const status = (() => {
+ if (this.threadDisplayStatusObject[id]) {
+ return this.threadDisplayStatusObject[id]
+ }
+ if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) {
+ return 'showing'
+ } else {
+ return 'hidden'
+ }
+ })()
+
+ a[id] = status
+ return a
+ }, {})
+ },
+ statusContentProperties () {
+ return this.conversation.reduce((a, k) => {
+ const id = k.id
+ const props = (() => {
+ const def = {
+ showingTall: false,
+ expandingSubject: false,
+ showingLongSubject: false,
+ isReplying: false,
+ mediaPlaying: []
+ }
+
+ if (this.statusContentPropertiesObject[id]) {
+ return {
+ ...def,
+ ...this.statusContentPropertiesObject[id]
+ }
+ }
+ return def
+ })()
+
+ a[id] = props
+ return a
+ }, {})
+ },
+ canDive () {
+ return this.isTreeView && this.isExpanded
+ },
+ focused () {
+ return (id) => {
+ return (this.isExpanded) && id === this.highlight
+ }
+ },
+ maybeHighlight () {
+ return this.isExpanded ? this.highlight : null
}
},
components: {
- Status
+ Status,
+ ThreadTree
},
watch: {
statusId (newVal, oldVal) {
@@ -132,6 +358,8 @@ const conversation = {
expanded (value) {
if (value) {
this.fetchConversation()
+ } else {
+ this.resetDisplayState()
}
},
virtualHidden (value) {
@@ -161,8 +389,8 @@ const conversation = {
getReplies (id) {
return this.replies[id] || []
},
- focused (id) {
- return (this.isExpanded) && id === this.statusId
+ getHighlight () {
+ return this.isExpanded ? this.highlight : null
},
setHighlight (id) {
if (!id) return
@@ -170,15 +398,139 @@ const conversation = {
this.$store.dispatch('fetchFavsAndRepeats', id)
this.$store.dispatch('fetchEmojiReactionsBy', id)
},
- getHighlight () {
- return this.isExpanded ? this.highlight : null
- },
toggleExpanded () {
this.expanded = !this.expanded
},
getConversationId (statusId) {
const status = this.$store.state.statuses.allStatusesObject[statusId]
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
+ },
+ setThreadDisplay (id, nextStatus) {
+ this.threadDisplayStatusObject = {
+ ...this.threadDisplayStatusObject,
+ [id]: nextStatus
+ }
+ },
+ toggleThreadDisplay (id) {
+ const curStatus = this.threadDisplayStatus[id]
+ const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
+ this.setThreadDisplay(id, nextStatus)
+ },
+ setThreadDisplayRecursively (id, nextStatus) {
+ this.setThreadDisplay(id, nextStatus)
+ this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus))
+ },
+ showThreadRecursively (id) {
+ this.setThreadDisplayRecursively(id, 'showing')
+ },
+ setStatusContentProperty (id, name, value) {
+ this.statusContentPropertiesObject = {
+ ...this.statusContentPropertiesObject,
+ [id]: {
+ ...this.statusContentPropertiesObject[id],
+ [name]: value
+ }
+ }
+ },
+ toggleStatusContentProperty (id, name) {
+ this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
+ },
+ leastVisibleAncestor (id) {
+ let cur = id
+ let parent = this.parentOf(cur)
+ while (cur) {
+ // if the parent is showing it means cur is visible
+ if (this.threadDisplayStatus[parent] === 'showing') {
+ return cur
+ }
+ parent = this.parentOf(parent)
+ cur = this.parentOf(cur)
+ }
+ // nothing found, fall back to toplevel
+ return this.topLevel[0] ? this.topLevel[0].id : undefined
+ },
+ diveIntoStatus (id, preventScroll) {
+ this.tryScrollTo(id)
+ },
+ diveToTopLevel () {
+ this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id)
+ },
+ // only used when we are not on a page
+ undive () {
+ this.inlineDivePosition = null
+ this.setHighlight(this.statusId)
+ },
+ tryScrollTo (id) {
+ if (!id) {
+ return
+ }
+ if (this.isPage) {
+ // set statusId
+ this.$router.push({ name: 'conversation', params: { id } })
+ } else {
+ this.inlineDivePosition = id
+ }
+ // Because the conversation can be unmounted when out of sight
+ // and mounted again when it comes into sight,
+ // the `mounted` or `created` function in `status` should not
+ // contain scrolling calls, as we do not want the page to jump
+ // when we scroll with an expanded conversation.
+ //
+ // Now the method is to rely solely on the `highlight` watcher
+ // in `status` components.
+ // In linear views, all statuses are rendered at all times, but
+ // in tree views, it is possible that a change in active status
+ // removes and adds status components (e.g. an originally child
+ // status becomes an ancestor status, and thus they will be
+ // different).
+ // Here, let the components be rendered first, in order to trigger
+ // the `highlight` watcher.
+ this.$nextTick(() => {
+ this.setHighlight(id)
+ })
+ },
+ goToCurrent () {
+ this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
+ },
+ statusById (id) {
+ return this.statusMap[id]
+ },
+ parentOf (id) {
+ const status = this.statusById(id)
+ if (!status) {
+ return undefined
+ }
+ const { in_reply_to_status_id: parentId } = status
+ if (!this.statusMap[parentId]) {
+ return undefined
+ }
+ return parentId
+ },
+ parentOrSelf (id) {
+ return this.parentOf(id) || id
+ },
+ // Ancestors of some status, from top to bottom
+ ancestorsOf (id) {
+ const ancestors = []
+ let cur = this.parentOf(id)
+ while (cur) {
+ ancestors.unshift(this.statusMap[cur])
+ cur = this.parentOf(cur)
+ }
+ return ancestors
+ },
+ topLevelAncestorOrSelfId (id) {
+ let cur = id
+ let parent = this.parentOf(id)
+ while (parent) {
+ cur = this.parentOf(cur)
+ parent = this.parentOf(parent)
+ }
+ return cur
+ },
+ resetDisplayState () {
+ this.undive()
+ this.threadDisplayStatusObject = {}
}
}
}
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 3fb26d92..7628ceaa 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -18,24 +18,168 @@
{{ $t('timeline.collapse') }}
-
+
+
+
+
+
+
+ {{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ $tc('status.ancestor_follow', getReplies(status.id).length - 1, { numReplies: getReplies(status.id).length - 1 }) }}
+
+
+
+
+
+
+
+
+
+
+
+
.conversation-status {
+ border-top-width: 1px;
+ border-top-style: solid;
+ border-top-color: var(--border, $fallback--border);
+ }
+
+ /* expanded conversation in timeline */
+ &.status-fadein.-expanded .thread-body {
+ border-left-width: 4px;
+ border-left-style: solid;
+ border-left-color: $fallback--cRed;
+ border-left-color: var(--cRed, $fallback--cRed);
+ border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
+ border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
+ border-bottom: 1px solid var(--border, $fallback--border);
}
}
diff --git a/src/components/settings_modal/helpers/integer_setting.js b/src/components/settings_modal/helpers/integer_setting.js
new file mode 100644
index 00000000..fa6569ab
--- /dev/null
+++ b/src/components/settings_modal/helpers/integer_setting.js
@@ -0,0 +1,37 @@
+import { get, set } from 'lodash'
+import ModifiedIndicator from './modified_indicator.vue'
+export default {
+ components: {
+ ModifiedIndicator
+ },
+ props: {
+ path: String,
+ disabled: Boolean,
+ min: 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
+ }
+ },
+ 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..8284abcc
--- /dev/null
+++ b/src/components/settings_modal/helpers/integer_setting.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
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 a7e30ce2..0774c818 100644
--- a/src/components/settings_modal/tabs/filtering_tab.vue
+++ b/src/components/settings_modal/tabs/filtering_tab.vue
@@ -70,17 +70,12 @@
{{ $t('settings.attachments') }}
-
-
+ {{ $t('settings.max_thumbnails') }}
+
diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js
index 952c328d..03ca99fa 100644
--- a/src/components/settings_modal/tabs/general_tab.js
+++ b/src/components/settings_modal/tabs/general_tab.js
@@ -1,5 +1,6 @@
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.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'
@@ -20,6 +21,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,6 +48,7 @@ const GeneralTab = {
components: {
BooleanSetting,
ChoiceSetting,
+ IntegerSetting,
InterfaceLanguageSwitcher
},
computed: {
diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue
index eba3b268..ba01fdeb 100644
--- a/src/components/settings_modal/tabs/general_tab.vue
+++ b/src/components/settings_modal/tabs/general_tab.vue
@@ -152,6 +152,47 @@
{{ $t('settings.show_yous') }}
+
+
+ {{ $t('settings.conversation_display') }}
+
+
+
+ -
+
+ {{ $t('settings.tree_advanced') }}
+
+
+ -
+
+ {{ $t('settings.tree_fade_ancestors') }}
+
+
+ -
+
+ {{ $t('settings.max_depth_in_thread') }}
+
+
+ -
+
+ {{ $t('settings.conversation_other_replies_button') }}
+
+
+
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
},
@@ -318,6 +377,12 @@ const Status = {
},
isSuspendable () {
return !this.replying && this.mediaPlaying.length === 0
+ },
+ inThreadForest () {
+ return !!this.controlledThreadDisplayStatus
+ },
+ threadShowing () {
+ return this.controlledThreadDisplayStatus === 'showing'
}
},
methods: {
@@ -340,7 +405,7 @@ const Status = {
this.error = undefined
},
toggleReplying () {
- this.replying = !this.replying
+ controlledOrUncontrolledToggle(this, 'replying')
},
gotoOriginal (id) {
if (this.inConversation) {
@@ -360,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) {
@@ -384,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 8f51a778..1679834e 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -221,6 +221,31 @@
class="fa-scale-110"
/>
+
+
-
{{ $t('status.replies_list') }}
+
+
+ {{ $t('status.replies_list') }}
+
{{ $t("status.hide_full_subject") }}
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
index dec8914a..cf72ccb8 100644
--- a/src/components/status_content/status_content.js
+++ b/src/components/status_content/status_content.js
@@ -23,6 +23,30 @@ library.add(
faPollH
)
+const camelCase = name => name.charAt(0).toUpperCase() + name.slice(1)
+
+const controlledOrUncontrolledGetters = list => list.reduce((res, name) => {
+ const camelized = camelCase(name)
+ const toggle = `controlledToggle${camelized}`
+ const controlledName = `controlled${camelized}`
+ const uncontrolledName = `uncontrolled${camelized}`
+ res[name] = function () {
+ return this[toggle] ? this[controlledName] : this[uncontrolledName]
+ }
+ return res
+}, {})
+
+const controlledOrUncontrolledToggle = (obj, name) => {
+ const camelized = camelCase(name)
+ const toggle = `controlledToggle${camelized}`
+ const uncontrolledName = `uncontrolled${camelized}`
+ if (obj[toggle]) {
+ obj[toggle]()
+ } else {
+ obj[uncontrolledName] = !obj[uncontrolledName]
+ }
+}
+
const StatusContent = {
name: 'StatusContent',
props: [
@@ -31,9 +55,22 @@ const StatusContent = {
'focused',
'noHeading',
'fullContent',
- 'singleLine'
+ 'singleLine',
+ 'controlledShowingTall',
+ 'controlledExpandingSubject',
+ 'controlledToggleShowingTall',
+ 'controlledToggleExpandingSubject'
],
+ data () {
+ return {
+ uncontrolledShowingTall: this.fullContent || (this.inConversation && this.focused),
+ uncontrolledShowingLongSubject: false,
+ // not as computed because it sets the initial state which will be changed later
+ uncontrolledExpandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
+ }
+ },
computed: {
+ ...controlledOrUncontrolledGetters(['showingTall', 'expandingSubject', 'showingLongSubject']),
hideAttachments () {
return (this.mergedConfig.hideAttachments && !this.inConversation) ||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)
@@ -71,6 +108,21 @@ const StatusContent = {
Gallery,
LinkPreview,
StatusBody
+ },
+ methods: {
+ toggleShowingTall () {
+ controlledOrUncontrolledToggle(this, 'showingTall')
+ },
+ toggleExpandingSubject () {
+ controlledOrUncontrolledToggle(this, 'expandingSubject')
+ },
+ toggleShowingLongSubject () {
+ controlledOrUncontrolledToggle(this, 'showingLongSubject')
+ },
+ setMedia () {
+ const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
+ return () => this.$store.dispatch('setMedia', attachments)
+ }
}
}
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
index 69635aad..9e7d7956 100644
--- a/src/components/status_content/status_content.vue
+++ b/src/components/status_content/status_content.vue
@@ -8,6 +8,12 @@
:status="status"
:compact="compact"
:single-line="singleLine"
+ :showing-tall="showingTall"
+ :expanding-subject="expandingSubject"
+ :showing-long-subject="showingLongSubject"
+ :toggle-showing-tall="toggleShowingTall"
+ :toggle-expanding-subject="toggleExpandingSubject"
+ :toggle-showing-long-subject="toggleShowingLongSubject"
@parseReady="$emit('parseReady', $event)"
>
@@ -52,10 +58,6 @@
diff --git a/src/i18n/en.json b/src/i18n/en.json
index bdf1f041..153a0a53 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -468,6 +468,15 @@
"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",
@@ -724,6 +733,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",
@@ -750,7 +760,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/modules/config.js b/src/modules/config.js
index 1aa2878e..c9ee4a51 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -12,6 +12,8 @@ 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
]
@@ -83,7 +85,12 @@ export const defaultState = {
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 41bcf329..79c54096 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -55,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: [],