mirror of
https://github.com/LiveFastEatTrashRaccoon/RaccoonForLemmy.git
synced 2025-02-09 13:08:46 +01:00
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 PersonMentionId = 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.CommunityService
|
||||
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.SiteService
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.UserService
|
||||
@ -48,6 +49,9 @@ internal class DefaultServiceProvider : ServiceProvider {
|
||||
override lateinit var search: SearchService
|
||||
private set
|
||||
|
||||
override lateinit var privateMessages: PrivateMessageService
|
||||
private set
|
||||
|
||||
private val baseUrl: String get() = "https://$currentInstance/api/$VERSION/"
|
||||
private val client = HttpClient {
|
||||
defaultRequest {
|
||||
@ -88,5 +92,6 @@ internal class DefaultServiceProvider : ServiceProvider {
|
||||
site = ktorfit.create()
|
||||
comment = 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.CommunityService
|
||||
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.SiteService
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.api.service.UserService
|
||||
@ -18,6 +19,7 @@ interface ServiceProvider {
|
||||
val site: SiteService
|
||||
val comment: CommentService
|
||||
val search: SearchService
|
||||
val privateMessages: PrivateMessageService
|
||||
|
||||
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)
|
||||
})
|
||||
Box(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.nestedScroll(fabNestedScrollConnection).pullRefresh(pullRefreshState),
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.nestedScroll(fabNestedScrollConnection)
|
||||
.pullRefresh(pullRefreshState),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(padding),
|
||||
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
|
||||
) {
|
||||
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.CommunityRepository
|
||||
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.UserRepository
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
@ -25,4 +26,5 @@ val repositoryModule = module {
|
||||
singleOf(::UserRepository)
|
||||
singleOf(::SiteRepository)
|
||||
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.PersonMentionView
|
||||
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.Hot
|
||||
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.PersonMentionModel
|
||||
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.UserModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserScoreModel
|
||||
@ -184,6 +186,16 @@ internal fun CommentReplyView.toModel() = PersonMentionModel(
|
||||
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 {
|
||||
val index = it.indexOf("/")
|
||||
if (index < 0) {
|
||||
|
@ -29,6 +29,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
@ -38,11 +39,14 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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 cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.navigator.bottomSheet.LocalBottomSheetNavigator
|
||||
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.architecture.bindToLifecycle
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreen
|
||||
@ -144,125 +148,134 @@ class PostListScreen : Screen {
|
||||
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
|
||||
) {
|
||||
itemsIndexed(uiState.posts) { idx, post ->
|
||||
SwipeableCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onGestureBegin = {
|
||||
model.reduce(PostListMviModel.Intent.HapticIndication)
|
||||
},
|
||||
onDismissToStart = {
|
||||
model.reduce(PostListMviModel.Intent.UpVotePost(idx))
|
||||
},
|
||||
onDismissToEnd = {
|
||||
model.reduce(PostListMviModel.Intent.DownVotePost(idx))
|
||||
},
|
||||
backgroundColor = {
|
||||
when (it) {
|
||||
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.secondary
|
||||
DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.tertiary
|
||||
else -> Color.Transparent
|
||||
}
|
||||
},
|
||||
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
|
||||
val themeRepository = remember { getThemeRepository() }
|
||||
val fontScale by themeRepository.contentFontScale.collectAsState()
|
||||
CompositionLocalProvider(
|
||||
LocalDensity provides Density(
|
||||
density = LocalDensity.current.density,
|
||||
fontScale = fontScale,
|
||||
),
|
||||
) {
|
||||
SwipeableCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onGestureBegin = {
|
||||
model.reduce(PostListMviModel.Intent.HapticIndication)
|
||||
},
|
||||
onDismissToStart = {
|
||||
model.reduce(PostListMviModel.Intent.UpVotePost(idx))
|
||||
},
|
||||
onDismissToEnd = {
|
||||
model.reduce(PostListMviModel.Intent.DownVotePost(idx))
|
||||
},
|
||||
backgroundColor = {
|
||||
when (it) {
|
||||
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.secondary
|
||||
DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.tertiary
|
||||
else -> Color.Transparent
|
||||
}
|
||||
},
|
||||
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 -> {
|
||||
Modifier.background(
|
||||
color = MaterialTheme.colorScheme.onTertiary,
|
||||
shape = CircleShape,
|
||||
) to MaterialTheme.colorScheme.tertiary
|
||||
}
|
||||
direction == DismissDirection.StartToEnd -> {
|
||||
Modifier.background(
|
||||
color = MaterialTheme.colorScheme.onTertiary,
|
||||
shape = CircleShape,
|
||||
) to MaterialTheme.colorScheme.tertiary
|
||||
}
|
||||
|
||||
direction == DismissDirection.EndToStart && post.myVote > 0 -> {
|
||||
Modifier.background(
|
||||
color = Color.Transparent,
|
||||
shape = CircleShape,
|
||||
) to MaterialTheme.colorScheme.onSecondary
|
||||
}
|
||||
direction == DismissDirection.EndToStart && post.myVote > 0 -> {
|
||||
Modifier.background(
|
||||
color = Color.Transparent,
|
||||
shape = CircleShape,
|
||||
) to MaterialTheme.colorScheme.onSecondary
|
||||
}
|
||||
|
||||
else -> {
|
||||
Modifier.background(
|
||||
color = MaterialTheme.colorScheme.onSecondary,
|
||||
shape = CircleShape,
|
||||
) to MaterialTheme.colorScheme.secondary
|
||||
else -> {
|
||||
Modifier.background(
|
||||
color = MaterialTheme.colorScheme.onSecondary,
|
||||
shape = CircleShape,
|
||||
) to MaterialTheme.colorScheme.secondary
|
||||
}
|
||||
}
|
||||
}
|
||||
Icon(
|
||||
modifier = iconModifier,
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = iconTint,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
PostCard(
|
||||
modifier = Modifier.onClick {
|
||||
navigator?.push(
|
||||
PostDetailScreen(post),
|
||||
)
|
||||
},
|
||||
post = post,
|
||||
blurNsfw = uiState.blurNsfw,
|
||||
onOpenCommunity = { community ->
|
||||
navigator?.push(
|
||||
CommunityDetailScreen(community),
|
||||
)
|
||||
},
|
||||
onOpenCreator = { user ->
|
||||
navigator?.push(
|
||||
UserDetailScreen(user),
|
||||
)
|
||||
},
|
||||
onUpVote = {
|
||||
model.reduce(
|
||||
PostListMviModel.Intent.UpVotePost(
|
||||
index = idx,
|
||||
feedback = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
onDownVote = {
|
||||
model.reduce(
|
||||
PostListMviModel.Intent.DownVotePost(
|
||||
index = idx,
|
||||
feedback = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
onSave = {
|
||||
model.reduce(
|
||||
PostListMviModel.Intent.SavePost(
|
||||
index = idx,
|
||||
feedback = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
onReply = {
|
||||
val screen = CreateCommentScreen(
|
||||
originalPost = post,
|
||||
)
|
||||
notificationCenter.addObserver({
|
||||
model.reduce(PostListMviModel.Intent.Refresh)
|
||||
}, key, screen.key)
|
||||
bottomSheetNavigator.show(screen)
|
||||
},
|
||||
onImageClick = { url ->
|
||||
navigator?.push(
|
||||
ZoomableImageScreen(url),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
Icon(
|
||||
modifier = iconModifier,
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = iconTint,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
PostCard(
|
||||
modifier = Modifier.onClick {
|
||||
navigator?.push(
|
||||
PostDetailScreen(post),
|
||||
)
|
||||
},
|
||||
post = post,
|
||||
blurNsfw = uiState.blurNsfw,
|
||||
onOpenCommunity = { community ->
|
||||
navigator?.push(
|
||||
CommunityDetailScreen(community),
|
||||
)
|
||||
},
|
||||
onOpenCreator = { user ->
|
||||
navigator?.push(
|
||||
UserDetailScreen(user),
|
||||
)
|
||||
},
|
||||
onUpVote = {
|
||||
model.reduce(
|
||||
PostListMviModel.Intent.UpVotePost(
|
||||
index = idx,
|
||||
feedback = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
onDownVote = {
|
||||
model.reduce(
|
||||
PostListMviModel.Intent.DownVotePost(
|
||||
index = idx,
|
||||
feedback = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
onSave = {
|
||||
model.reduce(
|
||||
PostListMviModel.Intent.SavePost(
|
||||
index = idx,
|
||||
feedback = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
onReply = {
|
||||
val screen = CreateCommentScreen(
|
||||
originalPost = post,
|
||||
)
|
||||
notificationCenter.addObserver({
|
||||
model.reduce(PostListMviModel.Intent.Refresh)
|
||||
}, key, screen.key)
|
||||
bottomSheetNavigator.show(screen)
|
||||
},
|
||||
onImageClick = { url ->
|
||||
navigator?.push(
|
||||
ZoomableImageScreen(url),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
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.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 org.koin.java.KoinJavaComponent
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koin.java.KoinJavaComponent.inject
|
||||
|
||||
actual fun getInboxViewModel(): InboxViewModel {
|
||||
val res: InboxViewModel by KoinJavaComponent.inject(InboxViewModel::class.java)
|
||||
val res: InboxViewModel by inject(InboxViewModel::class.java)
|
||||
return res
|
||||
}
|
||||
|
||||
actual fun getInboxRepliesViewModel(): InboxRepliesViewModel {
|
||||
val res: InboxRepliesViewModel by KoinJavaComponent.inject(InboxRepliesViewModel::class.java)
|
||||
val res: InboxRepliesViewModel by inject(InboxRepliesViewModel::class.java)
|
||||
return res
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -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.mentions.InboxMentionsMviModel
|
||||
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.InboxRepliesViewModel
|
||||
import org.koin.dsl.module
|
||||
@ -44,4 +48,26 @@ val inboxTabModule = module {
|
||||
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.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
|
||||
|
||||
expect fun getInboxViewModel(): InboxViewModel
|
||||
@ -9,3 +11,7 @@ expect fun getInboxViewModel(): InboxViewModel
|
||||
expect fun getInboxRepliesViewModel(): InboxRepliesViewModel
|
||||
|
||||
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.feature.inbox.di.getInboxViewModel
|
||||
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.resources.MR
|
||||
import com.github.diegoberaldin.raccoonforlemmy.resources.di.getLanguageRepository
|
||||
@ -69,7 +69,6 @@ object InboxScreen : Tab {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.padding(Spacing.xxs),
|
||||
topBar = {
|
||||
|
@ -9,8 +9,7 @@ interface InboxMentionsMviModel :
|
||||
sealed interface Intent {
|
||||
object Refresh : Intent
|
||||
object LoadNextPage : Intent
|
||||
data class ChangeUnreadOnly(val unread: Boolean) : Intent
|
||||
data class MarkMentionAsRead(val read: Boolean, val mentionId: Int) : Intent
|
||||
data class MarkAsRead(val read: Boolean, val mentionId: Int) : Intent
|
||||
object HapticIndication : Intent
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,6 @@ 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.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
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.userdetail.UserDetailScreen
|
||||
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 {
|
||||
|
||||
var parentModel: InboxViewModel? = null
|
||||
|
||||
override val options: TabOptions
|
||||
@Composable get() {
|
||||
return TabOptions(1u, "")
|
||||
@ -66,28 +58,8 @@ class InboxMentionsScreen : Tab {
|
||||
val model = rememberScreenModel { getInboxMentionsViewModel() }
|
||||
model.bindToLifecycle(key)
|
||||
val uiState by model.uiState.collectAsState()
|
||||
val parentUiState by (parentModel?.uiState
|
||||
?: MutableStateFlow(InboxMviModel.UiState())).collectAsState()
|
||||
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, {
|
||||
model.reduce(InboxMentionsMviModel.Intent.Refresh)
|
||||
})
|
||||
@ -113,7 +85,7 @@ class InboxMentionsScreen : Tab {
|
||||
},
|
||||
onDismissToStart = {
|
||||
model.reduce(
|
||||
InboxMentionsMviModel.Intent.MarkMentionAsRead(
|
||||
InboxMentionsMviModel.Intent.MarkAsRead(
|
||||
read = true,
|
||||
mentionId = mention.id,
|
||||
),
|
||||
@ -121,7 +93,7 @@ class InboxMentionsScreen : Tab {
|
||||
},
|
||||
onDismissToEnd = {
|
||||
model.reduce(
|
||||
InboxMentionsMviModel.Intent.MarkMentionAsRead(
|
||||
InboxMentionsMviModel.Intent.MarkAsRead(
|
||||
read = false,
|
||||
mentionId = mention.id,
|
||||
),
|
||||
|
@ -59,8 +59,7 @@ class InboxMentionsViewModel(
|
||||
when (intent) {
|
||||
InboxMentionsMviModel.Intent.LoadNextPage -> loadNextPage()
|
||||
InboxMentionsMviModel.Intent.Refresh -> refresh()
|
||||
is InboxMentionsMviModel.Intent.ChangeUnreadOnly -> changeUnreadOnly(intent.unread)
|
||||
is InboxMentionsMviModel.Intent.MarkMentionAsRead -> {
|
||||
is InboxMentionsMviModel.Intent.MarkAsRead -> {
|
||||
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 {
|
||||
object Refresh : Intent
|
||||
object LoadNextPage : Intent
|
||||
data class ChangeUnreadOnly(val unread: Boolean) : Intent
|
||||
data class MarkMentionAsRead(val read: Boolean, val mentionId: Int) : Intent
|
||||
data class MarkAsRead(val read: Boolean, val mentionId: Int) : Intent
|
||||
object HapticIndication : Intent
|
||||
}
|
||||
|
||||
|
@ -25,16 +25,20 @@ 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.graphics.Color
|
||||
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.communitydetail.CommunityDetailScreen
|
||||
@ -70,83 +74,92 @@ class InboxRepliesScreen : Tab {
|
||||
verticalArrangement = Arrangement.spacedBy(Spacing.xs),
|
||||
) {
|
||||
items(uiState.replies) { mention ->
|
||||
SwipeableCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
backgroundColor = {
|
||||
when (it) {
|
||||
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.secondary
|
||||
DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.tertiary
|
||||
else -> Color.Transparent
|
||||
}
|
||||
},
|
||||
onGestureBegin = {
|
||||
model.reduce(InboxRepliesMviModel.Intent.HapticIndication)
|
||||
},
|
||||
onDismissToStart = {
|
||||
model.reduce(
|
||||
InboxRepliesMviModel.Intent.MarkMentionAsRead(
|
||||
read = true,
|
||||
mentionId = mention.id,
|
||||
),
|
||||
)
|
||||
},
|
||||
onDismissToEnd = {
|
||||
model.reduce(
|
||||
InboxRepliesMviModel.Intent.MarkMentionAsRead(
|
||||
read = false,
|
||||
mentionId = mention.id,
|
||||
),
|
||||
)
|
||||
},
|
||||
swipeContent = { direction ->
|
||||
val icon = when (direction) {
|
||||
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
|
||||
val themeRepository = remember { getThemeRepository() }
|
||||
val fontScale by themeRepository.contentFontScale.collectAsState()
|
||||
CompositionLocalProvider(
|
||||
LocalDensity provides Density(
|
||||
density = LocalDensity.current.density,
|
||||
fontScale = fontScale,
|
||||
),
|
||||
) {
|
||||
SwipeableCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
backgroundColor = {
|
||||
when (it) {
|
||||
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.secondary
|
||||
DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.tertiary
|
||||
else -> Color.Transparent
|
||||
}
|
||||
},
|
||||
onGestureBegin = {
|
||||
model.reduce(InboxRepliesMviModel.Intent.HapticIndication)
|
||||
},
|
||||
onDismissToStart = {
|
||||
model.reduce(
|
||||
InboxRepliesMviModel.Intent.MarkAsRead(
|
||||
read = true,
|
||||
mentionId = mention.id,
|
||||
),
|
||||
)
|
||||
},
|
||||
onDismissToEnd = {
|
||||
model.reduce(
|
||||
InboxRepliesMviModel.Intent.MarkAsRead(
|
||||
read = false,
|
||||
mentionId = mention.id,
|
||||
),
|
||||
)
|
||||
},
|
||||
swipeContent = { direction ->
|
||||
val icon = when (direction) {
|
||||
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 -> {
|
||||
Modifier.background(
|
||||
color = MaterialTheme.colorScheme.onSecondary,
|
||||
shape = CircleShape,
|
||||
) to MaterialTheme.colorScheme.secondary
|
||||
}
|
||||
}
|
||||
|
||||
Icon(
|
||||
modifier = iconModifier.padding(Spacing.xs),
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = iconTint,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
InboxMentionCard(
|
||||
mention = mention,
|
||||
onOpenPost = { post ->
|
||||
navigator?.push(
|
||||
PostDetailScreen(post),
|
||||
)
|
||||
},
|
||||
onOpenCreator = { user ->
|
||||
navigator?.push(
|
||||
UserDetailScreen(user),
|
||||
)
|
||||
},
|
||||
onOpenCommunity = { community ->
|
||||
navigator?.push(
|
||||
CommunityDetailScreen(community),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
Icon(
|
||||
modifier = iconModifier.padding(Spacing.xs),
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = iconTint,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
InboxMentionCard(
|
||||
mention = mention,
|
||||
onOpenPost = { post ->
|
||||
navigator?.push(
|
||||
PostDetailScreen(post),
|
||||
)
|
||||
},
|
||||
onOpenCreator = { user ->
|
||||
navigator?.push(
|
||||
UserDetailScreen(user),
|
||||
)
|
||||
},
|
||||
onOpenCommunity = { community ->
|
||||
navigator?.push(
|
||||
CommunityDetailScreen(community),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
item {
|
||||
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
|
||||
|
@ -62,11 +62,8 @@ class InboxRepliesViewModel(
|
||||
when (intent) {
|
||||
InboxRepliesMviModel.Intent.LoadNextPage -> loadNextPage()
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -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.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 org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
actual fun getInboxViewModel() = InboxScreenModelHelper.model
|
||||
|
||||
@ -12,8 +15,21 @@ actual fun getInboxRepliesViewModel() = InboxScreenModelHelper.repliesModel
|
||||
|
||||
actual fun getInboxMentionsViewModel() = InboxScreenModelHelper.mentionsModel
|
||||
|
||||
actual fun getInboxMessagesViewModel() = InboxScreenModelHelper.messagesModel
|
||||
|
||||
actual fun getInboxChatViewModel(otherUserId: Int) =
|
||||
InboxScreenModelHelper.getChatViewModel(otherUserId)
|
||||
|
||||
object InboxScreenModelHelper : KoinComponent {
|
||||
val model: InboxViewModel by inject()
|
||||
val repliesModel: InboxRepliesViewModel 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