diff --git a/src/components/chat_new/chat_new.js b/src/components/chat_new/chat_new.js index b683a6eb7..4f015db6a 100644 --- a/src/components/chat_new/chat_new.js +++ b/src/components/chat_new/chat_new.js @@ -51,7 +51,7 @@ const chatNew = { this.$emit('cancel') }, goToChat(user) { - this.$router.push({ name: 'chat', params: { recipient_id: user.id } }) + this.$router.push({ name: 'chat', params: { chatUserId: user.id } }) }, onInput() { this.search(this.query) diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 234c8308c..4eb29bcd6 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -622,6 +622,7 @@ const conversation = { }, updateVirtualHeight() { if (this.hide) return // no updates when not rendering + if (!this.status) return // not loaded yet this.$nextTick(() => { this.virtualHeight = this.$refs.body.getBoundingClientRect().height this.$emit('update:virtualHeight', { diff --git a/src/components/follow_card/follow_card.js b/src/components/follow_card/follow_card.js index 9a8b8746f..3d9d895f0 100644 --- a/src/components/follow_card/follow_card.js +++ b/src/components/follow_card/follow_card.js @@ -15,7 +15,7 @@ const FollowCard = { }, computed: { isMe() { - return useUsersStore().currentUser.id === this.user.id + return useUsersStore().currentUser?.id === this.user.id }, loggedIn() { return useUsersStore().currentUser diff --git a/src/components/quick_filter_settings/quick_filter_settings.js b/src/components/quick_filter_settings/quick_filter_settings.js index a629266f8..13e97452e 100644 --- a/src/components/quick_filter_settings/quick_filter_settings.js +++ b/src/components/quick_filter_settings/quick_filter_settings.js @@ -5,8 +5,8 @@ import Popover from 'src/components/popover/popover.vue' import { useInterfaceStore } from 'src/stores/interface.js' import { useLocalConfigStore } from 'src/stores/local_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' -import { useStatusesStore } from 'src/stores/statuses.js' import { useSyncConfigStore } from 'src/stores/sync_config.js' +import { useTimelinesStore } from 'src/stores/timelines.js' import { useUsersStore } from 'src/stores/users.js' import { library } from '@fortawesome/fontawesome-svg-core' @@ -28,7 +28,7 @@ const QuickFilterSettings = { path: 'replyVisibility', value: visibility, }) - useStatusesStore().requireReloadAll() + useTimelinesStore().requireReloadAll() }, openTab(tab) { useInterfaceStore().openSettingsModalTab(tab) diff --git a/src/components/settings_modal/tabs/appearance_tab.js b/src/components/settings_modal/tabs/appearance_tab.js index 68c498624..f3bcf19c0 100644 --- a/src/components/settings_modal/tabs/appearance_tab.js +++ b/src/components/settings_modal/tabs/appearance_tab.js @@ -221,7 +221,7 @@ const AppearanceTab = { }, computed: { isDefaultBackground() { - return !useUsersStore().currentUser.background_image + return !useUsersStore().currentUser?.background_image }, switchInProgress() { return useInterfaceStore().themeChangeInProgress @@ -283,7 +283,7 @@ const AppearanceTab = { instanceWallpaperUsed() { return ( useInstanceStore().instanceIdentity.background && - !useUsersStore().currentUser.background_image + !useUsersStore().currentUser?.background_image ) }, customThemeVersion() { diff --git a/src/components/settings_modal/tabs/appearance_tab.vue b/src/components/settings_modal/tabs/appearance_tab.vue index 1343a36d5..e01a2b803 100644 --- a/src/components/settings_modal/tabs/appearance_tab.vue +++ b/src/components/settings_modal/tabs/appearance_tab.vue @@ -162,9 +162,9 @@
id)) + } + const amount = this.timeline.visibleStatusIds.size const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80)) const min = Math.max(0, this.virtualScrollIndex - statusesPerSide) diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js index ea0d9a884..185a127c4 100644 --- a/src/components/timeline_menu/timeline_menu.js +++ b/src/components/timeline_menu/timeline_menu.js @@ -68,7 +68,6 @@ const TimelineMenu = { return '#' + this.$route.params.tag } if (route === 'lists-timeline') { - console.log(useListsStore, this.$route.params.id) return useListsStore().findListTitle(this.$route.params.id) } if (route === 'bookmark-folder') { diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 5304608b8..739c1579b 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -238,7 +238,7 @@ export default { return useUsersStore().relationship(this.userId) }, isOtherUser() { - return this.user.id !== useUsersStore().currentUser.id + return this.user.id !== useUsersStore().currentUser?.id }, subscribeUrl() { const serverUrl = new URL(this.user.statusnet_profile_url) diff --git a/src/components/user_list_popover/user_list_popover.js b/src/components/user_list_popover/user_list_popover.js index aa530e3f4..f5a403be2 100644 --- a/src/components/user_list_popover/user_list_popover.js +++ b/src/components/user_list_popover/user_list_popover.js @@ -24,10 +24,13 @@ const UserListPopover = { UserAvatar, }, computed: { - usersCapped() { + users() { return [...this.userIds] - .slice(0, 16) .map((id) => useUsersStore().findUser(id)) + .filter(Boolean) + }, + usersCapped() { + return [...this.users].slice(0, 16) }, allowNonSquareEmoji() { return useMergedConfigStore().mergedConfig.nonSquareEmoji diff --git a/src/i18n/en.json b/src/i18n/en.json index 8c7379742..0e861fe77 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -1634,7 +1634,6 @@ "no_statuses": "No statuses", "socket_reconnected": "Realtime connection established", "socket_disconnected": "Realtime connection unavaialable", - "socket_closed": "Realtime connection closed", "socket_broke": "Realtime connection lost: CloseEvent code {0}", "quick_view_settings": "Quick view settings", "quick_filter_settings": "Quick filter settings", diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 9add0b702..88e2fd0a5 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -385,10 +385,13 @@ export const parseLinkHeaderPagination = (linkHeader, opts = {}) => { const maxId = parsedLinkHeader.next?.max_id const minId = parsedLinkHeader.prev?.min_id - return { - maxId: flakeId ? maxId : Number.parseInt(maxId, 10), - minId: flakeId ? minId : Number.parseInt(minId, 10), - } + const result = {} + if (maxId !== undefined) + result.maxId = flakeId ? maxId : Number.parseInt(maxId, 10) + if (minId !== undefined) + result.minId = flakeId ? minId : Number.parseInt(minId, 10) + + return result } export const parseChat = (chat) => { diff --git a/src/stores/fetchers/notifications_fetcher.js b/src/stores/fetchers/notifications_fetcher.js index a31877072..5403d59c9 100644 --- a/src/stores/fetchers/notifications_fetcher.js +++ b/src/stores/fetchers/notifications_fetcher.js @@ -36,7 +36,7 @@ const notificationsFetcher = (credentials) => { const notifications = response.data if (older && notifications.length === 0) bottomedOut.value = true - useNotificationsStore().addNewNotifications(response) + useNotificationsStore().addNewNotifications(response, older) } catch (error) { if ( error.statusCode === 400 && @@ -78,16 +78,13 @@ const notificationsFetcher = (credentials) => { args.timeline = 'notifications' if (older) { - if (timelineData.minId !== Number.POSITIVE_INFINITY) { + if (timelineData.minId !== '') { args.maxId = timelineData.minId } return await fetchNotifications({ args, older }) } else { // fetch new notifications - if ( - sinceId === undefined && - timelineData.maxId !== Number.POSITIVE_INFINITY - ) { + if (sinceId === undefined && timelineData.maxId !== '') { args.sinceId = timelineData.maxId } else if (sinceId !== null) { args.sinceId = sinceId diff --git a/src/stores/fetchers/timeline_fetcher.js b/src/stores/fetchers/timeline_fetcher.js index 494377b94..7fae0800d 100644 --- a/src/stores/fetchers/timeline_fetcher.js +++ b/src/stores/fetchers/timeline_fetcher.js @@ -52,7 +52,11 @@ const timelineFetcher = (timeline, argument, credentials) => { const numStatusesBeforeFetch = timeline.statusIds.size - if (older && bottomedOut.value) return + if (older && bottomedOut.value) { + loadingOlder.value = false + return + } + return fetchTimeline(args) .then(({ data, pagination, timestamp }) => { // No statuses for timeline, ever. @@ -135,6 +139,9 @@ const timelineFetcher = (timeline, argument, credentials) => { loadingOlder, loadingNewer, bottomedOut, + resetBottomedOut: () => { + bottomedOut.value = false + }, } } diff --git a/src/stores/interface.js b/src/stores/interface.js index e87a0ecd9..f2b2d86c7 100644 --- a/src/stores/interface.js +++ b/src/stores/interface.js @@ -134,14 +134,7 @@ export const useInterfaceStore = defineStore('interface', { 1001, // Going away ]) const { code } = closeEvent.original - if (intendedCodes.has(code)) { - this.pushGlobalNotice({ - level: 'success', - messageKey: 'timeline.socket_closed', - messageArgs: [code], - timeout: 5000, - }) - } else { + if (!intendedCodes.has(code)) { this.pushGlobalNotice({ level: 'error', messageKey: 'timeline.socket_broke', diff --git a/src/stores/streaming.js b/src/stores/streaming.js index aac1fa860..dae047f46 100644 --- a/src/stores/streaming.js +++ b/src/stores/streaming.js @@ -108,6 +108,8 @@ export const useStreamingStore = defineStore('streaming', { } }, initSocket(initial) { + if (this.socket) throw new Error('Socket already exists!') + this.state = initial ? WSConnectionStatus.STARTING_INITIAL : WSConnectionStatus.STARTING @@ -129,7 +131,11 @@ export const useStreamingStore = defineStore('streaming', { }, stopSocket() { this.socket.close() + this.socket = null this.state = WSConnectionStatus.CLOSED + this.retrying = false + this.retryMultiplier = 1 + this.error = null }, getSubArgs(stream) { @@ -229,6 +235,8 @@ export const useStreamingStore = defineStore('streaming', { ) setTimeout(() => { + if (this.retrying) return // retry aborted (i.e. due to logout) + this.initSocket() }, retryTimeout(this.retryMultiplier)) diff --git a/src/stores/timelines.js b/src/stores/timelines.js index 6dda2f23b..86a2c617e 100644 --- a/src/stores/timelines.js +++ b/src/stores/timelines.js @@ -81,6 +81,7 @@ export const ARGUMENT_MAP = { user: 'userId', userPinned: 'userId', media: 'userId', + favorites: 'userId', } const TIMELINES = new Set([ @@ -180,7 +181,7 @@ export const useTimelinesStore = defineStore('timelines', { timeline.socket.handlers timeline.socket.et.removeEventListener('open', openHandler) timeline.socket.et.removeEventListener('close', closeHandler) - timeline.socket.et.removeEventListener('message', messageHandler) + timeline.socket.et.removeEventListener('update', messageHandler) } this[timelineName] = emptyTl(timelineName) @@ -196,6 +197,7 @@ export const useTimelinesStore = defineStore('timelines', { timeline.maxId = '' timeline.minId = '' timeline.reloadNeeded = false + timeline.fetcher.resetBottomedOut() }, activatePersistents() { TIMELINES.forEach((name) => { @@ -266,8 +268,6 @@ export const useTimelinesStore = defineStore('timelines', { if (statuses.length === 0) return const timeline = this[timelineName] - this.populateRepeats(timeline, repeats) - // This makes sure that user timeline won't get data meant for other // user. I.e. opening different user profiles makes request which could // return data late after user already viewing different user profile @@ -278,9 +278,7 @@ export const useTimelinesStore = defineStore('timelines', { return } - if (!noIdUpdate) { - this.updateTimelineExtremes(timeline, pagination) - } + this.populateRepeats(timeline, repeats) const filtered = statuses.filter((id) => !timeline.statusIds.has(id)) if (older) { @@ -289,26 +287,36 @@ export const useTimelinesStore = defineStore('timelines', { timeline.order.unshift(...filtered) } + const newStatuses = new Set() + statuses.forEach((statusId) => { const isNew = !timeline.statusIds.has(statusId) timeline.statusIds.add(statusId) if (isNew) { - const seenBefore = this.checkSeenBefore(timeline, statusId) - if (!seenBefore) { - if (showImmediately) { - // Add it directly to the visibleStatuses, don't change - // newStatusCount - timeline.visibleStatusIds.add(statusId) - } else { - // Just change newStatuscount - timeline.newStatusCount += 1 - } - } else { - timeline.ignoredIds.add(statusId) - } + newStatuses.add(statusId) } }) + + newStatuses.forEach((statusId) => { + const seenBefore = this.checkSeenBefore(timeline, statusId) + if (!seenBefore) { + if (showImmediately) { + // Add it directly to the visibleStatuses, don't change + // newStatusCount + timeline.visibleStatusIds.add(statusId) + } else { + // Just change newStatuscount + timeline.newStatusCount += 1 + } + } else { + timeline.ignoredIds.add(statusId) + } + }) + + if (!noIdUpdate) { + this.updateTimelineExtremes(timeline, pagination) + } }, onStreamMessage(timelineName, argument, event) { this.addStatusesToTimeline(timelineName, argument, { @@ -347,7 +355,7 @@ export const useTimelinesStore = defineStore('timelines', { // If it's the only reprööt then we've never seen post before if (knownRepeats.size === 1) return false // If we're working on oldest known reprööt then we've never seen it before - return first(knownRepeats) !== statusId + return knownRepeats.values().next().value !== statusId }, // Poll & Push @@ -414,7 +422,7 @@ export const useTimelinesStore = defineStore('timelines', { }, // Queues & Timeline manip - updateTimelineExtremes(timeline, pagination = {}) { + updateTimelineExtremes(timeline, pagination = {}, force = false) { // Can't use Math.min/max because it doesn't work with string (duh) const minNew = pagination.maxId ?? last(timeline.order) ?? '' const maxNew = pagination.minId ?? first(timeline.order) ?? '' @@ -422,10 +430,10 @@ export const useTimelinesStore = defineStore('timelines', { const newer = maxNew > timeline.maxId const older = minNew < timeline.minId - if (newer || timeline.maxId === '') { + if (force || newer || timeline.maxId === '') { timeline.maxId = maxNew } - if (older || timeline.minId === '') { + if (force || older || timeline.minId === '') { timeline.minId = minNew } @@ -442,7 +450,8 @@ export const useTimelinesStore = defineStore('timelines', { timeline.visibleStatusIds = new Set([ ...timeline.order.filter((id) => !timeline.ignoredIds.has(id)), ]) - this.updateTimelineExtremes(timeline) + this.updateTimelineExtremes(timeline, {}, true) + timeline.fetcher.resetBottomedOut() }, syncOrder(timeline) { timeline.order = timeline.order.filter((id) => timeline.statusIds.has(id)) @@ -451,8 +460,10 @@ export const useTimelinesStore = defineStore('timelines', { this[timeline].reloadNeeded = true }, requireReloadAll() { - Object.keys(this).forEach((timeline) => { - this[timeline].reloadNeeded = true + TIMELINES.forEach((timelineName) => { + const timeline = this[timelineName] + + timeline.reloadNeeded = true }) }, diff --git a/src/stores/users.js b/src/stores/users.js index 305d0fc5b..87b177dfa 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -278,15 +278,21 @@ export const useUsersStore = defineStore('users', { const result = await promise - if (result) { - const { id, screen_name } = result + try { + if (result) { + const { id, screen_name } = result - // Save promise for future use - this.fetchesIds.set(id, promise) - this.fetchesNames.set(screen_name, promise) - return this.users.get(id) - } else { - return null + // Save promise for future use + this.fetchesIds.set(id, promise) + this.fetchesNames.set(screen_name, promise) + return this.users.get(id) + } else { + return null + } + } catch (e) { + console.error(`Failed fetching user ${identifier}`, e) + map.delete(identifier) + throw e } }, async fetchUser(id) { @@ -520,7 +526,7 @@ export const useUsersStore = defineStore('users', { /// Mute muteUser(id, expiresIn = 0) { - const predictedRelationship = this.relationships[id] || { id } + const predictedRelationship = this.relationships.get(id) || { id } predictedRelationship.muting = true this.updateUserRelationships({ optimism: true, @@ -539,7 +545,7 @@ export const useUsersStore = defineStore('users', { return Promise.all(data.map((d) => this.muteUser(d))) }, unmuteUser(id) { - const predictedRelationship = this.relationships[id] || { id } + const predictedRelationship = this.relationships.get(id) || { id } predictedRelationship.muting = false this.updateUserRelationships({ optimism: true, @@ -556,7 +562,7 @@ export const useUsersStore = defineStore('users', { /// Block blockUser(id, expiresIn = 0) { - const predictedRelationship = this.relationships[id] || { id } + const predictedRelationship = this.relationships.get(id) || { id } this.updateUserRelationships({ optimism: true, data: [predictedRelationship],