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' {
|
declare module '@vue/runtime-core' {
|
||||||
interface ComponentCustomProperties {
|
interface ComponentCustomProperties {
|
||||||
$store: Store<RootState>
|
store: Store<RootState>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { Module } from 'vuex'
|
import type { Module } from 'vuex'
|
||||||
import type { RootState } from '~/store/index'
|
import type { RootState } from '~/store/index'
|
||||||
|
import type { components } from '~/generated/types'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { merge } from 'lodash-es'
|
import { merge } from 'lodash-es'
|
||||||
|
@ -11,84 +12,80 @@ export interface State {
|
||||||
frontSettings: FrontendSettings
|
frontSettings: FrontendSettings
|
||||||
instanceUrl?: string
|
instanceUrl?: string
|
||||||
knownInstances: string[]
|
knownInstances: string[]
|
||||||
nodeinfo: NodeInfo | null
|
nodeinfo: components['schemas']['NodeInfo21'] | null
|
||||||
settings: Settings
|
settings: Settings
|
||||||
}
|
}
|
||||||
|
|
||||||
type TotalCount = {
|
// export interface NodeInfo {
|
||||||
total: number
|
// version: string;
|
||||||
}
|
// software: {
|
||||||
|
// name: string;
|
||||||
export interface NodeInfo {
|
// version: string;
|
||||||
version: string;
|
// }
|
||||||
software: {
|
// protocols: any[];
|
||||||
name: string;
|
// services?: {
|
||||||
version: string;
|
// inbound?: string[];
|
||||||
}
|
// outbound?: string[];
|
||||||
protocols: any[];
|
// }
|
||||||
services?: {
|
// openRegistrations: boolean;
|
||||||
inbound?: string[];
|
// usage: {
|
||||||
outbound?: string[];
|
// users: {
|
||||||
}
|
// total: number;
|
||||||
openRegistrations: boolean;
|
// activeHalfyear: number;
|
||||||
usage: {
|
// activeMonth: number;
|
||||||
users: {
|
// }
|
||||||
total: number;
|
// }
|
||||||
activeHalfyear: number;
|
// metadata: {
|
||||||
activeMonth: number;
|
// actorId: string
|
||||||
}
|
// 'private': boolean
|
||||||
}
|
// shortDescription: string
|
||||||
metadata: {
|
// longDescription: string
|
||||||
actorId: string
|
// rules: string
|
||||||
'private': boolean
|
// contactEmail: string
|
||||||
shortDescription: string
|
// terms: string
|
||||||
longDescription: string
|
// nodeName: string
|
||||||
rules: string
|
// banner: string
|
||||||
contactEmail: string
|
// defaultUploadQuota: number
|
||||||
terms: string
|
// content: {
|
||||||
nodeName: string
|
// federationEnabled: boolean
|
||||||
banner: string
|
// anonymousCanListen: boolean
|
||||||
defaultUploadQuota: number
|
// local: {
|
||||||
content: {
|
// tracks?: TotalCount
|
||||||
federationEnabled: boolean
|
// artists?: TotalCount
|
||||||
anonymousCanListen: boolean
|
// albums?: TotalCount
|
||||||
local: {
|
// hoursOfContent?: number }
|
||||||
tracks?: TotalCount
|
// }
|
||||||
artists?: TotalCount
|
// topMusicCategories: []
|
||||||
albums?: TotalCount
|
// topPodcastCategories: []
|
||||||
hoursOfContent?: number }
|
// federation: {
|
||||||
}
|
// followedInstances: number
|
||||||
topMusicCategories: []
|
// followingInstances: number
|
||||||
topPodcastCategories: []
|
// }
|
||||||
federation: {
|
// supportedUploadExtensions: string[]
|
||||||
followedInstances: number
|
// allowList: {
|
||||||
followingInstances: number
|
// enabled: boolean
|
||||||
}
|
// domains: string[]
|
||||||
supportedUploadExtensions: string[]
|
// }
|
||||||
allowList: {
|
// reportTypes: {
|
||||||
enabled: boolean
|
// 'type': string
|
||||||
domains: string[]
|
// label: string
|
||||||
}
|
// anonymous: boolean
|
||||||
reportTypes: {
|
// }[]
|
||||||
'type': string
|
// funkwhaleSupportMessageEnabled: boolean
|
||||||
label: string
|
// instanceSupportMessage: string
|
||||||
anonymous: boolean
|
// endpoints: {
|
||||||
}[]
|
// knownNodes?: string
|
||||||
funkwhaleSupportMessageEnabled: boolean
|
// channels?: string
|
||||||
instanceSupportMessage: string
|
// libraries?: string
|
||||||
endpoints: {
|
// }
|
||||||
knownNodes?: string
|
// usage: {
|
||||||
channels?: string
|
// favorites: { tracks: TotalCount }
|
||||||
libraries?: string
|
// listenings: TotalCount
|
||||||
}
|
// downloads: TotalCount
|
||||||
usage: {
|
// }
|
||||||
favorites: { tracks: TotalCount }
|
// features:[]
|
||||||
listenings: TotalCount
|
// }
|
||||||
downloads: TotalCount
|
// }
|
||||||
}
|
|
||||||
features:[]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FrontendSettings {
|
interface FrontendSettings {
|
||||||
defaultServerUrl: string
|
defaultServerUrl: string
|
||||||
|
|
|
@ -31,7 +31,9 @@ export interface CurrentRadio {
|
||||||
objectId: ObjectId | null
|
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()
|
const logger = useLogger()
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import type { Module } from 'vuex'
|
import type { Module } from 'vuex'
|
||||||
import type { RootState } from '~/store/index'
|
import type { RootState } from '~/store/index'
|
||||||
import type { SUPPORTED_LOCALES } from '~/init/locale'
|
import type { SUPPORTED_LOCALES } from '~/init/locale'
|
||||||
|
import type { Channel } from '~/types'
|
||||||
|
import type { components } from '~/generated/types'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
@ -32,6 +34,8 @@ interface Message {
|
||||||
|
|
||||||
type NotificationsKey = 'inbox' | 'pendingReviewEdits' | 'pendingReviewReports' | 'pendingReviewRequests'
|
type NotificationsKey = 'inbox' | 'pendingReviewEdits' | 'pendingReviewReports' | 'pendingReviewRequests'
|
||||||
|
|
||||||
|
type IsOpen = 'true' | 'undefined'
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
currentLanguage: 'en_US' | keyof typeof SUPPORTED_LOCALES
|
currentLanguage: 'en_US' | keyof typeof SUPPORTED_LOCALES
|
||||||
selectedLanguage: boolean
|
selectedLanguage: boolean
|
||||||
|
@ -47,9 +51,13 @@ export interface State {
|
||||||
width: number
|
width: number
|
||||||
}
|
}
|
||||||
pageTitle: null
|
pageTitle: null
|
||||||
|
modalsOpen: Set<string>
|
||||||
|
|
||||||
notifications: Record<NotificationsKey, number>
|
notifications: Record<NotificationsKey, number>
|
||||||
websocketEventsHandlers: Record<WebSocketEventName, WebSocketHandlers>
|
websocketEventsHandlers: Record<WebSocketEventName, WebSocketHandlers>
|
||||||
|
preselectedChannelForUpload: null | [Channel, 'podcast' | 'music']
|
||||||
|
|
||||||
|
tags: null | components['schemas']['Tag'][]
|
||||||
}
|
}
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
|
@ -85,7 +93,11 @@ const store: Module<State, RootState> = {
|
||||||
'user_request.created': {},
|
'user_request.created': {},
|
||||||
Listen: {}
|
Listen: {}
|
||||||
},
|
},
|
||||||
pageTitle: null
|
pageTitle: null,
|
||||||
|
modalsOpen: new Set([]),
|
||||||
|
preselectedChannelForUpload: null,
|
||||||
|
|
||||||
|
tags: null
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
showInstanceSupportMessage: (state, getters, rootState) => {
|
showInstanceSupportMessage: (state, getters, rootState) => {
|
||||||
|
@ -149,7 +161,9 @@ const store: Module<State, RootState> = {
|
||||||
} else {
|
} else {
|
||||||
return 'large'
|
return 'large'
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
modalIsOpen: (state, key) =>
|
||||||
|
state.modalsOpen.has(key)
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
addWebsocketEventHandler: (state, { eventName, id, handler }: { eventName: WebSocketEventName, id: string, handler: (event: any) => void}) => {
|
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) {
|
removeMessage (state, key) {
|
||||||
state.messages.splice(state.messages.findIndex(message => message.key === key), 1)
|
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 }) {
|
notifications (state, { type, count }: { type: NotificationsKey, count: number }) {
|
||||||
state.notifications[type] = count
|
state.notifications[type] = count
|
||||||
},
|
},
|
||||||
|
@ -205,6 +233,9 @@ const store: Module<State, RootState> = {
|
||||||
},
|
},
|
||||||
window: (state, value) => {
|
window: (state, value) => {
|
||||||
state.window = value
|
state.window = value
|
||||||
|
},
|
||||||
|
tags: (state, value) => {
|
||||||
|
state.tags = value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
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