fix: avoid concurrency issue while posts are being marked as read before opening detail (#262)

* update PostListScreen

* update CommunityDetailScreen

* update FilteredContentsScreen

* update MultiCommunityScreen

* update UserDetailScreen

* update ProfileLoggedScreen

* update InboxChatScreen

* update InboxMentionsScreen

* update InboxRepliesScreen
This commit is contained in:
Dieguitux 2025-01-13 09:56:32 +01:00 committed by GitHub
parent 9f4ebbcf38
commit 705c0893e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 406 additions and 213 deletions

View File

@ -277,7 +277,7 @@ class InboxChatScreen(
items( items(
items = uiState.messages, items = uiState.messages,
key = { key = {
it.id.toString() + (it.updateDate ?: it.publishDate) + it.read it.id.toString() + (it.updateDate ?: it.publishDate)
}, },
) { message -> ) { message ->
val isMyMessage = message.creator?.id == uiState.currentUserId val isMyMessage = message.creator?.id == uiState.currentUserId

View File

@ -166,21 +166,19 @@ class InboxChatViewModel(
} }
} }
private fun markAsRead( private suspend fun markAsRead(
read: Boolean, read: Boolean,
messageId: Long, messageId: Long,
) { ) {
val auth = identityRepository.authToken.value val auth = identityRepository.authToken.value
screenModelScope.launch { val newMessage =
val newMessage = messageRepository.markAsRead(
messageRepository.markAsRead( read = read,
read = read, messageId = messageId,
messageId = messageId, auth = auth,
auth = auth, )
) if (newMessage != null) {
if (newMessage != null) { handleMessageUpdate(newMessage)
handleMessageUpdate(newMessage)
}
} }
} }

View File

@ -102,7 +102,9 @@ interface CommunityDetailMviModel :
val value: Boolean, val value: Boolean,
) : Intent ) : Intent
data object WillOpenDetail : Intent data class WillOpenDetail(
val id: Long,
) : Intent
data object UnhideCommunity : Intent data object UnhideCommunity : Intent
@ -175,5 +177,9 @@ interface CommunityDetailMviModel :
) : Effect ) : Effect
data object Back : Effect data object Back : Effect
data class OpenDetail(
val post: PostModel,
) : Effect
} }
} }

View File

@ -246,6 +246,8 @@ class CommunityDetailScreen(
} }
CommunityDetailMviModel.Effect.Back -> navigationCoordinator.popScreen() CommunityDetailMviModel.Effect.Back -> navigationCoordinator.popScreen()
is CommunityDetailMviModel.Effect.OpenDetail ->
detailOpener.openPostDetail(effect.post)
} }
}.launchIn(this) }.launchIn(this)
} }
@ -822,7 +824,7 @@ class CommunityDetailScreen(
items( items(
items = uiState.posts, items = uiState.posts,
key = { key = {
it.id.toString() + (it.updateDate ?: it.publishDate) + it.read it.id.toString() + (it.updateDate ?: it.publishDate)
}, },
) { post -> ) { post ->
LaunchedEffect(post.id) { LaunchedEffect(post.id) {
@ -977,11 +979,10 @@ class CommunityDetailScreen(
meTagColor = uiState.meTagColor, meTagColor = uiState.meTagColor,
onClick = { onClick = {
model.reduce( model.reduce(
CommunityDetailMviModel.Intent.MarkAsRead( CommunityDetailMviModel.Intent.WillOpenDetail(
post.id, post.id,
), ),
) )
model.reduce(CommunityDetailMviModel.Intent.WillOpenDetail)
detailOpener.openPostDetail( detailOpener.openPostDetail(
post = post, post = post,
otherInstance = otherInstanceName, otherInstance = otherInstanceName,
@ -1027,11 +1028,10 @@ class CommunityDetailScreen(
onReply = onReply =
{ {
model.reduce( model.reduce(
CommunityDetailMviModel.Intent.MarkAsRead( CommunityDetailMviModel.Intent.WillOpenDetail(
post.id, post.id,
), ),
) )
model.reduce(CommunityDetailMviModel.Intent.WillOpenDetail)
detailOpener.openPostDetail(post) detailOpener.openPostDetail(post)
}.takeIf { uiState.isLogged && !isOnOtherInstance }, }.takeIf { uiState.isLogged && !isOnOtherInstance },
onOpenImage = { url -> onOpenImage = { url ->

View File

@ -299,8 +299,9 @@ class CommunityDetailViewModel(
CommunityDetailMviModel.Intent.Block -> blockCommunity() CommunityDetailMviModel.Intent.Block -> blockCommunity()
CommunityDetailMviModel.Intent.BlockInstance -> blockInstance() CommunityDetailMviModel.Intent.BlockInstance -> blockInstance()
is CommunityDetailMviModel.Intent.MarkAsRead -> { is CommunityDetailMviModel.Intent.MarkAsRead ->
markAsRead(uiState.value.posts.first { it.id == intent.id }) screenModelScope.launch {
markAsRead(uiState.value.posts.first { it.id == intent.id })
} }
CommunityDetailMviModel.Intent.ClearRead -> clearRead() CommunityDetailMviModel.Intent.ClearRead -> clearRead()
@ -367,9 +368,15 @@ class CommunityDetailViewModel(
is CommunityDetailMviModel.Intent.SetSearch -> updateSearchText(intent.value) is CommunityDetailMviModel.Intent.SetSearch -> updateSearchText(intent.value)
CommunityDetailMviModel.Intent.WillOpenDetail -> { is CommunityDetailMviModel.Intent.WillOpenDetail ->
val state = postPaginationManager.extractState() screenModelScope.launch {
postNavigationManager.push(state) uiState.value.posts
.firstOrNull { it.id == intent.id }
?.also { post ->
markAsRead(post)
val state = postPaginationManager.extractState()
postNavigationManager.push(state)
}
} }
CommunityDetailMviModel.Intent.UnhideCommunity -> { CommunityDetailMviModel.Intent.UnhideCommunity -> {
@ -552,24 +559,22 @@ class CommunityDetailViewModel(
} }
} }
private fun markAsRead(post: PostModel) { private suspend fun markAsRead(post: PostModel) {
if (post.read) { if (post.read) {
return return
} }
val newPost = post.copy(read = true) val newPost = post.copy(read = true)
screenModelScope.launch { try {
try { val auth = identityRepository.authToken.value.orEmpty()
val auth = identityRepository.authToken.value.orEmpty() postRepository.setRead(
postRepository.setRead( read = true,
read = true, postId = post.id,
postId = post.id, auth = auth,
auth = auth, )
) handlePostUpdate(newPost)
handlePostUpdate(newPost) } catch (e: Throwable) {
} catch (e: Throwable) { e.printStackTrace()
e.printStackTrace() handlePostUpdate(post)
handlePostUpdate(post)
}
} }
} }

View File

@ -100,7 +100,10 @@ interface FilteredContentsMviModel :
val commentId: Long, val commentId: Long,
) : Intent ) : Intent
data object WillOpenDetail : Intent data class WillOpenDetail(
val postId: Long,
val commentId: Long? = null,
) : Intent
} }
data class State( data class State(
@ -136,5 +139,10 @@ interface FilteredContentsMviModel :
sealed interface Effect { sealed interface Effect {
data object BackToTop : Effect data object BackToTop : Effect
data class OpenDetail(
val postId: Long,
val commentId: Long? = null,
) : Effect
} }
} }

View File

@ -154,6 +154,13 @@ class FilteredContentsScreen(
topAppBarState.contentOffset = 0f topAppBarState.contentOffset = 0f
} }
} }
is FilteredContentsMviModel.Effect.OpenDetail ->
detailOpener.openPostDetail(
post = PostModel(id = effect.postId),
highlightCommentId = effect.commentId,
isMod = true,
)
} }
}.launchIn(this) }.launchIn(this)
} }
@ -369,7 +376,7 @@ class FilteredContentsScreen(
items( items(
items = uiState.posts, items = uiState.posts,
key = { key = {
it.id.toString() + (it.updateDate ?: it.publishDate) + it.read it.id.toString() + (it.updateDate ?: it.publishDate)
}, },
) { post -> ) { post ->
@ -482,8 +489,11 @@ class FilteredContentsScreen(
botTagColor = uiState.botTagColor, botTagColor = uiState.botTagColor,
meTagColor = uiState.meTagColor, meTagColor = uiState.meTagColor,
onClick = { onClick = {
model.reduce(FilteredContentsMviModel.Intent.WillOpenDetail) model.reduce(
detailOpener.openPostDetail(post) FilteredContentsMviModel.Intent.WillOpenDetail(
postId = post.id,
),
)
}, },
onOpenCommunity = { community, instance -> onOpenCommunity = { community, instance ->
detailOpener.openCommunityDetail( detailOpener.openCommunityDetail(
@ -512,8 +522,11 @@ class FilteredContentsScreen(
) )
}, },
onReply = { onReply = {
model.reduce(FilteredContentsMviModel.Intent.WillOpenDetail) model.reduce(
detailOpener.openPostDetail(post) FilteredContentsMviModel.Intent.WillOpenDetail(
postId = post.id,
),
)
}, },
onOpenImage = { url -> onOpenImage = { url ->
navigationCoordinator.pushScreen( navigationCoordinator.pushScreen(
@ -820,11 +833,11 @@ class FilteredContentsScreen(
detailOpener.openUserDetail(user, instance) detailOpener.openUserDetail(user, instance)
}, },
onOpen = { onOpen = {
model.reduce(FilteredContentsMviModel.Intent.WillOpenDetail) model.reduce(
detailOpener.openPostDetail( FilteredContentsMviModel.Intent.WillOpenDetail(
post = PostModel(id = comment.postId), postId = comment.postId,
highlightCommentId = comment.id, commentId = comment.id,
isMod = true, ),
) )
}, },
onUpVote = { onUpVote = {

View File

@ -212,10 +212,19 @@ class FilteredContentsViewModel(
distinguish(comment) distinguish(comment)
} }
FilteredContentsMviModel.Intent.WillOpenDetail -> { is FilteredContentsMviModel.Intent.WillOpenDetail ->
val state = postPaginationManager.extractState() screenModelScope.launch {
postNavigationManager.push(state) if (intent.commentId == null) {
} val state = postPaginationManager.extractState()
postNavigationManager.push(state)
}
emitEffect(
FilteredContentsMviModel.Effect.OpenDetail(
postId = intent.postId,
commentId = intent.commentId,
),
)
}
} }
} }

View File

@ -7,6 +7,7 @@ import com.livefast.eattrash.raccoonforlemmy.core.appearance.data.VoteFormat
import com.livefast.eattrash.raccoonforlemmy.core.architecture.MviModel import com.livefast.eattrash.raccoonforlemmy.core.architecture.MviModel
import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.ActionOnSwipe import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.ActionOnSwipe
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.data.PersonMentionModel import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.data.PersonMentionModel
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.data.PostModel
@Stable @Stable
interface InboxMentionsMviModel : interface InboxMentionsMviModel :
@ -31,6 +32,12 @@ interface InboxMentionsMviModel :
data class DownVoteComment( data class DownVoteComment(
val id: Long, val id: Long,
) : Intent ) : Intent
data class WillOpenDetail(
val id: Long,
val post: PostModel,
val commentId: Long,
) : Intent
} }
data class UiState( data class UiState(
@ -58,5 +65,10 @@ interface InboxMentionsMviModel :
) : Effect ) : Effect
data object BackToTop : Effect data object BackToTop : Effect
data class OpenDetail(
val post: PostModel,
val commentId: Long,
) : Effect
} }
} }

View File

@ -99,6 +99,12 @@ class InboxMentionsScreen : Tab {
lazyListState.scrollToItem(0) lazyListState.scrollToItem(0)
} }
} }
is InboxMentionsMviModel.Effect.OpenDetail ->
detailOpener.openPostDetail(
post = effect.post,
highlightCommentId = effect.commentId,
)
} }
}.launchIn(this) }.launchIn(this)
} }
@ -138,7 +144,7 @@ class InboxMentionsScreen : Tab {
} }
items( items(
items = uiState.mentions, items = uiState.mentions,
key = { it.id.toString() + it.read + uiState.unreadOnly }, key = { it.id.toString() + uiState.unreadOnly },
) { mention -> ) { mention ->
@Composable @Composable
fun List<ActionOnSwipe>.toSwipeActions(): List<SwipeAction> = fun List<ActionOnSwipe>.toSwipeActions(): List<SwipeAction> =
@ -233,18 +239,12 @@ class InboxMentionsScreen : Tab {
downVoteEnabled = uiState.downVoteEnabled, downVoteEnabled = uiState.downVoteEnabled,
previewMaxLines = uiState.previewMaxLines, previewMaxLines = uiState.previewMaxLines,
onClick = { post -> onClick = { post ->
if (!mention.read) { model.reduce(
model.reduce( InboxMentionsMviModel.Intent.WillOpenDetail(
InboxMentionsMviModel.Intent.MarkAsRead( id = mention.id,
read = true, post = post,
id = mention.id, commentId = mention.comment.id,
), ),
)
}
detailOpener.openPostDetail(
post = post,
highlightCommentId = mention.comment.id,
otherInstance = "",
) )
}, },
onOpenCreator = { user, instance -> onOpenCreator = { user, instance ->

View File

@ -101,13 +101,14 @@ class InboxMentionsViewModel(
emitEffect(InboxMentionsMviModel.Effect.BackToTop) emitEffect(InboxMentionsMviModel.Effect.BackToTop)
} }
is InboxMentionsMviModel.Intent.MarkAsRead -> { is InboxMentionsMviModel.Intent.MarkAsRead ->
val mention = uiState.value.mentions.first { it.id == intent.id } screenModelScope.launch {
markAsRead( val mention = uiState.value.mentions.first { it.id == intent.id }
read = intent.read, markAsRead(
mention = mention, read = intent.read,
) mention = mention,
} )
}
InboxMentionsMviModel.Intent.HapticIndication -> hapticFeedback.vibrate() InboxMentionsMviModel.Intent.HapticIndication -> hapticFeedback.vibrate()
is InboxMentionsMviModel.Intent.DownVoteComment -> { is InboxMentionsMviModel.Intent.DownVoteComment -> {
@ -119,6 +120,24 @@ class InboxMentionsViewModel(
val mention = uiState.value.mentions.first { it.id == intent.id } val mention = uiState.value.mentions.first { it.id == intent.id }
toggleUpVoteComment(mention) toggleUpVoteComment(mention)
} }
is InboxMentionsMviModel.Intent.WillOpenDetail ->
screenModelScope.launch {
uiState.value.mentions.firstOrNull { it.id == intent.id }?.also { mention ->
if (!mention.read) {
markAsRead(
mention = mention,
read = true,
)
}
emitEffect(
InboxMentionsMviModel.Effect.OpenDetail(
post = intent.post,
commentId = intent.commentId,
),
)
}
}
} }
} }
@ -202,33 +221,31 @@ class InboxMentionsViewModel(
} }
} }
private fun markAsRead( private suspend fun markAsRead(
read: Boolean, read: Boolean,
mention: PersonMentionModel, mention: PersonMentionModel,
) { ) {
val auth = identityRepository.authToken.value val auth = identityRepository.authToken.value
screenModelScope.launch { userRepository.setMentionRead(
userRepository.setMentionRead( read = read,
read = read, mentionId = mention.id,
mentionId = mention.id, auth = auth,
auth = auth, )
) val currentState = uiState.value
val currentState = uiState.value if (read && currentState.unreadOnly) {
if (read && currentState.unreadOnly) { updateState {
updateState { it.copy(
it.copy( mentions =
mentions = currentState.mentions.filter { m ->
currentState.mentions.filter { m -> m.id != mention.id
m.id != mention.id },
}, )
)
}
} else {
val newMention = mention.copy(read = read)
handleItemUpdate(newMention)
} }
updateUnreadItems() } else {
val newMention = mention.copy(read = read)
handleItemUpdate(newMention)
} }
updateUnreadItems()
} }
private fun toggleUpVoteComment(mention: PersonMentionModel) { private fun toggleUpVoteComment(mention: PersonMentionModel) {

View File

@ -48,7 +48,9 @@ interface MultiCommunityMviModel :
val url: String, val url: String,
) : Intent ) : Intent
data object WillOpenDetail : Intent data class WillOpenDetail(
val id: Long,
) : Intent
} }
data class UiState( data class UiState(
@ -83,5 +85,9 @@ interface MultiCommunityMviModel :
sealed interface Effect { sealed interface Effect {
data object BackToTop : Effect data object BackToTop : Effect
data class OpenDetail(
val post: PostModel,
) : Effect
} }
} }

View File

@ -138,6 +138,9 @@ class MultiCommunityScreen(
topAppBarState.contentOffset = 0f topAppBarState.contentOffset = 0f
} }
} }
is MultiCommunityMviModel.Effect.OpenDetail ->
detailOpener.openPostDetail(post = effect.post)
} }
}.launchIn(this) }.launchIn(this)
} }
@ -282,7 +285,7 @@ class MultiCommunityScreen(
items( items(
items = uiState.posts, items = uiState.posts,
key = { key = {
it.id.toString() + (it.updateDate ?: it.publishDate) + it.read it.id.toString() + (it.updateDate ?: it.publishDate)
}, },
) { post -> ) { post ->
LaunchedEffect(post.id) { LaunchedEffect(post.id) {
@ -404,9 +407,9 @@ class MultiCommunityScreen(
botTagColor = uiState.botTagColor, botTagColor = uiState.botTagColor,
meTagColor = uiState.meTagColor, meTagColor = uiState.meTagColor,
onClick = { onClick = {
model.reduce(MultiCommunityMviModel.Intent.MarkAsRead(post.id)) model.reduce(
model.reduce(MultiCommunityMviModel.Intent.WillOpenDetail) MultiCommunityMviModel.Intent.WillOpenDetail(post.id),
detailOpener.openPostDetail(post) )
}, },
onDoubleClick = onDoubleClick =
{ {
@ -439,8 +442,9 @@ class MultiCommunityScreen(
) )
}, },
onReply = { onReply = {
model.reduce(MultiCommunityMviModel.Intent.WillOpenDetail) model.reduce(
detailOpener.openPostDetail(post) MultiCommunityMviModel.Intent.WillOpenDetail(post.id),
)
}, },
onOpenImage = { url -> onOpenImage = { url ->
model.reduce(MultiCommunityMviModel.Intent.MarkAsRead(post.id)) model.reduce(MultiCommunityMviModel.Intent.MarkAsRead(post.id))

View File

@ -191,19 +191,26 @@ class MultiCommunityViewModel(
MultiCommunityMviModel.Intent.ClearRead -> clearRead() MultiCommunityMviModel.Intent.ClearRead -> clearRead()
is MultiCommunityMviModel.Intent.MarkAsRead -> is MultiCommunityMviModel.Intent.MarkAsRead ->
markAsRead( screenModelScope.launch {
post = uiState.value.posts.first { it.id == intent.id }, markAsRead(
) post = uiState.value.posts.first { it.id == intent.id },
)
}
is MultiCommunityMviModel.Intent.Hide -> is MultiCommunityMviModel.Intent.Hide ->
hide( hide(
post = uiState.value.posts.first { it.id == intent.id }, post = uiState.value.posts.first { it.id == intent.id },
) )
MultiCommunityMviModel.Intent.WillOpenDetail -> { is MultiCommunityMviModel.Intent.WillOpenDetail ->
val state = postPaginationManager.extractState() screenModelScope.launch {
postNavigationManager.push(state) uiState.value.posts.firstOrNull { it.id == intent.id }?.also { post ->
} markAsRead(post)
val state = postPaginationManager.extractState()
postNavigationManager.push(state)
emitEffect(MultiCommunityMviModel.Effect.OpenDetail(post))
}
}
} }
} }
@ -314,24 +321,22 @@ class MultiCommunityViewModel(
} }
} }
private fun markAsRead(post: PostModel) { private suspend fun markAsRead(post: PostModel) {
if (post.read) { if (post.read) {
return return
} }
val newPost = post.copy(read = true) val newPost = post.copy(read = true)
screenModelScope.launch { try {
try { val auth = identityRepository.authToken.value.orEmpty()
val auth = identityRepository.authToken.value.orEmpty() postRepository.setRead(
postRepository.setRead( read = true,
read = true, postId = post.id,
postId = post.id, auth = auth,
auth = auth, )
) handlePostUpdate(newPost)
handlePostUpdate(newPost) } catch (e: Throwable) {
} catch (e: Throwable) { e.printStackTrace()
e.printStackTrace() handlePostUpdate(post)
handlePostUpdate(post)
}
} }
} }

View File

@ -63,7 +63,10 @@ interface ProfileLoggedMviModel :
val feedback: Boolean = false, val feedback: Boolean = false,
) : Intent ) : Intent
data object WillOpenDetail : Intent data class WillOpenDetail(
val postId: Long,
val commentId: Long? = null,
) : Intent
data class RestorePost( data class RestorePost(
val id: Long, val id: Long,
@ -96,5 +99,10 @@ interface ProfileLoggedMviModel :
val isModerator: Boolean = false, val isModerator: Boolean = false,
) )
sealed interface Effect sealed interface Effect {
data class OpenDetail(
val postId: Long,
val commentId: Long? = null,
) : Effect
}
} }

View File

@ -123,6 +123,18 @@ object ProfileLoggedScreen : Tab {
model.reduce(ProfileLoggedMviModel.Intent.Refresh) model.reduce(ProfileLoggedMviModel.Intent.Refresh)
}.launchIn(this) }.launchIn(this)
} }
LaunchedEffect(model) {
model.effects
.onEach { effect ->
when (effect) {
is ProfileLoggedMviModel.Effect.OpenDetail ->
detailOpener.openPostDetail(
post = PostModel(id = effect.postId),
highlightCommentId = effect.commentId,
)
}
}.launchIn(this)
}
if (uiState.initial) { if (uiState.initial) {
ProgressHud() ProgressHud()
@ -277,7 +289,7 @@ object ProfileLoggedScreen : Tab {
items( items(
items = uiState.posts, items = uiState.posts,
key = { key = {
it.id.toString() + (it.updateDate ?: it.publishDate) + it.read it.id.toString() + (it.updateDate ?: it.publishDate)
}, },
) { post -> ) { post ->
PostCard( PostCard(
@ -295,12 +307,18 @@ object ProfileLoggedScreen : Tab {
blurNsfw = false, blurNsfw = false,
downVoteEnabled = uiState.downVoteEnabled, downVoteEnabled = uiState.downVoteEnabled,
onClick = { onClick = {
model.reduce(ProfileLoggedMviModel.Intent.WillOpenDetail) model.reduce(
detailOpener.openPostDetail(post) ProfileLoggedMviModel.Intent.WillOpenDetail(
postId = post.id,
),
)
}, },
onReply = { onReply = {
model.reduce(ProfileLoggedMviModel.Intent.WillOpenDetail) model.reduce(
detailOpener.openPostDetail(post) ProfileLoggedMviModel.Intent.WillOpenDetail(
postId = post.id,
),
)
}, },
onOpenCommunity = { community, instance -> onOpenCommunity = { community, instance ->
detailOpener.openCommunityDetail(community, instance) detailOpener.openCommunityDetail(community, instance)
@ -494,9 +512,11 @@ object ProfileLoggedScreen : Tab {
detailOpener.openCommunityDetail(community, instance) detailOpener.openCommunityDetail(community, instance)
}, },
onClick = { onClick = {
detailOpener.openPostDetail( model.reduce(
post = PostModel(id = comment.postId), ProfileLoggedMviModel.Intent.WillOpenDetail(
highlightCommentId = comment.id, postId = comment.postId,
commentId = comment.id,
),
) )
}, },
onReply = { onReply = {

View File

@ -217,9 +217,18 @@ class ProfileLoggedViewModel(
} }
} }
ProfileLoggedMviModel.Intent.WillOpenDetail -> { is ProfileLoggedMviModel.Intent.WillOpenDetail ->
val state = postPaginationManager.extractState() screenModelScope.launch {
postNavigationManager.push(state) if (intent.commentId == null) {
val state = postPaginationManager.extractState()
postNavigationManager.push(state)
}
emitEffect(
ProfileLoggedMviModel.Effect.OpenDetail(
postId = intent.postId,
commentId = intent.commentId,
),
)
} }
is ProfileLoggedMviModel.Intent.RestorePost -> { is ProfileLoggedMviModel.Intent.RestorePost -> {

View File

@ -66,7 +66,9 @@ interface PostListMviModel :
data object PauseZombieMode : Intent data object PauseZombieMode : Intent
data object WillOpenDetail : Intent data class WillOpenDetail(
val id: Long,
) : Intent
} }
data class UiState( data class UiState(
@ -107,5 +109,9 @@ interface PostListMviModel :
data class ZombieModeTick( data class ZombieModeTick(
val index: Int, val index: Int,
) : Effect ) : Effect
data class OpenDetail(
val post: PostModel,
) : Effect
} }
} }

View File

@ -188,6 +188,9 @@ class PostListScreen : Screen {
} }
} }
} }
is PostListMviModel.Effect.OpenDetail ->
detailOpener.openPostDetail(effect.post)
} }
}.launchIn(this) }.launchIn(this)
} }
@ -361,7 +364,7 @@ class PostListScreen : Screen {
key = { key = {
it.id.toString() + ( it.id.toString() + (
it.updateDate ?: it.publishDate it.updateDate ?: it.publishDate
) + it.read + uiState.isLogged ) + uiState.isLogged
}, },
) { post -> ) { post ->
LaunchedEffect(post.id) { LaunchedEffect(post.id) {
@ -504,9 +507,7 @@ class PostListScreen : Screen {
botTagColor = uiState.botTagColor, botTagColor = uiState.botTagColor,
meTagColor = uiState.meTagColor, meTagColor = uiState.meTagColor,
onClick = { onClick = {
model.reduce(PostListMviModel.Intent.MarkAsRead(post.id)) model.reduce(PostListMviModel.Intent.WillOpenDetail(post.id))
model.reduce(PostListMviModel.Intent.WillOpenDetail)
detailOpener.openPostDetail(post)
}, },
onDoubleClick = onDoubleClick =
{ {
@ -549,9 +550,9 @@ class PostListScreen : Screen {
}, },
onReply = { onReply = {
if (uiState.isLogged) { if (uiState.isLogged) {
model.reduce(PostListMviModel.Intent.MarkAsRead(post.id)) model.reduce(
model.reduce(PostListMviModel.Intent.WillOpenDetail) PostListMviModel.Intent.WillOpenDetail(post.id),
detailOpener.openPostDetail(post) )
} }
}, },
onOpenImage = { url -> onOpenImage = { url ->

View File

@ -268,11 +268,12 @@ class PostListViewModel(
shareHelper.share(intent.url) shareHelper.share(intent.url)
} }
is PostListMviModel.Intent.MarkAsRead -> { is PostListMviModel.Intent.MarkAsRead ->
uiState.value.posts.firstOrNull { it.id == intent.id }?.also { post -> screenModelScope.launch {
markAsRead(post = post) uiState.value.posts.firstOrNull { it.id == intent.id }?.also { post ->
markAsRead(post = post)
}
} }
}
PostListMviModel.Intent.ClearRead -> clearRead() PostListMviModel.Intent.ClearRead -> clearRead()
is PostListMviModel.Intent.Hide -> { is PostListMviModel.Intent.Hide -> {
@ -298,9 +299,15 @@ class PostListViewModel(
} }
} }
PostListMviModel.Intent.WillOpenDetail -> { is PostListMviModel.Intent.WillOpenDetail -> {
val state = postPaginationManager.extractState() screenModelScope.launch {
postNavigationManager.push(state) uiState.value.posts.firstOrNull { it.id == intent.id }?.also { post ->
markAsRead(post)
val state = postPaginationManager.extractState()
postNavigationManager.push(state)
emitEffect(PostListMviModel.Effect.OpenDetail(post))
}
}
} }
} }
} }
@ -427,24 +434,22 @@ class PostListViewModel(
} }
} }
private fun markAsRead(post: PostModel) { private suspend fun markAsRead(post: PostModel) {
if (post.read) { if (post.read) {
return return
} }
val newPost = post.copy(read = true) val newPost = post.copy(read = true)
screenModelScope.launch { try {
try { val auth = identityRepository.authToken.value.orEmpty()
val auth = identityRepository.authToken.value.orEmpty() postRepository.setRead(
postRepository.setRead( read = true,
read = true, postId = post.id,
postId = post.id, auth = auth,
auth = auth, )
) handlePostUpdate(newPost)
handlePostUpdate(newPost) } catch (e: Throwable) {
} catch (e: Throwable) { e.printStackTrace()
e.printStackTrace() handlePostUpdate(post)
handlePostUpdate(post)
}
} }
} }

View File

@ -7,6 +7,7 @@ import com.livefast.eattrash.raccoonforlemmy.core.appearance.data.VoteFormat
import com.livefast.eattrash.raccoonforlemmy.core.architecture.MviModel import com.livefast.eattrash.raccoonforlemmy.core.architecture.MviModel
import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.ActionOnSwipe import com.livefast.eattrash.raccoonforlemmy.core.persistence.data.ActionOnSwipe
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.data.PersonMentionModel import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.data.PersonMentionModel
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.data.PostModel
@Stable @Stable
interface InboxRepliesMviModel : interface InboxRepliesMviModel :
@ -31,6 +32,12 @@ interface InboxRepliesMviModel :
data class DownVoteComment( data class DownVoteComment(
val id: Long, val id: Long,
) : Intent ) : Intent
data class WillOpenDetail(
val id: Long,
val post: PostModel,
val commentId: Long,
) : Intent
} }
data class UiState( data class UiState(
@ -58,5 +65,10 @@ interface InboxRepliesMviModel :
) : Effect ) : Effect
data object BackToTop : Effect data object BackToTop : Effect
data class OpenDetail(
val post: PostModel,
val commentId: Long,
) : Effect
} }
} }

View File

@ -99,6 +99,12 @@ class InboxRepliesScreen : Tab {
lazyListState.scrollToItem(0) lazyListState.scrollToItem(0)
} }
} }
is InboxRepliesMviModel.Effect.OpenDetail ->
detailOpener.openPostDetail(
post = effect.post,
highlightCommentId = effect.commentId,
)
} }
}.launchIn(this) }.launchIn(this)
} }
@ -138,7 +144,7 @@ class InboxRepliesScreen : Tab {
} }
items( items(
items = uiState.replies, items = uiState.replies,
key = { it.id.toString() + it.read + uiState.unreadOnly }, key = { it.id.toString() + uiState.unreadOnly },
) { reply -> ) { reply ->
@Composable @Composable
@ -234,17 +240,12 @@ class InboxRepliesScreen : Tab {
downVoteEnabled = uiState.downVoteEnabled, downVoteEnabled = uiState.downVoteEnabled,
previewMaxLines = uiState.previewMaxLines, previewMaxLines = uiState.previewMaxLines,
onClick = { post -> onClick = { post ->
if (!reply.read) { model.reduce(
model.reduce( InboxRepliesMviModel.Intent.WillOpenDetail(
InboxRepliesMviModel.Intent.MarkAsRead( id = reply.id,
read = true, post = post,
id = reply.id, commentId = reply.comment.id,
), ),
)
}
detailOpener.openPostDetail(
post = post,
highlightCommentId = reply.comment.id,
) )
}, },
onOpenCreator = { user, instance -> onOpenCreator = { user, instance ->

View File

@ -101,10 +101,11 @@ class InboxRepliesViewModel(
emitEffect(InboxRepliesMviModel.Effect.BackToTop) emitEffect(InboxRepliesMviModel.Effect.BackToTop)
} }
is InboxRepliesMviModel.Intent.MarkAsRead -> { is InboxRepliesMviModel.Intent.MarkAsRead ->
val reply = uiState.value.replies.first { it.id == intent.id } screenModelScope.launch {
markAsRead(read = intent.read, reply = reply) val reply = uiState.value.replies.first { it.id == intent.id }
} markAsRead(read = intent.read, reply = reply)
}
InboxRepliesMviModel.Intent.HapticIndication -> hapticFeedback.vibrate() InboxRepliesMviModel.Intent.HapticIndication -> hapticFeedback.vibrate()
is InboxRepliesMviModel.Intent.DownVoteComment -> { is InboxRepliesMviModel.Intent.DownVoteComment -> {
@ -116,6 +117,24 @@ class InboxRepliesViewModel(
val reply = uiState.value.replies.first { it.id == intent.id } val reply = uiState.value.replies.first { it.id == intent.id }
toggleUpVoteComment(reply) toggleUpVoteComment(reply)
} }
is InboxRepliesMviModel.Intent.WillOpenDetail ->
screenModelScope.launch {
uiState.value.replies.firstOrNull { it.id == intent.id }?.also { reply ->
if (!reply.read) {
markAsRead(
reply = reply,
read = true,
)
}
emitEffect(
InboxRepliesMviModel.Effect.OpenDetail(
post = intent.post,
commentId = intent.commentId,
),
)
}
}
} }
} }
@ -199,33 +218,31 @@ class InboxRepliesViewModel(
} }
} }
private fun markAsRead( private suspend fun markAsRead(
read: Boolean, read: Boolean,
reply: PersonMentionModel, reply: PersonMentionModel,
) { ) {
val auth = identityRepository.authToken.value val auth = identityRepository.authToken.value
screenModelScope.launch { userRepository.setReplyRead(
userRepository.setReplyRead( read = read,
read = read, replyId = reply.id,
replyId = reply.id, auth = auth,
auth = auth, )
) val currentState = uiState.value
val currentState = uiState.value if (read && currentState.unreadOnly) {
if (read && currentState.unreadOnly) { updateState {
updateState { it.copy(
it.copy( replies =
replies = currentState.replies.filter { r ->
currentState.replies.filter { r -> r.id != reply.id
r.id != reply.id },
}, )
)
}
} else {
val newItem = reply.copy(read = read)
handleItemUpdate(newItem)
} }
updateUnreadItems() } else {
val newItem = reply.copy(read = read)
handleItemUpdate(newItem)
} }
updateUnreadItems()
} }
private fun toggleUpVoteComment(mention: PersonMentionModel) { private fun toggleUpVoteComment(mention: PersonMentionModel) {

View File

@ -66,7 +66,10 @@ interface UserDetailMviModel :
data object BlockInstance : Intent data object BlockInstance : Intent
data object WillOpenDetail : Intent data class WillOpenDetail(
val postId: Long,
val commentId: Long? = null,
) : Intent
data class UpdateTags( data class UpdateTags(
val ids: List<Long>, val ids: List<Long>,
@ -122,5 +125,10 @@ interface UserDetailMviModel :
) : Effect ) : Effect
data object BackToTop : Effect data object BackToTop : Effect
data class OpenDetail(
val postId: Long,
val commentId: Long? = null,
) : Effect
} }
} }

View File

@ -197,6 +197,12 @@ class UserDetailScreen(
} }
} }
} }
is UserDetailMviModel.Effect.OpenDetail ->
detailOpener.openPostDetail(
post = PostModel(id = effect.postId),
highlightCommentId = effect.commentId,
)
} }
}.launchIn(this) }.launchIn(this)
} }
@ -546,7 +552,7 @@ class UserDetailScreen(
items( items(
items = uiState.posts, items = uiState.posts,
key = { key = {
it.id.toString() + (it.updateDate ?: it.publishDate) + it.read it.id.toString() + (it.updateDate ?: it.publishDate)
}, },
) { post -> ) { post ->
@ -665,8 +671,11 @@ class UserDetailScreen(
actionButtonsActive = uiState.isLogged, actionButtonsActive = uiState.isLogged,
downVoteEnabled = uiState.downVoteEnabled, downVoteEnabled = uiState.downVoteEnabled,
onClick = { onClick = {
model.reduce(UserDetailMviModel.Intent.WillOpenDetail) model.reduce(
detailOpener.openPostDetail(post) UserDetailMviModel.Intent.WillOpenDetail(
postId = post.id,
),
)
}, },
onDoubleClick = onDoubleClick =
{ {
@ -709,8 +718,11 @@ class UserDetailScreen(
}, },
onReply = onReply =
{ {
model.reduce(UserDetailMviModel.Intent.WillOpenDetail) model.reduce(
detailOpener.openPostDetail(post) UserDetailMviModel.Intent.WillOpenDetail(
postId = post.id,
),
)
}.takeIf { uiState.isLogged && !isOnOtherInstance }, }.takeIf { uiState.isLogged && !isOnOtherInstance },
onOpenImage = { url -> onOpenImage = { url ->
navigationCoordinator.pushScreen( navigationCoordinator.pushScreen(
@ -969,9 +981,11 @@ class UserDetailScreen(
downVoteEnabled = uiState.downVoteEnabled, downVoteEnabled = uiState.downVoteEnabled,
actionButtonsActive = uiState.isLogged, actionButtonsActive = uiState.isLogged,
onClick = { onClick = {
detailOpener.openPostDetail( model.reduce(
post = PostModel(id = comment.postId), UserDetailMviModel.Intent.WillOpenDetail(
highlightCommentId = comment.id, postId = comment.postId,
commentId = comment.id,
),
) )
}, },
onImageClick = { url -> onImageClick = { url ->

View File

@ -308,10 +308,19 @@ class UserDetailViewModel(
UserDetailMviModel.Intent.Block -> blockUser() UserDetailMviModel.Intent.Block -> blockUser()
UserDetailMviModel.Intent.BlockInstance -> blockInstance() UserDetailMviModel.Intent.BlockInstance -> blockInstance()
UserDetailMviModel.Intent.WillOpenDetail -> { is UserDetailMviModel.Intent.WillOpenDetail ->
val state = postPaginationManager.extractState() screenModelScope.launch {
postNavigationManager.push(state) if (intent.commentId == null) {
} val state = postPaginationManager.extractState()
postNavigationManager.push(state)
}
emitEffect(
UserDetailMviModel.Effect.OpenDetail(
postId = intent.postId,
commentId = intent.commentId,
),
)
}
is UserDetailMviModel.Intent.AddUserTag -> is UserDetailMviModel.Intent.AddUserTag ->
addUserTag(name = intent.name, color = intent.color) addUserTag(name = intent.name, color = intent.color)