mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-03-30 16:25:57 +00:00
android, desktop: compose UI (#6018)
This commit is contained in:
@@ -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}"
|
||||
|
||||
@@ -101,7 +101,7 @@ fun TerminalLayout(
|
||||
sendMsgEnabled = true,
|
||||
userCantSendReason = null,
|
||||
sendButtonEnabled = true,
|
||||
nextSendGrpInv = false,
|
||||
nextConnect = false,
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = false,
|
||||
allowVoiceToContact = {},
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user