android: create contacts with group members (#3078)

This commit is contained in:
spaced4ndy 2023-09-20 17:58:10 +04:00 committed by GitHub
parent 648a9761f9
commit ae6996b2ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 430 additions and 112 deletions

View File

@ -44,6 +44,7 @@ import java.net.URI
@Composable @Composable
actual fun PlatformTextField( actual fun PlatformTextField(
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
sendMsgEnabled: Boolean,
textStyle: MutableState<TextStyle>, textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>, showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean, userIsObserver: Boolean,
@ -60,6 +61,7 @@ actual fun PlatformTextField(
val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() } val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() }
val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() } val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() }
var showKeyboard by remember { mutableStateOf(false) } var showKeyboard by remember { mutableStateOf(false) }
var freeFocus by remember { mutableStateOf(false) }
LaunchedEffect(cs.contextItem) { LaunchedEffect(cs.contextItem) {
if (cs.contextItem is ComposeContextItem.QuotedItem) { if (cs.contextItem is ComposeContextItem.QuotedItem) {
delay(100) delay(100)
@ -70,6 +72,11 @@ actual fun PlatformTextField(
showKeyboard = true showKeyboard = true
} }
} }
LaunchedEffect(sendMsgEnabled) {
if (!sendMsgEnabled) {
freeFocus = true
}
}
AndroidView(modifier = Modifier, factory = { AndroidView(modifier = Modifier, factory = {
val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) { val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) {
@ -142,6 +149,11 @@ actual fun PlatformTextField(
imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT) imm.showSoftInput(it, InputMethodManager.SHOW_IMPLICIT)
showKeyboard = false showKeyboard = false
} }
if (freeFocus) {
it.clearFocus()
hideKeyboard(it)
freeFocus = false
}
showDeleteTextButton.value = it.lineCount >= 4 && !cs.inProgress showDeleteTextButton.value = it.lineCount >= 4 && !cs.inProgress
} }
if (composeState.value.preview is ComposePreview.VoicePreview) { if (composeState.value.preview is ComposePreview.VoicePreview) {

View File

@ -606,10 +606,13 @@ data class Chat (
val userCanSend: Boolean val userCanSend: Boolean
get() = when (chatInfo) { get() = when (chatInfo) {
is ChatInfo.Direct -> true is ChatInfo.Direct -> true
is ChatInfo.Group -> { is ChatInfo.Group -> chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Member
val m = chatInfo.groupInfo.membership else -> false
m.memberActive && m.memberRole >= GroupMemberRole.Member }
}
val nextSendGrpInv: Boolean
get() = when (chatInfo) {
is ChatInfo.Direct -> chatInfo.contact.nextSendGrpInv
else -> false else -> false
} }
@ -799,13 +802,18 @@ data class Contact(
val userPreferences: ChatPreferences, val userPreferences: ChatPreferences,
val mergedPreferences: ContactUserPreferences, val mergedPreferences: ContactUserPreferences,
override val createdAt: Instant, override val createdAt: Instant,
override val updatedAt: Instant override val updatedAt: Instant,
val contactGroupMemberId: Long? = null,
val contactGrpInvSent: Boolean
): SomeChat, NamedChat { ): SomeChat, NamedChat {
override val chatType get() = ChatType.Direct override val chatType get() = ChatType.Direct
override val id get() = "@$contactId" override val id get() = "@$contactId"
override val apiId get() = contactId override val apiId get() = contactId
override val ready get() = activeConn.connStatus == ConnStatus.Ready 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 ntfsEnabled get() = chatSettings.enableNtfs
override val incognito get() = contactConnIncognito override val incognito get() = contactConnIncognito
override fun featureEnabled(feature: ChatFeature) = when (feature) { override fun featureEnabled(feature: ChatFeature) = when (feature) {
@ -856,7 +864,8 @@ data class Contact(
userPreferences = ChatPreferences.sampleData, userPreferences = ChatPreferences.sampleData,
mergedPreferences = ContactUserPreferences.sampleData, mergedPreferences = ContactUserPreferences.sampleData,
createdAt = Clock.System.now(), createdAt = Clock.System.now(),
updatedAt = Clock.System.now() updatedAt = Clock.System.now(),
contactGrpInvSent = false
) )
} }
} }
@ -881,6 +890,7 @@ class ContactSubStatus(
data class Connection( data class Connection(
val connId: Long, val connId: Long,
val agentConnId: String, val agentConnId: String,
val peerChatVRange: VersionRange,
val connStatus: ConnStatus, val connStatus: ConnStatus,
val connLevel: Int, val connLevel: Int,
val viaGroupLink: Boolean, val viaGroupLink: Boolean,
@ -890,10 +900,17 @@ data class Connection(
) { ) {
val id: ChatId get() = ":$connId" val id: ChatId get() = ":$connId"
companion object { 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 @Serializable
data class SecurityCode(val securityCode: String, val verifiedAt: Instant) data class SecurityCode(val securityCode: String, val verifiedAt: Instant)
@ -1224,6 +1241,7 @@ class MemberSubError (
@Serializable @Serializable
class UserContactRequest ( class UserContactRequest (
val contactRequestId: Long, val contactRequestId: Long,
val cReqChatVRange: VersionRange,
override val localDisplayName: String, override val localDisplayName: String,
val profile: Profile, val profile: Profile,
override val createdAt: Instant, override val createdAt: Instant,
@ -1246,6 +1264,7 @@ class UserContactRequest (
companion object { companion object {
val sampleData = UserContactRequest( val sampleData = UserContactRequest(
contactRequestId = 1, contactRequestId = 1,
cReqChatVRange = VersionRange(1, 1),
localDisplayName = "alice", localDisplayName = "alice",
profile = Profile.sampleData, profile = Profile.sampleData,
createdAt = Clock.System.now(), createdAt = Clock.System.now(),
@ -1465,6 +1484,7 @@ data class ChatItem (
is RcvGroupEvent.GroupDeleted -> showNtfDir is RcvGroupEvent.GroupDeleted -> showNtfDir
is RcvGroupEvent.GroupUpdated -> false is RcvGroupEvent.GroupUpdated -> false
is RcvGroupEvent.InvitedViaGroupLink -> false is RcvGroupEvent.InvitedViaGroupLink -> false
is RcvGroupEvent.MemberCreatedContact -> false
} }
is CIContent.SndGroupEventContent -> showNtfDir is CIContent.SndGroupEventContent -> showNtfDir
is CIContent.RcvConnEventContent -> false is CIContent.RcvConnEventContent -> false
@ -2464,6 +2484,7 @@ sealed class RcvGroupEvent() {
@Serializable @SerialName("groupDeleted") class GroupDeleted(): RcvGroupEvent() @Serializable @SerialName("groupDeleted") class GroupDeleted(): RcvGroupEvent()
@Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): RcvGroupEvent() @Serializable @SerialName("groupUpdated") class GroupUpdated(val groupProfile: GroupProfile): RcvGroupEvent()
@Serializable @SerialName("invitedViaGroupLink") class InvitedViaGroupLink(): RcvGroupEvent() @Serializable @SerialName("invitedViaGroupLink") class InvitedViaGroupLink(): RcvGroupEvent()
@Serializable @SerialName("memberCreatedContact") class MemberCreatedContact(): RcvGroupEvent()
val text: String get() = when (this) { val text: String get() = when (this) {
is MemberAdded -> String.format(generalGetString(MR.strings.rcv_group_event_member_added), profile.profileViewName) 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 GroupDeleted -> generalGetString(MR.strings.rcv_group_event_group_deleted)
is GroupUpdated -> generalGetString(MR.strings.rcv_group_event_updated_group_profile) 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 InvitedViaGroupLink -> generalGetString(MR.strings.rcv_group_event_invited_via_your_group_link)
is MemberCreatedContact -> generalGetString(MR.strings.rcv_group_event_member_created_contact)
} }
} }

View File

@ -26,6 +26,12 @@ import java.util.Date
typealias ChatCtrl = Long 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 { enum class CallOnLockScreen {
DISABLE, DISABLE,
SHOW, SHOW,
@ -784,16 +790,18 @@ object ChatController {
return null return null
} }
suspend fun apiGetContactCode(contactId: Long): Pair<Contact, String> { suspend fun apiGetContactCode(contactId: Long): Pair<Contact, String>? {
val r = sendCmd(CC.APIGetContactCode(contactId)) val r = sendCmd(CC.APIGetContactCode(contactId))
if (r is CR.ContactCode) return r.contact to r.connectionCode 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)) val r = sendCmd(CC.APIGetGroupMemberCode(groupId, groupMemberId))
if (r is CR.GroupMemberCode) return r.member to r.connectionCode 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>? { 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) { suspend fun allowFeatureToContact(contact: Contact, feature: ChatFeature, param: Int? = null) {
val prefs = contact.mergedPreferences.toPreferences().setAllowed(feature, param = param) val prefs = contact.mergedPreferences.toPreferences().setAllowed(feature, param = param)
val toContact = apiSetContactPrefs(contact.contactId, prefs) val toContact = apiSetContactPrefs(contact.contactId, prefs)
@ -1527,6 +1559,10 @@ object ChatController {
if (active(r.user)) { if (active(r.user)) {
chatModel.updateGroup(r.toGroup) chatModel.updateGroup(r.toGroup)
} }
is CR.NewMemberContactReceivedInv ->
if (active(r.user)) {
chatModel.updateContact(r.contact)
}
is CR.RcvFileStart -> is CR.RcvFileStart ->
chatItemSimpleUpdate(r.user, r.chatItem) chatItemSimpleUpdate(r.user, r.chatItem)
is CR.RcvFileComplete -> is CR.RcvFileComplete ->
@ -1822,6 +1858,8 @@ sealed class CC {
class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC() class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC()
class APIDeleteGroupLink(val groupId: Long): CC() class APIDeleteGroupLink(val groupId: Long): CC()
class APIGetGroupLink(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 APIGetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol): CC()
class APISetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol, val servers: List<ServerCfg>): CC() class APISetUserProtoServers(val userId: Long, val serverProtocol: ServerProtocol, val servers: List<ServerCfg>): CC()
class APITestProtoServer(val userId: Long, val server: String): 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 APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}"
is APIDeleteGroupLink -> "/_delete link #$groupId" is APIDeleteGroupLink -> "/_delete link #$groupId"
is APIGetGroupLink -> "/_get 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 APIGetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()}"
is APISetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()} ${protoServersStr(servers)}" is APISetUserProtoServers -> "/_servers $userId ${serverProtocol.name.lowercase()} ${protoServersStr(servers)}"
is APITestProtoServer -> "/_server test $userId $server" is APITestProtoServer -> "/_server test $userId $server"
@ -2021,6 +2061,8 @@ sealed class CC {
is APIGroupLinkMemberRole -> "apiGroupLinkMemberRole" is APIGroupLinkMemberRole -> "apiGroupLinkMemberRole"
is APIDeleteGroupLink -> "apiDeleteGroupLink" is APIDeleteGroupLink -> "apiDeleteGroupLink"
is APIGetGroupLink -> "apiGetGroupLink" is APIGetGroupLink -> "apiGetGroupLink"
is APICreateMemberContact -> "apiCreateMemberContact"
is APISendMemberContactInvitation -> "apiSendMemberContactInvitation"
is APIGetUserProtoServers -> "apiGetUserProtoServers" is APIGetUserProtoServers -> "apiGetUserProtoServers"
is APISetUserProtoServers -> "apiSetUserProtoServers" is APISetUserProtoServers -> "apiSetUserProtoServers"
is APITestProtoServer -> "testProtoServer" 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("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("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("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 // receiving file events
@Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: UserRef, val chatItem: AChatItem): CR() @Serializable @SerialName("rcvFileAccepted") class RcvFileAccepted(val user: UserRef, val chatItem: AChatItem): CR()
@Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val user: UserRef, val rcvFileTransfer: RcvFileTransfer): CR() @Serializable @SerialName("rcvFileAcceptedSndCancelled") class RcvFileAcceptedSndCancelled(val user: UserRef, val rcvFileTransfer: RcvFileTransfer): CR()
@ -3438,6 +3483,9 @@ sealed class CR {
is GroupLinkCreated -> "groupLinkCreated" is GroupLinkCreated -> "groupLinkCreated"
is GroupLink -> "groupLink" is GroupLink -> "groupLink"
is GroupLinkDeleted -> "groupLinkDeleted" is GroupLinkDeleted -> "groupLinkDeleted"
is NewMemberContact -> "newMemberContact"
is NewMemberContactSentInv -> "newMemberContactSentInv"
is NewMemberContactReceivedInv -> "newMemberContactReceivedInv"
is RcvFileAcceptedSndCancelled -> "rcvFileAcceptedSndCancelled" is RcvFileAcceptedSndCancelled -> "rcvFileAcceptedSndCancelled"
is RcvFileAccepted -> "rcvFileAccepted" is RcvFileAccepted -> "rcvFileAccepted"
is RcvFileStart -> "rcvFileStart" is RcvFileStart -> "rcvFileStart"
@ -3563,6 +3611,9 @@ sealed class CR {
is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole") is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole")
is GroupLink -> 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 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 RcvFileAcceptedSndCancelled -> withUser(user, noDetails())
is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem)) is RcvFileAccepted -> withUser(user, json.encodeToString(chatItem))
is RcvFileStart -> withUser(user, json.encodeToString(chatItem)) is RcvFileStart -> withUser(user, json.encodeToString(chatItem))
@ -3820,6 +3871,7 @@ sealed class ChatErrorType {
is AgentCommandError -> "agentCommandError" is AgentCommandError -> "agentCommandError"
is InvalidFileDescription -> "invalidFileDescription" is InvalidFileDescription -> "invalidFileDescription"
is ConnectionIncognitoChangeProhibited -> "connectionIncognitoChangeProhibited" is ConnectionIncognitoChangeProhibited -> "connectionIncognitoChangeProhibited"
is PeerChatVRangeIncompatible -> "peerChatVRangeIncompatible"
is InternalError -> "internalError" is InternalError -> "internalError"
is CEException -> "exception $message" is CEException -> "exception $message"
} }
@ -3894,6 +3946,7 @@ sealed class ChatErrorType {
@Serializable @SerialName("agentCommandError") class AgentCommandError(val message: String): ChatErrorType() @Serializable @SerialName("agentCommandError") class AgentCommandError(val message: String): ChatErrorType()
@Serializable @SerialName("invalidFileDescription") class InvalidFileDescription(val message: String): ChatErrorType() @Serializable @SerialName("invalidFileDescription") class InvalidFileDescription(val message: String): ChatErrorType()
@Serializable @SerialName("connectionIncognitoChangeProhibited") object ConnectionIncognitoChangeProhibited: ChatErrorType() @Serializable @SerialName("connectionIncognitoChangeProhibited") object ConnectionIncognitoChangeProhibited: ChatErrorType()
@Serializable @SerialName("peerChatVRangeIncompatible") object PeerChatVRangeIncompatible: ChatErrorType()
@Serializable @SerialName("internalError") class InternalError(val message: String): ChatErrorType() @Serializable @SerialName("internalError") class InternalError(val message: String): ChatErrorType()
@Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType() @Serializable @SerialName("exception") class CEException(val message: String): ChatErrorType()
} }
@ -3922,6 +3975,7 @@ sealed class StoreError {
is GroupMemberNameNotFound -> "groupMemberNameNotFound" is GroupMemberNameNotFound -> "groupMemberNameNotFound"
is GroupMemberNotFound -> "groupMemberNotFound" is GroupMemberNotFound -> "groupMemberNotFound"
is GroupMemberNotFoundByMemberId -> "groupMemberNotFoundByMemberId" is GroupMemberNotFoundByMemberId -> "groupMemberNotFoundByMemberId"
is MemberContactGroupMemberNotFound -> "memberContactGroupMemberNotFound"
is GroupWithoutUser -> "groupWithoutUser" is GroupWithoutUser -> "groupWithoutUser"
is DuplicateGroupMember -> "duplicateGroupMember" is DuplicateGroupMember -> "duplicateGroupMember"
is GroupAlreadyJoined -> "groupAlreadyJoined" is GroupAlreadyJoined -> "groupAlreadyJoined"
@ -3979,6 +4033,7 @@ sealed class StoreError {
@Serializable @SerialName("groupMemberNameNotFound") class GroupMemberNameNotFound(val groupId: Long, val groupMemberName: String): StoreError() @Serializable @SerialName("groupMemberNameNotFound") class GroupMemberNameNotFound(val groupId: Long, val groupMemberName: String): StoreError()
@Serializable @SerialName("groupMemberNotFound") class GroupMemberNotFound(val groupMemberId: Long): StoreError() @Serializable @SerialName("groupMemberNotFound") class GroupMemberNotFound(val groupMemberId: Long): StoreError()
@Serializable @SerialName("groupMemberNotFoundByMemberId") class GroupMemberNotFoundByMemberId(val memberId: String): 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("groupWithoutUser") object GroupWithoutUser: StoreError()
@Serializable @SerialName("duplicateGroupMember") object DuplicateGroupMember: StoreError() @Serializable @SerialName("duplicateGroupMember") object DuplicateGroupMember: StoreError()
@Serializable @SerialName("groupAlreadyJoined") object GroupAlreadyJoined: StoreError() @Serializable @SerialName("groupAlreadyJoined") object GroupAlreadyJoined: StoreError()

View File

@ -8,6 +8,7 @@ import chat.simplex.common.views.chat.ComposeState
@Composable @Composable
expect fun PlatformTextField( expect fun PlatformTextField(
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
sendMsgEnabled: Boolean,
textStyle: MutableState<TextStyle>, textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>, showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean, userIsObserver: Boolean,

View File

@ -85,6 +85,8 @@ fun TerminalLayout(
recState = remember { mutableStateOf(RecordingState.NotStarted) }, recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = false, isDirectChat = false,
liveMessageAlertShown = SharedPreference(get = { false }, set = {}), liveMessageAlertShown = SharedPreference(get = { false }, set = {}),
sendMsgEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false, needToAllowVoiceToContact = false,
allowedVoiceByPrefs = false, allowedVoiceByPrefs = false,
userIsObserver = false, userIsObserver = false,

View File

@ -291,21 +291,23 @@ fun ChatInfoLayout(
SectionDividerSpaced() SectionDividerSpaced()
} }
SectionView { if (contact.ready) {
if (connectionCode != null) { SectionView {
VerifyCodeButton(contact.verified, verifyClicked) 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) SectionDividerSpaced()
SendReceiptsOption(currentUser, sendReceipts, setSendReceipts)
if (cStats != null && cStats.ratchetSyncAllowed) {
SynchronizeConnectionButton(syncContactConnection)
}
// } else if (developerTools) {
// SynchronizeConnectionButtonForce(syncContactConnectionForce)
// }
} }
SectionDividerSpaced()
if (contact.contactLink != null) { if (contact.contactLink != null) {
SectionView(stringResource(MR.strings.address_section_title).uppercase()) { SectionView(stringResource(MR.strings.address_section_title).uppercase()) {
QRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) QRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f))
@ -316,36 +318,40 @@ fun ChatInfoLayout(
SectionDividerSpaced() SectionDividerSpaced()
} }
SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) { if (contact.ready) {
SectionItemView({ SectionView(title = stringResource(MR.strings.conn_stats_section_title_servers)) {
AlertManager.shared.showAlertMsg( SectionItemView({
generalGetString(MR.strings.network_status), AlertManager.shared.showAlertMsg(
contactNetworkStatus.statusExplanation 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
) )
}) {
NetworkStatusRow(contactNetworkStatus)
} }
val rcvServers = cStats.rcvQueuesInfo.map { it.rcvServer } if (cStats != null) {
if (rcvServers.isNotEmpty()) { SwitchAddressButton(
SimplexServers(stringResource(MR.strings.receiving_via), rcvServers) disabled = cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null } || cStats.ratchetSyncSendProhibited,
} switchAddress = switchContactAddress
val sndServers = cStats.sndQueuesInfo.map { it.sndServer } )
if (sndServers.isNotEmpty()) { if (cStats.rcvQueuesInfo.any { it.rcvSwitchStatus != null }) {
SimplexServers(stringResource(MR.strings.sending_via), sndServers) 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 { SectionView {
ClearChatButton(clearChat) ClearChatButton(clearChat)
DeleteContactButton(deleteContact) DeleteContactButton(deleteContact)

View File

@ -114,7 +114,18 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
unreadCount, unreadCount,
composeState, composeState,
composeView = { 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( ComposeView(
chatModel, chat, composeState, attachmentOption, chatModel, chat, composeState, attachmentOption,
showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } } showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } }
@ -145,7 +156,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
var preloadedLink: Pair<String, GroupMemberRole>? = null var preloadedLink: Pair<String, GroupMemberRole>? = null
if (chat.chatInfo is ChatInfo.Direct) { if (chat.chatInfo is ChatInfo.Direct) {
preloadedContactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId) 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) { } else if (chat.chatInfo is ChatInfo.Group) {
setGroupMembers(chat.chatInfo.groupInfo, chatModel) setGroupMembers(chat.chatInfo.groupInfo, chatModel)
preloadedLink = chatModel.controller.apiGetGroupLink(chat.chatInfo.groupInfo.groupId) 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()) { KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) {
contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId) contactInfo = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
preloadedContactInfo = contactInfo preloadedContactInfo = contactInfo
code = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId).second code = chatModel.controller.apiGetContactCode(chat.chatInfo.apiId)?.second
preloadedCode = code preloadedCode = code
} }
ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close) 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 r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val stats = r?.second val stats = r?.second
val (_, code) = if (member.memberActive) { val (_, code) = if (member.memberActive) {
try { val memCode = chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) member to memCode?.second
} catch (e: Exception) {
Log.e(TAG, e.stackTraceToString())
member to null
}
} else { } else {
member to null member to null
} }
@ -280,6 +287,11 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
chatModel.controller.allowFeatureToContact(contact, feature, param) chatModel.controller.allowFeatureToContact(contact, feature, param)
} }
}, },
openDirectChat = { contactId ->
withApi {
openDirectChat(contactId, chatModel)
}
},
updateContactStats = { contact -> updateContactStats = { contact ->
withApi { withApi {
val r = chatModel.controller.apiContactInfo(chat.chatInfo.apiId) val r = chatModel.controller.apiContactInfo(chat.chatInfo.apiId)
@ -409,6 +421,7 @@ fun ChatLayout(
startCall: (CallMediaType) -> Unit, startCall: (CallMediaType) -> Unit,
acceptCall: (Contact) -> Unit, acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
openDirectChat: (Long) -> Unit,
updateContactStats: (Contact) -> Unit, updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit, syncContactConnection: (Contact) -> Unit,
@ -485,7 +498,7 @@ fun ChatLayout(
ChatItemsList( ChatItemsList(
chat, unreadCount, composeState, chatItems, searchValue, chat, unreadCount, composeState, chatItems, searchValue,
useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage,
receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat,
updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember,
setReaction, showItemDetails, markRead, setFloatingButton, onComposed, setReaction, showItemDetails, markRead, setFloatingButton, onComposed,
) )
@ -534,15 +547,22 @@ fun ChatInfoToolbar(
IconButton({ IconButton({
showMenu.value = false showMenu.value = false
startCall(CallMediaType.Audio) 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 { if (chat.chatInfo.contact.ready) {
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = { menuItems.add {
showMenu.value = false ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
startCall(CallMediaType.Video) showMenu.value = false
}) startCall(CallMediaType.Video)
})
}
} }
} else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers && !chat.chatInfo.incognito) { } else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers && !chat.chatInfo.incognito) {
barButtons.add { barButtons.add {
@ -554,20 +574,22 @@ fun ChatInfoToolbar(
} }
} }
} }
val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } if ((chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.ready) || chat.chatInfo is ChatInfo.Group) {
menuItems.add { val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) }
ItemAction( menuItems.add {
if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), ItemAction(
if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications), if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat),
onClick = { if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications),
showMenu.value = false onClick = {
// Just to make a delay before changing state of ntfsEnabled, otherwise it will redraw menu item with new value before closing the menu showMenu.value = false
scope.launch { // Just to make a delay before changing state of ntfsEnabled, otherwise it will redraw menu item with new value before closing the menu
delay(200) scope.launch {
changeNtfsState(!ntfsEnabled.value, ntfsEnabled) delay(200)
changeNtfsState(!ntfsEnabled.value, ntfsEnabled)
}
} }
} )
) }
} }
barButtons.add { barButtons.add {
@ -661,6 +683,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
joinGroup: (Long) -> Unit, joinGroup: (Long) -> Unit,
acceptCall: (Contact) -> Unit, acceptCall: (Contact) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
openDirectChat: (Long) -> Unit,
updateContactStats: (Contact) -> Unit, updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> Unit, syncContactConnection: (Contact) -> Unit,
@ -808,7 +831,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
) { ) {
MemberImage(member) 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 { } 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) .padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp)
.then(swipeableModifier) .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) .padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp)
.then(swipeableModifier) .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 } else { // direct message
@ -838,7 +861,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp, end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
).then(swipeableModifier) ).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 = {}, startCall = {},
acceptCall = { _ -> }, acceptCall = { _ -> },
acceptFeature = { _, _, _ -> }, acceptFeature = { _, _, _ -> },
openDirectChat = { _ -> },
updateContactStats = { }, updateContactStats = { },
updateMemberStats = { _, _ -> }, updateMemberStats = { _, _ -> },
syncContactConnection = { }, syncContactConnection = { },
@ -1330,6 +1354,7 @@ fun PreviewGroupChatLayout() {
startCall = {}, startCall = {},
acceptCall = { _ -> }, acceptCall = { _ -> },
acceptFeature = { _, _, _ -> }, acceptFeature = { _, _, _ -> },
openDirectChat = { _ -> },
updateContactStats = { }, updateContactStats = { },
updateMemberStats = { _, _ -> }, updateMemberStats = { _, _ -> },
syncContactConnection = { }, syncContactConnection = { },

View File

@ -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))
}
}

View File

@ -335,8 +335,6 @@ fun ComposeView(
return null return null
} }
suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? { suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): ChatItem? {
val cInfo = chat.chatInfo val cInfo = chat.chatInfo
val cs = composeState.value val cs = composeState.value
@ -358,6 +356,7 @@ fun ComposeView(
MsgContent.MCText(msgText) MsgContent.MCText(msgText)
} }
} }
else -> 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? { suspend fun updateMessage(ei: ChatItem, cInfo: ChatInfo, live: Boolean): ChatItem? {
val oldMsgContent = ei.content.msgContent val oldMsgContent = ei.content.msgContent
if (oldMsgContent != null) { if (oldMsgContent != null) {
@ -397,7 +404,10 @@ fun ComposeView(
} }
clearCurrentDraft() 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 val ei = cs.contextItem.chatItem
sent = updateMessage(ei, cInfo, live) sent = updateMessage(ei, cInfo, live)
} else if (liveMessage != null && liveMessage.sent) { } else if (liveMessage != null && liveMessage.sent) {
@ -655,9 +665,14 @@ fun ComposeView(
} }
val userCanSend = rememberUpdatedState(chat.userCanSend) val userCanSend = rememberUpdatedState(chat.userCanSend)
val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled)
val userIsObserver = rememberUpdatedState(chat.userIsObserver) val userIsObserver = rememberUpdatedState(chat.userIsObserver)
val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv)
Column { Column {
if (nextSendGrpInv.value) {
ComposeContextInvitingContactMemberView()
}
if (composeState.value.preview !is ComposePreview.VoicePreview || composeState.value.editing) { if (composeState.value.preview !is ComposePreview.VoicePreview || composeState.value.editing) {
contextItemView() contextItemView()
when { when {
@ -690,15 +705,21 @@ fun ComposeView(
} else { } else {
showChooseAttachment showChooseAttachment
} }
val attachmentEnabled =
!composeState.value.attachmentDisabled
&& sendMsgEnabled.value
&& userCanSend.value
&& !isGroupAndProhibitedFiles
&& !nextSendGrpInv.value
IconButton( IconButton(
attachmentClicked, attachmentClicked,
Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else 7.dp), Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else 7.dp),
enabled = !composeState.value.attachmentDisabled && rememberUpdatedState(chat.userCanSend).value enabled = attachmentEnabled
) { ) {
Icon( Icon(
painterResource(MR.images.ic_attach_file_filled_500), painterResource(MR.images.ic_attach_file_filled_500),
contentDescription = stringResource(MR.strings.attach), 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 modifier = Modifier
.size(28.dp) .size(28.dp)
.clip(CircleShape) .clip(CircleShape)
@ -774,6 +795,8 @@ fun ComposeView(
recState, recState,
chat.chatInfo is ChatInfo.Direct, chat.chatInfo is ChatInfo.Direct,
liveMessageAlertShown = chatModel.controller.appPrefs.liveMessageAlertShown, liveMessageAlertShown = chatModel.controller.appPrefs.liveMessageAlertShown,
sendMsgEnabled = sendMsgEnabled.value,
nextSendGrpInv = nextSendGrpInv.value,
needToAllowVoiceToContact, needToAllowVoiceToContact,
allowedVoiceByPrefs, allowedVoiceByPrefs,
allowVoiceToContact = ::allowVoiceToContact, allowVoiceToContact = ::allowVoiceToContact,

View File

@ -37,6 +37,8 @@ fun SendMsgView(
recState: MutableState<RecordingState>, recState: MutableState<RecordingState>,
isDirectChat: Boolean, isDirectChat: Boolean,
liveMessageAlertShown: SharedPreference<Boolean>, liveMessageAlertShown: SharedPreference<Boolean>,
sendMsgEnabled: Boolean,
nextSendGrpInv: Boolean,
needToAllowVoiceToContact: Boolean, needToAllowVoiceToContact: Boolean,
allowedVoiceByPrefs: Boolean, allowedVoiceByPrefs: Boolean,
userIsObserver: Boolean, userIsObserver: Boolean,
@ -74,16 +76,16 @@ fun SendMsgView(
false 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) cs.liveMessage == null && (cs.preview is ComposePreview.NoPreview || recState.value is RecordingState.Started)
val showDeleteTextButton = rememberSaveable { mutableStateOf(false) } val showDeleteTextButton = rememberSaveable { mutableStateOf(false) }
PlatformTextField(composeState, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage) { PlatformTextField(composeState, sendMsgEnabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage) {
if (!cs.inProgress) { if (!cs.inProgress) {
sendMessage(null) sendMessage(null)
} }
} }
// Disable clicks on text field // 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( Box(
Modifier Modifier
.matchParentSize() .matchParentSize()
@ -110,7 +112,7 @@ fun SendMsgView(
} }
when { when {
progressByTimeout -> ProgressIndicator() progressByTimeout -> ProgressIndicator()
showVoiceButton -> { showVoiceButton && sendMsgEnabled -> {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
val stopRecOnNextClick = remember { mutableStateOf(false) } val stopRecOnNextClick = remember { mutableStateOf(false) }
when { when {
@ -150,7 +152,7 @@ fun SendMsgView(
else -> { else -> {
val cs = composeState.value 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 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) || (!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) ||
cs.endLiveDisabled cs.endLiveDisabled
val showDropdown = rememberSaveable { mutableStateOf(false) } val showDropdown = rememberSaveable { mutableStateOf(false) }
@ -159,7 +161,7 @@ fun SendMsgView(
fun MenuItems(): List<@Composable () -> Unit> { fun MenuItems(): List<@Composable () -> Unit> {
val menuItems = mutableListOf<@Composable () -> Unit>() val menuItems = mutableListOf<@Composable () -> Unit>()
if (cs.liveMessage == null && !cs.editing) { if (cs.liveMessage == null && !cs.editing && !nextSendGrpInv || sendMsgEnabled) {
if ( if (
cs.preview !is ComposePreview.VoicePreview && cs.preview !is ComposePreview.VoicePreview &&
cs.contextItem is ComposeContextItem.NoContextItem && cs.contextItem is ComposeContextItem.NoContextItem &&
@ -599,6 +601,8 @@ fun PreviewSendMsgView() {
recState = remember { mutableStateOf(RecordingState.NotStarted) }, recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = true, isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }), liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
sendMsgEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false, needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true, allowedVoiceByPrefs = true,
userIsObserver = false, userIsObserver = false,
@ -630,6 +634,8 @@ fun PreviewSendMsgViewEditing() {
recState = remember { mutableStateOf(RecordingState.NotStarted) }, recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = true, isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }), liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
sendMsgEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false, needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true, allowedVoiceByPrefs = true,
userIsObserver = false, userIsObserver = false,
@ -661,6 +667,8 @@ fun PreviewSendMsgViewInProgress() {
recState = remember { mutableStateOf(RecordingState.NotStarted) }, recState = remember { mutableStateOf(RecordingState.NotStarted) },
isDirectChat = true, isDirectChat = true,
liveMessageAlertShown = SharedPreference(get = { true }, set = { }), liveMessageAlertShown = SharedPreference(get = { true }, set = { }),
sendMsgEnabled = true,
nextSendGrpInv = false,
needToAllowVoiceToContact = false, needToAllowVoiceToContact = false,
allowedVoiceByPrefs = true, allowedVoiceByPrefs = true,
userIsObserver = false, userIsObserver = false,

View File

@ -76,12 +76,8 @@ fun GroupChatInfoView(chatModel: ChatModel, groupLink: String?, groupLinkMemberR
val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId) val r = chatModel.controller.apiGroupMemberInfo(groupInfo.groupId, member.groupMemberId)
val stats = r?.second val stats = r?.second
val (_, code) = if (member.memberActive) { val (_, code) = if (member.memberActive) {
try { val memCode = chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId)
chatModel.controller.apiGetGroupMemberCode(groupInfo.apiId, member.groupMemberId) member to memCode?.second
} catch (e: Exception) {
Log.e(TAG, e.stackTraceToString())
member to null
}
} else { } else {
member to null member to null
} }

View File

@ -35,6 +35,7 @@ import chat.simplex.common.views.newchat.*
import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.common.model.GroupInfo import chat.simplex.common.model.GroupInfo
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.chatlist.openChat
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@ -52,6 +53,8 @@ fun GroupMemberInfoView(
val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value }
val connStats = remember { mutableStateOf(connectionStats) } val connStats = remember { mutableStateOf(connectionStats) }
val developerTools = chatModel.controller.appPrefs.developerTools.get() val developerTools = chatModel.controller.appPrefs.developerTools.get()
var progressIndicator by remember { mutableStateOf(false) }
if (chat != null) { if (chat != null) {
val newRole = remember { mutableStateOf(member.memberRole) } val newRole = remember { mutableStateOf(member.memberRole) }
GroupMemberInfoLayout( 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 -> connectViaAddress = { connReqUri ->
connectViaMemberAddressAlert(connReqUri) connectViaMemberAddressAlert(connReqUri)
}, },
@ -170,6 +187,10 @@ fun GroupMemberInfoView(
} }
} }
) )
if (progressIndicator) {
ProgressIndicator()
}
} }
} }
@ -201,6 +222,7 @@ fun GroupMemberInfoLayout(
connectionCode: String?, connectionCode: String?,
getContactChat: (Long) -> Chat?, getContactChat: (Long) -> Chat?,
openDirectChat: (Long) -> Unit, openDirectChat: (Long) -> Unit,
createMemberContact: () -> Unit,
connectViaAddress: (String) -> Unit, connectViaAddress: (String) -> Unit,
removeMember: () -> Unit, removeMember: () -> Unit,
onRoleSelected: (GroupMemberRole) -> Unit, onRoleSelected: (GroupMemberRole) -> Unit,
@ -237,9 +259,13 @@ fun GroupMemberInfoLayout(
if (member.memberActive) { if (member.memberActive) {
SectionView { SectionView {
if (contactId != null) { if (contactId != null && knownDirectChat(contactId) != null) {
if (knownDirectChat(contactId) != null || groupInfo.fullGroupPreferences.directMessages.on) { OpenChatButton(onClick = { openDirectChat(contactId) })
} else if (groupInfo.fullGroupPreferences.directMessages.on) {
if (contactId != null) {
OpenChatButton(onClick = { openDirectChat(contactId) }) OpenChatButton(onClick = { openDirectChat(contactId) })
} else if (member.activeConn?.peerChatVRange?.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) == true) {
OpenChatButton(onClick = { createMemberContact() })
} }
} }
if (connectionCode != null) { if (connectionCode != null) {
@ -498,6 +524,7 @@ fun PreviewGroupMemberInfoLayout() {
connectionCode = "123", connectionCode = "123",
getContactChat = { Chat.sampleData }, getContactChat = { Chat.sampleData },
openDirectChat = {}, openDirectChat = {},
createMemberContact = {},
connectViaAddress = {}, connectViaAddress = {},
removeMember = {}, removeMember = {},
onRoleSelected = {}, onRoleSelected = {},

View File

@ -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)
}
}
}

View File

@ -54,6 +54,7 @@ fun ChatItemView(
acceptCall: (Contact) -> Unit, acceptCall: (Contact) -> Unit,
scrollToItem: (Long) -> Unit, scrollToItem: (Long) -> Unit,
acceptFeature: (Contact, ChatFeature, Int?) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
openDirectChat: (Long) -> Unit,
updateContactStats: (Contact) -> Unit, updateContactStats: (Contact) -> Unit,
updateMemberStats: (GroupInfo, GroupMember) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit,
syncContactConnection: (Contact) -> 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.SndGroupInvitation -> CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito)
is CIContent.RcvGroupEventContent -> when (c.rcvGroupEvent) { is CIContent.RcvGroupEventContent -> when (c.rcvGroupEvent) {
is RcvGroupEvent.MemberConnected -> CIEventView(membersConnectedItemText()) is RcvGroupEvent.MemberConnected -> CIEventView(membersConnectedItemText())
is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat)
else -> EventItemView() else -> EventItemView()
} }
is CIContent.SndGroupEventContent -> EventItemView() is CIContent.SndGroupEventContent -> EventItemView()
@ -572,6 +574,7 @@ fun PreviewChatItemView() {
acceptCall = { _ -> }, acceptCall = { _ -> },
scrollToItem = {}, scrollToItem = {},
acceptFeature = { _, _, _ -> }, acceptFeature = { _, _, _ -> },
openDirectChat = { _ -> },
updateContactStats = { }, updateContactStats = { },
updateMemberStats = { _, _ -> }, updateMemberStats = { _, _ -> },
syncContactConnection = { }, syncContactConnection = { },
@ -601,6 +604,7 @@ fun PreviewChatItemViewDeletedContent() {
acceptCall = { _ -> }, acceptCall = { _ -> },
scrollToItem = {}, scrollToItem = {},
acceptFeature = { _, _, _ -> }, acceptFeature = { _, _, _ -> },
openDirectChat = { _ -> },
updateContactStats = { }, updateContactStats = { },
updateMemberStats = { _, _ -> }, updateMemberStats = { _, _ -> },
syncContactConnection = { }, syncContactConnection = { },

View File

@ -103,11 +103,7 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
} }
fun directChatAction(chatInfo: ChatInfo, chatModel: ChatModel) { fun directChatAction(chatInfo: ChatInfo, chatModel: ChatModel) {
if (chatInfo.ready) { withBGApi { openChat(chatInfo, chatModel) }
withBGApi { openChat(chatInfo, chatModel) }
} else {
pendingContactAlertDialog(chatInfo, chatModel)
}
} }
fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel) { fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel) {
@ -118,15 +114,28 @@ fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel) {
} }
} }
suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) { suspend fun openDirectChat(contactId: Long, chatModel: ChatModel) {
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId) val chat = chatModel.controller.apiGetChat(ChatType.Direct, contactId)
if (chat != null) { if (chat != null) {
chatModel.chatItems.clear() chatModel.chatItems.clear()
chatModel.chatItems.addAll(chat.chatItems) 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) { suspend fun apiLoadPrevMessages(chatInfo: ChatInfo, chatModel: ChatModel, beforeChatItemId: Long, search: String) {
val pagination = ChatPagination.Before(beforeChatItemId, ChatPagination.PRELOAD_COUNT) val pagination = ChatPagination.Before(beforeChatItemId, ChatPagination.PRELOAD_COUNT)
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId, pagination, search) ?: return val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId, pagination, search) ?: return

View File

@ -172,7 +172,9 @@ fun ChatPreviewView(
} else { } else {
when (cInfo) { when (cInfo) {
is ChatInfo.Direct -> 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) Text(stringResource(MR.strings.contact_connection_pending), color = MaterialTheme.colors.secondary)
} }
is ChatInfo.Group -> is ChatInfo.Group ->

View File

@ -272,6 +272,7 @@
<string name="this_text_is_available_in_settings">This text is available in settings</string> <string name="this_text_is_available_in_settings">This text is available in settings</string>
<string name="your_chats">Chats</string> <string name="your_chats">Chats</string>
<string name="contact_connection_pending">connecting…</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_you_are_invited">you are invited to group</string>
<string name="group_preview_join_as">join as %s</string> <string name="group_preview_join_as">join as %s</string>
<string name="group_connection_pending">connecting…</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="observer_cant_send_message_desc">Please contact group admin.</string>
<string name="files_and_media_prohibited">Files and media prohibited!</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="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 --> <!-- Images - chat.simplex.app.views.chat.item.CIImageView.kt -->
<string name="image_descr">Image</string> <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_group_deleted">deleted group</string>
<string name="rcv_group_event_updated_group_profile">updated group profile</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_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_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_changed_role_for_yourself">you changed role for yourself to %s</string>
<string name="snd_group_event_member_deleted">you removed %1$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_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_n_members_connected">%s, %s and %d other members connected</string>
<string name="rcv_group_event_open_chat">Open</string>
<!-- Conn event chat items --> <!-- 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_completed">changed address for you</string>
<string name="rcv_conn_event_switch_queue_phase_changing">changing address…</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_creating_link_for_group">Error creating group link</string>
<string name="error_updating_link_for_group">Error updating 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_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="only_group_owners_can_change_prefs">Only group owners can change group preferences.</string>
<string name="address_section_title">Address</string> <string name="address_section_title">Address</string>
<string name="share_address">Share address</string> <string name="share_address">Share address</string>

View File

@ -33,6 +33,7 @@ import kotlin.text.substring
@Composable @Composable
actual fun PlatformTextField( actual fun PlatformTextField(
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
sendMsgEnabled: Boolean,
textStyle: MutableState<TextStyle>, textStyle: MutableState<TextStyle>,
showDeleteTextButton: MutableState<Boolean>, showDeleteTextButton: MutableState<Boolean>,
userIsObserver: Boolean, userIsObserver: Boolean,
@ -42,6 +43,7 @@ actual fun PlatformTextField(
) { ) {
val cs = composeState.value val cs = composeState.value
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
val keyboard = LocalSoftwareKeyboardController.current val keyboard = LocalSoftwareKeyboardController.current
val padding = PaddingValues(12.dp, 12.dp, 45.dp, 0.dp) val padding = PaddingValues(12.dp, 12.dp, 45.dp, 0.dp)
LaunchedEffect(cs.contextItem) { LaunchedEffect(cs.contextItem) {
@ -51,6 +53,13 @@ actual fun PlatformTextField(
delay(50) delay(50)
keyboard?.show() 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))) } val isRtl = remember(cs.message) { isRtl(cs.message.subSequence(0, min(50, cs.message.length))) }
var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message)) } var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message)) }
val textFieldValue = textFieldValueState.copy(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 showDeleteTextButton.value = cs.message.split("\n").size >= 4 && !cs.inProgress
if (composeState.value.preview is ComposePreview.VoicePreview) { if (composeState.value.preview is ComposePreview.VoicePreview) {