Merge branch 'master' into remote-desktop
@ -8,12 +8,12 @@ RUN a=$(arch); curl https://downloads.haskell.org/~ghcup/$a-linux-ghcup -o /usr/
|
||||
chmod +x /usr/bin/ghcup
|
||||
|
||||
# Install ghc
|
||||
RUN ghcup install ghc 8.10.7
|
||||
RUN ghcup install ghc 9.6.2
|
||||
# Install cabal
|
||||
RUN ghcup install cabal
|
||||
RUN ghcup install cabal 3.10.1.0
|
||||
# Set both as default
|
||||
RUN ghcup set ghc 8.10.7 && \
|
||||
ghcup set cabal
|
||||
RUN ghcup set ghc 9.6.2 && \
|
||||
ghcup set cabal 3.10.1.0
|
||||
|
||||
COPY . /project
|
||||
WORKDIR /project
|
||||
|
@ -356,7 +356,7 @@ struct SimplexLockView: View {
|
||||
var id: Self { self }
|
||||
}
|
||||
|
||||
let laDelays: [Int] = [10, 30, 60, 180, 0]
|
||||
let laDelays: [Int] = [10, 30, 60, 180, 600, 0]
|
||||
|
||||
func laDelayText(_ t: Int) -> LocalizedStringKey {
|
||||
let m = t / 60
|
||||
@ -378,6 +378,7 @@ struct SimplexLockView: View {
|
||||
Text(mode.text)
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
if performLA {
|
||||
Picker("Lock after", selection: $laLockDelay) {
|
||||
let delays = laDelays.contains(laLockDelay) ? laDelays : [laLockDelay] + laDelays
|
||||
@ -385,6 +386,7 @@ struct SimplexLockView: View {
|
||||
Text(laDelayText(t))
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
if showChangePassword && laMode == .passcode {
|
||||
Button("Change passcode") {
|
||||
changeLAPassword()
|
||||
|
@ -16,7 +16,6 @@
|
||||
18415C6C56DBCEC2CBBD2F11 /* WebRTCClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415323A4082FC92887F906 /* WebRTCClient.swift */; };
|
||||
18415F9A2D551F9757DA4654 /* CIVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415FD2E36F13F596A45BB4 /* CIVideoView.swift */; };
|
||||
18415FEFE153C5920BFB7828 /* GroupWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1841516F0CE5992B0EDFB377 /* GroupWelcomeView.swift */; };
|
||||
3C71477A281C0F6800CB4D4B /* www in Resources */ = {isa = PBXBuildFile; fileRef = 3C714779281C0F6800CB4D4B /* www */; };
|
||||
3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */; };
|
||||
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */; };
|
||||
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CDBCF4727FF621E00354CDD /* CILinkView.swift */; };
|
||||
@ -85,11 +84,6 @@
|
||||
5CA059ED279559F40002BEB4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059C4279559F40002BEB4 /* ContentView.swift */; };
|
||||
5CA059EF279559F40002BEB4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5CA059C5279559F40002BEB4 /* Assets.xcassets */; };
|
||||
5CA7DFC329302AF000F7FDDE /* AppSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA7DFC229302AF000F7FDDE /* AppSheet.swift */; };
|
||||
5CA8D0162AD746C8001FD661 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA8D0112AD746C8001FD661 /* libgmpxx.a */; };
|
||||
5CA8D0172AD746C8001FD661 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA8D0122AD746C8001FD661 /* libffi.a */; };
|
||||
5CA8D0182AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA8D0132AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a */; };
|
||||
5CA8D0192AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA8D0142AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a */; };
|
||||
5CA8D01A2AD746C8001FD661 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CA8D0152AD746C8001FD661 /* libgmp.a */; };
|
||||
5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79929211BB900072E13 /* PreferencesView.swift */; };
|
||||
5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */; };
|
||||
5CB0BA882826CB3A00B3292C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5CB0BA862826CB3A00B3292C /* InfoPlist.strings */; };
|
||||
@ -123,6 +117,11 @@
|
||||
5CCB939C297EFCB100399E78 /* NavStackCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */; };
|
||||
5CCD403427A5F6DF00368C90 /* AddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403327A5F6DF00368C90 /* AddContactView.swift */; };
|
||||
5CCD403727A5F9A200368C90 /* ScanToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */; };
|
||||
5CD089312AE59CB300669208 /* libHSsimplex-chat-5.4.0.2-d5Ky77yoZRFE1pplaEhZO-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CD0892C2AE59CB300669208 /* libHSsimplex-chat-5.4.0.2-d5Ky77yoZRFE1pplaEhZO-ghc8.10.7.a */; };
|
||||
5CD089322AE59CB300669208 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CD0892D2AE59CB300669208 /* libffi.a */; };
|
||||
5CD089332AE59CB300669208 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CD0892E2AE59CB300669208 /* libgmpxx.a */; };
|
||||
5CD089342AE59CB300669208 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CD0892F2AE59CB300669208 /* libgmp.a */; };
|
||||
5CD089352AE59CB300669208 /* libHSsimplex-chat-5.4.0.2-d5Ky77yoZRFE1pplaEhZO.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CD089302AE59CB300669208 /* libHSsimplex-chat-5.4.0.2-d5Ky77yoZRFE1pplaEhZO.a */; };
|
||||
5CDCAD482818589900503DA2 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CDCAD472818589900503DA2 /* NotificationService.swift */; };
|
||||
5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
|
||||
5CE2BA712845308900EC33A6 /* SimpleXChat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
@ -176,11 +175,6 @@
|
||||
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649BCDA12805D6EF00C3A862 /* CIImageView.swift */; };
|
||||
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6827EE10C800AC7277 /* ContextItemView.swift */; };
|
||||
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */; };
|
||||
64AB9C832AD6B6B900B21C4C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64AB9C7E2AD6B6B900B21C4C /* libgmp.a */; };
|
||||
64AB9C842AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64AB9C7F2AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a */; };
|
||||
64AB9C852AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64AB9C802AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a */; };
|
||||
64AB9C862AD6B6B900B21C4C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64AB9C812AD6B6B900B21C4C /* libffi.a */; };
|
||||
64AB9C872AD6B6B900B21C4C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64AB9C822AD6B6B900B21C4C /* libgmpxx.a */; };
|
||||
64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */; };
|
||||
64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; };
|
||||
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
|
||||
@ -262,7 +256,6 @@
|
||||
18415B08031E8FB0F7FC27F9 /* CallViewRenderers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallViewRenderers.swift; sourceTree = "<group>"; };
|
||||
18415DAAAD1ADBEDB0EDA852 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = "<group>"; };
|
||||
18415FD2E36F13F596A45BB4 /* CIVideoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIVideoView.swift; sourceTree = "<group>"; };
|
||||
3C714779281C0F6800CB4D4B /* www */ = {isa = PBXFileReference; lastKnownFileType = folder; name = www; path = ../multiplatform/android/src/main/assets/www; sourceTree = "<group>"; };
|
||||
3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteToConnectView.swift; sourceTree = "<group>"; };
|
||||
3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = "<group>"; };
|
||||
3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = "<group>"; };
|
||||
@ -363,11 +356,6 @@
|
||||
5CA85D0A297218AA0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5CA85D0C297219EF0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = "it.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
5CA85D0D297219EF0095AF72 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
5CA8D0112AD746C8001FD661 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CA8D0122AD746C8001FD661 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CA8D0132AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a"; sourceTree = "<group>"; };
|
||||
5CA8D0142AD746C8001FD661 /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5CA8D0152AD746C8001FD661 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CAB912529E93F9400F34A95 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5CAC41182A192D8400C331A2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5CAC411A2A192DE800C331A2 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = "ja.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
@ -409,6 +397,11 @@
|
||||
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavStackCompat.swift; sourceTree = "<group>"; };
|
||||
5CCD403327A5F6DF00368C90 /* AddContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactView.swift; sourceTree = "<group>"; };
|
||||
5CCD403627A5F9A200368C90 /* ScanToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanToConnectView.swift; sourceTree = "<group>"; };
|
||||
5CD0892C2AE59CB300669208 /* libHSsimplex-chat-5.4.0.2-d5Ky77yoZRFE1pplaEhZO-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.2-d5Ky77yoZRFE1pplaEhZO-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5CD0892D2AE59CB300669208 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CD0892E2AE59CB300669208 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CD0892F2AE59CB300669208 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CD089302AE59CB300669208 /* libHSsimplex-chat-5.4.0.2-d5Ky77yoZRFE1pplaEhZO.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.2-d5Ky77yoZRFE1pplaEhZO.a"; sourceTree = "<group>"; };
|
||||
5CDCAD452818589900503DA2 /* SimpleX NSE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX NSE.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5CDCAD472818589900503DA2 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||
5CDCAD492818589900503DA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
@ -463,11 +456,6 @@
|
||||
649BCDA12805D6EF00C3A862 /* CIImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIImageView.swift; sourceTree = "<group>"; };
|
||||
64AA1C6827EE10C800AC7277 /* ContextItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextItemView.swift; sourceTree = "<group>"; };
|
||||
64AA1C6B27F3537400AC7277 /* DeletedItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedItemView.swift; sourceTree = "<group>"; };
|
||||
64AB9C7E2AD6B6B900B21C4C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
64AB9C7F2AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
64AB9C802AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a"; sourceTree = "<group>"; };
|
||||
64AB9C812AD6B6B900B21C4C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
64AB9C822AD6B6B900B21C4C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemInfoView.swift; sourceTree = "<group>"; };
|
||||
64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = "<group>"; };
|
||||
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = "<group>"; };
|
||||
@ -517,13 +505,13 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
64AB9C842AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a in Frameworks */,
|
||||
64AB9C862AD6B6B900B21C4C /* libffi.a in Frameworks */,
|
||||
64AB9C852AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a in Frameworks */,
|
||||
5CD089352AE59CB300669208 /* libHSsimplex-chat-5.4.0.2-d5Ky77yoZRFE1pplaEhZO.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
64AB9C832AD6B6B900B21C4C /* libgmp.a in Frameworks */,
|
||||
5CD089332AE59CB300669208 /* libgmpxx.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
64AB9C872AD6B6B900B21C4C /* libgmpxx.a in Frameworks */,
|
||||
5CD089312AE59CB300669208 /* libHSsimplex-chat-5.4.0.2-d5Ky77yoZRFE1pplaEhZO-ghc8.10.7.a in Frameworks */,
|
||||
5CD089342AE59CB300669208 /* libgmp.a in Frameworks */,
|
||||
5CD089322AE59CB300669208 /* libffi.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -584,11 +572,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
64AB9C812AD6B6B900B21C4C /* libffi.a */,
|
||||
64AB9C7E2AD6B6B900B21C4C /* libgmp.a */,
|
||||
64AB9C822AD6B6B900B21C4C /* libgmpxx.a */,
|
||||
64AB9C7F2AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b-ghc8.10.7.a */,
|
||||
64AB9C802AD6B6B900B21C4C /* libHSsimplex-chat-5.4.0.1-7lTZkX9ojv2DbehL2eOY1b.a */,
|
||||
5CD0892D2AE59CB300669208 /* libffi.a */,
|
||||
5CD0892F2AE59CB300669208 /* libgmp.a */,
|
||||
5CD0892E2AE59CB300669208 /* libgmpxx.a */,
|
||||
5CD0892C2AE59CB300669208 /* libHSsimplex-chat-5.4.0.2-d5Ky77yoZRFE1pplaEhZO-ghc8.10.7.a */,
|
||||
5CD089302AE59CB300669208 /* libHSsimplex-chat-5.4.0.2-d5Ky77yoZRFE1pplaEhZO.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@ -648,7 +636,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5C55A92D283D0FDE00C4E99E /* sounds */,
|
||||
3C714779281C0F6800CB4D4B /* www */,
|
||||
5CC2C0FD2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings */,
|
||||
5CC2C0FA2809BF11000C35E3 /* Localizable.strings */,
|
||||
5C422A7C27A9A6FA0097A1E1 /* SimpleX (iOS).entitlements */,
|
||||
@ -1060,7 +1047,6 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5C55A92E283D0FDE00C4E99E /* sounds in Resources */,
|
||||
3C71477A281C0F6800CB4D4B /* www in Resources */,
|
||||
5CA059EF279559F40002BEB4 /* Assets.xcassets in Resources */,
|
||||
5CC2C0FC2809BF11000C35E3 /* Localizable.strings in Resources */,
|
||||
5CC2C0FF2809BF11000C35E3 /* SimpleX--iOS--InfoPlist.strings in Resources */,
|
||||
@ -1496,7 +1482,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 176;
|
||||
CURRENT_PROJECT_VERSION = 180;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@ -1538,7 +1524,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 176;
|
||||
CURRENT_PROJECT_VERSION = 180;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@ -1618,7 +1604,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 176;
|
||||
CURRENT_PROJECT_VERSION = 180;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@ -1650,7 +1636,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 176;
|
||||
CURRENT_PROJECT_VERSION = 180;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@ -1682,7 +1668,7 @@
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 176;
|
||||
CURRENT_PROJECT_VERSION = 180;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@ -1728,7 +1714,7 @@
|
||||
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 176;
|
||||
CURRENT_PROJECT_VERSION = 180;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
|
@ -77,6 +77,7 @@ android {
|
||||
}
|
||||
jniLibs.useLegacyPackaging = rootProject.extra["compression.level"] as Int != 0
|
||||
}
|
||||
android.sourceSets["main"].assets.setSrcDirs(listOf("../common/src/commonMain/resources/assets"))
|
||||
val isRelease = gradle.startParameter.taskNames.find { it.toLowerCase().contains("release") } != null
|
||||
val isBundle = gradle.startParameter.taskNames.find { it.toLowerCase().contains("bundle") } != null
|
||||
// if (isRelease) {
|
||||
|
@ -98,6 +98,8 @@ kotlin {
|
||||
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
|
||||
implementation("org.slf4j:slf4j-simple:2.0.7")
|
||||
implementation("uk.co.caprica:vlcj:4.7.3")
|
||||
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf85a")
|
||||
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf85a")
|
||||
}
|
||||
}
|
||||
val desktopTest by getting
|
||||
|
@ -18,6 +18,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@ -43,6 +44,9 @@ import chat.simplex.res.MR
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
@ -52,7 +56,7 @@ actual fun ActiveCallView() {
|
||||
val chatModel = ChatModel
|
||||
BackHandler(onBack = {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) withApi { chatModel.callManager.endCall(call) }
|
||||
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
|
||||
})
|
||||
val audioViaBluetooth = rememberSaveable { mutableStateOf(false) }
|
||||
val ntfModeService = remember { chatModel.controller.appPrefs.notificationsMode.get() == NotificationsMode.SERVICE }
|
||||
@ -112,30 +116,30 @@ actual fun ActiveCallView() {
|
||||
if (call != null) {
|
||||
Log.d(TAG, "has active call $call")
|
||||
when (val r = apiMsg.resp) {
|
||||
is WCallResponse.Capabilities -> withApi {
|
||||
is WCallResponse.Capabilities -> withBGApi {
|
||||
val callType = CallType(call.localMedia, r.capabilities)
|
||||
chatModel.controller.apiSendCallInvitation(call.contact, callType)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities)
|
||||
}
|
||||
is WCallResponse.Offer -> withApi {
|
||||
is WCallResponse.Offer -> withBGApi {
|
||||
chatModel.controller.apiSendCallOffer(call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities)
|
||||
}
|
||||
is WCallResponse.Answer -> withApi {
|
||||
is WCallResponse.Answer -> withBGApi {
|
||||
chatModel.controller.apiSendCallAnswer(call.contact, r.answer, r.iceCandidates)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
|
||||
}
|
||||
is WCallResponse.Ice -> withApi {
|
||||
is WCallResponse.Ice -> withBGApi {
|
||||
chatModel.controller.apiSendCallExtraInfo(call.contact, r.iceCandidates)
|
||||
}
|
||||
is WCallResponse.Connection ->
|
||||
try {
|
||||
val callStatus = json.decodeFromString<WebRTCCallStatus>("\"${r.state.connectionState}\"")
|
||||
if (callStatus == WebRTCCallStatus.Connected) {
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Connected)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectedAt = Clock.System.now())
|
||||
setCallSound(call.soundSpeaker, audioViaBluetooth)
|
||||
}
|
||||
withApi { chatModel.controller.apiCallStatus(call.contact, callStatus) }
|
||||
withBGApi { chatModel.controller.apiCallStatus(call.contact, callStatus) }
|
||||
} catch (e: Error) {
|
||||
Log.d(TAG,"call status ${r.state.connectionState} not used")
|
||||
}
|
||||
@ -145,9 +149,12 @@ actual fun ActiveCallView() {
|
||||
setCallSound(call.soundSpeaker, audioViaBluetooth)
|
||||
}
|
||||
}
|
||||
is WCallResponse.End -> {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
}
|
||||
is WCallResponse.Ended -> {
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Ended)
|
||||
withApi { chatModel.callManager.endCall(call) }
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
chatModel.showCallView.value = false
|
||||
}
|
||||
is WCallResponse.Ok -> when (val cmd = apiMsg.command) {
|
||||
@ -162,7 +169,7 @@ actual fun ActiveCallView() {
|
||||
is WCallCommand.Camera -> {
|
||||
chatModel.activeCall.value = call.copy(localCamera = cmd.camera)
|
||||
if (!call.audioEnabled) {
|
||||
chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Audio, enable = false)
|
||||
chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false))
|
||||
}
|
||||
}
|
||||
is WCallCommand.End ->
|
||||
@ -187,11 +194,14 @@ actual fun ActiveCallView() {
|
||||
// Lock orientation to portrait in order to have good experience with calls
|
||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
chatModel.activeCallViewIsVisible.value = true
|
||||
// After the first call, End command gets added to the list which prevents making another calls
|
||||
chatModel.callCommand.removeAll { it is WCallCommand.End }
|
||||
onDispose {
|
||||
activity.volumeControlStream = prevVolumeControlStream
|
||||
// Unlock orientation
|
||||
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
chatModel.activeCallViewIsVisible.value = false
|
||||
chatModel.callCommand.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -201,9 +211,9 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetoot
|
||||
ActiveCallOverlayLayout(
|
||||
call = call,
|
||||
speakerCanBeEnabled = !audioViaBluetooth.value,
|
||||
dismiss = { withApi { chatModel.callManager.endCall(call) } },
|
||||
toggleAudio = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled) },
|
||||
toggleVideo = { chatModel.callCommand.value = WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled) },
|
||||
dismiss = { withBGApi { chatModel.callManager.endCall(call) } },
|
||||
toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled)) },
|
||||
toggleVideo = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled)) },
|
||||
toggleSound = {
|
||||
var call = chatModel.activeCall.value
|
||||
if (call != null) {
|
||||
@ -212,7 +222,7 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, audioViaBluetoot
|
||||
setCallSound(call.soundSpeaker, audioViaBluetooth)
|
||||
}
|
||||
},
|
||||
flipCamera = { chatModel.callCommand.value = WCallCommand.Camera(call.localCamera.flipped) }
|
||||
flipCamera = { chatModel.callCommand.add(WCallCommand.Camera(call.localCamera.flipped)) }
|
||||
)
|
||||
}
|
||||
|
||||
@ -439,7 +449,7 @@ private fun DisabledBackgroundCallsButton() {
|
||||
//}
|
||||
|
||||
@Composable
|
||||
fun WebRTCView(callCommand: MutableState<WCallCommand?>, onResponse: (WVAPIMessage) -> Unit) {
|
||||
fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val webView = remember { mutableStateOf<WebView?>(null) }
|
||||
val permissionsState = rememberMultiplePermissionsState(
|
||||
@ -470,13 +480,19 @@ fun WebRTCView(callCommand: MutableState<WCallCommand?>, onResponse: (WVAPIMessa
|
||||
webView.value = null
|
||||
}
|
||||
}
|
||||
LaunchedEffect(callCommand.value, webView.value) {
|
||||
val cmd = callCommand.value
|
||||
val wv = webView.value
|
||||
if (cmd != null && wv != null) {
|
||||
Log.d(TAG, "WebRTCView LaunchedEffect executing $cmd")
|
||||
processCommand(wv, cmd)
|
||||
callCommand.value = null
|
||||
val wv = webView.value
|
||||
if (wv != null) {
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { callCommand.firstOrNull() }
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.collect {
|
||||
while (callCommand.isNotEmpty()) {
|
||||
val cmd = callCommand.removeFirst()
|
||||
Log.d(TAG, "WebRTCView LaunchedEffect executing $cmd")
|
||||
processCommand(wv, cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val assetLoader = WebViewAssetLoader.Builder()
|
||||
@ -502,7 +518,7 @@ fun WebRTCView(callCommand: MutableState<WCallCommand?>, onResponse: (WVAPIMessa
|
||||
}
|
||||
}
|
||||
}
|
||||
this.webViewClient = LocalContentWebViewClient(assetLoader)
|
||||
this.webViewClient = LocalContentWebViewClient(webView, assetLoader)
|
||||
this.clearHistory()
|
||||
this.clearCache(true)
|
||||
this.addJavascriptInterface(WebRTCInterface(onResponse), "WebRTCInterface")
|
||||
@ -512,19 +528,10 @@ fun WebRTCView(callCommand: MutableState<WCallCommand?>, onResponse: (WVAPIMessa
|
||||
webViewSettings.javaScriptEnabled = true
|
||||
webViewSettings.mediaPlaybackRequiresUserGesture = false
|
||||
webViewSettings.cacheMode = WebSettings.LOAD_NO_CACHE
|
||||
this.loadUrl("file:android_asset/www/call.html")
|
||||
this.loadUrl("file:android_asset/www/android/call.html")
|
||||
}
|
||||
}
|
||||
) { wv ->
|
||||
Log.d(TAG, "WebRTCView: webview ready")
|
||||
// for debugging
|
||||
// wv.evaluateJavascript("sendMessageToNative = ({resp}) => WebRTCInterface.postMessage(JSON.stringify({command: resp}))", null)
|
||||
scope.launch {
|
||||
delay(2000L)
|
||||
wv.evaluateJavascript("sendMessageToNative = (msg) => WebRTCInterface.postMessage(JSON.stringify(msg))", null)
|
||||
webView.value = wv
|
||||
}
|
||||
}
|
||||
) { /* WebView */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -539,19 +546,28 @@ class WebRTCInterface(private val onResponse: (WVAPIMessage) -> Unit) {
|
||||
// for debugging
|
||||
// onResponse(message)
|
||||
onResponse(json.decodeFromString(message))
|
||||
} catch (e: Error) {
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "failed parsing WebView message: $message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class LocalContentWebViewClient(private val assetLoader: WebViewAssetLoader) : WebViewClientCompat() {
|
||||
private class LocalContentWebViewClient(val webView: MutableState<WebView?>, private val assetLoader: WebViewAssetLoader) : WebViewClientCompat() {
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
request: WebResourceRequest
|
||||
): WebResourceResponse? {
|
||||
return assetLoader.shouldInterceptRequest(request.url)
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
super.onPageFinished(view, url)
|
||||
view.evaluateJavascript("sendMessageToNative = (msg) => WebRTCInterface.postMessage(JSON.stringify(msg))", null)
|
||||
webView.value = view
|
||||
Log.d(TAG, "WebRTCView: webview ready")
|
||||
// for debugging
|
||||
// view.evaluateJavascript("sendMessageToNative = ({resp}) => WebRTCInterface.postMessage(JSON.stringify({command: resp}))", null)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
@ -0,0 +1,8 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Composable
|
||||
actual fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>) {}
|
@ -88,7 +88,7 @@ object ChatModel {
|
||||
val activeCallInvitation = mutableStateOf<RcvCallInvitation?>(null)
|
||||
val activeCall = mutableStateOf<Call?>(null)
|
||||
val activeCallViewIsVisible = mutableStateOf<Boolean>(false)
|
||||
val callCommand = mutableStateOf<WCallCommand?>(null)
|
||||
val callCommand = mutableStateListOf<WCallCommand>()
|
||||
val showCallView = mutableStateOf(false)
|
||||
val switchingCall = mutableStateOf(false)
|
||||
|
||||
|
@ -1702,25 +1702,25 @@ object ChatController {
|
||||
val useRelay = appPrefs.webrtcPolicyRelay.get()
|
||||
val iceServers = getIceServers()
|
||||
Log.d(TAG, ".callOffer iceServers $iceServers")
|
||||
chatModel.callCommand.value = WCallCommand.Offer(
|
||||
chatModel.callCommand.add(WCallCommand.Offer(
|
||||
offer = r.offer.rtcSession,
|
||||
iceCandidates = r.offer.rtcIceCandidates,
|
||||
media = r.callType.media,
|
||||
aesKey = r.sharedKey,
|
||||
iceServers = iceServers,
|
||||
relay = useRelay
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
is CR.CallAnswer -> {
|
||||
withCall(r, r.contact) { call ->
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.AnswerReceived)
|
||||
chatModel.callCommand.value = WCallCommand.Answer(answer = r.answer.rtcSession, iceCandidates = r.answer.rtcIceCandidates)
|
||||
chatModel.callCommand.add(WCallCommand.Answer(answer = r.answer.rtcSession, iceCandidates = r.answer.rtcIceCandidates))
|
||||
}
|
||||
}
|
||||
is CR.CallExtraInfo -> {
|
||||
withCall(r, r.contact) { _ ->
|
||||
chatModel.callCommand.value = WCallCommand.Ice(iceCandidates = r.extraInfo.rtcIceCandidates)
|
||||
chatModel.callCommand.add(WCallCommand.Ice(iceCandidates = r.extraInfo.rtcIceCandidates))
|
||||
}
|
||||
}
|
||||
is CR.CallEnded -> {
|
||||
@ -1729,7 +1729,7 @@ object ChatController {
|
||||
chatModel.callManager.reportCallRemoteEnded(invitation = invitation)
|
||||
}
|
||||
withCall(r, r.contact) { _ ->
|
||||
chatModel.callCommand.value = WCallCommand.End
|
||||
chatModel.callCommand.add(WCallCommand.End)
|
||||
withApi {
|
||||
chatModel.activeCall.value = null
|
||||
chatModel.showCallView.value = false
|
||||
|
@ -3,8 +3,6 @@ package chat.simplex.common.views.call
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.withApi
|
||||
import chat.simplex.common.views.helpers.withBGApi
|
||||
import chat.simplex.common.views.usersettings.showInDevelopingAlert
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
|
||||
@ -26,10 +24,6 @@ class CallManager(val chatModel: ChatModel) {
|
||||
}
|
||||
|
||||
fun acceptIncomingCall(invitation: RcvCallInvitation) {
|
||||
if (appPlatform.isDesktop) {
|
||||
return showInDevelopingAlert()
|
||||
}
|
||||
|
||||
val call = chatModel.activeCall.value
|
||||
if (call == null) {
|
||||
justAcceptIncomingCall(invitation = invitation)
|
||||
@ -58,12 +52,12 @@ class CallManager(val chatModel: ChatModel) {
|
||||
val useRelay = controller.appPrefs.webrtcPolicyRelay.get()
|
||||
val iceServers = getIceServers()
|
||||
Log.d(TAG, "answerIncomingCall iceServers: $iceServers")
|
||||
callCommand.value = WCallCommand.Start(
|
||||
callCommand.add(WCallCommand.Start(
|
||||
media = invitation.callType.media,
|
||||
aesKey = invitation.sharedKey,
|
||||
iceServers = iceServers,
|
||||
relay = useRelay
|
||||
)
|
||||
))
|
||||
callInvitations.remove(invitation.contact.id)
|
||||
if (invitation.contact.id == activeCallInvitation.value?.contact?.id) {
|
||||
activeCallInvitation.value = null
|
||||
@ -80,7 +74,7 @@ class CallManager(val chatModel: ChatModel) {
|
||||
showCallView.value = false
|
||||
} else {
|
||||
Log.d(TAG, "CallManager.endCall: ending call...")
|
||||
callCommand.value = WCallCommand.End
|
||||
callCommand.add(WCallCommand.End)
|
||||
showCallView.value = false
|
||||
controller.apiEndCall(call.contact)
|
||||
activeCall.value = null
|
||||
|
@ -1,7 +1,5 @@
|
||||
package chat.simplex.common.views.call
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.res.MR
|
||||
@ -23,16 +21,17 @@ data class Call(
|
||||
val videoEnabled: Boolean = localMedia == CallMediaType.Video,
|
||||
val soundSpeaker: Boolean = localMedia == CallMediaType.Video,
|
||||
var localCamera: VideoCamera = VideoCamera.User,
|
||||
val connectionInfo: ConnectionInfo? = null
|
||||
val connectionInfo: ConnectionInfo? = null,
|
||||
var connectedAt: Instant? = null
|
||||
) {
|
||||
val encrypted: Boolean get() = localEncrypted && sharedKey != null
|
||||
val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false
|
||||
|
||||
val encryptionStatus: String @Composable get() = when(callState) {
|
||||
val encryptionStatus: String get() = when(callState) {
|
||||
CallState.WaitCapabilities -> ""
|
||||
CallState.InvitationSent -> stringResource(if (localEncrypted) MR.strings.status_e2e_encrypted else MR.strings.status_no_e2e_encryption)
|
||||
CallState.InvitationAccepted -> stringResource(if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_contact_has_e2e_encryption)
|
||||
else -> stringResource(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)
|
||||
CallState.InvitationSent -> generalGetString(if (localEncrypted) MR.strings.status_e2e_encrypted else MR.strings.status_no_e2e_encryption)
|
||||
CallState.InvitationAccepted -> generalGetString(if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_contact_has_e2e_encryption)
|
||||
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
|
||||
@ -49,16 +48,16 @@ enum class CallState {
|
||||
Connected,
|
||||
Ended;
|
||||
|
||||
val text: String @Composable get() = when(this) {
|
||||
WaitCapabilities -> stringResource(MR.strings.callstate_starting)
|
||||
InvitationSent -> stringResource(MR.strings.callstate_waiting_for_answer)
|
||||
InvitationAccepted -> stringResource(MR.strings.callstate_starting)
|
||||
OfferSent -> stringResource(MR.strings.callstate_waiting_for_confirmation)
|
||||
OfferReceived -> stringResource(MR.strings.callstate_received_answer)
|
||||
AnswerReceived -> stringResource(MR.strings.callstate_received_confirmation)
|
||||
Negotiated -> stringResource(MR.strings.callstate_connecting)
|
||||
Connected -> stringResource(MR.strings.callstate_connected)
|
||||
Ended -> stringResource(MR.strings.callstate_ended)
|
||||
val text: String get() = when(this) {
|
||||
WaitCapabilities -> generalGetString(MR.strings.callstate_starting)
|
||||
InvitationSent -> generalGetString(MR.strings.callstate_waiting_for_answer)
|
||||
InvitationAccepted -> generalGetString(MR.strings.callstate_starting)
|
||||
OfferSent -> generalGetString(MR.strings.callstate_waiting_for_confirmation)
|
||||
OfferReceived -> generalGetString(MR.strings.callstate_received_answer)
|
||||
AnswerReceived -> generalGetString(MR.strings.callstate_received_confirmation)
|
||||
Negotiated -> generalGetString(MR.strings.callstate_connecting)
|
||||
Connected -> generalGetString(MR.strings.callstate_connected)
|
||||
Ended -> generalGetString(MR.strings.callstate_ended)
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,13 +66,14 @@ enum class CallState {
|
||||
|
||||
@Serializable
|
||||
sealed class WCallCommand {
|
||||
@Serializable @SerialName("capabilities") object Capabilities: WCallCommand()
|
||||
@Serializable @SerialName("capabilities") data class Capabilities(val media: CallMediaType): WCallCommand()
|
||||
@Serializable @SerialName("start") data class Start(val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): 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("camera") data class Camera(val camera: VideoCamera): WCallCommand()
|
||||
@Serializable @SerialName("description") data class Description(val state: String, val description: String): WCallCommand()
|
||||
@Serializable @SerialName("end") object End: WCallCommand()
|
||||
}
|
||||
|
||||
@ -85,6 +85,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("end") object End: WCallResponse()
|
||||
@Serializable @SerialName("ended") object Ended: WCallResponse()
|
||||
@Serializable @SerialName("ok") object Ok: WCallResponse()
|
||||
@Serializable @SerialName("error") data class Error(val message: String): WCallResponse()
|
||||
@ -106,14 +107,14 @@ sealed class WCallResponse {
|
||||
}
|
||||
@Serializable data class CallCapabilities(val encryption: Boolean)
|
||||
@Serializable data class ConnectionInfo(private val localCandidate: RTCIceCandidate?, private val remoteCandidate: RTCIceCandidate?) {
|
||||
val text: String @Composable get() {
|
||||
val text: String get() {
|
||||
val local = localCandidate?.candidateType
|
||||
val remote = remoteCandidate?.candidateType
|
||||
return when {
|
||||
local == RTCIceCandidateType.Host && remote == RTCIceCandidateType.Host ->
|
||||
stringResource(MR.strings.call_connection_peer_to_peer)
|
||||
generalGetString(MR.strings.call_connection_peer_to_peer)
|
||||
local == RTCIceCandidateType.Relay && remote == RTCIceCandidateType.Relay ->
|
||||
stringResource(MR.strings.call_connection_via_relay)
|
||||
generalGetString(MR.strings.call_connection_via_relay)
|
||||
else ->
|
||||
"${local?.value ?: "unknown"} / ${remote?.value ?: "unknown"}"
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.common.model.GroupInfo
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.platform.AudioPlayer
|
||||
import chat.simplex.common.views.usersettings.showInDevelopingAlert
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
@ -274,23 +273,24 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId:
|
||||
withApi { chatModel.controller.apiJoinGroup(groupId) }
|
||||
},
|
||||
startCall = out@ { media ->
|
||||
if (appPlatform.isDesktop) {
|
||||
return@out showInDevelopingAlert()
|
||||
}
|
||||
withBGApi {
|
||||
val cInfo = chat.chatInfo
|
||||
if (cInfo is ChatInfo.Direct) {
|
||||
chatModel.activeCall.value = Call(contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media)
|
||||
chatModel.showCallView.value = true
|
||||
chatModel.callCommand.value = WCallCommand.Capabilities
|
||||
chatModel.callCommand.add(WCallCommand.Capabilities(media))
|
||||
}
|
||||
}
|
||||
},
|
||||
endCall = {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) withApi { chatModel.callManager.endCall(call) }
|
||||
},
|
||||
acceptCall = { contact ->
|
||||
hideKeyboard(view)
|
||||
val invitation = chatModel.callInvitations.remove(contact.id)
|
||||
if (invitation == null) {
|
||||
AlertManager.shared.showAlertMsg("Call already ended!")
|
||||
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended))
|
||||
} else {
|
||||
chatModel.callManager.acceptIncomingCall(invitation = invitation)
|
||||
}
|
||||
@ -433,6 +433,7 @@ fun ChatLayout(
|
||||
cancelFile: (Long) -> Unit,
|
||||
joinGroup: (Long) -> Unit,
|
||||
startCall: (CallMediaType) -> Unit,
|
||||
endCall: () -> Unit,
|
||||
acceptCall: (Contact) -> Unit,
|
||||
acceptFeature: (Contact, ChatFeature, Int?) -> Unit,
|
||||
openDirectChat: (Long) -> Unit,
|
||||
@ -491,7 +492,7 @@ fun ChatLayout(
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { ChatInfoToolbar(chat, back, info, startCall, addMembers, changeNtfsState, onSearchValueChanged) },
|
||||
topBar = { ChatInfoToolbar(chat, back, info, startCall, endCall, addMembers, changeNtfsState, onSearchValueChanged) },
|
||||
bottomBar = composeView,
|
||||
modifier = Modifier.navigationBarsWithImePadding(),
|
||||
floatingActionButton = { floatingButton.value() },
|
||||
@ -520,6 +521,7 @@ fun ChatInfoToolbar(
|
||||
back: () -> Unit,
|
||||
info: () -> Unit,
|
||||
startCall: (CallMediaType) -> Unit,
|
||||
endCall: () -> Unit,
|
||||
addMembers: (GroupInfo) -> Unit,
|
||||
changeNtfsState: (Boolean, currentValue: MutableState<Boolean>) -> Unit,
|
||||
onSearchValueChanged: (String) -> Unit,
|
||||
@ -540,6 +542,7 @@ fun ChatInfoToolbar(
|
||||
}
|
||||
val barButtons = arrayListOf<@Composable RowScope.() -> Unit>()
|
||||
val menuItems = arrayListOf<@Composable () -> Unit>()
|
||||
val activeCall by remember { chatModel.activeCall }
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = {
|
||||
showMenu.value = false
|
||||
@ -548,20 +551,52 @@ fun ChatInfoToolbar(
|
||||
}
|
||||
|
||||
if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.allowsFeature(ChatFeature.Calls)) {
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Audio)
|
||||
},
|
||||
enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_500),
|
||||
stringResource(MR.strings.icon_descr_more_button),
|
||||
tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
if (activeCall == null) {
|
||||
barButtons.add {
|
||||
IconButton(
|
||||
{
|
||||
showMenu.value = false
|
||||
startCall(CallMediaType.Audio)
|
||||
},
|
||||
enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active
|
||||
) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_500),
|
||||
stringResource(MR.strings.icon_descr_more_button),
|
||||
tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (activeCall?.contact?.id == chat.id) {
|
||||
barButtons.add {
|
||||
val call = remember { chatModel.activeCall }.value
|
||||
val connectedAt = call?.connectedAt
|
||||
if (connectedAt != null) {
|
||||
val time = remember { mutableStateOf(durationText(0)) }
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
time.value = durationText((Clock.System.now() - connectedAt).inWholeSeconds.toInt())
|
||||
delay(250)
|
||||
}
|
||||
}
|
||||
val sp50 = with(LocalDensity.current) { 50.sp.toDp() }
|
||||
Text(time.value, Modifier.widthIn(min = sp50))
|
||||
}
|
||||
}
|
||||
barButtons.add {
|
||||
IconButton({
|
||||
showMenu.value = false
|
||||
endCall()
|
||||
}) {
|
||||
Icon(
|
||||
painterResource(MR.images.ic_call_end_filled),
|
||||
null,
|
||||
tint = MaterialTheme.colors.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) {
|
||||
if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active && activeCall == null) {
|
||||
menuItems.add {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = {
|
||||
showMenu.value = false
|
||||
@ -1290,6 +1325,7 @@ fun PreviewChatLayout() {
|
||||
cancelFile = {},
|
||||
joinGroup = {},
|
||||
startCall = {},
|
||||
endCall = {},
|
||||
acceptCall = { _ -> },
|
||||
acceptFeature = { _, _, _ -> },
|
||||
openDirectChat = { _ -> },
|
||||
@ -1359,6 +1395,7 @@ fun PreviewGroupChatLayout() {
|
||||
cancelFile = {},
|
||||
joinGroup = {},
|
||||
startCall = {},
|
||||
endCall = {},
|
||||
acceptCall = { _ -> },
|
||||
acceptFeature = { _, _, _ -> },
|
||||
openDirectChat = { _ -> },
|
||||
|
@ -3,7 +3,6 @@ package chat.simplex.common.views.chat.group
|
||||
import InfoRow
|
||||
import SectionBottomSpacer
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
@ -35,7 +34,7 @@ import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.common.views.usersettings.SettingsActionItem
|
||||
import chat.simplex.common.model.GroupInfo
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.chatlist.openChat
|
||||
import chat.simplex.common.views.chatlist.openLoadedChat
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Clock
|
||||
|
||||
@ -87,7 +86,7 @@ fun GroupMemberInfoView(
|
||||
if (memberContact != null) {
|
||||
val memberChat = Chat(ChatInfo.Direct(memberContact), chatItems = arrayListOf())
|
||||
chatModel.addChat(memberChat)
|
||||
openChat(memberChat, chatModel)
|
||||
openLoadedChat(memberChat, chatModel)
|
||||
closeAll()
|
||||
chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected())
|
||||
}
|
||||
|
@ -68,7 +68,7 @@ fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) {
|
||||
// sharedKey: invitation.sharedKey
|
||||
// )
|
||||
// m.showCallView = true
|
||||
// m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true)
|
||||
// m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey: true)
|
||||
// } else {
|
||||
// AlertManager.shared.showAlertMsg(title: "Call already ended!")
|
||||
// }
|
||||
@ -141,7 +141,7 @@ fun AcceptCallButton(cInfo: ChatInfo, acceptCall: (Contact) -> Unit) {
|
||||
// sharedKey: invitation.sharedKey
|
||||
// )
|
||||
// m.showCallView = true
|
||||
// m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true)
|
||||
// m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey: true)
|
||||
// } else {
|
||||
// AlertManager.shared.showAlertMsg(title: "Call already ended!")
|
||||
// }
|
||||
|
@ -4,7 +4,6 @@ import SectionItemView
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
@ -13,10 +12,6 @@ import dev.icerock.moko.resources.compose.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.InteractionSource
|
||||
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||
import androidx.compose.ui.platform.LocalViewConfiguration
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
@ -126,14 +121,14 @@ fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel) {
|
||||
suspend fun openDirectChat(contactId: Long, chatModel: ChatModel) {
|
||||
val chat = chatModel.controller.apiGetChat(ChatType.Direct, contactId)
|
||||
if (chat != null) {
|
||||
openChat(chat, chatModel)
|
||||
openLoadedChat(chat, chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun openGroupChat(groupId: Long, chatModel: ChatModel) {
|
||||
val chat = chatModel.controller.apiGetChat(ChatType.Group, groupId)
|
||||
if (chat != null) {
|
||||
openChat(chat, chatModel)
|
||||
openLoadedChat(chat, chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,12 +136,12 @@ suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) {
|
||||
Log.d(TAG, "TODOCHAT: openChat: opening ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
|
||||
val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId)
|
||||
if (chat != null) {
|
||||
openChat(chat, chatModel)
|
||||
openLoadedChat(chat, chatModel)
|
||||
Log.d(TAG, "TODOCHAT: openChat: opened ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun openChat(chat: Chat, chatModel: ChatModel) {
|
||||
fun openLoadedChat(chat: Chat, chatModel: ChatModel) {
|
||||
chatModel.chatItems.clear()
|
||||
chatModel.chatItemStatuses.clear()
|
||||
chatModel.chatItems.addAll(chat.chatItems)
|
||||
|
@ -12,6 +12,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
@ -29,6 +30,9 @@ import chat.simplex.common.views.onboarding.shouldShowWhatsNew
|
||||
import chat.simplex.common.views.usersettings.SettingsView
|
||||
import chat.simplex.common.views.usersettings.simplexTeamUri
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.call.Call
|
||||
import chat.simplex.common.views.call.CallMediaType
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.newchat.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
@ -121,6 +125,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
|
||||
}
|
||||
}
|
||||
if (searchInList.isEmpty()) {
|
||||
DesktopActiveCallOverlayLayout(newChatSheetState)
|
||||
NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet)
|
||||
}
|
||||
if (appPlatform.isAndroid) {
|
||||
@ -311,6 +316,9 @@ private fun ProgressIndicator() {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
expect fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>)
|
||||
|
||||
fun connectIfOpenedViaUri(uri: URI, chatModel: ChatModel) {
|
||||
Log.d(TAG, "connectIfOpenedViaUri: opened via link")
|
||||
if (chatModel.currentUser.value == null) {
|
||||
|
@ -50,11 +50,12 @@ class AlertManager {
|
||||
fun showAlertDialogButtonsColumn(
|
||||
title: String,
|
||||
text: AnnotatedString? = null,
|
||||
onDismissRequest: (() -> Unit)? = null,
|
||||
buttons: @Composable () -> Unit,
|
||||
) {
|
||||
showAlert {
|
||||
AlertDialog(
|
||||
onDismissRequest = ::hideAlert,
|
||||
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
|
||||
title = {
|
||||
Text(
|
||||
title,
|
||||
|
@ -81,6 +81,35 @@ fun ProfileImage(
|
||||
}
|
||||
}
|
||||
|
||||
/** [AccountCircleFilled] has its inner padding which leads to visible border if there is background underneath.
|
||||
* This is workaround
|
||||
* */
|
||||
@Composable
|
||||
fun ProfileImageForActiveCall(
|
||||
size: Dp,
|
||||
image: String? = null,
|
||||
color: Color = MaterialTheme.colors.secondaryVariant,
|
||||
) {
|
||||
if (image == null) {
|
||||
Box(Modifier.requiredSize(size).clip(CircleShape)) {
|
||||
Icon(
|
||||
AccountCircleFilled,
|
||||
contentDescription = stringResource(MR.strings.icon_descr_profile_image_placeholder),
|
||||
tint = color,
|
||||
modifier = Modifier.requiredSize(size + 14.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val imageBitmap = base64ToBitmap(image)
|
||||
Image(
|
||||
imageBitmap,
|
||||
stringResource(MR.strings.image_descr_profile_image),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.size(size).clip(CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
|
@ -317,7 +317,7 @@ private fun showUserGroupsReceiptsAlert(
|
||||
)
|
||||
}
|
||||
|
||||
private val laDelays = listOf(10, 30, 60, 180, 0)
|
||||
private val laDelays = listOf(10, 30, 60, 180, 600, 0)
|
||||
|
||||
@Composable
|
||||
fun SimplexLockView(
|
||||
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<link href="./style.css" rel="stylesheet" />
|
||||
<script src="./lz-string.min.js"></script>
|
||||
<script src="../lz-string.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<video
|
||||
@ -21,6 +21,6 @@
|
||||
></video>
|
||||
</body>
|
||||
<footer>
|
||||
<script src="./call.js"></script>
|
||||
<script src="../call.js"></script>
|
||||
</footer>
|
||||
</html>
|
@ -23,6 +23,9 @@ var TransformOperation;
|
||||
})(TransformOperation || (TransformOperation = {}));
|
||||
let activeCall;
|
||||
let answerTimeout = 30000;
|
||||
var useWorker = false;
|
||||
var localizedState = "";
|
||||
var localizedDescription = "";
|
||||
const processCommand = (function () {
|
||||
const defaultIceServers = [
|
||||
{ urls: ["stun:stun.simplex.im:443"] },
|
||||
@ -38,9 +41,9 @@ const processCommand = (function () {
|
||||
iceTransportPolicy: relay ? "relay" : "all",
|
||||
},
|
||||
iceCandidates: {
|
||||
delay: 3000,
|
||||
extrasInterval: 2000,
|
||||
extrasTimeout: 8000,
|
||||
delay: 750,
|
||||
extrasInterval: 1500,
|
||||
extrasTimeout: 12000,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -81,6 +84,8 @@ const processCommand = (function () {
|
||||
if (delay)
|
||||
clearTimeout(delay);
|
||||
resolved = true;
|
||||
// console.log("resolveIceCandidates", JSON.stringify(candidates))
|
||||
console.log("resolveIceCandidates");
|
||||
const iceCandidates = serialize(candidates);
|
||||
candidates = [];
|
||||
resolve(iceCandidates);
|
||||
@ -88,19 +93,21 @@ const processCommand = (function () {
|
||||
function sendIceCandidates() {
|
||||
if (candidates.length === 0)
|
||||
return;
|
||||
// console.log("sendIceCandidates", JSON.stringify(candidates))
|
||||
console.log("sendIceCandidates");
|
||||
const iceCandidates = serialize(candidates);
|
||||
candidates = [];
|
||||
sendMessageToNative({ resp: { type: "ice", iceCandidates } });
|
||||
}
|
||||
});
|
||||
}
|
||||
async function initializeCall(config, mediaType, aesKey, useWorker) {
|
||||
async function initializeCall(config, mediaType, aesKey) {
|
||||
const pc = new RTCPeerConnection(config.peerConnectionConfig);
|
||||
const remoteStream = new MediaStream();
|
||||
const localCamera = VideoCamera.User;
|
||||
const localStream = await getLocalMediaStream(mediaType, localCamera);
|
||||
const iceCandidates = getIceCandidates(pc, config);
|
||||
const call = { connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker };
|
||||
const call = { connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey };
|
||||
await setupMediaStreams(call);
|
||||
let connectionTimeout = setTimeout(connectionHandler, answerTimeout);
|
||||
pc.addEventListener("connectionstatechange", connectionStateChange);
|
||||
@ -178,17 +185,17 @@ const processCommand = (function () {
|
||||
// This request for local media stream is made to prompt for camera/mic permissions on call start
|
||||
if (command.media)
|
||||
await getLocalMediaStream(command.media, VideoCamera.User);
|
||||
const encryption = supportsInsertableStreams(command.useWorker);
|
||||
const encryption = supportsInsertableStreams(useWorker);
|
||||
resp = { type: "capabilities", capabilities: { encryption } };
|
||||
break;
|
||||
case "start": {
|
||||
console.log("starting incoming call - create webrtc session");
|
||||
if (activeCall)
|
||||
endCall();
|
||||
const { media, useWorker, iceServers, relay } = command;
|
||||
const { media, iceServers, relay } = command;
|
||||
const encryption = supportsInsertableStreams(useWorker);
|
||||
const aesKey = encryption ? command.aesKey : undefined;
|
||||
activeCall = await initializeCall(getCallConfig(encryption && !!aesKey, iceServers, relay), media, aesKey, useWorker);
|
||||
activeCall = await initializeCall(getCallConfig(encryption && !!aesKey, iceServers, relay), media, aesKey);
|
||||
const pc = activeCall.connection;
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
@ -202,7 +209,6 @@ const processCommand = (function () {
|
||||
// iceServers,
|
||||
// relay,
|
||||
// aesKey,
|
||||
// useWorker,
|
||||
// }
|
||||
resp = {
|
||||
type: "offer",
|
||||
@ -210,21 +216,23 @@ const processCommand = (function () {
|
||||
iceCandidates: await activeCall.iceCandidates,
|
||||
capabilities: { encryption },
|
||||
};
|
||||
// console.log("offer response", JSON.stringify(resp))
|
||||
break;
|
||||
}
|
||||
case "offer":
|
||||
if (activeCall) {
|
||||
resp = { type: "error", message: "accept: call already started" };
|
||||
}
|
||||
else if (!supportsInsertableStreams(command.useWorker) && command.aesKey) {
|
||||
else if (!supportsInsertableStreams(useWorker) && command.aesKey) {
|
||||
resp = { type: "error", message: "accept: encryption is not supported" };
|
||||
}
|
||||
else {
|
||||
const offer = parse(command.offer);
|
||||
const remoteIceCandidates = parse(command.iceCandidates);
|
||||
const { media, aesKey, useWorker, iceServers, relay } = command;
|
||||
activeCall = await initializeCall(getCallConfig(!!aesKey, iceServers, relay), media, aesKey, useWorker);
|
||||
const { media, aesKey, iceServers, relay } = command;
|
||||
activeCall = await initializeCall(getCallConfig(!!aesKey, iceServers, relay), media, aesKey);
|
||||
const pc = activeCall.connection;
|
||||
// console.log("offer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(offer));
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
@ -236,6 +244,7 @@ const processCommand = (function () {
|
||||
iceCandidates: await activeCall.iceCandidates,
|
||||
};
|
||||
}
|
||||
// console.log("answer response", JSON.stringify(resp))
|
||||
break;
|
||||
case "answer":
|
||||
if (!pc) {
|
||||
@ -250,6 +259,7 @@ const processCommand = (function () {
|
||||
else {
|
||||
const answer = parse(command.answer);
|
||||
const remoteIceCandidates = parse(command.iceCandidates);
|
||||
// console.log("answer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(answer));
|
||||
addIceCandidates(pc, remoteIceCandidates);
|
||||
resp = { type: "ok" };
|
||||
@ -286,6 +296,11 @@ const processCommand = (function () {
|
||||
resp = { type: "ok" };
|
||||
}
|
||||
break;
|
||||
case "description":
|
||||
localizedState = command.state;
|
||||
localizedDescription = command.description;
|
||||
resp = { type: "ok" };
|
||||
break;
|
||||
case "end":
|
||||
endCall();
|
||||
resp = { type: "ok" };
|
||||
@ -310,12 +325,14 @@ const processCommand = (function () {
|
||||
catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
shutdownCameraAndMic();
|
||||
activeCall = undefined;
|
||||
resetVideoElements();
|
||||
}
|
||||
function addIceCandidates(conn, iceCandidates) {
|
||||
for (const c of iceCandidates) {
|
||||
conn.addIceCandidate(new RTCIceCandidate(c));
|
||||
// console.log("addIceCandidates", JSON.stringify(c))
|
||||
}
|
||||
}
|
||||
async function setupMediaStreams(call) {
|
||||
@ -335,11 +352,11 @@ const processCommand = (function () {
|
||||
if (call.aesKey) {
|
||||
if (!call.key)
|
||||
call.key = await callCrypto.decodeAesKey(call.aesKey);
|
||||
if (call.useWorker && !call.worker) {
|
||||
if (useWorker && !call.worker) {
|
||||
const workerCode = `const callCrypto = (${callCryptoFunction.toString()})(); (${workerFunction.toString()})()`;
|
||||
call.worker = new Worker(URL.createObjectURL(new Blob([workerCode], { type: "text/javascript" })));
|
||||
call.worker.onerror = ({ error, filename, lineno, message }) => console.log(JSON.stringify({ error, filename, lineno, message }));
|
||||
call.worker.onmessage = ({ data }) => console.log(JSON.stringify({ message: data }));
|
||||
call.worker.onerror = ({ error, filename, lineno, message }) => console.log({ error, filename, lineno, message });
|
||||
// call.worker.onmessage = ({data}) => console.log(JSON.stringify({message: data}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -479,6 +496,11 @@ const processCommand = (function () {
|
||||
return (("createEncodedStreams" in RTCRtpSender.prototype && "createEncodedStreams" in RTCRtpReceiver.prototype) ||
|
||||
(!!useWorker && "RTCRtpScriptTransform" in window));
|
||||
}
|
||||
function shutdownCameraAndMic() {
|
||||
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localStream) {
|
||||
activeCall.localStream.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
}
|
||||
function resetVideoElements() {
|
||||
const videos = getVideoElements();
|
||||
if (!videos)
|
||||
@ -507,6 +529,15 @@ const processCommand = (function () {
|
||||
}
|
||||
return processCommand;
|
||||
})();
|
||||
function toggleMedia(s, media) {
|
||||
let res = false;
|
||||
const tracks = media == CallMediaType.Video ? s.getVideoTracks() : s.getAudioTracks();
|
||||
for (const t of tracks) {
|
||||
t.enabled = !t.enabled;
|
||||
res = t.enabled;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
// Cryptography function - it is loaded both in the main window and in worker context (if the worker is used)
|
||||
function callCryptoFunction() {
|
||||
const initialPlainTextRequired = {
|
@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>SimpleX Chat WebRTC call</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<link href="/desktop/style.css" rel="stylesheet" />
|
||||
<script src="/lz-string.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<video
|
||||
id="remote-video-stream"
|
||||
autoplay
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
></video>
|
||||
<video
|
||||
id="local-video-stream"
|
||||
muted
|
||||
autoplay
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
></video>
|
||||
<div id="progress"></div>
|
||||
<div id="info-block">
|
||||
<p id="state"></p>
|
||||
<p id="description"></p>
|
||||
</div>
|
||||
<div id="audio-call-icon">
|
||||
<img src="/desktop/images/ic_phone_in_talk.svg" />
|
||||
</div>
|
||||
<p id="manage-call">
|
||||
<button id="toggle-audio" style="display: none" onclick="javascript:toggleAudioManually()">
|
||||
<img src="/desktop/images/ic_mic.svg" />
|
||||
</button>
|
||||
<button id="end-call" onclick="javascript:endCallManually()">
|
||||
<img src="/desktop/images/ic_call_end_filled.svg" />
|
||||
</button>
|
||||
<button id="toggle-speaker" style="display: none" onclick="javascript:toggleSpeakerManually()">
|
||||
<img src="/desktop/images/ic_volume_up.svg" />
|
||||
</button>
|
||||
<button id="toggle-video" style="display: none" onclick="javascript:toggleVideoManually()">
|
||||
<img src="/desktop/images/ic_videocam_filled.svg" />
|
||||
</button>
|
||||
</p>
|
||||
</body>
|
||||
<footer>
|
||||
<script src="/call.js"></script>
|
||||
<script src="/desktop/ui.js"></script>
|
||||
</footer>
|
||||
</html>
|
@ -0,0 +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>
|
After Width: | Height: | Size: 435 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M480 630.5q-41.75 0-69.875-30.167Q382 570.167 382 527V278q0-40.417 28.566-68.708Q439.132 181 479.941 181t69.434 28.292Q578 237.583 578 278v249q0 43.167-28.125 73.333Q521.75 630.5 480 630.5Zm0-224.5Zm-.175 526q-12.325 0-20.325-8.375t-8-20.625V795.865Q354 786 285.25 719T206 557.5q-1.5-12.593 7.295-21.547Q222.091 527 235.5 527q9.917 0 18.148 7.542 8.232 7.541 9.852 18.458 10.5 80.5 72.044 134 61.543 53.5 144.347 53.5 82.805 0 144.457-53.5Q686 633.5 696.5 553q1.853-11.167 10.121-18.583Q714.89 527 725.543 527q12.91 0 21.434 8.953Q755.5 544.907 754 557.5 743.5 652 674.75 719T509 795.865V903q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375ZM480 573q18.075 0 29.288-13.5Q520.5 546 520.5 527V278.335q0-16.835-11.629-28.335-11.628-11.5-28.818-11.5t-28.872 11.356Q439.5 261.212 439.5 278v248.868q0 19.132 11.212 32.632Q461.925 573 480 573Z"/></svg>
|
After Width: | Height: | Size: 949 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M681.5 693 640 651.5q16.5-20.5 26-45.75T679 553q1.814-11.167 10.182-18.583Q697.551 527 707.847 527q13.153 0 21.653 8.953 8.5 8.954 7 21.547-4.5 37-18.5 71.75T681.5 693ZM554 566l-51-50V279.038q0-17.463-11.489-29.001-11.49-11.537-29.213-11.537t-29.01 11.431Q422 261.362 422 279v155l-57.5-57.5V279q0-40.833 28.515-69.417Q421.529 181 462.265 181q40.735 0 69.485 28.583Q560.5 238.167 560.5 279v248.23q0 7.103-1.5 19.186-1.5 12.084-5 19.584Zm-94.5-94.5Zm350.5 505L58.5 225q-8-7.444-8-18.222Q50.5 196 58.25 188q7.75-8 18.006-8 10.255 0 18.244 8L847 940.5q8 7.989 8 17.994 0 10.006-8 17.756-8 8.25-18.961 8.25-10.961 0-18.039-8ZM433.5 903V795.865Q336 786 267.5 719t-79-161.5q-2-12.5 7.045-21.5 9.046-9 22.455-9 9.5 0 17.75 7.5T246 553q10.053 80.713 71.588 134.107Q379.124 740.5 462.289 740.5q37.711 0 73.071-12.588Q570.721 715.325 599.5 693l41.5 41.5q-31 26-69.014 41.568Q533.972 791.635 491 796v107q0 12.25-8.463 20.625T462.325 932q-12.325 0-20.575-8.375T433.5 903Z"/></svg>
|
After Width: | Height: | Size: 1.0 KiB |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="100" viewBox="0 -960 960 960" width="100"><path fill="white" d="M774.5-488.5q-5.5-119.5-89-203.25t-203-88.75V-838q71 2.5 133.5 30.5t109.75 75.25q47.25 47.25 75.5 110T832-488.5h-57.5Zm-168 0q-6-49.5-40.5-83.75t-83.5-39.25V-669q73 5 124.25 56T664-488.5h-57.5Zm184 363.5Q677-125 558-180.5T338-338Q236-439 180.5-557.75T125-790.692q0-18.808 12.714-31.558Q150.429-835 169.5-835H306q14 0 23.75 9.75t13.75 24.75l26.929 123.641Q372-663.5 369.5-652q-2.5 11.5-10.229 19.226L261-533q26 44 54.688 81.658Q344.375-413.683 379-380q36.5 38 77.25 69.323Q497-279.353 542-255l95.544-98q9.456-10.5 21.357-14.25T682.5-369l117.362 25.438Q815-340 825-327.801q10 12.198 10 27.301v131q0 19.071-12.714 31.786Q809.571-125 790.5-125ZM232-585.5l81-82-23.5-110H183q1.5 41.5 13 88.25t36 103.75Zm364 358q40 19 88.166 31t93.334 14v-107l-102-21.5-79.5 83.5Zm-364-358Zm364 358Z"/></svg>
|
After Width: | Height: | Size: 898 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M143.5 891.5q-23.031 0-40.266-17.234Q86 857.031 86 834V318q0-23.031 17.234-40.266Q120.469 260.5 143.5 260.5h516.211q22.289 0 39.789 17.234Q717 294.969 717 318v215.5L849 401q8-7.5 16.75-3.75t8.75 13.063V741q0 10-8.75 13.75t-16.85-4.35L717 618.5V834q0 23.031-17.5 40.266Q682 891.5 659.711 891.5H143.5Z"/></svg>
|
After Width: | Height: | Size: 416 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M849.5 750.5 717 618.5v114L659.5 675V318H302l-57.5-57.5h415q22.969 0 40.234 17.266Q717 295.031 717 318v215l132.5-132.5q6.5-6.5 15.75-3.167 9.25 3.334 9.25 12.667v331q0 9.625-9.25 13.062Q856 757.5 849.5 750.5Zm-26.5 250-758-758q-8-7.547-8-19.069 0-11.522 9-20.431 8.5-8.5 20-8.5t20.5 8.5l758 758q7.5 7.93 7.5 19.465t-8.5 20.035q-9 9-20.5 9t-20-9Zm-340-502Zm-319.5-238L221 318h-77.5v516h516v-77.5L716 813v21q0 22.969-17.266 40.234Q681.469 891.5 658.5 891.5h-515q-22.969 0-40.234-17.266Q86 856.969 86 834V318q0-22.969 17.266-40.234Q120.531 260.5 143.5 260.5h20Zm236 316.5Z"/></svg>
|
After Width: | Height: | Size: 686 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M232.5 693q-12.5 0-20.5-8t-8-20.5v-177q0-12.5 8-20.5t20.5-8h129L509 311.5q13.5-13.5 31-6.5t17.5 26v489.5q0 19.5-17.5 26.5t-31-6.5L361.5 693h-129ZM615 742V409.5q55 17 88 63.25T736 576q0 58-33 103.25T615 742ZM500 408.5l-112.5 108h-126v119h126L500 744V408.5ZM379 576Z"/></svg>
|
After Width: | Height: | Size: 381 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M600.5 904.5q-15.5 5.5-28.25-4T559.5 874q0-7.5 4.25-13.5t11.75-8q89-31.5 143.75-107T774 575q0-94.5-54.5-170.5T575.5 298q-7-2-11.5-8.5t-4.5-14.5q0-16 13.25-25.25t27.75-4.25Q704.5 283 768 373t63.5 202q0 112.5-63.5 202.5t-167.5 127ZM157 693q-12.5 0-20.5-8t-8-20.5v-177q0-12.5 8-20.5t20.5-8h129l147.5-147.5q13.5-13.5 31-6.25T482 331v489.5q0 19-17.5 26.25t-31-6.25L286 693H157Zm382.5 49V409.5q55 17 88 63.25t33 103.25q0 58-33 103.25t-88 62.75Zm-115-333.5L312 516.5H186v119h126L424.5 744V408.5Zm-93 167.5Z"/></svg>
|
After Width: | Height: | Size: 616 B |
@ -0,0 +1,127 @@
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
#remote-video-stream {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-video-stream {
|
||||
position: absolute;
|
||||
width: 20%;
|
||||
max-width: 20%;
|
||||
object-fit: cover;
|
||||
margin: 16px;
|
||||
border-radius: 16px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
*::-webkit-media-controls {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
}
|
||||
*::-webkit-media-controls-panel {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
}
|
||||
*::-webkit-media-controls-play-button {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
}
|
||||
*::-webkit-media-controls-start-playback-button {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
}
|
||||
|
||||
#manage-call {
|
||||
position: absolute;
|
||||
width: fit-content;
|
||||
top: 90%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-column-gap: 30px;
|
||||
}
|
||||
|
||||
#manage-call button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
#progress {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin-left: -52px;
|
||||
margin-top: -52px;
|
||||
border-radius: 50%;
|
||||
border-top: 5px solid white;
|
||||
border-right: 5px solid white;
|
||||
border-bottom: 5px solid white;
|
||||
border-left: 5px solid black;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
-webkit-animation: spin 2s linear infinite;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#info-block {
|
||||
position: absolute;
|
||||
color: white;
|
||||
line-height: 10px;
|
||||
opacity: 0.8;
|
||||
width: 200px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
#info-block.audio {
|
||||
text-align: center;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin-left: -100px;
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
#info-block.video {
|
||||
left: 16px;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
#audio-call-icon {
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin-left: -50px;
|
||||
margin-top: -44px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
"use strict";
|
||||
// Override defaults to enable worker on Chrome and Safari
|
||||
useWorker = typeof window.Worker !== "undefined";
|
||||
// Create WebSocket connection.
|
||||
const socket = new WebSocket(`ws://${location.host}`);
|
||||
socket.addEventListener("open", (_event) => {
|
||||
console.log("Opened socket");
|
||||
sendMessageToNative = (msg) => {
|
||||
console.log("Message to server");
|
||||
socket.send(JSON.stringify(msg));
|
||||
};
|
||||
});
|
||||
socket.addEventListener("message", (event) => {
|
||||
const parsed = JSON.parse(event.data);
|
||||
reactOnMessageFromServer(parsed);
|
||||
processCommand(parsed);
|
||||
console.log("Message from server");
|
||||
});
|
||||
socket.addEventListener("close", (_event) => {
|
||||
console.log("Closed socket");
|
||||
sendMessageToNative = (_msg) => {
|
||||
console.log("Tried to send message to native but the socket was closed already");
|
||||
};
|
||||
window.close();
|
||||
});
|
||||
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 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" />';
|
||||
}
|
||||
}
|
||||
function toggleVideoManually() {
|
||||
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localMedia) {
|
||||
document.getElementById("toggle-video").innerHTML = toggleMedia(activeCall.localStream, CallMediaType.Video)
|
||||
? '<img src="/desktop/images/ic_videocam_filled.svg" />'
|
||||
: '<img src="/desktop/images/ic_videocam_off.svg" />';
|
||||
}
|
||||
}
|
||||
function reactOnMessageFromServer(msg) {
|
||||
var _a;
|
||||
switch ((_a = msg.command) === null || _a === void 0 ? void 0 : _a.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 == "video") {
|
||||
document.getElementById("toggle-video").style.display = "inline-block";
|
||||
}
|
||||
document.getElementById("info-block").className = msg.command.media;
|
||||
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";
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
function updateCallInfoView(state, description) {
|
||||
document.getElementById("state").innerText = state;
|
||||
document.getElementById("description").innerText = description;
|
||||
}
|
||||
//# sourceMappingURL=ui.js.map
|
@ -1,16 +1,15 @@
|
||||
package chat.simplex.common.platform
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.*
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.views.helpers.AlertManager
|
||||
import chat.simplex.common.views.helpers.generalGetString
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.*
|
||||
import uk.co.caprica.vlcj.player.base.MediaPlayer
|
||||
import uk.co.caprica.vlcj.player.base.State
|
||||
import uk.co.caprica.vlcj.player.component.AudioPlayerComponent
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
|
||||
actual class RecorderNative: RecorderInterface {
|
||||
@ -38,7 +37,7 @@ actual object AudioPlayer: AudioPlayerInterface {
|
||||
|
||||
// Returns real duration of the track
|
||||
private fun start(fileSource: CryptoFile, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? {
|
||||
val absoluteFilePath = getAppFilePath(fileSource.filePath)
|
||||
val absoluteFilePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath)
|
||||
if (!File(absoluteFilePath).exists()) {
|
||||
Log.e(TAG, "No such file: ${fileSource.filePath}")
|
||||
return null
|
||||
@ -208,6 +207,25 @@ val MediaPlayer.duration: Int
|
||||
get() = media().info().duration().toInt()
|
||||
|
||||
actual object SoundPlayer: SoundPlayerInterface {
|
||||
override fun start(scope: CoroutineScope, sound: Boolean) { /*LALAL*/ }
|
||||
override fun stop() { /*LALAL*/ }
|
||||
var playing = false
|
||||
|
||||
override fun start(scope: CoroutineScope, sound: Boolean) {
|
||||
withBGApi {
|
||||
val tmpFile = File(tmpDir, UUID.randomUUID().toString())
|
||||
tmpFile.deleteOnExit()
|
||||
SoundPlayer::class.java.getResource("/media/ring_once.mp3").openStream()!!.use { it.copyTo(tmpFile.outputStream()) }
|
||||
playing = true
|
||||
while (playing) {
|
||||
if (sound) {
|
||||
AudioPlayer.play(CryptoFile.plain(tmpFile.absolutePath), mutableStateOf(true), mutableStateOf(0), mutableStateOf(0), true)
|
||||
}
|
||||
delay(3500)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
playing = false
|
||||
AudioPlayer.stop()
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,243 @@
|
||||
package chat.simplex.common.views.call
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import org.nanohttpd.protocols.http.IHTTPSession
|
||||
import org.nanohttpd.protocols.http.response.Response
|
||||
import org.nanohttpd.protocols.http.response.Response.newFixedLengthResponse
|
||||
import org.nanohttpd.protocols.http.response.Status
|
||||
import org.nanohttpd.protocols.websockets.*
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
|
||||
private const val SERVER_HOST = "localhost"
|
||||
private const val SERVER_PORT = 50395
|
||||
val connections = ArrayList<WebSocket>()
|
||||
|
||||
@Composable
|
||||
actual fun ActiveCallView() {
|
||||
// LALAL
|
||||
val endCall = {
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) withBGApi { chatModel.callManager.endCall(call) }
|
||||
}
|
||||
BackHandler(onBack = endCall)
|
||||
WebRTCController(chatModel.callCommand) { apiMsg ->
|
||||
Log.d(TAG, "received from WebRTCController: $apiMsg")
|
||||
val call = chatModel.activeCall.value
|
||||
if (call != null) {
|
||||
Log.d(TAG, "has active call $call")
|
||||
when (val r = apiMsg.resp) {
|
||||
is WCallResponse.Capabilities -> withBGApi {
|
||||
val callType = CallType(call.localMedia, r.capabilities)
|
||||
chatModel.controller.apiSendCallInvitation(call.contact, callType)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities)
|
||||
}
|
||||
is WCallResponse.Offer -> withBGApi {
|
||||
chatModel.controller.apiSendCallOffer(call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities)
|
||||
}
|
||||
is WCallResponse.Answer -> withBGApi {
|
||||
chatModel.controller.apiSendCallAnswer(call.contact, r.answer, r.iceCandidates)
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
|
||||
}
|
||||
is WCallResponse.Ice -> withBGApi {
|
||||
chatModel.controller.apiSendCallExtraInfo(call.contact, r.iceCandidates)
|
||||
}
|
||||
is WCallResponse.Connection ->
|
||||
try {
|
||||
val callStatus = json.decodeFromString<WebRTCCallStatus>("\"${r.state.connectionState}\"")
|
||||
if (callStatus == WebRTCCallStatus.Connected) {
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectedAt = Clock.System.now())
|
||||
}
|
||||
withBGApi { chatModel.controller.apiCallStatus(call.contact, callStatus) }
|
||||
} catch (e: Error) {
|
||||
Log.d(TAG, "call status ${r.state.connectionState} not used")
|
||||
}
|
||||
is WCallResponse.Connected -> {
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo)
|
||||
}
|
||||
is WCallResponse.End -> {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
}
|
||||
is WCallResponse.Ended -> {
|
||||
chatModel.activeCall.value = call.copy(callState = CallState.Ended)
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
chatModel.showCallView.value = false
|
||||
}
|
||||
is WCallResponse.Ok -> when (val cmd = apiMsg.command) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
is WCallCommand.Camera -> {
|
||||
chatModel.activeCall.value = call.copy(localCamera = cmd.camera)
|
||||
if (!call.audioEnabled) {
|
||||
chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false))
|
||||
}
|
||||
}
|
||||
is WCallCommand.End ->
|
||||
chatModel.showCallView.value = false
|
||||
else -> {}
|
||||
}
|
||||
is WCallResponse.Error -> {
|
||||
Log.e(TAG, "ActiveCallView: command error ${r.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SendStateUpdates()
|
||||
DisposableEffect(Unit) {
|
||||
chatModel.activeCallViewIsVisible.value = true
|
||||
// After the first call, End command gets added to the list which prevents making another calls
|
||||
chatModel.callCommand.removeAll { it is WCallCommand.End }
|
||||
onDispose {
|
||||
chatModel.activeCallViewIsVisible.value = false
|
||||
chatModel.callCommand.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SendStateUpdates() {
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { chatModel.activeCall.value }
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.collect { call ->
|
||||
val state = call.callState.text
|
||||
val connInfo = call.connectionInfo
|
||||
// val connInfoText = if (connInfo == null) "" else " (${connInfo.text}, ${connInfo.protocolText})"
|
||||
val connInfoText = if (connInfo == null) "" else " (${connInfo.text})"
|
||||
val description = call.encryptionStatus + connInfoText
|
||||
chatModel.callCommand.add(WCallCommand.Description(state, description))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WebRTCController(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val server = remember {
|
||||
uriHandler.openUri("http://${SERVER_HOST}:$SERVER_PORT/simplex/call/")
|
||||
startServer(onResponse)
|
||||
}
|
||||
fun processCommand(cmd: WCallCommand) {
|
||||
val apiCall = WVAPICall(command = cmd)
|
||||
for (connection in connections.toList()) {
|
||||
try {
|
||||
connection.send(json.encodeToString(apiCall))
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to send message to browser: ${e.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
processCommand(WCallCommand.End)
|
||||
server.stop()
|
||||
connections.clear()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
snapshotFlow { callCommand.firstOrNull() }
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.collect {
|
||||
while (connections.isEmpty()) {
|
||||
delay(100)
|
||||
}
|
||||
while (callCommand.isNotEmpty()) {
|
||||
val cmd = callCommand.removeFirst()
|
||||
Log.d(TAG, "WebRTCController LaunchedEffect executing $cmd")
|
||||
processCommand(cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startServer(onResponse: (WVAPIMessage) -> Unit): NanoWSD {
|
||||
val server = object: NanoWSD(SERVER_HOST, SERVER_PORT) {
|
||||
override fun openWebSocket(session: IHTTPSession): WebSocket = MyWebSocket(onResponse, session)
|
||||
|
||||
@Suppress("NewApi")
|
||||
fun resourcesToResponse(path: String): Response {
|
||||
val uri = Class.forName("chat.simplex.common.AppKt").getResource("/assets/www$path") ?: return resourceNotFound
|
||||
val response = newFixedLengthResponse(
|
||||
Status.OK, getMimeTypeForFile(uri.file),
|
||||
uri.openStream().readAllBytes()
|
||||
)
|
||||
response.setKeepAlive(true)
|
||||
response.setUseGzip(true)
|
||||
return response
|
||||
}
|
||||
|
||||
val resourceNotFound = newFixedLengthResponse(Status.NOT_FOUND, "text/plain", "This page couldn't be found")
|
||||
|
||||
override fun handle(session: IHTTPSession): Response {
|
||||
return when {
|
||||
session.headers["upgrade"] == "websocket" -> super.handle(session)
|
||||
session.uri.contains("/simplex/call/") -> resourcesToResponse("/desktop/call.html")
|
||||
else -> resourcesToResponse(URI.create(session.uri).path)
|
||||
}
|
||||
}
|
||||
}
|
||||
server.start(60_000_000)
|
||||
return server
|
||||
}
|
||||
|
||||
class MyWebSocket(val onResponse: (WVAPIMessage) -> Unit, handshakeRequest: IHTTPSession) : WebSocket(handshakeRequest) {
|
||||
override fun onOpen() {
|
||||
connections.add(this)
|
||||
}
|
||||
|
||||
override fun onClose(closeCode: CloseCode?, reason: String?, initiatedByRemote: Boolean) {
|
||||
onResponse(WVAPIMessage(null, WCallResponse.End))
|
||||
}
|
||||
|
||||
override fun onMessage(message: WebSocketFrame) {
|
||||
Log.d(TAG, "MyWebSocket.onMessage")
|
||||
try {
|
||||
// for debugging
|
||||
// onResponse(message.textPayload)
|
||||
onResponse(json.decodeFromString(message.textPayload))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "failed parsing browser message: $message")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPong(pong: WebSocketFrame?) = Unit
|
||||
|
||||
override fun onException(exception: IOException) {
|
||||
Log.e(TAG, "WebSocket exception: ${exception.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.views.helpers.*
|
||||
|
||||
private object NoIndication : Indication {
|
||||
object NoIndication : Indication {
|
||||
private object NoIndicationInstance : IndicationInstance {
|
||||
override fun ContentDrawScope.drawIndication() {
|
||||
drawContent()
|
||||
|
@ -0,0 +1,75 @@
|
||||
package chat.simplex.common.views.chatlist
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.ui.theme.*
|
||||
import chat.simplex.common.views.call.CallMediaType
|
||||
import chat.simplex.common.views.chat.item.ItemAction
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@Composable
|
||||
actual fun DesktopActiveCallOverlayLayout(newChatSheetState: MutableStateFlow<AnimatedViewState>) {
|
||||
val call = remember { chatModel.activeCall}.value
|
||||
// if (call?.callState == CallState.Connected && !newChatSheetState.collectAsState().value.isVisible()) {
|
||||
if (call != null && !newChatSheetState.collectAsState().value.isVisible()) {
|
||||
val showMenu = remember { mutableStateOf(false) }
|
||||
val media = call.peerMedia ?: call.localMedia
|
||||
CompositionLocalProvider(
|
||||
LocalIndication provides NoIndication
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize(),
|
||||
contentAlignment = Alignment.BottomEnd
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(end = 71.dp, bottom = 92.dp)
|
||||
.size(67.dp)
|
||||
.combinedClickable(onClick = {
|
||||
val chat = chatModel.getChat(call.contact.id)
|
||||
if (chat != null) {
|
||||
withApi {
|
||||
openChat(chat.chatInfo, chatModel)
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongClick = { showMenu.value = true })
|
||||
.onRightClick { showMenu.value = true },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) {
|
||||
ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image)
|
||||
}
|
||||
Box(Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp).align(Alignment.TopEnd)) {
|
||||
if (media == CallMediaType.Video) {
|
||||
Icon(painterResource(MR.images.ic_videocam_filled), stringResource(MR.strings.icon_descr_video_call), Modifier.size(18.dp), tint = Color.White)
|
||||
} else {
|
||||
Icon(painterResource(MR.images.ic_call_filled), stringResource(MR.strings.icon_descr_audio_call), Modifier.size(18.dp), tint = Color.White)
|
||||
}
|
||||
}
|
||||
DefaultDropdownMenu(showMenu) {
|
||||
ItemAction(stringResource(MR.strings.icon_descr_hang_up), painterResource(MR.images.ic_call_end_filled), color = MaterialTheme.colors.error, onClick = {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
showMenu.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -25,11 +25,11 @@ android.nonTransitiveRClass=true
|
||||
android.enableJetifier=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
|
||||
android.version_name=5.4-beta.0
|
||||
android.version_code=156
|
||||
android.version_name=5.4-beta.2
|
||||
android.version_code=159
|
||||
|
||||
desktop.version_name=5.4-beta.0
|
||||
desktop.version_code=12
|
||||
desktop.version_name=5.4-beta.2
|
||||
desktop.version_code=15
|
||||
|
||||
kotlin.version=1.8.20
|
||||
gradle.plugin.version=7.4.2
|
||||
|
@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: deb3fc73595ceae34902d3402d075e3a531d5221
|
||||
tag: cf8b9c12ff5cbdc77d3b8866af2c761a546ec8fc
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
@ -1,5 +1,5 @@
|
||||
name: simplex-chat
|
||||
version: 5.4.0.1
|
||||
version: 5.4.0.2
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
|
@ -1,14 +1,24 @@
|
||||
#!/bin/sh
|
||||
|
||||
# it can be tested in the browser from dist folder
|
||||
cp ./src/call.html ./dist/call.html
|
||||
cp ./src/style.css ./dist/style.css
|
||||
mkdir -p dist/{android,desktop,desktop/images} 2>/dev/null
|
||||
cp ./src/android/call.html ./dist/android/call.html
|
||||
cp ./src/android/style.css ./dist/android/style.css
|
||||
cp ./src/desktop/call.html ./dist/desktop/call.html
|
||||
cp ./src/desktop/style.css ./dist/desktop/style.css
|
||||
cp ./src/desktop/images/* ./dist/desktop/images/
|
||||
cp ./node_modules/lz-string/libs/lz-string.min.js ./dist/lz-string.min.js
|
||||
cp ./src/webcall.html ./dist/webcall.html
|
||||
cp ./src/ui.js ./dist/ui.js
|
||||
|
||||
# copy to android app
|
||||
cp ./src/call.html ../../apps/multiplatform/android/src/main/assets/www/call.html
|
||||
cp ./src/style.css ../../apps/multiplatform/android/src/main/assets/www/style.css
|
||||
cp ./dist/call.js ../../apps/multiplatform/android/src/main/assets/www/call.js
|
||||
cp ./node_modules/lz-string/libs/lz-string.min.js ../../apps/multiplatform/android/src/main/assets/www/lz-string.min.js
|
||||
# copy to android and desktop apps
|
||||
mkdir -p ../../apps/multiplatform/common/src/commonMain/resources/assets/www/{android,desktop,desktop/images} 2>/dev/null
|
||||
cp ./src/android/call.html ../../apps/multiplatform/common/src/commonMain/resources/assets/www/android/call.html
|
||||
cp ./src/android/style.css ../../apps/multiplatform/common/src/commonMain/resources/assets/www/android/style.css
|
||||
cp ./src/desktop/call.html ../../apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/call.html
|
||||
cp ./src/desktop/style.css ../../apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/style.css
|
||||
cp ./src/desktop/images/* ../../apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/images/
|
||||
|
||||
cp ./dist/desktop/ui.js ../../apps/multiplatform/common/src/commonMain/resources/assets/www/desktop/ui.js
|
||||
cp ./dist/call.js ../../apps/multiplatform/common/src/commonMain/resources/assets/www/call.js
|
||||
cp ./node_modules/lz-string/libs/lz-string.min.js ../../apps/multiplatform/common/src/commonMain/resources/assets/www/lz-string.min.js
|
||||
|
@ -40,4 +40,4 @@
|
||||
"dependencies": {
|
||||
"lz-string": "^1.4.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
26
packages/simplex-chat-webrtc/src/android/call.html
Normal file
@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<link href="./style.css" rel="stylesheet" />
|
||||
<script src="../lz-string.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<video
|
||||
id="remote-video-stream"
|
||||
autoplay
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
></video>
|
||||
<video
|
||||
id="local-video-stream"
|
||||
muted
|
||||
autoplay
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
></video>
|
||||
</body>
|
||||
<footer>
|
||||
<script src="../call.js"></script>
|
||||
</footer>
|
||||
</html>
|
41
packages/simplex-chat-webrtc/src/android/style.css
Normal file
@ -0,0 +1,41 @@
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
#remote-video-stream {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-video-stream {
|
||||
position: absolute;
|
||||
width: 30%;
|
||||
max-width: 30%;
|
||||
object-fit: cover;
|
||||
margin: 16px;
|
||||
border-radius: 16px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
*::-webkit-media-controls {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
}
|
||||
*::-webkit-media-controls-panel {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
}
|
||||
*::-webkit-media-controls-play-button {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
}
|
||||
*::-webkit-media-controls-start-playback-button {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
}
|
@ -15,6 +15,7 @@ type WCallCommand =
|
||||
| WCallIceCandidates
|
||||
| WCEnableMedia
|
||||
| WCToggleCamera
|
||||
| WCDescription
|
||||
| WCEndCall
|
||||
|
||||
type WCallResponse =
|
||||
@ -24,14 +25,15 @@ type WCallResponse =
|
||||
| WCallIceCandidates
|
||||
| WRConnection
|
||||
| WRCallConnected
|
||||
| WRCallEnd
|
||||
| WRCallEnded
|
||||
| WROk
|
||||
| WRError
|
||||
| WCAcceptOffer
|
||||
|
||||
type WCallCommandTag = "capabilities" | "start" | "offer" | "answer" | "ice" | "media" | "camera" | "end"
|
||||
type WCallCommandTag = "capabilities" | "start" | "offer" | "answer" | "ice" | "media" | "camera" | "description" | "end"
|
||||
|
||||
type WCallResponseTag = "capabilities" | "offer" | "answer" | "ice" | "connection" | "connected" | "ended" | "ok" | "error"
|
||||
type WCallResponseTag = "capabilities" | "offer" | "answer" | "ice" | "connection" | "connected" | "end" | "ended" | "ok" | "error"
|
||||
|
||||
enum CallMediaType {
|
||||
Audio = "audio",
|
||||
@ -53,15 +55,13 @@ interface IWCallResponse {
|
||||
|
||||
interface WCCapabilities extends IWCallCommand {
|
||||
type: "capabilities"
|
||||
media?: CallMediaType
|
||||
useWorker?: boolean
|
||||
media: CallMediaType
|
||||
}
|
||||
|
||||
interface WCStartCall extends IWCallCommand {
|
||||
type: "start"
|
||||
media: CallMediaType
|
||||
aesKey?: string
|
||||
useWorker?: boolean
|
||||
iceServers?: RTCIceServer[]
|
||||
relay?: boolean
|
||||
}
|
||||
@ -76,7 +76,6 @@ interface WCAcceptOffer extends IWCallCommand {
|
||||
iceCandidates: string // JSON strings for RTCIceCandidateInit
|
||||
media: CallMediaType
|
||||
aesKey?: string
|
||||
useWorker?: boolean
|
||||
iceServers?: RTCIceServer[]
|
||||
relay?: boolean
|
||||
}
|
||||
@ -110,6 +109,12 @@ interface WCToggleCamera extends IWCallCommand {
|
||||
camera: VideoCamera
|
||||
}
|
||||
|
||||
interface WCDescription extends IWCallCommand {
|
||||
type: "description"
|
||||
state: string
|
||||
description: string
|
||||
}
|
||||
|
||||
interface WRCapabilities extends IWCallResponse {
|
||||
type: "capabilities"
|
||||
capabilities: CallCapabilities
|
||||
@ -134,6 +139,10 @@ interface WRCallConnected extends IWCallResponse {
|
||||
connectionInfo: ConnectionInfo
|
||||
}
|
||||
|
||||
interface WRCallEnd extends IWCallResponse {
|
||||
type: "end"
|
||||
}
|
||||
|
||||
interface WRCallEnded extends IWCallResponse {
|
||||
type: "ended"
|
||||
}
|
||||
@ -185,13 +194,15 @@ interface Call {
|
||||
localStream: MediaStream
|
||||
remoteStream: MediaStream
|
||||
aesKey?: string
|
||||
useWorker?: boolean
|
||||
worker?: Worker
|
||||
key?: CryptoKey
|
||||
}
|
||||
|
||||
let activeCall: Call | undefined
|
||||
let answerTimeout = 30_000
|
||||
var useWorker = false
|
||||
var localizedState = ""
|
||||
var localizedDescription = ""
|
||||
|
||||
const processCommand = (function () {
|
||||
type RTCRtpSenderWithEncryption = RTCRtpSender & {
|
||||
@ -232,9 +243,9 @@ const processCommand = (function () {
|
||||
iceTransportPolicy: relay ? "relay" : "all",
|
||||
},
|
||||
iceCandidates: {
|
||||
delay: 3000,
|
||||
extrasInterval: 2000,
|
||||
extrasTimeout: 8000,
|
||||
delay: 750,
|
||||
extrasInterval: 1500,
|
||||
extrasTimeout: 12000,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -274,6 +285,8 @@ const processCommand = (function () {
|
||||
function resolveIceCandidates() {
|
||||
if (delay) clearTimeout(delay)
|
||||
resolved = true
|
||||
// console.log("resolveIceCandidates", JSON.stringify(candidates))
|
||||
console.log("resolveIceCandidates")
|
||||
const iceCandidates = serialize(candidates)
|
||||
candidates = []
|
||||
resolve(iceCandidates)
|
||||
@ -281,6 +294,8 @@ const processCommand = (function () {
|
||||
|
||||
function sendIceCandidates() {
|
||||
if (candidates.length === 0) return
|
||||
// console.log("sendIceCandidates", JSON.stringify(candidates))
|
||||
console.log("sendIceCandidates")
|
||||
const iceCandidates = serialize(candidates)
|
||||
candidates = []
|
||||
sendMessageToNative({resp: {type: "ice", iceCandidates}})
|
||||
@ -288,13 +303,13 @@ const processCommand = (function () {
|
||||
})
|
||||
}
|
||||
|
||||
async function initializeCall(config: CallConfig, mediaType: CallMediaType, aesKey?: string, useWorker?: boolean): Promise<Call> {
|
||||
async function initializeCall(config: CallConfig, mediaType: CallMediaType, aesKey?: string): Promise<Call> {
|
||||
const pc = new RTCPeerConnection(config.peerConnectionConfig)
|
||||
const remoteStream = new MediaStream()
|
||||
const localCamera = VideoCamera.User
|
||||
const localStream = await getLocalMediaStream(mediaType, localCamera)
|
||||
const iceCandidates = getIceCandidates(pc, config)
|
||||
const call = {connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey, useWorker}
|
||||
const call = {connection: pc, iceCandidates, localMedia: mediaType, localCamera, localStream, remoteStream, aesKey}
|
||||
await setupMediaStreams(call)
|
||||
let connectionTimeout: number | undefined = setTimeout(connectionHandler, answerTimeout)
|
||||
pc.addEventListener("connectionstatechange", connectionStateChange)
|
||||
@ -374,16 +389,16 @@ const processCommand = (function () {
|
||||
if (activeCall) endCall()
|
||||
// This request for local media stream is made to prompt for camera/mic permissions on call start
|
||||
if (command.media) await getLocalMediaStream(command.media, VideoCamera.User)
|
||||
const encryption = supportsInsertableStreams(command.useWorker)
|
||||
const encryption = supportsInsertableStreams(useWorker)
|
||||
resp = {type: "capabilities", capabilities: {encryption}}
|
||||
break
|
||||
case "start": {
|
||||
console.log("starting incoming call - create webrtc session")
|
||||
if (activeCall) endCall()
|
||||
const {media, useWorker, iceServers, relay} = command
|
||||
const {media, iceServers, relay} = command
|
||||
const encryption = supportsInsertableStreams(useWorker)
|
||||
const aesKey = encryption ? command.aesKey : undefined
|
||||
activeCall = await initializeCall(getCallConfig(encryption && !!aesKey, iceServers, relay), media, aesKey, useWorker)
|
||||
activeCall = await initializeCall(getCallConfig(encryption && !!aesKey, iceServers, relay), media, aesKey)
|
||||
const pc = activeCall.connection
|
||||
const offer = await pc.createOffer()
|
||||
await pc.setLocalDescription(offer)
|
||||
@ -397,7 +412,6 @@ const processCommand = (function () {
|
||||
// iceServers,
|
||||
// relay,
|
||||
// aesKey,
|
||||
// useWorker,
|
||||
// }
|
||||
resp = {
|
||||
type: "offer",
|
||||
@ -405,19 +419,21 @@ const processCommand = (function () {
|
||||
iceCandidates: await activeCall.iceCandidates,
|
||||
capabilities: {encryption},
|
||||
}
|
||||
// console.log("offer response", JSON.stringify(resp))
|
||||
break
|
||||
}
|
||||
case "offer":
|
||||
if (activeCall) {
|
||||
resp = {type: "error", message: "accept: call already started"}
|
||||
} else if (!supportsInsertableStreams(command.useWorker) && command.aesKey) {
|
||||
} else if (!supportsInsertableStreams(useWorker) && command.aesKey) {
|
||||
resp = {type: "error", message: "accept: encryption is not supported"}
|
||||
} else {
|
||||
const offer: RTCSessionDescriptionInit = parse(command.offer)
|
||||
const remoteIceCandidates: RTCIceCandidateInit[] = parse(command.iceCandidates)
|
||||
const {media, aesKey, useWorker, iceServers, relay} = command
|
||||
activeCall = await initializeCall(getCallConfig(!!aesKey, iceServers, relay), media, aesKey, useWorker)
|
||||
const {media, aesKey, iceServers, relay} = command
|
||||
activeCall = await initializeCall(getCallConfig(!!aesKey, iceServers, relay), media, aesKey)
|
||||
const pc = activeCall.connection
|
||||
// console.log("offer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(offer))
|
||||
const answer = await pc.createAnswer()
|
||||
await pc.setLocalDescription(answer)
|
||||
@ -429,6 +445,7 @@ const processCommand = (function () {
|
||||
iceCandidates: await activeCall.iceCandidates,
|
||||
}
|
||||
}
|
||||
// console.log("answer response", JSON.stringify(resp))
|
||||
break
|
||||
case "answer":
|
||||
if (!pc) {
|
||||
@ -440,6 +457,7 @@ const processCommand = (function () {
|
||||
} else {
|
||||
const answer: RTCSessionDescriptionInit = parse(command.answer)
|
||||
const remoteIceCandidates: RTCIceCandidateInit[] = parse(command.iceCandidates)
|
||||
// console.log("answer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(answer))
|
||||
addIceCandidates(pc, remoteIceCandidates)
|
||||
resp = {type: "ok"}
|
||||
@ -472,6 +490,11 @@ const processCommand = (function () {
|
||||
resp = {type: "ok"}
|
||||
}
|
||||
break
|
||||
case "description":
|
||||
localizedState = command.state
|
||||
localizedDescription = command.description
|
||||
resp = {type: "ok"}
|
||||
break
|
||||
case "end":
|
||||
endCall()
|
||||
resp = {type: "ok"}
|
||||
@ -494,6 +517,7 @@ const processCommand = (function () {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
shutdownCameraAndMic()
|
||||
activeCall = undefined
|
||||
resetVideoElements()
|
||||
}
|
||||
@ -501,6 +525,7 @@ const processCommand = (function () {
|
||||
function addIceCandidates(conn: RTCPeerConnection, iceCandidates: RTCIceCandidateInit[]) {
|
||||
for (const c of iceCandidates) {
|
||||
conn.addIceCandidate(new RTCIceCandidate(c))
|
||||
// console.log("addIceCandidates", JSON.stringify(c))
|
||||
}
|
||||
}
|
||||
|
||||
@ -520,12 +545,11 @@ const processCommand = (function () {
|
||||
async function setupEncryptionWorker(call: Call) {
|
||||
if (call.aesKey) {
|
||||
if (!call.key) call.key = await callCrypto.decodeAesKey(call.aesKey)
|
||||
if (call.useWorker && !call.worker) {
|
||||
if (useWorker && !call.worker) {
|
||||
const workerCode = `const callCrypto = (${callCryptoFunction.toString()})(); (${workerFunction.toString()})()`
|
||||
call.worker = new Worker(URL.createObjectURL(new Blob([workerCode], {type: "text/javascript"})))
|
||||
call.worker.onerror = ({error, filename, lineno, message}: ErrorEvent) =>
|
||||
console.log(JSON.stringify({error, filename, lineno, message}))
|
||||
call.worker.onmessage = ({data}) => console.log(JSON.stringify({message: data}))
|
||||
call.worker.onerror = ({error, filename, lineno, message}: ErrorEvent) => console.log({error, filename, lineno, message})
|
||||
// call.worker.onmessage = ({data}) => console.log(JSON.stringify({message: data}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -680,6 +704,12 @@ const processCommand = (function () {
|
||||
remote: HTMLMediaElement
|
||||
}
|
||||
|
||||
function shutdownCameraAndMic() {
|
||||
if (activeCall?.localStream) {
|
||||
activeCall.localStream.getTracks().forEach((track) => track.stop())
|
||||
}
|
||||
}
|
||||
|
||||
function resetVideoElements() {
|
||||
const videos = getVideoElements()
|
||||
if (!videos) return
|
||||
@ -706,10 +736,19 @@ const processCommand = (function () {
|
||||
const tracks = media == CallMediaType.Video ? s.getVideoTracks() : s.getAudioTracks()
|
||||
for (const t of tracks) t.enabled = enable
|
||||
}
|
||||
|
||||
return processCommand
|
||||
})()
|
||||
|
||||
function toggleMedia(s: MediaStream, media: CallMediaType): boolean {
|
||||
let res = false
|
||||
const tracks = media == CallMediaType.Video ? s.getVideoTracks() : s.getAudioTracks()
|
||||
for (const t of tracks) {
|
||||
t.enabled = !t.enabled
|
||||
res = t.enabled
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
type TransformFrameFunc = (key: CryptoKey) => (frame: RTCEncodedVideoFrame, controller: TransformStreamDefaultController) => Promise<void>
|
||||
|
||||
interface CallCrypto {
|
||||
|
50
packages/simplex-chat-webrtc/src/desktop/call.html
Normal file
@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>SimpleX Chat WebRTC call</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<link href="/desktop/style.css" rel="stylesheet" />
|
||||
<script src="/lz-string.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<video
|
||||
id="remote-video-stream"
|
||||
autoplay
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
></video>
|
||||
<video
|
||||
id="local-video-stream"
|
||||
muted
|
||||
autoplay
|
||||
playsinline
|
||||
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
|
||||
></video>
|
||||
<div id="progress"></div>
|
||||
<div id="info-block">
|
||||
<p id="state"></p>
|
||||
<p id="description"></p>
|
||||
</div>
|
||||
<div id="audio-call-icon">
|
||||
<img src="/desktop/images/ic_phone_in_talk.svg" />
|
||||
</div>
|
||||
<p id="manage-call">
|
||||
<button id="toggle-audio" style="display: none" onclick="javascript:toggleAudioManually()">
|
||||
<img src="/desktop/images/ic_mic.svg" />
|
||||
</button>
|
||||
<button id="end-call" onclick="javascript:endCallManually()">
|
||||
<img src="/desktop/images/ic_call_end_filled.svg" />
|
||||
</button>
|
||||
<button id="toggle-speaker" style="display: none" onclick="javascript:toggleSpeakerManually()">
|
||||
<img src="/desktop/images/ic_volume_up.svg" />
|
||||
</button>
|
||||
<button id="toggle-video" style="display: none" onclick="javascript:toggleVideoManually()">
|
||||
<img src="/desktop/images/ic_videocam_filled.svg" />
|
||||
</button>
|
||||
</p>
|
||||
</body>
|
||||
<footer>
|
||||
<script src="/call.js"></script>
|
||||
<script src="/desktop/ui.js"></script>
|
||||
</footer>
|
||||
</html>
|
@ -0,0 +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>
|
After Width: | Height: | Size: 435 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M480 630.5q-41.75 0-69.875-30.167Q382 570.167 382 527V278q0-40.417 28.566-68.708Q439.132 181 479.941 181t69.434 28.292Q578 237.583 578 278v249q0 43.167-28.125 73.333Q521.75 630.5 480 630.5Zm0-224.5Zm-.175 526q-12.325 0-20.325-8.375t-8-20.625V795.865Q354 786 285.25 719T206 557.5q-1.5-12.593 7.295-21.547Q222.091 527 235.5 527q9.917 0 18.148 7.542 8.232 7.541 9.852 18.458 10.5 80.5 72.044 134 61.543 53.5 144.347 53.5 82.805 0 144.457-53.5Q686 633.5 696.5 553q1.853-11.167 10.121-18.583Q714.89 527 725.543 527q12.91 0 21.434 8.953Q755.5 544.907 754 557.5 743.5 652 674.75 719T509 795.865V903q0 12.25-8.425 20.625-8.426 8.375-20.75 8.375ZM480 573q18.075 0 29.288-13.5Q520.5 546 520.5 527V278.335q0-16.835-11.629-28.335-11.628-11.5-28.818-11.5t-28.872 11.356Q439.5 261.212 439.5 278v248.868q0 19.132 11.212 32.632Q461.925 573 480 573Z"/></svg>
|
After Width: | Height: | Size: 949 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M681.5 693 640 651.5q16.5-20.5 26-45.75T679 553q1.814-11.167 10.182-18.583Q697.551 527 707.847 527q13.153 0 21.653 8.953 8.5 8.954 7 21.547-4.5 37-18.5 71.75T681.5 693ZM554 566l-51-50V279.038q0-17.463-11.489-29.001-11.49-11.537-29.213-11.537t-29.01 11.431Q422 261.362 422 279v155l-57.5-57.5V279q0-40.833 28.515-69.417Q421.529 181 462.265 181q40.735 0 69.485 28.583Q560.5 238.167 560.5 279v248.23q0 7.103-1.5 19.186-1.5 12.084-5 19.584Zm-94.5-94.5Zm350.5 505L58.5 225q-8-7.444-8-18.222Q50.5 196 58.25 188q7.75-8 18.006-8 10.255 0 18.244 8L847 940.5q8 7.989 8 17.994 0 10.006-8 17.756-8 8.25-18.961 8.25-10.961 0-18.039-8ZM433.5 903V795.865Q336 786 267.5 719t-79-161.5q-2-12.5 7.045-21.5 9.046-9 22.455-9 9.5 0 17.75 7.5T246 553q10.053 80.713 71.588 134.107Q379.124 740.5 462.289 740.5q37.711 0 73.071-12.588Q570.721 715.325 599.5 693l41.5 41.5q-31 26-69.014 41.568Q533.972 791.635 491 796v107q0 12.25-8.463 20.625T462.325 932q-12.325 0-20.575-8.375T433.5 903Z"/></svg>
|
After Width: | Height: | Size: 1.0 KiB |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="100" viewBox="0 -960 960 960" width="100"><path fill="white" d="M774.5-488.5q-5.5-119.5-89-203.25t-203-88.75V-838q71 2.5 133.5 30.5t109.75 75.25q47.25 47.25 75.5 110T832-488.5h-57.5Zm-168 0q-6-49.5-40.5-83.75t-83.5-39.25V-669q73 5 124.25 56T664-488.5h-57.5Zm184 363.5Q677-125 558-180.5T338-338Q236-439 180.5-557.75T125-790.692q0-18.808 12.714-31.558Q150.429-835 169.5-835H306q14 0 23.75 9.75t13.75 24.75l26.929 123.641Q372-663.5 369.5-652q-2.5 11.5-10.229 19.226L261-533q26 44 54.688 81.658Q344.375-413.683 379-380q36.5 38 77.25 69.323Q497-279.353 542-255l95.544-98q9.456-10.5 21.357-14.25T682.5-369l117.362 25.438Q815-340 825-327.801q10 12.198 10 27.301v131q0 19.071-12.714 31.786Q809.571-125 790.5-125ZM232-585.5l81-82-23.5-110H183q1.5 41.5 13 88.25t36 103.75Zm364 358q40 19 88.166 31t93.334 14v-107l-102-21.5-79.5 83.5Zm-364-358Zm364 358Z"/></svg>
|
After Width: | Height: | Size: 898 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M143.5 891.5q-23.031 0-40.266-17.234Q86 857.031 86 834V318q0-23.031 17.234-40.266Q120.469 260.5 143.5 260.5h516.211q22.289 0 39.789 17.234Q717 294.969 717 318v215.5L849 401q8-7.5 16.75-3.75t8.75 13.063V741q0 10-8.75 13.75t-16.85-4.35L717 618.5V834q0 23.031-17.5 40.266Q682 891.5 659.711 891.5H143.5Z"/></svg>
|
After Width: | Height: | Size: 416 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M849.5 750.5 717 618.5v114L659.5 675V318H302l-57.5-57.5h415q22.969 0 40.234 17.266Q717 295.031 717 318v215l132.5-132.5q6.5-6.5 15.75-3.167 9.25 3.334 9.25 12.667v331q0 9.625-9.25 13.062Q856 757.5 849.5 750.5Zm-26.5 250-758-758q-8-7.547-8-19.069 0-11.522 9-20.431 8.5-8.5 20-8.5t20.5 8.5l758 758q7.5 7.93 7.5 19.465t-8.5 20.035q-9 9-20.5 9t-20-9Zm-340-502Zm-319.5-238L221 318h-77.5v516h516v-77.5L716 813v21q0 22.969-17.266 40.234Q681.469 891.5 658.5 891.5h-515q-22.969 0-40.234-17.266Q86 856.969 86 834V318q0-22.969 17.266-40.234Q120.531 260.5 143.5 260.5h20Zm236 316.5Z"/></svg>
|
After Width: | Height: | Size: 686 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M232.5 693q-12.5 0-20.5-8t-8-20.5v-177q0-12.5 8-20.5t20.5-8h129L509 311.5q13.5-13.5 31-6.5t17.5 26v489.5q0 19.5-17.5 26.5t-31-6.5L361.5 693h-129ZM615 742V409.5q55 17 88 63.25T736 576q0 58-33 103.25T615 742ZM500 408.5l-112.5 108h-126v119h126L500 744V408.5ZM379 576Z"/></svg>
|
After Width: | Height: | Size: 381 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M600.5 904.5q-15.5 5.5-28.25-4T559.5 874q0-7.5 4.25-13.5t11.75-8q89-31.5 143.75-107T774 575q0-94.5-54.5-170.5T575.5 298q-7-2-11.5-8.5t-4.5-14.5q0-16 13.25-25.25t27.75-4.25Q704.5 283 768 373t63.5 202q0 112.5-63.5 202.5t-167.5 127ZM157 693q-12.5 0-20.5-8t-8-20.5v-177q0-12.5 8-20.5t20.5-8h129l147.5-147.5q13.5-13.5 31-6.25T482 331v489.5q0 19-17.5 26.25t-31-6.25L286 693H157Zm382.5 49V409.5q55 17 88 63.25t33 103.25q0 58-33 103.25t-88 62.75Zm-115-333.5L312 516.5H186v119h126L424.5 744V408.5Zm-93 167.5Z"/></svg>
|
After Width: | Height: | Size: 616 B |
127
packages/simplex-chat-webrtc/src/desktop/style.css
Normal file
@ -0,0 +1,127 @@
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
#remote-video-stream {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
#local-video-stream {
|
||||
position: absolute;
|
||||
width: 20%;
|
||||
max-width: 20%;
|
||||
object-fit: cover;
|
||||
margin: 16px;
|
||||
border-radius: 16px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
*::-webkit-media-controls {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
}
|
||||
*::-webkit-media-controls-panel {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
}
|
||||
*::-webkit-media-controls-play-button {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
}
|
||||
*::-webkit-media-controls-start-playback-button {
|
||||
display: none !important;
|
||||
-webkit-appearance: none !important;
|
||||
}
|
||||
|
||||
#manage-call {
|
||||
position: absolute;
|
||||
width: fit-content;
|
||||
top: 90%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-column-gap: 30px;
|
||||
}
|
||||
|
||||
#manage-call button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
#progress {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin-left: -52px;
|
||||
margin-top: -52px;
|
||||
border-radius: 50%;
|
||||
border-top: 5px solid white;
|
||||
border-right: 5px solid white;
|
||||
border-bottom: 5px solid white;
|
||||
border-left: 5px solid black;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
-webkit-animation: spin 2s linear infinite;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes spin {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
#info-block {
|
||||
position: absolute;
|
||||
color: white;
|
||||
line-height: 10px;
|
||||
opacity: 0.8;
|
||||
width: 200px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
#info-block.audio {
|
||||
text-align: center;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin-left: -100px;
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
#info-block.video {
|
||||
left: 16px;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
#audio-call-icon {
|
||||
position: absolute;
|
||||
display: none;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin-left: -50px;
|
||||
margin-top: -44px;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
87
packages/simplex-chat-webrtc/src/desktop/ui.ts
Normal file
@ -0,0 +1,87 @@
|
||||
// Override defaults to enable worker on Chrome and Safari
|
||||
useWorker = typeof window.Worker !== "undefined"
|
||||
|
||||
// Create WebSocket connection.
|
||||
const socket = new WebSocket(`ws://${location.host}`)
|
||||
|
||||
socket.addEventListener("open", (_event) => {
|
||||
console.log("Opened socket")
|
||||
sendMessageToNative = (msg: WVApiMessage) => {
|
||||
console.log("Message to server")
|
||||
socket.send(JSON.stringify(msg))
|
||||
}
|
||||
})
|
||||
|
||||
socket.addEventListener("message", (event) => {
|
||||
const parsed = JSON.parse(event.data)
|
||||
reactOnMessageFromServer(parsed)
|
||||
processCommand(parsed)
|
||||
console.log("Message from server")
|
||||
})
|
||||
|
||||
socket.addEventListener("close", (_event) => {
|
||||
console.log("Closed socket")
|
||||
sendMessageToNative = (_msg: WVApiMessage) => {
|
||||
console.log("Tried to send message to native but the socket was closed already")
|
||||
}
|
||||
window.close()
|
||||
})
|
||||
|
||||
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 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" />'
|
||||
}
|
||||
}
|
||||
|
||||
function toggleVideoManually() {
|
||||
if (activeCall?.localMedia) {
|
||||
document.getElementById("toggle-video")!!.innerHTML = toggleMedia(activeCall.localStream, CallMediaType.Video)
|
||||
? '<img src="/desktop/images/ic_videocam_filled.svg" />'
|
||||
: '<img src="/desktop/images/ic_videocam_off.svg" />'
|
||||
}
|
||||
}
|
||||
|
||||
function reactOnMessageFromServer(msg: WVApiMessage) {
|
||||
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 == "video") {
|
||||
document.getElementById("toggle-video")!!.style.display = "inline-block"
|
||||
}
|
||||
document.getElementById("info-block")!!.className = msg.command.media
|
||||
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"
|
||||
}
|
||||
}
|
||||
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"."deb3fc73595ceae34902d3402d075e3a531d5221" = "031zrk32p8ji8hlvk8aj1v99g5zpcsran8qhq36sgi34sy6864z6";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."cf8b9c12ff5cbdc77d3b8866af2c761a546ec8fc" = "0xcbvxz2nszm1sdh6gvmfzjf9n2ldsarmmzbl6j6b5hg9i1mppc6";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/kazu-yamamoto/http2.git"."804fa283f067bd3fd89b8c5f8d25b3047813a517" = "1j67wp7rfybfx3ryx08z6gqmzj85j51hmzhgx47ihgmgr47sl895";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "0kiwhvml42g9anw4d2v0zd1fpc790pj9syg5x3ik4l97fnkbbwpp";
|
||||
|
@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 5.4.0.1
|
||||
version: 5.4.0.2
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
|
@ -154,7 +154,10 @@ defaultChatConfig =
|
||||
_defaultSMPServers :: NonEmpty SMPServerWithAuth
|
||||
_defaultSMPServers =
|
||||
L.fromList
|
||||
[ "smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion",
|
||||
[ "smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion",
|
||||
"smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion",
|
||||
"smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion",
|
||||
"smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion",
|
||||
"smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion",
|
||||
"smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion",
|
||||
"smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion",
|
||||
|
@ -49,9 +49,9 @@ extra-deps:
|
||||
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
||||
# - ../simplexmq
|
||||
- github: simplex-chat/simplexmq
|
||||
commit: deb3fc73595ceae34902d3402d075e3a531d5221
|
||||
commit: cf8b9c12ff5cbdc77d3b8866af2c761a546ec8fc
|
||||
- github: kazu-yamamoto/http2
|
||||
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
|
||||
commit: 804fa283f067bd3fd89b8c5f8d25b3047813a517
|
||||
# - ../direct-sqlcipher
|
||||
- github: simplex-chat/direct-sqlcipher
|
||||
commit: f814ee68b16a9447fbb467ccc8f29bdd3546bfd9
|
||||
|