android, desktop: compose UI (#6018)

This commit is contained in:
spaced4ndy
2025-06-30 15:22:25 +00:00
committed by GitHub
parent d8636953d3
commit c80ae1c64a
7 changed files with 390 additions and 260 deletions

View File

@@ -3688,8 +3688,8 @@ sealed class CC {
is APIPrepareGroup -> "/_prepare group $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} ${json.encodeToString(groupShortLinkData)}"
is APIChangePreparedContactUser -> "/_set contact user @$contactId $newUserId"
is APIChangePreparedGroupUser -> "/_set group user #$groupId $newUserId"
is APIConnectPreparedContact -> "/_connect contact @$contactId incognito=${onOff(incognito)} ${maybeContent(msg)}"
is APIConnectPreparedGroup -> "/_connect group #$groupId incognito=${onOff(incognito)} ${maybeContent(msg)}"
is APIConnectPreparedContact -> "/_connect contact @$contactId incognito=${onOff(incognito)}${maybeContent(msg)}"
is APIConnectPreparedGroup -> "/_connect group #$groupId incognito=${onOff(incognito)}${maybeContent(msg)}"
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} ${connLink.connFullLink} ${connLink.connShortLink ?: ""}"
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
is ApiDeleteChat -> "/_delete ${chatRef(type, id, scope = null)} ${chatDeleteMode.cmdString}"

View File

@@ -101,7 +101,7 @@ fun TerminalLayout(
sendMsgEnabled = true,
userCantSendReason = null,
sendButtonEnabled = true,
nextSendGrpInv = false,
nextConnect = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = false,
allowVoiceToContact = {},

View File

@@ -1,39 +0,0 @@
package chat.simplex.common.views.chat
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@Composable
fun ComposeContextInvitingContactMemberView() {
val sentColor = MaterialTheme.appColors.sentMessage
Row(
Modifier
.height(60.dp)
.fillMaxWidth()
.padding(top = 8.dp)
.background(sentColor),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(MR.images.ic_chat),
stringResource(MR.strings.button_send_direct_message),
modifier = Modifier
.padding(start = 12.dp, end = 8.dp)
.height(20.dp)
.width(20.dp),
tint = MaterialTheme.colors.secondary
)
Text(generalGetString(MR.strings.compose_send_direct_message_to_connect))
}
}

View File

@@ -2,6 +2,7 @@
package chat.simplex.common.views.chat
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
@@ -30,6 +31,7 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.serialization.*
@@ -151,7 +153,7 @@ data class ComposeState(
is ComposePreview.MediaPreview -> true
is ComposePreview.VoicePreview -> true
is ComposePreview.FilePreview -> true
else -> message.text.isNotEmpty() || forwarding || liveMessage != null || submittingValidReport
else -> !whitespaceOnly || forwarding || liveMessage != null || submittingValidReport
}
hasContent && !inProgress
}
@@ -199,7 +201,10 @@ data class ComposeState(
}
val empty: Boolean
get() = message.text.isEmpty() && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem
get() = whitespaceOnly && preview is ComposePreview.NoPreview && contextItem is ComposeContextItem.NoContextItem
val whitespaceOnly: Boolean
get() = message.text.all { it.isWhitespace() }
companion object {
fun saver(): Saver<MutableState<ComposeState>, *> = Saver(
@@ -486,6 +491,7 @@ fun ComposeView(
return null
}
// TODO [short links] connectCheckLinkPreview
fun checkLinkPreview(): MsgContent {
val msgText = composeState.value.message.text
return when (val composePreview = composeState.value.preview) {
@@ -504,8 +510,13 @@ fun ComposeView(
}
}
fun sending() {
composeState.value = composeState.value.copy(inProgress = true)
}
suspend fun sendMemberContactInvitation() {
val mc = checkLinkPreview()
sending()
val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc)
if (contact != null) {
withContext(Dispatchers.Main) {
@@ -513,11 +524,14 @@ fun ComposeView(
clearState()
chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected())
}
} else {
composeState.value = composeState.value.copy(inProgress = false)
}
}
suspend fun sendConnectPreparedContact() {
val mc = checkLinkPreview()
sending()
val contact = chatModel.controller.apiConnectPreparedContact(
rh = chat.remoteHostId,
contactId = chat.chatInfo.apiId,
@@ -530,11 +544,36 @@ fun ComposeView(
clearState()
chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected())
}
} else {
composeState.value = composeState.value.copy(inProgress = false)
}
}
suspend fun sendConnectPreparedGroup() {
// TODO [short links] different messages for business
fun showSendConnectPreparedContactAlert(sendConnect: () -> Unit) {
val empty = composeState.value.whitespaceOnly
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.compose_view_send_contact_request_alert_question),
text = generalGetString(MR.strings.compose_view_send_contact_request_alert_text),
confirmText = (
if (empty)
generalGetString(MR.strings.compose_view_send_request_without_message)
else
generalGetString(MR.strings.compose_view_send_request)
),
onConfirm = { sendConnect() },
dismissText = (
if (empty)
generalGetString(MR.strings.compose_view_add_message)
else
generalGetString(MR.strings.cancel_verb)
)
)
}
suspend fun connectPreparedGroup() {
val mc = checkLinkPreview()
sending()
val groupInfo = chatModel.controller.apiConnectPreparedGroup(
rh = chat.remoteHostId,
groupId = chat.chatInfo.apiId,
@@ -546,28 +585,8 @@ fun ComposeView(
chatsCtx.updateGroup(chat.remoteHostId, groupInfo)
clearState()
}
}
}
// TODO [short links] next connect button design, rework compose to not show send button, align with Swift
@Composable
fun NextConnectPreparedButton() {
TextButton(onClick = {
withBGApi {
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.nextSendGrpInv) {
sendMemberContactInvitation()
} else if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.nextConnectPrepared) {
sendConnectPreparedContact()
} else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.nextConnectPrepared) {
sendConnectPreparedGroup()
}
}
}) {
if (chat.chatInfo is ChatInfo.Group) {
Text("Join")
} else {
Text("Connect")
}
} else {
composeState.value = composeState.value.copy(inProgress = false)
}
}
@@ -578,10 +597,6 @@ fun ComposeView(
var lastMessageFailedToSend: ComposeState? = null
val msgText = text ?: cs.message.text
fun sending() {
composeState.value = composeState.value.copy(inProgress = true)
}
suspend fun forwardItem(rhId: Long?, forwardedItem: List<ChatItem>, fromChatInfo: ChatInfo, ttl: Int?): List<ChatItem>? {
val chatItems = controller.apiForwardChatItems(
rh = rhId,
@@ -689,10 +704,7 @@ fun ComposeView(
clearCurrentDraft()
}
if (chat.nextSendGrpInv) {
sendMemberContactInvitation()
sent = null
} else if (cs.contextItem is ComposeContextItem.ForwardingItems) {
if (cs.contextItem is ComposeContextItem.ForwardingItems) {
sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItems, cs.contextItem.fromChatInfo, ttl = ttl)
if (sent == null) {
lastMessageFailedToSend = constructFailedMessage(cs)
@@ -1046,6 +1058,250 @@ fun ComposeView(
}
}
val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled)
val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason)
val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv)
@Composable
fun AttachmentButton() {
val isGroupAndProhibitedFiles =
chatsCtx.secondaryContextFilter == null
&& chat.chatInfo is ChatInfo.Group
&& !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
val attachmentClicked = if (isGroupAndProhibitedFiles) {
{
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.files_and_media_prohibited),
text = generalGetString(MR.strings.only_owners_can_enable_files_and_media)
)
}
} else {
showChooseAttachment
}
val attachmentEnabled =
!composeState.value.attachmentDisabled
&& sendMsgEnabled.value
&& !isGroupAndProhibitedFiles
&& !nextSendGrpInv.value
IconButton(
attachmentClicked,
Modifier.padding(start = 3.dp, end = 1.dp, bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier),
enabled = attachmentEnabled
) {
Icon(
painterResource(MR.images.ic_attach_file_filled_500),
contentDescription = stringResource(MR.strings.attach),
tint = if (attachmentEnabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
modifier = Modifier
.size(28.dp)
.clip(CircleShape)
)
}
}
@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)
if (isInDarkTheme()) Indigo else Indigo.copy(alpha = 0.7F)
else MaterialTheme.colors.primary
SendMsgView(
composeState,
showVoiceRecordIcon = true,
recState,
chat.chatInfo is ChatInfo.Direct,
liveMessageAlertShown = chatModel.controller.appPrefs.liveMessageAlertShown,
sendMsgEnabled = sendMsgEnabled.value,
userCantSendReason = userCantSendReason.value,
sendButtonEnabled = sendMsgEnabled.value && !disableSendButton,
sendToConnect = sendToConnect,
hideSendButton = chat.chatInfo.nextConnect && !nextSendGrpInv.value && composeState.value.whitespaceOnly,
nextConnect = chat.chatInfo.nextConnect,
needToAllowVoiceToContact = needToAllowVoiceToContact,
allowedVoiceByPrefs = allowedVoiceByPrefs,
allowVoiceToContact = ::allowVoiceToContact,
sendButtonColor = sendButtonColor,
timedMessageAllowed = timedMessageAllowed,
customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime,
placeholder = placeholder ?: composeState.value.placeholder,
sendMessage = { ttl ->
sendMessage(ttl)
resetLinkPreview()
},
sendLiveMessage = if (chat.chatInfo.chatType != ChatType.Local) ::sendLiveMessage else null,
updateLiveMessage = ::updateLiveMessage,
cancelLiveMessage = {
composeState.value = composeState.value.copy(liveMessage = null)
chatModel.removeLiveDummy()
},
editPrevMessage = ::editPrevMessage,
onFilesPasted = { composeState.onFilesAttached(it) },
onMessageChange = ::onMessageChange,
textStyle = textStyle,
focusRequester = focusRequester,
)
}
@Composable
fun SendContactRequestView(
disableSendButton: Boolean,
icon: ImageResource,
sendRequest: () -> Unit
) {
Row(
Modifier.padding(horizontal = DEFAULT_PADDING_HALF),
verticalAlignment = Alignment.CenterVertically
) {
Box(
Modifier.weight(1f)
) {
SendMsgView_(
disableSendButton = disableSendButton,
placeholder = generalGetString(MR.strings.compose_view_add_message),
sendToConnect = sendRequest
)
}
if (composeState.value.whitespaceOnly) {
SimpleButtonIconEnded(
text = stringResource(MR.strings.compose_view_connect),
icon = painterResource(icon),
disabled = composeState.value.inProgress,
click = { withApi { sendRequest() } }
)
}
}
}
@Composable
fun ConnectButtonView(
text: String,
icon: ImageResource,
connect: () -> Unit
) {
var modifier = Modifier.height(60.dp).fillMaxWidth()
modifier = if (composeState.value.inProgress) modifier else modifier.clickable(onClick = { connect() })
Box(
modifier,
contentAlignment = Alignment.Center
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(
painterResource(icon),
contentDescription = null,
tint = MaterialTheme.colors.primary,
)
Text(
text,
style = MaterialTheme.typography.caption,
color = MaterialTheme.colors.primary
)
}
}
}
@Composable
fun ContextSendMessageToConnect(s: String) {
Row(
Modifier
.height(60.dp)
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(MR.images.ic_chat),
contentDescription = null,
modifier = Modifier
.padding(end = 8.dp)
.height(20.dp)
.width(20.dp),
tint = MaterialTheme.colors.secondary
)
Text(s)
}
}
// In case a user sent something, state is in progress, the user rotates a screen to different orientation.
// Without clearing the state the user will be unable to send anything until re-enters ChatView
LaunchedEffect(Unit) {
@@ -1071,17 +1327,7 @@ fun ComposeView(
chatModel.sharedContent.value = null
}
val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled)
val userCantSendReason = rememberUpdatedState(chat.chatInfo.userCantSendReason)
val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv)
Column {
// TODO [short links] move button to the right of send field, rework SendMsgView to not show send button, align with Swift
if (chat.chatInfo.nextConnect) {
NextConnectPreparedButton()
}
// TODO ^^^ (this shouldn't be here)
val currentUser = chatModel.currentUser.value
if ((
(chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.nextConnectPrepared)
@@ -1096,17 +1342,6 @@ fun ComposeView(
)
}
if (
chat.chatInfo is ChatInfo.Direct
&& chat.chatInfo.contact.nextAcceptContactRequest
&& chat.chatInfo.contact.contactRequestId != null
) {
ComposeContextContactRequestActionsView(
rhId = rhId,
contactRequestId = chat.chatInfo.contact.contactRequestId
)
}
if (
chat.chatInfo is ChatInfo.Group
&& chatsCtx.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext
@@ -1123,10 +1358,6 @@ fun ComposeView(
)
}
if (nextSendGrpInv.value) {
ComposeContextInvitingContactMemberView()
}
val ctx = composeState.value.contextItem
if (ctx is ComposeContextItem.ReportedItem) {
ReportReasonView(ctx.reason)
@@ -1135,6 +1366,7 @@ fun ComposeView(
val simplexLinkProhibited = chatsCtx.secondaryContextFilter == null && hasSimplexLink.value && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks)
val fileProhibited = chatsCtx.secondaryContextFilter == null && composeState.value.attachmentPreview && !chat.groupFeatureEnabled(GroupFeature.Files)
val voiceProhibited = composeState.value.preview is ComposePreview.VoicePreview && !chat.chatInfo.featureEnabled(ChatFeature.Voice)
val disableSendButton = simplexLinkProhibited || fileProhibited || voiceProhibited
if (composeState.value.preview !is ComposePreview.VoicePreview || composeState.value.editing) {
if (simplexLinkProhibited) {
MsgNotAllowedView(generalGetString(MR.strings.simplex_links_not_allowed), icon = painterResource(MR.images.ic_link))
@@ -1159,154 +1391,68 @@ fun ComposeView(
}
}
}
Surface(color = MaterialTheme.colors.background, contentColor = MaterialTheme.colors.onBackground) {
Divider()
Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) {
val isGroupAndProhibitedFiles =
chatsCtx.secondaryContextFilter == null
&& chat.chatInfo is ChatInfo.Group
&& !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership)
val attachmentClicked = if (isGroupAndProhibitedFiles) {
{
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.files_and_media_prohibited),
text = generalGetString(MR.strings.only_owners_can_enable_files_and_media)
)
}
if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.nextConnectPrepared) {
if (chat.chatInfo.groupInfo.businessChat == null) {
ConnectButtonView(
text = stringResource(MR.strings.compose_view_join_group),
icon = MR.images.ic_group_filled,
connect = { withApi { connectPreparedGroup() } }
)
} else {
showChooseAttachment
}
val attachmentEnabled =
!composeState.value.attachmentDisabled
&& sendMsgEnabled.value
&& !isGroupAndProhibitedFiles
&& !nextSendGrpInv.value
IconButton(
attachmentClicked,
Modifier.padding(start = 3.dp, end = 1.dp, bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier),
enabled = attachmentEnabled
) {
Icon(
painterResource(MR.images.ic_attach_file_filled_500),
contentDescription = stringResource(MR.strings.attach),
tint = if (attachmentEnabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
modifier = Modifier
.size(28.dp)
.clip(CircleShape)
SendContactRequestView(
disableSendButton = disableSendButton,
icon = MR.images.ic_work_filled,
sendRequest = { showSendConnectPreparedContactAlert(sendConnect = { withApi { connectPreparedGroup() } }) }
)
}
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()
} else if (nextSendGrpInv.value) {
Column {
ContextSendMessageToConnect(generalGetString(MR.strings.compose_send_direct_message_to_connect))
Divider()
Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) {
AttachmentButton()
SendMsgView_(
disableSendButton = disableSendButton,
sendToConnect = { withApi { sendMemberContactInvitation() } }
)
}
}
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
}
} else if (
chat.chatInfo is ChatInfo.Direct
&& chat.chatInfo.contact.nextConnectPrepared
&& chat.chatInfo.contact.preparedContact != null
) {
when (chat.chatInfo.contact.preparedContact.uiConnLinkType) {
ConnectionMode.Inv ->
ConnectButtonView(
text = stringResource(MR.strings.compose_view_connect),
icon = MR.images.ic_person_add_filled,
connect = { withApi { sendConnectPreparedContact() } }
)
ConnectionMode.Con ->
SendContactRequestView(
disableSendButton = disableSendButton,
icon = MR.images.ic_person_add_filled,
sendRequest = { showSendConnectPreparedContactAlert(sendConnect = { withApi { sendConnectPreparedContact() } }) }
)
}
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)
if (isInDarkTheme()) Indigo else Indigo.copy(alpha = 0.7F)
else MaterialTheme.colors.primary
SendMsgView(
composeState,
showVoiceRecordIcon = true,
recState,
chat.chatInfo is ChatInfo.Direct,
liveMessageAlertShown = chatModel.controller.appPrefs.liveMessageAlertShown,
sendMsgEnabled = sendMsgEnabled.value,
userCantSendReason = userCantSendReason.value,
sendButtonEnabled = sendMsgEnabled.value && !(simplexLinkProhibited || fileProhibited || voiceProhibited),
nextSendGrpInv = nextSendGrpInv.value,
needToAllowVoiceToContact,
allowedVoiceByPrefs,
allowVoiceToContact = ::allowVoiceToContact,
sendButtonColor = sendButtonColor,
timedMessageAllowed = timedMessageAllowed,
customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime,
placeholder = composeState.value.placeholder,
sendMessage = { ttl ->
sendMessage(ttl)
resetLinkPreview()
},
sendLiveMessage = if (chat.chatInfo.chatType != ChatType.Local) ::sendLiveMessage else null,
updateLiveMessage = ::updateLiveMessage,
cancelLiveMessage = {
composeState.value = composeState.value.copy(liveMessage = null)
chatModel.removeLiveDummy()
},
editPrevMessage = ::editPrevMessage,
onFilesPasted = { composeState.onFilesAttached(it) },
onMessageChange = ::onMessageChange,
textStyle = textStyle,
focusRequester = focusRequester,
} else if (
chat.chatInfo is ChatInfo.Direct
&& chat.chatInfo.contact.nextAcceptContactRequest
&& chat.chatInfo.contact.contactRequestId != null
) {
ComposeContextContactRequestActionsView(
rhId = rhId,
contactRequestId = chat.chatInfo.contact.contactRequestId
)
} else {
Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) {
AttachmentButton()
SendMsgView_(disableSendButton = disableSendButton)
}
}
}
}

View File

@@ -41,7 +41,9 @@ fun SendMsgView(
sendMsgEnabled: Boolean,
userCantSendReason: Pair<String, String?>?,
sendButtonEnabled: Boolean,
nextSendGrpInv: Boolean,
sendToConnect: (() -> Unit)? = null,
hideSendButton: Boolean = false,
nextConnect: Boolean,
needToAllowVoiceToContact: Boolean,
allowedVoiceByPrefs: Boolean,
sendButtonColor: Color = MaterialTheme.colors.primary,
@@ -72,7 +74,7 @@ fun SendMsgView(
false
}
}
val showVoiceButton = !nextSendGrpInv && cs.message.text.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
val showVoiceButton = !nextConnect && cs.message.text.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
!composeState.value.forwarding && cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started) && (cs.contextItem !is ComposeContextItem.ReportedItem)
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
val sendMsgButtonDisabled = !sendMsgEnabled || !cs.sendEnabled() ||
@@ -95,7 +97,11 @@ fun SendMsgView(
focusRequester
) {
if (!cs.inProgress) {
sendMessage(null)
if (sendToConnect != null) {
sendToConnect()
} else {
sendMessage(null)
}
}
}
if (clicksOnTextFieldDisabled) {
@@ -133,7 +139,7 @@ fun SendMsgView(
when {
progressByTimeout -> ProgressIndicator()
cs.contextItem is ComposeContextItem.ReportedItem -> {
SendMsgButton(painterResource(MR.images.ic_check_filled), sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage)
SendMsgButton(painterResource(MR.images.ic_check_filled), sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendToConnect, sendMessage)
}
showVoiceButton && sendMsgEnabled -> {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -181,7 +187,7 @@ fun SendMsgView(
fun MenuItems(): List<@Composable () -> Unit> {
val menuItems = mutableListOf<@Composable () -> Unit>()
if (cs.liveMessage == null && !cs.editing && !nextSendGrpInv || sendMsgEnabled) {
if (cs.liveMessage == null && !cs.editing && !nextConnect || sendMsgEnabled) {
if (
cs.preview !is ComposePreview.VoicePreview &&
cs.contextItem is ComposeContextItem.NoContextItem &&
@@ -215,19 +221,21 @@ fun SendMsgView(
return menuItems
}
val menuItems = MenuItems()
if (menuItems.isNotEmpty()) {
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage) { showDropdown.value = true }
DefaultDropdownMenu(showDropdown) {
menuItems.forEach { composable -> composable() }
if (!hideSendButton) {
val menuItems = MenuItems()
if (menuItems.isNotEmpty()) {
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendToConnect, sendMessage) { showDropdown.value = true }
DefaultDropdownMenu(showDropdown) {
menuItems.forEach { composable -> composable() }
}
CustomDisappearingMessageDialog(
showCustomDisappearingMessageDialog,
sendMessage = sendMessage,
customDisappearingMessageTimePref = customDisappearingMessageTimePref
)
} else {
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendToConnect, sendMessage)
}
CustomDisappearingMessageDialog(
showCustomDisappearingMessageDialog,
sendMessage = sendMessage,
customDisappearingMessageTimePref = customDisappearingMessageTimePref
)
} else {
SendMsgButton(icon, sendButtonSize, sendButtonAlpha, sendButtonColor, !sendMsgButtonDisabled, sendMessage)
}
}
}
@@ -430,6 +438,7 @@ private fun SendMsgButton(
alpha: Animatable<Float, AnimationVector1D>,
sendButtonColor: Color,
enabled: Boolean,
sendToConnect: (() -> Unit)?,
sendMessage: (Int?) -> Unit,
onLongClick: (() -> Unit)? = null
) {
@@ -438,7 +447,13 @@ private fun SendMsgButton(
Box(
modifier = Modifier.requiredSize(36.dp)
.combinedClickable(
onClick = { sendMessage(null) },
onClick = {
if (sendToConnect != null) {
sendToConnect()
} else {
sendMessage(null)
}
},
onLongClick = onLongClick,
enabled = enabled,
role = Role.Button,
@@ -581,7 +596,7 @@ fun PreviewSendMsgView() {
sendMsgEnabled = true,
userCantSendReason = null,
sendButtonEnabled = true,
nextSendGrpInv = false,
nextConnect = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
allowVoiceToContact = {},
@@ -616,7 +631,7 @@ fun PreviewSendMsgViewEditing() {
sendMsgEnabled = true,
userCantSendReason = null,
sendButtonEnabled = true,
nextSendGrpInv = false,
nextConnect = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
allowVoiceToContact = {},
@@ -651,7 +666,7 @@ fun PreviewSendMsgViewInProgress() {
sendMsgEnabled = true,
userCantSendReason = null,
sendButtonEnabled = true,
nextSendGrpInv = false,
nextConnect = false,
needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true,
allowVoiceToContact = {},

View File

@@ -523,6 +523,13 @@
<string name="report_compose_reason_header_other">Report other: only group moderators will see it.</string>
<string name="report_sent_alert_title">Report sent to moderators</string>
<string name="report_sent_alert_msg_view_in_support_chat">You can view your reports in Chat with admins.</string>
<string name="compose_view_join_group">Join group</string>
<string name="compose_view_add_message">Add message</string>
<string name="compose_view_connect">Connect</string>
<string name="compose_view_send_contact_request_alert_question">Send contact request?</string>
<string name="compose_view_send_contact_request_alert_text"><![CDATA[You will be able to send messages <b>only after your request is accepted</b>.]]></string>
<string name="compose_view_send_request_without_message">Send request without message</string>
<string name="compose_view_send_request">Send request</string>
<string name="cant_send_message_alert_title">You can\'t send messages!</string>
<string name="cant_send_message_contact_not_ready">contact not ready</string>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22" fill="#1f1f1f"><path d="M729-530.5H628.5q-12.25 0-20.37-8.53-8.13-8.54-8.13-20.4 0-11.85 8.13-20.21 8.12-8.36 20.37-8.36H729v-100.5q0-11.67 8.18-20.09 8.17-8.41 20.5-8.41 12.32 0 20.57 8.41 8.25 8.42 8.25 20.09V-588h100q12.25 0 20.63 8.46 8.37 8.47 8.37 20.22 0 12.32-8.37 20.57-8.38 8.25-20.63 8.25h-100v100q0 12.25-8.43 20.62-8.42 8.38-20.75 8.38-12.32 0-20.32-8.38-8-8.37-8-20.62v-100Zm-368.48 50Q295-480.5 254-521.48t-41-106.5q0-65.52 40.98-106.52t106.5-41q65.52 0 106.52 40.98t41 106.5q0 65.52-40.98 106.52t-106.5 41ZM45-224v-33.55q0-34.52 17.25-62.24 17.25-27.71 49.93-41.55Q186.5-394 243.84-407.5t116.5-13.5q59.16 0 116.08 13.5 56.93 13.5 131.56 45.99 32.52 14.51 50.27 41.89Q676-292.25 676-257.53v33.77q0 23.45-16.89 40.35-16.89 16.91-40.61 16.91h-516q-23.72 0-40.61-16.89T45-224Z"/></svg>

After

Width:  |  Height:  |  Size: 885 B