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
							
								
									bd76874779
								
							
						
					
					
						commit
						e2720007be
					
				| 
						 | 
					@ -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