Compare commits

...

18 Commits

Author SHA1 Message Date
Alexander Bondarenko 701b4186b6 Merge remote-tracking branch 'origin/master' into ios-profiling 2024-03-30 15:47:29 +03:00
Evgeny Poberezkin 9b6ca23dcb ios: fix UI freezes on start and when exiting background (#3972)
* ios: fix UI freezes on start and when exiting background

* remove unused code

* comments
2024-03-29 22:58:36 +00:00
Evgeny Poberezkin 257e03f10a Merge branch 'master' into ios-profiling 2024-03-29 19:44:17 +00:00
Evgeny Poberezkin 1410b4bd85 ios: prevent call to apiContactInfo after the app is suspended 2024-03-29 19:43:16 +00:00
Evgeny Poberezkin 42a35e8c72 ios: prevent call to listUsers after the app is suspended and database is closed 2024-03-29 18:58:55 +00:00
Evgeny Poberezkin 86fe28f1ed core: chat hooks allowing to extend or customize chat core (#3953)
* core: chat hooks allowing to extend or customize chat core

* update

* json

* custom response

* user in db queries
2024-03-29 18:30:17 +00:00
Evgeny Poberezkin 735359c279 android: strings for permissions 2024-03-29 18:28:54 +00:00
Stanislav Dmitrenko 3d20465662 android: more user-friendly way of granting permissions in call (#3945)
* android: more user-friendly way of granting permissions in call

* changes

* ask permissions without click at first

* changes in logic and UI

* enhancements

* show alert only when non in PiP

* change

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
2024-03-29 18:19:36 +00:00
Alexander Bondarenko 7b644c0dcf Merge remote-tracking branch 'origin/master' into ios-profiling 2024-03-28 20:10:35 +02:00
Alexander Bondarenko c3d9d9a7c3 Merge remote-tracking branch 'origin/master' into ios-profiling 2024-03-27 17:40:14 +02:00
Alexander Bondarenko 261767035e Merge remote-tracking branch 'origin/master' into ios-profiling 2024-03-20 11:22:48 +02:00
spaced4ndy c9b00b3054 Merge branch 'master' into ios-profiling 2024-03-13 11:50:17 +04:00
spaced4ndy 1697190189 remove print 2024-03-12 20:52:05 +04:00
spaced4ndy 24609a98c6 fix paths 2024-03-12 20:49:42 +04:00
Alexander Bondarenko 60752feb9c fix addr 2024-03-12 17:09:54 +02:00
Alexander Bondarenko ff5ef638cd add missing include 2024-03-12 17:09:54 +02:00
spaced4ndy df619d540b ios: save debug profile, event log buttons (#3899) 2024-03-12 19:06:25 +04:00
Alexander Bondarenko 53d8a85b8c add RTS tuning to haskell_init 2024-03-12 16:58:44 +02:00
34 changed files with 570 additions and 238 deletions
+20 -42
View File
@@ -21,6 +21,8 @@ public let CURRENT_CHAT_VERSION: Int = 2
// version range that supports establishing direct connection with a group member (xGrpDirectInvVRange in core) // version range that supports establishing direct connection with a group member (xGrpDirectInvVRange in core)
public let CREATE_MEMBER_CONTACT_VRANGE = VersionRange(minVersion: 2, maxVersion: CURRENT_CHAT_VERSION) public let CREATE_MEMBER_CONTACT_VRANGE = VersionRange(minVersion: 2, maxVersion: CURRENT_CHAT_VERSION)
private let networkStatusesLock = DispatchQueue(label: "chat.simplex.app.network-statuses.lock")
enum TerminalItem: Identifiable { enum TerminalItem: Identifiable {
case cmd(Date, ChatCommand) case cmd(Date, ChatCommand)
case resp(Date, ChatResponse) case resp(Date, ChatResponse)
@@ -1566,34 +1568,30 @@ func processReceivedMsg(_ res: ChatResponse) async {
m.removeChat(mergedContact.id) m.removeChat(mergedContact.id)
} }
} }
case let .contactsSubscribed(_, contactRefs):
await updateContactsStatus(contactRefs, status: .connected)
case let .contactsDisconnected(_, contactRefs):
await updateContactsStatus(contactRefs, status: .disconnected)
case let .contactSubSummary(_, contactSubscriptions):
await MainActor.run {
for sub in contactSubscriptions {
// no need to update contact here, and it is slow
// if active(user) {
// m.updateContact(sub.contact)
// }
if let err = sub.contactError {
processContactSubError(sub.contact, err)
} else {
m.setContactNetworkStatus(sub.contact, .connected)
}
}
}
case let .networkStatus(status, connections): case let .networkStatus(status, connections):
await MainActor.run { // dispatch queue to synchronize access
networkStatusesLock.sync {
var ns = m.networkStatuses
// slow loop is on the background thread
for cId in connections { for cId in connections {
m.networkStatuses[cId] = status ns[cId] = status
}
// fast model update is on the main thread
DispatchQueue.main.sync {
m.networkStatuses = ns
} }
} }
case let .networkStatuses(_, statuses): () case let .networkStatuses(_, statuses): ()
await MainActor.run { // dispatch queue to synchronize access
networkStatusesLock.sync {
var ns = m.networkStatuses
// slow loop is on the background thread
for s in statuses { for s in statuses {
m.networkStatuses[s.agentConnId] = s.networkStatus ns[s.agentConnId] = s.networkStatus
}
// fast model update is on the main thread
DispatchQueue.main.sync {
m.networkStatuses = ns
} }
} }
case let .newChatItem(user, aChatItem): case let .newChatItem(user, aChatItem):
@@ -1944,26 +1942,6 @@ func chatItemSimpleUpdate(_ user: any UserLike, _ aChatItem: AChatItem) async {
} }
} }
func updateContactsStatus(_ contactRefs: [ContactRef], status: NetworkStatus) async {
let m = ChatModel.shared
await MainActor.run {
for c in contactRefs {
m.networkStatuses[c.agentConnId] = status
}
}
}
func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
let m = ChatModel.shared
var err: String
switch chatError {
case .errorAgent(agentError: .BROKER(_, .NETWORK)): err = "network"
case .errorAgent(agentError: .SMP(smpErr: .AUTH)): err = "contact deleted"
default: err = String(describing: chatError)
}
m.setContactNetworkStatus(contact, .error(connectionError: err))
}
func refreshCallInvitations() throws { func refreshCallInvitations() throws {
let m = ChatModel.shared let m = ChatModel.shared
let callInvitations = try justRefreshCallInvitations() let callInvitations = try justRefreshCallInvitations()
+5 -1
View File
@@ -22,7 +22,11 @@ struct SimpleXApp: App {
init() { init() {
DispatchQueue.global(qos: .background).sync { DispatchQueue.global(qos: .background).sync {
haskell_init() // we have to use debug profile file name without extension here because .hp extension is added by profiler
haskell_init(
getAppEventLogPath().path,
getAppDebugProfilePrefixPath().path
)
// hs_init(0, nil) // hs_init(0, nil)
} }
UserDefaults.standard.register(defaults: appDefaults) UserDefaults.standard.register(defaults: appDefaults)
+4 -1
View File
@@ -17,6 +17,7 @@ struct ChatView: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@Environment(\.scenePhase) var scenePhase
@State @ObservedObject var chat: Chat @State @ObservedObject var chat: Chat
@State private var showChatInfoSheet: Bool = false @State private var showChatInfoSheet: Bool = false
@State private var showAddMembersSheet: Bool = false @State private var showAddMembersSheet: Bool = false
@@ -234,7 +235,9 @@ struct ChatView: View {
private func initChatView() { private func initChatView() {
let cInfo = chat.chatInfo let cInfo = chat.chatInfo
if case let .direct(contact) = cInfo { // This check prevents the call to apiContactInfo after the app is suspended, and the database is closed.
if case .active = scenePhase,
case let .direct(contact) = cInfo {
Task { Task {
do { do {
let (stats, _) = try await apiContactInfo(chat.chatInfo.apiId) let (stats, _) = try await apiContactInfo(chat.chatInfo.apiId)
@@ -12,6 +12,7 @@ private let fillColorLight = Color(uiColor: UIColor(red: 0.99, green: 0.99, blue
struct UserPicker: View { struct UserPicker: View {
@EnvironmentObject var m: ChatModel @EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@Environment(\.scenePhase) var scenePhase
@Binding var showSettings: Bool @Binding var showSettings: Bool
@Binding var showConnectDesktop: Bool @Binding var showConnectDesktop: Bool
@Binding var userPickerVisible: Bool @Binding var userPickerVisible: Bool
@@ -91,7 +92,10 @@ struct UserPicker: View {
.opacity(userPickerVisible ? 1.0 : 0.0) .opacity(userPickerVisible ? 1.0 : 0.0)
.onAppear { .onAppear {
do { do {
m.users = try listUsers() // This check prevents the call of listUsers after the app is suspended, and the database is closed.
if case .active = scenePhase {
m.users = try listUsers()
}
} catch let error { } catch let error {
logger.error("Error loading users \(responseError(error))") logger.error("Error loading users \(responseError(error))")
} }
@@ -45,6 +45,11 @@ struct DeveloperView: View {
} }
if developerTools { if developerTools {
Section {
exportDebugProfileButton()
exportEventLogButton()
}
Section { Section {
settingsRow("key") { settingsRow("key") {
Toggle("Post-quantum E2EE", isOn: $pqExperimentalEnabled) Toggle("Post-quantum E2EE", isOn: $pqExperimentalEnabled)
@@ -62,6 +67,24 @@ struct DeveloperView: View {
} }
} }
@ViewBuilder private func exportDebugProfileButton() -> some View {
let url = getAppDebugProfilePath()
settingsRow("square.and.arrow.up") {
Button("Export debugging profile") {
showShareSheet(items: [url])
}
}.disabled(!FileManager.default.fileExists(atPath: url.path))
}
@ViewBuilder private func exportEventLogButton() -> some View {
let url = getAppEventLogPath()
settingsRow("square.and.arrow.up") {
Button("Export event log") {
showShareSheet(items: [url])
}
}.disabled(!FileManager.default.fileExists(atPath: url.path))
}
private func setPQExperimentalEnabled(_ enable: Bool) { private func setPQExperimentalEnabled(_ enable: Bool) {
do { do {
try apiSetPQEncryption(enable) try apiSetPQEncryption(enable)
-11
View File
@@ -557,11 +557,6 @@ public enum ChatResponse: Decodable, Error {
case contactRequestRejected(user: UserRef) case contactRequestRejected(user: UserRef)
case contactUpdated(user: UserRef, toContact: Contact) case contactUpdated(user: UserRef, toContact: Contact)
case groupMemberUpdated(user: UserRef, groupInfo: GroupInfo, fromMember: GroupMember, toMember: GroupMember) case groupMemberUpdated(user: UserRef, groupInfo: GroupInfo, fromMember: GroupMember, toMember: GroupMember)
// TODO remove events below
case contactsSubscribed(server: String, contactRefs: [ContactRef])
case contactsDisconnected(server: String, contactRefs: [ContactRef])
case contactSubSummary(user: UserRef, contactSubscriptions: [ContactSubStatus])
// TODO remove events above
case networkStatus(networkStatus: NetworkStatus, connections: [String]) case networkStatus(networkStatus: NetworkStatus, connections: [String])
case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus]) case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus])
case groupSubscribed(user: UserRef, groupInfo: GroupRef) case groupSubscribed(user: UserRef, groupInfo: GroupRef)
@@ -724,9 +719,6 @@ public enum ChatResponse: Decodable, Error {
case .contactRequestRejected: return "contactRequestRejected" case .contactRequestRejected: return "contactRequestRejected"
case .contactUpdated: return "contactUpdated" case .contactUpdated: return "contactUpdated"
case .groupMemberUpdated: return "groupMemberUpdated" case .groupMemberUpdated: return "groupMemberUpdated"
case .contactsSubscribed: return "contactsSubscribed"
case .contactsDisconnected: return "contactsDisconnected"
case .contactSubSummary: return "contactSubSummary"
case .networkStatus: return "networkStatus" case .networkStatus: return "networkStatus"
case .networkStatuses: return "networkStatuses" case .networkStatuses: return "networkStatuses"
case .groupSubscribed: return "groupSubscribed" case .groupSubscribed: return "groupSubscribed"
@@ -885,9 +877,6 @@ public enum ChatResponse: Decodable, Error {
case .contactRequestRejected: return noDetails case .contactRequestRejected: return noDetails
case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact)) case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact))
case let .groupMemberUpdated(u, groupInfo, fromMember, toMember): return withUser(u, "groupInfo: \(groupInfo)\nfromMember: \(fromMember)\ntoMember: \(toMember)") case let .groupMemberUpdated(u, groupInfo, fromMember, toMember): return withUser(u, "groupInfo: \(groupInfo)\nfromMember: \(fromMember)\ntoMember: \(toMember)")
case let .contactsSubscribed(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
case let .contactsDisconnected(server, contactRefs): return "server: \(server)\ncontacts:\n\(String(describing: contactRefs))"
case let .contactSubSummary(u, contactSubscriptions): return withUser(u, String(describing: contactSubscriptions))
case let .networkStatus(status, conns): return "networkStatus: \(String(describing: status))\nconnections: \(String(describing: conns))" case let .networkStatus(status, conns): return "networkStatus: \(String(describing: status))\nconnections: \(String(describing: conns))"
case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses)) case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses))
case let .groupSubscribed(u, groupInfo): return withUser(u, String(describing: groupInfo)) case let .groupSubscribed(u, groupInfo): return withUser(u, String(describing: groupInfo))
+16
View File
@@ -52,6 +52,10 @@ func getAppDirectory() -> URL {
let DB_FILE_PREFIX = "simplex_v1" let DB_FILE_PREFIX = "simplex_v1"
let DEBUG_PROFILE_PREFIX = "simplex_debug"
let DEBUG_PROFILE_EXTENSION = ".hp"
func getLegacyDatabasePath() -> URL { func getLegacyDatabasePath() -> URL {
getDocumentsDirectory().appendingPathComponent("mobile_v1", isDirectory: false) getDocumentsDirectory().appendingPathComponent("mobile_v1", isDirectory: false)
} }
@@ -62,6 +66,18 @@ public func getAppDatabasePath() -> URL {
: getLegacyDatabasePath() : getLegacyDatabasePath()
} }
public func getAppDebugProfilePrefixPath() -> URL {
getAppDirectory().appendingPathComponent(DEBUG_PROFILE_PREFIX, isDirectory: false)
}
public func getAppDebugProfilePath() -> URL {
getAppDirectory().appendingPathComponent(DEBUG_PROFILE_PREFIX + DEBUG_PROFILE_EXTENSION, isDirectory: false)
}
public func getAppEventLogPath() -> URL {
getAppDirectory().appendingPathComponent("simplex.eventlog", isDirectory: false)
}
func fileModificationDate(_ path: String) -> Date? { func fileModificationDate(_ path: String) -> Date? {
do { do {
let attr = try FileManager.default.attributesOfItem(atPath: path) let attr = try FileManager.default.attributesOfItem(atPath: path)
+27 -10
View File
@@ -7,19 +7,36 @@
// //
#include "hs_init.h" #include "hs_init.h"
#include <string.h>
extern void hs_init_with_rtsopts(int * argc, char **argv[]); extern void hs_init_with_rtsopts(int * argc, char **argv[]);
void haskell_init(void) { void haskell_init(const char *eventlog, const char *heap_profile) {
int argc = 5; // setup static arena for bump allocation and passing to RTS
char *argv[] = { char *argv[32] = {0,};
"simplex", int argc = 0; // number of arguments used so far, always stands at the first NULL in argv
"+RTS", // requires `hs_init_with_rtsopts` // common args
"-A64m", // chunk size for new allocations argv[argc++] = "simplex"; // fake program name
"-H64m", // initial heap size argv[argc++] = "+RTS"; // start adding RTS options
"-xn", // non-moving GC argv[argc++] = "-T"; // make GC counters available from inside the program
0 argv[argc++] = "-A64m"; // chunk size for new allocations (less frequent GC)
}; argv[argc++] = "-H64m"; // larger heap size on start (faster boot)
// argv[argc++] = "-M8G"; // keep memory usage under 8G, collecting more aggressively when approaching it (and crashing sooner rather than taking down the whole system)
if (eventlog) {
static char ol[1024] = "-ol";
(void)strncpy(&ol[3], eventlog, sizeof(ol) - 3);
argv[argc++] = ol;
argv[argc++] = "-l-agu"; // collect GC and user events
}
if (heap_profile) {
static char po[1024] = "-po";
(void)strncpy(&po[3], heap_profile, sizeof(po) - 3);
argv[argc++] = po; // adds ".hp" extension
argv[argc++] = "-hT"; // emit heap profile by closure type
}
int non_moving_gc = !heap_profile; // not compatible with heap profile
if (non_moving_gc) argv[argc++] = "-xn";
// wrap args as expected by RTS
char **pargv = argv; char **pargv = argv;
hs_init_with_rtsopts(&argc, &pargv); hs_init_with_rtsopts(&argc, &pargv);
} }
+1 -1
View File
@@ -9,7 +9,7 @@
#ifndef hs_init_h #ifndef hs_init_h
#define hs_init_h #define hs_init_h
void haskell_init(void); void haskell_init(const char *eventlog, const char *heap_profile);
void haskell_init_nse(void); void haskell_init_nse(void);
@@ -143,6 +143,9 @@ dependencies {
implementation("com.jakewharton:process-phoenix:2.2.0") implementation("com.jakewharton:process-phoenix:2.2.0")
//Camera Permission
implementation("com.google.accompanist:accompanist-permissions:0.23.0")
//implementation("androidx.compose.material:material-icons-extended:$compose_version") //implementation("androidx.compose.material:material-icons-extended:$compose_version")
//implementation("androidx.compose.ui:ui-util:$compose_version") //implementation("androidx.compose.ui:ui-util:$compose_version")
@@ -1,7 +1,9 @@
package chat.simplex.app.views.call package chat.simplex.app.views.call
import android.Manifest
import android.app.* import android.app.*
import android.content.* import android.content.*
import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Rect import android.graphics.Rect
import android.os.* import android.os.*
@@ -28,6 +30,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import chat.simplex.app.* import chat.simplex.app.*
import chat.simplex.app.R import chat.simplex.app.R
@@ -36,10 +39,12 @@ import chat.simplex.app.model.NtfManager
import chat.simplex.app.model.NtfManager.AcceptCallAction import chat.simplex.app.model.NtfManager.AcceptCallAction
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.platform.chatModel
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.* import chat.simplex.common.views.call.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
@@ -109,9 +114,20 @@ class CallActivity: ComponentActivity(), ServiceConnection {
m.callCommand.add(WCallCommand.Layout(layoutType)) m.callCommand.add(WCallCommand.Layout(layoutType))
} }
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
return grantedAudio && grantedCamera
}
override fun onBackPressed() { override fun onBackPressed() {
if (isOnLockScreenNow()) { if (isOnLockScreenNow()) {
super.onBackPressed() super.onBackPressed()
} else if (!hasGrantedPermissions() && !callSupportsVideo()) {
val call = m.activeCall.value
if (call != null) {
withBGApi { chatModel.callManager.endCall(call) }
}
} else { } else {
m.activeCallViewIsCollapsed.value = true m.activeCallViewIsCollapsed.value = true
} }
@@ -223,8 +239,21 @@ fun CallActivityView() {
} }
Box(Modifier.background(Color.Black)) { Box(Modifier.background(Color.Black)) {
if (call != null) { if (call != null) {
val permissionsState = rememberMultiplePermissionsState(
permissions = if (callSupportsVideo()) {
listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
} else {
listOf(Manifest.permission.RECORD_AUDIO)
}
)
if (permissionsState.allPermissionsGranted) {
ActiveCallView()
} else {
CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callSupportsVideo()) {
withBGApi { chatModel.callManager.endCall(call) }
}
}
val view = LocalView.current val view = LocalView.current
ActiveCallView()
if (callSupportsVideo()) { if (callSupportsVideo()) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@@ -252,6 +281,9 @@ fun CallActivityView() {
} }
} }
} }
if (!m.activeCallViewIsCollapsed.value) {
AlertManager.shared.showInView()
}
} }
LaunchedEffect(call == null) { LaunchedEffect(call == null) {
if (call != null) { if (call != null) {
@@ -90,6 +90,9 @@ kotlin {
implementation("androidx.camera:camera-camera2:${cameraXVersion}") implementation("androidx.camera:camera-camera2:${cameraXVersion}")
implementation("androidx.camera:camera-lifecycle:${cameraXVersion}") implementation("androidx.camera:camera-lifecycle:${cameraXVersion}")
implementation("androidx.camera:camera-view:${cameraXVersion}") implementation("androidx.camera:camera-view:${cameraXVersion}")
// Calls lifecycle listener
implementation("androidx.lifecycle:lifecycle-process:2.4.1")
} }
} }
val desktopMain by getting { val desktopMain by getting {
@@ -0,0 +1,30 @@
package chat.simplex.common.helpers
import android.content.*
import android.net.Uri
import android.provider.Settings
import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.AlertManager
import chat.simplex.common.views.helpers.generalGetString
import chat.simplex.res.MR
fun Context.openAppSettingsInSystem() {
Intent().apply {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
data = Uri.parse("package:${androidAppContext.packageName}")
try {
startActivity(this)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, e.stackTraceToString())
}
}
}
fun Context.showAllowPermissionInSettingsAlert(action: () -> Unit = ::openAppSettingsInSystem) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.permissions_grant_in_settings),
text = generalGetString(MR.strings.permissions_find_in_settings_and_grant),
confirmText = generalGetString(MR.strings.permissions_open_settings),
onConfirm = action,
)
}
@@ -1,5 +1,7 @@
package chat.simplex.common.views.call package chat.simplex.common.views.call
import SectionSpacer
import SectionView
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
@@ -24,17 +26,18 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.*
import androidx.lifecycle.LifecycleEventObserver
import androidx.webkit.WebViewAssetLoader import androidx.webkit.WebViewAssetLoader
import androidx.webkit.WebViewClientCompat import androidx.webkit.WebViewClientCompat
import chat.simplex.common.helpers.showAllowPermissionInSettingsAlert
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.model.ChatModel import chat.simplex.common.model.ChatModel
@@ -42,13 +45,12 @@ import chat.simplex.common.model.Contact
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.*
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
// Should be destroy()'ed and set as null when call is ended. Otherwise, it will be a leak // Should be destroy()'ed and set as null when call is ended. Otherwise, it will be a leak
@@ -209,7 +211,6 @@ actual fun ActiveCallView() {
ActiveCallOverlay(call, chatModel, audioViaBluetooth) ActiveCallOverlay(call, chatModel, audioViaBluetooth)
} }
} }
val context = LocalContext.current val context = LocalContext.current
DisposableEffect(Unit) { DisposableEffect(Unit) {
val activity = context as? Activity ?: return@DisposableEffect onDispose {} val activity = context as? Activity ?: return@DisposableEffect onDispose {}
@@ -510,34 +511,138 @@ private fun DisabledBackgroundCallsButton() {
// } // }
//} //}
@Composable
fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Unit) {
val audioPermission = rememberPermissionState(Manifest.permission.RECORD_AUDIO)
val cameraPermission = rememberPermissionState(Manifest.permission.CAMERA)
val permissionsState = rememberMultiplePermissionsState(
permissions = if (hasVideo) {
listOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
} else {
listOf(Manifest.permission.RECORD_AUDIO)
}
)
val context = LocalContext.current
val buttonEnabled = remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
if (!pipActive) {
permissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled, context::showAllowPermissionInSettingsAlert)
}
}
if (pipActive) {
Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceEvenly) {
if (audioPermission.status is PermissionStatus.Denied) {
Icon(
painterResource(MR.images.ic_call_500),
stringResource(MR.strings.permissions_record_audio),
Modifier.size(24.dp),
tint = Color(0xFFFFFFD8)
)
}
if (hasVideo && cameraPermission.status is PermissionStatus.Denied) {
Icon(
painterResource(MR.images.ic_videocam),
stringResource(MR.strings.permissions_camera),
Modifier.size(24.dp),
tint = Color(0xFFFFFFD8)
)
}
}
} else {
ColumnWithScrollBar(Modifier.fillMaxSize()) {
Spacer(Modifier.height(AppBarHeight))
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)
}
}
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))
}
}
}
}
}
@Composable
private fun GrantPermissionButton(text: String, enabled: Boolean, onClick: () -> Unit) {
Row(
Modifier
.clickable(enabled = enabled, onClick = onClick)
.heightIn(min = 30.dp)
.background(WarningOrange.copy(0.3f), RoundedCornerShape(50)),
verticalAlignment = Alignment.CenterVertically
) {
Text(text, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), fontSize = 20.sp, color = WarningOrange)
}
}
/**
* The idea of this function is to ask system to show permission dialog and to see if it's really doing it.
* Otherwise, show alert with a button that opens settings for manual permission granting
* */
private fun MultiplePermissionsState.launchMultiplePermissionRequestWithFallback(buttonEnabled: MutableState<Boolean>, fallback: () -> Unit) {
buttonEnabled.value = false
val lifecycleOwner = ProcessLifecycleOwner.get().lifecycle
var useFallback = true
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_PAUSE) {
useFallback = false
buttonEnabled.value = true
}
}
lifecycleOwner.addObserver(observer)
withBGApi {
delay(2000)
if (useFallback && chatModel.activeCall.value != null) {
fallback()
}
buttonEnabled.value = true
}.invokeOnCompletion {
// Main thread only
withApi {
lifecycleOwner.removeObserver(observer)
}
}
launchMultiplePermissionRequest()
}
@Composable @Composable
fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) { fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIMessage) -> Unit) {
val webView = remember { mutableStateOf<WebView?>(null) } val webView = remember { mutableStateOf<WebView?>(null) }
val permissionsState = rememberMultiplePermissionsState(
permissions = listOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.MODIFY_AUDIO_SETTINGS,
Manifest.permission.INTERNET
)
)
fun processCommand(wv: WebView, cmd: WCallCommand) { fun processCommand(wv: WebView, cmd: WCallCommand) {
val apiCall = WVAPICall(command = cmd) val apiCall = WVAPICall(command = cmd)
wv.evaluateJavascript("processCommand(${json.encodeToString(apiCall)})", null) wv.evaluateJavascript("processCommand(${json.encodeToString(apiCall)})", null)
} }
val lifecycleOwner = LocalLifecycleOwner.current DisposableEffect(Unit) {
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME || event == Lifecycle.Event.ON_START) {
permissionsState.launchMultiplePermissionRequest()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { onDispose {
lifecycleOwner.lifecycle.removeObserver(observer) // val wv = webView.value
// val wv = webView.value // if (wv != null) processCommand(wv, WCallCommand.End)
// if (wv != null) processCommand(wv, WCallCommand.End) // webView.value?.destroy()
// webView.value?.destroy()
webView.value = null webView.value = null
} }
} }
@@ -560,44 +665,42 @@ fun WebRTCView(callCommand: SnapshotStateList<WCallCommand>, onResponse: (WVAPIM
.addPathHandler("/assets/www/", WebViewAssetLoader.AssetsPathHandler(LocalContext.current)) .addPathHandler("/assets/www/", WebViewAssetLoader.AssetsPathHandler(LocalContext.current))
.build() .build()
if (permissionsState.allPermissionsGranted) { Box(Modifier.fillMaxSize()) {
Box(Modifier.fillMaxSize()) { AndroidView(
AndroidView( factory = { AndroidViewContext ->
factory = { AndroidViewContext -> (staticWebView ?: WebView(androidAppContext)).apply {
(staticWebView ?: WebView(androidAppContext)).apply { layoutParams = ViewGroup.LayoutParams(
layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT, )
) this.webChromeClient = object: WebChromeClient() {
this.webChromeClient = object: WebChromeClient() { override fun onPermissionRequest(request: PermissionRequest) {
override fun onPermissionRequest(request: PermissionRequest) { if (request.origin.toString().startsWith("file:/")) {
if (request.origin.toString().startsWith("file:/")) { request.grant(request.resources)
request.grant(request.resources) } else {
} else { Log.d(TAG, "Permission request from webview denied.")
Log.d(TAG, "Permission request from webview denied.") request.deny()
request.deny()
}
} }
} }
this.webViewClient = LocalContentWebViewClient(webView, assetLoader) }
this.clearHistory() this.webViewClient = LocalContentWebViewClient(webView, assetLoader)
this.clearCache(true) this.clearHistory()
this.addJavascriptInterface(WebRTCInterface(onResponse), "WebRTCInterface") this.clearCache(true)
val webViewSettings = this.settings this.addJavascriptInterface(WebRTCInterface(onResponse), "WebRTCInterface")
webViewSettings.allowFileAccess = true val webViewSettings = this.settings
webViewSettings.allowContentAccess = true webViewSettings.allowFileAccess = true
webViewSettings.javaScriptEnabled = true webViewSettings.allowContentAccess = true
webViewSettings.mediaPlaybackRequiresUserGesture = false webViewSettings.javaScriptEnabled = true
webViewSettings.cacheMode = WebSettings.LOAD_NO_CACHE webViewSettings.mediaPlaybackRequiresUserGesture = false
if (staticWebView == null) { webViewSettings.cacheMode = WebSettings.LOAD_NO_CACHE
this.loadUrl("file:android_asset/www/android/call.html") if (staticWebView == null) {
} else { this.loadUrl("file:android_asset/www/android/call.html")
webView.value = this } else {
} webView.value = this
} }
} }
) { /* WebView */ } }
} ) { /* WebView */ }
} }
} }
@@ -1753,6 +1753,8 @@ object ChatController {
chatModel.removeChat(rhId, r.mergedContact.id) chatModel.removeChat(rhId, r.mergedContact.id)
} }
} }
// ContactsSubscribed, ContactsDisconnected and ContactSubSummary are only used in CLI,
// They have to be used here for remote desktop to process these status updates.
is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, NetworkStatus.Connected()) is CR.ContactsSubscribed -> updateContactsStatus(r.contactRefs, NetworkStatus.Connected())
is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, NetworkStatus.Disconnected()) is CR.ContactsDisconnected -> updateContactsStatus(r.contactRefs, NetworkStatus.Disconnected())
is CR.ContactSubSummary -> { is CR.ContactSubSummary -> {
@@ -190,6 +190,7 @@ class AlertManager {
fun showAlertMsg( fun showAlertMsg(
title: String, text: String? = null, title: String, text: String? = null,
confirmText: String = generalGetString(MR.strings.ok), confirmText: String = generalGetString(MR.strings.ok),
onConfirm: (() -> Unit)? = null,
hostDevice: Pair<Long?, String>? = null, hostDevice: Pair<Long?, String>? = null,
shareText: Boolean? = null shareText: Boolean? = null
) { ) {
@@ -220,6 +221,7 @@ class AlertManager {
} }
TextButton( TextButton(
onClick = { onClick = {
onConfirm?.invoke()
hideAlert() hideAlert()
}, },
Modifier.focusRequester(focusRequester) Modifier.focusRequester(focusRequester)
@@ -257,8 +259,9 @@ class AlertManager {
title: StringResource, title: StringResource,
text: StringResource? = null, text: StringResource? = null,
confirmText: StringResource = MR.strings.ok, confirmText: StringResource = MR.strings.ok,
onConfirm: (() -> Unit)? = null,
hostDevice: Pair<Long?, String>? = null, hostDevice: Pair<Long?, String>? = null,
) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), hostDevice) ) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), onConfirm, hostDevice)
@Composable @Composable
fun showInView() { fun showInView() {
@@ -814,6 +814,14 @@
<!-- CallView --> <!-- CallView -->
<string name="unable_to_open_browser_title">Error opening browser</string> <string name="unable_to_open_browser_title">Error opening browser</string>
<string name="unable_to_open_browser_desc">The default web browser is required for calls. Please configure the default browser in the system, and share more information with the developers.</string> <string name="unable_to_open_browser_desc">The default web browser is required for calls. Please configure the default browser in the system, and share more information with the developers.</string>
<string name="permissions_required">Grant permissions</string>
<string name="permissions_record_audio">Microphone</string>
<string name="permissions_camera">Camera</string>
<string name="permissions_camera_and_record_audio">Camera and microphone</string>
<string name="permissions_grant">Grant permission(s) to make calls</string>
<string name="permissions_grant_in_settings">Grant in settings</string>
<string name="permissions_find_in_settings_and_grant">Find this permission in Android settings and grant it manually.</string>
<string name="permissions_open_settings">Open settings</string>
<!-- SimpleXInfo --> <!-- SimpleXInfo -->
<string name="next_generation_of_private_messaging">The next generation of private messaging</string> <string name="next_generation_of_private_messaging">The next generation of private messaging</string>
+4 -57
View File
@@ -1,61 +1,8 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
module Main where module Main where
import Control.Concurrent (forkIO, threadDelay) import Server (simplexChatServer)
import Control.Concurrent.STM import Simplex.Chat.Terminal (terminalChatConfig)
import Control.Monad import Simplex.Chat.Terminal.Main (simplexChatCLI)
import Data.Time.Clock (getCurrentTime)
import Data.Time.LocalTime (getCurrentTimeZone)
import Server
import Simplex.Chat.Controller (ChatController (..), ChatResponse (..), currentRemoteHost, versionNumber, versionString)
import Simplex.Chat.Core
import Simplex.Chat.Options
import Simplex.Chat.Terminal
import Simplex.Chat.View (serializeChatResponse)
import Simplex.Messaging.Client (NetworkConfig (..))
import System.Directory (getAppUserDataDirectory)
import System.Terminal (withTerminal)
main :: IO () main :: IO ()
main = do main = simplexChatCLI terminalChatConfig (Just simplexChatServer)
appDir <- getAppUserDataDirectory "simplex"
opts@ChatOpts {chatCmd, chatServerPort} <- getChatOpts appDir "simplex_v1"
if null chatCmd
then case chatServerPort of
Just chatPort -> simplexChatServer defaultChatServerConfig {chatPort} terminalChatConfig opts
_ -> runCLI opts
else simplexChatCore terminalChatConfig opts $ runCommand opts
where
runCLI opts = do
welcome opts
t <- withTerminal pure
simplexChatTerminal terminalChatConfig opts t
runCommand ChatOpts {chatCmd, chatCmdLog, chatCmdDelay} user cc = do
when (chatCmdLog /= CCLNone) . void . forkIO . forever $ do
(_, _, r') <- atomically . readTBQueue $ outputQ cc
case r' of
CRNewChatItem {} -> printResponse r'
_ -> when (chatCmdLog == CCLAll) $ printResponse r'
sendChatCmdStr cc chatCmd >>= printResponse
threadDelay $ chatCmdDelay * 1000000
where
printResponse r = do
ts <- getCurrentTime
tz <- getCurrentTimeZone
rh <- readTVarIO $ currentRemoteHost cc
putStrLn $ serializeChatResponse (rh, Just user) ts tz rh r
welcome :: ChatOpts -> IO ()
welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, networkConfig}} =
mapM_
putStrLn
[ versionString versionNumber,
"db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db",
maybe
"direct network connection - use `/network` command or `-x` CLI option to connect via SOCKS5 at :9050"
(("using SOCKS5 proxy " <>) . show)
(socksProxy networkConfig),
"type \"/help\" or \"/h\" for usage info"
]
+3 -3
View File
@@ -28,9 +28,9 @@ import Simplex.Messaging.Util (raceAny_)
import UnliftIO.Exception import UnliftIO.Exception
import UnliftIO.STM import UnliftIO.STM
simplexChatServer :: ChatServerConfig -> ChatConfig -> ChatOpts -> IO () simplexChatServer :: ServiceName -> ChatConfig -> ChatOpts -> IO ()
simplexChatServer srvCfg cfg opts = simplexChatServer chatPort cfg opts =
simplexChatCore cfg opts . const $ runChatServer srvCfg simplexChatCore cfg opts . const $ runChatServer defaultChatServerConfig {chatPort}
data ChatServerConfig = ChatServerConfig data ChatServerConfig = ChatServerConfig
{ chatPort :: ServiceName, { chatPort :: ServiceName,
@@ -70,7 +70,7 @@ crDirectoryEvent = \case
CRChatItemDeleted {deletedChatItem = AChatItem _ SMDRcv (DirectChat ct) _, byUser = False} -> Just $ DEItemDeleteIgnored ct CRChatItemDeleted {deletedChatItem = AChatItem _ SMDRcv (DirectChat ct) _, byUser = False} -> Just $ DEItemDeleteIgnored ct
CRNewChatItem {chatItem = AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc, meta = CIMeta {itemLive}}} -> CRNewChatItem {chatItem = AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc, meta = CIMeta {itemLive}}} ->
Just $ case (mc, itemLive) of Just $ case (mc, itemLive) of
(MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly directoryCmdP $ T.dropWhileEnd isSpace t (MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly (directoryCmdP <* A.endOfInput) $ T.dropWhileEnd isSpace t
_ -> DEUnsupportedMessage ct ciId _ -> DEUnsupportedMessage ct ciId
where where
ciId = chatItemId' ci ciId = chatItemId' ci
+2
View File
@@ -139,6 +139,7 @@ library
Simplex.Chat.Migrations.M20240226_users_restrict Simplex.Chat.Migrations.M20240226_users_restrict
Simplex.Chat.Migrations.M20240228_pq Simplex.Chat.Migrations.M20240228_pq
Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id
Simplex.Chat.Migrations.M20240324_custom_data
Simplex.Chat.Mobile Simplex.Chat.Mobile
Simplex.Chat.Mobile.File Simplex.Chat.Mobile.File
Simplex.Chat.Mobile.Shared Simplex.Chat.Mobile.Shared
@@ -168,6 +169,7 @@ library
Simplex.Chat.Styled Simplex.Chat.Styled
Simplex.Chat.Terminal Simplex.Chat.Terminal
Simplex.Chat.Terminal.Input Simplex.Chat.Terminal.Input
Simplex.Chat.Terminal.Main
Simplex.Chat.Terminal.Notification Simplex.Chat.Terminal.Notification
Simplex.Chat.Terminal.Output Simplex.Chat.Terminal.Output
Simplex.Chat.Types Simplex.Chat.Types
+20 -10
View File
@@ -161,7 +161,8 @@ defaultChatConfig =
ciExpirationInterval = 30 * 60 * 1000000, -- 30 minutes ciExpirationInterval = 30 * 60 * 1000000, -- 30 minutes
coreApi = False, coreApi = False,
highlyAvailable = False, highlyAvailable = False,
deviceNameForRemote = "" deviceNameForRemote = "",
chatHooks = defaultChatHooks
} }
_defaultSMPServers :: NonEmpty SMPServerWithAuth _defaultSMPServers :: NonEmpty SMPServerWithAuth
@@ -424,7 +425,9 @@ execChatCommand rh s = do
Just rhId Just rhId
| allowRemoteCommand cmd -> execRemoteCommand u rhId cmd s | allowRemoteCommand cmd -> execRemoteCommand u rhId cmd s
| otherwise -> pure $ CRChatCmdError u $ ChatErrorRemoteHost (RHId rhId) $ RHELocalCommand | otherwise -> pure $ CRChatCmdError u $ ChatErrorRemoteHost (RHId rhId) $ RHELocalCommand
_ -> execChatCommand_ u cmd _ -> do
cc@ChatController {config = ChatConfig {chatHooks}} <- ask
liftIO (preCmdHook chatHooks cc cmd) >>= either pure (execChatCommand_ u)
execChatCommand' :: ChatMonad' m => ChatCommand -> m ChatResponse execChatCommand' :: ChatMonad' m => ChatCommand -> m ChatResponse
execChatCommand' cmd = asks currentUser >>= readTVarIO >>= (`execChatCommand_` cmd) execChatCommand' cmd = asks currentUser >>= readTVarIO >>= (`execChatCommand_` cmd)
@@ -2094,6 +2097,9 @@ processChatCommand' vr = \case
SubInfo {server, subError = Just e} -> M.alter (Just . maybe [e] (e :)) server m SubInfo {server, subError = Just e} -> M.alter (Just . maybe [e] (e :)) server m
_ -> m _ -> m
GetAgentSubsDetails -> CRAgentSubsDetails <$> withAgent getAgentSubscriptions GetAgentSubsDetails -> CRAgentSubsDetails <$> withAgent getAgentSubscriptions
-- CustomChatCommand is unsupported, it can be processed in preCmdHook
-- in a modified CLI app or core - the hook should return Either ChatResponse ChatCommand
CustomChatCommand _cmd -> withUser $ \user -> pure $ chatCmdError (Just user) "not supported"
where where
withChatLock name action = asks chatLock >>= \l -> withLock l name action withChatLock name action = asks chatLock >>= \l -> withLock l name action
-- below code would make command responses asynchronous where they can be slow -- below code would make command responses asynchronous where they can be slow
@@ -4553,25 +4559,28 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
when (sz > fileSize) $ receiveFile' user ft Nothing Nothing >>= toView when (sz > fileSize) $ receiveFile' user ft Nothing Nothing >>= toView
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> m () messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> m ()
messageFileDescription Contact {contactId} sharedMsgId fileDescr = do messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr = do
fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId fileId <- withStore $ \db -> getFileIdBySharedMsgId db userId contactId sharedMsgId
processFDMessage fileId fileDescr processFDMessage (CDDirectRcv ct) sharedMsgId fileId fileDescr
groupMessageFileDescription :: GroupInfo -> GroupMember -> SharedMsgId -> FileDescr -> m () groupMessageFileDescription :: GroupInfo -> GroupMember -> SharedMsgId -> FileDescr -> m ()
groupMessageFileDescription GroupInfo {groupId} _m sharedMsgId fileDescr = do groupMessageFileDescription g@GroupInfo {groupId} m sharedMsgId fileDescr = do
fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId fileId <- withStore $ \db -> getGroupFileIdBySharedMsgId db userId groupId sharedMsgId
processFDMessage fileId fileDescr processFDMessage (CDGroupRcv g m) sharedMsgId fileId fileDescr
processFDMessage :: FileTransferId -> FileDescr -> m () processFDMessage :: ChatTypeQuotable c => ChatDirection c 'MDRcv -> SharedMsgId -> FileTransferId -> FileDescr -> m ()
processFDMessage fileId fileDescr = do processFDMessage cd sharedMsgId fileId fileDescr = do
ft <- withStore $ \db -> getRcvFileTransfer db user fileId ft <- withStore $ \db -> getRcvFileTransfer db user fileId
unless (rcvFileCompleteOrCancelled ft) $ do unless (rcvFileCompleteOrCancelled ft) $ do
(rfd, RcvFileTransfer {fileStatus, xftpRcvFile, cryptoArgs}) <- withStore $ \db -> do (rfd@RcvFileDescr {fileDescrComplete}, ft'@RcvFileTransfer {fileStatus, xftpRcvFile, cryptoArgs}) <- withStore $ \db -> do
rfd <- appendRcvFD db userId fileId fileDescr rfd <- appendRcvFD db userId fileId fileDescr
-- reading second time in the same transaction as appending description -- reading second time in the same transaction as appending description
-- to prevent race condition with accept -- to prevent race condition with accept
ft' <- getRcvFileTransfer db user fileId ft' <- getRcvFileTransfer db user fileId
pure (rfd, ft') pure (rfd, ft')
when fileDescrComplete $ do
ci <- withStore $ \db -> getAChatItemBySharedMsgId db user cd sharedMsgId
toView $ CRRcvFileDescrReady user ci ft' rfd
case (fileStatus, xftpRcvFile) of case (fileStatus, xftpRcvFile) of
(RFSAccepted _, Just XFTPRcvFile {}) -> receiveViaCompleteFD user fileId rfd cryptoArgs (RFSAccepted _, Just XFTPRcvFile {}) -> receiveViaCompleteFD user fileId rfd cryptoArgs
_ -> pure () _ -> pure ()
@@ -7040,7 +7049,8 @@ chatCommandP =
"/get subs" $> GetAgentSubs, "/get subs" $> GetAgentSubs,
"/get subs details" $> GetAgentSubsDetails, "/get subs details" $> GetAgentSubsDetails,
"/get workers" $> GetAgentWorkers, "/get workers" $> GetAgentWorkers,
"/get workers details" $> GetAgentWorkersDetails "/get workers details" $> GetAgentWorkersDetails,
"//" *> (CustomChatCommand <$> A.takeByteString)
] ]
where where
choice = A.choice . map (\p -> p <* A.takeWhile (== ' ') <* A.endOfInput) choice = A.choice . map (\p -> p <* A.takeWhile (== ' ') <* A.endOfInput)
+28 -6
View File
@@ -144,9 +144,28 @@ data ChatConfig = ChatConfig
ciExpirationInterval :: Int64, -- microseconds ciExpirationInterval :: Int64, -- microseconds
coreApi :: Bool, coreApi :: Bool,
highlyAvailable :: Bool, highlyAvailable :: Bool,
deviceNameForRemote :: Text deviceNameForRemote :: Text,
chatHooks :: ChatHooks
} }
-- The hooks can be used to extend or customize chat core in mobile or CLI clients.
data ChatHooks = ChatHooks
{ -- preCmdHook can be used to process or modify the commands before they are processed.
-- This hook should be used to process CustomChatCommand.
-- if this hook returns ChatResponse, the command processing will be skipped.
preCmdHook :: ChatController -> ChatCommand -> IO (Either ChatResponse ChatCommand),
-- eventHook can be used to additionally process or modify events,
-- it is called before the event is sent to the user (or to the UI).
eventHook :: ChatController -> ChatResponse -> IO ChatResponse
}
defaultChatHooks :: ChatHooks
defaultChatHooks =
ChatHooks
{ preCmdHook = \_ -> pure . Right,
eventHook = \_ -> pure
}
data DefaultAgentServers = DefaultAgentServers data DefaultAgentServers = DefaultAgentServers
{ smp :: NonEmpty SMPServerWithAuth, { smp :: NonEmpty SMPServerWithAuth,
ntf :: [NtfServer], ntf :: [NtfServer],
@@ -471,6 +490,9 @@ data ChatCommand
| GetAgentSubsDetails | GetAgentSubsDetails
| GetAgentWorkers | GetAgentWorkers
| GetAgentWorkersDetails | GetAgentWorkersDetails
-- The parser will return this command for strings that start from "//".
-- This command should be processed in preCmdHook
| CustomChatCommand ByteString
deriving (Show) deriving (Show)
allowRemoteCommand :: ChatCommand -> Bool -- XXX: consider using Relay/Block/ForceLocal allowRemoteCommand :: ChatCommand -> Bool -- XXX: consider using Relay/Block/ForceLocal
@@ -597,10 +619,9 @@ data ChatResponse
| CRContactRequestAlreadyAccepted {user :: User, contact :: Contact} | CRContactRequestAlreadyAccepted {user :: User, contact :: Contact}
| CRLeftMemberUser {user :: User, groupInfo :: GroupInfo} | CRLeftMemberUser {user :: User, groupInfo :: GroupInfo}
| CRGroupDeletedUser {user :: User, groupInfo :: GroupInfo} | CRGroupDeletedUser {user :: User, groupInfo :: GroupInfo}
| CRRcvFileDescrReady {user :: User, chatItem :: AChatItem} | CRRcvFileDescrReady {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer, rcvFileDescr :: RcvFileDescr}
| CRRcvFileAccepted {user :: User, chatItem :: AChatItem} | CRRcvFileAccepted {user :: User, chatItem :: AChatItem}
| CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer} | CRRcvFileAcceptedSndCancelled {user :: User, rcvFileTransfer :: RcvFileTransfer}
| CRRcvFileDescrNotReady {user :: User, chatItem :: AChatItem}
| CRStandaloneFileInfo {fileMeta :: Maybe J.Value} | CRStandaloneFileInfo {fileMeta :: Maybe J.Value}
| CRRcvStandaloneFileCreated {user :: User, rcvFileTransfer :: RcvFileTransfer} -- returned by _download | CRRcvStandaloneFileCreated {user :: User, rcvFileTransfer :: RcvFileTransfer} -- returned by _download
| CRRcvFileStart {user :: User, chatItem :: AChatItem} -- sent by chats | CRRcvFileStart {user :: User, chatItem :: AChatItem} -- sent by chats
@@ -726,6 +747,7 @@ data ChatResponse
| CRArchiveImported {archiveErrors :: [ArchiveError]} | CRArchiveImported {archiveErrors :: [ArchiveError]}
| CRAppSettings {appSettings :: AppSettings} | CRAppSettings {appSettings :: AppSettings}
| CRTimedAction {action :: String, durationMilliseconds :: Int64} | CRTimedAction {action :: String, durationMilliseconds :: Int64}
| CRCustomChatResponse {user_ :: Maybe User, response :: Text}
deriving (Show) deriving (Show)
-- some of these can only be used as command responses -- some of these can only be used as command responses
@@ -1278,9 +1300,9 @@ throwChatError = throwError . ChatError
-- | Emit local events. -- | Emit local events.
toView :: ChatMonad' m => ChatResponse -> m () toView :: ChatMonad' m => ChatResponse -> m ()
toView event = do toView ev = do
localQ <- asks outputQ cc@ChatController {outputQ = localQ, remoteCtrlSession = session, config = ChatConfig {chatHooks}} <- ask
session <- asks remoteCtrlSession event <- liftIO $ eventHook chatHooks cc ev
atomically $ atomically $
readTVar session >>= \case readTVar session >>= \case
Just (_, RCSessionConnected {remoteOutputQ}) Just (_, RCSessionConnected {remoteOutputQ})
@@ -0,0 +1,20 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Migrations.M20240324_custom_data where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20240324_custom_data :: Query
m20240324_custom_data =
[sql|
ALTER TABLE contacts ADD COLUMN custom_data BLOB;
ALTER TABLE groups ADD COLUMN custom_data BLOB;
|]
down_m20240324_custom_data :: Query
down_m20240324_custom_data =
[sql|
ALTER TABLE contacts DROP COLUMN custom_data;
ALTER TABLE groups DROP COLUMN custom_data;
|]
+3 -1
View File
@@ -73,6 +73,7 @@ CREATE TABLE contacts(
REFERENCES group_members(group_member_id) ON DELETE SET NULL, REFERENCES group_members(group_member_id) ON DELETE SET NULL,
contact_grp_inv_sent INTEGER NOT NULL DEFAULT 0, contact_grp_inv_sent INTEGER NOT NULL DEFAULT 0,
contact_status TEXT NOT NULL DEFAULT 'active', contact_status TEXT NOT NULL DEFAULT 'active',
custom_data BLOB,
FOREIGN KEY(user_id, local_display_name) FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE ON DELETE CASCADE
@@ -120,7 +121,8 @@ CREATE TABLE groups(
favorite INTEGER NOT NULL DEFAULT 0, favorite INTEGER NOT NULL DEFAULT 0,
send_rcpts INTEGER, send_rcpts INTEGER,
via_group_link_uri_hash BLOB, via_group_link_uri_hash BLOB,
user_member_profile_sent_at TEXT, -- received user_member_profile_sent_at TEXT,
custom_data BLOB, -- received
FOREIGN KEY(user_id, local_display_name) FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE ON DELETE CASCADE
+5 -5
View File
@@ -75,19 +75,19 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
[sql| [sql|
SELECT SELECT
c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.via_group, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, c.contact_profile_id, c.local_display_name, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.via_group, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite,
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent, c.custom_data
FROM contacts c FROM contacts c
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0 WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0
|] |]
(userId, contactId) (userId, contactId)
toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool)] -> Either StoreError Contact toContact' :: Int64 -> Connection -> [(ProfileId, ContactName, Text, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Int64, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool, Maybe CustomData)] -> Either StoreError Contact
toContact' contactId conn [(profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)] = toContact' contactId conn [(profileId, localDisplayName, displayName, fullName, image, contactLink, localAlias, viaGroup, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData)] =
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
activeConn = Just conn activeConn = Just conn
in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent} in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData}
toContact' _ _ _ = Left $ SEInternalError "referenced contact not found" toContact' _ _ _ = Left $ SEInternalError "referenced contact not found"
getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember)
getGroupAndMember_ groupMemberId c = ExceptT $ do getGroupAndMember_ groupMemberId c = ExceptT $ do
@@ -99,7 +99,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
-- GroupInfo -- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data,
-- GroupInfo {membership} -- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
+11 -5
View File
@@ -68,6 +68,7 @@ module Simplex.Chat.Store.Direct
updateContactSettings, updateContactSettings,
setConnConnReqInv, setConnConnReqInv,
resetContactConnInitiated, resetContactConnInitiated,
setContactCustomData,
) )
where where
@@ -175,7 +176,7 @@ getContactByConnReqHash db vr user@User {userId} cReqHash =
SELECT SELECT
-- Contact -- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.custom_data,
-- Connection -- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
@@ -221,7 +222,7 @@ createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p
let profile = toLocalProfile profileId p localAlias let profile = toLocalProfile profileId p localAlias
userPreferences = emptyChatPrefs userPreferences = emptyChatPrefs
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
pure $ Contact {contactId, localDisplayName, profile, activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False} pure $ Contact {contactId, localDisplayName, profile, activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, customData = Nothing}
deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO () deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO ()
deleteContactConnectionsAndFiles db userId Contact {contactId} = do deleteContactConnectionsAndFiles db userId Contact {contactId} = do
@@ -578,7 +579,7 @@ createOrUpdateContactRequest db vr user@User {userId} userContactLinkId invId (V
SELECT SELECT
-- Contact -- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.custom_data,
-- Connection -- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
@@ -724,7 +725,7 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}
contactId <- insertedRowId db contactId <- insertedRowId db
conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False} pure $ Contact {contactId, localDisplayName, profile = toLocalProfile profileId profile "", activeConn = Just conn, viaGroup = Nothing, contactUsed, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = createdAt, updatedAt = createdAt, chatTs = Just createdAt, contactGroupMemberId = Nothing, contactGrpInvSent = False, customData = Nothing}
getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64 getContactIdByName :: DB.Connection -> User -> ContactName -> ExceptT StoreError IO Int64
getContactIdByName db User {userId} cName = getContactIdByName db User {userId} cName =
@@ -743,7 +744,7 @@ getContact_ db vr user@User {userId} contactId deleted =
SELECT SELECT
-- Contact -- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.custom_data,
-- Connection -- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter,
@@ -883,3 +884,8 @@ resetContactConnInitiated db User {userId} Connection {connId} = do
WHERE user_id = ? AND connection_id = ? WHERE user_id = ? AND connection_id = ?
|] |]
(updatedAt, userId, connId) (updatedAt, userId, connId)
setContactCustomData :: DB.Connection -> User -> Contact -> Maybe CustomData -> IO ()
setContactCustomData db User {userId} Contact {contactId} customData = do
updatedAt <- getCurrentTime
DB.execute db "UPDATE contacts SET custom_data = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (customData, updatedAt, userId, contactId)
+19 -11
View File
@@ -116,6 +116,7 @@ module Simplex.Chat.Store.Groups
createNewUnknownGroupMember, createNewUnknownGroupMember,
updateUnknownMemberAnnounced, updateUnknownMemberAnnounced,
updateUserMemberProfileSentAt, updateUserMemberProfileSentAt,
setGroupCustomData,
) )
where where
@@ -148,19 +149,19 @@ import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>))
import Simplex.Messaging.Version import Simplex.Messaging.Version
import UnliftIO.STM import UnliftIO.STM
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. GroupMemberRow type GroupInfoRow = (Int64, GroupName, GroupName, Text, Maybe Text, Maybe ImageData, Maybe ProfileId, Maybe MsgFilter, Maybe Bool, Bool, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe CustomData) :. GroupMemberRow
type GroupMemberRow = ((Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences)) type GroupMemberRow = ((Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences))
type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences)) type MaybeGroupMemberRow = ((Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe Bool, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences))
toGroupInfo :: (PQSupport -> VersionRangeChat) -> Int64 -> GroupInfoRow -> GroupInfo toGroupInfo :: (PQSupport -> VersionRangeChat) -> Int64 -> GroupInfoRow -> GroupInfo
toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. userMemberRow) = toGroupInfo vr userContactId ((groupId, localDisplayName, displayName, fullName, description, image, hostConnCustomUserProfileId, enableNtfs_, sendRcpts, favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, customData) :. userMemberRow) =
let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr PQSupportOff} let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr PQSupportOff}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
fullGroupPreferences = mergeGroupPreferences groupPreferences fullGroupPreferences = mergeGroupPreferences groupPreferences
groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences} groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences}
in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt} in GroupInfo {groupId, localDisplayName, groupProfile, fullGroupPreferences, membership, hostConnCustomUserProfileId, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, customData}
toGroupMember :: Int64 -> GroupMemberRow -> GroupMember toGroupMember :: Int64 -> GroupMemberRow -> GroupMember
toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) = toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences)) =
@@ -271,7 +272,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr =
-- GroupInfo -- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data,
-- GroupInfo {membership} -- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -344,7 +345,8 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc
createdAt = currentTs, createdAt = currentTs,
updatedAt = currentTs, updatedAt = currentTs,
chatTs = Just currentTs, chatTs = Just currentTs,
userMemberProfileSentAt = Just currentTs userMemberProfileSentAt = Just currentTs,
customData = Nothing
} }
-- | creates a new group record for the group the current user was invited to, or returns an existing one -- | creates a new group record for the group the current user was invited to, or returns an existing one
@@ -409,7 +411,8 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ
createdAt = currentTs, createdAt = currentTs,
updatedAt = currentTs, updatedAt = currentTs,
chatTs = Just currentTs, chatTs = Just currentTs,
userMemberProfileSentAt = Just currentTs userMemberProfileSentAt = Just currentTs,
customData = Nothing
}, },
groupMemberId groupMemberId
) )
@@ -628,7 +631,7 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ =
SELECT SELECT
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data,
mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction,
mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, pu.display_name, pu.full_name, pu.image, pu.contact_link, pu.local_alias, pu.preferences
FROM groups g FROM groups g
@@ -1293,7 +1296,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} =
-- GroupInfo -- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data,
-- GroupInfo {membership} -- GroupInfo {membership}
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -1389,7 +1392,7 @@ getGroupInfo db vr User {userId, userContactId} groupId =
-- GroupInfo -- GroupInfo
g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image, g.group_id, g.local_display_name, gp.display_name, gp.full_name, gp.description, gp.image,
g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, g.host_conn_custom_user_profile_id, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences,
g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.custom_data,
-- GroupMember - membership -- GroupMember - membership
mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.group_member_id, mu.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category,
mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id, mu.member_status, mu.show_messages, mu.member_restriction, mu.invited_by, mu.invited_by_group_member_id, mu.local_display_name, mu.contact_id, mu.contact_profile_id, pu.contact_profile_id,
@@ -1951,7 +1954,7 @@ createMemberContact
authErrCounter = 0 authErrCounter = 0
} }
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False} pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, customData = Nothing}
getMemberContact :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation) getMemberContact :: DB.Connection -> (PQSupport -> VersionRangeChat) -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation)
getMemberContact db vr user contactId = do getMemberContact db vr user contactId = do
@@ -1988,7 +1991,7 @@ createMemberContactInvited
contactId <- createContactUpdateMember currentTs userPreferences contactId <- createContactUpdateMember currentTs userPreferences
ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False} mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, customData = Nothing}
m' = m {memberContactId = Just contactId} m' = m {memberContactId = Just contactId}
pure (mCt', m') pure (mCt', m')
where where
@@ -2188,3 +2191,8 @@ updateUserMemberProfileSentAt db User {userId} GroupInfo {groupId} sentTs =
db db
"UPDATE groups SET user_member_profile_sent_at = ? WHERE user_id = ? AND group_id = ?" "UPDATE groups SET user_member_profile_sent_at = ? WHERE user_id = ? AND group_id = ?"
(sentTs, userId, groupId) (sentTs, userId, groupId)
setGroupCustomData :: DB.Connection -> User -> GroupInfo -> Maybe CustomData -> IO ()
setGroupCustomData db User {userId} GroupInfo {groupId} customData = do
updatedAt <- getCurrentTime
DB.execute db "UPDATE groups SET custom_data = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (customData, updatedAt, userId, groupId)
+10
View File
@@ -41,6 +41,7 @@ module Simplex.Chat.Store.Messages
getDirectChatItemLast, getDirectChatItemLast,
getAllChatItems, getAllChatItems,
getAChatItem, getAChatItem,
getAChatItemBySharedMsgId,
updateDirectChatItem, updateDirectChatItem,
updateDirectChatItem', updateDirectChatItem',
addInitialAndNewCIVersions, addInitialAndNewCIVersions,
@@ -2202,6 +2203,15 @@ getAChatItem db vr user chatRef itemId = case chatRef of
pure $ AChatItem SCTLocal msgDir (LocalChat nf) ci pure $ AChatItem SCTLocal msgDir (LocalChat nf) ci
_ -> throwError $ SEChatItemNotFound itemId _ -> throwError $ SEChatItemNotFound itemId
getAChatItemBySharedMsgId :: ChatTypeQuotable c => DB.Connection -> User -> ChatDirection c 'MDRcv -> SharedMsgId -> ExceptT StoreError IO AChatItem
getAChatItemBySharedMsgId db user cd sharedMsgId = case cd of
CDDirectRcv ct@Contact {contactId} -> do
(CChatItem msgDir ci) <- getDirectChatItemBySharedMsgId db user contactId sharedMsgId
pure $ AChatItem SCTDirect msgDir (DirectChat ct) ci
CDGroupRcv g@GroupInfo {groupId} GroupMember {groupMemberId} -> do
(CChatItem msgDir ci) <- getGroupChatItemBySharedMsgId db user groupId groupMemberId sharedMsgId
pure $ AChatItem SCTGroup msgDir (GroupChat g) ci
getChatItemVersions :: DB.Connection -> ChatItemId -> IO [ChatItemVersion] getChatItemVersions :: DB.Connection -> ChatItemId -> IO [ChatItemVersion]
getChatItemVersions db itemId = do getChatItemVersions db itemId = do
map toChatItemVersion map toChatItemVersion
+3 -1
View File
@@ -103,6 +103,7 @@ import Simplex.Chat.Migrations.M20240222_app_settings
import Simplex.Chat.Migrations.M20240226_users_restrict import Simplex.Chat.Migrations.M20240226_users_restrict
import Simplex.Chat.Migrations.M20240228_pq import Simplex.Chat.Migrations.M20240228_pq
import Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id import Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id
import Simplex.Chat.Migrations.M20240324_custom_data
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)] schemaMigrations :: [(String, Query, Maybe Query)]
@@ -205,7 +206,8 @@ schemaMigrations =
("20240222_app_settings", m20240222_app_settings, Just down_m20240222_app_settings), ("20240222_app_settings", m20240222_app_settings, Just down_m20240222_app_settings),
("20240226_users_restrict", m20240226_users_restrict, Just down_m20240226_users_restrict), ("20240226_users_restrict", m20240226_users_restrict, Just down_m20240226_users_restrict),
("20240228_pq", m20240228_pq, Just down_m20240228_pq), ("20240228_pq", m20240228_pq, Just down_m20240228_pq),
("20240313_drop_agent_ack_cmd_id", m20240313_drop_agent_ack_cmd_id, Just down_m20240313_drop_agent_ack_cmd_id) ("20240313_drop_agent_ack_cmd_id", m20240313_drop_agent_ack_cmd_id, Just down_m20240313_drop_agent_ack_cmd_id),
("20240324_custom_data", m20240324_custom_data, Just down_m20240324_custom_data)
] ]
-- | The list of migrations in ascending order by date -- | The list of migrations in ascending order by date
+3 -3
View File
@@ -371,16 +371,16 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId =
|] |]
[":user_id" := userId, ":profile_id" := profileId] [":user_id" := userId, ":profile_id" := profileId]
type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool) type ContactRow = (ContactId, ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool, Maybe CustomData)
toContact :: (PQSupport -> VersionRangeChat) -> User -> ContactRow :. MaybeConnectionRow -> Contact toContact :: (PQSupport -> VersionRangeChat) -> User -> ContactRow :. MaybeConnectionRow -> Contact
toContact vr user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent)) :. connRow) = toContact vr user (((contactId, profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData)) :. connRow) =
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
activeConn = toMaybeConnection vr connRow activeConn = toMaybeConnection vr connRow
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite}
incognito = maybe False connIncognito activeConn incognito = maybe False connIncognito activeConn
mergedPreferences = contactUserPreferences user userPreferences preferences incognito mergedPreferences = contactUserPreferences user userPreferences preferences incognito
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent} in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, customData}
getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile
getProfileById db userId profileId = getProfileById db userId profileId =
+64
View File
@@ -0,0 +1,64 @@
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
module Simplex.Chat.Terminal.Main where
import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.STM
import Control.Monad
import Data.Time.Clock (getCurrentTime)
import Data.Time.LocalTime (getCurrentTimeZone)
import Network.Socket
import Simplex.Chat.Controller (ChatConfig, ChatController (..), ChatResponse (..), currentRemoteHost, versionNumber, versionString)
import Simplex.Chat.Core
import Simplex.Chat.Options
import Simplex.Chat.Terminal
import Simplex.Chat.View (serializeChatResponse)
import Simplex.Messaging.Client (NetworkConfig (..))
import System.Directory (getAppUserDataDirectory)
import System.Exit (exitFailure)
import System.Terminal (withTerminal)
simplexChatCLI :: ChatConfig -> Maybe (ServiceName -> ChatConfig -> ChatOpts -> IO ()) -> IO ()
simplexChatCLI cfg server_ = do
appDir <- getAppUserDataDirectory "simplex"
opts@ChatOpts {chatCmd, chatServerPort} <- getChatOpts appDir "simplex_v1"
if null chatCmd
then case chatServerPort of
Just chatPort -> case server_ of
Just server -> server chatPort cfg opts
Nothing -> putStrLn "Not allowed to run as a WebSockets server" >> exitFailure
_ -> runCLI opts
else simplexChatCore cfg opts $ runCommand opts
where
runCLI opts = do
welcome opts
t <- withTerminal pure
simplexChatTerminal cfg opts t
runCommand ChatOpts {chatCmd, chatCmdLog, chatCmdDelay} user cc = do
when (chatCmdLog /= CCLNone) . void . forkIO . forever $ do
(_, _, r') <- atomically . readTBQueue $ outputQ cc
case r' of
CRNewChatItem {} -> printResponse r'
_ -> when (chatCmdLog == CCLAll) $ printResponse r'
sendChatCmdStr cc chatCmd >>= printResponse
threadDelay $ chatCmdDelay * 1000000
where
printResponse r = do
ts <- getCurrentTime
tz <- getCurrentTimeZone
rh <- readTVarIO $ currentRemoteHost cc
putStrLn $ serializeChatResponse (rh, Just user) ts tz rh r
welcome :: ChatOpts -> IO ()
welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, networkConfig}} =
mapM_
putStrLn
[ versionString versionNumber,
"db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db",
maybe
"direct network connection - use `/network` command or `-x` CLI option to connect via SOCKS5 at :9050"
(("using SOCKS5 proxy " <>) . show)
(socksProxy networkConfig),
"type \"/help\" or \"/h\" for usage info"
]
+18 -2
View File
@@ -174,10 +174,25 @@ data Contact = Contact
updatedAt :: UTCTime, updatedAt :: UTCTime,
chatTs :: Maybe UTCTime, chatTs :: Maybe UTCTime,
contactGroupMemberId :: Maybe GroupMemberId, contactGroupMemberId :: Maybe GroupMemberId,
contactGrpInvSent :: Bool contactGrpInvSent :: Bool,
customData :: Maybe CustomData
} }
deriving (Eq, Show) deriving (Eq, Show)
newtype CustomData = CustomData J.Object
deriving (Eq, Show)
instance ToJSON CustomData where
toJSON (CustomData v) = toJSON v
toEncoding (CustomData v) = toEncoding v
instance FromJSON CustomData where
parseJSON = J.withObject "CustomData" (pure . CustomData)
instance ToField CustomData where toField (CustomData v) = toField $ J.encode v
instance FromField CustomData where fromField = fromBlobField_ J.eitherDecodeStrict
contactConn :: Contact -> Maybe Connection contactConn :: Contact -> Maybe Connection
contactConn Contact {activeConn} = activeConn contactConn Contact {activeConn} = activeConn
@@ -356,7 +371,8 @@ data GroupInfo = GroupInfo
createdAt :: UTCTime, createdAt :: UTCTime,
updatedAt :: UTCTime, updatedAt :: UTCTime,
chatTs :: Maybe UTCTime, chatTs :: Maybe UTCTime,
userMemberProfileSentAt :: Maybe UTCTime userMemberProfileSentAt :: Maybe UTCTime,
customData :: Maybe CustomData
} }
deriving (Eq, Show) deriving (Eq, Show)
+9 -4
View File
@@ -183,8 +183,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRUnknownMemberBlocked u g byM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember byM <> " blocked an unknown member, creating unknown member record " <> ttyMember um] CRUnknownMemberBlocked u g byM um -> ttyUser u [ttyGroup' g <> ": " <> ttyMember byM <> " blocked an unknown member, creating unknown member record " <> ttyMember um]
CRUnknownMemberAnnounced u g _ um m -> ttyUser u [ttyGroup' g <> ": unknown member " <> ttyMember um <> " updated to " <> ttyMember m] CRUnknownMemberAnnounced u g _ um m -> ttyUser u [ttyGroup' g <> ": unknown member " <> ttyMember um <> " updated to " <> ttyMember m]
CRGroupDeletedUser u g -> ttyUser u [ttyGroup' g <> ": you deleted the group"] CRGroupDeletedUser u g -> ttyUser u [ttyGroup' g <> ": you deleted the group"]
CRRcvFileDescrReady _ _ -> [] CRRcvFileDescrReady _ _ _ _ -> []
CRRcvFileDescrNotReady _ _ -> []
CRRcvFileProgressXFTP {} -> [] CRRcvFileProgressXFTP {} -> []
CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci CRRcvFileAccepted u ci -> ttyUser u $ savingFile' ci
CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft CRRcvFileAcceptedSndCancelled u ft -> ttyUser u $ viewRcvFileSndCancelled ft
@@ -391,6 +390,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)] CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)]
CRAppSettings as -> ["app settings: " <> plain (LB.unpack $ J.encode as)] CRAppSettings as -> ["app settings: " <> plain (LB.unpack $ J.encode as)]
CRTimedAction _ _ -> [] CRTimedAction _ _ -> []
CRCustomChatResponse u r -> ttyUser' u $ [plain r]
where where
ttyUser :: User -> [StyledString] -> [StyledString] ttyUser :: User -> [StyledString] -> [StyledString]
ttyUser user@User {showNtfs, activeUser} ss ttyUser user@User {showNtfs, activeUser} ss
@@ -1167,7 +1167,7 @@ viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} =
] ]
viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString] viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString]
viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn} stats incognitoProfile = viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn, customData} stats incognitoProfile =
["contact ID: " <> sShow contactId] ["contact ID: " <> sShow contactId]
<> maybe [] viewConnectionStats stats <> maybe [] viewConnectionStats stats
<> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact l)]) contactLink <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact l)]) contactLink
@@ -1179,12 +1179,17 @@ viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, conta
<> [viewConnectionVerified (contactSecurityCode ct)] <> [viewConnectionVerified (contactSecurityCode ct)]
<> ["quantum resistant end-to-end encryption" | contactPQEnabled ct == CR.PQEncOn] <> ["quantum resistant end-to-end encryption" | contactPQEnabled ct == CR.PQEncOn]
<> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn <> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn
<> viewCustomData customData
viewGroupInfo :: GroupInfo -> GroupSummary -> [StyledString] viewGroupInfo :: GroupInfo -> GroupSummary -> [StyledString]
viewGroupInfo GroupInfo {groupId} s = viewGroupInfo GroupInfo {groupId, customData} s =
[ "group ID: " <> sShow groupId, [ "group ID: " <> sShow groupId,
"current members: " <> sShow (currentMembers s) "current members: " <> sShow (currentMembers s)
] ]
<> viewCustomData customData
viewCustomData :: Maybe CustomData -> [StyledString]
viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> plain (LB.toStrict . J.encode $ J.Object v)])
viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString] viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString]
viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias, contactLink}, activeConn} stats = viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias, contactLink}, activeConn} stats =