diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d7322cd92f..4c219bdd34 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -125,7 +125,9 @@ jobs: shell: bash run: | cabal build --enable-tests - echo "::set-output name=bin_path::$(cabal list-bin simplex-chat)" + path=$(cabal list-bin simplex-chat) + echo "bin_path=$path" >> $GITHUB_OUTPUT + echo "bin_hash=$(echo SHA2-512\(${{ matrix.asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - name: Unix upload CLI binary to release if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest' @@ -136,6 +138,16 @@ jobs: asset_name: ${{ matrix.asset_name }} tag: ${{ github.ref }} + - name: Unix update CLI binary hash + if: startsWith(github.ref, 'refs/tags/v') && matrix.os != 'windows-latest' + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + append_body: true + body: | + ${{ steps.unix_cli_build.outputs.bin_hash }} + - name: Setup Java if: startsWith(github.ref, 'refs/tags/v') uses: actions/setup-java@v3 @@ -152,7 +164,9 @@ jobs: scripts/desktop/build-lib-linux.sh cd apps/multiplatform ./gradlew packageDeb - echo "::set-output name=package_path::$(echo $PWD/release/main/deb/simplex_*_amd64.deb)" + path=$(echo $PWD/release/main/deb/simplex_*_amd64.deb) + echo "package_path=$path" >> $GITHUB_OUTPUT + echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - name: Linux make AppImage id: linux_appimage_build @@ -160,7 +174,9 @@ jobs: shell: bash run: | scripts/desktop/make-appimage-linux.sh - echo "::set-output name=appimage_path::$(echo $PWD/apps/multiplatform/release/main/*imple*.AppImage)" + path=$(echo $PWD/apps/multiplatform/release/main/*imple*.AppImage) + echo "appimage_path=$path" >> $GITHUB_OUTPUT + echo "appimage_hash=$(echo SHA2-512\(simplex-desktop-x86_64.AppImage\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - name: Mac build desktop id: mac_desktop_build @@ -171,8 +187,10 @@ jobs: APPLE_SIMPLEX_NOTARIZATION_APPLE_ID: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_APPLE_ID }} APPLE_SIMPLEX_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_SIMPLEX_NOTARIZATION_PASSWORD }} run: | - scripts/desktop/build-desktop-mac-ci.sh - echo "::set-output name=package_path::$(echo $PWD/release/main/dmg/SimpleX-*.dmg)" + scripts/build-desktop-mac.sh + path=$(echo $PWD/apps/multiplatform/release/main/dmg/SimpleX-*.dmg) + echo "package_path=$path" >> $GITHUB_OUTPUT + echo "package_hash=$(echo SHA2-512\(${{ matrix.desktop_asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - name: Linux upload desktop package to release if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04') @@ -183,6 +201,16 @@ jobs: asset_name: ${{ matrix.desktop_asset_name }} tag: ${{ github.ref }} + - name: Linux update desktop package hash + if: startsWith(github.ref, 'refs/tags/v') && (matrix.os == 'ubuntu-20.04' || matrix.os == 'ubuntu-22.04') + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + append_body: true + body: | + ${{ steps.linux_desktop_build.outputs.package_hash }} + - name: Linux upload AppImage to release if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04' uses: svenstaro/upload-release-action@v2 @@ -192,6 +220,16 @@ jobs: asset_name: simplex-desktop-x86_64.AppImage tag: ${{ github.ref }} + - name: Linux update AppImage hash + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'ubuntu-20.04' + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + append_body: true + body: | + ${{ steps.linux_appimage_build.outputs.appimage_hash }} + - name: Mac upload desktop package to release if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest' uses: svenstaro/upload-release-action@v2 @@ -201,6 +239,16 @@ jobs: asset_name: ${{ matrix.desktop_asset_name }} tag: ${{ github.ref }} + - name: Mac update desktop package hash + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'macos-latest' + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + append_body: true + body: | + ${{ steps.mac_desktop_build.outputs.package_hash }} + - name: Unix test if: matrix.os != 'windows-latest' timeout-minutes: 30 @@ -220,11 +268,12 @@ jobs: shell: cmd run: | cabal build --enable-tests - cabal list-bin simplex-chat > tmp_bin_path - set /p bin_path= < tmp_bin_path - echo ::set-output name=bin_path::%bin_path% + rm -rf dist-newstyle/src/direct-sq* + path=$(cabal list-bin simplex-chat | tail -n 1) + echo "bin_path=$path" >> $GITHUB_OUTPUT + echo "bin_hash=$(echo SHA2-512\(${{ matrix.asset_name }}\)= $(openssl sha512 $path | cut -d' ' -f 2))" >> $GITHUB_OUTPUT - - name: Windows upload binary to release + - name: Windows upload CLI binary to release if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest' uses: svenstaro/upload-release-action@v2 with: @@ -233,4 +282,14 @@ jobs: asset_name: ${{ matrix.asset_name }} tag: ${{ github.ref }} + - name: Windows update CLI binary hash + if: startsWith(github.ref, 'refs/tags/v') && matrix.os == 'windows-latest' + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + append_body: true + body: | + ${{ steps.windows_build.outputs.bin_hash }} + # Windows / diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 736ee80a21..161dbaa8e7 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -48,6 +48,11 @@ 5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */; }; 5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C55A922283CEDE600C4E99E /* SoundPlayer.swift */; }; 5C55A92E283D0FDE00C4E99E /* sounds in Resources */ = {isa = PBXBuildFile; fileRef = 5C55A92D283D0FDE00C4E99E /* sounds */; }; + 5C5624FC2ABB39B900A21210 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5624F72ABB39B900A21210 /* libgmpxx.a */; }; + 5C5624FD2ABB39B900A21210 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5624F82ABB39B900A21210 /* libgmp.a */; }; + 5C5624FE2ABB39B900A21210 /* libHSsimplex-chat-5.3.0.8-D1oMkI9pySuA3Aa2cfRrBY-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5624F92ABB39B900A21210 /* libHSsimplex-chat-5.3.0.8-D1oMkI9pySuA3Aa2cfRrBY-ghc8.10.7.a */; }; + 5C5624FF2ABB39B900A21210 /* libHSsimplex-chat-5.3.0.8-D1oMkI9pySuA3Aa2cfRrBY.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5624FA2ABB39B900A21210 /* libHSsimplex-chat-5.3.0.8-D1oMkI9pySuA3Aa2cfRrBY.a */; }; + 5C5625002ABB39B900A21210 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C5624FB2ABB39B900A21210 /* libffi.a */; }; 5C577F7D27C83AA10006112D /* MarkdownHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */; }; 5C58BCD6292BEBE600AF9E4F /* CIChatFeatureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C58BCD5292BEBE600AF9E4F /* CIChatFeatureView.swift */; }; 5C5DB70E289ABDD200730FFF /* AppearanceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5DB70D289ABDD200730FFF /* AppearanceSettings.swift */; }; @@ -78,11 +83,6 @@ 5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; }; 5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; }; 5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */; }; - 5C9E127E2AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9E12792AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t-ghc8.10.7.a */; }; - 5C9E127F2AAE62A500C9D8FF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9E127A2AAE62A500C9D8FF /* libgmpxx.a */; }; - 5C9E12802AAE62A500C9D8FF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9E127B2AAE62A500C9D8FF /* libgmp.a */; }; - 5C9E12812AAE62A500C9D8FF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9E127C2AAE62A500C9D8FF /* libffi.a */; }; - 5C9E12822AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9E127D2AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t.a */; }; 5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; }; 5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; }; 5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; }; @@ -293,6 +293,11 @@ 5C55A920283CCCB700C4E99E /* IncomingCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncomingCallView.swift; sourceTree = ""; }; 5C55A922283CEDE600C4E99E /* SoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundPlayer.swift; sourceTree = ""; }; 5C55A92D283D0FDE00C4E99E /* sounds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = sounds; sourceTree = ""; }; + 5C5624F72ABB39B900A21210 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5C5624F82ABB39B900A21210 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + 5C5624F92ABB39B900A21210 /* libHSsimplex-chat-5.3.0.8-D1oMkI9pySuA3Aa2cfRrBY-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.8-D1oMkI9pySuA3Aa2cfRrBY-ghc8.10.7.a"; sourceTree = ""; }; + 5C5624FA2ABB39B900A21210 /* libHSsimplex-chat-5.3.0.8-D1oMkI9pySuA3Aa2cfRrBY.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.8-D1oMkI9pySuA3Aa2cfRrBY.a"; sourceTree = ""; }; + 5C5624FB2ABB39B900A21210 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; 5C577F7C27C83AA10006112D /* MarkdownHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownHelp.swift; sourceTree = ""; }; 5C58BCD5292BEBE600AF9E4F /* CIChatFeatureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIChatFeatureView.swift; sourceTree = ""; }; 5C5B67912ABAF4B500DA9412 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; @@ -342,11 +347,6 @@ 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = ""; }; 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = ""; }; 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFile.swift; sourceTree = ""; }; - 5C9E12792AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t-ghc8.10.7.a"; sourceTree = ""; }; - 5C9E127A2AAE62A500C9D8FF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5C9E127B2AAE62A500C9D8FF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5C9E127C2AAE62A500C9D8FF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5C9E127D2AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t.a"; sourceTree = ""; }; 5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = ""; }; 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = ""; }; 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = ""; }; @@ -507,13 +507,13 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5C9E127F2AAE62A500C9D8FF /* libgmpxx.a in Frameworks */, - 5C9E12812AAE62A500C9D8FF /* libffi.a in Frameworks */, - 5C9E12802AAE62A500C9D8FF /* libgmp.a in Frameworks */, + 5C5624FE2ABB39B900A21210 /* libHSsimplex-chat-5.3.0.8-D1oMkI9pySuA3Aa2cfRrBY-ghc8.10.7.a in Frameworks */, + 5C5624FF2ABB39B900A21210 /* libHSsimplex-chat-5.3.0.8-D1oMkI9pySuA3Aa2cfRrBY.a in Frameworks */, + 5C5624FC2ABB39B900A21210 /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, + 5C5625002ABB39B900A21210 /* libffi.a in Frameworks */, + 5C5624FD2ABB39B900A21210 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - 5C9E127E2AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t-ghc8.10.7.a in Frameworks */, - 5C9E12822AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -574,11 +574,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5C9E127C2AAE62A500C9D8FF /* libffi.a */, - 5C9E127B2AAE62A500C9D8FF /* libgmp.a */, - 5C9E127A2AAE62A500C9D8FF /* libgmpxx.a */, - 5C9E12792AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t-ghc8.10.7.a */, - 5C9E127D2AAE62A500C9D8FF /* libHSsimplex-chat-5.3.0.7-6JlIR0UqFTrEzd5R0Y6B8t.a */, + 5C5624FB2ABB39B900A21210 /* libffi.a */, + 5C5624F82ABB39B900A21210 /* libgmp.a */, + 5C5624F72ABB39B900A21210 /* libgmpxx.a */, + 5C5624F92ABB39B900A21210 /* libHSsimplex-chat-5.3.0.8-D1oMkI9pySuA3Aa2cfRrBY-ghc8.10.7.a */, + 5C5624FA2ABB39B900A21210 /* libHSsimplex-chat-5.3.0.8-D1oMkI9pySuA3Aa2cfRrBY.a */, ); path = Libraries; sourceTree = ""; @@ -1486,7 +1486,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 170; + CURRENT_PROJECT_VERSION = 171; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1528,7 +1528,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 170; + CURRENT_PROJECT_VERSION = 171; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -1608,7 +1608,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 170; + CURRENT_PROJECT_VERSION = 171; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1640,7 +1640,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 170; + CURRENT_PROJECT_VERSION = 171; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GENERATE_INFOPLIST_FILE = YES; @@ -1672,7 +1672,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 170; + CURRENT_PROJECT_VERSION = 171; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1718,7 +1718,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 170; + CURRENT_PROJECT_VERSION = 171; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; 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 ad07c6a33c..10faa1a82b 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 cdabe71449..33b80322ad 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 3e2c79185f..4fba9b7cbf 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 4a8a2e204f..95b6a73ca4 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 e8af0e71a9..e471341669 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 170f870130..5fcb90c1c9 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 c8381cdcb7..31f6fee762 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 0000000000..20316dd524 --- /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 4d6bc297f0..f26ce0a7a4 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 205f18c46a..2d696b7781 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 40291b8fe0..f475d045cf 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 a3e5d5af18..e14089ec52 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 0000000000..2ade49b3fc --- /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 60ef7e8cfe..98811260d9 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 3886fc8c29..57575a1e75 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 95467111e5..780e3515df 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 3a2858a811..ab0d943f33 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 36feb1abdf..3b7ba84863 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) { diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 4352b8eb81..e8ca8faa21 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -25,11 +25,11 @@ android.nonTransitiveRClass=true android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 -android.version_name=5.3-beta.7 -android.version_code=149 +android.version_name=5.3-beta.8 +android.version_code=150 -desktop.version_name=1.5.0 -desktop.version_code=7 +desktop.version_name=1.6.0 +desktop.version_code=8 kotlin.version=1.8.20 gradle.plugin.version=7.4.2 diff --git a/cabal.project b/cabal.project index 58580d357f..7c8886d94a 100644 --- a/cabal.project +++ b/cabal.project @@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 343865553295da5edeef30c1d8589b47c66bd9b0 + tag: 53c793d5590d3c781aa3fbf72993eee262c7aa83 source-repository-package type: git diff --git a/docs/CLI.md b/docs/CLI.md index 65de29e601..7966627c4a 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -16,7 +16,7 @@ revision: 31.01.2023 - [Windows](#windows) - [Build from source](#build-from-source) - [Using Docker](#using-docker) - - [Using Haskell stack](#using-haskell-stack) + - [Using Haskell in any OS](#in-any-os) - [Usage](#usage) - [Running the chat client](#running-the-chat-client) - [Access messaging servers via Tor](#access-messaging-servers-via-tor-beta) @@ -102,27 +102,49 @@ DOCKER_BUILDKIT=1 docker build --output ~/.local/bin . #### In any OS -1. Install [Haskell GHCup](https://www.haskell.org/ghcup/), GHC 8.10.7 and cabal: +1. Install [Haskell GHCup](https://www.haskell.org/ghcup/), GHC 9.6.2 and cabal 3.10.1.0: ```shell curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh ``` -2. Build the project: +You can use `ghcup tui` to check or add GHC and cabal versions. + +2. Clone the source code: ```shell git clone git@github.com:simplex-chat/simplex-chat.git cd simplex-chat git checkout stable -# on Linux +# or to build a specific version: +# git checkout v5.3.0-beta.8 +``` + +`master` is a development branch, it may containt unstable code. + +3. Prepare the system: + +On Linux: + +```shell apt-get update && apt-get install -y build-essential libgmp3-dev zlib1g-dev cp scripts/cabal.project.local.linux cabal.project.local -# or on MacOS: -# brew install openssl@1.1 -# cp scripts/cabal.project.local.mac cabal.project.local -# you may need to amend cabal.project.local to point to the actual openssl location +``` + +On Mac: + +``` +brew install openssl@1.1 +cp scripts/cabal.project.local.mac cabal.project.local +``` + +You may need to amend cabal.project.local to point to the actual openssl location. + +4. Build the app: + +```shell cabal update -cabal install +cabal install simplex-chat ``` ## Usage diff --git a/docs/DOWNLOADS.md b/docs/DOWNLOADS.md new file mode 100644 index 0000000000..948fecfc2e --- /dev/null +++ b/docs/DOWNLOADS.md @@ -0,0 +1,42 @@ +--- +title: Download SimpleX apps +permalink: /downloads/index.html +revision: 20.09.2023 +--- + +| Updated 20.09.2023 | Languages: EN | +# Download SimpleX apps + +- [desktop](#desktop-app) +- [mobile](#mobile-apps) +- [terminal](#terminal-console-app) (console) + +## Desktop app + +desktop app + +The latest version of desktop app is v5.3-beta.8 (1.6.0 in the app). + +Using the same profile as on mobile device is not yet supported – you need to create a separate profile to use desktop apps. + +**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0-beta.8/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0-beta.8/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0-beta.8/simplex-desktop-ubuntu-22_04-x86_64.deb). + +**Mac**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0-beta.8/simplex-desktop-macos-x86_64.dmg) (Intel), [aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0-beta.8/simplex-desktop-macos-aarch64.dmg) (Apple Silicon). + +**Windows**: coming soon. + +## Mobile apps + +**iOS**: [App store](https://apps.apple.com/us/app/simplex-chat/id1605771084) (v5.2.3), [TestFlight](https://testflight.apple.com/join/DWuT2LQu) (v5.3-beta.8). + +**Android**: [Play store](https://play.google.com/store/apps/details?id=chat.simplex.app), [F-Droid](https://simplex.chat/fdroid/), [APK aarch64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0-beta.8/simplex.apk), [APK armv7](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0-beta.8/simplex-armv7a.apk). + +## Terminal (console) app + +See [Using terminal app](/docs/CLI.md). + +**Linux**: [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0-beta.8/simplex-chat-ubuntu-20_04-x86-64), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0-beta.8/simplex-chat-ubuntu-22_04-x86-64). + +**Mac** [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0-beta.8/simplex-chat-macos-x86-64), aarch64 - [compile from source](./CLI.md#). + +**Windows**: [x86_64](https://github.com/simplex-chat/simplex-chat/releases/download/v5.3.0-beta.8/simplex-chat-windows-x86-64). diff --git a/docs/JOIN_TEAM.md b/docs/JOIN_TEAM.md index c1f9e6c014..5a31b3e058 100644 --- a/docs/JOIN_TEAM.md +++ b/docs/JOIN_TEAM.md @@ -1,3 +1,9 @@ +--- +title: Join SimpleX Chat team +permalink: /jobs/index.html +layout: layouts/jobs.html +--- + # Join SimpleX Chat team SimpleX Chat Ltd is a seed stage startup with a lot of user growth in 2022-2023, and a lot of exciting technical and product problems to solve to grow faster. @@ -7,35 +13,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 +40,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/src/img/simplex-desktop-dark-1.png b/docs/images/simplex-desktop-dark-1.png similarity index 100% rename from website/src/img/simplex-desktop-dark-1.png rename to docs/images/simplex-desktop-dark-1.png diff --git a/website/src/img/simplex-desktop-dark-2.png b/docs/images/simplex-desktop-dark-2.png similarity index 100% rename from website/src/img/simplex-desktop-dark-2.png rename to docs/images/simplex-desktop-dark-2.png diff --git a/website/src/img/simplex-desktop-light.png b/docs/images/simplex-desktop-light.png similarity index 100% rename from website/src/img/simplex-desktop-light.png rename to docs/images/simplex-desktop-light.png diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 18f776b92a..2ac9c9f1f3 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."343865553295da5edeef30c1d8589b47c66bd9b0" = "01md63iq78swfr6rn0b3a10168ivcha4ql37hk0k4knfqc09fv45"; + "https://github.com/simplex-chat/simplexmq.git"."53c793d5590d3c781aa3fbf72993eee262c7aa83" = "0f0ldlgqwrapgfw5gnaj00xvb14c8nykyjr9fhy79h4r16g614x8"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb"; "https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd"; diff --git a/stack.yaml b/stack.yaml index 89729a58c2..868e19faf5 100644 --- a/stack.yaml +++ b/stack.yaml @@ -49,7 +49,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 343865553295da5edeef30c1d8589b47c66bd9b0 + commit: 53c793d5590d3c781aa3fbf72993eee262c7aa83 - github: kazu-yamamoto/http2 commit: b5a1b7200cf5bc7044af34ba325284271f6dff25 # - ../direct-sqlcipher diff --git a/website/.eleventy.js b/website/.eleventy.js index fb9fe108f2..09fc7c2c44 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("/") @@ -271,7 +315,8 @@ module.exports = function (ty) { referenceMenu.data.forEach(referenceSubmenu => { docs.forEach(doc => { const url = doc.url.replace("/docs/", "") - const urlParts = url.split("/") + let urlParts = url.split("/") + urlParts = urlParts.filter((ele) => ele !== "") if (doc.inputPath.split('/').includes(referenceSubmenu)) { if (urlParts.length === 1 && urlParts[0] !== "") { diff --git a/website/customize_docs_frontmatter.js b/website/customize_docs_frontmatter.js index 8f2546e168..10031b5436 100644 --- a/website/customize_docs_frontmatter.js +++ b/website/customize_docs_frontmatter.js @@ -54,13 +54,17 @@ 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; + + if (fileName === 'JOIN_TEAM') { + parsedMatter.data.active_jobs = true; + } + if (!parsedMatter.data.permalink) parsedMatter.data.permalink = permalink; // Update the frontmatter with the new languages list parsedMatter.data.supportedLangsForDoc = languages; // Add the layout value - parsedMatter.data.layout = 'layouts/doc.html'; + if (!parsedMatter.data.layout) parsedMatter.data.layout = 'layouts/doc.html'; if (fullPath.startsWith(path.join(directoryPath, langFolder))) { // Non-English files diff --git a/website/langs/ar.json b/website/langs/ar.json index bf44575f28..3fe698a3fe 100644 --- a/website/langs/ar.json +++ b/website/langs/ar.json @@ -132,7 +132,7 @@ "donate-here-to-help-us": "تبرّع هنا لمساعدتنا", "sign-up-to-receive-our-updates": "اشترك للحصول على آخر مستجداتنا", "enter-your-email-address": "أدخل عنوان بريدك الإلكتروني", - "get-simplex": "احصل على SimpleX", + "get-simplex": "احصل على SimpleX desktop app", "why-simplex-is": "لماذا SimpleX", "unique": "فريد من نوعه", "learn-more": "اقرأ أكثر", diff --git a/website/langs/cs.json b/website/langs/cs.json index 99febbeacd..93aed0c9b2 100644 --- a/website/langs/cs.json +++ b/website/langs/cs.json @@ -114,7 +114,7 @@ "donate-here-to-help-us": "Přispějte zde a pomozte nám", "sign-up-to-receive-our-updates": "Přihlaste se k odběru novinek", "enter-your-email-address": "vložte svou e-mailovou adresu", - "get-simplex": "Získat SimpleX", + "get-simplex": "Získat SimpleX desktop app", "why-simplex-is": "Proč je SimpleX", "unique": "jedinečný", "learn-more": "Další informace", diff --git a/website/langs/de.json b/website/langs/de.json index 6cb3f1d3f6..052b73235a 100644 --- a/website/langs/de.json +++ b/website/langs/de.json @@ -127,7 +127,7 @@ "donate-here-to-help-us": "Spenden Sie, um uns zu unterstützen", "sign-up-to-receive-our-updates": "Melden Sie sich an, um Updates von uns zu erhalten", "enter-your-email-address": "Geben Sie Ihre Mail-Adresse ein", - "get-simplex": "Laden Sie sich SimpleX herunter", + "get-simplex": "Laden Sie sich SimpleX herunter desktop app", "learn-more": "Erfahren Sie mehr darüber", "more-info": "Weitere Informationen", "hide-info": "Informationen verbergen", diff --git a/website/langs/en.json b/website/langs/en.json index d9ff80f3e4..434aed8343 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -30,10 +30,12 @@ "hero-p-1": "Other apps have user IDs: Signal, Matrix, Session, Briar, Jami, Cwtch, etc.
SimpleX does not, not even random numbers.
This radically improves your privacy.", "hero-overlay-1-textlink": "Why user IDs are bad for privacy?", "hero-overlay-2-textlink": "How does SimpleX work?", + "hero-overlay-3-textlink": "Security assessment", "hero-2-header": "Make a private connection", "hero-2-header-desc": "The video shows how you connect to your friend via their 1-time QR-code, in person or via a video link. You can also connect by sharing an invitation link.", "hero-overlay-1-title": "How does SimpleX work?", "hero-overlay-2-title": "Why user IDs are bad for privacy?", + "hero-overlay-3-title": "Security assessment", "feature-1-title": "E2E-encrypted messages with markdown and editing", "feature-2-title": "E2E-encrypted
images and files", "feature-3-title": "Decentralized secret groups —
only users know they exist", @@ -99,6 +101,9 @@ "hero-overlay-card-2-p-2": "They could then correlate this information with the existing public social networks, and determine some real identities.", "hero-overlay-card-2-p-3": "Even with the most private apps that use Tor v3 services, if you talk to two different contacts via the same profile they can prove that they are connected to the same person.", "hero-overlay-card-2-p-4": "SimpleX protects against these attacks by not having any user IDs in its design. And, if you use Incognito mode, you will have a different display name for each contact, avoiding any shared data between them.", + "hero-overlay-card-3-p-1": "Trail of Bits is a leading security and technology consultancy whose clients include big tech, governmental agencies and major blockchain projects.", + "hero-overlay-card-3-p-2": "Trail of Bits reviewed SimpleX platform cryptography and networking components in November 2022.", + "hero-overlay-card-3-p-3": "Read more in the announcement.", "simplex-network-overlay-card-1-p-1": "P2P messaging protocols and apps have various problems that make them less reliable than SimpleX, more complex to analyse, and vulnerable to several types of attack.", "simplex-network-overlay-card-1-li-1": "P2P networks rely on some variant of DHT to route messages. DHT designs have to balance delivery guarantee and latency. SimpleX has both better delivery guarantee and lower latency than P2P, because the message can be redundantly passed via several servers in parallel, using the servers chosen by the recipient. In P2P networks the message is passed through O(log N) nodes sequentially, using nodes chosen by the algorithm.", "simplex-network-overlay-card-1-li-2": "SimpleX design, unlike most P2P networks, has no global user identifiers of any kind, even temporary, and only uses temporary pairwise identifiers, providing better anonymity and metadata protection.", @@ -143,7 +148,7 @@ "donate-here-to-help-us": "Donate here to help us", "sign-up-to-receive-our-updates": "Sign up to receive our updates", "enter-your-email-address": "Enter your email address", - "get-simplex": "Get SimpleX", + "get-simplex": "Get SimpleX desktop app", "why-simplex-is": "Why SimpleX is", "unique": "unique", "learn-more": "Learn more", @@ -229,6 +234,7 @@ "docs-dropdown-6": "WebRTC servers", "docs-dropdown-7": "Translate SimpleX Chat", "docs-dropdown-8": "SimpleX Directory Service", + "docs-dropdown-9": "Downloads", "newer-version-of-eng-msg": "There is a newer version of this page in English.", "click-to-see": "Click to see", "menu": "Menu", @@ -243,5 +249,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/langs/es.json b/website/langs/es.json index 3f317ae4e6..dfbb95d725 100644 --- a/website/langs/es.json +++ b/website/langs/es.json @@ -137,7 +137,7 @@ "sign-up-to-receive-our-updates": "Suscríbase para recibir nuestras actualizaciones", "donate-here-to-help-us": "Para ayudarnos haga una donación aquí", "enter-your-email-address": "Escriba su dirección de correo electrónico", - "get-simplex": "Obtenga SimpleX", + "get-simplex": "Obtenga SimpleX desktop app", "why-simplex-is": "Por qué SimpleX es", "unique": "único", "learn-more": "Descubra más", diff --git a/website/langs/fi.json b/website/langs/fi.json index eaa774aa9d..963ed42c87 100644 --- a/website/langs/fi.json +++ b/website/langs/fi.json @@ -186,7 +186,7 @@ "donate-here-to-help-us": "Tue meitä täällä lahjoituksilla", "sign-up-to-receive-our-updates": "Tilaa päivityksemme", "enter-your-email-address": "Syötä sähköpostiosoitteesi", - "get-simplex": "Hanki SimpleX", + "get-simplex": "Hanki SimpleX desktop app", "why-simplex-is": "Miksi SimpleX on", "unique": "ainutlaatuinen", "learn-more": "Lue lisää", diff --git a/website/langs/fr.json b/website/langs/fr.json index 5d261d69ab..37dc6d5a14 100644 --- a/website/langs/fr.json +++ b/website/langs/fr.json @@ -143,7 +143,7 @@ "donate-here-to-help-us": "Faites un don ici pour nous aider", "sign-up-to-receive-our-updates": "Inscrivez-vous pour recevoir nos mises à jour", "enter-your-email-address": "Entrez votre adresse e-mail", - "get-simplex": "Obtenir SimpleX", + "get-simplex": "Obtenir SimpleX desktop app", "why-simplex-is": "Pourquoi SimpleX est", "unique": "unique", "learn-more": "En savoir plus", diff --git a/website/langs/it.json b/website/langs/it.json index d7ca0d9a34..fa254e66ef 100644 --- a/website/langs/it.json +++ b/website/langs/it.json @@ -81,7 +81,7 @@ "join": "Unisciti a", "we-invite-you-to-join-the-conversation": "Ti invitiamo a unirti alla conversazione", "enter-your-email-address": "Inserisci il tuo indirizzo email", - "get-simplex": "Ottieni SimpleX", + "get-simplex": "Ottieni SimpleX desktop app", "why-simplex-is": "Perché SimpleX è", "unique": "unico", "learn-more": "Maggiori informazioni", diff --git a/website/langs/ja.json b/website/langs/ja.json index cd11ffdb2a..b2ac7ecbc0 100644 --- a/website/langs/ja.json +++ b/website/langs/ja.json @@ -172,7 +172,7 @@ "simplex-unique-overlay-card-1-p-2": "メッセージを配信するために、SimpleX は一方向メッセージ キューのペアワイズ匿名アドレスを使用し、受信メッセージと送信メッセージに分けて、通常は異なるサーバーを経由します。 SimpleX を使用することは、別の「バーナー」 を使用するようなものです。 連絡先ごとにメールまたは電話を使用できるため、管理に手間がかかりません。", "simplex-unique-overlay-card-3-p-4": "送受信されるサーバー トラフィックの間に共通の識別子や暗号文はありません。 — 誰かがそれを観察している場合、たとえ TLS が侵害されたとしても、誰が誰と通信しているのかを簡単に判断することはできません。", "docs-dropdown-2": "Android ファイルへのアクセス", - "get-simplex": "SimpleXを入手する", + "get-simplex": "SimpleXを入手する desktop app", "privacy-matters-overlay-card-3-p-1": "誰もが通信のプライバシーとセキュリティに気を配る必要があります。 たとえ何も隠すものがなかったとしても、無害な会話はあなたを危険にさらす可能性があります。", "simplex-unique-2-title": "スパムや悪用から
保護されています", "comparison-section-list-point-2": "DNSベースのアドレス", diff --git a/website/langs/nl.json b/website/langs/nl.json index f8c5274453..02bf471d3b 100644 --- a/website/langs/nl.json +++ b/website/langs/nl.json @@ -194,7 +194,7 @@ "simplex-unique-overlay-card-4-p-3": "Als u overweegt om voor het SimpleX platform te ontwikkelen, bijvoorbeeld de chatbot voor gebruikers van de SimpleX app, of de integratie van de SimpleX Chat bibliotheek in uw mobiele apps, get in touch for any advice en ondersteuning.", "simplex-unique-card-1-p-1": "SimpleX beschermt de privacy van uw profiel, contacten en metadata en verbergt deze voor SimpleX platformservers en eventuele waarnemers.", "enter-your-email-address": "Voer uw e-mail adres in", - "get-simplex": "Verkrijg SimpleX", + "get-simplex": "Verkrijg SimpleX desktop app", "tap-to-close": "Tik om te sluiten", "contact-hero-header": "Je hebt een adres ontvangen om verbinding te maken met SimpleX Chat", "invitation-hero-header": "Je hebt een eenmalige link ontvangen om verbinding te maken met SimpleX Chat", diff --git a/website/langs/pl.json b/website/langs/pl.json index 86530071c7..f9b9936848 100644 --- a/website/langs/pl.json +++ b/website/langs/pl.json @@ -159,7 +159,7 @@ "simplex-unique-card-4-p-2": "Możesz używać SimpleX z własnymi serwerami lub z serwerami dostarczonymi przez nas — i nadal łączyć się z dowolnym użytkownikiem.", "we-invite-you-to-join-the-conversation": "Zapraszamy do udziału w rozmowie", "enter-your-email-address": "Wpisz swój adres e-mail", - "get-simplex": "Pobierz SimpleX", + "get-simplex": "Pobierz SimpleX desktop app", "why-simplex-is": "Dlaczego SimpleX jest", "join": "Dołącz do", "join-us-on-GitHub": "Dołącz do nas na GitHubie", diff --git a/website/langs/pt_BR.json b/website/langs/pt_BR.json index 083f4942ec..278d54c43a 100644 --- a/website/langs/pt_BR.json +++ b/website/langs/pt_BR.json @@ -146,7 +146,7 @@ "donate-here-to-help-us": "Doe aqui para nos ajudar", "sign-up-to-receive-our-updates": "Inscreva-se para receber nossas atualizações", "enter-your-email-address": "Digite seu endereço de e-mail", - "get-simplex": "Obtenha o SimpleX", + "get-simplex": "Obtenha o SimpleX desktop app", "why-simplex-is": "Por que o SimpleX é", "unique": "único", "learn-more": "Saiba mais", diff --git a/website/langs/uk.json b/website/langs/uk.json index eae508ed47..6a48cc5640 100644 --- a/website/langs/uk.json +++ b/website/langs/uk.json @@ -173,7 +173,7 @@ "donate-here-to-help-us": "Пожертвуйте тут, щоб допомогти нам", "sign-up-to-receive-our-updates": "Підпишіться на наші оновлення", "enter-your-email-address": "Введіть адресу вашої електронної пошти", - "get-simplex": "Отримати SimpleX", + "get-simplex": "Отримати SimpleX desktop app", "why-simplex-is": "Чому SimpleX це", "unique": "унікальний", "learn-more": "Дізнайтеся більше", diff --git a/website/langs/zh_Hans.json b/website/langs/zh_Hans.json index 4b631131ce..c327e160be 100644 --- a/website/langs/zh_Hans.json +++ b/website/langs/zh_Hans.json @@ -26,7 +26,7 @@ "simplex-unique-overlay-card-3-p-4": "发送和接收的服务器流量之间没有共同的标识符或密文—— 如果有人在观察它,他们也无法轻易确定谁与谁通信,即使 TLS 受到威胁。", "simplex-unique-card-4-p-1": "SimpleX 网络是完全去中心化的,并且独立于任何加密货币或除互联网以外的任何其他平台。", "join": "加入", - "get-simplex": "获取 SimpleX", + "get-simplex": "获取 SimpleX desktop app", "hide-info": "隐藏信息", "contact-hero-header": "您收到了一个用于连接 SimpleX Chat 的地址", "contact-hero-p-2": "还没有下载 SimpleX Chat 吗?", diff --git a/website/src/_data/docs_dropdown.json b/website/src/_data/docs_dropdown.json index cdcb595086..a8c6e634b2 100644 --- a/website/src/_data/docs_dropdown.json +++ b/website/src/_data/docs_dropdown.json @@ -31,6 +31,10 @@ { "title": "docs-dropdown-7", "url": "/docs/translations.html" + }, + { + "title": "docs-dropdown-9", + "url": "/downloads/" } ] } \ No newline at end of file diff --git a/website/src/_data/docs_sidebar.json b/website/src/_data/docs_sidebar.json index a45f69fb06..fcd980121d 100644 --- a/website/src/_data/docs_sidebar.json +++ b/website/src/_data/docs_sidebar.json @@ -26,7 +26,8 @@ "SERVER.md", "TRANSLATIONS.md", "WEBRTC.md", - "XFTP-SERVER.md" + "XFTP-SERVER.md", + "DOWNLOADS.md" ] }, { diff --git a/website/src/_data/hero_overlays.json b/website/src/_data/hero_overlays.json index d50ba0db68..7388994459 100644 --- a/website/src/_data/hero_overlays.json +++ b/website/src/_data/hero_overlays.json @@ -23,6 +23,18 @@ "showImage": true, "contentBody": "overlay_content/hero/card_2.html" } + }, + { + "id": 3, + "imgLight": "/img/trail-of-bits-light.png", + "imgDark": "/img/trail-of-bits-dark.png", + "overlayContent": { + "overlayId": "security-assessment", + "overlayScrollTo": "", + "title": "hero-overlay-3-title", + "showImage": true, + "contentBody": "overlay_content/hero/card_3.html" + } } ] } \ No newline at end of file diff --git a/website/src/_data/languages.json b/website/src/_data/languages.json index 3629172642..0ac05063cc 100644 --- a/website/src/_data/languages.json +++ b/website/src/_data/languages.json @@ -45,6 +45,12 @@ "flag": "/img/flags/it.svg", "enabled": true }, + { + "label": "ja", + "name": "日本語", + "flag": "/img/flags/jp.svg", + "enabled": true + }, { "label": "nl", "name": "Nederlands", diff --git a/website/src/_includes/hero.html b/website/src/_includes/hero.html index 161e0da4a8..c0e2b3f30b 100644 --- a/website/src/_includes/hero.html +++ b/website/src/_includes/hero.html @@ -17,6 +17,8 @@ {{ overlay(hero_overlays.sections[1], lang) }} {{ "hero-overlay-2-textlink" | i18n({}, lang ) | safe }} {{ overlay(hero_overlays.sections[0], lang) }} + {{ "hero-overlay-3-textlink" | i18n({}, lang ) | safe }} + {{ overlay(hero_overlays.sections[2], lang) }}
diff --git a/website/src/_includes/layouts/jobs.html b/website/src/_includes/layouts/jobs.html new file mode 100644 index 0000000000..6a68c0795f --- /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 beb3139c2b..55836bd576 100644 --- a/website/src/_includes/navbar.html +++ b/website/src/_includes/navbar.html @@ -100,6 +100,14 @@
+ + +
+