feat: user info bottom sheet; closes #342 (#367)

This commit is contained in:
Diego Beraldin 2023-12-24 14:21:22 +01:00 committed by GitHub
parent 8290e5ed99
commit ab5e45f7b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 703 additions and 33 deletions

View File

@ -1,4 +1,4 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.communityinfo
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
@ -15,7 +15,7 @@ import androidx.compose.ui.text.withStyle
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
@Composable
internal fun CommunityInfoItem(
fun DetailInfoItem(
modifier: Modifier = Modifier,
icon: ImageVector? = null,
title: String = "",

View File

@ -8,12 +8,14 @@ data class UserModel(
val name: String = "",
val displayName: String = "",
val avatar: String? = null,
val bio: String? = null,
val banner: String? = null,
val host: String = "",
val score: UserScoreModel? = null,
val accountAge: String = "",
val banned: Boolean = false,
val updateDate: String? = null,
val admin: Boolean = false,
) : JavaSerializable
fun List<UserModel>.containsId(value: Int?): Boolean = any { it.id == value }

View File

@ -112,7 +112,6 @@ internal class DefaultCommunityRepository(
}.orEmpty()
}.getOrElse { emptyList() }
override suspend fun subscribe(
auth: String?,
id: Int,

View File

@ -6,13 +6,13 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.MarkCommentAsReadFo
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.MarkPersonMentionAsReadForm
import com.github.diegoberaldin.raccoonforlemmy.core.api.provider.ServiceProvider
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PersonMentionModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toAuthHeader
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toCommentDto
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toHost
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toModel
internal class DefaultUserRepository(
@ -39,15 +39,7 @@ internal class DefaultUserRepository(
)
}
val dto = response.body() ?: return@runCatching null
UserModel(
id = dto.personView.person.id,
name = dto.personView.person.name,
avatar = dto.personView.person.avatar,
banner = dto.personView.person.banner,
host = dto.personView.person.actorId.toHost(),
score = dto.personView.counts.toModel(),
accountAge = dto.personView.person.published,
)
dto.personView.toModel()
}.getOrNull()
override suspend fun getPosts(
@ -237,4 +229,18 @@ internal class DefaultUserRepository(
form = data,
)
}
override suspend fun getModeratedCommunities(
auth: String?,
id: Int?,
): List<CommunityModel> = runCatching {
val response = services.user.getDetails(
authHeader = auth.toAuthHeader(),
auth = auth,
personId = id,
).body()
response?.moderates?.map {
it.community.toModel()
}.orEmpty()
}.getOrElse { emptyList() }
}

View File

@ -3,6 +3,7 @@ package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CommentReplyResponse
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PersonMentionResponse
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PersonMentionModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
@ -77,14 +78,19 @@ interface UserRepository {
suspend fun setMentionRead(
read: Boolean,
mentionId: Int,
auth: String? = null
auth: String? = null,
): Result<Response<PersonMentionResponse>>
suspend fun setReplyRead(
read: Boolean,
replyId: Int,
auth: String? = null
auth: String? = null,
): Result<Response<CommentReplyResponse>>
suspend fun block(id: Int, blocked: Boolean, auth: String? = null): Result<Unit>
suspend fun getModeratedCommunities(
auth: String? = null,
id: Int?,
): List<CommunityModel>
}

View File

@ -111,7 +111,9 @@ internal fun Person.toModel() = UserModel(
host = actorId.toHost(),
accountAge = published,
banned = banned,
bio = bio,
updateDate = updated,
admin = admin ?: false,
)
internal fun PersonView.toModel() = person.toModel()

View File

@ -280,4 +280,7 @@
<string name="modlog_item_community_transfer">تم نقل المجتمع إلى</string>
<string name="block_action_user">مستخدم محضور</string>
<string name="block_action_community">منع المجتمع</string>
<string name="user_detail_info">معلومات المستخدم</string>
<string name="user_info_moderates">مشرف</string>
<string name="user_info_admin">مدير</string>
</resources>

View File

@ -311,4 +311,7 @@
<string name="modlog_item_community_transfer">the community was transferred to</string>
<string name="block_action_user">Block user</string>
<string name="block_action_community">Block community</string>
<string name="user_detail_info">User info</string>
<string name="user_info_moderates">Moderator of</string>
<string name="user_info_admin">administrator</string>
</resources>

View File

@ -291,4 +291,7 @@
<string name="modlog_item_community_transfer">общността беше прехвърлена на</string>
<string name="block_action_user">Блокиране на потребител</string>
<string name="block_action_community">Блокиране на общността</string>
<string name="user_detail_info">Информация за потребителя</string>
<string name="user_info_moderates">Модератор на</string>
<string name="user_info_admin">администратор</string>
</resources>

View File

@ -282,4 +282,7 @@
<string name="modlog_item_community_transfer">komunita byla převedena do</string>
<string name="block_action_user">Blokovat uživatele</string>
<string name="block_action_community">Blokovat komunitu</string>
<string name="user_detail_info">Uživatelské informace</string>
<string name="user_info_moderates">Moderátor</string>
<string name="user_info_admin">správce</string>
</resources>

View File

@ -282,4 +282,7 @@
<string name="modlog_item_community_transfer">samfundet blev overført til</string>
<string name="block_action_user">Bloker bruger</string>
<string name="block_action_community">Bloker fællesskabet</string>
<string name="user_detail_info">Brugeroplysninger</string>
<string name="user_info_moderates">Moderator af</string>
<string name="user_info_admin">administrator</string>
</resources>

View File

@ -290,4 +290,7 @@
<string name="modlog_item_community_transfer">die Gemeinde wurde übertragen</string>
<string name="block_action_user">Benutzer blockieren</string>
<string name="block_action_community">Community blockieren</string>
<string name="user_detail_info">Nutzerinformation</string>
<string name="user_info_moderates">Moderator von</string>
<string name="user_info_admin">Administrator</string>
</resources>

View File

@ -292,4 +292,7 @@
<string name="modlog_item_community_transfer">η κοινότητα μεταφέρθηκε σε</string>
<string name="block_action_user">Αποκλεισμός χρήστη</string>
<string name="block_action_community">Αποκλεισμός κοινότητας</string>
<string name="user_detail_info">Πληροφορίες χρήστη</string>
<string name="user_info_moderates">Συντονιστής των</string>
<string name="user_info_admin">διαχειριστής</string>
</resources>

View File

@ -281,4 +281,7 @@
<string name="modlog_item_community_transfer">la komunumo estis translokigita al</string>
<string name="block_action_user">Bloki uzanton</string>
<string name="block_action_community">Bloki komunumon</string>
<string name="user_detail_info">Detaloj pri la uzanto</string>
<string name="user_info_moderates">Moderatoro de</string>
<string name="user_info_admin">administranto</string>
</resources>

View File

@ -288,4 +288,7 @@
<string name="modlog_item_community_transfer">la comunidad se trasladó a</string>
<string name="block_action_user">Bloquear usuario</string>
<string name="block_action_community">Bloquear comunidad</string>
<string name="user_detail_info">Informacion sobre el usuario</string>
<string name="user_info_moderates">Moderador de</string>
<string name="user_info_admin">administrador</string>
</resources>

View File

@ -282,4 +282,7 @@
<string name="modlog_item_community_transfer">kogukond viidi üle</string>
<string name="block_action_user">Blokeeri kasutaja</string>
<string name="block_action_community">Blokeeri kogukond</string>
<string name="user_detail_info">Kasutajateave</string>
<string name="user_info_moderates">Moderaator</string>
<string name="user_info_admin">administraator</string>
</resources>

View File

@ -282,4 +282,7 @@
<string name="modlog_item_community_transfer">yhteisö siirrettiin</string>
<string name="block_action_user">Estä käyttäjä</string>
<string name="block_action_community">Estä yhteisö</string>
<string name="user_detail_info">Käyttäjän tiedot</string>
<string name="user_info_moderates">Moderaattori</string>
<string name="user_info_admin">järjestelmänvalvoja</string>
</resources>

View File

@ -288,4 +288,7 @@
<string name="modlog_item_community_transfer">la communauté a été transférée à</string>
<string name="block_action_user">Bloquer l\'utilisateur</string>
<string name="block_action_community">Bloquer la communauté</string>
<string name="user_detail_info">Informations de l\'utilisateur</string>
<string name="user_info_moderates">Modérateur de</string>
<string name="user_info_admin">administrateur</string>
</resources>

View File

@ -291,4 +291,7 @@
<string name="modlog_item_community_transfer">aistríodh an pobal go</string>
<string name="block_action_user">Cuir bac ar úsáideoir</string>
<string name="block_action_community">Bloc pobail</string>
<string name="user_detail_info">Eolas úsáideora</string>
<string name="user_info_moderates">Modhnóir na</string>
<string name="user_info_admin">riarthóir</string>
</resources>

View File

@ -287,4 +287,7 @@
<string name="modlog_item_community_transfer">zajednica je prebačena u</string>
<string name="block_action_user">Blokirati korisnika</string>
<string name="block_action_community">Blokiraj zajednicu</string>
<string name="user_detail_info">Informacije o korisniku</string>
<string name="user_info_moderates">Moderator od</string>
<string name="user_info_admin">administrator</string>
</resources>

View File

@ -286,4 +286,7 @@
<string name="modlog_item_community_transfer">a közösség átkerült</string>
<string name="block_action_user">Felhasználó letiltása</string>
<string name="block_action_community">Közösség letiltása</string>
<string name="user_detail_info">Felhasználói információ</string>
<string name="user_info_moderates">Moderátora</string>
<string name="user_info_admin">adminisztrátor</string>
</resources>

View File

@ -286,4 +286,7 @@
<string name="modlog_item_community_transfer">la comunità è stata trasferita a</string>
<string name="block_action_user">Blocca utente</string>
<string name="block_action_community">Blocca comunità</string>
<string name="user_detail_info">Info utente</string>
<string name="user_info_moderates">Moderatore di</string>
<string name="user_info_admin">amministratore</string>
</resources>

View File

@ -285,4 +285,7 @@
<string name="modlog_item_community_transfer">bendruomenė buvo perduota</string>
<string name="block_action_user">Blokuoti vartotoją</string>
<string name="block_action_community">Blokuoti bendruomenę</string>
<string name="user_detail_info">Vartotojo informacija</string>
<string name="user_info_moderates">Moderatorius</string>
<string name="user_info_admin">administratorius</string>
</resources>

View File

@ -286,4 +286,7 @@
<string name="modlog_item_community_transfer">kopiena tika pārcelta uz</string>
<string name="block_action_user">Bloķēt lietotāju</string>
<string name="block_action_community">Bloķēt kopienu</string>
<string name="user_detail_info">Lietotāja informācija</string>
<string name="user_info_moderates">Moderators no</string>
<string name="user_info_admin">administrators</string>
</resources>

View File

@ -287,4 +287,7 @@
<string name="modlog_item_community_transfer">il-komunità ġiet trasferita</string>
<string name="block_action_user">Blokk utent</string>
<string name="block_action_community">Blokk komunità</string>
<string name="user_detail_info">Informazzjoni għall-utent</string>
<string name="user_info_moderates">Moderatur ta\'</string>
<string name="user_info_admin">amministratur</string>
</resources>

View File

@ -286,4 +286,7 @@
<string name="modlog_item_community_transfer">de gemeenschap werd overgedragen</string>
<string name="block_action_user">Blokkeer gebruiker</string>
<string name="block_action_community">Blokkeer gemeenschap</string>
<string name="user_detail_info">Gebruikers informatie</string>
<string name="user_info_moderates">Moderator van</string>
<string name="user_info_admin">beheerder</string>
</resources>

View File

@ -285,4 +285,7 @@
<string name="modlog_item_community_transfer">fellesskapet ble overført til</string>
<string name="block_action_user">Blokker bruker</string>
<string name="block_action_community">Blokker fellesskapet</string>
<string name="user_detail_info">Brukerinformasjon</string>
<string name="user_info_moderates">Moderator for</string>
<string name="user_info_admin">administrator</string>
</resources>

View File

@ -285,4 +285,7 @@
<string name="modlog_item_community_transfer">gmina została przeniesiona</string>
<string name="block_action_user">Zablokuj użytkownika</string>
<string name="block_action_community">Zablokuj społeczność</string>
<string name="user_detail_info">Informacje o użytkowniku</string>
<string name="user_info_moderates">Moderator</string>
<string name="user_info_admin">administrator</string>
</resources>

View File

@ -285,4 +285,7 @@
<string name="modlog_item_community_transfer">a comunidade foi transferida para</string>
<string name="block_action_user">Bloquear usuário</string>
<string name="block_action_community">Bloquear comunidade</string>
<string name="user_detail_info">Informação do usuário</string>
<string name="user_info_moderates">Moderador de</string>
<string name="user_info_admin">administrador</string>
</resources>

View File

@ -283,4 +283,7 @@
<string name="modlog_item_community_transfer">comunitatea a fost transferată la</string>
<string name="block_action_user">Blochează utilizatorul</string>
<string name="block_action_community">Blochează comunitatea</string>
<string name="user_detail_info">Informații despre utilizatorul</string>
<string name="user_info_moderates">Moderator al</string>
<string name="user_info_admin">administrator</string>
</resources>

View File

@ -286,4 +286,7 @@
<string name="modlog_item_community_transfer">община была переведена в</string>
<string name="block_action_user">Заблокировать пользователя</string>
<string name="block_action_community">Заблокировать сообщество</string>
<string name="user_detail_info">Информация о пользователе</string>
<string name="user_info_moderates">Модератор</string>
<string name="user_info_admin">администратор</string>
</resources>

View File

@ -283,4 +283,7 @@
<string name="modlog_item_community_transfer">samhället överfördes till</string>
<string name="block_action_user">Blockera användare</string>
<string name="block_action_community">Blockera gemenskap</string>
<string name="user_detail_info">Användar information</string>
<string name="user_info_moderates">Moderator för</string>
<string name="user_info_admin">administratör</string>
</resources>

View File

@ -284,4 +284,7 @@
<string name="modlog_item_community_transfer">komunita bola prevedená na</string>
<string name="block_action_user">Blokovať používateľa</string>
<string name="block_action_community">Blokovať komunitu</string>
<string name="user_detail_info">Informácie o používateľovi</string>
<string name="user_info_moderates">Moderátorka</string>
<string name="user_info_admin">správca</string>
</resources>

View File

@ -282,4 +282,7 @@
<string name="modlog_item_community_transfer">skupnost je bila prenesena na</string>
<string name="block_action_user">Blokiraj uporabnika</string>
<string name="block_action_community">Blokiraj skupnost</string>
<string name="user_detail_info">Informacije o uporabniku</string>
<string name="user_info_moderates">Moderator od</string>
<string name="user_info_admin">skrbnik</string>
</resources>

View File

@ -288,4 +288,7 @@
<string name="modlog_item_community_transfer">komuniteti u transferua në</string>
<string name="block_action_user">Blloko përdoruesin</string>
<string name="block_action_community">Blloko komunitetin</string>
<string name="user_detail_info">Informacioni i përdoruesit</string>
<string name="user_info_moderates">Moderator i</string>
<string name="user_info_admin">administratori</string>
</resources>

View File

@ -285,4 +285,7 @@
<string name="modlog_item_community_transfer">topluluk şuraya transfer edildi</string>
<string name="block_action_user">Kullanıcıyı engelle</string>
<string name="block_action_community">Topluluğu engelle</string>
<string name="user_detail_info">Kullanıcı bilgisi</string>
<string name="user_info_moderates">Moderatörü</string>
<string name="user_info_admin">yönetici</string>
</resources>

View File

@ -285,4 +285,7 @@
<string name="modlog_item_community_transfer">громаду було передано с</string>
<string name="block_action_user">Заблокувати користувача</string>
<string name="block_action_community">Заблокувати спільноту</string>
<string name="user_detail_info">Інформація про користувача</string>
<string name="user_info_moderates">Модератор</string>
<string name="user_info_admin">адміністратор</string>
</resources>

View File

@ -81,3 +81,4 @@ include(":unit:replies")
include(":unit:mentions")
include(":unit:messages")
include(":unit:modlog")
include(":unit:userinfo")

View File

@ -75,6 +75,7 @@ kotlin {
implementation(projects.unit.postdetail)
implementation(projects.unit.communitydetail)
implementation(projects.unit.userdetail)
implementation(projects.unit.userinfo)
implementation(projects.unit.managesubscriptions)
implementation(projects.unit.multicommunity)
implementation(projects.unit.modlog)

View File

@ -40,6 +40,7 @@ import com.github.diegoberaldin.raccoonforlemmy.unit.reportlist.di.reportListMod
import com.github.diegoberaldin.raccoonforlemmy.unit.saveditems.di.savedItemsModule
import com.github.diegoberaldin.raccoonforlemmy.unit.selectcommunity.di.selectCommunityModule
import com.github.diegoberaldin.raccoonforlemmy.unit.userdetail.di.userDetailModule
import com.github.diegoberaldin.raccoonforlemmy.unit.userinfo.di.userInfoModule
import com.github.diegoberaldin.raccoonforlemmy.unit.zoomableimage.di.zoomableImageModule
import org.koin.dsl.module
@ -85,6 +86,7 @@ val sharedHelperModule = module {
postDetailModule,
communityDetailModule,
userDetailModule,
userInfoModule,
manageSubscriptionsModule,
modlogModule,
)

View File

@ -40,6 +40,7 @@ import com.github.diegoberaldin.raccoonforlemmy.unit.reportlist.di.reportListMod
import com.github.diegoberaldin.raccoonforlemmy.unit.saveditems.di.savedItemsModule
import com.github.diegoberaldin.raccoonforlemmy.unit.selectcommunity.di.selectCommunityModule
import com.github.diegoberaldin.raccoonforlemmy.unit.userdetail.di.userDetailModule
import com.github.diegoberaldin.raccoonforlemmy.unit.userinfo.di.userInfoModule
import com.github.diegoberaldin.raccoonforlemmy.unit.zoomableimage.di.zoomableImageModule
import org.koin.core.context.startKoin
import platform.Foundation.NSBundle
@ -86,6 +87,7 @@ fun initKoin() {
postDetailModule,
communityDetailModule,
userDetailModule,
userInfoModule,
manageSubscriptionsModule,
modlogModule,
)

View File

@ -38,6 +38,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycl
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.BottomSheetHandle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CustomizedContent
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.detailopener.api.getDetailOpener
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.DetailInfoItem
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.PostCardBody
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallbackArgs
@ -45,6 +46,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.utils.datetime.prettifyDate
import com.github.diegoberaldin.raccoonforlemmy.core.utils.getPrettyNumber
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import com.github.diegoberaldin.raccoonforlemmy.unit.communityinfo.components.ModeratorCell
import com.github.diegoberaldin.raccoonforlemmy.unit.communityinfo.di.getCommunityInfoViewModel
import com.github.diegoberaldin.raccoonforlemmy.unit.web.WebViewScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.zoomableimage.ZoomableImageScreen
@ -113,12 +115,12 @@ class CommunityInfoScreen(
Column(
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
CommunityInfoItem(
DetailInfoItem(
modifier = Modifier.fillMaxWidth(),
icon = Icons.Default.Cake,
title = community.creationDate?.prettifyDate().orEmpty(),
)
CommunityInfoItem(
DetailInfoItem(
modifier = Modifier.fillMaxWidth(),
icon = Icons.Default.Padding,
title = stringResource(MR.strings.community_info_posts),
@ -127,7 +129,7 @@ class CommunityInfoScreen(
millionLabel = stringResource(MR.strings.profile_million_short),
),
)
CommunityInfoItem(
DetailInfoItem(
modifier = Modifier.fillMaxWidth(),
icon = Icons.Default.Reply,
title = stringResource(MR.strings.community_info_comments),
@ -136,7 +138,7 @@ class CommunityInfoScreen(
millionLabel = stringResource(MR.strings.profile_million_short),
),
)
CommunityInfoItem(
DetailInfoItem(
modifier = Modifier.fillMaxWidth(),
icon = Icons.Default.Group,
title = stringResource(MR.strings.community_info_subscribers),
@ -145,7 +147,7 @@ class CommunityInfoScreen(
millionLabel = stringResource(MR.strings.profile_million_short),
),
)
CommunityInfoItem(
DetailInfoItem(
modifier = Modifier.fillMaxWidth(),
icon = Icons.Default.CalendarViewMonth,
title = stringResource(MR.strings.community_info_monthly_active_users),
@ -154,7 +156,7 @@ class CommunityInfoScreen(
millionLabel = stringResource(MR.strings.profile_million_short),
),
)
CommunityInfoItem(
DetailInfoItem(
modifier = Modifier.fillMaxWidth(),
icon = Icons.Default.CalendarViewWeek,
title = stringResource(MR.strings.community_info_weekly_active_users),
@ -163,7 +165,7 @@ class CommunityInfoScreen(
millionLabel = stringResource(MR.strings.profile_million_short),
),
)
CommunityInfoItem(
DetailInfoItem(
modifier = Modifier.fillMaxWidth(),
icon = Icons.Default.CalendarViewDay,
title = stringResource(MR.strings.community_info_daily_active_users),

View File

@ -1,4 +1,4 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.communityinfo
package com.github.diegoberaldin.raccoonforlemmy.unit.communityinfo.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.IconSize
@ -30,9 +31,9 @@ internal fun ModeratorCell(
autoLoadImages: Boolean = true,
onOpenUser: ((UserModel) -> Unit)? = null,
) {
val creatorName = user?.name.orEmpty()
val creatorHost = user?.host.orEmpty()
val creatorAvatar = user?.avatar.orEmpty()
val creatorName = user.name
val creatorHost = user.host
val creatorAvatar = user.avatar.orEmpty()
val iconSize = IconSize.xl
val fullTextColor = MaterialTheme.colorScheme.onBackground
val ancillaryColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f)
@ -49,9 +50,7 @@ internal fun ModeratorCell(
.clip(RoundedCornerShape(iconSize / 2))
.onClick(
onClick = rememberCallback {
if (user != null) {
onOpenUser?.invoke(user)
}
onOpenUser?.invoke(user)
},
),
quality = FilterQuality.Low,
@ -64,9 +63,7 @@ internal fun ModeratorCell(
PlaceholderImage(
modifier = Modifier.onClick(
onClick = rememberCallback {
if (user != null) {
onOpenUser?.invoke(user)
}
onOpenUser?.invoke(user)
},
),
size = iconSize,
@ -80,6 +77,7 @@ internal fun ModeratorCell(
Text(
modifier = Modifier.widthIn(max = 100.dp),
text = creatorName,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelMedium,
color = fullTextColor,
maxLines = 1,
@ -89,9 +87,10 @@ internal fun ModeratorCell(
Text(
modifier = Modifier.widthIn(max = 100.dp),
text = creatorHost,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelSmall,
color = ancillaryColor,
maxLines = 1,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}

View File

@ -58,6 +58,7 @@ kotlin {
implementation(projects.unit.remove)
implementation(projects.unit.ban)
implementation(projects.unit.chat)
implementation(projects.unit.userinfo)
implementation(projects.domain.identity)
implementation(projects.domain.lemmy.data)

View File

@ -104,6 +104,7 @@ import com.github.diegoberaldin.raccoonforlemmy.unit.createcomment.CreateComment
import com.github.diegoberaldin.raccoonforlemmy.unit.createpost.CreatePostScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createreport.CreateReportScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.userdetail.di.getUserDetailViewModel
import com.github.diegoberaldin.raccoonforlemmy.unit.userinfo.UserInfoScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.web.WebViewScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.zoomableimage.ZoomableImageScreen
import dev.icerock.moko.resources.compose.stringResource
@ -216,6 +217,10 @@ class UserDetailScreen(
// options menu
Box {
val options = listOf(
Option(
OptionId.Info,
stringResource(MR.strings.user_detail_info)
),
Option(
OptionId.Block,
stringResource(MR.strings.community_detail_block)
@ -266,6 +271,12 @@ class UserDetailScreen(
UserDetailMviModel.Intent.Block
)
OptionId.Info -> {
navigationCoordinator.showBottomSheet(
UserInfoScreen(uiState.user),
)
}
else -> Unit
}
},

View File

@ -0,0 +1,75 @@
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.android.library)
alias(libs.plugins.compose)
}
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
kotlin {
applyDefaultHierarchyTemplate()
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "userinfo"
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.material3)
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
implementation(compose.components.resources)
implementation(compose.materialIconsExtended)
implementation(libs.koin.core)
implementation(libs.voyager.screenmodel)
implementation(libs.voyager.navigator)
implementation(projects.core.utils)
implementation(projects.core.appearance)
implementation(projects.core.architecture)
implementation(projects.core.commonui.components)
implementation(projects.core.commonui.lemmyui)
implementation(projects.unit.zoomableimage)
implementation(projects.unit.web)
implementation(projects.core.commonui.detailopenerApi)
implementation(projects.core.navigation)
implementation(projects.core.persistence)
implementation(projects.domain.identity)
implementation(projects.domain.lemmy.data)
implementation(projects.domain.lemmy.repository)
implementation(projects.resources)
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
}
}
android {
namespace = "com.github.diegoberaldin.raccoonforlemmy.unit.userinfo"
compileSdk = libs.versions.android.targetSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
}
}

View File

@ -0,0 +1,13 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.userinfo.di
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.unit.userinfo.UserInfoMviModel
import org.koin.core.parameter.parametersOf
import org.koin.java.KoinJavaComponent.inject
actual fun getUserInfoViewModel(user: UserModel): UserInfoMviModel {
val res: UserInfoMviModel by inject(
UserInfoMviModel::class.java,
parameters = { parametersOf(user) })
return res
}

View File

@ -0,0 +1,24 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.userinfo
import androidx.compose.runtime.Stable
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
@Stable
interface UserInfoMviModel :
MviModel<UserInfoMviModel.Intent, UserInfoMviModel.UiState, UserInfoMviModel.Effect>,
ScreenModel {
sealed interface Intent
data class UiState(
val user: UserModel = UserModel(),
val autoLoadImages: Boolean = true,
val moderatedCommunities: List<CommunityModel> = emptyList(),
)
sealed interface Effect
}

View File

@ -0,0 +1,247 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.userinfo
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Cake
import androidx.compose.material.icons.filled.Padding
import androidx.compose.material.icons.filled.Reply
import androidx.compose.material.icons.filled.Shield
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.di.getThemeRepository
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.toTypography
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.BottomSheetHandle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CustomizedContent
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.detailopener.api.getDetailOpener
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.DetailInfoItem
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.PostCardBody
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallbackArgs
import com.github.diegoberaldin.raccoonforlemmy.core.utils.datetime.prettifyDate
import com.github.diegoberaldin.raccoonforlemmy.core.utils.getPrettyNumber
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import com.github.diegoberaldin.raccoonforlemmy.unit.userinfo.components.ModeratedCommunityCell
import com.github.diegoberaldin.raccoonforlemmy.unit.userinfo.di.getUserInfoViewModel
import com.github.diegoberaldin.raccoonforlemmy.unit.web.WebViewScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.zoomableimage.ZoomableImageScreen
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class UserInfoScreen(
private val user: UserModel,
) : Screen {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val model = rememberScreenModel { getUserInfoViewModel(user) }
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState()
val navigationCoordinator = remember { getNavigationCoordinator() }
val scope = rememberCoroutineScope()
val detailOpener = remember { getDetailOpener() }
val themeRepository = remember { getThemeRepository() }
val family by themeRepository.contentFontFamily.collectAsState()
val typography = family.toTypography()
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.padding(
top = Spacing.s,
start = Spacing.s,
end = Spacing.s,
bottom = Spacing.m,
)
.fillMaxHeight(0.9f)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(Spacing.s),
) {
BottomSheetHandle(modifier = Modifier.align(Alignment.CenterHorizontally))
Scaffold(
topBar = {
TopAppBar(
title = {
Row {
Spacer(modifier = Modifier.weight(1f))
Text(
text = buildString {
append(uiState.user.name)
if (uiState.user.host.isNotEmpty()) {
append("@${uiState.user.host}")
}
},
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground,
)
Spacer(modifier = Modifier.weight(1f))
}
})
},
) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(top = Spacing.m),
verticalArrangement = Arrangement.spacedBy(Spacing.s),
horizontalAlignment = Alignment.CenterHorizontally,
) {
item {
Column(
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
DetailInfoItem(
modifier = Modifier.fillMaxWidth(),
icon = Icons.Default.Cake,
title = uiState.user.accountAge.prettifyDate(),
)
uiState.user.score?.also { score ->
DetailInfoItem(
modifier = Modifier.fillMaxWidth(),
icon = Icons.Default.Padding,
title = stringResource(MR.strings.community_info_posts),
value = score.postScore.getPrettyNumber(
thousandLabel = stringResource(MR.strings.profile_thousand_short),
millionLabel = stringResource(MR.strings.profile_million_short),
),
)
DetailInfoItem(
modifier = Modifier.fillMaxWidth(),
icon = Icons.Default.Reply,
title = stringResource(MR.strings.community_info_comments),
value = score.commentScore.getPrettyNumber(
thousandLabel = stringResource(MR.strings.profile_thousand_short),
millionLabel = stringResource(MR.strings.profile_million_short),
),
)
}
if (uiState.user.admin) {
DetailInfoItem(
modifier = Modifier.fillMaxWidth(),
icon = Icons.Default.Shield,
title = stringResource(MR.strings.user_info_admin),
)
}
}
}
uiState.user.displayName.takeIf { it.isNotEmpty() }?.also { name ->
item {
Text(
text = name,
style = typography.bodyLarge,
)
}
}
uiState.user.bio?.takeIf { it.isNotEmpty() }?.also { biography ->
item {
CustomizedContent {
PostCardBody(
modifier = Modifier.fillMaxWidth().padding(top = Spacing.m),
text = biography,
onOpenImage = { url ->
navigationCoordinator.hideBottomSheet()
scope.launch {
delay(100)
navigationCoordinator.pushScreen(ZoomableImageScreen(url))
}
},
onOpenCommunity = { community, instance ->
navigationCoordinator.hideBottomSheet()
scope.launch {
delay(100)
detailOpener.openCommunityDetail(community, instance)
}
},
onOpenPost = { post, instance ->
navigationCoordinator.hideBottomSheet()
scope.launch {
delay(100)
detailOpener.openPostDetail(post, instance)
}
},
onOpenUser = { user, instance ->
navigationCoordinator.hideBottomSheet()
scope.launch {
delay(100)
detailOpener.openUserDetail(user, instance)
}
},
onOpenWeb = { url ->
navigationCoordinator.hideBottomSheet()
scope.launch {
delay(100)
navigationCoordinator.pushScreen(WebViewScreen(url))
}
},
)
}
}
}
if (uiState.moderatedCommunities.isNotEmpty()) {
item {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(
top = Spacing.s,
bottom = Spacing.xs,
),
text = stringResource(MR.strings.user_info_moderates),
)
LazyRow(
horizontalArrangement = Arrangement.spacedBy(Spacing.s),
) {
items(
count = uiState.moderatedCommunities.size
) { idx ->
val community = uiState.moderatedCommunities[idx]
ModeratedCommunityCell(
autoLoadImages = uiState.autoLoadImages,
community = community,
onOpenCommunity = rememberCallbackArgs { _ ->
navigationCoordinator.hideBottomSheet()
scope.launch {
delay(100)
detailOpener.openCommunityDetail(community, "")
}
}
)
}
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,45 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.userinfo
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.SettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class UserInfoViewModel(
private val mvi: DefaultMviModel<UserInfoMviModel.Intent, UserInfoMviModel.UiState, UserInfoMviModel.Effect>,
private val user: UserModel,
private val userRepository: UserRepository,
private val settingsRepository: SettingsRepository,
) : UserInfoMviModel,
MviModel<UserInfoMviModel.Intent, UserInfoMviModel.UiState, UserInfoMviModel.Effect> by mvi {
override fun onStarted() {
mvi.onStarted()
mvi.updateState {
it.copy(user = user)
}
mvi.scope?.launch(Dispatchers.IO) {
settingsRepository.currentSettings.onEach {
mvi.updateState { it.copy(autoLoadImages = it.autoLoadImages) }
}.launchIn(this)
if (uiState.value.moderatedCommunities.isEmpty()) {
val updatedUser = userRepository.get(user.id)
if (updatedUser != null) {
mvi.updateState {
it.copy(user = updatedUser)
}
}
val communities = userRepository.getModeratedCommunities(id = user.id)
mvi.updateState { it.copy(moderatedCommunities = communities) }
}
}
}
}

View File

@ -0,0 +1,99 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.userinfo.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.IconSize
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CustomImage
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PlaceholderImage
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallback
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
@Composable
internal fun ModeratedCommunityCell(
community: CommunityModel,
autoLoadImages: Boolean = true,
onOpenCommunity: ((CommunityModel) -> Unit)? = null,
) {
val name = community.name
val host = community.host
val icon = community.icon.orEmpty()
val iconSize = IconSize.xl
val fullTextColor = MaterialTheme.colorScheme.onBackground
val ancillaryColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f)
Column(
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (icon.isNotEmpty()) {
CustomImage(
modifier = Modifier
.padding(Spacing.xxxs)
.size(iconSize)
.clip(RoundedCornerShape(iconSize / 2))
.onClick(
onClick = rememberCallback {
onOpenCommunity?.invoke(community)
},
),
quality = FilterQuality.Low,
url = icon,
autoload = autoLoadImages,
contentDescription = null,
contentScale = ContentScale.FillBounds,
)
} else {
PlaceholderImage(
modifier = Modifier.onClick(
onClick = rememberCallback {
onOpenCommunity?.invoke(community)
},
),
size = iconSize,
title = name,
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.widthIn(max = 100.dp),
text = name,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelMedium,
color = fullTextColor,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
if (host.isNotEmpty()) {
Text(
modifier = Modifier.widthIn(max = 100.dp),
text = host,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelSmall,
color = ancillaryColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}

View File

@ -0,0 +1,17 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.userinfo.di
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.unit.userinfo.UserInfoMviModel
import com.github.diegoberaldin.raccoonforlemmy.unit.userinfo.UserInfoViewModel
import org.koin.dsl.module
val userInfoModule = module {
factory<UserInfoMviModel> { params ->
UserInfoViewModel(
mvi = DefaultMviModel(UserInfoMviModel.UiState()),
user = params[0],
userRepository = get(),
settingsRepository = get(),
)
}
}

View File

@ -0,0 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.userinfo.di
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.unit.userinfo.UserInfoMviModel
expect fun getUserInfoViewModel(user: UserModel): UserInfoMviModel

View File

@ -0,0 +1,17 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.userinfo.di
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.unit.userinfo.UserInfoMviModel
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.koin.core.parameter.parametersOf
actual fun getUserInfoViewModel(user: UserModel): UserInfoMviModel =
UnitUserInfoDiHelper.getUserInfoViewModel(user)
object UnitUserInfoDiHelper : KoinComponent {
fun getUserInfoViewModel(user: UserModel): UserInfoMviModel {
val res: UserInfoMviModel by inject(parameters = { parametersOf(user) })
return res
}
}