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 @@
diveIntoStatus(status.id)"
@suspendable-state-change="onStatusSuspendStateChange"
+ @height-change="updateVirtualHeight"
/>
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)"
/>
$emit('goto', e)"
@dive="(e) => $emit('dive', e)"
@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 @@
-
-
+