diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index d6ab361d7..dfb27836f 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -1,4 +1,3 @@ -import { get } from 'lodash-es' import { storeToRefs } from 'pinia' import { computed, @@ -20,8 +19,6 @@ import ThreadTree from 'src/components/thread_tree/thread_tree.vue' import { useInterfaceStore } from 'src/stores/interface.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' -import { useStatusesStore } from 'src/stores/statuses.js' -import { useStreamingStore } from 'src/stores/streaming.js' import { useConversation } from 'src/composables/useConversation.js' import { useInterfaceSizes } from 'src/composables/useInterfaceSizes.js' @@ -29,8 +26,6 @@ import { useScrollPosition } from 'src/composables/useScrollPosition.js' import { useTreeConversationTopology } from 'src/composables/useTreeConversationTopology.js' import { useVirtualScrolling } from 'src/composables/useVirtualScrolling.js' -import { WSConnectionStatus } from 'src/api/websocket.js' - import { library } from '@fortawesome/fontawesome-svg-core' import { faAngleDoubleDown, @@ -72,17 +67,6 @@ export default { }, emits: ['heightChange', 'suspendableStateChange'], setup(props, { emit }) { - // # Helpers - const getStatusObject = (id) => useStatusesStore().allStatuses.get(id) - const getConversationId = (statusId) => { - const status = getStatusObject(statusId) - return get( - status, - 'retweeted_status.statusnet_conversation_id', - get(status, 'statusnet_conversation_id'), - ) - } - const scroller = useScrollPosition() const tryScrollTo = async (id) => { if (!id) { @@ -100,15 +84,30 @@ export default { const router = useRouter() + const onReset = () => { + resetThreadDisplay() + } + + // # Main things + const { + currentStatus, + mainStatus, + fullConversation, + replies, + getReplies, + fetchConversation, + loadError, + focusedId, + setFocused, + reset, + } = useConversation({ + statusId, + onReset, + }) + // # Main Configuration / global state const { mergedConfig } = storeToRefs(useMergedConfigStore()) - const { mastoUserSocketStatus } = storeToRefs(useStreamingStore()) const displayStyle = computed(() => mergedConfig.value.conversationDisplay) - const streamingEnabled = computed( - () => - mergedConfig.value.useStreamingApi && - mastoUserSocketStatus === WSConnectionStatus.JOINED, - ) const { layoutType } = storeToRefs(useInterfaceStore()) const mobileLayout = computed(() => layoutType.value === 'mobile') @@ -123,32 +122,11 @@ export default { provide('isPage', isPage) provide('expandable', true) - // # Focus - const focusedId = ref(statusId.value) const focused = computed(() => (isExpanded.value ? focusedId.value : null)) - const setFocused = (id) => { - if (!id) return - focusedId.value = id - - if (!streamingEnabled.value) { - useStatusesStore().fetchStatus(id) - } - - useStatusesStore().fetchFavsAndRepeats(id) - useStatusesStore().fetchEmojiReactions(id) - } provide('focused', focused) - - // # Main things - const { - currentStatus, - mainStatus, - conversation, - replies, - getReplies, - fetchConversation, - loadError, - } = useConversation(focusedId, isExpanded) + const conversation = computed(() => + !expanded.value ? [currentStatus.value] : fullConversation.value, + ) watch( expanded, @@ -156,32 +134,13 @@ export default { if (value) { await fetchConversation() } else { - resetDisplayState() + reset() } if (isPage.value) return }, { flush: 'post' }, ) - const resetDisplayState = () => { - setFocused(statusId.value) - resetThreadDisplay() - } - watch(statusId, (newVal, oldVal) => { - const newConversationId = getConversationId(newVal) - const oldConversationId = getConversationId(oldVal) - if ( - newConversationId && - oldConversationId && - newConversationId === oldConversationId - ) { - setFocused(newVal) - } else { - resetDisplayState() - fetchConversation() - } - }) - // Component created if (isPage.value) { fetchConversation() @@ -192,10 +151,17 @@ export default { const lastStatus = computed( () => conversation.value[conversation.value.legnth - 1], ) - const getStatusClasses = (status, active) => ({ - '-first': status.id === firstStatus.value?.id, - '-last': status.id === lastStatus.value?.id, - }) + const getStatusClasses = (status, ancestor) => { + const result = { + '-first': status.id === firstStatus.value?.id, + '-last': status.id === lastStatus.value?.id, + } + if (ancestor) { + result['-ancestor'] = true + result['-fade'] = mergedConfig.value.conversationTreeFadeAncestors + } + return result + } const { fontSize } = useInterfaceSizes() @@ -228,6 +194,9 @@ export default { ? mutedStatusHeight : normalStatusHeight + const anchorIds = computed( + () => new Set([mainStatus.value?.id, currentStatus.value?.id]), + ) // # Linear style stuff const isLinearView = computed(() => displayStyle.value !== 'tree') const linearElement = useTemplateRef('linear') @@ -241,8 +210,7 @@ export default { body: linearElement, scrollPositionInstance: scroller, scrollCompensation: linearScrollCompensation, - anchorId: mainStatus.value?.id, - anchorRepeatId: currentStatus.value?.id, + anchorIds, getPlaceholderHeight, }) const changeSuspendStateLinearLocal = (e) => { @@ -277,6 +245,7 @@ export default { body: ancestorsElement, scrollPositionInstance: scroller, scrollCompensation: treeScrollCompensation, + collapseMode: 'height', getPlaceholderHeight, }) const changeSuspendStateAncestorsLocal = (e) => { @@ -307,9 +276,6 @@ export default { const shouldShowAncestors = computed( () => isExpanded.value && heightChartAncestors.value.length > 0, ) - const shouldFadeAncestors = computed( - () => mergedConfig.value.conversationTreeFadeAncestors, - ) // # Scrolling const diveIntoStatus = (id) => tryScrollTo(id) @@ -367,7 +333,6 @@ export default { treeViewIsSimple, shouldShowAllConversationButton, shouldShowAncestors, - shouldFadeAncestors, // # Scrolling diveToTopLevel, diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 1c0d459f3..4e10bdcd0 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -91,15 +91,14 @@ ref="ancestors" class="thread-ancestors" > -
+ -
+ diff --git a/src/components/status/status.js b/src/components/status/status.js index 277421c6e..ec685b0d2 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -592,7 +592,7 @@ const Status = { const [entry] = e this.$emit('heightChange', { id: this.status.id, - height: entry.contentRect.height, + height: entry.contentRect.height + 1, element: this.$el, }) }, @@ -600,13 +600,18 @@ const Status = { mounted() { if (this.$refs.root) { this.resizeObserver.observe(this.$refs.root) + this.updateVirtualHeight([ + { + contentRect: this.$refs.root.getBoundingClientRect(), + }, + ]) } }, unmounted() { this.resizeObserver.disconnect() }, watch: { - hideStatus: function (element) { + hideStatus: function () { if (this.$refs.root) { this.resizeObserver.observe(this.$refs.root) } else { diff --git a/src/components/status/status.vue b/src/components/status/status.vue index c0a1b0238..fa288145f 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -1,5 +1,5 @@ - + diff --git a/src/composables/useConversation.js b/src/composables/useConversation.js index 9bec1e160..f6ba3a4e3 100644 --- a/src/composables/useConversation.js +++ b/src/composables/useConversation.js @@ -1,15 +1,26 @@ import { get } from 'lodash-es' -import { computed, provide, ref } from 'vue' +import { storeToRefs } from 'pinia' +import { computed, provide, ref, watch } from 'vue' +import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useOAuthStore } from 'src/stores/oauth.js' import { useStatusesStore } from 'src/stores/statuses.js' +import { useStreamingStore } from 'src/stores/streaming.js' import { fetchConversation as apiFetchConversation, fetchStatus as apiFetchStatus, } from 'src/api/public.js' -export function useConversation(statusId, expanded) { +export function useConversation({ statusId, onReset }) { + const { mastoUserSocketStatus } = storeToRefs(useStreamingStore()) + const { mergedConfig } = storeToRefs(useMergedConfigStore()) + const streamingEnabled = computed( + () => + mergedConfig.value.useStreamingApi && + mastoUserSocketStatus === WSConnectionStatus.JOINED, + ) + const getStatusObject = (id) => useStatusesStore().allStatuses.get(id) const getConversationId = (statusId) => { const status = getStatusObject(statusId) @@ -19,6 +30,37 @@ export function useConversation(statusId, expanded) { get(status, 'statusnet_conversation_id'), ) } + // # Focus + const focusedId = ref(statusId.value) + const setFocused = (id) => { + if (!id) return + focusedId.value = id + + if (!streamingEnabled.value) { + useStatusesStore().fetchStatus(id) + } + + useStatusesStore().fetchFavsAndRepeats(id) + useStatusesStore().fetchEmojiReactions(id) + } + const reset = () => { + onReset?.() + setFocused(statusId.value) + } + watch(statusId, (newVal, oldVal) => { + const newConversationId = getConversationId(newVal) + const oldConversationId = getConversationId(oldVal) + if ( + newConversationId && + oldConversationId && + newConversationId === oldConversationId + ) { + setFocused(newVal) + } else { + resetDisplayState() + fetchConversation() + } + }) const loadError = ref(null) const currentStatus = computed(() => getStatusObject(statusId.value)) @@ -49,10 +91,6 @@ export function useConversation(statusId, expanded) { return [] } - if (!expanded.value) { - return [currentStatus.value] - } - const conversation = useStatusesStore().conversations.get( conversationId.value, ) @@ -119,10 +157,13 @@ export function useConversation(statusId, expanded) { return { currentStatus, mainStatus, - conversation, + fullConversation: conversation, + focusedId, + setFocused, replies, getReplies, fetchConversation, + reset, loadError, } } diff --git a/src/composables/useVirtualScrolling.js b/src/composables/useVirtualScrolling.js index 07e0cae8c..d466fc059 100644 --- a/src/composables/useVirtualScrolling.js +++ b/src/composables/useVirtualScrolling.js @@ -1,3 +1,4 @@ +import { last } from 'lodash-es' import { computed, nextTick, ref, toValue, watch } from 'vue' import { useWindowSize } from 'src/composables/useWindowSize.js' @@ -9,12 +10,18 @@ export function useVirtualScrolling({ body, // useScrollPosition composable, used to prevent dupicating instances scrollPositionInstance, - // ID of anchor element, used for scroll compensation. - // Omitting it makes last element the anchor - anchorId, - anchorRepeatId, + // buffer zone, the amount of placeholder heights to include + buffer, // whether to use scroll compensation when elements above anchor change + // set to 'positive' to only compensate for positive increase (useful when + // combined with infinite scroll) scrollCompensation, + // How to handle collapse/expansion (going from 0 elements to full and back) + // - false - don't do scroll compensation at all + // - 'height' - compensate scroll according to list's height + collapseMode, + // Anchor. Set of IDs of element relative to which do scroll compensation + anchorIds, // Placeholder height specification. Must be a function. // function will be called either: // - without arguments (for generic placeholder, i.e. buffer zone size) @@ -38,14 +45,13 @@ export function useVirtualScrolling({ // Map every height and suspendable state const chart = list.value.map((item) => { const { id } = item - const height = - (() => { - if (heights.value.has(id)) { - return heights.value.get(id) - } else { - return getPlaceholderHeight(id).value - } - })() + 1 //including border + const height = (() => { + if (heights.value.has(id)) { + return heights.value.get(id) + } else { + return getPlaceholderHeight(id).value + } + })() const suspendable = !unsuspendibleIds.value.has(id) return { id, height, suspendable, item } }) @@ -69,41 +75,41 @@ export function useVirtualScrolling({ if (newVal.length === 0 && oldVal.length === 0) return pauseWatchers() - // If we're not given an achor, treat last element as one - const getAnchoredEl = (list) => - toValue(anchorId) - ? list.find( - ({ id }) => - id === toValue(anchorId) || id === toValue(anchorRepeatId), - ) - : list[list.length - 1] - - const oldElement = getAnchoredEl(oldVal) - const newElement = getAnchoredEl(newVal) + const expansion = oldVal.length === 0 && newVal.length !== 0 + const collapse = oldVal.length !== 0 && newVal.length === 0 const diff = (() => { - if (oldElement && newElement) { - // Generic shifting - const oldOffset = toValue(anchorId) - ? oldElement.top - : oldElement.top + oldElement.height - const newOffset = toValue(anchorId) - ? newElement.top - : newElement.top + newElement.height - return newOffset - oldOffset - } else if (!oldElement && newElement) { - // Expansion - return newElement.top + newElement.height - } else if (oldElement && !newElement) { - // Collapsing - return 0 - oldElement.top - oldElement.height + if (expansion || collapse) { + if (toValue(collapseMode) === 'height') { + const oldBottomElement = last(oldVal) + const newBottomElement = last(newVal) + + if (expansion) { + return newBottomElement.top + newBottomElement.height + } else if (collapse) { + return 0 - oldBottomElement.top - oldBottomElement.height + } + } + return 0 + } else if (toValue(anchorIds) != null) { + const anchorOld = oldVal.find(({ id }) => toValue(anchorIds).has(id)) + const anchorNew = newVal.find(({ id }) => toValue(anchorIds).has(id)) + if (anchorOld == null) { + throw new Error('Anchor not found!') + } + + const disappeared = anchorOld != null && anchorNew == null + if (disappeared) { + throw new Error('Anchor disappeared!') + } + + return anchorNew.top - anchorOld.top } else { - throw new Error( - "Somehow both new and old elements are missing, this shouldn't happen", - ) + return 0 } })() + console.log(diff) if (diff !== 0) { // Scroll by amount offset changed to keep it in view topScrollBoundary.value += diff @@ -160,7 +166,9 @@ export function useVirtualScrolling({ // # Visiblity // Add buffer zone to boundary, equal to approx 3 items heights - const buffer = computed(() => getPlaceholderHeight().value * 3) + const bufferZone = computed( + () => getPlaceholderHeight().value * (toValue(buffer) ?? 3), + ) const heightChartGrouped = computed(() => { // Determine visibility state @@ -169,9 +177,9 @@ export function useVirtualScrolling({ const itemBottomBoundary = heightChartItem.top + heightChartItem.height // Include buffer zone - const finalTopScrollBoundary = topScrollBoundary.value - buffer.value + const finalTopScrollBoundary = topScrollBoundary.value - bufferZone.value const finalBottomScrollBoundary = - bottomScrollBoundary.value + buffer.value + bottomScrollBoundary.value + bufferZone.value // To be visible, item's bottom boundary shoud be below top scroll boundary) const isBelowTopBoundary = itemBottomBoundary > finalTopScrollBoundary