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' {
 | 
			
		||||
  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