Skip to content

Commit c1a3c78

Browse files
committed
Handle successful message send updates and prevent duplicate forwarding
1 parent a5c2c1e commit c1a3c78

9 files changed

Lines changed: 135 additions & 35 deletions

File tree

data/src/main/java/org/monogram/data/datasource/cache/ChatLocalDataSource.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface ChatLocalDataSource {
2424
suspend fun getMessagesByIds(chatId: Long, messageIds: List<Long>): List<MessageEntity>
2525
suspend fun insertMessage(message: MessageEntity)
2626
suspend fun insertMessages(messages: List<MessageEntity>)
27+
suspend fun replaceMessageId(chatId: Long, oldMessageId: Long, message: MessageEntity)
2728
suspend fun markAsRead(chatId: Long, upToMessageId: Long)
2829
suspend fun updateMessageContent(
2930
chatId: Long,

data/src/main/java/org/monogram/data/datasource/cache/InMemoryChatLocalDataSource.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,15 @@ class InMemoryChatLocalDataSource : ChatLocalDataSource {
110110
messages.forEach { insertMessage(it) }
111111
}
112112

113+
override suspend fun replaceMessageId(
114+
chatId: Long,
115+
oldMessageId: Long,
116+
message: MessageEntity
117+
) {
118+
messages.getOrPut(chatId) { MutableStateFlow(emptyMap()) }
119+
.update { it - oldMessageId + (message.id to message) }
120+
}
121+
113122
override suspend fun markAsRead(chatId: Long, upToMessageId: Long) {
114123
messages[chatId]?.update { current ->
115124
current.mapValues { (_, msg) ->

data/src/main/java/org/monogram/data/datasource/cache/RoomChatLocalDataSource.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ class RoomChatLocalDataSource(
6060

6161
override suspend fun insertMessages(messages: List<MessageEntity>) = messageDao.insertMessages(messages)
6262

63+
override suspend fun replaceMessageId(
64+
chatId: Long,
65+
oldMessageId: Long,
66+
message: MessageEntity
67+
) {
68+
database.withTransaction {
69+
messageDao.deleteMessage(chatId, oldMessageId)
70+
messageDao.insertMessage(message)
71+
}
72+
}
73+
6374
override suspend fun markAsRead(chatId: Long, upToMessageId: Long) = messageDao.markAsRead(chatId, upToMessageId)
6475

6576
override suspend fun updateMessageContent(

data/src/main/java/org/monogram/data/repository/MessageRepositoryImpl.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,15 @@ class MessageRepositoryImpl(
186186
chatLocalDataSource.insertMessage(entity)
187187
}
188188

189+
is TdApi.UpdateMessageSendSucceeded -> {
190+
val entity = messageMapper.mapToEntity(update.message, ::resolveSenderName)
191+
chatLocalDataSource.replaceMessageId(
192+
chatId = update.message.chatId,
193+
oldMessageId = update.oldMessageId,
194+
message = entity
195+
)
196+
}
197+
189198
is TdApi.UpdateMessageContent -> {
190199
val extracted = messageMapper.extractCachedContent(update.newContent)
191200

data/src/test/java/org/monogram/data/datasource/cache/InMemoryChatLocalDataSourceTest.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import kotlinx.coroutines.runBlocking
44
import org.junit.Assert.assertEquals
55
import org.junit.Test
66
import org.monogram.data.db.model.ChatEntity
7+
import org.monogram.data.db.model.MessageEntity
78

89
class InMemoryChatLocalDataSourceTest {
910
@Test
@@ -23,6 +24,21 @@ class InMemoryChatLocalDataSourceTest {
2324
assertEquals(listOf(3L, 4L), result.map { it.id })
2425
}
2526

27+
@Test
28+
fun `replaceMessageId removes pending id and keeps final message`() = runBlocking {
29+
val source = InMemoryChatLocalDataSource()
30+
val pending = message(id = -10L, chatId = 1L, content = "pending")
31+
val sent = message(id = 20L, chatId = 1L, content = "sent")
32+
33+
source.insertMessage(pending)
34+
source.replaceMessageId(chatId = 1L, oldMessageId = -10L, message = sent)
35+
36+
val result = source.getLatestMessages(chatId = 1L, limit = 10)
37+
38+
assertEquals(listOf(20L), result.map { it.id })
39+
assertEquals("sent", result.single().content)
40+
}
41+
2642
private fun chat(id: Long, order: Long, isPinned: Boolean): ChatEntity =
2743
ChatEntity(
2844
id = id,
@@ -41,4 +57,16 @@ class InMemoryChatLocalDataSourceTest {
4157
memberCount = 0,
4258
onlineCount = 0
4359
)
60+
61+
private fun message(id: Long, chatId: Long, content: String): MessageEntity =
62+
MessageEntity(
63+
id = id,
64+
chatId = chatId,
65+
senderId = 1L,
66+
senderName = "sender",
67+
content = content,
68+
date = 100,
69+
isOutgoing = true,
70+
isRead = false
71+
)
4472
}

presentation/src/main/java/org/monogram/presentation/features/chats/list/ChatListComponent.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ interface ChatListComponent {
104104
val isArchivePinned: Boolean = true,
105105
val isArchiveAlwaysVisible: Boolean = false,
106106
val isForwarding: Boolean = false,
107+
val isForwardSubmitInProgress: Boolean = false,
107108
val canLoadMoreMessages: Boolean = false,
108109
val instantViewUrl: String? = null,
109110
val isProxyEnabled: Boolean = false,
@@ -128,6 +129,7 @@ interface ChatListComponent {
128129
val isArchivePinned: Boolean = true,
129130
val isArchiveAlwaysVisible: Boolean = false,
130131
val isForwarding: Boolean = false,
132+
val isForwardSubmitInProgress: Boolean = false,
131133
val instantViewUrl: String? = null,
132134
val isProxyEnabled: Boolean = false,
133135
val attachMenuBots: List<AttachMenuBotModel> = emptyList(),
@@ -177,6 +179,7 @@ interface ChatListComponent {
177179
val forwardTopicPickerChatTitle: String = "",
178180
val forwardTopics: List<TopicModel> = emptyList(),
179181
val isLoadingForwardTopics: Boolean = false,
182+
val isForwardSubmitInProgress: Boolean = false,
180183
val allPinned: Boolean = false,
181184
val allMuted: Boolean = false,
182185
val canMarkUnread: Boolean = false,

presentation/src/main/java/org/monogram/presentation/features/chats/list/ChatListContent.kt

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import androidx.compose.material3.TextFieldDefaults
8989
import androidx.compose.material3.TopAppBar
9090
import androidx.compose.material3.TopAppBarDefaults
9191
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
92+
import androidx.compose.material3.rememberModalBottomSheetState
9293
import androidx.compose.runtime.Composable
9394
import androidx.compose.runtime.DisposableEffect
9495
import androidx.compose.runtime.LaunchedEffect
@@ -820,6 +821,7 @@ fun ChatListContent(component: ChatListComponent) {
820821
removeCaption = forwardRemoveCaption,
821822
onRemoveCaptionChange = { forwardRemoveCaption = it },
822823
onRemoveTarget = component::onRemoveForwardTarget,
824+
isSending = uiState.isForwardSubmitInProgress,
823825
isCollapsed = isForwardPanelCollapsed,
824826
onCollapsedChange = { isForwardPanelCollapsed = it },
825827
onSend = {
@@ -1951,6 +1953,7 @@ private fun ForwardTopicPickerSheet(
19511953

19521954
ModalBottomSheet(
19531955
onDismissRequest = onDismiss,
1956+
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
19541957
dragHandle = { BottomSheetDefaults.DragHandle() },
19551958
containerColor = MaterialTheme.colorScheme.background,
19561959
contentColor = MaterialTheme.colorScheme.onSurface,
@@ -1976,29 +1979,35 @@ private fun ForwardTopicPickerSheet(
19761979
Spacer(Modifier.height(18.dp))
19771980

19781981
Surface(
1979-
modifier = Modifier.fillMaxWidth(),
1982+
modifier = Modifier
1983+
.fillMaxWidth()
1984+
.weight(1f, fill = false),
19801985
shape = RoundedCornerShape(24.dp),
19811986
color = MaterialTheme.colorScheme.surfaceContainerLow
19821987
) {
1983-
Column {
1984-
ForwardTopicRow(
1985-
title = stringResource(R.string.forward_main_chat),
1986-
subtitle = stringResource(R.string.forward_main_chat_subtitle),
1987-
onClick = { onTopicSelected(chatId, null) }
1988-
)
1989-
HorizontalDivider(
1990-
modifier = Modifier.padding(start = 68.dp),
1991-
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f)
1992-
)
1993-
if (isLoading) {
1994-
Text(
1995-
text = stringResource(R.string.loading_text),
1996-
modifier = Modifier.padding(horizontal = 16.dp, vertical = 18.dp),
1997-
style = MaterialTheme.typography.bodyMedium,
1998-
color = MaterialTheme.colorScheme.onSurfaceVariant
1988+
LazyColumn {
1989+
item {
1990+
ForwardTopicRow(
1991+
title = stringResource(R.string.forward_main_chat),
1992+
subtitle = stringResource(R.string.forward_main_chat_subtitle),
1993+
onClick = { onTopicSelected(chatId, null) }
1994+
)
1995+
HorizontalDivider(
1996+
modifier = Modifier.padding(start = 68.dp),
1997+
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f)
19991998
)
1999+
}
2000+
if (isLoading) {
2001+
item {
2002+
Text(
2003+
text = stringResource(R.string.loading_text),
2004+
modifier = Modifier.padding(horizontal = 16.dp, vertical = 18.dp),
2005+
style = MaterialTheme.typography.bodyMedium,
2006+
color = MaterialTheme.colorScheme.onSurfaceVariant
2007+
)
2008+
}
20002009
} else {
2001-
topics.forEachIndexed { index, topic ->
2010+
itemsIndexed(topics) { index, topic ->
20022011
ForwardTopicRow(
20032012
title = topic.name,
20042013
subtitle = topic.lastMessageText.takeIf { it.isNotBlank() },
@@ -2079,6 +2088,7 @@ private fun ForwardConfirmationPanel(
20792088
removeCaption: Boolean,
20802089
onRemoveCaptionChange: (Boolean) -> Unit,
20812090
onRemoveTarget: (ForwardTarget) -> Unit,
2091+
isSending: Boolean,
20822092
isCollapsed: Boolean,
20832093
onCollapsedChange: (Boolean) -> Unit,
20842094
onSend: () -> Unit
@@ -2139,6 +2149,7 @@ private fun ForwardConfirmationPanel(
21392149
targetsCount = targets.size,
21402150
onExpand = { onCollapsedChange(false) },
21412151
onSend = onSend,
2152+
isSending = isSending,
21422153
modifier = Modifier.forwardPanelSwipe()
21432154
)
21442155
} else {
@@ -2232,7 +2243,7 @@ private fun ForwardConfirmationPanel(
22322243
ForwardOptionRow(
22332244
title = stringResource(R.string.forward_without_author),
22342245
checked = sendCopy,
2235-
enabled = true,
2246+
enabled = !isSending,
22362247
onCheckedChange = onSendCopyChange
22372248
)
22382249
HorizontalDivider(
@@ -2242,14 +2253,15 @@ private fun ForwardConfirmationPanel(
22422253
ForwardOptionRow(
22432254
title = stringResource(R.string.forward_without_caption),
22442255
checked = removeCaption,
2245-
enabled = sendCopy,
2256+
enabled = sendCopy && !isSending,
22462257
onCheckedChange = onRemoveCaptionChange
22472258
)
22482259
}
22492260
}
22502261

22512262
Button(
22522263
onClick = onSend,
2264+
enabled = !isSending,
22532265
modifier = Modifier
22542266
.fillMaxWidth()
22552267
.height(ButtonDefaults.MediumContainerHeight),
@@ -2282,6 +2294,7 @@ private fun ForwardCollapsedPanel(
22822294
targetsCount: Int,
22832295
onExpand: () -> Unit,
22842296
onSend: () -> Unit,
2297+
isSending: Boolean,
22852298
modifier: Modifier = Modifier
22862299
) {
22872300
Row(
@@ -2304,6 +2317,7 @@ private fun ForwardCollapsedPanel(
23042317
)
23052318
Button(
23062319
onClick = onSend,
2320+
enabled = !isSending,
23072321
modifier = Modifier.height(40.dp),
23082322
shapes = org.monogram.presentation.core.ui.ExpressiveDefaults.buttonShapesFor(
23092323
ButtonDefaults.MediumContainerHeight

presentation/src/main/java/org/monogram/presentation/features/chats/list/DefaultChatListComponent.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ class DefaultChatListComponent(
109109
isArchivePinned = isArchivePinned,
110110
isArchiveAlwaysVisible = isArchiveAlwaysVisible,
111111
isForwarding = isForwarding,
112+
isForwardSubmitInProgress = isForwardSubmitInProgress,
112113
instantViewUrl = instantViewUrl,
113114
isProxyEnabled = isProxyEnabled,
114115
attachMenuBots = attachMenuBots,
@@ -153,6 +154,7 @@ class DefaultChatListComponent(
153154
forwardTopicPickerChatTitle = forwardTopicPickerChatTitle,
154155
forwardTopics = forwardTopics,
155156
isLoadingForwardTopics = isLoadingForwardTopics,
157+
isForwardSubmitInProgress = isForwardSubmitInProgress,
156158
allPinned = allPinned,
157159
allMuted = allMuted,
158160
canMarkUnread = canMarkUnread,
@@ -783,7 +785,9 @@ class DefaultChatListComponent(
783785
): ChatListStore.Label.ConfirmForward? {
784786
val fromChatId = forwardingFromChatId ?: return null
785787
val messageIds = forwardingMessageIds.takeIf { it.isNotEmpty() } ?: return null
786-
val targets = _state.value.selectedForwardTargets.takeIf { it.isNotEmpty() } ?: return null
788+
val state = _state.value
789+
if (state.isForwardSubmitInProgress) return null
790+
val targets = state.selectedForwardTargets.takeIf { it.isNotEmpty() } ?: return null
787791
val request = ForwardRequest(
788792
fromChatId = fromChatId,
789793
messageIds = messageIds,
@@ -795,6 +799,16 @@ class DefaultChatListComponent(
795799
commentEntities = commentEntities
796800
)
797801
)
802+
_state.update {
803+
it.copy(
804+
isForwardSubmitInProgress = true,
805+
selectedForwardTargets = emptyList(),
806+
forwardTopicPickerChatId = null,
807+
forwardTopicPickerChatTitle = "",
808+
forwardTopics = emptyList(),
809+
isLoadingForwardTopics = false
810+
)
811+
}
798812
return ChatListStore.Label.ConfirmForward(request)
799813
}
800814

presentation/src/main/java/org/monogram/presentation/root/DefaultRootComponent.kt

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -504,25 +504,36 @@ class DefaultRootComponent(
504504
},
505505
onSettingsClick = { navigation.bringToFront(Config.Settings) },
506506
onProxySettingsClick = { navigation.bringToFront(Config.Proxy) },
507-
onConfirmForward = { targetChatIds ->
507+
onConfirmForward = { request ->
508508
if (config.forwardingMessageIds != null) {
509509
scope.launch {
510-
messageRepository.forwardMessages(targetChatIds)
511-
val commentText = targetChatIds.options.commentText.trim()
512-
if (commentText.isNotEmpty()) {
513-
targetChatIds.targets.forEach { target ->
514-
messageRepository.sendMessage(
515-
chatId = target.chatId,
516-
text = commentText,
517-
replyToMsgId = null,
518-
entities = targetChatIds.options.commentEntities,
519-
threadId = target.forumTopicId?.toLong()
520-
)
510+
try {
511+
messageRepository.forwardMessages(request)
512+
val commentText = request.options.commentText.trim()
513+
if (commentText.isNotEmpty()) {
514+
request.targets.forEach { target ->
515+
messageRepository.sendMessage(
516+
chatId = target.chatId,
517+
text = commentText,
518+
replyToMsgId = null,
519+
entities = request.options.commentEntities,
520+
threadId = target.forumTopicId?.toLong()
521+
)
522+
}
523+
}
524+
} catch (error: Throwable) {
525+
Log.e(
526+
"DefaultRootComponent",
527+
"Failed to forward messages",
528+
error
529+
)
530+
} finally {
531+
navigation.pop()
532+
if (request.targets.size == 1) {
533+
navigateToChat(request.targets.first().chatId)
521534
}
522535
}
523536
}
524-
navigation.pop()
525-
if (targetChatIds.targets.size == 1) navigateToChat(targetChatIds.targets.first().chatId)
526537
}
527538
},
528539
isForwarding = config.forwardingMessageIds != null,

0 commit comments

Comments
 (0)