android: create contacts with group members (#3078)
This commit is contained in:
parent
648a9761f9
commit
ae6996b2ee
@ -44,6 +44,7 @@ import java.net.URI
|
||||
@Composable
|
||||
actual fun PlatformTextField(
|
||||
composeState: MutableState<ComposeState>,
|
||||
sendMsgEnabled: Boolean,
|
||||
textStyle: MutableState<TextStyle>,
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
@ -60,6 +61,7 @@ actual fun PlatformTextField(
|
||||
val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() }
|
||||
val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() }
|
||||
var showKeyboard by remember { mutableStateOf(false) }
|
||||
var freeFocus by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(cs.contextItem) {
|
||||
if (cs.contextItem is ComposeContextItem.QuotedItem) {
|
||||
delay(100)
|
||||
@ -70,6 +72,11 @@ actual fun PlatformTextField(
|
||||
showKeyboard = true
|
||||
}
|
||||
}
|
||||
LaunchedEffect(sendMsgEnabled) {
|
||||
if (!sendMsgEnabled) {
|
||||
freeFocus = true
|
||||
}
|
||||
}
|
||||
|
||||
AndroidView(modifier = Modifier, factory = {
|
||||
val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) {
|
||||
@ -142,6 +149,11 @@ actual fun PlatformTextField(
|
||||
imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT)
|
||||
showKeyboard = false
|
||||
}
|
||||
if (freeFocus) {
|
||||
it.clearFocus()
|
||||
hideKeyboard(it)
|
||||
freeFocus = false
|
||||
}
|
||||
showDeleteTextButton.value = it.lineCount >= 4 && !cs.inProgress
|
||||
}
|
||||
if (composeState.value.preview is ComposePreview.VoicePreview) {
|
||||
|
@ -606,10 +606,13 @@ data class Chat (
|
||||
val userCanSend: Boolean
|
||||
get() = when (chatInfo) {
|
||||
is ChatInfo.Direct -> true
|
||||
is ChatInfo.Group -> {
|
||||
val m = chatInfo.groupInfo.membership
|
||||
m.memberActive && m.memberRole >= GroupMemberRole.Member
|
||||
}
|
||||
is ChatInfo.Group -> chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Member
|
||||
else -> false
|
||||
}
|
||||
|
||||
val nextSendGrpInv: Boolean
|
||||
get() = when (chatInfo) {
|
||||
is ChatInfo.Direct -> chatInfo.contact.nextSendGrpInv
|
||||
else -> false
|
||||
}
|
||||
|
||||
@ -799,13 +802,18 @@ data class Contact(
|
||||
val userPreferences: ChatPreferences,
|
||||
val mergedPreferences: ContactUserPreferences,
|
||||
override val createdAt: Instant,
|
||||
override val updatedAt: Instant
|
||||
override val updatedAt: Instant,
|
||||
val contactGroupMemberId: Long? = null,
|
||||
val contactGrpInvSent: Boolean
|
||||
): SomeChat, NamedChat {
|
||||
override val chatType get() = ChatType.Direct
|
||||
override val id get() = "@$contactId"
|
||||
override val apiId get() = contactId
|
||||
override val ready get() = activeConn.connStatus == ConnStatus.Ready
|
||||
override val sendMsgEnabled get() = !(activeConn.connectionStats?.ratchetSyncSendProhibited ?: false)
|
||||
override val sendMsgEnabled get() =
|
||||
(ready && !(activeConn.connectionStats?.ratchetSyncSendProhibited ?: false))
|
||||
|| nextSendGrpInv
|
||||
val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent
|
||||
override val ntfsEnabled get() = chatSettings.enableNtfs
|
||||
override val incognito get() = contactConnIncognito
|
||||
override fun featureEnabled(feature: ChatFeature) = when (feature) {
|
||||
@ -856,7 +864,8 @@ data class Contact(
|
||||
userPreferences = ChatPreferences.sampleData,
|
||||
mergedPreferences = ContactUserPreferences.sampleData,
|
||||
createdAt = Clock.System.now(),
|
||||
updatedAt = Clock.System.now()
|
||||
updatedAt = Clock.System.now(),
|
||||
contactGrpInvSent = false
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -881,6 +890,7 @@ class ContactSubStatus(
|
||||
data class Connection(
|
||||
val connId: Long,
|
||||
val agentConnId: String,
|
||||
val peerChatVRange: VersionRange,
|
||||
val connStatus: ConnStatus,
|
||||
val connLevel: Int,
|
||||
val viaGroupLink: Boolean,
|
||||
@ -890,10 +900,17 @@ data class Connection(
|
||||
) {
|
||||
val id: ChatId get() = ":$connId"
|
||||
companion object {
|
||||
val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, customUserProfileId = null)
|
||||
val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class VersionRange(val minVersion: Int, val maxVersion: Int) {
|
||||
|
||||
fun isCompatibleRange(vRange: VersionRange): Boolean =
|
||||
this.minVersion <= vRange.maxVersion && vRange.minVersion <= this.maxVersion
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SecurityCode(val securityCode: String, val verifiedAt: Instant)
|
||||
|
||||
@ -1224,6 +1241,7 @@ class MemberSubError (
|
||||
@Serializable
|
||||
class UserContactRequest (
|
||||
val contactRequestId: Long,
|
||||
val cReqChatVRange: VersionRange,
|
||||
override val localDisplayName: String,
|
||||
val profile: Profile,
|
||||
override val createdAt: Instant,
|
||||
@ -1246,6 +1264,7 @@ class UserContactRequest (
|
||||
companion object {
|
||||
val sampleData = UserContactRequest(
|
||||
contactRequestId = 1,
|
||||
cReqChatVRange = VersionRange(1, 1),
|
||||
localDisplayName = "alice",
|
||||
profile = Profile.sampleData,
|
||||
createdAt = Clock.System.now(),
|
||||
@ -1465,6 +1484,7 @@ data class ChatItem (
|
||||
is RcvGroupEvent.GroupDeleted -> showNtfDir
|
||||
is RcvGroupEvent.GroupUpdated -> false
|
||||
is RcvGroupEvent.InvitedViaGroupLink -> false
|
||||
is RcvGroupEvent.MemberCreatedContact -> false
|
||||
}
|
||||
is CIContent.SndGroupEventContent -> showNtfDir
|
||||
is CIContent.RcvConnEventContent -> false
|
||||
@ -2464,6 +2484,7 @@ sealed class RcvGroupEvent() {
|
||||
@Serializable @SerialName("groupDeleted") class GroupDeleted(): RcvGroupEvent()
|
||||
@Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): RcvGroupEvent()
|
||||
@Serializable @SerialName("invitedViaGroupLink") class InvitedViaGroupLink(): RcvGroupEvent()
|
||||
@Serializable @SerialName("memberCreatedContact") class MemberCreatedContact(): RcvGroupEvent()
|
||||
|
||||
val text: String get() = when (this) {
|
||||
is MemberAdded -> String.format(generalGetString(MR.strings.rcv_group_event_member_added), profile.profileViewName)
|
||||
@ -2476,6 +2497,7 @@ sealed class RcvGroupEvent() {
|
||||
is GroupDeleted -> generalGetString(MR.strings.rcv_group_event_group_deleted)
|
||||
is GroupUpdated -> generalGetString(MR.strings.rcv_group_event_updated_group_profile)
|
||||
is InvitedViaGroupLink -> generalGetString(MR.strings.rcv_group_event_invited_via_your_group_link)
|
||||
is MemberCreatedContact -> generalGetString(MR.strings.rcv_group_event_member_created_contact)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,6 +26,12 @@ import java.util.Date
|
||||
|
||||
typealias ChatCtrl = Long
|
||||
|
||||
// currentChatVersion in core
|
||||
const val CURRENT_CHAT_VERSION: Int = 2
|
||||
|
||||
// version range that supports establishing direct connection with a group member (xGrpDirectInvVRange in core)
|
||||
val CREATE_MEMBER_CONTACT_VRANGE = VersionRange(minVersion = 2, maxVersion = CURRENT_CHAT_VERSION)
|
||||
|
||||
enum class CallOnLockScreen {
|
||||
DISABLE,
|
||||
SHOW,
|
||||
@ -784,16 +790,18 @@ object ChatController {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiGetContactCode(contactId: Long): Pair<Contact, String> {
|
||||
suspend fun apiGetContactCode(contactId: Long): Pair<Contact, String>? {
|
||||
val r = sendCmd(CC.APIGetContactCode(contactId))
|
||||
if (r is CR.ContactCode) return r.contact to r.connectionCode
|
||||
throw Exception("failed to get contact code: ${r.responseType} ${r.details}")
|
||||
Log.e(TAG,"failed to get contact code: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiGetGroupMemberCode(groupId: Long, groupMemberId: Long): Pair<GroupMember, String> {
|
||||
suspend fun apiGetGroupMemberCode(groupId: Long, groupMemberId: Long): Pair<GroupMember, String>? {
|
||||
val r = sendCmd(CC.APIGetGroupMemberCode(groupId, groupMemberId))
|
||||
if (r is CR.GroupMemberCode) return r.member to r.connectionCode
|
||||
throw Exception("failed to get group member code: ${r.responseType} ${r.details}")
|
||||
Log.e(TAG,"failed to get group member code: ${r.responseType} ${r.details}")
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun apiVerifyContact(contactId: Long, connectionCode: String?): Pair<Boolean, String>? {
|
||||
@ -1272,6 +1280,30 @@ object ChatController {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiCreateMemberContact(groupId: Long, groupMemberId: Long): Contact? {
|
||||
return when (val r = sendCmd(CC.APICreateMemberContact(groupId, groupMemberId))) {
|
||||
is CR.NewMemberContact -> r.contact
|
||||
else -> {
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiCreateMemberContact", generalGetString(MR.strings.error_creating_member_contact), r)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun apiSendMemberContactInvitation(contactId: Long, mc: MsgContent): Contact? {
|
||||
return when (val r = sendCmd(CC.APISendMemberContactInvitation(contactId, mc))) {
|
||||
is CR.NewMemberContactSentInv -> r.contact
|
||||
else -> {
|
||||
if (!(networkErrorAlert(r))) {
|
||||
apiErrorAlert("apiSendMemberContactInvitation", generalGetString(MR.strings.error_sending_message_contact_invitation), r)
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun allowFeatureToContact(contact: Contact, feature: ChatFeature, param: Int? = null) {
|
||||
val prefs = contact.mergedPreferences.toPreferences().setAllowed(feature, param = param)
|
||||
val toContact = apiSetContactPrefs(contact.contactId, prefs)
|
||||
@ -1527,6 +1559,10 @@ object ChatController {
|
||||
if (active(r.user)) {
|
||||
chatModel.updateGroup(r.toGroup)
|
||||
}
|
||||
is CR.NewMemberContactReceivedInv ->
|
||||
if (active(r.user)) {
|
||||
chatModel.updateContact(r.contact)
|
||||
}
|
||||
is CR.RcvFileStart ->
|
||||
chatItemSimpleUpdate(r.user, r.chatItem)
|
||||
is CR.RcvFileComplete ->
|
||||
@ -1822,6 +1858,8 @@ sealed class CC {
|
||||
class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC()
|
||||
class APIDeleteGroupLink(val groupId: Long): CC()
|
||||
class APIGetGroupLink(val groupId: Long): CC()
|
||||
class APICreateMemberContact(val groupId: Long, val groupMemberId: Long): CC()
|
||||
class APISendMemberContactInvitation(val contactId: Long, val mc: MsgContent): CC()
|
||||
class APIGetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol): CC()
|
||||
class APISetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol, val servers: List<ServerCfg>): CC()
|
||||
class APITestProtoServer(val userId: Long, val server: String): CC()
|
||||
@ -1927,6 +1965,8 @@ sealed class CC {
|
||||
is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}"
|
||||
is APIDeleteGroupLink -> "/_delete link #$groupId"
|
||||
is APIGetGroupLink -> "/_get link #$groupId"
|
||||
is APICreateMemberContact -> "/_create member contact #$groupId $groupMemberId"
|
||||
is APISendMemberContactInvitation -> "/_invite member contact @$contactId ${mc.cmdString}"
|
||||
is APIGetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()}"
|
||||
is APISetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()} ${protoServersStr(servers)}"
|
||||
is APITestProtoServer -> "/_server test $userId $server"
|
||||
@ -2021,6 +2061,8 @@ sealed class CC {
|
||||
is APIGroupLinkMemberRole -> "apiGroupLinkMemberRole"
|
||||
is APIDeleteGroupLink -> "apiDeleteGroupLink"
|
||||
is APIGetGroupLink -> "apiGetGroupLink"
|
||||
is APICreateMemberContact -> "apiCreateMemberContact"
|
||||
is APISendMemberContactInvitation -> "apiSendMemberContactInvitation"
|
||||
is APIGetUserProtoServers -> "apiGetUserProtoServers"
|
||||
is APISetUserProtoServers -> "apiSetUserProtoServers"
|
||||
is APITestProtoServer -> "testProtoServer"
|
||||
@ -3311,6 +3353,9 @@ sealed class CR {
|
||||
@Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR()
|
||||
@Serializable @SerialName("groupLink") class GroupLink(val user: UserRef, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR()
|
||||
@Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: UserRef, val groupInfo: GroupInfo): CR()
|
||||
@Serializable @SerialName("newMemberContact") class NewMemberContact(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("newMemberContactSentInv") class NewMemberContactSentInv(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR()
|
||||
@Serializable @SerialName("newMemberContactReceivedInv") class NewMemberContactReceivedInv(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR()
|
||||
// receiving file events
|
||||
@Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: UserRef, val chatItem: AChatItem): CR()
|
||||
@Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val user: UserRef, val rcvFileTransfer: RcvFileTransfer): CR()
|
||||
@ -3438,6 +3483,9 @@ sealed class CR {
|
||||
is GroupLinkCreated -> "groupLinkCreated"
|
||||
is GroupLink -> "groupLink"
|
||||
is GroupLinkDeleted -> "groupLinkDeleted"
|
||||
is NewMemberContact -> "newMemberContact"
|
||||
is NewMemberContactSentInv -> "newMemberContactSentInv"
|
||||
is NewMemberContactReceivedInv -> "newMemberContactReceivedInv"
|
||||
is RcvFileAcceptedSndCancelled -> "rcvFileAcceptedSndCancelled"
|
||||
is RcvFileAccepted -> "rcvFileAccepted"
|
||||
is RcvFileStart -> "rcvFileStart"
|
||||
@ -3563,6 +3611,9 @@ sealed class CR {
|
||||
is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole")
|
||||
is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole")
|
||||
is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo))
|
||||
is NewMemberContact -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member")
|
||||
is NewMemberContactSentInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member")
|
||||
is NewMemberContactReceivedInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member")
|
||||
is RcvFileAcceptedSndCancelled -> withUser(user, noDetails())
|
||||
is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem))
|
||||
is RcvFileStart -> withUser(user, json.encodeToString(chatItem))
|
||||
@ -3820,6 +3871,7 @@ sealed class ChatErrorType {
|
||||
is AgentCommandError -> "agentCommandError"
|
||||
is InvalidFileDescription -> "invalidFileDescription"
|
||||
is ConnectionIncognitoChangeProhibited -> "connectionIncognitoChangeProhibited"
|
||||
is PeerChatVRangeIncompatible -> "peerChatVRangeIncompatible"
|
||||
is InternalError -> "internalError"
|
||||
is CEException -> "exception $message"
|
||||
}
|
||||
@ -3894,6 +3946,7 @@ sealed class ChatErrorType {
|
||||
@Serializable @SerialName("agentCommandError") class AgentCommandError(val message: String): ChatErrorType()
|
||||
@Serializable @SerialName("invalidFileDescription") class InvalidFileDescription(val message: String): ChatErrorType()
|
||||
@Serializable @SerialName("connectionIncognitoChangeProhibited") object ConnectionIncognitoChangeProhibited: ChatErrorType()
|
||||
@Serializable @SerialName("peerChatVRangeIncompatible") object PeerChatVRangeIncompatible: ChatErrorType()
|
||||
@Serializable @SerialName("internalError") class InternalError(val message: String): ChatErrorType()
|
||||
@Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType()
|
||||
}
|
||||
@ -3922,6 +3975,7 @@ sealed class StoreError {
|
||||
is GroupMemberNameNotFound -> "groupMemberNameNotFound"
|
||||
is GroupMemberNotFound -> "groupMemberNotFound"
|
||||
is GroupMemberNotFoundByMemberId -> "groupMemberNotFoundByMemberId"
|
||||
is MemberContactGroupMemberNotFound -> "memberContactGroupMemberNotFound"
|
||||
is GroupWithoutUser -> "groupWithoutUser"
|
||||
is DuplicateGroupMember -> "duplicateGroupMember"
|
||||
is GroupAlreadyJoined -> "groupAlreadyJoined"
|
||||
@ -3979,6 +4033,7 @@ sealed class StoreError {
|
||||
@Serializable @SerialName("groupMemberNameNotFound") class GroupMemberNameNotFound(val groupId: Long, val groupMemberName: String): StoreError()
|
||||
@Serializable @SerialName("groupMemberNotFound") class GroupMemberNotFound(val groupMemberId: Long): StoreError()
|
||||
@Serializable @SerialName("groupMemberNotFoundByMemberId") class GroupMemberNotFoundByMemberId(val memberId: String): StoreError()
|
||||
@Serializable @SerialName("memberContactGroupMemberNotFound") class MemberContactGroupMemberNotFound(val contactId: Long): StoreError()
|
||||
@Serializable @SerialName("groupWithoutUser") object GroupWithoutUser: StoreError()
|
||||
@Serializable @SerialName("duplicateGroupMember") object DuplicateGroupMember: StoreError()
|
||||
@Serializable @SerialName("groupAlreadyJoined") object GroupAlreadyJoined: StoreError()
|
||||
|
@ -8,6 +8,7 @@ import chat.simplex.common.views.chat.ComposeState
|
||||
@Composable
|
||||
expect fun PlatformTextField(
|
||||
composeState: MutableState<ComposeState>,
|
||||
sendMsgEnabled: Boolean,
|
||||
textStyle: MutableState<TextStyle>,
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
|
@ -85,6 +85,8 @@ fun TerminalLayout(
|
||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||
isDirectChat = false,
|
||||
liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
|
||||
sendMsgEnabled = true,
|
||||
nextSendGrpInv = false,
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = false,
|
||||
userIsObserver = false,
|
||||
|
@ -291,21 +291,23 @@ fun ChatInfoLayout(
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
SectionView {
|
||||
if (connectionCode != null) {
|
||||
VerifyCodeButton(contact.verified, verifyClicked)
|
||||
if (contact.ready) {
|
||||
SectionView {
|
||||
if (connectionCode != null) {
|
||||
VerifyCodeButton(contact.verified, verifyClicked)
|
||||
}
|
||||
ContactPreferencesButton(openPreferences)
|
||||
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
|
||||
if (cStats != null && cStats.ratchetSyncAllowed) {
|
||||
SynchronizeConnectionButton(syncContactConnection)
|
||||
}
|
||||
// } else if (developerTools) {
|
||||
// SynchronizeConnectionButtonForce(syncContactConnectionForce)
|
||||
// }
|
||||
}
|
||||
ContactPreferencesButton(openPreferences)
|
||||
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
|
||||
if (cStats != null && cStats.ratchetSyncAllowed) {
|
||||
SynchronizeConnectionButton(syncContactConnection)
|
||||
}
|
||||
// } else if (developerTools) {
|
||||
// SynchronizeConnectionButtonForce(syncContactConnectionForce)
|
||||
// }
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
SectionDividerSpaced()
|
||||
if (contact.contactLink != null) {
|
||||
SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
|
||||
QRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
|
||||
@ -316,36 +318,40 @@ fun ChatInfoLayout(
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
|
||||
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.network_status),
|
||||
contactNetworkStatus.statusExplanation
|
||||
)}) {
|
||||
NetworkStatusRow(contactNetworkStatus)
|
||||
}
|
||||
if (cStats != null) {
|
||||
SwitchAddressButton(
|
||||
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited,
|
||||
switchAddress = switchContactAddress
|
||||
)
|
||||
if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) {
|
||||
AbortSwitchAddressButton(
|
||||
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || cStats.ratchetSyncSendProhibited,
|
||||
abortSwitchAddress = abortSwitchContactAddress
|
||||
if (contact.ready) {
|
||||
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
|
||||
SectionItemView({
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.network_status),
|
||||
contactNetworkStatus.statusExplanation
|
||||
)
|
||||
}) {
|
||||
NetworkStatusRow(contactNetworkStatus)
|
||||
}
|
||||
val rcvServers = cStats.rcvQueuesInfo.map { it.rcvServer }
|
||||
if (rcvServers.isNotEmpty()) {
|
||||
SimplexServers(stringResource(MR.strings.receiving_via), rcvServers)
|
||||
}
|
||||
val sndServers = cStats.sndQueuesInfo.map { it.sndServer }
|
||||
if (sndServers.isNotEmpty()) {
|
||||
SimplexServers(stringResource(MR.strings.sending_via), sndServers)
|
||||
if (cStats != null) {
|
||||
SwitchAddressButton(
|
||||
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited,
|
||||
switchAddress = switchContactAddress
|
||||
)
|
||||
if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) {
|
||||
AbortSwitchAddressButton(
|
||||
disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null && !it.canAbortSwitch } || cStats.ratchetSyncSendProhibited,
|
||||
abortSwitchAddress = abortSwitchContactAddress
|
||||
)
|
||||
}
|
||||
val rcvServers = cStats.rcvQueuesInfo.map { it.rcvServer }
|
||||
if (rcvServers.isNotEmpty()) {
|
||||
SimplexServers(stringResource(MR.strings.receiving_via), rcvServers)
|
||||
}
|
||||
val sndServers = cStats.sndQueuesInfo.map { it.sndServer }
|
||||
if (sndServers.isNotEmpty()) {
|
||||
SimplexServers(stringResource(MR.strings.sending_via), sndServers)
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
}
|
||||
SectionDividerSpaced()
|
||||
|
||||
SectionView {
|
||||
ClearChatButton(clearChat)
|
||||
DeleteContactButton(deleteContact)
|
||||
|
@ -114,7 +114,18 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
unreadCount,
|
||||
composeState,
|
||||
composeView = {
|
||||
if (chat.chatInfo.sendMsgEnabled) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (chat.chatInfo is ChatInfo.Direct && !chat.chatInfo.contact.ready && !chat.chatInfo.contact.nextSendGrpInv) {
|
||||
Text(
|
||||
generalGetString(MR.strings.contact_connection_pending),
|
||||
Modifier.padding(top = 4.dp),
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
ComposeView(
|
||||
chatModel, chat, composeState, attachmentOption,
|
||||
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }
|
||||
@ -145,7 +156,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
var preloadedLink: Pair<String, GroupMemberRole>? = null
|
||||
if (chat.chatInfo is ChatInfo.Direct) {
|
||||
preloadedContactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
|
||||
preloadedCode = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId).second
|
||||
preloadedCode = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)?.second
|
||||
} else if (chat.chatInfo is ChatInfo.Group) {
|
||||
setGroupMembers(chat.chatInfo.groupInfo, chatModel)
|
||||
preloadedLink = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId)
|
||||
@ -158,7 +169,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
|
||||
contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
|
||||
preloadedContactInfo = contactInfo
|
||||
code = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId).second
|
||||
code = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)?.second
|
||||
preloadedCode = code
|
||||
}
|
||||
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close)
|
||||
@ -183,12 +194,8 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
val (_, code) = if (member.memberActive) {
|
||||
try {
|
||||
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
member to null
|
||||
}
|
||||
val memCode = chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
|
||||
member to memCode?.second
|
||||
} else {
|
||||
member to null
|
||||
}
|
||||
@ -280,6 +287,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
chatModel.controller.allowFeatureToContact(contact, feature, param)
|
||||
}
|
||||
},
|
||||
openDirectChat = { contactId ->
|
||||
withApi {
|
||||
openDirectChat(contactId, chatModel)
|
||||
}
|
||||
},
|
||||
updateContactStats = { contact ->
|
||||
withApi {
|
||||
val r = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
|
||||
@ -409,6 +421,7 @@ fun ChatLayout(
|
||||
startCall: (CallMediaType) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
updateContactStats: (Contact) -> Unit,
|
||||
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
|
||||
syncContactConnection: (Contact) -> Unit,
|
||||
@ -485,7 +498,7 @@ fun ChatLayout(
|
||||
ChatItemsList(
|
||||
chat, unreadCount, composeState, chatItems, searchValue,
|
||||
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature,
|
||||
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat,
|
||||
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
|
||||
setReaction, showItemDetails, markRead, setFloatingButton, onComposed,
|
||||
)
|
||||
@ -534,15 +547,22 @@ fun ChatInfoToolbar(
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Audio)
|
||||
}) {
|
||||
Icon(painterResource(MR.images.ic_call_500), stringResource(MR.strings.icon_descr_more_button), tint = MaterialTheme.colors.primary)
|
||||
},
|
||||
enabled = chat.chatInfo.contact.ready) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_500),
|
||||
stringResource(MR.strings.icon_descr_more_button),
|
||||
tint = if (chat.chatInfo.contact.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Video)
|
||||
})
|
||||
if (chat.chatInfo.contact.ready) {
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Video)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers && !chat.chatInfo.incognito) {
|
||||
barButtons.add {
|
||||
@ -554,20 +574,22 @@ fun ChatInfoToolbar(
|
||||
}
|
||||
}
|
||||
}
|
||||
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
|
||||
menuItems.add {
|
||||
ItemAction(
|
||||
if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat),
|
||||
if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications),
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
// Just to make a delay before changing state of ntfsEnabled, otherwise it will redraw menu item with new value before closing the menu
|
||||
scope.launch {
|
||||
delay(200)
|
||||
changeNtfsState(!ntfsEnabled.value, ntfsEnabled)
|
||||
if ((chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.ready) || chat.chatInfo is ChatInfo.Group) {
|
||||
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
|
||||
menuItems.add {
|
||||
ItemAction(
|
||||
if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat),
|
||||
if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications),
|
||||
onClick = {
|
||||
showMenu.value = false
|
||||
// Just to make a delay before changing state of ntfsEnabled, otherwise it will redraw menu item with new value before closing the menu
|
||||
scope.launch {
|
||||
delay(200)
|
||||
changeNtfsState(!ntfsEnabled.value, ntfsEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
barButtons.add {
|
||||
@ -661,6 +683,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
joinGroup: (Long) -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
updateContactStats: (Contact) -> Unit,
|
||||
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
|
||||
syncContactConnection: (Contact) -> Unit,
|
||||
@ -808,7 +831,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
) {
|
||||
MemberImage(member)
|
||||
}
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames)
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -817,7 +840,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
.padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp)
|
||||
.then(swipeableModifier)
|
||||
) {
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames)
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, getConnectedMemberNames = ::getConnectedMemberNames)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -827,7 +850,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp)
|
||||
.then(swipeableModifier)
|
||||
) {
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = {}, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
|
||||
}
|
||||
}
|
||||
} else { // direct message
|
||||
@ -838,7 +861,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
|
||||
).then(swipeableModifier)
|
||||
) {
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
|
||||
ChatItemView(chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, deleteMessage = deleteMessage, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1263,6 +1286,7 @@ fun PreviewChatLayout() {
|
||||
startCall = {},
|
||||
acceptCall = { _ -> },
|
||||
acceptFeature = { _, _, _ -> },
|
||||
openDirectChat = { _ -> },
|
||||
updateContactStats = { },
|
||||
updateMemberStats = { _, _ -> },
|
||||
syncContactConnection = { },
|
||||
@ -1330,6 +1354,7 @@ fun PreviewGroupChatLayout() {
|
||||
startCall = {},
|
||||
acceptCall = { _ -> },
|
||||
acceptFeature = { _, _, _ -> },
|
||||
openDirectChat = { _ -> },
|
||||
updateContactStats = { },
|
||||
updateMemberStats = { _, _ -> },
|
||||
syncContactConnection = { },
|
||||
|
@ -0,0 +1,39 @@
|
||||
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 = CurrentColors.collectAsState().value.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))
|
||||
}
|
||||
}
|
@ -335,8 +335,6 @@ fun ComposeView(
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
|
||||
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? {
|
||||
val cInfo = chat.chatInfo
|
||||
val cs = composeState.value
|
||||
@ -358,6 +356,7 @@ fun ComposeView(
|
||||
MsgContent.MCText(msgText)
|
||||
}
|
||||
}
|
||||
|
||||
else -> MsgContent.MCText(msgText)
|
||||
}
|
||||
}
|
||||
@ -374,6 +373,14 @@ fun ComposeView(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendMemberContactInvitation() {
|
||||
val mc = checkLinkPreview()
|
||||
val contact = chatModel.controller.apiSendMemberContactInvitation(chat.chatInfo.apiId, mc)
|
||||
if (contact != null) {
|
||||
chatModel.updateContact(contact)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateMessage(ei: ChatItem, cInfo: ChatInfo, live: Boolean): ChatItem? {
|
||||
val oldMsgContent = ei.content.msgContent
|
||||
if (oldMsgContent != null) {
|
||||
@ -397,7 +404,10 @@ fun ComposeView(
|
||||
}
|
||||
clearCurrentDraft()
|
||||
|
||||
if (cs.contextItem is ComposeContextItem.EditingItem) {
|
||||
if (chat.nextSendGrpInv) {
|
||||
sendMemberContactInvitation()
|
||||
sent = null
|
||||
} else if (cs.contextItem is ComposeContextItem.EditingItem) {
|
||||
val ei = cs.contextItem.chatItem
|
||||
sent = updateMessage(ei, cInfo, live)
|
||||
} else if (liveMessage != null && liveMessage.sent) {
|
||||
@ -655,9 +665,14 @@ fun ComposeView(
|
||||
}
|
||||
|
||||
val userCanSend = rememberUpdatedState(chat.userCanSend)
|
||||
val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled)
|
||||
val userIsObserver = rememberUpdatedState(chat.userIsObserver)
|
||||
val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv)
|
||||
|
||||
Column {
|
||||
if (nextSendGrpInv.value) {
|
||||
ComposeContextInvitingContactMemberView()
|
||||
}
|
||||
if (composeState.value.preview !is ComposePreview.VoicePreview || composeState.value.editing) {
|
||||
contextItemView()
|
||||
when {
|
||||
@ -690,15 +705,21 @@ fun ComposeView(
|
||||
} else {
|
||||
showChooseAttachment
|
||||
}
|
||||
val attachmentEnabled =
|
||||
!composeState.value.attachmentDisabled
|
||||
&& sendMsgEnabled.value
|
||||
&& userCanSend.value
|
||||
&& !isGroupAndProhibitedFiles
|
||||
&& !nextSendGrpInv.value
|
||||
IconButton(
|
||||
attachmentClicked,
|
||||
Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else 7.dp),
|
||||
enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value
|
||||
enabled = attachmentEnabled
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_attach_file_filled_500),
|
||||
contentDescription = stringResource(MR.strings.attach),
|
||||
tint = if (!composeState.value.attachmentDisabled && userCanSend.value && !isGroupAndProhibitedFiles) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
tint = if (attachmentEnabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.clip(CircleShape)
|
||||
@ -774,6 +795,8 @@ fun ComposeView(
|
||||
recState,
|
||||
chat.chatInfo is ChatInfo.Direct,
|
||||
liveMessageAlertShown = chatModel.controller.appPrefs.liveMessageAlertShown,
|
||||
sendMsgEnabled = sendMsgEnabled.value,
|
||||
nextSendGrpInv = nextSendGrpInv.value,
|
||||
needToAllowVoiceToContact,
|
||||
allowedVoiceByPrefs,
|
||||
allowVoiceToContact = ::allowVoiceToContact,
|
||||
|
@ -37,6 +37,8 @@ fun SendMsgView(
|
||||
recState: MutableState<RecordingState>,
|
||||
isDirectChat: Boolean,
|
||||
liveMessageAlertShown: SharedPreference<Boolean>,
|
||||
sendMsgEnabled: Boolean,
|
||||
nextSendGrpInv: Boolean,
|
||||
needToAllowVoiceToContact: Boolean,
|
||||
allowedVoiceByPrefs: Boolean,
|
||||
userIsObserver: Boolean,
|
||||
@ -74,16 +76,16 @@ fun SendMsgView(
|
||||
false
|
||||
}
|
||||
}
|
||||
val showVoiceButton = cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
|
||||
val showVoiceButton = !nextSendGrpInv && cs.message.isEmpty() && showVoiceRecordIcon && !composeState.value.editing &&
|
||||
cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
|
||||
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
|
||||
PlatformTextField(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage) {
|
||||
PlatformTextField(composeState, sendMsgEnabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage) {
|
||||
if (!cs.inProgress) {
|
||||
sendMessage(null)
|
||||
}
|
||||
}
|
||||
// Disable clicks on text field
|
||||
if (cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) {
|
||||
if (!sendMsgEnabled || cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) {
|
||||
Box(
|
||||
Modifier
|
||||
.matchParentSize()
|
||||
@ -110,7 +112,7 @@ fun SendMsgView(
|
||||
}
|
||||
when {
|
||||
progressByTimeout -> ProgressIndicator()
|
||||
showVoiceButton -> {
|
||||
showVoiceButton && sendMsgEnabled -> {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val stopRecOnNextClick = remember { mutableStateOf(false) }
|
||||
when {
|
||||
@ -150,7 +152,7 @@ fun SendMsgView(
|
||||
else -> {
|
||||
val cs = composeState.value
|
||||
val icon = if (cs.editing || cs.liveMessage != null) painterResource(MR.images.ic_check_filled) else painterResource(MR.images.ic_arrow_upward)
|
||||
val disabled = !cs.sendEnabled() ||
|
||||
val disabled = !sendMsgEnabled || !cs.sendEnabled() ||
|
||||
(!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
|
||||
cs.endLiveDisabled
|
||||
val showDropdown = rememberSaveable { mutableStateOf(false) }
|
||||
@ -159,7 +161,7 @@ fun SendMsgView(
|
||||
fun MenuItems(): List<@Composable () -> Unit> {
|
||||
val menuItems = mutableListOf<@Composable () -> Unit>()
|
||||
|
||||
if (cs.liveMessage == null && !cs.editing) {
|
||||
if (cs.liveMessage == null && !cs.editing && !nextSendGrpInv || sendMsgEnabled) {
|
||||
if (
|
||||
cs.preview !is ComposePreview.VoicePreview &&
|
||||
cs.contextItem is ComposeContextItem.NoContextItem &&
|
||||
@ -599,6 +601,8 @@ fun PreviewSendMsgView() {
|
||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||
isDirectChat = true,
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
sendMsgEnabled = true,
|
||||
nextSendGrpInv = false,
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = true,
|
||||
userIsObserver = false,
|
||||
@ -630,6 +634,8 @@ fun PreviewSendMsgViewEditing() {
|
||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||
isDirectChat = true,
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
sendMsgEnabled = true,
|
||||
nextSendGrpInv = false,
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = true,
|
||||
userIsObserver = false,
|
||||
@ -661,6 +667,8 @@ fun PreviewSendMsgViewInProgress() {
|
||||
recState = remember { mutableStateOf(RecordingState.NotStarted) },
|
||||
isDirectChat = true,
|
||||
liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
|
||||
sendMsgEnabled = true,
|
||||
nextSendGrpInv = false,
|
||||
needToAllowVoiceToContact = false,
|
||||
allowedVoiceByPrefs = true,
|
||||
userIsObserver = false,
|
||||
|
@ -76,12 +76,8 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
|
||||
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
|
||||
val stats = r?.second
|
||||
val (_, code) = if (member.memberActive) {
|
||||
try {
|
||||
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, e.stackTraceToString())
|
||||
member to null
|
||||
}
|
||||
val memCode = chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
|
||||
member to memCode?.second
|
||||
} else {
|
||||
member to null
|
||||
}
|
||||
|
@ -35,6 +35,7 @@ import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItem
|
||||
import chat.simplex.common.model.GroupInfo
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chatlist.openChat
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@ -52,6 +53,8 @@ fun GroupMemberInfoView(
|
||||
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
|
||||
val connStats = remember { mutableStateOf(connectionStats) }
|
||||
val developerTools = chatModel.controller.appPrefs.developerTools.get()
|
||||
var progressIndicator by remember { mutableStateOf(false) }
|
||||
|
||||
if (chat != null) {
|
||||
val newRole = remember { mutableStateOf(member.memberRole) }
|
||||
GroupMemberInfoLayout(
|
||||
@ -76,6 +79,20 @@ fun GroupMemberInfoView(
|
||||
}
|
||||
}
|
||||
},
|
||||
createMemberContact = {
|
||||
withApi {
|
||||
progressIndicator = true
|
||||
val memberContact = chatModel.controller.apiCreateMemberContact(groupInfo.apiId, member.groupMemberId)
|
||||
if (memberContact != null) {
|
||||
val memberChat = Chat(ChatInfo.Direct(memberContact), chatItems = arrayListOf())
|
||||
chatModel.addChat(memberChat)
|
||||
openChat(memberChat, chatModel)
|
||||
closeAll()
|
||||
chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected())
|
||||
}
|
||||
progressIndicator = false
|
||||
}
|
||||
},
|
||||
connectViaAddress = { connReqUri ->
|
||||
connectViaMemberAddressAlert(connReqUri)
|
||||
},
|
||||
@ -170,6 +187,10 @@ fun GroupMemberInfoView(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (progressIndicator) {
|
||||
ProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -201,6 +222,7 @@ fun GroupMemberInfoLayout(
|
||||
connectionCode: String?,
|
||||
getContactChat: (Long) -> Chat?,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
createMemberContact: () -> Unit,
|
||||
connectViaAddress: (String) -> Unit,
|
||||
removeMember: () -> Unit,
|
||||
onRoleSelected: (GroupMemberRole) -> Unit,
|
||||
@ -237,9 +259,13 @@ fun GroupMemberInfoLayout(
|
||||
|
||||
if (member.memberActive) {
|
||||
SectionView {
|
||||
if (contactId != null) {
|
||||
if (knownDirectChat(contactId) != null || groupInfo.fullGroupPreferences.directMessages.on) {
|
||||
if (contactId != null && knownDirectChat(contactId) != null) {
|
||||
OpenChatButton(onClick = { openDirectChat(contactId) })
|
||||
} else if (groupInfo.fullGroupPreferences.directMessages.on) {
|
||||
if (contactId != null) {
|
||||
OpenChatButton(onClick = { openDirectChat(contactId) })
|
||||
} else if (member.activeConn?.peerChatVRange?.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) == true) {
|
||||
OpenChatButton(onClick = { createMemberContact() })
|
||||
}
|
||||
}
|
||||
if (connectionCode != null) {
|
||||
@ -498,6 +524,7 @@ fun PreviewGroupMemberInfoLayout() {
|
||||
connectionCode = "123",
|
||||
getContactChat = { Chat.sampleData },
|
||||
openDirectChat = {},
|
||||
createMemberContact = {},
|
||||
connectViaAddress = {},
|
||||
removeMember = {},
|
||||
onRoleSelected = {},
|
||||
|
@ -0,0 +1,70 @@
|
||||
package chat.simplex.common.views.chat.item
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun CIMemberCreatedContactView(
|
||||
chatItem: ChatItem,
|
||||
openDirectChat: (Long) -> Unit
|
||||
) {
|
||||
fun eventText(): AnnotatedString {
|
||||
val memberDisplayName = chatItem.memberDisplayName
|
||||
return if (memberDisplayName != null) {
|
||||
buildAnnotatedString {
|
||||
withStyle(chatEventStyle) { append(memberDisplayName) }
|
||||
append(" ")
|
||||
withStyle(chatEventStyle) { append(chatItem.content.text) }
|
||||
}
|
||||
} else {
|
||||
buildAnnotatedString {
|
||||
withStyle(chatEventStyle) { append(chatItem.content.text) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier.padding(horizontal = 6.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (chatItem.chatDir is CIDirection.GroupRcv && chatItem.chatDir.groupMember.memberContactId != null) {
|
||||
val openChatStyle = SpanStyle(color = MaterialTheme.colors.primary, fontSize = 12.sp)
|
||||
val annotatedText = buildAnnotatedString {
|
||||
append(eventText())
|
||||
append(" ")
|
||||
withAnnotation(tag = "Open", annotation = "Open") {
|
||||
withStyle(openChatStyle) { append(generalGetString(MR.strings.rcv_group_event_open_chat) + " ") }
|
||||
}
|
||||
withStyle(chatEventStyle) { append(chatItem.timestampText) }
|
||||
}
|
||||
|
||||
fun open(offset: Int): Boolean = annotatedText.getStringAnnotations(tag = "Open", start = offset, end = offset).isNotEmpty()
|
||||
ClickableText(
|
||||
annotatedText,
|
||||
onClick = {
|
||||
if (open(it)) {
|
||||
openDirectChat(chatItem.chatDir.groupMember.memberContactId)
|
||||
}
|
||||
},
|
||||
shouldConsumeEvent = ::open
|
||||
)
|
||||
} else {
|
||||
val annotatedText = buildAnnotatedString {
|
||||
append(eventText())
|
||||
append(" ")
|
||||
withStyle(chatEventStyle) { append(chatItem.timestampText) }
|
||||
}
|
||||
Text(annotatedText)
|
||||
}
|
||||
}
|
||||
}
|
@ -54,6 +54,7 @@ fun ChatItemView(
|
||||
acceptCall: (Contact) -> Unit,
|
||||
scrollToItem: (Long) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
updateContactStats: (Contact) -> Unit,
|
||||
updateMemberStats: (GroupInfo, GroupMember) -> Unit,
|
||||
syncContactConnection: (Contact) -> Unit,
|
||||
@ -348,6 +349,7 @@ fun ChatItemView(
|
||||
is CIContent.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
|
||||
is CIContent.RcvGroupEventContent -> when (c.rcvGroupEvent) {
|
||||
is RcvGroupEvent.MemberConnected -> CIEventView(membersConnectedItemText())
|
||||
is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat)
|
||||
else -> EventItemView()
|
||||
}
|
||||
is CIContent.SndGroupEventContent -> EventItemView()
|
||||
@ -572,6 +574,7 @@ fun PreviewChatItemView() {
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _, _ -> },
|
||||
openDirectChat = { _ -> },
|
||||
updateContactStats = { },
|
||||
updateMemberStats = { _, _ -> },
|
||||
syncContactConnection = { },
|
||||
@ -601,6 +604,7 @@ fun PreviewChatItemViewDeletedContent() {
|
||||
acceptCall = { _ -> },
|
||||
scrollToItem = {},
|
||||
acceptFeature = { _, _, _ -> },
|
||||
openDirectChat = { _ -> },
|
||||
updateContactStats = { },
|
||||
updateMemberStats = { _, _ -> },
|
||||
syncContactConnection = { },
|
||||
|
@ -103,11 +103,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
fun directChatAction(chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||
if (chatInfo.ready) {
|
||||
withBGApi { openChat(chatInfo, chatModel) }
|
||||
} else {
|
||||
pendingContactAlertDialog(chatInfo, chatModel)
|
||||
}
|
||||
withBGApi { openChat(chatInfo, chatModel) }
|
||||
}
|
||||
|
||||
fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
@ -118,15 +114,28 @@ fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId)
|
||||
suspend fun openDirectChat(contactId: Long, chatModel: ChatModel) {
|
||||
val chat = chatModel.controller.apiGetChat(ChatType.Direct, contactId)
|
||||
if (chat != null) {
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatItems.addAll(chat.chatItems)
|
||||
chatModel.chatId.value = chatInfo.id
|
||||
chatModel.chatId.value = "@$contactId"
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId)
|
||||
if (chat != null) {
|
||||
openChat(chat, chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun openChat(chat: Chat, chatModel: ChatModel) {
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatItems.addAll(chat.chatItems)
|
||||
chatModel.chatId.value = chat.chatInfo.id
|
||||
}
|
||||
|
||||
suspend fun apiLoadPrevMessages(chatInfo: ChatInfo, chatModel: ChatModel, beforeChatItemId: Long, search: String) {
|
||||
val pagination = ChatPagination.Before(beforeChatItemId, ChatPagination.PRELOAD_COUNT)
|
||||
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId, pagination, search) ?: return
|
||||
|
@ -172,7 +172,9 @@ fun ChatPreviewView(
|
||||
} else {
|
||||
when (cInfo) {
|
||||
is ChatInfo.Direct ->
|
||||
if (!cInfo.ready) {
|
||||
if (cInfo.contact.nextSendGrpInv) {
|
||||
Text(stringResource(MR.strings.member_contact_send_direct_message), color = MaterialTheme.colors.secondary)
|
||||
} else if (!cInfo.ready) {
|
||||
Text(stringResource(MR.strings.contact_connection_pending), color = MaterialTheme.colors.secondary)
|
||||
}
|
||||
is ChatInfo.Group ->
|
||||
|
@ -272,6 +272,7 @@
|
||||
<string name="this_text_is_available_in_settings">This text is available in settings</string>
|
||||
<string name="your_chats">Chats</string>
|
||||
<string name="contact_connection_pending">connecting…</string>
|
||||
<string name="member_contact_send_direct_message">send direct message</string>
|
||||
<string name="group_preview_you_are_invited">you are invited to group</string>
|
||||
<string name="group_preview_join_as">join as %s</string>
|
||||
<string name="group_connection_pending">connecting…</string>
|
||||
@ -304,6 +305,7 @@
|
||||
<string name="observer_cant_send_message_desc">Please contact group admin.</string>
|
||||
<string name="files_and_media_prohibited">Files and media prohibited!</string>
|
||||
<string name="only_owners_can_enable_files_and_media">Only group owners can enable files and media.</string>
|
||||
<string name="compose_send_direct_message_to_connect">Send direct message to connect</string>
|
||||
|
||||
<!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
|
||||
<string name="image_descr">Image</string>
|
||||
@ -1114,6 +1116,7 @@
|
||||
<string name="rcv_group_event_group_deleted">deleted group</string>
|
||||
<string name="rcv_group_event_updated_group_profile">updated group profile</string>
|
||||
<string name="rcv_group_event_invited_via_your_group_link">invited via your group link</string>
|
||||
<string name="rcv_group_event_member_created_contact">connected directly</string>
|
||||
<string name="snd_group_event_changed_member_role">you changed role of %s to %s</string>
|
||||
<string name="snd_group_event_changed_role_for_yourself">you changed role for yourself to %s</string>
|
||||
<string name="snd_group_event_member_deleted">you removed %1$s</string>
|
||||
@ -1124,6 +1127,8 @@
|
||||
<string name="rcv_group_event_3_members_connected">%s, %s and %s connected</string>
|
||||
<string name="rcv_group_event_n_members_connected">%s, %s and %d other members connected</string>
|
||||
|
||||
<string name="rcv_group_event_open_chat">Open</string>
|
||||
|
||||
<!-- Conn event chat items -->
|
||||
<string name="rcv_conn_event_switch_queue_phase_completed">changed address for you</string>
|
||||
<string name="rcv_conn_event_switch_queue_phase_changing">changing address…</string>
|
||||
@ -1201,6 +1206,8 @@
|
||||
<string name="error_creating_link_for_group">Error creating group link</string>
|
||||
<string name="error_updating_link_for_group">Error updating group link</string>
|
||||
<string name="error_deleting_link_for_group">Error deleting group link</string>
|
||||
<string name="error_creating_member_contact">Error creating member contact</string>
|
||||
<string name="error_sending_message_contact_invitation">Sending message contact invitation</string>
|
||||
<string name="only_group_owners_can_change_prefs">Only group owners can change group preferences.</string>
|
||||
<string name="address_section_title">Address</string>
|
||||
<string name="share_address">Share address</string>
|
||||
|
@ -33,6 +33,7 @@ import kotlin.text.substring
|
||||
@Composable
|
||||
actual fun PlatformTextField(
|
||||
composeState: MutableState<ComposeState>,
|
||||
sendMsgEnabled: Boolean,
|
||||
textStyle: MutableState<TextStyle>,
|
||||
showDeleteTextButton: MutableState<Boolean>,
|
||||
userIsObserver: Boolean,
|
||||
@ -42,6 +43,7 @@ actual fun PlatformTextField(
|
||||
) {
|
||||
val cs = composeState.value
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
val padding = PaddingValues(12.dp, 12.dp, 45.dp, 0.dp)
|
||||
LaunchedEffect(cs.contextItem) {
|
||||
@ -51,6 +53,13 @@ actual fun PlatformTextField(
|
||||
delay(50)
|
||||
keyboard?.show()
|
||||
}
|
||||
LaunchedEffect(sendMsgEnabled) {
|
||||
if (!sendMsgEnabled) {
|
||||
focusManager.clearFocus()
|
||||
delay(50)
|
||||
keyboard?.hide()
|
||||
}
|
||||
}
|
||||
val isRtl = remember(cs.message) { isRtl(cs.message.subSequence(0, min(50, cs.message.length))) }
|
||||
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message)) }
|
||||
val textFieldValue = textFieldValueState.copy(text = cs.message)
|
||||
@ -113,7 +122,8 @@ actual fun PlatformTextField(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
)
|
||||
showDeleteTextButton.value = cs.message.split("\n").size >= 4 && !cs.inProgress
|
||||
if (composeState.value.preview is ComposePreview.VoicePreview) {
|
||||
|
Loading…
Reference in New Issue
Block a user