mirror of
https://github.com/LiveFastEatTrashRaccoon/RaccoonForLemmy.git
synced 2025-02-01 22:17:05 +01:00
enhancement: user tags and roles management • iteration 2 (#225)
* exclude special tags from user detail even after creating new ones * prevent creation of tags with same name of an existing tag (case-insensitive) * load tags for main post in post detail screen * rename "User tags" to "User tags and roles" * introduce headers in tag list
This commit is contained in:
parent
67fbd75acc
commit
8bd1e7b0ad
@ -31,11 +31,14 @@ import com.livefast.eattrash.raccoonforlemmy.core.appearance.theme.Spacing
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.appearance.theme.toTypography
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.commonui.lemmyui.SettingsColorRow
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.l10n.LocalStrings
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.utils.ValidationError
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.utils.toReadableMessage
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun EditUserTagDialog(
|
||||
title: String,
|
||||
titleError: ValidationError? = null,
|
||||
value: String = "",
|
||||
canEditName: Boolean = true,
|
||||
color: Color = MaterialTheme.colorScheme.primary,
|
||||
@ -96,6 +99,15 @@ fun EditUserTagDialog(
|
||||
},
|
||||
textStyle = typography.bodyMedium,
|
||||
value = textFieldValue,
|
||||
isError = titleError != null,
|
||||
supportingText = {
|
||||
if (titleError != null) {
|
||||
Text(
|
||||
text = titleError.toReadableMessage(),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
|
@ -511,6 +511,8 @@ interface Strings {
|
||||
val userInfoModerates: String @Composable get
|
||||
val userPostScore: String @Composable get
|
||||
val userTagColor: String @Composable get
|
||||
val userTagsRegularSectionTitle: String @Composable get
|
||||
val userTagsSpecialSectionTitle: String @Composable get
|
||||
val userTagsTitle: String @Composable get
|
||||
|
||||
suspend fun inboxNotificationTitle(): String
|
||||
|
@ -456,7 +456,7 @@
|
||||
<string name="user_info_admin">administrator</string>
|
||||
<string name="user_info_moderates">Moderator of</string>
|
||||
<string name="user_tag_color">Tag color</string>
|
||||
<string name="user_tags_title">User tags</string>
|
||||
<string name="user_tags_title">User tags and roles</string>
|
||||
<string name="action_go_back">Go back</string>
|
||||
<string name="action_open_side_menu">Open side menu</string>
|
||||
<string name="action_close_side_menu">Close side menu</string>
|
||||
@ -510,6 +510,8 @@
|
||||
<string name="swipe_action_start_two">Start side (step 2)</string>
|
||||
<string name="swipe_action_end_one">End side(step 1)</string>
|
||||
<string name="swipe_action_end_two">End side (step 2)</string>
|
||||
<string name="user_tags_special_section_title">Special</string>
|
||||
<string name="user_tags_regular_section_title">Regular</string>
|
||||
|
||||
<plurals name="inbox_notification_content">
|
||||
<item quantity="one">There is %1$d unread item</item>
|
||||
|
@ -516,6 +516,8 @@ import raccoonforlemmy.shared.generated.resources.user_info_admin
|
||||
import raccoonforlemmy.shared.generated.resources.user_info_moderates
|
||||
import raccoonforlemmy.shared.generated.resources.user_post_score
|
||||
import raccoonforlemmy.shared.generated.resources.user_tag_color
|
||||
import raccoonforlemmy.shared.generated.resources.user_tags_regular_section_title
|
||||
import raccoonforlemmy.shared.generated.resources.user_tags_special_section_title
|
||||
import raccoonforlemmy.shared.generated.resources.user_tags_title
|
||||
|
||||
internal class SharedStrings : Strings {
|
||||
@ -1535,6 +1537,10 @@ internal class SharedStrings : Strings {
|
||||
@Composable get() = stringResource(Res.string.user_post_score)
|
||||
override val userTagColor: String
|
||||
@Composable get() = stringResource(Res.string.user_tag_color)
|
||||
override val userTagsRegularSectionTitle: String
|
||||
@Composable get() = stringResource(Res.string.user_tags_regular_section_title)
|
||||
override val userTagsSpecialSectionTitle: String
|
||||
@Composable get() = stringResource(Res.string.user_tags_special_section_title)
|
||||
override val userTagsTitle: String
|
||||
@Composable get() = stringResource(Res.string.user_tags_title)
|
||||
|
||||
|
@ -28,6 +28,7 @@ import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.repository.LemmyItemCa
|
||||
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.repository.LemmyValueCache
|
||||
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.repository.PostRepository
|
||||
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.repository.SiteRepository
|
||||
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.repository.UserTagHelper
|
||||
import com.livefast.eattrash.raccoonforlemmy.unit.postdetail.utils.populateLoadMoreComments
|
||||
import com.livefast.eattrash.raccoonforlemmy.unit.postdetail.utils.sortToNestedOrder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -59,6 +60,7 @@ class PostDetailViewModel(
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val userTagRepository: UserTagRepository,
|
||||
private val userTagHelper: UserTagHelper,
|
||||
private val shareHelper: ShareHelper,
|
||||
private val notificationCenter: NotificationCenter,
|
||||
private val hapticFeedback: HapticFeedback,
|
||||
@ -228,11 +230,16 @@ class PostDetailViewModel(
|
||||
|
||||
val auth = identityRepository.authToken.value
|
||||
val updatedPost =
|
||||
postRepository.get(
|
||||
id = postId,
|
||||
auth = auth,
|
||||
instance = otherInstance,
|
||||
)
|
||||
postRepository
|
||||
.get(
|
||||
id = postId,
|
||||
auth = auth,
|
||||
instance = otherInstance,
|
||||
)?.let {
|
||||
with(userTagHelper) {
|
||||
it.copy(creator = it.creator.withTags())
|
||||
}
|
||||
}
|
||||
if (updatedPost != null) {
|
||||
updateState {
|
||||
it.copy(post = updatedPost)
|
||||
@ -456,13 +463,18 @@ class PostDetailViewModel(
|
||||
val auth = identityRepository.authToken.value
|
||||
val post = uiState.value.post
|
||||
val updatedPost =
|
||||
postRepository.get(
|
||||
id = post.id,
|
||||
auth = auth,
|
||||
instance = otherInstance,
|
||||
) ?: post
|
||||
postRepository
|
||||
.get(
|
||||
id = post.id,
|
||||
auth = auth,
|
||||
instance = otherInstance,
|
||||
)?.let {
|
||||
with(userTagHelper) {
|
||||
it.copy(creator = it.creator.withTags())
|
||||
}
|
||||
}
|
||||
updateState {
|
||||
it.copy(post = updatedPost)
|
||||
it.copy(post = updatedPost ?: post)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -594,8 +606,14 @@ class PostDetailViewModel(
|
||||
|
||||
private fun handlePostUpdate(post: PostModel) {
|
||||
screenModelScope.launch {
|
||||
val newPost =
|
||||
post.let {
|
||||
with(userTagHelper) {
|
||||
it.copy(creator = it.creator.withTags())
|
||||
}
|
||||
}
|
||||
updateState {
|
||||
it.copy(post = post)
|
||||
it.copy(post = newPost)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ val postDetailModule =
|
||||
settingsRepository = instance(),
|
||||
accountRepository = instance(),
|
||||
userTagRepository = instance(),
|
||||
userTagHelper = instance(),
|
||||
shareHelper = instance(),
|
||||
notificationCenter = instance(),
|
||||
hapticFeedback = instance(),
|
||||
|
@ -98,6 +98,7 @@ import com.livefast.eattrash.raccoonforlemmy.core.notifications.NotificationCent
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.notifications.di.getNotificationCenter
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.ActionOnSwipe
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.persistence.di.getSettingsRepository
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.utils.ValidationError
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.utils.VoteAction
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.utils.toIcon
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.utils.toLocalDp
|
||||
@ -173,6 +174,7 @@ class UserDetailScreen(
|
||||
var copyPostBottomSheet by remember { mutableStateOf<PostModel?>(null) }
|
||||
var manageUserTagsBottomSheetOpened by remember { mutableStateOf(false) }
|
||||
var addNewUserTagDialogOpen by remember { mutableStateOf(false) }
|
||||
var addNewUserTagTitleError by remember { mutableStateOf<ValidationError?>(null) }
|
||||
|
||||
LaunchedEffect(model) {
|
||||
model.effects
|
||||
@ -1312,18 +1314,42 @@ class UserDetailScreen(
|
||||
}
|
||||
|
||||
if (addNewUserTagDialogOpen) {
|
||||
val forbiddenTagNames =
|
||||
buildList {
|
||||
addAll(
|
||||
listOf(
|
||||
LocalStrings.current.defaultTagAdmin,
|
||||
LocalStrings.current.defaultTagBot,
|
||||
LocalStrings.current.defaultTagCurrentUser,
|
||||
LocalStrings.current.defaultTagModerator,
|
||||
LocalStrings.current.defaultTagOriginalPoster,
|
||||
).map { it.lowercase() },
|
||||
)
|
||||
addAll(
|
||||
uiState.availableUserTags.map { it.name.lowercase() },
|
||||
)
|
||||
}
|
||||
EditUserTagDialog(
|
||||
title = LocalStrings.current.buttonAdd,
|
||||
titleError = addNewUserTagTitleError,
|
||||
value = "",
|
||||
onClose = { name, color ->
|
||||
addNewUserTagDialogOpen = false
|
||||
if (name != null) {
|
||||
model.reduce(
|
||||
UserDetailMviModel.Intent.AddUserTag(
|
||||
name = name,
|
||||
color = color?.toArgb(),
|
||||
),
|
||||
)
|
||||
addNewUserTagTitleError =
|
||||
if (name?.lowercase() in forbiddenTagNames) {
|
||||
ValidationError.InvalidField
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (addNewUserTagTitleError == null) {
|
||||
addNewUserTagDialogOpen = false
|
||||
if (name != null) {
|
||||
model.reduce(
|
||||
UserDetailMviModel.Intent.AddUserTag(
|
||||
name = name,
|
||||
color = color?.toArgb(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -688,9 +688,9 @@ class UserDetailViewModel(
|
||||
val accountId = accountRepository.getActive()?.id ?: return@launch
|
||||
val model = UserTagModel(name = name, color = color)
|
||||
userTagRepository.create(model = model, accountId = accountId)
|
||||
val allTags = userTagRepository.getAll(accountId)
|
||||
val tags = userTagRepository.getAll(accountId).filter { !it.isSpecial }
|
||||
updateState {
|
||||
it.copy(availableUserTags = allTags)
|
||||
it.copy(availableUserTags = tags)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,8 @@ interface UserTagsMviModel :
|
||||
data class UiState(
|
||||
val initial: Boolean = true,
|
||||
val refreshing: Boolean = false,
|
||||
val tags: List<UserTagModel> = emptyList(),
|
||||
val specialTags: List<UserTagModel> = emptyList(),
|
||||
val regularTags: List<UserTagModel> = emptyList(),
|
||||
)
|
||||
|
||||
sealed interface Effect {
|
||||
|
@ -47,6 +47,7 @@ import com.livefast.eattrash.raccoonforlemmy.core.commonui.components.FloatingAc
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.commonui.lemmyui.CommunityItemPlaceholder
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.commonui.lemmyui.Option
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.commonui.lemmyui.OptionId
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.commonui.lemmyui.SettingsHeader
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.commonui.lemmyui.UserTagItem
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.commonui.lemmyui.di.getFabNestedScrollConnection
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.commonui.modals.EditUserTagDialog
|
||||
@ -55,6 +56,7 @@ import com.livefast.eattrash.raccoonforlemmy.core.navigation.di.getNavigationCoo
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.UserTagModel
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.UserTagType
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.isSpecial
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.utils.ValidationError
|
||||
import com.livefast.eattrash.raccoonforlemmy.core.utils.compose.onClick
|
||||
import com.livefast.eattrash.raccoonforlemmy.unit.usertags.detail.UserTagDetailScreen
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@ -75,7 +77,9 @@ class UserTagsScreen : Screen {
|
||||
val lazyListState = rememberLazyListState()
|
||||
val scope = rememberCoroutineScope()
|
||||
var addTagDialogOpen by remember { mutableStateOf(false) }
|
||||
var addTagTitleError by remember { mutableStateOf<ValidationError?>(null) }
|
||||
var tagToEdit by remember { mutableStateOf<UserTagModel?>(null) }
|
||||
var editTagTitleError by remember { mutableStateOf<ValidationError?>(null) }
|
||||
|
||||
LaunchedEffect(model) {
|
||||
model.effects
|
||||
@ -181,16 +185,50 @@ class UserTagsScreen : Screen {
|
||||
CommunityItemPlaceholder()
|
||||
}
|
||||
}
|
||||
items(uiState.tags) { tag ->
|
||||
|
||||
if (uiState.specialTags.isNotEmpty()) {
|
||||
item {
|
||||
SettingsHeader(
|
||||
title = LocalStrings.current.userTagsSpecialSectionTitle,
|
||||
)
|
||||
}
|
||||
}
|
||||
items(uiState.specialTags) { tag ->
|
||||
UserTagItem(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
tag = tag,
|
||||
options =
|
||||
buildList {
|
||||
this +=
|
||||
Option(
|
||||
id = OptionId.Edit,
|
||||
text = LocalStrings.current.postActionEdit,
|
||||
)
|
||||
},
|
||||
onOptionSelected = { optionId ->
|
||||
when (optionId) {
|
||||
OptionId.Edit -> tagToEdit = tag
|
||||
else -> Unit
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if (uiState.regularTags.isNotEmpty()) {
|
||||
item {
|
||||
SettingsHeader(
|
||||
title = LocalStrings.current.userTagsRegularSectionTitle,
|
||||
)
|
||||
}
|
||||
}
|
||||
items(uiState.regularTags) { tag ->
|
||||
UserTagItem(
|
||||
modifier =
|
||||
Modifier.fillMaxWidth().onClick(
|
||||
onClick = {
|
||||
if (!tag.isSpecial) {
|
||||
tag.id?.also {
|
||||
val screen = UserTagDetailScreen(it)
|
||||
navigatorCoordinator.pushScreen(screen)
|
||||
}
|
||||
tag.id?.also {
|
||||
val screen = UserTagDetailScreen(it)
|
||||
navigatorCoordinator.pushScreen(screen)
|
||||
}
|
||||
},
|
||||
),
|
||||
@ -202,18 +240,15 @@ class UserTagsScreen : Screen {
|
||||
id = OptionId.Edit,
|
||||
text = LocalStrings.current.postActionEdit,
|
||||
)
|
||||
if (!tag.isSpecial) {
|
||||
this +=
|
||||
Option(
|
||||
id = OptionId.Delete,
|
||||
text = LocalStrings.current.commentActionDelete,
|
||||
)
|
||||
}
|
||||
this +=
|
||||
Option(
|
||||
id = OptionId.Delete,
|
||||
text = LocalStrings.current.commentActionDelete,
|
||||
)
|
||||
},
|
||||
onOptionSelected = { optionId ->
|
||||
when (optionId) {
|
||||
OptionId.Edit ->
|
||||
tagToEdit = tag
|
||||
OptionId.Edit -> tagToEdit = tag
|
||||
|
||||
OptionId.Delete ->
|
||||
tag.id?.also {
|
||||
@ -225,7 +260,7 @@ class UserTagsScreen : Screen {
|
||||
},
|
||||
)
|
||||
}
|
||||
if (uiState.tags.isEmpty() && !uiState.initial) {
|
||||
if ((uiState.specialTags + uiState.regularTags).isEmpty() && !uiState.initial) {
|
||||
item {
|
||||
Text(
|
||||
modifier =
|
||||
@ -247,41 +282,71 @@ class UserTagsScreen : Screen {
|
||||
}
|
||||
|
||||
if (addTagDialogOpen) {
|
||||
val forbiddenTagNames =
|
||||
(uiState.specialTags + uiState.regularTags).map {
|
||||
it.name.lowercase()
|
||||
}
|
||||
EditUserTagDialog(
|
||||
title = LocalStrings.current.buttonAdd,
|
||||
titleError = addTagTitleError,
|
||||
value = "",
|
||||
onClose = { name, color ->
|
||||
addTagDialogOpen = false
|
||||
if (name != null) {
|
||||
model.reduce(
|
||||
UserTagsMviModel.Intent.Add(
|
||||
name = name,
|
||||
color = color?.toArgb(),
|
||||
),
|
||||
)
|
||||
addTagTitleError =
|
||||
if (name?.lowercase() in forbiddenTagNames) {
|
||||
ValidationError.InvalidField
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (addTagTitleError == null) {
|
||||
addTagDialogOpen = false
|
||||
if (name != null) {
|
||||
model.reduce(
|
||||
UserTagsMviModel.Intent.Add(
|
||||
name = name,
|
||||
color = color?.toArgb(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
if (tagToEdit != null) {
|
||||
val forbiddenTagNames =
|
||||
(uiState.specialTags + uiState.regularTags).mapNotNull {
|
||||
if (it.id != tagToEdit?.id) {
|
||||
it.name.lowercase()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
EditUserTagDialog(
|
||||
title = LocalStrings.current.postActionEdit,
|
||||
titleError = editTagTitleError,
|
||||
value = tagToEdit?.name.orEmpty(),
|
||||
canEditName = tagToEdit?.isSpecial != true,
|
||||
color = tagToEdit?.color?.let { Color(it) } ?: MaterialTheme.colorScheme.primary,
|
||||
onClose = { name, color ->
|
||||
val tagId = tagToEdit?.id
|
||||
val type = tagToEdit?.type ?: UserTagType.Regular
|
||||
tagToEdit = null
|
||||
if (tagId != null && name != null) {
|
||||
model.reduce(
|
||||
UserTagsMviModel.Intent.Edit(
|
||||
id = tagId,
|
||||
name = name,
|
||||
type = type,
|
||||
color = color?.toArgb(),
|
||||
),
|
||||
)
|
||||
editTagTitleError =
|
||||
if (name?.lowercase() in forbiddenTagNames) {
|
||||
ValidationError.InvalidField
|
||||
} else {
|
||||
null
|
||||
}
|
||||
if (editTagTitleError == null) {
|
||||
val tagId = tagToEdit?.id
|
||||
val type = tagToEdit?.type ?: UserTagType.Regular
|
||||
tagToEdit = null
|
||||
if (tagId != null && name != null) {
|
||||
model.reduce(
|
||||
UserTagsMviModel.Intent.Edit(
|
||||
id = tagId,
|
||||
name = name,
|
||||
type = type,
|
||||
color = color?.toArgb(),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
@ -53,18 +53,17 @@ internal class UserTagsViewModel(
|
||||
refreshing = !initial,
|
||||
)
|
||||
}
|
||||
val tags =
|
||||
val (specialTags, regularTags) =
|
||||
userTagRepository
|
||||
.getAll(accountId)
|
||||
.partition { it.isSpecial }
|
||||
.let { (specialTags, regularTags) ->
|
||||
val sortedSpecial = specialTags.sortedBy { it.name }
|
||||
val sortedRegular = regularTags.sortedBy { it.name }
|
||||
sortedSpecial + sortedRegular
|
||||
specialTags.sortedBy { it.name } to regularTags.sortedBy { it.name }
|
||||
}
|
||||
updateState {
|
||||
it.copy(
|
||||
tags = tags,
|
||||
specialTags = specialTags,
|
||||
regularTags = regularTags,
|
||||
initial = false,
|
||||
refreshing = false,
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user