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

This commit is contained in:
Henry Jameson 2026-08-25 21:44:27 +03:00
commit c0bbb1a768
27 changed files with 227 additions and 97 deletions

View file

@ -1 +0,0 @@
If user avatars next to mentions are enabled it will show empty placeholder avatar next to label while user is being fetched, to avoid jumps

View file

@ -29,6 +29,7 @@ import {
import routes from './routes' import routes from './routes'
import { useAuthFlowStore } from 'src/stores/auth_flow' import { useAuthFlowStore } from 'src/stores/auth_flow'
import { useChatsStore } from 'src/stores/chats.js'
import { useEmojiStore } from 'src/stores/emoji.js' import { useEmojiStore } from 'src/stores/emoji.js'
import { useI18nStore } from 'src/stores/i18n' import { useI18nStore } from 'src/stores/i18n'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
@ -593,6 +594,7 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => {
useI18nStore().setI18n(i18n) useI18nStore().setI18n(i18n)
// Global WS handlers // Global WS handlers
useChatsStore().attachSocket()
useInterfaceStore().attachSocket() useInterfaceStore().attachSocket()
useStatusesStore().attachSocket() useStatusesStore().attachSocket()

View file

@ -89,14 +89,15 @@ const Chat = {
fetcher: null, fetcher: null,
socket: null, socket: null,
streaming: false, streaming: false,
fetching: true,
errorLoadingChat: false, errorLoadingChat: false,
messageRetriers: {}, messageRetriers: {},
idempotencyKeyIndex: {}, idempotencyKeyIndex: {},
} }
}, },
created() { async created() {
if (this.testMode) return if (this.testMode) return
this.activate() await this.activate()
this.attachSocket() this.attachSocket()
}, },
mounted() { mounted() {
@ -243,10 +244,11 @@ const Chat = {
accountId: this.chatUserId, accountId: this.chatUserId,
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}) })
useUsersStore().addNewUsers(result)
const { data } = result const { data } = result
useUsersStore().addNewUsers({ ...result, data: data.account })
data.account = useUsersStore().findUser(data.account.id) data.account = useUsersStore().findUser(data.account.id)
this.chat = data this.chat = data
this.maxId = this.chat.lastMessage?.id
} catch (e) { } catch (e) {
console.error('Error creating or getting a chat', e) console.error('Error creating or getting a chat', e)
this.errorLoadingChat = true this.errorLoadingChat = true
@ -254,33 +256,38 @@ const Chat = {
} }
if (this.isConversation || this.chat) { if (this.isConversation || this.chat) {
this.startFetching('Chat activated', true)
this.$nextTick(() => { this.$nextTick(() => {
this.scrollDown({ forceRead: true }) this.scrollDown({ forceRead: true })
}) })
this.startFetching('Chat activated', true)
} }
}, },
deactivate() { deactivate() {
this.clear() this.clear()
if (!this.streaming) { if (this.fetching) {
this.stopFetching() this.stopFetching('Chat deactivated')
} }
}, },
attachSocket() { attachSocket() {
const et = new EventTarget() const et = new EventTarget()
const socket = { et } const socket = {
name: 'chatview',
et,
}
et.addEventListener('update', this.onStreamMessage) et.addEventListener('update', this.onStreamMessage)
et.addEventListener('pleroma:chat_update', this.onChatUpdate)
et.addEventListener('open', this.onStreamConnect) et.addEventListener('open', this.onStreamConnect)
et.addEventListener('close', this.onStreamDisconnect) et.addEventListener('close', this.onStreamDisconnect)
useStreamingStore().addSubscriber(socket)
this.socket = socket this.socket = socket
useStreamingStore().addSubscriber(this.socket)
}, },
detachSocket() { detachSocket() {
const { et } = this.socket const { et } = this.socket
et.removeEventListener('update', this.onStreamMessage) et.removeEventListener('update', this.onStreamMessage)
et.removeEventListener('pleroma:chat_update', this.onChatUpdate)
et.removeEventListener('open', this.onStreamConnect) et.removeEventListener('open', this.onStreamConnect)
et.removeEventListener('close', this.onStreamDisconnect) et.removeEventListener('close', this.onStreamDisconnect)
@ -303,11 +310,13 @@ const Chat = {
5000, 5000,
) )
this.fetchChat({ isFirstFetch }) this.fetchChat({ isFirstFetch })
this.fetching = true
}, },
stopFetching(reason) { stopFetching(reason) {
console.debug('[Chat View] Stopped fetching', 'Reason:', reason) console.debug('[Chat View] Stopped fetching', 'Reason:', reason)
this.fetcher.stop() this.fetcher.stop()
this.fetcher = null this.fetcher = null
this.fetching = false
}, },
// Actions // Actions
@ -437,6 +446,10 @@ const Chat = {
) )
this.addMessages({ messages }) this.addMessages({ messages })
}, },
onChatUpdate({ data: { chatUpdate } }) {
const messages = [chatUpdate.lastMessage]
this.addMessages({ messages })
},
addMessages({ messages: newMessages }) { addMessages({ messages: newMessages }) {
for (let i = 0; i < newMessages.length; i++) { for (let i = 0; i < newMessages.length; i++) {
const message = newMessages[i] const message = newMessages[i]

View file

@ -155,9 +155,9 @@ const conversation = {
return this.otherRepliesButtonPosition === 'inside' return this.otherRepliesButtonPosition === 'inside'
}, },
suspendable() { suspendable() {
return this.unsuspendibleIds.size > 0 return this.unsuspendibleIds.size === 0
}, },
hideStatus() { hide() {
return this.virtualHidden && this.suspendable return this.virtualHidden && this.suspendable
}, },
originalStatusId() { originalStatusId() {
@ -365,7 +365,6 @@ const conversation = {
return !!(this.expanded || this.isPage) return !!(this.expanded || this.isPage)
}, },
hiddenStyle() { hiddenStyle() {
if (this.isExpanded || !this.virtualHidden) return {}
return { height: this.virtualHeight + 'px' } return { height: this.virtualHeight + 'px' }
}, },
threadDisplayStatus() { threadDisplayStatus() {
@ -621,6 +620,7 @@ const conversation = {
} }
}, },
updateVirtualHeight() { updateVirtualHeight() {
if (this.hide) return // no updates when not rendering
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,7 +1,6 @@
<template> <template>
<div <div
v-if="!hideStatus" v-if="!hide"
:style="hiddenStyle"
class="Conversation" class="Conversation"
:class="{ '-expanded' : isExpanded, 'panel' : isExpanded }" :class="{ '-expanded' : isExpanded, 'panel' : isExpanded }"
> >

View file

@ -62,7 +62,7 @@ const EmojiReactions = {
}, },
async fetchEmojiReactionsIfMissing() { async fetchEmojiReactionsIfMissing() {
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 useStatusesStore().fetchEmojiReactions(this.status.id) return await useStatusesStore().fetchEmojiReactions(this.status.id)
} }
}, },

View file

@ -8,20 +8,15 @@
:href="url" :href="url"
class="original" class="original"
target="_blank" target="_blank"
><!-- eslint-enable vue/no-v-html --> v-html="content"
<UserAvatar /><!-- eslint-enable vue/no-v-html -->
v-if="shouldShowAvatar"
class="mention-avatar"
:user-id="null"
/>
<span v-html="content" />
</a>
<UserPopover <UserPopover
v-else v-else
:user-id="user.id" :user-id="user.id"
:disabled="!shouldShowTooltip" :disabled="!shouldShowTooltip"
> >
<span <span
v-if="user"
class="new" class="new"
:style="style" :style="style"
:class="classnames" :class="classnames"

View file

@ -121,10 +121,10 @@ const Notifications = {
) )
}, },
loading() { loading() {
return useNotificationsStore().fetcher.loading.value return useNotificationsStore().fetcher.loading
}, },
bottomedOut() { bottomedOut() {
return useNotificationsStore().fetcher.bottomedOut.value return useNotificationsStore().fetcher.bottomedOut
}, },
noHeading() { noHeading() {
const { layoutType } = useInterfaceStore() const { layoutType } = useInterfaceStore()

View file

@ -119,7 +119,7 @@
> >
<router-link <router-link
class="list-item hashtag" class="list-item hashtag"
:to="{ name: 'tag-timeline', params: { tag: hashtag.name } }" :to="{ name: 'tag-timeline', params: { id: hashtag.name } }"
> >
<span class="name"> <span class="name">
#{{ hashtag.name }} #{{ hashtag.name }}

View file

@ -128,7 +128,6 @@ const Status = {
return { return {
replying: false, replying: false,
unmuted: false, unmuted: false,
userExpanded: false,
mediaPlaying: new Set(), mediaPlaying: new Set(),
error: null, error: null,
headTailLinks: null, headTailLinks: null,
@ -524,9 +523,6 @@ const Status = {
toggleMute() { toggleMute() {
this.unmuted = !this.unmuted this.unmuted = !this.unmuted
}, },
toggleUserExpanded() {
this.userExpanded = !this.userExpanded
},
generateUserProfileLink(id, name) { generateUserProfileLink(id, name) {
return generateProfileLink( return generateProfileLink(
id, id,
@ -576,6 +572,12 @@ const Status = {
this.$emit('heightChange') this.$emit('heightChange')
}, },
}, },
unmuted() {
this.$emit('heightChange')
},
error() {
this.$emit('heightChange')
},
replying() { replying() {
this.$emit('heightChange') this.$emit('heightChange')
}, },
@ -604,7 +606,6 @@ const Status = {
}, },
isSuspendable: function (suspend) { isSuspendable: function (suspend) {
this.$emit('suspendableStateChange', { id: this.status.id, suspend }) this.$emit('suspendableStateChange', { id: this.status.id, suspend })
this.$emit('heightChange')
}, },
}, },
} }

View file

@ -549,6 +549,7 @@
@posted="closeReplyForm" @posted="closeReplyForm"
@draft-done="closeReplyForm" @draft-done="closeReplyForm"
@close-accepted="closeReplyForm" @close-accepted="closeReplyForm"
@resize="$emit('heightChange')"
/> />
</div> </div>
</template> </template>

View file

@ -239,7 +239,7 @@ const Timeline = {
const bodyBRect = document.body.getBoundingClientRect() const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -bodyBRect.y) const height = Math.max(bodyBRect.height, -bodyBRect.y)
if ( if (
!this.timeline.fetcher.loadingOlder.value && !this.timeline.fetcher.loadingOlder &&
window.innerHeight + window.pageYOffset >= height - 750 window.innerHeight + window.pageYOffset >= height - 750
) { ) {
this.fetchOlderStatuses() this.fetchOlderStatuses()

View file

@ -12,7 +12,7 @@ const UserAvatar = {
props: { props: {
// UserID of a user to show avatar of // UserID of a user to show avatar of
userId: { userId: {
required: false, // You can pass null to just render a placeholder required: true,
type: String, type: String,
}, },
// Use less space and use alternative roundness // Use less space and use alternative roundness

View file

@ -70,7 +70,6 @@
&.-placeholder { &.-placeholder {
background-color: var(--background); background-color: var(--background);
border: 1px solid var(--border)
} }
} }

View file

@ -30,8 +30,8 @@ function showWhoToFollow(panel, reply) {
}) })
} }
function getWhoToFollow(panel) { function getWhoToFollow() {
const credentials = panel.$useUsersStore().currentUser.credentials const credentials = useOAuthStore().token
if (credentials) { if (credentials) {
panel.usersToFollow.forEach((toFollow) => { panel.usersToFollow.forEach((toFollow) => {
toFollow.name = 'Loading...' toFollow.name = 'Loading...'
@ -66,7 +66,7 @@ const WhoToFollowPanel = {
watch: { watch: {
user: function () { user: function () {
if (this.suggestionsEnabled) { if (this.suggestionsEnabled) {
getWhoToFollow(this) getWhoToFollow()
} }
}, },
}, },
@ -77,7 +77,7 @@ const WhoToFollowPanel = {
id: 0, id: 0,
})) }))
if (this.suggestionsEnabled) { if (this.suggestionsEnabled) {
getWhoToFollow(this) getWhoToFollow()
} }
}, },
} }

View file

@ -11,13 +11,14 @@ export const maybeShowChatNotification = (chat) => {
title: chat.account.name, title: chat.account.name,
icon: chat.account.profile_image_url, icon: chat.account.profile_image_url,
body: chat.lastMessage.content, body: chat.lastMessage.content,
type: 'chatMention',
} }
if (chat.lastMessage.attachment?.type === 'image') { if (chat.lastMessage.attachment?.type === 'image') {
opts.image = chat.lastMessage.attachment.preview_url opts.image = chat.lastMessage.attachment.preview_url
} }
showDesktopNotification(window.vuex.state, opts) showDesktopNotification(opts)
} }
export const buildFakeMessage = ({ export const buildFakeMessage = ({

View file

@ -2,7 +2,7 @@ import { map } from 'lodash'
import { useStatusesStore } from 'src/stores/statuses.js' import { useStatusesStore } from 'src/stores/statuses.js'
import { useTimelinesStore } from 'src/stores/timelines.js' import { useTimelinesStore } from 'src/stores/timelines.js'
import { useUsersStore } from 'src/stores/users.js' import { useOAuthStore } from 'src/stores/oauth.js'
import { import {
editStatus as apiEditStatus, editStatus as apiEditStatus,
@ -28,7 +28,7 @@ const postStatus = ({
const mediaIds = map(media, 'id') const mediaIds = map(media, 'id')
return apiPostStatus({ return apiPostStatus({
credentials: useUsersStore().currentUser.credentials, credentials: useOAuthStore().token,
status, status,
spoilerText, spoilerText,
visibility, visibility,
@ -71,7 +71,7 @@ const editStatus = ({
return apiEditStatus({ return apiEditStatus({
id: statusId, id: statusId,
credentials: useUsersStore().currentUser.credentials, credentials: useOAuthStore().token,
status, status,
spoilerText, spoilerText,
sensitive, sensitive,
@ -101,12 +101,12 @@ const editStatus = ({
} }
const uploadMedia = ({ store, formData }) => { const uploadMedia = ({ store, formData }) => {
const credentials = useUsersStore().currentUser.credentials const credentials = useOAuthStore().token
return apiUploadMedia({ credentials, formData }).then(({ data }) => data) return apiUploadMedia({ credentials, formData }).then(({ data }) => data)
} }
const setMediaDescription = ({ store, id, description }) => { const setMediaDescription = ({ store, id, description }) => {
const credentials = useUsersStore().currentUser.credentials const credentials = useOAuthStore().token
return apiSetMediaDescription({ credentials, id, description }).then( return apiSetMediaDescription({ credentials, id, description }).then(
({ data }) => data, ({ data }) => data,
) )

View file

@ -37,7 +37,10 @@ export const useChatsStore = defineStore('chats', {
actions: { actions: {
attachSocket() { attachSocket() {
const et = new EventTarget() const et = new EventTarget()
const socket = { et } const socket = {
name: 'chats',
et,
}
et.addEventListener('pleroma:chat_update', this.updateChat) et.addEventListener('pleroma:chat_update', this.updateChat)
@ -83,9 +86,6 @@ export const useChatsStore = defineStore('chats', {
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
if (isNewMessage && chat.unread) {
maybeShowChatNotification(chat)
}
} else { } else {
this.chatList.data.push(updatedChat) this.chatList.data.push(updatedChat)
this.chatList.idStore[updatedChat.id] = updatedChat this.chatList.idStore[updatedChat.id] = updatedChat
@ -98,16 +98,16 @@ export const useChatsStore = defineStore('chats', {
chat.unread = 0 chat.unread = 0
} }
}, },
updateChat({ chat: updatedChat }) { updateChat({ data: { chatUpdate: updatedChat } }) {
const chat = getChatById(this, updatedChat.id) const chat = getChatById(this, updatedChat.id)
if (chat) { if (chat) {
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
} } else {
if (!chat) {
this.chatList.data.unshift(updatedChat) this.chatList.data.unshift(updatedChat)
} }
maybeShowChatNotification(chat)
this.chatList.idStore[updatedChat.id] = updatedChat this.chatList.idStore[updatedChat.id] = updatedChat
}, },
deleteChat(id) { deleteChat(id) {

View file

@ -14,6 +14,7 @@ import {
import { useInterfaceStore } from 'src/stores/interface.js' import { useInterfaceStore } from 'src/stores/interface.js'
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { fetchKnownDomains } from 'src/api/public.js' import { fetchKnownDomains } from 'src/api/public.js'
@ -213,7 +214,7 @@ export const useInstanceStore = defineStore('instance', {
async getKnownDomains() { async getKnownDomains() {
try { try {
const { data } = await fetchKnownDomains({ const { data } = await fetchKnownDomains({
credentials: useUsersStore().currentUser.credentials, credentials: useOAuthStore().token
}) })
this.knownDomains = data this.knownDomains = data
} catch (e) { } catch (e) {

View file

@ -96,7 +96,10 @@ export const useInterfaceStore = defineStore('interface', {
actions: { actions: {
attachSocket() { attachSocket() {
const et = new EventTarget() const et = new EventTarget()
const socket = { et } const socket = {
name: 'interface',
et,
}
et.addEventListener('open', this.onStreamConnect) et.addEventListener('open', this.onStreamConnect)
et.addEventListener('close', this.onStreamDisconnect) et.addEventListener('close', this.onStreamDisconnect)
@ -854,7 +857,7 @@ export const useInterfaceStore = defineStore('interface', {
}, },
unregisterPushNotifications() { unregisterPushNotifications() {
const token = this.currentUser.credentials const token = useOAuthStore().token
unregisterPushNotifications(token) unregisterPushNotifications(token)
}, },

View file

@ -31,7 +31,9 @@ export const defaultState = () => ({
statusIdStore: new Set(), statusIdStore: new Set(),
socket: null, socket: null,
streaming: false, streaming: false,
fetching: true,
fetcher: null, fetcher: null,
paused: false,
}) })
export const useNotificationsStore = defineStore('notifications', { export const useNotificationsStore = defineStore('notifications', {
@ -40,14 +42,29 @@ export const useNotificationsStore = defineStore('notifications', {
// Init // Init
attachSocket() { attachSocket() {
const et = new EventTarget() const et = new EventTarget()
const socket = { et } const socket = {
name: 'notifications',
et,
}
et.addEventListener('notification', this.addNewNotifications) et.addEventListener('notification', this.addNewNotifications)
et.addEventListener('open', this.onStreamConnect) et.addEventListener('open', this.onStreamConnect)
et.addEventListener('close', this.onStreamDisconnect) et.addEventListener('close', this.onStreamDisconnect)
useStreamingStore().addSubscriber(socket)
this.socket = socket this.socket = socket
useStreamingStore().addSubscriber(this.socket)
},
pause() {
this.paused = true
if (this.fetcher && this.fetching) {
this.stopFetching('Notifications paused')
}
},
resume() {
this.paused = false
if (this.fetcher && this.fetching) {
this.startFetching('Notifications resumed')
}
}, },
activate() { activate() {
this.attachSocket() this.attachSocket()
@ -62,7 +79,7 @@ export const useNotificationsStore = defineStore('notifications', {
this.startFetching('Notifications activated') this.startFetching('Notifications activated')
}, },
deactivate() { deactivate() {
if (!this.streaming) { if (this.fetching) {
this.stopFetching('Notifications deactivated') this.stopFetching('Notifications deactivated')
} }
@ -77,6 +94,7 @@ export const useNotificationsStore = defineStore('notifications', {
Object.keys(blankState).forEach((k) => { Object.keys(blankState).forEach((k) => {
this[k] = blankState[k] this[k] = blankState[k]
}) })
console.log('[Notifications] Deactivated', this.fetcher)
}, },
// Poll & Push // Poll & Push
@ -91,20 +109,30 @@ export const useNotificationsStore = defineStore('notifications', {
this.startFetching('Socket disconnected') this.startFetching('Socket disconnected')
}, },
startFetching(reason) { startFetching(reason) {
if (this.paused) {
console.debug(
'[Notificatiosn] NOT Starting notifications fetcher because it is paused',
'Original Reason:',
reason,
)
return
}
console.debug( console.debug(
'[Notifications] Starting fetching notifications', '[Notifications] Starting notifications fetcher',
'Reason:', 'Reason:',
reason, reason,
) )
this.fetcher.startFetching() this.fetcher.startFetching()
this.fetching = true
}, },
stopFetching(reason) { stopFetching(reason) {
this.fetcher.stopFetching()
this.fetching = false
console.debug( console.debug(
'[Notifications] Stopped fetching notifications', '[Notifications] Stopped notifications fetcher',
'Reason:', 'Reason:',
reason, reason,
) )
this.fetcher.stopFetching()
}, },
// Updates // Updates
@ -223,7 +251,7 @@ export const useNotificationsStore = defineStore('notifications', {
case 'follow_request': case 'follow_request':
break break
default: default:
this.markSingleNotificationAsSeen({ id }) this.markSingleNotificationAsSeen(id)
} }
} }
}, },

View file

@ -19,7 +19,7 @@ export const useSearchStore = defineStore('search', {
credentials: useOAuthStore().token, credentials: useOAuthStore().token,
}) })
const { accounts, statuses } = data const { accounts, statuses, hashtags } = data
useUsersStore().addNewUsers({ useUsersStore().addNewUsers({
...rest, ...rest,
@ -36,6 +36,7 @@ export const useSearchStore = defineStore('search', {
useStatusesStore().allStatuses.get(s.id), useStatusesStore().allStatuses.get(s.id),
) )
output.accounts = accounts.map((s) => useUsersStore().findUser(s.id)) output.accounts = accounts.map((s) => useUsersStore().findUser(s.id))
output.hashtags = hashtags ?? []
return output return output
}, },

View file

@ -49,6 +49,7 @@ export const useStatusesStore = defineStore('statuses', {
data.forEach((id) => this.setDeleted(id)) data.forEach((id) => this.setDeleted(id))
const socket = { const socket = {
name: 'statuses',
et, et,
handlers: { handlers: {
handleUpdate, handleUpdate,
@ -64,15 +65,9 @@ export const useStatusesStore = defineStore('statuses', {
this.socket = socket this.socket = socket
}, },
resetStatuses() { resetStatuses() {
this.socket.et.removeEventListener('update', this.socket.handleUpdate)
this.socket.et.removeEventListener(
'status.update',
this.socket.handleUpdate,
)
this.socket.et.removeEventListener('delete', this.socket.handleDelete)
const emptyState = defaultState() const emptyState = defaultState()
Object.entries(emptyState).forEach(([key, value]) => { Object.entries(emptyState).forEach(([key, value]) => {
if (key === 'socket') return
this[key] = value this[key] = value
}) })
}, },
@ -154,7 +149,7 @@ export const useStatusesStore = defineStore('statuses', {
const newStatus = { const newStatus = {
...old, ...old,
...neu, ...Object.fromEntries(Object.entries(neu).filter(([, v]) => v !== undefined)),
user, user,
} }

View file

@ -131,6 +131,7 @@ export const useStreamingStore = defineStore('streaming', {
}, },
getSubArgs(stream) { getSubArgs(stream) {
if (stream === undefined) return undefined
const argumentKey = ARGUMENT_MAP[stream.name] const argumentKey = ARGUMENT_MAP[stream.name]
const args = argumentKey const args = argumentKey
? { ? {

View file

@ -16,9 +16,11 @@ const emptyTl = (name, argument = null) => {
maxId: '', maxId: '',
minId: '', minId: '',
streaming: false, streaming: false,
fetching: false,
reloadNeeded: false, reloadNeeded: false,
fetcher: null, fetcher: null,
socket: null, socket: null,
paused: false,
} }
const property = ARGUMENT_MAP[name] const property = ARGUMENT_MAP[name]
@ -105,16 +107,16 @@ export const useTimelinesStore = defineStore('timelines', {
const openHandler = () => this.onStreamConnect(timelineName, argument) const openHandler = () => this.onStreamConnect(timelineName, argument)
const closeHandler = () => const closeHandler = () =>
this.onStreamDisconnect(timelineName, argument) this.onStreamDisconnect(timelineName, argument)
const messageHandler = const messageHandler = (message) => {
() => this.onStreamMessage(timelineName, argument, message)
({ detail: message }) => }
this.onStreamMessage(timelineName, argument, message)
et.addEventListener('open', openHandler) et.addEventListener('open', openHandler)
et.addEventListener('close', closeHandler) et.addEventListener('close', closeHandler)
et.addEventListener('update', messageHandler) et.addEventListener('update', messageHandler)
timeline.socket = { timeline.socket = {
name: 'timelines',
stream: { stream: {
name: streamName, name: streamName,
argument, argument,
@ -133,7 +135,7 @@ export const useTimelinesStore = defineStore('timelines', {
deactivate(timelineName, persistent) { deactivate(timelineName, persistent) {
const timeline = this[timelineName] const timeline = this[timelineName]
if (timeline.persistent && !persistent) return if (timeline.persistent && !persistent) return
if (!timeline.streaming) { if (timeline.fetching) {
this.stopFetchingTimeline(timelineName, 'Timeline deactivation') this.stopFetchingTimeline(timelineName, 'Timeline deactivation')
} }
@ -177,6 +179,48 @@ export const useTimelinesStore = defineStore('timelines', {
}) })
}, },
// Pause
pause(name) {
const timeline = this[name]
timeline.paused = true
console.debug(
'[Timelines] Pausing timeline',
name,
)
if (timeline.fetcher && timeline.fetching) {
timeline.fetcher.stopFetching()
}
},
resume(name) {
const timeline = this[name]
timeline.paused = false
console.debug(
'[Timelines] Resuming timeline',
name,
)
if (timeline.fetcher && timeline.fetching) {
timeline.fetcher.startFetching()
}
},
pauseAll() {
TIMELINES.forEach((name) => {
try {
this.pause(name)
} catch (e) {
console.error(`[Timelines] Failed to pause timeline ${name}:`, e)
}
})
},
resumeAll() {
TIMELINES.forEach((name) => {
try {
this.resume(name)
} catch (e) {
console.error(`[Timelines] Failed to pause timeline ${name}:`, e)
}
})
},
// Update stuff // Update stuff
addStatusesToTimeline( addStatusesToTimeline(
timelineName, timelineName,
@ -231,7 +275,7 @@ export const useTimelinesStore = defineStore('timelines', {
}, },
onStreamMessage(timeline, argument, event) { onStreamMessage(timeline, argument, event) {
this.addStatusesToTimeline(timeline, argument, { this.addStatusesToTimeline(timeline, argument, {
statuses: [event.data.status.id], statuses: event.data.map(({ id }) => id),
}) })
}, },
@ -247,15 +291,28 @@ export const useTimelinesStore = defineStore('timelines', {
this.startFetchingTimeline(timeline, argument, 'Socket disconnected') this.startFetchingTimeline(timeline, argument, 'Socket disconnected')
}, },
startFetchingTimeline(timelineName, argument, reason) { startFetchingTimeline(timelineName, argument, reason) {
const timeline = this[timelineName]
console.log('[Timelines]', toValue(timeline))
if (timeline.paused) {
console.debug(
'[Timelines] NOT Starting timeline fetcher because it is paused',
timelineName,
argument,
'Original Reason:',
reason,
)
return
}
console.debug( console.debug(
'[Timelines] Starting fetching timeline', '[Timelines] Starting timeline fetcher',
timelineName, timelineName,
argument, argument,
'Reason:', 'Reason:',
reason, reason,
) )
const timeline = this[timelineName]
timeline.fetcher.startFetching() timeline.fetcher.startFetching()
timeline.fetching = true
}, },
stopFetchingTimeline(timelineName, reason) { stopFetchingTimeline(timelineName, reason) {
const timeline = this[timelineName] const timeline = this[timelineName]
@ -275,6 +332,7 @@ export const useTimelinesStore = defineStore('timelines', {
'Reason:', 'Reason:',
reason, reason,
) )
timeline.fetching = false
} }
}, },

View file

@ -2,6 +2,8 @@ import Cookies from 'js-cookie'
import { last } from 'lodash' import { last } from 'lodash'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { WSConnectionStatus } from 'src/api/websocket.js'
import { useAnnouncementsStore } from 'src/stores/announcements.js' import { useAnnouncementsStore } from 'src/stores/announcements.js'
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js' import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders.js'
import { useChatsStore } from 'src/stores/chats.js' import { useChatsStore } from 'src/stores/chats.js'
@ -615,12 +617,15 @@ export const useUsersStore = defineStore('users', {
user.domainMutes = new Set() user.domainMutes = new Set()
this.lastLoginName = user.screen_name this.lastLoginName = user.screen_name
this.currentUser = user useTimelinesStore().deactivateAll()
useStatusesStore().resetStatuses()
this.users = new Map() this.users = new Map()
this.usersByName = new Map() this.usersByName = new Map()
this.usersByURL = new Map() this.usersByURL = new Map()
this.relationships = new Map() this.relationships = new Map()
this.currentUser = user
this.addNewUsers({ data: user, ...rest })
useInterfaceStore().onLogin() useInterfaceStore().onLogin()
useSyncConfigStore() useSyncConfigStore()
@ -634,7 +639,6 @@ export const useUsersStore = defineStore('users', {
}) })
useUserHighlightStore().initUserHighlight(user) useUserHighlightStore().initUserHighlight(user)
this.addNewUsers({ data: user, ...rest })
useEmojiStore().fetchEmoji() useEmojiStore().fetchEmoji()
@ -660,8 +664,8 @@ export const useUsersStore = defineStore('users', {
} }
// DMs and Home // DMs and Home
useNotificationsStore().activate()
useTimelinesStore().activatePersistents() useTimelinesStore().activatePersistents()
useNotificationsStore().activate()
if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) { if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) {
// Start fetching chats // Start fetching chats
@ -708,6 +712,16 @@ export const useUsersStore = defineStore('users', {
const store = window.vuex const store = window.vuex
const oauth = useOAuthStore() const oauth = useOAuthStore()
// Pause fetching
useNotificationsStore().pause()
useTimelinesStore().pauseAll()
// Pause-less stores
useAnnouncementsStore().stopFetching()
useListsStore().stopFetching()
useBookmarkFoldersStore().stopFetching()
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,
// the token will be invalid too // the token will be invalid too
return oauth return oauth
@ -722,28 +736,53 @@ export const useUsersStore = defineStore('users', {
return revokeToken(params) return revokeToken(params)
}) })
.then(() => { .then(() => {
oauth.clearToken()
this.currentUser = null this.currentUser = null
this.lastLoginName = null this.lastLoginName = null
useNotificationsStore().deactivate()
// Full reset on logout success
useTimelinesStore().deactivateAll()
useStatusesStore().resetStatuses()
useChatsStore().resetChats()
this.users = new Map() this.users = new Map()
this.usersByName = new Map() this.usersByName = new Map()
this.usersByURL = new Map() this.usersByURL = new Map()
this.relationships = new Map() this.relationships = new Map()
useNotificationsStore().deactivate()
useAnnouncementsStore().stopFetching() // Socket is most likely already closed by server
useListsStore().stopFetching() if (
useBookmarkFoldersStore().stopFetching() useMergedConfigStore().mergedConfig.useStreamingApi
store?.dispatch('stopFetchingFollowRequests') && useStreamingStore().state !== WSConnectionStatus.CLOSED
useTimelinesStore().deactivateAll() ) {
useStatusesStore().resetStatuses()
if (useMergedConfigStore().mergedConfig.useStreamingApi) {
useStreamingStore().stopSocket() useStreamingStore().stopSocket()
} }
useChatsStore().resetChats()
oauth.clearToken()
Cookies.remove('__Host-pleroma_key', { path: '/' }) Cookies.remove('__Host-pleroma_key', { path: '/' })
useInterfaceStore().onLogout() useInterfaceStore().onLogout()
}) })
.catch((e) => {
useInterfaceStore().pushGlobalNotice({
messageKey: 'user.logout_failure',
messageArgs: {
error: e,
},
level: 'error',
})
console.error('Logout error!', e)
useAnnouncementsStore().startFetching()
useListsStore().startFetching()
useBookmarkFoldersStore().startFetching()
store?.dispatch('startFetchingFollowRequests')
})
.finally(() => {
useNotificationsStore().resume()
useTimelinesStore().resumeAll()
})
}, },
}, },
persist: { persist: {

View file

@ -354,13 +354,11 @@ describe('RichContent', () => {
'<span class="MentionLink mention-link">', '<span class="MentionLink mention-link">',
'<a href="lol" class="original" target="_blank">', '<a href="lol" class="original" target="_blank">',
'<span>', '<span>',
'<span>',
'https://</span>', 'https://</span>',
'<span>', '<span>',
'lol.tld/</span>', 'lol.tld/</span>',
'<span>', '<span>',
'</span>', '</span>',
'</span>',
'</a>', '</a>',
'</span>', '</span>',
'</span>', '</span>',
@ -420,25 +418,21 @@ describe('RichContent', () => {
'<span class="MentionLink mention-link">', '<span class="MentionLink mention-link">',
'<a href="lol" class="original" target="_blank">', '<a href="lol" class="original" target="_blank">',
'<span>', '<span>',
'<span>',
'https://</span>', 'https://</span>',
'<span>', '<span>',
'lol.tld/</span>', 'lol.tld/</span>',
'<span>', '<span>',
'</span>', '</span>',
'</span>',
'</a>', '</a>',
'</span>', '</span>',
'<span class="MentionLink mention-link">', '<span class="MentionLink mention-link">',
'<a href="lol" class="original" target="_blank">', '<a href="lol" class="original" target="_blank">',
'<span>', '<span>',
'<span>',
'https://</span>', 'https://</span>',
'<span>', '<span>',
'lol.tld/</span>', 'lol.tld/</span>',
'<span>', '<span>',
'</span>', '</span>',
'</span>',
'</a>', '</a>',
'</span>', '</span>',
'</span>', '</span>',