Merge branch 'users-statuses-pinia' into shigusegubu-themes3

This commit is contained in:
Henry Jameson 2026-09-01 16:57:56 +03:00
commit 410e3cff41
55 changed files with 430 additions and 251 deletions

View file

@ -90,7 +90,7 @@ const AccountActions = {
name: 'chat', name: 'chat',
params: { params: {
username: useUsersStore().currentUser.screen_name, username: useUsersStore().currentUser.screen_name,
recipient_id: this.user.id, chatUserId: this.user.id,
}, },
}) })
}, },

View file

@ -1,14 +1,19 @@
import UserAvatar from 'src/components/user_avatar/user_avatar.vue' import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useUsersStore } from 'src/stores/users.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const AvatarList = { const AvatarList = {
props: ['users'], props: {
userIds: Set,
},
computed: { computed: {
slicedUsers() { slicedUsers() {
return this.users ? this.users.slice(0, 15) : [] return [...(this.userIds ?? [])]
.slice(0, 15)
.map((id) => useUsersStore().findUser(id))
}, },
}, },
components: { components: {

View file

@ -41,7 +41,7 @@
{{ $t('admin_dash.users.labels.handle_colon') }} {{ $t('admin_dash.users.labels.handle_colon') }}
{{ ' ' }} {{ ' ' }}
</strong> </strong>
<user-link <UserLink
class="basic-user-card-screen-name" class="basic-user-card-screen-name"
:user="user" :user="user"
/> />

View file

@ -4,6 +4,7 @@ import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import UserAvatar from 'src/components/user_avatar/user_avatar.vue' import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
import { useOAuthStore } from 'src/stores/oauth.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { useSearchStore } from 'src/stores/search.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
import { chats } from 'src/api/chats.js' import { chats } from 'src/api/chats.js'
@ -50,7 +51,7 @@ const chatNew = {
this.$emit('cancel') this.$emit('cancel')
}, },
goToChat(user) { goToChat(user) {
this.$router.push({ name: 'chat', params: { recipient_id: user.id } }) this.$router.push({ name: 'chat', params: { chatUserId: user.id } })
}, },
onInput() { onInput() {
this.search(this.query) this.search(this.query)
@ -71,7 +72,7 @@ const chatNew = {
this.loading = true this.loading = true
this.userIds = [] this.userIds = []
this.$store this.$store
this.useSearchStore() useSearchStore()
.search({ q: query, resolve: true, type: 'accounts' }) .search({ q: query, resolve: true, type: 'accounts' })
.then((data) => { .then((data) => {
this.loading = false this.loading = false

View file

@ -472,8 +472,8 @@ const Chat = {
// Clear any known pending messages // Clear any known pending messages
if (message.idempotency_key) { if (message.idempotency_key) {
if (this.pendingMessagesIndex[message.idempotencyKeyIndex]) { if (this.pendingMessagesIndex[message.idempotency_key]) {
delete this.pendingMessagesIndex[message.idempotencyKeyIndex] delete this.pendingMessagesIndex[message.idempotency_key]
this.pendingMessages = this.pendingMessages.filter( this.pendingMessages = this.pendingMessages.filter(
({ idempotency_key }) => ({ idempotency_key }) =>
idempotency_key !== message.idempotency_key, idempotency_key !== message.idempotency_key,

View file

@ -185,6 +185,7 @@ const conversation = {
return [...conversation.keys()] return [...conversation.keys()]
.map((k) => useStatusesStore().allStatuses.get(k)) .map((k) => useStatusesStore().allStatuses.get(k))
.filter((status) => status.type != 'repeat') // Old backend behavior?
.toSorted(sortById) .toSorted(sortById)
}, },
statusMap() { statusMap() {
@ -621,6 +622,7 @@ const conversation = {
}, },
updateVirtualHeight() { updateVirtualHeight() {
if (this.hide) return // no updates when not rendering if (this.hide) return // no updates when not rendering
if (!this.status) return // not loaded yet
this.$nextTick(() => { this.$nextTick(() => {
this.virtualHeight = this.$refs.body.getBoundingClientRect().height this.virtualHeight = this.$refs.body.getBoundingClientRect().height
this.$emit('update:virtualHeight', { this.$emit('update:virtualHeight', {

View file

@ -1,4 +1,5 @@
import { get } from 'lodash' import { get } from 'lodash'
import { mapState } from 'pinia'
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import Modal from 'src/components/modal/modal.vue' import Modal from 'src/components/modal/modal.vue'
@ -19,18 +20,16 @@ const EditStatusModal = {
} }
}, },
computed: { computed: {
isLoggedIn() {
return !!useUsersStore().currentUser
},
modalActivated() { modalActivated() {
return useEditStatusStore().modalActivated return useEditStatusStore().modalActivated
}, },
isFormVisible() { isFormVisible() {
return this.isLoggedIn && !this.resettingForm && this.modalActivated return this.loggedIn && !this.resettingForm && this.modalActivated
}, },
params() { params() {
return useEditStatusStore().params || {} return useEditStatusStore().params || {}
}, },
...mapState(useUsersStore, ['loggedIn']),
}, },
watch: { watch: {
params(newVal, oldVal) { params(newVal, oldVal) {

View file

@ -37,9 +37,9 @@ const EmojiReactions = {
}, },
accountsForEmoji() { accountsForEmoji() {
return this.status.emoji_reactions.reduce((acc, reaction) => { return this.status.emoji_reactions.reduce((acc, reaction) => {
acc[reaction.name] = reaction.accounts || [] acc.set(reaction.name, new Set(reaction.account_ids))
return acc return acc
}, {}) }, new Map())
}, },
loggedIn() { loggedIn() {
return !!useUsersStore().currentUser return !!useUsersStore().currentUser

View file

@ -52,7 +52,7 @@
</FALayers> </FALayers>
</component> </component>
<UserListPopover <UserListPopover
:users="accountsForEmoji[reaction.name]" :user-ids="accountsForEmoji.get(reaction.name)"
class="emoji-reaction-popover" class="emoji-reaction-popover"
:normal-button="true" :normal-button="true"
:trigger-attrs="counterTriggerAttrs(reaction)" :trigger-attrs="counterTriggerAttrs(reaction)"

View file

@ -15,7 +15,7 @@ const FollowCard = {
}, },
computed: { computed: {
isMe() { isMe() {
return useUsersStore().currentUser.id === this.user.id return useUsersStore().currentUser?.id === this.user.id
}, },
loggedIn() { loggedIn() {
return useUsersStore().currentUser return useUsersStore().currentUser

View file

@ -2,6 +2,8 @@ import { debounce } from 'lodash'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
import { useSearchStore } from 'src/stores/search.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons' import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
@ -32,7 +34,7 @@ const ListsUserSearch = {
this.loading = true this.loading = true
this.$emit('loading') this.$emit('loading')
this.userIds = [] this.userIds = []
this.useSearchStore() useSearchStore()
.search({ .search({
q: query, q: query,
resolve: true, resolve: true,

View file

@ -1,4 +1,5 @@
import { debounce } from 'lodash' import { debounce } from 'lodash'
import { mapState } from 'pinia'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { usePostStatusStore } from 'src/stores/post_status.js' import { usePostStatusStore } from 'src/stores/post_status.js'
@ -34,9 +35,6 @@ const MobilePostStatusButton = {
window.removeEventListener('resize', this.handleOSK) window.removeEventListener('resize', this.handleOSK)
}, },
computed: { computed: {
isLoggedIn() {
return useUsersStore().loggedIn
},
isHidden() { isHidden() {
if (HIDDEN_FOR_PAGES.has(this.$route.name)) { if (HIDDEN_FOR_PAGES.has(this.$route.name)) {
return true return true
@ -52,6 +50,7 @@ const MobilePostStatusButton = {
autohideFloatingPostButton() { autohideFloatingPostButton() {
return !!useMergedConfigStore().mergedConfig.autohideFloatingPostButton return !!useMergedConfigStore().mergedConfig.autohideFloatingPostButton
}, },
...mapState(useUsersStore, ['loggedIn']),
}, },
watch: { watch: {
autohideFloatingPostButton: function (isEnabled) { autohideFloatingPostButton: function (isEnabled) {

View file

@ -1,6 +1,6 @@
<template> <template>
<button <button
v-if="isLoggedIn" v-if="loggedIn"
class="MobilePostButton button-default new-status-button" class="MobilePostButton button-default new-status-button"
:class="{ 'hidden': isHidden, 'always-show': isPersistent }" :class="{ 'hidden': isHidden, 'always-show': isPersistent }"
:title="$t('post_status.new_status')" :title="$t('post_status.new_status')"

View file

@ -24,7 +24,7 @@
class="Notification container -muted" class="Notification container -muted"
> >
<small> <small>
<user-link <UserLink
:user="notification.from_profile" :user="notification.from_profile"
:at="false" :at="false"
/> />
@ -215,7 +215,7 @@
v-if="notification.type === 'follow' || notification.type === 'follow_request'" v-if="notification.type === 'follow' || notification.type === 'follow_request'"
class="follow-text" class="follow-text"
> >
<user-link <UserLink
class="follow-name" class="follow-name"
:user="notification.from_profile" :user="notification.from_profile"
/> />
@ -249,7 +249,7 @@
v-else-if="notification.type === 'move'" v-else-if="notification.type === 'move'"
class="move-text" class="move-text"
> >
<user-link <UserLink
:user="notification.target" :user="notification.target"
/> />
</div> </div>

View file

@ -1,4 +1,5 @@
import { get } from 'lodash' import { get } from 'lodash'
import { mapState } from 'pinia'
import Modal from 'src/components/modal/modal.vue' import Modal from 'src/components/modal/modal.vue'
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue' import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
@ -17,18 +18,16 @@ const PostStatusModal = {
} }
}, },
computed: { computed: {
isLoggedIn() {
return !!useUsersStore().currentUser
},
modalActivated() { modalActivated() {
return usePostStatusStore().modalActivated return usePostStatusStore().modalActivated
}, },
isFormVisible() { isFormVisible() {
return this.isLoggedIn && !this.resettingForm && this.modalActivated return this.loggedIn && !this.resettingForm && this.modalActivated
}, },
params() { params() {
return usePostStatusStore().params || {} return usePostStatusStore().params || {}
}, },
...mapState(useUsersStore, ['loggedIn']),
}, },
watch: { watch: {
params(newVal, oldVal) { params(newVal, oldVal) {

View file

@ -1,6 +1,6 @@
<template> <template>
<Modal <Modal
v-if="isLoggedIn && !resettingForm" v-if="loggedIn && !resettingForm"
:is-open="modalActivated" :is-open="modalActivated"
class="post-form-modal-view" class="post-form-modal-view"
@backdrop-clicked="closeModal" @backdrop-clicked="closeModal"

View file

@ -5,8 +5,8 @@ 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 { useTimelinesStore } from 'src/stores/timelines.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'
@ -28,13 +28,14 @@ const QuickFilterSettings = {
path: 'replyVisibility', path: 'replyVisibility',
value: visibility, value: visibility,
}) })
useStatusesStore().requireReloadAll() useTimelinesStore().requireReloadAll()
}, },
openTab(tab) { openTab(tab) {
useInterfaceStore().openSettingsModalTab(tab) useInterfaceStore().openSettingsModalTab(tab)
}, },
}, },
computed: { computed: {
...mapState(useUsersStore, ['loggedIn']),
...mapState(useMergedConfigStore, ['mergedConfig']), ...mapState(useMergedConfigStore, ['mergedConfig']),
...mapState(useInterfaceStore, { ...mapState(useInterfaceStore, {
mobileLayout: (state) => state.layoutType === 'mobile', mobileLayout: (state) => state.layoutType === 'mobile',
@ -55,9 +56,6 @@ const QuickFilterSettings = {
return 'dropdown-item' return 'dropdown-item'
} }
}, },
loggedIn() {
return !!useUsersStore().currentUser
},
replyVisibilitySelf: { replyVisibilitySelf: {
get() { get() {
return this.mergedConfig.replyVisibility === 'self' return this.mergedConfig.replyVisibility === 'self'

View file

@ -36,9 +36,7 @@ const QuickViewSettings = {
...mapState(useInterfaceStore, { ...mapState(useInterfaceStore, {
mobileLayout: (state) => state.layoutType === 'mobile', mobileLayout: (state) => state.layoutType === 'mobile',
}), }),
loggedIn() { ...mapState(useUsersStore, ['loggedIn']),
return !!useUsersStore().currentUser
},
conversationDisplay: { conversationDisplay: {
get() { get() {
return this.mergedConfig.conversationDisplay return this.mergedConfig.conversationDisplay

View file

@ -4,6 +4,7 @@ import Checkbox from 'src/components/checkbox/checkbox.vue'
import Quote from './quote.vue' import Quote from './quote.vue'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useSearchStore } from 'src/stores/search.js'
export default { export default {
components: { components: {
@ -93,7 +94,7 @@ export default {
this.$emit('update:id', notice[3]) this.$emit('update:id', notice[3])
} else if (value) { } else if (value) {
this.loading = true this.loading = true
this.useSearchStore() useSearchStore()
.search({ .search({
q: value, q: value,
resolve: true, resolve: true,

View file

@ -100,9 +100,6 @@ const SettingsModalAdminContent = {
user() { user() {
return useUsersStore().currentUser return useUsersStore().currentUser
}, },
isLoggedIn() {
return !!useUsersStore().currentUser
},
open() { open() {
return useInterfaceStore().settingsModalState !== 'hidden' return useInterfaceStore().settingsModalState !== 'hidden'
}, },

View file

@ -1,3 +1,5 @@
import { mapState } from 'pinia'
import VerticalTabSwitcher from './helpers/vertical_tab_switcher.jsx' import VerticalTabSwitcher from './helpers/vertical_tab_switcher.jsx'
import AppearanceTab from './tabs/appearance_tab.vue' import AppearanceTab from './tabs/appearance_tab.vue'
import ClutterTab from './tabs/clutter_tab.vue' import ClutterTab from './tabs/clutter_tab.vue'
@ -75,9 +77,6 @@ const SettingsModalContent = {
OldThemeTab, OldThemeTab,
}, },
computed: { computed: {
isLoggedIn() {
return !!useUsersStore().currentUser
},
open() { open() {
return useInterfaceStore().settingsModalState !== 'hidden' return useInterfaceStore().settingsModalState !== 'hidden'
}, },
@ -87,6 +86,7 @@ const SettingsModalContent = {
expertLevel() { expertLevel() {
return useMergedConfigStore().mergedConfig.expertLevel return useMergedConfigStore().mergedConfig.expertLevel
}, },
...mapState(useUsersStore, ['loggedIn']),
}, },
data() { data() {
return { return {

View file

@ -14,7 +14,7 @@
<GeneralTab /> <GeneralTab />
</div> </div>
<div <div
v-if="isLoggedIn" v-if="loggedIn"
:label="$t('settings.profile_tab')" :label="$t('settings.profile_tab')"
icon="user" icon="user"
data-tab-name="profile" data-tab-name="profile"
@ -23,7 +23,7 @@
<ProfileTab /> <ProfileTab />
</div> </div>
<div <div
v-if="isLoggedIn" v-if="loggedIn"
:label="$t('settings.composing')" :label="$t('settings.composing')"
icon="pen-alt" icon="pen-alt"
data-tab-name="composing" data-tab-name="composing"
@ -57,7 +57,7 @@
<LayoutTab /> <LayoutTab />
</div> </div>
<div <div
v-if="isLoggedIn" v-if="loggedIn"
:full-width="true" :full-width="true"
:label="$t('settings.notifications')" :label="$t('settings.notifications')"
icon="bell" icon="bell"
@ -73,7 +73,7 @@
<FilteringTab /> <FilteringTab />
</div> </div>
<div <div
v-if="isLoggedIn" v-if="loggedIn"
:label="$t('settings.mutes_and_blocks')" :label="$t('settings.mutes_and_blocks')"
icon="eye-slash" icon="eye-slash"
data-tab-name="mutesAndBlocks" data-tab-name="mutesAndBlocks"
@ -90,7 +90,7 @@
<ClutterTab /> <ClutterTab />
</div> </div>
<div <div
v-if="isLoggedIn" v-if="loggedIn"
:label="$t('settings.security_tab')" :label="$t('settings.security_tab')"
icon="lock" icon="lock"
data-tab-name="security" data-tab-name="security"
@ -98,7 +98,7 @@
<SecurityTab /> <SecurityTab />
</div> </div>
<div <div
v-if="isLoggedIn" v-if="loggedIn"
:label="$t('settings.data_import_export_tab')" :label="$t('settings.data_import_export_tab')"
icon="download" icon="download"
data-tab-name="dataImportExport" data-tab-name="dataImportExport"

View file

@ -221,7 +221,7 @@ const AppearanceTab = {
}, },
computed: { computed: {
isDefaultBackground() { isDefaultBackground() {
return !useUsersStore().currentUser.background_image return !useUsersStore().currentUser?.background_image
}, },
switchInProgress() { switchInProgress() {
return useInterfaceStore().themeChangeInProgress return useInterfaceStore().themeChangeInProgress
@ -283,7 +283,7 @@ const AppearanceTab = {
instanceWallpaperUsed() { instanceWallpaperUsed() {
return ( return (
useInstanceStore().instanceIdentity.background && useInstanceStore().instanceIdentity.background &&
!useUsersStore().currentUser.background_image !useUsersStore().currentUser?.background_image
) )
}, },
customThemeVersion() { customThemeVersion() {

View file

@ -162,9 +162,9 @@
<div class="fun-monitor-display-bezel button-default"> <div class="fun-monitor-display-bezel button-default">
<div class="fun-monitor-display-screen input"> <div class="fun-monitor-display-screen input">
<img <img
v-if="backgroundPreview || user.background_image || instanceWallpaper" v-if="backgroundPreview || user?.background_image || instanceWallpaper"
class="fun-monitor-display-screen-image" class="fun-monitor-display-screen-image"
:src="backgroundPreview || user.background_image || instanceWallpaper" :src="backgroundPreview || user?.background_image || instanceWallpaper"
> >
<div <div
v-else v-else

View file

@ -11,7 +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' import { useTimelinesStore } from 'src/stores/timelines.js'
const ClutterTab = { const ClutterTab = {
components: { components: {
@ -36,7 +36,7 @@ const ClutterTab = {
// Updating nested properties // Updating nested properties
watch: { watch: {
replyVisibility() { replyVisibility() {
useStatusesStore().requireReloadAll() useTimelinesStore().requireReloadAll()
}, },
}, },
} }

View file

@ -14,8 +14,8 @@ 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 { useTimelinesStore } from 'src/stores/timelines.js'
import { import {
newExporter, newExporter,
@ -266,7 +266,7 @@ const FilteringTab = {
// Updating nested properties // Updating nested properties
watch: { watch: {
replyVisibility() { replyVisibility() {
useStatusesStore().requireReloadAll() useTimelinesStore().requireReloadAll()
}, },
muteFiltersObject() { muteFiltersObject() {
this.muteFiltersDraftObject = cloneDeep( this.muteFiltersDraftObject = cloneDeep(

View file

@ -27,7 +27,7 @@ const GeneralTab = {
value: mode, value: mode,
label: this.$t(`settings.absolute_time_format_12h_${mode}`), label: this.$t(`settings.absolute_time_format_12h_${mode}`),
})), })),
emailLanguage: useUsersStore().currentUser.language || [''], emailLanguage: useUsersStore().currentUser?.language || [''],
} }
}, },
components: { components: {
@ -72,6 +72,9 @@ const GeneralTab = {
useLocalConfigStore().set({ path, value }) useLocalConfigStore().set({ path, value })
}, },
toggleStreaming(value) { toggleStreaming(value) {
// Streaming is not available for the unauthenticated
if (!useOAuthStore().token) return
if (value) { if (value) {
useStreamingStore().initSocket() useStreamingStore().initSocket()
} else { } else {

View file

@ -1,4 +1,3 @@
import { uniqBy } from 'lodash'
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import AvatarList from 'src/components/avatar_list/avatar_list.vue' import AvatarList from 'src/components/avatar_list/avatar_list.vue'
@ -137,13 +136,30 @@ const Status = {
useScrobblesStore().getLatestScrobble(this.status.user.id) useScrobblesStore().getLatestScrobble(this.status.user.id)
}, },
computed: { computed: {
// Whatever we're given to work with
status() { status() {
return this.statusoid ?? useStatusesStore().allStatuses.get(this.statusId) return this.statusoid ?? useStatusesStore().allStatuses.get(this.statusId)
}, },
// Status repeated
repeatedStatus() { repeatedStatus() {
if (this.status.retweeted_status === undefined) return undefined if (this.status.retweeted_status === undefined) return undefined
return useStatusesStore().allStatuses.get(this.status.retweeted_status.id) return useStatusesStore().allStatuses.get(this.status.retweeted_status.id)
}, },
// THE repeat
repeatStatus() {
if (this.isRepeat) {
return this.status
} else {
return null
}
},
mainStatus() {
if (this.isRepeat) {
return this.repeatedStatus
} else {
return this.status
}
},
repeater() { repeater() {
return useUsersStore().findUser(this.status.user.id) return useUsersStore().findUser(this.status.user.id)
}, },
@ -152,7 +168,7 @@ const Status = {
}, },
showReasonMutedThread() { showReasonMutedThread() {
return ( return (
(this.mainStatus.thread_muted || this.mainSatus.reblog?.thread_muted) && (this.mainStatus.thread_muted || this.repeatStatus?.thread_muted) &&
!this.inConversation !this.inConversation
) )
}, },
@ -179,6 +195,12 @@ const Status = {
useUserHighlightStore().get(this.repeater.screen_name), useUserHighlightStore().get(this.repeater.screen_name),
) )
}, },
favoritedBy() {
return useStatusesStore().favs.get(this.mainStatus.id) ?? new Set()
},
repeatedBy() {
return useStatusesStore().repeats.get(this.mainStatus.id) ?? new Set()
},
userStyle() { userStyle() {
if (this.noHeading) return if (this.noHeading) return
return highlightStyle(useUserHighlightStore().get(this.user.screen_name)) return highlightStyle(useUserHighlightStore().get(this.user.screen_name))
@ -212,13 +234,6 @@ const Status = {
this.repeater.screen_name, this.repeater.screen_name,
) )
}, },
mainStatus() {
if (this.isRepeat) {
return this.repeatedStatus
} else {
return this.status
}
},
loggedIn() { loggedIn() {
return !!this.currentUser return !!this.currentUser
}, },
@ -383,12 +398,7 @@ const Status = {
} }
}, },
combinedFavsAndRepeatsUsers() { combinedFavsAndRepeatsUsers() {
// Use the status from the global status repository since favs and repeats are saved in it return new Set([...this.favoritedBy, ...this.repeatedBy])
const combinedUsers = [].concat(
this.mainStatus.favoritedBy,
this.mainStatus.rebloggedBy,
)
return uniqBy(combinedUsers, 'id')
}, },
tags() { tags() {
return [...this.status.tags] return [...this.status.tags]
@ -403,7 +413,7 @@ const Status = {
return ( return (
!this.hidePostStats && !this.hidePostStats &&
this.focused && this.focused &&
(this.combinedFavsAndRepeatsUsers.length > 0 || (this.combinedFavsAndRepeatsUsers.size > 0 ||
this.mainStatus.quotes_count) this.mainStatus.quotes_count)
) )
}, },
@ -586,22 +596,14 @@ const Status = {
}, },
'mainStatus.repeat_num': function (num) { 'mainStatus.repeat_num': function (num) {
// refetch repeats when repeat_num is changed in any way // refetch repeats when repeat_num is changed in any way
if ( if (this.focused && this.repeatedBy.size !== num) {
this.focused && useStatusesStore().fetchRepeats(this.mainStatus.id)
this.mainStatus.rebloggedBy &&
this.mainStatus.rebloggedBy.length !== num
) {
useStatusesStore().fetchRepeats(this.status.id)
} }
}, },
'mainStatus.fave_num': function (num) { 'mainStatus.fave_num': function (num) {
// refetch favs when fave_num is changed in any way // refetch favs when fave_num is changed in any way
if ( if (this.focused && this.favoritedBy.size !== num) {
this.focused && useStatusesStore().fetchFavs(this.mainStatus.id)
this.mainStatus.favoritedBy &&
this.mainStatus.favoritedBy.length !== num
) {
useStatusesStore().fetchFavs(this.status.id)
} }
}, },
isSuspendable: function (suspend) { isSuspendable: function (suspend) {

View file

@ -26,7 +26,7 @@
class="fa-scale-110 fa-old-padding repeat-icon" class="fa-scale-110 fa-old-padding repeat-icon"
icon="retweet" icon="retweet"
/> />
<user-link <UserLink
:user="repeater" :user="repeater"
:at="false" :at="false"
/> />
@ -154,7 +154,7 @@
> >
{{ user.name }} {{ user.name }}
</h4> </h4>
<user-link <UserLink
class="account-name" class="account-name"
:title="user.screen_name_ui" :title="user.screen_name_ui"
:user="user" :user="user"
@ -464,26 +464,26 @@
> >
<div class="stats"> <div class="stats">
<UserListPopover <UserListPopover
v-if="mainStatus.rebloggedBy && mainStatus.rebloggedBy.length > 0" v-if="repeatedBy.size > 0"
:users="mainStatus.rebloggedBy" :user-ids="repeatedBy"
> >
<div class="stat-count"> <div class="stat-count">
<a class="stat-title">{{ $t('status.repeats') }}</a> <a class="stat-title">{{ $t('status.repeats') }}</a>
<div class="stat-number"> <div class="stat-number">
{{ mainStatus.rebloggedBy.length }} {{ repeatedBy.size }}
</div> </div>
</div> </div>
</UserListPopover> </UserListPopover>
<UserListPopover <UserListPopover
v-if="mainStatus.favoritedBy && mainStatus.favoritedBy.length > 0" v-if="favoritedBy.size > 0"
:users="mainStatus.favoritedBy" :user-ids="favoritedBy"
> >
<div <div
class="stat-count" class="stat-count"
> >
<a class="stat-title">{{ $t('status.favorites') }}</a> <a class="stat-title">{{ $t('status.favorites') }}</a>
<div class="stat-number"> <div class="stat-number">
{{ mainStatus.favoritedBy.length }} {{ favoritedBy.size }}
</div> </div>
</div> </div>
</UserListPopover> </UserListPopover>
@ -501,7 +501,7 @@
</div> </div>
</router-link> </router-link>
<div class="avatar-row"> <div class="avatar-row">
<AvatarList :users="combinedFavsAndRepeatsUsers" /> <AvatarList :user-ids="combinedFavsAndRepeatsUsers" />
</div> </div>
</div> </div>
</div> </div>

View file

@ -102,7 +102,10 @@ const Timeline = {
} }
}, },
statusesToDisplay() { statusesToDisplay() {
if (!this.virtualScrollingEnabled) return this.visibleStatusIds if (!this.virtualScrollingEnabled) {
return new Set(this.filteredVisibleStatuses.map(({ id }) => id))
}
const amount = this.timeline.visibleStatusIds.size const amount = this.timeline.visibleStatusIds.size
const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80)) const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80))
const min = Math.max(0, this.virtualScrollIndex - statusesPerSide) const min = Math.max(0, this.virtualScrollIndex - statusesPerSide)
@ -214,6 +217,7 @@ const Timeline = {
let err = statuses[approxIndex].getBoundingClientRect().y let err = statuses[approxIndex].getBoundingClientRect().y
// if we have a previous scroll index that can be used, test if it's // if we have a previous scroll index that can be used, test if it's
// closer than the previous approximation, use it if so
const virtualScrollIndexY = const virtualScrollIndexY =
statuses[cappedScrollIndex].getBoundingClientRect().y statuses[cappedScrollIndex].getBoundingClientRect().y

View file

@ -10,7 +10,7 @@
:timeline-name="timelineRef.name" :timeline-name="timelineRef.name"
/> />
<div <div
v-if="timeline.fetcher.loadingNewer" v-if="timeline.fetcher.loadingNewer && !showLoadButton"
class="loadingIndicator" class="loadingIndicator"
> >
<FAIcon <FAIcon

View file

@ -62,6 +62,22 @@ const TimelineMenu = {
(route === 'bookmark-folder' || route === 'bookmarks') (route === 'bookmark-folder' || route === 'bookmarks')
) )
}, },
timelineName() {
const route = this.$route.name
if (route === 'tag-timeline') {
return '#' + this.$route.params.tag
}
if (route === 'lists-timeline') {
return useListsStore().findListTitle(this.$route.params.id)
}
if (route === 'bookmark-folder') {
return useBookmarkFoldersStore().findBookmarkFolderName(
this.$route.params.id,
)
}
const i18nkey = timelineNames(this.bookmarkFolders)[this.$route.name]
return i18nkey ? this.$t(i18nkey) : route
},
...mapState(useInstanceCapabilitiesStore, [ ...mapState(useInstanceCapabilitiesStore, [
'pleromaChatMessagesAvailable', 'pleromaChatMessagesAvailable',
'pleromaBookmarkFoldersAvailable', 'pleromaBookmarkFoldersAvailable',
@ -103,22 +119,6 @@ const TimelineMenu = {
event.stopPropagation() event.stopPropagation()
} }
}, },
timelineName() {
const route = this.$route.name
if (route === 'tag-timeline') {
return '#' + this.$route.params.tag
}
if (route === 'lists-timeline') {
return useListsStore().findListTitle(this.$route.params.id)
}
if (route === 'bookmark-folder') {
return useBookmarkFoldersStore().findBookmarkFolderName(
this.$route.params.id,
)
}
const i18nkey = timelineNames(this.bookmarkFolders)[this.$route.name]
return i18nkey ? this.$t(i18nkey) : route
},
}, },
} }

View file

@ -30,7 +30,7 @@
</template> </template>
<template #trigger> <template #trigger>
<span class="button-unstyled timeline-menu-title"> <span class="button-unstyled timeline-menu-title">
<h1 class="title timeline-title">{{ timelineName() }}</h1> <h1 class="title timeline-title">{{ timelineName }}</h1>
<span> <span>
<FAIcon <FAIcon
size="sm" size="sm"

View file

@ -40,7 +40,7 @@ const UserAvatar = {
return useUsersStore().findUser(this.userId) return useUsersStore().findUser(this.userId)
}, },
showActorTypeIndicator() { showActorTypeIndicator() {
return useMergedConfigStore().mergedConfig.hideBotIndication return !useMergedConfigStore().mergedConfig.hideBotIndication
}, },
}, },
methods: { methods: {

View file

@ -238,7 +238,7 @@ export default {
return useUsersStore().relationship(this.userId) return useUsersStore().relationship(this.userId)
}, },
isOtherUser() { isOtherUser() {
return this.user.id !== useUsersStore().currentUser.id return this.user.id !== useUsersStore().currentUser?.id
}, },
subscribeUrl() { subscribeUrl() {
const serverUrl = new URL(this.user.statusnet_profile_url) const serverUrl = new URL(this.user.statusnet_profile_url)

View file

@ -4,6 +4,7 @@ import UserAvatar from 'src/components/user_avatar/user_avatar.vue'
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 { useUsersStore } from 'src/stores/users.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -14,15 +15,22 @@ library.add(faCircleNotch)
const UserListPopover = { const UserListPopover = {
name: 'UserListPopover', name: 'UserListPopover',
props: ['users'], props: {
userIds: Set,
},
components: { components: {
UnicodeDomainIndicator, UnicodeDomainIndicator,
Popover, Popover,
UserAvatar, UserAvatar,
}, },
computed: { computed: {
users() {
return [...this.userIds]
.map((id) => useUsersStore().findUser(id))
.filter(Boolean)
},
usersCapped() { usersCapped() {
return this.users.slice(0, 16) return [...this.users].slice(0, 16)
}, },
allowNonSquareEmoji() { allowNonSquareEmoji() {
return useMergedConfigStore().mergedConfig.nonSquareEmoji return useMergedConfigStore().mergedConfig.nonSquareEmoji

View file

@ -9,7 +9,7 @@
</template> </template>
<template #content> <template #content>
<div class="user-list-popover"> <div class="user-list-popover">
<template v-if="users.length"> <template v-if="userIds.size > 0">
<router-link <router-link
v-for="(user) in usersCapped" v-for="(user) in usersCapped"
:key="user.id" :key="user.id"

View file

@ -49,11 +49,7 @@ const UserProfile = {
return useTimelinesStore().media return useTimelinesStore().media
}, },
isUs() { isUs() {
return ( return this.userId && this.userId === useUsersStore().currentUser?.id
this.userId &&
useUsersStore().currentUser.id &&
this.userId === useUsersStore().currentUser.id
)
}, },
user() { user() {
return useUsersStore().findUser(this.userId) return useUsersStore().findUser(this.userId)

View file

@ -28,11 +28,8 @@ const UserReportingModal = {
} }
}, },
computed: { computed: {
isLoggedIn() {
return !!useUsersStore().currentUser
},
isOpen() { isOpen() {
return this.isLoggedIn && this.reportModal.activated return this.loggedIn && this.reportModal.activated
}, },
userId() { userId() {
return this.reportModal.userId return this.reportModal.userId
@ -47,6 +44,7 @@ const UserReportingModal = {
) )
}, },
...mapState(useReportsStore, ['reportModal']), ...mapState(useReportsStore, ['reportModal']),
...mapState(useUsersStore, ['loggedIn']),
}, },
watch: { watch: {
userId: 'resetState', userId: 'resetState',

View file

@ -24,11 +24,9 @@ const WhoToFollow = {
id, id,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}).then((result) => { }).then((result) => {
const { data: externalUser } = result const [user] = useUsersStore().addNewUsers(result)
if (!externalUser.error) {
useUsersStore().addNewUsers(result) this.users.push(user)
this.users.push(externalUser)
}
}) })
}) })
}, },

View file

@ -1634,7 +1634,6 @@
"no_statuses": "No statuses", "no_statuses": "No statuses",
"socket_reconnected": "Realtime connection established", "socket_reconnected": "Realtime connection established",
"socket_disconnected": "Realtime connection unavaialable", "socket_disconnected": "Realtime connection unavaialable",
"socket_closed": "Realtime connection closed",
"socket_broke": "Realtime connection lost: CloseEvent code {0}", "socket_broke": "Realtime connection lost: CloseEvent code {0}",
"quick_view_settings": "Quick view settings", "quick_view_settings": "Quick view settings",
"quick_filter_settings": "Quick filter settings", "quick_filter_settings": "Quick filter settings",

View file

@ -385,10 +385,13 @@ export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
const maxId = parsedLinkHeader.next?.max_id const maxId = parsedLinkHeader.next?.max_id
const minId = parsedLinkHeader.prev?.min_id const minId = parsedLinkHeader.prev?.min_id
return { const result = {}
maxId: flakeId ? maxId : Number.parseInt(maxId, 10), if (maxId !== undefined)
minId: flakeId ? minId : Number.parseInt(minId, 10), result.maxId = flakeId ? maxId : Number.parseInt(maxId, 10)
} if (minId !== undefined)
result.minId = flakeId ? minId : Number.parseInt(minId, 10)
return result
} }
export const parseChat = (chat) => { export const parseChat = (chat) => {

View file

@ -400,9 +400,13 @@ export const useAdminSettingsStore = defineStore('adminSettings', {
return { return {
items: await Promise.all( items: await Promise.all(
users.map((user) => { users.map(async (user) => {
useUsersStore().updateUserAdminData(user.id, user) const fullUser = await useUsersStore().fetchUserIfMissing({
return useUsersStore().findUser(user.id) id: user.id,
})
if (fullUser) useUsersStore().updateUserAdminData(user.id, user)
return fullUser
}), }),
), ),
count, count,

View file

@ -77,7 +77,8 @@ export const useChatsStore = defineStore('chats', {
updateChat(updatedChat) { updateChat(updatedChat) {
const chat = this.data.get(updatedChat.id) const chat = this.data.get(updatedChat.id)
if (chat) { if (chat) {
const isNewMessage = chat.lastMessage !== updatedChat.lastMessage const isNewMessage =
chat.lastMessage?.id !== updatedChat.lastMessage?.id
chat.lastMessage = updatedChat.lastMessage chat.lastMessage = updatedChat.lastMessage
chat.unread = updatedChat.unread chat.unread = updatedChat.unread
chat.updated_at = updatedChat.updated_at chat.updated_at = updatedChat.updated_at

View file

@ -36,7 +36,7 @@ const notificationsFetcher = (credentials) => {
const notifications = response.data const notifications = response.data
if (older && notifications.length === 0) bottomedOut.value = true if (older && notifications.length === 0) bottomedOut.value = true
useNotificationsStore().addNewNotifications(response) useNotificationsStore().addNewNotifications(response, older)
} catch (error) { } catch (error) {
if ( if (
error.statusCode === 400 && error.statusCode === 400 &&
@ -78,16 +78,13 @@ const notificationsFetcher = (credentials) => {
args.timeline = 'notifications' args.timeline = 'notifications'
if (older) { if (older) {
if (timelineData.minId !== Number.POSITIVE_INFINITY) { if (timelineData.minId !== '') {
args.maxId = timelineData.minId args.maxId = timelineData.minId
} }
return await fetchNotifications({ args, older }) return await fetchNotifications({ args, older })
} else { } else {
// fetch new notifications // fetch new notifications
if ( if (sinceId === undefined && timelineData.maxId !== '') {
sinceId === undefined &&
timelineData.maxId !== Number.POSITIVE_INFINITY
) {
args.sinceId = timelineData.maxId args.sinceId = timelineData.maxId
} else if (sinceId !== null) { } else if (sinceId !== null) {
args.sinceId = sinceId args.sinceId = sinceId

View file

@ -52,7 +52,11 @@ const timelineFetcher = (timeline, argument, credentials) => {
const numStatusesBeforeFetch = timeline.statusIds.size const numStatusesBeforeFetch = timeline.statusIds.size
if (older && bottomedOut.value) return if (older && bottomedOut.value) {
loadingOlder.value = false
return
}
return fetchTimeline(args) return fetchTimeline(args)
.then(({ data, pagination, timestamp }) => { .then(({ data, pagination, timestamp }) => {
// No statuses for timeline, ever. // No statuses for timeline, ever.
@ -135,6 +139,9 @@ const timelineFetcher = (timeline, argument, credentials) => {
loadingOlder, loadingOlder,
loadingNewer, loadingNewer,
bottomedOut, bottomedOut,
resetBottomedOut: () => {
bottomedOut.value = false
},
} }
} }

View file

@ -134,14 +134,7 @@ export const useInterfaceStore = defineStore('interface', {
1001, // Going away 1001, // Going away
]) ])
const { code } = closeEvent.original const { code } = closeEvent.original
if (intendedCodes.has(code)) { if (!intendedCodes.has(code)) {
this.pushGlobalNotice({
level: 'success',
messageKey: 'timeline.socket_closed',
messageArgs: [code],
timeout: 5000,
})
} else {
this.pushGlobalNotice({ this.pushGlobalNotice({
level: 'error', level: 'error',
messageKey: 'timeline.socket_broke', messageKey: 'timeline.socket_broke',

View file

@ -49,6 +49,9 @@ export const useListsStore = defineStore('lists', {
}, },
setLists(value) { setLists(value) {
this.allLists = value this.allLists = value
this.allListsObject = Object.fromEntries(
value.map((list) => [list.id, list]),
)
}, },
async createList({ title }) { async createList({ title }) {
return await createList({ return await createList({

View file

@ -81,13 +81,15 @@ export const useNotificationsStore = defineStore('notifications', {
pause() { pause() {
this.paused = true this.paused = true
if (this.fetcher && this.fetching) { if (this.fetcher && this.fetching) {
this.stopFetching('Notifications paused') console.debug('[Notifications] Pausing notifications')
this.fetcher.stopFetching()
} }
}, },
resume() { resume() {
this.paused = false this.paused = false
if (this.fetcher && this.fetching) { if (this.fetcher && this.fetching) {
this.startFetching('Notifications resumed') console.debug('[Notifications] Resuming notifications')
this.fetcher.startFetching()
} }
}, },
activate() { activate() {

View file

@ -36,6 +36,8 @@ export const defaultState = () => ({
conversations: new Map(), conversations: new Map(),
favorites: new Set(), favorites: new Set(),
socket: null, socket: null,
favs: new Map(),
repeats: new Map(),
}) })
export const useStatusesStore = defineStore('statuses', { export const useStatusesStore = defineStore('statuses', {
@ -188,61 +190,69 @@ export const useStatusesStore = defineStore('statuses', {
return fetchEmojiReactions({ return fetchEmojiReactions({
id, id,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}).then(({ data: emojiReactions }) => { }).then(({ data, timestamp }) => {
this.addEmojiReactionsBy({ const reactions = data.map((reaction) => {
id, const users = useUsersStore().addNewUsers({
emojiReactions, timestamp,
data: reaction.accounts,
})
// Backend inconsistency - status data only has ids (account_ids)
// but reactions data has full info (accounts)
return {
...reaction,
accounts: users,
account_ids: users.map(({ id }) => id),
}
}) })
this.addEmojiReactionsBy(id, reactions)
}) })
}, },
fetchFavs(id) { fetchFavs(id) {
return fetchFavoritedByUsers({ return fetchFavoritedByUsers({
id, id,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}).then(({ data: favoritedByUsers }) => }).then((result) => {
this.addFavs({ const users = useUsersStore().addNewUsers(result)
id, return this.addFavs(id, new Set(users.map(({ id }) => id)))
favoritedByUsers, })
}),
)
}, },
fetchRepeats(id) { fetchRepeats(id) {
return fetchRebloggedByUsers({ return fetchRebloggedByUsers({
id, id,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}).then(({ data: rebloggedByUsers }) => }).then((result) => {
this.addRepeats({ const users = useUsersStore().addNewUsers(result)
id, return this.addRepeats(id, new Set(users.map(({ id }) => id)))
rebloggedByUsers, })
}),
)
}, },
fetchFavsAndRepeats(id) { fetchFavsAndRepeats(id) {
return Promise.all([this.fetchFavs(id), this.fetchRepeats(id)]) return Promise.all([this.fetchFavs(id), this.fetchRepeats(id)])
}, },
// Updates // Updates
addRepeats({ id, rebloggedByUsers }) { addRepeats(id, users) {
const currentUser = useUsersStore().currentUser const currentUser = useUsersStore().currentUser
const newStatus = this.allStatuses.get(id) const newStatus = this.allStatuses.get(id)
newStatus.rebloggedBy = rebloggedByUsers.filter(Boolean) this.repeats.set(id, users)
// repeats stats can be incorrect based on polling condition, let's update them using the most recent data
newStatus.repeat_num = newStatus.rebloggedBy.length // repeats stats can be incorrect based on polling
newStatus.repeated = !!newStatus.rebloggedBy.find( // condition, let's update them using the most recent data
({ id }) => currentUser?.id === id, newStatus.repeat_num = users.size
) newStatus.repeated = users.has(currentUser?.id)
}, },
addFavs({ id, favoritedByUsers }) { addFavs(id, users) {
const currentUser = useUsersStore().currentUser const currentUser = useUsersStore().currentUser
const newStatus = this.allStatuses.get(id) const newStatus = this.allStatuses.get(id)
newStatus.favoritedBy = favoritedByUsers.filter(Boolean) this.favs.set(id, users)
// favorites stats can be incorrect based on polling condition, let's update them using the most recent data
newStatus.fave_num = newStatus.favoritedBy.length // favorites stats can be incorrect based on polling
newStatus.favorited = !!newStatus.favoritedBy.find( // condition, let's update them using the most recent data
({ id }) => currentUser?.id === id, newStatus.fave_num = users.size
) newStatus.favorited = users.has(currentUser?.id)
}, },
addEmojiReactionsBy({ id, emojiReactions }) { addEmojiReactionsBy(id, emojiReactions) {
const status = this.allStatuses.get(id) const status = this.allStatuses.get(id)
status.emoji_reactions = emojiReactions status.emoji_reactions = emojiReactions
}, },
@ -287,7 +297,7 @@ export const useStatusesStore = defineStore('statuses', {
useInterfaceStore().pushGlobalNotice({ useInterfaceStore().pushGlobalNotice({
level: 'error', level: 'error',
messageKey: 'status.interact_error', messageKey: 'status.interact_error',
messageArgs: [error], messageArgs: { error },
timeout: 5000, timeout: 5000,
}) })
}) })
@ -393,6 +403,7 @@ export const useStatusesStore = defineStore('statuses', {
name: emoji, name: emoji,
count: 0, count: 0,
accounts: [], accounts: [],
account_ids: [],
} }
const count = value ? reaction.count + 1 : reaction.count - 1 const count = value ? reaction.count + 1 : reaction.count - 1
@ -400,12 +411,14 @@ export const useStatusesStore = defineStore('statuses', {
const accounts = value const accounts = value
? [...reaction.accounts, currentUser] ? [...reaction.accounts, currentUser]
: reaction.accounts.filter((acc) => acc.id !== currentUser.id) : reaction.accounts.filter((acc) => acc.id !== currentUser.id)
const account_ids = accounts.filter(Boolean).map(({ id }) => id)
const newReaction = { const newReaction = {
...reaction, ...reaction,
count, count,
me: value, me: value,
accounts, accounts,
account_ids,
} }
if (reactionPresent && count > 0) { if (reactionPresent && count > 0) {

View file

@ -99,6 +99,8 @@ export const useStreamingStore = defineStore('streaming', {
this.subscribers.delete(subscriber) this.subscribers.delete(subscriber)
if (stream) { if (stream) {
this.subscriptions.get(stream.name).delete(stream.argument) this.subscriptions.get(stream.name).delete(stream.argument)
} else {
this.globalSubscriptions.delete(subscriber)
} }
if (stream && this.state === WSConnectionStatus.JOINED) { if (stream && this.state === WSConnectionStatus.JOINED) {
@ -106,6 +108,8 @@ export const useStreamingStore = defineStore('streaming', {
} }
}, },
initSocket(initial) { initSocket(initial) {
if (this.socket) throw new Error('Socket already exists!')
this.state = initial this.state = initial
? WSConnectionStatus.STARTING_INITIAL ? WSConnectionStatus.STARTING_INITIAL
: WSConnectionStatus.STARTING : WSConnectionStatus.STARTING
@ -127,7 +131,11 @@ export const useStreamingStore = defineStore('streaming', {
}, },
stopSocket() { stopSocket() {
this.socket.close() this.socket.close()
this.socket = null
this.state = WSConnectionStatus.CLOSED this.state = WSConnectionStatus.CLOSED
this.retrying = false
this.retryMultiplier = 1
this.error = null
}, },
getSubArgs(stream) { getSubArgs(stream) {
@ -227,6 +235,8 @@ export const useStreamingStore = defineStore('streaming', {
) )
setTimeout(() => { setTimeout(() => {
if (this.retrying) return // retry aborted (i.e. due to logout)
this.initSocket() this.initSocket()
}, retryTimeout(this.retryMultiplier)) }, retryTimeout(this.retryMultiplier))

View file

@ -81,6 +81,7 @@ export const ARGUMENT_MAP = {
user: 'userId', user: 'userId',
userPinned: 'userId', userPinned: 'userId',
media: 'userId', media: 'userId',
favorites: 'userId',
} }
const TIMELINES = new Set([ const TIMELINES = new Set([
@ -105,8 +106,6 @@ export const defaultState = () => {
return Object.fromEntries([...TIMELINES].map((name) => [name, emptyTl(name)])) return Object.fromEntries([...TIMELINES].map((name) => [name, emptyTl(name)]))
} }
//const CUSTOM_SORT = new Set(['bookmarks', 'favorites'])
export const useTimelinesStore = defineStore('timelines', { export const useTimelinesStore = defineStore('timelines', {
state: defaultState, state: defaultState,
actions: { actions: {
@ -182,7 +181,7 @@ export const useTimelinesStore = defineStore('timelines', {
timeline.socket.handlers timeline.socket.handlers
timeline.socket.et.removeEventListener('open', openHandler) timeline.socket.et.removeEventListener('open', openHandler)
timeline.socket.et.removeEventListener('close', closeHandler) timeline.socket.et.removeEventListener('close', closeHandler)
timeline.socket.et.removeEventListener('message', messageHandler) timeline.socket.et.removeEventListener('update', messageHandler)
} }
this[timelineName] = emptyTl(timelineName) this[timelineName] = emptyTl(timelineName)
@ -198,6 +197,7 @@ export const useTimelinesStore = defineStore('timelines', {
timeline.maxId = '' timeline.maxId = ''
timeline.minId = '' timeline.minId = ''
timeline.reloadNeeded = false timeline.reloadNeeded = false
timeline.fetcher.resetBottomedOut()
}, },
activatePersistents() { activatePersistents() {
TIMELINES.forEach((name) => { TIMELINES.forEach((name) => {
@ -268,8 +268,6 @@ export const useTimelinesStore = defineStore('timelines', {
if (statuses.length === 0) return if (statuses.length === 0) return
const timeline = this[timelineName] const timeline = this[timelineName]
this.populateRepeats(timeline, repeats)
// 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
@ -280,9 +278,7 @@ export const useTimelinesStore = defineStore('timelines', {
return return
} }
if (!noIdUpdate) { this.populateRepeats(timeline, repeats)
this.updateTimelineExtremes(timeline, pagination)
}
const filtered = statuses.filter((id) => !timeline.statusIds.has(id)) const filtered = statuses.filter((id) => !timeline.statusIds.has(id))
if (older) { if (older) {
@ -291,29 +287,39 @@ export const useTimelinesStore = defineStore('timelines', {
timeline.order.unshift(...filtered) timeline.order.unshift(...filtered)
} }
const newStatuses = new Set()
statuses.forEach((statusId) => { statuses.forEach((statusId) => {
const isNew = !timeline.statusIds.has(statusId) const isNew = !timeline.statusIds.has(statusId)
timeline.statusIds.add(statusId) timeline.statusIds.add(statusId)
if (isNew) { if (isNew) {
const seenBefore = this.checkSeenBefore(timeline, statusId) newStatuses.add(statusId)
if (!seenBefore) {
if (showImmediately) {
// Add it directly to the visibleStatuses, don't change
// newStatusCount
timeline.visibleStatusIds.add(statusId)
} else {
// Just change newStatuscount
timeline.newStatusCount += 1
}
} else {
timeline.ignoredIds.add(statusId)
}
} }
}) })
newStatuses.forEach((statusId) => {
const seenBefore = this.checkSeenBefore(timeline, statusId)
if (!seenBefore) {
if (showImmediately) {
// Add it directly to the visibleStatuses, don't change
// newStatusCount
timeline.visibleStatusIds.add(statusId)
} else {
// Just change newStatuscount
timeline.newStatusCount += 1
}
} else {
timeline.ignoredIds.add(statusId)
}
})
if (!noIdUpdate) {
this.updateTimelineExtremes(timeline, pagination)
}
}, },
onStreamMessage(timeline, argument, event) { onStreamMessage(timelineName, argument, event) {
this.addStatusesToTimeline(timeline, argument, { this.addStatusesToTimeline(timelineName, argument, {
statuses: event.data.map(({ id }) => id), statuses: event.data.map(({ id }) => id),
repeats: event.data repeats: event.data
.filter(({ retweeted_status }) => Boolean(retweeted_status)) .filter(({ retweeted_status }) => Boolean(retweeted_status))
@ -349,7 +355,7 @@ export const useTimelinesStore = defineStore('timelines', {
// If it's the only reprööt then we've never seen post before // If it's the only reprööt then we've never seen post before
if (knownRepeats.size === 1) return false if (knownRepeats.size === 1) return false
// If we're working on oldest known reprööt then we've never seen it before // If we're working on oldest known reprööt then we've never seen it before
return first(knownRepeats) !== statusId return knownRepeats.values().next().value !== statusId
}, },
// Poll & Push // Poll & Push
@ -416,7 +422,7 @@ export const useTimelinesStore = defineStore('timelines', {
}, },
// Queues & Timeline manip // Queues & Timeline manip
updateTimelineExtremes(timeline, pagination = {}) { updateTimelineExtremes(timeline, pagination = {}, force = false) {
// Can't use Math.min/max because it doesn't work with string (duh) // Can't use Math.min/max because it doesn't work with string (duh)
const minNew = pagination.maxId ?? last(timeline.order) ?? '' const minNew = pagination.maxId ?? last(timeline.order) ?? ''
const maxNew = pagination.minId ?? first(timeline.order) ?? '' const maxNew = pagination.minId ?? first(timeline.order) ?? ''
@ -424,10 +430,10 @@ export const useTimelinesStore = defineStore('timelines', {
const newer = maxNew > timeline.maxId const newer = maxNew > timeline.maxId
const older = minNew < timeline.minId const older = minNew < timeline.minId
if (newer || timeline.maxId === '') { if (force || newer || timeline.maxId === '') {
timeline.maxId = maxNew timeline.maxId = maxNew
} }
if (older || timeline.minId === '') { if (force || older || timeline.minId === '') {
timeline.minId = minNew timeline.minId = minNew
} }
@ -444,7 +450,8 @@ export const useTimelinesStore = defineStore('timelines', {
timeline.visibleStatusIds = new Set([ timeline.visibleStatusIds = new Set([
...timeline.order.filter((id) => !timeline.ignoredIds.has(id)), ...timeline.order.filter((id) => !timeline.ignoredIds.has(id)),
]) ])
this.updateTimelineExtremes(timeline) this.updateTimelineExtremes(timeline, {}, true)
timeline.fetcher.resetBottomedOut()
}, },
syncOrder(timeline) { syncOrder(timeline) {
timeline.order = timeline.order.filter((id) => timeline.statusIds.has(id)) timeline.order = timeline.order.filter((id) => timeline.statusIds.has(id))
@ -453,8 +460,10 @@ export const useTimelinesStore = defineStore('timelines', {
this[timeline].reloadNeeded = true this[timeline].reloadNeeded = true
}, },
requireReloadAll() { requireReloadAll() {
Object.keys(this).forEach((timeline) => { TIMELINES.forEach((timelineName) => {
this[timeline].reloadNeeded = true const timeline = this[timelineName]
timeline.reloadNeeded = true
}) })
}, },

View file

@ -208,7 +208,12 @@ export const useUsersStore = defineStore('users', {
// Misc updates // Misc updates
updateUserAdminData(id, data) { updateUserAdminData(id, data) {
const user = this.users.get(id) const user = this.users.get(id)
if (!user) {
console.warn(
`User id ${id} somehow not found during admin data update!`,
)
return
}
user.adminData = data user.adminData = data
user.deactivated = !data.is_active user.deactivated = !data.is_active
user.tags = new Set(data.tags) user.tags = new Set(data.tags)
@ -271,15 +276,21 @@ export const useUsersStore = defineStore('users', {
const result = await promise const result = await promise
if (result) { try {
const { id, screen_name } = result if (result) {
const { id, screen_name } = result
// Save promise for future use // Save promise for future use
this.fetchesIds.set(id, promise) this.fetchesIds.set(id, promise)
this.fetchesNames.set(screen_name, promise) this.fetchesNames.set(screen_name, promise)
return this.users.get(id) return this.users.get(id)
} else { } else {
return null return null
}
} catch (e) {
console.error(`Failed fetching user ${identifier}`, e)
map.delete(identifier)
throw e
} }
}, },
async fetchUser(id) { async fetchUser(id) {
@ -513,7 +524,7 @@ export const useUsersStore = defineStore('users', {
/// Mute /// Mute
muteUser(id, expiresIn = 0) { muteUser(id, expiresIn = 0) {
const predictedRelationship = this.relationships[id] || { id } const predictedRelationship = this.relationships.get(id) || { id }
predictedRelationship.muting = true predictedRelationship.muting = true
this.updateUserRelationships({ this.updateUserRelationships({
optimism: true, optimism: true,
@ -532,7 +543,7 @@ export const useUsersStore = defineStore('users', {
return Promise.all(data.map((d) => this.muteUser(d))) return Promise.all(data.map((d) => this.muteUser(d)))
}, },
unmuteUser(id) { unmuteUser(id) {
const predictedRelationship = this.relationships[id] || { id } const predictedRelationship = this.relationships.get(id) || { id }
predictedRelationship.muting = false predictedRelationship.muting = false
this.updateUserRelationships({ this.updateUserRelationships({
optimism: true, optimism: true,
@ -549,7 +560,7 @@ export const useUsersStore = defineStore('users', {
/// Block /// Block
blockUser(id, expiresIn = 0) { blockUser(id, expiresIn = 0) {
const predictedRelationship = this.relationships[id] || { id } const predictedRelationship = this.relationships.get(id) || { id }
this.updateUserRelationships({ this.updateUserRelationships({
optimism: true, optimism: true,
data: [predictedRelationship], data: [predictedRelationship],
@ -718,6 +729,8 @@ export const useUsersStore = defineStore('users', {
useAnnouncementsStore().stopFetching() useAnnouncementsStore().stopFetching()
useListsStore().stopFetching() useListsStore().stopFetching()
useBookmarkFoldersStore().stopFetching() useBookmarkFoldersStore().stopFetching()
useChatsStore().stopFetching()
store?.dispatch('stopFetchingFollowRequests') store?.dispatch('stopFetchingFollowRequests')
// NOTE: No need to verify the app still exists, because if it doesn't, // NOTE: No need to verify the app still exists, because if it doesn't,
@ -743,7 +756,6 @@ export const useUsersStore = defineStore('users', {
// Full reset on logout success // Full reset on logout success
useTimelinesStore().deactivateAll() useTimelinesStore().deactivateAll()
useStatusesStore().resetStatuses() useStatusesStore().resetStatuses()
useChatsStore().stopFetching()
useChatsStore().resetChats() useChatsStore().resetChats()
this.users = new Map() this.users = new Map()

View file

@ -290,7 +290,10 @@ describe('Statuses store', () => {
'EmojiReactions', 'EmojiReactions',
[ [
{ {
accounts: [mockMastoAPIUser()], accounts: [
mockMastoAPIUser({ id: 'u1' }),
mockMastoAPIUser({ id: 'u2' }),
],
count: 1, count: 1,
me: false, me: false,
name: 'cofe', name: 'cofe',
@ -298,8 +301,14 @@ describe('Statuses store', () => {
}, },
], ],
], ],
['Favs', [mockMastoAPIUser()]], [
['Repeats', [mockMastoAPIUser()]], 'Favs',
[mockMastoAPIUser({ id: 'u1' }), mockMastoAPIUser({ id: 'u2' })],
],
[
'Repeats',
[mockMastoAPIUser({ id: 'u1' }), mockMastoAPIUser({ id: 'u2' })],
],
])('fetch%s', async (group, mockedResponse) => { ])('fetch%s', async (group, mockedResponse) => {
const mockFetch = vi.fn() const mockFetch = vi.fn()
mockFetch.mockResolvedValueOnce( mockFetch.mockResolvedValueOnce(
@ -309,6 +318,7 @@ describe('Statuses store', () => {
) )
vi.stubGlobal('fetch', mockFetch) vi.stubGlobal('fetch', mockFetch)
const addNewUsers = vi.spyOn(useUsersStore(), 'addNewUsers')
let urlKey let urlKey
let prefix = 'MASTODON' let prefix = 'MASTODON'
@ -335,13 +345,29 @@ describe('Statuses store', () => {
const result = await store[`fetch${group}`]('id') const result = await store[`fetch${group}`]('id')
const updated = store.allStatuses.get('id') const updated = store.allStatuses.get('id')
// Fetch called
expect(mockFetch).to.have.been.calledWith(url, DEFAULT_OPTIONS()) expect(mockFetch).to.have.been.calledWith(url, DEFAULT_OPTIONS())
// Users updated
if (group !== 'StatusSource') {
// first call is the one for the status
expect(addNewUsers).to.have.been.calledTwice
const secondCallData = addNewUsers.mock.calls[1][0].data
expect(secondCallData).to.have.length(2)
expect(secondCallData[0]).to.have.property('id', 'u1')
expect(secondCallData[1]).to.have.property('id', 'u2')
}
if (group === 'Favs') { if (group === 'Favs') {
expect(updated.favoritedBy).to.have.length(1) expect(store.favs).to.have.length(1)
expect(updated.fave_num).to.eql(1) expect(store.favs.get('id')).to.have.length(2)
expect(store.favs.get('id')).to.eql(new Set(['u1', 'u2']))
expect(updated.fave_num).to.eql(2)
} else if (group === 'Repeats') { } else if (group === 'Repeats') {
expect(updated.rebloggedBy).to.have.length(1) expect(store.repeats).to.have.length(1)
expect(updated.repeat_num).to.eql(1) expect(store.repeats.get('id')).to.have.length(2)
expect(store.repeats.get('id')).to.eql(new Set(['u1', 'u2']))
expect(updated.repeat_num).to.eql(2)
} else if (group === 'EmojiReactions') { } else if (group === 'EmojiReactions') {
expect(updated.emoji_reactions).to.have.length(mockedResponse.length) expect(updated.emoji_reactions).to.have.length(mockedResponse.length)
expect(updated.emoji_reactions[0].name).to.eql(mockedResponse[0].name) expect(updated.emoji_reactions[0].name).to.eql(mockedResponse[0].name)

View file

@ -740,19 +740,24 @@ describe('Users store', () => {
const spies = [ const spies = [
// Misc initialization // Misc initialization
vi.spyOn(useStatusesStore(), 'resetStatuses'), /* 0 */ vi.spyOn(useStatusesStore(), 'resetStatuses'),
vi.spyOn(useInterfaceStore(), 'onLogout'), /* 1 */ vi.spyOn(useInterfaceStore(), 'onLogout'),
// Timeline / Notifications // Timeline / Notifications
vi.spyOn(useNotificationsStore(), 'deactivate'), /* 2 */ vi.spyOn(useNotificationsStore(), 'deactivate'),
vi.spyOn(useTimelinesStore(), 'deactivateAll'), /* 3 */ vi.spyOn(useNotificationsStore(), 'pause'),
/* 4 */ vi.spyOn(useNotificationsStore(), 'resume'),
/* 5 */ vi.spyOn(useTimelinesStore(), 'deactivateAll'),
/* 6 */ vi.spyOn(useTimelinesStore(), 'pauseAll'),
/* 7 */ vi.spyOn(useTimelinesStore(), 'resumeAll'),
// Fetchers // Fetchers
vi.spyOn(useChatsStore(), 'resetChats'), /* 8 */ vi.spyOn(useChatsStore(), 'resetChats'),
vi.spyOn(useListsStore(), 'stopFetching'), /* 9 */ vi.spyOn(useChatsStore(), 'stopFetching'),
vi.spyOn(useAnnouncementsStore(), 'stopFetching'), /* 10 */ vi.spyOn(useListsStore(), 'stopFetching'),
vi.spyOn(useBookmarkFoldersStore(), 'stopFetching'), /* 11 */ vi.spyOn(useAnnouncementsStore(), 'stopFetching'),
vi.spyOn(useStreamingStore(), 'stopSocket'), /* 12 */ vi.spyOn(useBookmarkFoldersStore(), 'stopFetching'),
/* 13 */ vi.spyOn(useStreamingStore(), 'stopSocket'),
] ]
spies.forEach((spy) => { spies.forEach((spy) => {
@ -791,6 +796,91 @@ describe('Users store', () => {
expect(spy, `Spy ${index} has failed`).to.have.been.called expect(spy, `Spy ${index} has failed`).to.have.been.called
}) })
}) })
it('failed logout', async () => {
const revokeApi = vi
.fn()
.mockResolvedValueOnce(
// Ensure APP
new Response(JSON.stringify('ok'), {
headers: { 'Content-Type': 'application/json' },
}),
)
.mockResolvedValueOnce(
// Revoke Token
new Response(
JSON.stringify('Oopsie-woopsie pleroma made a fucky-wucky'),
{
status: 500,
statusText: 'Internal Server Error',
headers: { 'Content-Type': 'application/json' },
},
),
)
vi.stubGlobal('fetch', revokeApi)
// NOTE: Order is not checked for!
const spies = [
// ## PAUSE ##
// Timeline / Notifications
/* 0 */ vi.spyOn(useTimelinesStore(), 'pauseAll'),
/* 1 */ vi.spyOn(useNotificationsStore(), 'pause'),
// Fetchers (Pauseless)
/* 2 */ vi.spyOn(useListsStore(), 'stopFetching'),
/* 3 */ vi.spyOn(useChatsStore(), 'stopFetching'),
/* 4 */ vi.spyOn(useAnnouncementsStore(), 'stopFetching'),
/* 5 */ vi.spyOn(useBookmarkFoldersStore(), 'stopFetching'),
// ## RESUME ##
// Timeline / Notifications
/* 6 */ vi.spyOn(useNotificationsStore(), 'resume'),
/* 7 */ vi.spyOn(useTimelinesStore(), 'resumeAll'),
// Fetchers (Pauseless)
/* 8 */ vi.spyOn(useListsStore(), 'startFetching'),
/* 9 */ vi.spyOn(useChatsStore(), 'startFetching'),
/* 10 */ vi.spyOn(useAnnouncementsStore(), 'startFetching'),
/* 11 */ vi.spyOn(useBookmarkFoldersStore(), 'startFetching'),
]
spies.forEach((spy) => {
spy.mockImplementation(async () => {
/* no-op */
})
})
useInstanceCapabilitiesStore().pleromaChatMessagesAvailable = true
useMergedConfigStore().mergedConfig = { useStreamingApi: true }
const store = useUsersStore()
store.currentUser = mockUser()
// Adding some users to verify they are getting cleaned afterwards
store.addNewUsers({
data: [
mockUser(),
{
...mockUser({ name: 'John', screen_name: 'snake' }),
relationship: { id: userId, following: true },
},
{ ...mockUser({ name: 'David Oh', screen_name: 'zero' }) },
],
timestamp: 2000,
})
expect(store.loggedIn).to.eql(true)
await store.logout()
expect(store.loggedIn).to.eql(true)
expect(revokeApi).to.have.been.called
expect(store.users).to.have.length(1)
expect(store.usersByName).to.have.length(1)
expect(store.usersByURL).to.have.length(1)
expect(store.relationships).to.have.length(1)
spies.forEach((spy, index) => {
expect(spy, `Spy ${index} has failed`).to.have.been.called
})
})
}) })
}) })