diff --git a/changelog.d/avatar_mentions.change b/changelog.d/avatar_mentions.change new file mode 100644 index 000000000..f75f02ea4 --- /dev/null +++ b/changelog.d/avatar_mentions.change @@ -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 diff --git a/changelog.d/follower-remove.fix b/changelog.d/follower-remove.fix new file mode 100644 index 000000000..4b78b087d --- /dev/null +++ b/changelog.d/follower-remove.fix @@ -0,0 +1 @@ +Fix follower remove API call diff --git a/changelog.d/streaming_indicator.add b/changelog.d/streaming_indicator.add index 547307384..f2abe04a1 100644 --- a/changelog.d/streaming_indicator.add +++ b/changelog.d/streaming_indicator.add @@ -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 diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index cc4aecfe3..69fca2f2c 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -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, + }) + }) + }, }, } diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index ae6e3a0ce..cc1376978 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -40,6 +40,7 @@

@@ -56,6 +57,7 @@

diff --git a/src/components/desktop_nav/desktop_nav.js b/src/components/desktop_nav/desktop_nav.js index 32d0b37a9..15a5cfb15 100644 --- a/src/components/desktop_nav/desktop_nav.js +++ b/src/components/desktop_nav/desktop_nav.js @@ -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) { diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js index 6c2750677..8801d2c2e 100644 --- a/src/components/login_form/login_form.js +++ b/src/components/login_form/login_form.js @@ -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, }), diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js index 374e62259..5db90ab41 100644 --- a/src/components/mention_link/mention_link.js +++ b/src/components/mention_link/mention_link.js @@ -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] diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue index 33f0d9db7..3973522b0 100644 --- a/src/components/mention_link/mention_link.vue +++ b/src/components/mention_link/mention_link.vue @@ -8,15 +8,20 @@ :href="url" class="original" target="_blank" - v-html="content" - /> + > + + + 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 }) diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js index d1be73819..2efa94ee4 100644 --- a/src/components/navigation/navigation_pins.js +++ b/src/components/navigation/navigation_pins.js @@ -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() { diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index a589c0cf6..3c82f455c 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -118,9 +118,6 @@ const Notification = { useInstanceStore().restrictedNicknames, ) }, - getUser(notification) { - return this.$store.state.users.usersObject[notification.from_profile.id] - }, interacted() { this.$emit('interacted') }, diff --git a/src/components/oauth_callback/oauth_callback.js b/src/components/oauth_callback/oauth_callback.js index bf6e03c9e..824ab7bda 100644 --- a/src/components/oauth_callback/oauth_callback.js +++ b/src/components/oauth_callback/oauth_callback.js @@ -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' }) }) } diff --git a/src/components/quick_filter_settings/quick_filter_settings.js b/src/components/quick_filter_settings/quick_filter_settings.js index 089cb1d26..482524eb6 100644 --- a/src/components/quick_filter_settings/quick_filter_settings.js +++ b/src/components/quick_filter_settings/quick_filter_settings.js @@ -28,7 +28,7 @@ const QuickFilterSettings = { path: 'replyVisibility', value: visibility, }) - useStatusesStore().queueFlushAll() + useStatusesStore().requireReloadAll() }, openTab(tab) { useInterfaceStore().openSettingsModalTab(tab) diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue index 0abcfe656..0ad7c026a 100644 --- a/src/components/registration/registration.vue +++ b/src/components/registration/registration.vue @@ -303,7 +303,7 @@
diff --git a/src/components/settings_modal/helpers/vertical_tab_switcher.jsx b/src/components/settings_modal/helpers/vertical_tab_switcher.jsx index 996939c00..2f3d5e28f 100644 --- a/src/components/settings_modal/helpers/vertical_tab_switcher.jsx +++ b/src/components/settings_modal/helpers/vertical_tab_switcher.jsx @@ -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', }), }, diff --git a/src/components/settings_modal/tabs/clutter_tab.js b/src/components/settings_modal/tabs/clutter_tab.js index a3f6e77a5..18300231c 100644 --- a/src/components/settings_modal/tabs/clutter_tab.js +++ b/src/components/settings_modal/tabs/clutter_tab.js @@ -36,7 +36,7 @@ const ClutterTab = { // Updating nested properties watch: { replyVisibility() { - useStatusesStore().queueFlushAll() + useStatusesStore().requireReloadAll() }, }, } diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js index 9d49eca81..fbc7b8902 100644 --- a/src/components/settings_modal/tabs/filtering_tab.js +++ b/src/components/settings_modal/tabs/filtering_tab.js @@ -266,7 +266,7 @@ const FilteringTab = { // Updating nested properties watch: { replyVisibility() { - useStatusesStore().queueFlushAll() + useStatusesStore().requireReloadAll() }, muteFiltersObject() { this.muteFiltersDraftObject = cloneDeep( diff --git a/src/components/status/status.js b/src/components/status/status.js index 749e0a12e..16625235d 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -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') }, }, } diff --git a/src/components/status/status.vue b/src/components/status/status.vue index f0b2f1277..0db67f24b 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -169,7 +169,7 @@
- +
- + - diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx index 01913f709..b21d4eb04 100644 --- a/src/components/tab_switcher/tab_switcher.jsx +++ b/src/components/tab_switcher/tab_switcher.jsx @@ -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 (
diff --git a/src/components/thread_tree/thread_tree.js b/src/components/thread_tree/thread_tree.js index 491fab78f..6bcfa469f 100644 --- a/src/components/thread_tree/thread_tree.js +++ b/src/components/thread_tree/thread_tree.js @@ -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) diff --git a/src/components/thread_tree/thread_tree.vue b/src/components/thread_tree/thread_tree.vue index 2dae7beee..be95f8698 100644 --- a/src/components/thread_tree/thread_tree.vue +++ b/src/components/thread_tree/thread_tree.vue @@ -22,6 +22,7 @@ @goto="$emit('goto', statusId)" @toggle-expanded="toggleExpanded" @suspendable-state-change="e => $emit('suspendableStateChange', e)" + @height-change="e => $emit('heightChange', e)" />
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() diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 745d8c399..1435bba48 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -1,5 +1,6 @@