moderately usable state

This commit is contained in:
Henry Jameson 2026-08-18 21:35:06 +03:00
commit d0c3eafa84
15 changed files with 570 additions and 338 deletions

View file

@ -110,8 +110,6 @@ const conversation = {
},
computed: {
status() {
console.log(this.statusId)
console.log(useStatusesStore().allStatuses.get(this.statusId))
return useStatusesStore().allStatuses.get(this.statusId)
},
maxDepthToShowByDefault() {

View file

@ -23,11 +23,11 @@ import {
import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useScrobblesStore } from 'src/stores/scrobbles.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUserHighlightStore } from 'src/stores/user_highlight.js'
import { useUsersStore } from 'src/stores/users.js'
import { useScrobblesStore } from 'src/stores/scrobbles.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -136,6 +136,12 @@ const Status = {
status() {
return this.statusoid ?? useStatusesStore().allStatuses.get(this.statusId)
},
repeater() {
return useUsersStore().findUser(this.status.user.id)
},
user() {
return useUsersStore().findUser(this.mainStatus.user.id)
},
showReasonMutedThread() {
return (
(this.mainStatus.thread_muted ||
@ -153,38 +159,31 @@ const Status = {
return this.mergedConfig.scaleMfm
},
repeaterClass() {
const user = this.status.user
return highlightClass(user)
return highlightClass(this.repeater)
},
userClass() {
const user = this.retweet
? this.status.retweeted_status.user
: this.status.user
return highlightClass(user)
return highlightClass(this.user)
},
deleted() {
return this.status.deleted
},
repeaterStyle() {
const user = this.status.user
return highlightStyle(useUserHighlightStore().get(user.screen_name))
return highlightStyle(useUserHighlightStore().get(this.repeater.screen_name))
},
userStyle() {
if (this.noHeading) return
const user = this.retweet
? this.status.retweeted_status.user
: this.status.user
return highlightStyle(useUserHighlightStore().get(user.screen_name))
return highlightStyle(useUserHighlightStore().get(this.user.screen_name))
},
userProfileLink() {
return this.generateUserProfileLink(
this.status.user.id,
this.status.user.screen_name,
this.user.id,
this.user.screen_name,
)
},
replyProfileLink() {
if (this.isReply) {
const user = useUsersStore().findUser(this.status.in_reply_to_user_id)
const user = useUsersStore().findUser(this.mainStatus.in_reply_to_user_id)
// FIXME Why user not found sometimes???
return user ? user.statusnet_profile_url : 'NOT_FOUND'
}
@ -192,19 +191,16 @@ const Status = {
retweet() {
return !!this.status.retweeted_status
},
retweeterUser() {
return this.status.user
},
retweeter() {
repeaterName() {
return this.status.user.name || this.status.user.screen_name_ui
},
retweeterHtml() {
repeaterHtml() {
return this.status.user.name
},
retweeterProfileLink() {
repeaterProfileLink() {
return this.generateUserProfileLink(
this.status.user.id,
this.status.user.screen_name,
this.repeater.id,
this.repeater.screen_name,
)
},
mainStatus() {
@ -214,10 +210,6 @@ const Status = {
return this.status
}
},
statusFromGlobalRepository() {
// NOTE: Consider to replace status with statusFromGlobalRepository
return useStatusesStore().allStatuses.get(this.status.id)
},
loggedIn() {
return !!this.currentUser
},
@ -367,22 +359,22 @@ const Status = {
},
isReply() {
return !!(
this.status.in_reply_to_status_id && this.status.in_reply_to_user_id
this.mainStatus.in_reply_to_status_id && this.mainStatus.in_reply_to_user_id
)
},
replyToName() {
if (this.status.in_reply_to_screen_name) {
if (this.mainStatus.in_reply_to_screen_name) {
return this.status.in_reply_to_screen_name
} else {
const user = useUsersStore().findUser(this.status.in_reply_to_user_id)
const user = useUsersStore().findUser(this.mainStatus.in_reply_to_user_id)
return user?.screen_name_ui
}
},
combinedFavsAndRepeatsUsers() {
// Use the status from the global status repository since favs and repeats are saved in it
const combinedUsers = [].concat(
this.statusFromGlobalRepository.favoritedBy,
this.statusFromGlobalRepository.rebloggedBy,
this.mainStatus.favoritedBy,
this.mainStatus.rebloggedBy,
)
return uniqBy(combinedUsers, 'id')
},
@ -400,7 +392,7 @@ const Status = {
!this.hidePostStats &&
this.focused &&
(this.combinedFavsAndRepeatsUsers.length > 0 ||
this.statusFromGlobalRepository.quotes_count)
this.mainStatus.quotes_count)
)
},
muteBotStatuses() {
@ -431,7 +423,7 @@ const Status = {
return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility)
},
isEdited() {
return this.status.edited_at !== null
return this.mainStatus.edited_at !== null
},
editingAvailable() {
return useInstanceCapabilitiesStore().editingAvailable
@ -562,22 +554,22 @@ const Status = {
focused: function (id) {
this.scrollIfFocused(id)
},
'status.repeat_num': function (num) {
'mainStatus.repeat_num': function (num) {
// refetch repeats when repeat_num is changed in any way
if (
this.focused &&
this.statusFromGlobalRepository.rebloggedBy &&
this.statusFromGlobalRepository.rebloggedBy.length !== num
this.mainStatus.rebloggedBy &&
this.mainStatus.rebloggedBy.length !== num
) {
this.$store.dispatch('fetchRepeats', this.status.id)
}
},
'status.fave_num': function (num) {
'mainStatus.fave_num': function (num) {
// refetch favs when fave_num is changed in any way
if (
this.focused &&
this.statusFromGlobalRepository.favoritedBy &&
this.statusFromGlobalRepository.favoritedBy.length !== num
this.mainStatus.favoritedBy &&
this.mainStatus.favoritedBy.length !== num
) {
this.$store.dispatch('fetchFavs', this.status.id)
}

View file

@ -27,7 +27,7 @@
icon="retweet"
/>
<user-link
:user="status.user"
:user="repeater"
:at="false"
/>
</small>
@ -53,32 +53,31 @@
class="status-container repeat-info"
>
<UserAvatar
v-if="retweet"
class="left-side repeater-avatar"
:user-id="statusoid.user.id"
:user-id="repeater.id"
/>
<div class="right-side faint">
<bdi
class="status-username repeater-name"
:title="retweeter"
:title="repeaterName"
>
<router-link
v-if="retweeterHtml"
:to="retweeterProfileLink"
v-if="repeaterHtml"
:to="repeaterProfileLink"
>
<RichContent
:html="retweeterHtml"
:emoji="retweeterUser.emoji"
:html="repeaterHtml"
:emoji="repeater.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
:pause-mfm="pauseMfm"
:scale-mfm="scaleMfm"
:is-local="retweeterUser.is_local"
:is-local="repeater.is_local"
/>
</router-link>
<router-link
v-else
:to="retweeterProfileLink"
>{{ retweeter }}</router-link>
:to="repeaterProfileLink"
>{{ repeaterName }}</router-link>
</bdi>
<div class="repeat-label">
<FAIcon
@ -103,24 +102,24 @@
class="left-side"
>
<a
v-if="status.user?.name"
v-if="user.name"
:href="$router.resolve(userProfileLink).href"
@click.prevent
>
<UserPopover
:user-id="status.user.id"
:user-id="user.id"
:overlay-centers="true"
>
<UserAvatar
class="post-avatar"
:compact="compact"
:user-id="status?.user.id"
:user-id="user.id"
/>
</UserPopover>
</a>
<UserAvatar
v-else
:user-id="status?.user.id"
:user-id="user.id"
class="post-avatar"
:compact="compact"
:title="$t('status.unknown_user_info')"
@ -133,38 +132,38 @@
>
<div class="heading-name-row">
<div
v-if="status.user"
v-if="user"
class="heading-left"
>
<h4
v-if="status.user.name_html"
v-if="user.name_html"
class="status-username"
:title="status.user.name"
:title="user.name"
>
<RichContent
:html="status.user.name"
:emoji="status.user.emoji"
:html="user.name"
:emoji="user.emoji"
:allow-non-square-emoji="allowNonSquareEmoji"
:is-local="status.user.is_local"
:is-local="user.is_local"
/>
</h4>
<h4
v-else
class="status-username"
:title="status.user.name"
:title="user.name"
>
{{ status.user.name }}
{{ user.name }}
</h4>
<user-link
class="account-name"
:title="status.user.screen_name_ui"
:user="status.user"
:title="user.screen_name_ui"
:user="user"
:at="false"
/>
<img
v-if="!!(status.user && status.user.favicon)"
v-if="!!(user && user.favicon)"
class="status-favicon"
:src="status.user.favicon"
:src="user.favicon"
>
</div>
@ -184,12 +183,12 @@
:to="{ name: 'conversation', params: { id: status.id } }"
>
<Timeago
:time="status.created_at"
:time="mainStatus.created_at"
:auto-update="60"
/>
</router-link>
<span
v-if="status.visibility"
v-if="mainStatus.visibility"
class="visibility-icon"
:title="visibilityLocalized"
>
@ -305,10 +304,10 @@
<template #replyToWithIcon>
<StatusPopover
v-if="!isPreview"
:status-id="status.parent_visible && status.in_reply_to_status_id"
:status-id="mainStatus.parent_visible && mainStatus.in_reply_to_status_id"
class="reply-to-popover"
style="min-width: 0;"
:class="{ '-strikethrough': !status.parent_visible }"
:class="{ '-strikethrough': !mainStatus.parent_visible }"
>
<button
class="button-unstyled reply-to"
@ -397,7 +396,7 @@
<template #time>
<Timeago
template-key="time.in_past"
:time="status.edited_at"
:time="mainStatus.edited_at"
:auto-update="60"
:long-format="true"
/>
@ -462,31 +461,31 @@
>
<div class="stats">
<UserListPopover
v-if="statusFromGlobalRepository.rebloggedBy && statusFromGlobalRepository.rebloggedBy.length > 0"
:users="statusFromGlobalRepository.rebloggedBy"
v-if="mainStatus.rebloggedBy && mainStatus.rebloggedBy.length > 0"
:users="mainStatus.rebloggedBy"
>
<div class="stat-count">
<a class="stat-title">{{ $t('status.repeats') }}</a>
<div class="stat-number">
{{ statusFromGlobalRepository.rebloggedBy.length }}
{{ mainStatus.rebloggedBy.length }}
</div>
</div>
</UserListPopover>
<UserListPopover
v-if="statusFromGlobalRepository.favoritedBy && statusFromGlobalRepository.favoritedBy.length > 0"
:users="statusFromGlobalRepository.favoritedBy"
v-if="mainStatus.favoritedBy && mainStatus.favoritedBy.length > 0"
:users="mainStatus.favoritedBy"
>
<div
class="stat-count"
>
<a class="stat-title">{{ $t('status.favorites') }}</a>
<div class="stat-number">
{{ statusFromGlobalRepository.favoritedBy.length }}
{{ mainStatus.favoritedBy.length }}
</div>
</div>
</UserListPopover>
<router-link
v-if="statusFromGlobalRepository.quotes_count > 0"
v-if="mainStatus.quotes_count > 0"
:to="{ name: 'quotes', params: { id: status.id } }"
>
<div
@ -494,7 +493,7 @@
>
<a class="stat-title">{{ $t('status.quotes') }}</a>
<div class="stat-number">
{{ statusFromGlobalRepository.quotes_count }}
{{ mainStatus.quotes_count }}
</div>
</div>
</router-link>
@ -507,13 +506,13 @@
<EmojiReactions
v-if="(mergedConfig.emojiReactionsOnTimeline || focused) && (!noHeading && !isPreview)"
:status="status"
:status="mainStatus"
/>
<StatusActionButtons
v-if="!noHeading && !isPreview"
class="status-action-buttons"
:status="status"
:status="mainStatus"
:replying="replying"
@toggle-replying="toggleReplyForm"
/>

View file

@ -15,6 +15,11 @@ const StatusPopover = {
error: false,
}
},
computed: {
status() {
return useStatusesStore().allStatuses.get(this.statusId)
},
},
components: {
Popover,
},

View file

@ -8,6 +8,7 @@ import ScrollTopButton from 'src/components/scroll_top_button/scroll_top_button.
import TimelineMenu from 'src/components/timeline_menu/timeline_menu.vue'
import { useInterfaceStore } from 'src/stores/interface.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useTimelinesStore } from 'src/stores/timelines.js'
@ -54,8 +55,9 @@ const Timeline = {
return useTimelinesStore()[this.timelineRef.name]
},
filteredVisibleStatuses() {
return [...this.timeline.visibleStatusesIds.keys()]
.map((id) => this.timeline.statuses.get(id))
return this.timeline.order
.filter((id) => this.timeline.visibleStatusIds.has(id))
.map((id) => useStatusesStore().allStatuses.get(id))
.filter(({ pinned }) => (this.skipPinned ? !pinned : true))
},
newStatusCount() {
@ -98,12 +100,12 @@ const Timeline = {
}
},
statusesToDisplay() {
const amount = this.timeline.visibleStatusesIds.size
const amount = this.timeline.visibleStatusIds.size
const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80))
const min = Math.max(0, this.virtualScrollIndex - statusesPerSide)
const max = Math.min(amount, this.virtualScrollIndex + statusesPerSide)
return new Set(
[...this.timeline.visibleStatusesIds.keys()].slice(min, max),
this.timeline.order.slice(min, max),
)
},
virtualScrollingEnabled() {

View file

@ -81,7 +81,7 @@
<Timeline
key="media"
:label="$t('user_card.media')"
:disabled="!media.visibleStatusesIds.size"
:disabled="media.visibleStatusIds.size === 0"
:title="$t('user_card.media')"
:timeline-ref="{ name: 'media', argument: userId }"
embedded
@ -92,7 +92,7 @@
v-if="favoritesTabVisible"
key="favorites"
:label="$t('user_card.favorites')"
:disabled="!favorites.visibleStatusesIds.size"
:disabled="favorites.visibleStatusIds.size === 0"
:title="$t('user_card.favorites')"
:timeline-ref="{ name: 'favorites', argument: userId }"
:argument="isUs ? undefined : userId"

View file

@ -18,7 +18,7 @@ const REPLY_VISIBILITY_TIMELINES = new Set([
const fetchAndUpdate = (
{ timeline, argument, credentials },
{ maxId, sinceId, older = false, showImmediately = false },
{ older = false, showImmediately = false },
) => {
timeline.loading = true
const { hideMutedPosts, replyVisibility } =
@ -31,13 +31,9 @@ const fetchAndUpdate = (
if (older) {
// When minId = 0 we need to fetch without maxId param
args.maxId = maxId || timeline.minId || null
args.maxId = timeline.minId || null
} else {
if (sinceId === undefined) {
args.sinceId = timeline.maxId
} else if (sinceId !== null) {
args.sinceId = sinceId
}
args.sinceId = timeline.maxId || null
}
args.withMuted = !hideMutedPosts
@ -45,7 +41,7 @@ const fetchAndUpdate = (
args.replyVisibility = replyVisibility
}
const numStatusesBeforeFetch = timeline.statuses.size
const numStatusesBeforeFetch = timeline.statusIds.size
return fetchTimeline(args)
.then((response) => {
@ -62,10 +58,12 @@ const fetchAndUpdate = (
const processed = useStatusesStore()
.addNewStatuses({ statuses, timestamp })
.filter(Boolean)
.map(({ id }) => id)
useTimelinesStore().addStatusesToTimeline(timeline.name, argument, {
statuses: processed,
showImmediately,
older,
pagination,
})
return { statuses, pagination }
@ -95,8 +93,6 @@ const timelineFetcher = (timeline, argument, credentials) => {
const boundFetchAndUpdate = ({
showImmediately,
maxId,
sinceId,
older,
} = {}) =>
fetchAndUpdate(
@ -106,8 +102,6 @@ const timelineFetcher = (timeline, argument, credentials) => {
credentials,
},
{
maxId,
sinceId,
older,
showImmediately,
},
@ -117,7 +111,7 @@ const timelineFetcher = (timeline, argument, credentials) => {
if (state.interval) throw new Error('Interval already exists!')
boundFetchAndUpdate({
showImmediately: timeline.visibleStatusesIds.size === 0,
showImmediately: timeline.visibleStatusIds.size === 0,
})
state.interval = promiseInterval(boundFetchAndUpdate, 10000)

View file

@ -1,8 +1,9 @@
import { defineStore } from 'pinia'
import {
fetchScrobbles,
} from 'src/api/public.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useUsersStore } from 'src/stores/users.js'
import { fetchScrobbles } from 'src/api/public.js'
export const defaultState = () => ({
scrobblesNextFetch: new Map(),
@ -13,7 +14,7 @@ export const useScrobblesStore = defineStore('scrobbles', {
actions: {
getLatestScrobble(userId) {
const scrobblesSupport =
useInstanceCapabilitiesStore().pleromaScrobblesAvailable
useInstanceCapabilitiesStore().pleromaScrobblesAvailable
if (!scrobblesSupport) {
return
@ -29,12 +30,12 @@ export const useScrobblesStore = defineStore('scrobbles', {
.then(({ data: scrobbles }) => {
useUsersStore().findUser(userId).latestScrobble = scrobbles[0]
this.scrobblesNextFetch.set(user.id, Date.now() + 60 * 1000)
this.scrobblesNextFetch.set(userId, Date.now() + 60 * 1000)
})
.catch((e) => {
useInstanceCapabilitiesStore().set('pleromaScrobblesAvailable', false)
console.warn('cannot fetch scrobbles', e)
})
}
}
},
},
})

55
src/stores/search.js Normal file
View file

@ -0,0 +1,55 @@
import { defineStore } from 'pinia'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js'
import { search2 } from 'src/api/public.js'
export const useSearchStore = defineStore('search', {
actions: {
async search({ q, resolve, limit, offset, following, type }) {
const { data, ...rest } = await search2({
q,
resolve,
limit,
offset,
following,
type,
credentials: useOAuthStore().token,
})
const { accounts, statuses } = data
useUsersStore().addNewUsers({
...rest,
data: accounts,
})
useStatusesStore().addNewStatuses({
...rest,
statuses,
})
const output = {}
output.statuses = statuses.map((s) =>
useStatusesStore().allStatuses.get(s.id),
)
output.accounts = accounts.map((s) => useUsersStore().findUser(s.id))
return output
},
// Search
searchUsers({ query }) {
return searchUsers({
query,
credentials: useOAuthStore().token,
}).then((result) => {
const { data } = result
useUsersStore().addNewUsers(result)
return data.map((s) => useUsersStore().findUser(s.id))
})
},
},
})

View file

@ -1,6 +1,5 @@
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 { useStreamingStore } from 'src/stores/streaming.js'
@ -240,6 +239,10 @@ export const useStatusesStore = defineStore('statuses', {
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 }) {
@ -524,49 +527,20 @@ export const useStatusesStore = defineStore('statuses', {
// For when blocking a user
wipeUserStatuses(userId) {
const removed = new Set()
this.allStatuses.forEach((status) => {
if (status.user.id === userId) {
this.allStatuses.delete(status.id)
removed.add(status.id)
}
})
return removed
},
// 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,
})
data.statuses = data.statuses.map((s) => this.allStatuses.get(s.id))
return data
})
},
// Misc
setVirtualHeight({ statusId, height }) {
this.allStatuses.get(statusId).virtualHeight = height
},
updateStatusWithPoll({ id, poll }) {
const status = this.allStatuses.get(id)
status.poll = poll
},
},
})

View file

@ -12,16 +12,17 @@ import timelineFetcher from 'src/services/timeline_fetcher/timeline_fetcher.serv
const emptyTl = (name, argument = null) => {
const result = {
name,
statuses: new Map(),
visibleStatusesIds: new Set(),
order: [],
statusIds: new Set(),
visibleStatusIds: new Set(),
newStatusCount: 0,
maxId: '',
minId: '',
minVisibleId: '',
loading: false,
streaming: false,
flushMarker: 0,
fetcher: null,
socket: null,
}
const property = ARGUMENT_MAP[name]
@ -88,6 +89,11 @@ export const useTimelinesStore = defineStore('timelines', {
return
}
const property = ARGUMENT_MAP[timelineName]
if (property) {
timeline[property] = argument
}
timeline.fetcher = timelineFetcher(
timeline,
argument,
@ -156,7 +162,11 @@ export const useTimelinesStore = defineStore('timelines', {
},
deactivateAll() {
TIMELINES.forEach((name) => {
this.deactivate(name, true)
try {
this.deactivate(name, true)
} catch (e) {
console.error(`Failed to deactivate timeline ${name}`)
}
})
},
@ -169,7 +179,7 @@ export const useTimelinesStore = defineStore('timelines', {
showImmediately = false,
noIdUpdate = false,
pagination = {},
nested = false,
older = false
},
) {
if (statuses.length === 0) return
@ -179,7 +189,7 @@ export const useTimelinesStore = defineStore('timelines', {
// user. I.e. opening different user profiles makes request which could
// return data late after user already viewing different user profile
// Same can happen with tags etc.
const property = ARGUMENT_MAP[name]
const property = ARGUMENT_MAP[timelineName]
if (property && timeline[property] !== argument) {
return
@ -188,59 +198,36 @@ export const useTimelinesStore = defineStore('timelines', {
if (!noIdUpdate) {
this.updateTimelineExtremes(
timeline,
statuses.map((x) => x.id),
pagination,
)
}
statuses.forEach((status) => {
const isNew = !timeline.statuses.has(status.id)
timeline.statuses.set(status.id, status)
const filtered = statuses.filter((id) => !timeline.statusIds.has(id))
if (older) {
timeline.order.push(...filtered)
} else {
timeline.order.unshift(...filtered)
}
statuses.forEach((statusId) => {
const isNew = !timeline.statusIds.has(statusId)
timeline.statusIds.add(statusId)
if (isNew) {
if (showImmediately) {
// Add it directly to the visibleStatuses, don't change
// newStatusCount
timeline.visibleStatusesIds.add(status.id)
timeline.visibleStatusIds.add(statusId)
} else {
// Just change newStatuscount
timeline.newStatusCount += 1
}
}
if (nested) return
// We are mentioned in a post
if (
status.type === 'status' &&
status.attentions.some(
({ id }) => id === useUsersStore().currentUser?.id,
)
) {
// Add the mention to the mentions timeline
if (timeline !== this.mentions) {
this.addStatusesToTimeline('mentions', null, {
statuses: [status],
nested: true,
})
}
}
if (status.visibility === 'direct') {
if (timeline !== this.dms) {
this.addStatusesToTimeline('dms', null, {
statuses: [status],
nested: true,
})
}
}
})
},
onStreamMessage(timeline, argument, event) {
// This relies on statuses store to process this event first
const status = useStatusesStore().allStatuses.get(event.data.status.id)
this.addStatusesToTimeline(timeline, argument, {
statuses: [status],
statuses: [event.data.status.id],
})
},
@ -278,10 +265,10 @@ export const useTimelinesStore = defineStore('timelines', {
},
// Queues & Timeline manip
updateTimelineExtremes(timeline, statuses, pagination = {}) {
updateTimelineExtremes(timeline, pagination = {}) {
// Can't use Math.min/max because it doesn't work with string (duh)
const minNew = pagination.maxId ?? min(...statuses) ?? ''
const maxNew = pagination.minId ?? max(...statuses) ?? ''
const minNew = pagination.maxId ?? last(timeline.order) ?? ''
const maxNew = pagination.minId ?? first(timeline.order) ?? ''
const newer = maxNew > timeline.maxId
const older = minNew < timeline.minId
@ -292,20 +279,17 @@ export const useTimelinesStore = defineStore('timelines', {
if (older || timeline.minId === '') {
timeline.minId = minNew
}
this.syncOrder(timeline)
},
showNewStatuses(timelineName) {
const timeline = this[timelineName]
timeline.newStatusCount = 0
timeline.visibleStatusesIds = new Set(
[...timeline.statuses.keys()].slice(0, 50),
)
timeline.minVisibleId = last(timeline.visibleStatusesIds.keys())
timeline.minId = ''
timeline.maxId = ''
this.updateTimelineExtremes(timeline, [...timeline.statuses.keys()])
timeline.visibleStatusIds = new Set([...timeline.statusIds])
},
syncOrder(timeline) {
timeline.order = timeline.order.filter((id) => timeline.statusIds.has(id))
},
queueFlush(timeline, id) {
this[timeline].flushMarker = id
@ -317,23 +301,16 @@ export const useTimelinesStore = defineStore('timelines', {
},
// Misc
wipeUserStatuses(userId) {
wipeStatuses(ids) {
TIMELINES.forEach((timelineName) => {
const timeline = this.timelines[timelineName]
const timeline = this[timelineName]
timeline.statuses
.values()
.filter(({ user }) => user.id === userId)
.forEach(({ id }) => {
timeline.statuses.delete(id)
timeline.visibleStatusesIds.delete(id)
})
timeline.minVisibleId =
timeline.visibleStatusesIds.size > 0
? last(timeline.visibleStatusesIds).id
: 0
timeline.maxId =
timeline.statuses.length > 0 ? first(timeline.statuses).id : 0
ids.forEach((id) => {
timeline.statusIds.delete(id)
timeline.visibleStatusIds.delete(id)
})
this.syncOrder(timeline)
})
},
},

View file

@ -746,17 +746,6 @@ export const useUsersStore = defineStore('users', {
useInterfaceStore().onLogout()
})
},
// Search
searchUsers({ query }) {
return searchUsers({
query,
credentials: useOAuthStore().token,
}).then(({ data: users }) => {
this.addNewUsers(users)
return users
})
},
},
persist: {
paths: ['lastLoginName'],

View file

@ -51,125 +51,12 @@ const externalProfileStore = createStore({
mutations,
actions,
getters: testGetters,
state: {
interface: {
browserSupport: '',
},
instance: {
hideUserStats: true,
},
statuses: {
timelines: {
user: {
statuses: [],
statusesObject: {},
faves: [],
visibleStatuses: [],
visibleStatusesObject: {},
newStatusCount: 0,
maxId: 0,
minVisibleId: 0,
loading: false,
followers: [],
friends: [],
viewing: 'statuses',
userId: 100,
flushMarker: 0,
},
media: {
statuses: [],
statusesObject: {},
faves: [],
visibleStatuses: [],
visibleStatusesObject: {},
newStatusCount: 0,
maxId: 0,
minVisibleId: 0,
loading: false,
followers: [],
friends: [],
viewing: 'statuses',
userId: 100,
flushMarker: 0,
},
},
},
users: {
currentUser: {
credentials: '',
},
usersObject: { 100: extUser },
usersByNameObject: {},
users: [extUser],
relationships: {},
},
},
})
const localProfileStore = createStore({
mutations,
actions,
getters: testGetters,
state: {
interface: {
browserSupport: '',
},
config: {
colors: '',
highlight: {},
customTheme: {
colors: [],
},
},
instance: {
hideUserStats: true,
},
statuses: {
timelines: {
user: {
statuses: [],
statusesObject: {},
faves: [],
visibleStatuses: [],
visibleStatusesObject: {},
newStatusCount: 0,
maxId: 0,
minVisibleId: 0,
loading: false,
followers: [],
friends: [],
viewing: 'statuses',
userId: 100,
flushMarker: 0,
},
media: {
statuses: [],
statusesObject: {},
faves: [],
visibleStatuses: [],
visibleStatusesObject: {},
newStatusCount: 0,
maxId: 0,
minVisibleId: 0,
loading: false,
followers: [],
friends: [],
viewing: 'statuses',
userId: 100,
flushMarker: 0,
},
},
},
users: {
currentUser: {
credentials: '',
},
usersObject: { 100: localUser },
usersByNameObject: { testuser: localUser },
users: [localUser],
relationships: {},
},
},
})
// https://github.com/vuejs/test-utils/issues/1382

View file

@ -626,8 +626,9 @@ describe('Statuses store', () => {
timestamp: 1,
})
store.wipeUserStatuses('u19')
const result = store.wipeUserStatuses('u19')
expect(store.allStatuses).to.have.length(19)
expect(result).to.eql(new Set(['s19']))
})
})

View file

@ -0,0 +1,358 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useTimelinesStore } from 'src/stores/timelines.js'
import { useStreamingStore } from 'src/stores/streaming.js'
describe('Timelines store', () => {
beforeEach(() => {
vi.useFakeTimers()
setActivePinia(createTestingPinia({ stubActions: false }))
})
afterEach(() => {
useTimelinesStore().deactivateAll()
vi.useRealTimers()
})
describe('activate', () => {
it('streamable', () => {
const store = useTimelinesStore()
const sub = vi.fn()
useStreamingStore().addSubscriber = sub
console.log(store.activate)
store.activate('friends', undefined, true)
expect(sub).to.have.been.called
expect(store.friends.fetcher).to.not.be.null
expect(store.friends.socket).to.not.be.null
})
it('non-streamable', () => {
const store = useTimelinesStore()
const sub = vi.fn()
useStreamingStore().addSubscriber = sub
console.log(store.activate)
store.activate('user', '1')
expect(sub).to.not.have.been.called
expect(store.user.fetcher).to.not.be.null
expect(store.user.socket).to.be.null
})
})
describe('deactivate', () => {
it('streamable', () => {
const store = useTimelinesStore()
const unsub = vi.fn()
useStreamingStore().removeSubscriber = unsub
store.activate('friends', undefined, true)
// Checking so that they were set properly before
// since reset changes them to ''
store.friends.maxId = '3'
store.friends.minId = '4'
store.friends.statusIds = new Set(['3','4'])
store.friends.visibleStausIds = new Set(['3','4'])
store.deactivate('friends', true)
expect(unsub).to.have.been.called
expect(store.friends.fetcher).to.be.null
expect(store.friends.socket).to.be.null
expect(store.friends.statusIds).to.have.length(0)
expect(store.friends.visibleStatusIds).to.have.length(0)
expect(store.friends).to.have.property('maxId', '')
expect(store.friends).to.have.property('minId', '')
})
it('non-streamable', () => {
const store = useTimelinesStore()
const unsub = vi.fn()
useStreamingStore().removeSubscriber = unsub
store.activate('user', '1')
// Checking so that they were set properly before
// since reset changes them to ''
store.user.maxId = '3'
store.user.minId = '4'
store.user.statusIds = new Set(['3','4'])
store.user.visibleStausIds = new Set(['3','4'])
store.deactivate('user')
expect(unsub).to.not.have.been.called
expect(store.user.fetcher).to.be.null
expect(store.user.socket).to.be.null
expect(store.user.statusIds).to.have.length(0)
expect(store.user.visibleStatusIds).to.have.length(0)
expect(store.user).to.have.property('maxId', '')
expect(store.user).to.have.property('minId', '')
})
})
describe('updateTimelineExtremes', () => {
it('should derive extremes from data', () => {
const store = useTimelinesStore()
const timeline = useTimelinesStore().friends
timeline.order = ['4','1','3','2']
timeline.statusesIds = new Set(timeline.order)
store.updateTimelineExtremes(timeline)
expect(store.friends).to.have.property('maxId', '4')
expect(store.friends).to.have.property('minId', '2')
})
it('should use extremes from pagination', () => {
const store = useTimelinesStore()
const timeline = useTimelinesStore().friends
store.updateTimelineExtremes(
timeline,
{ maxId: '1', minId: '2' }
)
// Min and max are swapped!
expect(store.friends).to.have.property('maxId', '2')
expect(store.friends).to.have.property('minId', '1')
})
})
describe('addStatusesToTimeline', () => {
it('adds the status to the given timeline', () => {
const store = useTimelinesStore()
const statuses = ['1','2','3']
store.activate('list', '1')
store.addStatusesToTimeline(
'list',
'1',
{
statuses,
pagination: { minId: '1', maxId: '3' }
}
)
expect(store.list.order).to.eql(statuses)
expect(store.list.statusIds).to.eql(new Set(statuses))
expect(store.list.visibleStatusIds).to.eql(new Set())
expect(store.list.newStatusCount).to.equal(3)
expect(store.list).to.have.property('maxId', '1')
expect(store.list).to.have.property('minId', '3')
})
it('ignores duplicates', () => {
const store = useTimelinesStore()
const statuses = ['1','2','3']
store.activate('list', '1')
store.addStatusesToTimeline(
'list',
'1',
{
statuses,
pagination: { minId: '1', maxId: '3' }
}
)
store.addStatusesToTimeline(
'list',
'1',
{
statuses,
pagination: { minId: '1', maxId: '3' }
}
)
expect(store.list.order).to.eql(statuses)
expect(store.list.statusIds).to.eql(new Set(statuses))
expect(store.list.visibleStatusIds).to.eql(new Set())
expect(store.list.newStatusCount).to.equal(3)
expect(store.list).to.have.property('maxId', '1')
expect(store.list).to.have.property('minId', '3')
})
it('adds the status the given timeline, directly visible', () => {
const store = useTimelinesStore()
const statuses = ['1','2','3']
store.activate('list', '1')
store.addStatusesToTimeline(
'list',
'1',
{
statuses,
showImmediately: true,
pagination: { minId: '1', maxId: '3' }
}
)
expect(store.list.order).to.eql(statuses)
expect(store.list.statusIds).to.eql(new Set(statuses))
expect(store.list.visibleStatusIds).to.eql(new Set(statuses))
expect(store.list.newStatusCount).to.equal(0)
expect(store.list).to.have.property('maxId', '1')
expect(store.list).to.have.property('minId', '3')
})
it('does not update the maxId when the noIdUpdate flag is set', () => {
const store = useTimelinesStore()
const statuses = ['1','2','3']
store.activate('list', '1')
store.addStatusesToTimeline(
'list',
'1',
{
statuses,
noIdUpdate: true,
pagination: { minId: '1', maxId: '3' }
}
)
expect(store.list.order).to.eql(statuses)
expect(store.list.statusIds).to.eql(new Set(statuses))
expect(store.list.visibleStatusIds).to.eql(new Set())
expect(store.list.newStatusCount).to.equal(3)
expect(store.list).to.have.property('maxId', '')
expect(store.list).to.have.property('minId', '')
})
it('does not update timeline if it belongs to a different arugment', () => {
const store = useTimelinesStore()
const statuses = ['1','2','3']
store.activate('list', '1')
store.addStatusesToTimeline(
'list',
'2',
{
statuses,
noIdUpdate: true,
pagination: { minId: '1', maxId: '3' }
}
)
expect(store.list.order).to.eql([])
expect(store.list.statusIds).to.eql(new Set())
expect(store.list.visibleStatusIds).to.eql(new Set())
expect(store.list.newStatusCount).to.equal(0)
expect(store.list).to.have.property('maxId', '')
expect(store.list).to.have.property('minId', '')
})
it('prepends timeline with new statuses', () => {
const store = useTimelinesStore()
const statuses1 = ['3','2','1']
const statuses2 = ['6','5','4']
store.activate('list', '1')
store.addStatusesToTimeline(
'list',
'1',
{
statuses: statuses1,
pagination: { minId: '3', maxId: '1' }
}
)
store.addStatusesToTimeline(
'list',
'1',
{
statuses: statuses2,
pagination: { minId: '6', maxId: '4' }
}
)
const newOrder = [...statuses2, ...statuses1]
expect(store.list.order).to.eql(newOrder)
expect(store.list.statusIds).to.eql(new Set(newOrder))
expect(store.list.visibleStatusIds).to.eql(new Set())
expect(store.list.newStatusCount).to.equal(6)
expect(store.list).to.have.property('maxId', '6')
expect(store.list).to.have.property('minId', '1')
})
it('appends timeline with new statuses if fetching older', () => {
const store = useTimelinesStore()
const statuses1 = ['6','5','4']
const statuses2 = ['3','2','1']
store.activate('list', '1')
store.addStatusesToTimeline(
'list',
'1',
{
statuses: statuses1,
pagination: { minId: '6', maxId: '4' },
}
)
store.addStatusesToTimeline(
'list',
'1',
{
statuses: statuses2,
pagination: { minId: '3', maxId: '1' },
older: true,
}
)
const newOrder = [...statuses1, ...statuses2]
expect(store.list.order).to.eql(newOrder)
expect(store.list.statusIds).to.eql(new Set(newOrder))
expect(store.list.visibleStatusIds).to.eql(new Set())
expect(store.list.newStatusCount).to.equal(6)
expect(store.list).to.have.property('maxId', '6')
expect(store.list).to.have.property('minId', '1')
})
})
describe('showNewStatuses', () => {
it('resets counter and makes all ids visible', () => {
const store = useTimelinesStore()
const statuses = ['1','2','3']
store.activate('public')
store.addStatusesToTimeline(
'public',
undefined,
{
statuses,
pagination: { minId: '1', maxId: '3' }
}
)
expect(store.public.statusIds).to.eql(new Set(statuses))
expect(store.public.visibleStatusIds).to.eql(new Set())
expect(store.public.newStatusCount).to.equal(3)
store.showNewStatuses('public')
expect(store.public.visibleStatusIds).to.eql(new Set(statuses))
expect(store.public.newStatusCount).to.equal(0)
})
})
describe('wipeStatuses', () => {
it('clears all statuses', () => {
const store = useTimelinesStore()
store.activate('friends')
store.activate('public')
store.addStatusesToTimeline(
'public',
undefined,
{
statuses: ['1','2','3','0']
}
)
store.addStatusesToTimeline(
'friends',
undefined,
{
statuses: ['5','0','9','1']
}
)
store.wipeStatuses(['0'])
expect(store.friends.statusIds).to.not.have.members('0')
expect(store.public.statusIds).to.not.have.members('0')
})
})
})