Merge remote-tracking branch 'tusooa/from/develop/tusooa/tree-threading' into shigusegubu

* tusooa/from/develop/tusooa/tree-threading: (48 commits)
  Fix showingLongSubject not correctly propagated
  Fix timeline jump when scrolling
  Make replying and mediaPlaying controlled
  Clean up debug code for tree threading
  Fix virtual scrolling for tree threading
  Fix controlled status display toggles
  Add English translations for other replies count
  Add other replies count for reply list link
  Add English translation for position of other replies button pref
  Make position of other replies button a pref
  Add English translation for show all conversation button improvement
  Improve "show full conversation" interaction
  Make other replies button stretch along the row
  Optimise thread ancestor borders
  Optimise thread ancestor display style
  Reset thread open state when collapsed
  Clean up
  Highlight ancestor of the current status when diving back to top
  Add English translation for Misskey-style tree view
  Implement Misskey-style tree view
  ...
This commit is contained in:
Henry Jameson 2022-02-28 18:25:12 +02:00
commit af34c25512
19 changed files with 1104 additions and 73 deletions

View file

@ -5,18 +5,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased ## Unreleased
### Fixed ### Fixed
- AdminFE button no longer scrolls page to top when clicked
- Pinned statuses no longer appear at bottom of user timeline (still appear as part of the timeline when fetched deep enough)
- Fixed many many bugs related to new mentions, including spacing and alignment issues
- Links in profile bios now properly open in new tabs
- Inline images now respect their intended width/height attributes
- Links with `&` in them work properly now
- Interaction list popovers now properly emojify names
- Completely hidden posts still had 1px border - Completely hidden posts still had 1px border
- Attachments are ALWAYS in same order as user uploaded, no more "videos first" - Attachments are ALWAYS in same order as user uploaded, no more "videos first"
- Attachment description is prefilled with backend-provided default when uploading - Attachment description is prefilled with backend-provided default when uploading
- Proper visual feedback that next image is loading when browsing - Proper visual feedback that next image is loading when browsing
### Changed ### Changed
- (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out)
- User highlight background now also covers the `@`
- Reverted back to textual `@`, svg version is opt-in.
- Settings window has been throughly rearranged to make make more sense and make navication settings easier. - Settings window has been throughly rearranged to make make more sense and make navication settings easier.
- Uploaded attachments are uniform with displayed attachments - Uploaded attachments are uniform with displayed attachments
- Flash is watchable in media-modal (takes up nearly full screen though due to sizing issues) - Flash is watchable in media-modal (takes up nearly full screen though due to sizing issues)
- Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post. - Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post.
### Added ### Added
- Options to show domains in mentions
- Option to show user avatars in mention links (opt-in)
- Option to disable the tooltip for mentions
- Option to completely hide muted threads - Option to completely hide muted threads
- Ability to open videos in modal even if you disabled that feature, via an icon button - Ability to open videos in modal even if you disabled that feature, via an icon button
- New button on attachment that indicates that attachment has a description and shows a bar filled with description - New button on attachment that indicates that attachment has a description and shows a bar filled with description

View file

@ -30,3 +30,5 @@ $fallback--attachmentRadius: 10px;
$fallback--chatMessageRadius: 10px; $fallback--chatMessageRadius: 10px;
$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; $fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
$status-margin: 0.75em;

View file

@ -1,5 +1,19 @@
import { reduce, filter, findIndex, clone, get } from 'lodash' import { reduce, filter, findIndex, clone, get } from 'lodash'
import Status from '../status/status.vue' import Status from '../status/status.vue'
import ThreadTree from '../thread_tree/thread_tree.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faAngleDoubleDown,
faAngleDoubleLeft,
faChevronLeft
} from '@fortawesome/free-solid-svg-icons'
library.add(
faAngleDoubleDown,
faAngleDoubleLeft,
faChevronLeft
)
const sortById = (a, b) => { const sortById = (a, b) => {
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
@ -35,7 +49,10 @@ const conversation = {
data () { data () {
return { return {
highlight: null, highlight: null,
expanded: false expanded: false,
threadDisplayStatusObject: {}, // id => 'showing' | 'hidden'
statusContentPropertiesObject: {},
inlineDivePosition: null
} }
}, },
props: [ props: [
@ -53,12 +70,47 @@ const conversation = {
} }
}, },
computed: { computed: {
hideStatus () { maxDepthToShowByDefault () {
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) { // maxDepthInThread = max number of depths that is *visible*
return this.virtualHidden && this.$refs.statusComponent[0].suspendable // since our depth starts with 0 and "showing" means "showing children"
} else { // there is a -2 here
return this.virtualHidden const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
return maxDepth >= 1 ? maxDepth : 1
},
displayStyle () {
return this.$store.getters.mergedConfig.conversationDisplay
},
isTreeView () {
return this.displayStyle === 'tree' || this.displayStyle === 'simple_tree'
},
treeViewIsSimple () {
return this.displayStyle === 'simple_tree'
},
isLinearView () {
return this.displayStyle === 'linear'
},
otherRepliesButtonPosition () {
return this.$store.getters.mergedConfig.conversationOtherRepliesButton
},
showOtherRepliesButtonBelowStatus () {
return this.otherRepliesButtonPosition === 'below'
},
showOtherRepliesButtonInsideStatus () {
return this.otherRepliesButtonPosition === 'inside'
},
suspendable () {
if (this.isTreeView) {
return Object.entries(this.statusContentProperties)
.every(([k, prop]) => !prop.replying && prop.mediaPlaying.length === 0)
} }
if (this.$refs.statusComponent && this.$refs.statusComponent[0]) {
return this.$refs.statusComponent.every(s => s.suspendable)
} else {
return true
}
},
hideStatus () {
return this.virtualHidden && this.suspendable
}, },
status () { status () {
return this.$store.state.statuses.allStatusesObject[this.statusId] return this.$store.state.statuses.allStatusesObject[this.statusId]
@ -90,6 +142,123 @@ const conversation = {
return sortAndFilterConversation(conversation, this.status) return sortAndFilterConversation(conversation, this.status)
}, },
conversationDive () {
},
statusMap () {
return this.conversation.reduce((res, s) => {
res[s.id] = s
return res
}, {})
},
threadTree () {
const reverseLookupTable = this.conversation.reduce((table, status, index) => {
table[status.id] = index
return table
}, {})
const threads = this.conversation.reduce((a, cur) => {
const id = cur.id
a.forest[id] = this.getReplies(id)
.map(s => s.id)
return a
}, {
forest: {}
})
const walk = (forest, topLevel, depth = 0, processed = {}) => topLevel.map(id => {
if (processed[id]) {
return []
}
processed[id] = true
return [{
status: this.conversation[reverseLookupTable[id]],
id,
depth
}, walk(forest, forest[id], depth + 1, processed)].reduce((a, b) => a.concat(b), [])
}).reduce((a, b) => a.concat(b), [])
const linearized = walk(threads.forest, this.topLevel.map(k => k.id))
return linearized
},
replyIds () {
return this.conversation.map(k => k.id)
.reduce((res, id) => {
res[id] = (this.replies[id] || []).map(k => k.id)
return res
}, {})
},
totalReplyCount () {
const sizes = {}
const subTreeSizeFor = (id) => {
if (sizes[id]) {
return sizes[id]
}
sizes[id] = 1 + this.replyIds[id].map(cid => subTreeSizeFor(cid)).reduce((a, b) => a + b, 0)
return sizes[id]
}
this.conversation.map(k => k.id).map(subTreeSizeFor)
return Object.keys(sizes).reduce((res, id) => {
res[id] = sizes[id] - 1 // exclude itself
return res
}, {})
},
totalReplyDepth () {
const depths = {}
const subTreeDepthFor = (id) => {
if (depths[id]) {
return depths[id]
}
depths[id] = 1 + this.replyIds[id].map(cid => subTreeDepthFor(cid)).reduce((a, b) => a > b ? a : b, 0)
return depths[id]
}
this.conversation.map(k => k.id).map(subTreeDepthFor)
return Object.keys(depths).reduce((res, id) => {
res[id] = depths[id] - 1 // exclude itself
return res
}, {})
},
depths () {
return this.threadTree.reduce((a, k) => {
a[k.id] = k.depth
return a
}, {})
},
topLevel () {
const topLevel = this.conversation.reduce((tl, cur) =>
tl.filter(k => this.getReplies(cur.id).map(v => v.id).indexOf(k.id) === -1), this.conversation)
return topLevel
},
otherTopLevelCount () {
return this.topLevel.length - 1
},
showingTopLevel () {
if (this.canDive && this.diveRoot) {
return [this.statusMap[this.diveRoot]]
}
return this.topLevel
},
diveRoot () {
const statusId = this.inlineDivePosition || this.statusId
const isTopLevel = !this.parentOf(statusId)
return isTopLevel ? null : statusId
},
diveDepth () {
return this.canDive && this.diveRoot ? this.depths[this.diveRoot] : 0
},
diveMode () {
return this.canDive && !!this.diveRoot
},
shouldShowAllConversationButton () {
// The "show all conversation" button tells the user that there exist
// other toplevel statuses, so do not show it if there is only a single root
return this.isTreeView && this.isExpanded && this.diveMode && this.topLevel.length > 1
},
shouldShowAncestors () {
return this.isTreeView && this.isExpanded && this.ancestorsOf(this.diveRoot).length
},
replies () { replies () {
let i = 1 let i = 1
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
@ -109,15 +278,71 @@ const conversation = {
}, {}) }, {})
}, },
isExpanded () { isExpanded () {
return this.expanded || this.isPage return !!(this.expanded || this.isPage)
}, },
hiddenStyle () { hiddenStyle () {
const height = (this.status && this.status.virtualHeight) || '120px' const height = (this.status && this.status.virtualHeight) || '120px'
return this.virtualHidden ? { height } : {} return this.virtualHidden ? { height } : {}
},
threadDisplayStatus () {
return this.conversation.reduce((a, k) => {
const id = k.id
const depth = this.depths[id]
const status = (() => {
if (this.threadDisplayStatusObject[id]) {
return this.threadDisplayStatusObject[id]
}
if ((depth - this.diveDepth) <= this.maxDepthToShowByDefault) {
return 'showing'
} else {
return 'hidden'
}
})()
a[id] = status
return a
}, {})
},
statusContentProperties () {
return this.conversation.reduce((a, k) => {
const id = k.id
const props = (() => {
const def = {
showingTall: false,
expandingSubject: false,
showingLongSubject: false,
isReplying: false,
mediaPlaying: []
}
if (this.statusContentPropertiesObject[id]) {
return {
...def,
...this.statusContentPropertiesObject[id]
}
}
return def
})()
a[id] = props
return a
}, {})
},
canDive () {
return this.isTreeView && this.isExpanded
},
focused () {
return (id) => {
return (this.isExpanded) && id === this.highlight
}
},
maybeHighlight () {
return this.isExpanded ? this.highlight : null
} }
}, },
components: { components: {
Status Status,
ThreadTree
}, },
watch: { watch: {
statusId (newVal, oldVal) { statusId (newVal, oldVal) {
@ -132,6 +357,8 @@ const conversation = {
expanded (value) { expanded (value) {
if (value) { if (value) {
this.fetchConversation() this.fetchConversation()
} else {
this.resetDisplayState()
} }
}, },
virtualHidden (value) { virtualHidden (value) {
@ -161,8 +388,8 @@ const conversation = {
getReplies (id) { getReplies (id) {
return this.replies[id] || [] return this.replies[id] || []
}, },
focused (id) { getHighlight () {
return (this.isExpanded) && id === this.statusId return this.isExpanded ? this.highlight : null
}, },
setHighlight (id) { setHighlight (id) {
if (!id) return if (!id) return
@ -170,15 +397,139 @@ const conversation = {
this.$store.dispatch('fetchFavsAndRepeats', id) this.$store.dispatch('fetchFavsAndRepeats', id)
this.$store.dispatch('fetchEmojiReactionsBy', id) this.$store.dispatch('fetchEmojiReactionsBy', id)
}, },
getHighlight () {
return this.isExpanded ? this.highlight : null
},
toggleExpanded () { toggleExpanded () {
this.expanded = !this.expanded this.expanded = !this.expanded
}, },
getConversationId (statusId) { getConversationId (statusId) {
const status = this.$store.state.statuses.allStatusesObject[statusId] const status = this.$store.state.statuses.allStatusesObject[statusId]
return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id')) return get(status, 'retweeted_status.statusnet_conversation_id', get(status, 'statusnet_conversation_id'))
},
setThreadDisplay (id, nextStatus) {
this.threadDisplayStatusObject = {
...this.threadDisplayStatusObject,
[id]: nextStatus
}
},
toggleThreadDisplay (id) {
const curStatus = this.threadDisplayStatus[id]
const nextStatus = curStatus === 'showing' ? 'hidden' : 'showing'
this.setThreadDisplay(id, nextStatus)
},
setThreadDisplayRecursively (id, nextStatus) {
this.setThreadDisplay(id, nextStatus)
this.getReplies(id).map(k => k.id).map(id => this.setThreadDisplayRecursively(id, nextStatus))
},
showThreadRecursively (id) {
this.setThreadDisplayRecursively(id, 'showing')
},
setStatusContentProperty (id, name, value) {
this.statusContentPropertiesObject = {
...this.statusContentPropertiesObject,
[id]: {
...this.statusContentPropertiesObject[id],
[name]: value
}
}
},
toggleStatusContentProperty (id, name) {
this.setStatusContentProperty(id, name, !this.statusContentProperties[id][name])
},
leastVisibleAncestor (id) {
let cur = id
let parent = this.parentOf(cur)
while (cur) {
// if the parent is showing it means cur is visible
if (this.threadDisplayStatus[parent] === 'showing') {
return cur
}
parent = this.parentOf(parent)
cur = this.parentOf(cur)
}
// nothing found, fall back to toplevel
return this.topLevel[0] ? this.topLevel[0].id : undefined
},
diveIntoStatus (id, preventScroll) {
this.tryScrollTo(id)
},
diveToTopLevel () {
this.tryScrollTo(this.topLevelAncestorOrSelfId(this.diveRoot) || this.topLevel[0].id)
},
// only used when we are not on a page
undive () {
this.inlineDivePosition = null
this.setHighlight(this.statusId)
},
tryScrollTo (id) {
if (!id) {
return
}
if (this.isPage) {
// set statusId
this.$router.push({ name: 'conversation', params: { id } })
} else {
this.inlineDivePosition = id
}
// Because the conversation can be unmounted when out of sight
// and mounted again when it comes into sight,
// the `mounted` or `created` function in `status` should not
// contain scrolling calls, as we do not want the page to jump
// when we scroll with an expanded conversation.
//
// Now the method is to rely solely on the `highlight` watcher
// in `status` components.
// In linear views, all statuses are rendered at all times, but
// in tree views, it is possible that a change in active status
// removes and adds status components (e.g. an originally child
// status becomes an ancestor status, and thus they will be
// different).
// Here, let the components be rendered first, in order to trigger
// the `highlight` watcher.
this.$nextTick(() => {
this.setHighlight(id)
})
},
goToCurrent () {
this.tryScrollTo(this.diveRoot || this.topLevel[0].id)
},
statusById (id) {
return this.statusMap[id]
},
parentOf (id) {
const status = this.statusById(id)
if (!status) {
return undefined
}
const { in_reply_to_status_id: parentId } = status
if (!this.statusMap[parentId]) {
return undefined
}
return parentId
},
parentOrSelf (id) {
return this.parentOf(id) || id
},
// Ancestors of some status, from top to bottom
ancestorsOf (id) {
const ancestors = []
let cur = this.parentOf(id)
while (cur) {
ancestors.unshift(this.statusMap[cur])
cur = this.parentOf(cur)
}
return ancestors
},
topLevelAncestorOrSelfId (id) {
let cur = id
let parent = this.parentOf(id)
while (parent) {
cur = this.parentOf(cur)
parent = this.parentOf(parent)
}
return cur
},
resetDisplayState () {
this.undive()
this.threadDisplayStatusObject = {}
} }
} }
} }

View file

@ -18,24 +18,168 @@
{{ $t('timeline.collapse') }} {{ $t('timeline.collapse') }}
</button> </button>
</div> </div>
<status <div class="conversation-body panel-body">
v-for="status in conversation" <div
:key="status.id" v-if="shouldShowAllConversationButton"
ref="statusComponent" class="conversation-dive-to-top-level-box"
:inline-expanded="collapsable && isExpanded" >
:statusoid="status" <i18n
:expandable="!isExpanded" path="status.show_all_conversation_with_icon"
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]" tag="button"
:focused="focused(status.id)" class="button-unstyled -link"
:in-conversation="isExpanded" @click.prevent="diveToTopLevel"
:highlight="getHighlight()" >
:replies="getReplies(status.id)" <FAIcon
:in-profile="inProfile" place="icon"
:profile-user-id="profileUserId" icon="angle-double-left"
class="conversation-status status-fadein panel-body" />
@goto="setHighlight" <span place="text">
@toggleExpanded="toggleExpanded" {{ $tc('status.show_all_conversation', otherTopLevelCount, { numStatus: otherTopLevelCount }) }}
/> </span>
</i18n>
</div>
<div
v-if="isTreeView"
class="thread-body"
>
<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}"
>
<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>
<div <div
v-else v-else
@ -49,6 +193,46 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.Conversation { .Conversation {
.conversation-dive-to-top-level-box {
padding: $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: $status-margin;
border-left: 2px solid var(--border, $fallback--border);
}
.thread-ancestor .StatusContent {
--link: var(--faintLink);
--text: var(--faint);
color: var(--text);
}
.thread-ancestor-dive-box {
padding-left: $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: $status-margin;
//border-left: 2px solid var(--border, $fallback--border);
}
.conversation-status { .conversation-status {
border-bottom-width: 1px; border-bottom-width: 1px;
border-bottom-style: solid; border-bottom-style: solid;
@ -56,12 +240,33 @@
border-radius: 0; border-radius: 0;
} }
&.-expanded { .thread-ancestor-has-other-replies .conversation-status,
.conversation-status:last-child { .thread-ancestor:last-child .conversation-status,
border-bottom: none; .thread-ancestor:last-child .thread-ancestor-dive-box,
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; &.-expanded .thread-tree .conversation-status {
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); 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);
}
/* &.-expanded { */
/* .conversation-status:last-child { */
/* border-bottom: none; */
/* } */
/* } */
} }
</style> </style>

View file

@ -86,7 +86,7 @@
<span <span
class="counter" class="counter"
> >
{{ currentIndex + 1 }} / {{ media.length }} {{ $tc('media_modal.counter', currentIndex + 1, { current: currentIndex + 1, total: media.length }) }}
</span> </span>
<span <span
v-if="loading" v-if="loading"

View file

@ -20,6 +20,16 @@ const GeneralTab = {
value: mode, value: mode,
label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`) label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`)
})), })),
conversationDisplayOptions: ['tree', 'simple_tree', 'linear'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.conversation_display_${mode}`)
})),
conversationOtherRepliesButtonOptions: ['below', 'inside'].map(mode => ({
key: mode,
value: mode,
label: this.$t(`settings.conversation_other_replies_button_${mode}`)
})),
mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({ mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({
key: mode, key: mode,
value: mode, value: mode,

View file

@ -157,6 +157,42 @@
{{ $t('settings.show_yous') }} {{ $t('settings.show_yous') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<ChoiceSetting
id="conversationDisplay"
path="conversationDisplay"
:options="conversationDisplayOptions"
>
{{ $t('settings.conversation_display') }}
</ChoiceSetting>
</li>
<ul
v-if="conversationDisplay !== 'linear'"
class="setting-list suboptions"
>
<li>
<label for="maxDepthInThread">
{{ $t('settings.max_depth_in_thread') }}
</label>
<input
id="maxDepthInThread"
path.number="maxDepthInThread"
class="number-input"
type="number"
min="3"
step="1"
>
</li>
<li>
<ChoiceSetting
id="conversationOtherRepliesButton"
path="conversationOtherRepliesButton"
:options="conversationOtherRepliesButtonOptions"
>
{{ $t('settings.conversation_other_replies_button') }}
</ChoiceSetting>
</li>
</ul>
<li> <li>
<ChoiceSetting <ChoiceSetting
id="mentionLinkDisplay" id="mentionLinkDisplay"

View file

@ -35,7 +35,10 @@ import {
faStar, faStar,
faEyeSlash, faEyeSlash,
faEye, faEye,
faThumbtack faThumbtack,
faChevronUp,
faChevronDown,
faAngleDoubleRight
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
@ -52,9 +55,47 @@ library.add(
faEllipsisH, faEllipsisH,
faEyeSlash, faEyeSlash,
faEye, faEye,
faThumbtack faThumbtack,
faChevronUp,
faChevronDown,
faAngleDoubleRight
) )
const camelCase = name => name.charAt(0).toUpperCase() + name.slice(1)
const controlledOrUncontrolledGetters = list => list.reduce((res, name) => {
const camelized = camelCase(name)
const toggle = `controlledToggle${camelized}`
const controlledName = `controlled${camelized}`
const uncontrolledName = `uncontrolled${camelized}`
res[name] = function () {
return this[toggle] ? this[controlledName] : this[uncontrolledName]
}
return res
}, {})
const controlledOrUncontrolledToggle = (obj, name) => {
const camelized = camelCase(name)
const toggle = `controlledToggle${camelized}`
const uncontrolledName = `uncontrolled${camelized}`
if (obj[toggle]) {
obj[toggle]()
} else {
obj[uncontrolledName] = !obj[uncontrolledName]
}
}
const controlledOrUncontrolledSet = (obj, name, val) => {
const camelized = camelCase(name)
const set = `controlledSet${camelized}`
const uncontrolledName = `uncontrolled${camelized}`
if (obj[set]) {
obj[set](val)
} else {
obj[uncontrolledName] = val
}
}
const Status = { const Status = {
name: 'Status', name: 'Status',
components: { components: {
@ -89,20 +130,38 @@ const Status = {
'inlineExpanded', 'inlineExpanded',
'showPinned', 'showPinned',
'inProfile', 'inProfile',
'profileUserId' 'profileUserId',
'simpleTree',
'controlledThreadDisplayStatus',
'controlledToggleThreadDisplay',
'showOtherRepliesAsButton',
'controlledShowingTall',
'controlledToggleShowingTall',
'controlledExpandingSubject',
'controlledToggleExpandingSubject',
'controlledShowingLongSubject',
'controlledToggleShowingLongSubject',
'controlledReplying',
'controlledToggleReplying',
'controlledMediaPlaying',
'controlledSetMediaPlaying',
'dive'
], ],
data () { data () {
return { return {
replying: false, uncontrolledReplying: false,
unmuted: false, unmuted: false,
userExpanded: false, userExpanded: false,
mediaPlaying: [], uncontrolledMediaPlaying: [],
suspendable: true, suspendable: true,
error: null, error: null,
headTailLinks: null headTailLinks: null
} }
}, },
computed: { computed: {
...controlledOrUncontrolledGetters(['replying', 'mediaPlaying']),
muteWords () { muteWords () {
return this.mergedConfig.muteWords return this.mergedConfig.muteWords
}, },
@ -304,6 +363,12 @@ const Status = {
}, },
isSuspendable () { isSuspendable () {
return !this.replying && this.mediaPlaying.length === 0 return !this.replying && this.mediaPlaying.length === 0
},
inThreadForest () {
return !!this.controlledThreadDisplayStatus
},
threadShowing () {
return this.controlledThreadDisplayStatus === 'showing'
} }
}, },
methods: { methods: {
@ -326,7 +391,7 @@ const Status = {
this.error = undefined this.error = undefined
}, },
toggleReplying () { toggleReplying () {
this.replying = !this.replying controlledOrUncontrolledToggle(this, 'replying')
}, },
gotoOriginal (id) { gotoOriginal (id) {
if (this.inConversation) { if (this.inConversation) {
@ -346,17 +411,19 @@ const Status = {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
}, },
addMediaPlaying (id) { addMediaPlaying (id) {
this.mediaPlaying.push(id) controlledOrUncontrolledSet(this, 'mediaPlaying', this.mediaPlaying.concat(id))
}, },
removeMediaPlaying (id) { removeMediaPlaying (id) {
this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id) controlledOrUncontrolledSet(this, 'mediaPlaying', this.mediaPlaying.filter(mediaId => mediaId !== id))
}, },
setHeadTailLinks (headTailLinks) { setHeadTailLinks (headTailLinks) {
this.headTailLinks = headTailLinks this.headTailLinks = headTailLinks
} },
}, toggleThreadDisplay () {
watch: { this.controlledToggleThreadDisplay()
'highlight': function (id) { },
scrollIfHighlighted (highlightId) {
const id = highlightId
if (this.status.id === id) { if (this.status.id === id) {
let rect = this.$el.getBoundingClientRect() let rect = this.$el.getBoundingClientRect()
if (rect.top < 100) { if (rect.top < 100) {
@ -370,6 +437,11 @@ const Status = {
window.scrollBy(0, rect.bottom - window.innerHeight + 50) window.scrollBy(0, rect.bottom - window.innerHeight + 50)
} }
} }
}
},
watch: {
'highlight': function (id) {
this.scrollIfHighlighted(id)
}, },
'status.repeat_num': function (num) { 'status.repeat_num': function (num) {
// refetch repeats when repeat_num is changed in any way // refetch repeats when repeat_num is changed in any way

View file

@ -1,7 +1,5 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
$status-margin: 0.75em;
.Status { .Status {
min-width: 0; min-width: 0;
white-space: normal; white-space: normal;
@ -28,13 +26,6 @@ $status-margin: 0.75em;
--icon: var(--selectedPostIcon, $fallback--icon); --icon: var(--selectedPostIcon, $fallback--icon);
} }
&.-conversation {
border-left-width: 4px;
border-left-style: solid;
border-left-color: $fallback--cRed;
border-left-color: var(--cRed, $fallback--cRed);
}
.gravestone { .gravestone {
padding: $status-margin; padding: $status-margin;
color: $fallback--faint; color: $fallback--faint;

View file

@ -219,6 +219,31 @@
class="fa-scale-110" class="fa-scale-110"
/> />
</button> </button>
<button
v-if="inThreadForest && replies && replies.length && !simpleTree"
class="button-unstyled"
:title="threadShowing ? $t('status.thread_hide') : $t('status.thread_show')"
:aria-expanded="threadShowing ? 'true' : 'false'"
@click.prevent="toggleThreadDisplay"
>
<FAIcon
fixed-width
class="fa-scale-110"
:icon="threadShowing ? 'chevron-up' : 'chevron-down'"
/>
</button>
<button
v-if="dive && !simpleTree"
class="button-unstyled"
:title="$t('status.show_only_conversation_under_this')"
@click.prevent="dive"
>
<FAIcon
fixed-width
class="fa-scale-110"
:icon="'angle-double-right'"
/>
</button>
</span> </span>
</div> </div>
<div <div
@ -306,6 +331,12 @@
:no-heading="noHeading" :no-heading="noHeading"
:highlight="highlight" :highlight="highlight"
:focused="isFocused" :focused="isFocused"
:controlled-showing-tall="controlledShowingTall"
:controlled-expanding-subject="controlledExpandingSubject"
:controlled-showing-long-subject="controlledShowingLongSubject"
:controlled-toggle-showing-tall="controlledToggleShowingTall"
:controlled-toggle-expanding-subject="controlledToggleExpandingSubject"
:controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject"
@mediaplay="addMediaPlaying($event)" @mediaplay="addMediaPlaying($event)"
@mediapause="removeMediaPlaying($event)" @mediapause="removeMediaPlaying($event)"
@parseReady="setHeadTailLinks" @parseReady="setHeadTailLinks"
@ -315,7 +346,20 @@
v-if="inConversation && !isPreview && replies && replies.length" v-if="inConversation && !isPreview && replies && replies.length"
class="replies" class="replies"
> >
<span class="faint">{{ $t('status.replies_list') }}</span> <button
v-if="showOtherRepliesAsButton"
class="button-unstyled -link faint"
:title="$tc('status.ancestor_follow', replies.length - 1, { numReplies: replies.length - 1 })"
@click.prevent="dive"
>
{{ $tc('status.replies_list_with_others', replies.length - 1, { numReplies: replies.length - 1 }) }}
</button>
<span
v-else
class="faint"
>
{{ $t('status.replies_list') }}
</span>
<StatusPopover <StatusPopover
v-for="reply in replies" v-for="reply in replies"
:key="reply.id" :key="reply.id"

View file

@ -26,14 +26,16 @@ const StatusContent = {
'focused', 'focused',
'noHeading', 'noHeading',
'fullContent', 'fullContent',
'singleLine' 'singleLine',
'showingTall',
'expandingSubject',
'showingLongSubject',
'toggleShowingTall',
'toggleExpandingSubject',
'toggleShowingLongSubject'
], ],
data () { data () {
return { return {
showingTall: this.fullContent || (this.inConversation && this.focused),
showingLongSubject: false,
// not as computed because it sets the initial state which will be changed later
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
postLength: this.status.text.length, postLength: this.status.text.length,
parseReadyDone: false parseReadyDone: false
} }
@ -115,9 +117,9 @@ const StatusContent = {
}, },
toggleShowMore () { toggleShowMore () {
if (this.mightHideBecauseTall) { if (this.mightHideBecauseTall) {
this.showingTall = !this.showingTall this.toggleShowingTall()
} else if (this.mightHideBecauseSubject) { } else if (this.mightHideBecauseSubject) {
this.expandingSubject = !this.expandingSubject this.toggleExpandingSubject()
} }
}, },
generateTagLink (tag) { generateTagLink (tag) {

View file

@ -17,14 +17,14 @@
<button <button
v-if="longSubject && showingLongSubject" v-if="longSubject && showingLongSubject"
class="button-unstyled -link tall-subject-hider" class="button-unstyled -link tall-subject-hider"
@click.prevent="showingLongSubject=false" @click.prevent="toggleShowingLongSubject"
> >
{{ $t("status.hide_full_subject") }} {{ $t("status.hide_full_subject") }}
</button> </button>
<button <button
v-else-if="longSubject" v-else-if="longSubject"
class="button-unstyled -link tall-subject-hider" class="button-unstyled -link tall-subject-hider"
@click.prevent="showingLongSubject=true" @click.prevent="toggleShowingLongSubject"
> >
{{ $t("status.show_full_subject") }} {{ $t("status.show_full_subject") }}
</button> </button>

View file

@ -23,6 +23,30 @@ library.add(
faPollH faPollH
) )
const camelCase = name => name.charAt(0).toUpperCase() + name.slice(1)
const controlledOrUncontrolledGetters = list => list.reduce((res, name) => {
const camelized = camelCase(name)
const toggle = `controlledToggle${camelized}`
const controlledName = `controlled${camelized}`
const uncontrolledName = `uncontrolled${camelized}`
res[name] = function () {
return this[toggle] ? this[controlledName] : this[uncontrolledName]
}
return res
}, {})
const controlledOrUncontrolledToggle = (obj, name) => {
const camelized = camelCase(name)
const toggle = `controlledToggle${camelized}`
const uncontrolledName = `uncontrolled${camelized}`
if (obj[toggle]) {
obj[toggle]()
} else {
obj[uncontrolledName] = !obj[uncontrolledName]
}
}
const StatusContent = { const StatusContent = {
name: 'StatusContent', name: 'StatusContent',
props: [ props: [
@ -31,9 +55,22 @@ const StatusContent = {
'focused', 'focused',
'noHeading', 'noHeading',
'fullContent', 'fullContent',
'singleLine' 'singleLine',
'controlledShowingTall',
'controlledExpandingSubject',
'controlledToggleShowingTall',
'controlledToggleExpandingSubject'
], ],
data () {
return {
uncontrolledShowingTall: this.fullContent || (this.inConversation && this.focused),
uncontrolledShowingLongSubject: false,
// not as computed because it sets the initial state which will be changed later
uncontrolledExpandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
}
},
computed: { computed: {
...controlledOrUncontrolledGetters(['showingTall', 'expandingSubject', 'showingLongSubject']),
hideAttachments () { hideAttachments () {
return (this.mergedConfig.hideAttachments && !this.inConversation) || return (this.mergedConfig.hideAttachments && !this.inConversation) ||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation) (this.mergedConfig.hideAttachmentsInConv && this.inConversation)
@ -71,6 +108,21 @@ const StatusContent = {
Gallery, Gallery,
LinkPreview, LinkPreview,
StatusBody StatusBody
},
methods: {
toggleShowingTall () {
controlledOrUncontrolledToggle(this, 'showingTall')
},
toggleExpandingSubject () {
controlledOrUncontrolledToggle(this, 'expandingSubject')
},
toggleShowingLongSubject () {
controlledOrUncontrolledToggle(this, 'showingLongSubject')
},
setMedia () {
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
return () => this.$store.dispatch('setMedia', attachments)
}
} }
} }

View file

@ -8,6 +8,12 @@
:status="status" :status="status"
:compact="compact" :compact="compact"
:single-line="singleLine" :single-line="singleLine"
:showing-tall="showingTall"
:expanding-subject="expandingSubject"
:showing-long-subject="showingLongSubject"
:toggle-showing-tall="toggleShowingTall"
:toggle-expanding-subject="toggleExpandingSubject"
:toggle-showing-long-subject="toggleShowingLongSubject"
@parseReady="$emit('parseReady', $event)" @parseReady="$emit('parseReady', $event)"
> >
<div v-if="status.poll && status.poll.options && !compact"> <div v-if="status.poll && status.poll.options && !compact">

View file

@ -0,0 +1,90 @@
import Status from '../status/status.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faAngleDoubleDown,
faAngleDoubleRight
} from '@fortawesome/free-solid-svg-icons'
library.add(
faAngleDoubleDown,
faAngleDoubleRight
)
const ThreadTree = {
components: {
Status
},
name: 'ThreadTree',
props: {
depth: Number,
status: Object,
inProfile: Boolean,
conversation: Array,
collapsable: Boolean,
isExpanded: Boolean,
pinnedStatusIdsObject: Object,
profileUserId: String,
focused: Function,
highlight: String,
getReplies: Function,
setHighlight: Function,
toggleExpanded: Function,
simple: Boolean,
// to control display of the whole thread forest
toggleThreadDisplay: Function,
threadDisplayStatus: Object,
showThreadRecursively: Function,
totalReplyCount: Object,
totalReplyDepth: Object,
statusContentProperties: Object,
setStatusContentProperty: Function,
toggleStatusContentProperty: Function,
dive: Function
},
computed: {
suspendable () {
const selfSuspendable = this.$refs.statusComponent ? this.$refs.statusComponent.suspendable : true
if (this.$refs.childComponent) {
return selfSuspendable && this.$refs.childComponent.every(s => s.suspendable)
}
return selfSuspendable
},
reverseLookupTable () {
return this.conversation.reduce((table, status, index) => {
table[status.id] = index
return table
}, {})
},
currentReplies () {
return this.getReplies(this.status.id).map(({ id }) => this.statusById(id))
},
threadShowing () {
return this.threadDisplayStatus[this.status.id] === 'showing'
},
currentProp () {
return this.statusContentProperties[this.status.id]
}
},
methods: {
statusById (id) {
return this.conversation[this.reverseLookupTable[id]]
},
collapseThread () {
},
showThread () {
},
showAllSubthreads () {
},
toggleCurrentProp (name) {
this.toggleStatusContentProperty(this.status.id, name)
},
setCurrentProp (name, newVal) {
this.setStatusContentProperty(this.status.id, name)
}
}
}
export default ThreadTree

View file

@ -0,0 +1,128 @@
<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: $status-margin;
border-left: 2px solid var(--border, $fallback--border);
}
.thread-tree-replies-hidden {
padding: $status-margin;
//border-top: 1px solid var(--border, $fallback--border);
/* Make the button stretch along the whole row */
display: flex;
align-items: stretch;
flex-direction: column;
}
</style>

View file

@ -118,7 +118,8 @@
}, },
"media_modal": { "media_modal": {
"previous": "Previous", "previous": "Previous",
"next": "Next" "next": "Next",
"counter": "{current} / {total}"
}, },
"nav": { "nav": {
"about": "About", "about": "About",
@ -466,6 +467,14 @@
"subject_line_email": "Like email: \"re: subject\"", "subject_line_email": "Like email: \"re: subject\"",
"subject_line_mastodon": "Like mastodon: copy as is", "subject_line_mastodon": "Like mastodon: copy as is",
"subject_line_noop": "Do not copy", "subject_line_noop": "Do not copy",
"conversation_display": "Conversation display style",
"conversation_display_tree": "Tree-style",
"conversation_display_simple_tree": "Simplified tree-style",
"conversation_display_linear": "Linear-style",
"conversation_other_replies_button": "Show the \"other replies\" button",
"conversation_other_replies_button_below": "Below statuses",
"conversation_other_replies_button_inside": "Inside statuses",
"max_depth_in_thread": "Maximum number of levels in thread to display by default",
"post_status_content_type": "Post status content type", "post_status_content_type": "Post status content type",
"sensitive_by_default": "Mark posts as sensitive by default", "sensitive_by_default": "Mark posts as sensitive by default",
"stop_gifs": "Pause animated images until you hover on them", "stop_gifs": "Pause animated images until you hover on them",
@ -722,6 +731,7 @@
"reply_to": "Reply to", "reply_to": "Reply to",
"mentions": "Mentions", "mentions": "Mentions",
"replies_list": "Replies:", "replies_list": "Replies:",
"replies_list_with_others": "Replies (+{numReplies} other): | Replies (+{numReplies} others):",
"mute_conversation": "Mute conversation", "mute_conversation": "Mute conversation",
"unmute_conversation": "Unmute conversation", "unmute_conversation": "Unmute conversation",
"status_unavailable": "Status unavailable", "status_unavailable": "Status unavailable",
@ -748,7 +758,18 @@
"attachment_stop_flash": "Stop Flash player", "attachment_stop_flash": "Stop Flash player",
"move_up": "Shift attachment left", "move_up": "Shift attachment left",
"move_down": "Shift attachment right", "move_down": "Shift attachment right",
"open_gallery": "Open gallery" "open_gallery": "Open gallery",
"thread_hide": "Hide this thread",
"thread_show": "Show this thread",
"thread_show_full": "Show everything under this thread ({numStatus} status in total, max depth {depth}) | Show everything under this thread ({numStatus} statuses in total, max depth {depth})",
"thread_show_full_with_icon": "{icon} {text}",
"thread_follow": "See the remaining part of this thread ({numStatus} status in total) | See the remaining part of this thread ({numStatus} statuses in total)",
"thread_follow_with_icon": "{icon} {text}",
"ancestor_follow": "See {numReplies} other reply under this status | See {numReplies} other replies under this status",
"ancestor_follow_with_icon": "{icon} {text}",
"show_all_conversation_with_icon": "{icon} {text}",
"show_all_conversation": "Show full conversation ({numStatus} other status) | Show full conversation ({numStatus} other statuses)",
"show_only_conversation_under_this": "Only show replies to this status"
}, },
"user_card": { "user_card": {
"approve": "Approve", "approve": "Approve",

View file

@ -12,6 +12,8 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0]
export const multiChoiceProperties = [ export const multiChoiceProperties = [
'postContentType', 'postContentType',
'subjectLineBehavior', 'subjectLineBehavior',
'conversationDisplay', // tree | linear
'conversationOtherRepliesButton', // below | inside
'mentionLinkDisplay' // short | full_for_remote | full 'mentionLinkDisplay' // short | full_for_remote | full
] ]
@ -81,7 +83,10 @@ export const defaultState = {
hidePostStats: undefined, // instance default hidePostStats: undefined, // instance default
hideUserStats: undefined, // instance default hideUserStats: undefined, // instance default
virtualScrolling: undefined, // instance default virtualScrolling: undefined, // instance default
sensitiveByDefault: undefined // instance default sensitiveByDefault: undefined, // instance default
conversationDisplay: undefined, // instance default
conversationOtherRepliesButton: undefined, // instance default
maxDepthInThread: 6
} }
// caching the instance default properties // caching the instance default properties

View file

@ -53,6 +53,9 @@ const defaultState = {
theme: 'pleroma-dark', theme: 'pleroma-dark',
virtualScrolling: true, virtualScrolling: true,
sensitiveByDefault: false, sensitiveByDefault: false,
conversationDisplay: 'simple_tree',
conversationOtherRepliesButton: 'below',
maxDepthInThread: 6,
// Nasty stuff // Nasty stuff
customEmoji: [], customEmoji: [],