Compare commits

...

24 commits

Author SHA1 Message Date
Henry Jameson
a6f19bb213 Merge branch 'users-statuses-pinia' into shigusegubu-themes3 2026-08-24 19:47:49 +03:00
Henry Jameson
f76070640a Merge remote-tracking branch 'origin/develop' into users-statuses-pinia 2026-08-24 19:39:06 +03:00
Henry Jameson
e128e21315 fixes 2026-08-24 19:37:48 +03:00
Henry Jameson
4a3c5fade9 fix page conversation 2026-08-24 19:28:07 +03:00
Henry Jameson
556a37e836 login/logout/register woes 2026-08-24 19:24:14 +03:00
Henry Jameson
a392f8e4a9 fix tests 2026-08-24 18:10:10 +03:00
Henry Jameson
4d885055a7 lint 2026-08-24 18:04:59 +03:00
Henry Jameson
74319a5d4c fix admin view 2026-08-24 17:57:05 +03:00
Henry Jameson
b71488fa73 bring back status history 2026-08-24 17:53:57 +03:00
Henry Jameson
6946f7d201 changelog 2026-08-24 17:44:26 +03:00
Henry Jameson
fdea276b80 more precise height detection 2026-08-24 17:35:13 +03:00
Henry Jameson
121038dc96 split loading between older and newer, add newer loading indicator 2026-08-24 17:34:59 +03:00
Henry Jameson
954bbedb2d don't try to fetch older posts if bottomed out 2026-08-24 17:28:13 +03:00
Henry Jameson
5b79cff56c restoration of pinned 2026-08-24 17:28:05 +03:00
Henry Jameson
39cfa6a5dd more cleanup 2026-08-24 16:49:19 +03:00
Henry Jameson
8c5680bd05 fix warnings 2026-08-24 16:35:51 +03:00
Henry Jameson
034b2c3bf9 render placeholder avatars while fetching mentions 2026-08-24 16:35:13 +03:00
Henry Jameson
d521a06dad cleanup & lint 2026-08-24 16:08:47 +03:00
Henry Jameson
6adc4ef2c6 virtual scrolling improvements 2026-08-24 16:04:40 +03:00
Henry Jameson
58bc4b3347 virtual scrolling improvements 2026-08-24 15:36:28 +03:00
Henry Jameson
1c1526c065 clear up queueFlush nonsense 2026-08-24 15:01:59 +03:00
Henry Jameson
12df5d09ab logged-out fixes + rename mapPiniaState -> mapState 2026-08-24 14:52:26 +03:00
HJ
8c87f9fcff Merge pull request 'Fix follower remove api call' (#3560) from iamtakingiteasy/pleroma-fe:fix-follower-remove-api-call into develop
Reviewed-on: https://git.pleroma.social/pleroma/pleroma-fe/pulls/3560
2026-08-20 19:50:04 +00:00
Alexander Tumin
7528b0b122 fix follower remove api call 2026-08-16 09:23:03 +03:00
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),