Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85650999
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
34 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/stores/statuses.js b/src/stores/statuses.js
index 9df26465fb..890dd4002d 100644
--- a/src/stores/statuses.js
+++ b/src/stores/statuses.js
@@ -1,548 +1,551 @@
import { defineStore } from 'pinia'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useStreamingStore } from 'src/stores/streaming.js'
import { useUsersStore } from 'src/stores/users.js'
import {
fetchEmojiReactions,
fetchFavoritedByUsers,
fetchRebloggedByUsers,
fetchStatus,
fetchStatusSource,
} from 'src/api/public.js'
import {
bookmarkStatus,
deleteStatus,
favorite,
muteConversation,
pinOwnStatus,
reactWithEmoji,
retweet,
unbookmarkStatus,
unfavorite,
unmuteConversation,
unpinOwnStatus,
unreactWithEmoji,
unretweet,
} from 'src/api/user.js'
export const defaultState = () => ({
allStatuses: new Map(),
statusesPerUser: new Map(),
timestamps: new WeakMap(),
scrobblesNextFetch: {},
conversations: new Map(),
favorites: new Set(),
socket: null,
favs: new Map(),
repeats: new Map(),
})
export const useStatusesStore = defineStore('statuses', {
state: defaultState,
actions: {
// Init
attachSocket() {
const et = new EventTarget()
const handleUpdate = ({ data, timestamp }) =>
this.addNewStatuses({ statuses: data, timestamp })
const handleDelete = ({ data }) =>
data.forEach((id) => this.setDeleted(id))
const socket = {
name: 'statuses',
et,
handlers: {
handleUpdate,
handleDelete,
},
}
et.addEventListener('update', handleUpdate)
et.addEventListener('status.update', handleUpdate)
et.addEventListener('delete', handleDelete)
useStreamingStore().addSubscriber(socket)
this.socket = socket
},
resetStatuses() {
const emptyState = defaultState()
Object.entries(emptyState).forEach(([key, value]) => {
if (key === 'socket') return
this[key] = value
})
},
addNewStatuses({ statuses, timestamp }) {
// Sanity check
if (!Array.isArray(statuses)) {
throw new TypeError("Statuses aren't an array!")
}
// addStatus should always return "main" status,
// not "sub-status" i.e. retweeted/quoted/liked status
// in case of likes (which are not statuses) it should return null
const addStatus = (data) => {
const [status] = this.mergeOrAdd(this.allStatuses, data, timestamp)
let userSet = this.statusesPerUser.get(status.user.id)
if (userSet === undefined) {
userSet = new Set()
this.statusesPerUser.set(status.user.id, userSet)
}
userSet.add(status.id)
// Add to conversation
const conversations = this.conversations
const conversationId = status.statusnet_conversation_id
if (conversations.has(conversationId)) {
conversations.get(conversationId).add(status.id)
} else {
conversations.set(conversationId, new Set([status.id]))
}
// Work on quote
if (status.quote) {
status.quote = addStatus(status.quote)
}
return status
}
const processors = {
status: (status) => {
return addStatus(status)
},
edit: (status) => {
return addStatus(status)
},
retweet: (status) => {
if (status.retweeted_status) addStatus(status.retweeted_status)
return addStatus(status)
},
default: (unknown) => {
console.warn('unknown status type', unknown)
return null
},
}
return statuses.map((status) => {
const type = status.type
const processor = processors[type] ?? processors.default
return processor(status)
})
},
mergeOrAdd(map, status, timestamp) {
const existing = map.get(status.id) ?? {}
const oldTimestamp = this.timestamps.get(existing)
const { user: unused0, ...old } = existing
const { user: statusUser, ...neu } = status
const [user] = useUsersStore().addNewUsers({
data: statusUser,
timestamp,
})
const { in_reply_to_user_id } = status
if (in_reply_to_user_id) {
useUsersStore().fetchUserIfMissing({ id: in_reply_to_user_id })
}
existing.user = user // reactive update in case we return old
// implicit: if oldTimestamp is undefined this will still be false
if (oldTimestamp > timestamp) return [existing, false] // not overwriting old data with new
const newStatus = {
...old,
...Object.fromEntries(
Object.entries(neu).filter(([, v]) => v !== undefined),
),
user,
}
map.set(newStatus.id, newStatus)
this.timestamps.set(newStatus, timestamp)
return [map.get(newStatus.id), true]
},
// Fetches
fetchStatus(id) {
return fetchStatus({
id,
credentials: useOAuthStore().token,
}).then(({ data: status, timestamp }) =>
this.addNewStatuses({ statuses: [status], timestamp }),
)
},
fetchStatusSource(id) {
return fetchStatusSource({
id,
credentials: useOAuthStore().token,
}).then(({ data }) => data)
},
fetchEmojiReactions(id) {
return fetchEmojiReactions({
id,
credentials: useOAuthStore().token,
- }).then(({ data: emojiReactions }) => {
- this.addEmojiReactionsBy(id, emojiReactions)
+ }).then(({ data, timestamp }) => {
+ data.forEach((reaction) => {
+ const users = useUsersStore().addNewUsers({ timestamp, data: reaction.accounts })
+ })
+ this.addEmojiReactionsBy(id, data)
})
},
fetchFavs(id) {
return fetchFavoritedByUsers({
id,
credentials: useOAuthStore().token,
}).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((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, users) {
const currentUser = useUsersStore().currentUser
const newStatus = this.allStatuses.get(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, users) {
const currentUser = useUsersStore().currentUser
const newStatus = this.allStatuses.get(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) {
const status = this.allStatuses.get(id)
status.emoji_reactions = emojiReactions
},
updateStatusWithPoll(id, poll) {
const status = this.allStatuses.get(id)
status.poll = poll
},
// Actions
requestInteract({ name, id, optimisticCall, apiCall, argument, value }) {
const oldValue = !value // Assumption
const apiArgs = (() => {
switch (name) {
case 'emoji':
return { emoji: argument }
case 'bookmark':
return { folder_id: argument }
default:
return {}
}
})()
// Optimistic
optimisticCall(id, value, argument)
return apiCall({
id,
...apiArgs,
credentials: useOAuthStore().token,
})
.then(({ data: status, timestamp }) => {
this.addNewStatuses({
statuses: [status],
timestamp,
})
})
.catch((error) => {
optimisticCall(id, oldValue, argument)
console.error('Interact Error', error)
useInterfaceStore().pushGlobalNotice({
level: 'error',
messageKey: 'status.interact_error',
messageArgs: [error],
timeout: 5000,
})
})
},
/// Favorite
favorite(id) {
return this.requestInteract({
name: 'favorite',
id,
apiCall: favorite,
optimisticCall: this.setFavorited,
value: true,
})
},
unfavorite(id) {
return this.requestInteract({
name: 'favorite',
id,
apiCall: unfavorite,
optimisticCall: this.setFavorited,
value: false,
})
},
setFavorited(id, value) {
const newStatus = this.allStatuses.get(id)
if (newStatus.favorited !== value) {
if (value) {
newStatus.fave_num++
} else {
newStatus.fave_num--
}
}
newStatus.favorited = value
},
/// Reprööt
retweet(id) {
return this.requestInteract({
name: 'retweet',
id,
apiCall: retweet,
optimisticCall: this.setRetweeted,
value: true,
})
},
unretweet(id) {
return this.requestInteract({
name: 'retweet',
id,
apiCall: unretweet,
optimisticCall: this.setRetweeted,
value: false,
})
},
setRetweeted(id, value) {
const newStatus = this.allStatuses.get(id)
if (newStatus.repeated !== value) {
if (value) {
newStatus.repeat_num++
} else {
newStatus.repeat_num--
}
}
newStatus.repeated = value
},
// React
reactWithEmoji(id, emoji) {
return this.requestInteract({
name: 'emoji',
id,
apiCall: reactWithEmoji,
optimisticCall: this.setOwnReaction,
argument: emoji,
value: true,
})
},
unreactWithEmoji(id, emoji) {
return this.requestInteract({
name: 'emoji',
id,
apiCall: unreactWithEmoji,
optimisticCall: this.setOwnReaction,
argument: emoji,
value: false,
})
},
setOwnReaction(id, value, emoji) {
const currentUser = useUsersStore().currentUser
const status = this.allStatuses.get(id)
const reactionIndex = status.emoji_reactions.findIndex(
(react) => react.name === emoji,
)
const reactionPresent = reactionIndex >= 0
if (!value && !reactionPresent) return
const reaction = status.emoji_reactions[reactionIndex] || {
name: emoji,
count: 0,
accounts: [],
}
const count = value ? reaction.count + 1 : reaction.count - 1
const accounts = value
? [...reaction.accounts, currentUser]
: reaction.accounts.filter((acc) => acc.id !== currentUser.id)
const newReaction = {
...reaction,
count,
me: value,
accounts,
}
if (reactionPresent && count > 0) {
status.emoji_reactions[reactionIndex] = newReaction
} else if (count === 0) {
status.emoji_reactions = status.emoji_reactions.filter(
(r) => r.name !== emoji,
)
} else {
status.emoji_reactions.push(newReaction)
}
},
/// Bookmark
bookmark(id, bookmark_folder_id) {
return this.requestInteract({
name: 'bookmark',
id,
apiCall: bookmarkStatus,
optimisticCall: this.setBookmarked,
argument: bookmark_folder_id,
value: true,
})
},
unbookmark(id) {
return this.requestInteract({
name: 'bookmark',
id,
apiCall: unbookmarkStatus,
optimisticCall: this.setBookmarked,
value: false,
})
},
setBookmarked(id, value, bookmark_folder_id) {
const status = this.allStatuses.get(id)
status.bookmarked = value
// When unbookmarking we don't specify folder so we wanna keep
// reference to the folder even when setting bookmarked to false
// the proper reference will be updated when api call resolves
if (bookmark_folder_id) {
status.bookmark_folder_id = value ? bookmark_folder_id : null
}
},
/// Mute
muteConversation(id) {
return this.requestInteract({
name: 'mute',
id,
apiCall: muteConversation,
optimisticCall: this.setMutedStatus,
value: true,
})
},
unmuteConversation(id) {
return this.requestInteract({
name: 'mute',
id,
apiCall: unmuteConversation,
optimisticCall: this.setMutedStatus,
value: false,
})
},
setMutedStatus(id, value) {
// Setting thread_muted flag on all other known statuses
// belonging to same conversation
const newStatus = this.allStatuses.get(id)
newStatus.thread_muted = value
if (newStatus.thread_muted !== undefined) {
this.conversations
.get(newStatus.statusnet_conversation_id)
.forEach((statusId) => {
this.allStatuses.get(statusId).thread_muted = value
})
}
},
/// Pin
pinStatus(id) {
return this.requestInteract({
name: 'pin',
id,
apiCall: pinOwnStatus,
optimisticCall: () => {
/* no-op */
},
value: true,
})
},
unpinStatus(id) {
return this.requestInteract({
name: 'pin',
id,
apiCall: unpinOwnStatus,
optimisticCall: () => {
/* no-op */
},
value: false,
})
},
/// Delete
deleteStatus(id) {
return deleteStatus({
id,
credentials: useOAuthStore().token,
})
.then(() => {
this.setDeleted(id)
})
.catch((e) => {
useInterfaceStore().pushGlobalNotice({
level: 'error',
messageKey: 'status.delete_error',
messageArgs: [e.message],
timeout: 5000,
})
})
},
setDeleted(id) {
const newStatus = this.allStatuses.get(id)
if (newStatus) newStatus.deleted = true
},
// For when blocking a user
wipeUserStatuses(userId) {
const removed = this.statusesPerUser.get(userId) ?? new Set()
removed.forEach((statusId) => {
const status = this.allStatuses.get(statusId)
this.allStatuses.delete(statusId)
const conversationSet = this.conversations.get(
status.statusnet_conversation_id,
)
conversationSet.delete(statusId)
if (conversationSet.size === 0) {
this.conversations.delete(status.statusnet_conversation_id)
}
})
this.statusesPerUser.delete(userId)
return removed
},
},
})
diff --git a/test/unit/specs/stores/statuses.spec.js b/test/unit/specs/stores/statuses.spec.js
index 9be47145cc..31bf517d25 100644
--- a/test/unit/specs/stores/statuses.spec.js
+++ b/test/unit/specs/stores/statuses.spec.js
@@ -1,634 +1,651 @@
import { createTestingPinia } from '@pinia/testing'
import { snakeCase } from 'lodash'
import { setActivePinia } from 'pinia'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useStreamingStore } from 'src/stores/streaming.js'
import { useUsersStore } from 'src/stores/users.js'
import * as PUBLIC_API from 'src/api/public.js'
import * as USER_API from 'src/api/user.js'
const userId = '1'
const userScreenName = 'user'
const userName = 'Guy'
const userUrl = 'http://localhost/user'
const mockMastoAPIUser = ({
screen_name = userScreenName,
name = userName,
url = userUrl,
id = userId,
} = {}) => ({
id,
acct: screen_name,
display_name: name,
fields: [],
avatar: '',
url,
pleroma: {
emoji_reactions: [],
},
})
const mockUser = ({
screen_name = userScreenName,
id = userId,
name = userName,
url = userUrl,
} = {}) => ({
_original: mockMastoAPIUser({
screen_name,
id,
name,
url,
}),
id,
name,
screen_name,
url,
relationship: undefined,
})
const mockStatus = ({
id = '1',
text,
type = 'status',
statusUser = mockUser(),
} = {}) => ({
id,
user: statusUser,
name: 'status',
text: text ?? `Text number ${id}`,
uri: '',
type,
attentions: [],
statusnet_conversation_id: 'c1',
emoji_reactions: [],
})
const mockMastoAPIStatus = ({
id = '1',
text,
type = 'status',
statusUser = mockMastoAPIUser(),
} = {}) => ({
id,
account: statusUser,
name: 'status',
content: text ?? `Text number ${id}`,
uri: '',
type,
attentions: [],
statusnet_conversation_id: 'c1',
})
const DEFAULT_OPTIONS = (method = 'GET') => ({
method,
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})
describe('Statuses store', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})
it('init', () => {
const store = useStatusesStore()
const sub = vi.fn()
useStreamingStore().addSubscriber = sub
store.attachSocket()
expect(store.socket).to.not.be.null
expect(sub).to.have.been.called
})
it('resetStatuses', () => {
const store = useStatusesStore()
const statuses = [...new Array(20)].map((empty, index) =>
mockStatus({
id: 's' + index,
statusUser: mockUser({ id: 'u' + index }),
}),
)
store.attachSocket()
store.addNewStatuses({
statuses,
timestamp: 1,
})
store.resetStatuses()
expect(store.allStatuses).to.have.length(0)
})
describe('addNewStatuses', () => {
beforeEach(() => {
const usersStore = useUsersStore()
usersStore.addNewUsers = vi.fn().mockReturnValue([mockUser()])
})
it('adds the status to allStatuses', () => {
const store = useStatusesStore()
const usersStore = useUsersStore()
const status = mockStatus({ id: '1' })
store.addNewStatuses({
statuses: [status],
timestamp: 1,
})
expect(store.allStatuses).to.eql(new Map([['1', status]]))
expect(store.conversations).to.eql(new Map([['c1', new Set(['1'])]]))
expect(usersStore.addNewUsers).to.have.callCount(1)
})
it('splits retweets from their status and links them', () => {
const store = useStatusesStore()
const usersStore = useUsersStore()
const status = mockStatus({ id: '1' })
const retweet = mockStatus({
id: '2',
type: 'retweet',
user: mockUser({ id: '2' }),
})
retweet.type = 'retweet'
retweet.retweeted_status = status
store.addNewStatuses({
statuses: [retweet],
timestamp: 1,
})
expect(store.allStatuses).to.eql(
new Map([
['1', status],
['2', retweet],
]),
)
expect(usersStore.addNewUsers).to.have.callCount(2)
})
it('splits quotes from their status and links them', () => {
const store = useStatusesStore()
const usersStore = useUsersStore()
const status = mockStatus({ id: '1' })
const quote = mockStatus({ id: '2' })
quote.quote = status
store.addNewStatuses({
statuses: [quote],
timestamp: 1,
})
expect(store.allStatuses).to.eql(
new Map([
['1', status],
['2', quote],
]),
)
expect(usersStore.addNewUsers).to.have.callCount(2)
})
it('replaces existing statuses with the same id', () => {
const store = useStatusesStore()
const usersStore = useUsersStore()
const status = mockStatus({ id: '1' })
const modStatus = mockStatus({ id: '1', text: 'something else' })
store.addNewStatuses({
statuses: [status],
timestamp: 1991,
})
expect(store.allStatuses).to.eql(new Map([['1', status]]))
store.addNewStatuses({
statuses: [modStatus],
timestamp: 2000,
})
expect(store.allStatuses).to.eql(new Map([['1', modStatus]]))
expect(usersStore.addNewUsers).to.have.callCount(2)
})
it('handles edits', () => {
const store = useStatusesStore()
const usersStore = useUsersStore()
const status = mockStatus({ id: '1' })
const modStatus = mockStatus({
id: '1',
text: 'something else',
type: 'edit',
})
store.addNewStatuses({
statuses: [status],
timestamp: 1991,
})
expect(store.allStatuses).to.eql(new Map([['1', status]]))
store.addNewStatuses({
statuses: [modStatus],
timestamp: 2000,
})
expect(store.allStatuses).to.eql(new Map([['1', modStatus]]))
expect(usersStore.addNewUsers).to.have.callCount(2)
})
it('ignores updates with older timestamp', () => {
const store = useStatusesStore()
const usersStore = useUsersStore()
const status = mockStatus({ id: '1' })
const modStatus = mockStatus({ id: '1', text: 'something else' })
store.addNewStatuses({
statuses: [status],
timestamp: 2000,
})
expect(store.allStatuses).to.eql(new Map([['1', status]]))
store.addNewStatuses({
statuses: [modStatus],
timestamp: 1991,
})
expect(store.allStatuses).to.eql(new Map([['1', status]]))
expect(usersStore.addNewUsers).to.have.callCount(2)
})
it('calls useUsersStore().addNewUsers() even on older timestamp', () => {
const store = useStatusesStore()
const usersStore = useUsersStore()
const status = mockStatus({ id: '1' })
const modStatus = mockStatus({ id: '1', text: 'something else' })
store.addNewStatuses({
statuses: [status],
timestamp: 2000,
})
store.addNewStatuses({
statuses: [modStatus],
timestamp: 1991,
})
expect(usersStore.addNewUsers).to.have.callCount(2)
})
})
describe('fetchers', () => {
it.each([
[
'StatusSource',
{
content_type: 'text/plain',
text: 'Text',
spoiler_text: 'Text',
},
],
[
'EmojiReactions',
[
{
- accounts: [mockMastoAPIUser()],
+ accounts: [mockMastoAPIUser({ id: 'u1' }), mockMastoAPIUser({ id: 'u2' })],
count: 1,
me: false,
name: 'cofe',
url: '',
},
],
],
- ['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(
new Response(JSON.stringify(mockedResponse), {
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', mockFetch)
+ const addNewUsers = vi.spyOn(useUsersStore(), 'addNewUsers')
let urlKey
let prefix = 'MASTODON'
if (group === 'Favs') {
urlKey = 'STATUS_FAVORITEDBY'
} else if (group === 'Repeats') {
urlKey = 'STATUS_REBLOGGEDBY'
} else {
urlKey = snakeCase(group).toUpperCase()
}
if (group === 'EmojiReactions') {
prefix = 'PLEROMA'
}
const url = PUBLIC_API[`${prefix}_${urlKey}_URL`]('id')
const store = useStatusesStore()
const status = mockStatus({ id: 'id' })
store.addNewStatuses({
statuses: [status],
timestamp: 2000,
})
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)
} else {
expect(result).to.eql(mockedResponse)
}
})
it('fetchFavsAndRepeats', async () => {
const store = useStatusesStore()
store.fetchFavs = vi.fn().mockResolvedValue(async () => {
/* no-op */
})
store.fetchRepeats = vi.fn().mockResolvedValue(async () => {
/* no-op */
})
await store.fetchFavsAndRepeats('id')
expect(store.fetchFavs).to.have.been.calledWith('id')
expect(store.fetchRepeats).to.have.been.calledWith('id')
})
it('fetchStatus', async () => {
const mockFetch = vi.fn()
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify(mockMastoAPIStatus()), {
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', mockFetch)
const store = useStatusesStore()
await store.fetchStatus('id')
expect(mockFetch).to.have.been.calledWith(
PUBLIC_API.MASTODON_STATUS_URL('id'),
DEFAULT_OPTIONS(),
)
expect(store.allStatuses).to.have.length(1)
})
})
describe('interactions', () => {
it.each([
['favorite', 'MASTODON_FAVORITE_URL'],
['unfavorite', 'MASTODON_UNFAVORITE_URL'],
['retweet', 'MASTODON_RETWEET_URL'],
['unretweet', 'MASTODON_UNRETWEET_URL'],
['reactWithEmoji', 'PLEROMA_EMOJI_REACT_URL', 'PUT'],
['unreactWithEmoji', 'PLEROMA_EMOJI_UNREACT_URL', 'DELETE'],
[
'bookmark',
'MASTODON_BOOKMARK_STATUS_URL',
undefined,
{ folder_id: 'argument' },
],
['unbookmark', 'MASTODON_UNBOOKMARK_STATUS_URL'],
['pinStatus', 'MASTODON_PIN_OWN_STATUS_URL'],
['unpinStatus', 'MASTODON_UNPIN_OWN_STATUS_URL'],
['muteConversation', 'MASTODON_MUTE_CONVERSATION_URL'],
['unmuteConversation', 'MASTODON_UNMUTE_CONVERSATION_URL'],
['deleteStatus', 'MASTODON_DELETE_URL', 'DELETE'],
])('%s - api call', async (interaction, urlKey, method = 'POST', body) => {
const url = USER_API[urlKey]('1', 'argument')
const status = mockStatus()
const store = useStatusesStore()
store.addNewStatuses({
statuses: [status],
timestamp: 1,
})
const mockFetch = vi.fn()
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify(mockMastoAPIStatus({ text: 'Updated' })), {
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', mockFetch)
await store[interaction]('1', 'argument')
const expectedArg = DEFAULT_OPTIONS(method)
if (body) {
expectedArg.body = JSON.stringify(body)
}
expect(mockFetch).to.have.been.calledWith(url, expectedArg)
if (interaction === 'deleteStatus') {
expect(store.allStatuses.get('1').deleted).to.be.true
} else {
expect(store.allStatuses.get('1').text).to.eql('Updated')
}
})
const optimismInteractions = [
['favorite', 'favorited', 'fave_num'],
['retweet', 'repeated', 'repeat_num'],
['bookmark', 'bookmarked'],
['muteConversation', 'thread_muted'],
]
.map(([method, property, count]) => [
[method, property, count],
['un' + method, property, count],
])
.flat()
it.each(
optimismInteractions,
)('%s - optimism call', async (method, property, count) => {
// Prepare our status
const status = mockStatus()
const negate = method.startsWith('un')
const oldCount = 9
const newCount = negate ? 8 : 10
status[property] = negate
if (count) {
status[count] = oldCount
}
if (method === 'unbookmark') {
status.bookmark_folder_id = 'argument'
}
// Insert it
const store = useStatusesStore()
store.addNewStatuses({
statuses: [status],
timestamp: 1,
})
const mockFetch = vi.fn()
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify(mockMastoAPIStatus({ text: 'Updated' })), {
headers: { 'Content-Type': 'application/json' },
}),
)
expect(store.allStatuses.get('1')).to.have.property(property, negate)
if (count) {
expect(store.allStatuses.get('1')).to.have.property(count, oldCount)
}
vi.stubGlobal('fetch', mockFetch)
store[method]('1', 'argument')
expect(store.allStatuses.get('1')).to.have.property(property, !negate)
if (count) {
expect(store.allStatuses.get('1')).to.have.property(count, newCount)
}
if (property === 'bookmarked') {
expect(store.allStatuses.get('1')).to.have.property(
'bookmark_folder_id',
'argument',
)
}
})
it.each(
optimismInteractions,
)('%s - optimism fail', async (method, property, count) => {
// Prepare our status
const status = mockStatus()
const negate = method.startsWith('un')
const oldCount = 9
status[property] = negate
if (count) {
status[count] = oldCount
}
if (method === 'unbookmark') {
status.bookmark_folder_id = 'argument'
}
// Insert it
const store = useStatusesStore()
store.addNewStatuses({
statuses: [status],
timestamp: 1,
})
const mockFetch = vi.fn()
mockFetch.mockRejectedValueOnce(new Error('Failure!'))
expect(store.allStatuses.get('1')).to.have.property(property, negate)
if (count) {
expect(store.allStatuses.get('1')).to.have.property(count, oldCount)
}
vi.stubGlobal('fetch', mockFetch)
await store[method]('1', 'argument')
expect(store.allStatuses.get('1')).to.have.property(property, negate)
if (count) {
expect(store.allStatuses.get('1')).to.have.property(count, oldCount)
}
// Failing 'bookmark' method SHOULD clear folder id
if (method === 'unbookmark') {
expect(store.allStatuses.get('1')).to.have.property(
'bookmark_folder_id',
'argument',
)
}
})
it.each([
['reactWithEmoji', 0],
['unreactWithEmoji', 1],
['reactWithEmoji', 1],
['unreactWithEmoji', 2],
])('%s count: %s - optimism call', async (method, oldCount) => {
// Prepare our status
const status = mockStatus()
const negate = method.startsWith('un')
const newCount = negate ? oldCount - 1 : oldCount + 1
useUsersStore().currentUser = mockUser()
if (oldCount > 0) {
const reactors = [...new Array(oldCount)].map((empty, index) =>
mockUser({ id: 'o' + index }),
)
if (negate) {
// Replace one of reactors with ourselves
reactors[0] = useUsersStore().currentUser
}
status.emoji_reactions = [
{
name: 'hyperlol',
count: oldCount,
accounts: reactors,
},
]
} else {
status.emoji_reactions = []
}
// Insert it
const store = useStatusesStore()
store.addNewStatuses({
statuses: [status],
timestamp: 1,
})
const mockFetch = vi.fn()
mockFetch.mockResolvedValueOnce(
new Response(JSON.stringify(mockMastoAPIStatus({ text: 'Updated' })), {
headers: { 'Content-Type': 'application/json' },
}),
)
vi.stubGlobal('fetch', mockFetch)
const expected = store.allStatuses.get('1')
expect(expected.emoji_reactions).to.have.length(oldCount === 0 ? 0 : 1)
if (oldCount !== 0) {
expect(expected.emoji_reactions[0].name).to.eql('hyperlol')
expect(expected.emoji_reactions[0].count).to.eql(oldCount)
expect(expected.emoji_reactions[0].accounts).to.have.length(oldCount)
}
store[method]('1', 'hyperlol')
expect(expected.emoji_reactions).to.have.length(newCount === 0 ? 0 : 1)
if (newCount !== 0) {
expect(expected.emoji_reactions[0].name).to.eql('hyperlol')
expect(expected.emoji_reactions[0].count).to.eql(newCount)
expect(expected.emoji_reactions[0].accounts).to.have.length(newCount)
}
})
})
it('wipeUserStatuses', () => {
const store = useStatusesStore()
const statuses = [...new Array(20)].map((empty, index) =>
mockStatus({
id: 's' + index,
statusUser: mockUser({ id: 'u' + index }),
}),
)
store.addNewStatuses({
statuses,
timestamp: 1,
})
const result = store.wipeUserStatuses('u19')
expect(store.allStatuses).to.have.length(19)
expect(result).to.eql(new Set(['s19']))
})
})
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Aug 30, 9:41 AM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1737889
Default Alt Text
(34 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline
Log In to Comment