2026-08-10 22:26:22 +03:00
|
|
|
import { first, last, maxBy, minBy } from 'lodash'
|
2026-08-10 20:18:50 +03:00
|
|
|
import { defineStore } from 'pinia'
|
|
|
|
|
|
|
|
|
|
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
|
|
|
|
import { useInterfaceStore } from 'src/stores/interface.js'
|
|
|
|
|
import { useOAuthStore } from 'src/stores/oauth.js'
|
|
|
|
|
import { useUsersStore } from 'src/stores/users.js'
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
fetchEmojiReactions,
|
|
|
|
|
fetchFavoritedByUsers,
|
|
|
|
|
fetchPinnedStatuses,
|
|
|
|
|
fetchRebloggedByUsers,
|
|
|
|
|
fetchScrobbles,
|
|
|
|
|
fetchStatus,
|
|
|
|
|
fetchStatusHistory,
|
|
|
|
|
fetchStatusSource,
|
|
|
|
|
search2,
|
|
|
|
|
} from 'src/api/public.js'
|
|
|
|
|
import {
|
|
|
|
|
bookmarkStatus,
|
|
|
|
|
deleteStatus,
|
|
|
|
|
favorite,
|
|
|
|
|
muteConversation,
|
|
|
|
|
pinOwnStatus,
|
|
|
|
|
reactWithEmoji,
|
|
|
|
|
retweet,
|
|
|
|
|
unbookmarkStatus,
|
|
|
|
|
unfavorite,
|
|
|
|
|
unmuteConversation,
|
|
|
|
|
unpinOwnStatus,
|
|
|
|
|
unreactWithEmoji,
|
|
|
|
|
unretweet,
|
|
|
|
|
} from 'src/api/user.js'
|
|
|
|
|
|
2026-08-10 22:26:22 +03:00
|
|
|
const emptyTl = (userId) => ({
|
2026-08-10 20:18:50 +03:00
|
|
|
statuses: new Map(),
|
|
|
|
|
faves: [],
|
|
|
|
|
visibleStatuses: new Map(),
|
|
|
|
|
newStatusCount: 0,
|
|
|
|
|
maxId: '',
|
|
|
|
|
minId: '',
|
|
|
|
|
minVisibleId: 0,
|
|
|
|
|
loading: false,
|
|
|
|
|
followers: [],
|
|
|
|
|
friends: [],
|
|
|
|
|
userId,
|
|
|
|
|
flushMarker: 0,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
export const defaultState = () => ({
|
|
|
|
|
allStatuses: new Map(),
|
|
|
|
|
timestamps: new WeakMap(),
|
|
|
|
|
scrobblesNextFetch: {},
|
|
|
|
|
conversations: new Map(),
|
|
|
|
|
favorites: new Set(),
|
|
|
|
|
timelines: {
|
|
|
|
|
mentions: emptyTl(),
|
|
|
|
|
public: emptyTl(),
|
|
|
|
|
user: emptyTl(),
|
|
|
|
|
userPinned: emptyTl(),
|
|
|
|
|
favorites: emptyTl(),
|
|
|
|
|
media: emptyTl(),
|
|
|
|
|
publicAndExternal: emptyTl(),
|
|
|
|
|
friends: emptyTl(),
|
|
|
|
|
tag: emptyTl(),
|
|
|
|
|
dms: emptyTl(),
|
|
|
|
|
bookmarks: emptyTl(),
|
|
|
|
|
list: emptyTl(),
|
|
|
|
|
bubble: emptyTl(),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const getLatestScrobble = (user) => {
|
|
|
|
|
const scrobblesSupport =
|
|
|
|
|
useInstanceCapabilitiesStore().pleromaScrobblesAvailable
|
|
|
|
|
|
|
|
|
|
if (!scrobblesSupport || !user.name || user.id === 'undefined') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
this.scrobblesNextFetch[user.id] &&
|
|
|
|
|
this.scrobblesNextFetch[user.id] > Date.now()
|
|
|
|
|
) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.scrobblesNextFetch[user.id] = Date.now() + 24 * 60 * 60 * 1000
|
|
|
|
|
if (!scrobblesSupport) return
|
|
|
|
|
fetchScrobbles({ accountId: user.id })
|
|
|
|
|
.then(({ data: scrobbles }) => {
|
|
|
|
|
if (scrobbles?.error) {
|
|
|
|
|
useInstanceCapabilitiesStore().set('pleromaScrobblesAvailable', false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (scrobbles.length > 0) {
|
|
|
|
|
user.latestScrobble = scrobbles[0]
|
|
|
|
|
|
|
|
|
|
this.scrobblesNextFetch[user.id] = Date.now() + 60 * 1000
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((e) => {
|
|
|
|
|
console.warn('cannot fetch scrobbles', e)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const USER_TIMELINES = new Set(['user', 'userPinned', 'media'])
|
|
|
|
|
|
|
|
|
|
export const useStatusesStore = defineStore('statuses', {
|
|
|
|
|
state: defaultState,
|
|
|
|
|
actions: {
|
|
|
|
|
addNewStatuses({
|
|
|
|
|
statuses,
|
|
|
|
|
showImmediately = false,
|
|
|
|
|
timelineName,
|
|
|
|
|
user = {},
|
2026-08-10 22:26:22 +03:00
|
|
|
userId,
|
2026-08-10 20:18:50 +03:00
|
|
|
noIdUpdate = false,
|
|
|
|
|
pagination = {},
|
|
|
|
|
timestamp,
|
|
|
|
|
}) {
|
|
|
|
|
// Sanity check
|
|
|
|
|
if (!Array.isArray(statuses)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const timeline = this.timelines[timelineName]
|
|
|
|
|
|
|
|
|
|
if (timeline && !noIdUpdate && statuses.length > 0) {
|
|
|
|
|
this.updateTimelineExtremes(timeline, statuses, pagination)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
2026-08-10 22:26:22 +03:00
|
|
|
if (USER_TIMELINES.has(timelineName) && timeline.userId !== userId) {
|
2026-08-10 20:18:50 +03:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const addStatus = (data, showImmediately, addToTimeline = true) => {
|
|
|
|
|
getLatestScrobble(data.user)
|
|
|
|
|
|
2026-08-10 22:26:22 +03:00
|
|
|
const [status] = this.mergeOrAdd(this.allStatuses, data)
|
2026-08-10 20:18:50 +03:00
|
|
|
|
|
|
|
|
// Add to conversation
|
|
|
|
|
const conversations = this.conversations
|
|
|
|
|
const conversationId = status.statusnet_conversation_id
|
|
|
|
|
|
|
|
|
|
if (conversations.has(conversationId)) {
|
|
|
|
|
conversations.get(conversationId).set(status.id, status)
|
|
|
|
|
} else {
|
|
|
|
|
conversations.set(conversationId, new Map([[status.id, status]]))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We are mentioned in a post
|
|
|
|
|
if (
|
|
|
|
|
status.type === 'status' &&
|
|
|
|
|
status.attentions.some(({ id }) => id === user.id)
|
|
|
|
|
) {
|
|
|
|
|
const mentions = this.timelines.mentions
|
|
|
|
|
|
|
|
|
|
// Add the mention to the mentions timeline
|
|
|
|
|
if (timeline !== mentions) {
|
2026-08-10 22:26:22 +03:00
|
|
|
const [, isNew] = this.mergeOrAdd(mentions.statuses, data)
|
2026-08-10 20:18:50 +03:00
|
|
|
if (isNew) mentions.newStatusCount += 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status.visibility === 'direct') {
|
|
|
|
|
const dms = this.timelines.dms
|
|
|
|
|
|
2026-08-10 22:26:22 +03:00
|
|
|
const [, isNew] = this.mergeOrAdd(dms.statuses, data)
|
2026-08-10 20:18:50 +03:00
|
|
|
if (isNew) dms.newStatusCount += 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Some statuses should only be added to the global status repository.
|
|
|
|
|
if (timeline && addToTimeline) {
|
|
|
|
|
// Decide if we should treat the status as new for this timeline.
|
2026-08-10 22:26:22 +03:00
|
|
|
const [status, isNew] = this.mergeOrAdd(timeline.statuses, data)
|
|
|
|
|
if (isNew) {
|
|
|
|
|
if (showImmediately) {
|
|
|
|
|
// Add it directly to the visibleStatuses, don't change
|
|
|
|
|
// newStatusCount
|
|
|
|
|
timeline.visibleStatuses.set(status.id, status)
|
|
|
|
|
} else {
|
|
|
|
|
// Just change newStatuscount
|
|
|
|
|
timeline.newStatusCount += 1
|
|
|
|
|
}
|
2026-08-10 20:18:50 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status.quote) {
|
|
|
|
|
addStatus(
|
|
|
|
|
status.quote,
|
|
|
|
|
/* showImmediately = */ false,
|
|
|
|
|
/* addToTimeline = */ false,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return status
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const processors = {
|
|
|
|
|
status: (status) => {
|
|
|
|
|
addStatus(status, showImmediately)
|
|
|
|
|
},
|
|
|
|
|
edit: (status) => {
|
|
|
|
|
addStatus(status, showImmediately)
|
|
|
|
|
},
|
|
|
|
|
retweet: (status) => {
|
|
|
|
|
// RetweetedStatuses are never shown immediately
|
2026-08-10 22:26:22 +03:00
|
|
|
const retweetedStatus = addStatus(
|
|
|
|
|
status.retweeted_status,
|
|
|
|
|
false,
|
|
|
|
|
false,
|
|
|
|
|
)
|
2026-08-10 20:18:50 +03:00
|
|
|
|
|
|
|
|
let retweet
|
|
|
|
|
// If the retweeted status is already there, don't add the retweet
|
|
|
|
|
// to the timeline.
|
2026-08-10 22:26:22 +03:00
|
|
|
if (
|
|
|
|
|
[...(timeline?.statuses.values() ?? [])].some((s) => {
|
2026-08-10 20:18:50 +03:00
|
|
|
if (s.retweeted_status) {
|
|
|
|
|
return (
|
|
|
|
|
s.id === retweetedStatus.id ||
|
|
|
|
|
s.retweeted_status.id === retweetedStatus.id
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
return s.id === retweetedStatus.id
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
) {
|
|
|
|
|
// Already have it visible (either as the original or another RT), don't add to timeline, don't show.
|
|
|
|
|
retweet = addStatus(status, false, false)
|
|
|
|
|
} else {
|
|
|
|
|
retweet = addStatus(status, showImmediately)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
retweet.retweeted_status = retweetedStatus
|
|
|
|
|
},
|
|
|
|
|
favorite: (favorite) => {
|
|
|
|
|
// Only update if this is a new favorite.
|
|
|
|
|
// Ignore our own favorites because we get info about likes as response to like request
|
|
|
|
|
if (!this.favorites.has(favorite.id)) {
|
|
|
|
|
this.favorites.add(favorite.id)
|
|
|
|
|
|
|
|
|
|
const status = this.allStatuses.get(favorite.in_reply_to_status_id)
|
|
|
|
|
|
|
|
|
|
if (status) {
|
|
|
|
|
// This is our favorite, so the relevant bit.
|
|
|
|
|
if (favorite.user.id === user.id) {
|
|
|
|
|
status.favorited = true
|
|
|
|
|
} else {
|
|
|
|
|
status.fave_num += 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return status
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
follow: () => {
|
|
|
|
|
// NOOP, it is known status but we don't do anything about it for now
|
|
|
|
|
},
|
|
|
|
|
default: (unknown) => {
|
|
|
|
|
console.warn('unknown status type', unknown)
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
statuses.forEach((status) => {
|
|
|
|
|
const type = status.type
|
|
|
|
|
const processor = processors[type] ?? processors.default
|
|
|
|
|
processor(status)
|
|
|
|
|
})
|
|
|
|
|
},
|
2026-08-10 22:26:22 +03:00
|
|
|
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,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
...neu,
|
|
|
|
|
user,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
map.set(newStatus.id, newStatus)
|
|
|
|
|
|
|
|
|
|
this.timestamps.set(newStatus, timestamp)
|
|
|
|
|
|
|
|
|
|
return [map.get(newStatus.id), true]
|
|
|
|
|
},
|
2026-08-10 20:18:50 +03:00
|
|
|
fetchStatus(id) {
|
|
|
|
|
return fetchStatus({ id }).then(({ data: status, timestamp }) =>
|
|
|
|
|
this.addNewStatuses({ statuses: [status], timestamp }),
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
fetchPinnedStatuses(userId) {
|
|
|
|
|
return fetchPinnedStatuses({
|
|
|
|
|
id: userId,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
}).then(({ data: statuses, timestamp }) =>
|
|
|
|
|
this.addNewStatuses({
|
|
|
|
|
statuses,
|
|
|
|
|
timeline: 'userPinned',
|
|
|
|
|
userId,
|
|
|
|
|
showImmediately: true,
|
|
|
|
|
noIdUpdate: true,
|
|
|
|
|
timestamp,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
fetchStatusSource(id) {
|
|
|
|
|
return fetchStatusSource({
|
|
|
|
|
id,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
}).then(({ data }) => data)
|
|
|
|
|
},
|
|
|
|
|
fetchStatusHistory(status) {
|
|
|
|
|
return fetchStatusHistory({ status }).then(({ data }) => data)
|
|
|
|
|
},
|
|
|
|
|
fetchEmojiReactionsBy(id) {
|
|
|
|
|
return fetchEmojiReactions({
|
|
|
|
|
id,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
}).then(({ data: emojiReactions }) => {
|
|
|
|
|
this.addEmojiReactionsBy({
|
|
|
|
|
id,
|
|
|
|
|
emojiReactions,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
fetchFavs(id) {
|
|
|
|
|
return fetchFavoritedByUsers({
|
|
|
|
|
id,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
}).then(({ data: favoritedByUsers }) =>
|
|
|
|
|
this.addFavs({
|
|
|
|
|
id,
|
|
|
|
|
favoritedByUsers,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
fetchRepeats(id) {
|
|
|
|
|
return fetchRebloggedByUsers({
|
|
|
|
|
id,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
}).then(({ data: rebloggedByUsers }) =>
|
|
|
|
|
this.addRepeats({
|
|
|
|
|
id,
|
|
|
|
|
rebloggedByUsers,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
fetchFavsAndRepeats(id) {
|
2026-08-10 22:26:22 +03:00
|
|
|
return Promise.all([this.fetchFavs(id), this.fetchRepeats(id)])
|
2026-08-10 20:18:50 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Updates
|
|
|
|
|
addRepeats({ id, rebloggedByUsers }) {
|
|
|
|
|
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,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
addFavs({ id, favoritedByUsers }) {
|
|
|
|
|
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,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
addEmojiReactionsBy({ id, emojiReactions }) {
|
|
|
|
|
const status = this.allStatuses.get(id)
|
|
|
|
|
status.emoji_reactions = emojiReactions
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Queues & Timeline manip
|
|
|
|
|
updateTimelineExtremes(timeline, statuses, pagination = {}) {
|
|
|
|
|
// Can't use Math.min/max because it doesn't work with string (duh)
|
|
|
|
|
const minNew = pagination.maxId ?? minBy(statuses, 'id').id ?? ''
|
|
|
|
|
const maxNew = pagination.minId ?? maxBy(statuses, 'id').id ?? ''
|
|
|
|
|
|
2026-08-10 22:26:22 +03:00
|
|
|
const newer = maxNew > timeline.maxId || timeline.maxId === ''
|
|
|
|
|
const older = minNew < timeline.minId || timeline.minId === ''
|
2026-08-10 20:18:50 +03:00
|
|
|
|
|
|
|
|
if (newer) {
|
|
|
|
|
timeline.maxId = maxNew
|
|
|
|
|
}
|
|
|
|
|
if (older) {
|
|
|
|
|
timeline.minId = minNew
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
showNewStatuses(timelineName) {
|
|
|
|
|
const timeline = this.timelines[timelineName]
|
|
|
|
|
|
|
|
|
|
timeline.newStatusCount = 0
|
|
|
|
|
|
2026-08-10 22:26:22 +03:00
|
|
|
timeline.visibleStatuses = new Map(
|
|
|
|
|
[...timeline.statuses.entries()].slice(0, 50),
|
|
|
|
|
)
|
|
|
|
|
timeline.minVisibleId = last(timeline.visibleStatuses.keys())
|
2026-08-10 20:18:50 +03:00
|
|
|
timeline.minId = ''
|
|
|
|
|
timeline.maxId = ''
|
|
|
|
|
this.updateTimelineExtremes(timeline, [...timeline.statuses.values()])
|
|
|
|
|
},
|
|
|
|
|
resetStatuses() {
|
|
|
|
|
const emptyState = defaultState()
|
|
|
|
|
|
|
|
|
|
Object.entries(emptyState).forEach(([key, value]) => {
|
|
|
|
|
this[key] = value
|
|
|
|
|
})
|
|
|
|
|
},
|
2026-08-10 22:26:22 +03:00
|
|
|
clearTimeline({ timeline, excludeUserId = false }) {
|
|
|
|
|
const userId = excludeUserId ? this.timelines[timeline].userId : undefined
|
|
|
|
|
this.timelines[timeline] = emptyTl(userId)
|
2026-08-10 20:18:50 +03:00
|
|
|
},
|
|
|
|
|
queueFlush({ timeline, id }) {
|
|
|
|
|
this.timelines[timeline].flushMarker = id
|
|
|
|
|
},
|
|
|
|
|
queueFlushAll() {
|
|
|
|
|
Object.keys(this.timelines).forEach((timeline) => {
|
|
|
|
|
this.timelines[timeline].flushMarker = this.timelines[timeline].maxId
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Actions
|
|
|
|
|
/// Favorite
|
|
|
|
|
favorite(id) {
|
|
|
|
|
this.setFavorited({ id, value: true })
|
|
|
|
|
|
|
|
|
|
favorite({
|
|
|
|
|
id: status.id,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
}).then(({ data: status, timestamp }) => {
|
|
|
|
|
this.addNewStatuses({
|
|
|
|
|
statuses: [status],
|
|
|
|
|
timestamp,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
unfavorite(id) {
|
|
|
|
|
this.setFavorited({ id, value: false })
|
|
|
|
|
|
|
|
|
|
unfavorite({
|
|
|
|
|
id,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
}).then(({ data: status, timestamp }) => {
|
|
|
|
|
this.addNewStatuses({
|
|
|
|
|
statuses: [status],
|
|
|
|
|
timestamp,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
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) {
|
|
|
|
|
this.setRetweeted({ id, value: true })
|
|
|
|
|
|
|
|
|
|
retweet({
|
|
|
|
|
id,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
}).then(({ data: status, timestamp }) => {
|
|
|
|
|
this.addNewStatuses({
|
|
|
|
|
statuses: [status],
|
|
|
|
|
timestamp,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
unretweet(id) {
|
|
|
|
|
this.setRetweeted({ id, value: false })
|
|
|
|
|
|
|
|
|
|
unretweet({
|
|
|
|
|
id,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
}).then(({ data: status, timestamp }) => {
|
|
|
|
|
this.addNewStatuses({
|
|
|
|
|
statuses: [status],
|
|
|
|
|
timestamp,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
setRetweeted({ statusId, value }) {
|
|
|
|
|
const newStatus = this.allStatuses.get(statusId)
|
|
|
|
|
|
|
|
|
|
if (newStatus.repeated !== value) {
|
|
|
|
|
if (value) {
|
|
|
|
|
newStatus.repeat_num++
|
|
|
|
|
} else {
|
|
|
|
|
newStatus.repeat_num--
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newStatus.repeated = value
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// React
|
|
|
|
|
reactWithEmoji(id, emoji) {
|
|
|
|
|
this.addOwnReaction({ id, emoji })
|
|
|
|
|
|
|
|
|
|
reactWithEmoji({
|
|
|
|
|
id,
|
|
|
|
|
emoji,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
}).then(({ data: status, timestamp }) => {
|
|
|
|
|
this.addNewStatuses({
|
|
|
|
|
statuses: [status],
|
|
|
|
|
timestamp,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
unreactWithEmoji(id, emoji) {
|
|
|
|
|
this.removeOwnReaction({ id, emoji })
|
|
|
|
|
|
|
|
|
|
unreactWithEmoji({
|
|
|
|
|
id,
|
|
|
|
|
emoji,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
}).then(({ data: status, timestamp }) => {
|
|
|
|
|
this.addNewStatuses({
|
|
|
|
|
statuses: [status],
|
|
|
|
|
timestamp,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
addOwnReaction(id, emoji) {
|
|
|
|
|
const currentUser = useUsersStore().currentUser
|
|
|
|
|
const status = this.allStatuses.get(id)
|
|
|
|
|
const reactionIndex = status.emoji_reactions.findIndex(
|
|
|
|
|
(react) => react.name === emoji,
|
|
|
|
|
)
|
|
|
|
|
const reaction = status.emoji_reactions[reactionIndex] || {
|
|
|
|
|
name: emoji,
|
|
|
|
|
count: 0,
|
|
|
|
|
accounts: [],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newReaction = {
|
|
|
|
|
...reaction,
|
|
|
|
|
count: reaction.count + 1,
|
|
|
|
|
me: true,
|
|
|
|
|
accounts: [...reaction.accounts, currentUser],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update count of existing reaction if it exists, otherwise append at the end
|
|
|
|
|
if (reactionIndex >= 0) {
|
|
|
|
|
status.emoji_reactions[reactionIndex] = newReaction
|
|
|
|
|
} else {
|
|
|
|
|
status.emoji_reactions.push(newReaction)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
removeOwnReaction(id, emoji) {
|
|
|
|
|
const currentUser = useUsersStore().currentUser
|
|
|
|
|
const status = this.allStatuses.get(id)
|
|
|
|
|
const reactionIndex = status.emoji_reactions.findIndex(
|
|
|
|
|
(react) => react.name === emoji,
|
|
|
|
|
)
|
|
|
|
|
if (reactionIndex < 0) return
|
|
|
|
|
|
|
|
|
|
const reaction = status.emoji_reactions[reactionIndex]
|
|
|
|
|
const accounts = reaction.accounts || []
|
|
|
|
|
|
|
|
|
|
const newReaction = {
|
|
|
|
|
...reaction,
|
|
|
|
|
count: reaction.count - 1,
|
|
|
|
|
me: false,
|
|
|
|
|
accounts: accounts.filter((acc) => acc.id !== currentUser.id),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (newReaction.count > 0) {
|
|
|
|
|
status.emoji_reactions[reactionIndex] = newReaction
|
|
|
|
|
} else {
|
|
|
|
|
status.emoji_reactions = status.emoji_reactions.filter(
|
|
|
|
|
(r) => r.name !== emoji,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/// Bookmark
|
|
|
|
|
bookmark(id, bookmark_folder_id) {
|
|
|
|
|
this.setBookmarked({ id, value: true, bookmark_folder_id })
|
|
|
|
|
|
|
|
|
|
bookmarkStatus({
|
|
|
|
|
id,
|
|
|
|
|
folder_id: bookmark_folder_id,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
}).then(({ data: status, timestamp }) => {
|
|
|
|
|
this.addNewStatuses({
|
|
|
|
|
statuses: [status],
|
|
|
|
|
timestamp,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
unbookmark(id) {
|
|
|
|
|
this.setBookmarked({ id, value: false })
|
|
|
|
|
|
|
|
|
|
unbookmarkStatus({
|
|
|
|
|
id,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
}).then(({ data: status, timestamp }) => {
|
|
|
|
|
this.addNewStatuses({
|
|
|
|
|
statuses: [status],
|
|
|
|
|
timestamp,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
setBookmarked({ id, value, bookmark_folder_id }) {
|
|
|
|
|
const status = this.allStatuses.get(id)
|
2026-08-10 22:26:22 +03:00
|
|
|
status.bookmarked = value
|
|
|
|
|
status.bookmark_folder_id = value ? bookmark_folder_id : null
|
2026-08-10 20:18:50 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/// Mute
|
|
|
|
|
muteConversation(id) {
|
|
|
|
|
return muteConversation({
|
|
|
|
|
id,
|
|
|
|
|
credentials: useOAuthStore().token,
|
2026-08-10 22:26:22 +03:00
|
|
|
})
|
|
|
|
|
.then(({ data: status, timestamp }) => {
|
|
|
|
|
this.addNewStatuses({
|
|
|
|
|
statuses: [status],
|
|
|
|
|
timestamp,
|
|
|
|
|
})
|
|
|
|
|
return status
|
2026-08-10 20:18:50 +03:00
|
|
|
})
|
2026-08-10 22:26:22 +03:00
|
|
|
.then((status) => this.setMutedStatus(status))
|
2026-08-10 20:18:50 +03:00
|
|
|
},
|
|
|
|
|
unmuteConversation(id) {
|
|
|
|
|
return unmuteConversation({
|
|
|
|
|
id,
|
|
|
|
|
credentials: useOAuthStore().token,
|
2026-08-10 22:26:22 +03:00
|
|
|
})
|
|
|
|
|
.then(({ data: status, timestamp }) => {
|
|
|
|
|
this.addNewStatuses({
|
|
|
|
|
statuses: [status],
|
|
|
|
|
timestamp,
|
|
|
|
|
})
|
|
|
|
|
return status
|
2026-08-10 20:18:50 +03:00
|
|
|
})
|
2026-08-10 22:26:22 +03:00
|
|
|
.then((status) => this.setMutedStatus(status))
|
2026-08-10 20:18:50 +03:00
|
|
|
},
|
|
|
|
|
setMutedStatus({ id, thread_muted }) {
|
|
|
|
|
// Setting thread_muted flag on all other known statuses
|
|
|
|
|
// belonging to same conversation
|
|
|
|
|
const newStatus = this.allStatuses.get(id)
|
|
|
|
|
newStatus.thread_muted = thread_muted
|
|
|
|
|
|
|
|
|
|
if (newStatus.thread_muted !== undefined) {
|
2026-08-10 22:26:22 +03:00
|
|
|
this.conversations
|
|
|
|
|
.get(newStatus.statusnet_conversation_id)
|
|
|
|
|
.forEach((status) => {
|
2026-08-10 20:18:50 +03:00
|
|
|
status.thread_muted = thread_muted
|
2026-08-10 22:26:22 +03:00
|
|
|
})
|
2026-08-10 20:18:50 +03:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/// Pin
|
|
|
|
|
pinStatus(id) {
|
|
|
|
|
return pinOwnStatus({
|
|
|
|
|
id,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
}).then(({ data: status, timestamp }) => {
|
|
|
|
|
this.addNewStatuses({
|
|
|
|
|
statuses: [status],
|
|
|
|
|
timestamp,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
unpinStatus(id) {
|
|
|
|
|
return unpinOwnStatus({
|
|
|
|
|
id,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
}).then(({ data: status, timestamp }) => {
|
|
|
|
|
this.addNewStatuses({
|
|
|
|
|
statuses: [status],
|
|
|
|
|
timestamp,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/// Delete
|
|
|
|
|
deleteStatus(id) {
|
|
|
|
|
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
|
|
|
|
|
},
|
|
|
|
|
setManyDeleted(condition) {
|
|
|
|
|
this.allStatuses.values().forEach((status) => {
|
|
|
|
|
if (condition(status)) {
|
|
|
|
|
status.deleted = true
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Search
|
|
|
|
|
search({ q, resolve, limit, offset, following, type }) {
|
|
|
|
|
return search2({
|
|
|
|
|
q,
|
|
|
|
|
resolve,
|
|
|
|
|
limit,
|
|
|
|
|
offset,
|
|
|
|
|
following,
|
|
|
|
|
type,
|
|
|
|
|
credentials: useOAuthStore().token,
|
|
|
|
|
}).then((result) => {
|
|
|
|
|
const { data, ...rest } = result
|
|
|
|
|
useUsersStore().addNewUsers({
|
|
|
|
|
...rest,
|
|
|
|
|
data: data.accounts,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
useUsersStore().addNewUsers({
|
|
|
|
|
...rest,
|
|
|
|
|
data: data.statuses.map((s) => s.user).filter(Boolean),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
this.addNewStatuses({
|
|
|
|
|
statuses: data.statuses,
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-10 22:26:22 +03:00
|
|
|
data.statuses = data.statuses.map((s) => this.allStatuses.get(s.id))
|
2026-08-10 20:18:50 +03:00
|
|
|
return data
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Misc
|
|
|
|
|
removeUserStatuses({ timelineName, userId }) {
|
|
|
|
|
const timeline = this.timelines[timelineName]
|
|
|
|
|
|
2026-08-10 22:26:22 +03:00
|
|
|
timeline.statuses
|
2026-08-10 20:18:50 +03:00
|
|
|
.values()
|
|
|
|
|
.filter(({ user }) => user.id === userId)
|
|
|
|
|
.forEach(({ id }) => {
|
|
|
|
|
timeline.statuses.delete(id)
|
|
|
|
|
timeline.visibleStatuses.delete(id)
|
|
|
|
|
})
|
|
|
|
|
timeline.minVisibleId =
|
|
|
|
|
timeline.visibleStatuses.length > 0
|
2026-08-10 22:26:22 +03:00
|
|
|
? last(timeline.visibleStatuses).id
|
|
|
|
|
: 0
|
2026-08-10 20:18:50 +03:00
|
|
|
timeline.maxId =
|
|
|
|
|
timeline.statuses.length > 0 ? first(timeline.statuses).id : 0
|
|
|
|
|
},
|
|
|
|
|
setVirtualHeight({ statusId, height }) {
|
|
|
|
|
this.allStatuses.get(statusId).virtualHeight = height
|
|
|
|
|
},
|
|
|
|
|
updateStatusWithPoll({ id, poll }) {
|
|
|
|
|
const status = this.allStatuses.get(id)
|
|
|
|
|
status.poll = poll
|
|
|
|
|
},
|
2026-08-10 22:26:22 +03:00
|
|
|
setLoading({ timeline, value }) {
|
|
|
|
|
this.timelines[timeline].loading = value
|
2026-08-10 20:18:50 +03:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|