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 = uiState.messages,
key = {
it.id.toString() + (it.updateDate ?: it.publishDate) + it.read
it.id.toString() + (it.updateDate ?: it.publishDate)
},
) { message ->
val isMyMessage = message.creator?.id == uiState.currentUserId

View File

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

View File

@ -102,7 +102,9 @@ interface CommunityDetailMviModel :
val value: Boolean,
) : Intent
data object WillOpenDetail : Intent
data class WillOpenDetail(
val id: Long,
) : Intent
data object UnhideCommunity : Intent
@ -175,5 +177,9 @@ interface CommunityDetailMviModel :
) : 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()
is CommunityDetailMviModel.Effect.OpenDetail ->
detailOpener.openPostDetail(effect.post)
}
}.launchIn(this)
}
@ -822,7 +824,7 @@ class CommunityDetailScreen(
items(
items = uiState.posts,
key = {
it.id.toString() + (it.updateDate ?: it.publishDate) + it.read
it.id.toString() + (it.updateDate ?: it.publishDate)
},
) { post ->
LaunchedEffect(post.id) {
@ -977,11 +979,10 @@ class CommunityDetailScreen(
meTagColor = uiState.meTagColor,
onClick = {
model.reduce(
CommunityDetailMviModel.Intent.MarkAsRead(
CommunityDetailMviModel.Intent.WillOpenDetail(
post.id,
),
)
model.reduce(CommunityDetailMviModel.Intent.WillOpenDetail)
detailOpener.openPostDetail(
post = post,
otherInstance = otherInstanceName,
@ -1027,11 +1028,10 @@ class CommunityDetailScreen(
onReply =
{
model.reduce(
CommunityDetailMviModel.Intent.MarkAsRead(
CommunityDetailMviModel.Intent.WillOpenDetail(
post.id,
),
)
model.reduce(CommunityDetailMviModel.Intent.WillOpenDetail)
detailOpener.openPostDetail(post)
}.takeIf { uiState.isLogged && !isOnOtherInstance },
onOpenImage = { url ->

View File

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

View File

@ -100,7 +100,10 @@ interface FilteredContentsMviModel :
val commentId: Long,
) : Intent
data object WillOpenDetail : Intent
data class WillOpenDetail(
val postId: Long,
val commentId: Long? = null,
) : Intent
}
data class State(
@ -136,5 +139,10 @@ interface FilteredContentsMviModel :
sealed interface 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
}
}
is FilteredContentsMviModel.Effect.OpenDetail ->
detailOpener.openPostDetail(
post = PostModel(id = effect.postId),
highlightCommentId = effect.commentId,
isMod = true,
)
}
}.launchIn(this)
}
@ -369,7 +376,7 @@ class FilteredContentsScreen(
items(
items = uiState.posts,
key = {
it.id.toString() + (it.updateDate ?: it.publishDate) + it.read
it.id.toString() + (it.updateDate ?: it.publishDate)
},
) { post ->
@ -482,8 +489,11 @@ class FilteredContentsScreen(
botTagColor = uiState.botTagColor,
meTagColor = uiState.meTagColor,
onClick = {
model.reduce(FilteredContentsMviModel.Intent.WillOpenDetail)
detailOpener.openPostDetail(post)
model.reduce(
FilteredContentsMviModel.Intent.WillOpenDetail(
postId = post.id,
),
)
},
onOpenCommunity = { community, instance ->
detailOpener.openCommunityDetail(
@ -512,8 +522,11 @@ class FilteredContentsScreen(
)
},
onReply = {
model.reduce(FilteredContentsMviModel.Intent.WillOpenDetail)
detailOpener.openPostDetail(post)
model.reduce(
FilteredContentsMviModel.Intent.WillOpenDetail(
postId = post.id,
),
)
},
onOpenImage = { url ->
navigationCoordinator.pushScreen(
@ -820,11 +833,11 @@ class FilteredContentsScreen(
detailOpener.openUserDetail(user, instance)
},
onOpen = {
model.reduce(FilteredContentsMviModel.Intent.WillOpenDetail)
detailOpener.openPostDetail(
post = PostModel(id = comment.postId),
highlightCommentId = comment.id,
isMod = true,
model.reduce(
FilteredContentsMviModel.Intent.WillOpenDetail(
postId = comment.postId,
commentId = comment.id,
),
)
},
onUpVote = {

View File

@ -212,10 +212,19 @@ class FilteredContentsViewModel(
distinguish(comment)
}
FilteredContentsMviModel.Intent.WillOpenDetail -> {
val state = postPaginationManager.extractState()
postNavigationManager.push(state)
}
is FilteredContentsMviModel.Intent.WillOpenDetail ->
screenModelScope.launch {
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.persistence.data.ActionOnSwipe
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.data.PersonMentionModel
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.data.PostModel
@Stable
interface InboxMentionsMviModel :
@ -31,6 +32,12 @@ interface InboxMentionsMviModel :
data class DownVoteComment(
val id: Long,
) : Intent
data class WillOpenDetail(
val id: Long,
val post: PostModel,
val commentId: Long,
) : Intent
}
data class UiState(
@ -58,5 +65,10 @@ interface InboxMentionsMviModel :
) : 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)
}
}
is InboxMentionsMviModel.Effect.OpenDetail ->
detailOpener.openPostDetail(
post = effect.post,
highlightCommentId = effect.commentId,
)
}
}.launchIn(this)
}
@ -138,7 +144,7 @@ class InboxMentionsScreen : Tab {
}
items(
items = uiState.mentions,
key = { it.id.toString() + it.read + uiState.unreadOnly },
key = { it.id.toString() + uiState.unreadOnly },
) { mention ->
@Composable
fun List<ActionOnSwipe>.toSwipeActions(): List<SwipeAction> =
@ -233,18 +239,12 @@ class InboxMentionsScreen : Tab {
downVoteEnabled = uiState.downVoteEnabled,
previewMaxLines = uiState.previewMaxLines,
onClick = { post ->
if (!mention.read) {
model.reduce(
InboxMentionsMviModel.Intent.MarkAsRead(
read = true,
id = mention.id,
),
)
}
detailOpener.openPostDetail(
post = post,
highlightCommentId = mention.comment.id,
otherInstance = "",
model.reduce(
InboxMentionsMviModel.Intent.WillOpenDetail(
id = mention.id,
post = post,
commentId = mention.comment.id,
),
)
},
onOpenCreator = { user, instance ->

View File

@ -101,13 +101,14 @@ class InboxMentionsViewModel(
emitEffect(InboxMentionsMviModel.Effect.BackToTop)
}
is InboxMentionsMviModel.Intent.MarkAsRead -> {
val mention = uiState.value.mentions.first { it.id == intent.id }
markAsRead(
read = intent.read,
mention = mention,
)
}
is InboxMentionsMviModel.Intent.MarkAsRead ->
screenModelScope.launch {
val mention = uiState.value.mentions.first { it.id == intent.id }
markAsRead(
read = intent.read,
mention = mention,
)
}
InboxMentionsMviModel.Intent.HapticIndication -> hapticFeedback.vibrate()
is InboxMentionsMviModel.Intent.DownVoteComment -> {
@ -119,6 +120,24 @@ class InboxMentionsViewModel(
val mention = uiState.value.mentions.first { it.id == intent.id }
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,
mention: PersonMentionModel,
) {
val auth = identityRepository.authToken.value
screenModelScope.launch {
userRepository.setMentionRead(
read = read,
mentionId = mention.id,
auth = auth,
)
val currentState = uiState.value
if (read && currentState.unreadOnly) {
updateState {
it.copy(
mentions =
currentState.mentions.filter { m ->
m.id != mention.id
},
)
}
} else {
val newMention = mention.copy(read = read)
handleItemUpdate(newMention)
userRepository.setMentionRead(
read = read,
mentionId = mention.id,
auth = auth,
)
val currentState = uiState.value
if (read && currentState.unreadOnly) {
updateState {
it.copy(
mentions =
currentState.mentions.filter { m ->
m.id != mention.id
},
)
}
updateUnreadItems()
} else {
val newMention = mention.copy(read = read)
handleItemUpdate(newMention)
}
updateUnreadItems()
}
private fun toggleUpVoteComment(mention: PersonMentionModel) {

View File

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

View File

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

View File

@ -191,19 +191,26 @@ class MultiCommunityViewModel(
MultiCommunityMviModel.Intent.ClearRead -> clearRead()
is MultiCommunityMviModel.Intent.MarkAsRead ->
markAsRead(
post = uiState.value.posts.first { it.id == intent.id },
)
screenModelScope.launch {
markAsRead(
post = uiState.value.posts.first { it.id == intent.id },
)
}
is MultiCommunityMviModel.Intent.Hide ->
hide(
post = uiState.value.posts.first { it.id == intent.id },
)
MultiCommunityMviModel.Intent.WillOpenDetail -> {
val state = postPaginationManager.extractState()
postNavigationManager.push(state)
}
is MultiCommunityMviModel.Intent.WillOpenDetail ->
screenModelScope.launch {
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) {
return
}
val newPost = post.copy(read = true)
screenModelScope.launch {
try {
val auth = identityRepository.authToken.value.orEmpty()
postRepository.setRead(
read = true,
postId = post.id,
auth = auth,
)
handlePostUpdate(newPost)
} catch (e: Throwable) {
e.printStackTrace()
handlePostUpdate(post)
}
try {
val auth = identityRepository.authToken.value.orEmpty()
postRepository.setRead(
read = true,
postId = post.id,
auth = auth,
)
handlePostUpdate(newPost)
} catch (e: Throwable) {
e.printStackTrace()
handlePostUpdate(post)
}
}

View File

@ -63,7 +63,10 @@ interface ProfileLoggedMviModel :
val feedback: Boolean = false,
) : Intent
data object WillOpenDetail : Intent
data class WillOpenDetail(
val postId: Long,
val commentId: Long? = null,
) : Intent
data class RestorePost(
val id: Long,
@ -96,5 +99,10 @@ interface ProfileLoggedMviModel :
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)
}.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) {
ProgressHud()
@ -277,7 +289,7 @@ object ProfileLoggedScreen : Tab {
items(
items = uiState.posts,
key = {
it.id.toString() + (it.updateDate ?: it.publishDate) + it.read
it.id.toString() + (it.updateDate ?: it.publishDate)
},
) { post ->
PostCard(
@ -295,12 +307,18 @@ object ProfileLoggedScreen : Tab {
blurNsfw = false,
downVoteEnabled = uiState.downVoteEnabled,
onClick = {
model.reduce(ProfileLoggedMviModel.Intent.WillOpenDetail)
detailOpener.openPostDetail(post)
model.reduce(
ProfileLoggedMviModel.Intent.WillOpenDetail(
postId = post.id,
),
)
},
onReply = {
model.reduce(ProfileLoggedMviModel.Intent.WillOpenDetail)
detailOpener.openPostDetail(post)
model.reduce(
ProfileLoggedMviModel.Intent.WillOpenDetail(
postId = post.id,
),
)
},
onOpenCommunity = { community, instance ->
detailOpener.openCommunityDetail(community, instance)
@ -494,9 +512,11 @@ object ProfileLoggedScreen : Tab {
detailOpener.openCommunityDetail(community, instance)
},
onClick = {
detailOpener.openPostDetail(
post = PostModel(id = comment.postId),
highlightCommentId = comment.id,
model.reduce(
ProfileLoggedMviModel.Intent.WillOpenDetail(
postId = comment.postId,
commentId = comment.id,
),
)
},
onReply = {

View File

@ -217,9 +217,18 @@ class ProfileLoggedViewModel(
}
}
ProfileLoggedMviModel.Intent.WillOpenDetail -> {
val state = postPaginationManager.extractState()
postNavigationManager.push(state)
is ProfileLoggedMviModel.Intent.WillOpenDetail ->
screenModelScope.launch {
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 -> {

View File

@ -66,7 +66,9 @@ interface PostListMviModel :
data object PauseZombieMode : Intent
data object WillOpenDetail : Intent
data class WillOpenDetail(
val id: Long,
) : Intent
}
data class UiState(
@ -107,5 +109,9 @@ interface PostListMviModel :
data class ZombieModeTick(
val index: Int,
) : 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)
}
@ -361,7 +364,7 @@ class PostListScreen : Screen {
key = {
it.id.toString() + (
it.updateDate ?: it.publishDate
) + it.read + uiState.isLogged
) + uiState.isLogged
},
) { post ->
LaunchedEffect(post.id) {
@ -504,9 +507,7 @@ class PostListScreen : Screen {
botTagColor = uiState.botTagColor,
meTagColor = uiState.meTagColor,
onClick = {
model.reduce(PostListMviModel.Intent.MarkAsRead(post.id))
model.reduce(PostListMviModel.Intent.WillOpenDetail)
detailOpener.openPostDetail(post)
model.reduce(PostListMviModel.Intent.WillOpenDetail(post.id))
},
onDoubleClick =
{
@ -549,9 +550,9 @@ class PostListScreen : Screen {
},
onReply = {
if (uiState.isLogged) {
model.reduce(PostListMviModel.Intent.MarkAsRead(post.id))
model.reduce(PostListMviModel.Intent.WillOpenDetail)
detailOpener.openPostDetail(post)
model.reduce(
PostListMviModel.Intent.WillOpenDetail(post.id),
)
}
},
onOpenImage = { url ->

View File

@ -268,11 +268,12 @@ class PostListViewModel(
shareHelper.share(intent.url)
}
is PostListMviModel.Intent.MarkAsRead -> {
uiState.value.posts.firstOrNull { it.id == intent.id }?.also { post ->
markAsRead(post = post)
is PostListMviModel.Intent.MarkAsRead ->
screenModelScope.launch {
uiState.value.posts.firstOrNull { it.id == intent.id }?.also { post ->
markAsRead(post = post)
}
}
}
PostListMviModel.Intent.ClearRead -> clearRead()
is PostListMviModel.Intent.Hide -> {
@ -298,9 +299,15 @@ class PostListViewModel(
}
}
PostListMviModel.Intent.WillOpenDetail -> {
val state = postPaginationManager.extractState()
postNavigationManager.push(state)
is PostListMviModel.Intent.WillOpenDetail -> {
screenModelScope.launch {
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) {
return
}
val newPost = post.copy(read = true)
screenModelScope.launch {
try {
val auth = identityRepository.authToken.value.orEmpty()
postRepository.setRead(
read = true,
postId = post.id,
auth = auth,
)
handlePostUpdate(newPost)
} catch (e: Throwable) {
e.printStackTrace()
handlePostUpdate(post)
}
try {
val auth = identityRepository.authToken.value.orEmpty()
postRepository.setRead(
read = true,
postId = post.id,
auth = auth,
)
handlePostUpdate(newPost)
} catch (e: Throwable) {
e.printStackTrace()
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.persistence.data.ActionOnSwipe
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.data.PersonMentionModel
import com.livefast.eattrash.raccoonforlemmy.domain.lemmy.data.PostModel
@Stable
interface InboxRepliesMviModel :
@ -31,6 +32,12 @@ interface InboxRepliesMviModel :
data class DownVoteComment(
val id: Long,
) : Intent
data class WillOpenDetail(
val id: Long,
val post: PostModel,
val commentId: Long,
) : Intent
}
data class UiState(
@ -58,5 +65,10 @@ interface InboxRepliesMviModel :
) : 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)
}
}
is InboxRepliesMviModel.Effect.OpenDetail ->
detailOpener.openPostDetail(
post = effect.post,
highlightCommentId = effect.commentId,
)
}
}.launchIn(this)
}
@ -138,7 +144,7 @@ class InboxRepliesScreen : Tab {
}
items(
items = uiState.replies,
key = { it.id.toString() + it.read + uiState.unreadOnly },
key = { it.id.toString() + uiState.unreadOnly },
) { reply ->
@Composable
@ -234,17 +240,12 @@ class InboxRepliesScreen : Tab {
downVoteEnabled = uiState.downVoteEnabled,
previewMaxLines = uiState.previewMaxLines,
onClick = { post ->
if (!reply.read) {
model.reduce(
InboxRepliesMviModel.Intent.MarkAsRead(
read = true,
id = reply.id,
),
)
}
detailOpener.openPostDetail(
post = post,
highlightCommentId = reply.comment.id,
model.reduce(
InboxRepliesMviModel.Intent.WillOpenDetail(
id = reply.id,
post = post,
commentId = reply.comment.id,
),
)
},
onOpenCreator = { user, instance ->

View File

@ -101,10 +101,11 @@ class InboxRepliesViewModel(
emitEffect(InboxRepliesMviModel.Effect.BackToTop)
}
is InboxRepliesMviModel.Intent.MarkAsRead -> {
val reply = uiState.value.replies.first { it.id == intent.id }
markAsRead(read = intent.read, reply = reply)
}
is InboxRepliesMviModel.Intent.MarkAsRead ->
screenModelScope.launch {
val reply = uiState.value.replies.first { it.id == intent.id }
markAsRead(read = intent.read, reply = reply)
}
InboxRepliesMviModel.Intent.HapticIndication -> hapticFeedback.vibrate()
is InboxRepliesMviModel.Intent.DownVoteComment -> {
@ -116,6 +117,24 @@ class InboxRepliesViewModel(
val reply = uiState.value.replies.first { it.id == intent.id }
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,
reply: PersonMentionModel,
) {
val auth = identityRepository.authToken.value
screenModelScope.launch {
userRepository.setReplyRead(
read = read,
replyId = reply.id,
auth = auth,
)
val currentState = uiState.value
if (read && currentState.unreadOnly) {
updateState {
it.copy(
replies =
currentState.replies.filter { r ->
r.id != reply.id
},
)
}
} else {
val newItem = reply.copy(read = read)
handleItemUpdate(newItem)
userRepository.setReplyRead(
read = read,
replyId = reply.id,
auth = auth,
)
val currentState = uiState.value
if (read && currentState.unreadOnly) {
updateState {
it.copy(
replies =
currentState.replies.filter { r ->
r.id != reply.id
},
)
}
updateUnreadItems()
} else {
val newItem = reply.copy(read = read)
handleItemUpdate(newItem)
}
updateUnreadItems()
}
private fun toggleUpVoteComment(mention: PersonMentionModel) {

View File

@ -66,7 +66,10 @@ interface UserDetailMviModel :
data object BlockInstance : Intent
data object WillOpenDetail : Intent
data class WillOpenDetail(
val postId: Long,
val commentId: Long? = null,
) : Intent
data class UpdateTags(
val ids: List<Long>,
@ -122,5 +125,10 @@ interface UserDetailMviModel :
) : 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)
}
@ -546,7 +552,7 @@ class UserDetailScreen(
items(
items = uiState.posts,
key = {
it.id.toString() + (it.updateDate ?: it.publishDate) + it.read
it.id.toString() + (it.updateDate ?: it.publishDate)
},
) { post ->
@ -665,8 +671,11 @@ class UserDetailScreen(
actionButtonsActive = uiState.isLogged,
downVoteEnabled = uiState.downVoteEnabled,
onClick = {
model.reduce(UserDetailMviModel.Intent.WillOpenDetail)
detailOpener.openPostDetail(post)
model.reduce(
UserDetailMviModel.Intent.WillOpenDetail(
postId = post.id,
),
)
},
onDoubleClick =
{
@ -709,8 +718,11 @@ class UserDetailScreen(
},
onReply =
{
model.reduce(UserDetailMviModel.Intent.WillOpenDetail)
detailOpener.openPostDetail(post)
model.reduce(
UserDetailMviModel.Intent.WillOpenDetail(
postId = post.id,
),
)
}.takeIf { uiState.isLogged && !isOnOtherInstance },
onOpenImage = { url ->
navigationCoordinator.pushScreen(
@ -969,9 +981,11 @@ class UserDetailScreen(
downVoteEnabled = uiState.downVoteEnabled,
actionButtonsActive = uiState.isLogged,
onClick = {
detailOpener.openPostDetail(
post = PostModel(id = comment.postId),
highlightCommentId = comment.id,
model.reduce(
UserDetailMviModel.Intent.WillOpenDetail(
postId = comment.postId,
commentId = comment.id,
),
)
},
onImageClick = { url ->

View File

@ -308,10 +308,19 @@ class UserDetailViewModel(
UserDetailMviModel.Intent.Block -> blockUser()
UserDetailMviModel.Intent.BlockInstance -> blockInstance()
UserDetailMviModel.Intent.WillOpenDetail -> {
val state = postPaginationManager.extractState()
postNavigationManager.push(state)
}
is UserDetailMviModel.Intent.WillOpenDetail ->
screenModelScope.launch {
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 ->
addUserTag(name = intent.name, color = intent.color)