diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e46ded205..dcfe49aa5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -132,7 +132,6 @@ dependencies { // LiveData implementation("androidx.compose.runtime:runtime-livedata") implementation("androidx.lifecycle:lifecycle-runtime-compose") - implementation("androidx.lifecycle:lifecycle-livedata-ktx") // Images implementation("io.coil-kt:coil-compose:2.6.0") diff --git a/app/src/main/java/com/jerboa/JerboaAppState.kt b/app/src/main/java/com/jerboa/JerboaAppState.kt index 646c86478..fc07aa487 100644 --- a/app/src/main/java/com/jerboa/JerboaAppState.kt +++ b/app/src/main/java/com/jerboa/JerboaAppState.kt @@ -106,6 +106,8 @@ class JerboaAppState( fun toLookAndFeel() = navController.navigate(Route.LOOK_AND_FEEL) + fun toBlockView() = navController.navigate(Route.BLOCK_VIEW) + fun toAbout() = navController.navigate(Route.ABOUT) fun toCrashLogs() = navController.navigate(Route.CRASH_LOGS) diff --git a/app/src/main/java/com/jerboa/MainActivity.kt b/app/src/main/java/com/jerboa/MainActivity.kt index 9f206041d..bf68a999e 100644 --- a/app/src/main/java/com/jerboa/MainActivity.kt +++ b/app/src/main/java/com/jerboa/MainActivity.kt @@ -73,6 +73,7 @@ import com.jerboa.ui.components.reports.ReportsScreen import com.jerboa.ui.components.settings.SettingsActivity import com.jerboa.ui.components.settings.about.AboutScreen import com.jerboa.ui.components.settings.account.AccountSettingsScreen +import com.jerboa.ui.components.settings.block.BlocksScreen import com.jerboa.ui.components.settings.crashlogs.CrashLogsScreen import com.jerboa.ui.components.settings.lookandfeel.LookAndFeelScreen import com.jerboa.ui.components.viewvotes.comment.CommentLikesScreen @@ -715,6 +716,7 @@ class MainActivity : AppCompatActivity() { onBack = appState::popBackStack, onClickAbout = appState::toAbout, onClickAccountSettings = appState::toAccountSettings, + onClickBlocks = appState::toBlockView, onClickLookAndFeel = appState::toLookAndFeel, ) } @@ -751,6 +753,13 @@ class MainActivity : AppCompatActivity() { ) } + composable(route = Route.BLOCK_VIEW) { + BlocksScreen( + siteViewModel = siteViewModel, + onBack = appState::popBackStack, + ) + } + composable(route = Route.CRASH_LOGS) { CrashLogsScreen( onClickBack = appState::popBackStack, diff --git a/app/src/main/java/com/jerboa/Utils.kt b/app/src/main/java/com/jerboa/Utils.kt index 8d55a9084..fd03c59c0 100644 --- a/app/src/main/java/com/jerboa/Utils.kt +++ b/app/src/main/java/com/jerboa/Utils.kt @@ -53,7 +53,6 @@ import androidx.navigation.NavController import coil.annotation.ExperimentalCoilApi import coil.imageLoader import com.jerboa.api.API -import com.jerboa.api.ApiState import com.jerboa.datatypes.BanFromCommunityData import com.jerboa.datatypes.getDisplayName import com.jerboa.db.APP_SETTINGS_DEFAULT @@ -1046,54 +1045,6 @@ fun findAndUpdatePrivateMessageReport( } } -fun showBlockPersonToast( - blockPersonRes: ApiState, - ctx: Context, -) { - when (blockPersonRes) { - is ApiState.Success -> { - Toast.makeText( - ctx, - "${blockPersonRes.data.person_view.person.name} Blocked", - Toast.LENGTH_SHORT, - ) - .show() - } - - else -> {} - } -} - -fun showBlockCommunityToast( - blockCommunityRes: ApiState, - ctx: Context, -) { - when (blockCommunityRes) { - is ApiState.Success -> { - Toast.makeText( - ctx, - ctx.getString( - if (blockCommunityRes.data.blocked) { - R.string.blocked_community_toast - } else { - R.string.unblocked_community_toast - }, - blockCommunityRes.data.community_view.community.name, - ), - Toast.LENGTH_SHORT, - ).show() - } - - else -> { - Toast.makeText( - ctx, - ctx.getText(R.string.community_block_toast_failure), - Toast.LENGTH_SHORT, - ).show() - } - } -} - fun findAndUpdatePersonMention( mentions: List, updatedCommentView: CommentView, diff --git a/app/src/main/java/com/jerboa/api/ApiAction.kt b/app/src/main/java/com/jerboa/api/ApiAction.kt new file mode 100644 index 000000000..ea4e05aa6 --- /dev/null +++ b/app/src/main/java/com/jerboa/api/ApiAction.kt @@ -0,0 +1,9 @@ +package com.jerboa.api + +sealed class ApiAction(val data: T) { + class Ok(data: T) : ApiAction(data) + + class Loading(data: T) : ApiAction(data) + + class Failed(data: T, val err: Throwable) : ApiAction(data) +} diff --git a/app/src/main/java/com/jerboa/api/ApiState.kt b/app/src/main/java/com/jerboa/api/ApiState.kt new file mode 100644 index 000000000..03e579fb2 --- /dev/null +++ b/app/src/main/java/com/jerboa/api/ApiState.kt @@ -0,0 +1,19 @@ +package com.jerboa.api + +sealed class ApiState { + abstract class Holder(val data: T) : ApiState() + + class Success(data: T) : Holder(data) + + class Appending(data: T) : Holder(data) + + class AppendingFailure(data: T) : Holder(data) + + class Failure(val msg: Throwable) : ApiState() + + data object Loading : ApiState() + + data object Refreshing : ApiState() + + data object Empty : ApiState() +} diff --git a/app/src/main/java/com/jerboa/api/Http.kt b/app/src/main/java/com/jerboa/api/Http.kt index c66853d67..d5fcc0d11 100644 --- a/app/src/main/java/com/jerboa/api/Http.kt +++ b/app/src/main/java/com/jerboa/api/Http.kt @@ -208,24 +208,6 @@ object API { } } -sealed class ApiState { - abstract class Holder(val data: T) : ApiState() - - class Success(data: T) : Holder(data) - - class Appending(data: T) : Holder(data) - - class AppendingFailure(data: T) : Holder(data) - - class Failure(val msg: Throwable) : ApiState() - - data object Loading : ApiState() - - data object Refreshing : ApiState() - - data object Empty : ApiState() -} - fun Result.toApiState(): ApiState { return this.fold( onSuccess = { ApiState.Success(it) }, diff --git a/app/src/main/java/com/jerboa/feat/Block.kt b/app/src/main/java/com/jerboa/feat/Block.kt index dea604c26..bdafa2bda 100644 --- a/app/src/main/java/com/jerboa/feat/Block.kt +++ b/app/src/main/java/com/jerboa/feat/Block.kt @@ -1,15 +1,15 @@ package com.jerboa.feat import android.content.Context +import android.util.Log import android.widget.Toast import com.jerboa.R import com.jerboa.api.API -import com.jerboa.api.toApiState -import com.jerboa.showBlockCommunityToast -import com.jerboa.showBlockPersonToast import it.vercruysse.lemmyapi.v0x19.datatypes.BlockCommunity +import it.vercruysse.lemmyapi.v0x19.datatypes.BlockCommunityResponse import it.vercruysse.lemmyapi.v0x19.datatypes.BlockInstanceResponse import it.vercruysse.lemmyapi.v0x19.datatypes.BlockPerson +import it.vercruysse.lemmyapi.v0x19.datatypes.BlockPersonResponse import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -25,7 +25,7 @@ fun blockCommunity( ctx: Context, ) { scope.launch { - val res = API.getInstance().blockCommunity(form).toApiState() + val res = API.getInstance().blockCommunity(form) withContext(Dispatchers.Main) { showBlockCommunityToast(res, ctx) } @@ -38,38 +38,95 @@ fun blockPerson( ctx: Context, ) { scope.launch { - val res = API.getInstance().blockPerson(form).toApiState() + val res = API.getInstance().blockPerson(form) withContext(Dispatchers.Main) { showBlockPersonToast(res, ctx) } } } -fun showBlockCommunityToast( +fun showBlockInstanceToast( blockInstanceResp: Result, instance: String, ctx: Context, ) { blockInstanceResp .onSuccess { + makeSuccessfulBlockMessage( + it.blocked, + instance, + ctx, + ) + } + .onFailure { Toast.makeText( ctx, - ctx.getString( - if (it.blocked) { - R.string.blocked_community_toast - } else { - R.string.unblocked_community_toast - }, - instance, - ), + ctx.getText(R.string.instance_block_toast_failure), Toast.LENGTH_SHORT, ).show() + Log.i("Block", "failed", it) + } +} + +fun showBlockPersonToast( + blockPersonRes: Result, + ctx: Context, +) { + blockPersonRes + .onSuccess { + makeSuccessfulBlockMessage( + it.blocked, + it.person_view.person.name, + ctx, + ) } .onFailure { Toast.makeText( ctx, - ctx.getText(R.string.instance_block_toast_failure), + ctx.getText(R.string.user_block_toast_failure), + Toast.LENGTH_SHORT, + ).show() + Log.i("Block", "failed", it) + } +} + +fun showBlockCommunityToast( + blockCommunityRes: Result, + ctx: Context, +) { + blockCommunityRes + .onSuccess { + makeSuccessfulBlockMessage( + it.blocked, + it.community_view.community.name, + ctx, + ) + } + .onFailure { + Toast.makeText( + ctx, + ctx.getText(R.string.community_block_toast_failure), Toast.LENGTH_SHORT, ).show() + Log.i("Block", "failed", it) } } + +private fun makeSuccessfulBlockMessage( + isBlocked: Boolean, + name: String, + context: Context, +) { + Toast.makeText( + context, + context.getString( + if (isBlocked) { + R.string.blocked_element_toast + } else { + R.string.unblocked_element_toast + }, + name, + ), + Toast.LENGTH_SHORT, + ).show() +} diff --git a/app/src/main/java/com/jerboa/feed/ApiActionController.kt b/app/src/main/java/com/jerboa/feed/ApiActionController.kt new file mode 100644 index 000000000..7475226cc --- /dev/null +++ b/app/src/main/java/com/jerboa/feed/ApiActionController.kt @@ -0,0 +1,42 @@ +package com.jerboa.feed + +import com.jerboa.api.ApiAction + +class ApiActionController( + val idSelect: (T) -> Long, +) { + private val controller = FeedController>() + + val feed: List> + get() = controller.feed + + fun init(newItems: List) { + controller.init(newItems.map { ApiAction.Ok(it) }) + } + + fun setLoading(item: T) { + controller.safeUpdate({ items -> + items.indexOfFirst { idSelect(it.data) == idSelect(item) } + }) { ApiAction.Loading(it.data) } + } + + fun setOk(item: T) { + controller.safeUpdate({ items -> + items.indexOfFirst { idSelect(it.data) == idSelect(item) } + }) { ApiAction.Ok(it.data) } + } + + fun setFailed( + item: T, + error: Throwable, + ) { + controller.safeUpdate({ items -> + items.indexOfFirst { idSelect(it.data) == idSelect(item) } + }) { ApiAction.Failed(it.data, error) } + } + + fun removeItem(item: T) { + val index = feed.indexOfFirst { idSelect(it.data) == idSelect(item) } + controller.removeAt(index) + } +} diff --git a/app/src/main/java/com/jerboa/feed/FeedController.kt b/app/src/main/java/com/jerboa/feed/FeedController.kt index 2b37e78a1..511862ee0 100644 --- a/app/src/main/java/com/jerboa/feed/FeedController.kt +++ b/app/src/main/java/com/jerboa/feed/FeedController.kt @@ -55,10 +55,23 @@ open class FeedController { } } + fun init(newItems: List) { + items.clear() + items.addAll(newItems) + } + + fun get(index: Int): T? = items.getOrNull(index) + fun add(item: T) = items.add(item) fun remove(item: T) = items.remove(item) + fun removeAt(index: Int) { + if (isValidIndex(index)) { + items.removeAt(index) + } + } + fun clear() = items.clear() fun addAll(newItems: List) = items.addAll(newItems) diff --git a/app/src/main/java/com/jerboa/model/BlockViewModel.kt b/app/src/main/java/com/jerboa/model/BlockViewModel.kt new file mode 100644 index 000000000..83e1ad99b --- /dev/null +++ b/app/src/main/java/com/jerboa/model/BlockViewModel.kt @@ -0,0 +1,99 @@ +package com.jerboa.model + +import android.content.Context +import android.util.Log +import androidx.compose.runtime.derivedStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.jerboa.api.API +import com.jerboa.feat.showBlockCommunityToast +import com.jerboa.feat.showBlockInstanceToast +import com.jerboa.feat.showBlockPersonToast +import com.jerboa.feed.ApiActionController +import it.vercruysse.lemmyapi.v0x19.datatypes.BlockCommunity +import it.vercruysse.lemmyapi.v0x19.datatypes.BlockInstance +import it.vercruysse.lemmyapi.v0x19.datatypes.BlockPerson +import it.vercruysse.lemmyapi.v0x19.datatypes.Community +import it.vercruysse.lemmyapi.v0x19.datatypes.Instance +import it.vercruysse.lemmyapi.v0x19.datatypes.MyUserInfo +import it.vercruysse.lemmyapi.v0x19.datatypes.Person +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class BlockViewModel : ViewModel() { + private val instanceBlockController = ApiActionController { it.id } + private val communityBlockController = ApiActionController { it.id } + private val personBlockController = ApiActionController { it.id } + + val instanceBlocks = derivedStateOf { instanceBlockController.feed } + val communityBlocks = derivedStateOf { communityBlockController.feed } + val personBlocks = derivedStateOf { personBlockController.feed } + + fun initData(userInfo: MyUserInfo) { + Log.d("BlockViewModel", "initData") + instanceBlockController.init(userInfo.instance_blocks.map { it.instance }) + communityBlockController.init(userInfo.community_blocks.map { it.community }) + personBlockController.init(userInfo.person_blocks.map { it.target }) + } + + fun unBlockInstance( + instance: Instance, + ctx: Context, + ) { + instanceBlockController.setLoading(instance) + + viewModelScope.launch { + API.getInstance().blockInstance(BlockInstance(instance.id, false)) + .onSuccess { + instanceBlockController.removeItem(instance) + } + .onFailure { + instanceBlockController.setFailed(instance, it) + withContext(Dispatchers.Main) { + showBlockInstanceToast(Result.failure(it), instance.domain, ctx) + } + } + } + } + + fun unBlockCommunity( + community: Community, + ctx: Context, + ) { + communityBlockController.setLoading(community) + + viewModelScope.launch { + API.getInstance().blockCommunity(BlockCommunity(community.id, false)) + .onSuccess { + communityBlockController.removeItem(community) + } + .onFailure { + communityBlockController.setFailed(community, it) + withContext(Dispatchers.Main) { + showBlockCommunityToast(Result.failure(it), ctx) + } + } + } + } + + fun unBlockPerson( + person: Person, + ctx: Context, + ) { + personBlockController.setLoading(person) + + viewModelScope.launch { + API.getInstance().blockPerson(BlockPerson(person.id, false)) + .onSuccess { + personBlockController.removeItem(person) + } + .onFailure { + personBlockController.setFailed(person, it) + withContext(Dispatchers.Main) { + showBlockPersonToast(Result.failure(it), ctx) + } + } + } + } +} diff --git a/app/src/main/java/com/jerboa/model/CommunityViewModel.kt b/app/src/main/java/com/jerboa/model/CommunityViewModel.kt index e7a0e6dd8..b2d1ead07 100644 --- a/app/src/main/java/com/jerboa/model/CommunityViewModel.kt +++ b/app/src/main/java/com/jerboa/model/CommunityViewModel.kt @@ -13,8 +13,8 @@ import com.jerboa.api.API import com.jerboa.api.ApiState import com.jerboa.api.toApiState import com.jerboa.db.repository.AccountRepository +import com.jerboa.feat.showBlockCommunityToast import com.jerboa.jerboaApplication -import com.jerboa.showBlockCommunityToast import it.vercruysse.lemmyapi.v0x19.datatypes.BlockCommunity import it.vercruysse.lemmyapi.v0x19.datatypes.BlockCommunityResponse import it.vercruysse.lemmyapi.v0x19.datatypes.CommunityId @@ -78,9 +78,9 @@ class CommunityViewModel( ) { viewModelScope.launch { blockCommunityRes = ApiState.Loading - blockCommunityRes = API.getInstance().blockCommunity(form).toApiState() - - showBlockCommunityToast(blockCommunityRes, ctx) + val res = API.getInstance().blockCommunity(form) + blockCommunityRes = res.toApiState() + showBlockCommunityToast(res, ctx) when (val blockCommunity = blockCommunityRes) { is ApiState.Success -> { diff --git a/app/src/main/java/com/jerboa/model/InboxViewModel.kt b/app/src/main/java/com/jerboa/model/InboxViewModel.kt index e4cd86a14..a457575c9 100644 --- a/app/src/main/java/com/jerboa/model/InboxViewModel.kt +++ b/app/src/main/java/com/jerboa/model/InboxViewModel.kt @@ -14,13 +14,12 @@ import com.jerboa.api.ApiState import com.jerboa.api.toApiState import com.jerboa.db.entity.Account import com.jerboa.db.entity.isAnon +import com.jerboa.feat.showBlockPersonToast import com.jerboa.findAndUpdateCommentReply import com.jerboa.findAndUpdateMention import com.jerboa.findAndUpdatePersonMention import com.jerboa.findAndUpdatePrivateMessage import com.jerboa.getDeduplicateMerge -import com.jerboa.showBlockCommunityToast -import com.jerboa.showBlockPersonToast import it.vercruysse.lemmyapi.dto.CommentSortType import it.vercruysse.lemmyapi.v0x19.datatypes.* import kotlinx.coroutines.launch @@ -436,25 +435,15 @@ class InboxViewModel(account: Account, siteViewModel: SiteViewModel) : ViewModel } } - fun blockCommunity( - form: BlockCommunity, - ctx: Context, - ) { - viewModelScope.launch { - blockCommunityRes = ApiState.Loading - blockCommunityRes = API.getInstance().blockCommunity(form).toApiState() - showBlockCommunityToast(blockCommunityRes, ctx) - } - } - fun blockPerson( form: BlockPerson, ctx: Context, ) { viewModelScope.launch { blockPersonRes = ApiState.Loading - blockPersonRes = API.getInstance().blockPerson(form).toApiState() - showBlockPersonToast(blockPersonRes, ctx) + val res = API.getInstance().blockPerson(form) + blockPersonRes = res.toApiState() + showBlockPersonToast(res, ctx) } } diff --git a/app/src/main/java/com/jerboa/model/PersonProfileViewModel.kt b/app/src/main/java/com/jerboa/model/PersonProfileViewModel.kt index 10f8b36de..9a7004c2e 100644 --- a/app/src/main/java/com/jerboa/model/PersonProfileViewModel.kt +++ b/app/src/main/java/com/jerboa/model/PersonProfileViewModel.kt @@ -17,6 +17,8 @@ import com.jerboa.api.API import com.jerboa.api.ApiState import com.jerboa.api.toApiState import com.jerboa.datatypes.BanFromCommunityData +import com.jerboa.feat.showBlockCommunityToast +import com.jerboa.feat.showBlockPersonToast import com.jerboa.findAndUpdateComment import com.jerboa.findAndUpdateCommentCreator import com.jerboa.findAndUpdateCommentCreatorBannedFromCommunity @@ -25,8 +27,6 @@ import com.jerboa.findAndUpdatePostCreator import com.jerboa.findAndUpdatePostCreatorBannedFromCommunity import com.jerboa.findAndUpdatePostHidden import com.jerboa.getDeduplicateMerge -import com.jerboa.showBlockCommunityToast -import com.jerboa.showBlockPersonToast import it.vercruysse.lemmyapi.dto.SortType import it.vercruysse.lemmyapi.v0x19.datatypes.* import kotlinx.coroutines.launch @@ -264,8 +264,9 @@ class PersonProfileViewModel(personArg: Either, savedMode: Boo ) { viewModelScope.launch { blockCommunityRes = ApiState.Loading - blockCommunityRes = API.getInstance().blockCommunity(form).toApiState() - showBlockCommunityToast(blockCommunityRes, ctx) + val res = API.getInstance().blockCommunity(form) + blockCommunityRes = res.toApiState() + showBlockCommunityToast(res, ctx) } } @@ -275,8 +276,9 @@ class PersonProfileViewModel(personArg: Either, savedMode: Boo ) { viewModelScope.launch { blockPersonRes = ApiState.Loading - blockPersonRes = API.getInstance().blockPerson(form).toApiState() - showBlockPersonToast(blockPersonRes, ctx) + val res = API.getInstance().blockPerson(form) + blockPersonRes = res.toApiState() + showBlockPersonToast(res, ctx) } } diff --git a/app/src/main/java/com/jerboa/model/PostViewModel.kt b/app/src/main/java/com/jerboa/model/PostViewModel.kt index e45cd4822..d9a9edf87 100644 --- a/app/src/main/java/com/jerboa/model/PostViewModel.kt +++ b/app/src/main/java/com/jerboa/model/PostViewModel.kt @@ -17,10 +17,10 @@ import com.jerboa.api.ApiState import com.jerboa.api.toApiState import com.jerboa.appendData import com.jerboa.datatypes.BanFromCommunityData +import com.jerboa.feat.showBlockPersonToast import com.jerboa.findAndUpdateComment import com.jerboa.findAndUpdateCommentCreator import com.jerboa.findAndUpdateCommentCreatorBannedFromCommunity -import com.jerboa.showBlockPersonToast import it.vercruysse.lemmyapi.dto.CommentSortType import it.vercruysse.lemmyapi.dto.ListingType import it.vercruysse.lemmyapi.v0x19.datatypes.BlockPerson @@ -313,8 +313,9 @@ class PostViewModel(val id: Either) : ViewModel() { ) { viewModelScope.launch { blockPersonRes = ApiState.Loading - blockPersonRes = API.getInstance().blockPerson(form).toApiState() - showBlockPersonToast(blockPersonRes, ctx) + val res = API.getInstance().blockPerson(form) + blockPersonRes = res.toApiState() + showBlockPersonToast(res, ctx) } } diff --git a/app/src/main/java/com/jerboa/model/SiteViewModel.kt b/app/src/main/java/com/jerboa/model/SiteViewModel.kt index c6a1a3cc2..f179cca65 100644 --- a/app/src/main/java/com/jerboa/model/SiteViewModel.kt +++ b/app/src/main/java/com/jerboa/model/SiteViewModel.kt @@ -91,9 +91,9 @@ class SiteViewModel(private val accountRepository: AccountRepository) : ViewMode } } - fun getSite(): Job { + fun getSite(loadingState: ApiState = ApiState.Loading): Job { return viewModelScope.launch { - siteRes = ApiState.Loading + siteRes = loadingState siteRes = API.getInstance().getSite().toApiState() when (val res = siteRes) { diff --git a/app/src/main/java/com/jerboa/ui/components/common/Route.kt b/app/src/main/java/com/jerboa/ui/components/common/Route.kt index d1ad38e62..2b6352389 100644 --- a/app/src/main/java/com/jerboa/ui/components/common/Route.kt +++ b/app/src/main/java/com/jerboa/ui/components/common/Route.kt @@ -58,6 +58,8 @@ object Route { const val REGISTRATION_APPLICATIONS = "registrationApplications" const val REPORTS = "reports" + const val BLOCK_VIEW = "blockView" + val VIEW = ViewArgs.route class CommunityFromIdArgs(val id: CommunityId) { diff --git a/app/src/main/java/com/jerboa/ui/components/common/Text.kt b/app/src/main/java/com/jerboa/ui/components/common/Text.kt new file mode 100644 index 000000000..7751d48ce --- /dev/null +++ b/app/src/main/java/com/jerboa/ui/components/common/Text.kt @@ -0,0 +1,23 @@ +package com.jerboa.ui.components.common + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview + +@Preview( + showBackground = true, + widthDp = 360, +) +@Composable +private fun TitlePreview() { + Title("This is my title") +} + +@Composable +fun Title(text: String) { + Text( + text = text, + style = MaterialTheme.typography.titleMedium, + ) +} diff --git a/app/src/main/java/com/jerboa/ui/components/drawer/Drawer.kt b/app/src/main/java/com/jerboa/ui/components/drawer/Drawer.kt index 46f544493..10ecfdb9b 100644 --- a/app/src/main/java/com/jerboa/ui/components/drawer/Drawer.kt +++ b/app/src/main/java/com/jerboa/ui/components/drawer/Drawer.kt @@ -59,7 +59,6 @@ import com.jerboa.ui.components.common.LargerCircularIcon import com.jerboa.ui.components.common.PictrsBannerImage import com.jerboa.ui.components.common.customMarquee import com.jerboa.ui.components.common.getCurrentAccount -import com.jerboa.ui.components.common.simpleVerticalScrollbar import com.jerboa.ui.components.community.CommunityLinkLarger import com.jerboa.ui.components.home.NavTab import com.jerboa.ui.theme.DRAWER_BANNER_SIZE @@ -205,7 +204,6 @@ fun DrawerItemsMain( LazyColumn( state = listState, - modifier = Modifier.simpleVerticalScrollbar(listState), ) { if (follows.isNotEmpty()) { item { diff --git a/app/src/main/java/com/jerboa/ui/components/post/composables/PostOptionsDropdown.kt b/app/src/main/java/com/jerboa/ui/components/post/composables/PostOptionsDropdown.kt index 99a84f62c..652f8f8cd 100644 --- a/app/src/main/java/com/jerboa/ui/components/post/composables/PostOptionsDropdown.kt +++ b/app/src/main/java/com/jerboa/ui/components/post/composables/PostOptionsDropdown.kt @@ -42,7 +42,7 @@ import com.jerboa.feat.blockPerson import com.jerboa.feat.getInstanceFromCommunityUrl import com.jerboa.feat.shareLink import com.jerboa.feat.shareMedia -import com.jerboa.feat.showBlockCommunityToast +import com.jerboa.feat.showBlockInstanceToast import com.jerboa.isMedia import com.jerboa.ui.components.common.BanFromCommunityPopupMenuItem import com.jerboa.ui.components.common.BanPersonPopupMenuItem @@ -442,7 +442,7 @@ fun PostOptionsDropdown( ), ) withContext(Dispatchers.Main) { - showBlockCommunityToast(resp, instance, ctx) + showBlockInstanceToast(resp, instance, ctx) } } }, diff --git a/app/src/main/java/com/jerboa/ui/components/settings/SettingsScreen.kt b/app/src/main/java/com/jerboa/ui/components/settings/SettingsScreen.kt index b221f3755..ce5e44f00 100644 --- a/app/src/main/java/com/jerboa/ui/components/settings/SettingsScreen.kt +++ b/app/src/main/java/com/jerboa/ui/components/settings/SettingsScreen.kt @@ -6,6 +6,7 @@ import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Block import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.ManageAccounts import androidx.compose.material.icons.outlined.Palette @@ -33,6 +34,7 @@ fun SettingsActivity( onBack: () -> Unit, onClickLookAndFeel: () -> Unit, onClickAccountSettings: () -> Unit, + onClickBlocks: () -> Unit, onClickAbout: () -> Unit, ) { Log.d("jerboa", "Got to settings screen") @@ -78,6 +80,22 @@ fun SettingsActivity( onClick = onClickAccountSettings, ) } + if (!account.isAnon()) { + Preference( + title = { + Text( + stringResource(id = R.string.blocks), + ) + }, + icon = { + Icon( + imageVector = Icons.Outlined.Block, + contentDescription = null, + ) + }, + onClick = onClickBlocks, + ) + } Preference( title = { Text(stringResource(R.string.settings_screen_about)) }, icon = { diff --git a/app/src/main/java/com/jerboa/ui/components/settings/account/AccountSettings.kt b/app/src/main/java/com/jerboa/ui/components/settings/account/AccountSettings.kt index 25392fa9f..266f8f757 100644 --- a/app/src/main/java/com/jerboa/ui/components/settings/account/AccountSettings.kt +++ b/app/src/main/java/com/jerboa/ui/components/settings/account/AccountSettings.kt @@ -117,7 +117,7 @@ fun SettingsForm( ) { val luv = when (val siteRes = siteViewModel.siteRes) { - is ApiState.Success -> siteRes.data.my_user?.local_user_view + is ApiState.Holder -> siteRes.data.my_user?.local_user_view else -> null } diff --git a/app/src/main/java/com/jerboa/ui/components/settings/block/BlocksScreen.kt b/app/src/main/java/com/jerboa/ui/components/settings/block/BlocksScreen.kt new file mode 100644 index 000000000..db153f984 --- /dev/null +++ b/app/src/main/java/com/jerboa/ui/components/settings/block/BlocksScreen.kt @@ -0,0 +1,234 @@ +package com.jerboa.ui.components.settings.block + +import android.util.Log +import androidx.annotation.StringRes +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.jerboa.R +import com.jerboa.api.ApiAction +import com.jerboa.api.ApiState +import com.jerboa.model.BlockViewModel +import com.jerboa.model.SiteViewModel +import com.jerboa.ui.components.common.ApiEmptyText +import com.jerboa.ui.components.common.ApiErrorText +import com.jerboa.ui.components.common.ItemAndInstanceTitle +import com.jerboa.ui.components.common.JerboaLoadingBar +import com.jerboa.ui.components.common.JerboaSnackbarHost +import com.jerboa.ui.components.common.SimpleTopAppBar +import com.jerboa.ui.components.common.Title +import com.jerboa.ui.theme.LARGE_PADDING +import com.jerboa.ui.theme.MEDIUM_PADDING +import it.vercruysse.lemmyapi.v0x19.datatypes.MyUserInfo + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BlocksScreen( + siteViewModel: SiteViewModel, + onBack: () -> Unit, +) { + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(Unit) { + Log.d("BlocksScreen", "Refreshing site") + when (val res = siteViewModel.siteRes) { + is ApiState.Success -> siteViewModel.getSite(ApiState.Appending(res.data)) + is ApiState.Loading, is ApiState.Appending -> {} + else -> siteViewModel.getSite() + } + } + + Scaffold( + snackbarHost = { JerboaSnackbarHost(snackbarHostState) }, + topBar = { + SimpleTopAppBar(text = stringResource(R.string.blocks), onClickBack = onBack) + }, + content = { padding -> + PullToRefreshBox( + modifier = Modifier.padding(padding), + isRefreshing = siteViewModel.siteRes is ApiState.Loading, + onRefresh = { siteViewModel.getSite() }, + ) { + JerboaLoadingBar(siteViewModel.siteRes) + + when (val res = siteViewModel.siteRes) { + is ApiState.Failure -> ApiErrorText(res.msg) + is ApiState.Holder -> { + res.data.my_user?.let { + BlockList(it) + } ?: ApiEmptyText() + } + + else -> Unit + } + } + }, + ) +} + +@Composable +fun BlockList(userInfo: MyUserInfo) { + val ctx = LocalContext.current + val viewModel: BlockViewModel = viewModel() + + LaunchedEffect(userInfo) { + viewModel.initData(userInfo) + } + + LazyColumn( + contentPadding = PaddingValues(MEDIUM_PADDING, 0.dp), + ) { + blockListHeader(R.string.blocked_instances) + itemsWithEmpty( + items = viewModel.instanceBlocks.value, + key = { "I${it.data.id}" }, + emptyText = R.string.you_have_no_blocked_instances, + ) { + ItemBlockView(it.data.domain, null, it) { + viewModel.unBlockInstance(it.data, ctx) + } + } + + blockListHeader(R.string.blocked_communities) + itemsWithEmpty( + items = viewModel.communityBlocks.value, + key = { "C${it.data.id}" }, + emptyText = R.string.you_have_no_blocked_communities, + ) { + ItemBlockView(it.data.name, it.data.actor_id, it) { + viewModel.unBlockCommunity(it.data, ctx) + } + } + + blockListHeader(R.string.blocked_users) + itemsWithEmpty( + items = viewModel.personBlocks.value, + key = { "U${it.data.id}" }, + emptyText = R.string.you_have_no_blocked_users, + ) { + ItemBlockView(it.data.name, it.data.actor_id, it) { + viewModel.unBlockPerson(it.data, ctx) + } + } + } +} + +inline fun LazyListScope.itemsWithEmpty( + items: List, + noinline key: (T) -> Any, + @StringRes emptyText: Int, + crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit, +) { + if (items.isEmpty()) { + item( + contentType = "blockEmpty", + ) { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(LARGE_PADDING), + ) + Text(stringResource(emptyText)) + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(LARGE_PADDING), + ) + } + } else { + items( + items = items, + key = key, + contentType = { "blockItem" }, + itemContent = itemContent, + ) + } +} + +@Composable +fun ItemBlockView( + name: String, + actor: String?, + action: ApiAction<*>, + onUnblock: () -> Unit, +) { + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + ItemAndInstanceTitle( + title = name, + actorId = actor, + local = false, + onClick = null, + ) + + IconButton( + onClick = onUnblock, + enabled = action !is ApiAction.Loading, + ) { + when (action) { + is ApiAction.Loading -> CircularProgressIndicator(color = MaterialTheme.colorScheme.secondary) + is ApiAction.Failed -> Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = stringResource(id = R.string.retry), + tint = MaterialTheme.colorScheme.error, + ) + + is ApiAction.Ok -> Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(id = R.string.unblock), + tint = MaterialTheme.colorScheme.secondary, + ) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +fun LazyListScope.blockListHeader( + @StringRes resId: Int, +) { + stickyHeader( + contentType = "blockHeader", + ) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.background, + ) { + Title(stringResource(id = resId)) + } + } +} diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 41e06f874..899c3c224 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -365,8 +365,8 @@ وضع شريط إجراءات الموضوعات شغِّل الصور المتحركة تلقائيًّا ألغِ حظر المجتمع - %1$s محظور - %1$s أُلغيَ حظره + %1$s محظور + %1$s أُلغيَ حظره فشل إلغاء حظر هذا المجتمع تُحفَظ الوسائط… حُفِظت الوسائط diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 8e47d50bf..3c0fbb5d6 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -374,8 +374,8 @@ Post actionbar mode Auto play GIFs Отблокиране на общността - %1$s е блокирана - %1$s е отблокирана + %1$s е блокирана + %1$s е отблокирана Неуспешно (от)блокиране на тази общност Неуспешно (от)блокиране на тази инстанция Запазване на мултимедията… diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c289adff5..42b41e9d2 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -371,8 +371,8 @@ Режим панели действий поста Автоматическое воспроизведение GIF Разблокировать сообщество - %1s заблокирован - %1s разблокирован + %1s заблокирован + %1s разблокирован Не удалось (раз)блокировать это сообщество Не удалось (раз)блокировать этот экземпляр Сохраняем медиа… diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 4cfff1e60..9ffd1cfea 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -376,8 +376,8 @@ Eylem çubuğu modu sonrası GIF\'leri otomatik oynat Topluluğun Engelini Kaldır - %1$s engellendi - %1$s adlı kişinin engeli kaldırıldı + %1$s engellendi + %1$s adlı kişinin engeli kaldırıldı Bu topluluk engellenemedi (kaldırılamadı) Bu örneği engelleme (kaldırma) başarısız oldu Medya kaydediliyor... diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c25b31a72..031a97614 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -377,10 +377,11 @@ Post actionbar mode Auto play GIFs Unblock Community - %1$s is blocked - %1$s is unblocked + %1$s is blocked + %1$s is unblocked Failed to (un)block this community Failed to (un)block this instance + Failed to (un)block this user Saving media… Saved media Share image @@ -484,4 +485,12 @@ Installed Not installed Unable to retrieve + Blocks + Unblock + Blocked users + Blocked communities + Blocked instances + You have no blocked users + You have no blocked communities + You have no blocked instances diff --git a/benchmarks/src/main/java/com/jerboa/benchmarks/ScrollCommentsBenchmarks.kt b/benchmarks/src/main/java/com/jerboa/benchmarks/ScrollCommentsBenchmarks.kt index 9e712dd1d..dcb7a211b 100644 --- a/benchmarks/src/main/java/com/jerboa/benchmarks/ScrollCommentsBenchmarks.kt +++ b/benchmarks/src/main/java/com/jerboa/benchmarks/ScrollCommentsBenchmarks.kt @@ -7,6 +7,7 @@ import androidx.benchmark.macro.StartupMode import androidx.benchmark.macro.junit4.MacrobenchmarkRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import androidx.test.platform.app.InstrumentationRegistry import com.jerboa.actions.clickMostComments import com.jerboa.actions.closeChangeLogIfOpen import com.jerboa.actions.closePost @@ -34,7 +35,8 @@ class ScrollCommentsBenchmarks { private fun benchmark(compilationMode: CompilationMode) { rule.measureRepeated( - packageName = "com.jerboa", + packageName = InstrumentationRegistry.getArguments().getString("targetAppId") + ?: throw Exception("targetAppId not passed as instrumentation runner arg"), metrics = listOf(FrameTimingMetric()), compilationMode = compilationMode, startupMode = StartupMode.WARM, diff --git a/benchmarks/src/main/java/com/jerboa/benchmarks/ScrollPostsBenchmarks.kt b/benchmarks/src/main/java/com/jerboa/benchmarks/ScrollPostsBenchmarks.kt index 1858a2804..d4954bf10 100644 --- a/benchmarks/src/main/java/com/jerboa/benchmarks/ScrollPostsBenchmarks.kt +++ b/benchmarks/src/main/java/com/jerboa/benchmarks/ScrollPostsBenchmarks.kt @@ -7,6 +7,7 @@ import androidx.benchmark.macro.StartupMode import androidx.benchmark.macro.junit4.MacrobenchmarkRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import androidx.test.platform.app.InstrumentationRegistry import com.jerboa.actions.closeChangeLogIfOpen import com.jerboa.actions.scrollThroughPosts import com.jerboa.actions.waitUntilPostsActuallyVisible @@ -28,7 +29,8 @@ class ScrollPostsBenchmarks { private fun benchmark(compilationMode: CompilationMode) { rule.measureRepeated( - packageName = "com.jerboa", + packageName = InstrumentationRegistry.getArguments().getString("targetAppId") + ?: throw Exception("targetAppId not passed as instrumentation runner arg"), metrics = listOf(FrameTimingMetric()), compilationMode = compilationMode, startupMode = StartupMode.WARM, diff --git a/benchmarks/src/main/java/com/jerboa/benchmarks/TypicalUserJourneyBenchmarks.kt b/benchmarks/src/main/java/com/jerboa/benchmarks/TypicalUserJourneyBenchmarks.kt index 2a16cbbd0..d16673d07 100644 --- a/benchmarks/src/main/java/com/jerboa/benchmarks/TypicalUserJourneyBenchmarks.kt +++ b/benchmarks/src/main/java/com/jerboa/benchmarks/TypicalUserJourneyBenchmarks.kt @@ -7,6 +7,7 @@ import androidx.benchmark.macro.StartupMode import androidx.benchmark.macro.junit4.MacrobenchmarkRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest +import androidx.test.platform.app.InstrumentationRegistry import com.jerboa.actions.closeChangeLogIfOpen import com.jerboa.actions.doTypicalUserJourney import com.jerboa.actions.waitUntilLoadingDone @@ -29,7 +30,8 @@ class TypicalUserJourneyBenchmarks { private fun benchmark(compilationMode: CompilationMode) { rule.measureRepeated( - packageName = "com.jerboa", + packageName = InstrumentationRegistry.getArguments().getString("targetAppId") + ?: throw Exception("targetAppId not passed as instrumentation runner arg"), metrics = listOf(FrameTimingMetric()), compilationMode = compilationMode, startupMode = StartupMode.WARM,