From c37f41c1714fd01eafbbad1595afb3f9f3f98792 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 14 Feb 2022 19:36:15 +0000 Subject: [PATCH 01/46] use sync commands (#306) --- src/Simplex/Chat.hs | 20 ++++++++++---------- src/Simplex/Chat/View.hs | 6 +++--- tests/ChatTests.hs | 1 + 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 715e460c87..010cabc3d8 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -382,17 +382,17 @@ processChatCommand = \case withAgentLock a . withLock l $ action -- below code would make command responses asynchronous where they can be slow -- in View.hs `r'` should be defined as `id` in this case - procCmd :: m ChatResponse -> m ChatResponse - procCmd action = do - ChatController {chatLock = l, smpAgent = a, outputQ = q, idsDrg = gVar} <- ask - corrId <- liftIO $ SMP.CorrId <$> randomBytes gVar 8 - void . forkIO $ - withAgentLock a . withLock l $ - (atomically . writeTBQueue q) . (Just corrId,) =<< (action `catchError` (pure . CRChatError)) - pure $ CRCmdAccepted corrId - -- use function below to make commands "synchronous" -- procCmd :: m ChatResponse -> m ChatResponse - -- procCmd = id + -- procCmd action = do + -- ChatController {chatLock = l, smpAgent = a, outputQ = q, idsDrg = gVar} <- ask + -- corrId <- liftIO $ SMP.CorrId <$> randomBytes gVar 8 + -- void . forkIO $ + -- withAgentLock a . withLock l $ + -- (atomically . writeTBQueue q) . (Just corrId,) =<< (action `catchError` (pure . CRChatError)) + -- pure $ CRCmdAccepted corrId + -- use function below to make commands "synchronous" + procCmd :: m ChatResponse -> m ChatResponse + procCmd = id connectViaContact :: UserId -> ConnectionRequestUri 'CMContact -> Profile -> m ChatResponse connectViaContact userId cReq profile = withChatLock $ do let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 08ad47543c..dc77335354 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -67,7 +67,7 @@ responseToView cmd testView = \case CRSentConfirmation -> r' ["confirmation sent!"] CRSentInvitation -> r' ["connection request sent!"] CRContactDeleted c -> r' [ttyContact' c <> ": contact is deleted"] - CRAcceptingContactRequest c -> r' [ttyFullContact c <> ": accepting contact request..."] + CRAcceptingContactRequest c -> r [ttyFullContact c <> ": accepting contact request..."] CRContactAlreadyExists c -> r [ttyFullContact c <> ": contact already exists"] CRContactRequestAlreadyAccepted c -> r' [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] CRUserContactLinkCreated cReq -> r' $ connReqContact_ "Your new chat address is created!" cReq @@ -125,8 +125,8 @@ responseToView cmd testView = \case where r = (plain cmd :) -- this function should be `r` for "synchronous", `id` for "asynchronous" command responses - -- r' = r - r' = id + r' = r + -- r' = id testViewChats :: [AChat] -> [StyledString] testViewChats chats = [sShow $ map toChatView chats] where diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 9e7ecb1404..f10bba0d82 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -744,6 +744,7 @@ testUserContactLinkAutoAccept = cath ##> ("/c " <> cLink) cath <## "connection request sent!" + alice <## "" alice <## "cath (Catherine): accepting contact request..." concurrently_ (cath <## "alice (Alice): contact is connected") From b7fda194c8d0c1e60b636d0c38240244baed999a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 14 Feb 2022 21:38:12 +0000 Subject: [PATCH 02/46] update binaries in iOS app and build number (10) --- apps/ios/SimpleX.xcodeproj/project.pbxproj | 44 +++++++++++----------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 4ed34fa537..365311f057 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -25,15 +25,15 @@ 5C35CFC927B2782E00FB6C6D /* BGManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFC727B2782E00FB6C6D /* BGManager.swift */; }; 5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */; }; 5C35CFCC27B2E91D00FB6C6D /* NtfManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */; }; + 5C499F2D27BAF1E300ECB4C5 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C499F2827BAF1E300ECB4C5 /* libffi.a */; }; + 5C499F2E27BAF1E300ECB4C5 /* libHSsimplex-chat-1.2.0-KiHnJvLnz2iELJvTN47xo8.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C499F2927BAF1E300ECB4C5 /* libHSsimplex-chat-1.2.0-KiHnJvLnz2iELJvTN47xo8.a */; }; + 5C499F2F27BAF1E300ECB4C5 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C499F2A27BAF1E300ECB4C5 /* libgmpxx.a */; }; + 5C499F3027BAF1E300ECB4C5 /* libHSsimplex-chat-1.2.0-KiHnJvLnz2iELJvTN47xo8-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C499F2B27BAF1E300ECB4C5 /* libHSsimplex-chat-1.2.0-KiHnJvLnz2iELJvTN47xo8-ghc8.10.7.a */; }; + 5C499F3127BAF1E300ECB4C5 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C499F2C27BAF1E300ECB4C5 /* libgmp.a */; }; 5C5346A827B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; }; 5C5346A927B59A6A004DF848 /* ChatHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C5346A727B59A6A004DF848 /* ChatHelp.swift */; }; 5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; }; 5C6AD81427A834E300348BD7 /* NewChatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6AD81227A834E300348BD7 /* NewChatButton.swift */; }; - 5C75059C27B5CD9300BE3227 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C75059727B5CD9300BE3227 /* libgmp.a */; }; - 5C75059D27B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C75059827B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj-ghc8.10.7.a */; }; - 5C75059E27B5CD9300BE3227 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C75059927B5CD9300BE3227 /* libffi.a */; }; - 5C75059F27B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C75059A27B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj.a */; }; - 5C7505A027B5CD9300BE3227 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C75059B27B5CD9300BE3227 /* libgmpxx.a */; }; 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; }; 5C7505A327B65FDB00BE3227 /* CIMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */; }; 5C7505A527B679EE00BE3227 /* NavLinkPlain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */; }; @@ -126,13 +126,13 @@ 5C35CFC727B2782E00FB6C6D /* BGManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BGManager.swift; sourceTree = ""; }; 5C35CFCA27B2E91D00FB6C6D /* NtfManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NtfManager.swift; sourceTree = ""; }; 5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX (iOS).entitlements"; sourceTree = ""; }; + 5C499F2827BAF1E300ECB4C5 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + 5C499F2927BAF1E300ECB4C5 /* libHSsimplex-chat-1.2.0-KiHnJvLnz2iELJvTN47xo8.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.2.0-KiHnJvLnz2iELJvTN47xo8.a"; sourceTree = ""; }; + 5C499F2A27BAF1E300ECB4C5 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + 5C499F2B27BAF1E300ECB4C5 /* libHSsimplex-chat-1.2.0-KiHnJvLnz2iELJvTN47xo8-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.2.0-KiHnJvLnz2iELJvTN47xo8-ghc8.10.7.a"; sourceTree = ""; }; + 5C499F2C27BAF1E300ECB4C5 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 5C5346A727B59A6A004DF848 /* ChatHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatHelp.swift; sourceTree = ""; }; 5C6AD81227A834E300348BD7 /* NewChatButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatButton.swift; sourceTree = ""; }; - 5C75059727B5CD9300BE3227 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5C75059827B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj-ghc8.10.7.a"; sourceTree = ""; }; - 5C75059927B5CD9300BE3227 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5C75059A27B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj.a"; sourceTree = ""; }; - 5C75059B27B5CD9300BE3227 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 5C7505A127B65FDB00BE3227 /* CIMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMetaView.swift; sourceTree = ""; }; 5C7505A427B679EE00BE3227 /* NavLinkPlain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavLinkPlain.swift; sourceTree = ""; }; 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoToolbar.swift; sourceTree = ""; }; @@ -178,14 +178,14 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5C499F3027BAF1E300ECB4C5 /* libHSsimplex-chat-1.2.0-KiHnJvLnz2iELJvTN47xo8-ghc8.10.7.a in Frameworks */, + 5C499F2F27BAF1E300ECB4C5 /* libgmpxx.a in Frameworks */, + 5C499F3127BAF1E300ECB4C5 /* libgmp.a in Frameworks */, 5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */, - 5C75059E27B5CD9300BE3227 /* libffi.a in Frameworks */, + 5C499F2E27BAF1E300ECB4C5 /* libHSsimplex-chat-1.2.0-KiHnJvLnz2iELJvTN47xo8.a in Frameworks */, 5C764E83279C748B000C6508 /* libz.tbd in Frameworks */, - 5C75059F27B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj.a in Frameworks */, - 5C7505A027B5CD9300BE3227 /* libgmpxx.a in Frameworks */, - 5C75059C27B5CD9300BE3227 /* libgmp.a in Frameworks */, 5C764E82279C748B000C6508 /* libiconv.tbd in Frameworks */, - 5C75059D27B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj-ghc8.10.7.a in Frameworks */, + 5C499F2D27BAF1E300ECB4C5 /* libffi.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -246,11 +246,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5C75059927B5CD9300BE3227 /* libffi.a */, - 5C75059727B5CD9300BE3227 /* libgmp.a */, - 5C75059B27B5CD9300BE3227 /* libgmpxx.a */, - 5C75059827B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj-ghc8.10.7.a */, - 5C75059A27B5CD9300BE3227 /* libHSsimplex-chat-1.1.1-GMOrBlCwBvRIONTwKLN6tj.a */, + 5C499F2827BAF1E300ECB4C5 /* libffi.a */, + 5C499F2C27BAF1E300ECB4C5 /* libgmp.a */, + 5C499F2A27BAF1E300ECB4C5 /* libgmpxx.a */, + 5C499F2B27BAF1E300ECB4C5 /* libHSsimplex-chat-1.2.0-KiHnJvLnz2iELJvTN47xo8-ghc8.10.7.a */, + 5C499F2927BAF1E300ECB4C5 /* libHSsimplex-chat-1.2.0-KiHnJvLnz2iELJvTN47xo8.a */, ); path = Libraries; sourceTree = ""; @@ -799,7 +799,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; @@ -839,7 +839,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; From c81bb0f15daf7a35dafa4777aaedb5ab9dbabbae Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 15 Feb 2022 08:14:50 +0000 Subject: [PATCH 03/46] iOS: show dates in older messages --- apps/ios/Shared/Model/ChatModel.swift | 6 +++++- apps/ios/Shared/Views/Chat/ChatItemView.swift | 10 ---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 3aa05f6fed..e0caf9d1a6 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -571,7 +571,11 @@ struct CIMeta: Decodable { func timestampText(_ date: Date) -> String { - date.formatted(date: .omitted, time: .shortened) + let now = Calendar.current.dateComponents([.day, .hour], from: .now) + let dc = Calendar.current.dateComponents([.day, .hour], from: date) + return now.day == dc.day || ((now.day ?? 0) - (dc.day ?? 0) == 1 && (dc.hour ?? 0) >= 18 && (now.hour ?? 0) < 12) + ? date.formatted(date: .omitted, time: .shortened) + : String(date.formatted(date: .numeric, time: .omitted).prefix(5)) } enum CIStatus: Decodable { diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index a2189b5979..e287e8c1ae 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -8,8 +8,6 @@ import SwiftUI -private var dateFormatter: DateFormatter? - struct ChatItemView: View { var chatItem: ChatItem var width: CGFloat @@ -23,14 +21,6 @@ struct ChatItemView: View { } } -func getDateFormatter() -> DateFormatter { - if let df = dateFormatter { return df } - let df = DateFormatter() - df.dateFormat = "HH:mm" - dateFormatter = df - return df -} - struct ChatItemView_Previews: PreviewProvider { static var previews: some View { Group{ From d40ee71a2c1189806eb58082a209af39ea254839 Mon Sep 17 00:00:00 2001 From: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> Date: Wed, 16 Feb 2022 13:38:49 +0400 Subject: [PATCH 04/46] update simplexmq sha (#312) * update simplexmq sha * package build for iOS/Intel simulator Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- cabal.project | 2 +- flake.nix | 27 +++++++++++++++++++++++++++ sha256map.nix | 2 +- stack.yaml | 2 +- 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index 5df8a3dfe2..80421d01a3 100644 --- a/cabal.project +++ b/cabal.project @@ -3,7 +3,7 @@ packages: . source-repository-package type: git location: git://github.com/simplex-chat/simplexmq.git - tag: 229e2607d76f3d6baf0d2623b186c084e3908b8f + tag: 137afb68fe02ac05c439c870db7c626fb6ef7880 source-repository-package type: git diff --git a/flake.nix b/flake.nix index de81fb10d8..27c1a3044f 100644 --- a/flake.nix +++ b/flake.nix @@ -142,6 +142,33 @@ ''; }; }; + "x86_64-darwin" = { + "x86_64-darwin:lib:simplex-chat" = (drv pkgs).simplex-chat.components.library.override { + smallAddressSpace = true; enableShared = false; + # we need threaded here, otherwise all the queing logic doesn't work properly. + # for iOS we also use -staticlib, to get one rolled up library. + # still needs mac2ios patching of the archives. + ghcOptions = [ "-staticlib" "-threaded" ]; + postInstall = '' + ${pkgs.tree}/bin/tree $out + mkdir -p $out/_pkg + # copy over includes, we might want those, but maybe not. + # cp -r $out/lib/*/*/include $out/_pkg/ + # find the libHS...ghc-X.Y.Z.a static library; this is the + # rolled up one with all dependencies included. + find ./dist -name "libHS*.a" -exec cp {} $out/_pkg \; + find ${pkgs.libffi.overrideAttrs (old: { dontDisableStatic = true; })}/lib -name "*.a" -exec cp {} $out/_pkg \; + find ${pkgs.gmp6.override { withStatic = true; }}/lib -name "*.a" -exec cp {} $out/_pkg \; + # There is no static libc + ${pkgs.tree}/bin/tree $out/_pkg + (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg.zip *) + rm -fR $out/_pkg + mkdir -p $out/nix-support + echo "file binary-dist \"$(echo $out/*.zip)\"" \ + > $out/nix-support/hydra-build-products + ''; + }; + }; }.${system} or {}); # build all packages in hydra. hydraJobs = packages; diff --git a/sha256map.nix b/sha256map.nix index c6cbc22b77..cbcbeaddf2 100644 --- a/sha256map.nix +++ b/sha256map.nix @@ -1,5 +1,5 @@ { - "git://github.com/simplex-chat/simplexmq.git"."229e2607d76f3d6baf0d2623b186c084e3908b8f" = "1w7mrrsyjh2wskqgdp8ml33xivzzqhzig217q3f92nkzc87ziq4g"; + "git://github.com/simplex-chat/simplexmq.git"."137afb68fe02ac05c439c870db7c626fb6ef7880" = "12n5zxd2bpiidb4ps3hm4wwx22ynvcknsgfwq0868wg941rd238f"; "git://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "git://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "git://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index 09a567a0bc..09cffe958c 100644 --- a/stack.yaml +++ b/stack.yaml @@ -48,7 +48,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 229e2607d76f3d6baf0d2623b186c084e3908b8f + commit: 137afb68fe02ac05c439c870db7c626fb6ef7880 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From 322ab9d85492014743ebead99caf634b07584118 Mon Sep 17 00:00:00 2001 From: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> Date: Wed, 16 Feb 2022 16:48:28 +0400 Subject: [PATCH 05/46] use async commands (#313) * switch to async * make tests pass --- src/Simplex/Chat.hs | 20 ++++++++++---------- src/Simplex/Chat/View.hs | 6 +++--- tests/ChatTests.hs | 1 - 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 010cabc3d8..715e460c87 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -382,17 +382,17 @@ processChatCommand = \case withAgentLock a . withLock l $ action -- below code would make command responses asynchronous where they can be slow -- in View.hs `r'` should be defined as `id` in this case - -- procCmd :: m ChatResponse -> m ChatResponse - -- procCmd action = do - -- ChatController {chatLock = l, smpAgent = a, outputQ = q, idsDrg = gVar} <- ask - -- corrId <- liftIO $ SMP.CorrId <$> randomBytes gVar 8 - -- void . forkIO $ - -- withAgentLock a . withLock l $ - -- (atomically . writeTBQueue q) . (Just corrId,) =<< (action `catchError` (pure . CRChatError)) - -- pure $ CRCmdAccepted corrId - -- use function below to make commands "synchronous" procCmd :: m ChatResponse -> m ChatResponse - procCmd = id + procCmd action = do + ChatController {chatLock = l, smpAgent = a, outputQ = q, idsDrg = gVar} <- ask + corrId <- liftIO $ SMP.CorrId <$> randomBytes gVar 8 + void . forkIO $ + withAgentLock a . withLock l $ + (atomically . writeTBQueue q) . (Just corrId,) =<< (action `catchError` (pure . CRChatError)) + pure $ CRCmdAccepted corrId + -- use function below to make commands "synchronous" + -- procCmd :: m ChatResponse -> m ChatResponse + -- procCmd = id connectViaContact :: UserId -> ConnectionRequestUri 'CMContact -> Profile -> m ChatResponse connectViaContact userId cReq profile = withChatLock $ do let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index dc77335354..08ad47543c 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -67,7 +67,7 @@ responseToView cmd testView = \case CRSentConfirmation -> r' ["confirmation sent!"] CRSentInvitation -> r' ["connection request sent!"] CRContactDeleted c -> r' [ttyContact' c <> ": contact is deleted"] - CRAcceptingContactRequest c -> r [ttyFullContact c <> ": accepting contact request..."] + CRAcceptingContactRequest c -> r' [ttyFullContact c <> ": accepting contact request..."] CRContactAlreadyExists c -> r [ttyFullContact c <> ": contact already exists"] CRContactRequestAlreadyAccepted c -> r' [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] CRUserContactLinkCreated cReq -> r' $ connReqContact_ "Your new chat address is created!" cReq @@ -125,8 +125,8 @@ responseToView cmd testView = \case where r = (plain cmd :) -- this function should be `r` for "synchronous", `id` for "asynchronous" command responses - r' = r - -- r' = id + -- r' = r + r' = id testViewChats :: [AChat] -> [StyledString] testViewChats chats = [sShow $ map toChatView chats] where diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index f10bba0d82..9e7ecb1404 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -744,7 +744,6 @@ testUserContactLinkAutoAccept = cath ##> ("/c " <> cLink) cath <## "connection request sent!" - alice <## "" alice <## "cath (Catherine): accepting contact request..." concurrently_ (cath <## "alice (Alice): contact is connected") From ce02c514cf7d18864ca1260878e93fa12c203c82 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 16 Feb 2022 12:49:47 +0000 Subject: [PATCH 06/46] started android / compose app (#301) * new compose project * classes for chat command and response * use val with get() for commands and responses * chat model * initial jetpack compose set up * wire it up with chat * first ability to send and receive messages * refactor model/controller interface * JSON samples * terminal view with items * playing around with json * JSON serialization works * parsing API responses in the terminal * add subclass for contactSubscribed reponse * remove android-poc * remove JSON example Co-authored-by: IanRDavies --- apps/android/.gitignore | 11 +- apps/android/.idea/.gitignore | 3 - apps/android/.idea/codeStyles/Project.xml | 117 ------------- .../.idea/codeStyles/codeStyleConfig.xml | 5 - .../.idea/deploymentTargetDropDown.xml | 17 ++ .../inspectionProfiles/Project_Default.xml | 20 +++ apps/android/.idea/misc.xml | 7 +- apps/android/app/.gitignore | 1 + apps/android/app/build.gradle | 32 +++- apps/android/app/src/main/AndroidManifest.xml | 5 +- apps/android/app/src/main/cpp/simplex-api.c | 16 +- .../java/chat/simplex/app/MainActivity.kt | 134 +++------------ .../main/java/chat/simplex/app/SimplexApp.kt | 95 +++++++++++ .../java/chat/simplex/app/model/ChatModel.kt | 105 ++++++++++++ .../java/chat/simplex/app/model/SImpleXAPI.kt | 161 ++++++++++++++++++ .../java/chat/simplex/app/ui/theme/Color.kt | 8 + .../java/chat/simplex/app/ui/theme/Shape.kt | 11 ++ .../java/chat/simplex/app/ui/theme/Theme.kt | 44 +++++ .../java/chat/simplex/app/ui/theme/Type.kt | 28 +++ .../chat/simplex/app/views/TerminalView.kt | 38 +++++ .../simplex/app/views/chat/SendMsgView.kt | 40 +++++ .../app/src/main/res/layout/activity_main.xml | 49 ------ .../app/src/main/res/values-night/themes.xml | 16 -- .../app/src/main/res/values/themes.xml | 19 +-- apps/android/build.gradle | 13 +- apps/android/gradle.properties | 10 +- .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 4 +- apps/android/settings.gradle | 8 +- 29 files changed, 665 insertions(+), 352 deletions(-) delete mode 100644 apps/android/.idea/.gitignore delete mode 100644 apps/android/.idea/codeStyles/Project.xml delete mode 100644 apps/android/.idea/codeStyles/codeStyleConfig.xml create mode 100644 apps/android/.idea/deploymentTargetDropDown.xml create mode 100644 apps/android/.idea/inspectionProfiles/Project_Default.xml create mode 100644 apps/android/app/.gitignore create mode 100644 apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/model/SImpleXAPI.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/ui/theme/Shape.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt delete mode 100644 apps/android/app/src/main/res/layout/activity_main.xml delete mode 100644 apps/android/app/src/main/res/values-night/themes.xml create mode 100644 apps/android/gradle/wrapper/gradle-wrapper.jar diff --git a/apps/android/.gitignore b/apps/android/.gitignore index b2a8a63042..aa724b7707 100644 --- a/apps/android/.gitignore +++ b/apps/android/.gitignore @@ -8,17 +8,8 @@ /.idea/navEditor.xml /.idea/assetWizardSettings.xml .DS_Store -build/ -release/ -debug/ +/build /captures .externalNativeBuild .cxx local.properties -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar diff --git a/apps/android/.idea/.gitignore b/apps/android/.idea/.gitignore deleted file mode 100644 index 26d33521af..0000000000 --- a/apps/android/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/apps/android/.idea/codeStyles/Project.xml b/apps/android/.idea/codeStyles/Project.xml deleted file mode 100644 index 4bec4ea8ae..0000000000 --- a/apps/android/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/apps/android/.idea/codeStyles/codeStyleConfig.xml b/apps/android/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index a55e7a179b..0000000000 --- a/apps/android/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/apps/android/.idea/deploymentTargetDropDown.xml b/apps/android/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000000..b2a5949802 --- /dev/null +++ b/apps/android/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/android/.idea/inspectionProfiles/Project_Default.xml b/apps/android/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000000..28422375b6 --- /dev/null +++ b/apps/android/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/apps/android/.idea/misc.xml b/apps/android/.idea/misc.xml index 345106f12f..1c2eb53046 100644 --- a/apps/android/.idea/misc.xml +++ b/apps/android/.idea/misc.xml @@ -3,9 +3,10 @@ diff --git a/apps/android/app/.gitignore b/apps/android/app/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/apps/android/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index dc2b3ed857..0f2c5d0297 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -1,6 +1,7 @@ plugins { id 'com.android.application' - id 'kotlin-android' + id 'org.jetbrains.kotlin.android' + id 'org.jetbrains.kotlin.plugin.serialization' } android { @@ -17,6 +18,9 @@ android { ndk { abiFilters 'arm64-v8a' } + vectorDrawables { + useSupportLibrary true + } externalNativeBuild { cmake { cppFlags '' @@ -44,17 +48,29 @@ android { } } buildFeatures { - viewBinding true + compose true + } + composeOptions { + kotlinCompilerExtensionVersion compose_version + } + packagingOptions { + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' + } } } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' - implementation 'androidx.appcompat:appcompat:1.4.1' - implementation 'com.google.android.material:material:1.5.0' - implementation 'androidx.constraintlayout:constraintlayout:2.1.3' - testImplementation 'junit:junit:4.+' + implementation "androidx.compose.ui:ui:$compose_version" + implementation "androidx.compose.material:material:$compose_version" + implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' + implementation 'androidx.activity:activity-compose:1.3.1' + implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' -} \ No newline at end of file + androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" + debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" +} diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml index 7113a1c6ac..b0fdea62da 100644 --- a/apps/android/app/src/main/AndroidManifest.xml +++ b/apps/android/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + android:exported="true" + android:label="@string/app_name" + android:theme="@style/Theme.SimpleX"> diff --git a/apps/android/app/src/main/cpp/simplex-api.c b/apps/android/app/src/main/cpp/simplex-api.c index 7aa1924386..f2dadcb131 100644 --- a/apps/android/app/src/main/cpp/simplex-api.c +++ b/apps/android/app/src/main/cpp/simplex-api.c @@ -8,7 +8,7 @@ void setLineBuffering(void); int pipe_std_to_socket(const char * name); JNIEXPORT jint JNICALL -Java_chat_simplex_app_MainActivityKt_pipeStdOutToSocket(JNIEnv *env, __unused jclass clazz, jstring socket_name) { +Java_chat_simplex_app_SimplexAppKt_pipeStdOutToSocket(JNIEnv *env, __unused jclass clazz, jstring socket_name) { const char *name = (*env)->GetStringUTFChars(env, socket_name, JNI_FALSE); int ret = pipe_std_to_socket(name); (*env)->ReleaseStringUTFChars(env, socket_name, name); @@ -16,7 +16,7 @@ Java_chat_simplex_app_MainActivityKt_pipeStdOutToSocket(JNIEnv *env, __unused jc } JNIEXPORT void JNICALL -Java_chat_simplex_app_MainActivityKt_initHS(__unused JNIEnv *env, __unused jclass clazz) { +Java_chat_simplex_app_SimplexAppKt_initHS(__unused JNIEnv *env, __unused jclass clazz) { hs_init(NULL, NULL); setLineBuffering(); } @@ -33,7 +33,7 @@ extern char *chat_send_cmd(controller ctl, const char *cmd); extern char *chat_recv_msg(controller ctl); JNIEXPORT jlong JNICALL -Java_chat_simplex_app_MainActivityKt_chatInit(JNIEnv *env, __unused jclass clazz, jstring datadir) { +Java_chat_simplex_app_SimplexAppKt_chatInit(JNIEnv *env, __unused jclass clazz, jstring datadir) { const char *_data = (*env)->GetStringUTFChars(env, datadir, JNI_FALSE); jlong res = (jlong)chat_init_store(_data); (*env)->ReleaseStringUTFChars(env, datadir, _data); @@ -41,12 +41,12 @@ Java_chat_simplex_app_MainActivityKt_chatInit(JNIEnv *env, __unused jclass clazz } JNIEXPORT jstring JNICALL -Java_chat_simplex_app_MainActivityKt_chatGetUser(JNIEnv *env, __unused jclass clazz, jlong controller) { +Java_chat_simplex_app_SimplexAppKt_chatGetUser(JNIEnv *env, __unused jclass clazz, jlong controller) { return (*env)->NewStringUTF(env, chat_get_user((void*)controller)); } JNIEXPORT jstring JNICALL -Java_chat_simplex_app_MainActivityKt_chatCreateUser(JNIEnv *env, __unused jclass clazz, jlong controller, jstring data) { +Java_chat_simplex_app_SimplexAppKt_chatCreateUser(JNIEnv *env, __unused jclass clazz, jlong controller, jstring data) { const char *_data = (*env)->GetStringUTFChars(env, data, JNI_FALSE); jstring res = (*env)->NewStringUTF(env, chat_create_user((void*)controller, _data)); (*env)->ReleaseStringUTFChars(env, data, _data); @@ -54,12 +54,12 @@ Java_chat_simplex_app_MainActivityKt_chatCreateUser(JNIEnv *env, __unused jclass } JNIEXPORT jlong JNICALL -Java_chat_simplex_app_MainActivityKt_chatStart(JNIEnv *env, jclass clazz, jlong controller) { +Java_chat_simplex_app_SimplexAppKt_chatStart(JNIEnv *env, jclass clazz, jlong controller) { return (jlong)chat_start((void*)controller); } JNIEXPORT jstring JNICALL -Java_chat_simplex_app_MainActivityKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) { +Java_chat_simplex_app_SimplexAppKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) { const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE); jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg)); (*env)->ReleaseStringUTFChars(env, msg, _msg); @@ -67,6 +67,6 @@ Java_chat_simplex_app_MainActivityKt_chatSendCmd(JNIEnv *env, __unused jclass cl } JNIEXPORT jstring JNICALL -Java_chat_simplex_app_MainActivityKt_chatRecvMsg(JNIEnv *env, __unused jclass clazz, jlong controller) { +Java_chat_simplex_app_SimplexAppKt_chatRecvMsg(JNIEnv *env, __unused jclass clazz, jlong controller) { return (*env)->NewStringUTF(env, chat_recv_msg((void*)controller)); } diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 20897023c3..2a62c745fb 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -1,123 +1,37 @@ package chat.simplex.app -import android.net.LocalServerSocket +import android.app.Application import android.os.Bundle -import android.util.Log -import android.view.inputmethod.EditorInfo -import android.widget.ScrollView -import android.widget.TextView -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.AppCompatEditText -import java.io.BufferedReader -import java.io.InputStreamReader -import java.lang.ref.WeakReference -import java.util.* -import java.util.concurrent.Semaphore -import kotlin.concurrent.thread +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.compose.runtime.Composable +import chat.simplex.app.ui.theme.SimpleXTheme +import androidx.lifecycle.AndroidViewModel +import chat.simplex.app.model.* +import chat.simplex.app.views.TerminalView +import kotlinx.serialization.* +import kotlinx.serialization.json.* +import kotlinx.serialization.modules.* -// ghc's rts -external fun initHS() -// android-support -external fun pipeStdOutToSocket(socketName: String) : Int -// simplex-chat -typealias Store = Long -typealias Controller = Long -external fun chatInit(filesDir: String): Store -external fun chatGetUser(controller: Store) : String -external fun chatCreateUser(controller: Store, data: String) : String -external fun chatStart(controller: Store) : Controller -external fun chatSendCmd(controller: Controller, msg: String) : String -external fun chatRecvMsg(controller: Controller) : String - -class MainActivity : AppCompatActivity() { +class MainActivity: ComponentActivity() { + private val viewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { - weakActivity = WeakReference(this) - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - - val store : Store = chatInit(this.applicationContext.filesDir.toString()) - // create user if needed - if(chatGetUser(store) == "{}") { - chatCreateUser(store, """ - {"displayName": "test", "fullName": "android test"} - """.trimIndent()) - } - Log.d("SIMPLEX (user)", chatGetUser(store)) - - val controller = chatStart(store) - - val cmdinput = this.findViewById(R.id.cmdInput) - - cmdinput.setOnEditorActionListener { _, actionId, _ -> - when (actionId) { - EditorInfo.IME_ACTION_SEND -> { - Log.d("SIMPLEX SEND", chatSendCmd(controller, cmdinput.text.toString())) - cmdinput.text?.clear() - true - } - else -> false + setContent { + SimpleXTheme { + MainPage(viewModel) } } - - thread(name="receiver") { - val chatlog = FifoQueue(500) - while(true) { - val msg = chatRecvMsg(controller) - Log.d("SIMPLEX RECV", msg) - chatlog.add(msg) - val currentText = chatlog.joinToString("\n") - weakActivity.get()?.runOnUiThread { - val log = weakActivity.get()?.findViewById(R.id.chatlog) - val scroll = weakActivity.get()?.findViewById(R.id.scroller) - log?.text = currentText - scroll?.scrollTo(0, scroll.getChildAt(0).height) - } - } - } - } - - companion object { - lateinit var weakActivity : WeakReference - init { - val socketName = "local.socket.address.listen.native.cmd2" - - val s = Semaphore(0) - thread(name="stdout/stderr pipe") { - Log.d("SIMPLEX", "starting server") - val server = LocalServerSocket(socketName) - Log.d("SIMPLEX", "started server") - s.release() - val receiver = server.accept() - Log.d("SIMPLEX", "started receiver") - val logbuffer = FifoQueue(500) - if (receiver != null) { - val inStream = receiver.inputStream - val inStreamReader = InputStreamReader(inStream) - val input = BufferedReader(inStreamReader) - - while(true) { - val line = input.readLine() ?: break - Log.d("SIMPLEX (stdout/stderr)", line) - logbuffer.add(line) - } - } - } - - System.loadLibrary("app-lib") - - s.acquire() - pipeStdOutToSocket(socketName) - - initHS() - } } } -class FifoQueue(private var capacity: Int) : LinkedList() { - override fun add(element: E): Boolean { - if(size > capacity) removeFirst() - return super.add(element) - } +class SimplexViewModel(application: Application) : AndroidViewModel(application) { + val chatModel = getApplication().chatModel +} + +@Composable +fun MainPage(vm: SimplexViewModel) { + TerminalView(vm.chatModel) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt new file mode 100644 index 0000000000..b08096a37e --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -0,0 +1,95 @@ +package chat.simplex.app + +import android.app.Application +import android.net.LocalServerSocket +import android.util.Log +import chat.simplex.app.model.ChatController +import chat.simplex.app.model.ChatModel +import java.io.BufferedReader +import java.io.InputStreamReader +import java.lang.ref.WeakReference +import java.util.* +import java.util.concurrent.Semaphore +import kotlin.concurrent.thread + +// ghc's rts +external fun initHS() +// android-support +external fun pipeStdOutToSocket(socketName: String) : Int + +// SimpleX API +typealias Controller = Long +typealias Store = Long +external fun chatInit(filesDir: String): Store +external fun chatGetUser(controller: Store) : String +external fun chatCreateUser(controller: Store, data: String) : String +external fun chatStart(controller: Store) : Controller +external fun chatSendCmd(controller: Controller, msg: String) : String +external fun chatRecvMsg(controller: Controller) : String + +class SimplexApp: Application() { + private lateinit var controller: ChatController + + override fun onCreate() { + super.onCreate() + val store: Store = chatInit(applicationContext.filesDir.toString()) + // create user if needed + if (chatGetUser(store) == "{}") { + chatCreateUser(store, """ + {"displayName": "test", "fullName": "android test"} + """.trimIndent()) + } + Log.d("SIMPLEX (user)", chatGetUser(store)) + controller = ChatController(chatStart(store)) + } + + val chatModel by lazy { + val m = ChatModel(controller) + controller.setModel(m) + controller.startReceiver() + m + } + + companion object { + lateinit var weakActivity: WeakReference + init { + val socketName = "local.socket.address.listen.native.cmd2" + + val s = Semaphore(0) + thread(name="stdout/stderr pipe") { + Log.d("SIMPLEX", "starting server") + val server = LocalServerSocket(socketName) + Log.d("SIMPLEX", "started server") + s.release() + val receiver = server.accept() + Log.d("SIMPLEX", "started receiver") + val logbuffer = FifoQueue(500) + if (receiver != null) { + val inStream = receiver.inputStream + val inStreamReader = InputStreamReader(inStream) + val input = BufferedReader(inStreamReader) + + while(true) { + val line = input.readLine() ?: break + Log.d("SIMPLEX (stdout/stderr)", line) + logbuffer.add(line) + } + } + } + + System.loadLibrary("app-lib") + + s.acquire() + pipeStdOutToSocket(socketName) + + initHS() + } + } +} + +class FifoQueue(private var capacity: Int) : LinkedList() { + override fun add(element: E): Boolean { + if(size > capacity) removeFirst() + return super.add(element) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt new file mode 100644 index 0000000000..0ecab183ad --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -0,0 +1,105 @@ +package chat.simplex.app.model + +import androidx.compose.runtime.mutableStateListOf +import kotlinx.serialization.Serializable +import java.util.* + +class ChatModel(val controller: ChatController) { + val currentUser: User? = null + var terminalItems = mutableStateListOf() + + companion object { + val sampleData: ChatModel get() { + val m = ChatModel(ChatController.Mock()) + m.terminalItems = mutableStateListOf( + TerminalItem.Cmd(CC.ShowActiveUser()), + TerminalItem.Resp(CR.ActiveUser(User.sampleData)) + ) + return m + } + } +} + +enum class ChatType(val type: String) { + Direct("@"), + Group("#"), + ContactRequest("<@") +} + +@Serializable +class User ( + val userId: Int, + val userContactId: Int, + val localDisplayName: String, + val profile: Profile, + val activeUser: Boolean +) : NamedChat { + override val displayName: String get() = profile.displayName + override val fullName: String get() = profile.fullName + + companion object { + val sampleData = User( + userId = 1, + userContactId = 1, + localDisplayName = "alice", + profile = Profile.sampleData, + activeUser = true + ) + } +} + +typealias ChatId = String + +@Serializable +class Contact( + val contactId: Int, + val localDisplayName: String, + val profile: Profile, + val activeConn: Connection, + val viaGroup: Int? = null, +// no serializer for type Date? +// val createdAt: Date +): NamedChat { + val id: ChatId get() = "@$contactId" + val apiId: Int get() = contactId + val ready: Boolean get() = activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready" + override val displayName: String get() = profile.displayName + override val fullName: String get() = profile.fullName + + companion object { + val sampleData = Contact( + contactId = 1, + localDisplayName = "alice", + profile = Profile.sampleData, + activeConn = Connection.sampleData +// createdAt = Date() + ) + } +} + +@Serializable +class Connection(val connStatus: String) { + companion object { + val sampleData = Connection(connStatus = "ready") + } +} + +@Serializable +class Profile( + val displayName: String, + val fullName: String + ) { + companion object { + val sampleData = Profile( + displayName = "alice", + fullName = "Alice" + ) + } +} + +interface NamedChat { + abstract val displayName: String + abstract val fullName: String + val chatViewName: String + get() = displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SImpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SImpleXAPI.kt new file mode 100644 index 0000000000..c6bc16871a --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SImpleXAPI.kt @@ -0,0 +1,161 @@ +package chat.simplex.app.model + +import android.util.Log +import chat.simplex.app.chatRecvMsg +import chat.simplex.app.chatSendCmd +import kotlinx.serialization.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import java.lang.Exception +import java.util.* +import kotlin.concurrent.thread + +typealias Controller = Long + +open class ChatController(val ctrl: Controller) { + private lateinit var chatModel: ChatModel + + fun setModel(m: ChatModel) { + chatModel = m + } + + fun startReceiver() { + thread(name="receiver") { +// val chatlog = FifoQueue(500) + while(true) { + val json = chatRecvMsg(ctrl) + Log.d("SIMPLEX chatRecvMsg: ", json) + chatModel.terminalItems.add(TerminalItem.Resp(APIResponse.decodeStr(json))) + } + } + } + + fun sendCmd(cmd: String) { + val json = chatSendCmd(ctrl, cmd) + Log.d("SIMPLEX chatSendCmd: ", cmd) + Log.d("SIMPLEX chatSendCmd response: ", json) + chatModel.terminalItems.add(TerminalItem.Resp(APIResponse.decodeStr(json))) + } + + class Mock: ChatController(0) {} +} + +// ChatCommand +abstract class CC { + abstract val cmdString: String + abstract val cmdType: String + + class Console(val cmd: String): CC() { + override val cmdString get() = cmd + override val cmdType get() = "console command" + } + + class ShowActiveUser: CC() { + override val cmdString get() = "/u" + override val cmdType get() = "ShowActiveUser" + } + + class CreateActiveUser(val profile: Profile): CC() { + override val cmdString get() = "/u ${profile.displayName} ${profile.fullName}" + override val cmdType get() = "CreateActiveUser" + } + + class StartChat: CC() { + override val cmdString get() = "/_start" + override val cmdType get() = "StartChat" + } + + class ApiGetChats: CC() { + override val cmdString get() = "/_get chats" + override val cmdType get() = "ApiGetChats" + } + + companion object { + fun chatRef(type: ChatType, id: String) = "${type}${id}" + } +} + +val json = Json { + prettyPrint = true + ignoreUnknownKeys = true +} + +@Serializable +class APIResponse(val resp: CR) { + companion object { + fun decodeStr(str: String): CR { + try { + return json.decodeFromString(str).resp + } catch(e: Exception) { + try { + val data = json.parseToJsonElement(str) + return CR.Response(data.jsonObject["resp"]!!.jsonObject["type"]?.toString() ?: "invalid", json.encodeToString(data)) + } catch(e: Exception) { + return CR.Invalid(str) + } + } + } + } +} + +// ChatResponse +@Serializable +sealed class CR { + abstract val responseType: String + abstract val details: String + + @Serializable + @SerialName("activeUser") + class ActiveUser(val user: User): CR() { + override val responseType get() = "activeUser" + override val details get() = user.toString() + } + + @Serializable + @SerialName("contactSubscribed") + class ContactSubscribed(val contact: Contact): CR() { + override val responseType get() = "contactSubscribed" + override val details get() = contact.toString() + } + + @Serializable + class Response(val type: String, val json: String): CR() { + override val responseType get() = "* ${type}" + override val details get() = json + } + + @Serializable + class Invalid(val str: String): CR() { + override val responseType get() = "* invalid json" + override val details get() = str + } + + // {"resp": {"activeUser": {"user": {}}}} + // {"resp": {"anythingElse": }} -> Unknown(type = "anythingElse", json = "") + + // {"type": "activeUser", "user": } +} + +abstract class TerminalItem { + val date = Date() + abstract val label: String + abstract val details: String + + class Cmd(val cmd: CC): TerminalItem() { + override val label get() = "> ${cmd.cmdString.substring(0, 30)}" + override val details get() = cmd.cmdString + } + + class Resp(val resp: CR): TerminalItem() { + override val label get() = "< ${resp.responseType}" + override val details get() = resp.details + } + + companion object { + val sampleData = listOf( + TerminalItem.Cmd(CC.ShowActiveUser()), + TerminalItem.Resp(CR.ActiveUser(User.sampleData)) + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt new file mode 100644 index 0000000000..40f91fc96e --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt @@ -0,0 +1,8 @@ +package chat.simplex.app.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple200 = Color(0xFFBB86FC) +val Purple500 = Color(0xFF6200EE) +val Purple700 = Color(0xFF3700B3) +val Teal200 = Color(0xFF03DAC5) \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Shape.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Shape.kt new file mode 100644 index 0000000000..d0a00450f6 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Shape.kt @@ -0,0 +1,11 @@ +package chat.simplex.app.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Shapes +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(0.dp) +) \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt new file mode 100644 index 0000000000..3d9d30c142 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt @@ -0,0 +1,44 @@ +package chat.simplex.app.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable + +private val DarkColorPalette = darkColors( + primary = Purple200, + primaryVariant = Purple700, + secondary = Teal200 +) + +private val LightColorPalette = lightColors( + primary = Purple500, + primaryVariant = Purple700, + secondary = Teal200 + + /* Other default colors to override + background = Color.White, + surface = Color.White, + onPrimary = Color.White, + onSecondary = Color.Black, + onBackground = Color.Black, + onSurface = Color.Black, + */ +) + +@Composable +fun SimpleXTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { + val colors = if (darkTheme) { + DarkColorPalette + } else { + LightColorPalette + } + + MaterialTheme( + colors = colors, + typography = Typography, + shapes = Shapes, + content = content + ) +} \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt new file mode 100644 index 0000000000..bb89f6e849 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt @@ -0,0 +1,28 @@ +package chat.simplex.app.ui.theme + +import androidx.compose.material.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + body1 = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp + ) + /* Other default text styles to override + button = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.W500, + fontSize = 14.sp + ), + caption = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 12.sp + ) + */ +) \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt new file mode 100644 index 0000000000..c85f9124e1 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -0,0 +1,38 @@ +package chat.simplex.app.views + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import chat.simplex.app.model.ChatModel +import chat.simplex.app.model.TerminalItem +import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.chat.SendMsgView + + +@Composable +fun TerminalView(chatModel: ChatModel) { + Column { + TerminalLog(chatModel.terminalItems) + SendMsgView(chatModel.controller::sendCmd) + } +} + +@Composable +fun TerminalLog(terminalItems: List) { + LazyColumn { + items(terminalItems) { item -> + Text(item.label) + } + } +} + +@Preview +@Composable +fun PreviewSendMsgView() { + SimpleXTheme { + TerminalView(chatModel = ChatModel.sampleData) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt new file mode 100644 index 0000000000..0573b30e7d --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt @@ -0,0 +1,40 @@ +package chat.simplex.app.views.chat + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.ui.theme.SimpleXTheme + +@Composable +fun SendMsgView (sendMessage: (String) -> Unit) { + var cmd by remember { mutableStateOf("") } + Row { + TextField(value = cmd, onValueChange = { cmd = it }, modifier = Modifier.height(60.dp)) + Spacer(Modifier.height(10.dp)) + Button( + onClick = { + sendMessage(cmd) + cmd = "" + }, + modifier = Modifier.width(40.dp).height(60.dp), + enabled = cmd.isNotEmpty() + ) { + Text("Go") + } + } +} + +@Preview +@Composable +fun PreviewSendMsgView() { + SimpleXTheme { + SendMsgView( + sendMessage = { msg -> println(msg) } + ) + } +} diff --git a/apps/android/app/src/main/res/layout/activity_main.xml b/apps/android/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 87429b38a0..0000000000 --- a/apps/android/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - - - - diff --git a/apps/android/app/src/main/res/values-night/themes.xml b/apps/android/app/src/main/res/values-night/themes.xml deleted file mode 100644 index 639e5393c7..0000000000 --- a/apps/android/app/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/apps/android/app/src/main/res/values/themes.xml b/apps/android/app/src/main/res/values/themes.xml index 781d40bcf4..ced725b2b2 100644 --- a/apps/android/app/src/main/res/values/themes.xml +++ b/apps/android/app/src/main/res/values/themes.xml @@ -1,16 +1,7 @@ - - - \ No newline at end of file diff --git a/apps/android/build.gradle b/apps/android/build.gradle index 9988a75fc6..6cb503be83 100644 --- a/apps/android/build.gradle +++ b/apps/android/build.gradle @@ -1,16 +1,25 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext { + compose_version = '1.1.0' + } repositories { google() mavenCentral() } dependencies { - classpath "com.android.tools.build:gradle:7.0.4" + classpath 'com.android.tools.build:gradle:7.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10" + classpath "org.jetbrains.kotlin:kotlin-serialization:1.3.2" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } +}// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '7.1.1' apply false + id 'com.android.library' version '7.1.1' apply false + id 'org.jetbrains.kotlin.android' version '1.6.10' apply false + id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10' } task clean(type: Delete) { diff --git a/apps/android/gradle.properties b/apps/android/gradle.properties index 98bed167dc..831abc7d9f 100644 --- a/apps/android/gradle.properties +++ b/apps/android/gradle.properties @@ -15,7 +15,11 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # Android operating system, and which are packaged with your app"s APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official \ No newline at end of file +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true \ No newline at end of file diff --git a/apps/android/gradle/wrapper/gradle-wrapper.jar b/apps/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 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

K7UpUK$>l@ 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 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/apps/android/gradle/wrapper/gradle-wrapper.properties b/apps/android/gradle/wrapper/gradle-wrapper.properties index 05bd558a5e..2da1e43ef5 100644 --- a/apps/android/gradle/wrapper/gradle-wrapper.properties +++ b/apps/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jan 21 23:13:54 GMT 2022 +#Mon Feb 14 14:23:51 GMT 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/apps/android/settings.gradle b/apps/android/settings.gradle index 71e4f1f472..64c6620acf 100644 --- a/apps/android/settings.gradle +++ b/apps/android/settings.gradle @@ -1,9 +1,15 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() - jcenter() // Warning: this repository is going to shut down soon } } rootProject.name = "SimpleX" From 241d02584ab2a34e594555d078a9a0082f295d4e Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 16 Feb 2022 13:22:36 +0000 Subject: [PATCH 07/46] use different names for different build bundles (#315) --- flake.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.nix b/flake.nix index 27c1a3044f..6b07d1f2e6 100644 --- a/flake.nix +++ b/flake.nix @@ -75,7 +75,7 @@ mkdir -p $out/_pkg cp libsupport.so $out/_pkg ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsupport.so - (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg.zip *) + (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-android-libsupport.zip *) rm -fR $out/_pkg mkdir -p $out/nix-support @@ -107,7 +107,7 @@ ${pkgs.patchelf}/bin/patchelf --remove-needed libunwind.so.1 $out/_pkg/libsimplex.so ${pkgs.tree}/bin/tree $out/_pkg - (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg.zip *) + (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-android-libsimplex.zip *) rm -fR $out/_pkg mkdir -p $out/nix-support echo "file binary-dist \"$(echo $out/*.zip)\"" \ @@ -134,7 +134,7 @@ find ${pkgs.gmp6.override { withStatic = true; }}/lib -name "*.a" -exec cp {} $out/_pkg \; # There is no static libc ${pkgs.tree}/bin/tree $out/_pkg - (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg.zip *) + (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-ios-aarch64.zip *) rm -fR $out/_pkg mkdir -p $out/nix-support echo "file binary-dist \"$(echo $out/*.zip)\"" \ @@ -161,7 +161,7 @@ find ${pkgs.gmp6.override { withStatic = true; }}/lib -name "*.a" -exec cp {} $out/_pkg \; # There is no static libc ${pkgs.tree}/bin/tree $out/_pkg - (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg.zip *) + (cd $out/_pkg; ${pkgs.zip}/bin/zip -r -9 $out/pkg-ios-x86_64.zip *) rm -fR $out/_pkg mkdir -p $out/nix-support echo "file binary-dist \"$(echo $out/*.zip)\"" \ From 12b4325435a19a93b2cbb76587cc6327305b02b9 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 16 Feb 2022 17:36:49 +0000 Subject: [PATCH 08/46] switch to the new API (does not work) (#317) * switch to the new API (does not work) * kind of works without parsing JSON --- .../.idea/deploymentTargetDropDown.xml | 8 +-- apps/android/app/src/main/cpp/simplex-api.c | 32 ++---------- .../main/java/chat/simplex/app/SimplexApp.kt | 35 +++++++------ .../java/chat/simplex/app/model/SImpleXAPI.kt | 49 ++++++++++++++----- .../chat/simplex/app/views/TerminalView.kt | 5 +- 5 files changed, 67 insertions(+), 62 deletions(-) diff --git a/apps/android/.idea/deploymentTargetDropDown.xml b/apps/android/.idea/deploymentTargetDropDown.xml index b2a5949802..230c77f89f 100644 --- a/apps/android/.idea/deploymentTargetDropDown.xml +++ b/apps/android/.idea/deploymentTargetDropDown.xml @@ -6,12 +6,12 @@ - - + + - - + + \ No newline at end of file diff --git a/apps/android/app/src/main/cpp/simplex-api.c b/apps/android/app/src/main/cpp/simplex-api.c index f2dadcb131..e9178df5c4 100644 --- a/apps/android/app/src/main/cpp/simplex-api.c +++ b/apps/android/app/src/main/cpp/simplex-api.c @@ -22,42 +22,20 @@ Java_chat_simplex_app_SimplexAppKt_initHS(__unused JNIEnv *env, __unused jclass } // from simplex-chat -typedef void* chat_store; -typedef void* controller; +typedef void* chat_ctrl; -extern chat_store chat_init_store(const char * path); -extern char *chat_get_user(chat_store store); -extern char *chat_create_user(chat_store store, const char *data); -extern controller chat_start(chat_store store); -extern char *chat_send_cmd(controller ctl, const char *cmd); -extern char *chat_recv_msg(controller ctl); +extern chat_ctrl chat_init(const char * path); +extern char *chat_send_cmd(chat_ctrl ctrl, const char *cmd); +extern char *chat_recv_msg(chat_ctrl ctrl); JNIEXPORT jlong JNICALL Java_chat_simplex_app_SimplexAppKt_chatInit(JNIEnv *env, __unused jclass clazz, jstring datadir) { const char *_data = (*env)->GetStringUTFChars(env, datadir, JNI_FALSE); - jlong res = (jlong)chat_init_store(_data); + jlong res = (jlong)chat_init(_data); (*env)->ReleaseStringUTFChars(env, datadir, _data); return res; } -JNIEXPORT jstring JNICALL -Java_chat_simplex_app_SimplexAppKt_chatGetUser(JNIEnv *env, __unused jclass clazz, jlong controller) { - return (*env)->NewStringUTF(env, chat_get_user((void*)controller)); -} - -JNIEXPORT jstring JNICALL -Java_chat_simplex_app_SimplexAppKt_chatCreateUser(JNIEnv *env, __unused jclass clazz, jlong controller, jstring data) { - const char *_data = (*env)->GetStringUTFChars(env, data, JNI_FALSE); - jstring res = (*env)->NewStringUTF(env, chat_create_user((void*)controller, _data)); - (*env)->ReleaseStringUTFChars(env, data, _data); - return res; -} - -JNIEXPORT jlong JNICALL -Java_chat_simplex_app_SimplexAppKt_chatStart(JNIEnv *env, jclass clazz, jlong controller) { - return (jlong)chat_start((void*)controller); -} - JNIEXPORT jstring JNICALL Java_chat_simplex_app_SimplexAppKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) { const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE); diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index b08096a37e..2d940c067c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -3,8 +3,7 @@ package chat.simplex.app import android.app.Application import android.net.LocalServerSocket import android.util.Log -import chat.simplex.app.model.ChatController -import chat.simplex.app.model.ChatModel +import chat.simplex.app.model.* import java.io.BufferedReader import java.io.InputStreamReader import java.lang.ref.WeakReference @@ -18,29 +17,29 @@ external fun initHS() external fun pipeStdOutToSocket(socketName: String) : Int // SimpleX API -typealias Controller = Long -typealias Store = Long -external fun chatInit(filesDir: String): Store -external fun chatGetUser(controller: Store) : String -external fun chatCreateUser(controller: Store, data: String) : String -external fun chatStart(controller: Store) : Controller -external fun chatSendCmd(controller: Controller, msg: String) : String -external fun chatRecvMsg(controller: Controller) : String +typealias ChatCtrl = Long +external fun chatInit(path: String): ChatCtrl +external fun chatSendCmd(ctrl: ChatCtrl, msg: String) : String +external fun chatRecvMsg(ctrl: ChatCtrl) : String class SimplexApp: Application() { private lateinit var controller: ChatController override fun onCreate() { super.onCreate() - val store: Store = chatInit(applicationContext.filesDir.toString()) - // create user if needed - if (chatGetUser(store) == "{}") { - chatCreateUser(store, """ - {"displayName": "test", "fullName": "android test"} - """.trimIndent()) + controller = ChatController(chatInit(applicationContext.filesDir.toString())) + var user = controller.apiGetActiveUser() + if (user == null) { +// user = controller.apiCreateActiveUser(Profile("android", "Android test")) + } + Log.d("SIMPLEX (user)", user.toString()) + try { + controller.apiStartChat() + Log.d("SIMPLEX", "started chat") + } catch(e: Error) { + Log.d("SIMPLEX", "failed starting chat $e") +// throw e } - Log.d("SIMPLEX (user)", chatGetUser(store)) - controller = ChatController(chatStart(store)) } val chatModel by lazy { diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SImpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SImpleXAPI.kt index c6bc16871a..1dec8dabb6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SImpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SImpleXAPI.kt @@ -14,7 +14,7 @@ import kotlin.concurrent.thread typealias Controller = Long open class ChatController(val ctrl: Controller) { - private lateinit var chatModel: ChatModel + private var chatModel: ChatModel? = null fun setModel(m: ChatModel) { chatModel = m @@ -26,16 +26,35 @@ open class ChatController(val ctrl: Controller) { while(true) { val json = chatRecvMsg(ctrl) Log.d("SIMPLEX chatRecvMsg: ", json) - chatModel.terminalItems.add(TerminalItem.Resp(APIResponse.decodeStr(json))) + chatModel?.terminalItems?.add(TerminalItem.Resp(APIResponse.decodeStr(json))) } } } - fun sendCmd(cmd: String) { - val json = chatSendCmd(ctrl, cmd) - Log.d("SIMPLEX chatSendCmd: ", cmd) - Log.d("SIMPLEX chatSendCmd response: ", json) - chatModel.terminalItems.add(TerminalItem.Resp(APIResponse.decodeStr(json))) + fun sendCmd(cmd: CC): CR { + val c = cmd.cmdString + val json = chatSendCmd(ctrl, c) + Log.d("SIMPLEX", "sendCmd: $c") + Log.d("SIMPLEX", "sendCmd response $json") + chatModel?.terminalItems?.add(TerminalItem.Resp(APIResponse.decodeStr(json))) + return APIResponse.decodeStr(json) + } + + fun apiGetActiveUser(): User? { + val r = sendCmd(CC.ShowActiveUser()) + return if (r is CR.ActiveUser) r.user else null + } + + fun apiCreateActiveUser(p: Profile): User { + val r = sendCmd(CC.CreateActiveUser(p)) + if (r is CR.ActiveUser) return r.user + throw Error("failed creating user: ${r.toString()}") + } + + fun apiStartChat() { + val r = sendCmd(CC.StartChat()) + if (r is CR.ChatStarted) return + throw Error("failed starting chat: ${r.toString()}") } class Mock: ChatController(0) {} @@ -112,6 +131,13 @@ sealed class CR { override val details get() = user.toString() } + @Serializable + @SerialName("chatStarted") + class ChatStarted: CR() { + override val responseType get() = "chatStarted" + override val details get() = CR.noDetails(this) + } + @Serializable @SerialName("contactSubscribed") class ContactSubscribed(val contact: Contact): CR() { @@ -121,7 +147,7 @@ sealed class CR { @Serializable class Response(val type: String, val json: String): CR() { - override val responseType get() = "* ${type}" + override val responseType get() = "* $type" override val details get() = json } @@ -131,10 +157,9 @@ sealed class CR { override val details get() = str } - // {"resp": {"activeUser": {"user": {}}}} - // {"resp": {"anythingElse": }} -> Unknown(type = "anythingElse", json = "") - - // {"type": "activeUser", "user": } + companion object { + fun noDetails(r: CR): String ="${r.responseType}: no details" + } } abstract class TerminalItem { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index c85f9124e1..05f990dcf7 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview +import chat.simplex.app.model.CC import chat.simplex.app.model.ChatModel import chat.simplex.app.model.TerminalItem import chat.simplex.app.ui.theme.SimpleXTheme @@ -16,7 +17,9 @@ import chat.simplex.app.views.chat.SendMsgView fun TerminalView(chatModel: ChatModel) { Column { TerminalLog(chatModel.terminalItems) - SendMsgView(chatModel.controller::sendCmd) + SendMsgView(sendMessage = { cmd -> + chatModel.controller.sendCmd(CC.Console(cmd)) + }) } } From 7c8ad4aee491b0478b1347c8228ef3e54be69bc8 Mon Sep 17 00:00:00 2001 From: IanRDavies Date: Wed, 16 Feb 2022 18:00:59 +0000 Subject: [PATCH 09/46] Android compose navigation (#316) * initial rough ideas * refactor and put in high level navigation * refactor Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- apps/android/.idea/codeStyles/Project.xml | 117 ++++++++++++++++++ .../.idea/codeStyles/codeStyleConfig.xml | 5 + .../.idea/deploymentTargetDropDown.xml | 2 +- apps/android/.idea/misc.xml | 2 + apps/android/app/build.gradle | 1 + .../java/chat/simplex/app/MainActivity.kt | 29 +++-- .../main/java/chat/simplex/app/MainPage.kt | 10 ++ .../chat/simplex/app/views/TerminalView.kt | 51 ++++++-- .../chat/simplex/app/views/WelcomeView.kt | 48 +++++++ .../app/views/usersettings/SettingsView.kt | 61 +++++++++ .../app/src/main/res/drawable/logo.png | Bin 0 -> 19731 bytes 11 files changed, 306 insertions(+), 20 deletions(-) create mode 100644 apps/android/.idea/codeStyles/Project.xml create mode 100644 apps/android/.idea/codeStyles/codeStyleConfig.xml create mode 100644 apps/android/app/src/main/java/chat/simplex/app/MainPage.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt create mode 100644 apps/android/app/src/main/res/drawable/logo.png diff --git a/apps/android/.idea/codeStyles/Project.xml b/apps/android/.idea/codeStyles/Project.xml new file mode 100644 index 0000000000..4bec4ea8ae --- /dev/null +++ b/apps/android/.idea/codeStyles/Project.xml @@ -0,0 +1,117 @@ + + + +

+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+ + + + + \ No newline at end of file diff --git a/apps/android/.idea/codeStyles/codeStyleConfig.xml b/apps/android/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000000..a55e7a179b --- /dev/null +++ b/apps/android/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/apps/android/.idea/deploymentTargetDropDown.xml b/apps/android/.idea/deploymentTargetDropDown.xml index 230c77f89f..57b651efa0 100644 --- a/apps/android/.idea/deploymentTargetDropDown.xml +++ b/apps/android/.idea/deploymentTargetDropDown.xml @@ -12,6 +12,6 @@ - + \ No newline at end of file diff --git a/apps/android/.idea/misc.xml b/apps/android/.idea/misc.xml index 1c2eb53046..f0d8957869 100644 --- a/apps/android/.idea/misc.xml +++ b/apps/android/.idea/misc.xml @@ -7,6 +7,8 @@ + + diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 0f2c5d0297..082294a40e 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -73,4 +73,5 @@ dependencies { androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" + implementation "androidx.navigation:navigation-compose:2.4.1" } diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 2a62c745fb..dfd150c0d2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -8,11 +8,9 @@ import androidx.activity.viewModels import androidx.compose.runtime.Composable import chat.simplex.app.ui.theme.SimpleXTheme import androidx.lifecycle.AndroidViewModel -import chat.simplex.app.model.* -import chat.simplex.app.views.TerminalView -import kotlinx.serialization.* -import kotlinx.serialization.json.* -import kotlinx.serialization.modules.* +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController class MainActivity: ComponentActivity() { @@ -21,7 +19,7 @@ class MainActivity: ComponentActivity() { super.onCreate(savedInstanceState) setContent { SimpleXTheme { - MainPage(viewModel) + Navigation(viewModel = viewModel) } } } @@ -32,6 +30,21 @@ class SimplexViewModel(application: Application) : AndroidViewModel(application) } @Composable -fun MainPage(vm: SimplexViewModel) { - TerminalView(vm.chatModel) +fun Navigation(viewModel: SimplexViewModel) { + val navController = rememberNavController() + + NavHost(navController=navController, startDestination=Pages.Home.route){ + composable(route=Pages.Home.route){ + MainPage(vm = viewModel) + } +// composable(route=Pages.Welcome.route){ +// WelcomeView(vm.) +// } + } +} + +sealed class Pages(val route: String) { + object Home : Pages("home") + object Terminal : Pages("terminal") + object Welcome : Pages("welcome") } diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt new file mode 100644 index 0000000000..6a596e81e8 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt @@ -0,0 +1,10 @@ +package chat.simplex.app + +import androidx.compose.runtime.Composable +import chat.simplex.app.views.TerminalPage + +@Composable +fun MainPage(vm: SimplexViewModel) { + + TerminalPage(vm.chatModel) +} \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index 05f990dcf7..a129d75b96 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -3,20 +3,41 @@ package chat.simplex.app.views import androidx.compose.foundation.layout.Column import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.ClickableText import androidx.compose.material.Text +import androidx.compose.material.Button import androidx.compose.runtime.Composable +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview -import chat.simplex.app.model.CC -import chat.simplex.app.model.ChatModel -import chat.simplex.app.model.TerminalItem +import androidx.navigation.* +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import chat.simplex.app.model.* import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.SendMsgView @Composable -fun TerminalView(chatModel: ChatModel) { +fun TerminalPage(chatModel: ChatModel) { + val navController = rememberNavController() + NavHost(navController = navController, startDestination = "terminalView"){ + composable("terminalView") { TerminalView(chatModel, navController) } + composable( + "details" + "/{_details}", + arguments=listOf( + navArgument("_details"){ + type = NavType.StringType + } + ) + ) { entry -> DetailView( entry.arguments?.getString("_details"), navController)} + } +} + +@Composable +fun TerminalView(chatModel: ChatModel, navController: NavController) { Column { - TerminalLog(chatModel.terminalItems) + TerminalLog(chatModel.terminalItems, navController) SendMsgView(sendMessage = { cmd -> chatModel.controller.sendCmd(CC.Console(cmd)) }) @@ -24,18 +45,26 @@ fun TerminalView(chatModel: ChatModel) { } @Composable -fun TerminalLog(terminalItems: List) { +fun TerminalLog(terminalItems: List, navController: NavController) { LazyColumn { items(terminalItems) { item -> - Text(item.label) + ClickableText( + AnnotatedString(item.label), + onClick={navController.navigate("details/${item.details}")} + ) } } } -@Preview @Composable -fun PreviewSendMsgView() { - SimpleXTheme { - TerminalView(chatModel = ChatModel.sampleData) +fun DetailView(details: String?, navController: NavController){ + Column { + Text("$details") + + Button( + onClick={navController.popBackStack()} + ) + {Text("Back")} } } + diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt new file mode 100644 index 0000000000..e0ed85466f --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt @@ -0,0 +1,48 @@ +package chat.simplex.app.views + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.Image +import androidx.compose.ui.res.painterResource +import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import chat.simplex.app.R +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.Button +import androidx.compose.ui.unit.dp +import androidx.compose.ui.Modifier +import androidx.navigation.NavController + +@Composable +fun WelcomeView(createUser: (data: String) -> String, navController: NavController) { + Column { + Image( + painter=painterResource(R.drawable.logo), contentDescription = "Simplex Logo", + ) + Text("You control your chat!") + Text("The messaging and application platform protecting your privacy and security.") + Spacer(Modifier.height(8.dp)) + Text("We don't store any of your contacts or messages (once delivered) on the servers.") + Spacer(Modifier.height(24.dp)) + CreateProfilePanel(createUser, navController) + } +} + +@Composable +fun CreateProfilePanel(createUser: (data: String) -> String, navController: NavController) { + var displayName by remember { mutableStateOf("") } + var fullName by remember { mutableStateOf("") } + + Column { + Text("Create profile") + Text("Your profile is stored on your device and shared only with your contacts.") + Text("Display Name") + TextField(value = displayName, onValueChange = { displayName = it }, modifier = Modifier.height(30.dp)) + Text("Full Name (Optional)") + TextField(value = fullName, onValueChange = { fullName = it }, modifier = Modifier.height(30.dp)) + Button(onClick={ + createUser("{\"displayName\": $displayName, \"fullName\": $fullName}") + navController.navigate("home") { popUpTo("home") { inclusive = true }} + }) { Text("Create")} + } +} \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt new file mode 100644 index 0000000000..35c54c5f69 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -0,0 +1,61 @@ +package chat.simplex.app.views.usersettings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.model.Profile + +//@Preview(showBackground = true) +@Composable +fun SettingsView(profile: Profile) { + Column() { + Text("Your Settings") + Spacer(Modifier.height(4.dp)) + Text("YOU", style= MaterialTheme.typography.h4) + Button( + onClick = { println(profile.displayName) } + ) { + Text(profile.displayName) + } + Button( + onClick = { println(profile.hashCode()) } + ) { + Text("Your SimpleX contact address", style= MaterialTheme.typography.body1) + } + Spacer(Modifier.height(10.dp)) + Text("HELP", style= MaterialTheme.typography.h4) + Button( + onClick = { println("navigate to help") } + ) { + Text("How to use SimpleX Chat") + } + Button( + onClick = { println("start help chat") } + ) { + Text("Get help & advice via chat") + } + Button( + onClick = { println("navigate to email") } + ) { + Text("Ask questions via email") + } + Spacer(Modifier.height(10.dp)) + Text("DEVELOP", style= MaterialTheme.typography.h4) + Button( + onClick = { println("navigate to console") } + ) { + Text("Chat console") + } + Button( + onClick = { println("navigate to github") } + ) { + Text("Install SimpleX for terminal") + } + } +} + diff --git a/apps/android/app/src/main/res/drawable/logo.png b/apps/android/app/src/main/res/drawable/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7f25ea17d0894bc6192f012c9b16a697fcb9fc7a GIT binary patch literal 19731 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 Date: Wed, 16 Feb 2022 22:18:27 +0400 Subject: [PATCH 10/46] update simplexmq sha (#318) --- cabal.project | 2 +- sha256map.nix | 2 +- stack.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cabal.project b/cabal.project index 80421d01a3..276b67617f 100644 --- a/cabal.project +++ b/cabal.project @@ -3,7 +3,7 @@ packages: . source-repository-package type: git location: git://github.com/simplex-chat/simplexmq.git - tag: 137afb68fe02ac05c439c870db7c626fb6ef7880 + tag: 9c4778b12920db632322be27c3faf74bc9db3676 source-repository-package type: git diff --git a/sha256map.nix b/sha256map.nix index cbcbeaddf2..e8b0e6bb4c 100644 --- a/sha256map.nix +++ b/sha256map.nix @@ -1,5 +1,5 @@ { - "git://github.com/simplex-chat/simplexmq.git"."137afb68fe02ac05c439c870db7c626fb6ef7880" = "12n5zxd2bpiidb4ps3hm4wwx22ynvcknsgfwq0868wg941rd238f"; + "git://github.com/simplex-chat/simplexmq.git"."9c4778b12920db632322be27c3faf74bc9db3676" = "0vkqkg0nl1z0ccggj1bjqv1i57vijhmw4qfm2myv07z88ch8brg8"; "git://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "git://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "git://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index 09cffe958c..2b0db498a8 100644 --- a/stack.yaml +++ b/stack.yaml @@ -48,7 +48,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 137afb68fe02ac05c439c870db7c626fb6ef7880 + commit: 9c4778b12920db632322be27c3faf74bc9db3676 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 From ee07921d42d95393d32a6547d7d2b147d0b3b6c8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 16 Feb 2022 18:49:48 +0000 Subject: [PATCH 11/46] update cabal file - GHC option for android --- simplex-chat.cabal | 3 +++ 1 file changed, 3 insertions(+) diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 7f665785e3..6b8e4f4d0b 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -16,6 +16,9 @@ license-file: LICENSE build-type: Simple extra-source-files: README.md + +if (os(android)) + ghc-option: -DANDROID library exposed-modules: From ec706706309f0b545f1c83a888adbfc352b15366 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 16 Feb 2022 20:11:29 +0000 Subject: [PATCH 12/46] update condition in cabal file --- simplex-chat.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 6b8e4f4d0b..7b964a27d1 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -17,7 +17,7 @@ build-type: Simple extra-source-files: README.md -if (os(android)) +if (os(linux)) ghc-option: -DANDROID library From f8951b44fcbcbb7fcca0aeb0e8d40150de881608 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 16 Feb 2022 20:31:26 +0000 Subject: [PATCH 13/46] use sync commands (#319) --- src/Simplex/Chat.hs | 20 ++++++++++---------- src/Simplex/Chat/View.hs | 6 +++--- tests/ChatTests.hs | 1 + 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 715e460c87..010cabc3d8 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -382,17 +382,17 @@ processChatCommand = \case withAgentLock a . withLock l $ action -- below code would make command responses asynchronous where they can be slow -- in View.hs `r'` should be defined as `id` in this case - procCmd :: m ChatResponse -> m ChatResponse - procCmd action = do - ChatController {chatLock = l, smpAgent = a, outputQ = q, idsDrg = gVar} <- ask - corrId <- liftIO $ SMP.CorrId <$> randomBytes gVar 8 - void . forkIO $ - withAgentLock a . withLock l $ - (atomically . writeTBQueue q) . (Just corrId,) =<< (action `catchError` (pure . CRChatError)) - pure $ CRCmdAccepted corrId - -- use function below to make commands "synchronous" -- procCmd :: m ChatResponse -> m ChatResponse - -- procCmd = id + -- procCmd action = do + -- ChatController {chatLock = l, smpAgent = a, outputQ = q, idsDrg = gVar} <- ask + -- corrId <- liftIO $ SMP.CorrId <$> randomBytes gVar 8 + -- void . forkIO $ + -- withAgentLock a . withLock l $ + -- (atomically . writeTBQueue q) . (Just corrId,) =<< (action `catchError` (pure . CRChatError)) + -- pure $ CRCmdAccepted corrId + -- use function below to make commands "synchronous" + procCmd :: m ChatResponse -> m ChatResponse + procCmd = id connectViaContact :: UserId -> ConnectionRequestUri 'CMContact -> Profile -> m ChatResponse connectViaContact userId cReq profile = withChatLock $ do let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 08ad47543c..dc77335354 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -67,7 +67,7 @@ responseToView cmd testView = \case CRSentConfirmation -> r' ["confirmation sent!"] CRSentInvitation -> r' ["connection request sent!"] CRContactDeleted c -> r' [ttyContact' c <> ": contact is deleted"] - CRAcceptingContactRequest c -> r' [ttyFullContact c <> ": accepting contact request..."] + CRAcceptingContactRequest c -> r [ttyFullContact c <> ": accepting contact request..."] CRContactAlreadyExists c -> r [ttyFullContact c <> ": contact already exists"] CRContactRequestAlreadyAccepted c -> r' [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] CRUserContactLinkCreated cReq -> r' $ connReqContact_ "Your new chat address is created!" cReq @@ -125,8 +125,8 @@ responseToView cmd testView = \case where r = (plain cmd :) -- this function should be `r` for "synchronous", `id` for "asynchronous" command responses - -- r' = r - r' = id + r' = r + -- r' = id testViewChats :: [AChat] -> [StyledString] testViewChats chats = [sShow $ map toChatView chats] where diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index 9e7ecb1404..f10bba0d82 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -744,6 +744,7 @@ testUserContactLinkAutoAccept = cath ##> ("/c " <> cLink) cath <## "connection request sent!" + alice <## "" alice <## "cath (Catherine): accepting contact request..." concurrently_ (cath <## "alice (Alice): contact is connected") From d743804b1d8ffeaac1a2db7f186c224fa048a8fe Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 16 Feb 2022 21:31:22 +0000 Subject: [PATCH 14/46] update android api to call haskell off main thread (#320) --- .../main/java/chat/simplex/app/SimplexApp.kt | 26 ++++--- .../java/chat/simplex/app/model/SImpleXAPI.kt | 72 ++++++++++++------- .../chat/simplex/app/views/TerminalView.kt | 28 ++++---- 3 files changed, 81 insertions(+), 45 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index 2d940c067c..3487ad9305 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -4,6 +4,10 @@ import android.app.Application import android.net.LocalServerSocket import android.util.Log import chat.simplex.app.model.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.BufferedReader import java.io.InputStreamReader import java.lang.ref.WeakReference @@ -28,17 +32,21 @@ class SimplexApp: Application() { override fun onCreate() { super.onCreate() controller = ChatController(chatInit(applicationContext.filesDir.toString())) - var user = controller.apiGetActiveUser() - if (user == null) { + GlobalScope.launch { + withContext(Dispatchers.Main) { + var user = controller.apiGetActiveUser() + if (user == null) { // user = controller.apiCreateActiveUser(Profile("android", "Android test")) - } - Log.d("SIMPLEX (user)", user.toString()) - try { - controller.apiStartChat() - Log.d("SIMPLEX", "started chat") - } catch(e: Error) { - Log.d("SIMPLEX", "failed starting chat $e") + } + Log.d("SIMPLEX (user)", user.toString()) + try { + controller.apiStartChat() + Log.d("SIMPLEX", "started chat") + } catch(e: Error) { + Log.d("SIMPLEX", "failed starting chat $e") // throw e + } + } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SImpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SImpleXAPI.kt index 1dec8dabb6..82a514221e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SImpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SImpleXAPI.kt @@ -3,18 +3,20 @@ package chat.simplex.app.model import android.util.Log import chat.simplex.app.chatRecvMsg import chat.simplex.app.chatSendCmd +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import kotlinx.serialization.* import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive import java.lang.Exception import java.util.* import kotlin.concurrent.thread -typealias Controller = Long +typealias ChatCtrl = Long -open class ChatController(val ctrl: Controller) { +open class ChatController(val ctrl: ChatCtrl) { private var chatModel: ChatModel? = null + private val callbacks = mutableMapOf Unit>() fun setModel(m: ChatModel) { chatModel = m @@ -26,34 +28,46 @@ open class ChatController(val ctrl: Controller) { while(true) { val json = chatRecvMsg(ctrl) Log.d("SIMPLEX chatRecvMsg: ", json) - chatModel?.terminalItems?.add(TerminalItem.Resp(APIResponse.decodeStr(json))) + val r = APIResponse.decodeStr(json) + if (r.corr != null) { + val cb = callbacks[r.corr] + if (cb != null) cb(null, r.resp) + } + chatModel?.terminalItems?.add(TerminalItem.Resp(r.resp)) } } } - fun sendCmd(cmd: CC): CR { - val c = cmd.cmdString - val json = chatSendCmd(ctrl, c) - Log.d("SIMPLEX", "sendCmd: $c") - Log.d("SIMPLEX", "sendCmd response $json") - chatModel?.terminalItems?.add(TerminalItem.Resp(APIResponse.decodeStr(json))) - return APIResponse.decodeStr(json) + suspend fun sendCmd(cmd: CC): CR { + return withContext(Dispatchers.IO) { + val c = cmd.cmdString + chatModel?.terminalItems?.add(TerminalItem.Cmd(cmd)) + val json = chatSendCmd(ctrl, c) + Log.d("SIMPLEX", "sendCmd: $c") + Log.d("SIMPLEX", "sendCmd response $json") + val r = APIResponse.decodeStr(json) + chatModel?.terminalItems?.add(TerminalItem.Resp(r.resp)) + r.resp + } } - fun apiGetActiveUser(): User? { + suspend fun apiGetActiveUser(): User? { val r = sendCmd(CC.ShowActiveUser()) - return if (r is CR.ActiveUser) r.user else null + if (r is CR.ActiveUser) return r.user + Log.d("SIMPLEX", "apiGetActiveUser: ${r.toString()}") + return null } - fun apiCreateActiveUser(p: Profile): User { + suspend fun apiCreateActiveUser(p: Profile): User { val r = sendCmd(CC.CreateActiveUser(p)) if (r is CR.ActiveUser) return r.user - throw Error("failed creating user: ${r.toString()}") + Log.d("SIMPLEX", "apiCreateActiveUser: ${r.toString()}") + throw Error("user not created ${r.toString()}") } - fun apiStartChat() { + suspend fun apiStartChat() { val r = sendCmd(CC.StartChat()) - if (r is CR.ChatStarted) return + if (r is CR.ChatStarted ) return throw Error("failed starting chat: ${r.toString()}") } @@ -101,17 +115,20 @@ val json = Json { } @Serializable -class APIResponse(val resp: CR) { +class APIResponse(val resp: CR, val corr: String? = null) { companion object { - fun decodeStr(str: String): CR { + fun decodeStr(str: String): APIResponse { try { - return json.decodeFromString(str).resp + return json.decodeFromString(str) } catch(e: Exception) { try { - val data = json.parseToJsonElement(str) - return CR.Response(data.jsonObject["resp"]!!.jsonObject["type"]?.toString() ?: "invalid", json.encodeToString(data)) + val data = json.parseToJsonElement(str).jsonObject + return APIResponse( + resp = CR.Response(data["resp"]!!.jsonObject["type"]?.toString() ?: "invalid", json.encodeToString(data)), + corr = data["corr"]?.toString() + ) } catch(e: Exception) { - return CR.Invalid(str) + return APIResponse(CR.Invalid(str)) } } } @@ -145,6 +162,13 @@ sealed class CR { override val details get() = contact.toString() } + @Serializable + @SerialName("cmdAccepted") + class CmdAccepted(val corr: String): CR() { + override val responseType get() = "cmdAccepted" + override val details get() = "corrId: $corr" + } + @Serializable class Response(val type: String, val json: String): CR() { override val responseType get() = "* $type" @@ -168,7 +192,7 @@ abstract class TerminalItem { abstract val details: String class Cmd(val cmd: CC): TerminalItem() { - override val label get() = "> ${cmd.cmdString.substring(0, 30)}" + override val label get() = "> ${cmd.cmdString}" override val details get() = cmd.cmdString } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index a129d75b96..8f9fcf916e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -8,15 +8,16 @@ import androidx.compose.material.Text import androidx.compose.material.Button import androidx.compose.runtime.Composable import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.* import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.SendMsgView - +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @Composable fun TerminalPage(chatModel: ChatModel) { @@ -30,7 +31,7 @@ fun TerminalPage(chatModel: ChatModel) { type = NavType.StringType } ) - ) { entry -> DetailView( entry.arguments?.getString("_details"), navController)} + ) { entry -> DetailView( entry.arguments?.getString("_details"), navController) } } } @@ -39,7 +40,13 @@ fun TerminalView(chatModel: ChatModel, navController: NavController) { Column { TerminalLog(chatModel.terminalItems, navController) SendMsgView(sendMessage = { cmd -> - chatModel.controller.sendCmd(CC.Console(cmd)) + GlobalScope.launch { + withContext(Dispatchers.Main) { + // show "in progress" + chatModel.controller.sendCmd(CC.Console(cmd)) + // hide "in progress" + } + } }) } } @@ -50,7 +57,7 @@ fun TerminalLog(terminalItems: List, navController: NavController) items(terminalItems) { item -> ClickableText( AnnotatedString(item.label), - onClick={navController.navigate("details/${item.details}")} + onClick = { navController.navigate("details/${item.details}") } ) } } @@ -60,11 +67,8 @@ fun TerminalLog(terminalItems: List, navController: NavController) fun DetailView(details: String?, navController: NavController){ Column { Text("$details") - - Button( - onClick={navController.popBackStack()} - ) - {Text("Back")} + Button(onClick = { navController.popBackStack() }) { + Text("Back") + } } } - From dd256be4ec092b84dbbe2b54f56149f34b7461cd Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Wed, 16 Feb 2022 23:24:48 +0000 Subject: [PATCH 15/46] use tagged JSON on android, update tests (#321) --- .../.idea/deploymentTargetDropDown.xml | 17 ------------- cabal.project | 2 +- sha256map.nix | 2 +- simplex-chat.cabal | 4 ++-- stack.yaml | 2 +- tests/MobileTests.hs | 24 +++++++++++++++++-- 6 files changed, 27 insertions(+), 24 deletions(-) delete mode 100644 apps/android/.idea/deploymentTargetDropDown.xml diff --git a/apps/android/.idea/deploymentTargetDropDown.xml b/apps/android/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index 57b651efa0..0000000000 --- a/apps/android/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/cabal.project b/cabal.project index 276b67617f..7d40f79f38 100644 --- a/cabal.project +++ b/cabal.project @@ -3,7 +3,7 @@ packages: . source-repository-package type: git location: git://github.com/simplex-chat/simplexmq.git - tag: 9c4778b12920db632322be27c3faf74bc9db3676 + tag: c1f5f9d8461e254c0c67c65de36f0860306b3d72 source-repository-package type: git diff --git a/sha256map.nix b/sha256map.nix index e8b0e6bb4c..b03bfc42e1 100644 --- a/sha256map.nix +++ b/sha256map.nix @@ -1,5 +1,5 @@ { - "git://github.com/simplex-chat/simplexmq.git"."9c4778b12920db632322be27c3faf74bc9db3676" = "0vkqkg0nl1z0ccggj1bjqv1i57vijhmw4qfm2myv07z88ch8brg8"; + "git://github.com/simplex-chat/simplexmq.git"."c1f5f9d8461e254c0c67c65de36f0860306b3d72" = "1qbj4xgg27qwm67187vkn87jggd3q47v4vylh915kbxw7npilalj"; "git://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "git://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "git://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 7b964a27d1..d568e817a7 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -17,8 +17,8 @@ build-type: Simple extra-source-files: README.md -if (os(linux)) - ghc-option: -DANDROID +if (os(darwin)) + ghc-option: -DIOS library exposed-modules: diff --git a/stack.yaml b/stack.yaml index 2b0db498a8..739d0f5a1c 100644 --- a/stack.yaml +++ b/stack.yaml @@ -48,7 +48,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: 9c4778b12920db632322be27c3faf74bc9db3676 + commit: c1f5f9d8461e254c0c67c65de36f0860306b3d72 # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index f48cf71312..e0f219e801 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE NamedFieldPuns #-} module MobileTests where @@ -16,13 +17,32 @@ mobileTests = do it "start new chat with existing user" testChatApi noActiveUser :: String +#if defined(IOS) noActiveUser = "{\"resp\":{\"chatCmdError\":{\"chatError\":{\"error\":{\"errorType\":{\"noActiveUser\":{}}}}}}}" +#else +noActiveUser = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"noActiveUser\"}}}}" +#endif activeUserExists :: String +#if defined(IOS) activeUserExists = "{\"resp\":{\"chatCmdError\":{\"chatError\":{\"error\":{\"errorType\":{\"activeUserExists\":{}}}}}}}" +#else +activeUserExists = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"activeUserExists\"}}}}" +#endif activeUser :: String +#if defined(IOS) activeUser = "{\"resp\":{\"activeUser\":{\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"displayName\":\"alice\",\"fullName\":\"Alice\"},\"activeUser\":true}}}}" +#else +activeUser = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"displayName\":\"alice\",\"fullName\":\"Alice\"},\"activeUser\":true}}}" +#endif + +chatStarted :: String +#if defined(IOS) +chatStarted = "{\"resp\":{\"chatStarted\":{}}}" +#else +chatStarted = "{\"resp\":{\"type\":\"chatStarted\"}}" +#endif testChatApiNoUser :: IO () testChatApiNoUser = withTmpFiles $ do @@ -30,7 +50,7 @@ testChatApiNoUser = withTmpFiles $ do chatSendCmd cc "/u" `shouldReturn` noActiveUser chatSendCmd cc "/_start" `shouldReturn` noActiveUser chatSendCmd cc "/u alice Alice" `shouldReturn` activeUser - chatSendCmd cc "/_start" `shouldReturn` "{\"resp\":{\"chatStarted\":{}}}" + chatSendCmd cc "/_start" `shouldReturn` chatStarted testChatApi :: IO () testChatApi = withTmpFiles $ do @@ -40,4 +60,4 @@ testChatApi = withTmpFiles $ do cc <- chatInit testDBPrefix chatSendCmd cc "/u" `shouldReturn` activeUser chatSendCmd cc "/u alice Alice" `shouldReturn` activeUserExists - chatSendCmd cc "/_start" `shouldReturn` "{\"resp\":{\"chatStarted\":{}}}" + chatSendCmd cc "/_start" `shouldReturn` chatStarted From 7fafb258218f1fa2cea92876864754078ec75e9a Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Feb 2022 08:22:16 +0000 Subject: [PATCH 16/46] rename file in android app --- .../java/chat/simplex/app/model/{SImpleXAPI.kt => SimpleXAPI.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apps/android/app/src/main/java/chat/simplex/app/model/{SImpleXAPI.kt => SimpleXAPI.kt} (100%) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SImpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt similarity index 100% rename from apps/android/app/src/main/java/chat/simplex/app/model/SImpleXAPI.kt rename to apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt From f74f932dcd2fbde9e7c3670ef313d8e1e909b3e6 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Feb 2022 08:40:08 +0000 Subject: [PATCH 17/46] pass IOS devine via GHC options in flake.nix (#322) --- flake.nix | 4 ++-- simplex-chat.cabal | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/flake.nix b/flake.nix index 6b07d1f2e6..a827eabc56 100644 --- a/flake.nix +++ b/flake.nix @@ -121,7 +121,7 @@ # we need threaded here, otherwise all the queing logic doesn't work properly. # for iOS we also use -staticlib, to get one rolled up library. # still needs mac2ios patching of the archives. - ghcOptions = [ "-staticlib" "-threaded" ]; + ghcOptions = [ "-staticlib" "-threaded" "-DIOS" ]; postInstall = '' ${pkgs.tree}/bin/tree $out mkdir -p $out/_pkg @@ -148,7 +148,7 @@ # we need threaded here, otherwise all the queing logic doesn't work properly. # for iOS we also use -staticlib, to get one rolled up library. # still needs mac2ios patching of the archives. - ghcOptions = [ "-staticlib" "-threaded" ]; + ghcOptions = [ "-staticlib" "-threaded" "-DIOS" ]; postInstall = '' ${pkgs.tree}/bin/tree $out mkdir -p $out/_pkg diff --git a/simplex-chat.cabal b/simplex-chat.cabal index d568e817a7..7f665785e3 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -16,9 +16,6 @@ license-file: LICENSE build-type: Simple extra-source-files: README.md - -if (os(darwin)) - ghc-option: -DIOS library exposed-modules: From e7e777ec7b022634f8d4107e7b26d0bc953e780d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Feb 2022 09:15:54 +0000 Subject: [PATCH 18/46] 2 spaces holy war (#323) --- .../main/java/chat/simplex/app/MainPage.kt | 3 +- .../main/java/chat/simplex/app/SimplexApp.kt | 130 +++++++++--------- 2 files changed, 66 insertions(+), 67 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt index 6a596e81e8..69930bde53 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt @@ -5,6 +5,5 @@ import chat.simplex.app.views.TerminalPage @Composable fun MainPage(vm: SimplexViewModel) { - TerminalPage(vm.chatModel) -} \ No newline at end of file +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index 3487ad9305..8803729b2f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -27,76 +27,76 @@ external fun chatSendCmd(ctrl: ChatCtrl, msg: String) : String external fun chatRecvMsg(ctrl: ChatCtrl) : String class SimplexApp: Application() { - private lateinit var controller: ChatController + private lateinit var controller: ChatController - override fun onCreate() { - super.onCreate() - controller = ChatController(chatInit(applicationContext.filesDir.toString())) - GlobalScope.launch { - withContext(Dispatchers.Main) { - var user = controller.apiGetActiveUser() - if (user == null) { -// user = controller.apiCreateActiveUser(Profile("android", "Android test")) - } - Log.d("SIMPLEX (user)", user.toString()) - try { - controller.apiStartChat() - Log.d("SIMPLEX", "started chat") - } catch(e: Error) { - Log.d("SIMPLEX", "failed starting chat $e") -// throw e - } - } + override fun onCreate() { + super.onCreate() + controller = ChatController(chatInit(applicationContext.filesDir.toString())) + GlobalScope.launch { + withContext(Dispatchers.Main) { + var user = controller.apiGetActiveUser() + if (user == null) { +// user = controller.apiCreateActiveUser(Profile("android", "Android test")) } - } - - val chatModel by lazy { - val m = ChatModel(controller) - controller.setModel(m) - controller.startReceiver() - m - } - - companion object { - lateinit var weakActivity: WeakReference - init { - val socketName = "local.socket.address.listen.native.cmd2" - - val s = Semaphore(0) - thread(name="stdout/stderr pipe") { - Log.d("SIMPLEX", "starting server") - val server = LocalServerSocket(socketName) - Log.d("SIMPLEX", "started server") - s.release() - val receiver = server.accept() - Log.d("SIMPLEX", "started receiver") - val logbuffer = FifoQueue(500) - if (receiver != null) { - val inStream = receiver.inputStream - val inStreamReader = InputStreamReader(inStream) - val input = BufferedReader(inStreamReader) - - while(true) { - val line = input.readLine() ?: break - Log.d("SIMPLEX (stdout/stderr)", line) - logbuffer.add(line) - } - } - } - - System.loadLibrary("app-lib") - - s.acquire() - pipeStdOutToSocket(socketName) - - initHS() + Log.d("SIMPLEX (user)", user.toString()) + try { + controller.apiStartChat() + Log.d("SIMPLEX", "started chat") + } catch(e: Error) { + Log.d("SIMPLEX", "failed starting chat $e") +// throw e } + } } + } + + val chatModel by lazy { + val m = ChatModel(controller) + controller.setModel(m) + controller.startReceiver() + m + } + + companion object { + lateinit var weakActivity: WeakReference + init { + val socketName = "local.socket.address.listen.native.cmd2" + + val s = Semaphore(0) + thread(name="stdout/stderr pipe") { + Log.d("SIMPLEX", "starting server") + val server = LocalServerSocket(socketName) + Log.d("SIMPLEX", "started server") + s.release() + val receiver = server.accept() + Log.d("SIMPLEX", "started receiver") + val logbuffer = FifoQueue(500) + if (receiver != null) { + val inStream = receiver.inputStream + val inStreamReader = InputStreamReader(inStream) + val input = BufferedReader(inStreamReader) + + while(true) { + val line = input.readLine() ?: break + Log.d("SIMPLEX (stdout/stderr)", line) + logbuffer.add(line) + } + } + } + + System.loadLibrary("app-lib") + + s.acquire() + pipeStdOutToSocket(socketName) + + initHS() + } + } } class FifoQueue(private var capacity: Int) : LinkedList() { - override fun add(element: E): Boolean { - if(size > capacity) removeFirst() - return super.add(element) - } + override fun add(element: E): Boolean { + if(size > capacity) removeFirst() + return super.add(element) + } } From e8ff6f509b9691b553f72c259837442a5615a842 Mon Sep 17 00:00:00 2001 From: IanRDavies Date: Thu, 17 Feb 2022 10:52:56 +0000 Subject: [PATCH 19/46] Id/android navigation edits (#325) * add ids to terminalitems and work with these * remove unnecessary logging --- .../java/chat/simplex/app/model/ChatModel.kt | 4 +-- .../java/chat/simplex/app/model/SimpleXAPI.kt | 17 ++++++----- .../chat/simplex/app/views/TerminalView.kt | 30 ++++++++++++------- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 0ecab183ad..d5e2eb65c2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -12,8 +12,8 @@ class ChatModel(val controller: ChatController) { val sampleData: ChatModel get() { val m = ChatModel(ChatController.Mock()) m.terminalItems = mutableStateListOf( - TerminalItem.Cmd(CC.ShowActiveUser()), - TerminalItem.Resp(CR.ActiveUser(User.sampleData)) + TerminalItem.Cmd(0, CC.ShowActiveUser()), + TerminalItem.Resp(1, CR.ActiveUser(User.sampleData)) ) return m } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 82a514221e..c5449969ec 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -33,7 +33,7 @@ open class ChatController(val ctrl: ChatCtrl) { val cb = callbacks[r.corr] if (cb != null) cb(null, r.resp) } - chatModel?.terminalItems?.add(TerminalItem.Resp(r.resp)) + chatModel?.terminalItems?.add(TerminalItem.Resp(System.currentTimeMillis(), r.resp)) } } } @@ -41,12 +41,12 @@ open class ChatController(val ctrl: ChatCtrl) { suspend fun sendCmd(cmd: CC): CR { return withContext(Dispatchers.IO) { val c = cmd.cmdString - chatModel?.terminalItems?.add(TerminalItem.Cmd(cmd)) + chatModel?.terminalItems?.add(TerminalItem.Cmd(System.currentTimeMillis(), cmd)) val json = chatSendCmd(ctrl, c) Log.d("SIMPLEX", "sendCmd: $c") Log.d("SIMPLEX", "sendCmd response $json") val r = APIResponse.decodeStr(json) - chatModel?.terminalItems?.add(TerminalItem.Resp(r.resp)) + chatModel?.terminalItems?.add(TerminalItem.Resp(System.currentTimeMillis(), r.resp)) r.resp } } @@ -187,24 +187,27 @@ sealed class CR { } abstract class TerminalItem { + abstract val id: Long val date = Date() abstract val label: String abstract val details: String - class Cmd(val cmd: CC): TerminalItem() { + class Cmd(id: Long, val cmd: CC): TerminalItem() { + override val id = id override val label get() = "> ${cmd.cmdString}" override val details get() = cmd.cmdString } - class Resp(val resp: CR): TerminalItem() { + class Resp(id: Long, val resp: CR): TerminalItem() { + override val id = id override val label get() = "< ${resp.responseType}" override val details get() = resp.details } companion object { val sampleData = listOf( - TerminalItem.Cmd(CC.ShowActiveUser()), - TerminalItem.Resp(CR.ActiveUser(User.sampleData)) + TerminalItem.Cmd(0, CC.ShowActiveUser()), + TerminalItem.Resp(1, CR.ActiveUser(User.sampleData)) ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index 8f9fcf916e..6169cc09b8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -3,16 +3,24 @@ package chat.simplex.app.views import androidx.compose.foundation.layout.Column import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.material.Button import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import androidx.navigation.* import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import chat.simplex.app.model.* +import androidx.navigation.navArgument +import chat.simplex.app.Pages +import chat.simplex.app.model.ChatModel +import chat.simplex.app.model.TerminalItem +import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.SendMsgView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -22,16 +30,16 @@ import kotlinx.coroutines.withContext @Composable fun TerminalPage(chatModel: ChatModel) { val navController = rememberNavController() - NavHost(navController = navController, startDestination = "terminalView"){ - composable("terminalView") { TerminalView(chatModel, navController) } + NavHost(navController = navController, startDestination = Pages.Terminal.route){ + composable(Pages.Terminal.route) { TerminalView(chatModel, navController) } composable( - "details" + "/{_details}", + "details" + "/{identifier}", arguments=listOf( - navArgument("_details"){ - type = NavType.StringType + navArgument("identifier"){ + type = NavType.LongType } ) - ) { entry -> DetailView( entry.arguments?.getString("_details"), navController) } + ) { entry -> DetailView( entry.arguments!!.getLong("identifier"), chatModel.terminalItems, navController) } } } @@ -57,16 +65,18 @@ fun TerminalLog(terminalItems: List, navController: NavController) items(terminalItems) { item -> ClickableText( AnnotatedString(item.label), - onClick = { navController.navigate("details/${item.details}") } + onClick = { navController.navigate("details/${item.id}") } ) } } } @Composable -fun DetailView(details: String?, navController: NavController){ - Column { - Text("$details") +fun DetailView(identifier: Long, terminalItems: List, navController: NavController){ + Column( + modifier=Modifier.verticalScroll(rememberScrollState()) + ) { + Text((terminalItems.filter {it.id == identifier}).first().details) Button(onClick = { navController.popBackStack() }) { Text("Back") } From 9e46b5117df4555fb32194ef6200d08b8a2b98b9 Mon Sep 17 00:00:00 2001 From: IanRDavies Date: Thu, 17 Feb 2022 17:07:58 +0000 Subject: [PATCH 20/46] Id/conditional nav on launch (#326) * add initial conditional routing -- create user not working * only one nav controller * user check on launch works (kind of) * Apply suggestions from code review Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/MainActivity.kt | 28 +++++++++++--- .../main/java/chat/simplex/app/MainPage.kt | 11 ++++-- .../main/java/chat/simplex/app/SimplexApp.kt | 1 + .../java/chat/simplex/app/model/ChatModel.kt | 8 +++- .../java/chat/simplex/app/model/SimpleXAPI.kt | 3 ++ .../chat/simplex/app/views/TerminalView.kt | 16 -------- .../chat/simplex/app/views/WelcomeView.kt | 38 ++++++++++++++----- 7 files changed, 69 insertions(+), 36 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index dfd150c0d2..5870e1fa1d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -8,18 +8,24 @@ import androidx.activity.viewModels import androidx.compose.runtime.Composable import chat.simplex.app.ui.theme.SimpleXTheme import androidx.lifecycle.AndroidViewModel +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import chat.simplex.app.views.DetailView +import chat.simplex.app.views.TerminalView +import chat.simplex.app.views.WelcomeView class MainActivity: ComponentActivity() { private val viewModel by viewModels() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { SimpleXTheme { - Navigation(viewModel = viewModel) + Navigation(viewModel=viewModel) } } } @@ -35,11 +41,22 @@ fun Navigation(viewModel: SimplexViewModel) { NavHost(navController=navController, startDestination=Pages.Home.route){ composable(route=Pages.Home.route){ - MainPage(vm = viewModel) + MainPage(vm = viewModel, navController = navController) } -// composable(route=Pages.Welcome.route){ -// WelcomeView(vm.) -// } + composable(route = Pages.Welcome.route){ + WelcomeView(vm = viewModel) {navController.navigate(Pages.Home.route) { popUpTo(Pages.Home.route) { inclusive = true }}} + } + composable(route = Pages.Terminal.route) { + TerminalView(chatModel = viewModel.chatModel, navController = navController) + } + composable( + Pages.TerminalItemDetails.route + "/{identifier}", + arguments = listOf( + navArgument("identifier"){ + type = NavType.LongType + } + ) + ) { entry -> DetailView( entry.arguments!!.getLong("identifier"), viewModel.chatModel.terminalItems, navController) } } } @@ -47,4 +64,5 @@ sealed class Pages(val route: String) { object Home : Pages("home") object Terminal : Pages("terminal") object Welcome : Pages("welcome") + object TerminalItemDetails : Pages("details") } diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt index 69930bde53..d7abb01223 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt @@ -1,9 +1,14 @@ package chat.simplex.app import androidx.compose.runtime.Composable -import chat.simplex.app.views.TerminalPage +import androidx.navigation.NavController +import chat.simplex.app.views.TerminalView +import chat.simplex.app.views.WelcomeView @Composable -fun MainPage(vm: SimplexViewModel) { - TerminalPage(vm.chatModel) +fun MainPage(vm: SimplexViewModel, navController: NavController) { + if (vm.chatModel.currentUser.value == null) WelcomeView(vm=vm) { + navController.navigate(Pages.Terminal.route) + } + else TerminalView(vm.chatModel, navController) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index 8803729b2f..62a4113a1e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -35,6 +35,7 @@ class SimplexApp: Application() { GlobalScope.launch { withContext(Dispatchers.Main) { var user = controller.apiGetActiveUser() + controller.setCurrentUser(user) if (user == null) { // user = controller.apiCreateActiveUser(Profile("android", "Android test")) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index d5e2eb65c2..5c82f51225 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -1,13 +1,17 @@ package chat.simplex.app.model import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf import kotlinx.serialization.Serializable -import java.util.* class ChatModel(val controller: ChatController) { - val currentUser: User? = null + var currentUser = mutableStateOf(null) var terminalItems = mutableStateListOf() + fun setCurrentUser(u: User?){ + currentUser = mutableStateOf(u) + } + companion object { val sampleData: ChatModel get() { val m = ChatModel(ChatController.Mock()) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index c5449969ec..974e6d1776 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -21,6 +21,9 @@ open class ChatController(val ctrl: ChatCtrl) { fun setModel(m: ChatModel) { chatModel = m } + suspend fun setCurrentUser(u: User?){ + chatModel!!.setCurrentUser(u) + } fun startReceiver() { thread(name="receiver") { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index 6169cc09b8..4fde8eb87c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -27,22 +27,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -@Composable -fun TerminalPage(chatModel: ChatModel) { - val navController = rememberNavController() - NavHost(navController = navController, startDestination = Pages.Terminal.route){ - composable(Pages.Terminal.route) { TerminalView(chatModel, navController) } - composable( - "details" + "/{identifier}", - arguments=listOf( - navArgument("identifier"){ - type = NavType.LongType - } - ) - ) { entry -> DetailView( entry.arguments!!.getLong("identifier"), chatModel.terminalItems, navController) } - } -} - @Composable fun TerminalView(chatModel: ChatModel, navController: NavController) { Column { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt index e0ed85466f..30a4334a0a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt @@ -2,6 +2,8 @@ package chat.simplex.app.views import androidx.compose.foundation.layout.* import androidx.compose.foundation.Image +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.ui.res.painterResource import androidx.compose.runtime.* import androidx.compose.runtime.Composable @@ -11,11 +13,18 @@ import androidx.compose.material.TextField import androidx.compose.material.Button import androidx.compose.ui.unit.dp import androidx.compose.ui.Modifier -import androidx.navigation.NavController +import chat.simplex.app.SimplexViewModel +import chat.simplex.app.model.Profile +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @Composable -fun WelcomeView(createUser: (data: String) -> String, navController: NavController) { - Column { +fun WelcomeView(vm: SimplexViewModel, routeHome: () -> Unit) { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { Image( painter=painterResource(R.drawable.logo), contentDescription = "Simplex Logo", ) @@ -24,12 +33,12 @@ fun WelcomeView(createUser: (data: String) -> String, navController: NavControll Spacer(Modifier.height(8.dp)) Text("We don't store any of your contacts or messages (once delivered) on the servers.") Spacer(Modifier.height(24.dp)) - CreateProfilePanel(createUser, navController) + CreateProfilePanel(vm, routeHome) } } @Composable -fun CreateProfilePanel(createUser: (data: String) -> String, navController: NavController) { +fun CreateProfilePanel(vm: SimplexViewModel, routeHome: () -> Unit) { var displayName by remember { mutableStateOf("") } var fullName by remember { mutableStateOf("") } @@ -37,12 +46,21 @@ fun CreateProfilePanel(createUser: (data: String) -> String, navController: NavC Text("Create profile") Text("Your profile is stored on your device and shared only with your contacts.") Text("Display Name") - TextField(value = displayName, onValueChange = { displayName = it }, modifier = Modifier.height(30.dp)) + TextField(value = displayName, onValueChange = { value -> displayName = value }) Text("Full Name (Optional)") - TextField(value = fullName, onValueChange = { fullName = it }, modifier = Modifier.height(30.dp)) + TextField(value = fullName, onValueChange = { fullName = it }) Button(onClick={ - createUser("{\"displayName\": $displayName, \"fullName\": $fullName}") - navController.navigate("home") { popUpTo("home") { inclusive = true }} - }) { Text("Create")} + GlobalScope.launch { + withContext(Dispatchers.Main) { + val user = vm.chatModel.controller.apiCreateActiveUser( + Profile(displayName, fullName) + ) + vm.chatModel.setCurrentUser(user) + routeHome() + } + } + }, + enabled = displayName.isNotEmpty() + ) { Text("Create")} } } \ No newline at end of file From 423f54e95d117299d0824e593cd845c9e573d768 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Feb 2022 17:15:49 +0000 Subject: [PATCH 21/46] chats in android app (#324) * view placeholders for chats list and chat views * classes for chats * set the user to the model * use Long for IDs * chats/messages API (not working yet) * android api works * line breaks --- apps/android/.gitignore | 2 + apps/android/.idea/misc.xml | 21 -- .../main/java/chat/simplex/app/SimplexApp.kt | 3 +- .../java/chat/simplex/app/model/ChatModel.kt | 346 +++++++++++++++++- .../java/chat/simplex/app/model/SimpleXAPI.kt | 67 +++- .../chat/simplex/app/views/TerminalView.kt | 5 +- .../chat/simplex/app/views/chat/ChatView.kt | 10 + .../app/views/chat/item/ChatItemView.kt | 10 + .../app/views/chat/item/TextItemView.kt | 10 + .../app/views/chatlist/ChatListView.kt | 10 + .../app/views/chatlist/ChatPreviewView.kt | 10 + 11 files changed, 437 insertions(+), 57 deletions(-) delete mode 100644 apps/android/.idea/misc.xml create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt diff --git a/apps/android/.gitignore b/apps/android/.gitignore index aa724b7707..4d1f29a2d1 100644 --- a/apps/android/.gitignore +++ b/apps/android/.gitignore @@ -7,6 +7,8 @@ /.idea/workspace.xml /.idea/navEditor.xml /.idea/assetWizardSettings.xml +/.idea/deploymentTargetDropDown.xml +/.idea/misc.xml .DS_Store /build /captures diff --git a/apps/android/.idea/misc.xml b/apps/android/.idea/misc.xml deleted file mode 100644 index f0d8957869..0000000000 --- a/apps/android/.idea/misc.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index 62a4113a1e..8adeceecf6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -37,9 +37,10 @@ class SimplexApp: Application() { var user = controller.apiGetActiveUser() controller.setCurrentUser(user) if (user == null) { -// user = controller.apiCreateActiveUser(Profile("android", "Android test")) + user = controller.apiCreateActiveUser(Profile("android", "Android test")) } Log.d("SIMPLEX (user)", user.toString()) + chatModel.currentUser = user try { controller.apiStartChat() Log.d("SIMPLEX", "started chat") diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 5c82f51225..f8a7082595 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -1,8 +1,7 @@ package chat.simplex.app.model -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import kotlinx.serialization.Serializable +import androidx.compose.runtime.* +import kotlinx.serialization.* class ChatModel(val controller: ChatController) { var currentUser = mutableStateOf(null) @@ -32,8 +31,8 @@ enum class ChatType(val type: String) { @Serializable class User ( - val userId: Int, - val userContactId: Int, + val userId: Long, + val userContactId: Long, val localDisplayName: String, val profile: Profile, val activeUser: Boolean @@ -54,21 +53,111 @@ class User ( typealias ChatId = String +interface NamedChat { + val displayName: String + val fullName: String + val chatViewName: String + get() = displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") +} + +interface SomeChat { + val localDisplayName: String + val id: ChatId + val apiId: Long + val ready: Boolean +} + +@Serializable +class Chat ( + val chatInfo: ChatInfo, + val chatItems: List, + val chatStats: ChatStats, + val serverInfo: ServerInfo = ServerInfo(NetworkStatus.Unknown()) +) { + + @Serializable + class ChatStats { + + } + + @Serializable + class ServerInfo(val networkStatus: NetworkStatus) + + @Serializable + sealed class NetworkStatus { + abstract val statusString: String + abstract val statusExplanation: String + abstract val imageName: String + + @Serializable + class Unknown: NetworkStatus() { + override val statusString get() = "Server connected" + override val statusExplanation get() = "You are connected to the server you use to receve messages from this contact." + override val imageName get() = "circle.dotted" // ? + } + } +} + +@Serializable +sealed class ChatInfo: SomeChat, NamedChat { + @Serializable @SerialName("direct") + class Direct(val contact: Contact): ChatInfo() { + override val localDisplayName get() = contact.localDisplayName + override val id get() = contact.id + override val apiId get() = contact.apiId + override val ready get() = contact.ready + override val displayName get() = contact.displayName + override val fullName get() = contact.displayName + + companion object { + val sampleData = Direct(Contact.sampleData) + } + } + + @Serializable @SerialName("group") + class Group(val groupInfo: GroupInfo): ChatInfo() { + override val localDisplayName get() = groupInfo.localDisplayName + override val id get() = groupInfo.id + override val apiId get() = groupInfo.apiId + override val ready get() = groupInfo.ready + override val displayName get() = groupInfo.displayName + override val fullName get() = groupInfo.displayName + + companion object { + val sampleData = Group(GroupInfo.sampleData) + } + } + + @Serializable @SerialName("contactRequest") + class ContactRequest(val contactRequest: UserContactRequest): ChatInfo() { + override val localDisplayName get() = contactRequest.localDisplayName + override val id get() = contactRequest.id + override val apiId get() = contactRequest.apiId + override val ready get() = contactRequest.ready + override val displayName get() = contactRequest.displayName + override val fullName get() = contactRequest.displayName + + companion object { + val sampleData = ContactRequest(UserContactRequest.sampleData) + } + } +} + @Serializable class Contact( - val contactId: Int, - val localDisplayName: String, + val contactId: Long, + override val localDisplayName: String, val profile: Profile, val activeConn: Connection, - val viaGroup: Int? = null, + val viaGroup: Long? = null, // no serializer for type Date? // val createdAt: Date -): NamedChat { - val id: ChatId get() = "@$contactId" - val apiId: Int get() = contactId - val ready: Boolean get() = activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready" - override val displayName: String get() = profile.displayName - override val fullName: String get() = profile.fullName +): SomeChat, NamedChat { + override val id get() = "@$contactId" + override val apiId get() = contactId + override val ready get() = activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready" + override val displayName get() = profile.displayName + override val fullName get() = profile.fullName companion object { val sampleData = Contact( @@ -101,9 +190,228 @@ class Profile( } } -interface NamedChat { - abstract val displayName: String - abstract val fullName: String - val chatViewName: String - get() = displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") +@Serializable +class GroupInfo ( + val groupId: Long, + override val localDisplayName: String, + val groupProfile: GroupProfile, +// var createdAt: Date +): SomeChat, NamedChat { + override val id get() = "#$groupId" + override val apiId get() = groupId + override val ready get() = true + override val displayName get() = groupProfile.displayName + override val fullName get() = groupProfile.fullName + + companion object { + val sampleData = GroupInfo( + groupId = 1, + localDisplayName = "team", + groupProfile = GroupProfile.sampleData, +// createdAt: Date() + ) + } +} + +@Serializable +class GroupProfile ( + override val displayName: String, + override val fullName: String +): NamedChat { + companion object { + val sampleData = GroupProfile( + displayName = "team", + fullName = "My Team" + ) + } +} + +@Serializable +class GroupMember ( + val groupMemberId: Long, + val memberId: String, +// var memberRole: GroupMemberRole +// var memberCategory: GroupMemberCategory +// var memberStatus: GroupMemberStatus +// var invitedBy: InvitedBy + val localDisplayName: String, + val memberProfile: Profile, + val memberContactId: Long? +// var activeConn: Connection? +) { + companion object { + val sampleData = GroupMember( + groupMemberId = 1, + memberId = "abcd", + localDisplayName = "alice", + memberProfile = Profile.sampleData, + memberContactId = 1 + ) + } +} + +@Serializable +class UserContactRequest ( + val contactRequestId: Long, + override val localDisplayName: String, + val profile: Profile +// val createdAt: Date +): SomeChat, NamedChat { + override val id get() = "<@$contactRequestId" + override val apiId get() = contactRequestId + override val ready get() = true + override val displayName get() = profile.displayName + override val fullName get() = profile.fullName + + companion object { + val sampleData = UserContactRequest( + contactRequestId = 1, + localDisplayName = "alice", + profile = Profile.sampleData, +// createdAt: Date() + ) + } +} + +@Serializable +class AChatItem ( + val chatInfo: ChatInfo, + val chatItem: ChatItem +) + +@Serializable +class ChatItem ( + val chatDir: CIDirection, + val meta: CIMeta, + val content: CIContent +) { + val id: Long get() = meta.itemId + // val timestampText: String get() = meta.timestampText + val isRcvNew: Boolean get() = meta.itemStatus is CIStatus.RcvNew + + companion object { + fun getSampleData(id: Long, dir: CIDirection, ts: Date, text: String,status: CIStatus = CIStatus.SndNew()) = + ChatItem( + chatDir = dir, + meta = CIMeta.getSample(id, ts, text, status), + content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)) + ) + } +} + +@Serializable +sealed class CIDirection { + abstract val sent: Boolean + + @Serializable @SerialName("directSnd") + class DirectSnd: CIDirection() { + override val sent get() = true + } + + @Serializable @SerialName("directRcv") + class DirectRcv: CIDirection() { + override val sent get() = false + } + + @Serializable @SerialName("groupSnd") + class GroupSnd: CIDirection() { + override val sent get() = true + } + + @Serializable @SerialName("groupRcv") + class GroupRcv(val groupMember: GroupMember): CIDirection() { + override val sent get() = false + } +} + +@Serializable +class CIMeta ( + val itemId: Long, +// val itemTs: Date, + val itemText: String, + val itemStatus: CIStatus, +// val createdAt: Date +) { +// val timestampText: String get() = getTimestampText(itemTs) + + companion object { + fun getSample(id: Long, ts: Date, text: String, status: CIStatus = CIStatus.SndNew()): CIMeta = + CIMeta( + itemId = id, +// itemTs = ts, + itemText = text, + itemStatus = status, +// createdAt = ts + ) + } +} + +// TODO +fun getTimestampText(d: Date): String = "" + +@Serializable +sealed class CIStatus { + @Serializable @SerialName("sndNew") + class SndNew: CIStatus() + + @Serializable @SerialName("sndSent") + class SndSent: CIStatus() + + @Serializable @SerialName("sndErrorAuth") + class SndErrorAuth: CIStatus() + + @Serializable @SerialName("sndError") + class SndError(val agentError: AgentErrorType): CIStatus() + + @Serializable @SerialName("rcvNew") + class RcvNew: CIStatus() + + @Serializable @SerialName("rcvRead") + class RcvRead: CIStatus() +} + +@Serializable +sealed class CIContent { + abstract val text: String + + @Serializable @SerialName("sndMsgContent") + class SndMsgContent(val msgContent: MsgContent): CIContent() { + override val text get() = msgContent.text + } + + @Serializable @SerialName("rcvMsgContent") + class RcvMsgContent(val msgContent: MsgContent): CIContent() { + override val text get() = msgContent.text + } + + @Serializable @SerialName("sndFileInvitation") + class SndFileInvitation(val fileId: Long, val filePath: String): CIContent() { + override val text get() = "sending files is not supported yet" + } + + @Serializable @SerialName("rcvFileInvitation") + class RcvFileInvitation(val rcvFileTransfer: RcvFileTransfer): CIContent() { + override val text get() = "receiving files is not supported yet" + } +} + +@Serializable +sealed class MsgContent { + abstract val text: String + abstract val cmdString: String + + @Serializable @SerialName("text") + class MCText(override val text: String): MsgContent() { + override val cmdString get() = "text $text" + } +} + +@Serializable +class RcvFileTransfer { + +} + +@Serializable +class AgentErrorType { + } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 974e6d1776..bca01a174d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -46,9 +46,10 @@ open class ChatController(val ctrl: ChatCtrl) { val c = cmd.cmdString chatModel?.terminalItems?.add(TerminalItem.Cmd(System.currentTimeMillis(), cmd)) val json = chatSendCmd(ctrl, c) - Log.d("SIMPLEX", "sendCmd: $c") - Log.d("SIMPLEX", "sendCmd response $json") + Log.d("SIMPLEX", "sendCmd: ${cmd.cmdType}") + Log.d("SIMPLEX", "sendCmd response json $json") val r = APIResponse.decodeStr(json) + Log.d("SIMPLEX", "sendCmd response type ${r.resp.responseType}") chatModel?.terminalItems?.add(TerminalItem.Resp(System.currentTimeMillis(), r.resp)) r.resp } @@ -74,6 +75,12 @@ open class ChatController(val ctrl: ChatCtrl) { throw Error("failed starting chat: ${r.toString()}") } + suspend fun apiGetChats() { + val r = sendCmd(CC.ApiGetChats()) + if (r is CR.ApiChats ) return + throw Error("failed getting the list of chats: ${r.toString()}") + } + class Mock: ChatController(0) {} } @@ -89,26 +96,36 @@ abstract class CC { class ShowActiveUser: CC() { override val cmdString get() = "/u" - override val cmdType get() = "ShowActiveUser" + override val cmdType get() = "showActiveUser" } class CreateActiveUser(val profile: Profile): CC() { override val cmdString get() = "/u ${profile.displayName} ${profile.fullName}" - override val cmdType get() = "CreateActiveUser" + override val cmdType get() = "createActiveUser" } class StartChat: CC() { override val cmdString get() = "/_start" - override val cmdType get() = "StartChat" + override val cmdType get() = "startChat" } class ApiGetChats: CC() { override val cmdString get() = "/_get chats" - override val cmdType get() = "ApiGetChats" + override val cmdType get() = "apiGetChats" + } + + class ApiGetChat(val type: ChatType, val id: Long): CC() { + override val cmdString get() = "/_get chat ${CC.chatRef(type, id)} count=100" + override val cmdType get() = "apiGetChat" + } + + class ApiSendMessage(val type: ChatType, val id: Long, val mc: MsgContent): CC() { + override val cmdString get() = "/_send ${CC.chatRef(type, id)} ${mc.cmdString}" + override val cmdType get() = "apiGetChat" } companion object { - fun chatRef(type: ChatType, id: String) = "${type}${id}" + fun chatRef(type: ChatType, id: Long) = "${type}${id}" } } @@ -144,29 +161,49 @@ sealed class CR { abstract val responseType: String abstract val details: String - @Serializable - @SerialName("activeUser") + @Serializable @SerialName("activeUser") class ActiveUser(val user: User): CR() { override val responseType get() = "activeUser" override val details get() = user.toString() } - @Serializable - @SerialName("chatStarted") + @Serializable @SerialName("chatStarted") class ChatStarted: CR() { override val responseType get() = "chatStarted" override val details get() = CR.noDetails(this) } - @Serializable - @SerialName("contactSubscribed") + @Serializable @SerialName("apiChats") + class ApiChats(val chats: List): CR() { + override val responseType get() = "apiChats" + override val details get() = chats.toString() + } + + @Serializable @SerialName("apiChat") + class ApiChat(val chat: Chat): CR() { + override val responseType get() = "apiChats" + override val details get() = chat.toString() + } + + @Serializable @SerialName("contactSubscribed") class ContactSubscribed(val contact: Contact): CR() { override val responseType get() = "contactSubscribed" override val details get() = contact.toString() } - @Serializable - @SerialName("cmdAccepted") + @Serializable @SerialName("newChatItem") + class NewChatItem(val chatItem: AChatItem): CR() { + override val responseType get() = "newChatItem" + override val details get() = chatItem.toString() + } + + @Serializable @SerialName("chatItemUpdated") + class ChatItemUpdated(val chatItem: AChatItem): CR() { + override val responseType get() = "chatItemUpdated" + override val details get() = chatItem.toString() + } + + @Serializable @SerialName("cmdAccepted") class CmdAccepted(val corr: String): CR() { override val responseType get() = "cmdAccepted" override val details get() = "corrId: $corr" diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index 4fde8eb87c..bc7705f5e6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.material.Button @@ -60,9 +61,11 @@ fun DetailView(identifier: Long, terminalItems: List, navControlle Column( modifier=Modifier.verticalScroll(rememberScrollState()) ) { - Text((terminalItems.filter {it.id == identifier}).first().details) Button(onClick = { navController.popBackStack() }) { Text("Back") } + SelectionContainer { + Text((terminalItems.filter { it.id == identifier }).first().details) + } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt new file mode 100644 index 0000000000..4efa2c50d0 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -0,0 +1,10 @@ +package chat.simplex.app.views.chat + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import chat.simplex.app.model.Chat + +@Composable +fun ChatView(chat: Chat) { + Text("ChatView") +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt new file mode 100644 index 0000000000..10fc3c9f9c --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -0,0 +1,10 @@ +package chat.simplex.app.views.chat.item + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import chat.simplex.app.model.ChatItem + +@Composable +fun ChatItemView(chatItem: ChatItem) { + Text("ChatItemView") +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt new file mode 100644 index 0000000000..22bbdbb005 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt @@ -0,0 +1,10 @@ +package chat.simplex.app.views.chat.item + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import chat.simplex.app.model.ChatItem + +@Composable +fun TextItemView(chatItem: ChatItem) { + Text("TextItemView") +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt new file mode 100644 index 0000000000..634af6248d --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -0,0 +1,10 @@ +package chat.simplex.app.views.chatlist + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import chat.simplex.app.model.ChatModel + +@Composable +fun ChatListView(chatModel: ChatModel) { + Text("ChatListView") +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt new file mode 100644 index 0000000000..993e9b96a5 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -0,0 +1,10 @@ +package chat.simplex.app.views.chatlist + +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import chat.simplex.app.model.Chat + +@Composable +fun ChatPreviewView(chat: Chat) { + Text("ChatPreviewView") +} From 290a88fd90be2f5df499435d45367f25462f2327 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Feb 2022 20:30:21 +0000 Subject: [PATCH 22/46] list of chats and chat messages (#327) --- .../java/chat/simplex/app/MainActivity.kt | 35 ++++++--- .../main/java/chat/simplex/app/MainPage.kt | 10 ++- .../main/java/chat/simplex/app/SimplexApp.kt | 16 +--- .../java/chat/simplex/app/model/ChatModel.kt | 18 +++-- .../java/chat/simplex/app/model/SimpleXAPI.kt | 37 ++++++++-- .../chat/simplex/app/views/TerminalView.kt | 5 +- .../chat/simplex/app/views/WelcomeView.kt | 11 +-- .../chat/simplex/app/views/chat/ChatView.kt | 62 +++++++++++++++- .../app/views/chat/item/ChatItemView.kt | 2 +- .../app/views/chatlist/ChatListView.kt | 74 ++++++++++++++++++- .../app/views/chatlist/ChatPreviewView.kt | 28 ++++++- 11 files changed, 245 insertions(+), 53 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 5870e1fa1d..4a13ded802 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -13,9 +13,10 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument -import chat.simplex.app.views.DetailView -import chat.simplex.app.views.TerminalView -import chat.simplex.app.views.WelcomeView +import chat.simplex.app.model.ChatModel +import chat.simplex.app.views.* +import chat.simplex.app.views.chat.ChatView +import chat.simplex.app.views.chatlist.* class MainActivity: ComponentActivity() { @@ -25,7 +26,7 @@ class MainActivity: ComponentActivity() { super.onCreate(savedInstanceState) setContent { SimpleXTheme { - Navigation(viewModel=viewModel) + Navigation(viewModel.chatModel) } } } @@ -36,18 +37,28 @@ class SimplexViewModel(application: Application) : AndroidViewModel(application) } @Composable -fun Navigation(viewModel: SimplexViewModel) { - val navController = rememberNavController() +fun Navigation(chatModel: ChatModel) { + val nav = rememberNavController() - NavHost(navController=navController, startDestination=Pages.Home.route){ + NavHost(navController = nav, startDestination=Pages.Home.route){ composable(route=Pages.Home.route){ - MainPage(vm = viewModel, navController = navController) + MainPage(chatModel, nav) } composable(route = Pages.Welcome.route){ - WelcomeView(vm = viewModel) {navController.navigate(Pages.Home.route) { popUpTo(Pages.Home.route) { inclusive = true }}} + WelcomeView(chatModel) { + nav.navigate(Pages.Home.route) { + popUpTo(Pages.Home.route) { inclusive = true } + } + } + } + composable(route = Pages.Chats.route) { + ChatListView(chatModel, nav) + } + composable(route = Pages.Chat.route) { + ChatView(chatModel, nav) } composable(route = Pages.Terminal.route) { - TerminalView(chatModel = viewModel.chatModel, navController = navController) + TerminalView(chatModel, navController = nav) } composable( Pages.TerminalItemDetails.route + "/{identifier}", @@ -56,7 +67,7 @@ fun Navigation(viewModel: SimplexViewModel) { type = NavType.LongType } ) - ) { entry -> DetailView( entry.arguments!!.getLong("identifier"), viewModel.chatModel.terminalItems, navController) } + ) { entry -> DetailView( entry.arguments!!.getLong("identifier"), chatModel.terminalItems, nav) } } } @@ -65,4 +76,6 @@ sealed class Pages(val route: String) { object Terminal : Pages("terminal") object Welcome : Pages("welcome") object TerminalItemDetails : Pages("details") + object Chats: Pages("chats") + object Chat: Pages("chat") } diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt index d7abb01223..f59b66d05f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt @@ -2,13 +2,15 @@ package chat.simplex.app import androidx.compose.runtime.Composable import androidx.navigation.NavController +import chat.simplex.app.model.ChatModel import chat.simplex.app.views.TerminalView import chat.simplex.app.views.WelcomeView +import chat.simplex.app.views.chatlist.ChatListView @Composable -fun MainPage(vm: SimplexViewModel, navController: NavController) { - if (vm.chatModel.currentUser.value == null) WelcomeView(vm=vm) { - navController.navigate(Pages.Terminal.route) +fun MainPage(chatModel: ChatModel, nav: NavController) { + if (chatModel.currentUser.value == null) WelcomeView(chatModel) { + nav.navigate(Pages.Chats.route) } - else TerminalView(vm.chatModel, navController) + else ChatListView(chatModel, nav) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index 8adeceecf6..b6efb56ffd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -3,6 +3,8 @@ package chat.simplex.app import android.app.Application import android.net.LocalServerSocket import android.util.Log +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.snapshots.SnapshotStateList import chat.simplex.app.model.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -35,19 +37,7 @@ class SimplexApp: Application() { GlobalScope.launch { withContext(Dispatchers.Main) { var user = controller.apiGetActiveUser() - controller.setCurrentUser(user) - if (user == null) { - user = controller.apiCreateActiveUser(Profile("android", "Android test")) - } - Log.d("SIMPLEX (user)", user.toString()) - chatModel.currentUser = user - try { - controller.apiStartChat() - Log.d("SIMPLEX", "started chat") - } catch(e: Error) { - Log.d("SIMPLEX", "failed starting chat $e") -// throw e - } + if (user != null) controller.startChat(user) } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index f8a7082595..2fd3de45f6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -2,14 +2,15 @@ package chat.simplex.app.model import androidx.compose.runtime.* import kotlinx.serialization.* +import java.util.* class ChatModel(val controller: ChatController) { var currentUser = mutableStateOf(null) - var terminalItems = mutableStateListOf() + var chats = mutableStateListOf() + var chatId = mutableStateOf(null) + var chatItems = mutableStateListOf() - fun setCurrentUser(u: User?){ - currentUser = mutableStateOf(u) - } + var terminalItems = mutableStateListOf() companion object { val sampleData: ChatModel get() { @@ -36,7 +37,7 @@ class User ( val localDisplayName: String, val profile: Profile, val activeUser: Boolean -) : NamedChat { +): NamedChat { override val displayName: String get() = profile.displayName override val fullName: String get() = profile.fullName @@ -61,6 +62,7 @@ interface NamedChat { } interface SomeChat { + val chatType: ChatType val localDisplayName: String val id: ChatId val apiId: Long @@ -102,6 +104,7 @@ class Chat ( sealed class ChatInfo: SomeChat, NamedChat { @Serializable @SerialName("direct") class Direct(val contact: Contact): ChatInfo() { + override val chatType get() = ChatType.Direct override val localDisplayName get() = contact.localDisplayName override val id get() = contact.id override val apiId get() = contact.apiId @@ -116,6 +119,7 @@ sealed class ChatInfo: SomeChat, NamedChat { @Serializable @SerialName("group") class Group(val groupInfo: GroupInfo): ChatInfo() { + override val chatType get() = ChatType.Group override val localDisplayName get() = groupInfo.localDisplayName override val id get() = groupInfo.id override val apiId get() = groupInfo.apiId @@ -130,6 +134,7 @@ sealed class ChatInfo: SomeChat, NamedChat { @Serializable @SerialName("contactRequest") class ContactRequest(val contactRequest: UserContactRequest): ChatInfo() { + override val chatType get() = ChatType.ContactRequest override val localDisplayName get() = contactRequest.localDisplayName override val id get() = contactRequest.id override val apiId get() = contactRequest.apiId @@ -153,6 +158,7 @@ class Contact( // no serializer for type Date? // val createdAt: Date ): SomeChat, NamedChat { + override val chatType get() = ChatType.Direct override val id get() = "@$contactId" override val apiId get() = contactId override val ready get() = activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready" @@ -197,6 +203,7 @@ class GroupInfo ( val groupProfile: GroupProfile, // var createdAt: Date ): SomeChat, NamedChat { + override val chatType get() = ChatType.Group override val id get() = "#$groupId" override val apiId get() = groupId override val ready get() = true @@ -257,6 +264,7 @@ class UserContactRequest ( val profile: Profile // val createdAt: Date ): SomeChat, NamedChat { + override val chatType get() = ChatType.ContactRequest override val id get() = "<@$contactRequestId" override val apiId get() = contactRequestId override val ready get() = true diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index bca01a174d..72b4f048ad 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -1,6 +1,9 @@ package chat.simplex.app.model import android.util.Log +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.toMutableStateList import chat.simplex.app.chatRecvMsg import chat.simplex.app.chatSendCmd import kotlinx.coroutines.Dispatchers @@ -21,8 +24,18 @@ open class ChatController(val ctrl: ChatCtrl) { fun setModel(m: ChatModel) { chatModel = m } - suspend fun setCurrentUser(u: User?){ - chatModel!!.setCurrentUser(u) + + suspend fun startChat(u: User) { + chatModel!!.currentUser = mutableStateOf(u) + Log.d("SIMPLEX (user)", u.toString()) + apiStartChat() + try { + Log.d("SIMPLEX", "started chat") + chatModel!!.chats = apiGetChats().toMutableStateList() + } catch(e: Error) { + Log.d("SIMPLEX", "failed starting chat $e") + throw e + } } fun startReceiver() { @@ -75,12 +88,26 @@ open class ChatController(val ctrl: ChatCtrl) { throw Error("failed starting chat: ${r.toString()}") } - suspend fun apiGetChats() { + suspend fun apiGetChats(): List { val r = sendCmd(CC.ApiGetChats()) - if (r is CR.ApiChats ) return + if (r is CR.ApiChats ) return r.chats throw Error("failed getting the list of chats: ${r.toString()}") } + suspend fun apiGetChat(type: ChatType, id: Long): Chat? { + val r = sendCmd(CC.ApiGetChat(type, id)) + if (r is CR.ApiChat ) return r.chat + Log.d("SIMPLEX", "apiGetChat bad response: ${r.toString()}") + return null + } + + suspend fun apiSendMessage(type: ChatType, id: Long, mc: MsgContent): AChatItem? { + val r = sendCmd(CC.ApiSendMessage(type, id, mc)) + if (r is CR.NewChatItem ) return r.chatItem + Log.d("SIMPLEX", "apiSendMessage bad response: ${r.toString()}") + return null + } + class Mock: ChatController(0) {} } @@ -125,7 +152,7 @@ abstract class CC { } companion object { - fun chatRef(type: ChatType, id: Long) = "${type}${id}" + fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}" } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index bc7705f5e6..891903155d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -31,6 +31,9 @@ import kotlinx.coroutines.withContext @Composable fun TerminalView(chatModel: ChatModel, navController: NavController) { Column { + Button(onClick = { navController.popBackStack() }) { + Text("Back") + } TerminalLog(chatModel.terminalItems, navController) SendMsgView(sendMessage = { cmd -> GlobalScope.launch { @@ -59,7 +62,7 @@ fun TerminalLog(terminalItems: List, navController: NavController) @Composable fun DetailView(identifier: Long, terminalItems: List, navController: NavController){ Column( - modifier=Modifier.verticalScroll(rememberScrollState()) + modifier = Modifier.verticalScroll(rememberScrollState()) ) { Button(onClick = { navController.popBackStack() }) { Text("Back") diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt index 30a4334a0a..d2d92f27e8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt @@ -14,6 +14,7 @@ import androidx.compose.material.Button import androidx.compose.ui.unit.dp import androidx.compose.ui.Modifier import chat.simplex.app.SimplexViewModel +import chat.simplex.app.model.ChatModel import chat.simplex.app.model.Profile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -21,7 +22,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Composable -fun WelcomeView(vm: SimplexViewModel, routeHome: () -> Unit) { +fun WelcomeView(chatModel: ChatModel, routeHome: () -> Unit) { Column( modifier = Modifier.verticalScroll(rememberScrollState()) ) { @@ -33,12 +34,12 @@ fun WelcomeView(vm: SimplexViewModel, routeHome: () -> Unit) { Spacer(Modifier.height(8.dp)) Text("We don't store any of your contacts or messages (once delivered) on the servers.") Spacer(Modifier.height(24.dp)) - CreateProfilePanel(vm, routeHome) + CreateProfilePanel(chatModel, routeHome) } } @Composable -fun CreateProfilePanel(vm: SimplexViewModel, routeHome: () -> Unit) { +fun CreateProfilePanel(chatModel: ChatModel, routeHome: () -> Unit) { var displayName by remember { mutableStateOf("") } var fullName by remember { mutableStateOf("") } @@ -52,10 +53,10 @@ fun CreateProfilePanel(vm: SimplexViewModel, routeHome: () -> Unit) { Button(onClick={ GlobalScope.launch { withContext(Dispatchers.Main) { - val user = vm.chatModel.controller.apiCreateActiveUser( + val user = chatModel.controller.apiCreateActiveUser( Profile(displayName, fullName) ) - vm.chatModel.setCurrentUser(user) + chatModel.controller.startChat(user) routeHome() } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 4efa2c50d0..312e0b84bc 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -1,10 +1,66 @@ package chat.simplex.app.views.chat +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.Composable -import chat.simplex.app.model.Chat +import androidx.compose.runtime.MutableState +import androidx.navigation.NavController +import chat.simplex.app.model.* +import chat.simplex.app.views.chat.item.ChatItemView +import chat.simplex.app.views.chatlist.ChatPreviewView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @Composable -fun ChatView(chat: Chat) { - Text("ChatView") +fun ChatView(chatModel: ChatModel, nav: NavController) { + if (chatModel.chatId.value != null && chatModel.chats.count() > 0) { + val chat: Chat = chatModel.chats.first { chat -> chat.chatInfo.id == chatModel.chatId.value } + Column { + ChatInfoToolbar(chat, nav) + ChatItemsList(chatModel.chatItems) + SendMsgView(sendMessage = { msg -> + GlobalScope.launch { + withContext(Dispatchers.Main) { + // show "in progress" + val cInfo = chat.chatInfo + val newItem = chatModel.controller.apiSendMessage( + type = cInfo.chatType, + id = cInfo.apiId, + mc = MsgContent.MCText(msg) + ) + // hide "in progress" + // TODO add new item + } + } + }) + } + } +} + +@Composable +fun ChatInfoToolbar(chat: Chat, nav: NavController) { + Row { + Button(onClick = { nav.popBackStack() }) { + Text("Back") + } + Column { + Text(chat.chatInfo.displayName) + Text(chat.chatInfo.fullName) + } + } +} + +@Composable +fun ChatItemsList(chatItems: List) { + LazyColumn { + items(chatItems) { cItem -> + ChatItemView(cItem) + } + } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 10fc3c9f9c..1aba1845fc 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -6,5 +6,5 @@ import chat.simplex.app.model.ChatItem @Composable fun ChatItemView(chatItem: ChatItem) { - Text("ChatItemView") + Text(chatItem.content.text) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index 634af6248d..fa1774ec99 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -1,10 +1,78 @@ package chat.simplex.app.views.chatlist +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.Button import androidx.compose.material.Text import androidx.compose.runtime.Composable -import chat.simplex.app.model.ChatModel +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import chat.simplex.app.Pages +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.TerminalView +import chat.simplex.app.views.chat.SendMsgView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @Composable -fun ChatListView(chatModel: ChatModel) { - Text("ChatListView") +fun ChatListView(chatModel: ChatModel, navController: NavController) { + Column(modifier = Modifier.padding(all = 8.dp)) { + ChatListToolbar() + Button (onClick = { navController.navigate(Pages.Terminal.route) }) { + Text("Terminal") + } + ChatList(chatModel, navController) + } } + +@Composable +fun ChatListToolbar() { + Text("ChatListToolbar") +} + +@Composable +fun ChatList(chatModel: ChatModel, navController: NavController) { + LazyColumn { + items(chatModel.chats) { chat -> + Button(onClick = { + GlobalScope.launch { + withContext(Dispatchers.Main) { + val cInfo = chat.chatInfo + val chat = chatModel.controller.apiGetChat(cInfo.chatType, cInfo.apiId) + if (chat != null ) { + chatModel.chatId = mutableStateOf(cInfo.id) + chatModel.chatItems = chat.chatItems.toMutableStateList() + navController.navigate(Pages.Chat.route) + } else { + // TODO show error? or will apiGetChat show it + } + } + } + }) { + ChatPreviewView(chat) + } + } + } +} + +//@Preview +//@Composable +//fun PreviewSendMsgView() { +// SimpleXTheme { +// ChatListView( +// chats = listOf( +// Chat() +// ), +// +// ) +// } +//} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index 993e9b96a5..7954ed4c6a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -1,10 +1,34 @@ package chat.simplex.app.views.chatlist +import androidx.compose.foundation.layout.* import androidx.compose.material.Text import androidx.compose.runtime.Composable -import chat.simplex.app.model.Chat +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.SimpleXTheme @Composable fun ChatPreviewView(chat: Chat) { - Text("ChatPreviewView") + Column(modifier = Modifier.padding(all = 8.dp)) { + Text(chat.chatInfo.chatViewName) + if (chat.chatItems.count() > 0) { + Text(chat.chatItems.last().content.text) + } + } +} + +@Preview +@Composable +fun ChatPreviewView() { + SimpleXTheme { + ChatPreviewView( + chat = Chat( + chatInfo = ChatInfo.Direct.sampleData, + chatItems = listOf(), + chatStats = Chat.ChatStats() + ) + ) + } } From 52f758c6e16d2875b57456398f1ec4754b714dcd Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Thu, 17 Feb 2022 21:52:37 +0000 Subject: [PATCH 23/46] make chat model not nullable (#328) * make chat model not nullable * parse datetimes * smart constructors for TerminalItem --- apps/android/app/build.gradle | 1 + .../main/java/chat/simplex/app/SimplexApp.kt | 9 ++--- .../java/chat/simplex/app/model/ChatModel.kt | 33 +++++++++---------- .../java/chat/simplex/app/model/SimpleXAPI.kt | 31 ++++++++--------- 4 files changed, 35 insertions(+), 39 deletions(-) diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 082294a40e..2b2cfe5d38 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -67,6 +67,7 @@ dependencies { implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' implementation 'androidx.activity:activity-compose:1.3.1' + implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index b6efb56ffd..35893e8466 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -30,10 +30,12 @@ external fun chatRecvMsg(ctrl: ChatCtrl) : String class SimplexApp: Application() { private lateinit var controller: ChatController + lateinit var chatModel: ChatModel override fun onCreate() { super.onCreate() controller = ChatController(chatInit(applicationContext.filesDir.toString())) + chatModel = controller.chatModel GlobalScope.launch { withContext(Dispatchers.Main) { var user = controller.apiGetActiveUser() @@ -42,13 +44,6 @@ class SimplexApp: Application() { } } - val chatModel by lazy { - val m = ChatModel(controller) - controller.setModel(m) - controller.startReceiver() - m - } - companion object { lateinit var weakActivity: WeakReference init { diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 2fd3de45f6..306a4e8b69 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -1,8 +1,8 @@ package chat.simplex.app.model import androidx.compose.runtime.* +import kotlinx.datetime.* import kotlinx.serialization.* -import java.util.* class ChatModel(val controller: ChatController) { var currentUser = mutableStateOf(null) @@ -155,8 +155,7 @@ class Contact( val profile: Profile, val activeConn: Connection, val viaGroup: Long? = null, -// no serializer for type Date? -// val createdAt: Date + val createdAt: Instant ): SomeChat, NamedChat { override val chatType get() = ChatType.Direct override val id get() = "@$contactId" @@ -170,8 +169,8 @@ class Contact( contactId = 1, localDisplayName = "alice", profile = Profile.sampleData, - activeConn = Connection.sampleData -// createdAt = Date() + activeConn = Connection.sampleData, + createdAt = Clock.System.now() ) } } @@ -201,7 +200,7 @@ class GroupInfo ( val groupId: Long, override val localDisplayName: String, val groupProfile: GroupProfile, -// var createdAt: Date + val createdAt: Instant ): SomeChat, NamedChat { override val chatType get() = ChatType.Group override val id get() = "#$groupId" @@ -215,7 +214,7 @@ class GroupInfo ( groupId = 1, localDisplayName = "team", groupProfile = GroupProfile.sampleData, -// createdAt: Date() + createdAt = Clock.System.now() ) } } @@ -261,8 +260,8 @@ class GroupMember ( class UserContactRequest ( val contactRequestId: Long, override val localDisplayName: String, - val profile: Profile -// val createdAt: Date + val profile: Profile, + val createdAt: Instant ): SomeChat, NamedChat { override val chatType get() = ChatType.ContactRequest override val id get() = "<@$contactRequestId" @@ -276,7 +275,7 @@ class UserContactRequest ( contactRequestId = 1, localDisplayName = "alice", profile = Profile.sampleData, -// createdAt: Date() + createdAt = Clock.System.now() ) } } @@ -298,7 +297,7 @@ class ChatItem ( val isRcvNew: Boolean get() = meta.itemStatus is CIStatus.RcvNew companion object { - fun getSampleData(id: Long, dir: CIDirection, ts: Date, text: String,status: CIStatus = CIStatus.SndNew()) = + fun getSampleData(id: Long, dir: CIDirection, ts: Instant, text: String,status: CIStatus = CIStatus.SndNew()) = ChatItem( chatDir = dir, meta = CIMeta.getSample(id, ts, text, status), @@ -335,27 +334,27 @@ sealed class CIDirection { @Serializable class CIMeta ( val itemId: Long, -// val itemTs: Date, + val itemTs: Instant, val itemText: String, val itemStatus: CIStatus, -// val createdAt: Date + val createdAt: Instant ) { // val timestampText: String get() = getTimestampText(itemTs) companion object { - fun getSample(id: Long, ts: Date, text: String, status: CIStatus = CIStatus.SndNew()): CIMeta = + fun getSample(id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew()): CIMeta = CIMeta( itemId = id, -// itemTs = ts, + itemTs = ts, itemText = text, itemStatus = status, -// createdAt = ts + createdAt = ts ) } } // TODO -fun getTimestampText(d: Date): String = "" +fun getTimestampText(d: Instant): String = "" @Serializable sealed class CIStatus { diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 72b4f048ad..d7ff0be221 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -18,20 +18,20 @@ import kotlin.concurrent.thread typealias ChatCtrl = Long open class ChatController(val ctrl: ChatCtrl) { - private var chatModel: ChatModel? = null - private val callbacks = mutableMapOf Unit>() + var chatModel: ChatModel - fun setModel(m: ChatModel) { - chatModel = m + init { + chatModel = ChatModel(this) } suspend fun startChat(u: User) { - chatModel!!.currentUser = mutableStateOf(u) + chatModel.currentUser = mutableStateOf(u) Log.d("SIMPLEX (user)", u.toString()) - apiStartChat() try { + apiStartChat() + chatModel.chats.addAll(apiGetChats()) + startReceiver() Log.d("SIMPLEX", "started chat") - chatModel!!.chats = apiGetChats().toMutableStateList() } catch(e: Error) { Log.d("SIMPLEX", "failed starting chat $e") throw e @@ -45,11 +45,7 @@ open class ChatController(val ctrl: ChatCtrl) { val json = chatRecvMsg(ctrl) Log.d("SIMPLEX chatRecvMsg: ", json) val r = APIResponse.decodeStr(json) - if (r.corr != null) { - val cb = callbacks[r.corr] - if (cb != null) cb(null, r.resp) - } - chatModel?.terminalItems?.add(TerminalItem.Resp(System.currentTimeMillis(), r.resp)) + chatModel.terminalItems.add(TerminalItem.resp(r.resp)) } } } @@ -57,13 +53,15 @@ open class ChatController(val ctrl: ChatCtrl) { suspend fun sendCmd(cmd: CC): CR { return withContext(Dispatchers.IO) { val c = cmd.cmdString - chatModel?.terminalItems?.add(TerminalItem.Cmd(System.currentTimeMillis(), cmd)) + chatModel.terminalItems.add(TerminalItem.cmd(cmd)) val json = chatSendCmd(ctrl, c) Log.d("SIMPLEX", "sendCmd: ${cmd.cmdType}") - Log.d("SIMPLEX", "sendCmd response json $json") val r = APIResponse.decodeStr(json) Log.d("SIMPLEX", "sendCmd response type ${r.resp.responseType}") - chatModel?.terminalItems?.add(TerminalItem.Resp(System.currentTimeMillis(), r.resp)) + if (r.resp is CR.Response || r.resp is CR.Invalid) { + Log.d("SIMPLEX", "sendCmd response json $json") + } + chatModel.terminalItems.add(TerminalItem.resp(r.resp)) r.resp } } @@ -276,5 +274,8 @@ abstract class TerminalItem { TerminalItem.Cmd(0, CC.ShowActiveUser()), TerminalItem.Resp(1, CR.ActiveUser(User.sampleData)) ) + + fun cmd(c: CC) = TerminalItem.Cmd(System.currentTimeMillis(), c) + fun resp(r: CR) = TerminalItem.Resp(System.currentTimeMillis(), r) } } From 6daad1021054f9231596cb77e240c033411a27c8 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 18 Feb 2022 09:00:21 +0000 Subject: [PATCH 24/46] make condition depend on host os (#329) --- cabal.project | 2 +- sha256map.nix | 2 +- stack.yaml | 2 +- tests/MobileTests.hs | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cabal.project b/cabal.project index 7d40f79f38..dcee0f71a5 100644 --- a/cabal.project +++ b/cabal.project @@ -3,7 +3,7 @@ packages: . source-repository-package type: git location: git://github.com/simplex-chat/simplexmq.git - tag: c1f5f9d8461e254c0c67c65de36f0860306b3d72 + tag: dff5cad1bef67376e82c3dc15cccdb5ba9e675ab source-repository-package type: git diff --git a/sha256map.nix b/sha256map.nix index b03bfc42e1..e77e104a95 100644 --- a/sha256map.nix +++ b/sha256map.nix @@ -1,5 +1,5 @@ { - "git://github.com/simplex-chat/simplexmq.git"."c1f5f9d8461e254c0c67c65de36f0860306b3d72" = "1qbj4xgg27qwm67187vkn87jggd3q47v4vylh915kbxw7npilalj"; + "git://github.com/simplex-chat/simplexmq.git"."dff5cad1bef67376e82c3dc15cccdb5ba9e675ab" = "06291v6vw7i00r0j13qx5apkz794jak68n1yr875gi32dxx5lhnp"; "git://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp"; "git://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj"; "git://github.com/zw3rk/android-support.git"."3c3a5ab0b8b137a072c98d3d0937cbdc96918ddb" = "1r6jyxbim3dsvrmakqfyxbd6ms6miaghpbwyl0sr6dzwpgaprz97"; diff --git a/stack.yaml b/stack.yaml index 739d0f5a1c..86a9c6ce42 100644 --- a/stack.yaml +++ b/stack.yaml @@ -48,7 +48,7 @@ extra-deps: # - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561 # - ../simplexmq - github: simplex-chat/simplexmq - commit: c1f5f9d8461e254c0c67c65de36f0860306b3d72 + commit: dff5cad1bef67376e82c3dc15cccdb5ba9e675ab # - terminal-0.2.0.0@sha256:de6770ecaae3197c66ac1f0db5a80cf5a5b1d3b64a66a05b50f442de5ad39570,2977 - github: simplex-chat/aeson commit: 3eb66f9a68f103b5f1489382aad89f5712a64db7 diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index e0f219e801..02e1f7f4a4 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -17,28 +17,28 @@ mobileTests = do it "start new chat with existing user" testChatApi noActiveUser :: String -#if defined(IOS) +#if defined(darwin_HOST_OS) noActiveUser = "{\"resp\":{\"chatCmdError\":{\"chatError\":{\"error\":{\"errorType\":{\"noActiveUser\":{}}}}}}}" #else noActiveUser = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"noActiveUser\"}}}}" #endif activeUserExists :: String -#if defined(IOS) +#if defined(darwin_HOST_OS) activeUserExists = "{\"resp\":{\"chatCmdError\":{\"chatError\":{\"error\":{\"errorType\":{\"activeUserExists\":{}}}}}}}" #else activeUserExists = "{\"resp\":{\"type\":\"chatCmdError\",\"chatError\":{\"type\":\"error\",\"errorType\":{\"type\":\"activeUserExists\"}}}}" #endif activeUser :: String -#if defined(IOS) +#if defined(darwin_HOST_OS) activeUser = "{\"resp\":{\"activeUser\":{\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"displayName\":\"alice\",\"fullName\":\"Alice\"},\"activeUser\":true}}}}" #else activeUser = "{\"resp\":{\"type\":\"activeUser\",\"user\":{\"userId\":1,\"userContactId\":1,\"localDisplayName\":\"alice\",\"profile\":{\"displayName\":\"alice\",\"fullName\":\"Alice\"},\"activeUser\":true}}}" #endif chatStarted :: String -#if defined(IOS) +#if defined(darwin_HOST_OS) chatStarted = "{\"resp\":{\"chatStarted\":{}}}" #else chatStarted = "{\"resp\":{\"type\":\"chatStarted\"}}" From bd1318104224c8450819cb5964fdbe788343bbb1 Mon Sep 17 00:00:00 2001 From: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> Date: Fri, 18 Feb 2022 14:05:11 +0400 Subject: [PATCH 25/46] platform independent json encoding for db (#330) --- src/Simplex/Chat/Messages.hs | 40 +++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 1634c11b12..92b96b699e 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -35,7 +35,7 @@ import Simplex.Chat.Util (eitherToMaybe, safeDecodeUtf8) import Simplex.Messaging.Agent.Protocol (AgentErrorType, AgentMsgId, MsgMeta (..)) import Simplex.Messaging.Agent.Store.SQLite (fromTextField_) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Parsers (dropPrefix, enumJSON, sumTypeJSON) +import Simplex.Messaging.Parsers (dropPrefix, enumJSON, singleFieldJSON, sumTypeJSON) import Simplex.Messaging.Protocol (MsgBody) import Simplex.Messaging.Util ((<$?>)) @@ -302,20 +302,25 @@ ciContentToText = \case CISndFileInvitation fId fPath -> "you sent file #" <> T.pack (show fId) <> ": " <> T.pack fPath CIRcvFileInvitation RcvFileTransfer {fileInvitation = FileInvitation {fileName}} -> "file " <> T.pack fileName +-- platform independent instance ToField (CIContent d) where - toField = toField . safeDecodeUtf8 . LB.toStrict . J.encode + toField = toField . safeDecodeUtf8 . LB.toStrict . J.encode . dbJsonCIContent +-- platform specific instance ToJSON (CIContent d) where toJSON = J.toJSON . jsonCIContent toEncoding = J.toEncoding . jsonCIContent data ACIContent = forall d. ACIContent (SMsgDirection d) (CIContent d) +-- platform specific instance FromJSON ACIContent where parseJSON = fmap aciContentJSON . J.parseJSON -instance FromField ACIContent where fromField = fromTextField_ $ J.decode . LB.fromStrict . encodeUtf8 +-- platform independent +instance FromField ACIContent where fromField = fromTextField_ $ fmap aciContentDBJSON . J.decode . LB.fromStrict . encodeUtf8 +-- platform specific data JSONCIContent = JCISndMsgContent {msgContent :: MsgContent} | JCIRcvMsgContent {msgContent :: MsgContent} @@ -344,6 +349,35 @@ aciContentJSON = \case JCISndFileInvitation fId fPath -> ACIContent SMDSnd $ CISndFileInvitation fId fPath JCIRcvFileInvitation ft -> ACIContent SMDRcv $ CIRcvFileInvitation ft +-- platform independent +data DBJSONCIContent + = DBJCISndMsgContent {msgContent :: MsgContent} + | DBJCIRcvMsgContent {msgContent :: MsgContent} + | DBJCISndFileInvitation {fileId :: FileTransferId, filePath :: FilePath} + | DBJCIRcvFileInvitation {rcvFileTransfer :: RcvFileTransfer} + deriving (Generic) + +instance FromJSON DBJSONCIContent where + parseJSON = J.genericParseJSON . singleFieldJSON $ dropPrefix "DBJCI" + +instance ToJSON DBJSONCIContent where + toJSON = J.genericToJSON . singleFieldJSON $ dropPrefix "DBJCI" + toEncoding = J.genericToEncoding . singleFieldJSON $ dropPrefix "DBJCI" + +dbJsonCIContent :: CIContent d -> DBJSONCIContent +dbJsonCIContent = \case + CISndMsgContent mc -> DBJCISndMsgContent mc + CIRcvMsgContent mc -> DBJCIRcvMsgContent mc + CISndFileInvitation fId fPath -> DBJCISndFileInvitation fId fPath + CIRcvFileInvitation ft -> DBJCIRcvFileInvitation ft + +aciContentDBJSON :: DBJSONCIContent -> ACIContent +aciContentDBJSON = \case + DBJCISndMsgContent mc -> ACIContent SMDSnd $ CISndMsgContent mc + DBJCIRcvMsgContent mc -> ACIContent SMDRcv $ CIRcvMsgContent mc + DBJCISndFileInvitation fId fPath -> ACIContent SMDSnd $ CISndFileInvitation fId fPath + DBJCIRcvFileInvitation ft -> ACIContent SMDRcv $ CIRcvFileInvitation ft + data SChatType (c :: ChatType) where SCTDirect :: SChatType 'CTDirect SCTGroup :: SChatType 'CTGroup From f6503089862230cd4c3f7108425cc1183ceb7068 Mon Sep 17 00:00:00 2001 From: IanRDavies Date: Fri, 18 Feb 2022 13:10:24 +0000 Subject: [PATCH 26/46] initial chat list styling (#332) --- apps/android/app/build.gradle | 1 + .../java/chat/simplex/app/ui/theme/Color.kt | 3 +- .../java/chat/simplex/app/ui/theme/Theme.kt | 7 +- .../app/views/chatlist/ChatListView.kt | 89 ++++++++++++------- .../app/views/chatlist/ChatPreviewView.kt | 51 +++++++++-- 5 files changed, 108 insertions(+), 43 deletions(-) diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 2b2cfe5d38..f4c0a8434c 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -69,6 +69,7 @@ dependencies { implementation 'androidx.activity:activity-compose:1.3.1' implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' + implementation "androidx.compose.material:material-icons-extended:$compose_version" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt index 40f91fc96e..59390dc153 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt @@ -5,4 +5,5 @@ import androidx.compose.ui.graphics.Color val Purple200 = Color(0xFFBB86FC) val Purple500 = Color(0xFF6200EE) val Purple700 = Color(0xFF3700B3) -val Teal200 = Color(0xFF03DAC5) \ No newline at end of file +val Teal200 = Color(0xFF03DAC5) +val Gray = Color(0x22222222) diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt index 3d9d30c142..49a849e800 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt @@ -9,14 +9,15 @@ import androidx.compose.runtime.Composable private val DarkColorPalette = darkColors( primary = Purple200, primaryVariant = Purple700, - secondary = Teal200 + secondary = Teal200, + secondaryVariant = Gray ) private val LightColorPalette = lightColors( primary = Purple500, primaryVariant = Purple700, - secondary = Teal200 - + secondary = Teal200, + secondaryVariant = Gray /* Other default colors to override background = Color.White, surface = Color.White, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index fa1774ec99..11f346ebed 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -1,7 +1,6 @@ package chat.simplex.app.views.chatlist -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.Button @@ -15,55 +14,81 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import chat.simplex.app.Pages import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.SimpleXTheme -import chat.simplex.app.views.TerminalView -import chat.simplex.app.views.chat.SendMsgView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.text.font.FontWeight @Composable fun ChatListView(chatModel: ChatModel, navController: NavController) { - Column(modifier = Modifier.padding(all = 8.dp)) { - ChatListToolbar() - Button (onClick = { navController.navigate(Pages.Terminal.route) }) { - Text("Terminal") + Column(modifier = Modifier.padding(vertical = 8.dp).fillMaxWidth()) { + ChatListToolbar() + ChatList(chatModel, navController) + Button( + onClick = { navController.navigate(Pages.Terminal.route) }, + modifier = Modifier.padding(14.dp) + ) { + Text("Terminal") + } } - ChatList(chatModel, navController) - } } @Composable fun ChatListToolbar() { - Text("ChatListToolbar") + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .height(40.dp)) { + Icon( + Icons.Outlined.Settings, + "Settings Cog", + modifier = Modifier.padding(horizontal = 10.dp) + ) + Text("Your chats", fontWeight = FontWeight.Bold, modifier = Modifier.padding(5.dp)) + Icon( + Icons.Outlined.PersonAdd, + "Add Contact", + modifier = Modifier.padding(horizontal = 10.dp) + ) + } } -@Composable -fun ChatList(chatModel: ChatModel, navController: NavController) { - LazyColumn { - items(chatModel.chats) { chat -> - Button(onClick = { - GlobalScope.launch { - withContext(Dispatchers.Main) { - val cInfo = chat.chatInfo - val chat = chatModel.controller.apiGetChat(cInfo.chatType, cInfo.apiId) - if (chat != null ) { - chatModel.chatId = mutableStateOf(cInfo.id) - chatModel.chatItems = chat.chatItems.toMutableStateList() - navController.navigate(Pages.Chat.route) - } else { - // TODO show error? or will apiGetChat show it - } - } - } - }) { - ChatPreviewView(chat) + +fun goToChat(chat: Chat, chatModel: ChatModel, navController: NavController) { + GlobalScope.launch { + withContext(Dispatchers.Main) { + val cInfo = chat.chatInfo + val chat = chatModel.controller.apiGetChat(cInfo.chatType, cInfo.apiId) + if (chat != null ) { + chatModel.chatId = mutableStateOf(cInfo.id) + chatModel.chatItems = chat.chatItems.toMutableStateList() + navController.navigate(Pages.Chat.route) + } else { + // TODO show error? or will apiGetChat show it } } } } +@Composable +fun ChatList(chatModel: ChatModel, navController: NavController) { + LazyColumn( + modifier = Modifier.fillMaxWidth() + ) { + items(chatModel.chats) { chat -> + ChatPreviewView(chat) {goToChat(chat, chatModel, navController)} + } + } +} + + //@Preview //@Composable //fun PreviewSendMsgView() { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index 7954ed4c6a..9b19cd2c87 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -1,34 +1,71 @@ package chat.simplex.app.views.chatlist +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import chat.simplex.app.model.* +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Person +import androidx.compose.ui.Alignment +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.* import chat.simplex.app.ui.theme.SimpleXTheme @Composable -fun ChatPreviewView(chat: Chat) { - Column(modifier = Modifier.padding(all = 8.dp)) { - Text(chat.chatInfo.chatViewName) - if (chat.chatItems.count() > 0) { - Text(chat.chatItems.last().content.text) +fun ChatPreviewView(chat: Chat, goToChat: () -> Unit) { + Surface( + border=BorderStroke(0.5.dp, MaterialTheme.colors.secondaryVariant), + modifier= Modifier + .fillMaxWidth() + .clickable(onClick = goToChat) + + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + Icon( + Icons.Filled.Person, + contentDescription = "Avatar Placeholder", + modifier = Modifier + .size(30.dp) + .clip(CircleShape) + .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) + ) + + Spacer(modifier = Modifier.width(6.dp)) + Column(modifier = Modifier.padding(all = 8.dp)) { + Text(chat.chatInfo.chatViewName, fontWeight = FontWeight.Bold) + if (chat.chatItems.count() > 0) { + Text(chat.chatItems.last().content.text) + } + } } } } @Preview @Composable -fun ChatPreviewView() { +fun ChatPreviewViewExample() { SimpleXTheme { ChatPreviewView( chat = Chat( chatInfo = ChatInfo.Direct.sampleData, chatItems = listOf(), chatStats = Chat.ChatStats() - ) + ), + goToChat = {} ) } } From bba2783aa40cdc81195c5d4e45c457f429a5c785 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Fri, 18 Feb 2022 14:33:55 +0000 Subject: [PATCH 27/46] update model when messages arrive (#333) * update model when messages arrive * update chat in the list when message is added * copy methods with optional parameters * use data classes to have pre-defined copy methods --- .../java/chat/simplex/app/model/ChatModel.kt | 151 +++++++++++++++++- .../java/chat/simplex/app/model/SimpleXAPI.kt | 72 ++++++++- .../chat/simplex/app/views/chat/ChatView.kt | 2 +- apps/ios/Shared/Model/SimpleXAPI.swift | 3 + 4 files changed, 219 insertions(+), 9 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 306a4e8b69..0cd97da752 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -12,6 +12,148 @@ class ChatModel(val controller: ChatController) { var terminalItems = mutableStateListOf() + fun hasChat(id: String): Boolean = chats.firstOrNull() { it.id == id } != null + fun getChat(id: String): Chat? = chats.firstOrNull { it.id == id } + private fun getChatIndex(id: String): Int = chats.indexOfFirst { it.id == id } + fun addChat(chat: Chat) = chats.add(index = 0, chat) + +// func updateChatInfo(_ cInfo: ChatInfo) { +// if let i = getChatIndex(cInfo.id) { +// chats[i].chatInfo = cInfo +// } +// } +// +// func updateContact(_ contact: Contact) { +// let cInfo = ChatInfo.direct(contact: contact) +// if hasChat(contact.id) { +// updateChatInfo(cInfo) +// } else { +// addChat(Chat(chatInfo: cInfo, chatItems: [])) +// } +// } +// +// func updateNetworkStatus(_ contact: Contact, _ status: Chat.NetworkStatus) { +// if let ix = getChatIndex(contact.id) { +// chats[ix].serverInfo.networkStatus = status +// } +// } +// +// func replaceChat(_ id: String, _ chat: Chat) { +// if let i = getChatIndex(id) { +// chats[i] = chat +// } else { +// // invalid state, correcting +// chats.insert(chat, at: 0) +// } +// } + + fun addChatItem(cInfo: ChatInfo, cItem: ChatItem) { + // update previews + val i = getChatIndex(cInfo.id) + if (i >= 0) { + val chat = chats[i] + val updatedChat = chat.copy( + chatItems = arrayListOf(cItem), + chatStats = + if (cItem.meta.itemStatus is CIStatus.RcvNew) + chat.chatStats.copy(unreadCount = chat.chatStats.unreadCount + 1) + else + chat.chatStats + ) + chats.set(i, updatedChat) + if (i > 0) { + popChat_(i) + } + } else { + addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem))) + } + // add to current chat + if (chatId.value == cInfo.id) { + chatItems.add(cItem) + if (cItem.meta.itemStatus is CIStatus.RcvNew) { + // TODO mark item read via api and model +// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { +// if self.chatId == cInfo.id { +// SimpleX.markChatItemRead(cInfo, cItem) +// } +// } + } + } + } +// +// func upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool { +// // update previews +// var res: Bool +// if let chat = getChat(cInfo.id) { +// if let pItem = chat.chatItems.last, pItem.id == cItem.id { +// chat.chatItems = [cItem] +// } +// res = false +// } else { +// addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) +// res = true +// } +// // update current chat +// if chatId == cInfo.id { +// if let i = chatItems.firstIndex(where: { $0.id == cItem.id }) { +// withAnimation(.default) { +// self.chatItems[i] = cItem +// } +// return false +// } else { +// withAnimation { chatItems.append(cItem) } +// return true +// } +// } else { +// return res +// } +// } +// +// func markChatItemsRead(_ cInfo: ChatInfo) { +// // update preview +// if let chat = getChat(cInfo.id) { +// chat.chatStats = ChatStats() +// } +// // update current chat +// if chatId == cInfo.id { +// var i = 0 +// while i < chatItems.count { +// if case .rcvNew = chatItems[i].meta.itemStatus { +// chatItems[i].meta.itemStatus = .rcvRead +// } +// i = i + 1 +// } +// } +// } +// +// func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) { +// // update preview +// if let i = getChatIndex(cInfo.id) { +// chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount - 1 +// } +// // update current chat +// if chatId == cInfo.id, let j = chatItems.firstIndex(where: { $0.id == cItem.id }) { +// chatItems[j].meta.itemStatus = .rcvRead +// } +// } +// +// func popChat(_ id: String) { +// if let i = getChatIndex(id) { +// popChat_(i) +// } +// } +// + private fun popChat_(i: Int) { + val chat = chats.removeAt(i) + chats.add(index = 0, chat) + } +// +// func removeChat(_ id: String) { +// withAnimation { +// chats.removeAll(where: { $0.id == id }) +// } +// } + companion object { val sampleData: ChatModel get() { val m = ChatModel(ChatController.Mock()) @@ -70,17 +212,16 @@ interface SomeChat { } @Serializable -class Chat ( +data class Chat ( val chatInfo: ChatInfo, val chatItems: List, - val chatStats: ChatStats, + val chatStats: ChatStats = ChatStats(), val serverInfo: ServerInfo = ServerInfo(NetworkStatus.Unknown()) ) { + val id: String get() = chatInfo.id @Serializable - class ChatStats { - - } + data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0) @Serializable class ServerInfo(val networkStatus: NetworkStatus) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index d7ff0be221..5a5b74b4b4 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -43,9 +43,10 @@ open class ChatController(val ctrl: ChatCtrl) { // val chatlog = FifoQueue(500) while(true) { val json = chatRecvMsg(ctrl) - Log.d("SIMPLEX chatRecvMsg: ", json) - val r = APIResponse.decodeStr(json) - chatModel.terminalItems.add(TerminalItem.resp(r.resp)) + val r = APIResponse.decodeStr(json).resp + Log.d("SIMPLEX", "chatRecvMsg: ${r.responseType}") + if (r is CR.Response || r is CR.Invalid) Log.d("SIMPLEX", "chatRecvMsg json: $json") + processReceivedMsg(r) } } } @@ -106,6 +107,71 @@ open class ChatController(val ctrl: ChatCtrl) { return null } + fun processReceivedMsg(r: CR) { + chatModel.terminalItems.add(TerminalItem.resp(r)) + when { +// r is CR.ContactConnected -> return +// r is CR.UpdateNetworkStatus -> return +// r is CR.ReceivedContactRequest -> return +// r is CR.ContactUpdated -> return +// r is CR.ContactSubscribed -> return +// r is CR.ContactSubError -> return +// r is CR.UpdateContact -> return + r is CR.NewChatItem -> { + val cInfo = r.chatItem.chatInfo + val cItem = r.chatItem.chatItem + chatModel.addChatItem(cInfo, cItem) + } +// NtfManager.shared.notifyMessageReceived(cInfo, cItem) + +// switch res { +// case let .contactConnected(contact): +// chatModel.updateContact(contact) +// chatModel.updateNetworkStatus(contact, .connected) +// NtfManager.shared.notifyContactConnected(contact) +// case let .receivedContactRequest(contactRequest): + // chatModel.addChat(Chat( + // chatInfo: ChatInfo.contactRequest(contactRequest: contactRequest), + // chatItems: [] + // )) +// NtfManager.shared.notifyContactRequest(contactRequest) +// case let .contactUpdated(toContact): + // let cInfo = ChatInfo.direct(contact: toContact) + // if chatModel.hasChat(toContact.id) { + // chatModel.updateChatInfo(cInfo) + // } +// case let .contactSubscribed(contact): + // chatModel.updateContact(contact) + // chatModel.updateNetworkStatus(contact, .connected) + // case let .contactDisconnected(contact): + // chatModel.updateContact(contact) + // chatModel.updateNetworkStatus(contact, .disconnected) +// case let .contactSubError(contact, chatError): +// chatModel.updateContact(contact) +//// var err: String +//// switch chatError { +//// case .errorAgent(agentError: .BROKER(brokerErr: .NETWORK)): err = "network" +//// case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted" +//// default: err = String(describing: chatError) +//// } +//// chatModel.updateNetworkStatus(contact, .error(err)) +// case let .newChatItem(aChatItem): + // let cInfo = aChatItem.chatInfo + // let cItem = aChatItem.chatItem + // chatModel.addChatItem(cInfo, cItem) + // NtfManager.shared.notifyMessageReceived(cInfo, cItem) +// case let .chatItemUpdated(aChatItem): + // let cInfo = aChatItem.chatInfo + // let cItem = aChatItem.chatItem + // if chatModel.upsertChatItem(cInfo, cItem) { + // NtfManager.shared.notifyMessageReceived(cInfo, cItem) + // } +// default: +// logger.debug("unsupported event: \(res.responseType)") +// } + } + } + class Mock: ChatController(0) {} } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 312e0b84bc..cf4cea564b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -35,7 +35,7 @@ fun ChatView(chatModel: ChatModel, nav: NavController) { mc = MsgContent.MCText(msg) ) // hide "in progress" - // TODO add new item + if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem) } } }) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 051f6df905..7f5f50876c 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -237,6 +237,9 @@ func chatSendCmd(_ cmd: ChatCommand) throws -> ChatResponse { logger.debug("chatSendCmd \(cmd.cmdType)") let resp = chatResponse(chat_send_cmd(getChatCtrl(), &c)!) logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)") + if case let .response(_, json) = resp { + logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)") + } DispatchQueue.main.async { ChatModel.shared.terminalItems.append(.cmd(.now, cmd)) ChatModel.shared.terminalItems.append(.resp(.now, resp)) From 3820d08af887f4085b835245a5330ce6366cea40 Mon Sep 17 00:00:00 2001 From: IanRDavies Date: Fri, 18 Feb 2022 16:55:50 +0000 Subject: [PATCH 28/46] chat list styling round 2 (#334) * initial restyling: * polish styling a little * lint * more linting * add dependency * add time to messages when they exist * if no chat items show time from time chat created * playing with colours * rename shared colour * flip title text colour in dark mode --- apps/android/app/build.gradle | 2 +- .../java/chat/simplex/app/model/ChatModel.kt | 10 ++- .../java/chat/simplex/app/ui/theme/Color.kt | 5 ++ .../java/chat/simplex/app/ui/theme/Theme.kt | 26 ++++--- .../app/views/chatlist/ChatListView.kt | 17 ++++- .../app/views/chatlist/ChatPreviewView.kt | 71 ++++++++++++++----- 6 files changed, 100 insertions(+), 31 deletions(-) diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index f4c0a8434c..34c6c464b6 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -9,7 +9,7 @@ android { defaultConfig { applicationId "chat.simplex.app" - minSdk 24 + minSdk 26 targetSdk 32 versionCode 1 versionName "1.0" diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 0cd97da752..b53b62e839 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -209,6 +209,7 @@ interface SomeChat { val id: ChatId val apiId: Long val ready: Boolean + val createdAt: Instant } @Serializable @@ -250,6 +251,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = contact.id override val apiId get() = contact.apiId override val ready get() = contact.ready + override val createdAt get() = contact.createdAt override val displayName get() = contact.displayName override val fullName get() = contact.displayName @@ -265,6 +267,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = groupInfo.id override val apiId get() = groupInfo.apiId override val ready get() = groupInfo.ready + override val createdAt get() = groupInfo.createdAt override val displayName get() = groupInfo.displayName override val fullName get() = groupInfo.displayName @@ -280,6 +283,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = contactRequest.id override val apiId get() = contactRequest.apiId override val ready get() = contactRequest.ready + override val createdAt get() = contactRequest.createdAt override val displayName get() = contactRequest.displayName override val fullName get() = contactRequest.displayName @@ -296,7 +300,7 @@ class Contact( val profile: Profile, val activeConn: Connection, val viaGroup: Long? = null, - val createdAt: Instant + override val createdAt: Instant ): SomeChat, NamedChat { override val chatType get() = ChatType.Direct override val id get() = "@$contactId" @@ -341,7 +345,7 @@ class GroupInfo ( val groupId: Long, override val localDisplayName: String, val groupProfile: GroupProfile, - val createdAt: Instant + override val createdAt: Instant ): SomeChat, NamedChat { override val chatType get() = ChatType.Group override val id get() = "#$groupId" @@ -402,7 +406,7 @@ class UserContactRequest ( val contactRequestId: Long, override val localDisplayName: String, val profile: Profile, - val createdAt: Instant + override val createdAt: Instant ): SomeChat, NamedChat { override val chatType get() = ChatType.ContactRequest override val id get() = "<@$contactRequestId" diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt index 59390dc153..ec2f0b1f12 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt @@ -7,3 +7,8 @@ val Purple500 = Color(0xFF6200EE) val Purple700 = Color(0xFF3700B3) val Teal200 = Color(0xFF03DAC5) val Gray = Color(0x22222222) +val AppleBlue = Color(0, 136, 255, 255) +val AppleGreen = Color(98, 196, 103, 255) +val LightGray = Color(241, 242, 246, 255) +val DarkGray = Color(43, 44, 46, 255) +val HighOrLowlight = Color(134, 135, 139, 255) diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt index 49a849e800..d486720301 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt @@ -5,21 +5,29 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.darkColors import androidx.compose.material.lightColors import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color private val DarkColorPalette = darkColors( - primary = Purple200, - primaryVariant = Purple700, - secondary = Teal200, - secondaryVariant = Gray + primary = AppleBlue, + primaryVariant = AppleGreen, + secondary = DarkGray, + /* + background: Color = Color(0xFF121212), + surface: Color = Color(0xFF121212), + error: Color = Color(0xFFCF6679), + onPrimary: Color = Color.Black, + onSecondary: Color = Color.Black, + onBackground: Color = Color.White, + onSurface: Color = Color.White, + onError: Color = Color.Black + */ ) private val LightColorPalette = lightColors( - primary = Purple500, - primaryVariant = Purple700, - secondary = Teal200, - secondaryVariant = Gray + primary = AppleBlue, + primaryVariant = AppleGreen, + secondary = LightGray, /* Other default colors to override - background = Color.White, surface = Color.White, onPrimary = Color.White, onSecondary = Color.Black, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index 11f346ebed..8a08347af0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -1,5 +1,6 @@ package chat.simplex.app.views.chatlist +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -19,6 +20,7 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* import androidx.compose.ui.Alignment @@ -26,7 +28,11 @@ import androidx.compose.ui.text.font.FontWeight @Composable fun ChatListView(chatModel: ChatModel, navController: NavController) { - Column(modifier = Modifier.padding(vertical = 8.dp).fillMaxWidth()) { + Column(modifier = Modifier + .padding(vertical = 8.dp) + .fillMaxSize() + .background(MaterialTheme.colors.background) + ) { ChatListToolbar() ChatList(chatModel, navController) Button( @@ -49,12 +55,19 @@ fun ChatListToolbar() { Icon( Icons.Outlined.Settings, "Settings Cog", + tint = MaterialTheme.colors.primary, modifier = Modifier.padding(horizontal = 10.dp) ) - Text("Your chats", fontWeight = FontWeight.Bold, modifier = Modifier.padding(5.dp)) + Text( + "Your chats", + color=MaterialTheme.colors.onBackground, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(5.dp) + ) Icon( Icons.Outlined.PersonAdd, "Add Contact", + tint = MaterialTheme.colors.primary, modifier = Modifier.padding(horizontal = 10.dp) ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index 9b19cd2c87..a96155665e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -1,6 +1,8 @@ package chat.simplex.app.views.chatlist +import android.icu.text.SimpleDateFormat import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -16,39 +18,76 @@ import chat.simplex.app.model.* import androidx.compose.material.Icon import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Person -import androidx.compose.ui.Alignment import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.* +import androidx.compose.ui.text.style.TextOverflow +import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.SimpleXTheme +import kotlinx.datetime.Instant +import kotlinx.datetime.toJavaInstant +import java.time.LocalDateTime +import java.time.Instant as JavaInstant +import java.util.* + + + +fun getDisplayTime(t: Instant) : String { + val timeFormatter = SimpleDateFormat("HH:mm", Locale.getDefault()) + val dateFormatter = SimpleDateFormat("dd/MM/y", Locale.getDefault()) + val timeProvided: JavaInstant = t.toJavaInstant() + val tz = TimeZone.getDefault() + val dateProvided = LocalDateTime.ofInstant(timeProvided, tz.toZoneId()).toLocalDate() + val today = LocalDateTime.now().atZone(tz.toZoneId()).toLocalDate() + val yesterday = today.minusDays(1) + + return when (dateProvided) { + today -> timeFormatter.format(Date.from(timeProvided)) + yesterday -> "yesterday" + else -> dateFormatter.format(Date.from(timeProvided)) + } +} @Composable fun ChatPreviewView(chat: Chat, goToChat: () -> Unit) { Surface( - border=BorderStroke(0.5.dp, MaterialTheme.colors.secondaryVariant), + border=BorderStroke(0.5.dp, MaterialTheme.colors.secondary), modifier= Modifier .fillMaxWidth() .clickable(onClick = goToChat) - + .height(80.dp) ) { Row( modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp), - verticalAlignment = Alignment.CenterVertically ) { - - Icon( - Icons.Filled.Person, - contentDescription = "Avatar Placeholder", - modifier = Modifier - .size(30.dp) - .clip(CircleShape) - .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) - ) - + Column(verticalArrangement=Arrangement.Center, modifier=Modifier.fillMaxHeight()) { + Icon( + Icons.Filled.Person, + contentDescription = "Avatar Placeholder", + tint = MaterialTheme.colors.background, + modifier = Modifier + .size(55.dp) + .clip(CircleShape) + .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) + .background(MaterialTheme.colors.secondary) + ) + } Spacer(modifier = Modifier.width(6.dp)) Column(modifier = Modifier.padding(all = 8.dp)) { - Text(chat.chatInfo.chatViewName, fontWeight = FontWeight.Bold) + Row(horizontalArrangement=Arrangement.SpaceBetween, modifier=Modifier.fillMaxWidth()) { + Text(chat.chatInfo.chatViewName, fontWeight = FontWeight.Bold) + ( + if (chat.chatItems.count() > 0) { + Text(getDisplayTime(chat.chatItems.last().meta.itemTs), color = HighOrLowlight) + } + else Text(getDisplayTime(chat.chatInfo.createdAt), color = HighOrLowlight) + ) + } if (chat.chatItems.count() > 0) { - Text(chat.chatItems.last().content.text) + Text( + chat.chatItems.last().content.text, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) } } } From 6c4888d2752ab726bc576b9396a6d450eb564a8d Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 19 Feb 2022 10:15:18 +0000 Subject: [PATCH 29/46] android app: API, add chat sheet and view with QR code (#336) * add contact (WIP) * basic UI to create new chat, finalize API classes and functions (TODO: process chatRecvMsg messages) * add contact layout with QR code * refactor NewChatSheet to split layout, refactor withApi * add newlines * Update apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt Co-authored-by: IanRDavies Co-authored-by: IanRDavies --- apps/android/app/build.gradle | 1 + .../java/chat/simplex/app/MainActivity.kt | 12 +- .../main/java/chat/simplex/app/MainPage.kt | 4 +- .../main/java/chat/simplex/app/SimplexApp.kt | 1 - .../java/chat/simplex/app/model/ChatModel.kt | 32 +- .../java/chat/simplex/app/model/SimpleXAPI.kt | 325 ++++++++++++++++-- .../java/chat/simplex/app/ui/theme/Color.kt | 4 +- .../java/chat/simplex/app/ui/theme/Theme.kt | 9 +- .../java/chat/simplex/app/ui/theme/Type.kt | 32 +- .../chat/simplex/app/views/TerminalView.kt | 11 +- .../app/views/chatlist/ChatListView.kt | 54 ++- .../app/views/chatlist/ChatPreviewView.kt | 10 +- .../app/views/helpers/CloseSheetBar.kt | 42 +++ .../simplex/app/views/helpers/SimpleButton.kt | 44 +++ .../chat/simplex/app/views/helpers/Util.kt | 10 + .../app/views/newchat/AddContactView.kt | 92 +++++ .../simplex/app/views/newchat/NewChatSheet.kt | 66 ++++ .../chat/simplex/app/views/newchat/QRCode.kt | 46 +++ 18 files changed, 696 insertions(+), 99 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 34c6c464b6..368a46794a 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -70,6 +70,7 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' implementation "androidx.compose.material:material-icons-extended:$compose_version" + implementation 'com.google.zxing:core:3.4.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 4a13ded802..15bfceb682 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -5,6 +5,7 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.runtime.Composable import chat.simplex.app.ui.theme.SimpleXTheme import androidx.lifecycle.AndroidViewModel @@ -17,8 +18,10 @@ import chat.simplex.app.model.ChatModel import chat.simplex.app.views.* import chat.simplex.app.views.chat.ChatView import chat.simplex.app.views.chatlist.* +import chat.simplex.app.views.newchat.AddContactView +@ExperimentalMaterialApi class MainActivity: ComponentActivity() { private val viewModel by viewModels() @@ -36,6 +39,7 @@ class SimplexViewModel(application: Application) : AndroidViewModel(application) val chatModel = getApplication().chatModel } +@ExperimentalMaterialApi @Composable fun Navigation(chatModel: ChatModel) { val nav = rememberNavController() @@ -51,12 +55,15 @@ fun Navigation(chatModel: ChatModel) { } } } - composable(route = Pages.Chats.route) { + composable(route = Pages.ChatList.route) { ChatListView(chatModel, nav) } composable(route = Pages.Chat.route) { ChatView(chatModel, nav) } + composable(route = Pages.AddContact.route) { + AddContactView(chatModel, nav) + } composable(route = Pages.Terminal.route) { TerminalView(chatModel, navController = nav) } @@ -76,6 +83,7 @@ sealed class Pages(val route: String) { object Terminal : Pages("terminal") object Welcome : Pages("welcome") object TerminalItemDetails : Pages("details") - object Chats: Pages("chats") + object ChatList: Pages("chats") object Chat: Pages("chat") + object AddContact: Pages("add_contact") } diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt index f59b66d05f..47ffe9dd91 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt @@ -1,5 +1,6 @@ package chat.simplex.app +import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.runtime.Composable import androidx.navigation.NavController import chat.simplex.app.model.ChatModel @@ -7,10 +8,11 @@ import chat.simplex.app.views.TerminalView import chat.simplex.app.views.WelcomeView import chat.simplex.app.views.chatlist.ChatListView +@ExperimentalMaterialApi @Composable fun MainPage(chatModel: ChatModel, nav: NavController) { if (chatModel.currentUser.value == null) WelcomeView(chatModel) { - nav.navigate(Pages.Chats.route) + nav.navigate(Pages.ChatList.route) } else ChatListView(chatModel, nav) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index 35893e8466..502c2fb9fe 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -45,7 +45,6 @@ class SimplexApp: Application() { } companion object { - lateinit var weakActivity: WeakReference init { val socketName = "local.socket.address.listen.native.cmd2" diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index b53b62e839..79dbc612bc 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -10,6 +10,7 @@ class ChatModel(val controller: ChatController) { var chatId = mutableStateOf(null) var chatItems = mutableStateListOf() + var connReqInvitation: String? = null var terminalItems = mutableStateListOf() fun hasChat(id: String): Boolean = chats.firstOrNull() { it.id == id } != null @@ -17,21 +18,20 @@ class ChatModel(val controller: ChatController) { private fun getChatIndex(id: String): Int = chats.indexOfFirst { it.id == id } fun addChat(chat: Chat) = chats.add(index = 0, chat) -// func updateChatInfo(_ cInfo: ChatInfo) { -// if let i = getChatIndex(cInfo.id) { -// chats[i].chatInfo = cInfo -// } -// } -// -// func updateContact(_ contact: Contact) { -// let cInfo = ChatInfo.direct(contact: contact) -// if hasChat(contact.id) { -// updateChatInfo(cInfo) -// } else { -// addChat(Chat(chatInfo: cInfo, chatItems: [])) -// } -// } -// + fun updateChatInfo(cInfo: ChatInfo) { + val i = getChatIndex(cInfo.id) + if (i >= 0) chats[i] = chats[i].copy(chatInfo = cInfo) + } + + fun updateContact(contact: Contact) { + val cInfo = ChatInfo.Direct(contact) + if (hasChat(contact.id)) { + updateChatInfo(cInfo) + } else { + addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf())) + } + } + // func updateNetworkStatus(_ contact: Contact, _ status: Chat.NetworkStatus) { // if let ix = getChatIndex(contact.id) { // chats[ix].serverInfo.networkStatus = status @@ -60,7 +60,7 @@ class ChatModel(val controller: ChatController) { else chat.chatStats ) - chats.set(i, updatedChat) + chats[i] = updatedChat if (i > 0) { popChat_(i) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 5a5b74b4b4..831267325c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -1,9 +1,7 @@ package chat.simplex.app.model import android.util.Log -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.toMutableStateList import chat.simplex.app.chatRecvMsg import chat.simplex.app.chatSendCmd import kotlinx.coroutines.Dispatchers @@ -18,11 +16,7 @@ import kotlin.concurrent.thread typealias ChatCtrl = Long open class ChatController(val ctrl: ChatCtrl) { - var chatModel: ChatModel - - init { - chatModel = ChatModel(this) - } + var chatModel = ChatModel(this) suspend fun startChat(u: User) { chatModel.currentUser = mutableStateOf(u) @@ -70,47 +64,122 @@ open class ChatController(val ctrl: ChatCtrl) { suspend fun apiGetActiveUser(): User? { val r = sendCmd(CC.ShowActiveUser()) if (r is CR.ActiveUser) return r.user - Log.d("SIMPLEX", "apiGetActiveUser: ${r.toString()}") + Log.d("SIMPLEX", "apiGetActiveUser: ${r.responseType} ${r.details}") return null } suspend fun apiCreateActiveUser(p: Profile): User { val r = sendCmd(CC.CreateActiveUser(p)) if (r is CR.ActiveUser) return r.user - Log.d("SIMPLEX", "apiCreateActiveUser: ${r.toString()}") - throw Error("user not created ${r.toString()}") + Log.d("SIMPLEX", "apiCreateActiveUser: ${r.responseType} ${r.details}") + throw Error("user not created ${r.responseType} ${r.details}") } suspend fun apiStartChat() { val r = sendCmd(CC.StartChat()) if (r is CR.ChatStarted ) return - throw Error("failed starting chat: ${r.toString()}") + throw Error("failed starting chat: ${r.responseType} ${r.details}") } suspend fun apiGetChats(): List { val r = sendCmd(CC.ApiGetChats()) if (r is CR.ApiChats ) return r.chats - throw Error("failed getting the list of chats: ${r.toString()}") + throw Error("failed getting the list of chats: ${r.responseType} ${r.details}") } suspend fun apiGetChat(type: ChatType, id: Long): Chat? { val r = sendCmd(CC.ApiGetChat(type, id)) if (r is CR.ApiChat ) return r.chat - Log.d("SIMPLEX", "apiGetChat bad response: ${r.toString()}") + Log.d("SIMPLEX", "apiGetChat bad response: ${r.responseType} ${r.details}") return null } suspend fun apiSendMessage(type: ChatType, id: Long, mc: MsgContent): AChatItem? { val r = sendCmd(CC.ApiSendMessage(type, id, mc)) if (r is CR.NewChatItem ) return r.chatItem - Log.d("SIMPLEX", "apiSendMessage bad response: ${r.toString()}") + Log.d("SIMPLEX", "apiSendMessage bad response: ${r.responseType} ${r.details}") return null } + suspend fun apiAddContact(): String? { + val r = sendCmd(CC.AddContact()) + if (r is CR.Invitation) return r.connReqInvitation + Log.d("SIMPLEX", "apiAddContact bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun apiConnect(connReq: String): Boolean { + val r = sendCmd(CC.Connect(connReq)) + if (r is CR.SentConfirmation || r is CR.SentInvitation) return true + Log.d("SIMPLEX", "apiConnect bad response: ${r.responseType} ${r.details}") + return false + } + + suspend fun apiDeleteChat(type: ChatType, id: Long): Boolean { + val r = sendCmd(CC.ApiDeleteChat(type, id)) + if (r is CR.ContactDeleted) return true // TODO groups + Log.d("SIMPLEX", "apiDeleteChat bad response: ${r.responseType} ${r.details}") + return false + } + + suspend fun apiUpdateProfile(profile: Profile): Profile? { + val r = sendCmd(CC.UpdateProfile(profile)) + if (r is CR.UserProfileNoChange) return profile + if (r is CR.UserProfileUpdated) return r.toProfile + Log.d("SIMPLEX", "apiUpdateProfile bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun apiCreateUserAddress(): String? { + val r = sendCmd(CC.CreateMyAddress()) + if (r is CR.UserContactLinkCreated) return r.connReqContact + Log.d("SIMPLEX", "apiCreateUserAddress bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun apiDeleteUserAddress(): Boolean { + val r = sendCmd(CC.DeleteMyAddress()) + if (r is CR.UserContactLinkDeleted) return true + Log.d("SIMPLEX", "apiDeleteUserAddress bad response: ${r.responseType} ${r.details}") + return false + } + + suspend fun apiGetUserAddress(): String? { + val r = sendCmd(CC.ShowMyAddress()) + if (r is CR.UserContactLink) return r.connReqContact + if (r is CR.ChatCmdError && r.chatError is ChatError.ErrorStore + && r.chatError.storeError is StoreError.UserContactLinkNotFound) { + return null + } + Log.d("SIMPLEX", "apiGetUserAddress bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun apiAcceptContactRequest(contactReqId: Long): Contact? { + val r = sendCmd(CC.ApiAcceptContact(contactReqId)) + if (r is CR.AcceptingContactRequest) return r.contact + Log.d("SIMPLEX", "apiAcceptContactRequest bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun apiRejectContactRequest(contactReqId: Long): Boolean { + val r = sendCmd(CC.ApiRejectContact(contactReqId)) + if (r is CR.ContactRequestRejected) return true + Log.d("SIMPLEX", "apiRejectContactRequest bad response: ${r.responseType} ${r.details}") + return false + } + + suspend fun apiChatRead(type: ChatType, id: Long, range: CC.ItemRange): Boolean { + val r = sendCmd(CC.ApiChatRead(type, id, range)) + if (r is CR.CmdOk) return true + Log.d("SIMPLEX", "apiChatRead bad response: ${r.responseType} ${r.details}") + return false + } + fun processReceivedMsg(r: CR) { chatModel.terminalItems.add(TerminalItem.resp(r)) when { -// r is CR.ContactConnected -> return + r is CR.ContactConnected -> chatModel.updateContact(r.contact) // r is CR.UpdateNetworkStatus -> return // r is CR.ReceivedContactRequest -> return // r is CR.ContactUpdated -> return @@ -125,8 +194,6 @@ open class ChatController(val ctrl: ChatCtrl) { // NtfManager.shared.notifyMessageReceived(cInfo, cItem) // switch res { -// case let .contactConnected(contact): -// chatModel.updateContact(contact) // chatModel.updateNetworkStatus(contact, .connected) // NtfManager.shared.notifyContactConnected(contact) // case let .receivedContactRequest(contactRequest): @@ -206,15 +273,67 @@ abstract class CC { } class ApiGetChat(val type: ChatType, val id: Long): CC() { - override val cmdString get() = "/_get chat ${CC.chatRef(type, id)} count=100" + override val cmdString get() = "/_get chat ${chatRef(type, id)} count=100" override val cmdType get() = "apiGetChat" } class ApiSendMessage(val type: ChatType, val id: Long, val mc: MsgContent): CC() { - override val cmdString get() = "/_send ${CC.chatRef(type, id)} ${mc.cmdString}" + override val cmdString get() = "/_send ${chatRef(type, id)} ${mc.cmdString}" override val cmdType get() = "apiGetChat" } + class AddContact: CC() { + override val cmdString get() = "/connect" + override val cmdType get() = "addContact" + } + + class Connect(val connReq: String): CC() { + override val cmdString get() = "/connect $connReq" + override val cmdType get() = "connect" + } + + class ApiDeleteChat(val type: ChatType, val id: Long): CC() { + override val cmdString get() = "/_delete ${chatRef(type, id)}" + override val cmdType get() = "apiDeleteChat" + } + + class UpdateProfile(val profile: Profile): CC() { + override val cmdString get() = "/profile ${profile.displayName} ${profile.fullName}" + override val cmdType get() = "updateProfile" + } + + class CreateMyAddress: CC() { + override val cmdString get() = "/address" + override val cmdType get() = "createMyAddress" + } + + class DeleteMyAddress: CC() { + override val cmdString get() = "/delete_address" + override val cmdType get() = "deleteMyAddress" + } + + class ShowMyAddress: CC() { + override val cmdString get() = "/show_address" + override val cmdType get() = "showMyAddress" + } + + class ApiAcceptContact(val contactReqId: Long): CC() { + override val cmdString get() = "/_accept $contactReqId" + override val cmdType get() = "apiAcceptContact" + } + + class ApiRejectContact(val contactReqId: Long): CC() { + override val cmdString get() = "/_reject $contactReqId" + override val cmdType get() = "apiRejectContact" + } + + class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC() { + override val cmdString get() = "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}" + override val cmdType get() = "apiDeleteChat" + } + + data class ItemRange(val from: Long, val to: Long) + companion object { fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}" } @@ -261,7 +380,7 @@ sealed class CR { @Serializable @SerialName("chatStarted") class ChatStarted: CR() { override val responseType get() = "chatStarted" - override val details get() = CR.noDetails(this) + override val details get() = noDetails() } @Serializable @SerialName("apiChats") @@ -276,12 +395,126 @@ sealed class CR { override val details get() = chat.toString() } + @Serializable @SerialName("invitation") + class Invitation(val connReqInvitation: String): CR() { + override val responseType get() = "invitation" + override val details get() = connReqInvitation + } + + @Serializable @SerialName("sentConfirmation") + class SentConfirmation: CR() { + override val responseType get() = "sentConfirmation" + override val details get() = noDetails() + } + + @Serializable @SerialName("sentInvitation") + class SentInvitation: CR() { + override val responseType get() = "sentInvitation" + override val details get() = noDetails() + } + + @Serializable @SerialName("contactDeleted") + class ContactDeleted(val contact: Contact): CR() { + override val responseType get() = "contactDeleted" + override val details get() = contact.toString() + } + + @Serializable @SerialName("userProfileNoChange") + class UserProfileNoChange: CR() { + override val responseType get() = "userProfileNoChange" + override val details get() = noDetails() + } + + @Serializable @SerialName("userProfileUpdated") + class UserProfileUpdated(val fromProfile: Profile, val toProfile: Profile): CR() { + override val responseType get() = "userProfileUpdated" + override val details get() = toProfile.toString() + } + + @Serializable @SerialName("userContactLink") + class UserContactLink(val connReqContact: String): CR() { + override val responseType get() = "userContactLink" + override val details get() = connReqContact + } + + @Serializable @SerialName("userContactLinkCreated") + class UserContactLinkCreated(val connReqContact: String): CR() { + override val responseType get() = "userContactLinkCreated" + override val details get() = connReqContact + } + + @Serializable @SerialName("userContactLinkDeleted") + class UserContactLinkDeleted: CR() { + override val responseType get() = "userContactLinkDeleted" + override val details get() = noDetails() + } + + @Serializable @SerialName("contactConnected") + class ContactConnected(val contact: Contact): CR() { + override val responseType get() = "contactConnected" + override val details get() = contact.toString() + } + + @Serializable @SerialName("receivedContactRequest") + class ReceivedContactRequest(val contactRequest: UserContactRequest): CR() { + override val responseType get() = "receivedContactRequest" + override val details get() = contactRequest.toString() + } + + @Serializable @SerialName("acceptingContactRequest") + class AcceptingContactRequest(val contact: Contact): CR() { + override val responseType get() = "acceptingContactRequest" + override val details get() = contact.toString() + } + + @Serializable @SerialName("contactRequestRejected") + class ContactRequestRejected: CR() { + override val responseType get() = "contactRequestRejected" + override val details get() = noDetails() + } + + @Serializable @SerialName("contactUpdated") + class ContactUpdated(val toContact: Contact): CR() { + override val responseType get() = "contactUpdated" + override val details get() = toContact.toString() + } + @Serializable @SerialName("contactSubscribed") class ContactSubscribed(val contact: Contact): CR() { override val responseType get() = "contactSubscribed" override val details get() = contact.toString() } + @Serializable @SerialName("contactDisconnected") + class ContactDisconnected(val contact: Contact): CR() { + override val responseType get() = "contactDisconnected" + override val details get() = contact.toString() + } + + @Serializable @SerialName("contactSubError") + class ContactSubError(val contact: Contact, val chatError: ChatError): CR() { + override val responseType get() = "contactSubError" + override val details get() = "contact:\n${contact.toString()}\nerror:\n${chatError.toString()}" + } + + @Serializable @SerialName("groupSubscribed") + class GroupSubscribed(val group: GroupInfo): CR() { + override val responseType get() = "groupSubscribed" + override val details get() = group.toString() + } + + @Serializable @SerialName("groupEmpty") + class GroupEmpty(val group: GroupInfo): CR() { + override val responseType get() = "groupEmpty" + override val details get() = group.toString() + } + + @Serializable @SerialName("userContactLinkSubscribed") + class UserContactLinkSubscribed: CR() { + override val responseType get() = "userContactLinkSubscribed" + override val details get() = noDetails() + } + @Serializable @SerialName("newChatItem") class NewChatItem(val chatItem: AChatItem): CR() { override val responseType get() = "newChatItem" @@ -294,10 +527,22 @@ sealed class CR { override val details get() = chatItem.toString() } - @Serializable @SerialName("cmdAccepted") - class CmdAccepted(val corr: String): CR() { - override val responseType get() = "cmdAccepted" - override val details get() = "corrId: $corr" + @Serializable @SerialName("cmdOk") + class CmdOk: CR() { + override val responseType get() = "cmdOk" + override val details get() = noDetails() + } + + @Serializable @SerialName("chatCmdError") + class ChatCmdError(val chatError: ChatError): CR() { + override val responseType get() = "chatCmdError" + override val details get() = chatError.toString() + } + + @Serializable @SerialName("chatError") + class ChatRespError(val chatError: ChatError): CR() { + override val responseType get() = "chatError" + override val details get() = chatError.toString() } @Serializable @@ -312,9 +557,7 @@ sealed class CR { override val details get() = str } - companion object { - fun noDetails(r: CR): String ="${r.responseType}: no details" - } + fun noDetails(): String ="${responseType}: no details" } abstract class TerminalItem { @@ -323,25 +566,35 @@ abstract class TerminalItem { abstract val label: String abstract val details: String - class Cmd(id: Long, val cmd: CC): TerminalItem() { - override val id = id + class Cmd(override val id: Long, val cmd: CC): TerminalItem() { override val label get() = "> ${cmd.cmdString}" override val details get() = cmd.cmdString } - class Resp(id: Long, val resp: CR): TerminalItem() { - override val id = id + class Resp(override val id: Long, val resp: CR): TerminalItem() { override val label get() = "< ${resp.responseType}" override val details get() = resp.details } companion object { - val sampleData = listOf( - TerminalItem.Cmd(0, CC.ShowActiveUser()), - TerminalItem.Resp(1, CR.ActiveUser(User.sampleData)) + val sampleData = listOf( + Cmd(0, CC.ShowActiveUser()), + Resp(1, CR.ActiveUser(User.sampleData)) ) - fun cmd(c: CC) = TerminalItem.Cmd(System.currentTimeMillis(), c) - fun resp(r: CR) = TerminalItem.Resp(System.currentTimeMillis(), r) + fun cmd(c: CC) = Cmd(System.currentTimeMillis(), c) + fun resp(r: CR) = Resp(System.currentTimeMillis(), r) } } + +@Serializable +sealed class ChatError { + @Serializable @SerialName("errorStore") + class ErrorStore(val storeError: StoreError): ChatError() +} + +@Serializable +sealed class StoreError { + @Serializable @SerialName("userContactLinkNotFound") + class UserContactLinkNotFound: StoreError() +} \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt index ec2f0b1f12..4e6b8fc805 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Color.kt @@ -7,8 +7,8 @@ val Purple500 = Color(0xFF6200EE) val Purple700 = Color(0xFF3700B3) val Teal200 = Color(0xFF03DAC5) val Gray = Color(0x22222222) -val AppleBlue = Color(0, 136, 255, 255) -val AppleGreen = Color(98, 196, 103, 255) +val SimplexBlue = Color(0, 136, 255, 255) +val SimplexGreen = Color(98, 196, 103, 255) val LightGray = Color(241, 242, 246, 255) val DarkGray = Color(43, 44, 46, 255) val HighOrLowlight = Color(134, 135, 139, 255) diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt index d486720301..ef4d3784f1 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt @@ -5,11 +5,10 @@ import androidx.compose.material.MaterialTheme import androidx.compose.material.darkColors import androidx.compose.material.lightColors import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color private val DarkColorPalette = darkColors( - primary = AppleBlue, - primaryVariant = AppleGreen, + primary = SimplexBlue, + primaryVariant = SimplexGreen, secondary = DarkGray, /* background: Color = Color(0xFF121212), @@ -24,8 +23,8 @@ private val DarkColorPalette = darkColors( ) private val LightColorPalette = lightColors( - primary = AppleBlue, - primaryVariant = AppleGreen, + primary = SimplexBlue, + primaryVariant = SimplexGreen, secondary = LightGray, /* Other default colors to override surface = Color.White, diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt index bb89f6e849..f03afbd0ff 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt @@ -8,21 +8,29 @@ import androidx.compose.ui.unit.sp // Set of Material typography styles to start with val Typography = Typography( + h1 = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 32.sp, + ), + h2 = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 20.sp + ), body1 = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp + ), + button = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + ), + caption = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 14.sp ) - /* Other default text styles to override - button = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.W500, - fontSize = 14.sp - ), - caption = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 12.sp - ) - */ ) \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index 891903155d..5a73363cf3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -23,6 +23,7 @@ import chat.simplex.app.model.ChatModel import chat.simplex.app.model.TerminalItem import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.SendMsgView +import chat.simplex.app.views.helpers.withApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -36,12 +37,10 @@ fun TerminalView(chatModel: ChatModel, navController: NavController) { } TerminalLog(chatModel.terminalItems, navController) SendMsgView(sendMessage = { cmd -> - GlobalScope.launch { - withContext(Dispatchers.Main) { - // show "in progress" - chatModel.controller.sendCmd(CC.Console(cmd)) - // hide "in progress" - } + withApi { + // show "in progress" + chatModel.controller.sendCmd(CC.Console(cmd)) + // hide "in progress" } }) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index 8a08347af0..bead9f11c6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -1,14 +1,12 @@ package chat.simplex.app.views.chatlist +import androidx.compose.foundation.clickable import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.Button -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.toMutableStateList +import androidx.compose.material.* +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -25,27 +23,58 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* import androidx.compose.ui.Alignment import androidx.compose.ui.text.font.FontWeight +import chat.simplex.app.views.newchat.NewChatSheet +import kotlinx.coroutines.* + +@ExperimentalMaterialApi +class ScaffoldController(val state: BottomSheetScaffoldState, val scope: CoroutineScope) { + fun expand() = scope.launch { state.bottomSheetState.expand() } + fun collapse() = scope.launch { state.bottomSheetState.collapse() } + fun toggle() = scope.launch { + val s = state.bottomSheetState + if (s.isExpanded) s.collapse() else s.expand() + } +} + +@ExperimentalMaterialApi @Composable -fun ChatListView(chatModel: ChatModel, navController: NavController) { +fun scaffoldController(): ScaffoldController { + return ScaffoldController( + state = rememberBottomSheetScaffoldState(), + scope = rememberCoroutineScope() + ) +} + +@ExperimentalMaterialApi +@Composable +fun ChatListView(chatModel: ChatModel, nav: NavController) { + val newChatCtrl = scaffoldController() + BottomSheetScaffold( + scaffoldState = newChatCtrl.state, + sheetPeekHeight = 0.dp, + topBar = { ChatListToolbar(newChatCtrl) }, + sheetContent = { NewChatSheet(chatModel, newChatCtrl, nav) } + ) { Column(modifier = Modifier .padding(vertical = 8.dp) .fillMaxSize() .background(MaterialTheme.colors.background) ) { - ChatListToolbar() - ChatList(chatModel, navController) + ChatList(chatModel, nav) Button( - onClick = { navController.navigate(Pages.Terminal.route) }, + onClick = { nav.navigate(Pages.Terminal.route) }, modifier = Modifier.padding(14.dp) ) { Text("Terminal") } } + } } +@ExperimentalMaterialApi @Composable -fun ChatListToolbar() { +fun ChatListToolbar(newChatSheetCtrl: ScaffoldController) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, @@ -60,7 +89,7 @@ fun ChatListToolbar() { ) Text( "Your chats", - color=MaterialTheme.colors.onBackground, + color = MaterialTheme.colors.onBackground, fontWeight = FontWeight.Bold, modifier = Modifier.padding(5.dp) ) @@ -69,6 +98,7 @@ fun ChatListToolbar() { "Add Contact", tint = MaterialTheme.colors.primary, modifier = Modifier.padding(horizontal = 10.dp) + .clickable { newChatSheetCtrl.toggle() } ) } } @@ -104,7 +134,7 @@ fun ChatList(chatModel: ChatModel, navController: NavController) { //@Preview //@Composable -//fun PreviewSendMsgView() { +//fun PreviewChatListView() { // SimpleXTheme { // ChatListView( // chats = listOf( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index a96155665e..5b870cba81 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -29,8 +29,6 @@ import java.time.LocalDateTime import java.time.Instant as JavaInstant import java.util.* - - fun getDisplayTime(t: Instant) : String { val timeFormatter = SimpleDateFormat("HH:mm", Locale.getDefault()) val dateFormatter = SimpleDateFormat("dd/MM/y", Locale.getDefault()) @@ -50,8 +48,8 @@ fun getDisplayTime(t: Instant) : String { @Composable fun ChatPreviewView(chat: Chat, goToChat: () -> Unit) { Surface( - border=BorderStroke(0.5.dp, MaterialTheme.colors.secondary), - modifier= Modifier + border = BorderStroke(0.5.dp, MaterialTheme.colors.secondary), + modifier = Modifier .fillMaxWidth() .clickable(onClick = goToChat) .height(80.dp) @@ -59,7 +57,7 @@ fun ChatPreviewView(chat: Chat, goToChat: () -> Unit) { Row( modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp), ) { - Column(verticalArrangement=Arrangement.Center, modifier=Modifier.fillMaxHeight()) { + Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) { Icon( Icons.Filled.Person, contentDescription = "Avatar Placeholder", @@ -73,7 +71,7 @@ fun ChatPreviewView(chat: Chat, goToChat: () -> Unit) { } Spacer(modifier = Modifier.width(6.dp)) Column(modifier = Modifier.padding(all = 8.dp)) { - Row(horizontalArrangement=Arrangement.SpaceBetween, modifier=Modifier.fillMaxWidth()) { + Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { Text(chat.chatInfo.chatViewName, fontWeight = FontWeight.Bold) ( if (chat.chatItems.count() > 0) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt new file mode 100644 index 0000000000..0c3afe9282 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt @@ -0,0 +1,42 @@ +package chat.simplex.app.views.helpers + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Close +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.newchat.AddContactLayout + +@Composable +fun CloseSheetBar(close: () -> Unit) { + Row (Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + Icon( + Icons.Outlined.Close, + "Close button", + tint = MaterialTheme.colors.primary, + modifier = Modifier + .padding(horizontal = 10.dp) + .clickable { close() } + ) + } +} + +@Preview +@Composable +fun PreviewCloseSheetBar() { + SimpleXTheme { + CloseSheetBar(close = {}) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt new file mode 100644 index 0000000000..7f04d4401a --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt @@ -0,0 +1,44 @@ +package chat.simplex.app.ui.theme + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material.icons.outlined.Share +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.views.helpers.CloseSheetBar + +@Composable +fun SimpleButton(text: String, icon: ImageVector, click: () -> Unit) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clickable { click() } + ) { + Icon(icon, text, + tint = MaterialTheme.colors.primary, + modifier = Modifier.padding(horizontal = 10.dp) + ) + Text(text, + style = MaterialTheme.typography.button, + color = MaterialTheme.colors.primary + ) + } +} + +@Preview +@Composable +fun PreviewCloseSheetBar() { + SimpleXTheme { + SimpleButton(text = "Share", icon = Icons.Outlined.Share, click = {}) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt new file mode 100644 index 0000000000..ea38d0a509 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -0,0 +1,10 @@ +package chat.simplex.app.views.helpers + +import kotlinx.coroutines.* + +@DelicateCoroutinesApi +fun withApi(action: suspend CoroutineScope.() -> Unit): Job = + GlobalScope.launch { + withContext(Dispatchers.Main) { action() } + } + diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt new file mode 100644 index 0000000000..0714264bde --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt @@ -0,0 +1,92 @@ +package chat.simplex.app.views.newchat + +import android.content.Context +import android.content.Intent +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Share +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import chat.simplex.app.Pages +import chat.simplex.app.model.ChatModel +import chat.simplex.app.ui.theme.SimpleButton +import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.helpers.CloseSheetBar + +@Composable +fun AddContactView(chatModel: ChatModel, nav: NavController) { + val connReq = chatModel.connReqInvitation + if (connReq != null) { + val cxt = LocalContext.current + AddContactLayout( + connReq = connReq, + close = { nav.popBackStack() }, + share = { shareText(cxt, connReq) } + ) + } +} + +@Composable +fun AddContactLayout(connReq: String, close: () -> Unit, share: () -> Unit) { + Column( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxSize() + .background(MaterialTheme.colors.background), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CloseSheetBar(close) + Text( + "Add contact", + style = MaterialTheme.typography.h1, + modifier = Modifier.padding(bottom = 8.dp) + ) + Text( + "Show QR code to your contact\nto scan from the app", + style = MaterialTheme.typography.h2, + textAlign = TextAlign.Center + ) + QRCode(connReq) + Text( + "If you can't show QR code, you can share the invitation link via any channel.\nIt does not have to be secure or private, you just have to know who you are sharing it with.", + textAlign = TextAlign.Center, + style = MaterialTheme.typography.caption, + modifier = Modifier.padding(bottom = 16.dp).padding(horizontal = 16.dp) + ) + SimpleButton("Share invitation link", icon = Icons.Outlined.Share, click = share) + } +} + +fun shareText(cxt: Context, text: String) { + val sendIntent: Intent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, text) + type = "text/plain" + } + val shareIntent = Intent.createChooser(sendIntent, null) + cxt.startActivity(shareIntent) +} + +@Preview +@Composable +fun PreviewAddContactView() { + SimpleXTheme { + AddContactLayout( + connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", + close = {}, + share = {} + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt new file mode 100644 index 0000000000..4d74ff895b --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt @@ -0,0 +1,66 @@ +package chat.simplex.app.views.newchat + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Button +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.runtime.Composable +import androidx.compose.material.Text +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import chat.simplex.app.Pages +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.chatlist.ScaffoldController +import chat.simplex.app.views.helpers.withApi + +@ExperimentalMaterialApi +@Composable +fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController, nav: NavController) { + NewChatSheetLayout( + addContact = { + withApi { +// show spinner + chatModel.connReqInvitation = chatModel.controller.apiAddContact() +// hide spinner + if (chatModel.connReqInvitation != null) { + newChatCtrl.collapse() + nav.navigate(Pages.AddContact.route) + } + } + }, + scanCode = {}, + close = { + newChatCtrl.collapse() + } + ) +} + +@Composable +fun NewChatSheetLayout(addContact: () -> Unit, scanCode: () -> Unit, close: () -> Unit) { + Column ( Modifier.padding(all = 8.dp) ) { + Button(onClick = addContact) { + Text("Add contact") + } + Button(onClick = scanCode) { + Text("Scan QR code") + } + Button(onClick = close) { + Text("Cancel") + } + } +} + +@Preview +@Composable +fun PreviewNewChatSheet() { + SimpleXTheme { + NewChatSheetLayout( + addContact = {}, + scanCode = {}, + close = {}, + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt new file mode 100644 index 0000000000..0a0fc2a2e8 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt @@ -0,0 +1,46 @@ +package chat.simplex.app.views.newchat + +import android.graphics.Bitmap +import android.graphics.Color +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.toSize +import chat.simplex.app.ui.theme.SimpleXTheme +import com.google.zxing.BarcodeFormat +import com.google.zxing.EncodeHintType +import com.google.zxing.qrcode.QRCodeWriter + +@Composable +fun QRCode(connReq: String) { + Image( + bitmap = qrCodeBitmap(connReq, 1024).asImageBitmap(), + contentDescription = "QR Code" + ) +} + +fun qrCodeBitmap(content: String, size: Int): Bitmap { + val hints = hashMapOf().also { it[EncodeHintType.MARGIN] = 1 } + val bits = QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, size, size, hints) + return Bitmap.createBitmap(size, size, Bitmap.Config.RGB_565).also { + for (x in 0 until size) { + for (y in 0 until size) { + it.setPixel(x, y, if (bits[x, y]) Color.BLACK else Color.WHITE) + } + } + } +} + +@Preview +@Composable +fun PreviewQRCode() { + SimpleXTheme { + QRCode(connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D") + } +} From f57a7009a3bb43a19f1d188125352cad434b5e09 Mon Sep 17 00:00:00 2001 From: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> Date: Sat, 19 Feb 2022 17:22:52 +0400 Subject: [PATCH 30/46] chat view layout (#335) Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/model/ChatModel.kt | 16 ++- .../chat/simplex/app/views/chat/ChatView.kt | 134 +++++++++++++----- .../simplex/app/views/chat/SendMsgView.kt | 50 ++++--- .../simplex/app/views/chat/item/CIMetaView.kt | 29 ++++ .../app/views/chat/item/ChatItemView.kt | 40 +++++- .../app/views/chat/item/TextItemView.kt | 71 +++++++++- 6 files changed, 280 insertions(+), 60 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 79dbc612bc..8c64d423f0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -1,8 +1,10 @@ package chat.simplex.app.model +import android.annotation.SuppressLint import androidx.compose.runtime.* import kotlinx.datetime.* import kotlinx.serialization.* +import java.time.format.DateTimeFormatter class ChatModel(val controller: ChatController) { var currentUser = mutableStateOf(null) @@ -438,7 +440,7 @@ class ChatItem ( val content: CIContent ) { val id: Long get() = meta.itemId - // val timestampText: String get() = meta.timestampText + val timestampText: String get() = meta.timestampText val isRcvNew: Boolean get() = meta.itemStatus is CIStatus.RcvNew companion object { @@ -484,7 +486,7 @@ class CIMeta ( val itemStatus: CIStatus, val createdAt: Instant ) { -// val timestampText: String get() = getTimestampText(itemTs) + val timestampText: String get() = getTimestampText(itemTs) companion object { fun getSample(id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew()): CIMeta = @@ -498,8 +500,14 @@ class CIMeta ( } } -// TODO -fun getTimestampText(d: Instant): String = "" +// TODO use old api? +// TODO date for older timestamps +@SuppressLint("NewApi") +fun getTimestampText(d: Instant): String { + val dt = d.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime() + val formatter = DateTimeFormatter.ofPattern("HH:mm") + return dt.format(formatter) +} @Serializable sealed class CIStatus { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index cf4cea564b..72111b9d5b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -1,55 +1,87 @@ package chat.simplex.app.views.chat -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.Button -import androidx.compose.material.Text +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.material.Icon +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.navigation.NavController import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.item.ChatItemView -import chat.simplex.app.views.chatlist.ChatPreviewView -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import chat.simplex.app.views.helpers.withApi +import kotlinx.datetime.Clock @Composable fun ChatView(chatModel: ChatModel, nav: NavController) { if (chatModel.chatId.value != null && chatModel.chats.count() > 0) { val chat: Chat = chatModel.chats.first { chat -> chat.chatInfo.id == chatModel.chatId.value } - Column { - ChatInfoToolbar(chat, nav) - ChatItemsList(chatModel.chatItems) - SendMsgView(sendMessage = { msg -> - GlobalScope.launch { - withContext(Dispatchers.Main) { - // show "in progress" - val cInfo = chat.chatInfo - val newItem = chatModel.controller.apiSendMessage( - type = cInfo.chatType, - id = cInfo.apiId, - mc = MsgContent.MCText(msg) - ) - // hide "in progress" - if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem) - } - } - }) + ChatLayout(chat, chatModel.chatItems, back = { nav.popBackStack() }, sendMessage = { msg -> + withApi { + // show "in progress" + val cInfo = chat.chatInfo + val newItem = chatModel.controller.apiSendMessage( + type = cInfo.chatType, + id = cInfo.apiId, + mc = MsgContent.MCText(msg) + ) + // hide "in progress" + if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem) + } + }) + } +} + +@Composable +fun ChatLayout( + chat: Chat, chatItems: List, + back: () -> Unit, + sendMessage: (String) -> Unit +) { + Scaffold( + topBar = { ChatInfoToolbar(chat, back) }, + bottomBar = { SendMsgView(sendMessage) } + ) { contentPadding -> + Box( + modifier = Modifier + .padding(contentPadding) + .fillMaxWidth() + .background(MaterialTheme.colors.background) + ) { + ChatItemsList(chatItems) } } } @Composable -fun ChatInfoToolbar(chat: Chat, nav: NavController) { - Row { - Button(onClick = { nav.popBackStack() }) { - Text("Back") - } - Column { +fun ChatInfoToolbar(chat: Chat, back: () -> Unit) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterStart + ) { + Icon( + Icons.Outlined.ArrowBack, + "Back", + tint = MaterialTheme.colors.primary, + modifier = Modifier + .clickable(onClick = back) + .padding(start = 16.dp) + ) + Column( + Modifier + .padding(horizontal = 40.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { Text(chat.chatInfo.displayName) Text(chat.chatInfo.fullName) } @@ -64,3 +96,37 @@ fun ChatItemsList(chatItems: List) { } } } + +@Preview +@Composable +fun PreviewChatViewLayout() { + SimpleXTheme { + val chatItems = listOf( + ChatItem.getSampleData( + 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" + ), + ChatItem.getSampleData( + 2, CIDirection.DirectRcv(), Clock.System.now(), "hello" + ), + ChatItem.getSampleData( + 3, CIDirection.DirectSnd(), Clock.System.now(), "hello" + ), + ChatItem.getSampleData( + 4, CIDirection.DirectSnd(), Clock.System.now(), "hello" + ), + ChatItem.getSampleData( + 5, CIDirection.DirectRcv(), Clock.System.now(), "hello" + ) + ) + ChatLayout( + chat = Chat( + chatInfo = ChatInfo.Direct.sampleData, + chatItems = chatItems, + chatStats = Chat.ChatStats() + ), + chatItems = chatItems, + back = {}, + sendMessage = {} + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt index 0573b30e7d..8c51172d82 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/SendMsgView.kt @@ -1,31 +1,47 @@ package chat.simplex.app.views.chat +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.material.Button -import androidx.compose.material.Text -import androidx.compose.material.TextField +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowUpward import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import chat.simplex.app.ui.theme.SimpleXTheme @Composable -fun SendMsgView (sendMessage: (String) -> Unit) { +fun SendMsgView(sendMessage: (String) -> Unit) { var cmd by remember { mutableStateOf("") } - Row { - TextField(value = cmd, onValueChange = { cmd = it }, modifier = Modifier.height(60.dp)) - Spacer(Modifier.height(10.dp)) - Button( - onClick = { - sendMessage(cmd) - cmd = "" - }, - modifier = Modifier.width(40.dp).height(60.dp), - enabled = cmd.isNotEmpty() - ) { - Text("Go") - } + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterEnd + ) { + TextField( + value = cmd, + onValueChange = { cmd = it }, + modifier = Modifier + .height(60.dp) + .fillMaxWidth() + .padding(end = 40.dp) + .background(MaterialTheme.colors.background) + ) + val tint = if (cmd.isNotEmpty()) MaterialTheme.colors.primary else Color.Gray + Icon( + Icons.Outlined.ArrowUpward, + "Send Message", + tint = tint, + modifier = Modifier + .width(40.dp) + .clickable { + sendMessage(cmd) + cmd = "" + } + ) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt new file mode 100644 index 0000000000..73fa80ec01 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt @@ -0,0 +1,29 @@ +package chat.simplex.app.views.chat.item + +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import chat.simplex.app.model.CIDirection +import chat.simplex.app.model.ChatItem +import kotlinx.datetime.* + +@Composable +fun CIMetaView(chatItem: ChatItem) { + Text( + chatItem.timestampText, + color = Color.Gray, + style = MaterialTheme.typography.caption + ) +} + +@Preview +@Composable +fun PreviewCIMetaView() { + CIMetaView( + chatItem = ChatItem.getSampleData( + 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" + ) + ) +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 1aba1845fc..9ad9f16ad4 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -1,10 +1,44 @@ package chat.simplex.app.views.chat.item -import androidx.compose.material.Text +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import chat.simplex.app.model.ChatItem +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.SimpleXTheme +import kotlinx.datetime.Clock @Composable fun ChatItemView(chatItem: ChatItem) { - Text(chatItem.content.text) + val sent = chatItem.chatDir.sent + val alignment = if (sent) Alignment.CenterEnd else Alignment.CenterStart + + Box( + modifier = Modifier + .padding(bottom = 4.dp) + .fillMaxWidth() + .padding( + start = if (sent) 60.dp else 16.dp, + end = if (sent) 16.dp else 60.dp, + ), + contentAlignment = alignment, + ) { + TextItemView(chatItem) + } +} + +@Preview +@Composable +fun PreviewChatItemView() { + SimpleXTheme { + ChatItemView( + chatItem = ChatItem.getSampleData( + 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" + ) + ) + } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt index 22bbdbb005..6faafba256 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt @@ -1,10 +1,77 @@ package chat.simplex.app.views.chat.item +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable -import chat.simplex.app.model.ChatItem +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.SimpleXTheme +import kotlinx.datetime.* + +// TODO move to theme +val SentColorLight = Color(0x1E45B8FF) +val ReceivedColorLight = Color(0x1EF1F0F5) @Composable fun TextItemView(chatItem: ChatItem) { - Text("TextItemView") + val sent = chatItem.chatDir.sent + val color = if (sent) SentColorLight else ReceivedColorLight + + Surface( + shape = RoundedCornerShape(18.dp), + color = color + ) { + Box( + modifier = Modifier.padding(vertical = 6.dp, horizontal = 12.dp) + ) { + Column { + Text(text = chatItem.content.text) + CIMetaView(chatItem) + } + } + } +} + +@Preview +@Composable +fun PreviewTextItemViewSnd() { + SimpleXTheme { + TextItemView( + chatItem = ChatItem.getSampleData( + 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" + ) + ) + } +} + +@Preview +@Composable +fun PreviewTextItemViewRcv() { + SimpleXTheme { + TextItemView( + chatItem = ChatItem.getSampleData( + 1, CIDirection.DirectRcv(), Clock.System.now(), "hello" + ) + ) + } +} + +@Preview +@Composable +fun PreviewTextItemViewLong() { + SimpleXTheme { + TextItemView( + chatItem = ChatItem.getSampleData( + 1, + CIDirection.DirectSnd(), + Clock.System.now(), + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." + ) + ) + } } From d48d4ed8f90a2623884f377390cbe6470b19a54c Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sat, 19 Feb 2022 22:22:07 +0000 Subject: [PATCH 31/46] android app: connect via QR code (#338) * connecting via QR code works * add contact/scan qr code pages * new chat sheet layout * remove unused imports and some warnings --- apps/android/app/build.gradle | 17 ++- apps/android/app/src/main/AndroidManifest.xml | 2 + .../java/chat/simplex/app/MainActivity.kt | 9 +- .../main/java/chat/simplex/app/MainPage.kt | 2 + .../java/chat/simplex/app/ui/theme/Type.kt | 9 +- .../chat/simplex/app/views/TerminalView.kt | 12 +- .../chat/simplex/app/views/WelcomeView.kt | 23 ++-- .../chat/simplex/app/views/chat/ChatView.kt | 2 + .../app/views/chat/item/TextItemView.kt | 4 +- .../app/views/chatlist/ChatListView.kt | 72 +++++++----- .../app/views/helpers/CloseSheetBar.kt | 11 +- .../simplex/app/views/helpers/SimpleButton.kt | 2 +- .../chat/simplex/app/views/helpers/Util.kt | 5 +- .../app/views/newchat/AddContactView.kt | 22 ++-- .../app/views/newchat/ConnectContactView.kt | 90 ++++++++++++++ .../simplex/app/views/newchat/NewChatSheet.kt | 93 ++++++++++++--- .../chat/simplex/app/views/newchat/QRCode.kt | 6 - .../app/views/newchat/QRCodeScanner.kt | 110 ++++++++++++++++++ 18 files changed, 392 insertions(+), 99 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectContactView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt diff --git a/apps/android/app/build.gradle b/apps/android/app/build.gradle index 368a46794a..b5dc59627c 100644 --- a/apps/android/app/build.gradle +++ b/apps/android/app/build.gradle @@ -65,16 +65,27 @@ dependencies { implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.material:material:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' - implementation 'androidx.activity:activity-compose:1.3.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' + implementation 'androidx.activity:activity-compose:1.4.0' implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.3.2' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2' implementation "androidx.compose.material:material-icons-extended:$compose_version" + implementation "androidx.navigation:navigation-compose:2.4.1" + + def camerax_version = "1.1.0-beta01" + implementation "androidx.camera:camera-core:${camerax_version}" + implementation "androidx.camera:camera-camera2:${camerax_version}" + implementation "androidx.camera:camera-lifecycle:${camerax_version}" + implementation "androidx.camera:camera-view:${camerax_version}" + //Barcode implementation 'com.google.zxing:core:3.4.0' + implementation 'com.google.mlkit:barcode-scanning:17.0.2' + //Camera Permission + implementation "com.google.accompanist:accompanist-permissions:0.19.0" + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" - implementation "androidx.navigation:navigation-compose:2.4.1" } diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml index b0fdea62da..7a0ce24bb8 100644 --- a/apps/android/app/src/main/AndroidManifest.xml +++ b/apps/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 15bfceb682..76468bd013 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -19,8 +19,10 @@ import chat.simplex.app.views.* import chat.simplex.app.views.chat.ChatView import chat.simplex.app.views.chatlist.* import chat.simplex.app.views.newchat.AddContactView +import chat.simplex.app.views.newchat.ConnectContactView +import com.google.accompanist.permissions.ExperimentalPermissionsApi - +@ExperimentalPermissionsApi @ExperimentalMaterialApi class MainActivity: ComponentActivity() { private val viewModel by viewModels() @@ -39,6 +41,7 @@ class SimplexViewModel(application: Application) : AndroidViewModel(application) val chatModel = getApplication().chatModel } +@ExperimentalPermissionsApi @ExperimentalMaterialApi @Composable fun Navigation(chatModel: ChatModel) { @@ -64,6 +67,9 @@ fun Navigation(chatModel: ChatModel) { composable(route = Pages.AddContact.route) { AddContactView(chatModel, nav) } + composable(route = Pages.Connect.route) { + ConnectContactView(chatModel, nav) + } composable(route = Pages.Terminal.route) { TerminalView(chatModel, navController = nav) } @@ -86,4 +92,5 @@ sealed class Pages(val route: String) { object ChatList: Pages("chats") object Chat: Pages("chat") object AddContact: Pages("add_contact") + object Connect: Pages("connect") } diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt index 47ffe9dd91..f42208b2a0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt @@ -7,7 +7,9 @@ import chat.simplex.app.model.ChatModel import chat.simplex.app.views.TerminalView import chat.simplex.app.views.WelcomeView import chat.simplex.app.views.chatlist.ChatListView +import com.google.accompanist.permissions.ExperimentalPermissionsApi +@ExperimentalPermissionsApi @ExperimentalMaterialApi @Composable fun MainPage(chatModel: ChatModel, nav: NavController) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt index f03afbd0ff..10408878a6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Type.kt @@ -16,13 +16,18 @@ val Typography = Typography( h2 = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, - fontSize = 20.sp + fontSize = 24.sp ), body1 = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp ), + body2 = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 14.sp + ), button = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, @@ -31,6 +36,6 @@ val Typography = Typography( caption = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, - fontSize = 14.sp + fontSize = 20.sp ) ) \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index 5a73363cf3..d506754675 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -13,22 +13,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import androidx.navigation.* -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController import chat.simplex.app.model.* -import androidx.navigation.navArgument -import chat.simplex.app.Pages import chat.simplex.app.model.ChatModel import chat.simplex.app.model.TerminalItem -import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.SendMsgView import chat.simplex.app.views.helpers.withApi -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.DelicateCoroutinesApi +@DelicateCoroutinesApi @Composable fun TerminalView(chatModel: ChatModel, navController: NavController) { Column { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt index d2d92f27e8..3ac36a12b3 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt @@ -13,14 +13,12 @@ import androidx.compose.material.TextField import androidx.compose.material.Button import androidx.compose.ui.unit.dp import androidx.compose.ui.Modifier -import chat.simplex.app.SimplexViewModel import chat.simplex.app.model.ChatModel import chat.simplex.app.model.Profile -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import chat.simplex.app.views.helpers.withApi +import kotlinx.coroutines.DelicateCoroutinesApi +@DelicateCoroutinesApi @Composable fun WelcomeView(chatModel: ChatModel, routeHome: () -> Unit) { Column( @@ -38,6 +36,7 @@ fun WelcomeView(chatModel: ChatModel, routeHome: () -> Unit) { } } +@DelicateCoroutinesApi @Composable fun CreateProfilePanel(chatModel: ChatModel, routeHome: () -> Unit) { var displayName by remember { mutableStateOf("") } @@ -51,14 +50,12 @@ fun CreateProfilePanel(chatModel: ChatModel, routeHome: () -> Unit) { Text("Full Name (Optional)") TextField(value = fullName, onValueChange = { fullName = it }) Button(onClick={ - GlobalScope.launch { - withContext(Dispatchers.Main) { - val user = chatModel.controller.apiCreateActiveUser( - Profile(displayName, fullName) - ) - chatModel.controller.startChat(user) - routeHome() - } + withApi { + val user = chatModel.controller.apiCreateActiveUser( + Profile(displayName, fullName) + ) + chatModel.controller.startChat(user) + routeHome() } }, enabled = displayName.isNotEmpty() diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 72111b9d5b..35a537bd31 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -19,8 +19,10 @@ import chat.simplex.app.model.* import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.item.ChatItemView import chat.simplex.app.views.helpers.withApi +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.datetime.Clock +@DelicateCoroutinesApi @Composable fun ChatView(chatModel: ChatModel, nav: NavController) { if (chatModel.chatId.value != null && chatModel.chats.count() > 0) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt index 6faafba256..6974c34b14 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt @@ -20,11 +20,9 @@ val ReceivedColorLight = Color(0x1EF1F0F5) @Composable fun TextItemView(chatItem: ChatItem) { val sent = chatItem.chatDir.sent - val color = if (sent) SentColorLight else ReceivedColorLight - Surface( shape = RoundedCornerShape(18.dp), - color = color + color = if (sent) SentColorLight else ReceivedColorLight ) { Box( modifier = Modifier.padding(vertical = 6.dp, horizontal = 12.dp) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index bead9f11c6..cd6466c468 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -5,25 +5,25 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController import chat.simplex.app.Pages import chat.simplex.app.model.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight +import chat.simplex.app.views.helpers.withApi import chat.simplex.app.views.newchat.NewChatSheet +import com.google.accompanist.permissions.ExperimentalPermissionsApi import kotlinx.coroutines.* @@ -46,6 +46,8 @@ fun scaffoldController(): ScaffoldController { ) } +@DelicateCoroutinesApi +@ExperimentalPermissionsApi @ExperimentalMaterialApi @Composable fun ChatListView(chatModel: ChatModel, nav: NavController) { @@ -53,20 +55,31 @@ fun ChatListView(chatModel: ChatModel, nav: NavController) { BottomSheetScaffold( scaffoldState = newChatCtrl.state, sheetPeekHeight = 0.dp, - topBar = { ChatListToolbar(newChatCtrl) }, - sheetContent = { NewChatSheet(chatModel, newChatCtrl, nav) } + sheetContent = { NewChatSheet(chatModel, newChatCtrl, nav) }, + sheetShape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp), ) { - Column(modifier = Modifier - .padding(vertical = 8.dp) - .fillMaxSize() - .background(MaterialTheme.colors.background) - ) { - ChatList(chatModel, nav) - Button( - onClick = { nav.navigate(Pages.Terminal.route) }, - modifier = Modifier.padding(14.dp) + Box { + Column( + modifier = Modifier + .padding(vertical = 8.dp) + .fillMaxSize() + .background(MaterialTheme.colors.background) ) { - Text("Terminal") + ChatListToolbar(newChatCtrl) + ChatList(chatModel, nav) + Button( + onClick = { nav.navigate(Pages.Terminal.route) }, + modifier = Modifier.padding(14.dp) + ) { + Text("Terminal") + } + } + if (newChatCtrl.state.bottomSheetState.isExpanded) { + Surface(Modifier + .fillMaxSize() + .clickable { newChatCtrl.collapse() }, + color = Color.Black.copy(alpha = 0.12F) + ) {} } } } @@ -97,29 +110,30 @@ fun ChatListToolbar(newChatSheetCtrl: ScaffoldController) { Icons.Outlined.PersonAdd, "Add Contact", tint = MaterialTheme.colors.primary, - modifier = Modifier.padding(horizontal = 10.dp) + modifier = Modifier + .padding(horizontal = 10.dp) .clickable { newChatSheetCtrl.toggle() } ) } } -fun goToChat(chat: Chat, chatModel: ChatModel, navController: NavController) { - GlobalScope.launch { - withContext(Dispatchers.Main) { - val cInfo = chat.chatInfo - val chat = chatModel.controller.apiGetChat(cInfo.chatType, cInfo.apiId) - if (chat != null ) { - chatModel.chatId = mutableStateOf(cInfo.id) - chatModel.chatItems = chat.chatItems.toMutableStateList() - navController.navigate(Pages.Chat.route) - } else { - // TODO show error? or will apiGetChat show it - } +@DelicateCoroutinesApi +fun goToChat(chatPreview: Chat, chatModel: ChatModel, navController: NavController) { + withApi { + val cInfo = chatPreview.chatInfo + val chat = chatModel.controller.apiGetChat(cInfo.chatType, cInfo.apiId) + if (chat != null ) { + chatModel.chatId = mutableStateOf(cInfo.id) + chatModel.chatItems = chat.chatItems.toMutableStateList() + navController.navigate(Pages.Chat.route) + } else { + // TODO show error? or will apiGetChat show it } } } +@DelicateCoroutinesApi @Composable fun ChatList(chatModel: ChatModel, navController: NavController) { LazyColumn( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt index 0c3afe9282..c323f4317b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt @@ -18,16 +18,19 @@ import chat.simplex.app.views.newchat.AddContactLayout @Composable fun CloseSheetBar(close: () -> Unit) { - Row (Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.End + Row ( + Modifier + .fillMaxWidth() + .height(40.dp), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically ) { Icon( Icons.Outlined.Close, "Close button", tint = MaterialTheme.colors.primary, modifier = Modifier - .padding(horizontal = 10.dp) + .padding(vertical = 10.dp) .clickable { close() } ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt index 7f04d4401a..056dfe626e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt @@ -29,7 +29,7 @@ fun SimpleButton(text: String, icon: ImageVector, click: () -> Unit) { modifier = Modifier.padding(horizontal = 10.dp) ) Text(text, - style = MaterialTheme.typography.button, + style = MaterialTheme.typography.caption, color = MaterialTheme.colors.primary ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt index ea38d0a509..a8f5ec9dd4 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/Util.kt @@ -4,7 +4,4 @@ import kotlinx.coroutines.* @DelicateCoroutinesApi fun withApi(action: suspend CoroutineScope.() -> Unit): Job = - GlobalScope.launch { - withContext(Dispatchers.Main) { action() } - } - + GlobalScope.launch { withContext(Dispatchers.Main, action) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt index 0714264bde..7066024bf2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/AddContactView.kt @@ -3,11 +3,8 @@ package chat.simplex.app.views.newchat import android.content.Context import android.content.Intent import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.material.Button import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Share @@ -15,11 +12,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.* +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import chat.simplex.app.Pages import chat.simplex.app.model.ChatModel import chat.simplex.app.ui.theme.SimpleButton import chat.simplex.app.ui.theme.SimpleXTheme @@ -56,14 +54,24 @@ fun AddContactLayout(connReq: String, close: () -> Unit, share: () -> Unit) { Text( "Show QR code to your contact\nto scan from the app", style = MaterialTheme.typography.h2, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 8.dp) ) QRCode(connReq) Text( - "If you can't show QR code, you can share the invitation link via any channel.\nIt does not have to be secure or private, you just have to know who you are sharing it with.", + buildAnnotatedString { + append("If you cannot meet in person, you can ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append("scan QR code in the video call") + } + append(", or you can share the invitation link via any other channel.") + }, textAlign = TextAlign.Center, style = MaterialTheme.typography.caption, - modifier = Modifier.padding(bottom = 16.dp).padding(horizontal = 16.dp) + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 8.dp) + .padding(bottom = 16.dp) ) SimpleButton("Share invitation link", icon = Icons.Outlined.Share, click = share) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectContactView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectContactView.kt new file mode 100644 index 0000000000..be219e4c06 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectContactView.kt @@ -0,0 +1,90 @@ +package chat.simplex.app.views.newchat + +import androidx.compose.foundation.background +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.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import chat.simplex.app.model.ChatModel +import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.helpers.CloseSheetBar +import chat.simplex.app.views.helpers.withApi +import kotlinx.coroutines.DelicateCoroutinesApi + +@DelicateCoroutinesApi +@Composable +fun ConnectContactView(chatModel: ChatModel, nav: NavController) { + ConnectContactLayout( + qrCodeScanner = { + QRCodeScanner { connReqUri -> + withApi { + val res = chatModel.controller.apiConnect(connReqUri) + // check if it is valid + nav.popBackStack() + } + } + }, + close = { nav.popBackStack() } + ) +} + +@Composable +fun ConnectContactLayout(qrCodeScanner: @Composable () -> Unit, close: () -> Unit) { + Column( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxSize() + .background(MaterialTheme.colors.background), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CloseSheetBar(close) + Text( + "Scan QR code", + style = MaterialTheme.typography.h1, + modifier = Modifier.padding(bottom = 8.dp) + ) + Text( + "Your chat profile will be sent\nto your contact", + style = MaterialTheme.typography.h2, + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 16.dp) + ) + Box ( + Modifier + .fillMaxWidth() + .aspectRatio(ratio = 1F) + ) { qrCodeScanner() } + Text( + buildAnnotatedString { + append("If you cannot meet in person, you can ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append("scan QR code in the video call") + } + append(", or you can create the invitation link.") + }, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.caption, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 16.dp) + ) + } +} + +@Preview +@Composable +fun PreviewConnectContactLayout() { + SimpleXTheme { + ConnectContactLayout( + qrCodeScanner = { Surface {} }, + close = {}, + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt index 4d74ff895b..4a2f47f6f5 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt @@ -1,37 +1,57 @@ package chat.simplex.app.views.newchat -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Button -import androidx.compose.material.ExperimentalMaterialApi +import android.Manifest +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.Composable -import androidx.compose.material.Text +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController import chat.simplex.app.Pages import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.ui.theme.* import chat.simplex.app.views.chatlist.ScaffoldController import chat.simplex.app.views.helpers.withApi +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberPermissionState +import kotlinx.coroutines.DelicateCoroutinesApi +@DelicateCoroutinesApi +@ExperimentalPermissionsApi @ExperimentalMaterialApi @Composable fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController, nav: NavController) { + val cameraPermissionState = rememberPermissionState(permission = Manifest.permission.CAMERA) NewChatSheetLayout( addContact = { withApi { -// show spinner + // show spinner chatModel.connReqInvitation = chatModel.controller.apiAddContact() -// hide spinner + // hide spinner if (chatModel.connReqInvitation != null) { newChatCtrl.collapse() nav.navigate(Pages.AddContact.route) } } }, - scanCode = {}, + scanCode = { + newChatCtrl.collapse() + nav.navigate(Pages.Connect.route) + cameraPermissionState.launchPermissionRequest() + }, close = { newChatCtrl.collapse() } @@ -40,19 +60,60 @@ fun NewChatSheet(chatModel: ChatModel, newChatCtrl: ScaffoldController, nav: Nav @Composable fun NewChatSheetLayout(addContact: () -> Unit, scanCode: () -> Unit, close: () -> Unit) { - Column ( Modifier.padding(all = 8.dp) ) { - Button(onClick = addContact) { - Text("Add contact") + Row(Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 48.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Box(Modifier.weight(1F).fillMaxWidth()) { + ActionButton( + "Add contact", "(create QR code\nor link)", + Icons.Outlined.PersonAdd, click = addContact + ) } - Button(onClick = scanCode) { - Text("Scan QR code") + Box(Modifier.weight(1F).fillMaxWidth()) { + ActionButton( + "Scan QR code", "(in person or in video call)", + Icons.Outlined.QrCode, click = scanCode + ) } - Button(onClick = close) { - Text("Cancel") + Box(Modifier.weight(1F).fillMaxWidth()) { + ActionButton( + "Create Group", "(coming soon!)", + Icons.Outlined.GroupAdd, disabled = true + ) } } } +@Composable +fun ActionButton(text: String, comment: String, icon: ImageVector, disabled: Boolean = false, + click: () -> Unit = {}) { + Column( + Modifier + .clickable(onClick = click) + .padding(horizontal = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val tint = if (disabled) DarkGray else MaterialTheme.colors.primary + Icon(icon, text, + tint = tint, + modifier = Modifier + .size(40.dp) + .padding(bottom = 8.dp)) + Text(text, + textAlign = TextAlign.Center, + fontWeight = FontWeight.Bold, + color = tint, + modifier = Modifier.padding(bottom = 4.dp) + ) + Text(comment, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.body2 + ) + } +} + @Preview @Composable fun PreviewNewChatSheet() { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt index 0a0fc2a2e8..c3d1cd0814 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt @@ -3,15 +3,9 @@ package chat.simplex.app.views.newchat import android.graphics.Bitmap import android.graphics.Color import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.toSize import chat.simplex.app.ui.theme.SimpleXTheme import com.google.zxing.BarcodeFormat import com.google.zxing.EncodeHintType diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt new file mode 100644 index 0000000000..873d4e6ae6 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt @@ -0,0 +1,110 @@ +package chat.simplex.app.views.newchat + +import android.util.Log +import android.view.ViewGroup +import androidx.camera.core.* +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import com.google.common.util.concurrent.ListenableFuture +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.common.InputImage +import java.util.concurrent.TimeUnit + +// Bar code scanner adapted from https://github.com/MakeItEasyDev/Jetpack-Compose-BarCode-Scanner + +@Composable +fun QRCodeScanner(onBarcode: (String) -> Unit) { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + var preview by remember { mutableStateOf(null) } + + AndroidView( + factory = { AndroidViewContext -> + PreviewView(AndroidViewContext).apply { + this.scaleType = PreviewView.ScaleType.FILL_CENTER + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + implementationMode = PreviewView.ImplementationMode.COMPATIBLE + } + }, +// modifier = Modifier.fillMaxSize(), + update = { previewView -> + val cameraSelector: CameraSelector = CameraSelector.Builder() + .requireLensFacing(CameraSelector.LENS_FACING_BACK) + .build() + val cameraExecutor: ExecutorService = Executors.newSingleThreadExecutor() + val cameraProviderFuture: ListenableFuture = + ProcessCameraProvider.getInstance(context) + + cameraProviderFuture.addListener({ + preview = Preview.Builder().build().also { + it.setSurfaceProvider(previewView.surfaceProvider) + } + val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + val barcodeAnalyser = BarCodeAnalyser { barcodes -> + barcodes.firstOrNull()?.rawValue?.let(onBarcode) + } + val imageAnalysis: ImageAnalysis = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .build() + .also { it.setAnalyzer(cameraExecutor, barcodeAnalyser) } + + try { + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis) + } catch (e: Exception) { + Log.d("SIMPLEX", "CameraPreview: ${e.localizedMessage}") + } + }, ContextCompat.getMainExecutor(context)) + } + ) +} + +class BarCodeAnalyser( + private val onBarcodeDetected: (barcodes: List) -> Unit, +): ImageAnalysis.Analyzer { + private var lastAnalyzedTimeStamp = 0L + + @ExperimentalGetImage + override fun analyze(image: ImageProxy) { + val currentTimestamp = System.currentTimeMillis() + if (currentTimestamp - lastAnalyzedTimeStamp >= TimeUnit.SECONDS.toMillis(1)) { + image.image?.let { imageToAnalyze -> + val options = BarcodeScannerOptions.Builder() + .setBarcodeFormats(Barcode.FORMAT_ALL_FORMATS) + .build() + val barcodeScanner = BarcodeScanning.getClient(options) + val imageToProcess = InputImage.fromMediaImage(imageToAnalyze, image.imageInfo.rotationDegrees) + + barcodeScanner.process(imageToProcess) + .addOnSuccessListener { barcodes -> + if (barcodes.isNotEmpty()) { + onBarcodeDetected(barcodes) + } else { + Log.d("SIMPLEX", "BarcodeAnalyser: No barcode Scanned") + } + } + .addOnFailureListener { exception -> + Log.d("SIMPLEX", "BarcodeAnalyser: Something went wrong $exception") + } + .addOnCompleteListener { + image.close() + } + } + lastAnalyzedTimeStamp = currentTimestamp + } else { + image.close() + } + } +} From 7fc5b833aa8f02738895326855ddd992124dd596 Mon Sep 17 00:00:00 2001 From: IanRDavies Date: Sun, 20 Feb 2022 15:47:24 +0000 Subject: [PATCH 32/46] android: use deep links to connect (#339) * simple case * version almost working with true links * show alerts in imperative way, like they were meant to * connecting via links works * add error handling to connections Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../simplex/app/ExampleInstrumentedTest.kt | 6 +- apps/android/app/src/main/AndroidManifest.xml | 8 ++- .../java/chat/simplex/app/MainActivity.kt | 72 +++++++++++++++---- .../main/java/chat/simplex/app/MainPage.kt | 20 ------ .../main/java/chat/simplex/app/SimplexApp.kt | 59 +++++++++++---- .../java/chat/simplex/app/model/ChatModel.kt | 23 +++--- .../java/chat/simplex/app/model/SimpleXAPI.kt | 66 ++++++++++++----- .../java/chat/simplex/app/ui/theme/Theme.kt | 9 ++- .../chat/simplex/app/views/TerminalView.kt | 6 +- .../chat/simplex/app/views/WelcomeView.kt | 15 ++-- .../chat/simplex/app/views/chat/ChatView.kt | 3 +- .../simplex/app/views/chat/item/CIMetaView.kt | 2 +- .../app/views/chat/item/ChatItemView.kt | 7 +- .../app/views/chat/item/TextItemView.kt | 5 +- .../app/views/chatlist/ChatListView.kt | 20 +++--- .../app/views/chatlist/ChatPreviewView.kt | 25 +++---- .../app/views/helpers/CloseSheetBar.kt | 3 - .../simplex/app/views/helpers/SimpleButton.kt | 7 +- .../app/views/newchat/ConnectContactView.kt | 43 +++++++++-- .../simplex/app/views/newchat/NewChatSheet.kt | 10 +-- .../chat/simplex/app/views/newchat/QRCode.kt | 2 +- .../app/views/newchat/QRCodeScanner.kt | 4 +- .../app/views/usersettings/SettingsView.kt | 5 +- .../java/chat/simplex/app/ExampleUnitTest.kt | 3 +- 24 files changed, 258 insertions(+), 165 deletions(-) delete mode 100644 apps/android/app/src/main/java/chat/simplex/app/MainPage.kt diff --git a/apps/android/app/src/androidTest/java/chat/simplex/app/ExampleInstrumentedTest.kt b/apps/android/app/src/androidTest/java/chat/simplex/app/ExampleInstrumentedTest.kt index 72f99dd818..f49be92003 100644 --- a/apps/android/app/src/androidTest/java/chat/simplex/app/ExampleInstrumentedTest.kt +++ b/apps/android/app/src/androidTest/java/chat/simplex/app/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package chat.simplex.app -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml index 7a0ce24bb8..4df768a83c 100644 --- a/apps/android/app/src/main/AndroidManifest.xml +++ b/apps/android/app/src/main/AndroidManifest.xml @@ -23,9 +23,15 @@ android:theme="@style/Theme.SimpleX"> - + + + + + + + diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 76468bd013..64d2a26e9f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -1,46 +1,67 @@ package chat.simplex.app import android.app.Application +import android.content.Intent import android.os.Bundle +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels +import androidx.compose.foundation.layout.Box import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.runtime.Composable -import chat.simplex.app.ui.theme.SimpleXTheme import androidx.lifecycle.AndroidViewModel -import androidx.navigation.NavType -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument +import androidx.navigation.* +import androidx.navigation.compose.* import chat.simplex.app.model.ChatModel +import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.* import chat.simplex.app.views.chat.ChatView -import chat.simplex.app.views.chatlist.* -import chat.simplex.app.views.newchat.AddContactView -import chat.simplex.app.views.newchat.ConnectContactView +import chat.simplex.app.views.chatlist.ChatListView +import chat.simplex.app.views.helpers.withApi +import chat.simplex.app.views.newchat.* import com.google.accompanist.permissions.ExperimentalPermissionsApi +import kotlinx.coroutines.DelicateCoroutinesApi +@DelicateCoroutinesApi @ExperimentalPermissionsApi @ExperimentalMaterialApi class MainActivity: ComponentActivity() { - private val viewModel by viewModels() + private val vm by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + connectIfOpenedViaUri(intent, vm.chatModel) setContent { SimpleXTheme { - Navigation(viewModel.chatModel) + Navigation(vm.chatModel) } } } } +@DelicateCoroutinesApi class SimplexViewModel(application: Application) : AndroidViewModel(application) { val chatModel = getApplication().chatModel } +@DelicateCoroutinesApi +@ExperimentalPermissionsApi +@ExperimentalMaterialApi +@Composable +fun MainPage(chatModel: ChatModel, nav: NavController) { + Box { + if (chatModel.currentUser.value == null) WelcomeView(chatModel) { + nav.navigate(Pages.ChatList.route) + } else { + ChatListView(chatModel, nav) + } + val am = chatModel.alertManager + if (am.presentAlert.value) am.alertView.value?.invoke() + } +} + +@DelicateCoroutinesApi @ExperimentalPermissionsApi @ExperimentalMaterialApi @Composable @@ -51,7 +72,7 @@ fun Navigation(chatModel: ChatModel) { composable(route=Pages.Home.route){ MainPage(chatModel, nav) } - composable(route = Pages.Welcome.route){ + composable(route = Pages.Welcome.route) { WelcomeView(chatModel) { nav.navigate(Pages.Home.route) { popUpTo(Pages.Home.route) { inclusive = true } @@ -71,7 +92,7 @@ fun Navigation(chatModel: ChatModel) { ConnectContactView(chatModel, nav) } composable(route = Pages.Terminal.route) { - TerminalView(chatModel, navController = nav) + TerminalView(chatModel, nav) } composable( Pages.TerminalItemDetails.route + "/{identifier}", @@ -94,3 +115,28 @@ sealed class Pages(val route: String) { object AddContact: Pages("add_contact") object Connect: Pages("connect") } + +@DelicateCoroutinesApi +fun connectIfOpenedViaUri(intent: Intent?, chatModel: ChatModel) { + val uri = intent?.data + if (intent?.action == "android.intent.action.VIEW" && uri != null) { + Log.d("SIMPLEX", "connectIfOpenedViaUri: opened via link") + if (chatModel.currentUser.value == null) { + chatModel.appOpenUrl.value = uri + } else { + withUriAction(chatModel, uri) { action -> + chatModel.alertManager.showAlertMsg( + title = "Connect via $action link?", + text = "Your profile will be sent to the contact that you received this link from.", + confirmText = "Connect", + onConfirm = { + withApi { + Log.d("SIMPLEX", "connectIfOpenedViaUri: connecting") + connectViaUri(chatModel, action, uri) + } + } + ) + } + } + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt b/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt deleted file mode 100644 index f42208b2a0..0000000000 --- a/apps/android/app/src/main/java/chat/simplex/app/MainPage.kt +++ /dev/null @@ -1,20 +0,0 @@ -package chat.simplex.app - -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.runtime.Composable -import androidx.navigation.NavController -import chat.simplex.app.model.ChatModel -import chat.simplex.app.views.TerminalView -import chat.simplex.app.views.WelcomeView -import chat.simplex.app.views.chatlist.ChatListView -import com.google.accompanist.permissions.ExperimentalPermissionsApi - -@ExperimentalPermissionsApi -@ExperimentalMaterialApi -@Composable -fun MainPage(chatModel: ChatModel, nav: NavController) { - if (chatModel.currentUser.value == null) WelcomeView(chatModel) { - nav.navigate(Pages.ChatList.route) - } - else ChatListView(chatModel, nav) -} diff --git a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt index 502c2fb9fe..2602960e8b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/SimplexApp.kt @@ -3,16 +3,15 @@ package chat.simplex.app import android.app.Application import android.net.LocalServerSocket import android.util.Log -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.snapshots.SnapshotStateList -import chat.simplex.app.model.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import chat.simplex.app.model.ChatController +import chat.simplex.app.model.ChatModel +import chat.simplex.app.views.helpers.withApi +import kotlinx.coroutines.DelicateCoroutinesApi import java.io.BufferedReader import java.io.InputStreamReader -import java.lang.ref.WeakReference import java.util.* import java.util.concurrent.Semaphore import kotlin.concurrent.thread @@ -28,18 +27,52 @@ external fun chatInit(path: String): ChatCtrl external fun chatSendCmd(ctrl: ChatCtrl, msg: String) : String external fun chatRecvMsg(ctrl: ChatCtrl) : String +@DelicateCoroutinesApi class SimplexApp: Application() { private lateinit var controller: ChatController lateinit var chatModel: ChatModel override fun onCreate() { super.onCreate() - controller = ChatController(chatInit(applicationContext.filesDir.toString())) + val ctrl = chatInit(applicationContext.filesDir.toString()) + controller = ChatController(ctrl, AlertManager()) chatModel = controller.chatModel - GlobalScope.launch { - withContext(Dispatchers.Main) { - var user = controller.apiGetActiveUser() - if (user != null) controller.startChat(user) + withApi { + val user = controller.apiGetActiveUser() + if (user != null) controller.startChat(user) + } + } + + class AlertManager { + var alertView = mutableStateOf<(@Composable () -> Unit)?>(null) + var presentAlert = mutableStateOf(false) + + fun showAlert(alert: @Composable () -> Unit) { + Log.d("SIMPLEX", "AlertManager.showAlert") + alertView.value = alert + presentAlert.value = true + } + + fun hideAlert() { + presentAlert.value = false + alertView.value = null + } + + fun showAlertMsg(title: String, text: String? = null, + confirmText: String = "Ok", onConfirm: (() -> Unit)? = null) { + val alertText: (@Composable () -> Unit)? = if (text == null) null else { -> Text(text) } + showAlert { + AlertDialog( + onDismissRequest = this::hideAlert, + title = { Text(title) }, + text = alertText, + confirmButton = { + Button(onClick = { + onConfirm?.invoke() + hideAlert() + }) { Text(confirmText) } + } + ) } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 8c64d423f0..ff3ed6fd7c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -1,12 +1,16 @@ package chat.simplex.app.model import android.annotation.SuppressLint -import androidx.compose.runtime.* +import android.net.Uri +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import chat.simplex.app.SimplexApp import kotlinx.datetime.* -import kotlinx.serialization.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import java.time.format.DateTimeFormatter -class ChatModel(val controller: ChatController) { +class ChatModel(val controller: ChatController, val alertManager: SimplexApp.AlertManager) { var currentUser = mutableStateOf(null) var chats = mutableStateListOf() var chatId = mutableStateOf(null) @@ -14,6 +18,8 @@ class ChatModel(val controller: ChatController) { var connReqInvitation: String? = null var terminalItems = mutableStateListOf() + // set when app is opened via contact or invitation URI + var appOpenUrl = mutableStateOf(null) fun hasChat(id: String): Boolean = chats.firstOrNull() { it.id == id } != null fun getChat(id: String): Chat? = chats.firstOrNull { it.id == id } @@ -155,17 +161,6 @@ class ChatModel(val controller: ChatController) { // chats.removeAll(where: { $0.id == id }) // } // } - - companion object { - val sampleData: ChatModel get() { - val m = ChatModel(ChatController.Mock()) - m.terminalItems = mutableStateListOf( - TerminalItem.Cmd(0, CC.ShowActiveUser()), - TerminalItem.Resp(1, CR.ActiveUser(User.sampleData)) - ) - return m - } - } } enum class ChatType(val type: String) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 831267325c..94361b56fd 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -2,21 +2,19 @@ package chat.simplex.app.model import android.util.Log import androidx.compose.runtime.mutableStateOf -import chat.simplex.app.chatRecvMsg -import chat.simplex.app.chatSendCmd -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import chat.simplex.app.* +import kotlinx.coroutines.* import kotlinx.serialization.* import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject -import java.lang.Exception import java.util.* import kotlin.concurrent.thread typealias ChatCtrl = Long -open class ChatController(val ctrl: ChatCtrl) { - var chatModel = ChatModel(this) +@DelicateCoroutinesApi +open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.AlertManager) { + var chatModel = ChatModel(this, alertManager) suspend fun startChat(u: User) { chatModel.currentUser = mutableStateOf(u) @@ -110,9 +108,28 @@ open class ChatController(val ctrl: ChatCtrl) { suspend fun apiConnect(connReq: String): Boolean { val r = sendCmd(CC.Connect(connReq)) - if (r is CR.SentConfirmation || r is CR.SentInvitation) return true - Log.d("SIMPLEX", "apiConnect bad response: ${r.responseType} ${r.details}") - return false + when { + r is CR.SentConfirmation || r is CR.SentInvitation -> return true + r is CR.ContactAlreadyExists -> { + alertManager.showAlertMsg("Contact already exists", + "You are already connected to ${r.contact.displayName} via this link" + ) + return false + } + r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat + && r.chatError.errorType is ChatErrorType.InvalidConnReq -> { + alertManager.showAlertMsg("Invalid connection link", + "Please check that you used the correct link or ask your contact to send you another one." + ) + return false + } + else -> { + val errMsg = "${r.responseType}: ${r.details}" + Log.e("SIMPLEX", "apiConnect bad response: $errMsg") + alertManager.showAlertMsg("Connection error", errMsg) + return false + } + } } suspend fun apiDeleteChat(type: ChatType, id: Long): Boolean { @@ -147,7 +164,7 @@ open class ChatController(val ctrl: ChatCtrl) { suspend fun apiGetUserAddress(): String? { val r = sendCmd(CC.ShowMyAddress()) if (r is CR.UserContactLink) return r.connReqContact - if (r is CR.ChatCmdError && r.chatError is ChatError.ErrorStore + if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.UserContactLinkNotFound) { return null } @@ -238,8 +255,6 @@ open class ChatController(val ctrl: ChatCtrl) { // } } } - - class Mock: ChatController(0) {} } // ChatCommand @@ -348,17 +363,17 @@ val json = Json { class APIResponse(val resp: CR, val corr: String? = null) { companion object { fun decodeStr(str: String): APIResponse { - try { - return json.decodeFromString(str) + return try { + json.decodeFromString(str) } catch(e: Exception) { try { val data = json.parseToJsonElement(str).jsonObject - return APIResponse( + APIResponse( resp = CR.Response(data["resp"]!!.jsonObject["type"]?.toString() ?: "invalid", json.encodeToString(data)), corr = data["corr"]?.toString() ) } catch(e: Exception) { - return APIResponse(CR.Invalid(str)) + APIResponse(CR.Invalid(str)) } } } @@ -413,6 +428,12 @@ sealed class CR { override val details get() = noDetails() } + @Serializable @SerialName("contactAlreadyExists") + class ContactAlreadyExists(val contact: Contact): CR() { + override val responseType get() = "contactAlreadyExists" + override val details get() = contact.toString() + } + @Serializable @SerialName("contactDeleted") class ContactDeleted(val contact: Contact): CR() { override val responseType get() = "contactDeleted" @@ -589,8 +610,17 @@ abstract class TerminalItem { @Serializable sealed class ChatError { + @Serializable @SerialName("error") + class ChatErrorChat(val errorType: ChatErrorType): ChatError() + @Serializable @SerialName("errorStore") - class ErrorStore(val storeError: StoreError): ChatError() + class ChatErrorStore(val storeError: StoreError): ChatError() +} + +@Serializable +sealed class ChatErrorType { + @Serializable @SerialName("invalidConnReq") + class InvalidConnReq: ChatErrorType() } @Serializable diff --git a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt index ef4d3784f1..26669f83a0 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/ui/theme/Theme.kt @@ -1,15 +1,16 @@ package chat.simplex.app.ui.theme import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material.MaterialTheme -import androidx.compose.material.darkColors -import androidx.compose.material.lightColors +import androidx.compose.material.* import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color private val DarkColorPalette = darkColors( primary = SimplexBlue, primaryVariant = SimplexGreen, secondary = DarkGray, + background = Color.Black, + surface = Color.Black /* background: Color = Color(0xFF121212), surface: Color = Color(0xFF121212), @@ -26,6 +27,8 @@ private val LightColorPalette = lightColors( primary = SimplexBlue, primaryVariant = SimplexGreen, secondary = LightGray, + background = Color.White, + surface = Color.White /* Other default colors to override surface = Color.White, onPrimary = Color.White, diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index d506754675..480339e0b8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -7,15 +7,13 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Text import androidx.compose.material.Button +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString -import androidx.navigation.* +import androidx.navigation.NavController import chat.simplex.app.model.* -import chat.simplex.app.model.ChatModel -import chat.simplex.app.model.TerminalItem import chat.simplex.app.views.chat.SendMsgView import chat.simplex.app.views.helpers.withApi import kotlinx.coroutines.DelicateCoroutinesApi diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt index 3ac36a12b3..2c09ec0cb8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/WelcomeView.kt @@ -1,18 +1,13 @@ package chat.simplex.app.views +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.Image -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.ui.res.painterResource +import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.runtime.Composable -import chat.simplex.app.R -import androidx.compose.material.Text -import androidx.compose.material.TextField -import androidx.compose.material.Button -import androidx.compose.ui.unit.dp import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import chat.simplex.app.R import chat.simplex.app.model.ChatModel import chat.simplex.app.model.Profile import chat.simplex.app.views.helpers.withApi diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 35a537bd31..9b7ab104f8 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -7,11 +7,10 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.* +import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.material.Icon import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt index 73fa80ec01..aa2551efa6 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import chat.simplex.app.model.CIDirection import chat.simplex.app.model.ChatItem -import kotlinx.datetime.* +import kotlinx.datetime.Clock @Composable fun CIMetaView(chatItem: ChatItem) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt index 9ad9f16ad4..60ec4ee67e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/ChatItemView.kt @@ -1,14 +1,13 @@ package chat.simplex.app.views.chat.item -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import chat.simplex.app.model.* +import chat.simplex.app.model.CIDirection +import chat.simplex.app.model.ChatItem import chat.simplex.app.ui.theme.SimpleXTheme import kotlinx.datetime.Clock diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt index 6974c34b14..fdb2d5d80e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/TextItemView.kt @@ -9,9 +9,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import chat.simplex.app.model.* +import chat.simplex.app.model.CIDirection +import chat.simplex.app.model.ChatItem import chat.simplex.app.ui.theme.SimpleXTheme -import kotlinx.datetime.* +import kotlinx.datetime.Clock // TODO move to theme val SentColorLight = Color(0x1E45B8FF) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index cd6466c468..10d36a6663 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -1,32 +1,30 @@ package chat.simplex.app.views.chatlist -import androidx.compose.foundation.clickable import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.PersonAdd +import androidx.compose.material.icons.outlined.Settings import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavController import chat.simplex.app.Pages -import chat.simplex.app.model.* -import kotlinx.coroutines.launch -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight +import chat.simplex.app.model.Chat +import chat.simplex.app.model.ChatModel import chat.simplex.app.views.helpers.withApi import chat.simplex.app.views.newchat.NewChatSheet import com.google.accompanist.permissions.ExperimentalPermissionsApi import kotlinx.coroutines.* - @ExperimentalMaterialApi class ScaffoldController(val state: BottomSheetScaffoldState, val scope: CoroutineScope) { fun expand() = scope.launch { state.bottomSheetState.expand() } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index 5b870cba81..08fbb02511 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -1,33 +1,28 @@ package chat.simplex.app.views.chatlist import android.icu.text.SimpleDateFormat -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import chat.simplex.app.model.* -import androidx.compose.material.Icon +import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Person +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.font.* +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import chat.simplex.app.model.Chat +import chat.simplex.app.model.ChatInfo import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.SimpleXTheme import kotlinx.datetime.Instant import kotlinx.datetime.toJavaInstant import java.time.LocalDateTime -import java.time.Instant as JavaInstant import java.util.* +import java.time.Instant as JavaInstant fun getDisplayTime(t: Instant) : String { val timeFormatter = SimpleDateFormat("HH:mm", Locale.getDefault()) diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt index c323f4317b..90199cd953 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/CloseSheetBar.kt @@ -4,17 +4,14 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Close -import androidx.compose.material.icons.outlined.Settings import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import chat.simplex.app.ui.theme.SimpleXTheme -import chat.simplex.app.views.newchat.AddContactLayout @Composable fun CloseSheetBar(close: () -> Unit) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt index 056dfe626e..bd2f037c47 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt @@ -2,13 +2,9 @@ package chat.simplex.app.ui.theme import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.material.* import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Share import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -16,7 +12,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import chat.simplex.app.views.helpers.CloseSheetBar @Composable fun SimpleButton(text: String, icon: ImageVector, click: () -> Unit) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectContactView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectContactView.kt index be219e4c06..2a7274a1be 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectContactView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/ConnectContactView.kt @@ -1,5 +1,6 @@ package chat.simplex.app.views.newchat +import android.net.Uri import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* @@ -24,17 +25,51 @@ fun ConnectContactView(chatModel: ChatModel, nav: NavController) { ConnectContactLayout( qrCodeScanner = { QRCodeScanner { connReqUri -> - withApi { - val res = chatModel.controller.apiConnect(connReqUri) - // check if it is valid - nav.popBackStack() + try { + val uri = Uri.parse(connReqUri) + withUriAction(chatModel, uri) { action -> + connectViaUri(chatModel, action, uri) + } + } catch(e: RuntimeException) { + chatModel.alertManager.showAlertMsg( + title = "Invalid QR code", + text = "This QR code is not a link!" + ) } + nav.popBackStack() } }, close = { nav.popBackStack() } ) } +@DelicateCoroutinesApi +fun withUriAction(chatModel: ChatModel, uri: Uri, + run: suspend (String) -> Unit) { + val action = uri.path?.drop(1) + if (action == "contact" || action == "invitation") { + withApi { run(action) } + } else { + chatModel.alertManager.showAlertMsg( + title = "Invalid link!", + text = "This link is not a valid connection link!" + ) + } +} + +suspend fun connectViaUri(chatModel: ChatModel, action: String, uri: Uri) { + val r = chatModel.controller.apiConnect(uri.toString()) + if (r) { + val whenConnected = + if (action == "contact") "your connection request is accepted" + else "your contact's device is online" + chatModel.alertManager.showAlertMsg( + title = "Connection request sent!", + text = "You will be connected when $whenConnected, please wait or check later!" + ) + } +} + @Composable fun ConnectContactLayout(qrCodeScanner: @Composable () -> Unit, close: () -> Unit) { Column( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt index 4a2f47f6f5..b4f8094224 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/NewChatSheet.kt @@ -1,19 +1,14 @@ package chat.simplex.app.views.newchat import android.Manifest -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -21,8 +16,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController import chat.simplex.app.Pages -import chat.simplex.app.model.* -import chat.simplex.app.ui.theme.* +import chat.simplex.app.model.ChatModel +import chat.simplex.app.ui.theme.DarkGray +import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chatlist.ScaffoldController import chat.simplex.app.views.helpers.withApi import com.google.accompanist.permissions.ExperimentalPermissionsApi diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt index c3d1cd0814..6dee26d107 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCode.kt @@ -3,7 +3,7 @@ package chat.simplex.app.views.newchat import android.graphics.Bitmap import android.graphics.Color import androidx.compose.foundation.Image -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.tooling.preview.Preview import chat.simplex.app.ui.theme.SimpleXTheme diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt index 873d4e6ae6..8f2b940258 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/newchat/QRCodeScanner.kt @@ -11,13 +11,11 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat import com.google.common.util.concurrent.ListenableFuture -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors import com.google.mlkit.vision.barcode.BarcodeScannerOptions import com.google.mlkit.vision.barcode.BarcodeScanning import com.google.mlkit.vision.barcode.common.Barcode import com.google.mlkit.vision.common.InputImage -import java.util.concurrent.TimeUnit +import java.util.concurrent.* // Bar code scanner adapted from https://github.com/MakeItEasyDev/Jetpack-Compose-BarCode-Scanner diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index 35c54c5f69..527494be95 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -1,12 +1,9 @@ package chat.simplex.app.views.usersettings -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import chat.simplex.app.model.Profile diff --git a/apps/android/app/src/test/java/chat/simplex/app/ExampleUnitTest.kt b/apps/android/app/src/test/java/chat/simplex/app/ExampleUnitTest.kt index eb08839d27..c168b93f49 100644 --- a/apps/android/app/src/test/java/chat/simplex/app/ExampleUnitTest.kt +++ b/apps/android/app/src/test/java/chat/simplex/app/ExampleUnitTest.kt @@ -1,9 +1,8 @@ package chat.simplex.app +import org.junit.Assert.assertEquals import org.junit.Test -import org.junit.Assert.* - /** * Example local unit test, which will execute on the development machine (host). * From b3153ae0fdfe857a84bce7bdf58ce360bdfff205 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 20 Feb 2022 16:33:02 +0000 Subject: [PATCH 33/46] align time format with iOS app, use kotlix-datetime only (#340) --- .../java/chat/simplex/app/model/ChatModel.kt | 18 ++++++------ .../app/views/chatlist/ChatPreviewView.kt | 29 ++----------------- 2 files changed, 12 insertions(+), 35 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index ff3ed6fd7c..2f661fa973 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -1,14 +1,13 @@ package chat.simplex.app.model -import android.annotation.SuppressLint import android.net.Uri import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import chat.simplex.app.SimplexApp import kotlinx.datetime.* +import kotlinx.datetime.TimeZone import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import java.time.format.DateTimeFormatter class ChatModel(val controller: ChatController, val alertManager: SimplexApp.AlertManager) { var currentUser = mutableStateOf(null) @@ -495,13 +494,14 @@ class CIMeta ( } } -// TODO use old api? -// TODO date for older timestamps -@SuppressLint("NewApi") -fun getTimestampText(d: Instant): String { - val dt = d.toLocalDateTime(TimeZone.currentSystemDefault()).toJavaLocalDateTime() - val formatter = DateTimeFormatter.ofPattern("HH:mm") - return dt.format(formatter) +fun getTimestampText(t: Instant): String { + val tz = TimeZone.currentSystemDefault() + val now: LocalDateTime = Clock.System.now().toLocalDateTime(tz) + val time: LocalDateTime = t.toLocalDateTime(tz) + val recent = now.date == time.date || + (now.date.minus(time.date).days == 1 && now.hour < 12 && time.hour >= 18 ) + return if (recent) String.format("%02d:%02d", time.hour, time.minute) + else String.format("%02d/%02d", time.dayOfMonth, time.monthNumber) } @Serializable diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index 08fbb02511..a064d5e518 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -1,6 +1,5 @@ package chat.simplex.app.views.chatlist -import android.icu.text.SimpleDateFormat import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape @@ -14,31 +13,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import chat.simplex.app.model.Chat -import chat.simplex.app.model.ChatInfo +import chat.simplex.app.model.* import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.SimpleXTheme -import kotlinx.datetime.Instant -import kotlinx.datetime.toJavaInstant -import java.time.LocalDateTime -import java.util.* -import java.time.Instant as JavaInstant - -fun getDisplayTime(t: Instant) : String { - val timeFormatter = SimpleDateFormat("HH:mm", Locale.getDefault()) - val dateFormatter = SimpleDateFormat("dd/MM/y", Locale.getDefault()) - val timeProvided: JavaInstant = t.toJavaInstant() - val tz = TimeZone.getDefault() - val dateProvided = LocalDateTime.ofInstant(timeProvided, tz.toZoneId()).toLocalDate() - val today = LocalDateTime.now().atZone(tz.toZoneId()).toLocalDate() - val yesterday = today.minusDays(1) - - return when (dateProvided) { - today -> timeFormatter.format(Date.from(timeProvided)) - yesterday -> "yesterday" - else -> dateFormatter.format(Date.from(timeProvided)) - } -} @Composable fun ChatPreviewView(chat: Chat, goToChat: () -> Unit) { @@ -70,9 +47,9 @@ fun ChatPreviewView(chat: Chat, goToChat: () -> Unit) { Text(chat.chatInfo.chatViewName, fontWeight = FontWeight.Bold) ( if (chat.chatItems.count() > 0) { - Text(getDisplayTime(chat.chatItems.last().meta.itemTs), color = HighOrLowlight) + Text(getTimestampText(chat.chatItems.last().meta.itemTs), color = HighOrLowlight) } - else Text(getDisplayTime(chat.chatInfo.createdAt), color = HighOrLowlight) + else Text(getTimestampText(chat.chatInfo.createdAt), color = HighOrLowlight) ) } if (chat.chatItems.count() > 0) { From d37f493c6a6fcca20213c1b28f32393e588a9fb7 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 20 Feb 2022 21:17:24 +0000 Subject: [PATCH 34/46] android: add chat info page, delete contacts, show network connection status for contacts, improve error handling --- .../java/chat/simplex/app/MainActivity.kt | 83 +++--- .../java/chat/simplex/app/model/ChatModel.kt | 74 ++--- .../java/chat/simplex/app/model/SimpleXAPI.kt | 278 +++++++++++++++--- .../chat/simplex/app/views/TerminalView.kt | 2 +- .../simplex/app/views/chat/ChatInfoView.kt | 130 ++++++++ .../chat/simplex/app/views/chat/ChatView.kt | 71 +++-- .../simplex/app/views/chat/item/CIMetaView.kt | 6 +- .../app/views/chatlist/ChatListView.kt | 2 +- .../app/views/chatlist/ChatPreviewView.kt | 31 +- .../app/views/helpers/ChatInfoImage.kt | 44 +++ .../simplex/app/views/helpers/SimpleButton.kt | 13 +- apps/ios/Shared/Model/SimpleXAPI.swift | 8 +- 12 files changed, 558 insertions(+), 184 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index 64d2a26e9f..ac74e93a0d 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -16,6 +16,7 @@ import androidx.navigation.compose.* import chat.simplex.app.model.ChatModel import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.* +import chat.simplex.app.views.chat.ChatInfoView import chat.simplex.app.views.chat.ChatView import chat.simplex.app.views.chatlist.ChatListView import chat.simplex.app.views.helpers.withApi @@ -50,14 +51,10 @@ class SimplexViewModel(application: Application) : AndroidViewModel(application) @ExperimentalMaterialApi @Composable fun MainPage(chatModel: ChatModel, nav: NavController) { - Box { - if (chatModel.currentUser.value == null) WelcomeView(chatModel) { - nav.navigate(Pages.ChatList.route) - } else { - ChatListView(chatModel, nav) - } - val am = chatModel.alertManager - if (am.presentAlert.value) am.alertView.value?.invoke() + if (chatModel.currentUser.value == null) WelcomeView(chatModel) { + nav.navigate(Pages.ChatList.route) + } else { + ChatListView(chatModel, nav) } } @@ -68,40 +65,47 @@ fun MainPage(chatModel: ChatModel, nav: NavController) { fun Navigation(chatModel: ChatModel) { val nav = rememberNavController() - NavHost(navController = nav, startDestination=Pages.Home.route){ - composable(route=Pages.Home.route){ - MainPage(chatModel, nav) - } - composable(route = Pages.Welcome.route) { - WelcomeView(chatModel) { - nav.navigate(Pages.Home.route) { - popUpTo(Pages.Home.route) { inclusive = true } + Box { + NavHost(navController = nav, startDestination = Pages.Home.route) { + composable(route = Pages.Home.route) { + MainPage(chatModel, nav) + } + composable(route = Pages.Welcome.route) { + WelcomeView(chatModel) { + nav.navigate(Pages.Home.route) { + popUpTo(Pages.Home.route) { inclusive = true } + } } } + composable(route = Pages.ChatList.route) { + ChatListView(chatModel, nav) + } + composable(route = Pages.Chat.route) { + ChatView(chatModel, nav) + } + composable(route = Pages.AddContact.route) { + AddContactView(chatModel, nav) + } + composable(route = Pages.Connect.route) { + ConnectContactView(chatModel, nav) + } + composable(route = Pages.ChatInfo.route) { + ChatInfoView(chatModel, nav) + } + composable(route = Pages.Terminal.route) { + TerminalView(chatModel, nav) + } + composable( + Pages.TerminalItemDetails.route + "/{identifier}", + arguments = listOf( + navArgument("identifier") { + type = NavType.LongType + } + ) + ) { entry -> DetailView(entry.arguments!!.getLong("identifier"), chatModel.terminalItems, nav) } } - composable(route = Pages.ChatList.route) { - ChatListView(chatModel, nav) - } - composable(route = Pages.Chat.route) { - ChatView(chatModel, nav) - } - composable(route = Pages.AddContact.route) { - AddContactView(chatModel, nav) - } - composable(route = Pages.Connect.route) { - ConnectContactView(chatModel, nav) - } - composable(route = Pages.Terminal.route) { - TerminalView(chatModel, nav) - } - composable( - Pages.TerminalItemDetails.route + "/{identifier}", - arguments = listOf( - navArgument("identifier"){ - type = NavType.LongType - } - ) - ) { entry -> DetailView( entry.arguments!!.getLong("identifier"), chatModel.terminalItems, nav) } + val am = chatModel.alertManager + if (am.presentAlert.value) am.alertView.value?.invoke() } } @@ -114,6 +118,7 @@ sealed class Pages(val route: String) { object Chat: Pages("chat") object AddContact: Pages("add_contact") object Connect: Pages("connect") + object ChatInfo: Pages("chat_info") } @DelicateCoroutinesApi diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index 2f661fa973..cddc588055 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -5,9 +5,12 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import chat.simplex.app.SimplexApp import kotlinx.datetime.* -import kotlinx.datetime.TimeZone import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlin.Boolean +import kotlin.Int +import kotlin.Long +import kotlin.String class ChatModel(val controller: ChatController, val alertManager: SimplexApp.AlertManager) { var currentUser = mutableStateOf(null) @@ -39,12 +42,14 @@ class ChatModel(val controller: ChatController, val alertManager: SimplexApp.Ale } } -// func updateNetworkStatus(_ contact: Contact, _ status: Chat.NetworkStatus) { -// if let ix = getChatIndex(contact.id) { -// chats[ix].serverInfo.networkStatus = status -// } -// } -// + fun updateNetworkStatus(contact: Contact, status: Chat.NetworkStatus) { + val i = getChatIndex(contact.id) + if (i >= 0) { + val chat = chats[i] + chats[i] = chat.copy(serverInfo = chat.serverInfo.copy(networkStatus = status)) + } + } + // func replaceChat(_ id: String, _ chat: Chat) { // if let i = getChatIndex(id) { // chats[i] = chat @@ -59,7 +64,7 @@ class ChatModel(val controller: ChatController, val alertManager: SimplexApp.Ale val i = getChatIndex(cInfo.id) if (i >= 0) { val chat = chats[i] - val updatedChat = chat.copy( + chats[i] = chat.copy( chatItems = arrayListOf(cItem), chatStats = if (cItem.meta.itemStatus is CIStatus.RcvNew) @@ -67,7 +72,6 @@ class ChatModel(val controller: ChatController, val alertManager: SimplexApp.Ale else chat.chatStats ) - chats[i] = updatedChat if (i > 0) { popChat_(i) } @@ -154,18 +158,23 @@ class ChatModel(val controller: ChatController, val alertManager: SimplexApp.Ale val chat = chats.removeAt(i) chats.add(index = 0, chat) } -// -// func removeChat(_ id: String) { -// withAnimation { -// chats.removeAll(where: { $0.id == id }) -// } -// } + + fun removeChat(id: String) { + chats.removeAll { it.id == id } + } } enum class ChatType(val type: String) { Direct("@"), Group("#"), - ContactRequest("<@") + ContactRequest("<@"); + + val chatTypeName: String get () = + when (this) { + Direct -> "contact" + Group -> "group" + ContactRequest -> "contact request" + } } @Serializable @@ -221,20 +230,22 @@ data class Chat ( data class ChatStats(val unreadCount: Int = 0, val minUnreadItemId: Long = 0) @Serializable - class ServerInfo(val networkStatus: NetworkStatus) + data class ServerInfo(val networkStatus: NetworkStatus) @Serializable sealed class NetworkStatus { - abstract val statusString: String - abstract val statusExplanation: String - abstract val imageName: String + val statusString: String get() = if (this is Connected) "Server connected" else "Connecting server…" + val statusExplanation: String get() = + when { + this is Connected -> "You are connected to the server you use to receve messages from this contact." + this is Error -> "Trying to connect to the server you use to receve messages from this contact (error: $error)." + else -> "Trying to connect to the server you use to receve messages from this contact." + } - @Serializable - class Unknown: NetworkStatus() { - override val statusString get() = "Server connected" - override val statusExplanation get() = "You are connected to the server you use to receve messages from this contact." - override val imageName get() = "circle.dotted" // ? - } + @Serializable @SerialName("unknown") class Unknown: NetworkStatus() + @Serializable @SerialName("connected") class Connected: NetworkStatus() + @Serializable @SerialName("disconnected") class Disconnected: NetworkStatus() + @Serializable @SerialName("error") class Error(val error: String): NetworkStatus() } } @@ -249,7 +260,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val ready get() = contact.ready override val createdAt get() = contact.createdAt override val displayName get() = contact.displayName - override val fullName get() = contact.displayName + override val fullName get() = contact.fullName companion object { val sampleData = Direct(Contact.sampleData) @@ -265,7 +276,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val ready get() = groupInfo.ready override val createdAt get() = groupInfo.createdAt override val displayName get() = groupInfo.displayName - override val fullName get() = groupInfo.displayName + override val fullName get() = groupInfo.fullName companion object { val sampleData = Group(GroupInfo.sampleData) @@ -281,7 +292,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val ready get() = contactRequest.ready override val createdAt get() = contactRequest.createdAt override val displayName get() = contactRequest.displayName - override val fullName get() = contactRequest.displayName + override val fullName get() = contactRequest.fullName companion object { val sampleData = ContactRequest(UserContactRequest.sampleData) @@ -565,8 +576,3 @@ sealed class MsgContent { class RcvFileTransfer { } - -@Serializable -class AgentErrorType { - -} diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 94361b56fd..9771af6041 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -124,9 +124,7 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert return false } else -> { - val errMsg = "${r.responseType}: ${r.details}" - Log.e("SIMPLEX", "apiConnect bad response: $errMsg") - alertManager.showAlertMsg("Connection error", errMsg) + apiErrorAlert("apiConnect", "Connection error", r) return false } } @@ -134,8 +132,20 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert suspend fun apiDeleteChat(type: ChatType, id: Long): Boolean { val r = sendCmd(CC.ApiDeleteChat(type, id)) - if (r is CR.ContactDeleted) return true // TODO groups - Log.d("SIMPLEX", "apiDeleteChat bad response: ${r.responseType} ${r.details}") + when { + r is CR.ContactDeleted -> return true // TODO groups + r is CR.ChatCmdError -> { + val e = r.chatError + if (e is ChatError.ChatErrorChat && e.errorType is ChatErrorType.ContactGroups) { + alertManager.showAlertMsg( + "Can't delete contact!", + "Contact ${e.errorType.contact.displayName} cannot be deleted, it is a member of the group(s) ${e.errorType.groupNames}" + ) + return false + } + } + } + apiErrorAlert("apiDeleteChat", "Error deleting ${type.chatTypeName}", r) return false } @@ -193,22 +203,53 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert return false } + fun apiErrorAlert(method: String, title: String, r: CR) { + val errMsg = "${r.responseType}: ${r.details}" + Log.e("SIMPLEX", "$method bad response: $errMsg") + alertManager.showAlertMsg(title, errMsg) + } + fun processReceivedMsg(r: CR) { chatModel.terminalItems.add(TerminalItem.resp(r)) when { r is CR.ContactConnected -> chatModel.updateContact(r.contact) -// r is CR.UpdateNetworkStatus -> return // r is CR.ReceivedContactRequest -> return -// r is CR.ContactUpdated -> return -// r is CR.ContactSubscribed -> return -// r is CR.ContactSubError -> return -// r is CR.UpdateContact -> return + r is CR.ContactUpdated -> { + val cInfo = ChatInfo.Direct(r.toContact) + if (chatModel.hasChat(r.toContact.id)) { + chatModel.updateChatInfo(cInfo) + } + } + + r is CR.ContactSubscribed -> { + chatModel.updateContact(r.contact) + chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Connected()) + } + r is CR.ContactDisconnected -> { + chatModel.updateContact(r.contact) + chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Disconnected()) + } + r is CR.ContactSubError -> { + chatModel.updateContact(r.contact) + val e = r.chatError + val err: String = + if (e is ChatError.ChatErrorAgent) { + val a = e.agentError + when { + a is AgentErrorType.BROKER && a.brokerErr is BrokerErrorType.NETWORK -> "network" + a is AgentErrorType.SMP && a.smpErr is SMPErrorType.AUTH -> "contact deleted" + else -> e.string + } + } + else e.string + chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Error(err)) + } r is CR.NewChatItem -> { val cInfo = r.chatItem.chatInfo val cItem = r.chatItem.chatItem chatModel.addChatItem(cInfo, cItem) - } // NtfManager.shared.notifyMessageReceived(cInfo, cItem) + } // switch res { // chatModel.updateNetworkStatus(contact, .connected) @@ -219,31 +260,7 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert // chatItems: [] // )) // NtfManager.shared.notifyContactRequest(contactRequest) -// case let .contactUpdated(toContact): - // let cInfo = ChatInfo.direct(contact: toContact) - // if chatModel.hasChat(toContact.id) { - // chatModel.updateChatInfo(cInfo) - // } -// case let .contactSubscribed(contact): - // chatModel.updateContact(contact) - // chatModel.updateNetworkStatus(contact, .connected) - // case let .contactDisconnected(contact): - // chatModel.updateContact(contact) - // chatModel.updateNetworkStatus(contact, .disconnected) -// case let .contactSubError(contact, chatError): -// chatModel.updateContact(contact) -//// var err: String -//// switch chatError { -//// case .errorAgent(agentError: .BROKER(brokerErr: .NETWORK)): err = "network" -//// case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted" -//// default: err = String(describing: chatError) -//// } -//// chatModel.updateNetworkStatus(contact, .error(err)) -// case let .newChatItem(aChatItem): - // let cInfo = aChatItem.chatInfo - // let cItem = aChatItem.chatItem - // chatModel.addChatItem(cInfo, cItem) - // NtfManager.shared.notifyMessageReceived(cInfo, cItem) +// // case let .chatItemUpdated(aChatItem): // let cInfo = aChatItem.chatInfo // let cItem = aChatItem.chatItem @@ -557,13 +574,13 @@ sealed class CR { @Serializable @SerialName("chatCmdError") class ChatCmdError(val chatError: ChatError): CR() { override val responseType get() = "chatCmdError" - override val details get() = chatError.toString() + override val details get() = chatError.string } @Serializable @SerialName("chatError") class ChatRespError(val chatError: ChatError): CR() { override val responseType get() = "chatError" - override val details get() = chatError.toString() + override val details get() = chatError.string } @Serializable @@ -610,21 +627,186 @@ abstract class TerminalItem { @Serializable sealed class ChatError { - @Serializable @SerialName("error") - class ChatErrorChat(val errorType: ChatErrorType): ChatError() - - @Serializable @SerialName("errorStore") - class ChatErrorStore(val storeError: StoreError): ChatError() + val string: String get() = when { + this is ChatErrorChat -> "chat ${errorType.string}" + this is ChatErrorAgent -> "agent ${agentError.string}" + this is ChatErrorStore -> "store ${storeError.string}" + else -> "ChatError" + } + @Serializable @SerialName("error") class ChatErrorChat(val errorType: ChatErrorType): ChatError() + @Serializable @SerialName("errorAgent") class ChatErrorAgent(val agentError: AgentErrorType): ChatError() + @Serializable @SerialName("errorStore") class ChatErrorStore(val storeError: StoreError): ChatError() } @Serializable sealed class ChatErrorType { - @Serializable @SerialName("invalidConnReq") - class InvalidConnReq: ChatErrorType() + val string: String get() = when { + this is InvalidConnReq -> "invalidConnReq" + this is ContactGroups -> "groupNames $groupNames" + else -> "ChatErrorType" + } + @Serializable @SerialName("invalidConnReq") class InvalidConnReq: ChatErrorType() + @Serializable @SerialName("contactGroups") class ContactGroups(val contact: Contact, val groupNames: List): ChatErrorType() } @Serializable sealed class StoreError { - @Serializable @SerialName("userContactLinkNotFound") - class UserContactLinkNotFound: StoreError() -} \ No newline at end of file + val string: String get() = when { + this is UserContactLinkNotFound -> "userContactLinkNotFound" + else -> "StoreError" + } + @Serializable @SerialName("userContactLinkNotFound") class UserContactLinkNotFound: StoreError() +} + +@Serializable +sealed class AgentErrorType { + val string: String get() = when { + this is CMD -> "CMD ${cmdErr.string}" + this is CONN -> "CONN ${connErr.string}" + this is SMP -> "SMP ${smpErr.string}" + this is BROKER -> "BROKER ${brokerErr.string}" + this is AGENT -> "AGENT ${agentErr.string}" + this is INTERNAL -> "INTERNAL $internalErr" + else -> "AgentErrorType" + } + @Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): AgentErrorType() + @Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType() + @Serializable @SerialName("SMP") class SMP(val smpErr: SMPErrorType): AgentErrorType() + @Serializable @SerialName("BROKER") class BROKER(val brokerErr: BrokerErrorType): AgentErrorType() + @Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType() + @Serializable @SerialName("INTERNAL") class INTERNAL(val internalErr: String): AgentErrorType() +} + +@Serializable +sealed class CommandErrorType { + val string: String get() = when { + this is PROHIBITED -> "PROHIBITED" + this is SYNTAX -> "SYNTAX" + this is NO_CONN -> "NO_CONN" + this is SIZE -> "SIZE" + this is LARGE -> "LARGE" + else -> "CommandErrorType" + } + @Serializable @SerialName("PROHIBITED") class PROHIBITED: CommandErrorType() + @Serializable @SerialName("SYNTAX") class SYNTAX: CommandErrorType() + @Serializable @SerialName("NO_CONN") class NO_CONN: CommandErrorType() + @Serializable @SerialName("SIZE") class SIZE: CommandErrorType() + @Serializable @SerialName("LARGE") class LARGE: CommandErrorType() +} + +@Serializable +sealed class ConnectionErrorType { + val string: String get() = when { + this is NOT_FOUND -> "NOT_FOUND" + this is DUPLICATE -> "DUPLICATE" + this is SIMPLEX -> "SIMPLEX" + this is NOT_ACCEPTED -> "NOT_ACCEPTED" + this is NOT_AVAILABLE -> "NOT_AVAILABLE" + else -> "ConnectionErrorType" + } + @Serializable @SerialName("NOT_FOUND") class NOT_FOUND: ConnectionErrorType() + @Serializable @SerialName("DUPLICATE") class DUPLICATE: ConnectionErrorType() + @Serializable @SerialName("SIMPLEX") class SIMPLEX: ConnectionErrorType() + @Serializable @SerialName("NOT_ACCEPTED") class NOT_ACCEPTED: ConnectionErrorType() + @Serializable @SerialName("NOT_AVAILABLE") class NOT_AVAILABLE: ConnectionErrorType() +} + +@Serializable +sealed class BrokerErrorType { + val string: String get() = when { + this is RESPONSE -> "RESPONSE ${smpErr.string}" + this is UNEXPECTED -> "UNEXPECTED" + this is NETWORK -> "NETWORK" + this is TRANSPORT -> "TRANSPORT ${transportErr.string}" + this is TIMEOUT -> "TIMEOUT" + else -> "BrokerErrorType" + } + @Serializable @SerialName("RESPONSE") class RESPONSE(val smpErr: SMPErrorType): BrokerErrorType() + @Serializable @SerialName("UNEXPECTED") class UNEXPECTED: BrokerErrorType() + @Serializable @SerialName("NETWORK") class NETWORK: BrokerErrorType() + @Serializable @SerialName("TRANSPORT") class TRANSPORT(val transportErr: SMPTransportError): BrokerErrorType() + @Serializable @SerialName("TIMEOUT") class TIMEOUT: BrokerErrorType() +} + +@Serializable +sealed class SMPErrorType { + val string: String get() = when { + this is BLOCK -> "BLOCK" + this is SESSION -> "SESSION" + this is CMD -> "CMD ${cmdErr.string}" + this is AUTH -> "AUTH" + this is QUOTA -> "QUOTA" + this is NO_MSG -> "NO_MSG" + this is LARGE_MSG -> "LARGE_MSG" + this is INTERNAL -> "INTERNAL" + else -> "SMPErrorType" + } + @Serializable @SerialName("BLOCK") class BLOCK: SMPErrorType() + @Serializable @SerialName("SESSION") class SESSION: SMPErrorType() + @Serializable @SerialName("CMD") class CMD(val cmdErr: SMPCommandError): SMPErrorType() + @Serializable @SerialName("AUTH") class AUTH: SMPErrorType() + @Serializable @SerialName("QUOTA") class QUOTA: SMPErrorType() + @Serializable @SerialName("NO_MSG") class NO_MSG: SMPErrorType() + @Serializable @SerialName("LARGE_MSG") class LARGE_MSG: SMPErrorType() + @Serializable @SerialName("INTERNAL") class INTERNAL: SMPErrorType() +} + +@Serializable +sealed class SMPCommandError { + val string: String get() = when { + this is UNKNOWN -> "UNKNOWN" + this is SYNTAX -> "SYNTAX" + this is NO_AUTH -> "NO_AUTH" + this is HAS_AUTH -> "HAS_AUTH" + this is NO_QUEUE -> "NO_QUEUE" + else -> "SMPCommandError" + } + @Serializable @SerialName("UNKNOWN") class UNKNOWN: SMPCommandError() + @Serializable @SerialName("SYNTAX") class SYNTAX: SMPCommandError() + @Serializable @SerialName("NO_AUTH") class NO_AUTH: SMPCommandError() + @Serializable @SerialName("HAS_AUTH") class HAS_AUTH: SMPCommandError() + @Serializable @SerialName("NO_QUEUE") class NO_QUEUE: SMPCommandError() +} + +@Serializable +sealed class SMPTransportError { + val string: String get() = when { + this is BadBlock -> "badBlock" + this is LargeMsg -> "largeMsg" + this is BadSession -> "badSession" + this is Handshake -> "handshake ${handshakeErr.string}" + else -> "SMPTransportError" + } + @Serializable @SerialName("badBlock") class BadBlock: SMPTransportError() + @Serializable @SerialName("largeMsg") class LargeMsg: SMPTransportError() + @Serializable @SerialName("badSession") class BadSession: SMPTransportError() + @Serializable @SerialName("handshake") class Handshake(val handshakeErr: SMPHandshakeError): SMPTransportError() +} + +@Serializable +sealed class SMPHandshakeError { + val string: String get() = when { + this is PARSE -> "PARSE" + this is VERSION -> "VERSION" + this is IDENTITY -> "IDENTITY" + else -> "SMPHandshakeError" + } + @Serializable @SerialName("PARSE") class PARSE: SMPHandshakeError() + @Serializable @SerialName("VERSION") class VERSION: SMPHandshakeError() + @Serializable @SerialName("IDENTITY") class IDENTITY: SMPHandshakeError() +} + +@Serializable +sealed class SMPAgentError { + val string: String get() = when { + this is A_MESSAGE -> "A_MESSAGE" + this is A_PROHIBITED -> "A_PROHIBITED" + this is A_VERSION -> "A_VERSION" + this is A_ENCRYPTION -> "A_ENCRYPTION" + else -> "SMPAgentError" + } + @Serializable @SerialName("A_MESSAGE") class A_MESSAGE: SMPAgentError() + @Serializable @SerialName("A_PROHIBITED") class A_PROHIBITED: SMPAgentError() + @Serializable @SerialName("A_VERSION") class A_VERSION: SMPAgentError() + @Serializable @SerialName("A_ENCRYPTION") class A_ENCRYPTION: SMPAgentError() +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt index 480339e0b8..8dfdd66316 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/TerminalView.kt @@ -57,7 +57,7 @@ fun DetailView(identifier: Long, terminalItems: List, navControlle Text("Back") } SelectionContainer { - Text((terminalItems.filter { it.id == identifier }).first().details) + Text((terminalItems.firstOrNull { it.id == identifier })?.details ?: "") } } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt new file mode 100644 index 0000000000..bb9036b428 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatInfoView.kt @@ -0,0 +1,130 @@ +package chat.simplex.app.views.chat + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.outlined.Circle +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import chat.simplex.app.Pages +import chat.simplex.app.model.* +import chat.simplex.app.ui.theme.* +import chat.simplex.app.views.helpers.* +import kotlinx.coroutines.DelicateCoroutinesApi + +@DelicateCoroutinesApi +@Composable +fun ChatInfoView(chatModel: ChatModel, nav: NavController) { + val chat = chatModel.chats.firstOrNull { it.id == chatModel.chatId.value } + if (chat != null) { + ChatInfoLayout(chat, + close = { nav.popBackStack() }, + deleteContact = { + chatModel.alertManager.showAlertMsg( + title = "Delete contact?", + text = "Contact and all messages will be deleted - this cannot be undone!", + confirmText = "Delete", + onConfirm = { + val cInfo = chat.chatInfo + withApi { + val r = chatModel.controller.apiDeleteChat(cInfo.chatType, cInfo.apiId) + if (r) { + chatModel.removeChat(cInfo.id) + nav.navigate(Pages.ChatList.route) + } + } + } + ) + } + ) + } +} + +@Composable +fun ChatInfoLayout(chat: Chat, close: () -> Unit, deleteContact: () -> Unit) { + Column(Modifier + .fillMaxSize() + .padding(horizontal = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CloseSheetBar(close) + Spacer(Modifier.size(48.dp)) + ChatInfoImage(chat, size = 192.dp) + val cInfo = chat.chatInfo + Text( + cInfo.displayName, style = MaterialTheme.typography.h1, + modifier = Modifier.padding(top = 32.dp).padding(bottom = 8.dp) + ) + Text( + cInfo.fullName, style = MaterialTheme.typography.h2, + modifier = Modifier.padding(bottom = 16.dp) + ) + + if (cInfo is ChatInfo.Direct) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Row(Modifier.padding(horizontal = 32.dp)) { + ServerImage(chat) + Text( + chat.serverInfo.networkStatus.statusString, + textAlign = TextAlign.Center, + modifier = Modifier.padding(start = 8.dp) + ) + } + Text( + chat.serverInfo.networkStatus.statusExplanation, + style = MaterialTheme.typography.body2, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 16.dp).padding(horizontal = 16.dp) + ) + } + + Spacer(Modifier.weight(1F)) + + Box(Modifier.padding(24.dp)) { + SimpleButton( + "Delete contact", icon = Icons.Outlined.Delete, + color = Color.Red, + click = deleteContact + ) + } + } + } +} + +@Composable +fun ServerImage(chat: Chat) { + val status = chat.serverInfo.networkStatus + when { + status is Chat.NetworkStatus.Connected -> + Icon(Icons.Filled.Circle, "Connected", tint = MaterialTheme.colors.primaryVariant) + status is Chat.NetworkStatus.Disconnected -> + Icon(Icons.Filled.Pending, "Disconnected", tint = HighOrLowlight) + status is Chat.NetworkStatus.Error -> + Icon(Icons.Filled.Error, "Error", tint = HighOrLowlight) + else -> + Icon(Icons.Outlined.Circle, "Pending", tint = HighOrLowlight) + } +} + +@Preview +@Composable +fun PreviewChatInfoLayout() { + SimpleXTheme { + ChatInfoLayout( + chat = Chat( + chatInfo = ChatInfo.Direct.sampleData, + chatItems = arrayListOf(), + serverInfo = Chat.ServerInfo(Chat.NetworkStatus.Error("agent BROKER TIMEOUT")) + ), + close = {}, deleteContact = {} + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index 9b7ab104f8..f5e93b8258 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -11,12 +11,15 @@ import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController +import chat.simplex.app.Pages import chat.simplex.app.model.* import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.chat.item.ChatItemView +import chat.simplex.app.views.helpers.ChatInfoImage import chat.simplex.app.views.helpers.withApi import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.datetime.Clock @@ -25,20 +28,26 @@ import kotlinx.datetime.Clock @Composable fun ChatView(chatModel: ChatModel, nav: NavController) { if (chatModel.chatId.value != null && chatModel.chats.count() > 0) { - val chat: Chat = chatModel.chats.first { chat -> chat.chatInfo.id == chatModel.chatId.value } - ChatLayout(chat, chatModel.chatItems, back = { nav.popBackStack() }, sendMessage = { msg -> - withApi { - // show "in progress" - val cInfo = chat.chatInfo - val newItem = chatModel.controller.apiSendMessage( - type = cInfo.chatType, - id = cInfo.apiId, - mc = MsgContent.MCText(msg) - ) - // hide "in progress" - if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem) - } - }) + val chat: Chat? = chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value } + if (chat != null) { + ChatLayout(chat, chatModel.chatItems, + back = { nav.popBackStack() }, + info = { nav.navigate(Pages.ChatInfo.route) }, + sendMessage = { msg -> + withApi { + // show "in progress" + val cInfo = chat.chatInfo + val newItem = chatModel.controller.apiSendMessage( + type = cInfo.chatType, + id = cInfo.apiId, + mc = MsgContent.MCText(msg) + ) + // hide "in progress" + if (newItem != null) chatModel.addChatItem(cInfo, newItem.chatItem) + } + } + ) + } } } @@ -46,10 +55,11 @@ fun ChatView(chatModel: ChatModel, nav: NavController) { fun ChatLayout( chat: Chat, chatItems: List, back: () -> Unit, + info: () -> Unit, sendMessage: (String) -> Unit ) { Scaffold( - topBar = { ChatInfoToolbar(chat, back) }, + topBar = { ChatInfoToolbar(chat, back, info) }, bottomBar = { SendMsgView(sendMessage) } ) { contentPadding -> Box( @@ -64,9 +74,10 @@ fun ChatLayout( } @Composable -fun ChatInfoToolbar(chat: Chat, back: () -> Unit) { - Box( - modifier = Modifier.fillMaxWidth(), +fun ChatInfoToolbar(chat: Chat, back: () -> Unit, info: () -> Unit) { + Box(Modifier + .fillMaxWidth() + .height(60.dp), contentAlignment = Alignment.CenterStart ) { Icon( @@ -77,14 +88,23 @@ fun ChatInfoToolbar(chat: Chat, back: () -> Unit) { .clickable(onClick = back) .padding(start = 16.dp) ) - Column( - Modifier - .padding(horizontal = 40.dp) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + Row(Modifier + .padding(horizontal = 40.dp) + .fillMaxWidth() + .clickable(onClick = info), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically ) { - Text(chat.chatInfo.displayName) - Text(chat.chatInfo.fullName) + val cInfo = chat.chatInfo + ChatInfoImage(chat, size = 40.dp) + Column(Modifier.padding(start = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(cInfo.displayName, fontWeight = FontWeight.Bold) + if (cInfo.fullName != "" && cInfo.fullName != cInfo.displayName) { + Text(cInfo.fullName) + } + } } } } @@ -127,6 +147,7 @@ fun PreviewChatViewLayout() { ), chatItems = chatItems, back = {}, + info = {}, sendMessage = {} ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt index aa2551efa6..eb5c59572f 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/item/CIMetaView.kt @@ -3,18 +3,18 @@ package chat.simplex.app.views.chat.item import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import chat.simplex.app.model.CIDirection import chat.simplex.app.model.ChatItem +import chat.simplex.app.ui.theme.HighOrLowlight import kotlinx.datetime.Clock @Composable fun CIMetaView(chatItem: ChatItem) { Text( chatItem.timestampText, - color = Color.Gray, - style = MaterialTheme.typography.caption + color = HighOrLowlight, + style = MaterialTheme.typography.body2 ) } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index 10d36a6663..38a8736056 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -64,13 +64,13 @@ fun ChatListView(chatModel: ChatModel, nav: NavController) { .background(MaterialTheme.colors.background) ) { ChatListToolbar(newChatCtrl) - ChatList(chatModel, nav) Button( onClick = { nav.navigate(Pages.Terminal.route) }, modifier = Modifier.padding(14.dp) ) { Text("Terminal") } + ChatList(chatModel, nav) } if (newChatCtrl.state.bottomSheetState.isExpanded) { Surface(Modifier diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt index a064d5e518..f48ddeba93 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatPreviewView.kt @@ -1,14 +1,11 @@ package chat.simplex.app.views.chatlist -import androidx.compose.foundation.* +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Person import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -16,6 +13,7 @@ import androidx.compose.ui.unit.dp import chat.simplex.app.model.* import chat.simplex.app.ui.theme.HighOrLowlight import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.helpers.ChatInfoImage @Composable fun ChatPreviewView(chat: Chat, goToChat: () -> Unit) { @@ -27,30 +25,19 @@ fun ChatPreviewView(chat: Chat, goToChat: () -> Unit) { .height(80.dp) ) { Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 14.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp), ) { Column(verticalArrangement = Arrangement.Center, modifier = Modifier.fillMaxHeight()) { - Icon( - Icons.Filled.Person, - contentDescription = "Avatar Placeholder", - tint = MaterialTheme.colors.background, - modifier = Modifier - .size(55.dp) - .clip(CircleShape) - .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) - .background(MaterialTheme.colors.secondary) - ) + ChatInfoImage(chat, size = 60.dp) } Spacer(modifier = Modifier.width(6.dp)) Column(modifier = Modifier.padding(all = 8.dp)) { Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) { Text(chat.chatInfo.chatViewName, fontWeight = FontWeight.Bold) - ( - if (chat.chatItems.count() > 0) { - Text(getTimestampText(chat.chatItems.last().meta.itemTs), color = HighOrLowlight) - } - else Text(getTimestampText(chat.chatInfo.createdAt), color = HighOrLowlight) - ) + val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.createdAt) + Text(ts, color = HighOrLowlight, style = MaterialTheme.typography.body2) } if (chat.chatItems.count() > 0) { Text( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt new file mode 100644 index 0000000000..4bcd4352b8 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/ChatInfoImage.kt @@ -0,0 +1,44 @@ +package chat.simplex.app.views.helpers + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.SupervisedUserCircle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import chat.simplex.app.model.Chat +import chat.simplex.app.model.ChatInfo +import chat.simplex.app.ui.theme.SimpleXTheme + +@Composable +fun ChatInfoImage(chat: Chat, size: Dp) { + val icon = + if (chat.chatInfo is ChatInfo.Group) Icons.Filled.SupervisedUserCircle + else Icons.Filled.AccountCircle + Box(Modifier.size(size)) { + Icon(icon, + contentDescription = "Avatar Placeholder", + tint = MaterialTheme.colors.secondary, + modifier = Modifier.fillMaxSize(), +// .clip(CircleShape) +// .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) +// .background(MaterialTheme.colors.secondary) + ) + } +} + +@Preview +@Composable +fun PreviewChatInfoImage() { + SimpleXTheme { + ChatInfoImage( + chat = Chat(chatInfo = ChatInfo.Direct.sampleData, chatItems = arrayListOf()), + size = 55.dp + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt index bd2f037c47..c507d8ae2e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/helpers/SimpleButton.kt @@ -9,24 +9,23 @@ import androidx.compose.material.icons.outlined.Share import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @Composable -fun SimpleButton(text: String, icon: ImageVector, click: () -> Unit) { +fun SimpleButton(text: String, icon: ImageVector, + color: Color = MaterialTheme.colors.primary, + click: () -> Unit) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.clickable { click() } ) { - Icon(icon, text, - tint = MaterialTheme.colors.primary, + Icon(icon, text, tint = color, modifier = Modifier.padding(horizontal = 10.dp) ) - Text(text, - style = MaterialTheme.typography.caption, - color = MaterialTheme.colors.primary - ) + Text(text, style = MaterialTheme.typography.caption, color = color) } } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 7f5f50876c..0cccd292e4 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -685,10 +685,10 @@ enum SMPCommandError: Decodable { } enum SMPTransportError: Decodable { - case TEBadBlock - case TELargeMsg - case TEBadSession - case TEHandshake(handshakeErr: SMPHandshakeError) + case badBlock + case largeMsg + case badSession + case handshake(handshakeErr: SMPHandshakeError) } enum SMPHandshakeError: Decodable { From c9cfead9bc28d25f7f1cb8ba2a6fa9db5ab806e0 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 21 Feb 2022 09:10:51 +0000 Subject: [PATCH 35/46] android: refactor sum types (#342) --- apps/android/.idea/codeStyles/Project.xml | 29 + .../.idea/codeStyles/codeStyleConfig.xml | 2 +- .../java/chat/simplex/app/model/SimpleXAPI.kt | 566 +++++++----------- 3 files changed, 246 insertions(+), 351 deletions(-) diff --git a/apps/android/.idea/codeStyles/Project.xml b/apps/android/.idea/codeStyles/Project.xml index 4bec4ea8ae..5d7a0c0f3e 100644 --- a/apps/android/.idea/codeStyles/Project.xml +++ b/apps/android/.idea/codeStyles/Project.xml @@ -1,5 +1,16 @@ + + + + \ No newline at end of file diff --git a/apps/android/.idea/codeStyles/codeStyleConfig.xml b/apps/android/.idea/codeStyles/codeStyleConfig.xml index a55e7a179b..79ee123c2b 100644 --- a/apps/android/.idea/codeStyles/codeStyleConfig.xml +++ b/apps/android/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,5 @@ - \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index 9771af6041..c005dfd02e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -275,96 +275,66 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert } // ChatCommand -abstract class CC { - abstract val cmdString: String - abstract val cmdType: String +sealed class CC { + class Console(val cmd: String): CC() + class ShowActiveUser: CC() + class CreateActiveUser(val profile: Profile): CC() + class StartChat: CC() + class ApiGetChats: CC() + class ApiGetChat(val type: ChatType, val id: Long): CC() + class ApiSendMessage(val type: ChatType, val id: Long, val mc: MsgContent): CC() + class AddContact: CC() + class Connect(val connReq: String): CC() + class ApiDeleteChat(val type: ChatType, val id: Long): CC() + class UpdateProfile(val profile: Profile): CC() + class CreateMyAddress: CC() + class DeleteMyAddress: CC() + class ShowMyAddress: CC() + class ApiAcceptContact(val contactReqId: Long): CC() + class ApiRejectContact(val contactReqId: Long): CC() + class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC() - class Console(val cmd: String): CC() { - override val cmdString get() = cmd - override val cmdType get() = "console command" + val cmdString: String get() = when (this) { + is Console -> cmd + is ShowActiveUser -> "/u" + is CreateActiveUser -> "/u ${profile.displayName} ${profile.fullName}" + is StartChat -> "/_start" + is ApiGetChats -> "/_get chats" + is ApiGetChat -> "/_get chat ${chatRef(type, id)} count=100" + is ApiSendMessage -> "/_send ${chatRef(type, id)} ${mc.cmdString}" + is AddContact -> "/connect" + is Connect -> "/connect $connReq" + is ApiDeleteChat -> "/_delete ${chatRef(type, id)}" + is UpdateProfile -> "/profile ${profile.displayName} ${profile.fullName}" + is CreateMyAddress -> "/address" + is DeleteMyAddress -> "/delete_address" + is ShowMyAddress -> "/show_address" + is ApiAcceptContact -> "/_accept $contactReqId" + is ApiRejectContact -> "/_reject $contactReqId" + is ApiChatRead -> "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}" } - class ShowActiveUser: CC() { - override val cmdString get() = "/u" - override val cmdType get() = "showActiveUser" + val cmdType: String get() = when (this) { + is Console -> "console command" + is ShowActiveUser -> "showActiveUser" + is CreateActiveUser -> "createActiveUser" + is StartChat -> "startChat" + is ApiGetChats -> "apiGetChats" + is ApiGetChat -> "apiGetChat" + is ApiSendMessage -> "apiSendMessage" + is AddContact -> "addContact" + is Connect -> "connect" + is ApiDeleteChat -> "apiDeleteChat" + is UpdateProfile -> "updateProfile" + is CreateMyAddress -> "createMyAddress" + is DeleteMyAddress -> "deleteMyAddress" + is ShowMyAddress -> "showMyAddress" + is ApiAcceptContact -> "apiAcceptContact" + is ApiRejectContact -> "apiRejectContact" + is ApiChatRead -> "apiChatRead" } - class CreateActiveUser(val profile: Profile): CC() { - override val cmdString get() = "/u ${profile.displayName} ${profile.fullName}" - override val cmdType get() = "createActiveUser" - } - - class StartChat: CC() { - override val cmdString get() = "/_start" - override val cmdType get() = "startChat" - } - - class ApiGetChats: CC() { - override val cmdString get() = "/_get chats" - override val cmdType get() = "apiGetChats" - } - - class ApiGetChat(val type: ChatType, val id: Long): CC() { - override val cmdString get() = "/_get chat ${chatRef(type, id)} count=100" - override val cmdType get() = "apiGetChat" - } - - class ApiSendMessage(val type: ChatType, val id: Long, val mc: MsgContent): CC() { - override val cmdString get() = "/_send ${chatRef(type, id)} ${mc.cmdString}" - override val cmdType get() = "apiGetChat" - } - - class AddContact: CC() { - override val cmdString get() = "/connect" - override val cmdType get() = "addContact" - } - - class Connect(val connReq: String): CC() { - override val cmdString get() = "/connect $connReq" - override val cmdType get() = "connect" - } - - class ApiDeleteChat(val type: ChatType, val id: Long): CC() { - override val cmdString get() = "/_delete ${chatRef(type, id)}" - override val cmdType get() = "apiDeleteChat" - } - - class UpdateProfile(val profile: Profile): CC() { - override val cmdString get() = "/profile ${profile.displayName} ${profile.fullName}" - override val cmdType get() = "updateProfile" - } - - class CreateMyAddress: CC() { - override val cmdString get() = "/address" - override val cmdType get() = "createMyAddress" - } - - class DeleteMyAddress: CC() { - override val cmdString get() = "/delete_address" - override val cmdType get() = "deleteMyAddress" - } - - class ShowMyAddress: CC() { - override val cmdString get() = "/show_address" - override val cmdType get() = "showMyAddress" - } - - class ApiAcceptContact(val contactReqId: Long): CC() { - override val cmdString get() = "/_accept $contactReqId" - override val cmdType get() = "apiAcceptContact" - } - - class ApiRejectContact(val contactReqId: Long): CC() { - override val cmdString get() = "/_reject $contactReqId" - override val cmdType get() = "apiRejectContact" - } - - class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC() { - override val cmdString get() = "/_read chat ${chatRef(type, id)} from=${range.from} to=${range.to}" - override val cmdType get() = "apiDeleteChat" - } - - data class ItemRange(val from: Long, val to: Long) + class ItemRange(val from: Long, val to: Long) companion object { fun chatRef(chatType: ChatType, id: Long) = "${chatType.type}${id}" @@ -400,199 +370,107 @@ class APIResponse(val resp: CR, val corr: String? = null) { // ChatResponse @Serializable sealed class CR { - abstract val responseType: String - abstract val details: String + @Serializable @SerialName("activeUser") class ActiveUser(val user: User): CR() + @Serializable @SerialName("chatStarted") class ChatStarted: CR() + @Serializable @SerialName("apiChats") class ApiChats(val chats: List): CR() + @Serializable @SerialName("apiChat") class ApiChat(val chat: Chat): CR() + @Serializable @SerialName("invitation") class Invitation(val connReqInvitation: String): CR() + @Serializable @SerialName("sentConfirmation") class SentConfirmation: CR() + @Serializable @SerialName("sentInvitation") class SentInvitation: CR() + @Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val contact: Contact): CR() + @Serializable @SerialName("contactDeleted") class ContactDeleted(val contact: Contact): CR() + @Serializable @SerialName("userProfileNoChange") class UserProfileNoChange: CR() + @Serializable @SerialName("userProfileUpdated") class UserProfileUpdated(val fromProfile: Profile, val toProfile: Profile): CR() + @Serializable @SerialName("userContactLink") class UserContactLink(val connReqContact: String): CR() + @Serializable @SerialName("userContactLinkCreated") class UserContactLinkCreated(val connReqContact: String): CR() + @Serializable @SerialName("userContactLinkDeleted") class UserContactLinkDeleted: CR() + @Serializable @SerialName("contactConnected") class ContactConnected(val contact: Contact): CR() + @Serializable @SerialName("receivedContactRequest") class ReceivedContactRequest(val contactRequest: UserContactRequest): CR() + @Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val contact: Contact): CR() + @Serializable @SerialName("contactRequestRejected") class ContactRequestRejected: CR() + @Serializable @SerialName("contactUpdated") class ContactUpdated(val toContact: Contact): CR() + @Serializable @SerialName("contactSubscribed") class ContactSubscribed(val contact: Contact): CR() + @Serializable @SerialName("contactDisconnected") class ContactDisconnected(val contact: Contact): CR() + @Serializable @SerialName("contactSubError") class ContactSubError(val contact: Contact, val chatError: ChatError): CR() + @Serializable @SerialName("groupSubscribed") class GroupSubscribed(val group: GroupInfo): CR() + @Serializable @SerialName("groupEmpty") class GroupEmpty(val group: GroupInfo): CR() + @Serializable @SerialName("userContactLinkSubscribed") class UserContactLinkSubscribed: CR() + @Serializable @SerialName("newChatItem") class NewChatItem(val chatItem: AChatItem): CR() + @Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val chatItem: AChatItem): CR() + @Serializable @SerialName("cmdOk") class CmdOk: CR() + @Serializable @SerialName("chatCmdError") class ChatCmdError(val chatError: ChatError): CR() + @Serializable @SerialName("chatError") class ChatRespError(val chatError: ChatError): CR() + @Serializable class Response(val type: String, val json: String): CR() + @Serializable class Invalid(val str: String): CR() - @Serializable @SerialName("activeUser") - class ActiveUser(val user: User): CR() { - override val responseType get() = "activeUser" - override val details get() = user.toString() + val responseType: String get() = when(this) { + is ActiveUser -> "activeUser" + is ChatStarted -> "chatStarted" + is ApiChats -> "apiChats" + is ApiChat -> "apiChats" + is Invitation -> "invitation" + is SentConfirmation -> "sentConfirmation" + is SentInvitation -> "sentInvitation" + is ContactAlreadyExists -> "contactAlreadyExists" + is ContactDeleted -> "contactDeleted" + is UserProfileNoChange -> "userProfileNoChange" + is UserProfileUpdated -> "userProfileUpdated" + is UserContactLink -> "userContactLink" + is UserContactLinkCreated -> "userContactLinkCreated" + is UserContactLinkDeleted -> "userContactLinkDeleted" + is ContactConnected -> "contactConnected" + is ReceivedContactRequest -> "receivedContactRequest" + is AcceptingContactRequest -> "acceptingContactRequest" + is ContactRequestRejected -> "contactRequestRejected" + is ContactUpdated -> "contactUpdated" + is ContactSubscribed -> "contactSubscribed" + is ContactDisconnected -> "contactDisconnected" + is ContactSubError -> "contactSubError" + is GroupSubscribed -> "groupSubscribed" + is GroupEmpty -> "groupEmpty" + is UserContactLinkSubscribed -> "userContactLinkSubscribed" + is NewChatItem -> "newChatItem" + is ChatItemUpdated -> "chatItemUpdated" + is CmdOk -> "cmdOk" + is ChatCmdError -> "chatCmdError" + is ChatRespError -> "chatError" + is Response -> "* $type" + is Invalid -> "* invalid json" } - @Serializable @SerialName("chatStarted") - class ChatStarted: CR() { - override val responseType get() = "chatStarted" - override val details get() = noDetails() - } - - @Serializable @SerialName("apiChats") - class ApiChats(val chats: List): CR() { - override val responseType get() = "apiChats" - override val details get() = chats.toString() - } - - @Serializable @SerialName("apiChat") - class ApiChat(val chat: Chat): CR() { - override val responseType get() = "apiChats" - override val details get() = chat.toString() - } - - @Serializable @SerialName("invitation") - class Invitation(val connReqInvitation: String): CR() { - override val responseType get() = "invitation" - override val details get() = connReqInvitation - } - - @Serializable @SerialName("sentConfirmation") - class SentConfirmation: CR() { - override val responseType get() = "sentConfirmation" - override val details get() = noDetails() - } - - @Serializable @SerialName("sentInvitation") - class SentInvitation: CR() { - override val responseType get() = "sentInvitation" - override val details get() = noDetails() - } - - @Serializable @SerialName("contactAlreadyExists") - class ContactAlreadyExists(val contact: Contact): CR() { - override val responseType get() = "contactAlreadyExists" - override val details get() = contact.toString() - } - - @Serializable @SerialName("contactDeleted") - class ContactDeleted(val contact: Contact): CR() { - override val responseType get() = "contactDeleted" - override val details get() = contact.toString() - } - - @Serializable @SerialName("userProfileNoChange") - class UserProfileNoChange: CR() { - override val responseType get() = "userProfileNoChange" - override val details get() = noDetails() - } - - @Serializable @SerialName("userProfileUpdated") - class UserProfileUpdated(val fromProfile: Profile, val toProfile: Profile): CR() { - override val responseType get() = "userProfileUpdated" - override val details get() = toProfile.toString() - } - - @Serializable @SerialName("userContactLink") - class UserContactLink(val connReqContact: String): CR() { - override val responseType get() = "userContactLink" - override val details get() = connReqContact - } - - @Serializable @SerialName("userContactLinkCreated") - class UserContactLinkCreated(val connReqContact: String): CR() { - override val responseType get() = "userContactLinkCreated" - override val details get() = connReqContact - } - - @Serializable @SerialName("userContactLinkDeleted") - class UserContactLinkDeleted: CR() { - override val responseType get() = "userContactLinkDeleted" - override val details get() = noDetails() - } - - @Serializable @SerialName("contactConnected") - class ContactConnected(val contact: Contact): CR() { - override val responseType get() = "contactConnected" - override val details get() = contact.toString() - } - - @Serializable @SerialName("receivedContactRequest") - class ReceivedContactRequest(val contactRequest: UserContactRequest): CR() { - override val responseType get() = "receivedContactRequest" - override val details get() = contactRequest.toString() - } - - @Serializable @SerialName("acceptingContactRequest") - class AcceptingContactRequest(val contact: Contact): CR() { - override val responseType get() = "acceptingContactRequest" - override val details get() = contact.toString() - } - - @Serializable @SerialName("contactRequestRejected") - class ContactRequestRejected: CR() { - override val responseType get() = "contactRequestRejected" - override val details get() = noDetails() - } - - @Serializable @SerialName("contactUpdated") - class ContactUpdated(val toContact: Contact): CR() { - override val responseType get() = "contactUpdated" - override val details get() = toContact.toString() - } - - @Serializable @SerialName("contactSubscribed") - class ContactSubscribed(val contact: Contact): CR() { - override val responseType get() = "contactSubscribed" - override val details get() = contact.toString() - } - - @Serializable @SerialName("contactDisconnected") - class ContactDisconnected(val contact: Contact): CR() { - override val responseType get() = "contactDisconnected" - override val details get() = contact.toString() - } - - @Serializable @SerialName("contactSubError") - class ContactSubError(val contact: Contact, val chatError: ChatError): CR() { - override val responseType get() = "contactSubError" - override val details get() = "contact:\n${contact.toString()}\nerror:\n${chatError.toString()}" - } - - @Serializable @SerialName("groupSubscribed") - class GroupSubscribed(val group: GroupInfo): CR() { - override val responseType get() = "groupSubscribed" - override val details get() = group.toString() - } - - @Serializable @SerialName("groupEmpty") - class GroupEmpty(val group: GroupInfo): CR() { - override val responseType get() = "groupEmpty" - override val details get() = group.toString() - } - - @Serializable @SerialName("userContactLinkSubscribed") - class UserContactLinkSubscribed: CR() { - override val responseType get() = "userContactLinkSubscribed" - override val details get() = noDetails() - } - - @Serializable @SerialName("newChatItem") - class NewChatItem(val chatItem: AChatItem): CR() { - override val responseType get() = "newChatItem" - override val details get() = chatItem.toString() - } - - @Serializable @SerialName("chatItemUpdated") - class ChatItemUpdated(val chatItem: AChatItem): CR() { - override val responseType get() = "chatItemUpdated" - override val details get() = chatItem.toString() - } - - @Serializable @SerialName("cmdOk") - class CmdOk: CR() { - override val responseType get() = "cmdOk" - override val details get() = noDetails() - } - - @Serializable @SerialName("chatCmdError") - class ChatCmdError(val chatError: ChatError): CR() { - override val responseType get() = "chatCmdError" - override val details get() = chatError.string - } - - @Serializable @SerialName("chatError") - class ChatRespError(val chatError: ChatError): CR() { - override val responseType get() = "chatError" - override val details get() = chatError.string - } - - @Serializable - class Response(val type: String, val json: String): CR() { - override val responseType get() = "* $type" - override val details get() = json - } - - @Serializable - class Invalid(val str: String): CR() { - override val responseType get() = "* invalid json" - override val details get() = str + val details: String get() = when(this) { + is ActiveUser -> json.encodeToString(user) + is ChatStarted -> noDetails() + is ApiChats -> json.encodeToString(chats) + is ApiChat -> json.encodeToString(chat) + is Invitation -> connReqInvitation + is SentConfirmation -> noDetails() + is SentInvitation -> noDetails() + is ContactAlreadyExists -> json.encodeToString(contact) + is ContactDeleted -> json.encodeToString(contact) + is UserProfileNoChange -> noDetails() + is UserProfileUpdated -> json.encodeToString(toProfile) + is UserContactLink -> connReqContact + is UserContactLinkCreated -> connReqContact + is UserContactLinkDeleted -> noDetails() + is ContactConnected -> json.encodeToString(contact) + is ReceivedContactRequest -> json.encodeToString(contactRequest) + is AcceptingContactRequest -> json.encodeToString(contact) + is ContactRequestRejected -> noDetails() + is ContactUpdated -> json.encodeToString(toContact) + is ContactSubscribed -> json.encodeToString(contact) + is ContactDisconnected -> json.encodeToString(contact) + is ContactSubError -> "error:\n${chatError.string}\ncontact:\n${json.encodeToString(contact)}" + is GroupSubscribed -> json.encodeToString(group) + is GroupEmpty -> json.encodeToString(group) + is UserContactLinkSubscribed -> noDetails() + is NewChatItem -> json.encodeToString(chatItem) + is ChatItemUpdated -> json.encodeToString(chatItem) + is CmdOk -> noDetails() + is ChatCmdError -> chatError.string + is ChatRespError -> chatError.string + is Response -> json + is Invalid -> str } fun noDetails(): String ="${responseType}: no details" @@ -627,11 +505,10 @@ abstract class TerminalItem { @Serializable sealed class ChatError { - val string: String get() = when { - this is ChatErrorChat -> "chat ${errorType.string}" - this is ChatErrorAgent -> "agent ${agentError.string}" - this is ChatErrorStore -> "store ${storeError.string}" - else -> "ChatError" + val string: String get() = when (this) { + is ChatErrorChat -> "chat ${errorType.string}" + is ChatErrorAgent -> "agent ${agentError.string}" + is ChatErrorStore -> "store ${storeError.string}" } @Serializable @SerialName("error") class ChatErrorChat(val errorType: ChatErrorType): ChatError() @Serializable @SerialName("errorAgent") class ChatErrorAgent(val agentError: AgentErrorType): ChatError() @@ -640,10 +517,9 @@ sealed class ChatError { @Serializable sealed class ChatErrorType { - val string: String get() = when { - this is InvalidConnReq -> "invalidConnReq" - this is ContactGroups -> "groupNames $groupNames" - else -> "ChatErrorType" + val string: String get() = when (this) { + is InvalidConnReq -> "invalidConnReq" + is ContactGroups -> "groupNames $groupNames" } @Serializable @SerialName("invalidConnReq") class InvalidConnReq: ChatErrorType() @Serializable @SerialName("contactGroups") class ContactGroups(val contact: Contact, val groupNames: List): ChatErrorType() @@ -651,23 +527,21 @@ sealed class ChatErrorType { @Serializable sealed class StoreError { - val string: String get() = when { - this is UserContactLinkNotFound -> "userContactLinkNotFound" - else -> "StoreError" + val string: String get() = when (this) { + is UserContactLinkNotFound -> "userContactLinkNotFound" } @Serializable @SerialName("userContactLinkNotFound") class UserContactLinkNotFound: StoreError() } @Serializable sealed class AgentErrorType { - val string: String get() = when { - this is CMD -> "CMD ${cmdErr.string}" - this is CONN -> "CONN ${connErr.string}" - this is SMP -> "SMP ${smpErr.string}" - this is BROKER -> "BROKER ${brokerErr.string}" - this is AGENT -> "AGENT ${agentErr.string}" - this is INTERNAL -> "INTERNAL $internalErr" - else -> "AgentErrorType" + val string: String get() = when (this) { + is CMD -> "CMD ${cmdErr.string}" + is CONN -> "CONN ${connErr.string}" + is SMP -> "SMP ${smpErr.string}" + is BROKER -> "BROKER ${brokerErr.string}" + is AGENT -> "AGENT ${agentErr.string}" + is INTERNAL -> "INTERNAL $internalErr" } @Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): AgentErrorType() @Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType() @@ -679,13 +553,12 @@ sealed class AgentErrorType { @Serializable sealed class CommandErrorType { - val string: String get() = when { - this is PROHIBITED -> "PROHIBITED" - this is SYNTAX -> "SYNTAX" - this is NO_CONN -> "NO_CONN" - this is SIZE -> "SIZE" - this is LARGE -> "LARGE" - else -> "CommandErrorType" + val string: String get() = when (this) { + is PROHIBITED -> "PROHIBITED" + is SYNTAX -> "SYNTAX" + is NO_CONN -> "NO_CONN" + is SIZE -> "SIZE" + is LARGE -> "LARGE" } @Serializable @SerialName("PROHIBITED") class PROHIBITED: CommandErrorType() @Serializable @SerialName("SYNTAX") class SYNTAX: CommandErrorType() @@ -696,13 +569,12 @@ sealed class CommandErrorType { @Serializable sealed class ConnectionErrorType { - val string: String get() = when { - this is NOT_FOUND -> "NOT_FOUND" - this is DUPLICATE -> "DUPLICATE" - this is SIMPLEX -> "SIMPLEX" - this is NOT_ACCEPTED -> "NOT_ACCEPTED" - this is NOT_AVAILABLE -> "NOT_AVAILABLE" - else -> "ConnectionErrorType" + val string: String get() = when (this) { + is NOT_FOUND -> "NOT_FOUND" + is DUPLICATE -> "DUPLICATE" + is SIMPLEX -> "SIMPLEX" + is NOT_ACCEPTED -> "NOT_ACCEPTED" + is NOT_AVAILABLE -> "NOT_AVAILABLE" } @Serializable @SerialName("NOT_FOUND") class NOT_FOUND: ConnectionErrorType() @Serializable @SerialName("DUPLICATE") class DUPLICATE: ConnectionErrorType() @@ -713,13 +585,12 @@ sealed class ConnectionErrorType { @Serializable sealed class BrokerErrorType { - val string: String get() = when { - this is RESPONSE -> "RESPONSE ${smpErr.string}" - this is UNEXPECTED -> "UNEXPECTED" - this is NETWORK -> "NETWORK" - this is TRANSPORT -> "TRANSPORT ${transportErr.string}" - this is TIMEOUT -> "TIMEOUT" - else -> "BrokerErrorType" + val string: String get() = when (this) { + is RESPONSE -> "RESPONSE ${smpErr.string}" + is UNEXPECTED -> "UNEXPECTED" + is NETWORK -> "NETWORK" + is TRANSPORT -> "TRANSPORT ${transportErr.string}" + is TIMEOUT -> "TIMEOUT" } @Serializable @SerialName("RESPONSE") class RESPONSE(val smpErr: SMPErrorType): BrokerErrorType() @Serializable @SerialName("UNEXPECTED") class UNEXPECTED: BrokerErrorType() @@ -730,16 +601,15 @@ sealed class BrokerErrorType { @Serializable sealed class SMPErrorType { - val string: String get() = when { - this is BLOCK -> "BLOCK" - this is SESSION -> "SESSION" - this is CMD -> "CMD ${cmdErr.string}" - this is AUTH -> "AUTH" - this is QUOTA -> "QUOTA" - this is NO_MSG -> "NO_MSG" - this is LARGE_MSG -> "LARGE_MSG" - this is INTERNAL -> "INTERNAL" - else -> "SMPErrorType" + val string: String get() = when (this) { + is BLOCK -> "BLOCK" + is SESSION -> "SESSION" + is CMD -> "CMD ${cmdErr.string}" + is AUTH -> "AUTH" + is QUOTA -> "QUOTA" + is NO_MSG -> "NO_MSG" + is LARGE_MSG -> "LARGE_MSG" + is INTERNAL -> "INTERNAL" } @Serializable @SerialName("BLOCK") class BLOCK: SMPErrorType() @Serializable @SerialName("SESSION") class SESSION: SMPErrorType() @@ -753,13 +623,12 @@ sealed class SMPErrorType { @Serializable sealed class SMPCommandError { - val string: String get() = when { - this is UNKNOWN -> "UNKNOWN" - this is SYNTAX -> "SYNTAX" - this is NO_AUTH -> "NO_AUTH" - this is HAS_AUTH -> "HAS_AUTH" - this is NO_QUEUE -> "NO_QUEUE" - else -> "SMPCommandError" + val string: String get() = when (this) { + is UNKNOWN -> "UNKNOWN" + is SYNTAX -> "SYNTAX" + is NO_AUTH -> "NO_AUTH" + is HAS_AUTH -> "HAS_AUTH" + is NO_QUEUE -> "NO_QUEUE" } @Serializable @SerialName("UNKNOWN") class UNKNOWN: SMPCommandError() @Serializable @SerialName("SYNTAX") class SYNTAX: SMPCommandError() @@ -770,12 +639,11 @@ sealed class SMPCommandError { @Serializable sealed class SMPTransportError { - val string: String get() = when { - this is BadBlock -> "badBlock" - this is LargeMsg -> "largeMsg" - this is BadSession -> "badSession" - this is Handshake -> "handshake ${handshakeErr.string}" - else -> "SMPTransportError" + val string: String get() = when (this) { + is BadBlock -> "badBlock" + is LargeMsg -> "largeMsg" + is BadSession -> "badSession" + is Handshake -> "handshake ${handshakeErr.string}" } @Serializable @SerialName("badBlock") class BadBlock: SMPTransportError() @Serializable @SerialName("largeMsg") class LargeMsg: SMPTransportError() @@ -785,11 +653,10 @@ sealed class SMPTransportError { @Serializable sealed class SMPHandshakeError { - val string: String get() = when { - this is PARSE -> "PARSE" - this is VERSION -> "VERSION" - this is IDENTITY -> "IDENTITY" - else -> "SMPHandshakeError" + val string: String get() = when (this) { + is PARSE -> "PARSE" + is VERSION -> "VERSION" + is IDENTITY -> "IDENTITY" } @Serializable @SerialName("PARSE") class PARSE: SMPHandshakeError() @Serializable @SerialName("VERSION") class VERSION: SMPHandshakeError() @@ -798,12 +665,11 @@ sealed class SMPHandshakeError { @Serializable sealed class SMPAgentError { - val string: String get() = when { - this is A_MESSAGE -> "A_MESSAGE" - this is A_PROHIBITED -> "A_PROHIBITED" - this is A_VERSION -> "A_VERSION" - this is A_ENCRYPTION -> "A_ENCRYPTION" - else -> "SMPAgentError" + val string: String get() = when (this) { + is A_MESSAGE -> "A_MESSAGE" + is A_PROHIBITED -> "A_PROHIBITED" + is A_VERSION -> "A_VERSION" + is A_ENCRYPTION -> "A_ENCRYPTION" } @Serializable @SerialName("A_MESSAGE") class A_MESSAGE: SMPAgentError() @Serializable @SerialName("A_PROHIBITED") class A_PROHIBITED: SMPAgentError() From 739990c732e431a3fd8841b69c7e5dc55d7e34ee Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Mon, 21 Feb 2022 12:05:00 +0000 Subject: [PATCH 36/46] terminal: make input responsible for echo to keep commands synchronous (as in mobile) and avoid echo delays (#343) * terminal: make input responsible for echo to keep commands synchronous (as in mobile) and avoid echo delays * use echo Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> Co-authored-by: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> --- src/Simplex/Chat/Mobile.hs | 1 - src/Simplex/Chat/Terminal/Input.hs | 24 +++++++- src/Simplex/Chat/Terminal/Output.hs | 2 +- src/Simplex/Chat/View.hs | 96 ++++++++++++++--------------- tests/ChatTests.hs | 1 - 5 files changed, 68 insertions(+), 56 deletions(-) diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 1d1c94a1fa..577cdf8ca3 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -1,6 +1,5 @@ {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} -{-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} diff --git a/src/Simplex/Chat/Terminal/Input.hs b/src/Simplex/Chat/Terminal/Input.hs index e401b4b8c0..833911acd9 100644 --- a/src/Simplex/Chat/Terminal/Input.hs +++ b/src/Simplex/Chat/Terminal/Input.hs @@ -5,15 +5,19 @@ module Simplex.Chat.Terminal.Input where -import Control.Monad.IO.Unlift +import Control.Monad.Except import Control.Monad.Reader +import qualified Data.ByteString.Char8 as B +import Data.Char (isSpace) import Data.List (dropWhileEnd) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Simplex.Chat import Simplex.Chat.Controller +import Simplex.Chat.Styled import Simplex.Chat.Terminal.Output import Simplex.Chat.View +import Simplex.Messaging.Parsers (parseAll) import System.Exit (exitSuccess) import System.Terminal hiding (insertChars) import UnliftIO.STM @@ -28,9 +32,23 @@ getKey = runInputLoop :: ChatTerminal -> ChatController -> IO () runInputLoop ct cc = forever $ do s <- atomically . readTBQueue $ inputQ cc - r <- runReaderT (execChatCommand . encodeUtf8 $ T.pack s) cc + let bs = encodeUtf8 $ T.pack s + cmd = parseAll chatCommandP $ B.dropWhileEnd isSpace bs + unless (isMessage cmd) $ echo s + r <- runReaderT (execChatCommand bs) cc + case r of + CRChatCmdError _ -> when (isMessage cmd) $ echo s + _ -> pure () let testV = testView $ config cc - printToTerminal ct $ responseToView s testV r + printToTerminal ct $ responseToView testV r + where + echo s = printToTerminal ct [plain s] + isMessage = \case + Right SendMessage {} -> True + Right SendGroupMessage {} -> True + Right SendFile {} -> True + Right SendGroupFile {} -> True + _ -> False runTerminalInput :: ChatTerminal -> ChatController -> IO () runTerminalInput ct cc = withChatTerm ct $ do diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs index 79526e624f..d793266af3 100644 --- a/src/Simplex/Chat/Terminal/Output.hs +++ b/src/Simplex/Chat/Terminal/Output.hs @@ -76,7 +76,7 @@ runTerminalOutput :: ChatTerminal -> ChatController -> IO () runTerminalOutput ct cc = do let testV = testView $ config cc forever $ - atomically (readTBQueue $ outputQ cc) >>= printToTerminal ct . responseToView "" testV . snd + atomically (readTBQueue $ outputQ cc) >>= printToTerminal ct . responseToView testV . snd printToTerminal :: ChatTerminal -> [StyledString] -> IO () printToTerminal ct s = diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index dc77335354..1201c54758 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -30,58 +30,58 @@ import qualified Simplex.Messaging.Protocol as SMP import System.Console.ANSI.Types serializeChatResponse :: ChatResponse -> String -serializeChatResponse = unlines . map unStyle . responseToView "" False +serializeChatResponse = unlines . map unStyle . responseToView False -responseToView :: String -> Bool -> ChatResponse -> [StyledString] -responseToView cmd testView = \case - CRActiveUser User {profile} -> r $ viewUserProfile profile - CRChatStarted -> r ["chat started"] - CRApiChats chats -> r $ if testView then testViewChats chats else [sShow chats] - CRApiChat chat -> r $ if testView then testViewChat chat else [sShow chat] +responseToView :: Bool -> ChatResponse -> [StyledString] +responseToView testView = \case + CRActiveUser User {profile} -> viewUserProfile profile + CRChatStarted -> ["chat started"] + CRApiChats chats -> if testView then testViewChats chats else [sShow chats] + CRApiChat chat -> if testView then testViewChat chat else [sShow chat] CRNewChatItem (AChatItem _ _ chat item) -> viewChatItem chat item CRChatItemUpdated _ -> [] CRMsgIntegrityError mErr -> viewMsgIntegrityError mErr - CRCmdAccepted _ -> r [] - CRCmdOk -> r ["ok"] + CRCmdAccepted _ -> [] + CRCmdOk -> ["ok"] CRChatHelp section -> case section of - HSMain -> r chatHelpInfo - HSFiles -> r filesHelpInfo - HSGroups -> r groupsHelpInfo - HSMyAddress -> r myAddressHelpInfo - HSMarkdown -> r markdownInfo - CRWelcome user -> r $ chatWelcome user - CRContactsList cs -> r $ viewContactsList cs - CRUserContactLink cReqUri _ -> r $ connReqContact_ "Your chat address:" cReqUri - CRUserContactLinkUpdated _ autoAccept -> r ["auto_accept " <> if autoAccept then "on" else "off"] - CRContactRequestRejected UserContactRequest {localDisplayName = c} -> r [ttyContact c <> ": contact request rejected"] - CRGroupCreated g -> r $ viewGroupCreated g - CRGroupMembers g -> r $ viewGroupMembers g - CRGroupsList gs -> r $ viewGroupsList gs - CRSentGroupInvitation g c -> r ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c] - CRFileTransferStatus ftStatus -> r $ viewFileTransferStatus ftStatus - CRUserProfile p -> r $ viewUserProfile p - CRUserProfileNoChange -> r ["user profile did not change"] - CRVersionInfo _ -> r [plain versionStr, plain updateStr] - CRChatCmdError e -> r $ viewChatError e - CRInvitation cReq -> r' $ viewConnReqInvitation cReq - CRSentConfirmation -> r' ["confirmation sent!"] - CRSentInvitation -> r' ["connection request sent!"] - CRContactDeleted c -> r' [ttyContact' c <> ": contact is deleted"] - CRAcceptingContactRequest c -> r [ttyFullContact c <> ": accepting contact request..."] - CRContactAlreadyExists c -> r [ttyFullContact c <> ": contact already exists"] - CRContactRequestAlreadyAccepted c -> r' [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] - CRUserContactLinkCreated cReq -> r' $ connReqContact_ "Your new chat address is created!" cReq - CRUserContactLinkDeleted -> r' viewUserContactLinkDeleted - CRUserAcceptedGroupSent _g -> r' [] -- [ttyGroup' g <> ": joining the group..."] - CRUserDeletedMember g m -> r' [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"] - CRLeftMemberUser g -> r' $ [ttyGroup' g <> ": you left the group"] <> groupPreserved g - CRGroupDeletedUser g -> r' [ttyGroup' g <> ": you deleted the group"] + HSMain -> chatHelpInfo + HSFiles -> filesHelpInfo + HSGroups -> groupsHelpInfo + HSMyAddress -> myAddressHelpInfo + HSMarkdown -> markdownInfo + CRWelcome user -> chatWelcome user + CRContactsList cs -> viewContactsList cs + CRUserContactLink cReqUri _ -> connReqContact_ "Your chat address:" cReqUri + CRUserContactLinkUpdated _ autoAccept -> ["auto_accept " <> if autoAccept then "on" else "off"] + CRContactRequestRejected UserContactRequest {localDisplayName = c} -> [ttyContact c <> ": contact request rejected"] + CRGroupCreated g -> viewGroupCreated g + CRGroupMembers g -> viewGroupMembers g + CRGroupsList gs -> viewGroupsList gs + CRSentGroupInvitation g c -> ["invitation to join the group " <> ttyGroup' g <> " sent to " <> ttyContact' c] + CRFileTransferStatus ftStatus -> viewFileTransferStatus ftStatus + CRUserProfile p -> viewUserProfile p + CRUserProfileNoChange -> ["user profile did not change"] + CRVersionInfo _ -> [plain versionStr, plain updateStr] + CRChatCmdError e -> viewChatError e + CRInvitation cReq -> viewConnReqInvitation cReq + CRSentConfirmation -> ["confirmation sent!"] + CRSentInvitation -> ["connection request sent!"] + CRContactDeleted c -> [ttyContact' c <> ": contact is deleted"] + CRAcceptingContactRequest c -> [ttyFullContact c <> ": accepting contact request..."] + CRContactAlreadyExists c -> [ttyFullContact c <> ": contact already exists"] + CRContactRequestAlreadyAccepted c -> [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] + CRUserContactLinkCreated cReq -> connReqContact_ "Your new chat address is created!" cReq + CRUserContactLinkDeleted -> viewUserContactLinkDeleted + CRUserAcceptedGroupSent _g -> [] -- [ttyGroup' g <> ": joining the group..."] + CRUserDeletedMember g m -> [ttyGroup' g <> ": you removed " <> ttyMember m <> " from the group"] + CRLeftMemberUser g -> [ttyGroup' g <> ": you left the group"] <> groupPreserved g + CRGroupDeletedUser g -> [ttyGroup' g <> ": you deleted the group"] CRRcvFileAccepted RcvFileTransfer {fileId, senderDisplayName = c} filePath -> - r' ["saving file " <> sShow fileId <> " from " <> ttyContact c <> " to " <> plain filePath] - CRRcvFileAcceptedSndCancelled ft -> r' $ viewRcvFileSndCancelled ft - CRSndGroupFileCancelled fts -> r' $ viewSndGroupFileCancelled fts - CRRcvFileCancelled ft -> r' $ receivingFile_ "cancelled" ft - CRUserProfileUpdated p p' -> r' $ viewUserProfileUpdated p p' + ["saving file " <> sShow fileId <> " from " <> ttyContact c <> " to " <> plain filePath] + CRRcvFileAcceptedSndCancelled ft -> viewRcvFileSndCancelled ft + CRSndGroupFileCancelled fts -> viewSndGroupFileCancelled fts + CRRcvFileCancelled ft -> receivingFile_ "cancelled" ft + CRUserProfileUpdated p p' -> viewUserProfileUpdated p p' CRContactUpdated c c' -> viewContactUpdated c c' CRContactsMerged intoCt mergedCt -> viewContactsMerged intoCt mergedCt CRReceivedContactRequest UserContactRequest {localDisplayName = c, profile} -> viewReceivedContactRequest c profile @@ -123,10 +123,6 @@ responseToView cmd testView = \case CRMessageError prefix err -> [plain prefix <> ": " <> plain err] CRChatError e -> viewChatError e where - r = (plain cmd :) - -- this function should be `r` for "synchronous", `id` for "asynchronous" command responses - r' = r - -- r' = id testViewChats :: [AChat] -> [StyledString] testViewChats chats = [sShow $ map toChatView chats] where diff --git a/tests/ChatTests.hs b/tests/ChatTests.hs index f10bba0d82..9e7ecb1404 100644 --- a/tests/ChatTests.hs +++ b/tests/ChatTests.hs @@ -744,7 +744,6 @@ testUserContactLinkAutoAccept = cath ##> ("/c " <> cLink) cath <## "connection request sent!" - alice <## "" alice <## "cath (Catherine): accepting contact request..." concurrently_ (cath <## "alice (Alice): contact is connected") From 1edf60362e111fb79820929ba465f43bae72a835 Mon Sep 17 00:00:00 2001 From: Efim Poberezkin <8711996+efim-poberezkin@users.noreply.github.com> Date: Tue, 22 Feb 2022 00:09:51 +0400 Subject: [PATCH 37/46] android: UserProfileView (#341) * android: update user profile view logic * indentation * format * UserProfileView * remove prints * empty line * undo format * change by value * separate layout * layout * unconditionally editProfile = false * add header and close button to profile page, add links to "settings" * use generic navigate in settings, remove terminal button from the list of chats Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../.idea/codeStyles/codeStyleConfig.xml | 1 + .../java/chat/simplex/app/MainActivity.kt | 20 +- .../java/chat/simplex/app/model/ChatModel.kt | 13 +- .../java/chat/simplex/app/model/SimpleXAPI.kt | 23 ++- .../chat/simplex/app/views/chat/ChatView.kt | 4 +- .../app/views/chatlist/ChatListView.kt | 19 +- .../app/views/usersettings/SettingsView.kt | 115 ++++++----- .../app/views/usersettings/UserProfileView.kt | 191 ++++++++++++++++++ 8 files changed, 306 insertions(+), 80 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt diff --git a/apps/android/.idea/codeStyles/codeStyleConfig.xml b/apps/android/.idea/codeStyles/codeStyleConfig.xml index 79ee123c2b..6e6eec1148 100644 --- a/apps/android/.idea/codeStyles/codeStyleConfig.xml +++ b/apps/android/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,6 @@ \ No newline at end of file diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index ac74e93a0d..ed237c9df2 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -21,6 +21,8 @@ import chat.simplex.app.views.chat.ChatView import chat.simplex.app.views.chatlist.ChatListView import chat.simplex.app.views.helpers.withApi import chat.simplex.app.views.newchat.* +import chat.simplex.app.views.usersettings.SettingsView +import chat.simplex.app.views.usersettings.UserProfileView import com.google.accompanist.permissions.ExperimentalPermissionsApi import kotlinx.coroutines.DelicateCoroutinesApi @@ -42,7 +44,7 @@ class MainActivity: ComponentActivity() { } @DelicateCoroutinesApi -class SimplexViewModel(application: Application) : AndroidViewModel(application) { +class SimplexViewModel(application: Application): AndroidViewModel(application) { val chatModel = getApplication().chatModel } @@ -103,6 +105,12 @@ fun Navigation(chatModel: ChatModel) { } ) ) { entry -> DetailView(entry.arguments!!.getLong("identifier"), chatModel.terminalItems, nav) } + composable(route = Pages.Settings.route) { + SettingsView(chatModel, nav) + } + composable(route = Pages.UserProfile.route) { + UserProfileView(chatModel, nav) + } } val am = chatModel.alertManager if (am.presentAlert.value) am.alertView.value?.invoke() @@ -110,15 +118,17 @@ fun Navigation(chatModel: ChatModel) { } sealed class Pages(val route: String) { - object Home : Pages("home") - object Terminal : Pages("terminal") - object Welcome : Pages("welcome") - object TerminalItemDetails : Pages("details") + object Home: Pages("home") + object Terminal: Pages("terminal") + object Welcome: Pages("welcome") + object TerminalItemDetails: Pages("details") object ChatList: Pages("chats") object Chat: Pages("chat") object AddContact: Pages("add_contact") object Connect: Pages("connect") object ChatInfo: Pages("chat_info") + object Settings: Pages("settings") + object UserProfile: Pages("user_profile") } @DelicateCoroutinesApi diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index cddc588055..f634e1101b 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -7,10 +7,6 @@ import chat.simplex.app.SimplexApp import kotlinx.datetime.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlin.Boolean -import kotlin.Int -import kotlin.Long -import kotlin.String class ChatModel(val controller: ChatController, val alertManager: SimplexApp.AlertManager) { var currentUser = mutableStateOf(null) @@ -23,6 +19,13 @@ class ChatModel(val controller: ChatController, val alertManager: SimplexApp.Ale // set when app is opened via contact or invitation URI var appOpenUrl = mutableStateOf(null) + fun updateUserProfile(profile: Profile) { + val user = currentUser.value + if (user != null) { + currentUser.value = user.copy(profile = profile) + } + } + fun hasChat(id: String): Boolean = chats.firstOrNull() { it.id == id } != null fun getChat(id: String): Chat? = chats.firstOrNull { it.id == id } private fun getChatIndex(id: String): Int = chats.indexOfFirst { it.id == id } @@ -178,7 +181,7 @@ enum class ChatType(val type: String) { } @Serializable -class User ( +data class User( val userId: Long, val userContactId: Long, val localDisplayName: String, diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index c005dfd02e..b513098587 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -211,25 +211,28 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert fun processReceivedMsg(r: CR) { chatModel.terminalItems.add(TerminalItem.resp(r)) - when { - r is CR.ContactConnected -> chatModel.updateContact(r.contact) -// r is CR.ReceivedContactRequest -> return - r is CR.ContactUpdated -> { + when (r) { + is CR.ContactConnected -> { + chatModel.updateContact(r.contact) + chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Connected()) +// NtfManager.shared.notifyContactConnected(contact) + } +// is CR.ReceivedContactRequest -> return + is CR.ContactUpdated -> { val cInfo = ChatInfo.Direct(r.toContact) if (chatModel.hasChat(r.toContact.id)) { chatModel.updateChatInfo(cInfo) } } - - r is CR.ContactSubscribed -> { + is CR.ContactSubscribed -> { chatModel.updateContact(r.contact) chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Connected()) } - r is CR.ContactDisconnected -> { + is CR.ContactDisconnected -> { chatModel.updateContact(r.contact) chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Disconnected()) } - r is CR.ContactSubError -> { + is CR.ContactSubError -> { chatModel.updateContact(r.contact) val e = r.chatError val err: String = @@ -244,7 +247,7 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert else e.string chatModel.updateNetworkStatus(r.contact, Chat.NetworkStatus.Error(err)) } - r is CR.NewChatItem -> { + is CR.NewChatItem -> { val cInfo = r.chatItem.chatInfo val cItem = r.chatItem.chatItem chatModel.addChatItem(cInfo, cItem) @@ -252,8 +255,6 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert } // switch res { -// chatModel.updateNetworkStatus(contact, .connected) -// NtfManager.shared.notifyContactConnected(contact) // case let .receivedContactRequest(contactRequest): // chatModel.addChat(Chat( // chatInfo: ChatInfo.contactRequest(contactRequest: contactRequest), diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt index f5e93b8258..b2bb96331c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chat/ChatView.kt @@ -118,9 +118,9 @@ fun ChatItemsList(chatItems: List) { } } -@Preview +@Preview(showBackground = true) @Composable -fun PreviewChatViewLayout() { +fun PreviewChatLayout() { SimpleXTheme { val chatItems = listOf( ChatItem.getSampleData( diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt index 38a8736056..9b52c930ae 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/chatlist/ChatListView.kt @@ -63,13 +63,9 @@ fun ChatListView(chatModel: ChatModel, nav: NavController) { .fillMaxSize() .background(MaterialTheme.colors.background) ) { - ChatListToolbar(newChatCtrl) - Button( - onClick = { nav.navigate(Pages.Terminal.route) }, - modifier = Modifier.padding(14.dp) - ) { - Text("Terminal") - } + ChatListToolbar( + newChatCtrl, + settings = { nav.navigate(Pages.Settings.route) }) ChatList(chatModel, nav) } if (newChatCtrl.state.bottomSheetState.isExpanded) { @@ -85,7 +81,7 @@ fun ChatListView(chatModel: ChatModel, nav: NavController) { @ExperimentalMaterialApi @Composable -fun ChatListToolbar(newChatSheetCtrl: ScaffoldController) { +fun ChatListToolbar(newChatSheetCtrl: ScaffoldController, settings: () -> Unit) { Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, @@ -96,7 +92,9 @@ fun ChatListToolbar(newChatSheetCtrl: ScaffoldController) { Icons.Outlined.Settings, "Settings Cog", tint = MaterialTheme.colors.primary, - modifier = Modifier.padding(horizontal = 10.dp) + modifier = Modifier + .padding(horizontal = 10.dp) + .clickable(onClick = settings) ) Text( "Your chats", @@ -115,14 +113,13 @@ fun ChatListToolbar(newChatSheetCtrl: ScaffoldController) { } } - @DelicateCoroutinesApi fun goToChat(chatPreview: Chat, chatModel: ChatModel, navController: NavController) { withApi { val cInfo = chatPreview.chatInfo val chat = chatModel.controller.apiGetChat(cInfo.chatType, cInfo.apiId) if (chat != null ) { - chatModel.chatId = mutableStateOf(cInfo.id) + chatModel.chatId.value = cInfo.id chatModel.chatItems = chat.chatItems.toMutableStateList() navController.navigate(Pages.Chat.route) } else { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt index 527494be95..b0e3fde03e 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/SettingsView.kt @@ -4,55 +4,78 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import chat.simplex.app.Pages +import chat.simplex.app.model.ChatModel import chat.simplex.app.model.Profile +import chat.simplex.app.ui.theme.SimpleXTheme -//@Preview(showBackground = true) @Composable -fun SettingsView(profile: Profile) { - Column() { - Text("Your Settings") - Spacer(Modifier.height(4.dp)) - Text("YOU", style= MaterialTheme.typography.h4) - Button( - onClick = { println(profile.displayName) } - ) { - Text(profile.displayName) - } - Button( - onClick = { println(profile.hashCode()) } - ) { - Text("Your SimpleX contact address", style= MaterialTheme.typography.body1) - } - Spacer(Modifier.height(10.dp)) - Text("HELP", style= MaterialTheme.typography.h4) - Button( - onClick = { println("navigate to help") } - ) { - Text("How to use SimpleX Chat") - } - Button( - onClick = { println("start help chat") } - ) { - Text("Get help & advice via chat") - } - Button( - onClick = { println("navigate to email") } - ) { - Text("Ask questions via email") - } - Spacer(Modifier.height(10.dp)) - Text("DEVELOP", style= MaterialTheme.typography.h4) - Button( - onClick = { println("navigate to console") } - ) { - Text("Chat console") - } - Button( - onClick = { println("navigate to github") } - ) { - Text("Install SimpleX for terminal") - } - } +fun SettingsView(chatModel: ChatModel, nav: NavController) { + val user = chatModel.currentUser.value + if (user != null) { + SettingsLayout( + profile = user.profile, + back = nav::popBackStack, + navigate = nav::navigate + ) + } } +val simplexTeamUri = "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" + +@Composable +fun SettingsLayout( + profile: Profile, + back: () -> Unit, + navigate: (String) -> Unit +) { + val uriHandler = LocalUriHandler.current + Column() { + Button(onClick = back) { + Text("Back") + } + Text("Your Settings") + Spacer(Modifier.height(4.dp)) + + Text("YOU", style = MaterialTheme.typography.h4) + Button(onClick = { navigate(Pages.UserProfile.route) }) { + Text(profile.displayName) + } + + Text("HELP", style = MaterialTheme.typography.h4) + Button(onClick = { println("navigate to help") }) { + Text("How to use SimpleX Chat") + } + Button(onClick = { uriHandler.openUri(simplexTeamUri) }) { + Text("Get help & advice via chat") + } + Button(onClick = { uriHandler.openUri("mailto:chat@simplex.chat") }) { + Text("Ask questions via email") + } + Spacer(Modifier.height(10.dp)) + + Text("DEVELOP", style = MaterialTheme.typography.h4) + Button(onClick = { navigate(Pages.Terminal.route) }) { + Text("Chat console") + } + Button(onClick = { uriHandler.openUri("https://github.com/simplex-chat/simplex-chat") }) { + Text("Install SimpleX for terminal") + } + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewSettingsLayout() { + SimpleXTheme { + SettingsLayout( + profile = Profile.sampleData, + back = {}, + navigate = {} + ) + } +} diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt new file mode 100644 index 0000000000..dc646d3246 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserProfileView.kt @@ -0,0 +1,191 @@ +package chat.simplex.app.views.usersettings + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.navigation.NavController +import chat.simplex.app.model.ChatModel +import chat.simplex.app.model.Profile +import chat.simplex.app.ui.theme.SimpleXTheme +import chat.simplex.app.views.helpers.CloseSheetBar +import chat.simplex.app.views.helpers.withApi + +@Composable +fun UserProfileView(chatModel: ChatModel, nav: NavController) { + val user = chatModel.currentUser.value + if (user != null) { + var editProfile by remember { mutableStateOf(false) } + var profile by remember { mutableStateOf(user.profile) } + UserProfileLayout( + editProfile = editProfile, + profile = profile, + back = { nav.popBackStack() }, + editProfileOff = { editProfile = false }, + editProfileOn = { editProfile = true }, + saveProfile = { displayName: String, fullName: String -> + withApi { + val newProfile = chatModel.controller.apiUpdateProfile( + profile = Profile(displayName, fullName) + ) + if (newProfile != null) { + chatModel.updateUserProfile(newProfile) + profile = newProfile + } + editProfile = false + } + } + ) + } +} + +@Composable +fun UserProfileLayout( + editProfile: Boolean, + profile: Profile, + back: () -> Unit, + editProfileOff: () -> Unit, + editProfileOn: () -> Unit, + saveProfile: (String, String) -> Unit, +) { + Column( + modifier = Modifier + .padding(horizontal = 8.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + CloseSheetBar(back) + Text("Your chat profile", + Modifier.padding(bottom = 24.dp), + style = MaterialTheme.typography.h1 + ) + Text( + "Your profile is stored on your device and shared only with your contacts.\n" + + "SimpleX servers cannot see your profile.", + Modifier.padding(bottom = 24.dp) + ) + if (editProfile) { + var displayName by remember { mutableStateOf(profile.displayName) } + var fullName by remember { mutableStateOf(profile.fullName) } + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + // TODO hints + BasicTextField( + value = displayName, + onValueChange = { displayName = it }, + modifier = Modifier + .padding(bottom = 24.dp) + .fillMaxWidth(), + textStyle = TextStyle(fontSize = 16.sp), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrect = false + ), + singleLine = true + ) + BasicTextField( + value = fullName, + onValueChange = { fullName = it }, + modifier = Modifier + .padding(bottom = 24.dp) + .fillMaxWidth(), + textStyle = TextStyle(fontSize = 16.sp), + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + autoCorrect = false + ), + singleLine = true + ) + Row { + Text( + "Cancel", + color = MaterialTheme.colors.primary, + modifier = Modifier + .clickable(onClick = editProfileOff) + ) + Spacer(Modifier.padding(horizontal = 8.dp)) + Text( + "Save (and notify contacts)", + color = MaterialTheme.colors.primary, + modifier = Modifier + .clickable(onClick = { saveProfile(displayName, fullName) }) + ) + } + } + } else { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Row( + Modifier.padding(bottom = 24.dp) + ) { + Text("Display name:") + Spacer(Modifier.padding(horizontal = 4.dp)) + Text( + profile.displayName, + fontWeight = FontWeight.Bold + ) + } + Row( + Modifier.padding(bottom = 24.dp) + ) { + Text("Full name:") + Spacer(Modifier.padding(horizontal = 4.dp)) + Text( + profile.fullName, + fontWeight = FontWeight.Bold + ) + } + Text( + "Edit", + color = MaterialTheme.colors.primary, + modifier = Modifier + .clickable(onClick = editProfileOn) + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewUserProfileLayoutEditOff() { + SimpleXTheme { + UserProfileLayout( + profile = Profile.sampleData, + editProfile = false, + back = {}, + editProfileOff = {}, + editProfileOn = {}, + saveProfile = { _, _ -> } + ) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewUserProfileLayoutEditOn() { + SimpleXTheme { + UserProfileLayout( + profile = Profile.sampleData, + editProfile = true, + back = {}, + editProfileOff = {}, + editProfileOn = {}, + saveProfile = { _, _ -> } + ) + } +} From 69c79c5e0a5d166b8f832270f1a62175a371ddd9 Mon Sep 17 00:00:00 2001 From: IanRDavies Date: Tue, 22 Feb 2022 07:29:41 +0000 Subject: [PATCH 38/46] android: splash screen (to avoid showing welcome screen before the user is loaded) (#345) * initial attempt -- not recomposing * change to mutable state, still not working * two state works, why not three? * fix so we actually change state * remove unnecessary brackets * refactor * using Boolean? for userCreated Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> --- .../java/chat/simplex/app/MainActivity.kt | 8 +++---- .../java/chat/simplex/app/model/ChatModel.kt | 1 + .../java/chat/simplex/app/model/SimpleXAPI.kt | 2 ++ .../java/chat/simplex/app/views/SplashView.kt | 23 +++++++++++++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 apps/android/app/src/main/java/chat/simplex/app/views/SplashView.kt diff --git a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt index ed237c9df2..f00c39610c 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/MainActivity.kt @@ -53,10 +53,10 @@ class SimplexViewModel(application: Application): AndroidViewModel(application) @ExperimentalMaterialApi @Composable fun MainPage(chatModel: ChatModel, nav: NavController) { - if (chatModel.currentUser.value == null) WelcomeView(chatModel) { - nav.navigate(Pages.ChatList.route) - } else { - ChatListView(chatModel, nav) + when (chatModel.userCreated.value) { + null -> SplashView() + false -> WelcomeView(chatModel) { nav.navigate(Pages.ChatList.route) } + true -> ChatListView(chatModel, nav) } } diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index f634e1101b..ff4d78a426 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -10,6 +10,7 @@ import kotlinx.serialization.Serializable class ChatModel(val controller: ChatController, val alertManager: SimplexApp.AlertManager) { var currentUser = mutableStateOf(null) + var userCreated = mutableStateOf(null) var chats = mutableStateListOf() var chatId = mutableStateOf(null) var chatItems = mutableStateListOf() diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index b513098587..26fc56e069 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -18,6 +18,7 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert suspend fun startChat(u: User) { chatModel.currentUser = mutableStateOf(u) + chatModel.userCreated.value = true Log.d("SIMPLEX (user)", u.toString()) try { apiStartChat() @@ -63,6 +64,7 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert val r = sendCmd(CC.ShowActiveUser()) if (r is CR.ActiveUser) return r.user Log.d("SIMPLEX", "apiGetActiveUser: ${r.responseType} ${r.details}") + chatModel.userCreated.value = false return null } diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/SplashView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/SplashView.kt new file mode 100644 index 0000000000..1c43d35d57 --- /dev/null +++ b/apps/android/app/src/main/java/chat/simplex/app/views/SplashView.kt @@ -0,0 +1,23 @@ +package chat.simplex.app.views + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import chat.simplex.app.R + +@Composable +fun SplashView() { + Box(modifier = Modifier.fillMaxSize()) { + Image( + painter = painterResource(R.drawable.logo), + contentDescription = "Simplex Icon", + modifier = Modifier + .height(230.dp) + .align(Alignment.Center) + ) + } +} From a0a45490458c9d67a54897a1dcec9b2b6e8ba159 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Tue, 22 Feb 2022 07:46:42 +0000 Subject: [PATCH 39/46] android: improve chat, chat info, console (#344) * bigger fonts, text entry layout * resize scroll area when keyboard appears; automatically scroll on new messages * fix message entry in dark mode * imporove console layout * fix chat info with dark mode * fix typo * clean up * remove unused time formatter --- apps/android/.idea/codeStyles/Project.xml | 12 +-- apps/android/app/build.gradle | 3 +- apps/android/app/src/main/AndroidManifest.xml | 1 + .../java/chat/simplex/app/MainActivity.kt | 3 + .../java/chat/simplex/app/model/SimpleXAPI.kt | 4 +- .../java/chat/simplex/app/ui/theme/Theme.kt | 8 +- .../java/chat/simplex/app/ui/theme/Type.kt | 10 +- .../chat/simplex/app/views/TerminalView.kt | 99 ++++++++++++++----- .../simplex/app/views/chat/ChatInfoView.kt | 6 ++ .../chat/simplex/app/views/chat/ChatView.kt | 59 ++++++----- .../simplex/app/views/chat/SendMsgView.kt | 88 +++++++++++------ .../app/views/chatlist/ChatPreviewView.kt | 49 ++++++--- .../app/views/helpers/ChatInfoImage.kt | 8 +- 13 files changed, 234 insertions(+), 116 deletions(-) diff --git a/apps/android/.idea/codeStyles/Project.xml b/apps/android/.idea/codeStyles/Project.xml index 5d7a0c0f3e..1901f77e30 100644 --- a/apps/android/.idea/codeStyles/Project.xml +++ b/apps/android/.idea/codeStyles/Project.xml @@ -3,13 +3,8 @@ +