refactor: screen params (state restoration) (#402)

* chore: add local cache

* chore: remove possibility to disable bottom sheet gestures

* chore: add cache to detail opener and create entry points

* chore: add query by id to multi-community

* chore: update community detail

* chore: update community info

* chore: update create comment

* chore: update create post

* chore: update multi-community screens

* chore: update user detail

* chore: update user info

* chore: update post detail

* chore: update post list

* chore: update modal drawer

* chore: update subscription management

* chore: update profile

* chore: update saved items

* chore: remove JavaSerializable

closes #316
This commit is contained in:
Diego Beraldin 2023-12-30 10:15:22 +01:00 committed by GitHub
parent d28f843a51
commit e206de2e16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 907 additions and 850 deletions

View File

@ -1,6 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.detailopener.api
import androidx.compose.runtime.Stable
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.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
@ -23,4 +24,17 @@ interface DetailOpener {
highlightCommentId: Int? = null,
isMod: Boolean = false,
)
fun openReply(
originalPost: PostModel? = null,
originalComment: CommentModel? = null,
editedComment: CommentModel? = null,
initialText: String? = null,
)
fun openCreatePost(
editedPost: PostModel? = null,
crossPost: PostModel? = null,
communityId: Int? = null,
)
}

View File

@ -44,11 +44,15 @@ kotlin {
implementation(projects.core.navigation)
implementation(projects.core.commonui.detailopenerApi)
implementation(projects.domain.lemmy.data)
implementation(projects.domain.lemmy.repository)
implementation(projects.unit.postdetail)
implementation(projects.unit.communitydetail)
implementation(projects.unit.userdetail)
implementation(projects.domain.lemmy.data)
implementation(projects.unit.createpost)
implementation(projects.unit.createcomment)
implementation(projects.resources)
}

View File

@ -2,33 +2,50 @@ package com.github.diegoberaldin.raccoonforlemmy.core.commonui.detailopener.impl
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.detailopener.api.DetailOpener
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.NavigationCoordinator
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.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.LemmyItemCache
import com.github.diegoberaldin.raccoonforlemmy.unit.communitydetail.CommunityDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createcomment.CreateCommentScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createpost.CreatePostScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.postdetail.PostDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.userdetail.UserDetailScreen
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
class DefaultDetailOpener(
private val navigationCoordinator: NavigationCoordinator,
private val itemCache: LemmyItemCache,
) : DetailOpener {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
override fun openCommunityDetail(community: CommunityModel, otherInstance: String) {
navigationCoordinator.pushScreen(
CommunityDetailScreen(
community = community,
otherInstance = otherInstance,
),
)
scope.launch {
itemCache.putCommunity(community)
navigationCoordinator.pushScreen(
CommunityDetailScreen(
communityId = community.id,
otherInstance = otherInstance,
),
)
}
}
override fun openUserDetail(user: UserModel, otherInstance: String) {
navigationCoordinator.pushScreen(
UserDetailScreen(
user = user,
otherInstance = otherInstance,
),
)
scope.launch {
itemCache.putUser(user)
navigationCoordinator.pushScreen(
UserDetailScreen(
userId = user.id,
otherInstance = otherInstance,
),
)
}
}
override fun openPostDetail(
@ -37,13 +54,63 @@ class DefaultDetailOpener(
highlightCommentId: Int?,
isMod: Boolean,
) {
navigationCoordinator.pushScreen(
PostDetailScreen(
post = post,
highlightCommentId = highlightCommentId,
otherInstance = otherInstance,
isMod = isMod,
),
)
scope.launch {
itemCache.putPost(post)
navigationCoordinator.pushScreen(
PostDetailScreen(
postId = post.id,
highlightCommentId = highlightCommentId,
otherInstance = otherInstance,
isMod = isMod,
),
)
}
}
override fun openReply(
originalPost: PostModel?,
originalComment: CommentModel?,
editedComment: CommentModel?,
initialText: String?,
) {
scope.launch {
if (originalPost != null) {
itemCache.putPost(originalPost)
}
if (originalComment != null) {
itemCache.putComment(originalComment)
}
if (editedComment != null) {
itemCache.putComment(editedComment)
}
val screen = CreateCommentScreen(
originalPostId = originalPost?.id,
originalCommentId = originalComment?.id,
editedCommentId = editedComment?.id,
initialText = initialText,
)
navigationCoordinator.pushScreen(screen)
}
}
override fun openCreatePost(
editedPost: PostModel?,
crossPost: PostModel?,
communityId: Int?,
) {
scope.launch {
if (editedPost != null) {
itemCache.putPost(editedPost)
}
if (crossPost != null) {
itemCache.putPost(crossPost)
}
val screen = CreatePostScreen(
editedPostId = editedPost?.id,
crossPostId = crossPost?.id,
communityId = communityId,
)
navigationCoordinator.pushScreen(screen)
}
}
}

View File

@ -40,7 +40,6 @@ internal class DefaultNavigationCoordinator : NavigationCoordinator {
override val inboxUnread = MutableStateFlow(0)
override val canPop = MutableStateFlow(false)
override val exitMessageVisible = MutableStateFlow(false)
override val bottomSheetGesturesEnabled = MutableStateFlow(true)
private var connection: NestedScrollConnection? = null
private var navigator: Navigator? = null
@ -72,7 +71,6 @@ internal class DefaultNavigationCoordinator : NavigationCoordinator {
NavigationEvent.Hide -> {
bottomNavigator?.hide()
setBottomSheetGesturesEnabled(true)
}
}
}.launchIn(this)
@ -175,10 +173,6 @@ internal class DefaultNavigationCoordinator : NavigationCoordinator {
exitMessageVisible.value = value
}
override fun setBottomSheetGesturesEnabled(value: Boolean) {
bottomSheetGesturesEnabled.value = value
}
override fun setTabNavigator(value: TabNavigator) {
tabNavigator = value
}

View File

@ -27,7 +27,6 @@ interface NavigationCoordinator {
val deepLinkUrl: Flow<String?>
val canPop: StateFlow<Boolean>
val exitMessageVisible: StateFlow<Boolean>
val bottomSheetGesturesEnabled: StateFlow<Boolean>
fun setCurrentSection(section: TabNavigationSection)
fun submitDeeplink(url: String)
@ -43,7 +42,6 @@ interface NavigationCoordinator {
fun pushScreen(screen: Screen)
fun popScreen()
fun setExitMessageVisible(value: Boolean)
fun setBottomSheetGesturesEnabled(value: Boolean)
fun setTabNavigator(value: TabNavigator)
fun changeTab(value: Tab)
}

View File

@ -1,6 +1,5 @@
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class AccountModel(
val id: Long? = null,
@ -9,4 +8,4 @@ data class AccountModel(
val instance: String,
val jwt: String,
val active: Boolean = false,
) : JavaSerializable
)

View File

@ -1,8 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class FavoriteCommunityModel(
val id: Long? = null,
val communityId: Int? = null,
) : JavaSerializable
)

View File

@ -1,10 +1,8 @@
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class MultiCommunityModel(
val id: Long? = null,
val name: String = "",
val communityIds: List<Int> = emptyList(),
val icon: String? = null,
) : JavaSerializable
)

View File

@ -1,7 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.data
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.VoteFormat
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@ -40,4 +39,4 @@ data class SettingsModel(
val replyColor: Int? = null,
val searchPostTitleOnly: Boolean = false,
val edgeToEdge: Boolean = true,
) : JavaSerializable
)

View File

@ -13,12 +13,17 @@ internal class DefaultMultiCommunityRepository(
private val db = provider.getDatabase()
override suspend fun getAll(accountId: Long?): List<MultiCommunityModel> =
override suspend fun getAll(accountId: Long): List<MultiCommunityModel> =
withContext(Dispatchers.IO) {
db.multicommunitiesQueries.getAll(accountId)
.executeAsList().map { it.toModel() }
}
override suspend fun getById(id: Long): MultiCommunityModel? =
withContext(Dispatchers.IO) {
db.multicommunitiesQueries.getById(id).executeAsOneOrNull()?.toModel()
}
override suspend fun create(model: MultiCommunityModel, accountId: Long): Long =
withContext(Dispatchers.IO) {
db.multicommunitiesQueries.create(

View File

@ -3,7 +3,8 @@ package com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
interface MultiCommunityRepository {
suspend fun getAll(accountId: Long?): List<MultiCommunityModel>
suspend fun getById(id: Long): MultiCommunityModel?
suspend fun getAll(accountId: Long): List<MultiCommunityModel>
suspend fun create(model: MultiCommunityModel, accountId: Long): Long

View File

@ -18,6 +18,11 @@ SELECT *
FROM MultiCommunityEntity
WHERE name = ? AND account_id = ?;
getById:
SELECT *
FROM MultiCommunityEntity
WHERE id = ?;
create:
INSERT OR IGNORE INTO MultiCommunityEntity (
name,

View File

@ -1,3 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.core.utils
actual typealias JavaSerializable = java.io.Serializable

View File

@ -1,3 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.core.utils
expect interface JavaSerializable

View File

@ -0,0 +1,51 @@
package com.github.diegoberaldin.raccoonforlemmy.core.utils.cache
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
internal class DefaultLruCache<T>(val size: Int) : LruCache<T> {
private val values: MutableMap<Int, T> = mutableMapOf()
private var lastUsedIds: List<Int> = listOf()
private val mutex = Mutex()
override suspend fun get(key: Int): T? = mutex.withLock {
val res = values[key]
if (res != null) {
moveAtTheBeginning(key)
}
return res
}
override suspend fun put(value: T, key: Int) = mutex.withLock {
val old = values[key]
if (old != null) {
// already existing element
moveAtTheBeginning(key)
} else {
// new element
values[key] = value
lastUsedIds = buildList {
this += key
this += lastUsedIds
}
if (lastUsedIds.size > size) {
dropOldest()
}
}
}
private fun dropOldest() {
lastUsedIds.lastOrNull()?.also { oldest ->
values.remove(oldest)
lastUsedIds = lastUsedIds - oldest
}
}
private fun moveAtTheBeginning(key: Int) {
lastUsedIds = buildList {
this += key
this += (lastUsedIds - key)
}
}
}

View File

@ -0,0 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.core.utils.cache
interface LruCache<T> {
suspend fun get(key: Int): T?
suspend fun put(value: T, key: Int)
companion object {
fun <T> factory(size: Int): LruCache<T> = DefaultLruCache(size)
}
}

View File

@ -1,3 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.core.utils
actual interface JavaSerializable

View File

@ -1,9 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class AccountBansModel(
val users: List<UserModel> = emptyList(),
val communities: List<CommunityModel> = emptyList(),
val instances: List<InstanceModel> = emptyList(),
) : JavaSerializable
)

View File

@ -1,7 +1,5 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class AccountSettingsModel(
val avatar: String? = null,
val banner: String? = null,
@ -16,4 +14,4 @@ data class AccountSettingsModel(
val showNsfw: Boolean? = null,
val defaultListingType: ListingType? = null,
val defaultSortType: SortType? = null,
) : JavaSerializable
)

View File

@ -1,6 +1,5 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
import kotlin.jvm.Transient
data class CommentModel(
@ -27,7 +26,7 @@ data class CommentModel(
@Transient
val loadMoreButtonVisible: Boolean = false,
val languageId: Int = 0,
) : JavaSerializable {
) {
val depth: Int get() = (path.split(".").size - 2).coerceAtLeast(0)
val parentId: String?
get() = path.split(".")

View File

@ -1,7 +1,5 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class CommentReportModel(
val id: Int = 0,
val creator: UserModel? = null,
@ -13,4 +11,4 @@ data class CommentReportModel(
val resolver: UserModel? = null,
val publishDate: String? = null,
val updateDate: String? = null,
) : JavaSerializable
)

View File

@ -1,6 +1,5 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
import kotlin.jvm.Transient
data class CommunityModel(
@ -23,4 +22,4 @@ data class CommunityModel(
val comments: Int = 0,
val creationDate: String? = null,
@Transient val favorite: Boolean = false,
) : JavaSerializable
)

View File

@ -1,8 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class InstanceModel(
val id: Int = 0,
val domain: String = "",
) : JavaSerializable
)

View File

@ -5,11 +5,10 @@ import androidx.compose.material.icons.filled.Book
import androidx.compose.material.icons.filled.Cottage
import androidx.compose.material.icons.filled.Public
import androidx.compose.runtime.Composable
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
sealed interface ListingType : JavaSerializable {
sealed interface ListingType {
data object All : ListingType
data object Subscribed : ListingType
data object Local : ListingType

View File

@ -1,8 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class MetadataModel(
val title: String = "",
val description: String = "",
) : JavaSerializable
)

View File

@ -1,10 +1,8 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
sealed class ModlogItem(
val type: ModlogItemType,
) : JavaSerializable {
) {
abstract val id: Int
abstract val date: String?

View File

@ -1,8 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
sealed interface ModlogItemType : JavaSerializable {
sealed interface ModlogItemType {
data object All : ModlogItemType
data object ModRemovePost : ModlogItemType
data object ModLockPost : ModlogItemType

View File

@ -1,7 +1,5 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class PersonMentionModel(
val id: Int = 0,
val post: PostModel,
@ -16,4 +14,4 @@ data class PersonMentionModel(
val isOwnPost: Boolean = false,
val publishDate: String? = null,
val read: Boolean = false,
) : JavaSerializable
)

View File

@ -1,6 +1,5 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
import com.github.diegoberaldin.raccoonforlemmy.core.utils.looksLikeAnImage
data class PostModel(
@ -28,7 +27,7 @@ data class PostModel(
val removed: Boolean = false,
val locked: Boolean = false,
val languageId: Int = 0,
) : JavaSerializable
)
val PostModel.imageUrl: String
get() = url?.takeIf { it.looksLikeAnImage }?.takeIf { it.isNotEmpty() } ?: run {

View File

@ -1,6 +1,5 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
import com.github.diegoberaldin.raccoonforlemmy.core.utils.looksLikeAnImage
data class PostReportModel(
@ -16,7 +15,7 @@ data class PostReportModel(
val resolver: UserModel? = null,
val publishDate: String? = null,
val updateDate: String? = null,
) : JavaSerializable
)
val PostReportModel.imageUrl: String
get() = originalUrl?.takeIf { it.looksLikeAnImage }?.takeIf { it.isNotEmpty() } ?: run {

View File

@ -1,7 +1,5 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class PrivateMessageModel(
val id: Int = 0,
val content: String? = null,
@ -10,7 +8,7 @@ data class PrivateMessageModel(
val publishDate: String? = null,
val updateDate: String? = null,
val read: Boolean = false,
) : JavaSerializable
)
fun PrivateMessageModel.otherUser(currentUserId: Int): UserModel? = when (currentUserId) {
creator?.id -> recipient

View File

@ -1,8 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
sealed interface SearchResult : JavaSerializable {
sealed interface SearchResult {
data class Post(val model: PostModel) : SearchResult
data class Comment(val model: CommentModel) : SearchResult
data class User(val model: UserModel) : SearchResult

View File

@ -1,8 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
sealed interface SearchResultType : JavaSerializable {
sealed interface SearchResultType {
data object All : SearchResultType
data object Posts : SearchResultType
data object Comments : SearchResultType

View File

@ -12,11 +12,10 @@ import androidx.compose.material.icons.filled.Thunderstorm
import androidx.compose.material.icons.filled.TrendingUp
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.vector.ImageVector
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
sealed interface SortType : JavaSerializable {
sealed interface SortType {
data object Active : SortType
data object Hot : SortType
data object New : SortType

View File

@ -1,7 +1,5 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class UserModel(
val id: Int = 0,
val instanceId: Int = 0,
@ -17,6 +15,6 @@ data class UserModel(
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

@ -1,8 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
data class UserScoreModel(
val postScore: Int = 0,
val commentScore: Int = 0,
) : JavaSerializable
)

View File

@ -29,7 +29,10 @@ kotlin {
dependencies {
implementation(libs.koin.core)
implementation(libs.ktorfit.lib)
implementation(projects.core.api)
implementation(projects.core.utils)
implementation(projects.domain.lemmy.data)
}
}

View File

@ -0,0 +1,39 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository
import com.github.diegoberaldin.raccoonforlemmy.core.utils.cache.LruCache
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.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
internal class DefaultLemmyItemCache(
private val postCache: LruCache<PostModel>,
private val communityCache: LruCache<CommunityModel>,
private val commentCache: LruCache<CommentModel>,
private val userCache: LruCache<UserModel>,
) : LemmyItemCache {
override suspend fun putPost(value: PostModel) {
postCache.put(value = value, key = value.id)
}
override suspend fun getPost(id: Int): PostModel? = postCache.get(id)
override suspend fun putComment(value: CommentModel) {
commentCache.put(value = value, key = value.id)
}
override suspend fun getComment(id: Int): CommentModel? = commentCache.get(id)
override suspend fun putCommunity(value: CommunityModel) {
communityCache.put(value = value, key = value.id)
}
override suspend fun getCommunity(id: Int): CommunityModel? = communityCache.get(id)
override suspend fun putUser(value: UserModel) {
userCache.put(value = value, key = value.id)
}
override suspend fun getUser(id: Int): UserModel? = userCache.get(id)
}

View File

@ -0,0 +1,20 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository
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.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
interface LemmyItemCache {
suspend fun putPost(value: PostModel)
suspend fun getPost(id: Int): PostModel?
suspend fun putComment(value: CommentModel)
suspend fun getComment(id: Int): CommentModel?
suspend fun putCommunity(value: CommunityModel)
suspend fun getCommunity(id: Int): CommunityModel?
suspend fun putUser(value: UserModel)
suspend fun getUser(id: Int): UserModel?
}

View File

@ -1,16 +1,19 @@
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.di
import com.github.diegoberaldin.raccoonforlemmy.core.utils.cache.LruCache
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommunityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.DefaultCommentRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.DefaultCommunityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.DefaultGetSortTypesUseCase
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.DefaultLemmyItemCache
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.DefaultModlogRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.DefaultPostRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.DefaultPrivateMessageRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.DefaultSiteRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.DefaultUserRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.GetSortTypesUseCase
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.LemmyItemCache
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.ModlogRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PrivateMessageRepository
@ -65,4 +68,12 @@ val repositoryModule = module {
services = get(named("default")),
)
}
single<LemmyItemCache> {
DefaultLemmyItemCache(
postCache = LruCache.factory(5),
commentCache = LruCache.factory(5),
communityCache = LruCache.factory(5),
userCache = LruCache.factory(5),
)
}
}

View File

@ -90,7 +90,6 @@ fun App(onLoadingFinished: () -> Unit = {}) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val drawerCoordinator = remember { getDrawerCoordinator() }
val drawerGesturesEnabled by drawerCoordinator.gesturesEnabled.collectAsState()
val bottomSheetGesturesEnabled by navigationCoordinator.bottomSheetGesturesEnabled.collectAsState()
val detailOpener = remember { getDetailOpener() }
LaunchedEffect(Unit) {
@ -203,7 +202,9 @@ fun App(onLoadingFinished: () -> Unit = {}) {
}
is DrawerEvent.OpenMultiCommunity -> {
navigationCoordinator.pushScreen(MultiCommunityScreen(evt.community))
evt.community.id?.toInt()?.also {
navigationCoordinator.pushScreen(MultiCommunityScreen(it))
}
}
DrawerEvent.ManageSubscriptions -> {
@ -240,7 +241,6 @@ fun App(onLoadingFinished: () -> Unit = {}) {
topEnd = CornerSize.xl
),
sheetBackgroundColor = MaterialTheme.colorScheme.background,
sheetGesturesEnabled = bottomSheetGesturesEnabled,
) { bottomNavigator ->
navigationCoordinator.setBottomNavigator(bottomNavigator)

View File

@ -17,6 +17,7 @@ internal val internalSharedModule = module {
single<DetailOpener> {
DefaultDetailOpener(
navigationCoordinator = get(),
itemCache = get(),
)
}
}

View File

@ -101,15 +101,12 @@ import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallb
import com.github.diegoberaldin.raccoonforlemmy.core.utils.keepscreenon.rememberKeepScreenOn
import com.github.diegoberaldin.raccoonforlemmy.core.utils.toLocalDp
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.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.containsId
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toIcon
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import com.github.diegoberaldin.raccoonforlemmy.unit.ban.BanUserScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.communityinfo.CommunityInfoScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createcomment.CreateCommentScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createpost.CreatePostScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createreport.CreateReportScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.instanceinfo.InstanceInfoScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.modlog.ModlogScreen
@ -125,21 +122,21 @@ import kotlinx.coroutines.launch
import org.koin.core.parameter.parametersOf
class CommunityDetailScreen(
private val community: CommunityModel,
private val communityId: Int,
private val otherInstance: String = "",
) : Screen {
override val key: ScreenKey
get() = super.key + community.id.toString()
get() = super.key + communityId.toString()
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
override fun Content() {
val model = getScreenModel<CommunityDetailMviModel>(
tag = community.id.toString() + community.name,
parameters = { parametersOf(community, otherInstance) },
tag = communityId.toString(),
parameters = { parametersOf(communityId, otherInstance) },
)
model.bindToLifecycle(key + community.id.toString())
model.bindToLifecycle(key + communityId.toString())
val uiState by model.uiState.collectAsState()
val lazyListState = rememberLazyListState()
val scope = rememberCoroutineScope()
@ -366,7 +363,7 @@ class CommunityDetailScreen(
OptionId.Info -> {
navigationCoordinator.showBottomSheet(
CommunityInfoScreen(uiState.community),
CommunityInfoScreen(uiState.community.id),
)
}
@ -475,13 +472,9 @@ class CommunityDetailScreen(
icon = Icons.Default.Create,
text = stringResource(MR.strings.action_create_post),
onSelected = rememberCallback {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreatePostScreen(
communityId = uiState.community.id,
)
showBottomSheet(screen = screen)
}
detailOpener.openCreatePost(
communityId = uiState.community.id,
)
},
)
}
@ -612,13 +605,9 @@ class CommunityDetailScreen(
)
},
onSecondDismissToStart = rememberCallback(model) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = post,
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = post,
)
},
onDismissToEnd = rememberCallback(model) {
model.reduce(
@ -808,12 +797,7 @@ class CommunityDetailScreen(
)
OptionId.Edit -> {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
showBottomSheet(
CreatePostScreen(editedPost = post),
)
}
detailOpener.openCreatePost(editedPost = post)
}
OptionId.Report -> {
@ -823,12 +807,7 @@ class CommunityDetailScreen(
}
OptionId.CrossPost -> {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
showBottomSheet(
CreatePostScreen(crossPost = post),
)
}
detailOpener.openCreatePost(crossPost = post)
}
OptionId.SeeRaw -> {
@ -943,7 +922,8 @@ class CommunityDetailScreen(
if (rawContent != null) {
when (val content = rawContent) {
is PostModel -> {
RawContentDialog(title = content.title,
RawContentDialog(
title = content.title,
publishDate = content.publishDate,
updateDate = content.updateDate,
url = content.url,
@ -954,20 +934,17 @@ class CommunityDetailScreen(
onQuote = rememberCallbackArgs { quotation ->
rawContent = null
if (quotation != null) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
}
})
},
)
}
is CommentModel -> {
@ -981,16 +958,14 @@ class CommunityDetailScreen(
onQuote = rememberCallbackArgs { quotation ->
rawContent = null
if (quotation != null) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(originalComment = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
})
showBottomSheet(screen)
}
detailOpener.openReply(
originalComment = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
}
},
)

View File

@ -23,6 +23,7 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.imageUrl
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toSortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommunityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.GetSortTypesUseCase
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.LemmyItemCache
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepository
import kotlinx.coroutines.Dispatchers
@ -33,7 +34,7 @@ import kotlinx.coroutines.launch
class CommunityDetailViewModel(
private val mvi: DefaultMviModel<CommunityDetailMviModel.Intent, CommunityDetailMviModel.UiState, CommunityDetailMviModel.Effect>,
private val community: CommunityModel,
private val communityId: Int,
private val otherInstance: String,
private val identityRepository: IdentityRepository,
private val apiConfigurationRepository: ApiConfigurationRepository,
@ -50,6 +51,7 @@ class CommunityDetailViewModel(
private val imagePreloadManager: ImagePreloadManager,
private val getSortTypesUseCase: GetSortTypesUseCase,
private val notificationCenter: NotificationCenter,
private val itemCache: LemmyItemCache,
) : CommunityDetailMviModel,
MviModel<CommunityDetailMviModel.Intent, CommunityDetailMviModel.UiState, CommunityDetailMviModel.Effect> by mvi {
@ -59,16 +61,18 @@ class CommunityDetailViewModel(
override fun onStarted() {
mvi.onStarted()
mvi.updateState {
it.copy(
community = it.community.takeIf { c -> c.id != 0 } ?: community,
instance = otherInstance.takeIf { n -> n.isNotEmpty() }
?: apiConfigurationRepository.instance.value,
)
}
mvi.scope?.launch {
if (uiState.value.community.id == 0) {
val community = itemCache.getCommunity(communityId) ?: CommunityModel()
mvi.updateState {
it.copy(
community = community,
instance = otherInstance.takeIf { n -> n.isNotEmpty() }
?: apiConfigurationRepository.instance.value,
)
}
}
themeRepository.postLayout.onEach { layout ->
mvi.updateState { it.copy(postLayout = layout) }
}.launchIn(this)
@ -252,6 +256,7 @@ class CommunityDetailViewModel(
pageCursor = null
hideReadPosts = false
mvi.updateState { it.copy(canFetchMore = true, refreshing = true) }
val community = uiState.value.community
val auth = identityRepository.authToken.value
val accountId = accountRepository.getActive()?.id
val isFavorite = favoriteCommunityRepository.getBy(accountId, community.id) != null
@ -298,11 +303,11 @@ class CommunityDetailViewModel(
val auth = identityRepository.authToken.value
val refreshing = currentState.refreshing
val sort = currentState.sortType
val communityId = currentState.community.id
val community = currentState.community
val (itemList, nextPage) = postRepository.getAll(
auth = auth,
otherInstance = otherInstance,
communityId = communityId,
communityId = community.id,
communityName = community.name,
page = currentPage,
pageCursor = pageCursor,
@ -459,7 +464,7 @@ class CommunityDetailViewModel(
mvi.scope?.launch(Dispatchers.IO) {
communityRepository.subscribe(
auth = identityRepository.authToken.value,
id = community.id,
id = communityId,
)
// the first response isn't immediately true, simulate here
mvi.updateState { it.copy(community = it.community.copy(subscribed = true)) }
@ -471,7 +476,7 @@ class CommunityDetailViewModel(
mvi.scope?.launch(Dispatchers.IO) {
val community = communityRepository.unsubscribe(
auth = identityRepository.authToken.value,
id = community.id,
id = communityId,
)
if (community != null) {
mvi.updateState { it.copy(community = community) }
@ -501,7 +506,6 @@ class CommunityDetailViewModel(
mvi.updateState { it.copy(asyncInProgress = true) }
mvi.scope?.launch(Dispatchers.IO) {
try {
val communityId = community.id
val auth = identityRepository.authToken.value
communityRepository.block(communityId, true, auth).getOrThrow()
mvi.emitEffect(CommunityDetailMviModel.Effect.BlockSuccess)
@ -517,6 +521,7 @@ class CommunityDetailViewModel(
mvi.updateState { it.copy(asyncInProgress = true) }
mvi.scope?.launch(Dispatchers.IO) {
try {
val community = uiState.value.community
val instanceId = community.instanceId
val auth = identityRepository.authToken.value
siteRepository.block(instanceId, true, auth).getOrThrow()
@ -583,7 +588,7 @@ class CommunityDetailViewModel(
val auth = identityRepository.authToken.value.orEmpty()
val newModerators = communityRepository.addModerator(
auth = auth,
communityId = community.id,
communityId = communityId,
added = !isModerator,
userId = userId,
)
@ -594,10 +599,9 @@ class CommunityDetailViewModel(
}
private fun toggleFavorite() {
val communityId = community.id
mvi.scope?.launch(Dispatchers.IO) {
val accountId = accountRepository.getActive()?.id ?: 0L
val newValue = !community.favorite
val newValue = !uiState.value.community.favorite
if (newValue) {
val model = FavoriteCommunityModel(communityId = communityId)
favoriteCommunityRepository.create(model, accountId)

View File

@ -9,7 +9,7 @@ val communityDetailModule = module {
factory<CommunityDetailMviModel> { params ->
CommunityDetailViewModel(
mvi = DefaultMviModel(CommunityDetailMviModel.UiState()),
community = params[0],
communityId = params[0],
otherInstance = params[1],
identityRepository = get(),
apiConfigurationRepository = get(),
@ -26,6 +26,7 @@ val communityDetailModule = module {
getSortTypesUseCase = get(),
accountRepository = get(),
favoriteCommunityRepository = get(),
itemCache = get(),
)
}
}

View File

@ -32,7 +32,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import cafe.adriel.voyager.core.screen.Screen
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.getScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.BottomSheetHandle
@ -41,10 +40,10 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.detailopener.api.g
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.navigation.getScreenModel
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.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import com.github.diegoberaldin.raccoonforlemmy.unit.communityinfo.components.ModeratorCell
import com.github.diegoberaldin.raccoonforlemmy.unit.web.WebViewScreen
@ -55,14 +54,14 @@ import kotlinx.coroutines.launch
import org.koin.core.parameter.parametersOf
class CommunityInfoScreen(
private val community: CommunityModel,
private val communityId: Int,
) : Screen {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val model = getScreenModel<CommunityInfoMviModel>(
tag = community.id.toString() + community.name,
parameters = { parametersOf(community) },
tag = communityId.toString(),
parameters = { parametersOf(communityId) },
)
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState()

View File

@ -5,30 +5,35 @@ 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.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommunityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.LemmyItemCache
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class CommunityInfoViewModel(
private val mvi: DefaultMviModel<CommunityInfoMviModel.Intent, CommunityInfoMviModel.UiState, CommunityInfoMviModel.Effect>,
private val community: CommunityModel,
private val communityId: Int,
private val communityRepository: CommunityRepository,
private val settingsRepository: SettingsRepository,
private val itemCache: LemmyItemCache,
) : CommunityInfoMviModel,
MviModel<CommunityInfoMviModel.Intent, CommunityInfoMviModel.UiState, CommunityInfoMviModel.Effect> by mvi {
override fun onStarted() {
mvi.onStarted()
mvi.updateState { it.copy(community = community) }
mvi.scope?.launch {
if (uiState.value.community.id == 0) {
val community = itemCache.getCommunity(communityId) ?: CommunityModel()
mvi.updateState { it.copy(community = community) }
}
settingsRepository.currentSettings.onEach {
mvi.updateState { it.copy(autoLoadImages = it.autoLoadImages) }
}.launchIn(this)
if (uiState.value.moderators.isEmpty()) {
val moderators = communityRepository.getModerators(
id = community.id
id = communityId
)
mvi.updateState { it.copy(moderators = moderators) }
}

View File

@ -9,9 +9,10 @@ val communityInfoModule = module {
factory<CommunityInfoMviModel> { params ->
CommunityInfoViewModel(
mvi = DefaultMviModel(CommunityInfoMviModel.UiState()),
community = params[0],
communityId = params[0],
communityRepository = get(),
settingsRepository = get(),
itemCache = get(),
)
}
}

View File

@ -6,7 +6,9 @@ import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.VoteFormat
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.CreatePostSection
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.LanguageModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import dev.icerock.moko.resources.desc.StringDesc
@Stable
@ -37,6 +39,9 @@ interface CreateCommentMviModel :
}
data class UiState(
val originalPost: PostModel? = null,
val originalComment: CommentModel? = null,
val editedComment: CommentModel? = null,
val postLayout: PostLayout = PostLayout.Card,
val fullHeightImages: Boolean = true,
val voteFormat: VoteFormat = VoteFormat.Aggregated,

View File

@ -82,9 +82,9 @@ import kotlinx.coroutines.flow.onEach
import org.koin.core.parameter.parametersOf
class CreateCommentScreen(
private val originalPost: PostModel? = null,
private val originalComment: CommentModel? = null,
private val editedComment: CommentModel? = null,
private val originalPostId: Int? = null,
private val originalCommentId: Int? = null,
private val editedCommentId: Int? = null,
private val initialText: String? = null,
) : Screen {
@OptIn(ExperimentalMaterial3Api::class)
@ -92,9 +92,9 @@ class CreateCommentScreen(
override fun Content() {
val model = getScreenModel<CreateCommentMviModel> {
parametersOf(
originalPost?.id,
originalComment?.id,
editedComment?.id,
originalPostId,
originalCommentId,
editedCommentId,
)
}
model.bindToLifecycle(key)
@ -109,7 +109,7 @@ class CreateCommentScreen(
var textFieldValue by remember {
mutableStateOf(
TextFieldValue(
text = (initialText ?: editedComment?.text).orEmpty()
text = (initialText ?: uiState.editedComment?.text).orEmpty()
)
)
}
@ -122,7 +122,7 @@ class CreateCommentScreen(
var selectLanguageDialogOpen by remember { mutableStateOf(false) }
LaunchedEffect(model) {
if (editedComment != null) {
uiState.editedComment?.also { editedComment ->
model.reduce(CreateCommentMviModel.Intent.ChangeLanguage(editedComment.languageId))
}
model.effects.onEach { effect ->
@ -133,7 +133,7 @@ class CreateCommentScreen(
is CreateCommentMviModel.Effect.Success -> {
notificationCenter.send(event = NotificationCenterEvent.CommentCreated)
if (originalPost != null) {
uiState.originalPost?.also { originalPost ->
notificationCenter.send(
event = NotificationCenterEvent.PostUpdated(
originalPost.copy(
@ -146,7 +146,7 @@ class CreateCommentScreen(
),
)
}
navigationCoordinator.hideBottomSheet()
navigationCoordinator.popScreen()
}
is CreateCommentMviModel.Effect.AddImageToText -> {
@ -167,7 +167,7 @@ class CreateCommentScreen(
Image(
modifier = Modifier.padding(start = Spacing.s).onClick(
onClick = rememberCallback {
navigationCoordinator.hideBottomSheet()
navigationCoordinator.popScreen()
},
),
imageVector = Icons.Default.Close,
@ -189,7 +189,7 @@ class CreateCommentScreen(
BottomSheetHandle()
Text(
text = when {
editedComment != null -> {
uiState.editedComment != null -> {
stringResource(MR.strings.edit_comment_title)
}
@ -345,57 +345,55 @@ class CreateCommentScreen(
modifier = Modifier.padding(padding),
) {
item {
when {
originalComment != null -> {
CommentCard(
modifier = referenceModifier,
comment = originalComment,
hideIndent = true,
voteFormat = uiState.voteFormat,
autoLoadImages = uiState.autoLoadImages,
options = buildList {
add(
Option(
OptionId.SeeRaw,
stringResource(MR.strings.post_action_see_raw)
)
val originalComment = uiState.originalComment
val originalPost = uiState.originalPost
if (originalComment != null) {
CommentCard(
modifier = referenceModifier,
comment = originalComment,
hideIndent = true,
voteFormat = uiState.voteFormat,
autoLoadImages = uiState.autoLoadImages,
options = buildList {
add(
Option(
OptionId.SeeRaw,
stringResource(MR.strings.post_action_see_raw)
)
},
onOptionSelected = {
rawContent = originalComment
},
)
Divider()
}
originalPost != null -> {
PostCard(
modifier = referenceModifier,
postLayout = if (uiState.postLayout == PostLayout.Card) {
uiState.postLayout
} else {
PostLayout.Full
},
fullHeightImage = uiState.fullHeightImages,
post = originalPost,
limitBodyHeight = true,
blurNsfw = false,
includeFullBody = true,
voteFormat = uiState.voteFormat,
autoLoadImages = uiState.autoLoadImages,
options = buildList {
add(
Option(
OptionId.SeeRaw,
stringResource(MR.strings.post_action_see_raw)
)
)
},
onOptionSelected = {
rawContent = originalComment
},
)
Divider()
} else if (originalPost != null) {
PostCard(
modifier = referenceModifier,
postLayout = if (uiState.postLayout == PostLayout.Card) {
uiState.postLayout
} else {
PostLayout.Full
},
fullHeightImage = uiState.fullHeightImages,
post = originalPost,
limitBodyHeight = true,
blurNsfw = false,
includeFullBody = true,
voteFormat = uiState.voteFormat,
autoLoadImages = uiState.autoLoadImages,
options = buildList {
add(
Option(
OptionId.SeeRaw,
stringResource(MR.strings.post_action_see_raw)
)
},
onOptionSelected = {
rawContent = originalPost
},
)
}
)
},
onOptionSelected = {
rawContent = originalPost
},
)
}
}
}

View File

@ -8,6 +8,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationC
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.SettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.LemmyItemCache
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepository
import com.github.diegoberaldin.raccoonforlemmy.resources.MR.strings.message_missing_field
@ -30,15 +31,28 @@ class CreateCommentViewModel(
private val themeRepository: ThemeRepository,
private val settingsRepository: SettingsRepository,
private val notificationCenter: NotificationCenter,
private val itemCache: LemmyItemCache,
) : CreateCommentMviModel,
MviModel<CreateCommentMviModel.Intent, CreateCommentMviModel.UiState, CreateCommentMviModel.Effect> by mvi {
override fun onStarted() {
mvi.onStarted()
mvi.scope?.launch {
val originalPost = postId?.let { itemCache.getPost(it) }
val originalComment = parentId?.let { itemCache.getComment(it) }
val editedComment = editedCommentId?.let { itemCache.getComment(it) }
mvi.updateState {
it.copy(
originalPost = originalPost,
originalComment = originalComment,
editedComment = editedComment,
)
}
themeRepository.postLayout.onEach { layout ->
mvi.updateState { it.copy(postLayout = layout) }
}.launchIn(this)
if (uiState.value.currentUser.isEmpty()) {
val auth = identityRepository.authToken.value.orEmpty()
val currentUser = siteRepository.getCurrentUser(auth)

View File

@ -19,6 +19,7 @@ val createCommentModule = module {
themeRepository = get(),
settingsRepository = get(),
notificationCenter = get(),
itemCache = get(),
)
}
}

View File

@ -8,6 +8,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.CreatePostSection
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.LanguageModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import dev.icerock.moko.resources.desc.StringDesc
@Stable
@ -57,6 +58,8 @@ interface CreatePostMviModel :
}
data class UiState(
val editedPost: PostModel? = null,
val crossPost: PostModel? = null,
val communityInfo: String = "",
val communityId: Int? = null,
val communityError: StringDesc? = null,

View File

@ -87,14 +87,14 @@ import org.koin.core.parameter.parametersOf
class CreatePostScreen(
private val communityId: Int? = null,
private val editedPost: PostModel? = null,
private val crossPost: PostModel? = null,
private val editedPostId: Int? = null,
private val crossPostId: Int? = null,
) : Screen {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val model = getScreenModel<CreatePostMviModel> {
parametersOf(editedPost?.id)
parametersOf(editedPostId, crossPostId)
}
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState()
@ -103,22 +103,21 @@ class CreatePostScreen(
val notificationCenter = remember { getNotificationCenter() }
val galleryHelper = remember { getGalleryHelper() }
val crossPostText = stringResource(MR.strings.create_post_cross_post_text)
val crossPost = uiState.crossPost
val editedPost = uiState.editedPost
var bodyTextFieldValue by remember {
val text = when {
crossPost != null -> buildString {
val text = buildString {
if (crossPost != null) {
append(crossPostText)
append(" ")
append(crossPost.originalUrl)
} else if (editedPost != null) {
append(editedPost.text)
}
editedPost != null -> {
editedPost.text
}
else -> ""
}
mutableStateOf(TextFieldValue(text = text))
}
val bodyFocusRequester = remember { FocusRequester() }
val urlFocusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
@ -137,6 +136,7 @@ class CreatePostScreen(
model.reduce(CreatePostMviModel.Intent.InsertImageInBody(bytes))
}
}
var openSelectCommunity by remember { mutableStateOf(false) }
val topAppBarState = rememberTopAppBarState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(topAppBarState)
@ -146,7 +146,7 @@ class CreatePostScreen(
var selectLanguageDialogOpen by remember { mutableStateOf(false) }
LaunchedEffect(model) {
val referencePost = editedPost ?: crossPost
val referencePost = uiState.editedPost ?: uiState.crossPost
model.reduce(CreatePostMviModel.Intent.SetTitle(referencePost?.title.orEmpty()))
model.reduce(CreatePostMviModel.Intent.SetUrl(referencePost?.url.orEmpty()))
if (editedPost != null) {
@ -178,7 +178,7 @@ class CreatePostScreen(
notificationCenter.send(
event = NotificationCenterEvent.PostCreated,
)
navigationCoordinator.hideBottomSheet()
navigationCoordinator.popScreen()
}
is CreatePostMviModel.Effect.AddImageToBody -> {
@ -189,7 +189,8 @@ class CreatePostScreen(
}
}.launchIn(this)
}
LaunchedEffect(notificationCenter) {
LaunchedEffect(notificationCenter)
{
notificationCenter.subscribe(NotificationCenterEvent.SelectCommunity::class)
.onEach { evt ->
model.reduce(CreatePostMviModel.Intent.SetCommunity(evt.model))
@ -211,7 +212,7 @@ class CreatePostScreen(
Image(
modifier = Modifier.padding(start = Spacing.s).onClick(
onClick = rememberCallback {
navigationCoordinator.hideBottomSheet()
navigationCoordinator.popScreen()
},
),
imageVector = Icons.Default.Close,
@ -232,7 +233,6 @@ class CreatePostScreen(
Text(
text = when {
editedPost != null -> stringResource(MR.strings.edit_post_title)
else -> stringResource(MR.strings.create_post_title)
},
style = MaterialTheme.typography.titleLarge,
@ -254,7 +254,8 @@ class CreatePostScreen(
)
},
)
}, snackbarHost = {
}, snackbarHost =
{
SnackbarHost(snackbarHostState) { data ->
Snackbar(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
@ -262,7 +263,8 @@ class CreatePostScreen(
snackbarData = data,
)
}
}) { padding ->
})
{ padding ->
Column(
modifier = Modifier.padding(padding).verticalScroll(rememberScrollState()),
) {

View File

@ -6,6 +6,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.SettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.utils.StringUtils.isValidUrl
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.LemmyItemCache
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepository
import com.github.diegoberaldin.raccoonforlemmy.resources.MR.strings.message_invalid_field
@ -18,19 +19,29 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class CreatePostViewModel(
private val editedPostId: Int?,
private val mvi: DefaultMviModel<CreatePostMviModel.Intent, CreatePostMviModel.UiState, CreatePostMviModel.Effect>,
private val editedPostId: Int?,
private val crossPostId: Int?,
private val identityRepository: IdentityRepository,
private val postRepository: PostRepository,
private val siteRepository: SiteRepository,
private val themeRepository: ThemeRepository,
private val settingsRepository: SettingsRepository,
private val itemCache: LemmyItemCache,
) : CreatePostMviModel,
MviModel<CreatePostMviModel.Intent, CreatePostMviModel.UiState, CreatePostMviModel.Effect> by mvi {
override fun onStarted() {
mvi.onStarted()
mvi.scope?.launch {
val editedPost = editedPostId?.let {
itemCache.getPost(it)
}
val crossPost = crossPostId?.let {
itemCache.getPost(it)
}
mvi.updateState { it.copy(editedPost = editedPost, crossPost = crossPost) }
themeRepository.postLayout.onEach { layout ->
mvi.updateState { it.copy(postLayout = layout) }
}.launchIn(this)

View File

@ -10,11 +10,13 @@ val createPostModule = module {
CreatePostViewModel(
mvi = DefaultMviModel(CreatePostMviModel.UiState()),
editedPostId = params[0],
crossPostId = params[1],
identityRepository = get(),
postRepository = get(),
siteRepository = get(),
themeRepository = get(),
settingsRepository = get(),
itemCache = get(),
)
}
}

View File

@ -152,8 +152,9 @@ class ModalDrawerViewModel(
val res = it - favorites.toSet()
favorites + res
}
val multiCommunitites = multiCommunityRepository.getAll(accountId).sortedBy { it.name }
val multiCommunitites = accountId?.let {
multiCommunityRepository.getAll(it).sortedBy { e -> e.name }
}.orEmpty()
mvi.updateState {
it.copy(
refreshing = false,

View File

@ -190,9 +190,11 @@ class ManageSubscriptionsScreen : Screen {
modifier = Modifier.fillMaxWidth()
.background(MaterialTheme.colorScheme.background).onClick(
onClick = rememberCallback {
navigatorCoordinator.pushScreen(
MultiCommunityScreen(community),
)
community.id?.toInt()?.also {
navigatorCoordinator.pushScreen(
MultiCommunityScreen(it),
)
}
},
),
community = community,
@ -211,7 +213,7 @@ class ManageSubscriptionsScreen : Screen {
when (optionId) {
OptionId.Edit -> {
navigatorCoordinator.pushScreen(
MultiCommunityEditorScreen(community),
MultiCommunityEditorScreen(community.id?.toInt()),
)
}

View File

@ -4,6 +4,7 @@ import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.VoteFormat
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
@ -32,6 +33,7 @@ interface MultiCommunityMviModel :
val instance: String = "",
val isLogged: Boolean = false,
val sortType: SortType? = null,
val community: MultiCommunityModel = MultiCommunityModel(),
val posts: List<PostModel> = emptyList(),
val blurNsfw: Boolean = true,
val swipeActionsEnabled: Boolean = true,

View File

@ -75,7 +75,6 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.di.getFabN
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.ShareBottomSheet
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.SortBottomSheet
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.di.getSettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallback
@ -83,7 +82,6 @@ import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallb
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.getAdditionalLabel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toIcon
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import com.github.diegoberaldin.raccoonforlemmy.unit.createcomment.CreateCommentScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createreport.CreateReportScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.web.WebViewScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.zoomableimage.ZoomableImageScreen
@ -91,15 +89,18 @@ import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.koin.core.parameter.parametersOf
class MultiCommunityScreen(
private val community: MultiCommunityModel,
private val communityId: Int,
) : Screen {
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
override fun Content() {
val model = getScreenModel<MultiCommunityMviModel>()
val model = getScreenModel<MultiCommunityMviModel>(parameters = {
parametersOf(communityId)
})
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState()
val topAppBarState = rememberTopAppBarState()
@ -136,7 +137,7 @@ class MultiCommunityScreen(
},
title = {
Text(
text = community.name,
text = uiState.community.name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
@ -302,13 +303,9 @@ class MultiCommunityScreen(
model.reduce(MultiCommunityMviModel.Intent.UpVotePost(post.id))
},
onSecondDismissToStart = rememberCallback(model) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = post,
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = post,
)
},
onDismissToEnd = {
model.reduce(MultiCommunityMviModel.Intent.DownVotePost(post.id))

View File

@ -6,11 +6,11 @@ import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.MultiCommunityRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.SettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.utils.imagepreload.ImagePreloadManager
import com.github.diegoberaldin.raccoonforlemmy.core.utils.share.ShareHelper
import com.github.diegoberaldin.raccoonforlemmy.core.utils.vibrate.HapticFeedback
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.ApiConfigurationRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
@ -25,13 +25,14 @@ import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MultiCommunityViewModel(
private val mvi: DefaultMviModel<MultiCommunityMviModel.Intent, MultiCommunityMviModel.UiState, MultiCommunityMviModel.Effect>,
private val community: MultiCommunityModel,
private val communityId: Int,
private val postRepository: PostRepository,
private val identityRepository: IdentityRepository,
private val apiConfigurationRepository: ApiConfigurationRepository,
private val multiCommunityRepository: MultiCommunityRepository,
private val siteRepository: SiteRepository,
private val themeRepository: ThemeRepository,
private val shareHelper: ShareHelper,
@ -49,6 +50,11 @@ class MultiCommunityViewModel(
override fun onStarted() {
mvi.onStarted()
mvi.scope?.launch {
if ((uiState.value.community.id ?: 0) == 0L) {
val community =
multiCommunityRepository.getById(communityId.toLong()) ?: MultiCommunityModel()
mvi.updateState { it.copy(community = community) }
}
themeRepository.postLayout.onEach { layout ->
mvi.updateState { it.copy(postLayout = layout) }
}.launchIn(this)
@ -80,20 +86,19 @@ class MultiCommunityViewModel(
val user = siteRepository.getCurrentUser(auth)
mvi.updateState { it.copy(currentUserId = user?.id ?: 0) }
}
}
mvi.scope?.launch(Dispatchers.IO) {
if (uiState.value.posts.isEmpty()) {
val settings = settingsRepository.currentSettings.value
val sortTypes = getSortTypesUseCase.getTypesForPosts()
mvi.updateState {
it.copy(
sortType = settings.defaultPostSortType.toSortType(),
availableSortTypes = sortTypes,
)
withContext(Dispatchers.IO) {
if (uiState.value.posts.isEmpty()) {
val settings = settingsRepository.currentSettings.value
val sortTypes = getSortTypesUseCase.getTypesForPosts()
mvi.updateState {
it.copy(
sortType = settings.defaultPostSortType.toSortType(),
availableSortTypes = sortTypes,
)
}
paginator.setCommunities(uiState.value.community.communityIds)
refresh()
}
paginator.setCommunities(community.communityIds)
refresh()
}
}
}

View File

@ -13,10 +13,9 @@ val multiCommunityModule = module {
factory<MultiCommunityMviModel> { params ->
MultiCommunityViewModel(
mvi = DefaultMviModel(MultiCommunityMviModel.UiState()),
community = params[0],
communityId = params[0],
postRepository = get(),
identityRepository = get(),
apiConfigurationRepository = get(),
siteRepository = get(),
themeRepository = get(),
shareHelper = get(),
@ -26,6 +25,7 @@ val multiCommunityModule = module {
paginator = get(),
imagePreloadManager = get(),
getSortTypesUseCase = get(),
multiCommunityRepository = get(),
)
}
factory<MultiCommunityPaginator> {
@ -36,7 +36,7 @@ val multiCommunityModule = module {
factory<MultiCommunityEditorMviModel> { params ->
MultiCommunityEditorViewModel(
mvi = DefaultMviModel(MultiCommunityEditorMviModel.UiState()),
editedCommunity = params[0],
communityId = params[0],
identityRepository = get(),
communityRepository = get(),
accountRepository = get(),

View File

@ -60,7 +60,6 @@ import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycl
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CustomImage
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.CommunityItem
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallback
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
@ -71,13 +70,13 @@ import kotlinx.coroutines.flow.onEach
import org.koin.core.parameter.parametersOf
class MultiCommunityEditorScreen(
private val editedCommunity: MultiCommunityModel? = null,
private val communityId: Int? = null,
) : Screen {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val model = getScreenModel<MultiCommunityEditorMviModel> { parametersOf(editedCommunity) }
val model = getScreenModel<MultiCommunityEditorMviModel> { parametersOf(communityId) }
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState()
val navigationCoordinator = remember { getNavigationCoordinator() }

View File

@ -23,7 +23,7 @@ import kotlinx.coroutines.launch
class MultiCommunityEditorViewModel(
private val mvi: DefaultMviModel<MultiCommunityEditorMviModel.Intent, MultiCommunityEditorMviModel.UiState, MultiCommunityEditorMviModel.Effect>,
private val editedCommunity: MultiCommunityModel? = null,
private val communityId: Int?,
private val identityRepository: IdentityRepository,
private val communityRepository: CommunityRepository,
private val multiCommunityRepository: MultiCommunityRepository,
@ -60,6 +60,9 @@ class MultiCommunityEditorViewModel(
private fun populate() {
mvi.scope?.launch(Dispatchers.IO) {
val editedCommunity = communityId?.toLong()?.let {
multiCommunityRepository.getById(it)
}
val auth = identityRepository.authToken.value
communities = communityRepository.getSubscribed(auth).sortedBy { it.name }.map { c ->
c to (editedCommunity?.communityIds?.contains(c.id) == true)
@ -145,19 +148,21 @@ class MultiCommunityEditorViewModel(
return
}
val icon = currentState.icon
val communityIds = currentState.communities.filter { it.second }.map { it.first.id }
val multiCommunity = editedCommunity?.copy(
name = name,
icon = icon,
communityIds = communityIds,
) ?: MultiCommunityModel(
name = name,
icon = icon,
communityIds = communityIds,
)
mvi.scope?.launch(Dispatchers.IO) {
val icon = currentState.icon
val communityIds = currentState.communities.filter { it.second }.map { it.first.id }
val editedCommunity = communityId?.toLong()?.let {
multiCommunityRepository.getById(it)
}
val multiCommunity = editedCommunity?.copy(
name = name,
icon = icon,
communityIds = communityIds,
) ?: MultiCommunityModel(
name = name,
icon = icon,
communityIds = communityIds,
)
val accountId = accountRepository.getActive()?.id ?: return@launch
if (multiCommunity.id == null) {
val id = multiCommunityRepository.create(multiCommunity, accountId)

View File

@ -58,8 +58,6 @@ import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallb
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import com.github.diegoberaldin.raccoonforlemmy.unit.createcomment.CreateCommentScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createpost.CreatePostScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.rawcontent.RawContentDialog
import com.github.diegoberaldin.raccoonforlemmy.unit.web.WebViewScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.zoomableimage.ZoomableImageScreen
@ -259,14 +257,9 @@ object ProfileLoggedScreen : Tab {
)
OptionId.Edit -> {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
showBottomSheet(
CreatePostScreen(
editedPost = post,
)
)
}
detailOpener.openCreatePost(
editedPost = post,
)
}
OptionId.SeeRaw -> {
@ -327,8 +320,7 @@ object ProfileLoggedScreen : Tab {
items = uiState.comments,
key = { it.id.toString() + it.updateDate },
) { comment ->
CommentCard(
modifier = Modifier.background(MaterialTheme.colorScheme.background),
CommentCard(modifier = Modifier.background(MaterialTheme.colorScheme.background),
comment = comment,
voteFormat = uiState.voteFormat,
autoLoadImages = uiState.autoLoadImages,
@ -396,12 +388,9 @@ object ProfileLoggedScreen : Tab {
}
OptionId.Edit -> {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
showBottomSheet(
CreateCommentScreen(editedComment = comment)
)
}
detailOpener.openReply(
editedComment = comment,
)
}
OptionId.SeeRaw -> {
@ -410,8 +399,7 @@ object ProfileLoggedScreen : Tab {
else -> Unit
}
}
)
})
Divider(
modifier = Modifier.padding(vertical = Spacing.xxxs),
thickness = 0.25.dp
@ -466,8 +454,7 @@ object ProfileLoggedScreen : Tab {
if (rawContent != null) {
when (val content = rawContent) {
is PostModel -> {
RawContentDialog(
title = content.title,
RawContentDialog(title = content.title,
publishDate = content.publishDate,
updateDate = content.updateDate,
url = content.url,
@ -478,27 +465,20 @@ object ProfileLoggedScreen : Tab {
onQuote = rememberCallbackArgs { quotation ->
rawContent = null
if (quotation != null) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen =
CreateCommentScreen(
originalPost = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
}
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
}
}
)
})
}
is CommentModel -> {
RawContentDialog(
text = content.text,
RawContentDialog(text = content.text,
publishDate = content.publishDate,
updateDate = content.updateDate,
onDismiss = {
@ -507,21 +487,16 @@ object ProfileLoggedScreen : Tab {
onQuote = rememberCallbackArgs { quotation ->
rawContent = null
if (quotation != null) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalComment = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
}
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalComment = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
}
}
)
})
}
}
}

View File

@ -100,8 +100,6 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.containsId
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toIcon
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import com.github.diegoberaldin.raccoonforlemmy.unit.ban.BanUserScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createcomment.CreateCommentScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createpost.CreatePostScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createreport.CreateReportScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.rawcontent.RawContentDialog
import com.github.diegoberaldin.raccoonforlemmy.unit.remove.RemoveScreen
@ -114,14 +112,14 @@ import kotlinx.coroutines.launch
import org.koin.core.parameter.parametersOf
class PostDetailScreen(
private val post: PostModel,
private val postId: Int,
private val otherInstance: String = "",
private val highlightCommentId: Int? = null,
private val isMod: Boolean = false,
) : Screen {
override val key: ScreenKey
get() = super.key + post.id.toString()
get() = super.key + postId.toString()
@OptIn(
ExperimentalMaterial3Api::class,
@ -131,17 +129,16 @@ class PostDetailScreen(
@Composable
override fun Content() {
val model = getScreenModel<PostDetailMviModel>(
tag = post.id.toString() + highlightCommentId.toString(),
tag = postId.toString() + highlightCommentId.toString(),
parameters = {
parametersOf(
post,
postId,
otherInstance,
highlightCommentId,
isMod,
)
}
)
model.bindToLifecycle(key + post.id.toString())
})
model.bindToLifecycle(key + postId.toString())
val uiState by model.uiState.collectAsState()
val isOnOtherInstance = remember { otherInstance.isNotEmpty() }
val otherInstanceName = remember { otherInstance }
@ -169,9 +166,6 @@ class PostDetailScreen(
LaunchedEffect(notificationCenter) {
notificationCenter.resetCache()
}
LaunchedEffect(navigationCoordinator) {
navigationCoordinator.setBottomSheetGesturesEnabled(true)
}
LaunchedEffect(model) {
model.effects.onEach { evt ->
when (evt) {
@ -193,8 +187,7 @@ class PostDetailScreen(
}
Scaffold(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
modifier = Modifier.background(MaterialTheme.colorScheme.background)
.padding(Spacing.xs),
topBar = {
TopAppBar(
@ -268,13 +261,9 @@ class PostDetailScreen(
icon = Icons.Default.Reply,
text = stringResource(MR.strings.action_reply),
onSelected = rememberCallback {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = uiState.post,
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = uiState.post,
)
},
)
}
@ -297,8 +286,7 @@ class PostDetailScreen(
} else {
it
}
}.nestedScroll(fabNestedScrollConnection)
.pullRefresh(pullRefreshState),
}.nestedScroll(fabNestedScrollConnection).pullRefresh(pullRefreshState),
) {
LazyColumn(
state = lazyListState
@ -357,13 +345,9 @@ class PostDetailScreen(
},
onReply = rememberCallback {
if (uiState.isLogged && !isOnOtherInstance) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = uiState.post,
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = uiState.post,
)
}
},
options = buildList {
@ -441,12 +425,7 @@ class PostDetailScreen(
OptionId.Delete -> model.reduce(PostDetailMviModel.Intent.DeletePost)
OptionId.Edit -> {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
showBottomSheet(
CreatePostScreen(editedPost = uiState.post),
)
}
detailOpener.openCreatePost(editedPost = uiState.post)
}
OptionId.Report -> {
@ -456,12 +435,7 @@ class PostDetailScreen(
}
OptionId.CrossPost -> {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
showBottomSheet(
CreatePostScreen(crossPost = uiState.post),
)
}
detailOpener.openCreatePost(crossPost = uiState.post)
}
OptionId.SeeRaw -> {
@ -649,14 +623,10 @@ class PostDetailScreen(
)
},
onSecondDismissToStart = rememberCallback(model) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = uiState.post,
originalComment = comment,
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = uiState.post,
originalComment = comment,
)
},
onDismissToEnd = rememberCallback(model) {
model.reduce(
@ -762,14 +732,10 @@ class PostDetailScreen(
},
onReply = rememberCallback {
if (uiState.isLogged && !isOnOtherInstance) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = uiState.post,
originalComment = comment,
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = uiState.post,
originalComment = comment,
)
}
},
onOpenCreator = rememberCallbackArgs { user, instance ->
@ -777,8 +743,7 @@ class PostDetailScreen(
},
onOpenCommunity = rememberCallbackArgs { community, instance ->
detailOpener.openCommunityDetail(
community,
instance
community, instance
)
},
onOpenPost = rememberCallbackArgs { p, instance ->
@ -862,16 +827,9 @@ class PostDetailScreen(
)
OptionId.Edit -> {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(
false,
)
showBottomSheet(
CreateCommentScreen(
editedComment = comment,
),
)
}
detailOpener.openReply(
editedComment = comment,
)
}
OptionId.Report -> {
@ -979,14 +937,10 @@ class PostDetailScreen(
},
onReply = rememberCallback(model) {
if (uiState.isLogged && !isOnOtherInstance) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = uiState.post,
originalComment = comment,
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = uiState.post,
originalComment = comment,
)
}
},
onOpenCreator = rememberCallbackArgs { user ->
@ -1058,14 +1012,9 @@ class PostDetailScreen(
)
OptionId.Edit -> {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
showBottomSheet(
CreateCommentScreen(
editedComment = comment,
),
)
}
detailOpener.openReply(
editedComment = comment,
)
}
OptionId.Report -> {
@ -1174,8 +1123,7 @@ class PostDetailScreen(
Column {
if (uiState.post.comments == 0) {
Text(
modifier = Modifier
.fillMaxWidth()
modifier = Modifier.fillMaxWidth()
.padding(top = Spacing.xs),
textAlign = TextAlign.Center,
text = stringResource(MR.strings.message_empty_comments),
@ -1184,8 +1132,7 @@ class PostDetailScreen(
)
} else {
Text(
modifier = Modifier
.fillMaxWidth()
modifier = Modifier.fillMaxWidth()
.padding(top = Spacing.xs),
textAlign = TextAlign.Center,
text = stringResource(MR.strings.message_error_loading_comments),
@ -1240,18 +1187,14 @@ class PostDetailScreen(
onQuote = rememberCallbackArgs { quotation ->
rawContent = null
if (quotation != null) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
}
},
)
@ -1268,19 +1211,15 @@ class PostDetailScreen(
onQuote = rememberCallbackArgs { quotation ->
rawContent = null
if (quotation != null) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = uiState.post,
originalComment = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = uiState.post,
originalComment = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
}
},
)

View File

@ -18,6 +18,7 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toSortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommunityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.GetSortTypesUseCase
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.LemmyItemCache
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepository
import kotlinx.coroutines.Dispatchers
@ -28,7 +29,7 @@ import kotlinx.coroutines.launch
class PostDetailViewModel(
private val mvi: DefaultMviModel<PostDetailMviModel.Intent, PostDetailMviModel.UiState, PostDetailMviModel.Effect>,
private val post: PostModel,
private val postId: Int,
private val otherInstance: String,
private val highlightCommentId: Int?,
private val isModerator: Boolean,
@ -44,6 +45,7 @@ class PostDetailViewModel(
private val notificationCenter: NotificationCenter,
private val hapticFeedback: HapticFeedback,
private val getSortTypesUseCase: GetSortTypesUseCase,
private val itemCache: LemmyItemCache,
) : PostDetailMviModel,
MviModel<PostDetailMviModel.Intent, PostDetailMviModel.UiState, PostDetailMviModel.Effect> by mvi {
@ -60,6 +62,15 @@ class PostDetailViewModel(
)
}
mvi.scope?.launch {
if (uiState.value.post.id == 0) {
val post = itemCache.getPost(postId) ?: PostModel()
mvi.updateState {
it.copy(
post = post,
isModerator = isModerator,
)
}
}
notificationCenter.subscribe(NotificationCenterEvent.PostUpdated::class).onEach { evt ->
handlePostUpdate(evt.model)
}.launchIn(this)
@ -121,12 +132,11 @@ class PostDetailViewModel(
notificationCenter.subscribe(NotificationCenterEvent.Share::class).onEach { evt ->
shareHelper.share(evt.url)
}.launchIn(this)
}
mvi.scope?.launch(Dispatchers.IO) {
identityRepository.isLogged.onEach { logged ->
mvi.updateState { it.copy(isLogged = logged ?: false) }
}.launchIn(this)
if (uiState.value.currentUserId == null) {
val auth = identityRepository.authToken.value.orEmpty()
val user = siteRepository.getCurrentUser(auth)
@ -135,16 +145,9 @@ class PostDetailViewModel(
}
}
mvi.updateState {
it.copy(
post = post,
isModerator = isModerator,
)
}
val auth = identityRepository.authToken.value
val updatedPost = postRepository.get(
id = post.id,
id = postId,
auth = auth,
instance = otherInstance,
)
@ -163,7 +166,7 @@ class PostDetailViewModel(
highlightCommentPath = comment?.path
}
if (isModerator) {
post.community?.id?.also { communityId ->
uiState.value.post.community?.id?.also { communityId ->
val moderators = communityRepository.getModerators(
auth = auth,
id = communityId
@ -174,7 +177,7 @@ class PostDetailViewModel(
}
}
if (post.text.isEmpty() && post.title.isEmpty()) {
if (uiState.value.post.text.isEmpty() && uiState.value.post.title.isEmpty()) {
refreshPost()
}
if (mvi.uiState.value.comments.isEmpty()) {
@ -315,10 +318,10 @@ class PostDetailViewModel(
mvi.scope?.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value
val updatedPost = postRepository.get(
id = post.id,
id = postId,
auth = auth,
instance = otherInstance,
) ?: post
) ?: uiState.value.post
mvi.updateState {
it.copy(post = updatedPost)
}
@ -345,7 +348,7 @@ class PostDetailViewModel(
val sort = currentState.sortType
val itemList = commentRepository.getAll(
auth = auth,
postId = post.id,
postId = postId,
instance = otherInstance,
page = currentPage,
sort = sort,
@ -633,9 +636,9 @@ class PostDetailViewModel(
private fun deletePost() {
mvi.scope?.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value.orEmpty()
postRepository.delete(id = post.id, auth = auth)
postRepository.delete(id = postId, auth = auth)
notificationCenter.send(
event = NotificationCenterEvent.PostDeleted(post),
event = NotificationCenterEvent.PostDeleted(uiState.value.post),
)
mvi.emitEffect(PostDetailMviModel.Effect.Close)
}
@ -719,6 +722,7 @@ class PostDetailViewModel(
mvi.scope?.launch(Dispatchers.IO) {
val isModerator = uiState.value.moderators.containsId(userId)
val auth = identityRepository.authToken.value.orEmpty()
val post = uiState.value.post
val communityId = post.community?.id
if (communityId != null) {
val newModerators = communityRepository.addModerator(

View File

@ -9,7 +9,7 @@ val postDetailModule = module {
factory<PostDetailMviModel> { params ->
PostDetailViewModel(
mvi = DefaultMviModel(PostDetailMviModel.UiState()),
post = params[0],
postId = params[0],
otherInstance = params[1],
highlightCommentId = params[2],
isModerator = params[3],
@ -25,6 +25,7 @@ val postDetailModule = module {
notificationCenter = get(),
hapticFeedback = get(),
getSortTypesUseCase = get(),
itemCache = get(),
)
}
}

View File

@ -80,8 +80,6 @@ import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallb
import com.github.diegoberaldin.raccoonforlemmy.core.utils.keepscreenon.rememberKeepScreenOn
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import com.github.diegoberaldin.raccoonforlemmy.unit.createcomment.CreateCommentScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createpost.CreatePostScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createreport.CreateReportScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.rawcontent.RawContentDialog
import com.github.diegoberaldin.raccoonforlemmy.unit.web.WebViewScreen
@ -345,13 +343,9 @@ class PostListScreen : Screen {
model.reduce(PostListMviModel.Intent.UpVotePost(post.id))
},
onSecondDismissToStart = rememberCallback(model) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = post,
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = post,
)
},
onDismissToEnd = rememberCallback(model) {
model.reduce(PostListMviModel.Intent.DownVotePost(post.id))
@ -518,12 +512,7 @@ class PostListScreen : Screen {
)
OptionId.Edit -> {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
showBottomSheet(
CreatePostScreen(editedPost = post),
)
}
detailOpener.openCreatePost(editedPost = post)
}
OptionId.Report -> {
@ -533,12 +522,7 @@ class PostListScreen : Screen {
}
OptionId.CrossPost -> {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
showBottomSheet(
CreatePostScreen(crossPost = post),
)
}
detailOpener.openCreatePost(crossPost = post)
}
OptionId.SeeRaw -> {
@ -668,19 +652,14 @@ class PostListScreen : Screen {
onQuote = rememberCallbackArgs { quotation ->
rawContent = null
if (quotation != null) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen =
CreateCommentScreen(
originalPost = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
}
}
)

View File

@ -69,7 +69,6 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toIcon
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import com.github.diegoberaldin.raccoonforlemmy.unit.createcomment.CreateCommentScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createreport.CreateReportScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.rawcontent.RawContentDialog
import com.github.diegoberaldin.raccoonforlemmy.unit.web.WebViewScreen
@ -373,14 +372,11 @@ class SavedItemsScreen : Screen {
)
},
onReply = {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = PostModel(id = comment.postId),
originalComment = comment,
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = PostModel(id = comment.postId),
originalComment = comment,
)
},
options = buildList {
add(
@ -475,18 +471,14 @@ class SavedItemsScreen : Screen {
onQuote = rememberCallbackArgs { quotation ->
rawContent = null
if (quotation != null) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
}
},
)
@ -503,18 +495,14 @@ class SavedItemsScreen : Screen {
onQuote = rememberCallbackArgs { quotation ->
rawContent = null
if (quotation != null) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalComment = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalComment = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
}
}
)

View File

@ -100,12 +100,9 @@ import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallb
import com.github.diegoberaldin.raccoonforlemmy.core.utils.toLocalDp
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toIcon
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import com.github.diegoberaldin.raccoonforlemmy.unit.chat.InboxChatScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createcomment.CreateCommentScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createpost.CreatePostScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.createreport.CreateReportScreen
import com.github.diegoberaldin.raccoonforlemmy.unit.rawcontent.RawContentDialog
import com.github.diegoberaldin.raccoonforlemmy.unit.userinfo.UserInfoScreen
@ -118,21 +115,19 @@ import kotlinx.coroutines.launch
import org.koin.core.parameter.parametersOf
class UserDetailScreen(
private val user: UserModel,
private val userId: Int,
private val otherInstance: String = "",
) : Screen {
override val key: ScreenKey
get() = super.key + user.id.toString()
get() = super.key + userId.toString()
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable
override fun Content() {
val model = getScreenModel<UserDetailMviModel>(
tag = user.id.toString(),
parameters = { parametersOf(user, otherInstance) }
)
model.bindToLifecycle(key + user.id.toString())
val model = getScreenModel<UserDetailMviModel>(tag = userId.toString(),
parameters = { parametersOf(userId, otherInstance) })
model.bindToLifecycle(key + userId.toString())
val uiState by model.uiState.collectAsState()
val lazyListState = rememberLazyListState()
val scope = rememberCoroutineScope()
@ -181,194 +176,185 @@ class UserDetailScreen(
}.launchIn(this)
}
Scaffold(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.padding(Spacing.xs),
topBar = {
val userName = user.name
val userHost = user.host
val maxTopInset = Dimensions.topBarHeight.value.toInt()
var topInset by remember { mutableStateOf(maxTopInset) }
snapshotFlow { topAppBarState.collapsedFraction }.onEach {
topInset = (maxTopInset * (1 - it)).toInt()
}.launchIn(scope)
Scaffold(modifier = Modifier.background(MaterialTheme.colorScheme.background)
.padding(Spacing.xs), topBar = {
val userName = uiState.user.name
val userHost = uiState.user.host
val maxTopInset = Dimensions.topBarHeight.value.toInt()
var topInset by remember { mutableStateOf(maxTopInset) }
snapshotFlow { topAppBarState.collapsedFraction }.onEach {
topInset = (maxTopInset * (1 - it)).toInt()
}.launchIn(scope)
TopAppBar(
windowInsets = if (settings.edgeToEdge) {
WindowInsets(0, topInset, 0, 0)
} else {
TopAppBarDefaults.windowInsets
},
scrollBehavior = scrollBehavior,
title = {
Text(
modifier = Modifier.padding(horizontal = Spacing.s),
text = buildString {
append(userName)
if (userHost.isNotEmpty()) {
append("@$userHost")
}
TopAppBar(
windowInsets = if (settings.edgeToEdge) {
WindowInsets(0, topInset, 0, 0)
} else {
TopAppBarDefaults.windowInsets
},
scrollBehavior = scrollBehavior,
title = {
Text(
modifier = Modifier.padding(horizontal = Spacing.s),
text = buildString {
append(userName)
if (userHost.isNotEmpty()) {
append("@$userHost")
}
},
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
actions = {
// sort button
Image(
modifier = Modifier.onClick(
onClick = rememberCallback {
val sheet = SortBottomSheet(
sheetKey = key,
values = uiState.availableSortTypes,
comments = false,
expandTop = true,
)
navigationCoordinator.showBottomSheet(sheet)
},
maxLines = 1,
overflow = TextOverflow.Ellipsis,
),
imageVector = uiState.sortType.toIcon(),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
// options menu
Box {
val options = listOf(
Option(
OptionId.Info, stringResource(MR.strings.user_detail_info)
),
Option(
OptionId.Block,
stringResource(MR.strings.community_detail_block)
),
Option(
OptionId.BlockInstance,
stringResource(MR.strings.community_detail_block_instance)
),
)
},
actions = {
// sort button
var optionsExpanded by remember { mutableStateOf(false) }
var optionsOffset by remember { mutableStateOf(Offset.Zero) }
Image(
modifier = Modifier.onClick(
modifier = Modifier.onGloballyPositioned {
optionsOffset = it.positionInParent()
}.padding(start = Spacing.s).onClick(
onClick = rememberCallback {
val sheet = SortBottomSheet(
sheetKey = key,
values = uiState.availableSortTypes,
comments = false,
expandTop = true,
)
navigationCoordinator.showBottomSheet(sheet)
optionsExpanded = true
},
),
imageVector = uiState.sortType.toIcon(),
imageVector = Icons.Default.MoreVert,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
CustomDropDown(
expanded = optionsExpanded,
onDismiss = {
optionsExpanded = false
},
offset = DpOffset(
x = optionsOffset.x.toLocalDp(),
y = optionsOffset.y.toLocalDp(),
),
) {
options.forEach { option ->
Text(
modifier = Modifier.padding(
horizontal = Spacing.m,
vertical = Spacing.s,
).onClick(
onClick = rememberCallback {
optionsExpanded = false
when (option.id) {
OptionId.BlockInstance -> model.reduce(
UserDetailMviModel.Intent.BlockInstance
)
// options menu
Box {
val options = listOf(
Option(
OptionId.Info,
stringResource(MR.strings.user_detail_info)
),
Option(
OptionId.Block,
stringResource(MR.strings.community_detail_block)
),
Option(
OptionId.BlockInstance,
stringResource(MR.strings.community_detail_block_instance)
),
)
var optionsExpanded by remember { mutableStateOf(false) }
var optionsOffset by remember { mutableStateOf(Offset.Zero) }
Image(
modifier = Modifier.onGloballyPositioned {
optionsOffset = it.positionInParent()
}.padding(start = Spacing.s).onClick(
onClick = rememberCallback {
optionsExpanded = true
},
),
imageVector = Icons.Default.MoreVert,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
CustomDropDown(
expanded = optionsExpanded,
onDismiss = {
optionsExpanded = false
},
offset = DpOffset(
x = optionsOffset.x.toLocalDp(),
y = optionsOffset.y.toLocalDp(),
),
) {
options.forEach { option ->
Text(
modifier = Modifier.padding(
horizontal = Spacing.m,
vertical = Spacing.s,
).onClick(
onClick = rememberCallback {
optionsExpanded = false
when (option.id) {
OptionId.BlockInstance -> model.reduce(
UserDetailMviModel.Intent.BlockInstance
OptionId.Block -> model.reduce(
UserDetailMviModel.Intent.Block
)
OptionId.Info -> {
navigationCoordinator.showBottomSheet(
UserInfoScreen(uiState.user.id),
)
OptionId.Block -> model.reduce(
UserDetailMviModel.Intent.Block
)
OptionId.Info -> {
navigationCoordinator.showBottomSheet(
UserInfoScreen(uiState.user),
)
}
else -> Unit
}
},
),
text = option.text,
)
}
}
}
},
navigationIcon = {
if (navigationCoordinator.canPop.value) {
Image(
modifier = Modifier.onClick(
onClick = rememberCallback {
navigationCoordinator.popScreen()
},
),
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
}
},
)
},
floatingActionButton = {
AnimatedVisibility(
visible = isFabVisible,
enter = slideInVertically(
initialOffsetY = { it * 2 },
),
exit = slideOutVertically(
targetOffsetY = { it * 2 },
),
) {
FloatingActionButtonMenu(
items = buildList {
this += FloatingActionButtonMenuItem(
icon = Icons.Default.ExpandLess,
text = stringResource(MR.strings.action_back_to_top),
onSelected = rememberCallback {
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
},
)
if (uiState.isLogged && !isOnOtherInstance) {
this += FloatingActionButtonMenuItem(
icon = Icons.Default.Chat,
text = stringResource(MR.strings.action_chat),
onSelected = rememberCallback {
val screen = InboxChatScreen(otherUserId = user.id)
navigationCoordinator.pushScreen(screen)
},
else -> Unit
}
},
),
text = option.text,
)
}
}
}
},
navigationIcon = {
if (navigationCoordinator.canPop.value) {
Image(
modifier = Modifier.onClick(
onClick = rememberCallback {
navigationCoordinator.popScreen()
},
),
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
}
},
)
}, floatingActionButton = {
AnimatedVisibility(
visible = isFabVisible,
enter = slideInVertically(
initialOffsetY = { it * 2 },
),
exit = slideOutVertically(
targetOffsetY = { it * 2 },
),
) {
FloatingActionButtonMenu(items = buildList {
this += FloatingActionButtonMenuItem(
icon = Icons.Default.ExpandLess,
text = stringResource(MR.strings.action_back_to_top),
onSelected = rememberCallback {
scope.launch {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
},
)
}
},
snackbarHost = {
SnackbarHost(snackbarHostState) { data ->
Snackbar(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
snackbarData = data,
)
}
if (uiState.isLogged && !isOnOtherInstance) {
this += FloatingActionButtonMenuItem(
icon = Icons.Default.Chat,
text = stringResource(MR.strings.action_chat),
onSelected = rememberCallback {
val screen = InboxChatScreen(otherUserId = userId)
navigationCoordinator.pushScreen(screen)
},
)
}
})
}
) { padding ->
}, snackbarHost = {
SnackbarHost(snackbarHostState) { data ->
Snackbar(
containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
snackbarData = data,
)
}
}) { padding ->
val pullRefreshState = rememberPullRefreshState(
refreshing = uiState.refreshing,
onRefresh = rememberCallback(model) {
@ -376,17 +362,13 @@ class UserDetailScreen(
},
)
Box(
modifier = Modifier
.padding(padding)
.let {
if (settings.hideNavigationBarWhileScrolling) {
it.nestedScroll(scrollBehavior.nestedScrollConnection)
} else {
it
}
modifier = Modifier.padding(padding).let {
if (settings.hideNavigationBarWhileScrolling) {
it.nestedScroll(scrollBehavior.nestedScrollConnection)
} else {
it
}
.nestedScroll(fabNestedScrollConnection)
.pullRefresh(pullRefreshState),
}.nestedScroll(fabNestedScrollConnection).pullRefresh(pullRefreshState),
) {
LazyColumn(
state = lazyListState,
@ -506,13 +488,9 @@ class UserDetailScreen(
)
},
onSecondDismissToStart = rememberCallback(model) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = post,
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = post,
)
},
onDismissToEnd = rememberCallback(model) {
model.reduce(
@ -642,12 +620,7 @@ class UserDetailScreen(
}
OptionId.CrossPost -> {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
showBottomSheet(
CreatePostScreen(crossPost = post),
)
}
detailOpener.openCreatePost(crossPost = post)
}
OptionId.SeeRaw -> {
@ -773,14 +746,10 @@ class UserDetailScreen(
)
},
onSecondDismissToStart = rememberCallback(model) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = PostModel(id = comment.postId),
originalComment = comment,
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = PostModel(id = comment.postId),
originalComment = comment,
)
},
onDismissToEnd = rememberCallback(model) {
model.reduce(
@ -855,14 +824,10 @@ class UserDetailScreen(
null
} else {
rememberCallback {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = PostModel(id = comment.postId),
originalComment = comment,
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = PostModel(id = comment.postId),
originalComment = comment,
)
}
},
onOpenCommunity = rememberCallbackArgs { community, instance ->
@ -982,26 +947,21 @@ class UserDetailScreen(
onQuote = rememberCallbackArgs { quotation ->
rawContent = null
if (quotation != null) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalPost = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalPost = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
}
}
},
)
}
is CommentModel -> {
RawContentDialog(
text = content.text,
RawContentDialog(text = content.text,
publishDate = content.publishDate,
updateDate = content.updateDate,
onDismiss = {
@ -1010,21 +970,16 @@ class UserDetailScreen(
onQuote = rememberCallbackArgs { quotation ->
rawContent = null
if (quotation != null) {
with(navigationCoordinator) {
setBottomSheetGesturesEnabled(false)
val screen = CreateCommentScreen(
originalComment = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
showBottomSheet(screen)
}
detailOpener.openReply(
originalComment = content,
initialText = buildString {
append("> ")
append(quotation)
append("\n\n")
},
)
}
}
)
})
}
}
}

View File

@ -20,6 +20,7 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.imageUrl
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toSortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.GetSortTypesUseCase
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.LemmyItemCache
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository
@ -34,7 +35,7 @@ import kotlinx.coroutines.withContext
class UserDetailViewModel(
private val mvi: DefaultMviModel<UserDetailMviModel.Intent, UserDetailMviModel.UiState, UserDetailMviModel.Effect>,
private val user: UserModel,
private val userId: Int,
private val otherInstance: String = "",
private val identityRepository: IdentityRepository,
private val apiConfigurationRepository: ApiConfigurationRepository,
@ -49,6 +50,7 @@ class UserDetailViewModel(
private val notificationCenter: NotificationCenter,
private val imagePreloadManager: ImagePreloadManager,
private val getSortTypesUseCase: GetSortTypesUseCase,
private val itemCache: LemmyItemCache,
) : UserDetailMviModel,
MviModel<UserDetailMviModel.Intent, UserDetailMviModel.UiState, UserDetailMviModel.Effect> by mvi {
@ -63,6 +65,12 @@ class UserDetailViewModel(
)
}
mvi.scope?.launch {
if (uiState.value.user.id == 0) {
val user = itemCache.getUser(userId) ?: UserModel()
mvi.updateState {
it.copy(user = user)
}
}
themeRepository.postLayout.onEach { layout ->
mvi.updateState { it.copy(postLayout = layout) }
}.launchIn(this)
@ -77,11 +85,7 @@ class UserDetailViewModel(
shareHelper.share(evt.url)
}.launchIn(this)
}
mvi.updateState {
it.copy(
user = it.user.takeIf { u -> u.id != 0 } ?: user,
)
}
mvi.scope?.launch {
settingsRepository.currentSettings.onEach { settings ->
mvi.updateState {
@ -236,10 +240,10 @@ class UserDetailViewModel(
}
val auth = identityRepository.authToken.value
val refreshedUser = userRepository.get(
id = user.id,
id = userId,
auth = auth,
otherInstance = otherInstance,
username = user.name,
username = uiState.value.user.name,
)
if (refreshedUser != null) {
mvi.updateState { it.copy(user = refreshedUser) }
@ -266,7 +270,7 @@ class UserDetailViewModel(
id = userId,
page = currentPage,
sort = currentState.sortType,
username = user.name,
username = uiState.value.user.name,
otherInstance = otherInstance,
)
}.await()
@ -279,7 +283,7 @@ class UserDetailViewModel(
id = userId,
page = currentPage,
sort = currentState.sortType,
username = user.name,
username = uiState.value.user.name,
otherInstance = otherInstance,
).orEmpty()
} else {
@ -502,7 +506,6 @@ class UserDetailViewModel(
mvi.updateState { it.copy(asyncInProgress = true) }
mvi.scope?.launch(Dispatchers.IO) {
try {
val userId = user.id
val auth = identityRepository.authToken.value
userRepository.block(userId, true, auth).getOrThrow()
mvi.emitEffect(UserDetailMviModel.Effect.BlockSuccess)
@ -518,6 +521,7 @@ class UserDetailViewModel(
mvi.updateState { it.copy(asyncInProgress = true) }
mvi.scope?.launch(Dispatchers.IO) {
try {
val user = uiState.value.user
val instanceId = user.instanceId
val auth = identityRepository.authToken.value
siteRepository.block(instanceId, true, auth).getOrThrow()

View File

@ -9,7 +9,7 @@ val userDetailModule = module {
factory<UserDetailMviModel> { params ->
UserDetailViewModel(
mvi = DefaultMviModel(UserDetailMviModel.UiState()),
user = params[0],
userId = params[0],
otherInstance = params[1],
identityRepository = get(),
apiConfigurationRepository = get(),
@ -24,6 +24,7 @@ val userDetailModule = module {
notificationCenter = get(),
imagePreloadManager = get(),
getSortTypesUseCase = get(),
itemCache = get(),
)
}
}

View File

@ -46,7 +46,6 @@ import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigation
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.web.WebViewScreen
@ -57,12 +56,12 @@ import kotlinx.coroutines.launch
import org.koin.core.parameter.parametersOf
class UserInfoScreen(
private val user: UserModel,
private val userId: Int,
) : Screen {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val model = getScreenModel<UserInfoMviModel> { parametersOf(user) }
val model = getScreenModel<UserInfoMviModel> { parametersOf(userId) }
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState()
val navigationCoordinator = remember { getNavigationCoordinator() }

View File

@ -4,27 +4,29 @@ import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviMode
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.LemmyItemCache
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 userId: Int,
private val userRepository: UserRepository,
private val settingsRepository: SettingsRepository,
private val itemCache: LemmyItemCache,
) : 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) {
mvi.scope?.launch {
val user = itemCache.getUser(userId) ?: UserModel()
mvi.updateState {
it.copy(user = user)
}
settingsRepository.currentSettings.onEach {
mvi.updateState { it.copy(autoLoadImages = it.autoLoadImages) }
}.launchIn(this)

View File

@ -9,9 +9,10 @@ val userInfoModule = module {
factory<UserInfoMviModel> { params ->
UserInfoViewModel(
mvi = DefaultMviModel(UserInfoMviModel.UiState()),
user = params[0],
userId = params[0],
userRepository = get(),
settingsRepository = get(),
itemCache = get(),
)
}
}