Merge branch 'master' into lp/custom-user-picker-sheet

This commit is contained in:
Levitating Pineapple
2024-09-27 11:09:24 +03:00
49 changed files with 2633 additions and 686 deletions
@@ -75,9 +75,14 @@ enum MetaColorMode {
}
}
var statusSpacer: Text {
func statusSpacer(_ sent: Bool) -> Text {
switch self {
case .normal, .transparent: Text(Image(systemName: "circlebadge.fill")).foregroundColor(.clear)
case .normal, .transparent:
Text(
sent
? Image("checkmark.wide")
: Image(systemName: "circlebadge.fill")
).foregroundColor(.clear)
case .invertedMaterial: Text(" ").kerning(13)
}
}
@@ -130,10 +135,10 @@ func ciMetaText(
colorMode.resolve(statusColor)
}
r = r + colored(Text(image), metaColor)
space = Text(" ")
} else if !meta.disappearing {
space = colorMode.statusSpacer + Text(" ")
r = r + colorMode.statusSpacer(meta.itemStatus.sent)
}
space = Text(" ")
}
if let enc = encrypted {
appendSpace()
+30 -30
View File
@@ -219,11 +219,11 @@
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; };
E55128F12C9DA948001D165C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128EC2C9DA948001D165C /* libffi.a */; };
E55128F22C9DA948001D165C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128ED2C9DA948001D165C /* libgmpxx.a */; };
E55128F32C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128EE2C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx-ghc9.6.3.a */; };
E55128F42C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128EF2C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx.a */; };
E55128F52C9DA948001D165C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E55128F02C9DA948001D165C /* libgmp.a */; };
E5D826852CA5F56100A9B74D /* libHSsimplex-chat-6.1.0.4-5C0H3SCWHuhICcJbTCMAKi-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D826802CA5F56100A9B74D /* libHSsimplex-chat-6.1.0.4-5C0H3SCWHuhICcJbTCMAKi-ghc9.6.3.a */; };
E5D826862CA5F56100A9B74D /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D826812CA5F56100A9B74D /* libffi.a */; };
E5D826872CA5F56100A9B74D /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D826822CA5F56100A9B74D /* libgmp.a */; };
E5D826882CA5F56100A9B74D /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D826832CA5F56100A9B74D /* libgmpxx.a */; };
E5D826892CA5F56100A9B74D /* libHSsimplex-chat-6.1.0.4-5C0H3SCWHuhICcJbTCMAKi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D826842CA5F56100A9B74D /* libHSsimplex-chat-6.1.0.4-5C0H3SCWHuhICcJbTCMAKi.a */; };
E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; };
E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; };
@@ -560,11 +560,11 @@
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = "<group>"; };
E55128EC2C9DA948001D165C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
E55128ED2C9DA948001D165C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
E55128EE2C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx-ghc9.6.3.a"; sourceTree = "<group>"; };
E55128EF2C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx.a"; sourceTree = "<group>"; };
E55128F02C9DA948001D165C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
E5D826802CA5F56100A9B74D /* libHSsimplex-chat-6.1.0.4-5C0H3SCWHuhICcJbTCMAKi-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.4-5C0H3SCWHuhICcJbTCMAKi-ghc9.6.3.a"; sourceTree = "<group>"; };
E5D826812CA5F56100A9B74D /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
E5D826822CA5F56100A9B74D /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
E5D826832CA5F56100A9B74D /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
E5D826842CA5F56100A9B74D /* libHSsimplex-chat-6.1.0.4-5C0H3SCWHuhICcJbTCMAKi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.1.0.4-5C0H3SCWHuhICcJbTCMAKi.a"; sourceTree = "<group>"; };
E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -655,14 +655,14 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E55128F32C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx-ghc9.6.3.a in Frameworks */,
E55128F42C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx.a in Frameworks */,
E55128F52C9DA948001D165C /* libgmp.a in Frameworks */,
E5D826852CA5F56100A9B74D /* libHSsimplex-chat-6.1.0.4-5C0H3SCWHuhICcJbTCMAKi-ghc9.6.3.a in Frameworks */,
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
E55128F22C9DA948001D165C /* libgmpxx.a in Frameworks */,
E5D826892CA5F56100A9B74D /* libHSsimplex-chat-6.1.0.4-5C0H3SCWHuhICcJbTCMAKi.a in Frameworks */,
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
E5D826862CA5F56100A9B74D /* libffi.a in Frameworks */,
E5D826872CA5F56100A9B74D /* libgmp.a in Frameworks */,
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
E55128F12C9DA948001D165C /* libffi.a in Frameworks */,
E5D826882CA5F56100A9B74D /* libgmpxx.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -739,11 +739,11 @@
5C764E5C279C70B7000C6508 /* Libraries */ = {
isa = PBXGroup;
children = (
E55128EC2C9DA948001D165C /* libffi.a */,
E55128F02C9DA948001D165C /* libgmp.a */,
E55128ED2C9DA948001D165C /* libgmpxx.a */,
E55128EE2C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx-ghc9.6.3.a */,
E55128EF2C9DA948001D165C /* libHSsimplex-chat-6.1.0.3-7yIa9Uiui2A43fFRiuUJXx.a */,
E5D826812CA5F56100A9B74D /* libffi.a */,
E5D826822CA5F56100A9B74D /* libgmp.a */,
E5D826832CA5F56100A9B74D /* libgmpxx.a */,
E5D826802CA5F56100A9B74D /* libHSsimplex-chat-6.1.0.4-5C0H3SCWHuhICcJbTCMAKi-ghc9.6.3.a */,
E5D826842CA5F56100A9B74D /* libHSsimplex-chat-6.1.0.4-5C0H3SCWHuhICcJbTCMAKi.a */,
);
path = Libraries;
sourceTree = "<group>";
@@ -1899,7 +1899,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 240;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1948,7 +1948,7 @@
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 240;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
@@ -1989,7 +1989,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 240;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@@ -2009,7 +2009,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 240;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
@@ -2034,7 +2034,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 240;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
GCC_OPTIMIZATION_LEVEL = s;
@@ -2071,7 +2071,7 @@
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 240;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_BITCODE = NO;
ENABLE_CODE_COVERAGE = NO;
@@ -2108,7 +2108,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 240;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2159,7 +2159,7 @@
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 240;
DEFINES_MODULE = YES;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2210,7 +2210,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 240;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -2244,7 +2244,7 @@
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 238;
CURRENT_PROJECT_VERSION = 240;
DEVELOPMENT_TEAM = 5NN7GUYB6T;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
+14
View File
@@ -2820,6 +2820,20 @@ public enum CIStatus: Decodable, Hashable {
case .invalid: return "invalid"
}
}
public var sent: Bool {
switch self {
case .sndNew: true
case .sndSent: true
case .sndRcvd: true
case .sndErrorAuth: true
case .sndError: true
case .sndWarning: true
case .rcvNew: false
case .rcvRead: false
case .invalid: false
}
}
public func statusIcon(_ metaColor: Color, _ paleMetaColor: Color, _ primaryColor: Color = .accentColor) -> (Image, Color)? {
switch self {
@@ -2,6 +2,7 @@ package chat.simplex.app
import android.app.*
import android.content.*
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.graphics.Bitmap
import android.graphics.BitmapFactory
@@ -83,7 +84,7 @@ class CallService: Service() {
generalGetString(MR.strings.notification_preview_somebody)
else
call?.contact?.profile?.displayName ?: ""
val text = generalGetString(if (call?.supportsVideo() == true) MR.strings.call_service_notification_video_call else MR.strings.call_service_notification_audio_call)
val text = generalGetString(if (call?.hasVideo == true) MR.strings.call_service_notification_video_call else MR.strings.call_service_notification_audio_call)
val image = call?.contact?.image
val largeIcon = if (image == null || previewMode == NotificationPreviewMode.HIDDEN.name)
BitmapFactory.decodeResource(resources, R.drawable.icon)
@@ -105,7 +106,7 @@ class CallService: Service() {
0
}
} else if (Build.VERSION.SDK_INT >= 30) {
if (call.supportsVideo()) {
if (call.hasVideo && ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
} else {
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
@@ -116,7 +116,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
private fun hasGrantedPermissions(): Boolean {
val grantedAudio = ContextCompat.checkSelfPermission(this, android.Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
val grantedCamera = !callSupportsVideo() || ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
val grantedCamera = !callHasVideo() || ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
return grantedAudio && grantedCamera
}
@@ -124,7 +124,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
override fun onBackPressed() {
if (isOnLockScreenNow()) {
super.onBackPressed()
} else if (!hasGrantedPermissions() && !callSupportsVideo()) {
} else if (!hasGrantedPermissions() && !callHasVideo()) {
val call = m.activeCall.value
if (call != null) {
withBGApi { chatModel.callManager.endCall(call) }
@@ -142,7 +142,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
override fun onUserLeaveHint() {
super.onUserLeaveHint()
// On Android 12+ PiP is enabled automatically when a user hides the app
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callSupportsVideo() && platform.androidPictureInPictureAllowed()) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callHasVideo() && platform.androidPictureInPictureAllowed()) {
enterPictureInPictureMode()
}
}
@@ -198,7 +198,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
fun getKeyguardManager(context: Context): KeyguardManager =
context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
private fun callSupportsVideo() = m.activeCall.value?.supportsVideo() == true || m.activeCallInvitation.value?.callType?.media == CallMediaType.Video
private fun callHasVideo() = m.activeCall.value?.hasVideo == true || m.activeCallInvitation.value?.callType?.media == CallMediaType.Video
@Composable
fun CallActivityView() {
@@ -212,7 +212,7 @@ fun CallActivityView() {
.collect { collapsed ->
when {
collapsed -> {
if (!platform.androidPictureInPictureAllowed() || !callSupportsVideo()) {
if (!platform.androidPictureInPictureAllowed() || !callHasVideo()) {
activity.moveTaskToBack(true)
activity.startActivity(Intent(activity, MainActivity::class.java))
} else if (!activity.isInPictureInPictureMode && activity.lifecycle.currentState == Lifecycle.State.RESUMED) {
@@ -221,7 +221,7 @@ fun CallActivityView() {
activity.enterPictureInPictureMode()
}
}
callSupportsVideo() && !platform.androidPictureInPictureAllowed() -> {
callHasVideo() && !platform.androidPictureInPictureAllowed() -> {
// PiP disabled by user
platform.androidStartCallActivity(false)
}
@@ -242,28 +242,43 @@ fun CallActivityView() {
Box(Modifier.background(Color.Black)) {
if (call != null) {
val permissionsState = rememberMultiplePermissionsState(
permissions = if (callSupportsVideo()) {
permissions = if (callHasVideo()) {
listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
} else {
listOf(Manifest.permission.RECORD_AUDIO)
}
)
if (permissionsState.allPermissionsGranted) {
// callState == connected is needed in a situation when a peer enabled camera in audio call while a user didn't grant camera permission yet,
// so no need to hide active call view in this case
if (permissionsState.allPermissionsGranted || call.callState == CallState.Connected) {
ActiveCallView()
LaunchedEffect(Unit) {
activity.startServiceAndBind()
}
} else {
CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callSupportsVideo()) {
}
if ((!permissionsState.allPermissionsGranted && call.callState != CallState.Connected) || call.wantsToEnableCamera) {
CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callHasVideo() || call.wantsToEnableCamera) {
withBGApi { chatModel.callManager.endCall(call) }
}
val cameraAndMicPermissions = rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))
DisposableEffect(cameraAndMicPermissions.allPermissionsGranted) {
onDispose {
if (call.wantsToEnableCamera && cameraAndMicPermissions.allPermissionsGranted) {
val activeCall = chatModel.activeCall.value
if (activeCall != null && activeCall.contact.apiId == call.contact.apiId) {
chatModel.activeCall.value = activeCall.copy(wantsToEnableCamera = false)
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Camera, enable = true))
}
}
}
}
}
val view = LocalView.current
if (callSupportsVideo()) {
if (callHasVideo()) {
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) {
scope.launch {
activity.setPipParams(callSupportsVideo(), viewRatio = Rational(view.width, view.height))
activity.setPipParams(callHasVideo(), viewRatio = Rational(view.width, view.height))
activity.trackPipAnimationHintView(view)
}
}
@@ -47,7 +47,7 @@ class PostSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
Log.d(TAG, "Added audio devices2: ${devices.value.map { it.type }}")
if (devices.value.size - oldDevices.size > 0) {
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, false)
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, false)
}
}
@@ -116,14 +116,14 @@ class PreSCallAudioDeviceManager: CallAudioDeviceManagerInterface {
Log.d(TAG, "Added audio devices: ${addedDevices.map { it.type }}")
super.onAudioDevicesAdded(addedDevices)
devices.value = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { it.hasSupportedType() }.excludeSameType().excludeEarpieceIfWired()
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, false)
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, false)
}
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>) {
Log.d(TAG, "Removed audio devices: ${removedDevices.map { it.type }}")
super.onAudioDevicesRemoved(removedDevices)
devices.value = am.getDevices(AudioManager.GET_DEVICES_OUTPUTS).filter { it.hasSupportedType() }.excludeSameType().excludeEarpieceIfWired()
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.supportsVideo() == true, true)
selectLastExternalDeviceOrDefault(chatModel.activeCall.value?.hasVideo == true, true)
}
}
@@ -7,6 +7,7 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.content.*
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.media.*
import android.os.Build
import android.os.PowerManager
@@ -16,9 +17,12 @@ import android.view.ViewGroup
import android.webkit.*
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.snapshots.SnapshotStateList
@@ -27,12 +31,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.*
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.lifecycle.*
import androidx.webkit.WebViewAssetLoader
import androidx.webkit.WebViewClientCompat
@@ -119,26 +124,26 @@ actual fun ActiveCallView() {
val callRh = call.remoteHostId
when (val r = apiMsg.resp) {
is WCallResponse.Capabilities -> withBGApi {
val callType = CallType(call.localMedia, r.capabilities)
val callType = CallType(call.initialCallType, r.capabilities)
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
updateActiveCall(call) { it.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities) }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
// Starting is delayed to make Android <= 11 working good with Bluetooth
callAudioDeviceManager.start()
} else {
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.supportsVideo(), true)
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
}
CallSoundsPlayer.startConnectingCallSound(scope)
activeCallWaitDeliveryReceipt(scope)
}
is WCallResponse.Offer -> withBGApi {
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.initialCallType, r.capabilities)
updateActiveCall(call) { it.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities) }
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
// Starting is delayed to make Android <= 11 working good with Bluetooth
callAudioDeviceManager.start()
} else {
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.supportsVideo(), true)
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
}
}
is WCallResponse.Answer -> withBGApi {
@@ -162,6 +167,17 @@ actual fun ActiveCallView() {
is WCallResponse.Connected -> {
updateActiveCall(call) { it.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo) }
}
is WCallResponse.PeerMedia -> {
updateActiveCall(call) {
val sources = it.peerMediaSources
when (r.source) {
CallMediaSource.Mic -> it.copy(peerMediaSources = sources.copy(mic = r.enabled))
CallMediaSource.Camera -> it.copy(peerMediaSources = sources.copy(camera = r.enabled))
CallMediaSource.ScreenAudio -> it.copy(peerMediaSources = sources.copy(screenAudio = r.enabled))
CallMediaSource.ScreenVideo -> it.copy(peerMediaSources = sources.copy(screenVideo = r.enabled))
}
}
}
is WCallResponse.End -> {
withBGApi { chatModel.callManager.endCall(call) }
}
@@ -174,16 +190,19 @@ actual fun ActiveCallView() {
updateActiveCall(call) { it.copy(callState = CallState.Negotiated) }
is WCallCommand.Media -> {
updateActiveCall(call) {
when (cmd.media) {
CallMediaType.Video -> it.copy(videoEnabled = cmd.enable)
CallMediaType.Audio -> it.copy(audioEnabled = cmd.enable)
val sources = it.localMediaSources
when (cmd.source) {
CallMediaSource.Mic -> it.copy(localMediaSources = sources.copy(mic = cmd.enable))
CallMediaSource.Camera -> it.copy(localMediaSources = sources.copy(camera = cmd.enable))
CallMediaSource.ScreenAudio -> it.copy(localMediaSources = sources.copy(screenAudio = cmd.enable))
CallMediaSource.ScreenVideo -> it.copy(localMediaSources = sources.copy(screenVideo = cmd.enable))
}
}
}
is WCallCommand.Camera -> {
updateActiveCall(call) { it.copy(localCamera = cmd.camera) }
if (!call.audioEnabled) {
chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false))
if (!call.localMediaSources.mic) {
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = false))
}
}
is WCallCommand.End -> {
@@ -200,7 +219,6 @@ actual fun ActiveCallView() {
val showOverlay = when {
call == null -> false
!platform.androidPictureInPictureAllowed() -> true
!call.supportsVideo() -> true
!chatModel.activeCallViewIsCollapsed.value -> true
else -> false
}
@@ -208,6 +226,11 @@ actual fun ActiveCallView() {
ActiveCallOverlay(call, chatModel, callAudioDeviceManager)
}
}
KeyChangeEffect(call?.hasVideo) {
if (call != null) {
callAudioDeviceManager.selectLastExternalDeviceOrDefault(call.hasVideo, true)
}
}
val context = LocalContext.current
DisposableEffect(Unit) {
val activity = context as? Activity ?: return@DisposableEffect onDispose {}
@@ -237,9 +260,15 @@ private fun ActiveCallOverlay(call: Call, chatModel: ChatModel, callAudioDeviceM
devices = remember { callAudioDeviceManager.devices }.value,
currentDevice = remember { callAudioDeviceManager.currentDevice },
dismiss = { withBGApi { chatModel.callManager.endCall(call) } },
toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = !call.audioEnabled)) },
toggleAudio = { chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = !call.localMediaSources.mic)) },
selectDevice = { callAudioDeviceManager.selectDevice(it.id) },
toggleVideo = { chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Video, enable = !call.videoEnabled)) },
toggleVideo = {
if (ContextCompat.checkSelfPermission(androidAppContext, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Camera, enable = !call.localMediaSources.camera))
} else {
updateActiveCall(call) { it.copy(wantsToEnableCamera = true) }
}
},
toggleSound = {
val enableSpeaker = callAudioDeviceManager.currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE
val preferredInternalDevice = callAudioDeviceManager.devices.value.firstOrNull { it.type == if (enableSpeaker) AudioDeviceInfo.TYPE_BUILTIN_SPEAKER else AudioDeviceInfo.TYPE_BUILTIN_EARPIECE }
@@ -293,30 +322,30 @@ private fun ActiveCallOverlayLayout(
flipCamera: () -> Unit
) {
Column {
val media = call.peerMedia ?: call.localMedia
CloseSheetBar({ chatModel.activeCallViewIsCollapsed.value = true }, true, tintColor = Color(0xFFFFFFD8)) {
if (media == CallMediaType.Video) {
if (call.hasVideo) {
Text(call.contact.chatViewName, Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), color = Color(0xFFFFFFD8), style = MaterialTheme.typography.h2, overflow = TextOverflow.Ellipsis, maxLines = 1)
}
}
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
@Composable
fun SelectSoundDevice() {
fun SelectSoundDevice(size: Dp) {
if (devices.size == 2 &&
devices.all { it.type == AudioDeviceInfo.TYPE_BUILTIN_EARPIECE || it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER } ||
currentDevice.value == null ||
devices.none { it.id == currentDevice.value?.id }
) {
val isSpeaker = currentDevice.value?.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER
ToggleSoundButton(call, enabled, isSpeaker, toggleSound)
ToggleSoundButton(enabled, isSpeaker, !call.peerMediaSources.mic, toggleSound, size = size)
} else {
ExposedDropDownSettingWithIcon(
devices.map { Triple(it, it.icon, if (it.name != null) generalGetString(it.name!!) else it.productName.toString()) },
devices.map { Triple(it, if (call.peerMediaSources.mic) it.icon else MR.images.ic_volume_off, if (it.name != null) generalGetString(it.name!!) else it.productName.toString()) },
currentDevice,
fontSize = 18.sp,
iconSize = 40.dp,
boxSize = size,
listIconSize = 30.dp,
iconColor = Color(0xFFFFFFD8),
background = controlButtonsBackground(),
minWidth = 300.dp,
onSelected = {
if (it != null) {
@@ -327,29 +356,9 @@ private fun ActiveCallOverlayLayout(
}
}
when (media) {
CallMediaType.Video -> {
VideoCallInfoView(call)
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
DisabledBackgroundCallsButton()
}
Row(Modifier.fillMaxWidth().padding(horizontal = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
ToggleAudioButton(call, enabled, toggleAudio)
SelectSoundDevice()
IconButton(onClick = dismiss, enabled = enabled) {
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp))
}
if (call.videoEnabled) {
ControlButton(call, painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, enabled, flipCamera)
ControlButton(call, painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, enabled, toggleVideo)
} else {
Spacer(Modifier.size(48.dp))
ControlButton(call, painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, enabled, toggleVideo)
}
}
}
CallMediaType.Audio -> {
when (call.hasVideo) {
true -> VideoCallInfoView(call)
false -> {
Spacer(Modifier.fillMaxHeight().weight(1f))
Column(
Modifier.fillMaxWidth(),
@@ -359,23 +368,26 @@ private fun ActiveCallOverlayLayout(
ProfileImage(size = 192.dp, image = call.contact.profile.image)
AudioCallInfoView(call)
}
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
DisabledBackgroundCallsButton()
}
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.CenterStart) {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
IconButton(onClick = dismiss, enabled = enabled) {
Icon(painterResource(MR.images.ic_call_end_filled), stringResource(MR.strings.icon_descr_hang_up), tint = if (enabled) Color.Red else MaterialTheme.colors.secondary, modifier = Modifier.size(64.dp))
}
}
Box(Modifier.padding(start = 32.dp)) {
ToggleAudioButton(call, enabled, toggleAudio)
}
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) {
Box(Modifier.padding(end = 32.dp)) {
SelectSoundDevice()
}
}
}
}
Box(Modifier.fillMaxWidth().fillMaxHeight().weight(1f), contentAlignment = Alignment.BottomCenter) {
DisabledBackgroundCallsButton()
}
BoxWithConstraints(Modifier.padding(start = 6.dp, end = 6.dp, bottom = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) {
val size = ((maxWidth - DEFAULT_PADDING_HALF * 4) / 5).coerceIn(0.dp, 60.dp)
// limiting max width for tablets/wide screens, will be displayed in the center
val padding = ((min(420.dp, maxWidth) - size * 5) / 4).coerceAtLeast(0.dp)
Row(horizontalArrangement = Arrangement.spacedBy(padding), verticalAlignment = Alignment.CenterVertically) {
ToggleMicButton(call, enabled, toggleAudio, size = size)
SelectSoundDevice(size = size)
ControlButton(painterResource(MR.images.ic_call_end_filled), MR.strings.icon_descr_hang_up, enabled = enabled, dismiss, background = Color.Red, size = size, iconPaddingPercent = 0.166f)
if (call.localMediaSources.camera) {
ControlButton(painterResource(MR.images.ic_flip_camera_android_filled), MR.strings.icon_descr_flip_camera, enabled, flipCamera, size = size)
ControlButton(painterResource(MR.images.ic_videocam_filled), MR.strings.icon_descr_video_off, enabled, toggleVideo, size = size)
} else {
Spacer(Modifier.size(size))
ControlButton(painterResource(MR.images.ic_videocam_off), MR.strings.icon_descr_video_on, enabled, toggleVideo, size = size)
}
}
}
@@ -384,34 +396,52 @@ private fun ActiveCallOverlayLayout(
}
@Composable
private fun ControlButton(call: Call, icon: Painter, iconText: StringResource, enabled: Boolean = true, action: () -> Unit) {
if (call.hasMedia) {
IconButton(onClick = action, enabled = enabled) {
Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.size(40.dp))
}
} else {
Spacer(Modifier.size(40.dp))
private fun ControlButton(icon: Painter, iconText: StringResource, enabled: Boolean = true, action: () -> Unit, background: Color = controlButtonsBackground(), size: Dp, iconPaddingPercent: Float = 0.2f) {
ControlButtonWrap(enabled, action, background, size) {
Icon(icon, stringResource(iconText), tint = if (enabled) Color(0xFFFFFFD8) else MaterialTheme.colors.secondary, modifier = Modifier.padding(size * iconPaddingPercent).fillMaxSize())
}
}
@Composable
private fun ToggleAudioButton(call: Call, enabled: Boolean = true, toggleAudio: () -> Unit) {
if (call.audioEnabled) {
ControlButton(call, painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, enabled, toggleAudio)
} else {
ControlButton(call, painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, enabled, toggleAudio)
private fun ControlButtonWrap(enabled: Boolean = true, action: () -> Unit, background: Color = controlButtonsBackground(), size: Dp, content: @Composable () -> Unit) {
Box(
Modifier
.background(background, CircleShape)
.size(size)
.clickable(
onClick = action,
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = size / 2, color = background.lighter(0.1f)),
enabled = enabled
),
contentAlignment = Alignment.Center
) {
content()
}
}
@Composable
private fun ToggleSoundButton(call: Call, enabled: Boolean, speaker: Boolean, toggleSound: () -> Unit) {
if (speaker) {
ControlButton(call, painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, enabled, toggleSound)
private fun ToggleMicButton(call: Call, enabled: Boolean = true, toggleAudio: () -> Unit, size: Dp) {
if (call.localMediaSources.mic) {
ControlButton(painterResource(MR.images.ic_mic), MR.strings.icon_descr_audio_off, enabled, toggleAudio, size = size)
} else {
ControlButton(call, painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, enabled, toggleSound)
ControlButton(painterResource(MR.images.ic_mic_off), MR.strings.icon_descr_audio_on, enabled, toggleAudio, size = size)
}
}
@Composable
private fun ToggleSoundButton(enabled: Boolean, speaker: Boolean, muted: Boolean, toggleSound: () -> Unit, size: Dp) {
when {
muted -> ControlButton(painterResource(MR.images.ic_volume_off), MR.strings.icon_descr_sound_muted, enabled, toggleSound, size = size)
speaker -> ControlButton(painterResource(MR.images.ic_volume_up), MR.strings.icon_descr_speaker_off, enabled, toggleSound, size = size)
else -> ControlButton(painterResource(MR.images.ic_volume_down), MR.strings.icon_descr_speaker_on, enabled, toggleSound, size = size)
}
}
@Composable
fun controlButtonsBackground(): Color = if (chatModel.activeCall.value?.peerMediaSources?.hasVideo == true) Color.Black.copy(0.2f) else Color.White.copy(0.2f)
@Composable
fun AudioCallInfoView(call: Call) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
@@ -553,38 +583,39 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni
}
}
} else {
ColumnWithScrollBar(Modifier.fillMaxSize()) {
Spacer(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier))
AppBarTitle(stringResource(MR.strings.permissions_required))
Spacer(Modifier.weight(1f))
val onClick = {
if (permissionsState.shouldShowRationale) {
context.showAllowPermissionInSettingsAlert()
} else {
permissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled, context::showAllowPermissionInSettingsAlert)
ModalView(background = Color.Black, showClose = false, close = {}) {
ColumnWithScrollBar(Modifier.fillMaxSize()) {
AppBarTitle(stringResource(MR.strings.permissions_required))
Spacer(Modifier.weight(1f))
val onClick = {
if (permissionsState.shouldShowRationale) {
context.showAllowPermissionInSettingsAlert()
} else {
permissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled, context::showAllowPermissionInSettingsAlert)
}
}
}
Text(stringResource(MR.strings.permissions_grant), Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), textAlign = TextAlign.Center, color = Color(0xFFFFFFD8))
SectionSpacer()
SectionView {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
val text = if (hasVideo && audioPermission.status is PermissionStatus.Denied && cameraPermission.status is PermissionStatus.Denied) {
stringResource(MR.strings.permissions_camera_and_record_audio)
} else if (audioPermission.status is PermissionStatus.Denied) {
stringResource(MR.strings.permissions_record_audio)
} else if (hasVideo && cameraPermission.status is PermissionStatus.Denied) {
stringResource(MR.strings.permissions_camera)
} else ""
GrantPermissionButton(text, buttonEnabled.value, onClick)
Text(stringResource(MR.strings.permissions_grant), Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), textAlign = TextAlign.Center, color = Color(0xFFFFFFD8))
SectionSpacer()
SectionView {
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
val text = if (hasVideo && audioPermission.status is PermissionStatus.Denied && cameraPermission.status is PermissionStatus.Denied) {
stringResource(MR.strings.permissions_camera_and_record_audio)
} else if (audioPermission.status is PermissionStatus.Denied) {
stringResource(MR.strings.permissions_record_audio)
} else if (hasVideo && cameraPermission.status is PermissionStatus.Denied) {
stringResource(MR.strings.permissions_camera)
} else null
if (text != null) {
GrantPermissionButton(text, buttonEnabled.value, onClick)
}
}
}
}
Spacer(Modifier.weight(1f))
Box(Modifier.fillMaxWidth().padding(bottom = if (hasVideo) 0.dp else DEFAULT_BOTTOM_PADDING), contentAlignment = Alignment.Center) {
SimpleButtonFrame(cancel, Modifier.height(64.dp)) {
Text(stringResource(MR.strings.call_service_notification_end_call), fontSize = 20.sp, color = Color(0xFFFFFFD8))
Spacer(Modifier.weight(1f))
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING), contentAlignment = Alignment.Center) {
SimpleButtonFrame(cancel, Modifier.height(60.dp)) {
Text(stringResource(MR.strings.call_service_notification_end_call), fontSize = 20.sp, color = Color(0xFFFFFFD8))
}
}
}
}
@@ -768,8 +799,8 @@ fun PreviewActiveCallOverlayVideo() {
userProfile = Profile.sampleData,
contact = Contact.sampleData,
callState = CallState.Negotiated,
localMedia = CallMediaType.Video,
peerMedia = CallMediaType.Video,
initialCallType = CallMediaType.Video,
peerMediaSources = CallMediaSources(),
callUUID = "",
connectionInfo = ConnectionInfo(
RTCIceCandidate(RTCIceCandidateType.Host, "tcp"),
@@ -798,8 +829,8 @@ fun PreviewActiveCallOverlayAudio() {
userProfile = Profile.sampleData,
contact = Contact.sampleData,
callState = CallState.Negotiated,
localMedia = CallMediaType.Audio,
peerMedia = CallMediaType.Audio,
initialCallType = CallMediaType.Audio,
peerMediaSources = CallMediaSources(),
callUUID = "",
connectionInfo = ConnectionInfo(
RTCIceCandidate(RTCIceCandidateType.Host, "udp"),
@@ -54,8 +54,7 @@ actual fun ActiveCallInteractiveArea(call: Call) {
.align(Alignment.BottomCenter),
contentAlignment = Alignment.Center
) {
val media = call.peerMedia ?: call.localMedia
if (media == CallMediaType.Video) {
if (call.hasVideo) {
Icon(painterResource(MR.images.ic_videocam_filled), null, Modifier.size(27.dp).offset(x = 2.5.dp, y = 2.dp), tint = Color.White)
} else {
Icon(painterResource(MR.images.ic_call_filled), null, Modifier.size(27.dp).offset(x = -0.5.dp, y = 2.dp), tint = Color.White)
@@ -2356,7 +2356,8 @@ data class CIMeta (
val deletable: Boolean,
val editable: Boolean
) {
val timestampText: String get() = getTimestampText(itemTs)
val timestampText: String get() = getTimestampText(itemTs, true)
val recent: Boolean get() = updatedAt + 10.toDuration(DurationUnit.SECONDS) > Clock.System.now()
val isLive: Boolean get() = itemLive == true
val disappearing: Boolean get() = !isRcvNew && itemTimed?.deleteAt != null
@@ -2420,7 +2421,18 @@ data class CITimed(
val deleteAt: Instant?
)
fun getTimestampText(t: Instant): String {
fun getTimestampDateText(t: Instant): String {
val tz = TimeZone.currentSystemDefault()
val time = t.toLocalDateTime(tz).toJavaLocalDateTime()
val weekday = time.format(DateTimeFormatter.ofPattern("EEE"))
val dayMonthYear = time.format(DateTimeFormatter.ofPattern(
if (Clock.System.now().toLocalDateTime(tz).year == time.year) "d MMM" else "d MMM YYYY")
)
return "$weekday, $dayMonthYear"
}
fun getTimestampText(t: Instant, shortFormat: Boolean = false): String {
val tz = TimeZone.currentSystemDefault()
val now: LocalDateTime = Clock.System.now().toLocalDateTime(tz)
val time: LocalDateTime = t.toLocalDateTime(tz)
@@ -2428,16 +2440,23 @@ fun getTimestampText(t: Instant): String {
val recent = now.date == time.date ||
(period.years == 0 && period.months == 0 && period.days == 1 && now.hour < 12 && time.hour >= 18 )
val dateFormatter =
if (recent) {
if (recent || shortFormat) {
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
} else {
val dayMonthFormat = when (Locale.getDefault().country) {
"US" -> "M/dd"
"DE" -> "dd.MM"
"RU" -> "dd.MM"
else -> "dd/MM"
}
val dayMonthYearFormat = when (Locale.getDefault().country) {
"US" -> "M/dd/yy"
"DE" -> "dd.MM.yy"
"RU" -> "dd.MM.yy"
else -> "dd/MM/yy"
}
DateTimeFormatter.ofPattern(
when (Locale.getDefault().country) {
"US" -> "M/dd"
"DE" -> "dd.MM"
"RU" -> "dd.MM"
else -> "dd/MM"
}
if (now.year == time.year) dayMonthFormat else dayMonthYearFormat
)
// DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
}
@@ -2525,7 +2525,7 @@ object ChatController {
// TODO askConfirmation?
// TODO check encryption is compatible
withCall(r, r.contact) { call ->
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, peerMedia = r.callType.media, sharedKey = r.sharedKey)
chatModel.activeCall.value = call.copy(callState = CallState.OfferReceived, sharedKey = r.sharedKey)
val useRelay = appPrefs.webrtcPolicyRelay.get()
val iceServers = getIceServers()
Log.d(TAG, ".callOffer iceServers $iceServers")
@@ -49,7 +49,7 @@ class CallManager(val chatModel: ChatModel) {
contact = invitation.contact,
callUUID = invitation.callUUID,
callState = CallState.InvitationAccepted,
localMedia = invitation.callType.media,
initialCallType = invitation.callType.media,
sharedKey = invitation.sharedKey,
)
showCallView.value = true
@@ -2,6 +2,7 @@ package chat.simplex.common.views.call
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.common.model.*
import chat.simplex.common.platform.appPlatform
import chat.simplex.res.MR
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
@@ -15,18 +16,21 @@ data class Call(
val contact: Contact,
val callUUID: String?,
val callState: CallState,
val localMedia: CallMediaType,
val initialCallType: CallMediaType,
val localMediaSources: CallMediaSources = CallMediaSources(mic = true, camera = initialCallType == CallMediaType.Video && appPlatform.isAndroid),
val localCapabilities: CallCapabilities? = null,
val peerMedia: CallMediaType? = null,
val peerMediaSources: CallMediaSources = CallMediaSources(),
val sharedKey: String? = null,
val audioEnabled: Boolean = true,
val videoEnabled: Boolean = localMedia == CallMediaType.Video,
var localCamera: VideoCamera = VideoCamera.User,
val connectionInfo: ConnectionInfo? = null,
var connectedAt: Instant? = null,
// When a user has audio call, and then he wants to enable camera but didn't grant permissions for using camera yet,
// we show permissions view without enabling camera before permissions are granted. After they are granted, enabling camera
val wantsToEnableCamera: Boolean = false
) {
val encrypted: Boolean get() = localEncrypted && sharedKey != null
val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false
private val localEncrypted: Boolean get() = localCapabilities?.encryption ?: false
val encryptionStatus: String get() = when(callState) {
CallState.WaitCapabilities -> ""
@@ -35,10 +39,8 @@ data class Call(
else -> generalGetString(if (!localEncrypted) MR.strings.status_no_e2e_encryption else if (sharedKey == null) MR.strings.status_contact_has_no_e2e_encryption else MR.strings.status_e2e_encrypted)
}
val hasMedia: Boolean get() = callState == CallState.OfferSent || callState == CallState.Negotiated || callState == CallState.Connected
fun supportsVideo(): Boolean = peerMedia == CallMediaType.Video || localMedia == CallMediaType.Video
val hasVideo: Boolean
get() = localMediaSources.hasVideo || peerMediaSources.hasVideo
}
enum class CallState {
@@ -68,6 +70,16 @@ enum class CallState {
@Serializable data class WVAPICall(val corrId: Int? = null, val command: WCallCommand)
@Serializable data class WVAPIMessage(val corrId: Int? = null, val resp: WCallResponse, val command: WCallCommand? = null)
@Serializable data class CallMediaSources(
val mic: Boolean = false,
val camera: Boolean = false,
val screenAudio: Boolean = false,
val screenVideo: Boolean = false
) {
val hasVideo: Boolean
get() = camera || screenVideo
}
@Serializable
sealed class WCallCommand {
@Serializable @SerialName("capabilities") data class Capabilities(val media: CallMediaType): WCallCommand()
@@ -75,7 +87,7 @@ sealed class WCallCommand {
@Serializable @SerialName("offer") data class Offer(val offer: String, val iceCandidates: String, val media: CallMediaType, val aesKey: String? = null, val iceServers: List<RTCIceServer>? = null, val relay: Boolean? = null): WCallCommand()
@Serializable @SerialName("answer") data class Answer (val answer: String, val iceCandidates: String): WCallCommand()
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallCommand()
@Serializable @SerialName("media") data class Media(val media: CallMediaType, val enable: Boolean): WCallCommand()
@Serializable @SerialName("media") data class Media(val source: CallMediaSource, val enable: Boolean): WCallCommand()
@Serializable @SerialName("camera") data class Camera(val camera: VideoCamera): WCallCommand()
@Serializable @SerialName("description") data class Description(val state: String, val description: String): WCallCommand()
@Serializable @SerialName("layout") data class Layout(val layout: LayoutType): WCallCommand()
@@ -90,6 +102,7 @@ sealed class WCallResponse {
@Serializable @SerialName("ice") data class Ice(val iceCandidates: String): WCallResponse()
@Serializable @SerialName("connection") data class Connection(val state: ConnectionState): WCallResponse()
@Serializable @SerialName("connected") data class Connected(val connectionInfo: ConnectionInfo): WCallResponse()
@Serializable @SerialName("peerMedia") data class PeerMedia(val source: CallMediaSource, val enabled: Boolean): WCallResponse()
@Serializable @SerialName("end") object End: WCallResponse()
@Serializable @SerialName("ended") object Ended: WCallResponse()
@Serializable @SerialName("ok") object Ok: WCallResponse()
@@ -165,6 +178,14 @@ enum class CallMediaType {
@SerialName("audio") Audio
}
@Serializable
enum class CallMediaSource {
@SerialName("mic") Mic,
@SerialName("camera") Camera,
@SerialName("screenAudio") ScreenAudio,
@SerialName("screenVideo") ScreenVideo
}
@Serializable
enum class VideoCamera {
@SerialName("user") User,
@@ -25,6 +25,7 @@ import androidx.compose.ui.text.*
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import chat.simplex.common.model.*
import chat.simplex.common.model.CIDirection.GroupRcv
import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.controller
import chat.simplex.common.model.ChatModel.withChats
@@ -41,11 +42,14 @@ import chat.simplex.common.views.newchat.ContactConnectionInfoView
import chat.simplex.res.MR
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.datetime.Clock
import kotlinx.datetime.*
import java.io.File
import java.net.URI
import kotlin.math.abs
import kotlin.math.sign
data class ItemSeparation(val timestamp: Boolean, val largeGap: Boolean, val date: Instant?)
@Composable
// staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat
// to chat list smooth. Otherwise, chat view will become blank right before the transition starts
@@ -576,7 +580,7 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType)
if (chatInfo is ChatInfo.Direct) {
val contactInfo = chatModel.controller.apiContactInfo(remoteHostId, chatInfo.contact.contactId)
val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile)
chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, initialCallType = media, userProfile = profile)
chatModel.showCallView.value = true
chatModel.callCommand.add(WCallCommand.Capabilities(media))
}
@@ -1048,16 +1052,16 @@ fun BoxWithConstraintsScope.ChatItemsList(
val revealed = remember { mutableStateOf(false) }
@Composable
fun ChatItemViewShortHand(cItem: ChatItem, range: IntRange?, fillMaxWidth: Boolean = true) {
fun ChatItemViewShortHand(cItem: ChatItem, itemSeparation: ItemSeparation, range: IntRange?, fillMaxWidth: Boolean = true) {
tryOrShowError("${cItem.id}ChatItem", error = {
CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart)
}) {
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy)
ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, fillMaxWidth = fillMaxWidth, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy, showTimestamp = itemSeparation.timestamp)
}
}
@Composable
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?) {
fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?, itemSeparation: ItemSeparation, previousItemSeparation: ItemSeparation?) {
val dismissState = rememberDismissState(initialValue = DismissValue.Default) {
if (it == DismissValue.DismissedToStart) {
scope.launch {
@@ -1078,7 +1082,26 @@ fun BoxWithConstraintsScope.ChatItemsList(
swipeDistance = with(LocalDensity.current) { 30.dp.toPx() },
)
val sent = cItem.chatDir.sent
Box(Modifier.padding(bottom = 4.dp)) {
@Composable
fun ChatItemBox(modifier: Modifier = Modifier, content: @Composable () -> Unit = { }) {
Box(
modifier = modifier.padding(
bottom = if (itemSeparation.largeGap) {
if (i == 0) {
8.dp
} else {
4.dp
}
} else 1.dp, top = if (previousItemSeparation?.largeGap == true) 4.dp else 1.dp
),
contentAlignment = Alignment.CenterStart
) {
content()
}
}
Box {
val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null
val selectionVisible = selectedChatItems.value != null && cItem.canBeDeletedForSelf
val selectionOffset by animateDpAsState(if (selectionVisible && !sent) 4.dp + 22.dp * fontSizeMultiplier else 0.dp)
@@ -1130,7 +1153,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
@Composable
fun Item() {
Box(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID), contentAlignment = Alignment.CenterStart) {
ChatItemBox(Modifier.layoutId(CHAT_BUBBLE_LAYOUT_ID)) {
androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedChatItem(Modifier, cItem.id, selectedChatItems)
}
@@ -1139,7 +1162,9 @@ fun BoxWithConstraintsScope.ChatItemsList(
Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) {
MemberImage(member)
}
ChatItemViewShortHand(cItem, range, false)
Box(modifier = Modifier.padding(top = 2.dp)) {
ChatItemViewShortHand(cItem, itemSeparation, range, false)
}
}
}
}
@@ -1153,7 +1178,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
}
}
} else {
Box(contentAlignment = Alignment.CenterStart) {
ChatItemBox {
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
}
@@ -1162,12 +1187,12 @@ fun BoxWithConstraintsScope.ChatItemsList(
.padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp)
.then(swipeableOrSelectionModifier)
) {
ChatItemViewShortHand(cItem, range)
ChatItemViewShortHand(cItem, itemSeparation, range)
}
}
}
} else {
Box(contentAlignment = Alignment.CenterStart) {
ChatItemBox {
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
}
@@ -1176,12 +1201,12 @@ fun BoxWithConstraintsScope.ChatItemsList(
.padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp)
.then(if (selectionVisible) Modifier else swipeableModifier)
) {
ChatItemViewShortHand(cItem, range)
ChatItemViewShortHand(cItem, itemSeparation, range)
}
}
}
} else { // direct message
Box(contentAlignment = Alignment.CenterStart) {
ChatItemBox {
AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) {
SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems)
}
@@ -1191,7 +1216,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp,
).then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier)
) {
ChatItemViewShortHand(cItem, range)
ChatItemViewShortHand(cItem, itemSeparation, range)
}
}
}
@@ -1210,17 +1235,30 @@ fun BoxWithConstraintsScope.ChatItemsList(
// memberConnected events and deleted items are aggregated at the last chat item in a row, see ChatItemView
} else {
val (prevHidden, prevItem) = chatModel.getPrevShownChatItem(currIndex, ciCategory)
val itemSeparation = getItemSeparation(cItem, nextItem)
val previousItemSeparation = if (prevItem != null) getItemSeparation(prevItem, cItem) else null
if (itemSeparation.date != null) {
DateSeparator(itemSeparation.date)
}
val range = chatViewItemsRange(currIndex, prevHidden)
if (revealed.value && range != null) {
reversedChatItems.subList(range.first, range.last + 1).forEachIndexed { index, ci ->
val prev = if (index + range.first == prevHidden) prevItem else reversedChatItems[index + range.first + 1]
ChatItemView(ci, null, prev)
ChatItemView(ci, null, prev, itemSeparation, previousItemSeparation)
}
} else {
ChatItemView(cItem, range, prevItem)
ChatItemView(cItem, range, prevItem, itemSeparation, previousItemSeparation)
}
if (i == reversedChatItems.lastIndex) {
DateSeparator(cItem.meta.itemTs)
}
}
if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) {
LaunchedEffect(cItem.id) {
scope.launch {
@@ -1424,7 +1462,7 @@ private fun showMemberImage(member: GroupMember, prevItem: ChatItem?): Boolean =
else -> false
}
val MEMBER_IMAGE_SIZE: Dp = 38.dp
val MEMBER_IMAGE_SIZE: Dp = 37.dp
@Composable
fun MemberImage(member: GroupMember) {
@@ -1518,6 +1556,18 @@ private fun ButtonRow(horizontalArrangement: Arrangement.Horizontal, content: @C
}
}
@Composable
private fun DateSeparator(date: Instant) {
Text(
text = getTimestampDateText(date),
Modifier.padding(DEFAULT_PADDING).fillMaxWidth(),
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
textAlign = TextAlign.Center,
color = MaterialTheme.colors.secondary
)
}
val chatViewScrollState = MutableStateFlow(false)
fun addGroupMembers(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) {
@@ -1891,6 +1941,23 @@ private fun handleForwardConfirmation(
)
}
private fun getItemSeparation(chatItem: ChatItem, nextItem: ChatItem?): ItemSeparation {
if (nextItem == null) {
return ItemSeparation(timestamp = true, largeGap = true, date = null)
}
val sameMemberAndDirection = if (nextItem.chatDir is GroupRcv && chatItem.chatDir is GroupRcv) {
chatItem.chatDir.groupMember.groupMemberId == nextItem.chatDir.groupMember.groupMemberId
} else chatItem.chatDir.sent == nextItem.chatDir.sent
val largeGap = !sameMemberAndDirection || (abs(nextItem.meta.createdAt.epochSeconds - chatItem.meta.createdAt.epochSeconds) >= 60)
return ItemSeparation(
timestamp = largeGap || nextItem.meta.timestampText != chatItem.meta.timestampText,
largeGap = largeGap,
date = if (getTimestampDateText(chatItem.meta.itemTs) == getTimestampDateText(nextItem.meta.itemTs)) null else nextItem.meta.itemTs
)
}
@Preview/*(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true,
@@ -20,6 +20,7 @@ fun CICallItemView(
cItem: ChatItem,
status: CICallStatus,
duration: Int,
showTimestamp: Boolean,
acceptCall: (Contact) -> Unit,
timedMessagesTTL: Int?
) {
@@ -47,7 +48,7 @@ fun CICallItemView(
CICallStatus.Error -> {}
}
CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false)
CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false, showTimestamp = showTimestamp)
}
}
@@ -26,6 +26,7 @@ fun CIGroupInvitationView(
ci: ChatItem,
groupInvitation: CIGroupInvitation,
memberRole: GroupMemberRole,
showTimestamp: Boolean,
chatIncognito: Boolean = false,
joinGroup: (Long, () -> Unit) -> Unit,
timedMessagesTTL: Int?
@@ -118,7 +119,7 @@ fun CIGroupInvitationView(
Text(
buildAnnotatedString {
append(generalGetString(if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join))
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor)) }
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor, showTimestamp = showTimestamp)) }
},
color = if (inProgress.value)
MaterialTheme.colors.secondary
@@ -129,7 +130,7 @@ fun CIGroupInvitationView(
Text(
buildAnnotatedString {
append(groupInvitationStr())
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor)) }
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor, showTimestamp = showTimestamp)) }
}
)
}
@@ -145,7 +146,7 @@ fun CIGroupInvitationView(
}
}
CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false)
CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false, showTimestamp = showTimestamp)
}
}
}
@@ -162,7 +163,8 @@ fun PendingCIGroupInvitationViewPreview() {
groupInvitation = CIGroupInvitation.getSample(),
memberRole = GroupMemberRole.Admin,
joinGroup = { _, _ -> },
timedMessagesTTL = null
timedMessagesTTL = null,
showTimestamp = true,
)
}
}
@@ -179,8 +181,9 @@ fun CIGroupInvitationViewAcceptedPreview() {
groupInvitation = CIGroupInvitation.getSample(status = CIGroupInvitationStatus.Accepted),
memberRole = GroupMemberRole.Admin,
joinGroup = { _, _ -> },
timedMessagesTTL = null
)
timedMessagesTTL = null,
showTimestamp = true,
)
}
}
@@ -196,7 +199,8 @@ fun CIGroupInvitationViewLongNamePreview() {
),
memberRole = GroupMemberRole.Admin,
joinGroup = { _, _ -> },
timedMessagesTTL = null
)
timedMessagesTTL = null,
showTimestamp = true,
)
}
}
@@ -35,7 +35,8 @@ fun CIMetaView(
},
showStatus: Boolean = true,
showEdited: Boolean = true,
showViaProxy: Boolean
showTimestamp: Boolean,
showViaProxy: Boolean,
) {
Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) {
if (chatItem.isDeletedContent) {
@@ -54,7 +55,8 @@ fun CIMetaView(
paleMetaColor,
showStatus = showStatus,
showEdited = showEdited,
showViaProxy = showViaProxy
showViaProxy = showViaProxy,
showTimestamp = showTimestamp
)
}
}
@@ -70,11 +72,11 @@ private fun CIMetaText(
paleColor: Color,
showStatus: Boolean = true,
showEdited: Boolean = true,
showViaProxy: Boolean
showTimestamp: Boolean,
showViaProxy: Boolean,
) {
if (showEdited && meta.itemEdited) {
StatusIconText(painterResource(MR.images.ic_edit), color)
Spacer(Modifier.width(3.dp))
}
if (meta.disappearing) {
StatusIconText(painterResource(MR.images.ic_timer), color)
@@ -82,12 +84,13 @@ private fun CIMetaText(
if (ttl != chatTTL) {
Text(shortTimeText(ttl), color = color, fontSize = 12.sp)
}
Spacer(Modifier.width(4.dp))
}
if (showViaProxy && meta.sentViaProxy == true) {
Spacer(Modifier.width(4.dp))
Icon(painterResource(MR.images.ic_arrow_forward), null, Modifier.height(17.dp), tint = MaterialTheme.colors.secondary)
}
if (showStatus) {
Spacer(Modifier.width(4.dp))
val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor)
if (statusIcon != null) {
val (icon, statusColor) = statusIcon
@@ -96,17 +99,19 @@ private fun CIMetaText(
} else {
StatusIconText(painterResource(icon), statusColor)
}
Spacer(Modifier.width(4.dp))
} else if (!meta.disappearing) {
StatusIconText(painterResource(MR.images.ic_circle_filled), Color.Transparent)
Spacer(Modifier.width(4.dp))
}
}
if (encrypted != null) {
StatusIconText(painterResource(if (encrypted) MR.images.ic_lock else MR.images.ic_lock_open_right), color)
Spacer(Modifier.width(4.dp))
StatusIconText(painterResource(if (encrypted) MR.images.ic_lock else MR.images.ic_lock_open_right), color)
}
if (showTimestamp) {
Spacer(Modifier.width(4.dp))
Text(meta.timestampText, color = color, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Text(meta.timestampText, color = color, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
// the conditions in this function should match CIMetaText
@@ -117,28 +122,56 @@ fun reserveSpaceForMeta(
secondaryColor: Color,
showStatus: Boolean = true,
showEdited: Boolean = true,
showViaProxy: Boolean = false
showViaProxy: Boolean = false,
showTimestamp: Boolean
): String {
val iconSpace = " "
var res = ""
if (showEdited && meta.itemEdited) res += iconSpace
val whiteSpace = " "
var res = iconSpace
var space: String? = null
fun appendSpace() {
if (space != null) {
res += space
space = null
}
}
if (showEdited && meta.itemEdited) {
res += iconSpace
}
if (meta.itemTimed != null) {
res += iconSpace
val ttl = meta.itemTimed.ttl
if (ttl != chatTTL) {
res += shortTimeText(ttl)
}
space = whiteSpace
}
if (showViaProxy && meta.sentViaProxy == true) {
appendSpace()
res += iconSpace
}
if (showStatus && (meta.statusIcon(secondaryColor) != null || !meta.disappearing)) {
res += iconSpace
if (showStatus) {
appendSpace()
if (meta.statusIcon(secondaryColor) != null) {
res += iconSpace
} else if (!meta.disappearing) {
res += iconSpace
}
space = whiteSpace
}
if (encrypted != null) {
appendSpace()
res += iconSpace
space = whiteSpace
}
return res + meta.timestampText
if (showTimestamp) {
appendSpace()
res += meta.timestampText
}
return res
}
@Composable
@@ -154,7 +187,8 @@ fun PreviewCIMetaView() {
1, CIDirection.DirectSnd(), Clock.System.now(), "hello"
),
null,
showViaProxy = false
showViaProxy = false,
showTimestamp = true
)
}
@@ -167,7 +201,8 @@ fun PreviewCIMetaViewUnread() {
status = CIStatus.RcvNew()
),
null,
showViaProxy = false
showViaProxy = false,
showTimestamp = true
)
}
@@ -180,7 +215,8 @@ fun PreviewCIMetaViewSendFailed() {
status = CIStatus.CISSndError(SndError.Other("CMD SYNTAX"))
),
null,
showViaProxy = false
showViaProxy = false,
showTimestamp = true
)
}
@@ -192,7 +228,8 @@ fun PreviewCIMetaViewSendNoAuth() {
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndErrorAuth()
),
null,
showViaProxy = false
showViaProxy = false,
showTimestamp = true
)
}
@@ -204,7 +241,8 @@ fun PreviewCIMetaViewSendSent() {
1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent(SndCIStatusProgress.Complete)
),
null,
showViaProxy = false
showViaProxy = false,
showTimestamp = true
)
}
@@ -217,7 +255,8 @@ fun PreviewCIMetaViewEdited() {
itemEdited = true
),
null,
showViaProxy = false
showViaProxy = false,
showTimestamp = true
)
}
@@ -231,7 +270,8 @@ fun PreviewCIMetaViewEditedUnread() {
status= CIStatus.RcvNew()
),
null,
showViaProxy = false
showViaProxy = false,
showTimestamp = true
)
}
@@ -245,7 +285,8 @@ fun PreviewCIMetaViewEditedSent() {
status= CIStatus.SndSent(SndCIStatusProgress.Complete)
),
null,
showViaProxy = false
showViaProxy = false,
showTimestamp = true
)
}
@@ -255,6 +296,7 @@ fun PreviewCIMetaViewDeletedContent() {
CIMetaView(
chatItem = ChatItem.getDeletedContentSampleData(),
null,
showViaProxy = false
showViaProxy = false,
showTimestamp = true
)
}
@@ -169,14 +169,14 @@ fun DecryptionErrorItemFixButton(
Text(
buildAnnotatedString {
append(generalGetString(MR.strings.fix_connection))
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor)) }
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor, showTimestamp = true)) }
withStyle(reserveTimestampStyle) { append(" ") } // for icon
},
color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
)
}
}
CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false)
CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false, showTimestamp = true)
}
}
}
@@ -201,11 +201,11 @@ fun DecryptionErrorItem(
Text(
buildAnnotatedString {
withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) }
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor)) }
withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor, showTimestamp = true)) }
},
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp)
)
CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false)
CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false, showTimestamp = true)
}
}
}
@@ -38,6 +38,7 @@ fun CIVoiceView(
ci: ChatItem,
timedMessagesTTL: Int?,
showViaProxy: Boolean,
showTimestamp: Boolean,
smallView: Boolean = false,
longClick: () -> Unit,
receiveFile: (Long) -> Unit,
@@ -86,7 +87,7 @@ fun CIVoiceView(
durationText(time / 1000)
}
}
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, sizeMultiplier, play, pause, longClick, receiveFile) {
VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, showTimestamp, sizeMultiplier, play, pause, longClick, receiveFile) {
AudioPlayer.seekTo(it, progress, fileSource.value?.filePath)
}
if (smallView) {
@@ -120,6 +121,7 @@ private fun VoiceLayout(
hasText: Boolean,
timedMessagesTTL: Int?,
showViaProxy: Boolean,
showTimestamp: Boolean,
sizeMultiplier: Float,
play: () -> Unit,
pause: () -> Unit,
@@ -200,7 +202,7 @@ private fun VoiceLayout(
VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, 1f, play, pause, longClick, receiveFile)
}
Box(Modifier.padding(top = 6.sp.toDp() * sizeMultiplier, end = 6.sp.toDp() * sizeMultiplier)) {
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
}
}
}
@@ -215,7 +217,7 @@ private fun VoiceLayout(
}
}
Box(Modifier.padding(top = 6.sp.toDp() * sizeMultiplier)) {
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
}
}
}
@@ -73,6 +73,7 @@ fun ChatItemView(
showItemDetails: (ChatInfo, ChatItem) -> Unit,
developerTools: Boolean,
showViaProxy: Boolean,
showTimestamp: Boolean,
preview: Boolean = false,
) {
val uriHandler = LocalUriHandler.current
@@ -132,7 +133,7 @@ fun ChatItemView(
) {
@Composable
fun framedItemView() {
FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, receiveFile, onLinkLongClick, scrollToItem)
FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, showTimestamp = showTimestamp, receiveFile, onLinkLongClick, scrollToItem)
}
fun deleteMessageQuestionText(): String {
@@ -355,14 +356,14 @@ fun ChatItemView(
fun ContentItem() {
val mc = cItem.content.msgContent
if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) {
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy)
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
MarkedDeletedItemDropdownMenu()
} else {
if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy)
EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile = receiveFile)
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile)
} else {
framedItemView()
}
@@ -374,7 +375,7 @@ fun ChatItemView(
}
@Composable fun LegacyDeletedItem() {
DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy)
DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
DefaultDropdownMenu(showMenu) {
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages)
@@ -386,7 +387,7 @@ fun ChatItemView(
}
@Composable fun CallItem(status: CICallStatus, duration: Int) {
CICallItemView(cInfo, cItem, status, duration, acceptCall, cInfo.timedMessagesTTL)
CICallItemView(cInfo, cItem, status, duration, showTimestamp = showTimestamp, acceptCall, cInfo.timedMessagesTTL)
DeleteItemMenu()
}
@@ -431,7 +432,7 @@ fun ChatItemView(
@Composable
fun DeletedItem() {
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy)
MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
DefaultDropdownMenu(showMenu) {
ItemInfoAction(cInfo, cItem, showItemDetails, showMenu)
DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages)
@@ -474,7 +475,7 @@ fun ChatItemView(
is CIContent.SndCall -> CallItem(c.status, c.duration)
is CIContent.RcvCall -> CallItem(c.status, c.duration)
is CIContent.RcvIntegrityError -> if (developerTools) {
IntegrityErrorItemView(c.msgError, cItem, cInfo.timedMessagesTTL)
IntegrityErrorItemView(c.msgError, cItem, showTimestamp, cInfo.timedMessagesTTL)
DeleteItemMenu()
} else {
Box(Modifier.size(0.dp)) {}
@@ -484,11 +485,11 @@ fun ChatItemView(
DeleteItemMenu()
}
is CIContent.RcvGroupInvitation -> {
CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, timedMessagesTTL = cInfo.timedMessagesTTL)
CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL)
DeleteItemMenu()
}
is CIContent.SndGroupInvitation -> {
CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, timedMessagesTTL = cInfo.timedMessagesTTL)
CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL)
DeleteItemMenu()
}
is CIContent.RcvDirectEventContent -> {
@@ -928,6 +929,7 @@ fun PreviewChatItemView(
showItemDetails = { _, _ -> },
developerTools = false,
showViaProxy = false,
showTimestamp = true,
preview = true,
)
}
@@ -968,6 +970,7 @@ fun PreviewChatItemViewDeletedContent() {
developerTools = false,
showViaProxy = false,
preview = true,
showTimestamp = true
)
}
}
@@ -16,7 +16,7 @@ import chat.simplex.common.model.ChatItem
import chat.simplex.common.ui.theme.*
@Composable
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) {
fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean, showTimestamp: Boolean) {
val sent = ci.chatDir.sent
val sentColor = MaterialTheme.appColors.sentMessage
val receivedColor = MaterialTheme.appColors.receivedMessage
@@ -36,7 +36,7 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean)
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
modifier = Modifier.padding(end = 8.dp)
)
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
}
}
}
@@ -51,7 +51,8 @@ fun PreviewDeletedItemView() {
DeletedItemView(
ChatItem.getDeletedContentSampleData(),
null,
showViaProxy = false
showViaProxy = false,
showTimestamp = true
)
}
}
@@ -12,18 +12,19 @@ import androidx.compose.ui.unit.sp
import chat.simplex.common.model.ChatItem
import chat.simplex.common.model.MREmojiChar
import chat.simplex.common.ui.theme.EmojiFont
import java.sql.Timestamp
val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp, fontFamily = EmojiFont)
val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp, fontFamily = EmojiFont)
@Composable
fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) {
fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean, showTimestamp: Boolean) {
Column(
Modifier.padding(vertical = 8.dp, horizontal = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
EmojiText(chatItem.content.text)
CIMetaView(chatItem, timedMessagesTTL, showViaProxy = showViaProxy)
CIMetaView(chatItem, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
}
}
@@ -35,6 +35,7 @@ fun FramedItemView(
linkMode: SimplexLinkMode,
showViaProxy: Boolean,
showMenu: MutableState<Boolean>,
showTimestamp: Boolean,
receiveFile: (Long) -> Unit,
onLinkLongClick: (link: String) -> Unit = {},
scrollToItem: (Long) -> Unit = {},
@@ -47,7 +48,7 @@ fun FramedItemView(
}
@Composable
fun ciQuotedMsgTextView(qi: CIQuote, lines: Int) {
fun ciQuotedMsgTextView(qi: CIQuote, lines: Int, showTimestamp: Boolean) {
MarkdownText(
qi.text,
qi.formattedText,
@@ -56,7 +57,8 @@ fun FramedItemView(
overflow = TextOverflow.Ellipsis,
style = TextStyle(fontSize = 15.sp, color = MaterialTheme.colors.onSurface),
linkMode = linkMode,
uriHandler = if (appPlatform.isDesktop) uriHandler else null
uriHandler = if (appPlatform.isDesktop) uriHandler else null,
showTimestamp = showTimestamp
)
}
@@ -76,10 +78,10 @@ fun FramedItemView(
style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary),
maxLines = 1
)
ciQuotedMsgTextView(qi, lines = 2)
ciQuotedMsgTextView(qi, lines = 2, showTimestamp = showTimestamp)
}
} else {
ciQuotedMsgTextView(qi, lines = 3)
ciQuotedMsgTextView(qi, lines = 3, showTimestamp = showTimestamp)
}
}
}
@@ -178,7 +180,7 @@ fun FramedItemView(
fun ciFileView(ci: ChatItem, text: String) {
CIFileView(ci.file, ci.meta.itemEdited, showMenu, false, receiveFile)
if (text != "" || ci.meta.isLive) {
CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy)
CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
}
}
@@ -242,7 +244,7 @@ fun FramedItemView(
if (mc.text == "" && !ci.meta.isLive) {
metaColor = Color.White
} else {
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy)
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
}
}
is MsgContent.MCVideo -> {
@@ -250,35 +252,35 @@ fun FramedItemView(
if (mc.text == "" && !ci.meta.isLive) {
metaColor = Color.White
} else {
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy)
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
}
}
is MsgContent.MCVoice -> {
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile = receiveFile)
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile)
if (mc.text != "") {
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy)
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
}
}
is MsgContent.MCFile -> ciFileView(ci, mc.text)
is MsgContent.MCUnknown ->
if (ci.file == null) {
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
} else {
ciFileView(ci, mc.text)
}
is MsgContent.MCLink -> {
ChatItemLinkView(mc.preview, showMenu, onLongClick = { showMenu.value = true })
Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) {
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
}
}
else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy)
else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
}
}
}
}
Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) {
CIMetaView(ci, chatTTL, metaColor, showViaProxy = showViaProxy)
CIMetaView(ci, chatTTL, metaColor, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
}
}
}
@@ -291,14 +293,15 @@ fun CIMarkdownText(
linkMode: SimplexLinkMode,
uriHandler: UriHandler?,
onLinkLongClick: (link: String) -> Unit = {},
showViaProxy: Boolean
showViaProxy: Boolean,
showTimestamp: Boolean,
) {
Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) {
Box(Modifier.padding(vertical = 7.dp, horizontal = 12.dp)) {
val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text
MarkdownText(
text, if (text.isEmpty()) emptyList() else ci.formattedText, toggleSecrets = true,
meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode,
uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy
uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy, showTimestamp = showTimestamp
)
}
}
@@ -23,8 +23,8 @@ import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
@Composable
fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTTL: Int?) {
CIMsgError(ci, timedMessagesTTL) {
fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, showTimestamp: Boolean, timedMessagesTTL: Int?) {
CIMsgError(ci, showTimestamp, timedMessagesTTL) {
when (msgError) {
is MsgErrorType.MsgSkipped ->
AlertManager.shared.showAlertMsg(
@@ -49,7 +49,7 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT
}
@Composable
fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) {
fun CIMsgError(ci: ChatItem, showTimestamp: Boolean, timedMessagesTTL: Int?, onClick: () -> Unit) {
val receivedColor = MaterialTheme.appColors.receivedMessage
Surface(
Modifier.clickable(onClick = onClick),
@@ -68,7 +68,7 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) {
style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp),
modifier = Modifier.padding(end = 8.dp)
)
CIMetaView(ci, timedMessagesTTL, showViaProxy = false)
CIMetaView(ci, timedMessagesTTL, showViaProxy = false, showTimestamp = showTimestamp)
}
}
}
@@ -83,7 +83,8 @@ fun IntegrityErrorItemViewPreview() {
IntegrityErrorItemView(
MsgErrorType.MsgBadHash(),
ChatItem.getDeletedContentSampleData(),
null
showTimestamp = true,
null,
)
}
}
@@ -20,7 +20,7 @@ import dev.icerock.moko.resources.compose.stringResource
import kotlinx.datetime.Clock
@Composable
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState<Boolean>, showViaProxy: Boolean) {
fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState<Boolean>, showViaProxy: Boolean, showTimestamp: Boolean) {
val sentColor = MaterialTheme.appColors.sentMessage
val receivedColor = MaterialTheme.appColors.receivedMessage
Surface(
@@ -35,7 +35,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: Mutabl
Box(Modifier.weight(1f, false)) {
MergedMarkedDeletedText(ci, revealed)
}
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy)
CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
}
}
}
@@ -113,7 +113,8 @@ fun PreviewMarkedDeletedItemView() {
DeletedItemView(
ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted(Clock.System.now())),
null,
showViaProxy = false
showViaProxy = false,
showTimestamp = true
)
}
}
@@ -70,7 +70,8 @@ fun MarkdownText (
linkMode: SimplexLinkMode,
inlineContent: Pair<AnnotatedString.Builder.() -> Unit, Map<String, InlineTextContent>>? = null,
onLinkLongClick: (link: String) -> Unit = {},
showViaProxy: Boolean = false
showViaProxy: Boolean = false,
showTimestamp: Boolean = true
) {
val textLayoutDirection = remember (text) {
if (isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr
@@ -78,7 +79,7 @@ fun MarkdownText (
val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) {
"\n"
} else if (meta != null) {
reserveSpaceForMeta(meta, chatTTL, null, secondaryColor = MaterialTheme.colors.secondary, showViaProxy = showViaProxy)
reserveSpaceForMeta(meta, chatTTL, null, secondaryColor = MaterialTheme.colors.secondary, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
} else {
" "
}
@@ -256,7 +256,7 @@ fun ChatPreviewView(
}
}
is MsgContent.MCVoice -> SmallContentPreviewVoice() {
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = false, ci, cInfo.timedMessagesTTL, showViaProxy = false, smallView = true, longClick = {}) {
CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = false, ci, cInfo.timedMessagesTTL, showViaProxy = false, showTimestamp = true, smallView = true, longClick = {}) {
val user = chatModel.currentUser.value ?: return@CIVoiceView
withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) }
}
@@ -332,7 +332,7 @@ fun ChatPreviewView(
chatPreviewTitle()
}
Spacer(Modifier.width(8.sp.toDp()))
val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.chatTs)
val ts = getTimestampText(chat.chatItems.lastOrNull()?.meta?.itemTs ?: chat.chatInfo.chatTs)
ChatListTimestampView(ts)
}
Row(Modifier.heightIn(min = 46.sp.toDp()).fillMaxWidth()) {
@@ -137,9 +137,10 @@ fun ProfileImageForActiveCall(
size: Dp,
image: String? = null,
color: Color = MaterialTheme.colors.secondaryVariant,
) {
backgroundColor: Color? = null,
) {
if (image == null) {
Box(Modifier.requiredSize(size).clip(CircleShape)) {
Box(Modifier.requiredSize(size).clip(CircleShape).then(if (backgroundColor != null) Modifier.background(backgroundColor) else Modifier)) {
Icon(
AccountCircleFilled,
contentDescription = stringResource(MR.strings.icon_descr_profile_image_placeholder),
@@ -1,7 +1,12 @@
package chat.simplex.common.views.helpers
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.ripple.rememberRipple
import dev.icerock.moko.resources.compose.painterResource
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -9,6 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import chat.simplex.res.MR
@@ -85,10 +91,12 @@ fun <T> ExposedDropDownSettingWithIcon(
values: List<Triple<T, ImageResource, String>>,
selection: State<T>,
fontSize: TextUnit = 16.sp,
iconSize: Dp = 40.dp,
iconPaddingPercent: Float = 0.2f,
listIconSize: Dp = 30.dp,
boxSize: Dp = 60.dp,
iconColor: Color = MenuTextColor,
enabled: State<Boolean> = mutableStateOf(true),
background: Color,
minWidth: Dp = 200.dp,
onSelected: (T) -> Unit
) {
@@ -99,13 +107,21 @@ fun <T> ExposedDropDownSettingWithIcon(
expanded.value = !expanded.value && enabled.value
}
) {
Row(
Modifier.padding(start = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
Box(
Modifier
.background(background, CircleShape)
.size(boxSize)
.clickable(
onClick = {},
role = Role.Button,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = boxSize / 2, color = background.lighter(0.1f)),
enabled = enabled.value
),
contentAlignment = Alignment.Center
) {
val choice = values.first { it.first == selection.value }
Icon(painterResource(choice.second), choice.third, Modifier.size(iconSize), tint = iconColor)
Icon(painterResource(choice.second), choice.third, Modifier.padding(boxSize * iconPaddingPercent).fillMaxSize(), tint = iconColor)
}
DefaultExposedDropdownMenu(
modifier = Modifier.widthIn(min = minWidth),
@@ -1075,6 +1075,7 @@
<string name="icon_descr_audio_on">Audio on</string>
<string name="icon_descr_speaker_off">Speaker off</string>
<string name="icon_descr_speaker_on">Speaker on</string>
<string name="icon_descr_sound_muted">Sound muted</string>
<string name="icon_descr_flip_camera">Flip camera</string>
<!-- Call items -->
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="m809.5-61.5-133-133q-27 19-58.25 33.25T553.5-139.5V-199q21.5-6.5 42.5-14.5t39.5-22L476-396v229L280-363H122.5v-234H274L54.5-816.5 96-858l755 754-41.5 42.5ZM770-291l-41.5-41.5q20-33 29.75-70.67Q768-440.85 768-481q0-100.82-58.75-180.41T553.5-763v-59.5q120 28 196 123.25t76 218.25q0 50.5-14 98.75T770-291ZM642.5-418.5l-89-89v-132q46.5 21.5 73.75 64.75T654.5-480q0 16-3 31.5t-9 30ZM476-585 372-689l104-104v208Zm-57.5 278v-145.5l-87-87H180v119h124.5l114 113.5ZM375-496Z"/></svg>

After

Width:  |  Height:  |  Size: 569 B

@@ -6,6 +6,15 @@
<script src="../lz-string.min.js"></script>
</head>
<body>
<video
id="remote-screen-video-stream"
class="inline"
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="visibility: hidden"
onclick="javascript:toggleRemoteScreenVideoFitFill()"
></video>
<video
id="remote-video-stream"
class="inline"
@@ -14,6 +23,7 @@
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
onclick="javascript:toggleRemoteVideoFitFill()"
></video>
<video
id="local-video-stream"
class="inline"
@@ -22,6 +32,15 @@
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
></video>
<video
id="local-screen-video-stream"
class="inline"
muted
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="visibility: hidden"
></video>
</body>
<footer>
<script src="../call.js"></script>
@@ -12,6 +12,35 @@ body {
object-fit: cover;
}
#remote-video-stream.collapsed {
position: absolute;
max-width: 30%;
max-height: 30%;
object-fit: cover;
margin: 16px;
border-radius: 16px;
bottom: 80px;
right: 0;
}
#remote-video-stream.collapsed-pip {
position: absolute;
max-width: 50%;
max-height: 50%;
object-fit: cover;
margin: 8px;
border-radius: 8px;
bottom: 0;
right: 0;
}
#remote-screen-video-stream.inline {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
}
#local-video-stream.inline {
position: absolute;
width: 30%;
@@ -23,6 +52,17 @@ body {
right: 0;
}
#local-screen-video-stream.inline {
position: absolute;
width: 30%;
max-width: 30%;
object-fit: cover;
margin: 16px;
border-radius: 16px;
top: 30%;
right: 0;
}
#remote-video-stream.fullscreen {
position: absolute;
height: 100%;
@@ -30,6 +70,13 @@ body {
object-fit: cover;
}
#remote-screen-video-stream.fullscreen {
position: absolute;
height: 100%;
width: 100%;
object-fit: cover;
}
#local-video-stream.fullscreen {
position: absolute;
height: 100%;
@@ -37,6 +84,13 @@ body {
object-fit: cover;
}
#local-screen-video-stream.fullscreen {
position: absolute;
height: 100%;
width: 100%;
object-fit: cover;
}
*::-webkit-media-controls {
display: none !important;
-webkit-appearance: none !important;
File diff suppressed because it is too large Load Diff
@@ -7,6 +7,15 @@
<script src="/lz-string.min.js"></script>
</head>
<body>
<video
id="remote-screen-video-stream"
class="inline"
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="visibility: hidden"
onclick="javascript:toggleRemoteScreenVideoFitFill()"
></video>
<video
id="remote-video-stream"
class="inline"
@@ -14,6 +23,7 @@
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
onclick="javascript:toggleRemoteVideoFitFill()"
></video>
<video
id="local-video-stream"
class="inline"
@@ -21,28 +31,39 @@
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
></video>
<video
id="local-screen-video-stream"
class="inline"
muted
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="visibility: hidden"
></video>
<div id="progress"></div>
<div id="info-block">
<p id="state"></p>
<p id="description"></p>
<b id="media-sources" style="color: #fff"></b>
</div>
<div id="audio-call-icon">
<img src="/desktop/images/ic_phone_in_talk.svg" />
</div>
<p id="manage-call">
<button id="toggle-screen" style="display: none" onclick="javascript:toggleScreenManually()">
<button id="toggle-screen" onclick="javascript:toggleScreenManually()">
<img src="/desktop/images/ic_screen_share.svg" />
</button>
<button id="toggle-audio" style="display: none" onclick="javascript:toggleAudioManually()">
<button id="toggle-mic" onclick="javascript:toggleMicManually()">
<img src="/desktop/images/ic_mic.svg" />
</button>
<button id="end-call" onclick="javascript:endCallManually()">
<button id="end-call" style="background: red" onclick="javascript:endCallManually()">
<img src="/desktop/images/ic_call_end_filled.svg" />
</button>
<button id="toggle-speaker" style="display: none" onclick="javascript:toggleSpeakerManually()">
<button id="toggle-speaker" onclick="javascript:toggleSpeakerManually()">
<img src="/desktop/images/ic_volume_up.svg" />
</button>
<button id="toggle-video" style="display: none" onclick="javascript:toggleVideoManually()">
<button id="toggle-camera" onclick="javascript:toggleCameraManually()">
<img src="/desktop/images/ic_videocam_off.svg" />
</button>
</p>
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="red" d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.25 20L821 748q-8 8-22.5 8.75t-23-5.75l-113-84.5q-6-4.5-8.75-10.25T651 643.5v-139q-42-16-85.5-22.5t-85-6.5q-42 0-85.5 6.5t-85.5 22.5v139q0 6.5-2.75 12.5T298 666.5L184.5 751q-11.5 8.5-23.5 7.5T139.5 748L46 654.5q-8.5-8.5-8.25-20t8.25-21q81.5-95 195.25-145.25T480 418Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.25 20L821 748q-8 8-22.5 8.75t-23-5.75l-113-84.5q-6-4.5-8.75-10.25T651 643.5v-139q-42-16-85.5-22.5t-85-6.5q-42 0-85.5 6.5t-85.5 22.5v139q0 6.5-2.75 12.5T298 666.5L184.5 751q-11.5 8.5-23.5 7.5T139.5 748L46 654.5q-8.5-8.5-8.25-20t8.25-21q81.5-95 195.25-145.25T480 418Z"/></svg>

Before

Width:  |  Height:  |  Size: 435 B

After

Width:  |  Height:  |  Size: 437 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 -960 960 960" width="44" fill="white"><path d="m809.5-61.5-133-133q-27 19-58.25 33.25T553.5-139.5V-199q21.5-6.5 42.5-14.5t39.5-22L476-396v229L280-363H122.5v-234H274L54.5-816.5 96-858l755 754-41.5 42.5ZM770-291l-41.5-41.5q20-33 29.75-70.67Q768-440.85 768-481q0-100.82-58.75-180.41T553.5-763v-59.5q120 28 196 123.25t76 218.25q0 50.5-14 98.75T770-291ZM642.5-418.5l-89-89v-132q46.5 21.5 73.75 64.75T654.5-480q0 16-3 31.5t-9 30ZM476-585 372-689l104-104v208Zm-57.5 278v-145.5l-87-87H180v119h124.5l114 113.5ZM375-496Z"/></svg>

After

Width:  |  Height:  |  Size: 582 B

@@ -12,7 +12,37 @@ body {
object-fit: cover;
}
#remote-video-stream.collapsed {
position: absolute;
max-width: 20%;
max-height: 20%;
object-fit: cover;
margin: 16px;
border-radius: 16px;
bottom: 80px;
right: 0;
}
#remote-video-stream.collapsed-pip {
position: absolute;
max-width: 30%;
max-height: 30%;
object-fit: cover;
margin: 8px;
border-radius: 8px;
bottom: 0;
right: 0;
}
#remote-screen-video-stream.inline {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
}
#local-video-stream.inline {
background-color: black;
position: absolute;
width: 20%;
max-width: 20%;
@@ -23,6 +53,17 @@ body {
right: 0;
}
#local-screen-video-stream.inline {
position: absolute;
width: 20%;
max-width: 20%;
object-fit: cover;
margin: 16px;
border-radius: 16px;
top: 33%;
right: 0;
}
#remote-video-stream.fullscreen {
position: absolute;
height: 100%;
@@ -30,6 +71,13 @@ body {
object-fit: cover;
}
#remote-screen-video-stream.fullscreen {
position: absolute;
height: 100%;
width: 100%;
object-fit: cover;
}
#local-video-stream.fullscreen {
position: absolute;
height: 100%;
@@ -37,6 +85,13 @@ body {
object-fit: cover;
}
#local-screen-video-stream.fullscreen {
position: absolute;
height: 100%;
width: 100%;
object-fit: cover;
}
*::-webkit-media-controls {
display: none !important;
-webkit-appearance: none !important;
@@ -57,19 +112,32 @@ body {
#manage-call {
position: absolute;
width: fit-content;
top: 90%;
bottom: 15px;
left: 50%;
transform: translate(-50%, 0);
display: grid;
grid-auto-flow: column;
grid-column-gap: 30px;
grid-column-gap: 38px;
}
#manage-call button {
border: none;
cursor: pointer;
appearance: none;
background-color: inherit;
background-color: #ffffff33;
border-radius: 50%;
padding: 13px;
width: 61px;
height: 61px;
}
#manage-call img {
width: 35px;
height: 35px;
}
#manage-call button .video {
background: #00000033;
}
#progress {
@@ -110,7 +178,6 @@ body {
#info-block {
position: absolute;
color: white;
line-height: 10px;
opacity: 0.8;
width: 200px;
font-family: Arial, Helvetica, sans-serif;
@@ -9,6 +9,7 @@ socket.addEventListener("open", (_event) => {
sendMessageToNative = (msg) => {
console.log("Message to server");
socket.send(JSON.stringify(msg));
reactOnMessageToServer(msg);
};
});
socket.addEventListener("message", (event) => {
@@ -27,71 +28,156 @@ socket.addEventListener("close", (_event) => {
function endCallManually() {
sendMessageToNative({ resp: { type: "end" } });
}
function toggleAudioManually() {
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localMedia) {
document.getElementById("toggle-audio").innerHTML = toggleMedia(activeCall.localStream, CallMediaType.Audio)
? '<img src="/desktop/images/ic_mic.svg" />'
: '<img src="/desktop/images/ic_mic_off.svg" />';
}
function toggleMicManually() {
const enable = activeCall ? !activeCall.localMediaSources.mic : !inactiveCallMediaSources.mic;
const apiCall = {
command: { type: "media", source: CallMediaSource.Mic, enable: enable },
};
processCommand(apiCall);
}
function toggleSpeakerManually() {
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.remoteStream) {
document.getElementById("toggle-speaker").innerHTML = toggleMedia(activeCall.remoteStream, CallMediaType.Audio)
? '<img src="/desktop/images/ic_volume_up.svg" />'
: '<img src="/desktop/images/ic_volume_down.svg" />';
if ((activeCall === null || activeCall === void 0 ? void 0 : activeCall.remoteStream) && activeCall.peerMediaSources.mic) {
enableSpeakerIcon(togglePeerMedia(activeCall.remoteStream, CallMediaType.Audio), !activeCall.peerMediaSources.mic);
}
}
function toggleVideoManually() {
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.localMedia) {
let res;
if (activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled) {
activeCall.cameraEnabled = !activeCall.cameraEnabled;
res = activeCall.cameraEnabled;
}
else {
res = toggleMedia(activeCall.localStream, CallMediaType.Video);
}
document.getElementById("toggle-video").innerHTML = res
? '<img src="/desktop/images/ic_videocam_filled.svg" />'
: '<img src="/desktop/images/ic_videocam_off.svg" />';
}
function toggleCameraManually() {
const enable = activeCall ? !activeCall.localMediaSources.camera : !inactiveCallMediaSources.camera;
const apiCall = {
command: { type: "media", source: CallMediaSource.Camera, enable: enable },
};
processCommand(apiCall);
}
async function toggleScreenManually() {
const was = activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled;
await toggleScreenShare();
if (was != (activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled)) {
document.getElementById("toggle-screen").innerHTML = (activeCall === null || activeCall === void 0 ? void 0 : activeCall.screenShareEnabled)
? '<img src="/desktop/images/ic_stop_screen_share.svg" />'
: '<img src="/desktop/images/ic_screen_share.svg" />';
}
// override function in call.ts to adapt UI to enabled media sources
localOrPeerMediaSourcesChanged = (call) => {
enableMicIcon(call.localMediaSources.mic);
enableCameraIcon(call.localMediaSources.camera);
enableScreenIcon(call.localMediaSources.screenVideo);
const className = localMedia(call) == CallMediaType.Video || peerMedia(call) == CallMediaType.Video ? CallMediaType.Video : CallMediaType.Audio;
document.getElementById("info-block").className = className;
if (call.connection.iceConnectionState == "connected") {
document.getElementById("audio-call-icon").style.display = className == CallMediaType.Audio ? "block" : "none";
}
// document.getElementById("media-sources")!.innerText = mediaSourcesStatus(call)
document.getElementById("manage-call").className = localMedia(call) == CallMediaType.Video ? CallMediaType.Video : "";
};
// override function in call.ts to adapt UI to enabled media sources
inactiveCallMediaSourcesChanged = (inactiveCallMediaSources) => {
const mic = inactiveCallMediaSources.mic;
const camera = inactiveCallMediaSources.camera;
const screenVideo = inactiveCallMediaSources.screenVideo;
enableMicIcon(mic);
enableCameraIcon(camera);
enableScreenIcon(screenVideo);
const className = camera ? CallMediaType.Video : CallMediaType.Audio;
document.getElementById("info-block").className = className;
// document.getElementById("media-sources")!.innerText = inactiveCallMediaSourcesStatus(inactiveCallMediaSources)
};
function enableMicIcon(enabled) {
document.getElementById("toggle-mic").innerHTML = enabled
? '<img src="/desktop/images/ic_mic.svg" />'
: '<img src="/desktop/images/ic_mic_off.svg" />';
}
function enableCameraIcon(enabled) {
document.getElementById("toggle-camera").innerHTML = enabled
? '<img src="/desktop/images/ic_videocam_filled.svg" />'
: '<img src="/desktop/images/ic_videocam_off.svg" />';
}
function enableScreenIcon(enabled) {
document.getElementById("toggle-screen").innerHTML = enabled
? '<img src="/desktop/images/ic_stop_screen_share.svg" />'
: '<img src="/desktop/images/ic_screen_share.svg" />';
}
function enableSpeakerIcon(enabled, muted) {
document.getElementById("toggle-speaker").innerHTML = muted
? '<img src="/desktop/images/ic_volume_off.svg" />'
: enabled
? '<img src="/desktop/images/ic_volume_up.svg" />'
: '<img src="/desktop/images/ic_volume_down.svg" />';
document.getElementById("toggle-speaker").style.opacity = muted ? "0.7" : "1";
}
function mediaSourcesStatus(call) {
let status = "local";
if (call.localMediaSources.mic)
status += " mic";
if (call.localMediaSources.camera)
status += " cam";
if (call.localMediaSources.screenAudio)
status += " scrA";
if (call.localMediaSources.screenVideo)
status += " scrV";
status += " | peer";
if (call.peerMediaSources.mic)
status += " mic";
if (call.peerMediaSources.camera)
status += " cam";
if (call.peerMediaSources.screenAudio)
status += " scrA";
if (call.peerMediaSources.screenVideo)
status += " scrV";
return status;
}
function inactiveCallMediaSourcesStatus(inactiveCallMediaSources) {
let status = "local";
const mic = inactiveCallMediaSources.mic;
const camera = inactiveCallMediaSources.camera;
const screenAudio = inactiveCallMediaSources.screenAudio;
const screenVideo = inactiveCallMediaSources.screenVideo;
if (mic)
status += " mic";
if (camera)
status += " cam";
if (screenAudio)
status += " scrA";
if (screenVideo)
status += " scrV";
return status;
}
function reactOnMessageFromServer(msg) {
var _a;
switch ((_a = msg.command) === null || _a === void 0 ? void 0 : _a.type) {
var _a, _b, _c;
// screen is not allowed to be enabled before connection estabilished
if (((_a = msg.command) === null || _a === void 0 ? void 0 : _a.type) == "capabilities" || ((_b = msg.command) === null || _b === void 0 ? void 0 : _b.type) == "offer") {
document.getElementById("toggle-screen").style.opacity = "0.7";
}
else if (activeCall) {
document.getElementById("toggle-screen").style.opacity = "1";
}
switch ((_c = msg.command) === null || _c === void 0 ? void 0 : _c.type) {
case "capabilities":
document.getElementById("info-block").className = msg.command.media;
break;
case "offer":
case "start":
document.getElementById("toggle-audio").style.display = "inline-block";
document.getElementById("toggle-speaker").style.display = "inline-block";
if (msg.command.media == CallMediaType.Video) {
document.getElementById("toggle-video").style.display = "inline-block";
document.getElementById("toggle-screen").style.display = "inline-block";
}
document.getElementById("info-block").className = msg.command.media;
document.getElementById("toggle-mic").style.display = "inline-block";
document.getElementById("toggle-speaker").style.display = "inline-block";
document.getElementById("toggle-camera").style.display = "inline-block";
document.getElementById("toggle-screen").style.display = "inline-block";
enableSpeakerIcon(true, true);
break;
case "description":
updateCallInfoView(msg.command.state, msg.command.description);
if ((activeCall === null || activeCall === void 0 ? void 0 : activeCall.connection.connectionState) == "connected") {
document.getElementById("progress").style.display = "none";
if (document.getElementById("info-block").className == CallMediaType.Audio) {
document.getElementById("audio-call-icon").style.display = "block";
}
document.getElementById("audio-call-icon").style.display =
document.getElementById("info-block").className == CallMediaType.Audio ? "block" : "none";
}
break;
}
}
function reactOnMessageToServer(msg) {
var _a;
if (!activeCall)
return;
switch ((_a = msg.resp) === null || _a === void 0 ? void 0 : _a.type) {
case "peerMedia":
const className = localMedia(activeCall) == CallMediaType.Video || peerMedia(activeCall) == CallMediaType.Video ? "video" : "audio";
document.getElementById("info-block").className = className;
document.getElementById("audio-call-icon").style.display = className == CallMediaType.Audio ? "block" : "none";
enableSpeakerIcon(activeCall.remoteStream.getAudioTracks().every((elem) => elem.enabled), !activeCall.peerMediaSources.mic);
break;
}
}
function updateCallInfoView(state, description) {
document.getElementById("state").innerText = state;
document.getElementById("description").innerText = description;
@@ -34,14 +34,14 @@ actual fun ActiveCallView() {
val callRh = call.remoteHostId
when (val r = apiMsg.resp) {
is WCallResponse.Capabilities -> withBGApi {
val callType = CallType(call.localMedia, r.capabilities)
val callType = CallType(call.initialCallType, r.capabilities)
chatModel.controller.apiSendCallInvitation(callRh, call.contact, callType)
chatModel.activeCall.value = call.copy(callState = CallState.InvitationSent, localCapabilities = r.capabilities)
CallSoundsPlayer.startConnectingCallSound(scope)
activeCallWaitDeliveryReceipt(scope)
}
is WCallResponse.Offer -> withBGApi {
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.localMedia, r.capabilities)
chatModel.controller.apiSendCallOffer(callRh, call.contact, r.offer, r.iceCandidates, call.initialCallType, r.capabilities)
chatModel.activeCall.value = call.copy(callState = CallState.OfferSent, localCapabilities = r.capabilities)
}
is WCallResponse.Answer -> withBGApi {
@@ -65,6 +65,15 @@ actual fun ActiveCallView() {
is WCallResponse.Connected -> {
chatModel.activeCall.value = call.copy(callState = CallState.Connected, connectionInfo = r.connectionInfo)
}
is WCallResponse.PeerMedia -> {
val sources = call.peerMediaSources
chatModel.activeCall.value = when (r.source) {
CallMediaSource.Mic -> call.copy(peerMediaSources = sources.copy(mic = r.enabled))
CallMediaSource.Camera -> call.copy(peerMediaSources = sources.copy(camera = r.enabled))
CallMediaSource.ScreenAudio -> call.copy(peerMediaSources = sources.copy(screenAudio = r.enabled))
CallMediaSource.ScreenVideo -> call.copy(peerMediaSources = sources.copy(screenVideo = r.enabled))
}
}
is WCallResponse.End -> {
withBGApi { chatModel.callManager.endCall(call) }
}
@@ -77,15 +86,18 @@ actual fun ActiveCallView() {
is WCallCommand.Answer ->
chatModel.activeCall.value = call.copy(callState = CallState.Negotiated)
is WCallCommand.Media -> {
when (cmd.media) {
CallMediaType.Video -> chatModel.activeCall.value = call.copy(videoEnabled = cmd.enable)
CallMediaType.Audio -> chatModel.activeCall.value = call.copy(audioEnabled = cmd.enable)
val sources = call.localMediaSources
when (cmd.source) {
CallMediaSource.Mic -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(mic = cmd.enable))
CallMediaSource.Camera -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(camera = cmd.enable))
CallMediaSource.ScreenAudio -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(screenAudio = cmd.enable))
CallMediaSource.ScreenVideo -> chatModel.activeCall.value = call.copy(localMediaSources = sources.copy(screenVideo = cmd.enable))
}
}
is WCallCommand.Camera -> {
chatModel.activeCall.value = call.copy(localCamera = cmd.camera)
if (!call.audioEnabled) {
chatModel.callCommand.add(WCallCommand.Media(CallMediaType.Audio, enable = false))
if (!call.localMediaSources.mic) {
chatModel.callCommand.add(WCallCommand.Media(CallMediaSource.Mic, enable = false))
}
}
is WCallCommand.End ->
@@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
@Composable
actual fun ActiveCallInteractiveArea(call: Call) {
val showMenu = remember { mutableStateOf(false) }
val media = call.peerMedia ?: call.localMedia
CompositionLocalProvider(
LocalIndication provides NoIndication
) {
@@ -56,7 +55,7 @@ actual fun ActiveCallInteractiveArea(call: Call) {
Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp)
.align(Alignment.TopEnd)
) {
if (media == CallMediaType.Video) {
if (call.hasVideo) {
Icon(
painterResource(MR.images.ic_videocam_filled),
stringResource(MR.strings.icon_descr_video_call),
+4 -4
View File
@@ -26,11 +26,11 @@ android.enableJetifier=true
kotlin.mpp.androidSourceSetLayoutVersion=2
kotlin.jvm.target=11
android.version_name=6.1-beta.1
android.version_code=242
android.version_name=6.1-beta.2
android.version_code=243
desktop.version_name=6.1-beta.1
desktop.version_code=67
desktop.version_name=6.1-beta.2
desktop.version_code=69
kotlin.version=1.9.23
gradle.plugin.version=8.2.0
@@ -6,6 +6,15 @@
<script src="../lz-string.min.js"></script>
</head>
<body>
<video
id="remote-screen-video-stream"
class="inline"
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="visibility: hidden"
onclick="javascript:toggleRemoteScreenVideoFitFill()"
></video>
<video
id="remote-video-stream"
class="inline"
@@ -14,6 +23,7 @@
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
onclick="javascript:toggleRemoteVideoFitFill()"
></video>
<video
id="local-video-stream"
class="inline"
@@ -22,6 +32,15 @@
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
></video>
<video
id="local-screen-video-stream"
class="inline"
muted
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="visibility: hidden"
></video>
</body>
<footer>
<script src="../call.js"></script>
@@ -12,6 +12,35 @@ body {
object-fit: cover;
}
#remote-video-stream.collapsed {
position: absolute;
max-width: 30%;
max-height: 30%;
object-fit: cover;
margin: 16px;
border-radius: 16px;
bottom: 80px;
right: 0;
}
#remote-video-stream.collapsed-pip {
position: absolute;
max-width: 50%;
max-height: 50%;
object-fit: cover;
margin: 8px;
border-radius: 8px;
bottom: 0;
right: 0;
}
#remote-screen-video-stream.inline {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
}
#local-video-stream.inline {
position: absolute;
width: 30%;
@@ -23,6 +52,17 @@ body {
right: 0;
}
#local-screen-video-stream.inline {
position: absolute;
width: 30%;
max-width: 30%;
object-fit: cover;
margin: 16px;
border-radius: 16px;
top: 30%;
right: 0;
}
#remote-video-stream.fullscreen {
position: absolute;
height: 100%;
@@ -30,6 +70,13 @@ body {
object-fit: cover;
}
#remote-screen-video-stream.fullscreen {
position: absolute;
height: 100%;
width: 100%;
object-fit: cover;
}
#local-video-stream.fullscreen {
position: absolute;
height: 100%;
@@ -37,6 +84,13 @@ body {
object-fit: cover;
}
#local-screen-video-stream.fullscreen {
position: absolute;
height: 100%;
width: 100%;
object-fit: cover;
}
*::-webkit-media-controls {
display: none !important;
-webkit-appearance: none !important;
File diff suppressed because it is too large Load Diff
@@ -7,6 +7,15 @@
<script src="/lz-string.min.js"></script>
</head>
<body>
<video
id="remote-screen-video-stream"
class="inline"
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="visibility: hidden"
onclick="javascript:toggleRemoteScreenVideoFitFill()"
></video>
<video
id="remote-video-stream"
class="inline"
@@ -14,6 +23,7 @@
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
onclick="javascript:toggleRemoteVideoFitFill()"
></video>
<video
id="local-video-stream"
class="inline"
@@ -21,28 +31,39 @@
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
></video>
<video
id="local-screen-video-stream"
class="inline"
muted
playsinline
poster="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVORK5CYII="
style="visibility: hidden"
></video>
<div id="progress"></div>
<div id="info-block">
<p id="state"></p>
<p id="description"></p>
<b id="media-sources" style="color: #fff"></b>
</div>
<div id="audio-call-icon">
<img src="/desktop/images/ic_phone_in_talk.svg" />
</div>
<p id="manage-call">
<button id="toggle-screen" style="display: none" onclick="javascript:toggleScreenManually()">
<button id="toggle-screen" onclick="javascript:toggleScreenManually()">
<img src="/desktop/images/ic_screen_share.svg" />
</button>
<button id="toggle-audio" style="display: none" onclick="javascript:toggleAudioManually()">
<button id="toggle-mic" onclick="javascript:toggleMicManually()">
<img src="/desktop/images/ic_mic.svg" />
</button>
<button id="end-call" onclick="javascript:endCallManually()">
<button id="end-call" style="background: red" onclick="javascript:endCallManually()">
<img src="/desktop/images/ic_call_end_filled.svg" />
</button>
<button id="toggle-speaker" style="display: none" onclick="javascript:toggleSpeakerManually()">
<button id="toggle-speaker" onclick="javascript:toggleSpeakerManually()">
<img src="/desktop/images/ic_volume_up.svg" />
</button>
<button id="toggle-video" style="display: none" onclick="javascript:toggleVideoManually()">
<button id="toggle-camera" onclick="javascript:toggleCameraManually()">
<img src="/desktop/images/ic_videocam_off.svg" />
</button>
</p>
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="red" d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.25 20L821 748q-8 8-22.5 8.75t-23-5.75l-113-84.5q-6-4.5-8.75-10.25T651 643.5v-139q-42-16-85.5-22.5t-85-6.5q-42 0-85.5 6.5t-85.5 22.5v139q0 6.5-2.75 12.5T298 666.5L184.5 751q-11.5 8.5-23.5 7.5T139.5 748L46 654.5q-8.5-8.5-8.25-20t8.25-21q81.5-95 195.25-145.25T480 418Z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 96 960 960" width="44"><path fill="white" d="M480 418q125 0 238.75 50.25T914 613.5q8 9.5 8.25 21t-8.25 20L821 748q-8 8-22.5 8.75t-23-5.75l-113-84.5q-6-4.5-8.75-10.25T651 643.5v-139q-42-16-85.5-22.5t-85-6.5q-42 0-85.5 6.5t-85.5 22.5v139q0 6.5-2.75 12.5T298 666.5L184.5 751q-11.5 8.5-23.5 7.5T139.5 748L46 654.5q-8.5-8.5-8.25-20t8.25-21q81.5-95 195.25-145.25T480 418Z"/></svg>

Before

Width:  |  Height:  |  Size: 435 B

After

Width:  |  Height:  |  Size: 437 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="44" viewBox="0 -960 960 960" width="44" fill="white"><path d="m809.5-61.5-133-133q-27 19-58.25 33.25T553.5-139.5V-199q21.5-6.5 42.5-14.5t39.5-22L476-396v229L280-363H122.5v-234H274L54.5-816.5 96-858l755 754-41.5 42.5ZM770-291l-41.5-41.5q20-33 29.75-70.67Q768-440.85 768-481q0-100.82-58.75-180.41T553.5-763v-59.5q120 28 196 123.25t76 218.25q0 50.5-14 98.75T770-291ZM642.5-418.5l-89-89v-132q46.5 21.5 73.75 64.75T654.5-480q0 16-3 31.5t-9 30ZM476-585 372-689l104-104v208Zm-57.5 278v-145.5l-87-87H180v119h124.5l114 113.5ZM375-496Z"/></svg>

After

Width:  |  Height:  |  Size: 582 B

@@ -12,7 +12,37 @@ body {
object-fit: cover;
}
#remote-video-stream.collapsed {
position: absolute;
max-width: 20%;
max-height: 20%;
object-fit: cover;
margin: 16px;
border-radius: 16px;
bottom: 80px;
right: 0;
}
#remote-video-stream.collapsed-pip {
position: absolute;
max-width: 30%;
max-height: 30%;
object-fit: cover;
margin: 8px;
border-radius: 8px;
bottom: 0;
right: 0;
}
#remote-screen-video-stream.inline {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
}
#local-video-stream.inline {
background-color: black;
position: absolute;
width: 20%;
max-width: 20%;
@@ -23,6 +53,17 @@ body {
right: 0;
}
#local-screen-video-stream.inline {
position: absolute;
width: 20%;
max-width: 20%;
object-fit: cover;
margin: 16px;
border-radius: 16px;
top: 33%;
right: 0;
}
#remote-video-stream.fullscreen {
position: absolute;
height: 100%;
@@ -30,6 +71,13 @@ body {
object-fit: cover;
}
#remote-screen-video-stream.fullscreen {
position: absolute;
height: 100%;
width: 100%;
object-fit: cover;
}
#local-video-stream.fullscreen {
position: absolute;
height: 100%;
@@ -37,6 +85,13 @@ body {
object-fit: cover;
}
#local-screen-video-stream.fullscreen {
position: absolute;
height: 100%;
width: 100%;
object-fit: cover;
}
*::-webkit-media-controls {
display: none !important;
-webkit-appearance: none !important;
@@ -57,19 +112,32 @@ body {
#manage-call {
position: absolute;
width: fit-content;
top: 90%;
bottom: 15px;
left: 50%;
transform: translate(-50%, 0);
display: grid;
grid-auto-flow: column;
grid-column-gap: 30px;
grid-column-gap: 38px;
}
#manage-call button {
border: none;
cursor: pointer;
appearance: none;
background-color: inherit;
background-color: #ffffff33;
border-radius: 50%;
padding: 13px;
width: 61px;
height: 61px;
}
#manage-call img {
width: 35px;
height: 35px;
}
#manage-call button .video {
background: #00000033;
}
#progress {
@@ -110,7 +178,6 @@ body {
#info-block {
position: absolute;
color: white;
line-height: 10px;
opacity: 0.8;
width: 200px;
font-family: Arial, Helvetica, sans-serif;
+129 -37
View File
@@ -10,6 +10,7 @@ socket.addEventListener("open", (_event) => {
sendMessageToNative = (msg: WVApiMessage) => {
console.log("Message to server")
socket.send(JSON.stringify(msg))
reactOnMessageToServer(msg)
}
})
@@ -32,74 +33,165 @@ function endCallManually() {
sendMessageToNative({resp: {type: "end"}})
}
function toggleAudioManually() {
if (activeCall?.localMedia) {
document.getElementById("toggle-audio")!!.innerHTML = toggleMedia(activeCall.localStream, CallMediaType.Audio)
? '<img src="/desktop/images/ic_mic.svg" />'
: '<img src="/desktop/images/ic_mic_off.svg" />'
function toggleMicManually() {
const enable = activeCall ? !activeCall.localMediaSources.mic : !inactiveCallMediaSources.mic
const apiCall: WVAPICall = {
command: {type: "media", source: CallMediaSource.Mic, enable: enable},
}
processCommand(apiCall)
}
function toggleSpeakerManually() {
if (activeCall?.remoteStream) {
document.getElementById("toggle-speaker")!!.innerHTML = toggleMedia(activeCall.remoteStream, CallMediaType.Audio)
? '<img src="/desktop/images/ic_volume_up.svg" />'
: '<img src="/desktop/images/ic_volume_down.svg" />'
if (activeCall?.remoteStream && activeCall.peerMediaSources.mic) {
enableSpeakerIcon(togglePeerMedia(activeCall.remoteStream, CallMediaType.Audio), !activeCall.peerMediaSources.mic)
}
}
function toggleVideoManually() {
if (activeCall?.localMedia) {
let res: boolean
if (activeCall?.screenShareEnabled) {
activeCall.cameraEnabled = !activeCall.cameraEnabled
res = activeCall.cameraEnabled
} else {
res = toggleMedia(activeCall.localStream, CallMediaType.Video)
}
document.getElementById("toggle-video")!!.innerHTML = res
? '<img src="/desktop/images/ic_videocam_filled.svg" />'
: '<img src="/desktop/images/ic_videocam_off.svg" />'
function toggleCameraManually() {
const enable = activeCall ? !activeCall.localMediaSources.camera : !inactiveCallMediaSources.camera
const apiCall: WVAPICall = {
command: {type: "media", source: CallMediaSource.Camera, enable: enable},
}
processCommand(apiCall)
}
async function toggleScreenManually() {
const was = activeCall?.screenShareEnabled
await toggleScreenShare()
if (was != activeCall?.screenShareEnabled) {
document.getElementById("toggle-screen")!!.innerHTML = activeCall?.screenShareEnabled
? '<img src="/desktop/images/ic_stop_screen_share.svg" />'
: '<img src="/desktop/images/ic_screen_share.svg" />'
}
// override function in call.ts to adapt UI to enabled media sources
localOrPeerMediaSourcesChanged = (call: Call) => {
enableMicIcon(call.localMediaSources.mic)
enableCameraIcon(call.localMediaSources.camera)
enableScreenIcon(call.localMediaSources.screenVideo)
const className =
localMedia(call) == CallMediaType.Video || peerMedia(call) == CallMediaType.Video ? CallMediaType.Video : CallMediaType.Audio
document.getElementById("info-block")!.className = className
if (call.connection.iceConnectionState == "connected") {
document.getElementById("audio-call-icon")!.style.display = className == CallMediaType.Audio ? "block" : "none"
}
// document.getElementById("media-sources")!.innerText = mediaSourcesStatus(call)
document.getElementById("manage-call")!.className = localMedia(call) == CallMediaType.Video ? CallMediaType.Video : ""
}
// override function in call.ts to adapt UI to enabled media sources
inactiveCallMediaSourcesChanged = (inactiveCallMediaSources: CallMediaSources) => {
const mic = inactiveCallMediaSources.mic
const camera = inactiveCallMediaSources.camera
const screenVideo = inactiveCallMediaSources.screenVideo
enableMicIcon(mic)
enableCameraIcon(camera)
enableScreenIcon(screenVideo)
const className = camera ? CallMediaType.Video : CallMediaType.Audio
document.getElementById("info-block")!.className = className
// document.getElementById("media-sources")!.innerText = inactiveCallMediaSourcesStatus(inactiveCallMediaSources)
}
function enableMicIcon(enabled: boolean) {
document.getElementById("toggle-mic")!.innerHTML = enabled
? '<img src="/desktop/images/ic_mic.svg" />'
: '<img src="/desktop/images/ic_mic_off.svg" />'
}
function enableCameraIcon(enabled: boolean) {
document.getElementById("toggle-camera")!.innerHTML = enabled
? '<img src="/desktop/images/ic_videocam_filled.svg" />'
: '<img src="/desktop/images/ic_videocam_off.svg" />'
}
function enableScreenIcon(enabled: boolean) {
document.getElementById("toggle-screen")!.innerHTML = enabled
? '<img src="/desktop/images/ic_stop_screen_share.svg" />'
: '<img src="/desktop/images/ic_screen_share.svg" />'
}
function enableSpeakerIcon(enabled: boolean, muted: boolean) {
document.getElementById("toggle-speaker")!!.innerHTML = muted
? '<img src="/desktop/images/ic_volume_off.svg" />'
: enabled
? '<img src="/desktop/images/ic_volume_up.svg" />'
: '<img src="/desktop/images/ic_volume_down.svg" />'
document.getElementById("toggle-speaker")!!.style.opacity = muted ? "0.7" : "1"
}
function mediaSourcesStatus(call: Call): string {
let status = "local"
if (call.localMediaSources.mic) status += " mic"
if (call.localMediaSources.camera) status += " cam"
if (call.localMediaSources.screenAudio) status += " scrA"
if (call.localMediaSources.screenVideo) status += " scrV"
status += " | peer"
if (call.peerMediaSources.mic) status += " mic"
if (call.peerMediaSources.camera) status += " cam"
if (call.peerMediaSources.screenAudio) status += " scrA"
if (call.peerMediaSources.screenVideo) status += " scrV"
return status
}
function inactiveCallMediaSourcesStatus(inactiveCallMediaSources: CallMediaSources): string {
let status = "local"
const mic = inactiveCallMediaSources.mic
const camera = inactiveCallMediaSources.camera
const screenAudio = inactiveCallMediaSources.screenAudio
const screenVideo = inactiveCallMediaSources.screenVideo
if (mic) status += " mic"
if (camera) status += " cam"
if (screenAudio) status += " scrA"
if (screenVideo) status += " scrV"
return status
}
function reactOnMessageFromServer(msg: WVApiMessage) {
// screen is not allowed to be enabled before connection estabilished
if (msg.command?.type == "capabilities" || msg.command?.type == "offer") {
document.getElementById("toggle-screen")!!.style.opacity = "0.7"
} else if (activeCall) {
document.getElementById("toggle-screen")!!.style.opacity = "1"
}
switch (msg.command?.type) {
case "capabilities":
document.getElementById("info-block")!!.className = msg.command.media
break
case "offer":
case "start":
document.getElementById("toggle-audio")!!.style.display = "inline-block"
document.getElementById("toggle-speaker")!!.style.display = "inline-block"
if (msg.command.media == CallMediaType.Video) {
document.getElementById("toggle-video")!!.style.display = "inline-block"
document.getElementById("toggle-screen")!!.style.display = "inline-block"
}
document.getElementById("info-block")!!.className = msg.command.media
document.getElementById("toggle-mic")!!.style.display = "inline-block"
document.getElementById("toggle-speaker")!!.style.display = "inline-block"
document.getElementById("toggle-camera")!!.style.display = "inline-block"
document.getElementById("toggle-screen")!!.style.display = "inline-block"
enableSpeakerIcon(true, true)
break
case "description":
updateCallInfoView(msg.command.state, msg.command.description)
if (activeCall?.connection.connectionState == "connected") {
document.getElementById("progress")!.style.display = "none"
if (document.getElementById("info-block")!!.className == CallMediaType.Audio) {
document.getElementById("audio-call-icon")!.style.display = "block"
}
document.getElementById("audio-call-icon")!.style.display =
document.getElementById("info-block")!!.className == CallMediaType.Audio ? "block" : "none"
}
break
}
}
function reactOnMessageToServer(msg: WVApiMessage) {
if (!activeCall) return
switch (msg.resp?.type) {
case "peerMedia":
const className = localMedia(activeCall) == CallMediaType.Video || peerMedia(activeCall) == CallMediaType.Video ? "video" : "audio"
document.getElementById("info-block")!!.className = className
document.getElementById("audio-call-icon")!.style.display = className == CallMediaType.Audio ? "block" : "none"
enableSpeakerIcon(
activeCall.remoteStream.getAudioTracks().every((elem) => elem.enabled),
!activeCall.peerMediaSources.mic
)
break
}
}
function updateCallInfoView(state: string, description: string) {
document.getElementById("state")!!.innerText = state
document.getElementById("description")!!.innerText = description