From ae6996b2eea0a19ea65ba55a007b86612d1f37c7 Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 20 Sep 2023 17:58:10 +0400 Subject: [PATCH 1/8] android: create contacts with group members (#3078) --- .../platform/PlatformTextField.android.kt | 12 +++ .../chat/simplex/common/model/ChatModel.kt | 38 ++++++-- .../chat/simplex/common/model/SimpleXAPI.kt | 63 ++++++++++++- .../common/platform/PlatformTextField.kt | 1 + .../chat/simplex/common/views/TerminalView.kt | 2 + .../simplex/common/views/chat/ChatInfoView.kt | 80 ++++++++-------- .../simplex/common/views/chat/ChatView.kt | 93 ++++++++++++------- ...ComposeContextInvitingContactMemberView.kt | 39 ++++++++ .../simplex/common/views/chat/ComposeView.kt | 33 ++++++- .../simplex/common/views/chat/SendMsgView.kt | 20 ++-- .../views/chat/group/GroupChatInfoView.kt | 8 +- .../views/chat/group/GroupMemberInfoView.kt | 31 ++++++- .../chat/item/CIMemberCreatedContactView.kt | 70 ++++++++++++++ .../common/views/chat/item/ChatItemView.kt | 4 + .../views/chatlist/ChatListNavLinkView.kt | 25 +++-- .../common/views/chatlist/ChatPreviewView.kt | 4 +- .../commonMain/resources/MR/base/strings.xml | 7 ++ .../platform/PlatformTextField.desktop.kt | 12 ++- 18 files changed, 430 insertions(+), 112 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMemberCreatedContactView.kt diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt index ad07c6a33..10faa1a82 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt @@ -44,6 +44,7 @@ import java.net.URI @Composable actual fun PlatformTextField( composeState: MutableState, + sendMsgEnabled: Boolean, textStyle: MutableState, showDeleteTextButton: MutableState, 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) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index cdabe7144..33b80322a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -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) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 3e2c79185..4fba9b7cb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -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 { + suspend fun apiGetContactCode(contactId: Long): Pair? { 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 { + suspend fun apiGetGroupMemberCode(groupId: Long, groupMemberId: Long): Pair? { 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? { @@ -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): 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() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt index 4a8a2e204..95b6a73ca 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt @@ -8,6 +8,7 @@ import chat.simplex.common.views.chat.ComposeState @Composable expect fun PlatformTextField( composeState: MutableState, + sendMsgEnabled: Boolean, textStyle: MutableState, showDeleteTextButton: MutableState, userIsObserver: Boolean, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt index e8af0e71a..e47134166 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -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, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 170f87013..5fcb90c1c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -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) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index c8381cdcb..31f6fee76 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -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? = 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 = { }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt new file mode 100644 index 000000000..20316dd52 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt @@ -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)) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 4d6bc297f..f26ce0a7a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -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, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index 205f18c46..2d696b778 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -37,6 +37,8 @@ fun SendMsgView( recState: MutableState, isDirectChat: Boolean, liveMessageAlertShown: SharedPreference, + 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, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 40291b8fe..f475d045c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -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 } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index a3e5d5af1..e14089ec5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -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 = {}, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMemberCreatedContactView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMemberCreatedContactView.kt new file mode 100644 index 000000000..2ade49b3f --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMemberCreatedContactView.kt @@ -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) + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 60ef7e8cf..98811260d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -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 = { }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 3886fc8c2..57575a1e7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -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 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 95467111e..780e3515d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -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 -> diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 3a2858a81..ab0d943f3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -272,6 +272,7 @@ This text is available in settings Chats connecting… + send direct message you are invited to group join as %s connecting… @@ -304,6 +305,7 @@ Please contact group admin. Files and media prohibited! Only group owners can enable files and media. + Send direct message to connect Image @@ -1114,6 +1116,7 @@ deleted group updated group profile invited via your group link + connected directly you changed role of %s to %s you changed role for yourself to %s you removed %1$s @@ -1124,6 +1127,8 @@ %s, %s and %s connected %s, %s and %d other members connected + Open + changed address for you changing address… @@ -1201,6 +1206,8 @@ Error creating group link Error updating group link Error deleting group link + Error creating member contact + Sending message contact invitation Only group owners can change group preferences. Address Share address diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt index 36feb1abd..3b7ba8486 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt @@ -33,6 +33,7 @@ import kotlin.text.substring @Composable actual fun PlatformTextField( composeState: MutableState, + sendMsgEnabled: Boolean, textStyle: MutableState, showDeleteTextButton: MutableState, 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) { From 0a2513c9e7a18bcbeb67439f19c3d268ed860858 Mon Sep 17 00:00:00 2001 From: "M. Sarmad Qadeer" Date: Wed, 20 Sep 2023 21:54:02 +0500 Subject: [PATCH 2/8] website: add careers page (#3039) * website: add careers page * website: pagename from careers to career * website: change pagename from career to jobs * website: add jobs string to english language strings file * website: add job tabs * update --------- Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- docs/JOIN_TEAM.md | 58 ++++++++++++------------- website/.eleventy.js | 44 +++++++++++++++++++ website/customize_docs_frontmatter.js | 47 ++++++++++++-------- website/langs/en.json | 3 +- website/src/_includes/layouts/jobs.html | 45 +++++++++++++++++++ website/src/_includes/navbar.html | 10 ++++- website/src/css/style.css | 16 ++++--- 7 files changed, 168 insertions(+), 55 deletions(-) create mode 100644 website/src/_includes/layouts/jobs.html diff --git a/docs/JOIN_TEAM.md b/docs/JOIN_TEAM.md index c1f9e6c01..ed04ca8d2 100644 --- a/docs/JOIN_TEAM.md +++ b/docs/JOIN_TEAM.md @@ -7,35 +7,6 @@ We currently have 4 full-time people in the team - all engineers, including the We want to add up to 3 people to the team. -**You**: - -- **Passionate about joining SimpleX Chat team**: - - already use SimpleX Chat to communicate with friends/family or participate in public SimpleX Chat groups. - - passionate about privacy, security and communications. - - interested to make contributions to SimpleX Chat open-source project in your free time before we hire you, as an extended test. - -- **Exceptionally pragmatic, very fast and customer-focussed**: - - care about the customers (aka users) and about the product we build much more than about the code quality, technology stack, etc. - - believe that the simplest solution is the best. - - 2-3x faster than the most competent people you worked with. - - focus on solving only today's problems and resist engineering for the future (aka over-engineering) – see [The Duct Tape Programmer](https://www.joelonsoftware.com/2009/09/23/the-duct-tape-programmer/) and [Why I Hate Frameworks](https://medium.com/@johnfliu/why-i-hate-frameworks-6af8cbadba42). - - do not suffer from "not invented here" syndrome, at the same time interested to design and implement protocols and systems from the ground up when appropriate. - -- **Love software engineering**: - - have 5y+ of software engineering experience in complex projects, - - great understanding of the common principles: - - data structures, bits and byte manipulation - - text encoding and manipulation - - software design and algorithms - - concurrency - - networking - -- **Want to join a very early stage startup**: - - high pace and intensity, longer hours. - - a substantial part of the compensation is stock options. - - full transparency – we believe that too much [autonomy](https://twitter.com/KentBeck/status/851459129830850561) hurts learning and slows down progress. - - ## Who we are looking for ### Systems Haskell engineer @@ -63,6 +34,35 @@ You are a product UX expert who designs great user experiences directly in iOS c Knowledge of Android and Kotlin Multiplatform would be a bonus - we use Kotlin Jetpack Compose for our Android and desktop apps. +## About you + +- **Passionate about joining SimpleX Chat team**: + - already use SimpleX Chat to communicate with friends/family or participate in public SimpleX Chat groups. + - passionate about privacy, security and communications. + - interested to make contributions to SimpleX Chat open-source project in your free time before we hire you, as an extended test. + +- **Exceptionally pragmatic, very fast and customer-focussed**: + - care about the customers (aka users) and about the product we build much more than about the code quality, technology stack, etc. + - believe that the simplest solution is the best. + - 2-3x faster than the most competent people you worked with. + - focus on solving only today's problems and resist engineering for the future (aka over-engineering) – see [The Duct Tape Programmer](https://www.joelonsoftware.com/2009/09/23/the-duct-tape-programmer/) and [Why I Hate Frameworks](https://medium.com/@johnfliu/why-i-hate-frameworks-6af8cbadba42). + - do not suffer from "not invented here" syndrome, at the same time interested to design and implement protocols and systems from the ground up when appropriate. + +- **Love software engineering**: + - have 5y+ of software engineering experience in complex projects, + - great understanding of the common principles: + - data structures, bits and byte manipulation + - text encoding and manipulation + - software design and algorithms + - concurrency + - networking + +- **Want to join a very early stage startup**: + - high pace and intensity, longer hours. + - a substantial part of the compensation is stock options. + - full transparency – we believe that too much [autonomy](https://twitter.com/KentBeck/status/851459129830850561) hurts learning and slows down progress. + + ## How to join the team 1. [Install the app](../README.md#install-the-app), try using it with the friends and [join some user groups](https://github.com/simplex-chat/simplex-chat#join-user-groups) – you will discover a lot of things that need improvements. diff --git a/website/.eleventy.js b/website/.eleventy.js index fb9fe108f..351a70f71 100644 --- a/website/.eleventy.js +++ b/website/.eleventy.js @@ -188,6 +188,50 @@ module.exports = function (ty) { return dom.serialize() }) + ty.addFilter('wrapH3s', function (content, page) { + if (!page.url.includes("/jobs/")) { + return content + } + + const dom = new JSDOM(content) + const document = dom.window.document + + const makeBlock = (block) => { + const jobTab = document.createElement('div') + jobTab.className = "job-tab" + + const flexDiv = document.createElement('div') + flexDiv.className = "flex items-center justify-between job-tab-btn cursor-pointer" + flexDiv.innerHTML = ` + <${block.tagName}>${block.innerHTML} + + + + ` + jobTab.appendChild(flexDiv) + + const jobContent = document.createElement('div') + jobContent.className = "job-tab-content" + jobTab.appendChild(jobContent) + + block.parentNode.insertBefore(jobTab, block) + block.remove() + + let sibling = jobTab.nextElementSibling + const siblingsToMove = [] + while (sibling && !['H3', 'H2'].includes(sibling.tagName)) { + siblingsToMove.push(sibling) + sibling = sibling.nextElementSibling + } + + siblingsToMove.forEach(el => jobContent.appendChild(el)) + } + + Array.from(document.querySelectorAll("h3")).forEach(makeBlock) + + return dom.serialize() + }) + ty.addShortcode("completeRoute", (obj) => { const urlParts = obj.url.split("/") diff --git a/website/customize_docs_frontmatter.js b/website/customize_docs_frontmatter.js index 8f2546e16..efb060779 100644 --- a/website/customize_docs_frontmatter.js +++ b/website/customize_docs_frontmatter.js @@ -54,32 +54,41 @@ Object.entries(fileLanguageMapping).forEach(([fileName, languages]) => { // Calculate the permalink based on the file's location const linkPath = path.relative(directoryPath, fullPath).replace(/\.md$/, '.html'); const permalink = `/docs/${linkPath}`.toLowerCase(); - parsedMatter.data.permalink = permalink; - // Update the frontmatter with the new languages list - parsedMatter.data.supportedLangsForDoc = languages; + if (fileName === 'JOIN_TEAM') { + parsedMatter.data.title = 'SimpleX Chat - Jobs'; + parsedMatter.data.permalink = '/jobs/index.html'; + parsedMatter.data.layout = 'layouts/jobs.html'; + parsedMatter.data.active_jobs = true; + } + else { + parsedMatter.data.permalink = permalink; - // Add the layout value - parsedMatter.data.layout = 'layouts/doc.html'; + // Update the frontmatter with the new languages list + parsedMatter.data.supportedLangsForDoc = languages; - if (fullPath.startsWith(path.join(directoryPath, langFolder))) { - // Non-English files - const [language, ...rest] = relativePath.split(path.sep).slice(1); - const enFilePath = path.join(directoryPath, ...rest); + // Add the layout value + parsedMatter.data.layout = 'layouts/doc.html'; - if (enFiles[enFilePath]) { - const enRevision = new Date(enFiles[enFilePath].revision); - const currentRevision = new Date(parsedMatter.data.revision); + if (fullPath.startsWith(path.join(directoryPath, langFolder))) { + // Non-English files + const [language, ...rest] = relativePath.split(path.sep).slice(1); + const enFilePath = path.join(directoryPath, ...rest); - const isOld = currentRevision < enRevision; + if (enFiles[enFilePath]) { + const enRevision = new Date(enFiles[enFilePath].revision); + const currentRevision = new Date(parsedMatter.data.revision); + + const isOld = currentRevision < enRevision; + // Add the version value + parsedMatter.data.version = isOld ? 'old' : 'new'; + } + } else { + // English files + enFiles[fullPath] = { revision: parsedMatter.data.revision }; // Add the version value - parsedMatter.data.version = isOld ? 'old' : 'new'; + parsedMatter.data.version = 'new'; } - } else { - // English files - enFiles[fullPath] = { revision: parsedMatter.data.revision }; - // Add the version value - parsedMatter.data.version = 'new'; } // Save the updated frontmatter and content back to the file diff --git a/website/langs/en.json b/website/langs/en.json index d9ff80f3e..c73695de5 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -243,5 +243,6 @@ "f-droid-org-repo": "F-Droid.org repo", "stable-versions-built-by-f-droid-org": "Stable versions built by F-Droid.org", "releases-to-this-repo-are-done-1-2-days-later": "The releases to this repo are done 1-2 days later", - "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat and F-Droid.org repositories sign builds with the different keys. To switch, please export the chat database and re-install the app." + "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat and F-Droid.org repositories sign builds with the different keys. To switch, please export the chat database and re-install the app.", + "jobs": "Join team" } diff --git a/website/src/_includes/layouts/jobs.html b/website/src/_includes/layouts/jobs.html new file mode 100644 index 000000000..6a68c0795 --- /dev/null +++ b/website/src/_includes/layouts/jobs.html @@ -0,0 +1,45 @@ + + + + + + + + {{ title }} + + + + + + + + + + + +
+ {% include "navbar.html" %} +
+ +
+
+
{{ content | wrapH3s(page) | safe }}
+
+
+ + {% include "footer.html" %} + + + + \ No newline at end of file diff --git a/website/src/_includes/navbar.html b/website/src/_includes/navbar.html index beb3139c2..f62a788a7 100644 --- a/website/src/_includes/navbar.html +++ b/website/src/_includes/navbar.html @@ -100,6 +100,14 @@
+ + +
+