Compare commits

...

13 commits

Author SHA1 Message Date
Henry Jameson
c91e52f18f Merge branch 'users-statuses-pinia' into shigusegubu-themes3 2026-08-26 18:58:20 +03:00
Henry Jameson
18bdc84200 lint 2026-08-26 18:49:23 +03:00
Henry Jameson
9676f95cf4 remove persist plugin from users 2026-08-26 18:48:36 +03:00
Henry Jameson
713eb828c8 fix chats fetcher again 2026-08-26 18:38:33 +03:00
Henry Jameson
7c46ba462f documentation 2026-08-26 18:35:10 +03:00
Henry Jameson
4e1d6f704d even more login/logout woes! 2026-08-26 18:24:22 +03:00
Henry Jameson
a44b60dbc8 fix reports 2026-08-26 18:12:54 +03:00
Henry Jameson
b112bf11c2 fix reply visibility timelines 2026-08-26 18:07:12 +03:00
Henry Jameson
f46575add2 better fix 2026-08-26 18:05:15 +03:00
Henry Jameson
a4b02936ac fix chat periodic notification 2026-08-26 17:50:14 +03:00
Henry Jameson
d9f03f61ae fix chats fetcher 2026-08-26 17:43:48 +03:00
Henry Jameson
ae36bcbb4e fix admin store 2026-08-26 17:40:34 +03:00
Henry Jameson
82d6b66386 fix plugin early exit 2026-08-26 17:39:37 +03:00
11 changed files with 82 additions and 22 deletions

View file

@ -37,9 +37,7 @@ export const piniaPushNotificationsPlugin = ({ store }) => {
if (store.$id === 'interface') { if (store.$id === 'interface') {
if (actionName === 'setNotificationPermission') { if (actionName === 'setNotificationPermission') {
permissionGranted = args[0] === 'granted' permissionGranted = args[0] === 'granted'
} else if (actionName === 'setLoginStatus') { } else if (actionName !== 'onLogin' && actionName !== 'onLogout') {
user = args[0]
} else {
return return
} }
} else if (store.$id === 'sync_config') { } else if (store.$id === 'sync_config') {

View file

@ -3,8 +3,11 @@ import { showDesktopNotification } from '../desktop_notification_utils/desktop_n
import { useUsersStore } from 'src/stores/users.js' import { useUsersStore } from 'src/stores/users.js'
export const maybeShowChatNotification = (chat) => { export const maybeShowChatNotification = (chat) => {
// No messages
if (!chat.lastMessage) return if (!chat.lastMessage) return
// No unreads to display
if (chat.unread === 0) return if (chat.unread === 0) return
// Don't notify on outgoing message (shouldn't happen with condition above)
if (useUsersStore().currentUser.id === chat.lastMessage.account_id) return if (useUsersStore().currentUser.id === chat.lastMessage.account_id) return
const opts = { const opts = {

View file

@ -430,7 +430,7 @@ export const useAdminSettingsStore = defineStore('adminSettings', {
}) })
resultUserIds.data.forEach((userId) => { resultUserIds.data.forEach((userId) => {
useStatusesStore().wipeUserStatuses(status.user.id) useStatusesStore().wipeUserStatuses(userId)
// Users are technically never deleted, just deactivated // Users are technically never deleted, just deactivated
// so there's no real need to delete them from store. // so there's no real need to delete them from store.
}) })

View file

@ -40,8 +40,8 @@ export const useChatsStore = defineStore('chats', {
useStreamingStore().addSubscriber(socket) useStreamingStore().addSubscriber(socket)
}, },
startFetching() { startFetching() {
this.fetcher = () => promiseInterval(() => this.fetchChats(), 5000) this.fetcher = promiseInterval(() => this.fetchChats(), 5000)
this.fetcher() this.fetchChats()
}, },
stopFetching() { stopFetching() {
this.fetcher?.stop() this.fetcher?.stop()
@ -56,8 +56,6 @@ export const useChatsStore = defineStore('chats', {
}, },
resetChats() { resetChats() {
this.data = new Map() this.data = new Map()
this.stopFetching()
this.startFetching()
}, },
addNewChats(result) { addNewChats(result) {
useUsersStore().addNewUsers({ useUsersStore().addNewUsers({
@ -79,13 +77,15 @@ 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
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) maybeShowChatNotification(chat)
} else { } else {
this.data.set(updatedChat.id, updatedChat) this.data.set(updatedChat.id, updatedChat)
maybeShowChatNotification(updatedChat)
} }
maybeShowChatNotification(chat ?? updatedChat)
}, },
deleteChat(id) { deleteChat(id) {
this.data.delete(id) this.data.delete(id)

View file

@ -46,7 +46,7 @@ const timelineFetcher = (timeline, argument, credentials) => {
} }
args.withMuted = !hideMutedPosts args.withMuted = !hideMutedPosts
if (loggedIn && REPLY_VISIBILITY_TIMELINES.has(timeline)) { if (loggedIn && REPLY_VISIBILITY_TIMELINES.has(timeline.name)) {
args.replyVisibility = replyVisibility args.replyVisibility = replyVisibility
} }

View file

@ -22,17 +22,41 @@ import {
import { isStatusNotification } from 'src/services/notification_utils/notification_utils_sw.js' import { isStatusNotification } from 'src/services/notification_utils/notification_utils_sw.js'
export const defaultState = () => ({ export const defaultState = () => ({
// Prevents desktop notification spam on startup
desktopNotificationSilence: true, desktopNotificationSilence: true,
// Pagination
maxId: '', maxId: '',
minId: '', minId: '',
// Order
data: [], data: [],
// TODO: Implement!
// Useful for making notification as seen
// when interacting with status
statusNotificationRelations: new WeakMap(), statusNotificationRelations: new WeakMap(),
// ID to Object notification
idStore: new Map(), idStore: new Map(),
statusIdStore: new Set(),
// Reference to WS subscriber
socket: null, socket: null,
// Indicates whether notifications receive push updates
streaming: false, streaming: false,
// Indicates whether notifications are SUPPOSED to be fetching
// this is partiualrly useful for when pausing/resuming. I.e.
// whether we need to start fetching again if timeline was resumed.
fetching: true, fetching: true,
// Reference to fetcher, used for polling for new notifications
// and manually fetching old notifications
fetcher: null, fetcher: null,
// Whether notifications fetcher has been paused - it stops fetching
// (but still receives pushes!)
paused: false, paused: false,
}) })

View file

@ -19,8 +19,10 @@ export const useReportsStore = defineStore('reports', {
actions: { actions: {
openUserReportingModal({ userId, statusIds = [] }) { openUserReportingModal({ userId, statusIds = [] }) {
const preTickedIds = new Set(statusIds) const preTickedIds = new Set(statusIds)
// There shouldn't be a case where this is undefined // There could be a case (i.e. user is only ever mentioned in someone else's post -> user popover)
const userAllStatusesIds = useStatusesStore().statusesPerUser.get(userId) // where user has no known posts
const userAllStatusesIds =
useStatusesStore().statusesPerUser.get(userId) ?? new Set()
// Set constructor should take care of duplicated IDs and order, // Set constructor should take care of duplicated IDs and order,
// later duplicated IDs will be dropped in favor of earlier // later duplicated IDs will be dropped in favor of earlier
const sortedIds = new Set([...preTickedIds, ...userAllStatusesIds]) const sortedIds = new Set([...preTickedIds, ...userAllStatusesIds])

View file

@ -534,7 +534,7 @@ export const useStatusesStore = defineStore('statuses', {
// For when blocking a user // For when blocking a user
wipeUserStatuses(userId) { wipeUserStatuses(userId) {
const removed = this.statusesPerUser.get(userId) const removed = this.statusesPerUser.get(userId) ?? new Set()
removed.forEach((statusId) => { removed.forEach((statusId) => {
const status = this.allStatuses.get(statusId) const status = this.allStatuses.get(statusId)
this.allStatuses.delete(statusId) this.allStatuses.delete(statusId)

View file

@ -8,18 +8,49 @@ import { TIMELINE_STREAM_MAP, useStreamingStore } from 'src/stores/streaming.js'
const emptyTl = (name, argument = null) => { const emptyTl = (name, argument = null) => {
const result = { const result = {
// Name of the timeline. Useful for debugging and logging
name, name,
// Order of statuses, important for timelines that
// have different ordering, i.e. bookmarks and favorites
order: [], order: [],
// All statuses belonging to the timeline
statusIds: new Set(), statusIds: new Set(),
// Statuses shown to user
visibleStatusIds: new Set(), visibleStatusIds: new Set(),
// Number of statuses not shown yet
newStatusCount: 0, newStatusCount: 0,
// Pagination
maxId: '', maxId: '',
minId: '', minId: '',
// Indicates whether timeline receives push updates
streaming: false, streaming: false,
// Indicates whether timeline is SUPPOSED to be fetching
// this is partiualrly useful for when pausing/resuming
// timeline. I.e. whether we need to start fetching again
// if timeline was resumed.
fetching: false, fetching: false,
// Indicates that in recent poll update we've hit more than or
// equal to 20 statuses and most likely missed some statuses
// between polls
reloadNeeded: false, reloadNeeded: false,
// Reference to fetcher, used for polling for new statuses and
// manually fetching old statuses
fetcher: null, fetcher: null,
// Reference to WS subscriber
socket: null, socket: null,
// Whether the timeline has been paused - it stops fetching
// (but still receives pushes!)
paused: false, paused: false,
} }
@ -316,6 +347,14 @@ export const useTimelinesStore = defineStore('timelines', {
reason, reason,
) )
return return
} else if (timeline.paused) {
console.debug(
'[Timelines] Deactivating paused timeline',
timelineName,
'Reason:',
reason,
)
timeline.fetching = false
} else { } else {
timeline.fetcher.stopFetching() timeline.fetcher.stopFetching()
console.debug( console.debug(

View file

@ -50,7 +50,6 @@ import { promiseInterval } from 'src/services/promise_interval/promise_interval.
export const useUsersStore = defineStore('users', { export const useUsersStore = defineStore('users', {
state: () => ({ state: () => ({
loggingIn: false, loggingIn: false,
lastLoginName: null,
currentUser: null, currentUser: null,
users: new Map(), users: new Map(),
usersByName: new Map(), usersByName: new Map(),
@ -616,7 +615,6 @@ export const useUsersStore = defineStore('users', {
user.muteIds = new Set() user.muteIds = new Set()
user.domainMutes = new Set() user.domainMutes = new Set()
this.lastLoginName = user.screen_name
useTimelinesStore().deactivateAll() useTimelinesStore().deactivateAll()
useStatusesStore().resetStatuses() useStatusesStore().resetStatuses()
@ -739,13 +737,13 @@ export const useUsersStore = defineStore('users', {
oauth.clearToken() oauth.clearToken()
this.currentUser = null this.currentUser = null
this.lastLoginName = null
useNotificationsStore().deactivate() useNotificationsStore().deactivate()
// 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()
@ -777,6 +775,7 @@ export const useUsersStore = defineStore('users', {
useAnnouncementsStore().startFetching() useAnnouncementsStore().startFetching()
useListsStore().startFetching() useListsStore().startFetching()
useBookmarkFoldersStore().startFetching() useBookmarkFoldersStore().startFetching()
useChatsStore().startFetching()
store?.dispatch('startFetchingFollowRequests') store?.dispatch('startFetchingFollowRequests')
}) })
.finally(() => { .finally(() => {
@ -785,7 +784,4 @@ export const useUsersStore = defineStore('users', {
}) })
}, },
}, },
persist: {
paths: ['lastLoginName'],
},
}) })

View file

@ -679,7 +679,6 @@ describe('Users store', () => {
expect(store.usersByName).to.have.length(1) expect(store.usersByName).to.have.length(1)
expect(store.usersByURL).to.have.length(1) expect(store.usersByURL).to.have.length(1)
expect(store.relationships).to.have.length(0) expect(store.relationships).to.have.length(0)
expect(store.lastLoginName).to.eql(userScreenName)
spies.forEach((spy, index) => { spies.forEach((spy, index) => {
expect(spy, `Spy ${index} has failed`).to.have.been.called expect(spy, `Spy ${index} has failed`).to.have.been.called
}) })
@ -783,7 +782,6 @@ describe('Users store', () => {
expect(store.loggedIn).to.eql(true) expect(store.loggedIn).to.eql(true)
await store.logout() await store.logout()
expect(store.loggedIn).to.eql(false) expect(store.loggedIn).to.eql(false)
expect(store.lastLoginName).to.eql(null)
expect(revokeApi).to.have.been.called expect(revokeApi).to.have.been.called
expect(store.users).to.have.length(0) expect(store.users).to.have.length(0)
expect(store.usersByName).to.have.length(0) expect(store.usersByName).to.have.length(0)