Merge branch 'master' into master-android
@@ -72,7 +72,7 @@ You must:
|
||||
|
||||
Messages not following these rules will be deleted, the right to send messages may be revoked, and the access to the new members to the group may be temporarily restricted, to prevent re-joining under a different name - our imperfect group moderation does not have a better solution at the moment.
|
||||
|
||||
You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
|
||||
You can join an English-speaking users group if you want to ask any questions: [#SimpleX users group](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2Fos8FftfoV8zjb2T89fUEjJtF7y64p5av%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAQqMgh0fw2lPhjn3PDIEfAKA_E0-gf8Hr8zzhYnDivRs%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22lBPiveK2mjfUH43SN77R0w%3D%3D%22%7D)
|
||||
|
||||
There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FvYCRjIflKNMGYlfTkuHe4B40qSlQ0439%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHNdcqNbzXZhyMoSBjT2R0-Eb1EPaLyUg3KZjn-kmM1w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22PD20tcXjw7IpkkMCfR6HLA%3D%3D%22%7D) for developers who build on SimpleX platform:
|
||||
|
||||
@@ -83,7 +83,7 @@ There is also a group [#simplex-devs](https://simplex.chat/contact#/?v=1-4&smp=s
|
||||
|
||||
There are groups in other languages, that we have the apps interface translated into. These groups are for testing, and asking questions to other SimpleX Chat users:
|
||||
|
||||
[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FkIEl7OQzcp-J6aDmjdlQbRJwqkcZE7XR%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAR16PCu02MobRmKAsjzhDWMZcWP9hS8l5AUZi-Gs8z18%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22puYPMCQt11yPUvgmI5jCiw%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FaJ8O1O8A8GbeoaHTo_V8dcefaCl7ouPb%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA034qWTA3sWcTsi6aWhNf9BA34vKVCFaEBdP2R66z6Ao%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22wiZ1v_wNjLPlT-nCSB-bRA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FvIHQDxTor53nwnWWTy5cHNwQQAdWN5Hw%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAPdgK1eBnETmgiqEQufbUkydKBJafoRx4iRrtrC2NAGc%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%221FyUryBPza-1ZFFE80Ekbg%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FXZyt3hJmWsycpN7Dqve_wbrAqb6myk1R%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAMFVIoytozTEa_QXOgoZFq_oe0IwZBYKvW50trSFXzXo%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22xz05ngjA3pNIxLZ32a8Vxg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F0weR-ZgDUl7ruOtI_8TZwEsnJP6UiImA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAq4PSThO9Fvb5ydF48wB0yNbpzCbuQJCW3vZ9BGUfcxk%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22e-iceLA0SctC62eARgYDWg%3D%3D%22%7D) (Italian-speaking).
|
||||
[\#SimpleX-DE](https://simplex.chat/contact#/?v=1-4&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FmfiivxDKWFuowXrQOp11jsY8TuP__rBL%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAiz3pKNwvKudckFYMUfgoT0s96B0jfZ7ALHAu7rtE9HQ%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22jZeJpXGrRXQJU_-MSJ_v2A%3D%3D%22%7D) (German-speaking), [\#SimpleX-ES](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FJ5ES83pJimY2BRklS8fvy_iQwIU37xra%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA0F0STP6UqN_12_k2cjjTrIjFgBGeWhOAmbY1qlk3pnM%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22VmUU0fqmYdCRmVCyvStvHA%3D%3D%22%7D) (Spanish-speaking), [\#SimpleX-FR](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FxCHBE_6PBRMqNEpm4UQDHXb9cz-mN7dd%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAetqlcM7zTCRw-iatnwCrvpJSto7lq5Yv6AsBMWv7GSM%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22foO5Xw4hhjOa_x7zET7otw%3D%3D%22%7D) (French-speaking), [\#SimpleX-RU](https://simplex.chat/contact#/?v=2-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FVXQTB0J2lLjYkgjWByhl6-1qmb5fgZHh%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAI6JaEWezfSwvcoTEkk6au-gkjrXR2ew2OqZYMYBvayk%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22ORH9OEe8Duissh-hslfeVg%3D%3D%22%7D) (Russian-speaking), [\#SimpleX-IT](https://simplex.chat/contact#/?v=2-7&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FqpHu0psOUdYfc11yQCzSyq5JhijrBzZT%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEACZ_7fbwlM45wl6cGif8cY47oPQ_AMdP0ATqOYLA6zHY%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%229uRQRTir3ealdcSfB0zsrw%3D%3D%22%7D) (Italian-speaking).
|
||||
|
||||
You can join either by opening these links in the app or by opening them in a desktop browser and scanning the QR code.
|
||||
|
||||
|
||||
@@ -75,9 +75,14 @@ enum MetaColorMode {
|
||||
}
|
||||
}
|
||||
|
||||
var statusSpacer: Text {
|
||||
func statusSpacer(_ sent: Bool) -> Text {
|
||||
switch self {
|
||||
case .normal, .transparent: Text(Image(systemName: "circlebadge.fill")).foregroundColor(.clear)
|
||||
case .normal, .transparent:
|
||||
Text(
|
||||
sent
|
||||
? Image("checkmark.wide")
|
||||
: Image(systemName: "circlebadge.fill")
|
||||
).foregroundColor(.clear)
|
||||
case .invertedMaterial: Text(" ").kerning(13)
|
||||
}
|
||||
}
|
||||
@@ -130,10 +135,10 @@ func ciMetaText(
|
||||
colorMode.resolve(statusColor)
|
||||
}
|
||||
r = r + colored(Text(image), metaColor)
|
||||
space = Text(" ")
|
||||
} else if !meta.disappearing {
|
||||
space = colorMode.statusSpacer + Text(" ")
|
||||
r = r + colorMode.statusSpacer(meta.itemStatus.sent)
|
||||
}
|
||||
space = Text(" ")
|
||||
}
|
||||
if let enc = encrypted {
|
||||
appendSpace()
|
||||
|
||||
@@ -147,6 +147,11 @@
|
||||
6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; };
|
||||
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; };
|
||||
6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; };
|
||||
64227CFA2CAAA7C200E910A3 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64227CF52CAAA7C200E910A3 /* libgmpxx.a */; };
|
||||
64227CFB2CAAA7C200E910A3 /* libHSsimplex-chat-6.1.0.4-8Sjoh2YTHoMJcEgF7NicnN.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64227CF62CAAA7C200E910A3 /* libHSsimplex-chat-6.1.0.4-8Sjoh2YTHoMJcEgF7NicnN.a */; };
|
||||
64227CFC2CAAA7C200E910A3 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64227CF72CAAA7C200E910A3 /* libgmp.a */; };
|
||||
64227CFD2CAAA7C200E910A3 /* libHSsimplex-chat-6.1.0.4-8Sjoh2YTHoMJcEgF7NicnN-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64227CF82CAAA7C200E910A3 /* libHSsimplex-chat-6.1.0.4-8Sjoh2YTHoMJcEgF7NicnN-ghc9.6.3.a */; };
|
||||
64227CFE2CAAA7C200E910A3 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64227CF92CAAA7C200E910A3 /* libffi.a */; };
|
||||
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; };
|
||||
6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; };
|
||||
6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; };
|
||||
@@ -223,11 +228,6 @@
|
||||
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
|
||||
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
|
||||
E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; };
|
||||
E55128F12C9DA948001D165C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128EC2C9DA948001D165C /* libffi.a */; };
|
||||
E55128F22C9DA948001D165C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128ED2C9DA948001D165C /* libgmpxx.a */; };
|
||||
E55128F32C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128EE2C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx-ghc9.6.3.a */; };
|
||||
E55128F42C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128EF2C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx.a */; };
|
||||
E55128F52C9DA948001D165C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128F02C9DA948001D165C /* libgmp.a */; };
|
||||
E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
|
||||
E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; };
|
||||
E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; };
|
||||
@@ -493,6 +493,11 @@
|
||||
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = "<group>"; };
|
||||
6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = "<group>"; };
|
||||
6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = "<group>"; };
|
||||
64227CF52CAAA7C200E910A3 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
64227CF62CAAA7C200E910A3 /* libHSsimplex-chat-6.1.0.4-8Sjoh2YTHoMJcEgF7NicnN.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.4-8Sjoh2YTHoMJcEgF7NicnN.a"; sourceTree = "<group>"; };
|
||||
64227CF72CAAA7C200E910A3 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
64227CF82CAAA7C200E910A3 /* libHSsimplex-chat-6.1.0.4-8Sjoh2YTHoMJcEgF7NicnN-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.4-8Sjoh2YTHoMJcEgF7NicnN-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
64227CF92CAAA7C200E910A3 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = "<group>"; };
|
||||
6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = "<group>"; };
|
||||
6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = "<group>"; };
|
||||
@@ -568,11 +573,6 @@
|
||||
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = "<group>"; };
|
||||
E55128EC2C9DA948001D165C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
E55128ED2C9DA948001D165C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
E55128EE2C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
E55128EF2C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx.a"; sourceTree = "<group>"; };
|
||||
E55128F02C9DA948001D165C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
@@ -663,14 +663,14 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E55128F32C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx-ghc9.6.3.a in Frameworks */,
|
||||
E55128F42C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx.a in Frameworks */,
|
||||
E55128F52C9DA948001D165C /* libgmp.a in Frameworks */,
|
||||
64227CFA2CAAA7C200E910A3 /* libgmpxx.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
E55128F22C9DA948001D165C /* libgmpxx.a in Frameworks */,
|
||||
64227CFD2CAAA7C200E910A3 /* libHSsimplex-chat-6.1.0.4-8Sjoh2YTHoMJcEgF7NicnN-ghc9.6.3.a in Frameworks */,
|
||||
64227CFE2CAAA7C200E910A3 /* libffi.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
64227CFB2CAAA7C200E910A3 /* libHSsimplex-chat-6.1.0.4-8Sjoh2YTHoMJcEgF7NicnN.a in Frameworks */,
|
||||
64227CFC2CAAA7C200E910A3 /* libgmp.a in Frameworks */,
|
||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
||||
E55128F12C9DA948001D165C /* libffi.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -747,11 +747,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E55128EC2C9DA948001D165C /* libffi.a */,
|
||||
E55128F02C9DA948001D165C /* libgmp.a */,
|
||||
E55128ED2C9DA948001D165C /* libgmpxx.a */,
|
||||
E55128EE2C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx-ghc9.6.3.a */,
|
||||
E55128EF2C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx.a */,
|
||||
64227CF92CAAA7C200E910A3 /* libffi.a */,
|
||||
64227CF72CAAA7C200E910A3 /* libgmp.a */,
|
||||
64227CF52CAAA7C200E910A3 /* libgmpxx.a */,
|
||||
64227CF82CAAA7C200E910A3 /* libHSsimplex-chat-6.1.0.4-8Sjoh2YTHoMJcEgF7NicnN-ghc9.6.3.a */,
|
||||
64227CF62CAAA7C200E910A3 /* libHSsimplex-chat-6.1.0.4-8Sjoh2YTHoMJcEgF7NicnN.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -1905,7 +1905,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 238;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1954,7 +1954,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 238;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1995,7 +1995,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 238;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
@@ -2015,7 +2015,7 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 238;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
@@ -2040,7 +2040,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 238;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
@@ -2077,7 +2077,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 238;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
@@ -2114,7 +2114,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 238;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2165,7 +2165,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 238;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2216,7 +2216,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 238;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2250,7 +2250,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 238;
|
||||
CURRENT_PROJECT_VERSION = 240;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
|
||||
@@ -1353,6 +1353,7 @@ public struct NetCfg: Codable, Equatable {
|
||||
public var sessionMode = TransportSessionMode.user
|
||||
public var smpProxyMode: SMPProxyMode = .unknown
|
||||
public var smpProxyFallback: SMPProxyFallback = .allowProtected
|
||||
var smpWebPort = false
|
||||
public var tcpConnectTimeout: Int // microseconds
|
||||
public var tcpTimeout: Int // microseconds
|
||||
public var tcpTimeoutPerKb: Int // microseconds
|
||||
|
||||
@@ -2820,6 +2820,20 @@ public enum CIStatus: Decodable, Hashable {
|
||||
case .invalid: return "invalid"
|
||||
}
|
||||
}
|
||||
|
||||
public var sent: Bool {
|
||||
switch self {
|
||||
case .sndNew: true
|
||||
case .sndSent: true
|
||||
case .sndRcvd: true
|
||||
case .sndErrorAuth: true
|
||||
case .sndError: true
|
||||
case .sndWarning: true
|
||||
case .rcvNew: false
|
||||
case .rcvRead: false
|
||||
case .invalid: false
|
||||
}
|
||||
}
|
||||
|
||||
public func statusIcon(_ metaColor: Color, _ paleMetaColor: Color, _ primaryColor: Color = .accentColor) -> (Image, Color)? {
|
||||
switch self {
|
||||
|
||||
@@ -2,6 +2,7 @@ package chat.simplex.app
|
||||
|
||||
import android.app.*
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
@@ -83,7 +84,7 @@ class CallService: Service() {
|
||||
generalGetString(MR.strings.notification_preview_somebody)
|
||||
else
|
||||
call?.contact?.profile?.displayName ?: ""
|
||||
val text = generalGetString(if (call?.supportsVideo() == true) MR.strings.call_service_notification_video_call else MR.strings.call_service_notification_audio_call)
|
||||
val text = generalGetString(if (call?.hasVideo == true) MR.strings.call_service_notification_video_call else MR.strings.call_service_notification_audio_call)
|
||||
val image = call?.contact?.image
|
||||
val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name)
|
||||
BitmapFactory.decodeResource(resources, R.drawable.icon)
|
||||
@@ -105,7 +106,7 @@ class CallService: Service() {
|
||||
0
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= 30) {
|
||||
if (call.supportsVideo()) {
|
||||
if (call.hasVideo && ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
|
||||
} else {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
|
||||
@@ -116,7 +116,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
|
||||
|
||||
private fun hasGrantedPermissions(): Boolean {
|
||||
val grantedAudio = ContextCompat.checkSelfPermission(this, android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
|
||||
val grantedCamera = !callSupportsVideo() || ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
||||
val grantedCamera = !callHasVideo() || ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
||||
return grantedAudio && grantedCamera
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
|
||||
override fun onBackPressed() {
|
||||
if (isOnLockScreenNow()) {
|
||||
super.onBackPressed()
|
||||
} else if (!hasGrantedPermissions() && !callSupportsVideo()) {
|
||||
} else if (!hasGrantedPermissions() && !callHasVideo()) {
|
||||
val call = m.activeCall.value
|
||||
if (call != null) {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
@@ -142,7 +142,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
// On Android 12+ PiP is enabled automatically when a user hides the app
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callSupportsVideo() && platform.androidPictureInPictureAllowed()) {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callHasVideo() && platform.androidPictureInPictureAllowed()) {
|
||||
enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
@@ -198,7 +198,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
|
||||
fun getKeyguardManager(context: Context): KeyguardManager =
|
||||
context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
|
||||
private fun callSupportsVideo() = m.activeCall.value?.supportsVideo() == true || m.activeCallInvitation.value?.callType?.media == CallMediaType.Video
|
||||
private fun callHasVideo() = m.activeCall.value?.hasVideo == true || m.activeCallInvitation.value?.callType?.media == CallMediaType.Video
|
||||
|
||||
@Composable
|
||||
fun CallActivityView() {
|
||||
@@ -212,7 +212,7 @@ fun CallActivityView() {
|
||||
.collect { collapsed ->
|
||||
when {
|
||||
collapsed -> {
|
||||
if (!platform.androidPictureInPictureAllowed() || !callSupportsVideo()) {
|
||||
if (!platform.androidPictureInPictureAllowed() || !callHasVideo()) {
|
||||
activity.moveTaskToBack(true)
|
||||
activity.startActivity(Intent(activity, MainActivity::class.java))
|
||||
} else if (!activity.isInPictureInPictureMode && activity.lifecycle.currentState == Lifecycle.State.RESUMED) {
|
||||
@@ -221,7 +221,7 @@ fun CallActivityView() {
|
||||
activity.enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
callSupportsVideo() && !platform.androidPictureInPictureAllowed() -> {
|
||||
callHasVideo() && !platform.androidPictureInPictureAllowed() -> {
|
||||
// PiP disabled by user
|
||||
platform.androidStartCallActivity(false)
|
||||
}
|
||||
@@ -242,28 +242,43 @@ fun CallActivityView() {
|
||||
Box(Modifier.background(Color.Black)) {
|
||||
if (call != null) {
|
||||
val permissionsState = rememberMultiplePermissionsState(
|
||||
permissions = if (callSupportsVideo()) {
|
||||
permissions = if (callHasVideo()) {
|
||||
listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
|
||||
} else {
|
||||
listOf(Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
)
|
||||
if (permissionsState.allPermissionsGranted) {
|
||||
// callState == connected is needed in a situation when a peer enabled camera in audio call while a user didn't grant camera permission yet,
|
||||
// so no need to hide active call view in this case
|
||||
if (permissionsState.allPermissionsGranted || call.callState == CallState.Connected) {
|
||||
ActiveCallView()
|
||||
LaunchedEffect(Unit) {
|
||||
activity.startServiceAndBind()
|
||||
}
|
||||
} else {
|
||||
CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callSupportsVideo()) {
|
||||
}
|
||||
if ((!permissionsState.allPermissionsGranted && call.callState != CallState.Connected) || call.wantsToEnableCamera) {
|
||||
CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callHasVideo() || call.wantsToEnableCamera) {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
}
|
||||
val cameraAndMicPermissions = rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))
|
||||
DisposableEffect(cameraAndMicPermissions.allPermissionsGranted) {
|
||||
onDispose {
|
||||
if (call.wantsToEnableCamera && cameraAndMicPermissions.allPermissionsGranted) {
|
||||
val activeCall = chatModel.activeCall.value
|
||||
if (activeCall != null && activeCall.contact.apiId == call.contact.apiId) {
|
||||
chatModel.activeCall.value = activeCall.copy(wantsToEnableCamera = false)
|
||||
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Camera, enable = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val view = LocalView.current
|
||||
if (callSupportsVideo()) {
|
||||
if (callHasVideo()) {
|
||||
val scope = rememberCoroutineScope()
|
||||
LaunchedEffect(Unit) {
|
||||
scope.launch {
|
||||
activity.setPipParams(callSupportsVideo(), viewRatio = Rational(view.width, view.height))
|
||||
activity.setPipParams(callHasVideo(), viewRatio = Rational(view.width, view.height))
|
||||
activity.trackPipAnimationHintView(view)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
|
||||
Log.d(TAG, "Added audio devices2: ${devices.value.map { it.type }}")
|
||||
|
||||
if (devices.value.size - oldDevices.size > 0) {
|
||||
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, false)
|
||||
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,14 +116,14 @@ class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
|
||||
Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}")
|
||||
super.onAudioDevicesAdded(addedDevices)
|
||||
devices.value = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { it.hasSupportedType() }.excludeSameType().excludeEarpieceIfWired()
|
||||
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, false)
|
||||
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, false)
|
||||
}
|
||||
|
||||
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
|
||||
Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}")
|
||||
super.onAudioDevicesRemoved(removedDevices)
|
||||
devices.value = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { it.hasSupportedType() }.excludeSameType().excludeEarpieceIfWired()
|
||||
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, true)
|
||||
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.*
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
@@ -16,9 +17,12 @@ import android.view.ViewGroup
|
||||
import android.webkit.*
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
@@ -27,12 +31,13 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.*
|
||||
import androidx.webkit.WebViewAssetLoader
|
||||
import androidx.webkit.WebViewClientCompat
|
||||
@@ -119,26 +124,26 @@ actual fun ActiveCallView() {
|
||||
val callRh = call.remoteHostId
|
||||
when (val r = apiMsg.resp) {
|
||||
is WCallResponse.Capabilities -> withBGApi {
|
||||
val callType = CallType(call.localMedia, r.capabilities)
|
||||
val callType = CallType(call.initialCallType, r.capabilities)
|
||||
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
|
||||
updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) }
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
// Starting is delayed to make Android <= 11 working good with Bluetooth
|
||||
callAudioDeviceManager.start()
|
||||
} else {
|
||||
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.supportsVideo(), true)
|
||||
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
|
||||
}
|
||||
CallSoundsPlayer.startConnectingCallSound(scope)
|
||||
activeCallWaitDeliveryReceipt(scope)
|
||||
}
|
||||
is WCallResponse.Offer -> withBGApi {
|
||||
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
|
||||
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.initialCallType, r.capabilities)
|
||||
updateActiveCall(call) { it.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities) }
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
|
||||
// Starting is delayed to make Android <= 11 working good with Bluetooth
|
||||
callAudioDeviceManager.start()
|
||||
} else {
|
||||
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.supportsVideo(), true)
|
||||
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
|
||||
}
|
||||
}
|
||||
is WCallResponse.Answer -> withBGApi {
|
||||
@@ -162,6 +167,17 @@ actual fun ActiveCallView() {
|
||||
is WCallResponse.Connected -> {
|
||||
updateActiveCall(call) { it.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo) }
|
||||
}
|
||||
is WCallResponse.PeerMedia -> {
|
||||
updateActiveCall(call) {
|
||||
val sources = it.peerMediaSources
|
||||
when (r.source) {
|
||||
CallMediaSource.Mic -> it.copy(peerMediaSources = sources.copy(mic = r.enabled))
|
||||
CallMediaSource.Camera -> it.copy(peerMediaSources = sources.copy(camera = r.enabled))
|
||||
CallMediaSource.ScreenAudio -> it.copy(peerMediaSources = sources.copy(screenAudio = r.enabled))
|
||||
CallMediaSource.ScreenVideo -> it.copy(peerMediaSources = sources.copy(screenVideo = r.enabled))
|
||||
}
|
||||
}
|
||||
}
|
||||
is WCallResponse.End -> {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
}
|
||||
@@ -174,16 +190,19 @@ actual fun ActiveCallView() {
|
||||
updateActiveCall(call) { it.copy(callState = CallState.Negotiated) }
|
||||
is WCallCommand.Media -> {
|
||||
updateActiveCall(call) {
|
||||
when (cmd.media) {
|
||||
CallMediaType.Video -> it.copy(videoEnabled = cmd.enable)
|
||||
CallMediaType.Audio -> it.copy(audioEnabled = cmd.enable)
|
||||
val sources = it.localMediaSources
|
||||
when (cmd.source) {
|
||||
CallMediaSource.Mic -> it.copy(localMediaSources = sources.copy(mic = cmd.enable))
|
||||
CallMediaSource.Camera -> it.copy(localMediaSources = sources.copy(camera = cmd.enable))
|
||||
CallMediaSource.ScreenAudio -> it.copy(localMediaSources = sources.copy(screenAudio = cmd.enable))
|
||||
CallMediaSource.ScreenVideo -> it.copy(localMediaSources = sources.copy(screenVideo = cmd.enable))
|
||||
}
|
||||
}
|
||||
}
|
||||
is WCallCommand.Camera -> {
|
||||
updateActiveCall(call) { it.copy(localCamera = cmd.camera) }
|
||||
if (!call.audioEnabled) {
|
||||
chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false))
|
||||
if (!call.localMediaSources.mic) {
|
||||
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = false))
|
||||
}
|
||||
}
|
||||
is WCallCommand.End -> {
|
||||
@@ -200,7 +219,6 @@ actual fun ActiveCallView() {
|
||||
val showOverlay = when {
|
||||
call == null -> false
|
||||
!platform.androidPictureInPictureAllowed() -> true
|
||||
!call.supportsVideo() -> true
|
||||
!chatModel.activeCallViewIsCollapsed.value -> true
|
||||
else -> false
|
||||
}
|
||||
@@ -208,6 +226,11 @@ actual fun ActiveCallView() {
|
||||
ActiveCallOverlay(call, chatModel, callAudioDeviceManager)
|
||||
}
|
||||
}
|
||||
KeyChangeEffect(call?.hasVideo) {
|
||||
if (call != null) {
|
||||
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
|
||||
}
|
||||
}
|
||||
val context = LocalContext.current
|
||||
DisposableEffect(Unit) {
|
||||
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
|
||||
@@ -237,9 +260,15 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, callAudioDeviceM
|
||||
devices = remember { callAudioDeviceManager.devices }.value,
|
||||
currentDevice = remember { callAudioDeviceManager.currentDevice },
|
||||
dismiss = { withBGApi { chatModel.callManager.endCall(call) } },
|
||||
toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled)) },
|
||||
toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = !call.localMediaSources.mic)) },
|
||||
selectDevice = { callAudioDeviceManager.selectDevice(it.id) },
|
||||
toggleVideo = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled)) },
|
||||
toggleVideo = {
|
||||
if (ContextCompat.checkSelfPermission(androidAppContext, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Camera, enable = !call.localMediaSources.camera))
|
||||
} else {
|
||||
updateActiveCall(call) { it.copy(wantsToEnableCamera = true) }
|
||||
}
|
||||
},
|
||||
toggleSound = {
|
||||
val enableSpeaker = callAudioDeviceManager.currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
|
||||
val preferredInternalDevice = callAudioDeviceManager.devices.value.firstOrNull { it.type == if (enableSpeaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE }
|
||||
@@ -293,30 +322,30 @@ private fun ActiveCallOverlayLayout(
|
||||
flipCamera: () -> Unit
|
||||
) {
|
||||
Column {
|
||||
val media = call.peerMedia ?: call.localMedia
|
||||
CloseSheetBar({ chatModel.activeCallViewIsCollapsed.value = true }, true, tintColor = Color(0xFFFFFFD8)) {
|
||||
if (media == CallMediaType.Video) {
|
||||
if (call.hasVideo) {
|
||||
Text(call.contact.chatViewName, Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1)
|
||||
}
|
||||
}
|
||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
@Composable
|
||||
fun SelectSoundDevice() {
|
||||
fun SelectSoundDevice(size: Dp) {
|
||||
if (devices.size == 2 &&
|
||||
devices.all { it.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE || it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } ||
|
||||
currentDevice.value == null ||
|
||||
devices.none { it.id == currentDevice.value?.id }
|
||||
) {
|
||||
val isSpeaker = currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
|
||||
ToggleSoundButton(call, enabled, isSpeaker, toggleSound)
|
||||
ToggleSoundButton(enabled, isSpeaker, !call.peerMediaSources.mic, toggleSound, size = size)
|
||||
} else {
|
||||
ExposedDropDownSettingWithIcon(
|
||||
devices.map { Triple(it, it.icon, if (it.name != null) generalGetString(it.name!!) else it.productName.toString()) },
|
||||
devices.map { Triple(it, if (call.peerMediaSources.mic) it.icon else MR.images.ic_volume_off, if (it.name != null) generalGetString(it.name!!) else it.productName.toString()) },
|
||||
currentDevice,
|
||||
fontSize = 18.sp,
|
||||
iconSize = 40.dp,
|
||||
boxSize = size,
|
||||
listIconSize = 30.dp,
|
||||
iconColor = Color(0xFFFFFFD8),
|
||||
background = controlButtonsBackground(),
|
||||
minWidth = 300.dp,
|
||||
onSelected = {
|
||||
if (it != null) {
|
||||
@@ -327,29 +356,9 @@ private fun ActiveCallOverlayLayout(
|
||||
}
|
||||
}
|
||||
|
||||
when (media) {
|
||||
CallMediaType.Video -> {
|
||||
VideoCallInfoView(call)
|
||||
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
|
||||
DisabledBackgroundCallsButton()
|
||||
}
|
||||
Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
ToggleAudioButton(call, enabled, toggleAudio)
|
||||
SelectSoundDevice()
|
||||
IconButton(onClick = dismiss, enabled = enabled) {
|
||||
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp))
|
||||
}
|
||||
if (call.videoEnabled) {
|
||||
ControlButton(call, painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, enabled, flipCamera)
|
||||
ControlButton(call, painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, enabled, toggleVideo)
|
||||
} else {
|
||||
Spacer(Modifier.size(48.dp))
|
||||
ControlButton(call, painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, enabled, toggleVideo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CallMediaType.Audio -> {
|
||||
when (call.hasVideo) {
|
||||
true -> VideoCallInfoView(call)
|
||||
false -> {
|
||||
Spacer(Modifier.fillMaxHeight().weight(1f))
|
||||
Column(
|
||||
Modifier.fillMaxWidth(),
|
||||
@@ -359,23 +368,26 @@ private fun ActiveCallOverlayLayout(
|
||||
ProfileImage(size = 192.dp, image = call.contact.profile.image)
|
||||
AudioCallInfoView(call)
|
||||
}
|
||||
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
|
||||
DisabledBackgroundCallsButton()
|
||||
}
|
||||
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
IconButton(onClick = dismiss, enabled = enabled) {
|
||||
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp))
|
||||
}
|
||||
}
|
||||
Box(Modifier.padding(start = 32.dp)) {
|
||||
ToggleAudioButton(call, enabled, toggleAudio)
|
||||
}
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
|
||||
Box(Modifier.padding(end = 32.dp)) {
|
||||
SelectSoundDevice()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
|
||||
DisabledBackgroundCallsButton()
|
||||
}
|
||||
|
||||
BoxWithConstraints(Modifier.padding(start = 6.dp, end = 6.dp, bottom = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) {
|
||||
val size = ((maxWidth - DEFAULT_PADDING_HALF * 4) / 5).coerceIn(0.dp, 60.dp)
|
||||
// limiting max width for tablets/wide screens, will be displayed in the center
|
||||
val padding = ((min(420.dp, maxWidth) - size * 5) / 4).coerceAtLeast(0.dp)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(padding), verticalAlignment = Alignment.CenterVertically) {
|
||||
ToggleMicButton(call, enabled, toggleAudio, size = size)
|
||||
SelectSoundDevice(size = size)
|
||||
ControlButton(painterResource(MR.images.ic_call_end_filled), MR.strings.icon_descr_hang_up, enabled = enabled, dismiss, background = Color.Red, size = size, iconPaddingPercent = 0.166f)
|
||||
if (call.localMediaSources.camera) {
|
||||
ControlButton(painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, enabled, flipCamera, size = size)
|
||||
ControlButton(painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, enabled, toggleVideo, size = size)
|
||||
} else {
|
||||
Spacer(Modifier.size(size))
|
||||
ControlButton(painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, enabled, toggleVideo, size = size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,34 +396,52 @@ private fun ActiveCallOverlayLayout(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, enabled: Boolean = true, action: () -> Unit) {
|
||||
if (call.hasMedia) {
|
||||
IconButton(onClick = action, enabled = enabled) {
|
||||
Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.size(40.dp))
|
||||
}
|
||||
} else {
|
||||
Spacer(Modifier.size(40.dp))
|
||||
private fun ControlButton(icon: Painter, iconText: StringResource, enabled: Boolean = true, action: () -> Unit, background: Color = controlButtonsBackground(), size: Dp, iconPaddingPercent: Float = 0.2f) {
|
||||
ControlButtonWrap(enabled, action, background, size) {
|
||||
Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.padding(size * iconPaddingPercent).fillMaxSize())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleAudioButton(call: Call, enabled: Boolean = true, toggleAudio: () -> Unit) {
|
||||
if (call.audioEnabled) {
|
||||
ControlButton(call, painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, enabled, toggleAudio)
|
||||
} else {
|
||||
ControlButton(call, painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, enabled, toggleAudio)
|
||||
private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, background: Color = controlButtonsBackground(), size: Dp, content: @Composable () -> Unit) {
|
||||
Box(
|
||||
Modifier
|
||||
.background(background, CircleShape)
|
||||
.size(size)
|
||||
.clickable(
|
||||
onClick = action,
|
||||
role = Role.Button,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false, radius = size / 2, color = background.lighter(0.1f)),
|
||||
enabled = enabled
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleSoundButton(call: Call, enabled: Boolean, speaker: Boolean, toggleSound: () -> Unit) {
|
||||
if (speaker) {
|
||||
ControlButton(call, painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, enabled, toggleSound)
|
||||
private fun ToggleMicButton(call: Call, enabled: Boolean = true, toggleAudio: () -> Unit, size: Dp) {
|
||||
if (call.localMediaSources.mic) {
|
||||
ControlButton(painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, enabled, toggleAudio, size = size)
|
||||
} else {
|
||||
ControlButton(call, painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, enabled, toggleSound)
|
||||
ControlButton(painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, enabled, toggleAudio, size = size)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleSoundButton(enabled: Boolean, speaker: Boolean, muted: Boolean, toggleSound: () -> Unit, size: Dp) {
|
||||
when {
|
||||
muted -> ControlButton(painterResource(MR.images.ic_volume_off), MR.strings.icon_descr_sound_muted, enabled, toggleSound, size = size)
|
||||
speaker -> ControlButton(painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, enabled, toggleSound, size = size)
|
||||
else -> ControlButton(painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, enabled, toggleSound, size = size)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun controlButtonsBackground(): Color = if (chatModel.activeCall.value?.peerMediaSources?.hasVideo == true) Color.Black.copy(0.2f) else Color.White.copy(0.2f)
|
||||
|
||||
@Composable
|
||||
fun AudioCallInfoView(call: Call) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
@@ -553,38 +583,39 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ColumnWithScrollBar(Modifier.fillMaxSize()) {
|
||||
Spacer(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier))
|
||||
|
||||
AppBarTitle(stringResource(MR.strings.permissions_required))
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
val onClick = {
|
||||
if (permissionsState.shouldShowRationale) {
|
||||
context.showAllowPermissionInSettingsAlert()
|
||||
} else {
|
||||
permissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled, context::showAllowPermissionInSettingsAlert)
|
||||
ModalView(background = Color.Black, showClose = false, close = {}) {
|
||||
ColumnWithScrollBar(Modifier.fillMaxSize()) {
|
||||
AppBarTitle(stringResource(MR.strings.permissions_required))
|
||||
Spacer(Modifier.weight(1f))
|
||||
val onClick = {
|
||||
if (permissionsState.shouldShowRationale) {
|
||||
context.showAllowPermissionInSettingsAlert()
|
||||
} else {
|
||||
permissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled, context::showAllowPermissionInSettingsAlert)
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(stringResource(MR.strings.permissions_grant), Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), textAlign = TextAlign.Center, color = Color(0xFFFFFFD8))
|
||||
SectionSpacer()
|
||||
SectionView {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
val text = if (hasVideo && audioPermission.status is PermissionStatus.Denied && cameraPermission.status is PermissionStatus.Denied) {
|
||||
stringResource(MR.strings.permissions_camera_and_record_audio)
|
||||
} else if (audioPermission.status is PermissionStatus.Denied) {
|
||||
stringResource(MR.strings.permissions_record_audio)
|
||||
} else if (hasVideo && cameraPermission.status is PermissionStatus.Denied) {
|
||||
stringResource(MR.strings.permissions_camera)
|
||||
} else ""
|
||||
GrantPermissionButton(text, buttonEnabled.value, onClick)
|
||||
Text(stringResource(MR.strings.permissions_grant), Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), textAlign = TextAlign.Center, color = Color(0xFFFFFFD8))
|
||||
SectionSpacer()
|
||||
SectionView {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
val text = if (hasVideo && audioPermission.status is PermissionStatus.Denied && cameraPermission.status is PermissionStatus.Denied) {
|
||||
stringResource(MR.strings.permissions_camera_and_record_audio)
|
||||
} else if (audioPermission.status is PermissionStatus.Denied) {
|
||||
stringResource(MR.strings.permissions_record_audio)
|
||||
} else if (hasVideo && cameraPermission.status is PermissionStatus.Denied) {
|
||||
stringResource(MR.strings.permissions_camera)
|
||||
} else null
|
||||
if (text != null) {
|
||||
GrantPermissionButton(text, buttonEnabled.value, onClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
Box(Modifier.fillMaxWidth().padding(bottom = if (hasVideo) 0.dp else DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.Center) {
|
||||
SimpleButtonFrame(cancel, Modifier.height(64.dp)) {
|
||||
Text(stringResource(MR.strings.call_service_notification_end_call), fontSize = 20.sp, color = Color(0xFFFFFFD8))
|
||||
Spacer(Modifier.weight(1f))
|
||||
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING), contentAlignment = Alignment.Center) {
|
||||
SimpleButtonFrame(cancel, Modifier.height(60.dp)) {
|
||||
Text(stringResource(MR.strings.call_service_notification_end_call), fontSize = 20.sp, color = Color(0xFFFFFFD8))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -706,7 +737,11 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error initializing WebView: ${e.stackTraceToString()}")
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), generalGetString(MR.strings.error_initializing_web_view).format(e.stackTraceToString()))
|
||||
if (e.stackTraceToString().contains("/lib64")) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), generalGetString(MR.strings.error_initializing_web_view_wrong_arch).format(e.stackTraceToString()))
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), generalGetString(MR.strings.error_initializing_web_view).format(e.stackTraceToString()))
|
||||
}
|
||||
return@AndroidView View(androidAppContext)
|
||||
}
|
||||
}
|
||||
@@ -768,8 +803,8 @@ fun PreviewActiveCallOverlayVideo() {
|
||||
userProfile = Profile.sampleData,
|
||||
contact = Contact.sampleData,
|
||||
callState = CallState.Negotiated,
|
||||
localMedia = CallMediaType.Video,
|
||||
peerMedia = CallMediaType.Video,
|
||||
initialCallType = CallMediaType.Video,
|
||||
peerMediaSources = CallMediaSources(),
|
||||
callUUID = "",
|
||||
connectionInfo = ConnectionInfo(
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "tcp"),
|
||||
@@ -798,8 +833,8 @@ fun PreviewActiveCallOverlayAudio() {
|
||||
userProfile = Profile.sampleData,
|
||||
contact = Contact.sampleData,
|
||||
callState = CallState.Negotiated,
|
||||
localMedia = CallMediaType.Audio,
|
||||
peerMedia = CallMediaType.Audio,
|
||||
initialCallType = CallMediaType.Audio,
|
||||
peerMediaSources = CallMediaSources(),
|
||||
callUUID = "",
|
||||
connectionInfo = ConnectionInfo(
|
||||
RTCIceCandidate(RTCIceCandidateType.Host, "udp"),
|
||||
|
||||
@@ -54,8 +54,7 @@ actual fun ActiveCallInteractiveArea(call: Call) {
|
||||
.align(Alignment.BottomCenter),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val media = call.peerMedia ?: call.localMedia
|
||||
if (media == CallMediaType.Video) {
|
||||
if (call.hasVideo) {
|
||||
Icon(painterResource(MR.images.ic_videocam_filled), null, Modifier.size(27.dp).offset(x = 2.5.dp, y = 2.dp), tint = Color.White)
|
||||
} else {
|
||||
Icon(painterResource(MR.images.ic_call_filled), null, Modifier.size(27.dp).offset(x = -0.5.dp, y = 2.dp), tint = Color.White)
|
||||
|
||||
@@ -2356,7 +2356,8 @@ data class CIMeta (
|
||||
val deletable: Boolean,
|
||||
val editable: Boolean
|
||||
) {
|
||||
val timestampText: String get() = getTimestampText(itemTs)
|
||||
val timestampText: String get() = getTimestampText(itemTs, true)
|
||||
|
||||
val recent: Boolean get() = updatedAt + 10.toDuration(DurationUnit.SECONDS) > Clock.System.now()
|
||||
val isLive: Boolean get() = itemLive == true
|
||||
val disappearing: Boolean get() = !isRcvNew && itemTimed?.deleteAt != null
|
||||
@@ -2420,7 +2421,18 @@ data class CITimed(
|
||||
val deleteAt: Instant?
|
||||
)
|
||||
|
||||
fun getTimestampText(t: Instant): String {
|
||||
fun getTimestampDateText(t: Instant): String {
|
||||
val tz = TimeZone.currentSystemDefault()
|
||||
val time = t.toLocalDateTime(tz).toJavaLocalDateTime()
|
||||
val weekday = time.format(DateTimeFormatter.ofPattern("EEE"))
|
||||
val dayMonthYear = time.format(DateTimeFormatter.ofPattern(
|
||||
if (Clock.System.now().toLocalDateTime(tz).year == time.year) "d MMM" else "d MMM YYYY")
|
||||
)
|
||||
|
||||
return "$weekday, $dayMonthYear"
|
||||
}
|
||||
|
||||
fun getTimestampText(t: Instant, shortFormat: Boolean = false): String {
|
||||
val tz = TimeZone.currentSystemDefault()
|
||||
val now: LocalDateTime = Clock.System.now().toLocalDateTime(tz)
|
||||
val time: LocalDateTime = t.toLocalDateTime(tz)
|
||||
@@ -2428,16 +2440,23 @@ fun getTimestampText(t: Instant): String {
|
||||
val recent = now.date == time.date ||
|
||||
(period.years == 0 && period.months == 0 && period.days == 1 && now.hour < 12 && time.hour >= 18 )
|
||||
val dateFormatter =
|
||||
if (recent) {
|
||||
if (recent || shortFormat) {
|
||||
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
} else {
|
||||
val dayMonthFormat = when (Locale.getDefault().country) {
|
||||
"US" -> "M/dd"
|
||||
"DE" -> "dd.MM"
|
||||
"RU" -> "dd.MM"
|
||||
else -> "dd/MM"
|
||||
}
|
||||
val dayMonthYearFormat = when (Locale.getDefault().country) {
|
||||
"US" -> "M/dd/yy"
|
||||
"DE" -> "dd.MM.yy"
|
||||
"RU" -> "dd.MM.yy"
|
||||
else -> "dd/MM/yy"
|
||||
}
|
||||
DateTimeFormatter.ofPattern(
|
||||
when (Locale.getDefault().country) {
|
||||
"US" -> "M/dd"
|
||||
"DE" -> "dd.MM"
|
||||
"RU" -> "dd.MM"
|
||||
else -> "dd/MM"
|
||||
}
|
||||
if (now.year == time.year) dayMonthFormat else dayMonthYearFormat
|
||||
)
|
||||
// DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
|
||||
}
|
||||
|
||||
@@ -2525,7 +2525,7 @@ object ChatController {
|
||||
// TODO askConfirmation?
|
||||
// TODO check encryption is compatible
|
||||
withCall(r, r.contact) { call ->
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, peerMedia = r.callType.media, sharedKey = r.sharedKey)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, sharedKey = r.sharedKey)
|
||||
val useRelay = appPrefs.webrtcPolicyRelay.get()
|
||||
val iceServers = getIceServers()
|
||||
Log.d(TAG, ".callOffer iceServers $iceServers")
|
||||
@@ -3653,6 +3653,7 @@ data class NetCfg(
|
||||
val sessionMode: TransportSessionMode = TransportSessionMode.User,
|
||||
val smpProxyMode: SMPProxyMode = SMPProxyMode.Unknown,
|
||||
val smpProxyFallback: SMPProxyFallback = SMPProxyFallback.AllowProtected,
|
||||
val smpWebPort: Boolean = false,
|
||||
val tcpConnectTimeout: Long, // microseconds
|
||||
val tcpTimeout: Long, // microseconds
|
||||
val tcpTimeoutPerKb: Long, // microseconds
|
||||
|
||||
@@ -49,7 +49,7 @@ class CallManager(val chatModel: ChatModel) {
|
||||
contact = invitation.contact,
|
||||
callUUID = invitation.callUUID,
|
||||
callState = CallState.InvitationAccepted,
|
||||
localMedia = invitation.callType.media,
|
||||
initialCallType = invitation.callType.media,
|
||||
sharedKey = invitation.sharedKey,
|
||||
)
|
||||
showCallView.value = true
|
||||
|
||||
@@ -2,6 +2,7 @@ package chat.simplex.common.views.call
|
||||
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.appPlatform
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.SerialName
|
||||
@@ -15,18 +16,21 @@ data class Call(
|
||||
val contact: Contact,
|
||||
val callUUID: String?,
|
||||
val callState: CallState,
|
||||
val localMedia: CallMediaType,
|
||||
val initialCallType: CallMediaType,
|
||||
val localMediaSources: CallMediaSources = CallMediaSources(mic = true, camera = initialCallType == CallMediaType.Video),
|
||||
val localCapabilities: CallCapabilities? = null,
|
||||
val peerMedia: CallMediaType? = null,
|
||||
val peerMediaSources: CallMediaSources = CallMediaSources(),
|
||||
val sharedKey: String? = null,
|
||||
val audioEnabled: Boolean = true,
|
||||
val videoEnabled: Boolean = localMedia == CallMediaType.Video,
|
||||
var localCamera: VideoCamera = VideoCamera.User,
|
||||
val connectionInfo: ConnectionInfo? = null,
|
||||
var connectedAt: Instant? = null,
|
||||
|
||||
// When a user has audio call, and then he wants to enable camera but didn't grant permissions for using camera yet,
|
||||
// we show permissions view without enabling camera before permissions are granted. After they are granted, enabling camera
|
||||
val wantsToEnableCamera: Boolean = false
|
||||
) {
|
||||
val encrypted: Boolean get() = localEncrypted && sharedKey != null
|
||||
val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false
|
||||
private val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false
|
||||
|
||||
val encryptionStatus: String get() = when(callState) {
|
||||
CallState.WaitCapabilities -> ""
|
||||
@@ -35,10 +39,8 @@ data class Call(
|
||||
else -> generalGetString(if (!localEncrypted) MR.strings.status_no_e2e_encryption else if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_e2e_encrypted)
|
||||
}
|
||||
|
||||
val hasMedia: Boolean get() = callState == CallState.OfferSent || callState == CallState.Negotiated || callState == CallState.Connected
|
||||
|
||||
fun supportsVideo(): Boolean = peerMedia == CallMediaType.Video || localMedia == CallMediaType.Video
|
||||
|
||||
val hasVideo: Boolean
|
||||
get() = localMediaSources.hasVideo || peerMediaSources.hasVideo
|
||||
}
|
||||
|
||||
enum class CallState {
|
||||
@@ -68,6 +70,16 @@ enum class CallState {
|
||||
@Serializable data class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
|
||||
@Serializable data class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
|
||||
|
||||
@Serializable data class CallMediaSources(
|
||||
val mic: Boolean = false,
|
||||
val camera: Boolean = false,
|
||||
val screenAudio: Boolean = false,
|
||||
val screenVideo: Boolean = false
|
||||
) {
|
||||
val hasVideo: Boolean
|
||||
get() = camera || screenVideo
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class WCallCommand {
|
||||
@Serializable @SerialName("capabilities") data class Capabilities(val media: CallMediaType): WCallCommand()
|
||||
@@ -75,7 +87,7 @@ sealed class WCallCommand {
|
||||
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
|
||||
@Serializable @SerialName("answer") data class Answer (val answer: String, val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallCommand()
|
||||
@Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
|
||||
@Serializable @SerialName("media") data class Media(val source: CallMediaSource, val enable: Boolean): WCallCommand()
|
||||
@Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand()
|
||||
@Serializable @SerialName("description") data class Description(val state: String, val description: String): WCallCommand()
|
||||
@Serializable @SerialName("layout") data class Layout(val layout: LayoutType): WCallCommand()
|
||||
@@ -90,6 +102,7 @@ sealed class WCallResponse {
|
||||
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallResponse()
|
||||
@Serializable @SerialName("connection") data class Connection(val state: ConnectionState): WCallResponse()
|
||||
@Serializable @SerialName("connected") data class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
|
||||
@Serializable @SerialName("peerMedia") data class PeerMedia(val source: CallMediaSource, val enabled: Boolean): WCallResponse()
|
||||
@Serializable @SerialName("end") object End: WCallResponse()
|
||||
@Serializable @SerialName("ended") object Ended: WCallResponse()
|
||||
@Serializable @SerialName("ok") object Ok: WCallResponse()
|
||||
@@ -165,6 +178,14 @@ enum class CallMediaType {
|
||||
@SerialName("audio") Audio
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class CallMediaSource {
|
||||
@SerialName("mic") Mic,
|
||||
@SerialName("camera") Camera,
|
||||
@SerialName("screenAudio") ScreenAudio,
|
||||
@SerialName("screenVideo") ScreenVideo
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class VideoCamera {
|
||||
@SerialName("user") User,
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.*
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.layout.layoutId
|
||||
@@ -25,6 +26,7 @@ import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.CIDirection.GroupRcv
|
||||
import chat.simplex.common.model.ChatController.appPrefs
|
||||
import chat.simplex.common.model.ChatModel.controller
|
||||
import chat.simplex.common.model.ChatModel.withChats
|
||||
@@ -41,11 +43,14 @@ import chat.simplex.common.views.newchat.ContactConnectionInfoView
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.*
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.sign
|
||||
|
||||
data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val date: Instant?)
|
||||
|
||||
@Composable
|
||||
// staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat
|
||||
// to chat list smooth. Otherwise, chat view will become blank right before the transition starts
|
||||
@@ -576,7 +581,7 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType)
|
||||
if (chatInfo is ChatInfo.Direct) {
|
||||
val contactInfo = chatModel.controller.apiContactInfo(remoteHostId, chatInfo.contact.contactId)
|
||||
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
|
||||
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
|
||||
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, initialCallType = media, userProfile = profile)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
@@ -775,30 +780,16 @@ fun ChatInfoToolbar(
|
||||
if (chatInfo is ChatInfo.Direct && chatInfo.contact.mergedPreferences.calls.enabled.forUser) {
|
||||
if (activeCall == null) {
|
||||
barButtons.add {
|
||||
if (appPlatform.isAndroid) {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Audio)
|
||||
}, enabled = chatInfo.contact.ready && chatInfo.contact.active
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_500),
|
||||
stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current),
|
||||
tint = if (chatInfo.contact.ready && chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Video)
|
||||
}, enabled = chatInfo.contact.ready && chatInfo.contact.active
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_videocam),
|
||||
stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current),
|
||||
tint = if (chatInfo.contact.ready && chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Audio)
|
||||
}, enabled = chatInfo.contact.ready && chatInfo.contact.active
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_500),
|
||||
stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current),
|
||||
tint = if (chatInfo.contact.ready && chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (activeCall?.contact?.id == chatInfo.id && appPlatform.isDesktop) {
|
||||
@@ -832,17 +823,10 @@ fun ChatInfoToolbar(
|
||||
}
|
||||
if (chatInfo.contact.ready && chatInfo.contact.active && activeCall == null) {
|
||||
menuItems.add {
|
||||
if (appPlatform.isAndroid) {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Video)
|
||||
})
|
||||
} else {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current), painterResource(MR.images.ic_call_500), onClick = {
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Audio)
|
||||
})
|
||||
}
|
||||
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Video)
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canAddMembers) {
|
||||
@@ -1048,16 +1032,16 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
val revealed = remember { mutableStateOf(false) }
|
||||
|
||||
@Composable
|
||||
fun ChatItemViewShortHand(cItem: ChatItem, range: IntRange?, fillMaxWidth: Boolean = true) {
|
||||
fun ChatItemViewShortHand(cItem: ChatItem, itemSeparation: ItemSeparation, range: IntRange?, fillMaxWidth: Boolean = true) {
|
||||
tryOrShowError("${cItem.id}ChatItem", error = {
|
||||
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
|
||||
}) {
|
||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy)
|
||||
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, showTimestamp = itemSeparation.timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?) {
|
||||
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?, itemSeparation: ItemSeparation, previousItemSeparation: ItemSeparation?) {
|
||||
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
|
||||
if (it == DismissValue.DismissedToStart) {
|
||||
scope.launch {
|
||||
@@ -1078,7 +1062,26 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
|
||||
)
|
||||
val sent = cItem.chatDir.sent
|
||||
Box(Modifier.padding(bottom = 4.dp)) {
|
||||
|
||||
@Composable
|
||||
fun ChatItemBox(modifier: Modifier = Modifier, content: @Composable () -> Unit = { }) {
|
||||
Box(
|
||||
modifier = modifier.padding(
|
||||
bottom = if (itemSeparation.largeGap) {
|
||||
if (i == 0) {
|
||||
8.dp
|
||||
} else {
|
||||
4.dp
|
||||
}
|
||||
} else 1.dp, top = if (previousItemSeparation?.largeGap == true) 4.dp else 1.dp
|
||||
),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null
|
||||
val selectionVisible = selectedChatItems.value != null && cItem.canBeDeletedForSelf
|
||||
val selectionOffset by animateDpAsState(if (selectionVisible && !sent) 4.dp + 22.dp * fontSizeMultiplier else 0.dp)
|
||||
@@ -1130,7 +1133,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
|
||||
@Composable
|
||||
fun Item() {
|
||||
Box(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID), contentAlignment = Alignment.CenterStart) {
|
||||
ChatItemBox(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID)) {
|
||||
androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier, cItem.id, selectedChatItems)
|
||||
}
|
||||
@@ -1139,7 +1142,9 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
|
||||
MemberImage(member)
|
||||
}
|
||||
ChatItemViewShortHand(cItem, range, false)
|
||||
Box(modifier = Modifier.padding(top = 2.dp)) {
|
||||
ChatItemViewShortHand(cItem, itemSeparation, range, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1153,7 +1158,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
ChatItemBox {
|
||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||
}
|
||||
@@ -1162,12 +1167,12 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
.padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp)
|
||||
.then(swipeableOrSelectionModifier)
|
||||
) {
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
ChatItemViewShortHand(cItem, itemSeparation, range)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
ChatItemBox {
|
||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||
}
|
||||
@@ -1176,12 +1181,12 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp)
|
||||
.then(if (selectionVisible) Modifier else swipeableModifier)
|
||||
) {
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
ChatItemViewShortHand(cItem, itemSeparation, range)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // direct message
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
ChatItemBox {
|
||||
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
|
||||
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
|
||||
}
|
||||
@@ -1191,7 +1196,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
|
||||
).then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier)
|
||||
) {
|
||||
ChatItemViewShortHand(cItem, range)
|
||||
ChatItemViewShortHand(cItem, itemSeparation, range)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1210,17 +1215,30 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
// memberConnected events and deleted items are aggregated at the last chat item in a row, see ChatItemView
|
||||
} else {
|
||||
val (prevHidden, prevItem) = chatModel.getPrevShownChatItem(currIndex, ciCategory)
|
||||
|
||||
val itemSeparation = getItemSeparation(cItem, nextItem)
|
||||
val previousItemSeparation = if (prevItem != null) getItemSeparation(prevItem, cItem) else null
|
||||
|
||||
if (itemSeparation.date != null) {
|
||||
DateSeparator(itemSeparation.date)
|
||||
}
|
||||
|
||||
val range = chatViewItemsRange(currIndex, prevHidden)
|
||||
if (revealed.value && range != null) {
|
||||
reversedChatItems.subList(range.first, range.last + 1).forEachIndexed { index, ci ->
|
||||
val prev = if (index + range.first == prevHidden) prevItem else reversedChatItems[index + range.first + 1]
|
||||
ChatItemView(ci, null, prev)
|
||||
ChatItemView(ci, null, prev, itemSeparation, previousItemSeparation)
|
||||
}
|
||||
} else {
|
||||
ChatItemView(cItem, range, prevItem)
|
||||
ChatItemView(cItem, range, prevItem, itemSeparation, previousItemSeparation)
|
||||
}
|
||||
|
||||
if (i == reversedChatItems.lastIndex) {
|
||||
DateSeparator(cItem.meta.itemTs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) {
|
||||
LaunchedEffect(cItem.id) {
|
||||
scope.launch {
|
||||
@@ -1233,6 +1251,12 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
}
|
||||
FloatingButtons(chatModel.chatItems, unreadCount, remoteHostId, chatInfo, searchValue, markRead, setFloatingButton, listState)
|
||||
|
||||
FloatingDate(
|
||||
Modifier.padding(top = 10.dp).align(Alignment.TopCenter),
|
||||
listState,
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { listState.isScrollInProgress }
|
||||
.collect {
|
||||
@@ -1424,7 +1448,7 @@ private fun showMemberImage(member: GroupMember, prevItem: ChatItem?): Boolean =
|
||||
else -> false
|
||||
}
|
||||
|
||||
val MEMBER_IMAGE_SIZE: Dp = 38.dp
|
||||
val MEMBER_IMAGE_SIZE: Dp = 37.dp
|
||||
|
||||
@Composable
|
||||
fun MemberImage(member: GroupMember) {
|
||||
@@ -1459,6 +1483,108 @@ private fun TopEndFloatingButton(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FloatingDate(
|
||||
modifier: Modifier,
|
||||
listState: LazyListState,
|
||||
) {
|
||||
var nearBottomIndex by remember { mutableStateOf(-1) }
|
||||
var isNearBottom by remember { mutableStateOf(true) }
|
||||
val lastVisibleItemDate = remember {
|
||||
derivedStateOf {
|
||||
if (listState.layoutInfo.visibleItemsInfo.lastIndex >= 0 && listState.firstVisibleItemIndex >= 0) {
|
||||
val lastVisibleChatItemIndex = chatModel.chatItems.value.lastIndex - listState.firstVisibleItemIndex - listState.layoutInfo.visibleItemsInfo.lastIndex
|
||||
val item = chatModel.chatItems.value.getOrNull(lastVisibleChatItemIndex)
|
||||
val timeZone = TimeZone.currentSystemDefault()
|
||||
item?.meta?.itemTs?.toLocalDateTime(timeZone)?.date?.atStartOfDayIn(timeZone)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
val showDate = remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
launch {
|
||||
snapshotFlow { chatModel.chatId.value }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
showDate.value = false
|
||||
isNearBottom = true
|
||||
nearBottomIndex = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { listState.layoutInfo.visibleItemsInfo }
|
||||
.collect { visibleItemsInfo ->
|
||||
if (visibleItemsInfo.find { it.index == 0 } != null) {
|
||||
var elapsedOffset = 0
|
||||
|
||||
for (it in visibleItemsInfo) {
|
||||
if (elapsedOffset >= listState.layoutInfo.viewportSize.height / 2.5) {
|
||||
nearBottomIndex = it.index
|
||||
break;
|
||||
}
|
||||
elapsedOffset += it.size
|
||||
}
|
||||
}
|
||||
|
||||
isNearBottom = if (nearBottomIndex == -1) true else (visibleItemsInfo.firstOrNull()?.index ?: 0) <= nearBottomIndex
|
||||
}
|
||||
}
|
||||
|
||||
fun setDateVisibility(isVisible: Boolean) {
|
||||
if (isVisible) {
|
||||
val now = Clock.System.now()
|
||||
val date = lastVisibleItemDate.value
|
||||
if (!isNearBottom && !showDate.value && date != null && getTimestampDateText(date) != getTimestampDateText(now)) {
|
||||
showDate.value = true
|
||||
}
|
||||
} else if (showDate.value) {
|
||||
showDate.value = false
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
var hideDateWhenNotScrolling: Job = Job()
|
||||
snapshotFlow { listState.firstVisibleItemScrollOffset }
|
||||
.collect {
|
||||
setDateVisibility(true)
|
||||
hideDateWhenNotScrolling.cancel()
|
||||
hideDateWhenNotScrolling = launch {
|
||||
delay(1000)
|
||||
setDateVisibility(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
modifier = modifier,
|
||||
visible = showDate.value,
|
||||
enter = fadeIn(tween(durationMillis = 350)),
|
||||
exit = fadeOut(tween(durationMillis = 350))
|
||||
) {
|
||||
val date = lastVisibleItemDate.value
|
||||
Column {
|
||||
Text(
|
||||
text = if (date != null) getTimestampDateText(date) else "",
|
||||
Modifier
|
||||
.background(
|
||||
color = MaterialTheme.colors.secondaryVariant,
|
||||
RoundedCornerShape(25.dp)
|
||||
)
|
||||
.padding(vertical = 4.dp, horizontal = 8.dp)
|
||||
.clip(RoundedCornerShape(25.dp)),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DownloadFilesButton(
|
||||
forwardConfirmation: ForwardConfirmation.FilesNotAccepted,
|
||||
@@ -1518,6 +1644,18 @@ private fun ButtonRow(horizontalArrangement: Arrangement.Horizontal, content: @C
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DateSeparator(date: Instant) {
|
||||
Text(
|
||||
text = getTimestampDateText(date),
|
||||
Modifier.padding(vertical = DEFAULT_PADDING_HALF + 4.dp, horizontal = DEFAULT_PADDING_HALF).fillMaxWidth(),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
|
||||
val chatViewScrollState = MutableStateFlow(false)
|
||||
|
||||
fun addGroupMembers(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) {
|
||||
@@ -1891,6 +2029,23 @@ private fun handleForwardConfirmation(
|
||||
)
|
||||
}
|
||||
|
||||
private fun getItemSeparation(chatItem: ChatItem, nextItem: ChatItem?): ItemSeparation {
|
||||
if (nextItem == null) {
|
||||
return ItemSeparation(timestamp = true, largeGap = true, date = null)
|
||||
}
|
||||
|
||||
val sameMemberAndDirection = if (nextItem.chatDir is GroupRcv && chatItem.chatDir is GroupRcv) {
|
||||
chatItem.chatDir.groupMember.groupMemberId == nextItem.chatDir.groupMember.groupMemberId
|
||||
} else chatItem.chatDir.sent == nextItem.chatDir.sent
|
||||
val largeGap = !sameMemberAndDirection || (abs(nextItem.meta.createdAt.epochSeconds - chatItem.meta.createdAt.epochSeconds) >= 60)
|
||||
|
||||
return ItemSeparation(
|
||||
timestamp = largeGap || nextItem.meta.timestampText != chatItem.meta.timestampText,
|
||||
largeGap = largeGap,
|
||||
date = if (getTimestampDateText(chatItem.meta.itemTs) == getTimestampDateText(nextItem.meta.itemTs)) null else nextItem.meta.itemTs
|
||||
)
|
||||
}
|
||||
|
||||
@Preview/*(
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES,
|
||||
showBackground = true,
|
||||
|
||||
@@ -20,6 +20,7 @@ fun CICallItemView(
|
||||
cItem: ChatItem,
|
||||
status: CICallStatus,
|
||||
duration: Int,
|
||||
showTimestamp: Boolean,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
timedMessagesTTL: Int?
|
||||
) {
|
||||
@@ -47,7 +48,7 @@ fun CICallItemView(
|
||||
CICallStatus.Error -> {}
|
||||
}
|
||||
|
||||
CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false)
|
||||
CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ fun CIGroupInvitationView(
|
||||
ci: ChatItem,
|
||||
groupInvitation: CIGroupInvitation,
|
||||
memberRole: GroupMemberRole,
|
||||
showTimestamp: Boolean,
|
||||
chatIncognito: Boolean = false,
|
||||
joinGroup: (Long, () -> Unit) -> Unit,
|
||||
timedMessagesTTL: Int?
|
||||
@@ -118,7 +119,7 @@ fun CIGroupInvitationView(
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join))
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor)) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor, showTimestamp = showTimestamp)) }
|
||||
},
|
||||
color = if (inProgress.value)
|
||||
MaterialTheme.colors.secondary
|
||||
@@ -129,7 +130,7 @@ fun CIGroupInvitationView(
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
append(groupInvitationStr())
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor)) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor, showTimestamp = showTimestamp)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -145,7 +146,7 @@ fun CIGroupInvitationView(
|
||||
}
|
||||
}
|
||||
|
||||
CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false)
|
||||
CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,7 +163,8 @@ fun PendingCIGroupInvitationViewPreview() {
|
||||
groupInvitation = CIGroupInvitation.getSample(),
|
||||
memberRole = GroupMemberRole.Admin,
|
||||
joinGroup = { _, _ -> },
|
||||
timedMessagesTTL = null
|
||||
timedMessagesTTL = null,
|
||||
showTimestamp = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -179,8 +181,9 @@ fun CIGroupInvitationViewAcceptedPreview() {
|
||||
groupInvitation = CIGroupInvitation.getSample(status = CIGroupInvitationStatus.Accepted),
|
||||
memberRole = GroupMemberRole.Admin,
|
||||
joinGroup = { _, _ -> },
|
||||
timedMessagesTTL = null
|
||||
)
|
||||
timedMessagesTTL = null,
|
||||
showTimestamp = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +199,8 @@ fun CIGroupInvitationViewLongNamePreview() {
|
||||
),
|
||||
memberRole = GroupMemberRole.Admin,
|
||||
joinGroup = { _, _ -> },
|
||||
timedMessagesTTL = null
|
||||
)
|
||||
timedMessagesTTL = null,
|
||||
showTimestamp = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ fun CIMetaView(
|
||||
},
|
||||
showStatus: Boolean = true,
|
||||
showEdited: Boolean = true,
|
||||
showViaProxy: Boolean
|
||||
showTimestamp: Boolean,
|
||||
showViaProxy: Boolean,
|
||||
) {
|
||||
Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (chatItem.isDeletedContent) {
|
||||
@@ -54,7 +55,8 @@ fun CIMetaView(
|
||||
paleMetaColor,
|
||||
showStatus = showStatus,
|
||||
showEdited = showEdited,
|
||||
showViaProxy = showViaProxy
|
||||
showViaProxy = showViaProxy,
|
||||
showTimestamp = showTimestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -70,11 +72,11 @@ private fun CIMetaText(
|
||||
paleColor: Color,
|
||||
showStatus: Boolean = true,
|
||||
showEdited: Boolean = true,
|
||||
showViaProxy: Boolean
|
||||
showTimestamp: Boolean,
|
||||
showViaProxy: Boolean,
|
||||
) {
|
||||
if (showEdited && meta.itemEdited) {
|
||||
StatusIconText(painterResource(MR.images.ic_edit), color)
|
||||
Spacer(Modifier.width(3.dp))
|
||||
}
|
||||
if (meta.disappearing) {
|
||||
StatusIconText(painterResource(MR.images.ic_timer), color)
|
||||
@@ -82,12 +84,13 @@ private fun CIMetaText(
|
||||
if (ttl != chatTTL) {
|
||||
Text(shortTimeText(ttl), color = color, fontSize = 12.sp)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
}
|
||||
if (showViaProxy && meta.sentViaProxy == true) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Icon(painterResource(MR.images.ic_arrow_forward), null, Modifier.height(17.dp), tint = MaterialTheme.colors.secondary)
|
||||
}
|
||||
if (showStatus) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor)
|
||||
if (statusIcon != null) {
|
||||
val (icon, statusColor) = statusIcon
|
||||
@@ -96,17 +99,19 @@ private fun CIMetaText(
|
||||
} else {
|
||||
StatusIconText(painterResource(icon), statusColor)
|
||||
}
|
||||
Spacer(Modifier.width(4.dp))
|
||||
} else if (!meta.disappearing) {
|
||||
StatusIconText(painterResource(MR.images.ic_circle_filled), Color.Transparent)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
}
|
||||
}
|
||||
if (encrypted != null) {
|
||||
StatusIconText(painterResource(if (encrypted) MR.images.ic_lock else MR.images.ic_lock_open_right), color)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
StatusIconText(painterResource(if (encrypted) MR.images.ic_lock else MR.images.ic_lock_open_right), color)
|
||||
}
|
||||
|
||||
if (showTimestamp) {
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text(meta.timestampText, color = color, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
Text(meta.timestampText, color = color, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
|
||||
// the conditions in this function should match CIMetaText
|
||||
@@ -117,28 +122,56 @@ fun reserveSpaceForMeta(
|
||||
secondaryColor: Color,
|
||||
showStatus: Boolean = true,
|
||||
showEdited: Boolean = true,
|
||||
showViaProxy: Boolean = false
|
||||
showViaProxy: Boolean = false,
|
||||
showTimestamp: Boolean
|
||||
): String {
|
||||
val iconSpace = " "
|
||||
var res = ""
|
||||
if (showEdited && meta.itemEdited) res += iconSpace
|
||||
val iconSpace = " \u00A0\u00A0\u00A0"
|
||||
val whiteSpace = "\u00A0"
|
||||
var res = if (showTimestamp) "" else iconSpace
|
||||
var space: String? = null
|
||||
|
||||
fun appendSpace() {
|
||||
if (space != null) {
|
||||
res += space
|
||||
space = null
|
||||
}
|
||||
}
|
||||
|
||||
if (showEdited && meta.itemEdited) {
|
||||
res += iconSpace
|
||||
}
|
||||
if (meta.itemTimed != null) {
|
||||
res += iconSpace
|
||||
val ttl = meta.itemTimed.ttl
|
||||
if (ttl != chatTTL) {
|
||||
res += shortTimeText(ttl)
|
||||
}
|
||||
space = whiteSpace
|
||||
}
|
||||
if (showViaProxy && meta.sentViaProxy == true) {
|
||||
appendSpace()
|
||||
res += iconSpace
|
||||
}
|
||||
if (showStatus && (meta.statusIcon(secondaryColor) != null || !meta.disappearing)) {
|
||||
res += iconSpace
|
||||
if (showStatus) {
|
||||
appendSpace()
|
||||
if (meta.statusIcon(secondaryColor) != null) {
|
||||
res += iconSpace
|
||||
} else if (!meta.disappearing) {
|
||||
res += iconSpace
|
||||
}
|
||||
space = whiteSpace
|
||||
}
|
||||
|
||||
if (encrypted != null) {
|
||||
appendSpace()
|
||||
res += iconSpace
|
||||
space = whiteSpace
|
||||
}
|
||||
return res + meta.timestampText
|
||||
if (showTimestamp) {
|
||||
appendSpace()
|
||||
res += meta.timestampText
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -154,7 +187,8 @@ fun PreviewCIMetaView() {
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
|
||||
),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -167,7 +201,8 @@ fun PreviewCIMetaViewUnread() {
|
||||
status = CIStatus.RcvNew()
|
||||
),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -180,7 +215,8 @@ fun PreviewCIMetaViewSendFailed() {
|
||||
status = CIStatus.CISSndError(SndError.Other("CMD SYNTAX"))
|
||||
),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -192,7 +228,8 @@ fun PreviewCIMetaViewSendNoAuth() {
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndErrorAuth()
|
||||
),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -204,7 +241,8 @@ fun PreviewCIMetaViewSendSent() {
|
||||
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent(SndCIStatusProgress.Complete)
|
||||
),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -217,7 +255,8 @@ fun PreviewCIMetaViewEdited() {
|
||||
itemEdited = true
|
||||
),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -231,7 +270,8 @@ fun PreviewCIMetaViewEditedUnread() {
|
||||
status= CIStatus.RcvNew()
|
||||
),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -245,7 +285,8 @@ fun PreviewCIMetaViewEditedSent() {
|
||||
status= CIStatus.SndSent(SndCIStatusProgress.Complete)
|
||||
),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -255,6 +296,7 @@ fun PreviewCIMetaViewDeletedContent() {
|
||||
CIMetaView(
|
||||
chatItem = ChatItem.getDeletedContentSampleData(),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -169,14 +169,14 @@ fun DecryptionErrorItemFixButton(
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
append(generalGetString(MR.strings.fix_connection))
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor)) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor, showTimestamp = true)) }
|
||||
withStyle(reserveTimestampStyle) { append(" ") } // for icon
|
||||
},
|
||||
color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false)
|
||||
CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false, showTimestamp = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,11 +201,11 @@ fun DecryptionErrorItem(
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor)) }
|
||||
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor, showTimestamp = true)) }
|
||||
},
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
|
||||
)
|
||||
CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false)
|
||||
CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false, showTimestamp = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ fun CIVoiceView(
|
||||
ci: ChatItem,
|
||||
timedMessagesTTL: Int?,
|
||||
showViaProxy: Boolean,
|
||||
showTimestamp: Boolean,
|
||||
smallView: Boolean = false,
|
||||
longClick: () -> Unit,
|
||||
receiveFile: (Long) -> Unit,
|
||||
@@ -86,7 +87,7 @@ fun CIVoiceView(
|
||||
durationText(time / 1000)
|
||||
}
|
||||
}
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, sizeMultiplier, play, pause, longClick, receiveFile) {
|
||||
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, showTimestamp, sizeMultiplier, play, pause, longClick, receiveFile) {
|
||||
AudioPlayer.seekTo(it, progress, fileSource.value?.filePath)
|
||||
}
|
||||
if (smallView) {
|
||||
@@ -120,6 +121,7 @@ private fun VoiceLayout(
|
||||
hasText: Boolean,
|
||||
timedMessagesTTL: Int?,
|
||||
showViaProxy: Boolean,
|
||||
showTimestamp: Boolean,
|
||||
sizeMultiplier: Float,
|
||||
play: () -> Unit,
|
||||
pause: () -> Unit,
|
||||
@@ -200,7 +202,7 @@ private fun VoiceLayout(
|
||||
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, 1f, play, pause, longClick, receiveFile)
|
||||
}
|
||||
Box(Modifier.padding(top = 6.sp.toDp() * sizeMultiplier, end = 6.sp.toDp() * sizeMultiplier)) {
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,7 +217,7 @@ private fun VoiceLayout(
|
||||
}
|
||||
}
|
||||
Box(Modifier.padding(top = 6.sp.toDp() * sizeMultiplier)) {
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ fun ChatItemView(
|
||||
showItemDetails: (ChatInfo, ChatItem) -> Unit,
|
||||
developerTools: Boolean,
|
||||
showViaProxy: Boolean,
|
||||
showTimestamp: Boolean,
|
||||
preview: Boolean = false,
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
@@ -132,7 +133,7 @@ fun ChatItemView(
|
||||
) {
|
||||
@Composable
|
||||
fun framedItemView() {
|
||||
FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, receiveFile, onLinkLongClick, scrollToItem)
|
||||
FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, receiveFile, onLinkLongClick, scrollToItem)
|
||||
}
|
||||
|
||||
fun deleteMessageQuestionText(): String {
|
||||
@@ -355,14 +356,14 @@ fun ChatItemView(
|
||||
fun ContentItem() {
|
||||
val mc = cItem.content.msgContent
|
||||
if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy)
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
MarkedDeletedItemDropdownMenu()
|
||||
} else {
|
||||
if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
|
||||
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
|
||||
EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
|
||||
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile = receiveFile)
|
||||
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile)
|
||||
} else {
|
||||
framedItemView()
|
||||
}
|
||||
@@ -374,7 +375,7 @@ fun ChatItemView(
|
||||
}
|
||||
|
||||
@Composable fun LegacyDeletedItem() {
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
|
||||
@@ -386,7 +387,7 @@ fun ChatItemView(
|
||||
}
|
||||
|
||||
@Composable fun CallItem(status: CICallStatus, duration: Int) {
|
||||
CICallItemView(cInfo, cItem, status, duration, acceptCall, cInfo.timedMessagesTTL)
|
||||
CICallItemView(cInfo, cItem, status, duration, showTimestamp = showTimestamp, acceptCall, cInfo.timedMessagesTTL)
|
||||
DeleteItemMenu()
|
||||
}
|
||||
|
||||
@@ -431,7 +432,7 @@ fun ChatItemView(
|
||||
|
||||
@Composable
|
||||
fun DeletedItem() {
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy)
|
||||
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
|
||||
DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages)
|
||||
@@ -474,7 +475,7 @@ fun ChatItemView(
|
||||
is CIContent.SndCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvCall -> CallItem(c.status, c.duration)
|
||||
is CIContent.RcvIntegrityError -> if (developerTools) {
|
||||
IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL)
|
||||
IntegrityErrorItemView(c.msgError, cItem, showTimestamp, cInfo.timedMessagesTTL)
|
||||
DeleteItemMenu()
|
||||
} else {
|
||||
Box(Modifier.size(0.dp)) {}
|
||||
@@ -484,11 +485,11 @@ fun ChatItemView(
|
||||
DeleteItemMenu()
|
||||
}
|
||||
is CIContent.RcvGroupInvitation -> {
|
||||
CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, timedMessagesTTL = cInfo.timedMessagesTTL)
|
||||
CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL)
|
||||
DeleteItemMenu()
|
||||
}
|
||||
is CIContent.SndGroupInvitation -> {
|
||||
CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, timedMessagesTTL = cInfo.timedMessagesTTL)
|
||||
CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL)
|
||||
DeleteItemMenu()
|
||||
}
|
||||
is CIContent.RcvDirectEventContent -> {
|
||||
@@ -928,6 +929,7 @@ fun PreviewChatItemView(
|
||||
showItemDetails = { _, _ -> },
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
showTimestamp = true,
|
||||
preview = true,
|
||||
)
|
||||
}
|
||||
@@ -968,6 +970,7 @@ fun PreviewChatItemViewDeletedContent() {
|
||||
developerTools = false,
|
||||
showViaProxy = false,
|
||||
preview = true,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import chat.simplex.common.model.ChatItem
|
||||
import chat.simplex.common.ui.theme.*
|
||||
|
||||
@Composable
|
||||
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) {
|
||||
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean, showTimestamp: Boolean) {
|
||||
val sent = ci.chatDir.sent
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
@@ -36,7 +36,7 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean)
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,8 @@ fun PreviewDeletedItemView() {
|
||||
DeletedItemView(
|
||||
ChatItem.getDeletedContentSampleData(),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,18 +12,19 @@ import androidx.compose.ui.unit.sp
|
||||
import chat.simplex.common.model.ChatItem
|
||||
import chat.simplex.common.model.MREmojiChar
|
||||
import chat.simplex.common.ui.theme.EmojiFont
|
||||
import java.sql.Timestamp
|
||||
|
||||
val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp, fontFamily = EmojiFont)
|
||||
val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp, fontFamily = EmojiFont)
|
||||
|
||||
@Composable
|
||||
fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) {
|
||||
fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean, showTimestamp: Boolean) {
|
||||
Column(
|
||||
Modifier.padding(vertical = 8.dp, horizontal = 12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
EmojiText(chatItem.content.text)
|
||||
CIMetaView(chatItem, timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
CIMetaView(chatItem, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ fun FramedItemView(
|
||||
linkMode: SimplexLinkMode,
|
||||
showViaProxy: Boolean,
|
||||
showMenu: MutableState<Boolean>,
|
||||
showTimestamp: Boolean,
|
||||
receiveFile: (Long) -> Unit,
|
||||
onLinkLongClick: (link: String) -> Unit = {},
|
||||
scrollToItem: (Long) -> Unit = {},
|
||||
@@ -47,7 +48,7 @@ fun FramedItemView(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ciQuotedMsgTextView(qi: CIQuote, lines: Int) {
|
||||
fun ciQuotedMsgTextView(qi: CIQuote, lines: Int, showTimestamp: Boolean) {
|
||||
MarkdownText(
|
||||
qi.text,
|
||||
qi.formattedText,
|
||||
@@ -56,7 +57,8 @@ fun FramedItemView(
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface),
|
||||
linkMode = linkMode,
|
||||
uriHandler = if (appPlatform.isDesktop) uriHandler else null
|
||||
uriHandler = if (appPlatform.isDesktop) uriHandler else null,
|
||||
showTimestamp = showTimestamp
|
||||
)
|
||||
}
|
||||
|
||||
@@ -76,10 +78,10 @@ fun FramedItemView(
|
||||
style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary),
|
||||
maxLines = 1
|
||||
)
|
||||
ciQuotedMsgTextView(qi, lines = 2)
|
||||
ciQuotedMsgTextView(qi, lines = 2, showTimestamp = showTimestamp)
|
||||
}
|
||||
} else {
|
||||
ciQuotedMsgTextView(qi, lines = 3)
|
||||
ciQuotedMsgTextView(qi, lines = 3, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -178,7 +180,7 @@ fun FramedItemView(
|
||||
fun ciFileView(ci: ChatItem, text: String) {
|
||||
CIFileView(ci.file, ci.meta.itemEdited, showMenu, false, receiveFile)
|
||||
if (text != "" || ci.meta.isLive) {
|
||||
CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy)
|
||||
CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +244,7 @@ fun FramedItemView(
|
||||
if (mc.text == "" && !ci.meta.isLive) {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy)
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVideo -> {
|
||||
@@ -250,35 +252,35 @@ fun FramedItemView(
|
||||
if (mc.text == "" && !ci.meta.isLive) {
|
||||
metaColor = Color.White
|
||||
} else {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy)
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVoice -> {
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile = receiveFile)
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile)
|
||||
if (mc.text != "") {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy)
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
is MsgContent.MCFile -> ciFileView(ci, mc.text)
|
||||
is MsgContent.MCUnknown ->
|
||||
if (ci.file == null) {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
} else {
|
||||
ciFileView(ci, mc.text)
|
||||
}
|
||||
is MsgContent.MCLink -> {
|
||||
ChatItemLinkView(mc.preview, showMenu, onLongClick = { showMenu.value = true })
|
||||
Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) {
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
|
||||
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
|
||||
else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) {
|
||||
CIMetaView(ci, chatTTL, metaColor, showViaProxy = showViaProxy)
|
||||
CIMetaView(ci, chatTTL, metaColor, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,14 +293,15 @@ fun CIMarkdownText(
|
||||
linkMode: SimplexLinkMode,
|
||||
uriHandler: UriHandler?,
|
||||
onLinkLongClick: (link: String) -> Unit = {},
|
||||
showViaProxy: Boolean
|
||||
showViaProxy: Boolean,
|
||||
showTimestamp: Boolean,
|
||||
) {
|
||||
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
|
||||
Box(Modifier.padding(vertical = 7.dp, horizontal = 12.dp)) {
|
||||
val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text
|
||||
MarkdownText(
|
||||
text, if (text.isEmpty()) emptyList() else ci.formattedText, toggleSecrets = true,
|
||||
meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode,
|
||||
uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy
|
||||
uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.res.MR
|
||||
|
||||
@Composable
|
||||
fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?) {
|
||||
CIMsgError(ci, timedMessagesTTL) {
|
||||
fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, showTimestamp: Boolean, timedMessagesTTL: Int?) {
|
||||
CIMsgError(ci, showTimestamp, timedMessagesTTL) {
|
||||
when (msgError) {
|
||||
is MsgErrorType.MsgSkipped ->
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -49,7 +49,7 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) {
|
||||
fun CIMsgError(ci: ChatItem, showTimestamp: Boolean, timedMessagesTTL: Int?, onClick: () -> Unit) {
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Surface(
|
||||
Modifier.clickable(onClick = onClick),
|
||||
@@ -68,7 +68,7 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) {
|
||||
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = false)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = false, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,7 +83,8 @@ fun IntegrityErrorItemViewPreview() {
|
||||
IntegrityErrorItemView(
|
||||
MsgErrorType.MsgBadHash(),
|
||||
ChatItem.getDeletedContentSampleData(),
|
||||
null
|
||||
showTimestamp = true,
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@Composable
|
||||
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState<Boolean>, showViaProxy: Boolean) {
|
||||
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState<Boolean>, showViaProxy: Boolean, showTimestamp: Boolean) {
|
||||
val sentColor = MaterialTheme.appColors.sentMessage
|
||||
val receivedColor = MaterialTheme.appColors.receivedMessage
|
||||
Surface(
|
||||
@@ -35,7 +35,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: Mutabl
|
||||
Box(Modifier.weight(1f, false)) {
|
||||
MergedMarkedDeletedText(ci, revealed)
|
||||
}
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
|
||||
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,8 @@ fun PreviewMarkedDeletedItemView() {
|
||||
DeletedItemView(
|
||||
ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted(Clock.System.now())),
|
||||
null,
|
||||
showViaProxy = false
|
||||
showViaProxy = false,
|
||||
showTimestamp = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,8 @@ fun MarkdownText (
|
||||
linkMode: SimplexLinkMode,
|
||||
inlineContent: Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>>? = null,
|
||||
onLinkLongClick: (link: String) -> Unit = {},
|
||||
showViaProxy: Boolean = false
|
||||
showViaProxy: Boolean = false,
|
||||
showTimestamp: Boolean = true
|
||||
) {
|
||||
val textLayoutDirection = remember (text) {
|
||||
if (isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr
|
||||
@@ -78,7 +79,7 @@ fun MarkdownText (
|
||||
val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) {
|
||||
"\n"
|
||||
} else if (meta != null) {
|
||||
reserveSpaceForMeta(meta, chatTTL, null, secondaryColor = MaterialTheme.colors.secondary, showViaProxy = showViaProxy)
|
||||
reserveSpaceForMeta(meta, chatTTL, null, secondaryColor = MaterialTheme.colors.secondary, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
|
||||
} else {
|
||||
" "
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ fun ChatPreviewView(
|
||||
}
|
||||
}
|
||||
is MsgContent.MCVoice -> SmallContentPreviewVoice() {
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = false, ci, cInfo.timedMessagesTTL, showViaProxy = false, smallView = true, longClick = {}) {
|
||||
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = false, ci, cInfo.timedMessagesTTL, showViaProxy = false, showTimestamp = true, smallView = true, longClick = {}) {
|
||||
val user = chatModel.currentUser.value ?: return@CIVoiceView
|
||||
withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) }
|
||||
}
|
||||
@@ -332,7 +332,7 @@ fun ChatPreviewView(
|
||||
chatPreviewTitle()
|
||||
}
|
||||
Spacer(Modifier.width(8.sp.toDp()))
|
||||
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.chatTs)
|
||||
val ts = getTimestampText(chat.chatItems.lastOrNull()?.meta?.itemTs ?: chat.chatInfo.chatTs)
|
||||
ChatListTimestampView(ts)
|
||||
}
|
||||
Row(Modifier.heightIn(min = 46.sp.toDp()).fillMaxWidth()) {
|
||||
|
||||
@@ -137,9 +137,10 @@ fun ProfileImageForActiveCall(
|
||||
size: Dp,
|
||||
image: String? = null,
|
||||
color: Color = MaterialTheme.colors.secondaryVariant,
|
||||
) {
|
||||
backgroundColor: Color? = null,
|
||||
) {
|
||||
if (image == null) {
|
||||
Box(Modifier.requiredSize(size).clip(CircleShape)) {
|
||||
Box(Modifier.requiredSize(size).clip(CircleShape).then(if (backgroundColor != null) Modifier.background(backgroundColor) else Modifier)) {
|
||||
Icon(
|
||||
AccountCircleFilled,
|
||||
contentDescription = stringResource(MR.strings.icon_descr_profile_image_placeholder),
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package chat.simplex.common.views.helpers
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -9,6 +14,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.*
|
||||
import chat.simplex.res.MR
|
||||
@@ -85,10 +91,12 @@ fun <T> ExposedDropDownSettingWithIcon(
|
||||
values: List<Triple<T, ImageResource, String>>,
|
||||
selection: State<T>,
|
||||
fontSize: TextUnit = 16.sp,
|
||||
iconSize: Dp = 40.dp,
|
||||
iconPaddingPercent: Float = 0.2f,
|
||||
listIconSize: Dp = 30.dp,
|
||||
boxSize: Dp = 60.dp,
|
||||
iconColor: Color = MenuTextColor,
|
||||
enabled: State<Boolean> = mutableStateOf(true),
|
||||
background: Color,
|
||||
minWidth: Dp = 200.dp,
|
||||
onSelected: (T) -> Unit
|
||||
) {
|
||||
@@ -99,13 +107,21 @@ fun <T> ExposedDropDownSettingWithIcon(
|
||||
expanded.value = !expanded.value && enabled.value
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
Modifier.padding(start = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
Box(
|
||||
Modifier
|
||||
.background(background, CircleShape)
|
||||
.size(boxSize)
|
||||
.clickable(
|
||||
onClick = {},
|
||||
role = Role.Button,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false, radius = boxSize / 2, color = background.lighter(0.1f)),
|
||||
enabled = enabled.value
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
val choice = values.first { it.first == selection.value }
|
||||
Icon(painterResource(choice.second), choice.third, Modifier.size(iconSize), tint = iconColor)
|
||||
Icon(painterResource(choice.second), choice.third, Modifier.padding(boxSize * iconPaddingPercent).fillMaxSize(), tint = iconColor)
|
||||
}
|
||||
DefaultExposedDropdownMenu(
|
||||
modifier = Modifier.widthIn(min = minWidth),
|
||||
|
||||
@@ -989,6 +989,7 @@
|
||||
<string name="audio_device_wired_headphones">Headphones</string>
|
||||
<string name="audio_device_bluetooth">Bluetooth</string>
|
||||
<string name="error_initializing_web_view">Error initializing WebView. Update your system to the new version. Please contact developers.\nError: %s</string>
|
||||
<string name="error_initializing_web_view_wrong_arch">Error initializing WebView. Make sure you have WebView installed and it\'s supported architecture is arm64.\nError: %s</string>
|
||||
|
||||
<!-- SimpleXInfo -->
|
||||
<string name="next_generation_of_private_messaging">The next generation\nof private messaging</string>
|
||||
@@ -1075,6 +1076,7 @@
|
||||
<string name="icon_descr_audio_on">Audio on</string>
|
||||
<string name="icon_descr_speaker_off">Speaker off</string>
|
||||
<string name="icon_descr_speaker_on">Speaker on</string>
|
||||
<string name="icon_descr_sound_muted">Sound muted</string>
|
||||
<string name="icon_descr_flip_camera">Flip camera</string>
|
||||
|
||||
<!-- Call items -->
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="m809.5-61.5-133-133q-27 19-58.25 33.25T553.5-139.5V-199q21.5-6.5 42.5-14.5t39.5-22L476-396v229L280-363H122.5v-234H274L54.5-816.5 96-858l755 754-41.5 42.5ZM770-291l-41.5-41.5q20-33 29.75-70.67Q768-440.85 768-481q0-100.82-58.75-180.41T553.5-763v-59.5q120 28 196 123.25t76 218.25q0 50.5-14 98.75T770-291ZM642.5-418.5l-89-89v-132q46.5 21.5 73.75 64.75T654.5-480q0 16-3 31.5t-9 30ZM476-585 372-689l104-104v208Zm-57.5 278v-145.5l-87-87H180v119h124.5l114 113.5ZM375-496Z"/></svg>
|
||||
|
After Width: | Height: | Size: 569 B |
@@ -6,6 +6,15 @@
|
||||
<script src="../lz-string.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<video
|
||||
id="remote-screen-video-stream"
|
||||
class="inline"
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
style="visibility: hidden"
|
||||
onclick="javascript:toggleRemoteScreenVideoFitFill()"
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="remote-video-stream"
|
||||
class="inline"
|
||||
@@ -14,6 +23,7 @@
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
onclick="javascript:toggleRemoteVideoFitFill()"
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="local-video-stream"
|
||||
class="inline"
|
||||
@@ -22,6 +32,15 @@
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="local-screen-video-stream"
|
||||
class="inline"
|
||||
muted
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
style="visibility: hidden"
|
||||
></video>
|
||||
</body>
|
||||
<footer>
|
||||
<script src="../call.js"></script>
|
||||
|
||||
@@ -12,6 +12,35 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#remote-video-stream.collapsed {
|
||||
position: absolute;
|
||||
max-width: 30%;
|
||||
max-height: 30%;
|
||||
object-fit: cover;
|
||||
margin: 16px;
|
||||
border-radius: 16px;
|
||||
bottom: 80px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#remote-video-stream.collapsed-pip {
|
||||
position: absolute;
|
||||
max-width: 50%;
|
||||
max-height: 50%;
|
||||
object-fit: cover;
|
||||
margin: 8px;
|
||||
border-radius: 8px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#remote-screen-video-stream.inline {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-video-stream.inline {
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
@@ -23,6 +52,17 @@ body {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#local-screen-video-stream.inline {
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
max-width: 30%;
|
||||
object-fit: cover;
|
||||
margin: 16px;
|
||||
border-radius: 16px;
|
||||
top: 30%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#remote-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
@@ -30,6 +70,13 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#remote-screen-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
@@ -37,6 +84,13 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-screen-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
*::-webkit-media-controls {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
<script src="/lz-string.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<video
|
||||
id="remote-screen-video-stream"
|
||||
class="inline"
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
style="visibility: hidden"
|
||||
onclick="javascript:toggleRemoteScreenVideoFitFill()"
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="remote-video-stream"
|
||||
class="inline"
|
||||
@@ -14,6 +23,7 @@
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
onclick="javascript:toggleRemoteVideoFitFill()"
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="local-video-stream"
|
||||
class="inline"
|
||||
@@ -21,28 +31,39 @@
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="local-screen-video-stream"
|
||||
class="inline"
|
||||
muted
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
style="visibility: hidden"
|
||||
></video>
|
||||
|
||||
<div id="progress"></div>
|
||||
<div id="info-block">
|
||||
<p id="state"></p>
|
||||
<p id="description"></p>
|
||||
<b id="media-sources" style="color: #fff"></b>
|
||||
</div>
|
||||
<div id="audio-call-icon">
|
||||
<img src="/desktop/images/ic_phone_in_talk.svg" />
|
||||
</div>
|
||||
<p id="manage-call">
|
||||
<button id="toggle-screen" style="display: none" onclick="javascript:toggleScreenManually()">
|
||||
<button id="toggle-screen" onclick="javascript:toggleScreenManually()">
|
||||
<img src="/desktop/images/ic_screen_share.svg" />
|
||||
</button>
|
||||
<button id="toggle-audio" style="display: none" onclick="javascript:toggleAudioManually()">
|
||||
<button id="toggle-mic" onclick="javascript:toggleMicManually()">
|
||||
<img src="/desktop/images/ic_mic.svg" />
|
||||
</button>
|
||||
<button id="end-call" onclick="javascript:endCallManually()">
|
||||
<button id="end-call" style="background: red" onclick="javascript:endCallManually()">
|
||||
<img src="/desktop/images/ic_call_end_filled.svg" />
|
||||
</button>
|
||||
<button id="toggle-speaker" style="display: none" onclick="javascript:toggleSpeakerManually()">
|
||||
<button id="toggle-speaker" onclick="javascript:toggleSpeakerManually()">
|
||||
<img src="/desktop/images/ic_volume_up.svg" />
|
||||
</button>
|
||||
<button id="toggle-video" style="display: none" onclick="javascript:toggleVideoManually()">
|
||||
<button id="toggle-camera" onclick="javascript:toggleCameraManually()">
|
||||
<img src="/desktop/images/ic_videocam_off.svg" />
|
||||
</button>
|
||||
</p>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="red" d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.25 20L821 748q-8 8-22.5 8.75t-23-5.75l-113-84.5q-6-4.5-8.75-10.25T651 643.5v-139q-42-16-85.5-22.5t-85-6.5q-42 0-85.5 6.5t-85.5 22.5v139q0 6.5-2.75 12.5T298 666.5L184.5 751q-11.5 8.5-23.5 7.5T139.5 748L46 654.5q-8.5-8.5-8.25-20t8.25-21q81.5-95 195.25-145.25T480 418Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.25 20L821 748q-8 8-22.5 8.75t-23-5.75l-113-84.5q-6-4.5-8.75-10.25T651 643.5v-139q-42-16-85.5-22.5t-85-6.5q-42 0-85.5 6.5t-85.5 22.5v139q0 6.5-2.75 12.5T298 666.5L184.5 751q-11.5 8.5-23.5 7.5T139.5 748L46 654.5q-8.5-8.5-8.25-20t8.25-21q81.5-95 195.25-145.25T480 418Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 435 B After Width: | Height: | Size: 437 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 -960 960 960" width="44" fill="white"><path d="m809.5-61.5-133-133q-27 19-58.25 33.25T553.5-139.5V-199q21.5-6.5 42.5-14.5t39.5-22L476-396v229L280-363H122.5v-234H274L54.5-816.5 96-858l755 754-41.5 42.5ZM770-291l-41.5-41.5q20-33 29.75-70.67Q768-440.85 768-481q0-100.82-58.75-180.41T553.5-763v-59.5q120 28 196 123.25t76 218.25q0 50.5-14 98.75T770-291ZM642.5-418.5l-89-89v-132q46.5 21.5 73.75 64.75T654.5-480q0 16-3 31.5t-9 30ZM476-585 372-689l104-104v208Zm-57.5 278v-145.5l-87-87H180v119h124.5l114 113.5ZM375-496Z"/></svg>
|
||||
|
After Width: | Height: | Size: 582 B |
@@ -12,7 +12,37 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#remote-video-stream.collapsed {
|
||||
position: absolute;
|
||||
max-width: 20%;
|
||||
max-height: 20%;
|
||||
object-fit: cover;
|
||||
margin: 16px;
|
||||
border-radius: 16px;
|
||||
bottom: 80px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#remote-video-stream.collapsed-pip {
|
||||
position: absolute;
|
||||
max-width: 30%;
|
||||
max-height: 30%;
|
||||
object-fit: cover;
|
||||
margin: 8px;
|
||||
border-radius: 8px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#remote-screen-video-stream.inline {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-video-stream.inline {
|
||||
background-color: black;
|
||||
position: absolute;
|
||||
width: 20%;
|
||||
max-width: 20%;
|
||||
@@ -23,6 +53,17 @@ body {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#local-screen-video-stream.inline {
|
||||
position: absolute;
|
||||
width: 20%;
|
||||
max-width: 20%;
|
||||
object-fit: cover;
|
||||
margin: 16px;
|
||||
border-radius: 16px;
|
||||
top: 33%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#remote-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
@@ -30,6 +71,13 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#remote-screen-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
@@ -37,6 +85,13 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-screen-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
*::-webkit-media-controls {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
@@ -57,19 +112,32 @@ body {
|
||||
#manage-call {
|
||||
position: absolute;
|
||||
width: fit-content;
|
||||
top: 90%;
|
||||
bottom: 15px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-column-gap: 30px;
|
||||
grid-column-gap: 38px;
|
||||
}
|
||||
|
||||
#manage-call button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-color: inherit;
|
||||
background-color: #ffffff33;
|
||||
border-radius: 50%;
|
||||
padding: 13px;
|
||||
width: 61px;
|
||||
height: 61px;
|
||||
}
|
||||
|
||||
#manage-call img {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
#manage-call button .video {
|
||||
background: #00000033;
|
||||
}
|
||||
|
||||
#progress {
|
||||
@@ -110,7 +178,6 @@ body {
|
||||
#info-block {
|
||||
position: absolute;
|
||||
color: white;
|
||||
line-height: 10px;
|
||||
opacity: 0.8;
|
||||
width: 200px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
|
||||
@@ -9,6 +9,7 @@ socket.addEventListener("open", (_event) => {
|
||||
sendMessageToNative = (msg) => {
|
||||
console.log("Message to server");
|
||||
socket.send(JSON.stringify(msg));
|
||||
reactOnMessageToServer(msg);
|
||||
};
|
||||
});
|
||||
socket.addEventListener("message", (event) => {
|
||||
@@ -27,71 +28,156 @@ socket.addEventListener("close", (_event) => {
|
||||
function endCallManually() {
|
||||
sendMessageToNative({ resp: { type: "end" } });
|
||||
}
|
||||
function toggleAudioManually() {
|
||||
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localMedia) {
|
||||
document.getElementById("toggle-audio").innerHTML = toggleMedia(activeCall.localStream, CallMediaType.Audio)
|
||||
? '<img src="/desktop/images/ic_mic.svg" />'
|
||||
: '<img src="/desktop/images/ic_mic_off.svg" />';
|
||||
}
|
||||
function toggleMicManually() {
|
||||
const enable = activeCall ? !activeCall.localMediaSources.mic : !inactiveCallMediaSources.mic;
|
||||
const apiCall = {
|
||||
command: { type: "media", source: CallMediaSource.Mic, enable: enable },
|
||||
};
|
||||
processCommand(apiCall);
|
||||
}
|
||||
function toggleSpeakerManually() {
|
||||
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.remoteStream) {
|
||||
document.getElementById("toggle-speaker").innerHTML = toggleMedia(activeCall.remoteStream, CallMediaType.Audio)
|
||||
? '<img src="/desktop/images/ic_volume_up.svg" />'
|
||||
: '<img src="/desktop/images/ic_volume_down.svg" />';
|
||||
if ((activeCall === null || activeCall === void 0 ? void 0 : activeCall.remoteStream) && activeCall.peerMediaSources.mic) {
|
||||
enableSpeakerIcon(togglePeerMedia(activeCall.remoteStream, CallMediaType.Audio), !activeCall.peerMediaSources.mic);
|
||||
}
|
||||
}
|
||||
function toggleVideoManually() {
|
||||
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localMedia) {
|
||||
let res;
|
||||
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled) {
|
||||
activeCall.cameraEnabled = !activeCall.cameraEnabled;
|
||||
res = activeCall.cameraEnabled;
|
||||
}
|
||||
else {
|
||||
res = toggleMedia(activeCall.localStream, CallMediaType.Video);
|
||||
}
|
||||
document.getElementById("toggle-video").innerHTML = res
|
||||
? '<img src="/desktop/images/ic_videocam_filled.svg" />'
|
||||
: '<img src="/desktop/images/ic_videocam_off.svg" />';
|
||||
}
|
||||
function toggleCameraManually() {
|
||||
const enable = activeCall ? !activeCall.localMediaSources.camera : !inactiveCallMediaSources.camera;
|
||||
const apiCall = {
|
||||
command: { type: "media", source: CallMediaSource.Camera, enable: enable },
|
||||
};
|
||||
processCommand(apiCall);
|
||||
}
|
||||
async function toggleScreenManually() {
|
||||
const was = activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled;
|
||||
await toggleScreenShare();
|
||||
if (was != (activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled)) {
|
||||
document.getElementById("toggle-screen").innerHTML = (activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled)
|
||||
? '<img src="/desktop/images/ic_stop_screen_share.svg" />'
|
||||
: '<img src="/desktop/images/ic_screen_share.svg" />';
|
||||
}
|
||||
// override function in call.ts to adapt UI to enabled media sources
|
||||
localOrPeerMediaSourcesChanged = (call) => {
|
||||
enableMicIcon(call.localMediaSources.mic);
|
||||
enableCameraIcon(call.localMediaSources.camera);
|
||||
enableScreenIcon(call.localMediaSources.screenVideo);
|
||||
const className = localMedia(call) == CallMediaType.Video || peerMedia(call) == CallMediaType.Video ? CallMediaType.Video : CallMediaType.Audio;
|
||||
document.getElementById("info-block").className = className;
|
||||
if (call.connection.iceConnectionState == "connected") {
|
||||
document.getElementById("audio-call-icon").style.display = className == CallMediaType.Audio ? "block" : "none";
|
||||
}
|
||||
// document.getElementById("media-sources")!.innerText = mediaSourcesStatus(call)
|
||||
document.getElementById("manage-call").className = localMedia(call) == CallMediaType.Video ? CallMediaType.Video : "";
|
||||
};
|
||||
// override function in call.ts to adapt UI to enabled media sources
|
||||
inactiveCallMediaSourcesChanged = (inactiveCallMediaSources) => {
|
||||
const mic = inactiveCallMediaSources.mic;
|
||||
const camera = inactiveCallMediaSources.camera;
|
||||
const screenVideo = inactiveCallMediaSources.screenVideo;
|
||||
enableMicIcon(mic);
|
||||
enableCameraIcon(camera);
|
||||
enableScreenIcon(screenVideo);
|
||||
const className = camera ? CallMediaType.Video : CallMediaType.Audio;
|
||||
document.getElementById("info-block").className = className;
|
||||
// document.getElementById("media-sources")!.innerText = inactiveCallMediaSourcesStatus(inactiveCallMediaSources)
|
||||
};
|
||||
function enableMicIcon(enabled) {
|
||||
document.getElementById("toggle-mic").innerHTML = enabled
|
||||
? '<img src="/desktop/images/ic_mic.svg" />'
|
||||
: '<img src="/desktop/images/ic_mic_off.svg" />';
|
||||
}
|
||||
function enableCameraIcon(enabled) {
|
||||
document.getElementById("toggle-camera").innerHTML = enabled
|
||||
? '<img src="/desktop/images/ic_videocam_filled.svg" />'
|
||||
: '<img src="/desktop/images/ic_videocam_off.svg" />';
|
||||
}
|
||||
function enableScreenIcon(enabled) {
|
||||
document.getElementById("toggle-screen").innerHTML = enabled
|
||||
? '<img src="/desktop/images/ic_stop_screen_share.svg" />'
|
||||
: '<img src="/desktop/images/ic_screen_share.svg" />';
|
||||
}
|
||||
function enableSpeakerIcon(enabled, muted) {
|
||||
document.getElementById("toggle-speaker").innerHTML = muted
|
||||
? '<img src="/desktop/images/ic_volume_off.svg" />'
|
||||
: enabled
|
||||
? '<img src="/desktop/images/ic_volume_up.svg" />'
|
||||
: '<img src="/desktop/images/ic_volume_down.svg" />';
|
||||
document.getElementById("toggle-speaker").style.opacity = muted ? "0.7" : "1";
|
||||
}
|
||||
function mediaSourcesStatus(call) {
|
||||
let status = "local";
|
||||
if (call.localMediaSources.mic)
|
||||
status += " mic";
|
||||
if (call.localMediaSources.camera)
|
||||
status += " cam";
|
||||
if (call.localMediaSources.screenAudio)
|
||||
status += " scrA";
|
||||
if (call.localMediaSources.screenVideo)
|
||||
status += " scrV";
|
||||
status += " | peer";
|
||||
if (call.peerMediaSources.mic)
|
||||
status += " mic";
|
||||
if (call.peerMediaSources.camera)
|
||||
status += " cam";
|
||||
if (call.peerMediaSources.screenAudio)
|
||||
status += " scrA";
|
||||
if (call.peerMediaSources.screenVideo)
|
||||
status += " scrV";
|
||||
return status;
|
||||
}
|
||||
function inactiveCallMediaSourcesStatus(inactiveCallMediaSources) {
|
||||
let status = "local";
|
||||
const mic = inactiveCallMediaSources.mic;
|
||||
const camera = inactiveCallMediaSources.camera;
|
||||
const screenAudio = inactiveCallMediaSources.screenAudio;
|
||||
const screenVideo = inactiveCallMediaSources.screenVideo;
|
||||
if (mic)
|
||||
status += " mic";
|
||||
if (camera)
|
||||
status += " cam";
|
||||
if (screenAudio)
|
||||
status += " scrA";
|
||||
if (screenVideo)
|
||||
status += " scrV";
|
||||
return status;
|
||||
}
|
||||
function reactOnMessageFromServer(msg) {
|
||||
var _a;
|
||||
switch ((_a = msg.command) === null || _a === void 0 ? void 0 : _a.type) {
|
||||
var _a, _b, _c;
|
||||
// screen is not allowed to be enabled before connection estabilished
|
||||
if (((_a = msg.command) === null || _a === void 0 ? void 0 : _a.type) == "capabilities" || ((_b = msg.command) === null || _b === void 0 ? void 0 : _b.type) == "offer") {
|
||||
document.getElementById("toggle-screen").style.opacity = "0.7";
|
||||
}
|
||||
else if (activeCall) {
|
||||
document.getElementById("toggle-screen").style.opacity = "1";
|
||||
}
|
||||
switch ((_c = msg.command) === null || _c === void 0 ? void 0 : _c.type) {
|
||||
case "capabilities":
|
||||
document.getElementById("info-block").className = msg.command.media;
|
||||
break;
|
||||
case "offer":
|
||||
case "start":
|
||||
document.getElementById("toggle-audio").style.display = "inline-block";
|
||||
document.getElementById("toggle-speaker").style.display = "inline-block";
|
||||
if (msg.command.media == CallMediaType.Video) {
|
||||
document.getElementById("toggle-video").style.display = "inline-block";
|
||||
document.getElementById("toggle-screen").style.display = "inline-block";
|
||||
}
|
||||
document.getElementById("info-block").className = msg.command.media;
|
||||
document.getElementById("toggle-mic").style.display = "inline-block";
|
||||
document.getElementById("toggle-speaker").style.display = "inline-block";
|
||||
document.getElementById("toggle-camera").style.display = "inline-block";
|
||||
document.getElementById("toggle-screen").style.display = "inline-block";
|
||||
enableSpeakerIcon(true, true);
|
||||
break;
|
||||
case "description":
|
||||
updateCallInfoView(msg.command.state, msg.command.description);
|
||||
if ((activeCall === null || activeCall === void 0 ? void 0 : activeCall.connection.connectionState) == "connected") {
|
||||
document.getElementById("progress").style.display = "none";
|
||||
if (document.getElementById("info-block").className == CallMediaType.Audio) {
|
||||
document.getElementById("audio-call-icon").style.display = "block";
|
||||
}
|
||||
document.getElementById("audio-call-icon").style.display =
|
||||
document.getElementById("info-block").className == CallMediaType.Audio ? "block" : "none";
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
function reactOnMessageToServer(msg) {
|
||||
var _a;
|
||||
if (!activeCall)
|
||||
return;
|
||||
switch ((_a = msg.resp) === null || _a === void 0 ? void 0 : _a.type) {
|
||||
case "peerMedia":
|
||||
const className = localMedia(activeCall) == CallMediaType.Video || peerMedia(activeCall) == CallMediaType.Video ? "video" : "audio";
|
||||
document.getElementById("info-block").className = className;
|
||||
document.getElementById("audio-call-icon").style.display = className == CallMediaType.Audio ? "block" : "none";
|
||||
enableSpeakerIcon(activeCall.remoteStream.getAudioTracks().every((elem) => elem.enabled), !activeCall.peerMediaSources.mic);
|
||||
break;
|
||||
}
|
||||
}
|
||||
function updateCallInfoView(state, description) {
|
||||
document.getElementById("state").innerText = state;
|
||||
document.getElementById("description").innerText = description;
|
||||
|
||||
@@ -1 +1 @@
|
||||
var LZString=function(){function o(o,r){if(!t[o]){t[o]={};for(var n=0;n<o.length;n++)t[o][o.charAt(n)]=n}return t[o][r]}var r=String.fromCharCode,n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",t={},i={compressToBase64:function(o){if(null==o)return"";var r=i._compress(o,6,function(o){return n.charAt(o)});switch(r.length%4){default:case 0:return r;case 1:return r+"===";case 2:return r+"==";case 3:return r+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(e){return o(n,r.charAt(e))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(o){return null==o?"":""==o?null:i._decompress(o.length,16384,function(r){return o.charCodeAt(r)-32})},compressToUint8Array:function(o){for(var r=i.compress(o),n=new Uint8Array(2*r.length),e=0,t=r.length;t>e;e++){var s=r.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null===o||void 0===o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;t>e;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(o){return null==o?"":i._compress(o,6,function(o){return e.charAt(o)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(n){return o(e,r.charAt(n))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(o,r,n){if(null==o)return"";var e,t,i,s={},p={},u="",c="",a="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<o.length;i+=1)if(u=o.charAt(i),Object.prototype.hasOwnProperty.call(s,u)||(s[u]=f++,p[u]=!0),c=a+u,Object.prototype.hasOwnProperty.call(s,c))a=c;else{if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++),s[c]=f++,a=String(u)}if(""!==a){if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==r-1){d.push(n(m));break}v++}return d.join("")},decompress:function(o){return null==o?"":""==o?null:i._decompress(o.length,32768,function(r){return o.charCodeAt(r)})},_decompress:function(o,n,e){var t,i,s,p,u,c,a,l,f=[],h=4,d=4,m=3,v="",w=[],A={val:e(0),position:n,index:1};for(i=0;3>i;i+=1)f[i]=i;for(p=0,c=Math.pow(2,2),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(t=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 2:return""}for(f[3]=l,s=l,w.push(l);;){if(A.index>o)return"";for(p=0,c=Math.pow(2,m),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(l=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 2:return w.join("")}if(0==h&&(h=Math.pow(2,m),m++),f[l])v=f[l];else{if(l!==d)return null;v=s+s.charAt(0)}w.push(v),f[d++]=s+v.charAt(0),h--,s=v,0==h&&(h=Math.pow(2,m),m++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module&&(module.exports=LZString);
|
||||
var LZString=function(){var r=String.fromCharCode,o="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",e={};function t(r,o){if(!e[r]){e[r]={};for(var n=0;n<r.length;n++)e[r][r.charAt(n)]=n}return e[r][o]}var i={compressToBase64:function(r){if(null==r)return"";var n=i._compress(r,6,function(r){return o.charAt(r)});switch(n.length%4){default:case 0:return n;case 1:return n+"===";case 2:return n+"==";case 3:return n+"="}},decompressFromBase64:function(r){return null==r?"":""==r?null:i._decompress(r.length,32,function(n){return t(o,r.charAt(n))})},compressToUTF16:function(o){return null==o?"":i._compress(o,15,function(o){return r(o+32)})+" "},decompressFromUTF16:function(r){return null==r?"":""==r?null:i._decompress(r.length,16384,function(o){return r.charCodeAt(o)-32})},compressToUint8Array:function(r){for(var o=i.compress(r),n=new Uint8Array(2*o.length),e=0,t=o.length;e<t;e++){var s=o.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null==o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;e<t;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(r){return null==r?"":i._compress(r,6,function(r){return n.charAt(r)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(o){return t(n,r.charAt(o))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(r,o,n){if(null==r)return"";var e,t,i,s={},u={},a="",p="",c="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;i<r.length;i+=1)if(a=r.charAt(i),Object.prototype.hasOwnProperty.call(s,a)||(s[a]=f++,u[a]=!0),p=c+a,Object.prototype.hasOwnProperty.call(s,p))c=p;else{if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++),s[p]=f++,c=String(a)}if(""!==c){if(Object.prototype.hasOwnProperty.call(u,c)){if(c.charCodeAt(0)<256){for(e=0;e<h;e++)m<<=1,v==o-1?(v=0,d.push(n(m)),m=0):v++;for(t=c.charCodeAt(0),e=0;e<8;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;e<h;e++)m=m<<1|t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=c.charCodeAt(0),e=0;e<16;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}0==--l&&(l=Math.pow(2,h),h++),delete u[c]}else for(t=s[c],e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;0==--l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;e<h;e++)m=m<<1|1&t,v==o-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==o-1){d.push(n(m));break}v++}return d.join("")},decompress:function(r){return null==r?"":""==r?null:i._decompress(r.length,32768,function(o){return r.charCodeAt(o)})},_decompress:function(o,n,e){var t,i,s,u,a,p,c,l=[],f=4,h=4,d=3,m="",v=[],g={val:e(0),position:n,index:1};for(t=0;t<3;t+=1)l[t]=t;for(s=0,a=Math.pow(2,2),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;c=r(s);break;case 2:return""}for(l[3]=c,i=c,v.push(c);;){if(g.index>o)return"";for(s=0,a=Math.pow(2,d),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;switch(c=s){case 0:for(s=0,a=Math.pow(2,8),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 1:for(s=0,a=Math.pow(2,16),p=1;p!=a;)u=g.val&g.position,g.position>>=1,0==g.position&&(g.position=n,g.val=e(g.index++)),s|=(u>0?1:0)*p,p<<=1;l[h++]=r(s),c=h-1,f--;break;case 2:return v.join("")}if(0==f&&(f=Math.pow(2,d),d++),l[c])m=l[c];else{if(c!==h)return null;m=i+i.charAt(0)}v.push(m),l[h++]=i+m.charAt(0),i=m,0==--f&&(f=Math.pow(2,d),d++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module?module.exports=LZString:"undefined"!=typeof angular&&null!=angular&&angular.module("LZString",[]).factory("LZString",function(){return LZString});
|
||||
@@ -34,14 +34,14 @@ actual fun ActiveCallView() {
|
||||
val callRh = call.remoteHostId
|
||||
when (val r = apiMsg.resp) {
|
||||
is WCallResponse.Capabilities -> withBGApi {
|
||||
val callType = CallType(call.localMedia, r.capabilities)
|
||||
val callType = CallType(call.initialCallType, r.capabilities)
|
||||
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities)
|
||||
CallSoundsPlayer.startConnectingCallSound(scope)
|
||||
activeCallWaitDeliveryReceipt(scope)
|
||||
}
|
||||
is WCallResponse.Offer -> withBGApi {
|
||||
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
|
||||
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.initialCallType, r.capabilities)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities)
|
||||
}
|
||||
is WCallResponse.Answer -> withBGApi {
|
||||
@@ -65,6 +65,15 @@ actual fun ActiveCallView() {
|
||||
is WCallResponse.Connected -> {
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo)
|
||||
}
|
||||
is WCallResponse.PeerMedia -> {
|
||||
val sources = call.peerMediaSources
|
||||
chatModel.activeCall.value = when (r.source) {
|
||||
CallMediaSource.Mic -> call.copy(peerMediaSources = sources.copy(mic = r.enabled))
|
||||
CallMediaSource.Camera -> call.copy(peerMediaSources = sources.copy(camera = r.enabled))
|
||||
CallMediaSource.ScreenAudio -> call.copy(peerMediaSources = sources.copy(screenAudio = r.enabled))
|
||||
CallMediaSource.ScreenVideo -> call.copy(peerMediaSources = sources.copy(screenVideo = r.enabled))
|
||||
}
|
||||
}
|
||||
is WCallResponse.End -> {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
}
|
||||
@@ -77,15 +86,18 @@ actual fun ActiveCallView() {
|
||||
is WCallCommand.Answer ->
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
|
||||
is WCallCommand.Media -> {
|
||||
when (cmd.media) {
|
||||
CallMediaType.Video -> chatModel.activeCall.value = call.copy(videoEnabled = cmd.enable)
|
||||
CallMediaType.Audio -> chatModel.activeCall.value = call.copy(audioEnabled = cmd.enable)
|
||||
val sources = call.localMediaSources
|
||||
when (cmd.source) {
|
||||
CallMediaSource.Mic -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(mic = cmd.enable))
|
||||
CallMediaSource.Camera -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(camera = cmd.enable))
|
||||
CallMediaSource.ScreenAudio -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(screenAudio = cmd.enable))
|
||||
CallMediaSource.ScreenVideo -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(screenVideo = cmd.enable))
|
||||
}
|
||||
}
|
||||
is WCallCommand.Camera -> {
|
||||
chatModel.activeCall.value = call.copy(localCamera = cmd.camera)
|
||||
if (!call.audioEnabled) {
|
||||
chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false))
|
||||
if (!call.localMediaSources.mic) {
|
||||
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = false))
|
||||
}
|
||||
}
|
||||
is WCallCommand.End ->
|
||||
|
||||
@@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@Composable
|
||||
actual fun ActiveCallInteractiveArea(call: Call) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val media = call.peerMedia ?: call.localMedia
|
||||
CompositionLocalProvider(
|
||||
LocalIndication provides NoIndication
|
||||
) {
|
||||
@@ -56,7 +55,7 @@ actual fun ActiveCallInteractiveArea(call: Call) {
|
||||
Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
) {
|
||||
if (media == CallMediaType.Video) {
|
||||
if (call.hasVideo) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_videocam_filled),
|
||||
stringResource(MR.strings.icon_descr_video_call),
|
||||
|
||||
@@ -26,11 +26,11 @@ android.enableJetifier=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=6.1-beta.1
|
||||
android.version_code=242
|
||||
android.version_name=6.1-beta.2
|
||||
android.version_code=243
|
||||
|
||||
desktop.version_name=6.1-beta.1
|
||||
desktop.version_code=67
|
||||
desktop.version_name=6.1-beta.2
|
||||
desktop.version_code=69
|
||||
|
||||
kotlin.version=1.9.23
|
||||
gradle.plugin.version=8.2.0
|
||||
|
||||
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 03168b9fbfd8a507a5374ff3375609e705225e29
|
||||
tag: da79d544cf990fb0638ae5434407d6a30724fb87
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
@@ -49,3 +49,17 @@ source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/zip.git
|
||||
tag: bd421c6b19cc4c465cd7af1f6f26169fb8ee1ebc
|
||||
|
||||
-- waiting for published warp-tls-3.4.7
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/yesodweb/wai.git
|
||||
tag: ec5e017d896a78e787a5acea62b37a4e677dec2e
|
||||
subdir: warp-tls
|
||||
|
||||
-- backported fork due http-5.0
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/wai.git
|
||||
tag: 2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0
|
||||
subdir: warp
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: simplex-chat
|
||||
version: 6.1.0.4
|
||||
version: 6.1.0.5
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"author": "SimpleX Chat",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"devDependencies": {
|
||||
"@types/lz-string": "^1.3.34",
|
||||
"@types/lz-string": "1.3.34",
|
||||
"husky": "^7.0.4",
|
||||
"isomorphic-webcrypto": "^2.3.8",
|
||||
"lint-staged": "^12.4.1",
|
||||
@@ -38,6 +38,6 @@
|
||||
"**/*": "prettier --write --ignore-unknown"
|
||||
},
|
||||
"dependencies": {
|
||||
"lz-string": "^1.4.4"
|
||||
"lz-string": "1.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,15 @@
|
||||
<script src="../lz-string.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<video
|
||||
id="remote-screen-video-stream"
|
||||
class="inline"
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
style="visibility: hidden"
|
||||
onclick="javascript:toggleRemoteScreenVideoFitFill()"
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="remote-video-stream"
|
||||
class="inline"
|
||||
@@ -14,6 +23,7 @@
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
onclick="javascript:toggleRemoteVideoFitFill()"
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="local-video-stream"
|
||||
class="inline"
|
||||
@@ -22,6 +32,15 @@
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="local-screen-video-stream"
|
||||
class="inline"
|
||||
muted
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
style="visibility: hidden"
|
||||
></video>
|
||||
</body>
|
||||
<footer>
|
||||
<script src="../call.js"></script>
|
||||
|
||||
@@ -12,6 +12,35 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#remote-video-stream.collapsed {
|
||||
position: absolute;
|
||||
max-width: 30%;
|
||||
max-height: 30%;
|
||||
object-fit: cover;
|
||||
margin: 16px;
|
||||
border-radius: 16px;
|
||||
bottom: 80px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#remote-video-stream.collapsed-pip {
|
||||
position: absolute;
|
||||
max-width: 50%;
|
||||
max-height: 50%;
|
||||
object-fit: cover;
|
||||
margin: 8px;
|
||||
border-radius: 8px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#remote-screen-video-stream.inline {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-video-stream.inline {
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
@@ -23,6 +52,17 @@ body {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#local-screen-video-stream.inline {
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
max-width: 30%;
|
||||
object-fit: cover;
|
||||
margin: 16px;
|
||||
border-radius: 16px;
|
||||
top: 30%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#remote-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
@@ -30,6 +70,13 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#remote-screen-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
@@ -37,6 +84,13 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-screen-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
*::-webkit-media-controls {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
<script src="/lz-string.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<video
|
||||
id="remote-screen-video-stream"
|
||||
class="inline"
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
style="visibility: hidden"
|
||||
onclick="javascript:toggleRemoteScreenVideoFitFill()"
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="remote-video-stream"
|
||||
class="inline"
|
||||
@@ -14,6 +23,7 @@
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
onclick="javascript:toggleRemoteVideoFitFill()"
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="local-video-stream"
|
||||
class="inline"
|
||||
@@ -21,28 +31,39 @@
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
></video>
|
||||
|
||||
<video
|
||||
id="local-screen-video-stream"
|
||||
class="inline"
|
||||
muted
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
style="visibility: hidden"
|
||||
></video>
|
||||
|
||||
<div id="progress"></div>
|
||||
<div id="info-block">
|
||||
<p id="state"></p>
|
||||
<p id="description"></p>
|
||||
<b id="media-sources" style="color: #fff"></b>
|
||||
</div>
|
||||
<div id="audio-call-icon">
|
||||
<img src="/desktop/images/ic_phone_in_talk.svg" />
|
||||
</div>
|
||||
<p id="manage-call">
|
||||
<button id="toggle-screen" style="display: none" onclick="javascript:toggleScreenManually()">
|
||||
<button id="toggle-screen" onclick="javascript:toggleScreenManually()">
|
||||
<img src="/desktop/images/ic_screen_share.svg" />
|
||||
</button>
|
||||
<button id="toggle-audio" style="display: none" onclick="javascript:toggleAudioManually()">
|
||||
<button id="toggle-mic" onclick="javascript:toggleMicManually()">
|
||||
<img src="/desktop/images/ic_mic.svg" />
|
||||
</button>
|
||||
<button id="end-call" onclick="javascript:endCallManually()">
|
||||
<button id="end-call" style="background: red" onclick="javascript:endCallManually()">
|
||||
<img src="/desktop/images/ic_call_end_filled.svg" />
|
||||
</button>
|
||||
<button id="toggle-speaker" style="display: none" onclick="javascript:toggleSpeakerManually()">
|
||||
<button id="toggle-speaker" onclick="javascript:toggleSpeakerManually()">
|
||||
<img src="/desktop/images/ic_volume_up.svg" />
|
||||
</button>
|
||||
<button id="toggle-video" style="display: none" onclick="javascript:toggleVideoManually()">
|
||||
<button id="toggle-camera" onclick="javascript:toggleCameraManually()">
|
||||
<img src="/desktop/images/ic_videocam_off.svg" />
|
||||
</button>
|
||||
</p>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="red" d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.25 20L821 748q-8 8-22.5 8.75t-23-5.75l-113-84.5q-6-4.5-8.75-10.25T651 643.5v-139q-42-16-85.5-22.5t-85-6.5q-42 0-85.5 6.5t-85.5 22.5v139q0 6.5-2.75 12.5T298 666.5L184.5 751q-11.5 8.5-23.5 7.5T139.5 748L46 654.5q-8.5-8.5-8.25-20t8.25-21q81.5-95 195.25-145.25T480 418Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.25 20L821 748q-8 8-22.5 8.75t-23-5.75l-113-84.5q-6-4.5-8.75-10.25T651 643.5v-139q-42-16-85.5-22.5t-85-6.5q-42 0-85.5 6.5t-85.5 22.5v139q0 6.5-2.75 12.5T298 666.5L184.5 751q-11.5 8.5-23.5 7.5T139.5 748L46 654.5q-8.5-8.5-8.25-20t8.25-21q81.5-95 195.25-145.25T480 418Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 435 B After Width: | Height: | Size: 437 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 -960 960 960" width="44" fill="white"><path d="m809.5-61.5-133-133q-27 19-58.25 33.25T553.5-139.5V-199q21.5-6.5 42.5-14.5t39.5-22L476-396v229L280-363H122.5v-234H274L54.5-816.5 96-858l755 754-41.5 42.5ZM770-291l-41.5-41.5q20-33 29.75-70.67Q768-440.85 768-481q0-100.82-58.75-180.41T553.5-763v-59.5q120 28 196 123.25t76 218.25q0 50.5-14 98.75T770-291ZM642.5-418.5l-89-89v-132q46.5 21.5 73.75 64.75T654.5-480q0 16-3 31.5t-9 30ZM476-585 372-689l104-104v208Zm-57.5 278v-145.5l-87-87H180v119h124.5l114 113.5ZM375-496Z"/></svg>
|
||||
|
After Width: | Height: | Size: 582 B |
@@ -12,7 +12,37 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#remote-video-stream.collapsed {
|
||||
position: absolute;
|
||||
max-width: 20%;
|
||||
max-height: 20%;
|
||||
object-fit: cover;
|
||||
margin: 16px;
|
||||
border-radius: 16px;
|
||||
bottom: 80px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#remote-video-stream.collapsed-pip {
|
||||
position: absolute;
|
||||
max-width: 30%;
|
||||
max-height: 30%;
|
||||
object-fit: cover;
|
||||
margin: 8px;
|
||||
border-radius: 8px;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#remote-screen-video-stream.inline {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-video-stream.inline {
|
||||
background-color: black;
|
||||
position: absolute;
|
||||
width: 20%;
|
||||
max-width: 20%;
|
||||
@@ -23,6 +53,17 @@ body {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#local-screen-video-stream.inline {
|
||||
position: absolute;
|
||||
width: 20%;
|
||||
max-width: 20%;
|
||||
object-fit: cover;
|
||||
margin: 16px;
|
||||
border-radius: 16px;
|
||||
top: 33%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#remote-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
@@ -30,6 +71,13 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#remote-screen-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
@@ -37,6 +85,13 @@ body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-screen-video-stream.fullscreen {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
*::-webkit-media-controls {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
@@ -57,19 +112,32 @@ body {
|
||||
#manage-call {
|
||||
position: absolute;
|
||||
width: fit-content;
|
||||
top: 90%;
|
||||
bottom: 15px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-column-gap: 30px;
|
||||
grid-column-gap: 38px;
|
||||
}
|
||||
|
||||
#manage-call button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-color: inherit;
|
||||
background-color: #ffffff33;
|
||||
border-radius: 50%;
|
||||
padding: 13px;
|
||||
width: 61px;
|
||||
height: 61px;
|
||||
}
|
||||
|
||||
#manage-call img {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
#manage-call button .video {
|
||||
background: #00000033;
|
||||
}
|
||||
|
||||
#progress {
|
||||
@@ -110,7 +178,6 @@ body {
|
||||
#info-block {
|
||||
position: absolute;
|
||||
color: white;
|
||||
line-height: 10px;
|
||||
opacity: 0.8;
|
||||
width: 200px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
|
||||
@@ -10,6 +10,7 @@ socket.addEventListener("open", (_event) => {
|
||||
sendMessageToNative = (msg: WVApiMessage) => {
|
||||
console.log("Message to server")
|
||||
socket.send(JSON.stringify(msg))
|
||||
reactOnMessageToServer(msg)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -32,74 +33,165 @@ function endCallManually() {
|
||||
sendMessageToNative({resp: {type: "end"}})
|
||||
}
|
||||
|
||||
function toggleAudioManually() {
|
||||
if (activeCall?.localMedia) {
|
||||
document.getElementById("toggle-audio")!!.innerHTML = toggleMedia(activeCall.localStream, CallMediaType.Audio)
|
||||
? '<img src="/desktop/images/ic_mic.svg" />'
|
||||
: '<img src="/desktop/images/ic_mic_off.svg" />'
|
||||
function toggleMicManually() {
|
||||
const enable = activeCall ? !activeCall.localMediaSources.mic : !inactiveCallMediaSources.mic
|
||||
const apiCall: WVAPICall = {
|
||||
command: {type: "media", source: CallMediaSource.Mic, enable: enable},
|
||||
}
|
||||
processCommand(apiCall)
|
||||
}
|
||||
|
||||
function toggleSpeakerManually() {
|
||||
if (activeCall?.remoteStream) {
|
||||
document.getElementById("toggle-speaker")!!.innerHTML = toggleMedia(activeCall.remoteStream, CallMediaType.Audio)
|
||||
? '<img src="/desktop/images/ic_volume_up.svg" />'
|
||||
: '<img src="/desktop/images/ic_volume_down.svg" />'
|
||||
if (activeCall?.remoteStream && activeCall.peerMediaSources.mic) {
|
||||
enableSpeakerIcon(togglePeerMedia(activeCall.remoteStream, CallMediaType.Audio), !activeCall.peerMediaSources.mic)
|
||||
}
|
||||
}
|
||||
|
||||
function toggleVideoManually() {
|
||||
if (activeCall?.localMedia) {
|
||||
let res: boolean
|
||||
if (activeCall?.screenShareEnabled) {
|
||||
activeCall.cameraEnabled = !activeCall.cameraEnabled
|
||||
res = activeCall.cameraEnabled
|
||||
} else {
|
||||
res = toggleMedia(activeCall.localStream, CallMediaType.Video)
|
||||
}
|
||||
document.getElementById("toggle-video")!!.innerHTML = res
|
||||
? '<img src="/desktop/images/ic_videocam_filled.svg" />'
|
||||
: '<img src="/desktop/images/ic_videocam_off.svg" />'
|
||||
function toggleCameraManually() {
|
||||
const enable = activeCall ? !activeCall.localMediaSources.camera : !inactiveCallMediaSources.camera
|
||||
const apiCall: WVAPICall = {
|
||||
command: {type: "media", source: CallMediaSource.Camera, enable: enable},
|
||||
}
|
||||
processCommand(apiCall)
|
||||
}
|
||||
|
||||
async function toggleScreenManually() {
|
||||
const was = activeCall?.screenShareEnabled
|
||||
await toggleScreenShare()
|
||||
if (was != activeCall?.screenShareEnabled) {
|
||||
document.getElementById("toggle-screen")!!.innerHTML = activeCall?.screenShareEnabled
|
||||
? '<img src="/desktop/images/ic_stop_screen_share.svg" />'
|
||||
: '<img src="/desktop/images/ic_screen_share.svg" />'
|
||||
}
|
||||
|
||||
// override function in call.ts to adapt UI to enabled media sources
|
||||
localOrPeerMediaSourcesChanged = (call: Call) => {
|
||||
enableMicIcon(call.localMediaSources.mic)
|
||||
enableCameraIcon(call.localMediaSources.camera)
|
||||
enableScreenIcon(call.localMediaSources.screenVideo)
|
||||
|
||||
const className =
|
||||
localMedia(call) == CallMediaType.Video || peerMedia(call) == CallMediaType.Video ? CallMediaType.Video : CallMediaType.Audio
|
||||
document.getElementById("info-block")!.className = className
|
||||
|
||||
if (call.connection.iceConnectionState == "connected") {
|
||||
document.getElementById("audio-call-icon")!.style.display = className == CallMediaType.Audio ? "block" : "none"
|
||||
}
|
||||
|
||||
// document.getElementById("media-sources")!.innerText = mediaSourcesStatus(call)
|
||||
document.getElementById("manage-call")!.className = localMedia(call) == CallMediaType.Video ? CallMediaType.Video : ""
|
||||
}
|
||||
|
||||
// override function in call.ts to adapt UI to enabled media sources
|
||||
inactiveCallMediaSourcesChanged = (inactiveCallMediaSources: CallMediaSources) => {
|
||||
const mic = inactiveCallMediaSources.mic
|
||||
const camera = inactiveCallMediaSources.camera
|
||||
const screenVideo = inactiveCallMediaSources.screenVideo
|
||||
enableMicIcon(mic)
|
||||
enableCameraIcon(camera)
|
||||
enableScreenIcon(screenVideo)
|
||||
|
||||
const className = camera ? CallMediaType.Video : CallMediaType.Audio
|
||||
document.getElementById("info-block")!.className = className
|
||||
// document.getElementById("media-sources")!.innerText = inactiveCallMediaSourcesStatus(inactiveCallMediaSources)
|
||||
}
|
||||
|
||||
function enableMicIcon(enabled: boolean) {
|
||||
document.getElementById("toggle-mic")!.innerHTML = enabled
|
||||
? '<img src="/desktop/images/ic_mic.svg" />'
|
||||
: '<img src="/desktop/images/ic_mic_off.svg" />'
|
||||
}
|
||||
|
||||
function enableCameraIcon(enabled: boolean) {
|
||||
document.getElementById("toggle-camera")!.innerHTML = enabled
|
||||
? '<img src="/desktop/images/ic_videocam_filled.svg" />'
|
||||
: '<img src="/desktop/images/ic_videocam_off.svg" />'
|
||||
}
|
||||
|
||||
function enableScreenIcon(enabled: boolean) {
|
||||
document.getElementById("toggle-screen")!.innerHTML = enabled
|
||||
? '<img src="/desktop/images/ic_stop_screen_share.svg" />'
|
||||
: '<img src="/desktop/images/ic_screen_share.svg" />'
|
||||
}
|
||||
|
||||
function enableSpeakerIcon(enabled: boolean, muted: boolean) {
|
||||
document.getElementById("toggle-speaker")!!.innerHTML = muted
|
||||
? '<img src="/desktop/images/ic_volume_off.svg" />'
|
||||
: enabled
|
||||
? '<img src="/desktop/images/ic_volume_up.svg" />'
|
||||
: '<img src="/desktop/images/ic_volume_down.svg" />'
|
||||
document.getElementById("toggle-speaker")!!.style.opacity = muted ? "0.7" : "1"
|
||||
}
|
||||
|
||||
function mediaSourcesStatus(call: Call): string {
|
||||
let status = "local"
|
||||
if (call.localMediaSources.mic) status += " mic"
|
||||
if (call.localMediaSources.camera) status += " cam"
|
||||
if (call.localMediaSources.screenAudio) status += " scrA"
|
||||
if (call.localMediaSources.screenVideo) status += " scrV"
|
||||
|
||||
status += " | peer"
|
||||
|
||||
if (call.peerMediaSources.mic) status += " mic"
|
||||
if (call.peerMediaSources.camera) status += " cam"
|
||||
if (call.peerMediaSources.screenAudio) status += " scrA"
|
||||
if (call.peerMediaSources.screenVideo) status += " scrV"
|
||||
return status
|
||||
}
|
||||
|
||||
function inactiveCallMediaSourcesStatus(inactiveCallMediaSources: CallMediaSources): string {
|
||||
let status = "local"
|
||||
const mic = inactiveCallMediaSources.mic
|
||||
const camera = inactiveCallMediaSources.camera
|
||||
const screenAudio = inactiveCallMediaSources.screenAudio
|
||||
const screenVideo = inactiveCallMediaSources.screenVideo
|
||||
if (mic) status += " mic"
|
||||
if (camera) status += " cam"
|
||||
if (screenAudio) status += " scrA"
|
||||
if (screenVideo) status += " scrV"
|
||||
return status
|
||||
}
|
||||
|
||||
function reactOnMessageFromServer(msg: WVApiMessage) {
|
||||
// screen is not allowed to be enabled before connection estabilished
|
||||
if (msg.command?.type == "capabilities" || msg.command?.type == "offer") {
|
||||
document.getElementById("toggle-screen")!!.style.opacity = "0.7"
|
||||
} else if (activeCall) {
|
||||
document.getElementById("toggle-screen")!!.style.opacity = "1"
|
||||
}
|
||||
switch (msg.command?.type) {
|
||||
case "capabilities":
|
||||
document.getElementById("info-block")!!.className = msg.command.media
|
||||
break
|
||||
case "offer":
|
||||
case "start":
|
||||
document.getElementById("toggle-audio")!!.style.display = "inline-block"
|
||||
document.getElementById("toggle-speaker")!!.style.display = "inline-block"
|
||||
if (msg.command.media == CallMediaType.Video) {
|
||||
document.getElementById("toggle-video")!!.style.display = "inline-block"
|
||||
document.getElementById("toggle-screen")!!.style.display = "inline-block"
|
||||
}
|
||||
document.getElementById("info-block")!!.className = msg.command.media
|
||||
document.getElementById("toggle-mic")!!.style.display = "inline-block"
|
||||
document.getElementById("toggle-speaker")!!.style.display = "inline-block"
|
||||
document.getElementById("toggle-camera")!!.style.display = "inline-block"
|
||||
document.getElementById("toggle-screen")!!.style.display = "inline-block"
|
||||
enableSpeakerIcon(true, true)
|
||||
break
|
||||
case "description":
|
||||
updateCallInfoView(msg.command.state, msg.command.description)
|
||||
if (activeCall?.connection.connectionState == "connected") {
|
||||
document.getElementById("progress")!.style.display = "none"
|
||||
if (document.getElementById("info-block")!!.className == CallMediaType.Audio) {
|
||||
document.getElementById("audio-call-icon")!.style.display = "block"
|
||||
}
|
||||
document.getElementById("audio-call-icon")!.style.display =
|
||||
document.getElementById("info-block")!!.className == CallMediaType.Audio ? "block" : "none"
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function reactOnMessageToServer(msg: WVApiMessage) {
|
||||
if (!activeCall) return
|
||||
|
||||
switch (msg.resp?.type) {
|
||||
case "peerMedia":
|
||||
const className = localMedia(activeCall) == CallMediaType.Video || peerMedia(activeCall) == CallMediaType.Video ? "video" : "audio"
|
||||
document.getElementById("info-block")!!.className = className
|
||||
document.getElementById("audio-call-icon")!.style.display = className == CallMediaType.Audio ? "block" : "none"
|
||||
enableSpeakerIcon(
|
||||
activeCall.remoteStream.getAudioTracks().every((elem) => elem.enabled),
|
||||
!activeCall.peerMediaSources.mic
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function updateCallInfoView(state: string, description: string) {
|
||||
document.getElementById("state")!!.innerText = state
|
||||
document.getElementById("description")!!.innerText = description
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."03168b9fbfd8a507a5374ff3375609e705225e29" = "0g6xry0far2wppfxv99hqh3ckldglmpazib5l8vsvcmivcz5zww1";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."da79d544cf990fb0638ae5434407d6a30724fb87" = "11rxpcg2g781rbr5d20fw6zpv81q06w3c9bh6qh5cpnza230yvl3";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||
@@ -7,4 +7,6 @@
|
||||
"https://github.com/simplex-chat/haskell-terminal.git"."f708b00009b54890172068f168bf98508ffcd495" = "0zmq7lmfsk8m340g47g5963yba7i88n4afa6z93sg9px5jv1mijj";
|
||||
"https://github.com/simplex-chat/android-support.git"."9aa09f148089d6752ce563b14c2df1895718d806" = "0pbf2pf13v2kjzi397nr13f1h3jv0imvsq8rpiyy2qyx5vd50pqn";
|
||||
"https://github.com/simplex-chat/zip.git"."bd421c6b19cc4c465cd7af1f6f26169fb8ee1ebc" = "1csqfjhvc8wb5h4kxxndmb6iw7b4ib9ff2n81hrizsmnf45a6gg0";
|
||||
"https://github.com/yesodweb/wai.git"."ec5e017d896a78e787a5acea62b37a4e677dec2e" = "1ckcpmpjfy9jiqrb52q20lj7ln4hmq9v2jk6kpkf3m68c1m9c2bx";
|
||||
"https://github.com/simplex-chat/wai.git"."2f6e5aa5f05ba9140ac99e195ee647b4f7d926b0" = "199g4rjdf1zp1fcw8nqdsyr1h36hmg424qqx03071jk7j00z7ay4";
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 6.1.0.4
|
||||
version: 6.1.0.5
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
|
||||
@@ -52,7 +52,7 @@ import Simplex.Messaging.Protocol (srvHostnamesSMPClientVersion)
|
||||
import Simplex.Messaging.Server (runSMPServerBlocking)
|
||||
import Simplex.Messaging.Server.Env.STM
|
||||
import Simplex.Messaging.Transport
|
||||
import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig)
|
||||
import Simplex.Messaging.Transport.Server (ServerCredentials (..), TransportServerConfig (..), defaultTransportServerConfig)
|
||||
import Simplex.Messaging.Version
|
||||
import Simplex.Messaging.Version.Internal
|
||||
import System.Directory (createDirectoryIfMissing, removeDirectoryRecursive)
|
||||
@@ -420,7 +420,7 @@ concurrentlyN_ = mapConcurrently_ id
|
||||
smpServerCfg :: ServerConfig
|
||||
smpServerCfg =
|
||||
ServerConfig
|
||||
{ transports = [(serverPort, transport @TLS)],
|
||||
{ transports = [(serverPort, transport @TLS, False)],
|
||||
tbqSize = 1,
|
||||
-- serverTbqSize = 1,
|
||||
msgQueueQuota = 16,
|
||||
@@ -428,23 +428,30 @@ smpServerCfg =
|
||||
msgIdBytes = 6,
|
||||
storeLogFile = Nothing,
|
||||
storeMsgsFile = Nothing,
|
||||
storeNtfsFile = Nothing,
|
||||
allowNewQueues = True,
|
||||
-- server password is disabled as otherwise v1 tests fail
|
||||
newQueueBasicAuth = Nothing, -- Just "server_password",
|
||||
controlPortUserAuth = Nothing,
|
||||
controlPortAdminAuth = Nothing,
|
||||
messageExpiration = Just defaultMessageExpiration,
|
||||
notificationExpiration = defaultNtfExpiration,
|
||||
inactiveClientExpiration = Just defaultInactiveClientExpiration,
|
||||
caCertificateFile = "tests/fixtures/tls/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/tls/server.key",
|
||||
certificateFile = "tests/fixtures/tls/server.crt",
|
||||
smpCredentials =
|
||||
ServerCredentials
|
||||
{ caCertificateFile = Just "tests/fixtures/tls/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/tls/server.key",
|
||||
certificateFile = "tests/fixtures/tls/server.crt"
|
||||
},
|
||||
httpCredentials = Nothing,
|
||||
logStatsInterval = Nothing,
|
||||
logStatsStartTime = 0,
|
||||
serverStatsLogFile = "tests/smp-server-stats.daily.log",
|
||||
serverStatsBackupFile = Nothing,
|
||||
pendingENDInterval = 500000,
|
||||
ntfDeliveryInterval = 200000,
|
||||
smpServerVRange = supportedServerSMPRelayVRange,
|
||||
transportConfig = defaultTransportServerConfig {alpn = Just supportedSMPHandshakes},
|
||||
transportConfig = defaultTransportServerConfig,
|
||||
smpHandshakeTimeout = 1000000,
|
||||
controlPort = Nothing,
|
||||
smpAgentCfg = defaultSMPClientAgentConfig,
|
||||
@@ -457,7 +464,7 @@ withSmpServer :: IO () -> IO ()
|
||||
withSmpServer = withSmpServer' smpServerCfg
|
||||
|
||||
withSmpServer' :: ServerConfig -> IO () -> IO ()
|
||||
withSmpServer' cfg = serverBracket (`runSMPServerBlocking` cfg)
|
||||
withSmpServer' cfg = serverBracket (\started -> runSMPServerBlocking started cfg Nothing)
|
||||
|
||||
xftpTestPort :: ServiceName
|
||||
xftpTestPort = "7002"
|
||||
@@ -481,16 +488,19 @@ xftpServerConfig =
|
||||
fileExpiration = Just defaultFileExpiration,
|
||||
fileTimeout = 10000000,
|
||||
inactiveClientExpiration = Just defaultInactiveClientExpiration,
|
||||
caCertificateFile = "tests/fixtures/tls/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/tls/server.key",
|
||||
certificateFile = "tests/fixtures/tls/server.crt",
|
||||
xftpCredentials =
|
||||
ServerCredentials
|
||||
{ caCertificateFile = Just "tests/fixtures/tls/ca.crt",
|
||||
privateKeyFile = "tests/fixtures/tls/server.key",
|
||||
certificateFile = "tests/fixtures/tls/server.crt"
|
||||
},
|
||||
xftpServerVRange = supportedFileServerVRange,
|
||||
logStatsInterval = Nothing,
|
||||
logStatsStartTime = 0,
|
||||
serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log",
|
||||
serverStatsBackupFile = Nothing,
|
||||
controlPort = Nothing,
|
||||
transportConfig = defaultTransportServerConfig {alpn = Just supportedXFTPhandshakes},
|
||||
transportConfig = defaultTransportServerConfig,
|
||||
responseDelay = 0
|
||||
}
|
||||
|
||||
@@ -502,7 +512,7 @@ withXFTPServer' cfg =
|
||||
serverBracket
|
||||
( \started -> do
|
||||
createDirectoryIfMissing False xftpServerFiles
|
||||
runXFTPServerBlocking started cfg
|
||||
runXFTPServerBlocking started cfg Nothing
|
||||
)
|
||||
|
||||
serverBracket :: (TMVar Bool -> IO ()) -> IO () -> IO ()
|
||||
|
||||
@@ -6419,7 +6419,7 @@ testGroupMemberInactive tmp = do
|
||||
where
|
||||
serverCfg' =
|
||||
smpServerCfg
|
||||
{ transports = [("7003", transport @TLS)],
|
||||
{ transports = [("7003", transport @TLS, False)],
|
||||
msgQueueQuota = 2
|
||||
}
|
||||
fastRetryInterval = defaultReconnectInterval {initialInterval = 50_000} -- same as in agent tests
|
||||
|
||||
@@ -1689,7 +1689,7 @@ testChangePCCUserDiffSrv tmp = do
|
||||
where
|
||||
serverCfg' =
|
||||
smpServerCfg
|
||||
{ transports = [("7003", transport @TLS), ("7002", transport @TLS)],
|
||||
{ transports = [("7003", transport @TLS, False), ("7002", transport @TLS, False)],
|
||||
msgQueueQuota = 2
|
||||
}
|
||||
|
||||
|
||||