mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 701b4186b6 | |||
| 9b6ca23dcb | |||
| 257e03f10a | |||
| 1410b4bd85 | |||
| 42a35e8c72 | |||
| 86fe28f1ed | |||
| 735359c279 | |||
| 3d20465662 | |||
| 7b644c0dcf | |||
| c3d9d9a7c3 | |||
| 261767035e | |||
| c9b00b3054 | |||
| 1697190189 | |||
| 24609a98c6 | |||
| 60752feb9c | |||
| ff5ef638cd | |||
| df619d540b | |||
| 53d8a85b8c |
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
+33
-1
@@ -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 {
|
||||||
|
|||||||
+30
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
+163
-60
@@ -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 */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
@@ -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 -> {
|
||||||
|
|||||||
+4
-1
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|]
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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"
|
||||||
|
]
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
Reference in New Issue
Block a user