Merge branch 'virtual-scrolling-2.0' into shigusegubu-themes3
This commit is contained in:
commit
24ac79a41d
23 changed files with 760 additions and 891 deletions
|
|
@ -67,8 +67,6 @@
|
|||
"@vue/devtools-api": "8.2.0",
|
||||
"@vue/test-utils": "2.5.0",
|
||||
"autoprefixer": "10.5.4",
|
||||
"chalk": "6.0.0",
|
||||
"cross-spawn": "7.0.6",
|
||||
"iso-639-1": "3.1.6",
|
||||
"playwright": "1.61.0",
|
||||
"postcss": "8.5.28",
|
||||
|
|
|
|||
|
|
@ -83,8 +83,9 @@ export default () => {
|
|||
},
|
||||
{
|
||||
name: 'conversation',
|
||||
path: '/notice/:id',
|
||||
path: '/notice/:statusId',
|
||||
component: ConversationPage,
|
||||
props: true,
|
||||
meta: { dontScroll: true },
|
||||
},
|
||||
{
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -6,6 +6,15 @@
|
|||
backdrop-filter: var(--__panel-backdrop-filter);
|
||||
}
|
||||
|
||||
.conversation-status:not(.-last) {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.conversation-status:not(.-last)
|
||||
.conversation-status:not(.-first) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.conversation-dive-to-top-level-box {
|
||||
padding: var(--status-margin);
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
|
@ -49,26 +58,12 @@
|
|||
padding: var(--status-margin);
|
||||
}
|
||||
|
||||
.conversation-status {
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.thread-ancestor-has-other-replies .conversation-status,
|
||||
&:last-child:not(.-expanded) .conversation-status,
|
||||
&.-expanded .conversation-status:last-child,
|
||||
.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: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* expanded conversation in timeline */
|
||||
&.status-fadein.-expanded .thread-body {
|
||||
&.-expanded .thread-body {
|
||||
border-left: 4px solid var(--cRed);
|
||||
border-radius: var(--roundness);
|
||||
border-top-left-radius: 0;
|
||||
|
|
@ -76,7 +71,7 @@
|
|||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
&.-expanded.status-fadein {
|
||||
&.-expanded:not(.-page) {
|
||||
--___margin: calc(var(--status-margin) / 2);
|
||||
|
||||
background: var(--background);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="!hide"
|
||||
ref="root"
|
||||
class="Conversation"
|
||||
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
|
||||
:class="{ '-expanded' : isExpanded, '-page': isPage, 'panel' : isExpanded }"
|
||||
>
|
||||
<div
|
||||
v-if="isExpanded"
|
||||
|
|
@ -19,19 +19,19 @@
|
|||
</template>
|
||||
</h1>
|
||||
<button
|
||||
v-if="collapsable"
|
||||
v-if="!isPage"
|
||||
class="button-unstyled -link"
|
||||
@click.prevent="toggleExpanded"
|
||||
>
|
||||
{{ $t('timeline.collapse') }}
|
||||
</button>
|
||||
<QuickFilterSettings
|
||||
v-if="!collapsable && mobileLayout"
|
||||
v-if="isPage && mobileLayout"
|
||||
:conversation="true"
|
||||
class="rightside-button"
|
||||
/>
|
||||
<QuickViewSettings
|
||||
v-if="!collapsable"
|
||||
v-if="isPage"
|
||||
:conversation="true"
|
||||
class="rightside-button"
|
||||
/>
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
</template>
|
||||
<template #text>
|
||||
<span>
|
||||
{{ $t('status.show_all_conversation', { numStatus: otherTopLevelCount }, otherTopLevelCount) }}
|
||||
{{ $t('status.show_all_conversation', { numStatus: topLevel.length - 1 }, topLevel.length - 1) }}
|
||||
</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
|
|
@ -91,36 +91,27 @@
|
|||
class="thread-ancestors"
|
||||
>
|
||||
<article
|
||||
v-for="status in ancestorsOf(diveRoot)"
|
||||
:key="status.id"
|
||||
v-for="status in currentAncestors"
|
||||
class="thread-ancestor"
|
||||
:class="{'thread-ancestor-has-other-replies': getReplies(status.id).length > 1, '-faded': shouldFadeAncestors}"
|
||||
:class="{'thread-ancestor-has-other-replies': statusReplies.size > 1, '-faded': shouldFadeAncestors}"
|
||||
>
|
||||
<Status
|
||||
ref="statusComponent"
|
||||
class="conversation-status status-fadein panel-body"
|
||||
class="conversation-status panel-body"
|
||||
:class="getStatusClasses(status)"
|
||||
|
||||
:status-id="status.id"
|
||||
:replies="getReplies(status.id)"
|
||||
:replies="statusReplies"
|
||||
|
||||
:expandable="!isExpanded"
|
||||
:focused="maybeFocused === status.id"
|
||||
:inline-expanded="collapsable && isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:in-profile="inProfile"
|
||||
:in-conversation="isExpanded"
|
||||
:profile-user-id="profileUserId"
|
||||
:simple-tree="treeViewIsSimple"
|
||||
:show-other-replies-as-button="showOtherRepliesButtonInsideStatus"
|
||||
:focused="focused === status.id"
|
||||
can-dive
|
||||
|
||||
@goto="setFocused"
|
||||
@dive="() => diveIntoStatus(status.id)"
|
||||
@dive="diveIntoStatus(status.id)"
|
||||
@suspendable-state-change="onStatusSuspendStateChange"
|
||||
@height-change="updateVirtualHeight"
|
||||
/>
|
||||
<div
|
||||
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
|
||||
v-if="shouldShowOtherRepliesButton && statusReplies.size > 1"
|
||||
class="thread-ancestor-dive-box"
|
||||
>
|
||||
<div
|
||||
|
|
@ -140,7 +131,7 @@
|
|||
</template>
|
||||
<template #text>
|
||||
<span>
|
||||
{{ $t('status.ancestor_follow', { numReplies: getReplies(status.id, getReplies(status.id).length - 1).length - 1 }) }}
|
||||
{{ $t('status.ancestor_follow', { numReplies: statusReplies.size - 1 }) }}
|
||||
</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
|
|
@ -149,32 +140,13 @@
|
|||
</article>
|
||||
</div>
|
||||
<ThreadTree
|
||||
v-for="status in showingTopLevel"
|
||||
:key="status.id"
|
||||
ref="statusComponent"
|
||||
:depth="0"
|
||||
|
||||
:status-id="status.id"
|
||||
:in-profile="inProfile"
|
||||
:conversation="conversation"
|
||||
:collapsable="collapsable"
|
||||
:is-expanded="isExpanded"
|
||||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||
:profile-user-id="profileUserId"
|
||||
|
||||
:get-replies="getReplies"
|
||||
:focused="maybeFocused"
|
||||
:toggle-expanded="toggleExpanded"
|
||||
|
||||
:simple="treeViewIsSimple"
|
||||
:thread-display-status="threadDisplayStatus"
|
||||
:show-thread-recursively="showThreadRecursively"
|
||||
:total-reply-count="totalReplyCount"
|
||||
:total-reply-depth="totalReplyDepth"
|
||||
:can-dive="canDive"
|
||||
:depth="0"
|
||||
|
||||
@goto="setFocused"
|
||||
@dive="diveIntoStatus"
|
||||
@toggle-expanded="toggleExpanded"
|
||||
@show-thread-recursively="showThreadRecursively"
|
||||
@suspendable-state-change="onStatusSuspendStateChange"
|
||||
@height-change="updateVirtualHeight"
|
||||
/>
|
||||
|
|
@ -183,22 +155,24 @@
|
|||
v-else-if="isLinearView"
|
||||
class="thread-body"
|
||||
>
|
||||
<article>
|
||||
<article
|
||||
v-for="element in heightChartLinear"
|
||||
class="panel-body"
|
||||
:key="element.id ?? element.ids"
|
||||
>
|
||||
<div
|
||||
v-if="element.type === 'spacer'"
|
||||
class="virtual-spacer"
|
||||
:style="{ height: element.height + 'px' }"
|
||||
/>
|
||||
<Status
|
||||
v-for="status in conversation"
|
||||
:key="status.id"
|
||||
ref="statusComponent"
|
||||
class="conversation-status status-fadein panel-body"
|
||||
:status-id="status.id"
|
||||
v-if="element.type === 'status'"
|
||||
class="conversation-status"
|
||||
:class="getStatusClasses(status)"
|
||||
:status-id="element.status.id"
|
||||
:replies="getReplies(status.id)"
|
||||
|
||||
:expandable="!isExpanded"
|
||||
:focused="maybeFocused === status.id || maybeFocused === status.retweeted_status?.id"
|
||||
:inline-expanded="collapsable && isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:in-profile="inProfile"
|
||||
:in-conversation="isExpanded"
|
||||
:profile-user-id="profileUserId"
|
||||
:focused="focused === element.id || focused === element.status.retweeted_status?.id"
|
||||
|
||||
@goto="setFocused"
|
||||
@toggle-expanded="toggleExpanded"
|
||||
|
|
@ -209,11 +183,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="Conversation -hidden"
|
||||
:style="hiddenStyle"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script src="./conversation.js"></script>
|
||||
|
|
|
|||
21
src/components/conversation/useScrollPosition.js
Normal file
21
src/components/conversation/useScrollPosition.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
export function useScrollPosition() {
|
||||
const x = ref(0)
|
||||
const y = ref(0)
|
||||
|
||||
const update = (e) => {
|
||||
x.value = window.scrollX
|
||||
y.value = window.scrollY
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('scroll', update)
|
||||
update()
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('scroll', update)
|
||||
})
|
||||
|
||||
return { x, y }
|
||||
}
|
||||
18
src/components/conversation/useWindowSize.js
Normal file
18
src/components/conversation/useWindowSize.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
export function useWindowSize() {
|
||||
const height = ref(0)
|
||||
const width = ref(0)
|
||||
|
||||
const update = () => {
|
||||
height.value = window.innerHeight
|
||||
width.value = window.innerWidth
|
||||
}
|
||||
|
||||
onMounted(() => window.addEventListener('resize', update))
|
||||
onUnmounted(() => window.removeEventListener('resize', update))
|
||||
|
||||
update()
|
||||
|
||||
return { height, width }
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
<template #statusLink>
|
||||
<router-link
|
||||
class="faint-link"
|
||||
:to="{ name: 'conversation', params: { id: draft.refId } }"
|
||||
:to="{ name: 'conversation', params: { statusId: draft.refId } }"
|
||||
>
|
||||
{{ refStatus ? refStatus.external_url : $t('drafts.unavailable') }}
|
||||
</router-link>
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@
|
|||
>
|
||||
<router-link
|
||||
v-if="notification.status"
|
||||
:to="{ name: 'conversation', params: { id: notification.status.id } }"
|
||||
:to="{ name: 'conversation', params: { statusId: notification.status.id } }"
|
||||
class="timeago-link faint"
|
||||
>
|
||||
<Timeago
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
<Status
|
||||
v-if="shouldDisplayQuote"
|
||||
:statusoid="quotedStatus"
|
||||
:in-quote="true"
|
||||
in-quote
|
||||
/>
|
||||
</article>
|
||||
<p
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
<router-link
|
||||
v-for="status in report.statuses"
|
||||
:key="status.id"
|
||||
:to="{ name: 'conversation', params: { id: status.id } }"
|
||||
:to="{ name: 'conversation', params: { statusId: status.id } }"
|
||||
class="reported-status"
|
||||
>
|
||||
<div class="reported-status-heading">
|
||||
|
|
|
|||
|
|
@ -96,25 +96,17 @@ const Status = {
|
|||
props: {
|
||||
statusId: String,
|
||||
statusoid: Object,
|
||||
replies: Array,
|
||||
replies: Set,
|
||||
|
||||
expandable: Boolean,
|
||||
focused: Boolean,
|
||||
compact: Boolean,
|
||||
isPreview: Boolean,
|
||||
noHeading: Boolean,
|
||||
inlineExpanded: Boolean,
|
||||
inProfile: Boolean,
|
||||
inConversation: Boolean,
|
||||
inQuote: Boolean,
|
||||
|
||||
profileUserId: String,
|
||||
simpleTree: Boolean,
|
||||
showOtherRepliesAsButton: Boolean,
|
||||
canDive: Boolean,
|
||||
ignoreMute: Boolean,
|
||||
|
||||
threadDisplayStatus: String,
|
||||
threadDisplayState: String,
|
||||
},
|
||||
emits: [
|
||||
'goto',
|
||||
|
|
@ -123,8 +115,14 @@ const Status = {
|
|||
'suspendableStateChange',
|
||||
'heightChange',
|
||||
],
|
||||
inject: {
|
||||
profileUserId: { default: null },
|
||||
isPage: { default: false },
|
||||
isExpanded: { default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
resizeObserver: new ResizeObserver(this.updateVirtualHeight),
|
||||
replying: false,
|
||||
unmuted: false,
|
||||
mediaPlaying: new Set(),
|
||||
|
|
@ -140,6 +138,12 @@ const Status = {
|
|||
status() {
|
||||
return this.statusoid ?? useStatusesStore().allStatuses.get(this.statusId)
|
||||
},
|
||||
inConversation() {
|
||||
return this.isExpanded
|
||||
},
|
||||
inProfile() {
|
||||
return this.profileUserId != null
|
||||
},
|
||||
// Status repeated
|
||||
repeatedStatus() {
|
||||
if (this.status.retweeted_status === undefined) return undefined
|
||||
|
|
@ -166,6 +170,12 @@ const Status = {
|
|||
user() {
|
||||
return useUsersStore().findUser(this.mainStatus.user.id)
|
||||
},
|
||||
simpleTree() {
|
||||
return !this.mergedConfig.conversationTreeAdvanced
|
||||
},
|
||||
showOtherRepliesAsButton() {
|
||||
return this.mergedConfig.conversationOtherRepliesButton === 'inside'
|
||||
},
|
||||
showReasonMutedThread() {
|
||||
return (
|
||||
(this.mainStatus.thread_muted || this.repeatStatus?.thread_muted) &&
|
||||
|
|
@ -436,10 +446,10 @@ const Status = {
|
|||
return !this.replying && this.mediaPlaying.size === 0
|
||||
},
|
||||
inThreadForest() {
|
||||
return !!this.threadDisplayStatus
|
||||
return !!this.threadDisplayState
|
||||
},
|
||||
threadShowing() {
|
||||
return this.threadDisplayStatus === 'showing'
|
||||
return this.threadDisplayState === 'showing'
|
||||
},
|
||||
visibilityLocalized() {
|
||||
return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility)
|
||||
|
|
@ -553,47 +563,22 @@ const Status = {
|
|||
// FIXME
|
||||
this.controlledToggleThreadDisplay()
|
||||
},
|
||||
scrollIfFocused(focused) {
|
||||
if (this.$el.getBoundingClientRect == null) return
|
||||
if (focused) {
|
||||
const rect = this.$el.getBoundingClientRect()
|
||||
if (rect.top < 100) {
|
||||
// Post is above screen, match its top to screen top
|
||||
window.scrollBy(0, rect.top - 100)
|
||||
} else if (rect.height >= window.innerHeight - 50) {
|
||||
// Post we want to see is taller than screen so match its top to screen top
|
||||
window.scrollBy(0, rect.top - 100)
|
||||
} else if (rect.bottom > window.innerHeight - 50) {
|
||||
// Post is below screen, match its bottom to screen bottom
|
||||
window.scrollBy(0, rect.bottom - window.innerHeight + 50)
|
||||
}
|
||||
}
|
||||
},
|
||||
onTransitionEnd() {
|
||||
this.$nextTick(() => {
|
||||
this.$emit('heightChange')
|
||||
updateVirtualHeight(e) {
|
||||
const [entry] = e
|
||||
this.$emit('heightChange', {
|
||||
id: this.status.id,
|
||||
height: entry.contentRect.height,
|
||||
element: this.$el,
|
||||
})
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.resizeObserver.observe(this.$el)
|
||||
},
|
||||
unmounted() {
|
||||
this.resizeObserver.disconnect()
|
||||
},
|
||||
watch: {
|
||||
status: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.$emit('heightChange')
|
||||
},
|
||||
},
|
||||
unmuted() {
|
||||
this.$emit('heightChange')
|
||||
},
|
||||
error() {
|
||||
this.$emit('heightChange')
|
||||
},
|
||||
replying() {
|
||||
this.$emit('heightChange')
|
||||
},
|
||||
focused: function (id) {
|
||||
this.scrollIfFocused(id)
|
||||
},
|
||||
'mainStatus.repeat_num': function (num) {
|
||||
// refetch repeats when repeat_num is changed in any way
|
||||
if (this.focused && this.repeatedBy.size !== num) {
|
||||
|
|
|
|||
|
|
@ -271,21 +271,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.status-fadein {
|
||||
animation-duration: 0.4s;
|
||||
animation-name: fadein;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.status-actions {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
v-if="!hideStatus"
|
||||
ref="root"
|
||||
class="Status"
|
||||
:class="[{ '-focused': focused }, { '-conversation': inlineExpanded }]"
|
||||
:class="[{ '-focused': focused }, { '-conversation': !isPage && isExpanded }]"
|
||||
>
|
||||
<div
|
||||
v-if="error"
|
||||
|
|
@ -180,7 +180,7 @@
|
|||
</span>
|
||||
<router-link
|
||||
class="timeago faint"
|
||||
:to="{ name: 'conversation', params: { id: status.id } }"
|
||||
:to="{ name: 'conversation', params: { statusId: status.id } }"
|
||||
>
|
||||
<Timeago
|
||||
:time="mainStatus.created_at"
|
||||
|
|
@ -199,7 +199,7 @@
|
|||
/>
|
||||
</span>
|
||||
<button
|
||||
v-if="expandable && !isPreview"
|
||||
v-if="!isExpanded && !isPreview"
|
||||
class="button-unstyled"
|
||||
:title="$t('status.expand')"
|
||||
@click.prevent="toggleExpanded"
|
||||
|
|
@ -222,7 +222,7 @@
|
|||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="inThreadForest && replies?.length && !simpleTree"
|
||||
v-if="inThreadForest && replies?.size && !simpleTree"
|
||||
class="button-unstyled"
|
||||
:title="threadShowing ? $t('status.thread_hide') : $t('status.thread_show')"
|
||||
:aria-expanded="threadShowing ? 'true' : 'false'"
|
||||
|
|
@ -235,7 +235,7 @@
|
|||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDive && !simpleTree"
|
||||
v-if="isExpanded && !simpleTree"
|
||||
class="button-unstyled"
|
||||
:title="$t('status.show_only_conversation_under_this')"
|
||||
@click.prevent="$emit('dive')"
|
||||
|
|
@ -423,16 +423,16 @@
|
|||
/>
|
||||
|
||||
<div
|
||||
v-if="inConversation && !isPreview && replies?.length"
|
||||
v-if="inConversation && !isPreview && replies?.size"
|
||||
class="replies"
|
||||
>
|
||||
<button
|
||||
v-if="showOtherRepliesAsButton && replies.length > 1"
|
||||
v-if="showOtherRepliesAsButton && replies.size > 1"
|
||||
class="button-unstyled -link"
|
||||
:title="$t('status.ancestor_follow', { numReplies: replies.length - 1 }, replies.length - 1)"
|
||||
:title="$t('status.ancestor_follow', { numReplies: replies.size - 1 }, replies.size - 1)"
|
||||
@click.prevent="$emit('dive')"
|
||||
>
|
||||
{{ $t('status.replies_list_with_others', { numReplies: replies.length - 1 }, replies.length - 1) }}
|
||||
{{ $t('status.replies_list_with_others', { numReplies: replies.size - 1 }, replies.size - 1) }}
|
||||
</button>
|
||||
<span
|
||||
v-else
|
||||
|
|
@ -441,7 +441,7 @@
|
|||
{{ $t('status.replies_list') }}
|
||||
</span>
|
||||
<StatusPopover
|
||||
v-for="reply in replies"
|
||||
v-for="reply in replies.values()"
|
||||
:key="reply.id"
|
||||
:status-id="reply.id"
|
||||
>
|
||||
|
|
@ -454,10 +454,7 @@
|
|||
</StatusPopover>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
@after-leave="onTransitionEnd"
|
||||
name="fade"
|
||||
>
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="shouldDisplayFavsAndRepeats"
|
||||
class="favs-repeated-users"
|
||||
|
|
@ -549,7 +546,6 @@
|
|||
@posted="closeReplyForm"
|
||||
@draft-done="closeReplyForm"
|
||||
@close-accepted="closeReplyForm"
|
||||
@resize="$emit('heightChange')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -234,7 +234,7 @@ export const BUTTONS = [
|
|||
return chatView
|
||||
},
|
||||
action({ router, status }) {
|
||||
router.push({ name: 'conversation', params: { id: status.id } })
|
||||
router.push({ name: 'conversation', params: { statusId: status.id } })
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -293,8 +293,10 @@ export const BUTTONS = [
|
|||
navigator.clipboard.writeText(
|
||||
[
|
||||
useInstanceStore().server,
|
||||
router.resolve({ name: 'conversation', params: { id: status.id } })
|
||||
.href,
|
||||
router.resolve({
|
||||
name: 'conversation',
|
||||
params: { statusId: status.id },
|
||||
}).href,
|
||||
].join(''),
|
||||
)
|
||||
return Promise.resolve()
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
:key="status.id"
|
||||
:statusoid="status"
|
||||
:is-preview="true"
|
||||
class="conversation-status status-fadein panel-body"
|
||||
class="conversation-status panel-body"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faAngleDoubleDown,
|
||||
|
|
@ -10,33 +12,85 @@ const ThreadTree = {
|
|||
components: {},
|
||||
name: 'ThreadTree',
|
||||
props: {
|
||||
depth: Number,
|
||||
statusId: String,
|
||||
inProfile: Boolean,
|
||||
conversation: Array,
|
||||
collapsable: Boolean,
|
||||
isExpanded: Boolean,
|
||||
pinnedStatusIdsObject: Object,
|
||||
profileUserId: String,
|
||||
|
||||
focused: String,
|
||||
getReplies: Function,
|
||||
toggleExpanded: Function,
|
||||
|
||||
simple: Boolean,
|
||||
canDive: Boolean,
|
||||
threadDisplayStatus: Object,
|
||||
showThreadRecursively: Function,
|
||||
totalReplyCount: Object,
|
||||
totalReplyDepth: Object,
|
||||
depth: Number,
|
||||
},
|
||||
emits: ['suspendableStateChange', 'goto', 'dive', 'heightChange'],
|
||||
emits: [
|
||||
'suspendableStateChange',
|
||||
'goto',
|
||||
'dive',
|
||||
'toggleExpanded',
|
||||
'showThreadRecursively',
|
||||
],
|
||||
inject: [
|
||||
'conversation',
|
||||
'focused',
|
||||
'replies',
|
||||
'threadDisplay',
|
||||
'threadDisplayDefault',
|
||||
'isExpanded',
|
||||
'isPage',
|
||||
],
|
||||
computed: {
|
||||
currentReplies() {
|
||||
return this.getReplies(this.statusId).map(({ id }) => id)
|
||||
return [...this.getReplies(this.statusId)].map(({ id }) => id)
|
||||
},
|
||||
simple() {
|
||||
return !useMergedConfigStore().mergedConfig.conversationTreeAdvanced
|
||||
},
|
||||
threadShowing() {
|
||||
return this.threadDisplayStatus[this.statusId] === 'showing'
|
||||
const result =
|
||||
this.threadDisplay.get(this.statusId) ??
|
||||
this.threadDisplayDefault.get(this.statusId)
|
||||
return result === 'showing'
|
||||
},
|
||||
canDive() {
|
||||
return this.isExpanded
|
||||
},
|
||||
totalReplyCount() {
|
||||
const sizes = {}
|
||||
const subTreeSizeFor = (id) => {
|
||||
if (sizes[id]) {
|
||||
return sizes[id]
|
||||
}
|
||||
sizes[id] =
|
||||
1 +
|
||||
[...this.getReplies(id)]
|
||||
.map(({ id }) => id)
|
||||
.map((cid) => subTreeSizeFor(cid))
|
||||
.reduce((a, b) => a + b, 0)
|
||||
return sizes[id]
|
||||
}
|
||||
this.conversation.map((k) => k.id).forEach(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.getReplies(id)]
|
||||
.map(({ id }) => id)
|
||||
.map((cid) => subTreeDepthFor(cid))
|
||||
.reduce((a, b) => (a > b ? a : b), 0)
|
||||
return depths[id]
|
||||
}
|
||||
this.conversation.map((k) => k.id).forEach(subTreeDepthFor)
|
||||
return Object.keys(depths).reduce((res, id) => {
|
||||
res[id] = depths[id] - 1 // exclude itself
|
||||
return res
|
||||
}, {})
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getReplies(id) {
|
||||
return this.replies.get(id) ?? new Set()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,27 +2,17 @@
|
|||
<article class="thread-tree">
|
||||
<Status
|
||||
:key="statusId"
|
||||
ref="statusComponent"
|
||||
class="conversation-status conversation-status-treeview panel-body"
|
||||
:status-id="statusId"
|
||||
:replies="getReplies(statusId)"
|
||||
:inline-expanded="collapsable && isExpanded"
|
||||
:expandable="!isExpanded"
|
||||
:show-pinned="pinnedStatusIdsObject && pinnedStatusIdsObject[status.id]"
|
||||
:in-conversation="isExpanded"
|
||||
:focused="focused === statusId"
|
||||
:in-profile="inProfile"
|
||||
:profile-user-id="profileUserId"
|
||||
class="conversation-status conversation-status-treeview status-fadein panel-body"
|
||||
|
||||
:simple-tree="simple"
|
||||
:thread-display-status="threadDisplayStatus[statusId]"
|
||||
:can-dive="canDive"
|
||||
:thread-display-state="threadDisplay.get(statusId)"
|
||||
|
||||
@dive="$emit('dive', statusId)"
|
||||
@goto="$emit('goto', statusId)"
|
||||
@toggle-expanded="toggleExpanded"
|
||||
@suspendable-state-change="e => $emit('suspendableStateChange', e)"
|
||||
@height-change="e => $emit('heightChange', e)"
|
||||
@toggle-expanded="$emit('toggleExpanded', statusId)"
|
||||
@suspendable-state-change="$emit('suspendableStateChange', e)"
|
||||
/>
|
||||
<div
|
||||
v-if="currentReplies.length > 0 && threadShowing"
|
||||
|
|
@ -31,32 +21,14 @@
|
|||
<ThreadTree
|
||||
v-for="replyStatusId in currentReplies"
|
||||
:key="replyStatusId"
|
||||
ref="childComponent"
|
||||
:depth="depth + 1"
|
||||
:status-id="replyStatusId"
|
||||
|
||||
:in-profile="inProfile"
|
||||
:conversation="conversation"
|
||||
:collapsable="collapsable"
|
||||
:is-expanded="isExpanded"
|
||||
:pinned-status-ids-object="pinnedStatusIdsObject"
|
||||
:profile-user-id="profileUserId"
|
||||
|
||||
:get-replies="getReplies"
|
||||
:focused="focused"
|
||||
:toggle-expanded="toggleExpanded"
|
||||
|
||||
:simple="simple"
|
||||
:thread-display-status="threadDisplayStatus"
|
||||
:show-thread-recursively="showThreadRecursively"
|
||||
:total-reply-count="totalReplyCount"
|
||||
:total-reply-depth="totalReplyDepth"
|
||||
|
||||
:can-dive="canDive"
|
||||
@show-thread-recursively="(e) => $emit('showThreadRecursively', e)"
|
||||
@goto="(e) => $emit('goto', e)"
|
||||
@dive="(e) => $emit('dive', e)"
|
||||
@suspendable-state-change="e => $emit('suspendableStateChange', e)"
|
||||
@height-change="e => $emit('heightChange', e)"
|
||||
@toggle-expanded="(e) => $emit('toggleExpanded', e)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -88,7 +60,7 @@
|
|||
tag="button"
|
||||
keypath="status.thread_show_full_with_icon"
|
||||
class="button-unstyled -link thread-tree-show-replies-button"
|
||||
@click.prevent="showThreadRecursively(statusId)"
|
||||
@click.prevent="$emit('showThreadRecursively', statusId)"
|
||||
>
|
||||
<template #icon>
|
||||
<FAIcon
|
||||
|
|
|
|||
|
|
@ -38,10 +38,14 @@ const Timeline = {
|
|||
showScrollTop: false,
|
||||
paused: false,
|
||||
unfocused: false,
|
||||
virtualScrollIndex: 0,
|
||||
blockingClicks: false,
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
profileUserId: this.inProfile && this.timelineRef.argument,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ScrollTopButton,
|
||||
Conversation,
|
||||
|
|
@ -102,20 +106,7 @@ const Timeline = {
|
|||
}
|
||||
},
|
||||
statusesToDisplay() {
|
||||
if (!this.virtualScrollingEnabled) {
|
||||
return new Set(this.filteredVisibleStatuses.map(({ id }) => id))
|
||||
}
|
||||
|
||||
const amount = this.timeline.visibleStatusIds.size
|
||||
const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80))
|
||||
const min = Math.max(0, this.virtualScrollIndex - statusesPerSide)
|
||||
const max = Math.min(amount, this.virtualScrollIndex + statusesPerSide)
|
||||
return new Set(
|
||||
this.filteredVisibleStatuses.slice(min, max).map(({ id }) => id),
|
||||
)
|
||||
},
|
||||
virtualScrollingEnabled() {
|
||||
return useMergedConfigStore().mergedConfig.virtualScrolling
|
||||
return new Set(this.filteredVisibleStatuses.map(({ id }) => id))
|
||||
},
|
||||
...mapState(useInterfaceStore, {
|
||||
mobileLayout: (store) => store.layoutType === 'mobile',
|
||||
|
|
@ -135,7 +126,6 @@ const Timeline = {
|
|||
}
|
||||
window.addEventListener('keydown', this.handleShortKey)
|
||||
window.addEventListener('scroll', this.handleScroll)
|
||||
setTimeout(this.determineVisibleStatuses, 250)
|
||||
},
|
||||
unmounted() {
|
||||
this.timelineChange(null, this.timelineRef)
|
||||
|
|
@ -193,54 +183,6 @@ const Timeline = {
|
|||
1000,
|
||||
this,
|
||||
),
|
||||
determineVisibleStatuses() {
|
||||
if (!this.$refs.timeline) return
|
||||
if (!this.virtualScrollingEnabled) return
|
||||
|
||||
const statuses = this.$refs.timeline.children
|
||||
if (statuses.length === 0) return
|
||||
const cappedScrollIndex = Math.max(
|
||||
0,
|
||||
Math.min(this.virtualScrollIndex, statuses.length - 1),
|
||||
)
|
||||
|
||||
const height = Math.max(document.body.offsetHeight, window.pageYOffset)
|
||||
|
||||
const centerOfScreen = window.pageYOffset + window.innerHeight * 0.5
|
||||
|
||||
// Start from approximating the index of some visible status by using the
|
||||
// the center of the screen on the timeline.
|
||||
let approxIndex = Math.min(
|
||||
Math.floor(statuses.length * (centerOfScreen / height)),
|
||||
statuses.length - 1,
|
||||
)
|
||||
let err = statuses[approxIndex].getBoundingClientRect().y
|
||||
|
||||
// if we have a previous scroll index that can be used, test if it's
|
||||
// closer than the previous approximation, use it if so
|
||||
|
||||
const virtualScrollIndexY =
|
||||
statuses[cappedScrollIndex].getBoundingClientRect().y
|
||||
if (Math.abs(err) > virtualScrollIndexY) {
|
||||
approxIndex = cappedScrollIndex
|
||||
err = virtualScrollIndexY
|
||||
}
|
||||
|
||||
// if the status is too far from viewport, check the next/previous ones if
|
||||
// they happen to be better
|
||||
while (err < -20 && approxIndex < statuses.length - 1) {
|
||||
err += statuses[approxIndex].offsetHeight
|
||||
approxIndex++
|
||||
}
|
||||
while (err > window.innerHeight + 100 && approxIndex > 0) {
|
||||
approxIndex--
|
||||
err -= statuses[approxIndex].offsetHeight
|
||||
}
|
||||
|
||||
// this status is now the center point for virtual scrolling and visible
|
||||
// statuses will be nearby statuses before and after it
|
||||
this.virtualScrollIndex = approxIndex
|
||||
},
|
||||
scrollLoad() {
|
||||
// TODO simplify this logic
|
||||
const bodyBRect = document.body.getBoundingClientRect()
|
||||
|
|
@ -253,7 +195,6 @@ const Timeline = {
|
|||
}
|
||||
},
|
||||
handleScroll: throttle(function (e) {
|
||||
this.determineVisibleStatuses()
|
||||
this.scrollLoad(e)
|
||||
}, 200),
|
||||
handleVisibilityChange() {
|
||||
|
|
@ -264,9 +205,6 @@ const Timeline = {
|
|||
timelineRef(newTimeline, oldTimeline) {
|
||||
this.timelineChange(newTimeline, oldTimeline)
|
||||
},
|
||||
filteredVisibleStatuses() {
|
||||
this.determineVisibleStatuses()
|
||||
},
|
||||
newStatusCount(count) {
|
||||
if (!useMergedConfigStore().mergedConfig.streaming) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@
|
|||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.vue-recycle-scroller.timeline {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.timeline-placeholder {
|
||||
background: var(--background);
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -78,20 +78,15 @@
|
|||
</div>
|
||||
<div :class="classes.body">
|
||||
<div
|
||||
ref="timeline"
|
||||
class="timeline"
|
||||
ref="timeline"
|
||||
role="feed"
|
||||
>
|
||||
<Conversation
|
||||
v-for="status in filteredVisibleStatuses"
|
||||
:key="status.id"
|
||||
role="listitem"
|
||||
class="status-fadein"
|
||||
:status-id="status.id"
|
||||
:in-profile="inProfile"
|
||||
:profile-user-id="timelineRef.argument"
|
||||
:virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.has(status.id)"
|
||||
collapsable
|
||||
role="listitem"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="!hideEmpty && count === 0">
|
||||
|
|
|
|||
|
|
@ -457,10 +457,6 @@ export const INSTANCE_DEFAULT_CONFIG_DEFINITIONS = {
|
|||
description: 'Hide user stats (followers etc)',
|
||||
default: false,
|
||||
},
|
||||
virtualScrolling: {
|
||||
description: 'Timeline virtual scrolling',
|
||||
default: true,
|
||||
},
|
||||
sensitiveByDefault: {
|
||||
description: 'Assume attachments are NSFW by default',
|
||||
default: false,
|
||||
|
|
|
|||
38
yarn.lock
38
yarn.lock
|
|
@ -1499,11 +1499,6 @@ chai@^6.2.2:
|
|||
resolved "https://registry.yarnpkg.com/chai/-/chai-6.2.2.tgz#ae41b52c9aca87734505362717f3255facda360e"
|
||||
integrity sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==
|
||||
|
||||
chalk@6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-6.0.0.tgz#a3bae843bc8454f41ef66ef3fd584dcf34dd805d"
|
||||
integrity sha512-2uNTXIuTTxk7ciZgAU1BQcgnchcG0xXnrs6jzkQfj9SsRa9M2s5zE8WT96hS6KmG4MzWHSrvH43DF1m4XRkrFg==
|
||||
|
||||
chokidar@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-5.0.0.tgz#949c126a9238a80792be9a0265934f098af369a5"
|
||||
|
|
@ -1608,15 +1603,6 @@ cropperjs@2.2.0:
|
|||
"@cropper/elements" "^2.2.0"
|
||||
"@cropper/utils" "^2.2.0"
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
||||
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
|
||||
dependencies:
|
||||
path-key "^3.1.0"
|
||||
shebang-command "^2.0.0"
|
||||
which "^2.0.1"
|
||||
|
||||
crossws@^0.4.12:
|
||||
version "0.4.12"
|
||||
resolved "https://registry.yarnpkg.com/crossws/-/crossws-0.4.12.tgz#3be6300b1e5a06db08e890e72eb1c7f6c809c9af"
|
||||
|
|
@ -2617,11 +2603,6 @@ path-exists@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
|
||||
integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
|
||||
|
||||
path-key@^3.1.0:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
|
||||
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
|
||||
|
||||
path-scurry@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.2.tgz#6be0d0ee02a10d9e0de7a98bae65e182c9061f85"
|
||||
|
|
@ -3083,18 +3064,6 @@ setprototypeof@~1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
||||
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
|
||||
|
||||
shebang-command@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
|
||||
integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
|
||||
dependencies:
|
||||
shebang-regex "^3.0.0"
|
||||
|
||||
shebang-regex@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
|
||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
||||
|
||||
siginfo@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30"
|
||||
|
|
@ -3603,13 +3572,6 @@ which@^1.3.1:
|
|||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
which@^2.0.1:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
||||
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
why-is-node-running@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue