mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-29 01:19:28 +01:00
reimpl all music state into zustand
This commit is contained in:
parent
300d0bd1b9
commit
33dc0be02b
@ -1,38 +1,13 @@
|
|||||||
import {
|
import { selectMusic } from '@app/state/music'
|
||||||
mapAlbumID3toAlbumListItem,
|
|
||||||
mapArtistID3toArtist,
|
|
||||||
mapChildToSong,
|
|
||||||
mapPlaylistListItem,
|
|
||||||
} from '@app/models/music'
|
|
||||||
import {
|
|
||||||
albumListAtom,
|
|
||||||
albumListUpdatingAtom,
|
|
||||||
artistsAtom,
|
|
||||||
artistsUpdatingAtom,
|
|
||||||
homeListsUpdatingAtom,
|
|
||||||
homeListsWriteAtom,
|
|
||||||
playlistsAtom,
|
|
||||||
playlistsUpdatingAtom,
|
|
||||||
searchResultsAtom,
|
|
||||||
searchResultsUpdatingAtom,
|
|
||||||
} from '@app/state/music'
|
|
||||||
import { selectSettings } from '@app/state/settings'
|
import { selectSettings } from '@app/state/settings'
|
||||||
import { Store, useStore } from '@app/state/store'
|
import { Store, useStore } from '@app/state/store'
|
||||||
import { SubsonicApiClient } from '@app/subsonic/api'
|
import { SubsonicApiClient } from '@app/subsonic/api'
|
||||||
import { GetAlbumList2Type, GetCoverArtParams } from '@app/subsonic/params'
|
import { GetCoverArtParams } from '@app/subsonic/params'
|
||||||
import { useAtom } from 'jotai'
|
|
||||||
import { useUpdateAtom } from 'jotai/utils'
|
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
const selectors = {
|
|
||||||
fetchArtistInfo: (state: Store) => state.fetchArtistInfo,
|
|
||||||
fetchAlbum: (state: Store) => state.fetchAlbum,
|
|
||||||
fetchPlaylist: (state: Store) => state.fetchPlaylist,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useArtistInfo = (id: string) => {
|
export const useArtistInfo = (id: string) => {
|
||||||
const artistInfo = useStore(useCallback((state: Store) => state.artistInfo[id], [id]))
|
const artistInfo = useStore(useCallback((state: Store) => state.artistInfo[id], [id]))
|
||||||
const fetchArtistInfo = useStore(selectors.fetchArtistInfo)
|
const fetchArtistInfo = useStore(selectMusic.fetchArtistInfo)
|
||||||
|
|
||||||
if (!artistInfo) {
|
if (!artistInfo) {
|
||||||
fetchArtistInfo(id)
|
fetchArtistInfo(id)
|
||||||
@ -42,8 +17,8 @@ export const useArtistInfo = (id: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useAlbumWithSongs = (id: string) => {
|
export const useAlbumWithSongs = (id: string) => {
|
||||||
const album = useStore(useCallback((state: Store) => state.albums[id], [id]))
|
const album = useStore(useCallback((state: Store) => state.albumsWithSongs[id], [id]))
|
||||||
const fetchAlbum = useStore(selectors.fetchAlbum)
|
const fetchAlbum = useStore(selectMusic.fetchAlbumWithSongs)
|
||||||
|
|
||||||
if (!album) {
|
if (!album) {
|
||||||
fetchAlbum(id)
|
fetchAlbum(id)
|
||||||
@ -53,8 +28,8 @@ export const useAlbumWithSongs = (id: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const usePlaylistWithSongs = (id: string) => {
|
export const usePlaylistWithSongs = (id: string) => {
|
||||||
const playlist = useStore(useCallback((state: Store) => state.playlists[id], [id]))
|
const playlist = useStore(useCallback((state: Store) => state.playlistsWithSongs[id], [id]))
|
||||||
const fetchPlaylist = useStore(selectors.fetchPlaylist)
|
const fetchPlaylist = useStore(selectMusic.fetchPlaylistWithSongs)
|
||||||
|
|
||||||
if (!playlist) {
|
if (!playlist) {
|
||||||
fetchPlaylist(id)
|
fetchPlaylist(id)
|
||||||
@ -63,148 +38,6 @@ export const usePlaylistWithSongs = (id: string) => {
|
|||||||
return playlist
|
return playlist
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUpdateArtists = () => {
|
|
||||||
const server = useStore(selectSettings.activeServer)
|
|
||||||
const [updating, setUpdating] = useAtom(artistsUpdatingAtom)
|
|
||||||
const setArtists = useUpdateAtom(artistsAtom)
|
|
||||||
|
|
||||||
if (!server) {
|
|
||||||
return () => Promise.resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
return async () => {
|
|
||||||
if (updating) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setUpdating(true)
|
|
||||||
|
|
||||||
const client = new SubsonicApiClient(server)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await client.getArtists()
|
|
||||||
setArtists(response.data.artists.map(mapArtistID3toArtist))
|
|
||||||
} finally {
|
|
||||||
setUpdating(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useUpdateHomeLists = () => {
|
|
||||||
const server = useStore(selectSettings.activeServer)
|
|
||||||
const types = useStore(selectSettings.homeLists)
|
|
||||||
const updateHomeList = useUpdateAtom(homeListsWriteAtom)
|
|
||||||
const [updating, setUpdating] = useAtom(homeListsUpdatingAtom)
|
|
||||||
|
|
||||||
if (!server) {
|
|
||||||
return async () => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return async () => {
|
|
||||||
if (updating) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setUpdating(true)
|
|
||||||
|
|
||||||
const client = new SubsonicApiClient(server)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const promises: Promise<any>[] = []
|
|
||||||
for (const type of types) {
|
|
||||||
promises.push(
|
|
||||||
client.getAlbumList2({ type: type as GetAlbumList2Type, size: 20 }).then(response => {
|
|
||||||
updateHomeList({ type, albums: response.data.albums.map(mapAlbumID3toAlbumListItem) })
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
await Promise.all(promises)
|
|
||||||
} finally {
|
|
||||||
setUpdating(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useUpdateSearchResults = () => {
|
|
||||||
const server = useStore(selectSettings.activeServer)
|
|
||||||
const updateList = useUpdateAtom(searchResultsAtom)
|
|
||||||
const [updating, setUpdating] = useAtom(searchResultsUpdatingAtom)
|
|
||||||
|
|
||||||
if (!server) {
|
|
||||||
return async () => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return async (query: string) => {
|
|
||||||
if (updating || query.length < 2) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setUpdating(true)
|
|
||||||
|
|
||||||
const client = new SubsonicApiClient(server)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await client.search3({ query })
|
|
||||||
updateList({
|
|
||||||
artists: response.data.artists.map(mapArtistID3toArtist),
|
|
||||||
albums: response.data.albums.map(mapAlbumID3toAlbumListItem),
|
|
||||||
songs: response.data.songs.map(a => mapChildToSong(a, client)),
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setUpdating(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useUpdatePlaylists = () => {
|
|
||||||
const server = useStore(selectSettings.activeServer)
|
|
||||||
const updateList = useUpdateAtom(playlistsAtom)
|
|
||||||
const [updating, setUpdating] = useAtom(playlistsUpdatingAtom)
|
|
||||||
|
|
||||||
if (!server) {
|
|
||||||
return async () => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return async () => {
|
|
||||||
if (updating) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setUpdating(true)
|
|
||||||
|
|
||||||
const client = new SubsonicApiClient(server)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await client.getPlaylists()
|
|
||||||
updateList(response.data.playlists.map(mapPlaylistListItem))
|
|
||||||
} finally {
|
|
||||||
setUpdating(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useUpdateAlbumList = () => {
|
|
||||||
const server = useStore(selectSettings.activeServer)
|
|
||||||
const updateList = useUpdateAtom(albumListAtom)
|
|
||||||
const [updating, setUpdating] = useAtom(albumListUpdatingAtom)
|
|
||||||
|
|
||||||
if (!server) {
|
|
||||||
return async () => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return async () => {
|
|
||||||
if (updating) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setUpdating(true)
|
|
||||||
|
|
||||||
const client = new SubsonicApiClient(server)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size: 500 })
|
|
||||||
updateList(response.data.albums.map(mapAlbumID3toAlbumListItem))
|
|
||||||
} finally {
|
|
||||||
setUpdating(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCoverArtUri = () => {
|
export const useCoverArtUri = () => {
|
||||||
const server = useStore(selectSettings.activeServer)
|
const server = useStore(selectSettings.activeServer)
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,11 @@
|
|||||||
import { albumListAtom, artistsAtom, homeListsAtom, playlistsAtom, searchResultsAtom } from '@app/state/music'
|
|
||||||
import { selectSettings } from '@app/state/settings'
|
import { selectSettings } from '@app/state/settings'
|
||||||
import { useStore } from '@app/state/store'
|
import { useStore } from '@app/state/store'
|
||||||
import { useReset } from '@app/state/trackplayer'
|
import { useReset } from '@app/state/trackplayer'
|
||||||
import { useUpdateAtom } from 'jotai/utils'
|
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
export const useSwitchActiveServer = () => {
|
export const useSwitchActiveServer = () => {
|
||||||
const activeServer = useStore(selectSettings.activeServer)
|
const activeServer = useStore(selectSettings.activeServer)
|
||||||
const setActiveServer = useStore(selectSettings.setActiveServer)
|
const setActiveServer = useStore(selectSettings.setActiveServer)
|
||||||
const setArtists = useUpdateAtom(artistsAtom)
|
|
||||||
const setHomeLists = useUpdateAtom(homeListsAtom)
|
|
||||||
const setSearchResults = useUpdateAtom(searchResultsAtom)
|
|
||||||
const setPlaylists = useUpdateAtom(playlistsAtom)
|
|
||||||
const setAlbumLists = useUpdateAtom(albumListAtom)
|
|
||||||
const resetPlayer = useReset()
|
const resetPlayer = useReset()
|
||||||
|
|
||||||
return async (id: string) => {
|
return async (id: string) => {
|
||||||
@ -21,13 +14,6 @@ export const useSwitchActiveServer = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await resetPlayer()
|
await resetPlayer()
|
||||||
|
|
||||||
setArtists([])
|
|
||||||
setHomeLists({})
|
|
||||||
setSearchResults({ artists: [], albums: [], songs: [] })
|
|
||||||
setPlaylists([])
|
|
||||||
setAlbumLists([])
|
|
||||||
|
|
||||||
setActiveServer(id)
|
setActiveServer(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,6 +29,14 @@ export const useActiveListRefresh = (list: unknown[], update: () => void) => {
|
|||||||
}, [activeServer])
|
}, [activeServer])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useActiveListRefresh2 = (update: () => void) => {
|
||||||
|
const activeServer = useStore(selectSettings.activeServer)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
update()
|
||||||
|
}, [activeServer, update])
|
||||||
|
}
|
||||||
|
|
||||||
export const useActiveServerRefresh = (update: () => void) => {
|
export const useActiveServerRefresh = (update: () => void) => {
|
||||||
const activeServer = useStore(selectSettings.activeServer)
|
const activeServer = useStore(selectSettings.activeServer)
|
||||||
|
|
||||||
|
|||||||
@ -80,6 +80,8 @@ export interface Song {
|
|||||||
|
|
||||||
export type ListableItem = Song | AlbumListItem | Artist | PlaylistListItem
|
export type ListableItem = Song | AlbumListItem | Artist | PlaylistListItem
|
||||||
|
|
||||||
|
export type HomeLists = { [key: string]: AlbumListItem[] }
|
||||||
|
|
||||||
export type DownloadedSong = {
|
export type DownloadedSong = {
|
||||||
id: string
|
id: string
|
||||||
type: 'song'
|
type: 'song'
|
||||||
|
|||||||
@ -3,18 +3,16 @@ import GradientScrollView from '@app/components/GradientScrollView'
|
|||||||
import Header from '@app/components/Header'
|
import Header from '@app/components/Header'
|
||||||
import NothingHere from '@app/components/NothingHere'
|
import NothingHere from '@app/components/NothingHere'
|
||||||
import PressableOpacity from '@app/components/PressableOpacity'
|
import PressableOpacity from '@app/components/PressableOpacity'
|
||||||
import { useUpdateHomeLists } from '@app/hooks/music'
|
import { useActiveListRefresh2 } from '@app/hooks/server'
|
||||||
import { useActiveServerRefresh } from '@app/hooks/server'
|
|
||||||
import { AlbumListItem } from '@app/models/music'
|
import { AlbumListItem } from '@app/models/music'
|
||||||
import { homeListsAtom, homeListsUpdatingAtom } from '@app/state/music'
|
import { selectMusic } from '@app/state/music'
|
||||||
import { selectSettings } from '@app/state/settings'
|
import { selectSettings } from '@app/state/settings'
|
||||||
import { useStore } from '@app/state/store'
|
import { useStore } from '@app/state/store'
|
||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
import font from '@app/styles/font'
|
import font from '@app/styles/font'
|
||||||
import { GetAlbumListType } from '@app/subsonic/params'
|
import { GetAlbumListType } from '@app/subsonic/params'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { useAtomValue } from 'jotai/utils'
|
import React, { useCallback } from 'react'
|
||||||
import React from 'react'
|
|
||||||
import { RefreshControl, ScrollView, StatusBar, StyleSheet, Text, View } from 'react-native'
|
import { RefreshControl, ScrollView, StatusBar, StyleSheet, Text, View } from 'react-native'
|
||||||
|
|
||||||
const titles: { [key in GetAlbumListType]?: string } = {
|
const titles: { [key in GetAlbumListType]?: string } = {
|
||||||
@ -78,11 +76,17 @@ const Category = React.memo<{
|
|||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const types = useStore(selectSettings.homeLists)
|
const types = useStore(selectSettings.homeLists)
|
||||||
const lists = useAtomValue(homeListsAtom)
|
const lists = useStore(selectMusic.homeLists)
|
||||||
const updating = useAtomValue(homeListsUpdatingAtom)
|
const updating = useStore(selectMusic.homeListsUpdating)
|
||||||
const update = useUpdateHomeLists()
|
const update = useStore(selectMusic.fetchHomeLists)
|
||||||
|
const clear = useStore(selectMusic.clearHomeLists)
|
||||||
|
|
||||||
useActiveServerRefresh(update)
|
useActiveListRefresh2(
|
||||||
|
useCallback(() => {
|
||||||
|
clear()
|
||||||
|
update()
|
||||||
|
}, [clear, update]),
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GradientScrollView
|
<GradientScrollView
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
import CoverArt from '@app/components/CoverArt'
|
import CoverArt from '@app/components/CoverArt'
|
||||||
import GradientFlatList from '@app/components/GradientFlatList'
|
import GradientFlatList from '@app/components/GradientFlatList'
|
||||||
import PressableOpacity from '@app/components/PressableOpacity'
|
import PressableOpacity from '@app/components/PressableOpacity'
|
||||||
import { useUpdateAlbumList } from '@app/hooks/music'
|
import { useActiveListRefresh2 } from '@app/hooks/server'
|
||||||
import { useActiveListRefresh } from '@app/hooks/server'
|
|
||||||
import { Album } from '@app/models/music'
|
import { Album } from '@app/models/music'
|
||||||
import { albumListAtom, albumListUpdatingAtom } from '@app/state/music'
|
import { selectMusic } from '@app/state/music'
|
||||||
|
import { useStore } from '@app/state/store'
|
||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
import font from '@app/styles/font'
|
import font from '@app/styles/font'
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { useAtomValue } from 'jotai/utils'
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { StyleSheet, Text, useWindowDimensions, View } from 'react-native'
|
import { StyleSheet, Text, useWindowDimensions, View } from 'react-native'
|
||||||
|
|
||||||
@ -53,11 +52,11 @@ const AlbumListRenderItem: React.FC<{
|
|||||||
)
|
)
|
||||||
|
|
||||||
const AlbumsList = () => {
|
const AlbumsList = () => {
|
||||||
const list = useAtomValue(albumListAtom)
|
const list = useStore(selectMusic.albums)
|
||||||
const updating = useAtomValue(albumListUpdatingAtom)
|
const updating = useStore(selectMusic.albumsUpdating)
|
||||||
const updateList = useUpdateAlbumList()
|
const updateList = useStore(selectMusic.fetchAlbums)
|
||||||
|
|
||||||
useActiveListRefresh(list, updateList)
|
useActiveListRefresh2(updateList)
|
||||||
|
|
||||||
const layout = useWindowDimensions()
|
const layout = useWindowDimensions()
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import GradientFlatList from '@app/components/GradientFlatList'
|
import GradientFlatList from '@app/components/GradientFlatList'
|
||||||
import ListItem from '@app/components/ListItem'
|
import ListItem from '@app/components/ListItem'
|
||||||
import { useUpdateArtists } from '@app/hooks/music'
|
import { useActiveListRefresh2 } from '@app/hooks/server'
|
||||||
import { useActiveListRefresh } from '@app/hooks/server'
|
|
||||||
import { Artist } from '@app/models/music'
|
import { Artist } from '@app/models/music'
|
||||||
import { artistsAtom, artistsUpdatingAtom } from '@app/state/music'
|
import { selectMusic } from '@app/state/music'
|
||||||
import { useAtomValue } from 'jotai/utils'
|
import { useStore } from '@app/state/store'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { StyleSheet } from 'react-native'
|
import { StyleSheet } from 'react-native'
|
||||||
|
|
||||||
@ -13,11 +12,11 @@ const ArtistRenderItem: React.FC<{ item: Artist }> = ({ item }) => (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const ArtistsList = () => {
|
const ArtistsList = () => {
|
||||||
const artists = useAtomValue(artistsAtom)
|
const artists = useStore(selectMusic.artists)
|
||||||
const updating = useAtomValue(artistsUpdatingAtom)
|
const updating = useStore(selectMusic.artistsUpdating)
|
||||||
const updateArtists = useUpdateArtists()
|
const updateArtists = useStore(selectMusic.fetchArtists)
|
||||||
|
|
||||||
useActiveListRefresh(artists, updateArtists)
|
useActiveListRefresh2(updateArtists)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GradientFlatList
|
<GradientFlatList
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import GradientFlatList from '@app/components/GradientFlatList'
|
import GradientFlatList from '@app/components/GradientFlatList'
|
||||||
import ListItem from '@app/components/ListItem'
|
import ListItem from '@app/components/ListItem'
|
||||||
import { useUpdatePlaylists } from '@app/hooks/music'
|
import { useActiveListRefresh2 } from '@app/hooks/server'
|
||||||
import { useActiveListRefresh } from '@app/hooks/server'
|
|
||||||
import { PlaylistListItem } from '@app/models/music'
|
import { PlaylistListItem } from '@app/models/music'
|
||||||
import { playlistsAtom, playlistsUpdatingAtom } from '@app/state/music'
|
import { selectMusic } from '@app/state/music'
|
||||||
import { useAtomValue } from 'jotai/utils'
|
import { useStore } from '@app/state/store'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { StyleSheet } from 'react-native'
|
import { StyleSheet } from 'react-native'
|
||||||
|
|
||||||
@ -13,11 +12,11 @@ const PlaylistRenderItem: React.FC<{ item: PlaylistListItem }> = ({ item }) => (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const PlaylistsList = () => {
|
const PlaylistsList = () => {
|
||||||
const playlists = useAtomValue(playlistsAtom)
|
const playlists = useStore(selectMusic.playlists)
|
||||||
const updating = useAtomValue(playlistsUpdatingAtom)
|
const updating = useStore(selectMusic.playlistsUpdating)
|
||||||
const updatePlaylists = useUpdatePlaylists()
|
const updatePlaylists = useStore(selectMusic.fetchPlaylists)
|
||||||
|
|
||||||
useActiveListRefresh(playlists, updatePlaylists)
|
useActiveListRefresh2(updatePlaylists)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GradientFlatList
|
<GradientFlatList
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import GradientScrollView from '@app/components/GradientScrollView'
|
|||||||
import Header from '@app/components/Header'
|
import Header from '@app/components/Header'
|
||||||
import ListItem from '@app/components/ListItem'
|
import ListItem from '@app/components/ListItem'
|
||||||
import NothingHere from '@app/components/NothingHere'
|
import NothingHere from '@app/components/NothingHere'
|
||||||
import { useUpdateSearchResults } from '@app/hooks/music'
|
import { useActiveListRefresh2 } from '@app/hooks/server'
|
||||||
import { ListableItem, SearchResults, Song } from '@app/models/music'
|
import { ListableItem, SearchResults, Song } from '@app/models/music'
|
||||||
import { searchResultsAtom, searchResultsUpdatingAtom } from '@app/state/music'
|
import { selectMusic } from '@app/state/music'
|
||||||
|
import { useStore } from '@app/state/store'
|
||||||
import { useSetQueue } from '@app/state/trackplayer'
|
import { useSetQueue } from '@app/state/trackplayer'
|
||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
import font from '@app/styles/font'
|
import font from '@app/styles/font'
|
||||||
import { useAtomValue } from 'jotai/utils'
|
|
||||||
import debounce from 'lodash.debounce'
|
import debounce from 'lodash.debounce'
|
||||||
import React, { useCallback, useMemo, useState } from 'react'
|
import React, { useCallback, useMemo, useState } from 'react'
|
||||||
import { ActivityIndicator, StatusBar, StyleSheet, TextInput, View } from 'react-native'
|
import { ActivityIndicator, StatusBar, StyleSheet, TextInput, View } from 'react-native'
|
||||||
@ -61,10 +61,19 @@ const Results = React.memo<{
|
|||||||
})
|
})
|
||||||
|
|
||||||
const Search = () => {
|
const Search = () => {
|
||||||
|
const updateSearch = useStore(selectMusic.fetchSearchResults)
|
||||||
|
const clearSearch = useStore(selectMusic.clearSearchResults)
|
||||||
|
const updating = useStore(selectMusic.searchResultsUpdating)
|
||||||
|
const results = useStore(selectMusic.searchResults)
|
||||||
|
|
||||||
|
useActiveListRefresh2(
|
||||||
|
useCallback(() => {
|
||||||
|
setText('')
|
||||||
|
clearSearch()
|
||||||
|
}, [clearSearch]),
|
||||||
|
)
|
||||||
|
|
||||||
const [text, setText] = useState('')
|
const [text, setText] = useState('')
|
||||||
const updateSearch = useUpdateSearchResults()
|
|
||||||
const updating = useAtomValue(searchResultsUpdatingAtom)
|
|
||||||
const results = useAtomValue(searchResultsAtom)
|
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const debouncedonUpdateSearch = useMemo(() => debounce(updateSearch, 400), [])
|
const debouncedonUpdateSearch = useMemo(() => debounce(updateSearch, 400), [])
|
||||||
|
|||||||
@ -3,39 +3,101 @@ import {
|
|||||||
AlbumWithSongs,
|
AlbumWithSongs,
|
||||||
Artist,
|
Artist,
|
||||||
ArtistInfo,
|
ArtistInfo,
|
||||||
|
HomeLists,
|
||||||
|
mapAlbumID3toAlbumListItem,
|
||||||
mapAlbumID3WithSongstoAlbunWithSongs,
|
mapAlbumID3WithSongstoAlbunWithSongs,
|
||||||
|
mapArtistID3toArtist,
|
||||||
mapArtistInfo,
|
mapArtistInfo,
|
||||||
|
mapChildToSong,
|
||||||
|
mapPlaylistListItem,
|
||||||
mapPlaylistWithSongs,
|
mapPlaylistWithSongs,
|
||||||
PlaylistListItem,
|
PlaylistListItem,
|
||||||
PlaylistWithSongs,
|
PlaylistWithSongs,
|
||||||
SearchResults,
|
SearchResults,
|
||||||
} from '@app/models/music'
|
} from '@app/models/music'
|
||||||
import { Store } from '@app/state/store'
|
import { Store } from '@app/state/store'
|
||||||
|
import { GetAlbumList2Type } from '@app/subsonic/params'
|
||||||
import produce from 'immer'
|
import produce from 'immer'
|
||||||
import { atom } from 'jotai'
|
|
||||||
import { GetState, SetState } from 'zustand'
|
import { GetState, SetState } from 'zustand'
|
||||||
|
|
||||||
export type MusicSlice = {
|
export type MusicSlice = {
|
||||||
|
//
|
||||||
|
// family-style state
|
||||||
|
//
|
||||||
cacheSize: number
|
cacheSize: number
|
||||||
|
|
||||||
artistInfo: { [id: string]: ArtistInfo | undefined }
|
artistInfo: { [id: string]: ArtistInfo | undefined }
|
||||||
artistInfoCache: string[]
|
artistInfoCache: string[]
|
||||||
albums: { [id: string]: AlbumWithSongs | undefined }
|
|
||||||
albumsCache: string[]
|
|
||||||
playlists: { [id: string]: PlaylistWithSongs | undefined }
|
|
||||||
playlistsCache: string[]
|
|
||||||
fetchArtistInfo: (id: string) => Promise<ArtistInfo | undefined>
|
fetchArtistInfo: (id: string) => Promise<ArtistInfo | undefined>
|
||||||
fetchAlbum: (id: string) => Promise<AlbumWithSongs | undefined>
|
|
||||||
fetchPlaylist: (id: string) => Promise<PlaylistWithSongs | undefined>
|
albumsWithSongs: { [id: string]: AlbumWithSongs | undefined }
|
||||||
|
albumsWithSongsCache: string[]
|
||||||
|
fetchAlbumWithSongs: (id: string) => Promise<AlbumWithSongs | undefined>
|
||||||
|
|
||||||
|
playlistsWithSongs: { [id: string]: PlaylistWithSongs | undefined }
|
||||||
|
playlistsWithSongsCache: string[]
|
||||||
|
fetchPlaylistWithSongs: (id: string) => Promise<PlaylistWithSongs | undefined>
|
||||||
|
|
||||||
|
//
|
||||||
|
// lists-style state
|
||||||
|
//
|
||||||
|
artists: Artist[]
|
||||||
|
artistsUpdating: boolean
|
||||||
|
fetchArtists: () => Promise<void>
|
||||||
|
|
||||||
|
playlists: PlaylistListItem[]
|
||||||
|
playlistsUpdating: boolean
|
||||||
|
fetchPlaylists: () => Promise<void>
|
||||||
|
|
||||||
|
albums: AlbumListItem[]
|
||||||
|
albumsUpdating: boolean
|
||||||
|
fetchAlbums: (size?: number, offset?: number) => Promise<void>
|
||||||
|
|
||||||
|
searchResults: SearchResults
|
||||||
|
searchResultsUpdating: boolean
|
||||||
|
fetchSearchResults: (query: string) => Promise<void>
|
||||||
|
clearSearchResults: () => void
|
||||||
|
|
||||||
|
homeLists: HomeLists
|
||||||
|
homeListsUpdating: boolean
|
||||||
|
fetchHomeLists: () => Promise<void>
|
||||||
|
clearHomeLists: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectMusic = {
|
||||||
|
fetchArtistInfo: (state: Store) => state.fetchArtistInfo,
|
||||||
|
fetchAlbumWithSongs: (state: Store) => state.fetchAlbumWithSongs,
|
||||||
|
fetchPlaylistWithSongs: (state: Store) => state.fetchPlaylistWithSongs,
|
||||||
|
|
||||||
|
artists: (store: MusicSlice) => store.artists,
|
||||||
|
artistsUpdating: (store: MusicSlice) => store.artistsUpdating,
|
||||||
|
fetchArtists: (store: MusicSlice) => store.fetchArtists,
|
||||||
|
|
||||||
|
playlists: (store: MusicSlice) => store.playlists,
|
||||||
|
playlistsUpdating: (store: MusicSlice) => store.playlistsUpdating,
|
||||||
|
fetchPlaylists: (store: MusicSlice) => store.fetchPlaylists,
|
||||||
|
|
||||||
|
albums: (store: MusicSlice) => store.albums,
|
||||||
|
albumsUpdating: (store: MusicSlice) => store.albumsUpdating,
|
||||||
|
fetchAlbums: (store: MusicSlice) => store.fetchAlbums,
|
||||||
|
|
||||||
|
searchResults: (store: MusicSlice) => store.searchResults,
|
||||||
|
searchResultsUpdating: (store: MusicSlice) => store.searchResultsUpdating,
|
||||||
|
fetchSearchResults: (store: MusicSlice) => store.fetchSearchResults,
|
||||||
|
clearSearchResults: (store: MusicSlice) => store.clearSearchResults,
|
||||||
|
|
||||||
|
homeLists: (store: MusicSlice) => store.homeLists,
|
||||||
|
homeListsUpdating: (store: MusicSlice) => store.homeListsUpdating,
|
||||||
|
fetchHomeLists: (store: MusicSlice) => store.fetchHomeLists,
|
||||||
|
clearHomeLists: (store: MusicSlice) => store.clearHomeLists,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): MusicSlice => ({
|
export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): MusicSlice => ({
|
||||||
cacheSize: 100,
|
cacheSize: 100,
|
||||||
|
|
||||||
artistInfo: {},
|
artistInfo: {},
|
||||||
artistInfoCache: [],
|
artistInfoCache: [],
|
||||||
albums: {},
|
|
||||||
albumsCache: [],
|
|
||||||
playlists: {},
|
|
||||||
playlistsCache: [],
|
|
||||||
fetchArtistInfo: async id => {
|
fetchArtistInfo: async id => {
|
||||||
const client = get().client
|
const client = get().client
|
||||||
if (!client) {
|
if (!client) {
|
||||||
@ -58,7 +120,7 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
|
|||||||
set(
|
set(
|
||||||
produce<MusicSlice>(state => {
|
produce<MusicSlice>(state => {
|
||||||
if (state.artistInfoCache.length >= state.cacheSize) {
|
if (state.artistInfoCache.length >= state.cacheSize) {
|
||||||
delete state.albums[state.artistInfoCache.shift() as string]
|
delete state.albumsWithSongs[state.artistInfoCache.shift() as string]
|
||||||
}
|
}
|
||||||
state.artistInfo[id] = artistInfo
|
state.artistInfo[id] = artistInfo
|
||||||
state.artistInfoCache.push(id)
|
state.artistInfoCache.push(id)
|
||||||
@ -69,7 +131,11 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchAlbum: async id => {
|
|
||||||
|
albumsWithSongs: {},
|
||||||
|
albumsWithSongsCache: [],
|
||||||
|
|
||||||
|
fetchAlbumWithSongs: async id => {
|
||||||
const client = get().client
|
const client = get().client
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return undefined
|
return undefined
|
||||||
@ -81,11 +147,11 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
|
|||||||
|
|
||||||
set(
|
set(
|
||||||
produce<MusicSlice>(state => {
|
produce<MusicSlice>(state => {
|
||||||
if (state.albumsCache.length >= state.cacheSize) {
|
if (state.albumsWithSongsCache.length >= state.cacheSize) {
|
||||||
delete state.albums[state.albumsCache.shift() as string]
|
delete state.albumsWithSongs[state.albumsWithSongsCache.shift() as string]
|
||||||
}
|
}
|
||||||
state.albums[id] = album
|
state.albumsWithSongs[id] = album
|
||||||
state.albumsCache.push(id)
|
state.albumsWithSongsCache.push(id)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
return album
|
return album
|
||||||
@ -93,7 +159,11 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchPlaylist: async id => {
|
|
||||||
|
playlistsWithSongs: {},
|
||||||
|
playlistsWithSongsCache: [],
|
||||||
|
|
||||||
|
fetchPlaylistWithSongs: async id => {
|
||||||
const client = get().client
|
const client = get().client
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return undefined
|
return undefined
|
||||||
@ -105,11 +175,11 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
|
|||||||
|
|
||||||
set(
|
set(
|
||||||
produce<MusicSlice>(state => {
|
produce<MusicSlice>(state => {
|
||||||
if (state.playlistsCache.length >= state.cacheSize) {
|
if (state.playlistsWithSongsCache.length >= state.cacheSize) {
|
||||||
delete state.playlists[state.playlistsCache.shift() as string]
|
delete state.playlistsWithSongs[state.playlistsWithSongsCache.shift() as string]
|
||||||
}
|
}
|
||||||
state.playlists[id] = playlist
|
state.playlistsWithSongs[id] = playlist
|
||||||
state.playlistsCache.push(id)
|
state.playlistsWithSongsCache.push(id)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
return playlist
|
return playlist
|
||||||
@ -117,35 +187,155 @@ export const createMusicSlice = (set: SetState<Store>, get: GetState<Store>): Mu
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
|
||||||
|
|
||||||
export const artistsAtom = atom<Artist[]>([])
|
artists: [],
|
||||||
export const artistsUpdatingAtom = atom(false)
|
artistsUpdating: false,
|
||||||
|
|
||||||
export type HomeLists = { [key: string]: AlbumListItem[] }
|
fetchArtists: async () => {
|
||||||
|
const client = get().client
|
||||||
|
if (!client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
export const homeListsUpdatingAtom = atom(false)
|
if (get().artistsUpdating) {
|
||||||
export const homeListsAtom = atom<HomeLists>({})
|
return
|
||||||
export const homeListsWriteAtom = atom<HomeLists, { type: string; albums: AlbumListItem[] }>(
|
}
|
||||||
get => get(homeListsAtom),
|
set({ artistsUpdating: true })
|
||||||
(get, set, { type, albums }) => {
|
|
||||||
const lists = get(homeListsAtom)
|
try {
|
||||||
set(homeListsAtom, {
|
const response = await client.getArtists()
|
||||||
...lists,
|
set({ artists: response.data.artists.map(mapArtistID3toArtist) })
|
||||||
[type]: albums,
|
} finally {
|
||||||
|
set({ artistsUpdating: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
playlists: [],
|
||||||
|
playlistsUpdating: false,
|
||||||
|
|
||||||
|
fetchPlaylists: async () => {
|
||||||
|
const client = get().client
|
||||||
|
if (!client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get().playlistsUpdating) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set({ playlistsUpdating: true })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await client.getPlaylists()
|
||||||
|
set({ playlists: response.data.playlists.map(mapPlaylistListItem) })
|
||||||
|
} finally {
|
||||||
|
set({ playlistsUpdating: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
albums: [],
|
||||||
|
albumsUpdating: false,
|
||||||
|
|
||||||
|
fetchAlbums: async (size = 500, offset = 0) => {
|
||||||
|
const client = get().client
|
||||||
|
if (!client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get().albumsUpdating) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set({ albumsUpdating: true })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size, offset })
|
||||||
|
set({ albums: response.data.albums.map(mapAlbumID3toAlbumListItem) })
|
||||||
|
} finally {
|
||||||
|
set({ albumsUpdating: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
searchResults: {
|
||||||
|
artists: [],
|
||||||
|
albums: [],
|
||||||
|
songs: [],
|
||||||
|
},
|
||||||
|
searchResultsUpdating: false,
|
||||||
|
|
||||||
|
fetchSearchResults: async query => {
|
||||||
|
if (query.length < 2) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = get().client
|
||||||
|
if (!client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get().searchResultsUpdating) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
set({ searchResultsUpdating: true })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await client.search3({ query })
|
||||||
|
set({
|
||||||
|
searchResults: {
|
||||||
|
artists: response.data.artists.map(mapArtistID3toArtist),
|
||||||
|
albums: response.data.albums.map(mapAlbumID3toAlbumListItem),
|
||||||
|
songs: response.data.songs.map(a => mapChildToSong(a, client)),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
set({ searchResultsUpdating: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearSearchResults: () => {
|
||||||
|
set({
|
||||||
|
searchResults: {
|
||||||
|
artists: [],
|
||||||
|
albums: [],
|
||||||
|
songs: [],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
|
||||||
|
|
||||||
export const searchResultsUpdatingAtom = atom(false)
|
homeLists: {},
|
||||||
export const searchResultsAtom = atom<SearchResults>({
|
homeListsUpdating: false,
|
||||||
artists: [],
|
|
||||||
albums: [],
|
fetchHomeLists: async () => {
|
||||||
songs: [],
|
const client = get().client
|
||||||
|
if (!client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get().homeListsUpdating) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
set({ homeListsUpdating: true })
|
||||||
|
|
||||||
|
const types = get().settings.home.lists
|
||||||
|
|
||||||
|
try {
|
||||||
|
const promises: Promise<any>[] = []
|
||||||
|
for (const type of types) {
|
||||||
|
promises.push(
|
||||||
|
client.getAlbumList2({ type: type as GetAlbumList2Type, size: 20 }).then(response => {
|
||||||
|
set(
|
||||||
|
produce<MusicSlice>(state => {
|
||||||
|
state.homeLists[type] = response.data.albums.map(mapAlbumID3toAlbumListItem)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
await Promise.all(promises)
|
||||||
|
} finally {
|
||||||
|
set({ homeListsUpdating: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearHomeLists: () => {
|
||||||
|
set({ homeLists: {} })
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export const playlistsUpdatingAtom = atom(false)
|
|
||||||
export const playlistsAtom = atom<PlaylistListItem[]>([])
|
|
||||||
|
|
||||||
export const albumListUpdatingAtom = atom(false)
|
|
||||||
export const albumListAtom = atom<AlbumListItem[]>([])
|
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { GetState, SetState } from 'zustand'
|
|||||||
export type SettingsSlice = {
|
export type SettingsSlice = {
|
||||||
settings: AppSettings
|
settings: AppSettings
|
||||||
client?: SubsonicApiClient
|
client?: SubsonicApiClient
|
||||||
createClient: () => void
|
createClient: (id?: string) => void
|
||||||
setActiveServer: (id?: string) => void
|
setActiveServer: (id?: string) => void
|
||||||
setServers: (servers: Server[]) => void
|
setServers: (servers: Server[]) => void
|
||||||
}
|
}
|
||||||
@ -19,23 +19,26 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
|
|||||||
lists: ['recent', 'random', 'frequent', 'starred'],
|
lists: ['recent', 'random', 'frequent', 'starred'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
createClient: () => {
|
createClient: (id?: string) => {
|
||||||
const server = get().settings.servers.find(s => s.id === get().settings.activeServer)
|
if (!id) {
|
||||||
if (!server) {
|
set({ client: undefined })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
set(
|
const server = get().settings.servers.find(s => s.id === id)
|
||||||
produce<Store>(state => {
|
if (!server) {
|
||||||
state.client = new SubsonicApiClient(server)
|
set({ client: undefined })
|
||||||
}),
|
return
|
||||||
)
|
}
|
||||||
|
|
||||||
|
set({ client: new SubsonicApiClient(server) })
|
||||||
},
|
},
|
||||||
setActiveServer: id => {
|
setActiveServer: id => {
|
||||||
const servers = get().settings.servers
|
const servers = get().settings.servers
|
||||||
const currentActiveServerId = get().settings.activeServer
|
const currentActiveServerId = get().settings.activeServer
|
||||||
|
const newActiveServer = servers.find(s => s.id === id)
|
||||||
|
|
||||||
if (!servers.find(s => s.id === id)) {
|
if (!newActiveServer) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (currentActiveServerId === id) {
|
if (currentActiveServerId === id) {
|
||||||
@ -43,25 +46,24 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
|
|||||||
}
|
}
|
||||||
|
|
||||||
set(
|
set(
|
||||||
produce<Store>(state => {
|
produce<SettingsSlice>(state => {
|
||||||
state.settings.activeServer = id
|
state.settings.activeServer = id
|
||||||
|
state.client = new SubsonicApiClient(newActiveServer)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
get().createClient()
|
|
||||||
},
|
},
|
||||||
setServers: servers =>
|
setServers: servers =>
|
||||||
set(
|
set(
|
||||||
produce<Store>(state => {
|
produce<SettingsSlice>(state => {
|
||||||
state.settings.servers = servers
|
state.settings.servers = servers
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const selectSettings = {
|
export const selectSettings = {
|
||||||
activeServer: (state: Store) => state.settings.servers.find(s => s.id === state.settings.activeServer),
|
activeServer: (state: SettingsSlice) => state.settings.servers.find(s => s.id === state.settings.activeServer),
|
||||||
setActiveServer: (state: Store) => state.setActiveServer,
|
setActiveServer: (state: SettingsSlice) => state.setActiveServer,
|
||||||
servers: (state: Store) => state.settings.servers,
|
servers: (state: SettingsSlice) => state.settings.servers,
|
||||||
setServers: (state: Store) => state.setServers,
|
setServers: (state: SettingsSlice) => state.setServers,
|
||||||
homeLists: (state: Store) => state.settings.home.lists,
|
homeLists: (state: SettingsSlice) => state.settings.home.lists,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export const useStore = create<Store>(
|
|||||||
whitelist: ['settings'],
|
whitelist: ['settings'],
|
||||||
onRehydrateStorage: _preState => {
|
onRehydrateStorage: _preState => {
|
||||||
return (postState, _error) => {
|
return (postState, _error) => {
|
||||||
postState?.createClient()
|
postState?.createClient(postState.settings.activeServer)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user