feat(inbox): private messages (#18)
This commit is contained in:
parent
98da3be105
commit
19ecdf92fc
@ -12,3 +12,4 @@ typealias LocalUserId = Int
|
|||||||
typealias CustomEmojiId = Int
|
typealias CustomEmojiId = Int
|
||||||
typealias PersonMentionId = Int
|
typealias PersonMentionId = Int
|
||||||
typealias CommentReplyId = Int
|
typealias CommentReplyId = Int
|
||||||
|
typealias PrivateMessageId = Int
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CreatePrivateMessageForm(
|
||||||
|
@SerialName("content")
|
||||||
|
val content: String,
|
||||||
|
@SerialName("recipient_id")
|
||||||
|
val recipientId: PersonId,
|
||||||
|
@SerialName("auth")
|
||||||
|
val auth: String,
|
||||||
|
)
|
@ -0,0 +1,14 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class MarkPrivateMessageAsReadForm(
|
||||||
|
@SerialName("private_message_id")
|
||||||
|
val privateMessageId: PrivateMessageId,
|
||||||
|
@SerialName("read")
|
||||||
|
val read: Boolean,
|
||||||
|
@SerialName("auth")
|
||||||
|
val auth: String,
|
||||||
|
)
|
@ -0,0 +1,28 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PrivateMessage(
|
||||||
|
@SerialName("id")
|
||||||
|
val id: PrivateMessageId,
|
||||||
|
@SerialName("creator_id")
|
||||||
|
val creatorId: PersonId,
|
||||||
|
@SerialName("recipient_id")
|
||||||
|
val recipientId: PersonId,
|
||||||
|
@SerialName("content")
|
||||||
|
val content: String,
|
||||||
|
@SerialName("deleted")
|
||||||
|
val deleted: Boolean,
|
||||||
|
@SerialName("read")
|
||||||
|
val read: Boolean,
|
||||||
|
@SerialName("published")
|
||||||
|
val published: String,
|
||||||
|
@SerialName("? ")
|
||||||
|
val updated: String? = null,
|
||||||
|
@SerialName("ap_id")
|
||||||
|
val apId: String,
|
||||||
|
@SerialName("local")
|
||||||
|
val local: Boolean,
|
||||||
|
)
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PrivateMessageResponse(
|
||||||
|
@SerialName("private_message_view")
|
||||||
|
val privateMessageView: PrivateMessageView,
|
||||||
|
)
|
@ -0,0 +1,14 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PrivateMessageView(
|
||||||
|
@SerialName("private_message")
|
||||||
|
val privateMessage: PrivateMessage,
|
||||||
|
@SerialName("creator")
|
||||||
|
val creator: Person,
|
||||||
|
@SerialName("recipient")
|
||||||
|
val recipient: Person,
|
||||||
|
)
|
@ -0,0 +1,10 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.core.api.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PrivateMessagesResponse(
|
||||||
|
@SerialName("private_messages")
|
||||||
|
val privateMessages: List<PrivateMessageView>,
|
||||||
|
)
|
@ -4,6 +4,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.service.AuthService
|
|||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.CommentService
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.CommentService
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.CommunityService
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.CommunityService
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.PostService
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.PostService
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.PrivateMessageService
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.SearchService
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.SearchService
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.SiteService
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.SiteService
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.UserService
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.UserService
|
||||||
@ -48,6 +49,9 @@ internal class DefaultServiceProvider : ServiceProvider {
|
|||||||
override lateinit var search: SearchService
|
override lateinit var search: SearchService
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
override lateinit var privateMessages: PrivateMessageService
|
||||||
|
private set
|
||||||
|
|
||||||
private val baseUrl: String get() = "https://$currentInstance/api/$VERSION/"
|
private val baseUrl: String get() = "https://$currentInstance/api/$VERSION/"
|
||||||
private val client = HttpClient {
|
private val client = HttpClient {
|
||||||
defaultRequest {
|
defaultRequest {
|
||||||
@ -88,5 +92,6 @@ internal class DefaultServiceProvider : ServiceProvider {
|
|||||||
site = ktorfit.create()
|
site = ktorfit.create()
|
||||||
comment = ktorfit.create()
|
comment = ktorfit.create()
|
||||||
search = ktorfit.create()
|
search = ktorfit.create()
|
||||||
|
privateMessages = ktorfit.create()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.service.AuthService
|
|||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.CommentService
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.CommentService
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.CommunityService
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.CommunityService
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.PostService
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.PostService
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.PrivateMessageService
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.SearchService
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.SearchService
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.SiteService
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.SiteService
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.UserService
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.UserService
|
||||||
@ -18,6 +19,7 @@ interface ServiceProvider {
|
|||||||
val site: SiteService
|
val site: SiteService
|
||||||
val comment: CommentService
|
val comment: CommentService
|
||||||
val search: SearchService
|
val search: SearchService
|
||||||
|
val privateMessages: PrivateMessageService
|
||||||
|
|
||||||
fun changeInstance(value: String)
|
fun changeInstance(value: String)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.core.api.service
|
||||||
|
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CreatePrivateMessageForm
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.MarkPrivateMessageAsReadForm
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PrivateMessageResponse
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PrivateMessagesResponse
|
||||||
|
import de.jensklingenberg.ktorfit.Response
|
||||||
|
import de.jensklingenberg.ktorfit.http.Body
|
||||||
|
import de.jensklingenberg.ktorfit.http.GET
|
||||||
|
import de.jensklingenberg.ktorfit.http.Headers
|
||||||
|
import de.jensklingenberg.ktorfit.http.POST
|
||||||
|
import de.jensklingenberg.ktorfit.http.Query
|
||||||
|
|
||||||
|
interface PrivateMessageService {
|
||||||
|
@GET("private_message/list")
|
||||||
|
suspend fun getPrivateMessages(
|
||||||
|
@Query("auth") auth: String? = null,
|
||||||
|
@Query("page") page: Int? = null,
|
||||||
|
@Query("limit") limit: Int? = null,
|
||||||
|
@Query("unread_only") unreadOnly: Boolean? = null,
|
||||||
|
): Response<PrivateMessagesResponse>
|
||||||
|
|
||||||
|
@POST("private_message")
|
||||||
|
@Headers("Content-Type: application/json")
|
||||||
|
suspend fun createPrivateMessage(@Body form: CreatePrivateMessageForm): Response<PrivateMessageResponse>
|
||||||
|
|
||||||
|
@POST("private_message/mark_as_read")
|
||||||
|
@Headers("Content-Type: application/json")
|
||||||
|
suspend fun markPrivateMessageAsRead(@Body form: MarkPrivateMessageAsReadForm): Response<PrivateMessageResponse>
|
||||||
|
}
|
@ -208,11 +208,13 @@ class PostDetailScreen(
|
|||||||
model.reduce(PostDetailMviModel.Intent.Refresh)
|
model.reduce(PostDetailMviModel.Intent.Refresh)
|
||||||
})
|
})
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
|
modifier = Modifier
|
||||||
.nestedScroll(fabNestedScrollConnection).pullRefresh(pullRefreshState),
|
.padding(padding)
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
|
.nestedScroll(fabNestedScrollConnection)
|
||||||
|
.pullRefresh(pullRefreshState),
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.padding(padding),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
|
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
|
||||||
|
|
||||||
|
data class PrivateMessageModel(
|
||||||
|
val id: Int = 0,
|
||||||
|
val content: String? = null,
|
||||||
|
val creator: UserModel? = null,
|
||||||
|
val recipient: UserModel? = null,
|
||||||
|
val publishDate: String? = null,
|
||||||
|
val updateDate: String? = null,
|
||||||
|
val read: Boolean = false,
|
||||||
|
)
|
@ -0,0 +1,53 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository
|
||||||
|
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.CreatePrivateMessageForm
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.MarkPrivateMessageAsReadForm
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.provider.ServiceProvider
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PrivateMessageModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.utils.toModel
|
||||||
|
|
||||||
|
class PrivateMessageRepository(
|
||||||
|
private val serviceProvider: ServiceProvider,
|
||||||
|
) {
|
||||||
|
suspend fun getAll(
|
||||||
|
auth: String? = null,
|
||||||
|
page: Int,
|
||||||
|
limit: Int = PostsRepository.DEFAULT_PAGE_SIZE,
|
||||||
|
unreadOnly: Boolean = true,
|
||||||
|
): List<PrivateMessageModel> = runCatching {
|
||||||
|
val response = serviceProvider.privateMessages.getPrivateMessages(
|
||||||
|
auth = auth,
|
||||||
|
limit = limit,
|
||||||
|
page = page,
|
||||||
|
unreadOnly = unreadOnly,
|
||||||
|
)
|
||||||
|
val dto = response.body() ?: return@runCatching emptyList()
|
||||||
|
dto.privateMessages.map { it.toModel() }
|
||||||
|
}.getOrElse { emptyList() }
|
||||||
|
|
||||||
|
suspend fun create(
|
||||||
|
message: String,
|
||||||
|
auth: String? = null,
|
||||||
|
recipiendId: Int,
|
||||||
|
) {
|
||||||
|
val data = CreatePrivateMessageForm(
|
||||||
|
content = message,
|
||||||
|
auth = auth.orEmpty(),
|
||||||
|
recipientId = recipiendId,
|
||||||
|
)
|
||||||
|
serviceProvider.privateMessages.createPrivateMessage(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun markAsRead(
|
||||||
|
messageId: Int,
|
||||||
|
auth: String? = null,
|
||||||
|
read: Boolean = true,
|
||||||
|
) {
|
||||||
|
val data = MarkPrivateMessageAsReadForm(
|
||||||
|
privateMessageId = messageId,
|
||||||
|
auth = auth.orEmpty(),
|
||||||
|
read = read,
|
||||||
|
)
|
||||||
|
serviceProvider.privateMessages.markPrivateMessageAsRead(data)
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.di
|
|||||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommentRepository
|
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.CommunityRepository
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostsRepository
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostsRepository
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PrivateMessageRepository
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepository
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepository
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository
|
||||||
import org.koin.core.module.dsl.singleOf
|
import org.koin.core.module.dsl.singleOf
|
||||||
@ -25,4 +26,5 @@ val repositoryModule = module {
|
|||||||
singleOf(::UserRepository)
|
singleOf(::UserRepository)
|
||||||
singleOf(::SiteRepository)
|
singleOf(::SiteRepository)
|
||||||
singleOf(::CommentRepository)
|
singleOf(::CommentRepository)
|
||||||
|
singleOf(::PrivateMessageRepository)
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.Person
|
|||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PersonAggregates
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PersonAggregates
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PersonMentionView
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PersonMentionView
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PostView
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PostView
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.PrivateMessageView
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SortType.Active
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SortType.Active
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SortType.Hot
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SortType.Hot
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SortType.MostComments
|
import com.github.diegoberaldin.raccoonforlemmy.core.api.dto.SortType.MostComments
|
||||||
@ -30,6 +31,7 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
|
|||||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.ListingType
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.ListingType
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PersonMentionModel
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PersonMentionModel
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PrivateMessageModel
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserScoreModel
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserScoreModel
|
||||||
@ -184,6 +186,16 @@ internal fun CommentReplyView.toModel() = PersonMentionModel(
|
|||||||
publishDate = commentReply.published,
|
publishDate = commentReply.published,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal fun PrivateMessageView.toModel() = PrivateMessageModel(
|
||||||
|
id = privateMessage.id,
|
||||||
|
content = privateMessage.content,
|
||||||
|
creator = creator.toModel(),
|
||||||
|
recipient = recipient.toModel(),
|
||||||
|
publishDate = privateMessage.published,
|
||||||
|
updateDate = privateMessage.updated,
|
||||||
|
read = privateMessage.read,
|
||||||
|
)
|
||||||
|
|
||||||
internal fun String.toHost(): String = this.replace("https://", "").let {
|
internal fun String.toHost(): String = this.replace("https://", "").let {
|
||||||
val index = it.indexOf("/")
|
val index = it.indexOf("/")
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
|
@ -29,6 +29,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@ -38,11 +39,14 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
import cafe.adriel.voyager.navigator.bottomSheet.LocalBottomSheetNavigator
|
import cafe.adriel.voyager.navigator.bottomSheet.LocalBottomSheetNavigator
|
||||||
import com.github.diegoberaldin.racconforlemmy.core.utils.onClick
|
import com.github.diegoberaldin.racconforlemmy.core.utils.onClick
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.di.getThemeRepository
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
|
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
|
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreen
|
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreen
|
||||||
@ -144,125 +148,134 @@ class PostListScreen : Screen {
|
|||||||
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
|
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
|
||||||
) {
|
) {
|
||||||
itemsIndexed(uiState.posts) { idx, post ->
|
itemsIndexed(uiState.posts) { idx, post ->
|
||||||
SwipeableCard(
|
val themeRepository = remember { getThemeRepository() }
|
||||||
modifier = Modifier.fillMaxWidth(),
|
val fontScale by themeRepository.contentFontScale.collectAsState()
|
||||||
onGestureBegin = {
|
CompositionLocalProvider(
|
||||||
model.reduce(PostListMviModel.Intent.HapticIndication)
|
LocalDensity provides Density(
|
||||||
},
|
density = LocalDensity.current.density,
|
||||||
onDismissToStart = {
|
fontScale = fontScale,
|
||||||
model.reduce(PostListMviModel.Intent.UpVotePost(idx))
|
),
|
||||||
},
|
) {
|
||||||
onDismissToEnd = {
|
SwipeableCard(
|
||||||
model.reduce(PostListMviModel.Intent.DownVotePost(idx))
|
modifier = Modifier.fillMaxWidth(),
|
||||||
},
|
onGestureBegin = {
|
||||||
backgroundColor = {
|
model.reduce(PostListMviModel.Intent.HapticIndication)
|
||||||
when (it) {
|
},
|
||||||
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.secondary
|
onDismissToStart = {
|
||||||
DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.tertiary
|
model.reduce(PostListMviModel.Intent.UpVotePost(idx))
|
||||||
else -> Color.Transparent
|
},
|
||||||
}
|
onDismissToEnd = {
|
||||||
},
|
model.reduce(PostListMviModel.Intent.DownVotePost(idx))
|
||||||
swipeContent = { direction ->
|
},
|
||||||
val icon = when (direction) {
|
backgroundColor = {
|
||||||
DismissDirection.StartToEnd -> Icons.Default.ArrowCircleDown
|
when (it) {
|
||||||
DismissDirection.EndToStart -> Icons.Default.ArrowCircleUp
|
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.secondary
|
||||||
}
|
DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.tertiary
|
||||||
val (iconModifier, iconTint) = when {
|
else -> Color.Transparent
|
||||||
direction == DismissDirection.StartToEnd && post.myVote < 0 -> {
|
|
||||||
Modifier.background(
|
|
||||||
color = Color.Transparent,
|
|
||||||
shape = CircleShape,
|
|
||||||
) to MaterialTheme.colorScheme.onTertiary
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
swipeContent = { direction ->
|
||||||
|
val icon = when (direction) {
|
||||||
|
DismissDirection.StartToEnd -> Icons.Default.ArrowCircleDown
|
||||||
|
DismissDirection.EndToStart -> Icons.Default.ArrowCircleUp
|
||||||
|
}
|
||||||
|
val (iconModifier, iconTint) = when {
|
||||||
|
direction == DismissDirection.StartToEnd && post.myVote < 0 -> {
|
||||||
|
Modifier.background(
|
||||||
|
color = Color.Transparent,
|
||||||
|
shape = CircleShape,
|
||||||
|
) to MaterialTheme.colorScheme.onTertiary
|
||||||
|
}
|
||||||
|
|
||||||
direction == DismissDirection.StartToEnd -> {
|
direction == DismissDirection.StartToEnd -> {
|
||||||
Modifier.background(
|
Modifier.background(
|
||||||
color = MaterialTheme.colorScheme.onTertiary,
|
color = MaterialTheme.colorScheme.onTertiary,
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
) to MaterialTheme.colorScheme.tertiary
|
) to MaterialTheme.colorScheme.tertiary
|
||||||
}
|
}
|
||||||
|
|
||||||
direction == DismissDirection.EndToStart && post.myVote > 0 -> {
|
direction == DismissDirection.EndToStart && post.myVote > 0 -> {
|
||||||
Modifier.background(
|
Modifier.background(
|
||||||
color = Color.Transparent,
|
color = Color.Transparent,
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
) to MaterialTheme.colorScheme.onSecondary
|
) to MaterialTheme.colorScheme.onSecondary
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Modifier.background(
|
Modifier.background(
|
||||||
color = MaterialTheme.colorScheme.onSecondary,
|
color = MaterialTheme.colorScheme.onSecondary,
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
) to MaterialTheme.colorScheme.secondary
|
) to MaterialTheme.colorScheme.secondary
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
Icon(
|
||||||
Icon(
|
modifier = iconModifier,
|
||||||
modifier = iconModifier,
|
imageVector = icon,
|
||||||
imageVector = icon,
|
contentDescription = null,
|
||||||
contentDescription = null,
|
tint = iconTint,
|
||||||
tint = iconTint,
|
)
|
||||||
)
|
},
|
||||||
},
|
content = {
|
||||||
content = {
|
PostCard(
|
||||||
PostCard(
|
modifier = Modifier.onClick {
|
||||||
modifier = Modifier.onClick {
|
navigator?.push(
|
||||||
navigator?.push(
|
PostDetailScreen(post),
|
||||||
PostDetailScreen(post),
|
)
|
||||||
)
|
},
|
||||||
},
|
post = post,
|
||||||
post = post,
|
blurNsfw = uiState.blurNsfw,
|
||||||
blurNsfw = uiState.blurNsfw,
|
onOpenCommunity = { community ->
|
||||||
onOpenCommunity = { community ->
|
navigator?.push(
|
||||||
navigator?.push(
|
CommunityDetailScreen(community),
|
||||||
CommunityDetailScreen(community),
|
)
|
||||||
)
|
},
|
||||||
},
|
onOpenCreator = { user ->
|
||||||
onOpenCreator = { user ->
|
navigator?.push(
|
||||||
navigator?.push(
|
UserDetailScreen(user),
|
||||||
UserDetailScreen(user),
|
)
|
||||||
)
|
},
|
||||||
},
|
onUpVote = {
|
||||||
onUpVote = {
|
model.reduce(
|
||||||
model.reduce(
|
PostListMviModel.Intent.UpVotePost(
|
||||||
PostListMviModel.Intent.UpVotePost(
|
index = idx,
|
||||||
index = idx,
|
feedback = true,
|
||||||
feedback = true,
|
),
|
||||||
),
|
)
|
||||||
)
|
},
|
||||||
},
|
onDownVote = {
|
||||||
onDownVote = {
|
model.reduce(
|
||||||
model.reduce(
|
PostListMviModel.Intent.DownVotePost(
|
||||||
PostListMviModel.Intent.DownVotePost(
|
index = idx,
|
||||||
index = idx,
|
feedback = true,
|
||||||
feedback = true,
|
),
|
||||||
),
|
)
|
||||||
)
|
},
|
||||||
},
|
onSave = {
|
||||||
onSave = {
|
model.reduce(
|
||||||
model.reduce(
|
PostListMviModel.Intent.SavePost(
|
||||||
PostListMviModel.Intent.SavePost(
|
index = idx,
|
||||||
index = idx,
|
feedback = true,
|
||||||
feedback = true,
|
),
|
||||||
),
|
)
|
||||||
)
|
},
|
||||||
},
|
onReply = {
|
||||||
onReply = {
|
val screen = CreateCommentScreen(
|
||||||
val screen = CreateCommentScreen(
|
originalPost = post,
|
||||||
originalPost = post,
|
)
|
||||||
)
|
notificationCenter.addObserver({
|
||||||
notificationCenter.addObserver({
|
model.reduce(PostListMviModel.Intent.Refresh)
|
||||||
model.reduce(PostListMviModel.Intent.Refresh)
|
}, key, screen.key)
|
||||||
}, key, screen.key)
|
bottomSheetNavigator.show(screen)
|
||||||
bottomSheetNavigator.show(screen)
|
},
|
||||||
},
|
onImageClick = { url ->
|
||||||
onImageClick = { url ->
|
navigator?.push(
|
||||||
navigator?.push(
|
ZoomableImageScreen(url),
|
||||||
ZoomableImageScreen(url),
|
)
|
||||||
)
|
},
|
||||||
},
|
)
|
||||||
)
|
},
|
||||||
},
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
|
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
|
||||||
|
@ -2,20 +2,38 @@ package com.github.diegoberaldin.raccoonforlemmy.feature.inbox.di
|
|||||||
|
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxViewModel
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxViewModel
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.mentions.InboxMentionsViewModel
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.mentions.InboxMentionsViewModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.detail.InboxChatViewModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.list.InboxMessagesViewModel
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.replies.InboxRepliesViewModel
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.replies.InboxRepliesViewModel
|
||||||
import org.koin.java.KoinJavaComponent
|
import org.koin.core.parameter.parametersOf
|
||||||
|
import org.koin.java.KoinJavaComponent.inject
|
||||||
|
|
||||||
actual fun getInboxViewModel(): InboxViewModel {
|
actual fun getInboxViewModel(): InboxViewModel {
|
||||||
val res: InboxViewModel by KoinJavaComponent.inject(InboxViewModel::class.java)
|
val res: InboxViewModel by inject(InboxViewModel::class.java)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun getInboxRepliesViewModel(): InboxRepliesViewModel {
|
actual fun getInboxRepliesViewModel(): InboxRepliesViewModel {
|
||||||
val res: InboxRepliesViewModel by KoinJavaComponent.inject(InboxRepliesViewModel::class.java)
|
val res: InboxRepliesViewModel by inject(InboxRepliesViewModel::class.java)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun getInboxMentionsViewModel(): InboxMentionsViewModel {
|
actual fun getInboxMentionsViewModel(): InboxMentionsViewModel {
|
||||||
val res: InboxMentionsViewModel by KoinJavaComponent.inject(InboxMentionsViewModel::class.java)
|
val res: InboxMentionsViewModel by inject(InboxMentionsViewModel::class.java)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun getInboxMessagesViewModel(): InboxMessagesViewModel {
|
||||||
|
val res: InboxMessagesViewModel by inject(InboxMessagesViewModel::class.java)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun getInboxChatViewModel(otherUserId: Int): InboxChatViewModel {
|
||||||
|
val res: InboxChatViewModel by inject(
|
||||||
|
InboxChatViewModel::class.java,
|
||||||
|
parameters = {
|
||||||
|
parametersOf(otherUserId)
|
||||||
|
},
|
||||||
|
)
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,10 @@ import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxMviModel
|
|||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxViewModel
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxViewModel
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.mentions.InboxMentionsMviModel
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.mentions.InboxMentionsMviModel
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.mentions.InboxMentionsViewModel
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.mentions.InboxMentionsViewModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.detail.InboxChatMviModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.detail.InboxChatViewModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.list.InboxMessagesMviModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.list.InboxMessagesViewModel
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.replies.InboxRepliesMviModel
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.replies.InboxRepliesMviModel
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.replies.InboxRepliesViewModel
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.replies.InboxRepliesViewModel
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
@ -44,4 +48,26 @@ val inboxTabModule = module {
|
|||||||
notificationCenter = get(),
|
notificationCenter = get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
factory {
|
||||||
|
InboxMessagesViewModel(
|
||||||
|
mvi = DefaultMviModel(InboxMessagesMviModel.UiState()),
|
||||||
|
identityRepository = get(),
|
||||||
|
siteRepository = get(),
|
||||||
|
messageRepository = get(),
|
||||||
|
coordinator = get(),
|
||||||
|
notificationCenter = get(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
factory {
|
||||||
|
InboxChatViewModel(
|
||||||
|
otherUserId = it[0],
|
||||||
|
mvi = DefaultMviModel(InboxChatMviModel.UiState()),
|
||||||
|
identityRepository = get(),
|
||||||
|
siteRepository = get(),
|
||||||
|
userRepository = get(),
|
||||||
|
messageRepository = get(),
|
||||||
|
coordinator = get(),
|
||||||
|
notificationCenter = get(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ package com.github.diegoberaldin.raccoonforlemmy.feature.inbox.di
|
|||||||
|
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxViewModel
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxViewModel
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.mentions.InboxMentionsViewModel
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.mentions.InboxMentionsViewModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.detail.InboxChatViewModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.list.InboxMessagesViewModel
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.replies.InboxRepliesViewModel
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.replies.InboxRepliesViewModel
|
||||||
|
|
||||||
expect fun getInboxViewModel(): InboxViewModel
|
expect fun getInboxViewModel(): InboxViewModel
|
||||||
@ -9,3 +11,7 @@ expect fun getInboxViewModel(): InboxViewModel
|
|||||||
expect fun getInboxRepliesViewModel(): InboxRepliesViewModel
|
expect fun getInboxRepliesViewModel(): InboxRepliesViewModel
|
||||||
|
|
||||||
expect fun getInboxMentionsViewModel(): InboxMentionsViewModel
|
expect fun getInboxMentionsViewModel(): InboxMentionsViewModel
|
||||||
|
|
||||||
|
expect fun getInboxMessagesViewModel(): InboxMessagesViewModel
|
||||||
|
|
||||||
|
expect fun getInboxChatViewModel(otherUserId: Int): InboxChatViewModel
|
||||||
|
@ -37,7 +37,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.InboxTypeSh
|
|||||||
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
|
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.di.getInboxViewModel
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.di.getInboxViewModel
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.mentions.InboxMentionsScreen
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.mentions.InboxMentionsScreen
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.InboxMessagesScreen
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.list.InboxMessagesScreen
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.replies.InboxRepliesScreen
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.replies.InboxRepliesScreen
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
|
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.resources.di.getLanguageRepository
|
import com.github.diegoberaldin.raccoonforlemmy.resources.di.getLanguageRepository
|
||||||
@ -69,7 +69,6 @@ object InboxScreen : Tab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.padding(Spacing.xxs),
|
modifier = Modifier.padding(Spacing.xxs),
|
||||||
topBar = {
|
topBar = {
|
||||||
|
@ -9,8 +9,7 @@ interface InboxMentionsMviModel :
|
|||||||
sealed interface Intent {
|
sealed interface Intent {
|
||||||
object Refresh : Intent
|
object Refresh : Intent
|
||||||
object LoadNextPage : Intent
|
object LoadNextPage : Intent
|
||||||
data class ChangeUnreadOnly(val unread: Boolean) : Intent
|
data class MarkAsRead(val read: Boolean, val mentionId: Int) : Intent
|
||||||
data class MarkMentionAsRead(val read: Boolean, val mentionId: Int) : Intent
|
|
||||||
object HapticIndication : Intent
|
object HapticIndication : Intent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,7 +25,6 @@ import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
|||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -45,16 +44,9 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCo
|
|||||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailScreen
|
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailScreen
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailScreen
|
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailScreen
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.di.getInboxMentionsViewModel
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.di.getInboxMentionsViewModel
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxMviModel
|
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxViewModel
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
|
|
||||||
class InboxMentionsScreen : Tab {
|
class InboxMentionsScreen : Tab {
|
||||||
|
|
||||||
var parentModel: InboxViewModel? = null
|
|
||||||
|
|
||||||
override val options: TabOptions
|
override val options: TabOptions
|
||||||
@Composable get() {
|
@Composable get() {
|
||||||
return TabOptions(1u, "")
|
return TabOptions(1u, "")
|
||||||
@ -66,28 +58,8 @@ class InboxMentionsScreen : Tab {
|
|||||||
val model = rememberScreenModel { getInboxMentionsViewModel() }
|
val model = rememberScreenModel { getInboxMentionsViewModel() }
|
||||||
model.bindToLifecycle(key)
|
model.bindToLifecycle(key)
|
||||||
val uiState by model.uiState.collectAsState()
|
val uiState by model.uiState.collectAsState()
|
||||||
val parentUiState by (parentModel?.uiState
|
|
||||||
?: MutableStateFlow(InboxMviModel.UiState())).collectAsState()
|
|
||||||
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
|
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
|
||||||
|
|
||||||
LaunchedEffect(parentModel) {
|
|
||||||
parentModel?.also { parentModel ->
|
|
||||||
parentModel.uiState.onEach {
|
|
||||||
if (it.unreadOnly != model.uiState.value.unreadOnly) {
|
|
||||||
model.reduce(InboxMentionsMviModel.Intent.ChangeUnreadOnly(unread = it.unreadOnly))
|
|
||||||
}
|
|
||||||
}.launchIn(this)
|
|
||||||
|
|
||||||
parentModel.effects.onEach {
|
|
||||||
when (it) {
|
|
||||||
InboxMviModel.Effect.Refresh -> {
|
|
||||||
model.reduce(InboxMentionsMviModel.Intent.Refresh)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.launchIn(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
|
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
|
||||||
model.reduce(InboxMentionsMviModel.Intent.Refresh)
|
model.reduce(InboxMentionsMviModel.Intent.Refresh)
|
||||||
})
|
})
|
||||||
@ -113,7 +85,7 @@ class InboxMentionsScreen : Tab {
|
|||||||
},
|
},
|
||||||
onDismissToStart = {
|
onDismissToStart = {
|
||||||
model.reduce(
|
model.reduce(
|
||||||
InboxMentionsMviModel.Intent.MarkMentionAsRead(
|
InboxMentionsMviModel.Intent.MarkAsRead(
|
||||||
read = true,
|
read = true,
|
||||||
mentionId = mention.id,
|
mentionId = mention.id,
|
||||||
),
|
),
|
||||||
@ -121,7 +93,7 @@ class InboxMentionsScreen : Tab {
|
|||||||
},
|
},
|
||||||
onDismissToEnd = {
|
onDismissToEnd = {
|
||||||
model.reduce(
|
model.reduce(
|
||||||
InboxMentionsMviModel.Intent.MarkMentionAsRead(
|
InboxMentionsMviModel.Intent.MarkAsRead(
|
||||||
read = false,
|
read = false,
|
||||||
mentionId = mention.id,
|
mentionId = mention.id,
|
||||||
),
|
),
|
||||||
|
@ -59,8 +59,7 @@ class InboxMentionsViewModel(
|
|||||||
when (intent) {
|
when (intent) {
|
||||||
InboxMentionsMviModel.Intent.LoadNextPage -> loadNextPage()
|
InboxMentionsMviModel.Intent.LoadNextPage -> loadNextPage()
|
||||||
InboxMentionsMviModel.Intent.Refresh -> refresh()
|
InboxMentionsMviModel.Intent.Refresh -> refresh()
|
||||||
is InboxMentionsMviModel.Intent.ChangeUnreadOnly -> changeUnreadOnly(intent.unread)
|
is InboxMentionsMviModel.Intent.MarkAsRead -> {
|
||||||
is InboxMentionsMviModel.Intent.MarkMentionAsRead -> {
|
|
||||||
markAsRead(read = intent.read, mentionId = intent.mentionId)
|
markAsRead(read = intent.read, mentionId = intent.mentionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
package com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import cafe.adriel.voyager.navigator.tab.Tab
|
|
||||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
|
||||||
|
|
||||||
class InboxMessagesScreen : Tab {
|
|
||||||
|
|
||||||
override val options: TabOptions
|
|
||||||
@Composable get() {
|
|
||||||
return TabOptions(2u, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun Content() {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "\uD83D\uDEA7 Work in progress! \uD83D\uDEA7",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.detail
|
||||||
|
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PrivateMessageModel
|
||||||
|
|
||||||
|
interface InboxChatMviModel :
|
||||||
|
MviModel<InboxChatMviModel.Intent, InboxChatMviModel.UiState, InboxChatMviModel.SideEffect> {
|
||||||
|
sealed interface Intent {
|
||||||
|
object LoadNextPage : Intent
|
||||||
|
data class SetNewMessageContent(val value: String) : Intent
|
||||||
|
object SubmitNewMessage : Intent
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UiState(
|
||||||
|
val refreshing: Boolean = false,
|
||||||
|
val loading: Boolean = false,
|
||||||
|
val canFetchMore: Boolean = true,
|
||||||
|
val currentUserId: Int = 0,
|
||||||
|
val otherUserName: String = "",
|
||||||
|
val otherUserAvatar: String? = null,
|
||||||
|
val messages: List<PrivateMessageModel> = emptyList(),
|
||||||
|
val newMessageContent: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface SideEffect
|
||||||
|
}
|
@ -0,0 +1,226 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.detail
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Send
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import com.github.diegoberaldin.racconforlemmy.core.utils.onClick
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.di.getThemeRepository
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.CornerSize
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.di.getInboxChatViewModel
|
||||||
|
import io.kamel.image.KamelImage
|
||||||
|
import io.kamel.image.asyncPainterResource
|
||||||
|
|
||||||
|
class InboxChatScreen(
|
||||||
|
private val otherUserId: Int,
|
||||||
|
) : Screen {
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val model = rememberScreenModel { getInboxChatViewModel(otherUserId) }
|
||||||
|
model.bindToLifecycle(key)
|
||||||
|
val uiState by model.uiState.collectAsState()
|
||||||
|
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.background(MaterialTheme.colorScheme.surface).padding(Spacing.xs),
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Spacing.xxs),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
val iconSize = 23.dp
|
||||||
|
val avatar = uiState.otherUserAvatar.orEmpty()
|
||||||
|
if (avatar.isNotEmpty()) {
|
||||||
|
val painterResource = asyncPainterResource(data = avatar)
|
||||||
|
KamelImage(
|
||||||
|
modifier = Modifier.padding(Spacing.xxxs).size(iconSize)
|
||||||
|
.clip(RoundedCornerShape(iconSize / 2)),
|
||||||
|
resource = painterResource,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillBounds,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(horizontal = Spacing.s),
|
||||||
|
text = uiState.otherUserName,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.onClick {
|
||||||
|
navigator?.pop()
|
||||||
|
},
|
||||||
|
imageVector = Icons.Default.ArrowBack,
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
if (uiState.currentUserId != 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(padding)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.weight(1f).fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
|
||||||
|
reverseLayout = true,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(Spacing.s))
|
||||||
|
}
|
||||||
|
items(uiState.messages) { message ->
|
||||||
|
val themeRepository = remember { getThemeRepository() }
|
||||||
|
val fontScale by themeRepository.contentFontScale.collectAsState()
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalDensity provides Density(
|
||||||
|
density = LocalDensity.current.density,
|
||||||
|
fontScale = fontScale,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
val isMyMessage = message.creator?.id == uiState.currentUserId
|
||||||
|
val content = message.content.orEmpty()
|
||||||
|
val date = message.publishDate.orEmpty()
|
||||||
|
MessageCard(
|
||||||
|
isMyMessage = isMyMessage,
|
||||||
|
content = content,
|
||||||
|
date = date,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
|
||||||
|
model.reduce(InboxChatMviModel.Intent.LoadNextPage)
|
||||||
|
}
|
||||||
|
if (uiState.loading && !uiState.refreshing) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(Spacing.xs),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(25.dp),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(Spacing.xxxl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.weight(1f).background(
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f),
|
||||||
|
shape = RoundedCornerShape(CornerSize.l)
|
||||||
|
).padding(Spacing.s)
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 20.dp, max = 200.dp),
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface),
|
||||||
|
value = uiState.newMessageContent,
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Ascii,
|
||||||
|
autoCorrect = false,
|
||||||
|
imeAction = ImeAction.Send,
|
||||||
|
),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onSend = {
|
||||||
|
model.reduce(InboxChatMviModel.Intent.SubmitNewMessage)
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
onValueChange = { value ->
|
||||||
|
model.reduce(
|
||||||
|
InboxChatMviModel.Intent.SetNewMessageContent(
|
||||||
|
value
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
model.reduce(InboxChatMviModel.Intent.SubmitNewMessage)
|
||||||
|
focusManager.clearFocus()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Send,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,163 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.detail
|
||||||
|
|
||||||
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
|
||||||
|
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.PrivateMessageRepository
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepository
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.InboxCoordinator
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxMviModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.IO
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class InboxChatViewModel(
|
||||||
|
private val otherUserId: Int,
|
||||||
|
private val mvi: DefaultMviModel<InboxChatMviModel.Intent, InboxChatMviModel.UiState, InboxChatMviModel.SideEffect>,
|
||||||
|
private val identityRepository: IdentityRepository,
|
||||||
|
private val siteRepository: SiteRepository,
|
||||||
|
private val messageRepository: PrivateMessageRepository,
|
||||||
|
private val userRepository: UserRepository,
|
||||||
|
private val coordinator: InboxCoordinator,
|
||||||
|
private val notificationCenter: NotificationCenter,
|
||||||
|
) : ScreenModel,
|
||||||
|
MviModel<InboxChatMviModel.Intent, InboxChatMviModel.UiState, InboxChatMviModel.SideEffect> by mvi {
|
||||||
|
|
||||||
|
|
||||||
|
private var currentPage: Int = 1
|
||||||
|
|
||||||
|
override fun onStarted() {
|
||||||
|
mvi.onStarted()
|
||||||
|
mvi.scope.launch {
|
||||||
|
coordinator.effects.onEach {
|
||||||
|
when (it) {
|
||||||
|
InboxMviModel.Effect.Refresh -> refresh()
|
||||||
|
}
|
||||||
|
}.launchIn(this)
|
||||||
|
notificationCenter.events.onEach { evt ->
|
||||||
|
when (evt) {
|
||||||
|
NotificationCenter.Event.Logout -> {
|
||||||
|
mvi.updateState { it.copy(messages = emptyList()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}.launchIn(this)
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
val auth = identityRepository.authToken.value.orEmpty()
|
||||||
|
|
||||||
|
launch {
|
||||||
|
val currentUserId = siteRepository.getCurrentUser(auth)?.id ?: 0
|
||||||
|
mvi.updateState { it.copy(currentUserId = currentUserId) }
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
val user = userRepository.get(
|
||||||
|
id = otherUserId,
|
||||||
|
auth = auth,
|
||||||
|
)
|
||||||
|
mvi.updateState {
|
||||||
|
it.copy(
|
||||||
|
otherUserName = user?.name.orEmpty(),
|
||||||
|
otherUserAvatar = user?.avatar,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reduce(intent: InboxChatMviModel.Intent) {
|
||||||
|
when (intent) {
|
||||||
|
InboxChatMviModel.Intent.LoadNextPage -> loadNextPage()
|
||||||
|
is InboxChatMviModel.Intent.SetNewMessageContent -> setNewMessageContent(intent.value)
|
||||||
|
InboxChatMviModel.Intent.SubmitNewMessage -> submitNewMessage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refresh() {
|
||||||
|
currentPage = 1
|
||||||
|
mvi.updateState { it.copy(canFetchMore = true, refreshing = true) }
|
||||||
|
loadNextPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadNextPage() {
|
||||||
|
val currentState = mvi.uiState.value
|
||||||
|
if (!currentState.canFetchMore || currentState.loading) {
|
||||||
|
mvi.updateState { it.copy(refreshing = false) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mvi.scope.launch(Dispatchers.IO) {
|
||||||
|
mvi.updateState { it.copy(loading = true) }
|
||||||
|
val auth = identityRepository.authToken.value
|
||||||
|
val refreshing = currentState.refreshing
|
||||||
|
val itemList = messageRepository.getAll(
|
||||||
|
auth = auth,
|
||||||
|
page = currentPage,
|
||||||
|
unreadOnly = false,
|
||||||
|
).filter {
|
||||||
|
it.creator?.id == otherUserId || it.recipient?.id == otherUserId
|
||||||
|
}.onEach {
|
||||||
|
if (!it.read) {
|
||||||
|
launch {
|
||||||
|
markAsRead(true, it.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentPage++
|
||||||
|
val canFetchMore = itemList.size >= CommentRepository.DEFAULT_PAGE_SIZE
|
||||||
|
|
||||||
|
mvi.updateState {
|
||||||
|
val newItems = if (refreshing) {
|
||||||
|
itemList
|
||||||
|
} else {
|
||||||
|
it.messages + itemList
|
||||||
|
}
|
||||||
|
it.copy(
|
||||||
|
messages = newItems,
|
||||||
|
loading = false,
|
||||||
|
canFetchMore = canFetchMore,
|
||||||
|
refreshing = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun markAsRead(read: Boolean, messageId: Int) {
|
||||||
|
val auth = identityRepository.authToken.value
|
||||||
|
mvi.scope.launch(Dispatchers.IO) {
|
||||||
|
messageRepository.markAsRead(
|
||||||
|
read = read,
|
||||||
|
messageId = messageId,
|
||||||
|
auth = auth,
|
||||||
|
)
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setNewMessageContent(text: String) {
|
||||||
|
mvi.updateState { it.copy(newMessageContent = text) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun submitNewMessage() {
|
||||||
|
val text = uiState.value.newMessageContent
|
||||||
|
if (text.isNotEmpty()) {
|
||||||
|
mvi.scope.launch {
|
||||||
|
val auth = identityRepository.authToken.value
|
||||||
|
messageRepository.create(
|
||||||
|
message = text,
|
||||||
|
auth = auth,
|
||||||
|
recipiendId = otherUserId,
|
||||||
|
)
|
||||||
|
mvi.updateState { it.copy(newMessageContent = "") }
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,173 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.detail
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Schedule
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.github.diegoberaldin.racconforlemmy.core.utils.DateTime
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.CornerSize
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
|
||||||
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun MessageCard(
|
||||||
|
isMyMessage: Boolean = false,
|
||||||
|
content: String = "",
|
||||||
|
date: String = "",
|
||||||
|
) {
|
||||||
|
val color = if (isMyMessage) {
|
||||||
|
MaterialTheme.colorScheme.tertiary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.secondary
|
||||||
|
}
|
||||||
|
val textColor = if (isMyMessage) {
|
||||||
|
MaterialTheme.colorScheme.onTertiary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSecondary
|
||||||
|
}
|
||||||
|
val longDistance = Spacing.l
|
||||||
|
val mediumDistance = Spacing.s
|
||||||
|
Box {
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier.size(mediumDistance).let {
|
||||||
|
if (isMyMessage) {
|
||||||
|
it.align(Alignment.TopEnd)
|
||||||
|
} else {
|
||||||
|
it.align(Alignment.TopStart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (isMyMessage) {
|
||||||
|
val path = Path().apply {
|
||||||
|
moveTo(0f, 0f)
|
||||||
|
lineTo(size.width, 0f)
|
||||||
|
lineTo(0f, size.height)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
drawPath(path = path, color = color)
|
||||||
|
} else {
|
||||||
|
val path = Path().apply {
|
||||||
|
moveTo(size.width, 0f)
|
||||||
|
lineTo(0f, 0f)
|
||||||
|
lineTo(size.width, size.height)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
drawPath(path = path, color = color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.let {
|
||||||
|
if (isMyMessage) {
|
||||||
|
it.padding(start = longDistance, end = mediumDistance)
|
||||||
|
} else {
|
||||||
|
it.padding(end = longDistance, start = mediumDistance)
|
||||||
|
}
|
||||||
|
}.background(
|
||||||
|
color = color, shape = RoundedCornerShape(
|
||||||
|
topStart = if (isMyMessage) CornerSize.m else 0.dp,
|
||||||
|
topEnd = if (isMyMessage) 0.dp else CornerSize.m,
|
||||||
|
bottomStart = CornerSize.m,
|
||||||
|
bottomEnd = CornerSize.m,
|
||||||
|
)
|
||||||
|
).fillMaxWidth().padding(Spacing.s)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = content,
|
||||||
|
color = textColor,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Spacing.xxxs),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
if (date.isNotEmpty()) {
|
||||||
|
val buttonModifier = Modifier.size(22.dp).padding(3.dp)
|
||||||
|
Icon(
|
||||||
|
modifier = buttonModifier.padding(1.dp),
|
||||||
|
imageVector = Icons.Default.Schedule,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = textColor,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = date.let {
|
||||||
|
when {
|
||||||
|
it.isEmpty() -> it
|
||||||
|
!it.endsWith("Z") -> {
|
||||||
|
DateTime.getPrettyDate(
|
||||||
|
iso8601Timestamp = it + "Z",
|
||||||
|
yearLabel = stringResource(
|
||||||
|
MR.strings.profile_year_short
|
||||||
|
),
|
||||||
|
monthLabel = stringResource(
|
||||||
|
MR.strings.profile_month_short
|
||||||
|
),
|
||||||
|
dayLabel = stringResource(MR.strings.profile_day_short),
|
||||||
|
hourLabel = stringResource(
|
||||||
|
MR.strings.post_hour_short
|
||||||
|
),
|
||||||
|
minuteLabel = stringResource(
|
||||||
|
MR.strings.post_minute_short
|
||||||
|
),
|
||||||
|
secondLabel = stringResource(
|
||||||
|
MR.strings.post_second_short
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
DateTime.getPrettyDate(
|
||||||
|
iso8601Timestamp = it,
|
||||||
|
yearLabel = stringResource(
|
||||||
|
MR.strings.profile_year_short
|
||||||
|
),
|
||||||
|
monthLabel = stringResource(
|
||||||
|
MR.strings.profile_month_short
|
||||||
|
),
|
||||||
|
dayLabel = stringResource(MR.strings.profile_day_short),
|
||||||
|
hourLabel = stringResource(
|
||||||
|
MR.strings.post_hour_short
|
||||||
|
),
|
||||||
|
minuteLabel = stringResource(
|
||||||
|
MR.strings.post_minute_short
|
||||||
|
),
|
||||||
|
secondLabel = stringResource(
|
||||||
|
MR.strings.post_second_short
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = textColor,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,166 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.list
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Schedule
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.github.diegoberaldin.racconforlemmy.core.utils.DateTime
|
||||||
|
import com.github.diegoberaldin.racconforlemmy.core.utils.onClick
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
|
||||||
|
import dev.icerock.moko.resources.compose.stringResource
|
||||||
|
import io.kamel.image.KamelImage
|
||||||
|
import io.kamel.image.asyncPainterResource
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ChatCard(
|
||||||
|
user: UserModel?,
|
||||||
|
lastMessage: String,
|
||||||
|
lastMessageDate: String? = null,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onOpenUser: ((UserModel) -> Unit)? = null,
|
||||||
|
onOpen: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.padding(Spacing.xs)
|
||||||
|
.onClick {
|
||||||
|
onOpen?.invoke()
|
||||||
|
},
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Spacing.m),
|
||||||
|
) {
|
||||||
|
val creatorName = user?.name.orEmpty()
|
||||||
|
val creatorHost = user?.host.orEmpty()
|
||||||
|
val creatorAvatar = user?.avatar.orEmpty()
|
||||||
|
val iconSize = 46.dp
|
||||||
|
|
||||||
|
if (creatorAvatar.isNotEmpty()) {
|
||||||
|
val painterResource = asyncPainterResource(data = creatorAvatar)
|
||||||
|
KamelImage(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(Spacing.xxxs)
|
||||||
|
.size(iconSize)
|
||||||
|
.clip(RoundedCornerShape(iconSize / 2))
|
||||||
|
.onClick {
|
||||||
|
if (user != null) {
|
||||||
|
onOpenUser?.invoke(user)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resource = painterResource,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.FillBounds,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(Spacing.xxxs)
|
||||||
|
.size(iconSize)
|
||||||
|
.background(
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
shape = RoundedCornerShape(iconSize / 2),
|
||||||
|
).onClick {
|
||||||
|
if (user != null) {
|
||||||
|
onOpenUser?.invoke(user)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = creatorName.firstOrNull()?.toString().orEmpty().uppercase(),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Spacing.xxs)
|
||||||
|
) {
|
||||||
|
// user name
|
||||||
|
Text(
|
||||||
|
text = buildString {
|
||||||
|
append(creatorName)
|
||||||
|
if (creatorHost.isNotEmpty()) {
|
||||||
|
append("@$creatorHost")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
// last message text
|
||||||
|
Text(
|
||||||
|
text = lastMessage,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.9f),
|
||||||
|
)
|
||||||
|
|
||||||
|
// last message date
|
||||||
|
if (lastMessageDate != null) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(Spacing.xxs),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
val buttonModifier = Modifier.size(24.dp).padding(3.25.dp)
|
||||||
|
Icon(
|
||||||
|
modifier = buttonModifier.padding(1.dp),
|
||||||
|
imageVector = Icons.Default.Schedule,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = lastMessageDate.let {
|
||||||
|
when {
|
||||||
|
it.isEmpty() -> it
|
||||||
|
!it.endsWith("Z") -> {
|
||||||
|
DateTime.getPrettyDate(
|
||||||
|
iso8601Timestamp = it + "Z",
|
||||||
|
yearLabel = stringResource(MR.strings.profile_year_short),
|
||||||
|
monthLabel = stringResource(MR.strings.profile_month_short),
|
||||||
|
dayLabel = stringResource(MR.strings.profile_day_short),
|
||||||
|
hourLabel = stringResource(MR.strings.post_hour_short),
|
||||||
|
minuteLabel = stringResource(MR.strings.post_minute_short),
|
||||||
|
secondLabel = stringResource(MR.strings.post_second_short),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
DateTime.getPrettyDate(
|
||||||
|
iso8601Timestamp = it,
|
||||||
|
yearLabel = stringResource(MR.strings.profile_year_short),
|
||||||
|
monthLabel = stringResource(MR.strings.profile_month_short),
|
||||||
|
dayLabel = stringResource(MR.strings.profile_day_short),
|
||||||
|
hourLabel = stringResource(MR.strings.post_hour_short),
|
||||||
|
minuteLabel = stringResource(MR.strings.post_minute_short),
|
||||||
|
secondLabel = stringResource(MR.strings.post_second_short),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.list
|
||||||
|
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PrivateMessageModel
|
||||||
|
|
||||||
|
interface InboxMessagesMviModel :
|
||||||
|
MviModel<InboxMessagesMviModel.Intent, InboxMessagesMviModel.UiState, InboxMessagesMviModel.SideEffect> {
|
||||||
|
sealed interface Intent {
|
||||||
|
object Refresh : Intent
|
||||||
|
object LoadNextPage : Intent
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UiState(
|
||||||
|
val refreshing: Boolean = false,
|
||||||
|
val loading: Boolean = false,
|
||||||
|
val canFetchMore: Boolean = true,
|
||||||
|
val unreadOnly: Boolean = true,
|
||||||
|
val currentUserId: Int = 0,
|
||||||
|
val chats: List<PrivateMessageModel> = emptyList(),
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface SideEffect
|
||||||
|
}
|
@ -0,0 +1,129 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.list
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.navigator.tab.Tab
|
||||||
|
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.di.getThemeRepository
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailScreen
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.di.getInboxMessagesViewModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.detail.InboxChatScreen
|
||||||
|
|
||||||
|
class InboxMessagesScreen : Tab {
|
||||||
|
|
||||||
|
override val options: TabOptions
|
||||||
|
@Composable get() {
|
||||||
|
return TabOptions(2u, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val model = rememberScreenModel { getInboxMessagesViewModel() }
|
||||||
|
model.bindToLifecycle(key)
|
||||||
|
val uiState by model.uiState.collectAsState()
|
||||||
|
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
|
||||||
|
|
||||||
|
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
|
||||||
|
model.reduce(InboxMessagesMviModel.Intent.Refresh)
|
||||||
|
})
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.pullRefresh(pullRefreshState),
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
|
||||||
|
) {
|
||||||
|
items(uiState.chats) { chat ->
|
||||||
|
val themeRepository = remember { getThemeRepository() }
|
||||||
|
val fontScale by themeRepository.contentFontScale.collectAsState()
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalDensity provides Density(
|
||||||
|
density = LocalDensity.current.density,
|
||||||
|
fontScale = fontScale,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
val otherUser = if (chat.creator?.id == uiState.currentUserId) {
|
||||||
|
chat.recipient
|
||||||
|
} else {
|
||||||
|
chat.creator
|
||||||
|
}
|
||||||
|
ChatCard(
|
||||||
|
user = otherUser,
|
||||||
|
lastMessage = chat.content.orEmpty(),
|
||||||
|
lastMessageDate = chat.publishDate,
|
||||||
|
onOpenUser = { user ->
|
||||||
|
navigator?.push(
|
||||||
|
UserDetailScreen(user)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onOpen = {
|
||||||
|
val userId = otherUser?.id
|
||||||
|
if (userId != null) {
|
||||||
|
navigator?.push(
|
||||||
|
InboxChatScreen(userId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
|
||||||
|
model.reduce(InboxMessagesMviModel.Intent.LoadNextPage)
|
||||||
|
}
|
||||||
|
if (uiState.loading && !uiState.refreshing) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(Spacing.xs),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(25.dp),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(Spacing.xxxl))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PullRefreshIndicator(
|
||||||
|
refreshing = uiState.refreshing,
|
||||||
|
state = pullRefreshState,
|
||||||
|
modifier = Modifier.align(Alignment.TopCenter),
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.surface,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
package com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.list
|
||||||
|
|
||||||
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
|
||||||
|
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.PrivateMessageRepository
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.SiteRepository
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.InboxCoordinator
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxMviModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.IO
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class InboxMessagesViewModel(
|
||||||
|
private val mvi: DefaultMviModel<InboxMessagesMviModel.Intent, InboxMessagesMviModel.UiState, InboxMessagesMviModel.SideEffect>,
|
||||||
|
private val identityRepository: IdentityRepository,
|
||||||
|
private val siteRepository: SiteRepository,
|
||||||
|
private val messageRepository: PrivateMessageRepository,
|
||||||
|
private val coordinator: InboxCoordinator,
|
||||||
|
private val notificationCenter: NotificationCenter,
|
||||||
|
) : ScreenModel,
|
||||||
|
MviModel<InboxMessagesMviModel.Intent, InboxMessagesMviModel.UiState, InboxMessagesMviModel.SideEffect> by mvi {
|
||||||
|
|
||||||
|
private var currentPage: Int = 1
|
||||||
|
|
||||||
|
override fun onStarted() {
|
||||||
|
mvi.onStarted()
|
||||||
|
mvi.scope.launch {
|
||||||
|
coordinator.effects.onEach {
|
||||||
|
when (it) {
|
||||||
|
InboxMviModel.Effect.Refresh -> refresh()
|
||||||
|
}
|
||||||
|
}.launchIn(this)
|
||||||
|
coordinator.unreadOnly.onEach {
|
||||||
|
if (it != uiState.value.unreadOnly) {
|
||||||
|
changeUnreadOnly(it)
|
||||||
|
}
|
||||||
|
}.launchIn(this)
|
||||||
|
notificationCenter.events.onEach { evt ->
|
||||||
|
when (evt) {
|
||||||
|
NotificationCenter.Event.Logout -> {
|
||||||
|
mvi.updateState { it.copy(chats = emptyList()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}.launchIn(this)
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
val auth = identityRepository.authToken.value.orEmpty()
|
||||||
|
val currentUserId = siteRepository.getCurrentUser(auth)?.id ?: 0
|
||||||
|
mvi.updateState { it.copy(currentUserId = currentUserId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun reduce(intent: InboxMessagesMviModel.Intent) {
|
||||||
|
when (intent) {
|
||||||
|
InboxMessagesMviModel.Intent.LoadNextPage -> loadNextPage()
|
||||||
|
InboxMessagesMviModel.Intent.Refresh -> refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refresh() {
|
||||||
|
currentPage = 1
|
||||||
|
mvi.updateState { it.copy(canFetchMore = true, refreshing = true) }
|
||||||
|
loadNextPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun changeUnreadOnly(value: Boolean) {
|
||||||
|
mvi.updateState { it.copy(unreadOnly = value) }
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadNextPage() {
|
||||||
|
val currentState = mvi.uiState.value
|
||||||
|
if (!currentState.canFetchMore || currentState.loading) {
|
||||||
|
mvi.updateState { it.copy(refreshing = false) }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mvi.scope.launch(Dispatchers.IO) {
|
||||||
|
mvi.updateState { it.copy(loading = true) }
|
||||||
|
val auth = identityRepository.authToken.value
|
||||||
|
val refreshing = currentState.refreshing
|
||||||
|
val unreadOnly = currentState.unreadOnly
|
||||||
|
val itemList = messageRepository.getAll(
|
||||||
|
auth = auth,
|
||||||
|
page = currentPage,
|
||||||
|
unreadOnly = unreadOnly,
|
||||||
|
).groupBy {
|
||||||
|
val creatorId = it.creator?.id ?: 0
|
||||||
|
val recipientId = it.recipient?.id ?: 0
|
||||||
|
listOf(creatorId, recipientId).sorted().toString()
|
||||||
|
}.mapNotNull {
|
||||||
|
val messages = it.value.sortedBy { m -> m.publishDate }
|
||||||
|
messages.lastOrNull()
|
||||||
|
}
|
||||||
|
currentPage++
|
||||||
|
val canFetchMore = itemList.size >= CommentRepository.DEFAULT_PAGE_SIZE
|
||||||
|
mvi.updateState {
|
||||||
|
val newItems = if (refreshing) {
|
||||||
|
itemList
|
||||||
|
} else {
|
||||||
|
it.chats + itemList
|
||||||
|
}
|
||||||
|
it.copy(
|
||||||
|
chats = newItems,
|
||||||
|
loading = false,
|
||||||
|
canFetchMore = canFetchMore,
|
||||||
|
refreshing = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -9,8 +9,7 @@ interface InboxRepliesMviModel :
|
|||||||
sealed interface Intent {
|
sealed interface Intent {
|
||||||
object Refresh : Intent
|
object Refresh : Intent
|
||||||
object LoadNextPage : Intent
|
object LoadNextPage : Intent
|
||||||
data class ChangeUnreadOnly(val unread: Boolean) : Intent
|
data class MarkAsRead(val read: Boolean, val mentionId: Int) : Intent
|
||||||
data class MarkMentionAsRead(val read: Boolean, val mentionId: Int) : Intent
|
|
||||||
object HapticIndication : Intent
|
object HapticIndication : Intent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,16 +25,20 @@ import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
|||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
import cafe.adriel.voyager.navigator.tab.Tab
|
import cafe.adriel.voyager.navigator.tab.Tab
|
||||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.di.getThemeRepository
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
|
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
|
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreen
|
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreen
|
||||||
@ -70,83 +74,92 @@ class InboxRepliesScreen : Tab {
|
|||||||
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
|
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
|
||||||
) {
|
) {
|
||||||
items(uiState.replies) { mention ->
|
items(uiState.replies) { mention ->
|
||||||
SwipeableCard(
|
val themeRepository = remember { getThemeRepository() }
|
||||||
modifier = Modifier.fillMaxWidth(),
|
val fontScale by themeRepository.contentFontScale.collectAsState()
|
||||||
backgroundColor = {
|
CompositionLocalProvider(
|
||||||
when (it) {
|
LocalDensity provides Density(
|
||||||
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.secondary
|
density = LocalDensity.current.density,
|
||||||
DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.tertiary
|
fontScale = fontScale,
|
||||||
else -> Color.Transparent
|
),
|
||||||
}
|
) {
|
||||||
},
|
SwipeableCard(
|
||||||
onGestureBegin = {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
model.reduce(InboxRepliesMviModel.Intent.HapticIndication)
|
backgroundColor = {
|
||||||
},
|
when (it) {
|
||||||
onDismissToStart = {
|
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.secondary
|
||||||
model.reduce(
|
DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.tertiary
|
||||||
InboxRepliesMviModel.Intent.MarkMentionAsRead(
|
else -> Color.Transparent
|
||||||
read = true,
|
}
|
||||||
mentionId = mention.id,
|
},
|
||||||
),
|
onGestureBegin = {
|
||||||
)
|
model.reduce(InboxRepliesMviModel.Intent.HapticIndication)
|
||||||
},
|
},
|
||||||
onDismissToEnd = {
|
onDismissToStart = {
|
||||||
model.reduce(
|
model.reduce(
|
||||||
InboxRepliesMviModel.Intent.MarkMentionAsRead(
|
InboxRepliesMviModel.Intent.MarkAsRead(
|
||||||
read = false,
|
read = true,
|
||||||
mentionId = mention.id,
|
mentionId = mention.id,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
swipeContent = { direction ->
|
onDismissToEnd = {
|
||||||
val icon = when (direction) {
|
model.reduce(
|
||||||
DismissDirection.StartToEnd -> Icons.Default.MarkChatUnread
|
InboxRepliesMviModel.Intent.MarkAsRead(
|
||||||
DismissDirection.EndToStart -> Icons.Default.MarkChatRead
|
read = false,
|
||||||
}
|
mentionId = mention.id,
|
||||||
val (iconModifier, iconTint) = when (direction) {
|
),
|
||||||
DismissDirection.StartToEnd -> {
|
)
|
||||||
Modifier.background(
|
},
|
||||||
color = MaterialTheme.colorScheme.onTertiary,
|
swipeContent = { direction ->
|
||||||
shape = CircleShape,
|
val icon = when (direction) {
|
||||||
) to MaterialTheme.colorScheme.tertiary
|
DismissDirection.StartToEnd -> Icons.Default.MarkChatUnread
|
||||||
|
DismissDirection.EndToStart -> Icons.Default.MarkChatRead
|
||||||
|
}
|
||||||
|
val (iconModifier, iconTint) = when (direction) {
|
||||||
|
DismissDirection.StartToEnd -> {
|
||||||
|
Modifier.background(
|
||||||
|
color = MaterialTheme.colorScheme.onTertiary,
|
||||||
|
shape = CircleShape,
|
||||||
|
) to MaterialTheme.colorScheme.tertiary
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
Modifier.background(
|
||||||
|
color = MaterialTheme.colorScheme.onSecondary,
|
||||||
|
shape = CircleShape,
|
||||||
|
) to MaterialTheme.colorScheme.secondary
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
Icon(
|
||||||
Modifier.background(
|
modifier = iconModifier.padding(Spacing.xs),
|
||||||
color = MaterialTheme.colorScheme.onSecondary,
|
imageVector = icon,
|
||||||
shape = CircleShape,
|
contentDescription = null,
|
||||||
) to MaterialTheme.colorScheme.secondary
|
tint = iconTint,
|
||||||
}
|
)
|
||||||
}
|
},
|
||||||
|
content = {
|
||||||
Icon(
|
InboxMentionCard(
|
||||||
modifier = iconModifier.padding(Spacing.xs),
|
mention = mention,
|
||||||
imageVector = icon,
|
onOpenPost = { post ->
|
||||||
contentDescription = null,
|
navigator?.push(
|
||||||
tint = iconTint,
|
PostDetailScreen(post),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
content = {
|
onOpenCreator = { user ->
|
||||||
InboxMentionCard(
|
navigator?.push(
|
||||||
mention = mention,
|
UserDetailScreen(user),
|
||||||
onOpenPost = { post ->
|
)
|
||||||
navigator?.push(
|
},
|
||||||
PostDetailScreen(post),
|
onOpenCommunity = { community ->
|
||||||
)
|
navigator?.push(
|
||||||
},
|
CommunityDetailScreen(community),
|
||||||
onOpenCreator = { user ->
|
)
|
||||||
navigator?.push(
|
},
|
||||||
UserDetailScreen(user),
|
)
|
||||||
)
|
},
|
||||||
},
|
)
|
||||||
onOpenCommunity = { community ->
|
}
|
||||||
navigator?.push(
|
|
||||||
CommunityDetailScreen(community),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
|
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
|
||||||
|
@ -62,11 +62,8 @@ class InboxRepliesViewModel(
|
|||||||
when (intent) {
|
when (intent) {
|
||||||
InboxRepliesMviModel.Intent.LoadNextPage -> loadNextPage()
|
InboxRepliesMviModel.Intent.LoadNextPage -> loadNextPage()
|
||||||
InboxRepliesMviModel.Intent.Refresh -> refresh()
|
InboxRepliesMviModel.Intent.Refresh -> refresh()
|
||||||
is InboxRepliesMviModel.Intent.ChangeUnreadOnly -> {
|
|
||||||
changeUnreadOnly(intent.unread)
|
|
||||||
}
|
|
||||||
|
|
||||||
is InboxRepliesMviModel.Intent.MarkMentionAsRead -> {
|
is InboxRepliesMviModel.Intent.MarkAsRead -> {
|
||||||
markAsRead(read = intent.read, replyId = intent.mentionId)
|
markAsRead(read = intent.read, replyId = intent.mentionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,9 +2,12 @@ package com.github.diegoberaldin.raccoonforlemmy.feature.inbox.di
|
|||||||
|
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxViewModel
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.main.InboxViewModel
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.mentions.InboxMentionsViewModel
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.mentions.InboxMentionsViewModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.detail.InboxChatViewModel
|
||||||
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.messages.list.InboxMessagesViewModel
|
||||||
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.replies.InboxRepliesViewModel
|
import com.github.diegoberaldin.raccoonforlemmy.feature.inbox.replies.InboxRepliesViewModel
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
actual fun getInboxViewModel() = InboxScreenModelHelper.model
|
actual fun getInboxViewModel() = InboxScreenModelHelper.model
|
||||||
|
|
||||||
@ -12,8 +15,21 @@ actual fun getInboxRepliesViewModel() = InboxScreenModelHelper.repliesModel
|
|||||||
|
|
||||||
actual fun getInboxMentionsViewModel() = InboxScreenModelHelper.mentionsModel
|
actual fun getInboxMentionsViewModel() = InboxScreenModelHelper.mentionsModel
|
||||||
|
|
||||||
|
actual fun getInboxMessagesViewModel() = InboxScreenModelHelper.messagesModel
|
||||||
|
|
||||||
|
actual fun getInboxChatViewModel(otherUserId: Int) =
|
||||||
|
InboxScreenModelHelper.getChatViewModel(otherUserId)
|
||||||
|
|
||||||
object InboxScreenModelHelper : KoinComponent {
|
object InboxScreenModelHelper : KoinComponent {
|
||||||
val model: InboxViewModel by inject()
|
val model: InboxViewModel by inject()
|
||||||
val repliesModel: InboxRepliesViewModel by inject()
|
val repliesModel: InboxRepliesViewModel by inject()
|
||||||
val mentionsModel: InboxMentionsViewModel by inject()
|
val mentionsModel: InboxMentionsViewModel by inject()
|
||||||
|
val messagesModel: InboxMessagesViewModel by inject()
|
||||||
|
|
||||||
|
fun getChatViewModel(otherUserId: Int): InboxChatViewModel {
|
||||||
|
val model: InboxChatViewModel by inject(
|
||||||
|
parameters = { parametersOf(otherUserId) }
|
||||||
|
)
|
||||||
|
return model
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user