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:
parent
eadaa72c27
commit
22b4c5128b
|
@ -3,6 +3,6 @@ import { RootState } from '~/store'
|
|||
|
||||
declare module '@vue/runtime-core' {
|
||||
interface ComponentCustomProperties {
|
||||
$store: Store<RootState>
|
||||
store: Store<RootState>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
// ...
|
Loading…
Reference in New Issue