-
+
{
- const { data: externalUser } = result
- if (!externalUser.error) {
- useUsersStore().addNewUsers(result)
- this.users.push(externalUser)
- }
+ const [user] = useUsersStore().addNewUsers(result)
+
+ this.users.push(user)
})
})
},
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/admin_settings.js b/src/stores/admin_settings.js
index 1e47cce2e..4961761e7 100644
--- a/src/stores/admin_settings.js
+++ b/src/stores/admin_settings.js
@@ -400,9 +400,13 @@ export const useAdminSettingsStore = defineStore('adminSettings', {
return {
items: await Promise.all(
- users.map((user) => {
- useUsersStore().updateUserAdminData(user.id, user)
- return useUsersStore().findUser(user.id)
+ users.map(async (user) => {
+ const fullUser = await useUsersStore().fetchUserIfMissing({
+ id: user.id,
+ })
+
+ if (fullUser) useUsersStore().updateUserAdminData(user.id, user)
+ return fullUser
}),
),
count,
diff --git a/src/stores/chats.js b/src/stores/chats.js
index 6fcfbcd1e..32296ff4f 100644
--- a/src/stores/chats.js
+++ b/src/stores/chats.js
@@ -77,7 +77,8 @@ export const useChatsStore = defineStore('chats', {
updateChat(updatedChat) {
const chat = this.data.get(updatedChat.id)
if (chat) {
- const isNewMessage = chat.lastMessage !== updatedChat.lastMessage
+ const isNewMessage =
+ chat.lastMessage?.id !== updatedChat.lastMessage?.id
chat.lastMessage = updatedChat.lastMessage
chat.unread = updatedChat.unread
chat.updated_at = updatedChat.updated_at
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/lists.js b/src/stores/lists.js
index 37471d46e..fcc3fefde 100644
--- a/src/stores/lists.js
+++ b/src/stores/lists.js
@@ -49,6 +49,9 @@ export const useListsStore = defineStore('lists', {
},
setLists(value) {
this.allLists = value
+ this.allListsObject = Object.fromEntries(
+ value.map((list) => [list.id, list]),
+ )
},
async createList({ title }) {
return await createList({
diff --git a/src/stores/notifications.js b/src/stores/notifications.js
index c69e9c164..bed0596ea 100644
--- a/src/stores/notifications.js
+++ b/src/stores/notifications.js
@@ -81,13 +81,15 @@ export const useNotificationsStore = defineStore('notifications', {
pause() {
this.paused = true
if (this.fetcher && this.fetching) {
- this.stopFetching('Notifications paused')
+ console.debug('[Notifications] Pausing notifications')
+ this.fetcher.stopFetching()
}
},
resume() {
this.paused = false
if (this.fetcher && this.fetching) {
- this.startFetching('Notifications resumed')
+ console.debug('[Notifications] Resuming notifications')
+ this.fetcher.startFetching()
}
},
activate() {
diff --git a/src/stores/statuses.js b/src/stores/statuses.js
index ceb51ceb6..5fd7a3f2c 100644
--- a/src/stores/statuses.js
+++ b/src/stores/statuses.js
@@ -36,6 +36,8 @@ export const defaultState = () => ({
conversations: new Map(),
favorites: new Set(),
socket: null,
+ favs: new Map(),
+ repeats: new Map(),
})
export const useStatusesStore = defineStore('statuses', {
@@ -188,61 +190,69 @@ export const useStatusesStore = defineStore('statuses', {
return fetchEmojiReactions({
id,
credentials: useOAuthStore().token,
- }).then(({ data: emojiReactions }) => {
- this.addEmojiReactionsBy({
- id,
- emojiReactions,
+ }).then(({ data, timestamp }) => {
+ const reactions = data.map((reaction) => {
+ const users = useUsersStore().addNewUsers({
+ timestamp,
+ data: reaction.accounts,
+ })
+
+ // Backend inconsistency - status data only has ids (account_ids)
+ // but reactions data has full info (accounts)
+ return {
+ ...reaction,
+ accounts: users,
+ account_ids: users.map(({ id }) => id),
+ }
})
+
+ this.addEmojiReactionsBy(id, reactions)
})
},
fetchFavs(id) {
return fetchFavoritedByUsers({
id,
credentials: useOAuthStore().token,
- }).then(({ data: favoritedByUsers }) =>
- this.addFavs({
- id,
- favoritedByUsers,
- }),
- )
+ }).then((result) => {
+ const users = useUsersStore().addNewUsers(result)
+ return this.addFavs(id, new Set(users.map(({ id }) => id)))
+ })
},
fetchRepeats(id) {
return fetchRebloggedByUsers({
id,
credentials: useOAuthStore().token,
- }).then(({ data: rebloggedByUsers }) =>
- this.addRepeats({
- id,
- rebloggedByUsers,
- }),
- )
+ }).then((result) => {
+ const users = useUsersStore().addNewUsers(result)
+ return this.addRepeats(id, new Set(users.map(({ id }) => id)))
+ })
},
fetchFavsAndRepeats(id) {
return Promise.all([this.fetchFavs(id), this.fetchRepeats(id)])
},
// Updates
- addRepeats({ id, rebloggedByUsers }) {
+ addRepeats(id, users) {
const currentUser = useUsersStore().currentUser
const newStatus = this.allStatuses.get(id)
- newStatus.rebloggedBy = rebloggedByUsers.filter(Boolean)
- // repeats stats can be incorrect based on polling condition, let's update them using the most recent data
- newStatus.repeat_num = newStatus.rebloggedBy.length
- newStatus.repeated = !!newStatus.rebloggedBy.find(
- ({ id }) => currentUser?.id === id,
- )
+ this.repeats.set(id, users)
+
+ // repeats stats can be incorrect based on polling
+ // condition, let's update them using the most recent data
+ newStatus.repeat_num = users.size
+ newStatus.repeated = users.has(currentUser?.id)
},
- addFavs({ id, favoritedByUsers }) {
+ addFavs(id, users) {
const currentUser = useUsersStore().currentUser
const newStatus = this.allStatuses.get(id)
- newStatus.favoritedBy = favoritedByUsers.filter(Boolean)
- // favorites stats can be incorrect based on polling condition, let's update them using the most recent data
- newStatus.fave_num = newStatus.favoritedBy.length
- newStatus.favorited = !!newStatus.favoritedBy.find(
- ({ id }) => currentUser?.id === id,
- )
+ this.favs.set(id, users)
+
+ // favorites stats can be incorrect based on polling
+ // condition, let's update them using the most recent data
+ newStatus.fave_num = users.size
+ newStatus.favorited = users.has(currentUser?.id)
},
- addEmojiReactionsBy({ id, emojiReactions }) {
+ addEmojiReactionsBy(id, emojiReactions) {
const status = this.allStatuses.get(id)
status.emoji_reactions = emojiReactions
},
@@ -287,7 +297,7 @@ export const useStatusesStore = defineStore('statuses', {
useInterfaceStore().pushGlobalNotice({
level: 'error',
messageKey: 'status.interact_error',
- messageArgs: [error],
+ messageArgs: { error },
timeout: 5000,
})
})
@@ -393,6 +403,7 @@ export const useStatusesStore = defineStore('statuses', {
name: emoji,
count: 0,
accounts: [],
+ account_ids: [],
}
const count = value ? reaction.count + 1 : reaction.count - 1
@@ -400,12 +411,14 @@ export const useStatusesStore = defineStore('statuses', {
const accounts = value
? [...reaction.accounts, currentUser]
: reaction.accounts.filter((acc) => acc.id !== currentUser.id)
+ const account_ids = accounts.filter(Boolean).map(({ id }) => id)
const newReaction = {
...reaction,
count,
me: value,
accounts,
+ account_ids,
}
if (reactionPresent && count > 0) {
diff --git a/src/stores/streaming.js b/src/stores/streaming.js
index a360731bf..dae047f46 100644
--- a/src/stores/streaming.js
+++ b/src/stores/streaming.js
@@ -99,6 +99,8 @@ export const useStreamingStore = defineStore('streaming', {
this.subscribers.delete(subscriber)
if (stream) {
this.subscriptions.get(stream.name).delete(stream.argument)
+ } else {
+ this.globalSubscriptions.delete(subscriber)
}
if (stream && this.state === WSConnectionStatus.JOINED) {
@@ -106,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
@@ -127,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) {
@@ -227,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 ed4ecfaec..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([
@@ -105,8 +106,6 @@ export const defaultState = () => {
return Object.fromEntries([...TIMELINES].map((name) => [name, emptyTl(name)]))
}
-//const CUSTOM_SORT = new Set(['bookmarks', 'favorites'])
-
export const useTimelinesStore = defineStore('timelines', {
state: defaultState,
actions: {
@@ -182,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)
@@ -198,6 +197,7 @@ export const useTimelinesStore = defineStore('timelines', {
timeline.maxId = ''
timeline.minId = ''
timeline.reloadNeeded = false
+ timeline.fetcher.resetBottomedOut()
},
activatePersistents() {
TIMELINES.forEach((name) => {
@@ -268,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
@@ -280,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) {
@@ -291,29 +287,39 @@ 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(timeline, argument, event) {
- this.addStatusesToTimeline(timeline, argument, {
+ onStreamMessage(timelineName, argument, event) {
+ this.addStatusesToTimeline(timelineName, argument, {
statuses: event.data.map(({ id }) => id),
repeats: event.data
.filter(({ retweeted_status }) => Boolean(retweeted_status))
@@ -349,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
@@ -416,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) ?? ''
@@ -424,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
}
@@ -444,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))
@@ -453,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 1c32ab679..d3c8ad6cf 100644
--- a/src/stores/users.js
+++ b/src/stores/users.js
@@ -208,7 +208,12 @@ export const useUsersStore = defineStore('users', {
// Misc updates
updateUserAdminData(id, data) {
const user = this.users.get(id)
-
+ if (!user) {
+ console.warn(
+ `User id ${id} somehow not found during admin data update!`,
+ )
+ return
+ }
user.adminData = data
user.deactivated = !data.is_active
user.tags = new Set(data.tags)
@@ -271,15 +276,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) {
@@ -513,7 +524,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,
@@ -532,7 +543,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,
@@ -549,7 +560,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],
@@ -718,6 +729,8 @@ export const useUsersStore = defineStore('users', {
useAnnouncementsStore().stopFetching()
useListsStore().stopFetching()
useBookmarkFoldersStore().stopFetching()
+ useChatsStore().stopFetching()
+
store?.dispatch('stopFetchingFollowRequests')
// NOTE: No need to verify the app still exists, because if it doesn't,
@@ -743,7 +756,6 @@ export const useUsersStore = defineStore('users', {
// Full reset on logout success
useTimelinesStore().deactivateAll()
useStatusesStore().resetStatuses()
- useChatsStore().stopFetching()
useChatsStore().resetChats()
this.users = new Map()
diff --git a/test/unit/specs/stores/statuses.spec.js b/test/unit/specs/stores/statuses.spec.js
index 9be47145c..f4064ec9d 100644
--- a/test/unit/specs/stores/statuses.spec.js
+++ b/test/unit/specs/stores/statuses.spec.js
@@ -290,7 +290,10 @@ describe('Statuses store', () => {
'EmojiReactions',
[
{
- accounts: [mockMastoAPIUser()],
+ accounts: [
+ mockMastoAPIUser({ id: 'u1' }),
+ mockMastoAPIUser({ id: 'u2' }),
+ ],
count: 1,
me: false,
name: 'cofe',
@@ -298,8 +301,14 @@ describe('Statuses store', () => {
},
],
],
- ['Favs', [mockMastoAPIUser()]],
- ['Repeats', [mockMastoAPIUser()]],
+ [
+ 'Favs',
+ [mockMastoAPIUser({ id: 'u1' }), mockMastoAPIUser({ id: 'u2' })],
+ ],
+ [
+ 'Repeats',
+ [mockMastoAPIUser({ id: 'u1' }), mockMastoAPIUser({ id: 'u2' })],
+ ],
])('fetch%s', async (group, mockedResponse) => {
const mockFetch = vi.fn()
mockFetch.mockResolvedValueOnce(
@@ -309,6 +318,7 @@ describe('Statuses store', () => {
)
vi.stubGlobal('fetch', mockFetch)
+ const addNewUsers = vi.spyOn(useUsersStore(), 'addNewUsers')
let urlKey
let prefix = 'MASTODON'
@@ -335,13 +345,29 @@ describe('Statuses store', () => {
const result = await store[`fetch${group}`]('id')
const updated = store.allStatuses.get('id')
+ // Fetch called
expect(mockFetch).to.have.been.calledWith(url, DEFAULT_OPTIONS())
+
+ // Users updated
+ if (group !== 'StatusSource') {
+ // first call is the one for the status
+ expect(addNewUsers).to.have.been.calledTwice
+ const secondCallData = addNewUsers.mock.calls[1][0].data
+ expect(secondCallData).to.have.length(2)
+ expect(secondCallData[0]).to.have.property('id', 'u1')
+ expect(secondCallData[1]).to.have.property('id', 'u2')
+ }
+
if (group === 'Favs') {
- expect(updated.favoritedBy).to.have.length(1)
- expect(updated.fave_num).to.eql(1)
+ expect(store.favs).to.have.length(1)
+ expect(store.favs.get('id')).to.have.length(2)
+ expect(store.favs.get('id')).to.eql(new Set(['u1', 'u2']))
+ expect(updated.fave_num).to.eql(2)
} else if (group === 'Repeats') {
- expect(updated.rebloggedBy).to.have.length(1)
- expect(updated.repeat_num).to.eql(1)
+ expect(store.repeats).to.have.length(1)
+ expect(store.repeats.get('id')).to.have.length(2)
+ expect(store.repeats.get('id')).to.eql(new Set(['u1', 'u2']))
+ expect(updated.repeat_num).to.eql(2)
} else if (group === 'EmojiReactions') {
expect(updated.emoji_reactions).to.have.length(mockedResponse.length)
expect(updated.emoji_reactions[0].name).to.eql(mockedResponse[0].name)
diff --git a/test/unit/specs/stores/users.spec.js b/test/unit/specs/stores/users.spec.js
index 2709d680a..c44c749f3 100644
--- a/test/unit/specs/stores/users.spec.js
+++ b/test/unit/specs/stores/users.spec.js
@@ -740,19 +740,24 @@ describe('Users store', () => {
const spies = [
// Misc initialization
- vi.spyOn(useStatusesStore(), 'resetStatuses'),
- vi.spyOn(useInterfaceStore(), 'onLogout'),
+ /* 0 */ vi.spyOn(useStatusesStore(), 'resetStatuses'),
+ /* 1 */ vi.spyOn(useInterfaceStore(), 'onLogout'),
// Timeline / Notifications
- vi.spyOn(useNotificationsStore(), 'deactivate'),
- vi.spyOn(useTimelinesStore(), 'deactivateAll'),
+ /* 2 */ vi.spyOn(useNotificationsStore(), 'deactivate'),
+ /* 3 */ vi.spyOn(useNotificationsStore(), 'pause'),
+ /* 4 */ vi.spyOn(useNotificationsStore(), 'resume'),
+ /* 5 */ vi.spyOn(useTimelinesStore(), 'deactivateAll'),
+ /* 6 */ vi.spyOn(useTimelinesStore(), 'pauseAll'),
+ /* 7 */ vi.spyOn(useTimelinesStore(), 'resumeAll'),
// Fetchers
- vi.spyOn(useChatsStore(), 'resetChats'),
- vi.spyOn(useListsStore(), 'stopFetching'),
- vi.spyOn(useAnnouncementsStore(), 'stopFetching'),
- vi.spyOn(useBookmarkFoldersStore(), 'stopFetching'),
- vi.spyOn(useStreamingStore(), 'stopSocket'),
+ /* 8 */ vi.spyOn(useChatsStore(), 'resetChats'),
+ /* 9 */ vi.spyOn(useChatsStore(), 'stopFetching'),
+ /* 10 */ vi.spyOn(useListsStore(), 'stopFetching'),
+ /* 11 */ vi.spyOn(useAnnouncementsStore(), 'stopFetching'),
+ /* 12 */ vi.spyOn(useBookmarkFoldersStore(), 'stopFetching'),
+ /* 13 */ vi.spyOn(useStreamingStore(), 'stopSocket'),
]
spies.forEach((spy) => {
@@ -791,6 +796,91 @@ describe('Users store', () => {
expect(spy, `Spy ${index} has failed`).to.have.been.called
})
})
+
+ it('failed logout', async () => {
+ const revokeApi = vi
+ .fn()
+ .mockResolvedValueOnce(
+ // Ensure APP
+ new Response(JSON.stringify('ok'), {
+ headers: { 'Content-Type': 'application/json' },
+ }),
+ )
+ .mockResolvedValueOnce(
+ // Revoke Token
+ new Response(
+ JSON.stringify('Oopsie-woopsie pleroma made a fucky-wucky'),
+ {
+ status: 500,
+ statusText: 'Internal Server Error',
+ headers: { 'Content-Type': 'application/json' },
+ },
+ ),
+ )
+
+ vi.stubGlobal('fetch', revokeApi)
+
+ // NOTE: Order is not checked for!
+ const spies = [
+ // ## PAUSE ##
+ // Timeline / Notifications
+ /* 0 */ vi.spyOn(useTimelinesStore(), 'pauseAll'),
+ /* 1 */ vi.spyOn(useNotificationsStore(), 'pause'),
+
+ // Fetchers (Pauseless)
+ /* 2 */ vi.spyOn(useListsStore(), 'stopFetching'),
+ /* 3 */ vi.spyOn(useChatsStore(), 'stopFetching'),
+ /* 4 */ vi.spyOn(useAnnouncementsStore(), 'stopFetching'),
+ /* 5 */ vi.spyOn(useBookmarkFoldersStore(), 'stopFetching'),
+
+ // ## RESUME ##
+ // Timeline / Notifications
+ /* 6 */ vi.spyOn(useNotificationsStore(), 'resume'),
+ /* 7 */ vi.spyOn(useTimelinesStore(), 'resumeAll'),
+
+ // Fetchers (Pauseless)
+ /* 8 */ vi.spyOn(useListsStore(), 'startFetching'),
+ /* 9 */ vi.spyOn(useChatsStore(), 'startFetching'),
+ /* 10 */ vi.spyOn(useAnnouncementsStore(), 'startFetching'),
+ /* 11 */ vi.spyOn(useBookmarkFoldersStore(), 'startFetching'),
+ ]
+
+ spies.forEach((spy) => {
+ spy.mockImplementation(async () => {
+ /* no-op */
+ })
+ })
+
+ useInstanceCapabilitiesStore().pleromaChatMessagesAvailable = true
+ useMergedConfigStore().mergedConfig = { useStreamingApi: true }
+
+ const store = useUsersStore()
+ store.currentUser = mockUser()
+
+ // Adding some users to verify they are getting cleaned afterwards
+ store.addNewUsers({
+ data: [
+ mockUser(),
+ {
+ ...mockUser({ name: 'John', screen_name: 'snake' }),
+ relationship: { id: userId, following: true },
+ },
+ { ...mockUser({ name: 'David Oh', screen_name: 'zero' }) },
+ ],
+ timestamp: 2000,
+ })
+ expect(store.loggedIn).to.eql(true)
+ await store.logout()
+ expect(store.loggedIn).to.eql(true)
+ expect(revokeApi).to.have.been.called
+ expect(store.users).to.have.length(1)
+ expect(store.usersByName).to.have.length(1)
+ expect(store.usersByURL).to.have.length(1)
+ expect(store.relationships).to.have.length(1)
+ spies.forEach((spy, index) => {
+ expect(spy, `Spy ${index} has failed`).to.have.been.called
+ })
+ })
})
})