From 219d1734b373744978bfe7edb739e5457164c585 Mon Sep 17 00:00:00 2001 From: Evgeny Date: Tue, 15 Jul 2025 10:21:37 +0100 Subject: [PATCH] ui: fix drafts for prepared chats (#6070) * ios: fix placeholder for prepared chats * android, desktop: fix drafts for prepared chats * take effects outside of SndMsgView_ * revert KeyChangeEffect --- .../ComposeMessage/NativeTextEditor.swift | 3 +- .../simplex/common/views/chat/ComposeView.kt | 149 +++++++++--------- .../common/views/chatlist/ChatPreviewView.kt | 120 +++++++------- 3 files changed, 144 insertions(+), 128 deletions(-) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift index 0dd26c630d..31d4ceecc6 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift @@ -63,9 +63,10 @@ struct NativeTextEditor: UIViewRepresentable { field.textAlignment = alignment(text) field.updateFont() field.updateHeight(updateBindingNow: false) + field.placeholder = text.isEmpty ? placeholder : "" } if field.placeholder != placeholder { - field.placeholder = placeholder + field.placeholder = text.isEmpty ? placeholder : "" } if field.selectedRange != selectedRange { field.selectedRange = selectedRange diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index f4ae0d7607..7fc1e9ae56 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -1101,86 +1101,87 @@ fun ComposeView( } } + val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Voice) } + LaunchedEffect(allowedVoiceByPrefs) { + if (!allowedVoiceByPrefs && composeState.value.preview is ComposePreview.VoicePreview) { + // Voice was disabled right when this user records it, just cancel it + cancelVoice() + } + } + val needToAllowVoiceToContact = remember(chat.chatInfo) { + chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.voice) { + ((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) && + contactPreference.allow == FeatureAllowed.YES + } + } + LaunchedEffect(Unit) { + snapshotFlow { recState.value } + .distinctUntilChanged() + .collect { + when (it) { + is RecordingState.Started -> onAudioAdded(it.filePath, it.progressMs, false) + is RecordingState.Finished -> if (it.durationMs > 300) { + onAudioAdded(it.filePath, it.durationMs, true) + } else { + cancelVoice() + } + is RecordingState.NotStarted -> {} + } + } + } + + LaunchedEffect(rememberUpdatedState(chat.chatInfo.sendMsgEnabled).value) { + if (!chat.chatInfo.sendMsgEnabled) { + clearCurrentDraft() + clearState() + } + } + + KeyChangeEffect(chatModel.chatId.value) { prevChatId -> + val cs = composeState.value + if (cs.liveMessage != null && (cs.message.text.isNotEmpty() || cs.liveMessage.sent)) { + sendMessage(null) + resetLinkPreview() + clearPrevDraft(prevChatId) + deleteUnusedFiles() + } else if (cs.inProgress) { + clearPrevDraft(prevChatId) + } else if (!cs.empty) { + if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) { + composeState.value = cs.copy(preview = cs.preview.copy(finished = true)) + } + if (saveLastDraft) { + chatModel.draft.value = composeState.value + chatModel.draftChatId.value = prevChatId + } + composeState.value = ComposeState(useLinkPreviews = useLinkPreviews) + } else if (chatModel.draftChatId.value == chatModel.chatId.value && chatModel.draft.value != null) { + composeState.value = chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews) + } else { + clearPrevDraft(prevChatId) + deleteUnusedFiles() + } + chatModel.removeLiveDummy() + CIFile.cachedRemoteFileRequests.clear() + } + if (appPlatform.isDesktop) { + // Don't enable this on Android, it breaks it, This method only works on desktop. For Android there is a `KeyChangeEffect(chatModel.chatId.value)` + DisposableEffect(Unit) { + onDispose { + if (chatModel.sharedContent.value is SharedContent.Forward && saveLastDraft && !composeState.value.empty) { + chatModel.draft.value = composeState.value + chatModel.draftChatId.value = chat.id + } + } + } + } + @Composable fun SendMsgView_( disableSendButton: Boolean, placeholder: String? = null, sendToConnect: (() -> Unit)? = null ) { - val allowedVoiceByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Voice) } - LaunchedEffect(allowedVoiceByPrefs) { - if (!allowedVoiceByPrefs && composeState.value.preview is ComposePreview.VoicePreview) { - // Voice was disabled right when this user records it, just cancel it - cancelVoice() - } - } - val needToAllowVoiceToContact = remember(chat.chatInfo) { - chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.voice) { - ((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) && - contactPreference.allow == FeatureAllowed.YES - } - } - LaunchedEffect(Unit) { - snapshotFlow { recState.value } - .distinctUntilChanged() - .collect { - when (it) { - is RecordingState.Started -> onAudioAdded(it.filePath, it.progressMs, false) - is RecordingState.Finished -> if (it.durationMs > 300) { - onAudioAdded(it.filePath, it.durationMs, true) - } else { - cancelVoice() - } - is RecordingState.NotStarted -> {} - } - } - } - - LaunchedEffect(rememberUpdatedState(chat.chatInfo.sendMsgEnabled).value) { - if (!chat.chatInfo.sendMsgEnabled) { - clearCurrentDraft() - clearState() - } - } - - KeyChangeEffect(chatModel.chatId.value) { prevChatId -> - val cs = composeState.value - if (cs.liveMessage != null && (cs.message.text.isNotEmpty() || cs.liveMessage.sent)) { - sendMessage(null) - resetLinkPreview() - clearPrevDraft(prevChatId) - deleteUnusedFiles() - } else if (cs.inProgress) { - clearPrevDraft(prevChatId) - } else if (!cs.empty) { - if (cs.preview is ComposePreview.VoicePreview && !cs.preview.finished) { - composeState.value = cs.copy(preview = cs.preview.copy(finished = true)) - } - if (saveLastDraft) { - chatModel.draft.value = composeState.value - chatModel.draftChatId.value = prevChatId - } - composeState.value = ComposeState(useLinkPreviews = useLinkPreviews) - } else if (chatModel.draftChatId.value == chatModel.chatId.value && chatModel.draft.value != null) { - composeState.value = chatModel.draft.value ?: ComposeState(useLinkPreviews = useLinkPreviews) - } else { - clearPrevDraft(prevChatId) - deleteUnusedFiles() - } - chatModel.removeLiveDummy() - CIFile.cachedRemoteFileRequests.clear() - } - if (appPlatform.isDesktop) { - // Don't enable this on Android, it breaks it, This method only works on desktop. For Android there is a `KeyChangeEffect(chatModel.chatId.value)` - DisposableEffect(Unit) { - onDispose { - if (chatModel.sharedContent.value is SharedContent.Forward && saveLastDraft && !composeState.value.empty) { - chatModel.draft.value = composeState.value - chatModel.draftChatId.value = chat.id - } - } - } - } val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) } val sendButtonColor = if (chat.chatInfo.incognito) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index dc208ae099..fdf149e065 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -212,61 +212,75 @@ fun ChatPreviewView( fun chatPreviewText() { val previewText = chatPreviewInfoText() val ci = chat.chatItems.lastOrNull() - if (ci?.content?.hasMsgContent != true && previewText != null) { + if (chatModelDraftChatId == chat.id && chatModelDraft != null) { + val sp20 = with(LocalDensity.current) { 20.sp.toDp() } + val (text: CharSequence, inlineTextContent) = remember(chatModelDraft) { chatModelDraft.message.text to messageDraft(chatModelDraft, sp20) } + val formattedText = null + MarkdownText( + text, + formattedText, + toggleSecrets = false, + linkMode = linkMode, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + fontFamily = Inter, + fontSize = 15.sp, + color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, + lineHeight = 21.sp + ), + inlineContent = inlineTextContent, + modifier = Modifier.fillMaxWidth() + ) + } else if (ci?.content?.hasMsgContent != true && previewText != null) { Text(previewText.first, color = previewText.second) - } else if (ci != null) { - if (showChatPreviews || (chatModelDraftChatId == chat.id && chatModelDraft != null)) { - val sp20 = with(LocalDensity.current) { 20.sp.toDp() } - val (text: CharSequence, inlineTextContent) = when { - chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { chatModelDraft.message.text to messageDraft(chatModelDraft, sp20) } - ci.meta.itemDeleted == null -> ci.text to null - else -> markedDeletedText(ci, chat.chatInfo) to null - } - val formattedText = when { - chatModelDraftChatId == chat.id && chatModelDraft != null -> null - ci.meta.itemDeleted == null -> ci.formattedText - else -> null - } - val prefix = when (val mc = ci.content.msgContent) { - is MsgContent.MCReport -> - buildAnnotatedString { - withStyle(SpanStyle(color = Color.Red, fontStyle = FontStyle.Italic)) { - append(if (text.isEmpty()) mc.reason.text else "${mc.reason.text}: ") - } - } - - else -> null - } - - MarkdownText( - text, - formattedText, - sender = when { - chatModelDraftChatId == chat.id && chatModelDraft != null -> null - cInfo is ChatInfo.Group && !ci.chatDir.sent && !ci.meta.showGroupAsSender -> ci.memberDisplayName - else -> null - }, - mentions = ci.mentions, - userMemberId = when { - cInfo is ChatInfo.Group -> cInfo.groupInfo.membership.memberId - else -> null - }, - toggleSecrets = false, - linkMode = linkMode, - senderBold = true, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - style = TextStyle( - fontFamily = Inter, - fontSize = 15.sp, - color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, - lineHeight = 21.sp - ), - inlineContent = inlineTextContent, - modifier = Modifier.fillMaxWidth(), - prefix = prefix - ) + } else if (ci != null && showChatPreviews) { + val (text: CharSequence, inlineTextContent) = when { + ci.meta.itemDeleted == null -> ci.text to null + else -> markedDeletedText(ci, chat.chatInfo) to null } + val formattedText = when { + ci.meta.itemDeleted == null -> ci.formattedText + else -> null + } + val prefix = when (val mc = ci.content.msgContent) { + is MsgContent.MCReport -> + buildAnnotatedString { + withStyle(SpanStyle(color = Color.Red, fontStyle = FontStyle.Italic)) { + append(if (text.isEmpty()) mc.reason.text else "${mc.reason.text}: ") + } + } + + else -> null + } + + MarkdownText( + text, + formattedText, + sender = when { + cInfo is ChatInfo.Group && !ci.chatDir.sent && !ci.meta.showGroupAsSender -> ci.memberDisplayName + else -> null + }, + mentions = ci.mentions, + userMemberId = when { + cInfo is ChatInfo.Group -> cInfo.groupInfo.membership.memberId + else -> null + }, + toggleSecrets = false, + linkMode = linkMode, + senderBold = true, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + fontFamily = Inter, + fontSize = 15.sp, + color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, + lineHeight = 21.sp + ), + inlineContent = inlineTextContent, + modifier = Modifier.fillMaxWidth(), + prefix = prefix + ) } }