Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85826359
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Authored By
Unknown
Size
32 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Oct 30, 6:44 AM (20 h, 22 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1799708
Default Alt Text
(32 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline