Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
32 KB
Referenced Files
None
Subscribers
None
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 3a4239cb2f..a9a3c8fc23 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -1,367 +1,349 @@
import { debounce, throttle } from 'lodash-es'
import { storeToRefs } from 'pinia'
import {
computed,
inject,
nextTick,
onMounted,
onUnmounted,
ref,
toRefs,
useId,
useTemplateRef,
watch,
} from 'vue'
import { useI18n } from 'vue-i18n'
import Conversation from 'src/components/conversation/conversation.vue'
import QuickFilterSettings from 'src/components/quick_filter_settings/quick_filter_settings.vue'
import QuickViewSettings from 'src/components/quick_view_settings/quick_view_settings.vue'
import ScrollTopButton from 'src/components/scroll_top_button/scroll_top_button.vue'
import TimelineMenu from 'src/components/timeline_menu/timeline_menu.vue'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useTimelinesStore } from 'src/stores/timelines.js'
import { useDocumentFocus } from 'src/composables/useDocumentFocus.js'
import { useInterfaceSizes } from 'src/composables/useInterfaceSizes.js'
import { useVirtualScrolling } from 'src/composables/useVirtualScrolling.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faArrowUp,
faCheck,
faCircleNotch,
faCirclePlus,
faCog,
faMinus,
} from '@fortawesome/free-solid-svg-icons'
library.add(faCircleNotch, faCog, faMinus, faArrowUp, faCirclePlus, faCheck)
const Timeline = {
props: {
timelineRef: Object,
footerSlipgate: Object, // reference to an element where we should put our footer
embedded: Boolean,
skipPinned: Boolean,
hideEmpty: Boolean,
},
components: {
ScrollTopButton,
Conversation,
TimelineMenu,
QuickFilterSettings,
QuickViewSettings,
},
setup(props, ctx) {
const { t } = useI18n()
const { mergedConfig } = storeToRefs(useMergedConfigStore())
const { focused } = useDocumentFocus()
const unfocused = computed(() => !focused.value)
// Timeline
const { timelineRef } = toRefs(props)
const timeline = computed(() => useTimelinesStore()[timelineRef.value.name])
const { skipPinned } = toRefs(props)
const filteredVisibleStatuses = computed(() => {
return timeline.value.order
.filter((id) => timeline.value.visibleStatusIds.has(id))
.map((id) => useStatusesStore().allStatuses.get(id))
.filter(({ pinned }) => (skipPinned.value ? !pinned : true))
})
// Scroll position
const scroller = inject('bodyScrollPosition')
const {
hasReachedTop,
shouldLoadBottom,
top: scrollY,
scrollToPriority,
} = scroller
// Pagination
const paginationEnabled = computed(
() => mergedConfig.value.timelinePagination,
)
const page = ref(0)
const pageLoading = ref(false)
- const PAGE_SIZE = 20
-
- const pageStart = computed(() => page.value * PAGE_SIZE)
- const pageEnd = computed(() => pageStart.value + PAGE_SIZE)
- const currentSlice = computed(() => {
- if (paginationEnabled.value) {
- return filteredVisibleStatuses.value.slice(
- pageStart.value,
- pageEnd.value,
- )
- } else {
- return filteredVisibleStatuses.value
- }
- })
- const nextSlice = computed(() => {
+ const PAGE_SIZE = 20
+ const getSlice = (page) => {
if (paginationEnabled.value) {
+ const pageStart = page * PAGE_SIZE
+ const pageEnd = pageStart + PAGE_SIZE
+
return filteredVisibleStatuses.value.slice(
- pageStart.value + PAGE_SIZE,
- pageEnd.value + PAGE_SIZE,
+ pageStart,
+ pageEnd,
)
} else {
return filteredVisibleStatuses.value
}
- })
- const nextSliceReady = computed(() => {
- if (nextSlice.value.length >= PAGE_SIZE) {
- return true
- } else {
- return nextSlice.value.find(
- ({ id }) => timeline.value.minId,
- )
- }
- })
+ }
+
+ const currentSlice = computed(() => getSlice(page.value))
+ const nextSlice = computed(() => getSlice(page.value + 1))
+ const nextSliceReady = computed(() => nextSlice.value.length >= PAGE_SIZE)
const isFirstPage = computed(() => page.value <= 0)
- const isLastPage = computed(() => {
- const bottomedOut = timeline.value.fetcher.bottomedOut
- const lastId = timeline.value.minId
- const lastIdInPage = currentSlice.value.find(
- ({ id }) => timeline.value.minId,
- )
- return bottomedOut && lastId && lastIdInPage
- })
+ const isLastPage = computed(() => timeline.value.fetcher.bottomedOut && nextSlice.value.length === 0)
const nextPage = async () => {
if (isLastPage.value) return
if (!nextSliceReady.value) {
pageLoading.value = true
- await fetchOlderStatuses()
+ // Until either next slice is full or fetcher bottomed out
+ // (leaving less than 20 entries in next slice)
+ do {
+ await timeline.value.fetcher.fetchOlder()
+ } while (!nextSliceReady.value && !timeline.value.fetcher.bottomedOut)
pageLoading.value = false
}
page.value++
scrollToPriority({ top: 0, behavior: 'smooth' })
}
const previousPage = () => {
if (isFirstPage.value) return
page.value--
scrollToPriority({ top: 0, behavior: 'smooth' })
}
watch(timeline, () => {
page.value = 0
})
// Virtual scrolling
const { fontSize, navbarSize } = useInterfaceSizes()
// Placeholder heights.
const mutedStatusHeight = computed(() => fontSize.value * 1.5)
const normalStatusHeight = computed(() => fontSize.value * 10)
const getPlaceholderHeight = (id) =>
currentSlice.value.find((item) => item.id === id)?.muted
? mutedStatusHeight
: normalStatusHeight
const body = useTemplateRef('timeline')
const offset = computed(() => {
if (embedded.value) {
// The fontsize after navbar is the little gap between navbar and content
return navbarSize.value + fontSize.value
} else {
return 0
}
})
const { heightChart, changeSuspendState, updateVirtualHeight } =
useVirtualScrolling({
name: 'Timeline',
enabled: ref(true),
list: currentSlice,
body,
offset,
scrollPositionInstance: scroller,
scrollCompensation: true,
getPlaceholderHeight,
})
// Counter
const count = computed(() => timeline.value.order.length)
const newStatusCount = computed(() => timeline.value.newStatusCount)
const showLoadButton = computed(
() => timeline.value.newStatusCount > 0 || timeline.value.reloadNeeded,
)
// Showing new
const paused = ref(false)
const unfocusedPause = computed(
() => unfocused.value && mergedConfig.value.pauseOnUnfocused,
)
const showNewAutomatically = computed(() => mergedConfig.value.streaming)
watch(newStatusCount, (count) => {
if (!showNewAutomatically.value) return
if (count <= 0) return
// only 'stream' them when you're scrolled to the top
if (
hasReachedTop.value &&
!paused.value &&
!unfocusedPause.value &&
page.value === 0
) {
showNewStatuses(true)
} else {
paused.value = true
}
})
const showNewStatuses = async (fast = false) => {
if (timeline.value.reloadNeeded) {
useTimelinesStore().clearTimeline(timelineRef.value.name)
fetchOlderStatuses()
} else {
blockClicksTemporarily()
useTimelinesStore().showNewStatuses(timelineRef.value.name)
}
page.value = 0
paused.value = false
await nextTick()
scroller.scrollToPriority({
top: 0,
behavior: fast ? 'instant' : 'smooth',
})
}
const fetchOlderStatuses = throttle(async () => {
await timeline.value.fetcher.fetchOlder()
}, 1000)
// Timeline change
const timelineChange = (newTimeline, oldTimeline) => {
const sameName = newTimeline?.name === oldTimeline?.name
const sameArgument = newTimeline?.argument === oldTimeline?.argument
if (sameName && sameArgument) return
if (oldTimeline) {
useTimelinesStore().deactivate(oldTimeline.name)
}
if (newTimeline) {
useTimelinesStore().activate(newTimeline.name, newTimeline.argument)
}
}
watch(timelineRef, timelineChange, { immediate: true })
onUnmounted(() => {
timelineChange(null, timelineRef.value)
})
// Misclick prevention
const blockingClicks = ref(false)
const stopBlockingClicks = debounce(() => {
blockingClicks.value = false
}, 1000)
const blockClicksTemporarily = () => {
if (!blockingClicks.value) {
blockingClicks.value = true
}
stopBlockingClicks()
}
// Shortcuts
const handleShortKey = (e) => {
// Ignore when input fields are focused
if (['textarea', 'input'].includes(e.target.tagName.toLowerCase())) return
if (e.key === '.') showNewStatuses()
}
onMounted(() => {
window.addEventListener('keydown', handleShortKey)
})
onUnmounted(() => {
window.removeEventListener('keydown', handleShortKey)
})
const infiniteLoadWatcher = debounce(([value]) => {
if (!value) return
if (paginationEnabled.value) return
fetchOlderStatuses()
}, 100)
// Scroll
watch([shouldLoadBottom, scrollY], infiniteLoadWatcher)
onUnmounted(() => {
infiniteLoadWatcher.cancel()
})
// Misc UI things
const classes = computed(() => {
let rootClasses = !embedded.value
? ['panel', 'panel-default']
: ['-embedded']
if (blockingClicks.value)
rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention'])
return {
root: rootClasses,
header: ['timeline-heading'].concat(
!embedded.value ? ['panel-heading', '-sticky'] : ['panel-body'],
),
body: ['timeline-body'].concat(
!embedded.value ? ['panel-body'] : ['panel-body'],
),
footer: ['timeline-footer'].concat(
!embedded.value ? ['panel-footer'] : ['panel-body'],
),
}
})
const loadButtonString = computed(() => {
if (timeline.value.reloadNeeded) {
return t('timeline.reload')
} else {
return `${t('timeline.show_new')} (${newStatusCount.value})`
}
})
const mobileLoadButtonString = computed(() => {
if (timeline.value.reloadNeeded) {
return '+'
} else {
return newStatusCount.value > 99 ? '∞' : newStatusCount.value
}
})
const { layoutType } = storeToRefs(useInterfaceStore())
const mobileLayout = computed(() => layoutType.value === 'mobile')
const { footerSlipgate, embedded, hideEmpty } = toRefs(props)
const uniqueId = useId()
return {
timelineRef,
timeline,
paginationEnabled,
page,
pageLoading,
nextPage,
previousPage,
isLastPage,
isFirstPage,
heightChart,
updateVirtualHeight,
changeSuspendState,
count,
showLoadButton,
showNewStatuses,
fetchOlderStatuses,
classes,
loadButtonString,
mobileLoadButtonString,
mobileLayout,
footerSlipgate,
embedded,
hideEmpty,
uniqueId,
}
},
}
export default Timeline
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index bb539dedf5..a25f733cd6 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -1,223 +1,223 @@
<template>
<!-- there is a brief moment during logout when old timeline gets forcibly deactivated -->
<div
v-if="timeline.fetcher"
:class="['Timeline', classes.root]"
>
<div
v-if="!embedded"
:class="classes.header"
>
<TimelineMenu
v-if="!embedded"
:timeline-name="timelineRef.name"
:id="'timeline-header-' + uniqueId"
/>
<div
v-if="timeline.fetcher.loadingNewer && !showLoadButton"
class="loadingIndicator"
>
<FAIcon
fixed-width
icon="circle-notch"
spin
/>
</div>
<ScrollTopButton />
<template v-if="mobileLayout">
<div
v-if="showLoadButton"
class="rightside-button"
>
<button
class="button-unstyled loadmore-button"
:title="loadButtonString"
@click.prevent="showNewStatuses(false)"
>
<FAIcon
fixed-width
icon="circle-plus"
/>
<div class="badge -counter">
{{ mobileLoadButtonString }}
</div>
</button>
</div>
<div
v-else-if="!timeline.fetcher.loadingNewer"
class="loadmore-text faint veryfaint rightside-icon"
:title="$t('timeline.up_to_date')"
:aria-disabled="true"
@click.prevent
>
<FAIcon
fixed-width
icon="check"
/>
</div>
</template>
<template v-else>
<button
v-if="showLoadButton"
class="button-default loadmore-button"
@click.prevent="showNewStatuses(false)"
>
{{ loadButtonString }}
</button>
<div
v-else
class="loadmore-text faint"
@click.prevent
>
{{ $t('timeline.up_to_date') }}
</div>
</template>
<QuickFilterSettings
v-if="!mobileLayout"
class="rightside-button"
/>
<QuickViewSettings
class="rightside-button"
/>
</div>
<div
v-if="paginationEnabled && page > 0"
class="pagination panel-subheading"
>
<button
class="panel-body button-unstyled"
@click.left.prevent="previousPage"
>
<div class="new-status-notification text-center">
{{ $t('timeline.previous_page') }}
</div>
</button>
</div>
<div :class="classes.body">
<div
class="timeline"
ref="timeline"
role="feed"
:aria-labelledby="embedded ? undefined : ('timeline-header-'+ uniqueId)"
>
<template
v-for="element in heightChart"
:key="element.id"
>
<Conversation
v-if="element.type === 'item'"
class="timeline-conversation"
:status-id="element.id"
:scroll-compensation="element.visible"
:top="element.top"
:data-vs-height="element.height"
:data-vs-top="element.top"
:data-vs-id="element.id"
:data-vs-visible="element.visible"
@suspendable-state-change="changeSuspendState"
@height-change="updateVirtualHeight"
/>
<div
v-if="element.type === 'spacer'"
class="virtual-spacer"
aria-hidden="true"
:style="{ height: element.height + 'px' }"
/>
</template>
</div>
<template v-if="!hideEmpty && count === 0">
<div
v-if="timeline.fetcher.loadingNewer || timeline.fetcher.loadingOlder"
class="timeline-placeholder"
>
<FAIcon
icon="circle-notch"
spin
size="4x"
/>
</div>
<div
v-else
class="timeline-placeholder faint"
>
{{ $t('timeline.no_statuses') }}
</div>
</template>
</div>
<div v-if="!embedded || footerSlipgate" :class="classes.footer">
<teleport
:to="footerSlipgate"
:disabled="!embedded || !footerSlipgate"
>
<div
v-if="paginationEnabled"
class="pagination"
>
+ <button
+ v-if="!isLastPage && !pageLoading"
+ class="button-unstyled"
+ @click.left.prevent="nextPage"
+ >
+ <div class="new-status-notification text-center">
+ {{ $t('timeline.next_page') }}
+ </div>
+ </button>
<div
- v-if="pageLoading"
+ v-else-if="pageLoading"
class="new-status-notification text-center"
>
<FAIcon
icon="circle-notch"
spin
size="lg"
/>
</div>
- <button
- v-else-if="!isLastPage"
- class="button-unstyled"
- @click.left.prevent="nextPage"
- >
- <div class="new-status-notification text-center">
- {{ $t('timeline.next_page') }}
- </div>
- </button>
<div
- v-else
+ v-else-if="!embedded || footerSlipgate"
class="new-status-notification text-center faint"
>
{{ $t('timeline.no_more_statuses') }}
</div>
</div>
<div
v-else
class="infinite-scroll"
>
- <div
- v-if="timeline.fetcher.bottomedOut"
- class="new-status-notification text-center faint"
+ <button
+ v-if="!timeline.fetcher.bottomedOut && !timeline.fetcher.loadingOlder"
+ class="button-unstyled"
+ @click.prevent="fetchOlderStatuses()"
>
- {{ $t('timeline.no_more_statuses') }}
- </div>
+ <div class="new-status-notification text-center">
+ {{ $t('timeline.load_older') }}
+ </div>
+ </button>
<div
v-else-if="timeline.fetcher.loadingOlder"
class="new-status-notification text-center"
>
<FAIcon
icon="circle-notch"
spin
size="lg"
/>
</div>
- <button
- v-else-if="timeline.minId !== ''"
- class="button-unstyled"
- @click.prevent="fetchOlderStatuses()"
+ <div
+ v-else-if="!embedded || footerSlipgate"
+ class="new-status-notification text-center faint"
>
- <div class="new-status-notification text-center">
- {{ $t('timeline.load_older') }}
- </div>
- </button>
+ {{ $t('timeline.no_more_statuses') }}
+ </div>
</div>
</teleport>
<!-- spacer to avoid having empty shrug -->
<span v-if="embedded && footerSlipgate" />
</div>
</div>
</template>
<script src="./timeline.js"></script>
<style src="./timeline.scss" lang="scss"> </style>
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 17d2f1f017..1f06cc582e 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -1,130 +1,131 @@
<template>
<div
v-if="user"
class="user-profile panel panel-default"
>
<div class="panel-body card-wrapper">
<UserCard
:user-id="userId"
:switcher="true"
:compact="compactProfiles"
avatar-action="zoom"
:has-note-editor="true"
/>
</div>
<tab-switcher
v-if="userId"
:active-tab="tab"
:render-only-focused="true"
:on-switch="onTabSwitch"
>
<div
key="statuses"
class="statuses"
:label="$t('user_card.statuses')"
:title="$t('user_profile.timeline_title')"
>
<Timeline
key="statuses"
:timeline-ref="{ name: 'userPinned', argument: userId }"
embedded
in-profile
hide-empty
/>
<Timeline
+ key="statuses"
:timeline-ref="{ name: 'user', argument: userId }"
embedded
in-profile
skip-pinned
:footer-slipgate="footerRef"
/>
</div>
<div
v-if="followsTabVisible && user"
key="followees"
class="panel-body"
:label="$t('user_card.followees')"
:disabled="!user.friends_count"
>
<List
:fetch-function="fetchUsers('Friends')"
:external-items="friends"
>
<template #item="{item}">
<FollowCard :user="item" />
</template>
</List>
</div>
<div
v-if="followersTabVisible && user"
key="followers"
class="panel-body"
:label="$t('user_card.followers')"
:disabled="!user.followers_count"
>
<List
:fetch-function="fetchUsers('Followers')"
:external-items="followers"
>
<template #item="{item}">
<FollowCard
:user="item"
:no-follows-you="isUs"
/>
</template>
</List>
</div>
<Timeline
key="media"
:label="$t('user_card.media')"
:title="$t('user_card.media')"
:timeline-ref="{ name: 'media', argument: userId }"
embedded
in-profile
:footer-slipgate="footerRef"
/>
<Timeline
v-if="favoritesTabVisible"
key="favorites"
:label="$t('user_card.favorites')"
:title="$t('user_card.favorites')"
:timeline-ref="{ name: 'favorites', argument: userId }"
:argument="isUs ? undefined : userId"
embedded
in-profile
:footer-slipgate="footerRef"
/>
</tab-switcher>
<div
:ref="setFooterRef"
class="panel-footer"
/>
</div>
<div
v-else
class="panel user-profile-placeholder"
>
<div class="panel-heading">
<h1 class="title">
{{ $t('settings.profile_tab') }}
</h1>
</div>
<div class="panel-body">
<div
v-if="error"
class="alert error"
>
<span class="error-message">{{ error }}</span>
</div>
<FAIcon
v-else
spin
icon="circle-notch"
/>
</div>
</div>
</template>
<script src="./user_profile.js"></script>
<style src="./user_profile.scss" lang="scss"></style>
diff --git a/src/panel.scss b/src/panel.scss
index 5ff8844ebb..aabbbb575f 100644
--- a/src/panel.scss
+++ b/src/panel.scss
@@ -1,217 +1,224 @@
/* stylelint-disable no-descending-specificity */
.panel {
--__panel-background: var(--background);
--__panel-backdrop-filter: var(--backdrop-filter);
--_shadow: var(--shadow);
.tab-switcher .tabs {
background: var(--__panel-background);
backdrop-filter: var(--__panel-backdrop-filter);
}
position: relative;
display: flex;
flex-direction: column;
.panel-heading {
background-color: inherit;
}
&::after,
& {
border-radius: var(--roundness);
}
&::after {
content: "";
position: absolute;
inset: 0;
z-index: 5;
box-shadow: var(--_shadow);
pointer-events: none;
}
}
.panel-body {
padding: var(--panel-body-padding, 0);
background: var(--background);
backdrop-filter: var(--__panel-backdrop-filter);
.tab-switcher .tabs {
background: none;
backdrop-filter: none;
}
&:empty::before {
content: "¯\\_(ツ)_/¯"; // Could use words but it'd require translations
display: block;
padding: 1em;
text-align: center;
}
> p {
line-height: 1.3;
padding: 1em;
margin: 0;
}
}
.panel-heading,
.panel-subheading,
.panel-footer {
--panel-heading-height-padding: calc(var(--panel-header-height) * 0.2);
--__panel-heading-gap: calc(var(--panel-header-height) * 0.1565);
--__panel-heading-height: var(--panel-header-height);
--__panel-heading-height-inner: calc(var(--__panel-heading-height) - 2 * var(--panel-heading-height-padding, 0));
box-sizing: border-box;
background-color: var(--__panel-background);
padding: var(--panel-heading-height-padding);
height: var(--__panel-heading-height);
line-height: var(--__panel-heading-height-inner);
&.-flexible-height {
--__panel-heading-height: auto;
&::after,
&::before {
display: none;
}
}
}
.panel-heading,
.panel-footer {
font-size: calc(var(--panelHeaderSize) / 3.2);
backdrop-filter: var(--__panel-backdrop-filter);
position: relative;
background-size: cover;
z-index: 4;
&.-stub {
&,
&::after {
border-radius: var(--roundness);
}
}
&.-sticky {
position: sticky;
top: var(--navbar-height);
}
}
// TODO Should refactor panels into separate component and utilize slots
.panel-heading {
display: grid;
grid-auto-flow: column;
grid-template-columns: minmax(auto, 1fr);
grid-auto-columns: auto;
column-gap: var(--__panel-heading-gap);
border-radius: var(--roundness) var(--roundness) 0 0;
border-width: 0 0 1px;
align-items: start;
background-image:
linear-gradient(to bottom, var(--background), var(--background)),
linear-gradient(to bottom, var(--__panel-background), var(--__panel-background));
overflow: clip;
overflow-clip-margin: 1em;
text-overflow: ellipsis;
--_shadow: var(--shadow);
.title {
font-size: 1.3em;
font-weight: normal;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0;
}
.rightside-button {
align-self: stretch;
text-align: center;
width: var(--__panel-heading-height);
height: var(--__panel-heading-height);
margin: calc(-1 * var(--panel-heading-height-padding)) 0;
margin-right: calc(-1 * var(--__panel-heading-gap));
> button {
box-sizing: border-box;
padding: calc(1 * var(--panel-heading-height-padding)) 0;
height: 100%;
width: 100%;
text-align: center;
svg {
font-size: 1.2em;
}
}
}
.rightside-icon {
align-self: stretch;
text-align: center;
width: var(--__panel-heading-height);
margin-right: calc(-1 * var(--__panel-heading-gap));
svg {
font-size: 1.2em;
}
}
.alert {
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
}
&::after,
&::before {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
}
&::after {
background-color: var(--background);
z-index: -2;
border-radius: var(--roundness) var(--roundness) 0 0;
box-shadow: var(--_shadow);
}
&:not(.-flexible-height) {
> .button-default {
flex-shrink: 0;
}
> .button-default,
> .alert {
height: var(--__panel-heading-height-inner);
min-height: 0;
box-sizing: border-box;
margin: 0;
min-width: 1px;
padding-top: 0;
padding-bottom: 0;
align-self: stretch;
}
> .alert {
line-height: calc(var(--__panel-heading-height-inner) - 2px);
}
}
}
+.panel-subheading {
+ border-radius: 0;
+ border-width: 0 0 1px;
+ border-style: solid;
+ border-color: var(--border);
+}
+
.panel-footer {
border-top-left-radius: 0;
border-top-right-radius: 0;
border-width: 1px 0 0;
border-style: solid;
border-color: var(--border);
}
/* stylelint-enable no-descending-specificity */
diff --git a/src/stores/fetchers/timeline_fetcher.js b/src/stores/fetchers/timeline_fetcher.js
index 7fae0800da..52742a30bb 100644
--- a/src/stores/fetchers/timeline_fetcher.js
+++ b/src/stores/fetchers/timeline_fetcher.js
@@ -1,148 +1,151 @@
import { ref } from 'vue'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { ARGUMENT_MAP, useTimelinesStore } from 'src/stores/timelines.js'
import { useUsersStore } from 'src/stores/users.js'
import { fetchTimeline } from 'src/api/timelines.js'
import { promiseInterval } from 'src/services/promise_interval/promise_interval.js'
const REPLY_VISIBILITY_TIMELINES = new Set([
'friends',
'public',
'publicAndExternal',
'bubble',
])
const timelineFetcher = (timeline, argument, credentials) => {
const loadingNewer = ref(false)
const loadingOlder = ref(false)
const bottomedOut = ref(false)
const interval = ref(null)
const fetchAndUpdate = ({ older = false, showImmediately = false } = {}) => {
if (older) {
loadingOlder.value = true
} else {
loadingNewer.value = true
}
const { hideMutedPosts, replyVisibility } =
useMergedConfigStore().mergedConfig
const loggedIn = useUsersStore().loggedIn
const args = { timeline: timeline.name, credentials }
const mainArg = ARGUMENT_MAP[timeline.name]
if (mainArg) args[mainArg] = argument
if (older) {
// When minId = 0 we need to fetch without maxId param
args.maxId = timeline.minId || null
} else {
args.sinceId = timeline.maxId || null
}
args.withMuted = !hideMutedPosts
if (loggedIn && REPLY_VISIBILITY_TIMELINES.has(timeline.name)) {
args.replyVisibility = replyVisibility
}
const numStatusesBeforeFetch = timeline.statusIds.size
if (older && bottomedOut.value) {
loadingOlder.value = false
return
}
return fetchTimeline(args)
.then(({ data, pagination, timestamp }) => {
// No statuses for timeline, ever.
if (timeline.order.length === 0 && data.length === 0) {
bottomedOut.value = true
}
if (!older && data.length >= 20 && numStatusesBeforeFetch > 0) {
useTimelinesStore().requireReload(timeline.name)
}
- if (older && data.length === 0) {
+ if (older && data.length < 20) {
bottomedOut.value = true
}
const processed = useStatusesStore().addNewStatuses({
statuses: data,
timestamp,
})
const statuses = processed.map(({ id }) => id)
const repeats = processed
.filter(({ retweeted_status }) => Boolean(retweeted_status))
.map(({ id, retweeted_status: { id: repeatedId } }) => [
id,
repeatedId,
])
useTimelinesStore().addStatusesToTimeline(timeline.name, argument, {
statuses,
repeats,
showImmediately,
older,
pagination,
})
return { statuses, pagination }
})
.catch((error) => {
if (error.statusCode === 403 && timeline.name === 'favorites') {
useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable = false
return
}
console.error('Timeline Error', error)
useInterfaceStore().pushGlobalNotice({
level: 'error',
messageKey: 'timeline.error',
messageArgs: [error.message],
timeout: 5000,
})
})
.finally(() => {
if (older) {
loadingOlder.value = false
} else {
loadingNewer.value = false
}
})
}
- const startFetching = () => {
+ const startFetching = async () => {
if (interval.value) throw new Error('Interval already exists!')
- fetchAndUpdate({
+ await fetchAndUpdate({
showImmediately: timeline.visibleStatusIds.size === 0,
})
+ if (timeline.order.length < 20) {
+ bottomedOut.value = true
+ }
interval.value = promiseInterval(fetchAndUpdate, 10000)
}
const stopFetching = () => {
interval.value.stop()
interval.value = null
}
return {
startFetching,
stopFetching,
fetchOlder: () => fetchAndUpdate({ showImmediately: true, older: true }),
loadingOlder,
loadingNewer,
bottomedOut,
resetBottomedOut: () => {
bottomedOut.value = false
},
}
}
export default timelineFetcher

File Metadata

Mime Type
text/x-diff
Expires
Fri, Oct 30, 6:41 AM (21 h, 24 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1799708
Default Alt Text
(32 KB)

Event Timeline