Merge branch 'users-statuses-pinia' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2026-08-24 19:47:49 +03:00
commit a6f19bb213
43 changed files with 208 additions and 131 deletions

View file

@ -0,0 +1 @@
If user avatars next to mentions are enabled it will show empty placeholder avatar next to label while user is being fetched, to avoid jumps

View file

@ -0,0 +1 @@
Fix follower remove API call

View file

@ -1 +1,3 @@
Added an indicator next to instance's name showing WebSocket connection status (if enabled)
Added an indicator next to instance's name showing WebSocket connection status (if enabled).
Timeline no longer show "loading" indicator at the bottom when fetching newer posts.
Added small indicator on top of timeline when new posts are being fetched

View file

@ -1,6 +1,5 @@
import { get, reduce } from 'lodash'
import { mapState as mapPiniaState } from 'pinia'
import { mapState } from 'vuex'
import { mapState } from 'pinia'
import ChatMessageList from 'src/components/chat_message_list/chat_message_list.vue'
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
@ -13,6 +12,7 @@ import { useInterfaceStore } from 'src/stores/interface.js'
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, fetchStatus } from 'src/api/public.js'
import { WSConnectionStatus } from 'src/api/websocket.js'
@ -93,6 +93,7 @@ const conversation = {
default: false,
},
},
emits: ['update:virtualHeight'],
data() {
return {
focused: null,
@ -101,6 +102,7 @@ const conversation = {
inlineDivePosition: null,
loadStatusError: null,
unsuspendibleIds: new Set(),
virtualHeight: 120,
}
},
created() {
@ -108,6 +110,9 @@ const conversation = {
this.fetchConversation()
}
},
mounted() {
this.updateVirtualHeight()
},
computed: {
status() {
return useStatusesStore().allStatuses.get(this.statusId)
@ -360,8 +365,8 @@ const conversation = {
return !!(this.expanded || this.isPage)
},
hiddenStyle() {
const height = this.status?.virtualHeight || '120px'
return this.virtualHidden ? { height } : {}
if (this.isExpanded) return {}
return { height: this.virtualHeight + 'px' }
},
threadDisplayStatus() {
return this.conversation.reduce((a, k) => {
@ -388,11 +393,11 @@ const conversation = {
maybeFocused() {
return this.isExpanded ? this.focused : null
},
...mapPiniaState(useMergedConfigStore, ['mergedConfig']),
...mapState({
mastoUserSocketStatus: (state) => state.api.mastoUserSocketStatus,
...mapState(useMergedConfigStore, ['mergedConfig']),
...mapState(useStreamingStore, {
mastoUserSocketStatus: (state) => state.state,
}),
...mapPiniaState(useInterfaceStore, {
...mapState(useInterfaceStore, {
mobileLayout: (store) => store.layoutType === 'mobile',
}),
},
@ -426,10 +431,7 @@ const conversation = {
}
},
virtualHidden() {
useStatusesStore().setVirtualHeight({
statusId: this.statusId,
height: `${this.$el.clientHeight}px`,
})
this.updateVirtualHeight()
},
},
methods: {
@ -618,6 +620,16 @@ const conversation = {
this.$router.push({ name: 'conversation', params: { id: data.id } })
}
},
updateVirtualHeight() {
this.$nextTick(() => {
this.virtualHeight = this.$refs.body.getBoundingClientRect().height
this.$emit('update:virtualHeight', {
id: this.status.id,
height: this.virtualHeight,
top: this.$el.clientTop,
})
})
},
},
}

View file

@ -40,6 +40,7 @@
<div
v-if="isPage && !status"
class="conversation-body"
ref="body"
:class="{ 'panel-body': isExpanded }"
>
<p v-if="!loadStatusError">
@ -56,6 +57,7 @@
<div
v-else
class="conversation-body"
ref="body"
:class="{ 'panel-body': isExpanded }"
>
<div
@ -116,6 +118,7 @@
@goto="setFocused"
@dive="() => diveIntoStatus(status.id)"
@suspendable-state-change="onStatusSuspendStateChange"
@height-change="updateVirtualHeight"
/>
<div
v-if="showOtherRepliesButtonBelowStatus && getReplies(status.id).length > 1"
@ -174,6 +177,7 @@
@goto="setFocused"
@dive="diveIntoStatus"
@suspendable-state-change="onStatusSuspendStateChange"
@height-change="updateVirtualHeight"
/>
</div>
<div
@ -200,6 +204,7 @@
@goto="setFocused"
@toggle-expanded="toggleExpanded"
@suspendable-state-change="onStatusSuspendStateChange"
@height-change="updateVirtualHeight"
/>
</article>
</div>

View file

@ -135,9 +135,9 @@ export default {
this.showConfirmLogout()
}
},
doLogout() {
async doLogout() {
await useUsersStore().logout()
this.$router.replace('/main/public')
useUsersStore().logout()
this.hideConfirmLogout()
},
onSearchBarToggled(hidden) {

View file

@ -1,9 +1,9 @@
import { mapActions, mapState as mapPiniaState } from 'pinia'
import { mapState } from 'vuex'
import { mapActions, mapState } from 'pinia'
import { useAuthFlowStore } from 'src/stores/auth_flow.js'
import { useInstanceStore } from 'src/stores/instance.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useUsersStore } from 'src/stores/users.js'
import { getLoginUrl, getTokenWithCredentials } from 'src/api/oauth.js'
@ -18,12 +18,10 @@ const LoginForm = {
error: false,
}),
computed: {
...mapState({
loggingIn: (state) => state.users.loggingIn,
}),
...mapPiniaState(useOAuthStore, ['clientId', 'clientSecret']),
...mapPiniaState(useInstanceStore, ['server', 'registrationOpen']),
...mapPiniaState(useAuthFlowStore, {
...mapState(useUsersStore, ['loggingIn']),
...mapState(useOAuthStore, ['clientId', 'clientSecret']),
...mapState(useInstanceStore, ['server', 'registrationOpen']),
...mapState(useAuthFlowStore, {
isTokenAuth: (store) => store.requiredToken,
isPasswordAuth: (store) => !store.requiredToken,
}),

View file

@ -29,7 +29,7 @@ const MentionLink = {
},
props: {
url: {
required: true,
required: false,
type: String,
},
content: {
@ -75,11 +75,11 @@ const MentionLink = {
},
computed: {
user() {
return this.url && useUsersStore().findUserByUrl(this.url)
return this.url ? useUsersStore().findUserByUrl(this.url) : null
},
isYou() {
// FIXME why user !== currentUser???
return this.user?.id === this.currentUser.id
if (!this.currentUser) return false
return this.user === this.currentUser
},
userName() {
return this.user && this.userNameFullUi.split('@')[0]

View file

@ -8,15 +8,20 @@
:href="url"
class="original"
target="_blank"
v-html="content"
/><!-- eslint-enable vue/no-v-html -->
><!-- eslint-enable vue/no-v-html -->
<UserAvatar
v-if="shouldShowAvatar"
class="mention-avatar"
:user-id="null"
/>
<span v-html="content" />
</a>
<UserPopover
v-else
:user-id="user.id"
:disabled="!shouldShowTooltip"
>
<span
v-if="user"
class="new"
:style="style"
:class="classnames"

View file

@ -145,9 +145,9 @@ const MobileNav = {
}
},
doLogout() {
this.$router.replace('/main/public')
useUsersStore().logout()
this.hideConfirmLogout()
this.$router.replace('/main/public')
},
markNotificationsAsSeen() {
useNotificationsStore().markNotificationsAsSeen()

View file

@ -1,5 +1,5 @@
import { mapState as mapPiniaState } from 'pinia'
import { mapState } from 'vuex'
import { mapState } from 'pinia'
import { mapState as mapVuexState } from 'vuex'
import BookmarkFoldersMenuContent from 'src/components/bookmark_folders_menu/bookmark_folders_menu_content.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
@ -111,29 +111,29 @@ const NavPanel = {
},
},
computed: {
...mapPiniaState(useAnnouncementsStore, {
...mapState(useAnnouncementsStore, {
unreadAnnouncementCount: 'unreadAnnouncementCount',
supportsAnnouncements: (store) => store.supportsAnnouncements,
}),
...mapPiniaState(useInstanceCapabilitiesStore, [
...mapState(useInstanceCapabilitiesStore, [
'pleromaChatMessagesAvailable',
'pleromaBookmarkFoldersAvailable',
'localBubble',
]),
...mapPiniaState(useInstanceStore, ['federating']),
...mapPiniaState(useInstanceStore, {
...mapState(useInstanceStore, ['federating']),
...mapState(useInstanceStore, {
privateMode: (store) => store.private,
}),
...mapPiniaState(useSyncConfigStore, {
...mapState(useSyncConfigStore, {
collapsed: (store) => store.prefsStorage.simple.collapseNav,
pinnedItems: (store) =>
new Set(store.prefsStorage.collections.pinnedNavItems),
}),
...mapPiniaState(useUsersStore, ['currentUser']),
...mapState({
...mapState(useUsersStore, ['currentUser']),
...mapVuexState({
followRequestCount: (state) => state.api.followRequests.length,
}),
...mapPiniaState(useChatsStore, ['unreadChatsCount']),
...mapState(useChatsStore, ['unreadChatsCount']),
timelinesItems() {
return filterNavigation(
Object.entries({ ...TIMELINES })

View file

@ -1,5 +1,5 @@
import { mapState as mapPiniaState } from 'pinia'
import { mapState } from 'vuex'
import { mapState } from 'pinia'
import { mapState as mapVuexState } from 'vuex'
import {
filterNavigation,
@ -59,26 +59,26 @@ const NavPanel = {
getters() {
return this.$store.getters
},
...mapPiniaState(useListsStore, {
...mapState(useListsStore, {
lists: getListEntries,
}),
...mapPiniaState(useAnnouncementsStore, {
...mapState(useAnnouncementsStore, {
supportsAnnouncements: (store) => store.supportsAnnouncements,
}),
...mapPiniaState(useBookmarkFoldersStore, {
...mapState(useBookmarkFoldersStore, {
bookmarks: getBookmarkFolderEntries,
}),
...mapPiniaState(useSyncConfigStore, {
...mapState(useSyncConfigStore, {
pinnedItems: (store) =>
new Set(store.prefsStorage.collections.pinnedNavItems),
}),
...mapPiniaState(useInstanceStore, ['privateMode', 'federating']),
...mapPiniaState(useInstanceCapabilitiesStore, [
...mapState(useInstanceStore, ['privateMode', 'federating']),
...mapState(useInstanceCapabilitiesStore, [
'pleromaChatMessagesAvailable',
'localBubble',
]),
...mapPiniaState(useUsersStore, ['currentUser']),
...mapState({
...mapState(useUsersStore, ['currentUser']),
...mapVuexState({
followRequestCount: (state) => state.api.followRequests.length,
}),
pinnedList() {

View file

@ -118,9 +118,6 @@ const Notification = {
useInstanceStore().restrictedNicknames,
)
},
getUser(notification) {
return this.$store.state.users.usersObject[notification.from_profile.id]
},
interacted() {
this.$emit('interacted')
},

View file

@ -16,10 +16,10 @@ const oac = {
clientSecret,
instance: useInstanceStore().server,
code: this.code,
}).then(({ data: result }) => {
}).then(async ({ data: result }) => {
oauthStore.setToken(result.access_token)
useUsersStore().loginUser(result.access_token)
await useUsersStore().loginUser(result.access_token)
this.$router.push({ name: 'friends' })
})
}

View file

@ -28,7 +28,7 @@ const QuickFilterSettings = {
path: 'replyVisibility',
value: visibility,
})
useStatusesStore().queueFlushAll()
useStatusesStore().requireReloadAll()
},
openTab(tab) {
useInterfaceStore().openSettingsModalTab(tab)

View file

@ -303,7 +303,7 @@
<!-- eslint-enable vue/no-v-html -->
</div>
<div
v-if="serverValidationErrors.length"
v-if="signUpErrors.length"
class="form-group"
>
<div class="alert error">

View file

@ -1,6 +1,6 @@
// eslint-disable-next-line no-unused
import { mapState as mapPiniaState } from 'pinia'
import { mapState } from 'pinia'
import { Fragment } from 'vue'
import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome'
@ -60,7 +60,7 @@ export default {
return this.$slots.default().findIndex(isWanted) === this.activeIndex
}
},
...mapPiniaState(useInterfaceStore, {
...mapState(useInterfaceStore, {
mobileLayout: (store) => store.layoutType === 'mobile',
}),
},

View file

@ -36,7 +36,7 @@ const ClutterTab = {
// Updating nested properties
watch: {
replyVisibility() {
useStatusesStore().queueFlushAll()
useStatusesStore().requireReloadAll()
},
},
}

View file

@ -266,7 +266,7 @@ const FilteringTab = {
// Updating nested properties
watch: {
replyVisibility() {
useStatusesStore().queueFlushAll()
useStatusesStore().requireReloadAll()
},
muteFiltersObject() {
this.muteFiltersDraftObject = cloneDeep(

View file

@ -105,7 +105,6 @@ const Status = {
isPreview: Boolean,
noHeading: Boolean,
inlineExpanded: Boolean,
showPinned: Boolean,
inProfile: Boolean,
inConversation: Boolean,
inQuote: Boolean,
@ -118,7 +117,13 @@ const Status = {
threadDisplayStatus: String,
},
emits: ['goto', 'dive', 'toggleExpanded', 'suspendableStateChange'],
emits: [
'goto',
'dive',
'toggleExpanded',
'suspendableStateChange',
'heightChange',
],
data() {
return {
replying: false,
@ -189,7 +194,7 @@ const Status = {
)
// User referenced in post might not be yet present in store
// since their data is not included in status data
// since their data is not included in status data, just the id
return user?.statusnet_profile_url
}
},
@ -370,7 +375,7 @@ const Status = {
},
replyToName() {
if (this.mainStatus.in_reply_to_screen_name) {
return this.status.in_reply_to_screen_name
return this.mainStatus.in_reply_to_screen_name
} else {
const user = useUsersStore().findUser(
this.mainStatus.in_reply_to_user_id,
@ -387,7 +392,7 @@ const Status = {
return uniqBy(combinedUsers, 'id')
},
tags() {
return this.status.tags
return [...this.status.tags]
.filter((tagObj) => Object.hasOwn(tagObj, 'name'))
.map((tagObj) => tagObj.name)
.join(' ')
@ -539,6 +544,7 @@ const Status = {
this.headTailLinks = headTailLinks
},
toggleThreadDisplay() {
// FIXME
this.controlledToggleThreadDisplay()
},
scrollIfFocused(focused) {
@ -557,8 +563,22 @@ const Status = {
}
}
},
onTransitionEnd() {
this.$nextTick(() => {
this.$emit('heightChange')
})
},
},
watch: {
status: {
deep: true,
handler() {
this.$emit('heightChange')
},
},
replying() {
this.$emit('heightChange')
},
focused: function (id) {
this.scrollIfFocused(id)
},
@ -584,6 +604,7 @@ const Status = {
},
isSuspendable: function (suspend) {
this.$emit('suspendableStateChange', { id: this.status.id, suspend })
this.$emit('heightChange')
},
},
}

View file

@ -169,7 +169,7 @@
<span class="heading-right">
<span
v-if="showPinned"
v-if="mainStatus.pinned"
class="pin"
>
<FAIcon
@ -454,7 +454,10 @@
</StatusPopover>
</div>
<transition name="fade">
<Transition
@after-leave="onTransitionEnd"
name="fade"
>
<div
v-if="shouldDisplayFavsAndRepeats"
class="favs-repeated-users"
@ -502,7 +505,7 @@
</div>
</div>
</div>
</transition>
</Transition>
<EmojiReactions
v-if="(mergedConfig.emojiReactionsOnTimeline || focused) && (!noHeading && !isPreview)"

View file

@ -1,14 +1,14 @@
import { mapState } from 'pinia'
import Modal from 'src/components/modal/modal.vue'
import StatusContent from 'src/components/status_content/status_content.vue'
import Status from 'src/components/status/status.vue'
import { useStatusHistoryStore } from 'src/stores/statusHistory.js'
const StatusHistoryModal = {
components: {
Modal,
StatusContent,
Status,
},
data() {
return {

View file

@ -15,10 +15,10 @@
v-if="historyCount > 0"
class="history-body"
>
<StatusContent
<Status
v-for="status in history"
:key="status.id"
:status="status"
:statusoid="status"
:is-preview="true"
class="conversation-status status-fadein panel-body"
/>

View file

@ -101,6 +101,10 @@ export default {
classesTab.push('active')
classesWrapper.push('active')
}
if (props.disabled) {
classesTab.push('disabled')
classesWrapper.push('disabled')
}
if (props.image) {
return (
<div class={classesWrapper.join(' ')}>

View file

@ -30,7 +30,7 @@ const ThreadTree = {
totalReplyCount: Object,
totalReplyDepth: Object,
},
emits: ['suspendableStateChange', 'goto', 'dive'],
emits: ['suspendableStateChange', 'goto', 'dive', 'heightChange'],
computed: {
currentReplies() {
return this.getReplies(this.statusId).map(({ id }) => id)

View file

@ -22,6 +22,7 @@
@goto="$emit('goto', statusId)"
@toggle-expanded="toggleExpanded"
@suspendable-state-change="e => $emit('suspendableStateChange', e)"
@height-change="e => $emit('heightChange', e)"
/>
<div
v-if="currentReplies.length > 0 && threadShowing"
@ -55,6 +56,7 @@
@goto="(e) => $emit('goto', e)"
@dive="(e) => $emit('dive', e)"
@suspendable-state-change="e => $emit('suspendableStateChange', e)"
@height-change="e => $emit('heightChange', e)"
/>
</div>
<div

View file

@ -27,7 +27,6 @@ library.add(faCircleNotch, faCog, faMinus, faArrowUp, faCirclePlus, faCheck)
const Timeline = {
props: {
timelineRef: Object,
count: Number,
footerSlipgate: Object, // reference to an element where we should put our footer
embedded: Boolean,
inProfile: Boolean,
@ -59,21 +58,24 @@ const Timeline = {
.map((id) => useStatusesStore().allStatuses.get(id))
.filter(({ pinned }) => (this.skipPinned ? !pinned : true))
},
count() {
return this.timeline.order.length
},
newStatusCount() {
return this.timeline.newStatusCount
},
showLoadButton() {
return this.timeline.newStatusCount > 0 || this.timeline.flushMarker !== 0
return this.timeline.newStatusCount > 0 || this.timeline.reloadNeeded
},
loadButtonString() {
if (this.timeline.flushMarker !== 0) {
if (this.timeline.reloadNeeded) {
return this.$t('timeline.reload')
} else {
return `${this.$t('timeline.show_new')} (${this.newStatusCount})`
}
},
mobileLoadButtonString() {
if (this.timeline.flushMarker !== 0) {
if (this.timeline.reloadNeeded) {
return '+'
} else {
return this.newStatusCount > 99 ? '∞' : this.newStatusCount
@ -99,6 +101,7 @@ const Timeline = {
}
},
statusesToDisplay() {
if (!this.virtualScrollingEnabled) return this.visibleStatusIds
const amount = this.timeline.visibleStatusIds.size
const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80))
const min = Math.max(0, this.virtualScrollIndex - statusesPerSide)
@ -167,9 +170,8 @@ const Timeline = {
if (e.key === '.') this.showNewStatuses()
},
showNewStatuses() {
if (this.timeline.flushMarker !== 0) {
if (this.timeline.reloadNeeded) {
useTimelinesStore().clearTimeline(this.timelineRef.name)
useTimelinesStore().queueFlush(this.timelineRef.name, '')
this.fetchOlderStatuses()
} else {
this.blockClicksTemporarily()
@ -190,13 +192,12 @@ const Timeline = {
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),
)
if (statuses.length === 0) return
const height = Math.max(document.body.offsetHeight, window.pageYOffset)
const centerOfScreen = window.pageYOffset + window.innerHeight * 0.5
@ -234,11 +235,11 @@ const Timeline = {
this.virtualScrollIndex = approxIndex
},
scrollLoad() {
// TODO simplify this logic
const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -bodyBRect.y)
if (
!this.timeline.fetcher.loading.value &&
this.$el.offsetHeight > 0 &&
!this.timeline.fetcher.loadingOlder.value &&
window.innerHeight + window.pageYOffset >= height - 750
) {
this.fetchOlderStatuses()

View file

@ -1,5 +1,6 @@
<template>
<div :class="['Timeline', classes.root]">
<!-- 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"
@ -8,6 +9,16 @@
v-if="!embedded"
:timeline-name="timelineRef.name"
/>
<div
v-if="timeline.fetcher.loadingNewer"
class="loadingIndicator"
>
<FAIcon
fixed-width
icon="circle-notch"
spin
/>
</div>
<ScrollTopButton />
<template v-if="mobileLayout">
<div
@ -84,7 +95,7 @@
/>
</div>
</div>
<div :class="classes.footer">
<div v-if="!embedded || footerSlipgate" :class="classes.footer">
<teleport
:to="footerSlipgate"
:disabled="!embedded || !footerSlipgate"
@ -102,7 +113,7 @@
{{ $t('timeline.no_more_statuses') }}
</div>
<button
v-else-if="!timeline.fetcher.loading"
v-else-if="!timeline.fetcher.loadingOlder"
class="button-unstyled -link"
@click.prevent="fetchOlderStatuses()"
>

View file

@ -12,7 +12,7 @@ const UserAvatar = {
props: {
// UserID of a user to show avatar of
userId: {
required: true,
required: false, // You can pass null to just render a placeholder
type: String,
},
// Use less space and use alternative roundness

View file

@ -70,6 +70,7 @@
&.-placeholder {
background-color: var(--background);
border: 1px solid var(--border)
}
}

View file

@ -100,7 +100,6 @@ const UserProfile = {
},
load(userNameOrId) {
const loadById = (userId) => {
console.log('LOAD', userId)
this.userId = userId
}
@ -139,7 +138,6 @@ const UserProfile = {
}
},
switchUser(userNameOrId) {
console.log('USER SWITCH')
this.load(userNameOrId)
},
onTabSwitch(tab) {

View file

@ -23,18 +23,14 @@
key="statuses"
class="statuses"
:label="$t('user_card.statuses')"
:count="user.statuses_count"
:title="$t('user_profile.timeline_title')"
>
<!--
<Timeline
key="statuses"
:timeline-ref="{ name: 'userPinned', argument: userId }"
embedded
in-profile
:footer-slipgate="footerRef"
/>
-->
<Timeline
:timeline-ref="{ name: 'user', argument: userId }"
embedded
@ -81,7 +77,6 @@
<Timeline
key="media"
:label="$t('user_card.media')"
:disabled="media.visibleStatusIds.size === 0"
:title="$t('user_card.media')"
:timeline-ref="{ name: 'media', argument: userId }"
embedded

View file

@ -47,10 +47,12 @@ const UserProfileAdminView = {
},
methods: {
fetchStatuses(page) {
return useAdminSettingsStore().fetchStatuses({
...this.fetchOptions,
page,
})
return useAdminSettingsStore()
.fetchStatuses({
...this.fetchOptions,
page,
})
.then(({ items }) => items)
},
},
components: {

View file

@ -259,7 +259,7 @@ export const parseStatus = (data) => {
output.raw_html = data.content
output.emojis = data.emojis
output.tags = data.tags
output.tags = new Set(data.tags ?? [])
output.edited_at = data.edited_at

View file

@ -63,8 +63,8 @@ export const useAuthFlowStore = defineStore('authFlow', {
this.settings = {}
},
async login({ access_token: accessToken }) {
useOAuthStore().setToken(accessToken)
useUsersStore().loginUser(accessToken, { root: true })
await useOAuthStore().setToken(accessToken)
await useUsersStore().loginUser(accessToken, { root: true })
this.resetState()
},
},

View file

@ -18,12 +18,17 @@ const REPLY_VISIBILITY_TIMELINES = new Set([
])
const timelineFetcher = (timeline, argument, credentials) => {
const loading = ref(false)
const loadingNewer = ref(false)
const loadingOlder = ref(false)
const bottomedOut = ref(false)
const interval = ref(null)
const fetchAndUpdate = ({ older = false, showImmediately = false } = {}) => {
loading.value = true
if (older) {
loadingOlder.value = true
} else {
loadingNewer.value = true
}
const { hideMutedPosts, replyVisibility } =
useMergedConfigStore().mergedConfig
@ -47,10 +52,11 @@ const timelineFetcher = (timeline, argument, credentials) => {
const numStatusesBeforeFetch = timeline.statusIds.size
if (bottomedOut.value) return
return fetchTimeline(args)
.then(({ data: statuses, pagination, timestamp }) => {
if (!older && statuses.length >= 20 && numStatusesBeforeFetch > 0) {
useTimelinesStore().queueFlush(timeline.name, timeline.maxId)
useTimelinesStore().requireReload(timeline.name)
}
if (older && statuses.length === 0) {
@ -84,7 +90,11 @@ const timelineFetcher = (timeline, argument, credentials) => {
})
})
.finally(() => {
loading.value = false
if (older) {
loadingOlder.value = false
} else {
loadingNewer.value = false
}
})
}
@ -107,7 +117,8 @@ const timelineFetcher = (timeline, argument, credentials) => {
startFetching,
stopFetching,
fetchOlder: () => fetchAndUpdate({ showImmediately: true, older: true }),
loading,
loadingOlder,
loadingNewer,
bottomedOut,
}
}

View file

@ -53,7 +53,7 @@ export const useOAuthStore = defineStore('oauth', {
this.userToken = token
},
clearToken() {
this.userToken = false
this.userToken = null
},
async createApp() {
const instance = useInstanceStore().server

View file

@ -540,10 +540,5 @@ export const useStatusesStore = defineStore('statuses', {
})
return removed
},
// Misc
setVirtualHeight({ statusId, height }) {
this.allStatuses.get(statusId).virtualHeight = height
},
},
})

View file

@ -180,7 +180,6 @@ export const useStreamingStore = defineStore('streaming', {
case 'delete':
return [data.id]
default:
console.log('UNKNOWN', eventName, eventStream, data)
return data
}
})()

View file

@ -16,7 +16,7 @@ const emptyTl = (name, argument = null) => {
maxId: '',
minId: '',
streaming: false,
flushMarker: 0,
reloadNeeded: false,
fetcher: null,
socket: null,
}
@ -158,7 +158,7 @@ export const useTimelinesStore = defineStore('timelines', {
timeline.newStatusCount = 0
timeline.maxId = ''
timeline.minId = ''
timeline.flushMarker = 0
timeline.reloadNeeded = false
},
activatePersistents() {
TIMELINES.forEach((name) => {
@ -258,14 +258,24 @@ export const useTimelinesStore = defineStore('timelines', {
timeline.fetcher.startFetching()
},
stopFetchingTimeline(timelineName, reason) {
console.debug(
'[Timelines] Stopped fetching timeline',
timelineName,
'Reason:',
reason,
)
const timeline = this[timelineName]
timeline.fetcher.stopFetching()
if (timeline.fetcher === null) {
console.debug(
'[Timelines] Already inactive timeline',
timelineName,
'Reason:',
reason,
)
return
} else {
timeline.fetcher.stopFetching()
console.debug(
'[Timelines] Stopped fetching timeline',
timelineName,
'Reason:',
reason,
)
}
},
// Queues & Timeline manip
@ -295,12 +305,12 @@ export const useTimelinesStore = defineStore('timelines', {
syncOrder(timeline) {
timeline.order = timeline.order.filter((id) => timeline.statusIds.has(id))
},
queueFlush(timeline, id) {
this[timeline].flushMarker = id
requireReload(timeline, id) {
this[timeline].reloadNeeded = true
},
queueFlushAll() {
requireReloadAll() {
Object.keys(this).forEach((timeline) => {
this[timeline].flushMarker = this[timeline].maxId
this[timeline].reloadNeeded = true
})
},

View file

@ -354,11 +354,13 @@ describe('RichContent', () => {
'<span class="MentionLink mention-link">',
'<a href="lol" class="original" target="_blank">',
'<span>',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</span>',
'</a>',
'</span>',
'</span>',
@ -418,21 +420,25 @@ describe('RichContent', () => {
'<span class="MentionLink mention-link">',
'<a href="lol" class="original" target="_blank">',
'<span>',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</span>',
'</a>',
'</span>',
'<span class="MentionLink mention-link">',
'<a href="lol" class="original" target="_blank">',
'<span>',
'<span>',
'https://</span>',
'<span>',
'lol.tld/</span>',
'<span>',
'</span>',
'</span>',
'</a>',
'</span>',
'</span>',

View file

@ -21,7 +21,6 @@ describe('Timelines store', () => {
const sub = vi.fn()
useStreamingStore().addSubscriber = sub
console.log(store.activate)
store.activate('friends', undefined, true)
expect(sub).to.have.been.called
@ -34,7 +33,6 @@ describe('Timelines store', () => {
const sub = vi.fn()
useStreamingStore().addSubscriber = sub
console.log(store.activate)
store.activate('user', '1')
expect(sub).to.not.have.been.called

View file

@ -1095,7 +1095,6 @@ describe('Users store', () => {
const store = useUsersStore()
const { storeAction, apiUrl } = actionKeys(action)
await store[storeAction](userId)
console.log(apiUrl)
expect(mockFetch).to.have.been.calledWith(
USER_API[apiUrl](userId),