Refactor(front): [WIP] use store to cache and distribute user state

Co-Authored-By: ArneBo <arne@ecobasa.org>
Co-Authored-By: Flupsi <upsiflu@gmail.com>
Co-Authored-By: jon r <jon@allmende.io>
This commit is contained in:
jon r 2025-04-18 11:14:02 +02:00
parent eadaa72c27
commit 22b4c5128b
5 changed files with 204 additions and 79 deletions

View File

@ -3,6 +3,6 @@ import { RootState } from '~/store'
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$store: Store<RootState>
store: Store<RootState>
}
}

View File

@ -1,5 +1,6 @@
import type { Module } from 'vuex'
import type { RootState } from '~/store/index'
import type { components } from '~/generated/types'
import axios from 'axios'
import { merge } from 'lodash-es'
@ -11,84 +12,80 @@ export interface State {
frontSettings: FrontendSettings
instanceUrl?: string
knownInstances: string[]
nodeinfo: NodeInfo | null
nodeinfo: components['schemas']['NodeInfo21'] | null
settings: Settings
}
type TotalCount = {
total: number
}
export interface NodeInfo {
version: string;
software: {
name: string;
version: string;
}
protocols: any[];
services?: {
inbound?: string[];
outbound?: string[];
}
openRegistrations: boolean;
usage: {
users: {
total: number;
activeHalfyear: number;
activeMonth: number;
}
}
metadata: {
actorId: string
'private': boolean
shortDescription: string
longDescription: string
rules: string
contactEmail: string
terms: string
nodeName: string
banner: string
defaultUploadQuota: number
content: {
federationEnabled: boolean
anonymousCanListen: boolean
local: {
tracks?: TotalCount
artists?: TotalCount
albums?: TotalCount
hoursOfContent?: number }
}
topMusicCategories: []
topPodcastCategories: []
federation: {
followedInstances: number
followingInstances: number
}
supportedUploadExtensions: string[]
allowList: {
enabled: boolean
domains: string[]
}
reportTypes: {
'type': string
label: string
anonymous: boolean
}[]
funkwhaleSupportMessageEnabled: boolean
instanceSupportMessage: string
endpoints: {
knownNodes?: string
channels?: string
libraries?: string
}
usage: {
favorites: { tracks: TotalCount }
listenings: TotalCount
downloads: TotalCount
}
features:[]
}
}
// export interface NodeInfo {
// version: string;
// software: {
// name: string;
// version: string;
// }
// protocols: any[];
// services?: {
// inbound?: string[];
// outbound?: string[];
// }
// openRegistrations: boolean;
// usage: {
// users: {
// total: number;
// activeHalfyear: number;
// activeMonth: number;
// }
// }
// metadata: {
// actorId: string
// 'private': boolean
// shortDescription: string
// longDescription: string
// rules: string
// contactEmail: string
// terms: string
// nodeName: string
// banner: string
// defaultUploadQuota: number
// content: {
// federationEnabled: boolean
// anonymousCanListen: boolean
// local: {
// tracks?: TotalCount
// artists?: TotalCount
// albums?: TotalCount
// hoursOfContent?: number }
// }
// topMusicCategories: []
// topPodcastCategories: []
// federation: {
// followedInstances: number
// followingInstances: number
// }
// supportedUploadExtensions: string[]
// allowList: {
// enabled: boolean
// domains: string[]
// }
// reportTypes: {
// 'type': string
// label: string
// anonymous: boolean
// }[]
// funkwhaleSupportMessageEnabled: boolean
// instanceSupportMessage: string
// endpoints: {
// knownNodes?: string
// channels?: string
// libraries?: string
// }
// usage: {
// favorites: { tracks: TotalCount }
// listenings: TotalCount
// downloads: TotalCount
// }
// features:[]
// }
// }
interface FrontendSettings {
defaultServerUrl: string

View File

@ -31,7 +31,9 @@ export interface CurrentRadio {
objectId: ObjectId | null
}
export type RadioConfig = { type: 'tag', names: string[] } | { type: 'artist' | 'playlist', ids: string[] }
export type RadioConfig
= { type: 'tag', names: string[] }
| { type: 'artist' | 'playlist', ids: string[] }
const logger = useLogger()

View File

@ -1,6 +1,8 @@
import type { Module } from 'vuex'
import type { RootState } from '~/store/index'
import type { SUPPORTED_LOCALES } from '~/init/locale'
import type { Channel } from '~/types'
import type { components } from '~/generated/types'
import axios from 'axios'
import moment from 'moment'
@ -32,6 +34,8 @@ interface Message {
type NotificationsKey = 'inbox' | 'pendingReviewEdits' | 'pendingReviewReports' | 'pendingReviewRequests'
type IsOpen = 'true' | 'undefined'
export interface State {
currentLanguage: 'en_US' | keyof typeof SUPPORTED_LOCALES
selectedLanguage: boolean
@ -47,9 +51,13 @@ export interface State {
width: number
}
pageTitle: null
modalsOpen: Set<string>
notifications: Record<NotificationsKey, number>
websocketEventsHandlers: Record<WebSocketEventName, WebSocketHandlers>
preselectedChannelForUpload: null | [Channel, 'podcast' | 'music']
tags: null | components['schemas']['Tag'][]
}
const logger = useLogger()
@ -85,7 +93,11 @@ const store: Module<State, RootState> = {
'user_request.created': {},
Listen: {}
},
pageTitle: null
pageTitle: null,
modalsOpen: new Set([]),
preselectedChannelForUpload: null,
tags: null
},
getters: {
showInstanceSupportMessage: (state, getters, rootState) => {
@ -149,7 +161,9 @@ const store: Module<State, RootState> = {
} else {
return 'large'
}
}
},
modalIsOpen: (state, key) =>
state.modalsOpen.has(key)
},
mutations: {
addWebsocketEventHandler: (state, { eventName, id, handler }: { eventName: WebSocketEventName, id: string, handler: (event: any) => void}) => {
@ -190,6 +204,20 @@ const store: Module<State, RootState> = {
removeMessage (state, key) {
state.messages.splice(state.messages.findIndex(message => message.key === key), 1)
},
addModal (state, key) {
state.modalsOpen.add(key)
},
removeModal (state, key) {
state.modalsOpen.delete(key)
},
toggleModal (state, key) {
state.modalsOpen.has(key) ? state.modalsOpen.delete(key) : state.modalsOpen.add(key)
},
setModal (state, [key, isOpen]:[string, IsOpen]) {
isOpen ? state.modalsOpen.add(key) : state.modalsOpen.delete(key)
},
notifications (state, { type, count }: { type: NotificationsKey, count: number }) {
state.notifications[type] = count
},
@ -205,6 +233,9 @@ const store: Module<State, RootState> = {
},
window: (state, value) => {
state.window = value
},
tags: (state, value) => {
state.tags = value
}
},
actions: {

View File

@ -0,0 +1,95 @@
import type { paths } from '~/generated/types.ts'
import { computed, ref, type Ref } from 'vue'
import { useStore } from '~/store'
import axios from 'axios'
export type Item = { type: 'custom' | 'preset', label: string }
export type Model = { currents: Item[], others?: Item[] }
/**
* Load and cache all tags.
* - Two-way binding with store (any change to the store will be reflected in `others`)
* - Two-way binding with ref
* @param currents Selected tags
* @returns an object with `currents` and `others`, ready to be used inside
*/
export const useTags = (currents: Ref<string[], string[]>) => {
const store = useStore()
// Ignore quick successive fetch triggers
const ignorePeriod = 500
// Wait between consecutiv fetches
const waitInterval = 3000
// Wait after changing a tag before re-fetching
const refetchInterval = 6000
// Number of tags to load on one page
const maxTags = 100000
const lastFetched = ref<number>(0)
const fetch = async () => {
// console.log('FETCH TAGS')
// Ignore subsequent fetch commands triggered in quick succession
if (lastFetched.value + ignorePeriod > Date.now())
return
// Always wait some milliseconds before re-fetching
if (lastFetched.value + waitInterval > Date.now()) {
window.setTimeout(fetch, lastFetched.value + waitInterval - Date.now())
return
}
const response = await axios.get<paths['/api/v2/tags/']['get']['responses']['200']['content']['application/json']>(
'/tags',
{ params: { page: 1, page_size: maxTags } }
)
// console.log('TAGS RESPONSE.data.results', response.data.results)
store.commit('ui/tags', response.data.results)
}
fetch();
/**
* @returns v-model for `Pills` component
*/
return computed({
get () {
// console.log("GET TAGS")
return ({
// Get `currents` from parameter
currents: currents.value.map(tag => ({
label: tag,
type: (store.state.ui.tags || []).some(({ name }) => tag === name) ? 'preset' : 'custom'
} as const)),
// Get `others` from cache
others: (store.state.ui.tags || [])
.filter(({ name }) => !currents.value.includes(name))
.map(({ name }) => ({
label: name,
type: 'preset'
} as const))
})},
set (model) {
// console.log('SET TAGS', response.data.results)
// Set parameter `currents` from `model.currents`
currents.value = model.currents.map(({ label }) => label)
// Set runtime-only options from `model.others` and `model.current`
// TODO: Broadcast new custom tags so that other pills components can use them
// Re-fetch after each new setting
window.setTimeout(fetch, refetchInterval)
}
})
}
// alternative ways to generate tags
// ...