i think it's usable now

This commit is contained in:
Henry Jameson 2026-08-10 22:26:22 +03:00
commit 37f501108b
42 changed files with 296 additions and 1295 deletions

View file

@ -1,15 +1,11 @@
import AuthForm from 'src/components/auth_form/auth_form.js' import AuthForm from 'src/components/auth_form/auth_form.js'
import BookmarkTimeline from 'src/components/bookmark_timeline/bookmark_timeline.vue' import BookmarkTimeline from 'src/components/bookmark_timeline/bookmark_timeline.vue'
import BubbleTimeline from 'src/components/bubble_timeline/bubble_timeline.vue'
import ConversationPage from 'src/components/conversation-page/conversation-page.vue' import ConversationPage from 'src/components/conversation-page/conversation-page.vue'
import DMs from 'src/components/dm_timeline/dm_timeline.vue'
import FriendsTimeline from 'src/components/friends_timeline/friends_timeline.vue'
import NavPanel from 'src/components/nav_panel/nav_panel.vue' import NavPanel from 'src/components/nav_panel/nav_panel.vue'
import PublicAndExternalTimeline from 'src/components/public_and_external_timeline/public_and_external_timeline.vue'
import PublicTimeline from 'src/components/public_timeline/public_timeline.vue'
import QuotesTimeline from 'src/components/quotes_timeline/quotes_timeline.vue' import QuotesTimeline from 'src/components/quotes_timeline/quotes_timeline.vue'
import RemoteUserResolver from 'src/components/remote_user_resolver/remote_user_resolver.vue' import RemoteUserResolver from 'src/components/remote_user_resolver/remote_user_resolver.vue'
import TagTimeline from 'src/components/tag_timeline/tag_timeline.vue' import TagTimeline from 'src/components/tag_timeline/tag_timeline.vue'
import Timeline from 'src/components/timeline/timeline.vue'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
@ -42,22 +38,38 @@ export default (store) => {
{ {
name: 'public-external-timeline', name: 'public-external-timeline',
path: '/main/all', path: '/main/all',
component: PublicAndExternalTimeline, component: Timeline,
props: () => ({
timelineName: 'publicAndExternal',
}),
}, },
{ {
name: 'public-timeline', name: 'public-timeline',
path: '/main/public', path: '/main/public',
component: PublicTimeline, component: Timeline,
props: () => ({
timelineName: 'public',
}),
}, },
{ {
name: 'friends', name: 'friends',
path: '/main/friends', path: '/main/friends',
component: FriendsTimeline, component: Timeline,
beforeEnter: validateAuthenticatedRoute, beforeEnter: validateAuthenticatedRoute,
props: () => ({
timelineName: 'friends',
}),
}, },
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline }, { name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline }, { name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
{ name: 'bubble', path: '/bubble', component: BubbleTimeline }, {
name: 'bubble',
path: '/bubble',
component: Timeline,
props: () => ({
timelineName: 'bubble',
}),
},
{ {
name: 'conversation', name: 'conversation',
path: '/notice/:id', path: '/notice/:id',
@ -105,8 +117,11 @@ export default (store) => {
{ {
name: 'dms', name: 'dms',
path: '/users/:username/dms', path: '/users/:username/dms',
component: DMs, component: Timeline,
beforeEnter: validateAuthenticatedRoute, beforeEnter: validateAuthenticatedRoute,
props: () => ({
timelineName: 'dms',
}),
}, },
{ {
name: 'registration', name: 'registration',

View file

@ -1,8 +1,10 @@
import Timeline from 'src/components/timeline/timeline.vue' import Timeline from 'src/components/timeline/timeline.vue'
import { useStatusesStore } from 'src/stores/statuses.js'
const Bookmarks = { const Bookmarks = {
created() { created() {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) useStatusesStore().clearTimeline({ timeline: 'bookmarks' })
this.$store.dispatch('startFetchingTimeline', { this.$store.dispatch('startFetchingTimeline', {
timeline: 'bookmarks', timeline: 'bookmarks',
bookmarkFolderId: this.folderId || null, bookmarkFolderId: this.folderId || null,
@ -21,7 +23,7 @@ const Bookmarks = {
}, },
watch: { watch: {
folderId() { folderId() {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) useStatusesStore().clearTimeline({ timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks') this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
this.$store.dispatch('startFetchingTimeline', { this.$store.dispatch('startFetchingTimeline', {
timeline: 'bookmarks', timeline: 'bookmarks',
@ -30,7 +32,7 @@ const Bookmarks = {
}, },
}, },
unmounted() { unmounted() {
this.$store.commit('clearTimeline', { timeline: 'bookmarks' }) useStatusesStore().clearTimeline({ timeline: 'bookmarks' })
this.$store.dispatch('stopFetchingTimeline', 'bookmarks') this.$store.dispatch('stopFetchingTimeline', 'bookmarks')
}, },
} }

View file

@ -1,9 +0,0 @@
<template>
<Timeline
:title="$t('nav.bubble')"
:timeline="timeline"
:timeline-name="'bubble'"
/>
</template>
<script src="./bubble_timeline.js"></script>

View file

@ -19,6 +19,7 @@ import UserPopover from 'src/components/user_popover/user_popover.vue'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInterfaceStore } from 'src/stores/interface' import { useInterfaceStore } from 'src/stores/interface'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@ -100,9 +101,9 @@ const ChatMessage = {
return !this.message.in_reply_to_status_id return !this.message.in_reply_to_status_id
}, },
customReplyTo() { customReplyTo() {
return this.$store.state.statuses.allStatusesObject[ return useStatusesStore().allStatuses.get(
this.message.in_reply_to_status_id this.message.in_reply_to_status_id,
] )
}, },
replyToName() { replyToName() {
if (this.message.in_reply_to_screen_name) { if (this.message.in_reply_to_screen_name) {

View file

@ -19,6 +19,7 @@ import { useChatsStore } from 'src/stores/chats.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
import { import {
@ -122,7 +123,7 @@ const Chat = {
}, },
computed: { computed: {
conversationId() { conversationId() {
const status = this.$store.state.statuses.allStatusesObject[this.statusId] const status = useStatusesStore().allStatuses.get(this.statusId)
return get( return get(
status, status,
'retweeted_status.statusnet_conversation_id', 'retweeted_status.statusnet_conversation_id',

View file

@ -1,4 +1,4 @@
import { clone, filter, findIndex, get, reduce } from 'lodash' import { get, reduce } from 'lodash'
import { mapState as mapPiniaState } from 'pinia' import { mapState as mapPiniaState } from 'pinia'
import { mapState } from 'vuex' import { mapState } from 'vuex'
@ -9,9 +9,10 @@ import QuickViewSettings from 'src/components/quick_view_settings/quick_view_set
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import ThreadTree from 'src/components/thread_tree/thread_tree.vue' import ThreadTree from 'src/components/thread_tree/thread_tree.vue'
import { useInterfaceStore } from 'src/stores/interface' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { fetchConversation, fetchStatus } from 'src/api/public.js' import { fetchConversation, fetchStatus } from 'src/api/public.js'
import { WSConnectionStatus } from 'src/api/websocket.js' import { WSConnectionStatus } from 'src/api/websocket.js'
@ -51,20 +52,6 @@ const sortById = (a, b) => {
} }
} }
const sortAndFilterConversation = (conversation, statusoid) => {
if (statusoid.type === 'retweet') {
conversation = filter(
conversation,
(status) =>
status.type === 'retweet' ||
status.id !== statusoid.retweeted_status.id,
)
} else {
conversation = filter(conversation, (status) => status.type !== 'retweet')
}
return conversation.filter(Boolean).sort(sortById)
}
const conversation = { const conversation = {
props: { props: {
statusId: { statusId: {
@ -166,7 +153,7 @@ const conversation = {
return this.virtualHidden && this.suspendable return this.virtualHidden && this.suspendable
}, },
status() { status() {
return this.$store.state.statuses.allStatusesObject[this.statusId] return useStatusesStore().allStatuses.get(this.statusId)
}, },
originalStatusId() { originalStatusId() {
if (this.status.retweeted_status) { if (this.status.retweeted_status) {
@ -187,15 +174,11 @@ const conversation = {
return [this.status] return [this.status]
} }
const conversation = clone( const conversation = useStatusesStore().conversations.get(
this.$store.state.statuses.conversationsObject[this.conversationId], this.conversationId,
) )
const statusIndex = findIndex(conversation, { id: this.originalStatusId })
if (statusIndex !== -1) {
conversation[statusIndex] = this.status
}
return sortAndFilterConversation(conversation, this.status) return [...conversation.values()].toSorted(sortById)
}, },
statusMap() { statusMap() {
return this.conversation.reduce((res, s) => { return this.conversation.reduce((res, s) => {
@ -441,7 +424,7 @@ const conversation = {
} }
}, },
virtualHidden() { virtualHidden() {
this.$store.dispatch('setVirtualHeight', { useStatusesStore().setVirtualHeight({
statusId: this.statusId, statusId: this.statusId,
height: `${this.$el.clientHeight}px`, height: `${this.$el.clientHeight}px`,
}) })
@ -453,9 +436,12 @@ const conversation = {
fetchConversation({ fetchConversation({
id: this.statusId, id: this.statusId,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}).then(({ data: { ancestors, descendants } }) => { }).then(({ data: { ancestors, descendants }, timestamp }) => {
this.$store.dispatch('addNewStatuses', { statuses: ancestors }) useStatusesStore().addNewStatuses({ statuses: ancestors, timestamp })
this.$store.dispatch('addNewStatuses', { statuses: descendants }) useStatusesStore().addNewStatuses({
statuses: descendants,
timestamp,
})
this.setFocused(this.originalStatusId) this.setFocused(this.originalStatusId)
}) })
} else { } else {
@ -482,17 +468,17 @@ const conversation = {
this.focused = id this.focused = id
if (!this.streamingEnabled) { if (!this.streamingEnabled) {
this.$store.dispatch('fetchStatus', id) useStatusesStore().fetchStatus(id)
} }
this.$store.dispatch('fetchFavsAndRepeats', id) useStatusesStore().fetchFavsAndRepeats(id)
this.$store.dispatch('fetchEmojiReactionsBy', id) useStatusesStore().fetchEmojiReactionsBy(id)
}, },
toggleExpanded() { toggleExpanded() {
this.expanded = !this.expanded this.expanded = !this.expanded
}, },
getConversationId(statusId) { getConversationId(statusId) {
const status = this.$store.state.statuses.allStatusesObject[statusId] const status = useStatusesStore().allStatuses.get(statusId)
return get( return get(
status, status,
'retweeted_status.statusnet_conversation_id', 'retweeted_status.statusnet_conversation_id',

View file

@ -1,14 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
const DMs = {
computed: {
timeline() {
return this.$store.state.statuses.timelines.dms
},
},
components: {
Timeline,
},
}
export default DMs

View file

@ -1,9 +0,0 @@
<template>
<Timeline
:title="$t('nav.dms')"
:timeline="timeline"
:timeline-name="'dms'"
/>
</template>
<script src="./dm_timeline.js"></script>

View file

@ -6,6 +6,7 @@ import PostStatusForm from 'src/components/post_status_form/post_status_form.vue
import StatusContent from 'src/components/status_content/status_content.vue' import StatusContent from 'src/components/status_content/status_content.vue'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faPollH } from '@fortawesome/free-solid-svg-icons' import { faPollH } from '@fortawesome/free-solid-svg-icons'
@ -65,7 +66,7 @@ const Draft = {
}, },
refStatus() { refStatus() {
return this.draft.refId return this.draft.refId
? this.$store.state.statuses.allStatusesObject[this.draft.refId] ? useStatusesStore().allStatuses.get(this.draft.refId)
: undefined : undefined
}, },
localCollapseSubjectDefault() { localCollapseSubjectDefault() {

View file

@ -3,6 +3,7 @@ import UserListPopover from 'src/components/user_list_popover/user_list_popover.
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@ -62,10 +63,7 @@ const EmojiReactions = {
async fetchEmojiReactionsByIfMissing() { async fetchEmojiReactionsByIfMissing() {
const hasNoAccounts = this.status.emoji_reactions.find((r) => !r.accounts) const hasNoAccounts = this.status.emoji_reactions.find((r) => !r.accounts)
if (hasNoAccounts) { if (hasNoAccounts) {
return await this.$store.dispatch( return await useStatusesStore().fetchEmojiReactionsBy(this.status.id)
'fetchEmojiReactionsBy',
this.status.id,
)
} }
}, },
reactWith(emoji) { reactWith(emoji) {

View file

@ -1,14 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
const FriendsTimeline = {
components: {
Timeline,
},
computed: {
timeline() {
return this.$store.state.statuses.timelines.friends
},
},
}
export default FriendsTimeline

View file

@ -1,9 +0,0 @@
<template>
<Timeline
:title="$t('nav.timeline')"
:timeline="timeline"
:timeline-name="'friends'"
/>
</template>
<script src="./friends_timeline.js"></script>

View file

@ -1,6 +1,7 @@
import Timeline from 'src/components/timeline/timeline.vue' import Timeline from 'src/components/timeline/timeline.vue'
import { useListsStore } from 'src/stores/lists.js' import { useListsStore } from 'src/stores/lists.js'
import { useStatusesStore } from 'src/stores/statuses.js'
const ListsTimeline = { const ListsTimeline = {
data() { data() {
@ -21,7 +22,7 @@ const ListsTimeline = {
if (route.name === 'lists-timeline' && route.params.id !== this.listId) { if (route.name === 'lists-timeline' && route.params.id !== this.listId) {
this.listId = route.params.id this.listId = route.params.id
this.$store.dispatch('stopFetchingTimeline', 'list') this.$store.dispatch('stopFetchingTimeline', 'list')
this.$store.commit('clearTimeline', { timeline: 'list' }) useStatusesStore().clearTimeline({ timeline: 'list' })
useListsStore().fetchList({ listId: this.listId }) useListsStore().fetchList({ listId: this.listId })
this.$store.dispatch('startFetchingTimeline', { this.$store.dispatch('startFetchingTimeline', {
timeline: 'list', timeline: 'list',
@ -40,7 +41,7 @@ const ListsTimeline = {
}, },
unmounted() { unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'list') this.$store.dispatch('stopFetchingTimeline', 'list')
this.$store.commit('clearTimeline', { timeline: 'list' }) useStatusesStore().clearTimeline({ timeline: 'list' })
}, },
} }

View file

@ -1,22 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
const PublicAndExternalTimeline = {
components: {
Timeline,
},
computed: {
timeline() {
return this.$store.state.statuses.timelines.publicAndExternal
},
},
created() {
this.$store.dispatch('startFetchingTimeline', {
timeline: 'publicAndExternal',
})
},
unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'publicAndExternal')
},
}
export default PublicAndExternalTimeline

View file

@ -1,9 +0,0 @@
<template>
<Timeline
:title="$t('nav.twkn')"
:timeline="timeline"
:timeline-name="'publicAndExternal'"
/>
</template>
<script src="./public_and_external_timeline.js"></script>

View file

@ -1,20 +0,0 @@
import Timeline from 'src/components/timeline/timeline.vue'
const PublicTimeline = {
components: {
Timeline,
},
computed: {
timeline() {
return this.$store.state.statuses.timelines.public
},
},
created() {
this.$store.dispatch('startFetchingTimeline', { timeline: 'public' })
},
unmounted() {
this.$store.dispatch('stopFetchingTimeline', 'public')
},
}
export default PublicTimeline

View file

@ -1,9 +0,0 @@
<template>
<Timeline
:title="$t('nav.public_tl')"
:timeline="timeline"
:timeline-name="'public'"
/>
</template>
<script src="./public_timeline.js"></script>

View file

@ -5,6 +5,7 @@ import Popover from 'src/components/popover/popover.vue'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useLocalConfigStore } from 'src/stores/local_config.js' import { useLocalConfigStore } from 'src/stores/local_config.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
@ -27,7 +28,7 @@ const QuickFilterSettings = {
path: 'replyVisibility', path: 'replyVisibility',
value: visibility, value: visibility,
}) })
this.$store.dispatch('queueFlushAll') useStatusesStore().queueFlushAll()
}, },
openTab(tab) { openTab(tab) {
useInterfaceStore().openSettingsModalTab(tab) useInterfaceStore().openSettingsModalTab(tab)

View file

@ -1,3 +1,5 @@
import { useStatusesStore } from 'src/stores/statuses.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
@ -45,7 +47,7 @@ export default {
computed: { computed: {
quotedStatus() { quotedStatus() {
return this.statusId return this.statusId
? this.$store.state.statuses.allStatusesObject[this.statusId] ? useStatusesStore().allStatuses.get(this.statusId)
: undefined : undefined
}, },
shouldDisplayQuote() { shouldDisplayQuote() {
@ -79,8 +81,8 @@ export default {
this.fetchAttempted = true this.fetchAttempted = true
this.fetching = true this.fetching = true
this.$emit('loading', true) this.$emit('loading', true)
this.$store useStatusesStore()
.dispatch('fetchStatus', this.statusId) .fetchStatus(this.statusId)
.then(() => { .then(() => {
this.displayQuote = true this.displayQuote = true
}) })

View file

@ -1,8 +1,10 @@
import Timeline from 'src/components/timeline/timeline.vue' import Timeline from 'src/components/timeline/timeline.vue'
import { useStatusesStore } from 'src/stores/statuses.js'
const QuotesTimeline = { const QuotesTimeline = {
created() { created() {
this.$store.commit('clearTimeline', { timeline: 'quotes' }) useStatusesStore().clearTimeline({ timeline: 'tag' })
this.$store.dispatch('startFetchingTimeline', { this.$store.dispatch('startFetchingTimeline', {
timeline: 'quotes', timeline: 'quotes',
statusId: this.statusId, statusId: this.statusId,
@ -21,7 +23,7 @@ const QuotesTimeline = {
}, },
watch: { watch: {
statusId() { statusId() {
this.$store.commit('clearTimeline', { timeline: 'quotes' }) useStatusesStore().clearTimeline({ timeline: 'tag' })
this.$store.dispatch('startFetchingTimeline', { this.$store.dispatch('startFetchingTimeline', {
timeline: 'quotes', timeline: 'quotes',
statusId: this.statusId, statusId: this.statusId,

View file

@ -4,6 +4,7 @@ import Conversation from 'src/components/conversation/conversation.vue'
import FollowCard from 'src/components/follow_card/follow_card.vue' import FollowCard from 'src/components/follow_card/follow_card.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@ -39,11 +40,11 @@ const Search = {
return this.userIds.map((userId) => useUsersStore().findUser(userId)) return this.userIds.map((userId) => useUsersStore().findUser(userId))
}, },
visibleStatuses() { visibleStatuses() {
const allStatusesObject = this.$store.state.statuses.allStatusesObject const allStatuses = useStatusesStore().allStatuses
return this.statuses.filter( return this.statuses.filter(
(status) => (status) =>
allStatusesObject[status.id] && !allStatusesObject[status.id].deleted, allStatuses.has(status.id) && !allStatuses.get(status.id).deleted,
) )
}, },
}, },

View file

@ -11,6 +11,7 @@ import UnitSetting from '../helpers/unit_setting.vue'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useStatusesStore } from 'src/stores/statuses.js'
const ClutterTab = { const ClutterTab = {
components: { components: {
@ -35,7 +36,7 @@ const ClutterTab = {
// Updating nested properties // Updating nested properties
watch: { watch: {
replyVisibility() { replyVisibility() {
this.$store.dispatch('queueFlushAll') useStatusesStore().queueFlushAll()
}, },
}, },
} }

View file

@ -14,6 +14,7 @@ import UnitSetting from '../helpers/unit_setting.vue'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface' import { useInterfaceStore } from 'src/stores/interface'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { import {
@ -265,7 +266,7 @@ const FilteringTab = {
// Updating nested properties // Updating nested properties
watch: { watch: {
replyVisibility() { replyVisibility() {
this.$store.dispatch('queueFlushAll') useStatusesStore().queueFlushAll()
}, },
muteFiltersObject() { muteFiltersObject() {
this.muteFiltersDraftObject = cloneDeep( this.muteFiltersDraftObject = cloneDeep(

View file

@ -23,6 +23,7 @@ import {
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUserHighlightStore } from 'src/stores/user_highlight.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
@ -206,7 +207,7 @@ const Status = {
}, },
statusFromGlobalRepository() { statusFromGlobalRepository() {
// NOTE: Consider to replace status with statusFromGlobalRepository // NOTE: Consider to replace status with statusFromGlobalRepository
return this.$store.state.statuses.allStatusesObject[this.status.id] return useStatusesStore().allStatuses.get(this.status.id)
}, },
loggedIn() { loggedIn() {
return !!this.currentUser return !!this.currentUser

View file

@ -3,6 +3,7 @@ import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useReportsStore } from 'src/stores/reports.js' import { useReportsStore } from 'src/stores/reports.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useStatusHistoryStore } from 'src/stores/statusHistory.js' import { useStatusHistoryStore } from 'src/stores/statusHistory.js'
const PRIVATE_SCOPES = new Set(['private', 'direct']) const PRIVATE_SCOPES = new Set(['private', 'direct'])
@ -52,7 +53,7 @@ export const BUTTONS = [
(currentUser.id === status.user.id || (currentUser.id === status.user.id ||
!PRIVATE_SCOPES.has(status.visibility)), !PRIVATE_SCOPES.has(status.visibility)),
toggleable: true, toggleable: true,
confirm: ({ status, getters }) => confirm: ({ status }) =>
!status.repeated && useMergedConfigStore().mergedConfig.modalOnRepeat, !status.repeated && useMergedConfigStore().mergedConfig.modalOnRepeat,
confirmStrings: { confirmStrings: {
title: 'status.repeat_confirm_title', title: 'status.repeat_confirm_title',
@ -60,11 +61,11 @@ export const BUTTONS = [
confirm: 'status.repeat_confirm_accept_button', confirm: 'status.repeat_confirm_accept_button',
cancel: 'status.repeat_confirm_cancel_button', cancel: 'status.repeat_confirm_cancel_button',
}, },
action({ status, dispatch }) { action({ status }) {
if (!status.repeated) { if (!status.repeated) {
return dispatch('retweet', { id: status.id }) return useStatusesStore().retweet(status.id)
} else { } else {
return dispatch('unretweet', { id: status.id }) return useStatusesStore().unretweet(status.id)
} }
}, },
}, },
@ -82,11 +83,11 @@ export const BUTTONS = [
counter: ({ status }) => status.fave_num, counter: ({ status }) => status.fave_num,
anonLink: true, anonLink: true,
toggleable: true, toggleable: true,
action({ status, dispatch }) { action({ status }) {
if (!status.favorited) { if (!status.favorited) {
return dispatch('favorite', { id: status.id }) return useStatusesStore().favorite(status.id)
} else { } else {
return dispatch('unfavorite', { id: status.id }) return useStatusesStore().unfavorite(status.id)
} }
}, },
}, },
@ -112,7 +113,7 @@ export const BUTTONS = [
if: ({ loggedIn }) => loggedIn, if: ({ loggedIn }) => loggedIn,
toggleable: false, toggleable: false,
dropdown: true, dropdown: true,
action({ status, dispatch, emit }) { action({ status, emit }) {
/* prevent hiding */ /* prevent hiding */
}, },
}, },
@ -130,11 +131,11 @@ export const BUTTONS = [
PUBLIC_SCOPES.has(status.visibility) PUBLIC_SCOPES.has(status.visibility)
) )
}, },
action({ status, dispatch }) { action({ status }) {
if (status.pinned) { if (status.pinned) {
return dispatch('unpinStatus', status.id) return useStatusesStore().unpinStatus(status.id)
} else { } else {
return dispatch('pinStatus', status.id) return useStatusesStore().pinStatus(status.id)
} }
}, },
}, },
@ -150,11 +151,11 @@ export const BUTTONS = [
label: ({ status }) => label: ({ status }) =>
status.bookmarked ? 'status.unbookmark' : 'status.bookmark', status.bookmarked ? 'status.unbookmark' : 'status.bookmark',
if: ({ loggedIn }) => loggedIn, if: ({ loggedIn }) => loggedIn,
action({ status, dispatch }) { action({ status }) {
if (status.bookmarked) { if (status.bookmarked) {
return dispatch('unbookmark', { id: status.id }) return useStatusesStore().unbookmark(status.id)
} else { } else {
return dispatch('bookmark', { id: status.id }) return useStatusesStore().bookmark(status.id)
} }
}, },
}, },
@ -165,7 +166,7 @@ export const BUTTONS = [
name: 'editHistory', name: 'editHistory',
icon: 'history', icon: 'history',
label: 'status.status_history', label: 'status.status_history',
if({ status, state }) { if({ status }) {
return ( return (
useInstanceCapabilitiesStore().editingAvailable && useInstanceCapabilitiesStore().editingAvailable &&
status.edited_at !== null status.edited_at !== null
@ -196,26 +197,28 @@ export const BUTTONS = [
name: 'edit', name: 'edit',
icon: 'pen', icon: 'pen',
label: 'status.edit', label: 'status.edit',
if({ status, loggedIn, currentUser, state }) { if({ status, loggedIn, currentUser }) {
return ( return (
loggedIn && loggedIn &&
useInstanceCapabilitiesStore().editingAvailable && useInstanceCapabilitiesStore().editingAvailable &&
status.user.id === currentUser.id status.user.id === currentUser.id
) )
}, },
action({ dispatch, status }) { action({ status }) {
return dispatch('fetchStatusSource', { id: status.id }).then((data) => return useStatusesStore()
useEditStatusStore().openEditStatusModal({ .fetchStatusSource(status.id)
statusId: status.id, .then((data) =>
statusSubject: data.spoiler_text, useEditStatusStore().openEditStatusModal({
statusText: data.text, statusId: status.id,
statusIsSensitive: status.nsfw, statusSubject: data.spoiler_text,
statusPoll: status.poll, statusText: data.text,
statusFiles: [...status.attachments], statusIsSensitive: status.nsfw,
statusVisibility: status.visibility, statusPoll: status.poll,
statusContentType: data.content_type, statusFiles: [...status.attachments],
}), statusVisibility: status.visibility,
) statusContentType: data.content_type,
}),
)
}, },
}, },
{ {
@ -260,15 +263,15 @@ export const BUTTONS = [
currentUser.privileges.has('messages_delete')) currentUser.privileges.has('messages_delete'))
) )
}, },
confirm: ({ getters }) => useMergedConfigStore().mergedConfig.modalOnDelete, confirm: () => useMergedConfigStore().mergedConfig.modalOnDelete,
confirmStrings: { confirmStrings: {
title: 'status.delete_confirm_title', title: 'status.delete_confirm_title',
body: 'status.delete_confirm', body: 'status.delete_confirm',
confirm: 'status.delete_confirm_accept_button', confirm: 'status.delete_confirm_accept_button',
cancel: 'status.delete_confirm_cancel_button', cancel: 'status.delete_confirm_cancel_button',
}, },
action({ dispatch, status }) { action({ status }) {
return dispatch('deleteStatus', { id: status.id }) return useStatusesStore().deleteStatus(status.id)
}, },
}, },
{ {
@ -287,7 +290,7 @@ export const BUTTONS = [
}, },
toggleable: false, toggleable: false,
dropdown: true, dropdown: true,
action({ status, dispatch, emit }) { action({ status, emit }) {
/* prevent hiding */ /* prevent hiding */
}, },
}, },
@ -298,7 +301,7 @@ export const BUTTONS = [
name: 'share', name: 'share',
icon: 'share-alt', icon: 'share-alt',
label: 'status.copy_link', label: 'status.copy_link',
action({ state, status, router }) { action({ status, router }) {
navigator.clipboard.writeText( navigator.clipboard.writeText(
[ [
useInstanceStore().server, useInstanceStore().server,

View file

@ -2,6 +2,7 @@ import { get } from 'lodash'
import Modal from 'src/components/modal/modal.vue' import Modal from 'src/components/modal/modal.vue'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useStatusHistoryStore } from 'src/stores/statusHistory.js' import { useStatusHistoryStore } from 'src/stores/statusHistory.js'
const StatusHistoryModal = { const StatusHistoryModal = {
@ -50,9 +51,11 @@ const StatusHistoryModal = {
this.statuses = [] this.statuses = []
}, },
fetchStatusHistory() { fetchStatusHistory() {
this.$store.dispatch('fetchStatusHistory', this.params).then((data) => { useStatusesStore()
this.statuses = data .fetchStatusHistory(this.params)
}) .then((data) => {
this.statuses = data
})
}, },
closeModal() { closeModal() {
useStatusHistoryStore().closeStatusHistoryModal() useStatusHistoryStore().closeStatusHistoryModal()

View file

@ -1,7 +1,7 @@
import { find } from 'lodash'
import Popover from 'src/components/popover/popover.vue' import Popover from 'src/components/popover/popover.vue'
import { useStatusesStore } from 'src/stores/statuses.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons'
@ -17,7 +17,7 @@ const StatusPopover = {
}, },
computed: { computed: {
status() { status() {
return find(this.$store.state.statuses.allStatuses, { id: this.statusId }) return useStatusesStore().allStatuses.get(this.statusId)
}, },
}, },
components: { components: {
@ -30,8 +30,8 @@ const StatusPopover = {
this.error = true this.error = true
return return
} }
this.$store useStatusesStore()
.dispatch('fetchStatus', this.statusId) .fetchStatus(this.statusId)
.then(() => (this.error = false)) .then(() => (this.error = false))
.catch(() => (this.error = true)) .catch(() => (this.error = true))
} }

View file

@ -1,8 +1,10 @@
import Timeline from 'src/components/timeline/timeline.vue' import Timeline from 'src/components/timeline/timeline.vue'
import { useStatusesStore } from 'src/stores/statuses.js'
const TagTimeline = { const TagTimeline = {
created() { created() {
this.$store.commit('clearTimeline', { timeline: 'tag' }) useStatusesStore().clearTimeline({ timeline: 'tag' })
this.$store.dispatch('startFetchingTimeline', { this.$store.dispatch('startFetchingTimeline', {
timeline: 'tag', timeline: 'tag',
tag: this.tag, tag: this.tag,
@ -21,7 +23,7 @@ const TagTimeline = {
}, },
watch: { watch: {
tag() { tag() {
this.$store.commit('clearTimeline', { timeline: 'tag' }) useStatusesStore().clearTimeline({ timeline: 'tag' })
this.$store.dispatch('startFetchingTimeline', { this.$store.dispatch('startFetchingTimeline', {
timeline: 'tag', timeline: 'tag',
tag: this.tag, tag: this.tag,

View file

@ -9,6 +9,7 @@ import TimelineMenu from 'src/components/timeline_menu/timeline_menu.vue'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
import timelineFetcher from 'src/services/timeline_fetcher/timeline_fetcher.service.js' import timelineFetcher from 'src/services/timeline_fetcher/timeline_fetcher.service.js'
@ -26,21 +27,19 @@ import {
library.add(faCircleNotch, faCog, faMinus, faArrowUp, faCirclePlus, faCheck) library.add(faCircleNotch, faCog, faMinus, faArrowUp, faCirclePlus, faCheck)
const Timeline = { const Timeline = {
props: [ props: {
'timeline', timelineName: String,
'timelineName', userId: String,
'title', listId: String,
'userId', statusId: String,
'listId', bookmarkFolderId: String,
'statusId', tag: String,
'bookmarkFolderId', embedded: Boolean,
'tag', count: Number,
'embedded', pinnedStatusIds: Set,
'count', inProfile: Boolean,
'pinnedStatusIds', footerSlipgate: Object, // reference to an element where we should put our footer
'inProfile', },
'footerSlipgate', // reference to an element where we should put our footer
],
data() { data() {
return { return {
showScrollTop: false, showScrollTop: false,
@ -59,8 +58,11 @@ const Timeline = {
QuickViewSettings, QuickViewSettings,
}, },
computed: { computed: {
timeline() {
return useStatusesStore().timelines[this.timelineName]
},
filteredVisibleStatuses() { filteredVisibleStatuses() {
return this.timeline.visibleStatuses.filter( return [...this.timeline.visibleStatuses.values()].filter(
(status) => (status) =>
this.timelineName !== 'user' || this.timelineName !== 'user' ||
(status.id >= this.timeline.minId && (status.id >= this.timeline.minId &&
@ -116,13 +118,13 @@ const Timeline = {
return keyBy(this.pinnedStatusIds) return keyBy(this.pinnedStatusIds)
}, },
statusesToDisplay() { statusesToDisplay() {
const amount = this.timeline.visibleStatuses.length const amount = this.timeline.visibleStatuses.size
const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80)) const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80))
const nonPinnedIndex = const nonPinnedIndex =
this.virtualScrollIndex - this.filteredPinnedStatusIds.length this.virtualScrollIndex - this.filteredPinnedStatusIds.length
const min = Math.max(0, nonPinnedIndex - statusesPerSide) const min = Math.max(0, nonPinnedIndex - statusesPerSide)
const max = Math.min(amount, nonPinnedIndex + statusesPerSide) const max = Math.min(amount, nonPinnedIndex + statusesPerSide)
return this.timeline.visibleStatuses.slice(min, max).map((_) => _.id) return new Set([...this.timeline.visibleStatuses.keys()].slice(min, max))
}, },
virtualScrollingEnabled() { virtualScrollingEnabled() {
return useMergedConfigStore().mergedConfig.virtualScrolling return useMergedConfigStore().mergedConfig.virtualScrolling
@ -143,7 +145,6 @@ const Timeline = {
} }
timelineFetcher.fetchAndUpdate({ timelineFetcher.fetchAndUpdate({
store,
credentials, credentials,
timeline: this.timelineName, timeline: this.timelineName,
showImmediately, showImmediately,
@ -175,7 +176,7 @@ const Timeline = {
this.handleVisibilityChange, this.handleVisibilityChange,
false, false,
) )
this.$store.commit('setLoading', { useStatusesStore().setLoading({
timeline: this.timelineName, timeline: this.timelineName,
value: false, value: false,
}) })
@ -197,30 +198,31 @@ const Timeline = {
}, },
showNewStatuses() { showNewStatuses() {
if (this.timeline.flushMarker !== 0) { if (this.timeline.flushMarker !== 0) {
this.$store.commit('clearTimeline', { useStatusesStore().clearTimeline({
timeline: this.timelineName, timeline: this.timelineName,
excludeUserId: true, excludeUserId: true,
}) })
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 }) useStatusesStore().queueFlush({ timeline: this.timelineName, id: 0 })
if (this.timelineName === 'user') { if (this.timelineName === 'user') {
this.$store.dispatch('fetchPinnedStatuses', this.userId) this.$store.dispatch('fetchPinnedStatuses', this.userId)
} }
this.fetchOlderStatuses() this.fetchOlderStatuses()
} else { } else {
this.blockClicksTemporarily() this.blockClicksTemporarily()
this.$store.commit('showNewStatuses', { timeline: this.timelineName }) useStatusesStore().showNewStatuses(this.timelineName)
this.paused = false this.paused = false
} }
window.scrollTo({ top: 0 }) window.scrollTo({ top: 0 })
}, },
fetchOlderStatuses: throttle( fetchOlderStatuses: throttle(
function () { function () {
const store = this.$store
const credentials = useUsersStore().currentUser.credentials const credentials = useUsersStore().currentUser.credentials
store.commit('setLoading', { timeline: this.timelineName, value: true }) useStatusesStore().setLoading({
timeline: this.timelineName,
value: true,
})
timelineFetcher timelineFetcher
.fetchAndUpdate({ .fetchAndUpdate({
store,
credentials, credentials,
timeline: this.timelineName, timeline: this.timelineName,
older: true, older: true,
@ -237,7 +239,7 @@ const Timeline = {
} }
}) })
.finally(() => .finally(() =>
store.commit('setLoading', { useStatusesStore().setLoading({
timeline: this.timelineName, timeline: this.timelineName,
value: false, value: false,
}), }),

View file

@ -90,7 +90,7 @@
:status-id="status.id" :status-id="status.id"
:in-profile="inProfile" :in-profile="inProfile"
:profile-user-id="userId" :profile-user-id="userId"
:virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.includes(status.id)" :virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.has(status.id)"
collapsable collapsable
/> />
</div> </div>

View file

@ -9,6 +9,7 @@ import UserCard from 'src/components/user_card/user_card.vue'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@ -106,7 +107,9 @@ const UserProfile = {
const startFetchingTimeline = (timeline, userId) => { const startFetchingTimeline = (timeline, userId) => {
// Clear timeline only if load another user's profile // Clear timeline only if load another user's profile
if (userId !== this.$store.state.statuses.timelines[timeline].userId) { if (userId !== this.$store.state.statuses.timelines[timeline].userId) {
this.$store.commit('clearTimeline', { timeline }) useStatusesStore().clearTimeline({ timeline: 'user' })
useStatusesStore().clearTimeline({ timeline: 'userPinned' })
useStatusesStore().clearTimeline({ timeline: 'media' })
} }
this.$store.dispatch('startFetchingTimeline', { timeline, userId }) this.$store.dispatch('startFetchingTimeline', { timeline, userId })
} }

View file

@ -8,6 +8,7 @@ import { useInterfaceStore } from 'src/stores/interface.js'
import { useNotificationsStore } from 'src/stores/notifications.js' import { useNotificationsStore } from 'src/stores/notifications.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useShoutStore } from 'src/stores/shout.js' import { useShoutStore } from 'src/stores/shout.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { fetchTimeline } from 'src/api/timelines.js' import { fetchTimeline } from 'src/api/timelines.js'
import { import {
@ -123,14 +124,16 @@ const api = {
data: message.notification, data: message.notification,
}) })
} else if (message.event === 'update') { } else if (message.event === 'update') {
dispatch('addNewStatuses', { useStatusesStore().addNewStatuses({
timestamp: Date.now(),
statuses: [message.status], statuses: [message.status],
userId: false, userId: false,
showImmediately: timelineData.visibleStatuses.length === 0, showImmediately: timelineData.visibleStatuses.length === 0,
timeline: 'friends', timeline: 'friends',
}) })
} else if (message.event === 'status.update') { } else if (message.event === 'status.update') {
dispatch('addNewStatuses', { useStatusesStore().addNewStatuses({
timestamp: Date.now(),
statuses: [message.status], statuses: [message.status],
userId: false, userId: false,
showImmediately: showImmediately:

View file

@ -1,10 +1,8 @@
import api from './api.js' import api from './api.js'
import drafts from './drafts.js' import drafts from './drafts.js'
import profileConfig from './profileConfig.js' import profileConfig from './profileConfig.js'
import statuses from './statuses.js'
export default { export default {
statuses,
api, api,
profileConfig, profileConfig,
drafts, drafts,

View file

@ -1,906 +0,0 @@
import {
each,
find,
findIndex,
first,
last,
maxBy,
merge,
minBy,
omitBy,
remove,
} from 'lodash'
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'
const emptyTl = (userId = 0) => ({
statuses: [],
statusesObject: {},
faves: [],
visibleStatuses: [],
visibleStatusesObject: {},
newStatusCount: 0,
maxId: '',
minId: '',
minVisibleId: 0,
loading: false,
followers: [],
friends: [],
userId,
flushMarker: 0,
})
export const defaultState = () => ({
allStatuses: [],
scrobblesNextFetch: {},
allStatusesObject: {},
conversationsObject: {},
maxId: '',
favorites: new Set(),
timelines: {
mentions: emptyTl(),
public: emptyTl(),
user: emptyTl(),
favorites: emptyTl(),
media: emptyTl(),
publicAndExternal: emptyTl(),
friends: emptyTl(),
tag: emptyTl(),
dms: emptyTl(),
bookmarks: emptyTl(),
list: emptyTl(),
bubble: emptyTl(),
},
})
export const prepareStatus = (status) => {
// Set deleted flag
status.deleted = false
// To make the array reactive
status.attachments = status.attachments || []
return status
}
const mergeOrAdd = (arr, obj, item) => {
const oldItem = obj[item.id]
if (oldItem) {
// We already have this, so only merge the new info.
// We ignore null values to avoid overwriting existing properties with missing data
// we also skip 'user' because that is handled by users module
merge(
oldItem,
omitBy(item, (v, k) => v === null || k === 'user'),
)
// Reactivity fix.
oldItem.attachments.splice(oldItem.attachments.length)
return { item: oldItem, new: false }
} else {
// This is a new item, prepare it
prepareStatus(item)
arr.push(item)
obj[item.id] = item
return { item, new: true }
}
}
const sortById = (a, b) => {
const seqA = Number(a.id)
const seqB = Number(b.id)
const isSeqA = !Number.isNaN(seqA)
const isSeqB = !Number.isNaN(seqB)
if (isSeqA && isSeqB) {
return seqA > seqB ? -1 : 1
} else if (isSeqA && !isSeqB) {
return 1
} else if (!isSeqA && isSeqB) {
return -1
} else {
return a.id > b.id ? -1 : 1
}
}
const sortTimeline = (timeline) => {
timeline.visibleStatuses = timeline.visibleStatuses.sort(sortById)
timeline.statuses = timeline.statuses.sort(sortById)
timeline.minVisibleId = last(timeline.visibleStatuses)?.id
return timeline
}
const getLatestScrobble = (state, user) => {
const scrobblesSupport =
useInstanceCapabilitiesStore().pleromaScrobblesAvailable
if (!scrobblesSupport || !user.name || user.id === 'undefined') {
return
}
if (
state.scrobblesNextFetch[user.id] &&
state.scrobblesNextFetch[user.id] > Date.now()
) {
return
}
state.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]
state.scrobblesNextFetch[user.id] = Date.now() + 60 * 1000
}
})
.catch((e) => {
console.warn('cannot fetch scrobbles', e)
})
}
// Add status to the global storages (arrays and objects maintaining statuses) except timelines
const addStatusToGlobalStorage = (state, data) => {
getLatestScrobble(state, data.user)
const result = mergeOrAdd(state.allStatuses, state.allStatusesObject, data)
if (result.new) {
// Add to conversation
const status = result.item
const conversationsObject = state.conversationsObject
const conversationId = status.statusnet_conversation_id
if (conversationsObject[conversationId]) {
conversationsObject[conversationId].push(status)
} else {
conversationsObject[conversationId] = [status]
}
}
return result
}
const addNewStatuses = (
state,
{
statuses,
showImmediately = false,
timeline,
user = {},
noIdUpdate = false,
userId,
pagination = {},
},
) => {
// Sanity check
if (!Array.isArray(statuses)) {
return false
}
const allStatuses = state.allStatuses
const timelineObject = state.timelines[timeline]
// Mismatch between API pagination and our internal minId/maxId tracking systems:
// pagination.maxId is the oldest of the returned statuses when fetching older,
// and pagination.minId is the newest when fetching newer. The names come directly
// from the arguments they're supposed to be passed as for the next fetch.
const minNew =
pagination.maxId || (statuses.length > 0 ? minBy(statuses, 'id').id : 0)
const maxNew =
pagination.minId || (statuses.length > 0 ? maxBy(statuses, 'id').id : 0)
const newer =
timeline &&
(maxNew > timelineObject.maxId || timelineObject.maxId === '') &&
statuses.length > 0
const older =
timeline &&
(minNew < timelineObject.minId || timelineObject.minId === '') &&
statuses.length > 0
if (!noIdUpdate && newer) {
timelineObject.maxId = maxNew
}
if (!noIdUpdate && older) {
timelineObject.minId = minNew
}
// 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
if (
(timeline === 'user' || timeline === 'media') &&
timelineObject.userId !== userId
) {
return
}
const addStatus = (data, showImmediately, addToTimeline = true) => {
const result = addStatusToGlobalStorage(state, data)
const status = result.item
if (result.new) {
// We are mentioned in a post
if (
status.type === 'status' &&
find(status.attentions, { id: user.id })
) {
const mentions = state.timelines.mentions
// Add the mention to the mentions timeline
if (timelineObject !== mentions) {
mergeOrAdd(mentions.statuses, mentions.statusesObject, status)
mentions.newStatusCount += 1
sortTimeline(mentions)
}
}
if (status.visibility === 'direct') {
const dms = state.timelines.dms
mergeOrAdd(dms.statuses, dms.statusesObject, status)
dms.newStatusCount += 1
sortTimeline(dms)
}
}
// Decide if we should treat the status as new for this timeline.
let resultForCurrentTimeline
// Some statuses should only be added to the global status repository.
if (timeline && addToTimeline) {
resultForCurrentTimeline = mergeOrAdd(
timelineObject.statuses,
timelineObject.statusesObject,
status,
)
}
if (timeline && showImmediately) {
// Add it directly to the visibleStatuses, don't change
// newStatusCount
mergeOrAdd(
timelineObject.visibleStatuses,
timelineObject.visibleStatusesObject,
status,
)
} else if (timeline && addToTimeline && resultForCurrentTimeline.new) {
// Just change newStatuscount
timelineObject.newStatusCount += 1
}
if (status.quote) {
addStatus(
status.quote,
/* showImmediately = */ false,
/* addToTimeline = */ false,
)
}
return status
}
const favoriteStatus = (favorite) => {
const status = find(allStatuses, { id: 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
}
const processors = {
status: (status) => {
addStatus(status, showImmediately)
},
edit: (status) => {
addStatus(status, showImmediately)
},
retweet: (status) => {
// RetweetedStatuses are never shown immediately
const retweetedStatus = addStatus(status.retweeted_status, false, false)
let retweet
// If the retweeted status is already there, don't add the retweet
// to the timeline.
if (
timeline &&
find(timelineObject.statuses, (s) => {
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 (!state.favorites.has(favorite.id)) {
state.favorites.add(favorite.id)
favoriteStatus(favorite)
}
},
follow: () => {
// NOOP, it is known status but we don't do anything about it for now
},
default: (unknown) => {
console.warn('unknown status type', unknown)
},
}
each(statuses, (status) => {
const type = status.type
const processor = processors[type] || processors.default
processor(status)
})
// Keep the visible statuses sorted
if (timeline && !(timeline === 'bookmarks')) {
sortTimeline(timelineObject)
}
}
const removeStatus = (state, { timeline, userId }) => {
const timelineObject = state.timelines[timeline]
if (userId) {
remove(timelineObject.statuses, { user: { id: userId } })
remove(timelineObject.visibleStatuses, { user: { id: userId } })
timelineObject.minVisibleId =
timelineObject.visibleStatuses.length > 0
? last(timelineObject.visibleStatuses).id
: 0
timelineObject.maxId =
timelineObject.statuses.length > 0 ? first(timelineObject.statuses).id : 0
}
}
export const mutations = {
addNewStatuses,
removeStatus,
showNewStatuses(state, { timeline }) {
const oldTimeline = state.timelines[timeline]
oldTimeline.newStatusCount = 0
oldTimeline.visibleStatuses = oldTimeline.statuses.slice(0, 50)
oldTimeline.minVisibleId = last(oldTimeline.visibleStatuses).id
oldTimeline.minId = oldTimeline.minVisibleId
oldTimeline.visibleStatusesObject = {}
each(oldTimeline.visibleStatuses, (status) => {
oldTimeline.visibleStatusesObject[status.id] = status
})
},
resetStatuses(state) {
const emptyState = defaultState()
Object.entries(emptyState).forEach(([key, value]) => {
state[key] = value
})
},
clearTimeline(state, { timeline, excludeUserId = false }) {
const userId = excludeUserId ? state.timelines[timeline].userId : undefined
state.timelines[timeline] = emptyTl(userId)
},
setFavorited(state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
if (newStatus.favorited !== value) {
if (value) {
newStatus.fave_num++
} else {
newStatus.fave_num--
}
}
newStatus.favorited = value
},
setFavoritedConfirm(state, { status, user }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.favorited = status.favorited
newStatus.fave_num = status.fave_num
const index = findIndex(newStatus.favoritedBy, { id: user.id })
if (index !== -1 && !newStatus.favorited) {
newStatus.favoritedBy.splice(index, 1)
} else if (index === -1 && newStatus.favorited) {
newStatus.favoritedBy.push(user)
}
},
setMutedStatus(state, status) {
const newStatus = state.allStatusesObject[status.id]
newStatus.thread_muted = status.thread_muted
if (newStatus.thread_muted !== undefined) {
state.conversationsObject[newStatus.statusnet_conversation_id].forEach(
(status) => {
status.thread_muted = newStatus.thread_muted
},
)
}
},
setRetweeted(state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
if (newStatus.repeated !== value) {
if (value) {
newStatus.repeat_num++
} else {
newStatus.repeat_num--
}
}
newStatus.repeated = value
},
setRetweetedConfirm(state, { status, user }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.repeated = status.repeated
newStatus.repeat_num = status.repeat_num
const index = findIndex(newStatus.rebloggedBy, { id: user.id })
if (index !== -1 && !newStatus.repeated) {
newStatus.rebloggedBy.splice(index, 1)
} else if (index === -1 && newStatus.repeated) {
newStatus.rebloggedBy.push(user)
}
},
setBookmarked(state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.bookmarked = value
newStatus.bookmark_folder_id = status.bookmark_folder_id
},
setBookmarkedConfirm(state, { status }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.bookmarked = status.bookmarked
if (status.pleroma)
newStatus.bookmark_folder_id = status.pleroma.bookmark_folder
},
setDeleted(state, { status }) {
const newStatus = state.allStatusesObject[status.id]
if (newStatus) newStatus.deleted = true
},
setManyDeleted(state, condition) {
Object.values(state.allStatusesObject).forEach((status) => {
if (condition(status)) {
status.deleted = true
}
})
},
setLoading(state, { timeline, value }) {
state.timelines[timeline].loading = value
},
setNsfw(state, { id, nsfw }) {
const newStatus = state.allStatusesObject[id]
newStatus.nsfw = nsfw
},
queueFlush(state, { timeline, id }) {
state.timelines[timeline].flushMarker = id
},
queueFlushAll(state) {
Object.keys(state.timelines).forEach((timeline) => {
state.timelines[timeline].flushMarker = state.timelines[timeline].maxId
})
},
addRepeats(state, { id, rebloggedByUsers, currentUser }) {
const newStatus = state.allStatusesObject[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(state, { id, favoritedByUsers, currentUser }) {
const newStatus = state.allStatusesObject[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(state, { id, emojiReactions }) {
const status = state.allStatusesObject[id]
status.emoji_reactions = emojiReactions
},
addOwnReaction(state, { id, emoji, currentUser }) {
const status = state.allStatusesObject[id]
const reactionIndex = findIndex(status.emoji_reactions, { 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 = [...status.emoji_reactions, newReaction]
}
},
removeOwnReaction(state, { id, emoji, currentUser }) {
const status = state.allStatusesObject[id]
const reactionIndex = findIndex(status.emoji_reactions, { 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,
)
}
},
updateStatusWithPoll(state, { id, poll }) {
const status = state.allStatusesObject[id]
status.poll = poll
},
setVirtualHeight(state, { statusId, height }) {
state.allStatusesObject[statusId].virtualHeight = height
},
}
const statuses = {
state: defaultState(),
actions: {
addNewStatuses(
{ rootState, commit },
{
statuses,
showImmediately = false,
timeline = false,
noIdUpdate = false,
userId,
pagination,
},
) {
return commit('addNewStatuses', {
statuses,
showImmediately,
timeline,
noIdUpdate,
user: useUsersStore().currentUser,
userId,
pagination,
})
},
fetchStatus({ rootState, dispatch }, id) {
return fetchStatus({ id }).then(({ data: status }) =>
dispatch('addNewStatuses', { statuses: [status] }),
)
},
fetchStatusSource({ rootState }, status) {
return fetchStatusSource({
id: status.id,
credentials: useOAuthStore().token,
}).then(({ data }) => data)
},
fetchStatusHistory(_, status) {
return fetchStatusHistory({ status }).then(({ data }) => data)
},
deleteStatus({ rootState, commit }, status) {
deleteStatus({
id: status.id,
credentials: useOAuthStore().token,
})
.then(() => {
commit('setDeleted', { status })
})
.catch((e) => {
useInterfaceStore().pushGlobalNotice({
level: 'error',
messageKey: 'status.delete_error',
messageArgs: [e.message],
timeout: 5000,
})
})
},
deleteStatusById({ rootState, commit }, id) {
const status = rootState.statuses.allStatusesObject[id]
commit('setDeleted', { status })
},
markStatusesAsDeleted({ commit }, condition) {
commit('setManyDeleted', condition)
},
favorite({ rootState, commit }, status) {
// Optimistic favoriting...
commit('setFavorited', { status, value: true })
favorite({
id: status.id,
credentials: useOAuthStore().token,
}).then(({ data: status }) =>
commit('setFavoritedConfirm', {
status,
user: useUsersStore().currentUser,
}),
)
},
unfavorite({ rootState, commit }, status) {
// Optimistic unfavoriting...
commit('setFavorited', { status, value: false })
unfavorite({
id: status.id,
credentials: useOAuthStore().token,
}).then(({ data: status }) =>
commit('setFavoritedConfirm', {
status,
user: useUsersStore().currentUser,
}),
)
},
fetchPinnedStatuses({ rootState, dispatch }, userId) {
fetchPinnedStatuses({
id: userId,
credentials: useOAuthStore().token,
}).then(({ data: statuses }) =>
dispatch('addNewStatuses', {
statuses,
timeline: 'user',
userId,
showImmediately: true,
noIdUpdate: true,
}),
)
},
pinStatus({ rootState, dispatch }, statusId) {
return pinOwnStatus({
id: statusId,
credentials: useOAuthStore().token,
}).then(({ data: status }) =>
dispatch('addNewStatuses', { statuses: [status] }),
)
},
unpinStatus({ rootState, dispatch }, statusId) {
return unpinOwnStatus({
id: statusId,
credentials: useOAuthStore().token,
}).then(({ data: status }) =>
dispatch('addNewStatuses', { statuses: [status] }),
)
},
muteConversation({ rootState, commit }, { id: statusId }) {
return muteConversation({
id: statusId,
credentials: useOAuthStore().token,
}).then(({ data: status }) => commit('setMutedStatus', status))
},
unmuteConversation({ rootState, commit }, { id: statusId }) {
return unmuteConversation({
id: statusId,
credentials: useOAuthStore().token,
}).then(({ data: status }) => commit('setMutedStatus', status))
},
retweet({ rootState, commit }, status) {
// Optimistic retweeting...
commit('setRetweeted', { status, value: true })
retweet({
id: status.id,
credentials: useOAuthStore().token,
}).then(({ data: status }) =>
commit('setRetweetedConfirm', {
status: status.retweeted_status,
user: useUsersStore().currentUser,
}),
)
},
unretweet({ rootState, commit }, status) {
// Optimistic unretweeting...
commit('setRetweeted', { status, value: false })
unretweet({
id: status.id,
credentials: useOAuthStore().token,
}).then(({ data: status }) =>
commit('setRetweetedConfirm', {
status,
user: useUsersStore().currentUser,
}),
)
},
bookmark({ rootState, commit }, status) {
commit('setBookmarked', { status, value: true })
bookmarkStatus({
id: status.id,
folder_id: status.bookmark_folder_id,
credentials: useOAuthStore().token,
}).then(({ data: status }) => {
commit('setBookmarkedConfirm', { status })
})
},
unbookmark({ rootState, commit }, status) {
commit('setBookmarked', { status, value: false })
unbookmarkStatus({
id: status.id,
credentials: useOAuthStore().token,
}).then(({ data: status }) => {
commit('setBookmarkedConfirm', { status })
})
},
queueFlush({ commit }, { timeline, id }) {
commit('queueFlush', { timeline, id })
},
queueFlushAll({ commit }) {
commit('queueFlushAll')
},
fetchFavsAndRepeats({ rootState, commit }, id) {
Promise.all([
fetchFavoritedByUsers({
id,
credentials: useOAuthStore().token,
}).then(({ data }) => data),
fetchRebloggedByUsers({
id,
credentials: useOAuthStore().token,
}).then(({ data }) => data),
]).then(([favoritedByUsers, rebloggedByUsers]) => {
commit('addFavs', {
id,
favoritedByUsers,
currentUser: useUsersStore().currentUser,
})
commit('addRepeats', {
id,
rebloggedByUsers,
currentUser: useUsersStore().currentUser,
})
})
},
reactWithEmoji({ rootState, dispatch, commit }, { id, emoji }) {
const currentUser = useUsersStore().currentUser
if (!currentUser) return
commit('addOwnReaction', { id, emoji, currentUser })
reactWithEmoji({
id,
emoji,
credentials: useOAuthStore().token,
}).then(() => {
dispatch('fetchEmojiReactionsBy', id)
})
},
unreactWithEmoji({ rootState, dispatch, commit }, { id, emoji }) {
const currentUser = useUsersStore().currentUser
if (!currentUser) return
commit('removeOwnReaction', { id, emoji, currentUser })
unreactWithEmoji({
id,
emoji,
currentUser: useUsersStore().currentUser,
}).then(() => {
dispatch('fetchEmojiReactionsBy', id)
})
},
fetchEmojiReactionsBy({ rootState, commit }, id) {
return fetchEmojiReactions({
id,
credentials: useOAuthStore().token,
}).then(({ data: emojiReactions }) => {
commit('addEmojiReactionsBy', {
id,
emojiReactions,
currentUser: useUsersStore().currentUser,
})
})
},
fetchFavs({ rootState, commit }, id) {
fetchFavoritedByUsers({
id,
credentials: useOAuthStore().token,
}).then(({ data: favoritedByUsers }) =>
commit('addFavs', {
id,
favoritedByUsers,
currentUser: useUsersStore().currentUser,
}),
)
},
fetchRepeats({ rootState, commit }, id) {
fetchRebloggedByUsers({
id,
credentials: useOAuthStore().token,
}).then(({ data: rebloggedByUsers }) =>
commit('addRepeats', {
id,
rebloggedByUsers,
currentUser: useUsersStore().currentUser,
}),
)
},
search(store, { 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),
})
store.commit('addNewStatuses', {
statuses: data.statuses,
})
data.statuses = data.statuses.map(
(s) => store.state.allStatusesObject[s.id],
)
return data
})
},
setVirtualHeight({ commit }, { statusId, height }) {
commit('setVirtualHeight', { statusId, height })
},
},
mutations,
}
export default statuses

View file

@ -1,5 +1,6 @@
import { map } from 'lodash' import { map } from 'lodash'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
import { import {
@ -38,9 +39,10 @@ const postStatus = ({
poll, poll,
preview, preview,
idempotencyKey, idempotencyKey,
}).then(({ data }) => { }).then(({ data, timestamp }) => {
if (!preview) if (!preview)
store.dispatch('addNewStatuses', { useStatusesStore().addNewStatuses({
timestamp,
statuses: [data], statuses: [data],
timeline: 'friends', timeline: 'friends',
showImmediately: true, showImmediately: true,

View file

@ -5,12 +5,12 @@ import { promiseInterval } from '../promise_interval/promise_interval.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
import { fetchTimeline } from 'src/api/timelines.js' import { fetchTimeline } from 'src/api/timelines.js'
const update = ({ const update = ({
store,
statuses, statuses,
timeline, timeline,
showImmediately, showImmediately,
@ -20,8 +20,8 @@ const update = ({
}) => { }) => {
const ccTimeline = camelCase(timeline) const ccTimeline = camelCase(timeline)
store.dispatch('addNewStatuses', { useStatusesStore().addNewStatuses({
timeline: ccTimeline, timelineName: ccTimeline,
userId, userId,
listId, listId,
statuses, statuses,
@ -31,7 +31,6 @@ const update = ({
} }
const fetchAndUpdate = ({ const fetchAndUpdate = ({
store,
credentials, credentials,
timeline = 'friends', timeline = 'friends',
older = false, older = false,
@ -45,8 +44,7 @@ const fetchAndUpdate = ({
sinceId, sinceId,
}) => { }) => {
const args = { timeline, credentials } const args = { timeline, credentials }
const rootState = store.rootState || store.state const timelineData = useStatusesStore().timelines[camelCase(timeline)]
const timelineData = rootState.statuses.timelines[camelCase(timeline)]
const { hideMutedPosts, replyVisibility } = const { hideMutedPosts, replyVisibility } =
useMergedConfigStore().mergedConfig useMergedConfigStore().mergedConfig
const loggedIn = !!useUsersStore().currentUser const loggedIn = !!useUsersStore().currentUser
@ -86,10 +84,9 @@ const fetchAndUpdate = ({
!timelineData.loading && !timelineData.loading &&
numStatusesBeforeFetch > 0 numStatusesBeforeFetch > 0
) { ) {
store.dispatch('queueFlush', { timeline, id: timelineData.maxId }) useStatusesStore().queueFlush({ timeline, id: timelineData.maxId })
} }
update({ update({
store,
statuses, statuses,
timeline, timeline,
showImmediately, showImmediately,
@ -116,23 +113,21 @@ const fetchAndUpdate = ({
const startFetching = ({ const startFetching = ({
timeline = 'friends', timeline = 'friends',
credentials, credentials,
store,
userId, userId,
listId, listId,
statusId, statusId,
bookmarkFolderId, bookmarkFolderId,
tag, tag,
}) => { }) => {
const rootState = store.rootState || store.state const timelineData = useStatusesStore().timelines[camelCase(timeline)]
const timelineData = rootState.statuses.timelines[camelCase(timeline)] const showImmediately = timelineData.visibleStatuses.size === 0
const showImmediately = timelineData.visibleStatuses.length === 0 console.log(timeline)
timelineData.userId = userId timelineData.userId = userId
timelineData.listId = listId timelineData.listId = listId
timelineData.bookmarkFolderId = bookmarkFolderId timelineData.bookmarkFolderId = bookmarkFolderId
fetchAndUpdate({ fetchAndUpdate({
timeline, timeline,
credentials, credentials,
store,
showImmediately, showImmediately,
userId, userId,
listId, listId,
@ -144,7 +139,6 @@ const startFetching = ({
fetchAndUpdate({ fetchAndUpdate({
timeline, timeline,
credentials, credentials,
store,
userId, userId,
listId, listId,
statusId, statusId,

View file

@ -2,6 +2,7 @@ import { cloneDeep, differenceWith, get, isEqual, set } from 'lodash'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { import {
addNewEmojiFile, addNewEmojiFile,
@ -362,6 +363,7 @@ export const useAdminSettingsStore = defineStore('adminSettings', {
async fetchStatuses(opts) { async fetchStatuses(opts) {
const { const {
data: { total, activities }, data: { total, activities },
timestamp,
} = await listStatuses({ } = await listStatuses({
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
opts, opts,
@ -369,7 +371,7 @@ export const useAdminSettingsStore = defineStore('adminSettings', {
const statuses = activities.map(parseStatus) const statuses = activities.map(parseStatus)
await window.vuex.dispatch('addNewStatuses', { statuses }) useStatusesStore().addNewStatuses({ statuses, timestamp })
return { return {
items: statuses, items: statuses,
@ -377,13 +379,13 @@ export const useAdminSettingsStore = defineStore('adminSettings', {
} }
}, },
async changeStatusScope(opts) { async changeStatusScope(opts) {
const { data } = await changeStatusScope({ const { data, timestamp } = await changeStatusScope({
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
opts, opts,
}) })
const status = parseStatus(data) const status = parseStatus(data)
await window.vuex.dispatch('addNewStatuses', { statuses: [status] }) useStatusesStore().addNewStatuses({ statuses: [status], timestamp })
}, },
// Users stuff // Users stuff

View file

@ -14,6 +14,7 @@ import { useI18nStore } from 'src/stores/i18n.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useReportsStore } from 'src/stores/reports.js' import { useReportsStore } from 'src/stores/reports.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
@ -80,7 +81,7 @@ export const useNotificationsStore = defineStore('notifications', {
) )
// Synchronous commit to add all the statuses // Synchronous commit to add all the statuses
window.vuex.commit('addNewStatuses', { useStatusesStore().addNewStatuses({
timestamp, timestamp,
statuses: statusNotifications.map( statuses: statusNotifications.map(
(notification) => notification.status, (notification) => notification.status,
@ -90,7 +91,7 @@ export const useNotificationsStore = defineStore('notifications', {
// Update references to statuses in notifications to ones in the store // Update references to statuses in notifications to ones in the store
statusNotifications.forEach((notification) => { statusNotifications.forEach((notification) => {
const id = notification.status.id const id = notification.status.id
const referenceStatus = window.vuex.state.statuses.allStatusesObject[id] const referenceStatus = useStatusesStore().allStatuses.get(id)
if (referenceStatus) { if (referenceStatus) {
notification.status = referenceStatus notification.status = referenceStatus
@ -103,7 +104,7 @@ export const useNotificationsStore = defineStore('notifications', {
} }
if (notification.type === 'pleroma:emoji_reaction') { if (notification.type === 'pleroma:emoji_reaction') {
window.vuex.dispatch('fetchEmojiReactionsBy', notification.status.id) useStatusesStore().fetchEmojiReactionsBy(notification.status.id)
} }
// Only add a new notification if we don't have one for the same action // Only add a new notification if we don't have one for the same action

View file

@ -3,6 +3,7 @@ import { defineStore } from 'pinia'
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { setReportState } from 'src/api/admin.js' import { setReportState } from 'src/api/admin.js'
@ -18,8 +19,8 @@ export const useReportsStore = defineStore('reports', {
}), }),
actions: { actions: {
openUserReportingModal({ userId, statusIds = [] }) { openUserReportingModal({ userId, statusIds = [] }) {
const preTickedStatuses = statusIds.map( const preTickedStatuses = statusIds.map((id) =>
(id) => window.vuex.state.statuses.allStatusesObject[id], useStatusesStore().allStatuses.get(id),
) )
const preTickedIds = statusIds const preTickedIds = statusIds
const statuses = preTickedStatuses.concat( const statuses = preTickedStatuses.concat(

View file

@ -1,4 +1,4 @@
import { each, first, last, maxBy, merge, minBy, omitBy, remove } from 'lodash' import { first, last, maxBy, minBy } from 'lodash'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
@ -33,7 +33,7 @@ import {
unretweet, unretweet,
} from 'src/api/user.js' } from 'src/api/user.js'
const emptyTl = () => ({ const emptyTl = (userId) => ({
statuses: new Map(), statuses: new Map(),
faves: [], faves: [],
visibleStatuses: new Map(), visibleStatuses: new Map(),
@ -71,49 +71,6 @@ export const defaultState = () => ({
}, },
}) })
const 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
// 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(item.id, item)
this.timestamps.set(newStatus, timestamp)
return [map.get(item.id), true]
}
const sortById = (a, b) => {
const seqA = Number(a.id)
const seqB = Number(b.id)
const isSeqA = !Number.isNaN(seqA)
const isSeqB = !Number.isNaN(seqB)
if (isSeqA && isSeqB) {
return seqA > seqB ? -1 : 1
} else if (isSeqA && !isSeqB) {
return 1
} else if (!isSeqA && isSeqB) {
return -1
} else {
return a.id > b.id ? -1 : 1
}
}
const getLatestScrobble = (user) => { const getLatestScrobble = (user) => {
const scrobblesSupport = const scrobblesSupport =
useInstanceCapabilitiesStore().pleromaScrobblesAvailable useInstanceCapabilitiesStore().pleromaScrobblesAvailable
@ -149,7 +106,6 @@ const getLatestScrobble = (user) => {
}) })
} }
const USER_TIMELINES = new Set(['user', 'userPinned', 'media']) const USER_TIMELINES = new Set(['user', 'userPinned', 'media'])
export const useStatusesStore = defineStore('statuses', { export const useStatusesStore = defineStore('statuses', {
@ -160,8 +116,8 @@ export const useStatusesStore = defineStore('statuses', {
showImmediately = false, showImmediately = false,
timelineName, timelineName,
user = {}, user = {},
userId,
noIdUpdate = false, noIdUpdate = false,
Id,
pagination = {}, pagination = {},
timestamp, timestamp,
}) { }) {
@ -179,17 +135,14 @@ export const useStatusesStore = defineStore('statuses', {
// This makes sure that user timeline won't get data meant for other // This makes sure that user timeline won't get data meant for other
// user. I.e. opening different user profiles makes request which could // user. I.e. opening different user profiles makes request which could
// return data late after user already viewing different user profile // return data late after user already viewing different user profile
if ( if (USER_TIMELINES.has(timelineName) && timeline.userId !== userId) {
USER_TIMELINES.has(timelineName) &&
timeline.userId !== userId
) {
return return
} }
const addStatus = (data, showImmediately, addToTimeline = true) => { const addStatus = (data, showImmediately, addToTimeline = true) => {
getLatestScrobble(data.user) getLatestScrobble(data.user)
const [status] = mergeOrAdd(this.allStatuses, data) const [status] = this.mergeOrAdd(this.allStatuses, data)
// Add to conversation // Add to conversation
const conversations = this.conversations const conversations = this.conversations
@ -210,7 +163,7 @@ export const useStatusesStore = defineStore('statuses', {
// Add the mention to the mentions timeline // Add the mention to the mentions timeline
if (timeline !== mentions) { if (timeline !== mentions) {
const [, isNew] = mergeOrAdd(mentions.statuses, status) const [, isNew] = this.mergeOrAdd(mentions.statuses, data)
if (isNew) mentions.newStatusCount += 1 if (isNew) mentions.newStatusCount += 1
} }
} }
@ -218,21 +171,23 @@ export const useStatusesStore = defineStore('statuses', {
if (status.visibility === 'direct') { if (status.visibility === 'direct') {
const dms = this.timelines.dms const dms = this.timelines.dms
const [, isNew] = mergeOrAdd(dms.statuses, status) const [, isNew] = this.mergeOrAdd(dms.statuses, data)
if (isNew) dms.newStatusCount += 1 if (isNew) dms.newStatusCount += 1
} }
// Some statuses should only be added to the global status repository. // Some statuses should only be added to the global status repository.
if (timeline && addToTimeline) { if (timeline && addToTimeline) {
// Decide if we should treat the status as new for this timeline. // Decide if we should treat the status as new for this timeline.
const [status, isNew] = mergeOrAdd(timeline.statuses, status) const [status, isNew] = this.mergeOrAdd(timeline.statuses, data)
if (showImmediately) { if (isNew) {
// Add it directly to the visibleStatuses, don't change if (showImmediately) {
// newStatusCount // Add it directly to the visibleStatuses, don't change
timeline.visibleStatuses.add(status.id, status) // newStatusCount
} else { timeline.visibleStatuses.set(status.id, status)
// Just change newStatuscount } else {
timeline.newStatusCount += 1 // Just change newStatuscount
timeline.newStatusCount += 1
}
} }
} }
@ -256,12 +211,17 @@ export const useStatusesStore = defineStore('statuses', {
}, },
retweet: (status) => { retweet: (status) => {
// RetweetedStatuses are never shown immediately // RetweetedStatuses are never shown immediately
const retweetedStatus = addStatus(status.retweeted_status, false, false) const retweetedStatus = addStatus(
status.retweeted_status,
false,
false,
)
let retweet let retweet
// If the retweeted status is already there, don't add the retweet // If the retweeted status is already there, don't add the retweet
// to the timeline. // to the timeline.
if (timeline?.statuses.values().some((s) => { if (
[...(timeline?.statuses.values() ?? [])].some((s) => {
if (s.retweeted_status) { if (s.retweeted_status) {
return ( return (
s.id === retweetedStatus.id || s.id === retweetedStatus.id ||
@ -313,6 +273,35 @@ export const useStatusesStore = defineStore('statuses', {
processor(status) 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,
})
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]
},
fetchStatus(id) { fetchStatus(id) {
return fetchStatus({ id }).then(({ data: status, timestamp }) => return fetchStatus({ id }).then(({ data: status, timestamp }) =>
this.addNewStatuses({ statuses: [status], timestamp }), this.addNewStatuses({ statuses: [status], timestamp }),
@ -376,10 +365,7 @@ export const useStatusesStore = defineStore('statuses', {
) )
}, },
fetchFavsAndRepeats(id) { fetchFavsAndRepeats(id) {
return Promise.all([ return Promise.all([this.fetchFavs(id), this.fetchRepeats(id)])
this.fetchFavs(id),
this.fetchRepeats(id),
])
}, },
// Updates // Updates
@ -414,8 +400,8 @@ export const useStatusesStore = defineStore('statuses', {
const minNew = pagination.maxId ?? minBy(statuses, 'id').id ?? '' const minNew = pagination.maxId ?? minBy(statuses, 'id').id ?? ''
const maxNew = pagination.minId ?? maxBy(statuses, 'id').id ?? '' const maxNew = pagination.minId ?? maxBy(statuses, 'id').id ?? ''
const newer = (maxNew > timeline.maxId || timeline.maxId === '') const newer = maxNew > timeline.maxId || timeline.maxId === ''
const older = (minNew < timeline.minId || timeline.minId === '') const older = minNew < timeline.minId || timeline.minId === ''
if (newer) { if (newer) {
timeline.maxId = maxNew timeline.maxId = maxNew
@ -429,8 +415,10 @@ export const useStatusesStore = defineStore('statuses', {
timeline.newStatusCount = 0 timeline.newStatusCount = 0
timeline.visibleStatuses = new Map([...timeline.statuses.entries()].slice(0, 50)) timeline.visibleStatuses = new Map(
timeline.minVisibleId = last(timeline.visibleStatuses).id [...timeline.statuses.entries()].slice(0, 50),
)
timeline.minVisibleId = last(timeline.visibleStatuses.keys())
timeline.minId = '' timeline.minId = ''
timeline.maxId = '' timeline.maxId = ''
this.updateTimelineExtremes(timeline, [...timeline.statuses.values()]) this.updateTimelineExtremes(timeline, [...timeline.statuses.values()])
@ -442,9 +430,9 @@ export const useStatusesStore = defineStore('statuses', {
this[key] = value this[key] = value
}) })
}, },
clearTimeline(state, { timeline, excludeUserId = false }) { clearTimeline({ timeline, excludeUserId = false }) {
const userId = excludeUserId ? state.timelines[timeline].userId : undefined const userId = excludeUserId ? this.timelines[timeline].userId : undefined
state.timelines[timeline] = emptyTl(userId) this.timelines[timeline] = emptyTl(userId)
}, },
queueFlush({ timeline, id }) { queueFlush({ timeline, id }) {
this.timelines[timeline].flushMarker = id this.timelines[timeline].flushMarker = id
@ -650,8 +638,8 @@ export const useStatusesStore = defineStore('statuses', {
}, },
setBookmarked({ id, value, bookmark_folder_id }) { setBookmarked({ id, value, bookmark_folder_id }) {
const status = this.allStatuses.get(id) const status = this.allStatuses.get(id)
newStatus.bookmarked = value status.bookmarked = value
newStatus.bookmark_folder_id = value ? bookmark_folder_id : null status.bookmark_folder_id = value ? bookmark_folder_id : null
}, },
/// Mute /// Mute
@ -659,25 +647,29 @@ export const useStatusesStore = defineStore('statuses', {
return muteConversation({ return muteConversation({
id, id,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}).then(({ data: status, timestamp }) => { })
this.addNewStatuses({ .then(({ data: status, timestamp }) => {
statuses: [status], this.addNewStatuses({
timestamp, statuses: [status],
timestamp,
})
return status
}) })
return status .then((status) => this.setMutedStatus(status))
}).then((status) => this.setMutedStatus(status))
}, },
unmuteConversation(id) { unmuteConversation(id) {
return unmuteConversation({ return unmuteConversation({
id, id,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}).then(({ data: status, timestamp }) => { })
this.addNewStatuses({ .then(({ data: status, timestamp }) => {
statuses: [status], this.addNewStatuses({
timestamp, statuses: [status],
timestamp,
})
return status
}) })
return status .then((status) => this.setMutedStatus(status))
}).then((status) => this.setMutedStatus(status))
}, },
setMutedStatus({ id, thread_muted }) { setMutedStatus({ id, thread_muted }) {
// Setting thread_muted flag on all other known statuses // Setting thread_muted flag on all other known statuses
@ -686,11 +678,11 @@ export const useStatusesStore = defineStore('statuses', {
newStatus.thread_muted = thread_muted newStatus.thread_muted = thread_muted
if (newStatus.thread_muted !== undefined) { if (newStatus.thread_muted !== undefined) {
state.conversations.get(newStatus.statusnet_conversation_id).forEach( this.conversations
(status) => { .get(newStatus.statusnet_conversation_id)
.forEach((status) => {
status.thread_muted = thread_muted status.thread_muted = thread_muted
}, })
)
} }
}, },
@ -774,9 +766,7 @@ export const useStatusesStore = defineStore('statuses', {
statuses: data.statuses, statuses: data.statuses,
}) })
data.statuses = data.statuses.map( data.statuses = data.statuses.map((s) => this.allStatuses.get(s.id))
(s) => this.allStatuses.get(s.id),
)
return data return data
}) })
}, },
@ -785,8 +775,7 @@ export const useStatusesStore = defineStore('statuses', {
removeUserStatuses({ timelineName, userId }) { removeUserStatuses({ timelineName, userId }) {
const timeline = this.timelines[timelineName] const timeline = this.timelines[timelineName]
timeline timeline.statuses
.statuses
.values() .values()
.filter(({ user }) => user.id === userId) .filter(({ user }) => user.id === userId)
.forEach(({ id }) => { .forEach(({ id }) => {
@ -795,8 +784,8 @@ export const useStatusesStore = defineStore('statuses', {
}) })
timeline.minVisibleId = timeline.minVisibleId =
timeline.visibleStatuses.length > 0 timeline.visibleStatuses.length > 0
? last(timeline.visibleStatuses).id ? last(timeline.visibleStatuses).id
: 0 : 0
timeline.maxId = timeline.maxId =
timeline.statuses.length > 0 ? first(timeline.statuses).id : 0 timeline.statuses.length > 0 ? first(timeline.statuses).id : 0
}, },
@ -807,8 +796,8 @@ export const useStatusesStore = defineStore('statuses', {
const status = this.allStatuses.get(id) const status = this.allStatuses.get(id)
status.poll = poll status.poll = poll
}, },
setLoading(state, { timeline, value }) { setLoading({ timeline, value }) {
state.timelines[timeline].loading = value this.timelines[timeline].loading = value
}, },
}, },
}) })

View file

@ -20,7 +20,9 @@ import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.j
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useListsStore } from 'src/stores/lists.js' import { useListsStore } from 'src/stores/lists.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useNotificationsStore } from 'src/stores/notifications.js'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js' import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUserHighlightStore } from 'src/stores/user_highlight.js' import { useUserHighlightStore } from 'src/stores/user_highlight.js'
@ -144,12 +146,12 @@ export const useUsersStore = defineStore('users', {
const { data, timestamp } = response const { data, timestamp } = response
const users = Array.isArray(data) ? data : [data] const users = Array.isArray(data) ? data : [data]
users.forEach((user) => { return users.map((user) => {
const existing = this.users.get(user.id) ?? {} const existing = this.users.get(user.id) ?? {}
const oldTimestamp = this.timestamps.get(existing) const oldTimestamp = this.timestamps.get(existing)
// implicit: if oldTimestamp is undefined this will still be false // implicit: if oldTimestamp is undefined this will still be false
if (oldTimestamp > timestamp) return // not overwriting old data with new if (oldTimestamp > timestamp) return existing // not overwriting old data with new
const { relationship: unused0, ...old } = existing const { relationship: unused0, ...old } = existing
const { relationship: unused1, ...neu } = user const { relationship: unused1, ...neu } = user
@ -163,6 +165,8 @@ export const useUsersStore = defineStore('users', {
if (user.id === this.currentUser.id) { if (user.id === this.currentUser.id) {
this.currentUser = newUser this.currentUser = newUser
} }
return this.users.get(user.id)
}) })
}, },
updateUserRelationship(relationships) { updateUserRelationship(relationships) {
@ -594,7 +598,7 @@ export const useUsersStore = defineStore('users', {
useBookmarkFoldersStore().stopFetching() useBookmarkFoldersStore().stopFetching()
store.dispatch('stopFetchingFollowRequests') store.dispatch('stopFetchingFollowRequests')
store.commit('clearNotifications') store.commit('clearNotifications')
store.commit('resetStatuses') useStatusesStore().resetStatuses()
useChatsStore().resetChats() useChatsStore().resetChats()
oauth.clearToken() oauth.clearToken()
Cookies.remove('__Host-pleroma_key', { path: '/' }) Cookies.remove('__Host-pleroma_key', { path: '/' })

View file

@ -6,6 +6,7 @@ import { mountOpts } from '../../../fixtures/setup_test'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
const currentUser = { const currentUser = {
@ -37,7 +38,7 @@ const replyMountOpts = (props) =>
props, props,
afterStore(store) { afterStore(store) {
useUsersStore().currentUser = currentUser useUsersStore().currentUser = currentUser
store.state.statuses.allStatusesObject = { useStatusesStore().allStatuses = {
[repliedStatus.id]: repliedStatus, [repliedStatus.id]: repliedStatus,
} }
}, },