Use Compose on BrowseSourceScreens (#7901)
(cherry picked from commit d4b764fa31)
# Conflicts:
# app/src/main/java/eu/kanade/presentation/library/components/LibraryGridCover.kt
# app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SourceSearchController.kt
# app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt
# app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt
# app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/Pager.kt
# app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceComfortableGridHolder.kt
# app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceCompactGridHolder.kt
# app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceHolder.kt
# app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceItem.kt
# app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/SourceListHolder.kt
# app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/latest/LatestUpdatesController.kt
# app/src/main/res/layout/source_comfortable_grid_item.xml
# app/src/main/res/layout/source_compact_grid_item.xml
# app/src/main/res/menu/source_browse.xml
This commit is contained in:
@@ -1,12 +1,20 @@
|
||||
package exh.md.follows
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.core.os.bundleOf
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.presentation.browse.BrowseMangadexFollowsScreen
|
||||
import eu.kanade.presentation.browse.components.RemoveMangaDialog
|
||||
import eu.kanade.presentation.components.ChangeCategoryDialog
|
||||
import eu.kanade.presentation.components.DuplicateMangaDialog
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
|
||||
/**
|
||||
* Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
|
||||
@@ -19,22 +27,69 @@ class MangaDexFollowsController(bundle: Bundle) : BrowseSourceController(bundle)
|
||||
),
|
||||
)
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return view?.context?.getString(R.string.mangadex_follows)
|
||||
}
|
||||
|
||||
override fun createPresenter(): BrowseSourcePresenter {
|
||||
return MangaDexFollowsPresenter(args.getLong(SOURCE_ID_KEY))
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
menu.findItem(R.id.action_search).isVisible = false
|
||||
menu.findItem(R.id.action_open_in_web_view).isVisible = false
|
||||
menu.findItem(R.id.action_settings).isVisible = false
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
BrowseMangadexFollowsScreen(
|
||||
presenter = presenter,
|
||||
navigateUp = { router.popCurrentController() },
|
||||
onDisplayModeChange = { presenter.displayMode = (it) },
|
||||
onMangaClick = {
|
||||
router.pushController(MangaController(it.id, true))
|
||||
},
|
||||
onMangaLongClick = { manga ->
|
||||
scope.launchIO {
|
||||
val duplicateManga = presenter.getDuplicateLibraryManga(manga)
|
||||
when {
|
||||
manga.favorite -> presenter.dialog = BrowseSourcePresenter.Dialog.RemoveManga(manga)
|
||||
duplicateManga != null -> presenter.dialog = BrowseSourcePresenter.Dialog.AddDuplicateManga(manga, duplicateManga)
|
||||
else -> presenter.addFavorite(manga)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
val onDismissRequest = { presenter.dialog = null }
|
||||
when (val dialog = presenter.dialog) {
|
||||
is BrowseSourcePresenter.Dialog.AddDuplicateManga -> {
|
||||
DuplicateMangaDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirm = { presenter.addFavorite(dialog.manga) },
|
||||
onOpenManga = { router.pushController(MangaController(dialog.duplicate.id)) },
|
||||
duplicateFrom = presenter.getSourceOrStub(dialog.duplicate),
|
||||
)
|
||||
}
|
||||
is BrowseSourcePresenter.Dialog.RemoveManga -> {
|
||||
RemoveMangaDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
onConfirm = {
|
||||
presenter.changeMangaFavorite(dialog.manga)
|
||||
},
|
||||
)
|
||||
}
|
||||
is BrowseSourcePresenter.Dialog.ChangeMangaCategory -> {
|
||||
ChangeCategoryDialog(
|
||||
initialSelection = dialog.initialSelection,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onEditCategories = {
|
||||
router.pushController(CategoryController())
|
||||
},
|
||||
onConfirm = { include, _ ->
|
||||
presenter.changeMangaFavorite(dialog.manga)
|
||||
presenter.moveMangaToCategories(dialog.manga, include)
|
||||
},
|
||||
)
|
||||
}
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
|
||||
override fun initFilterSheet() {
|
||||
// No-op: we don't allow filtering in latest
|
||||
// No-op: we don't allow filtering in mangadex follows
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package exh.md.follows
|
||||
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
|
||||
|
||||
/**
|
||||
* LatestUpdatesPager inherited from the general Pager.
|
||||
*/
|
||||
class MangaDexFollowsPager(val source: MangaDex) : Pager() {
|
||||
|
||||
override suspend fun requestNextPage() {
|
||||
onPageReceived(source.fetchFollows(currentPage))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package exh.md.follows
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowsePagingSource
|
||||
|
||||
/**
|
||||
* LatestUpdatesPager inherited from the general Pager.
|
||||
*/
|
||||
class MangaDexFollowsPagingSource(val source: MangaDex) : BrowsePagingSource() {
|
||||
|
||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
||||
return source.fetchFollows(currentPage)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
package exh.md.follows
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.paging.PagingSource
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.source.getMainSource
|
||||
|
||||
/**
|
||||
@@ -11,8 +18,12 @@ import exh.source.getMainSource
|
||||
*/
|
||||
class MangaDexFollowsPresenter(sourceId: Long) : BrowseSourcePresenter(sourceId) {
|
||||
|
||||
override fun createPager(query: String, filters: FilterList): Pager {
|
||||
val sourceAsMangaDex = source.getMainSource() as MangaDex
|
||||
return MangaDexFollowsPager(sourceAsMangaDex)
|
||||
override fun createPager(query: String, filters: FilterList): PagingSource<Long, Pair<SManga, RaisedSearchMetadata?>> {
|
||||
return MangaDexFollowsPagingSource(source!!.getMainSource() as MangaDex)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun getRaisedSearchMetadata(manga: Manga, initialMetadata: RaisedSearchMetadata?): State<RaisedSearchMetadata?> {
|
||||
return remember { mutableStateOf(initialMetadata) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
package exh.md.similar
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.os.bundleOf
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.presentation.browse.BrowseRecommendationsScreen
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
|
||||
/**
|
||||
* Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
|
||||
@@ -22,36 +26,28 @@ class MangaDexSimilarController(bundle: Bundle) : BrowseSourceController(bundle)
|
||||
),
|
||||
)
|
||||
|
||||
private val mangaTitle = args.getString(MANGA_TITLE)
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return view?.context?.getString(R.string.similar, mangaTitle)
|
||||
}
|
||||
private val mangaTitle = args.getString(MANGA_TITLE, "")
|
||||
|
||||
override fun createPresenter(): BrowseSourcePresenter {
|
||||
return MangaDexSimilarPresenter(args.getLong(MANGA_ID), args.getLong(SOURCE_ID_KEY))
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
menu.findItem(R.id.action_search).isVisible = false
|
||||
menu.findItem(R.id.action_open_in_web_view).isVisible = false
|
||||
menu.findItem(R.id.action_settings).isVisible = false
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
BrowseRecommendationsScreen(
|
||||
presenter = presenter,
|
||||
navigateUp = { router.popCurrentController() },
|
||||
title = stringResource(R.string.similar, mangaTitle),
|
||||
onMangaClick = {
|
||||
router.pushController(MangaController(it.id, true))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun initFilterSheet() {
|
||||
// No-op: we don't allow filtering in similar
|
||||
}
|
||||
|
||||
override fun onItemLongClick(position: Int) {
|
||||
return
|
||||
}
|
||||
|
||||
override fun onAddPageError(error: Throwable) {
|
||||
super.onAddPageError(error)
|
||||
binding.emptyView.show(activity!!.getString(R.string.similar_no_results))
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MANGA_ID = "manga_id"
|
||||
const val MANGA_TITLE = "manga_title"
|
||||
|
||||
+6
-9
@@ -1,19 +1,20 @@
|
||||
package exh.md.similar
|
||||
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.MetadataMangasPage
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowsePagingSource
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
|
||||
/**
|
||||
* MangaDexSimilarPager inherited from the general Pager.
|
||||
* MangaDexSimilarPagingSource inherited from the general Pager.
|
||||
*/
|
||||
class MangaDexSimilarPager(val manga: Manga, val source: MangaDex) : Pager() {
|
||||
class MangaDexSimilarPagingSource(val manga: Manga, val source: MangaDex) : BrowsePagingSource() {
|
||||
|
||||
override suspend fun requestNextPage() {
|
||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
||||
val mangasPage = coroutineScope {
|
||||
val similarPageDef = async { source.getMangaSimilar(manga.toSManga()) }
|
||||
val relatedPageDef = async { source.getMangaRelated(manga.toSManga()) }
|
||||
@@ -27,10 +28,6 @@ class MangaDexSimilarPager(val manga: Manga, val source: MangaDex) : Pager() {
|
||||
)
|
||||
}
|
||||
|
||||
if (mangasPage.mangas.isNotEmpty()) {
|
||||
onPageReceived(mangasPage)
|
||||
} else {
|
||||
throw NoResultsException()
|
||||
}
|
||||
return mangasPage.takeIf { it.mangas.isNotEmpty() } ?: throw NoResultsException()
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
package exh.md.similar
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.paging.PagingSource
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import exh.source.getMainSource
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
@@ -22,9 +29,17 @@ class MangaDexSimilarPresenter(
|
||||
|
||||
var manga: Manga? = null
|
||||
|
||||
override fun createPager(query: String, filters: FilterList): Pager {
|
||||
val sourceAsMangaDex = source.getMainSource() as MangaDex
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
this.manga = runBlocking { getManga.await(mangaId) }
|
||||
return MangaDexSimilarPager(manga!!, sourceAsMangaDex)
|
||||
}
|
||||
|
||||
override fun createPager(query: String, filters: FilterList): PagingSource<Long, Pair<SManga, RaisedSearchMetadata?>> {
|
||||
return MangaDexSimilarPagingSource(manga!!, source!!.getMainSource() as MangaDex)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun getRaisedSearchMetadata(manga: Manga, initialMetadata: RaisedSearchMetadata?): State<RaisedSearchMetadata?> {
|
||||
return remember { mutableStateOf(initialMetadata) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
package exh.recs
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.core.os.bundleOf
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.presentation.browse.BrowseRecommendationsScreen
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.SourcesController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceItem
|
||||
|
||||
/**
|
||||
* Controller that shows the latest manga from the catalogue. Inherit [BrowseSourceController].
|
||||
@@ -24,31 +22,26 @@ class RecommendsController(bundle: Bundle) : BrowseSourceController(bundle) {
|
||||
),
|
||||
)
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return (presenter as? RecommendsPresenter)?.manga?.title
|
||||
}
|
||||
|
||||
override fun createPresenter(): RecommendsPresenter {
|
||||
return RecommendsPresenter(args.getLong(MANGA_ID), args.getLong(SOURCE_ID_KEY))
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
menu.findItem(R.id.action_search).isVisible = false
|
||||
menu.findItem(R.id.action_open_in_web_view).isVisible = false
|
||||
menu.findItem(R.id.action_settings).isVisible = false
|
||||
@Composable
|
||||
override fun ComposeContent() {
|
||||
BrowseRecommendationsScreen(
|
||||
presenter = presenter,
|
||||
navigateUp = { router.popCurrentController() },
|
||||
title = (presenter as RecommendsPresenter).manga!!.title,
|
||||
onMangaClick = { manga ->
|
||||
openSmartSearch(manga.ogTitle)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun initFilterSheet() {
|
||||
// No-op: we don't allow filtering in recs
|
||||
}
|
||||
|
||||
override fun onItemClick(view: View, position: Int): Boolean {
|
||||
val item = adapter?.getItem(position) as? SourceItem ?: return false
|
||||
openSmartSearch(item.manga.ogTitle)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun openSmartSearch(title: String) {
|
||||
val smartSearchConfig = SourcesController.SmartSearchConfig(title)
|
||||
router.pushController(
|
||||
@@ -60,10 +53,6 @@ class RecommendsController(bundle: Bundle) : BrowseSourceController(bundle) {
|
||||
)
|
||||
}
|
||||
|
||||
override fun onItemLongClick(position: Int) {
|
||||
return
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MANGA_ID = "manga_id"
|
||||
}
|
||||
|
||||
+7
-12
@@ -8,12 +8,13 @@ import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowsePagingSource
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.NoResultsException
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import exh.util.MangaType
|
||||
import exh.util.mangaType
|
||||
import exh.util.nullIfEmpty
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
@@ -191,12 +192,12 @@ class Anilist : API("https://graphql.anilist.co/") {
|
||||
}
|
||||
}
|
||||
|
||||
open class RecommendsPager(
|
||||
open class RecommendsPagingSource(
|
||||
private val manga: Manga,
|
||||
private val smart: Boolean = true,
|
||||
private var preferredApi: API = API.MYANIMELIST,
|
||||
) : Pager() {
|
||||
override suspend fun requestNextPage() {
|
||||
) : BrowsePagingSource() {
|
||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
||||
if (smart) preferredApi = if (manga.mangaType() != MangaType.TYPE_MANGA) API.ANILIST else preferredApi
|
||||
|
||||
val apiList = API_MAP.toList().sortedByDescending { it.first == preferredApi }
|
||||
@@ -210,15 +211,9 @@ open class RecommendsPager(
|
||||
logcat(LogPriority.ERROR, e) { key.toString() }
|
||||
null
|
||||
}
|
||||
}.orEmpty()
|
||||
}?.nullIfEmpty() ?: throw NoResultsException()
|
||||
|
||||
val mangasPage = MangasPage(recs, false)
|
||||
|
||||
if (mangasPage.mangas.isNotEmpty()) {
|
||||
onPageReceived(mangasPage)
|
||||
} else {
|
||||
throw NoResultsException()
|
||||
}
|
||||
return MangasPage(recs, false)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -1,10 +1,13 @@
|
||||
package exh.recs
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.paging.PagingSource
|
||||
import eu.kanade.domain.manga.interactor.GetManga
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter
|
||||
import eu.kanade.tachiyomi.ui.browse.source.browse.Pager
|
||||
import exh.metadata.metadata.base.RaisedSearchMetadata
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -20,8 +23,12 @@ class RecommendsPresenter(
|
||||
|
||||
var manga: Manga? = null
|
||||
|
||||
override fun createPager(query: String, filters: FilterList): Pager {
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
this.manga = runBlocking { getManga.await(mangaId) }
|
||||
return RecommendsPager(manga!!)
|
||||
}
|
||||
|
||||
override fun createPager(query: String, filters: FilterList): PagingSource<Long, Pair<SManga, RaisedSearchMetadata?>> {
|
||||
return RecommendsPagingSource(manga!!)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user