mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e5ee981e5 | |||
| c59eb06b8a | |||
| 423fc96638 | |||
| ba203faad4 | |||
| ec7b35adb9 | |||
| d2d450d1d7 |
@@ -927,14 +927,19 @@ func standaloneFileInfo(url: String, ctrl: chat_ctrl? = nil) async -> MigrationF
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func receiveFile(user: any UserLike, fileId: Int64, auto: Bool = false) async {
|
func receiveFile(user: any UserLike, fileId: Int64, userApprovedRelays: Bool = false, auto: Bool = false) async {
|
||||||
if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get(), auto: auto) {
|
if let chatItem = await apiReceiveFile(
|
||||||
|
fileId: fileId,
|
||||||
|
userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(),
|
||||||
|
encrypted: privacyEncryptLocalFilesGroupDefault.get(),
|
||||||
|
auto: auto
|
||||||
|
) {
|
||||||
await chatItemSimpleUpdate(user, chatItem)
|
await chatItemSimpleUpdate(user, chatItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? {
|
func apiReceiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? {
|
||||||
let r = await chatSendCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline))
|
let r = await chatSendCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline))
|
||||||
let am = AlertManager.shared
|
let am = AlertManager.shared
|
||||||
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
|
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
|
||||||
if case .rcvFileAcceptedSndCancelled = r {
|
if case .rcvFileAcceptedSndCancelled = r {
|
||||||
@@ -947,19 +952,50 @@ func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: B
|
|||||||
}
|
}
|
||||||
} else if let networkErrorAlert = networkErrorAlert(r) {
|
} else if let networkErrorAlert = networkErrorAlert(r) {
|
||||||
logger.error("apiReceiveFile network error: \(String(describing: r))")
|
logger.error("apiReceiveFile network error: \(String(describing: r))")
|
||||||
am.showAlert(networkErrorAlert)
|
if !auto {
|
||||||
|
am.showAlert(networkErrorAlert)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
switch chatError(r) {
|
switch chatError(r) {
|
||||||
case .fileCancelled:
|
case .fileCancelled:
|
||||||
logger.debug("apiReceiveFile ignoring fileCancelled error")
|
logger.debug("apiReceiveFile ignoring fileCancelled error")
|
||||||
case .fileAlreadyReceiving:
|
case .fileAlreadyReceiving:
|
||||||
logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error")
|
logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error")
|
||||||
|
case let .fileNotApproved(fileId, unknownServers):
|
||||||
|
logger.debug("apiReceiveFile fileNotApproved error")
|
||||||
|
if !auto {
|
||||||
|
let srvs = unknownServers.map { s in
|
||||||
|
if let srv = parseServerAddress(s), !srv.hostnames.isEmpty {
|
||||||
|
srv.hostnames[0]
|
||||||
|
} else {
|
||||||
|
serverHost(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
am.showAlert(Alert(
|
||||||
|
title: Text("Unknown servers!"),
|
||||||
|
message: Text("Without Tor or VPN, your IP address will be visible to these XFTP relays: \(srvs.sorted().joined(separator: ", "))."),
|
||||||
|
primaryButton: .default(
|
||||||
|
Text("Download"),
|
||||||
|
action: {
|
||||||
|
Task {
|
||||||
|
logger.debug("apiReceiveFile fileNotApproved alert - in Task")
|
||||||
|
if let user = ChatModel.shared.currentUser {
|
||||||
|
await receiveFile(user: user, fileId: fileId, userApprovedRelays: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
secondaryButton: .cancel()
|
||||||
|
))
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
logger.error("apiReceiveFile error: \(String(describing: r))")
|
logger.error("apiReceiveFile error: \(String(describing: r))")
|
||||||
am.showAlertMsg(
|
if !auto {
|
||||||
title: "Error receiving file",
|
am.showAlertMsg(
|
||||||
message: "Error: \(String(describing: r))"
|
title: "Error receiving file",
|
||||||
)
|
message: "Error: \(String(describing: r))"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -1286,10 +1322,6 @@ func apiGetVersion() throws -> CoreVersionInfo {
|
|||||||
throw r
|
throw r
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiSetAppLogLevel(_ ll: ChatLogLevel) async throws {
|
|
||||||
try await sendCommandOkResp(.setAppLogLevel(appLogLevel: ll))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func currentUserId(_ funcName: String) throws -> Int64 {
|
private func currentUserId(_ funcName: String) throws -> Int64 {
|
||||||
if let userId = ChatModel.shared.currentUser?.userId {
|
if let userId = ChatModel.shared.currentUser?.userId {
|
||||||
return userId
|
return userId
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func localizedInfoRow(_ title: LocalizedStringKey, _ value: LocalizedStringKey)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func serverHost(_ s: String) -> String {
|
func serverHost(_ s: String) -> String {
|
||||||
if let i = s.range(of: "@")?.lowerBound {
|
if let i = s.range(of: "@")?.lowerBound {
|
||||||
return String(s[i...].dropFirst())
|
return String(s[i...].dropFirst())
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ struct CIFileView: View {
|
|||||||
case .rcvInvitation: return true
|
case .rcvInvitation: return true
|
||||||
case .rcvAccepted: return true
|
case .rcvAccepted: return true
|
||||||
case .rcvTransfer: return false
|
case .rcvTransfer: return false
|
||||||
|
case .rcvAborted: return true
|
||||||
case .rcvComplete: return true
|
case .rcvComplete: return true
|
||||||
case .rcvCancelled: return false
|
case .rcvCancelled: return false
|
||||||
case .rcvError: return false
|
case .rcvError: return false
|
||||||
@@ -73,10 +74,10 @@ struct CIFileView: View {
|
|||||||
logger.debug("CIFileView fileAction")
|
logger.debug("CIFileView fileAction")
|
||||||
if let file = file {
|
if let file = file {
|
||||||
switch (file.fileStatus) {
|
switch (file.fileStatus) {
|
||||||
case .rcvInvitation:
|
case .rcvInvitation, .rcvAborted:
|
||||||
if fileSizeValid(file) {
|
if fileSizeValid(file) {
|
||||||
Task {
|
Task {
|
||||||
logger.debug("CIFileView fileAction - in .rcvInvitation, in Task")
|
logger.debug("CIFileView fileAction - in .rcvInvitation, .rcvAborted, in Task")
|
||||||
if let user = m.currentUser {
|
if let user = m.currentUser {
|
||||||
await receiveFile(user: user, fileId: file.fileId)
|
await receiveFile(user: user, fileId: file.fileId)
|
||||||
}
|
}
|
||||||
@@ -148,6 +149,8 @@ struct CIFileView: View {
|
|||||||
} else {
|
} else {
|
||||||
progressView()
|
progressView()
|
||||||
}
|
}
|
||||||
|
case .rcvAborted:
|
||||||
|
fileIcon("doc.fill", color: .accentColor, innerIcon: "exclamationmark.arrow.circlepath", innerIconSize: 12)
|
||||||
case .rcvComplete: fileIcon("doc.fill")
|
case .rcvComplete: fileIcon("doc.fill")
|
||||||
case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||||
case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10)
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ struct CIImageView: View {
|
|||||||
let maxWidth: CGFloat
|
let maxWidth: CGFloat
|
||||||
@Binding var imgWidth: CGFloat?
|
@Binding var imgWidth: CGFloat?
|
||||||
@State var scrollProxy: ScrollViewProxy?
|
@State var scrollProxy: ScrollViewProxy?
|
||||||
@State var metaColor: Color
|
|
||||||
@State private var showFullScreenImage = false
|
@State private var showFullScreenImage = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -38,7 +37,7 @@ struct CIImageView: View {
|
|||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if let file = file {
|
if let file = file {
|
||||||
switch file.fileStatus {
|
switch file.fileStatus {
|
||||||
case .rcvInvitation:
|
case .rcvInvitation, .rcvAborted:
|
||||||
Task {
|
Task {
|
||||||
if let user = m.currentUser {
|
if let user = m.currentUser {
|
||||||
await receiveFile(user: user, fileId: file.fileId)
|
await receiveFile(user: user, fileId: file.fileId)
|
||||||
@@ -103,6 +102,7 @@ struct CIImageView: View {
|
|||||||
case .rcvInvitation: fileIcon("arrow.down", 10, 13)
|
case .rcvInvitation: fileIcon("arrow.down", 10, 13)
|
||||||
case .rcvAccepted: fileIcon("ellipsis", 14, 11)
|
case .rcvAccepted: fileIcon("ellipsis", 14, 11)
|
||||||
case .rcvTransfer: progressView()
|
case .rcvTransfer: progressView()
|
||||||
|
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
|
||||||
case .rcvCancelled: fileIcon("xmark", 10, 13)
|
case .rcvCancelled: fileIcon("xmark", 10, 13)
|
||||||
case .rcvError: fileIcon("xmark", 10, 13)
|
case .rcvError: fileIcon("xmark", 10, 13)
|
||||||
case .invalid: fileIcon("questionmark", 10, 13)
|
case .invalid: fileIcon("questionmark", 10, 13)
|
||||||
@@ -116,7 +116,7 @@ struct CIImageView: View {
|
|||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.foregroundColor(metaColor)
|
.foregroundColor(.white)
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ struct CIVideoView: View {
|
|||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if let file = file {
|
if let file = file {
|
||||||
switch file.fileStatus {
|
switch file.fileStatus {
|
||||||
case .rcvInvitation:
|
case .rcvInvitation, .rcvAborted:
|
||||||
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
||||||
case .rcvAccepted:
|
case .rcvAccepted:
|
||||||
switch file.fileProtocol {
|
switch file.fileProtocol {
|
||||||
@@ -95,7 +95,7 @@ struct CIVideoView: View {
|
|||||||
}
|
}
|
||||||
durationProgress()
|
durationProgress()
|
||||||
}
|
}
|
||||||
if let file = file, case .rcvInvitation = file.fileStatus {
|
if let file = file, showDownloadButton(file.fileStatus) {
|
||||||
Button {
|
Button {
|
||||||
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
||||||
} label: {
|
} label: {
|
||||||
@@ -105,6 +105,14 @@ struct CIVideoView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func showDownloadButton(_ fileStatus: CIFileStatus) -> Bool {
|
||||||
|
switch fileStatus {
|
||||||
|
case .rcvInvitation: true
|
||||||
|
case .rcvAborted: true
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func videoViewEncrypted(_ file: CIFile, _ defaultPreview: UIImage, _ duration: Int) -> some View {
|
private func videoViewEncrypted(_ file: CIFile, _ defaultPreview: UIImage, _ duration: Int) -> some View {
|
||||||
return ZStack(alignment: .topTrailing) {
|
return ZStack(alignment: .topTrailing) {
|
||||||
ZStack(alignment: .center) {
|
ZStack(alignment: .center) {
|
||||||
@@ -280,6 +288,7 @@ struct CIVideoView: View {
|
|||||||
} else {
|
} else {
|
||||||
progressView()
|
progressView()
|
||||||
}
|
}
|
||||||
|
case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11)
|
||||||
case .rcvCancelled: fileIcon("xmark", 10, 13)
|
case .rcvCancelled: fileIcon("xmark", 10, 13)
|
||||||
case .rcvError: fileIcon("xmark", 10, 13)
|
case .rcvError: fileIcon("xmark", 10, 13)
|
||||||
case .invalid: fileIcon("questionmark", 10, 13)
|
case .invalid: fileIcon("questionmark", 10, 13)
|
||||||
@@ -318,10 +327,10 @@ struct CIVideoView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO encrypt: where file size is checked?
|
// TODO encrypt: where file size is checked?
|
||||||
private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool) async -> Void) {
|
private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) {
|
||||||
Task {
|
Task {
|
||||||
if let user = m.currentUser {
|
if let user = m.currentUser {
|
||||||
await receiveFile(user, file.fileId, false)
|
await receiveFile(user, file.fileId, false, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,9 +139,10 @@ struct VoiceMessagePlayer: View {
|
|||||||
case .sndComplete: playbackButton()
|
case .sndComplete: playbackButton()
|
||||||
case .sndCancelled: playbackButton()
|
case .sndCancelled: playbackButton()
|
||||||
case .sndError: playbackButton()
|
case .sndError: playbackButton()
|
||||||
case .rcvInvitation: downloadButton(recordingFile)
|
case .rcvInvitation: downloadButton(recordingFile, "play.fill")
|
||||||
case .rcvAccepted: loadingIcon()
|
case .rcvAccepted: loadingIcon()
|
||||||
case .rcvTransfer: loadingIcon()
|
case .rcvTransfer: loadingIcon()
|
||||||
|
case .rcvAborted: downloadButton(recordingFile, "exclamationmark.arrow.circlepath")
|
||||||
case .rcvComplete: playbackButton()
|
case .rcvComplete: playbackButton()
|
||||||
case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
||||||
case .rcvError: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
case .rcvError: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel))
|
||||||
@@ -217,7 +218,7 @@ struct VoiceMessagePlayer: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func downloadButton(_ recordingFile: CIFile) -> some View {
|
private func downloadButton(_ recordingFile: CIFile, _ icon: String) -> some View {
|
||||||
Button {
|
Button {
|
||||||
Task {
|
Task {
|
||||||
if let user = chatModel.currentUser {
|
if let user = chatModel.currentUser {
|
||||||
@@ -225,7 +226,7 @@ struct VoiceMessagePlayer: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
playPauseIcon("play.fill")
|
playPauseIcon(icon)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ struct FramedItemView: View {
|
|||||||
} else {
|
} else {
|
||||||
switch (chatItem.content.msgContent) {
|
switch (chatItem.content.msgContent) {
|
||||||
case let .image(text, image):
|
case let .image(text, image):
|
||||||
CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy, metaColor: metaColor)
|
CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy)
|
||||||
.overlay(DetermineWidth())
|
.overlay(DetermineWidth())
|
||||||
if text == "" && !chatItem.meta.isLive {
|
if text == "" && !chatItem.meta.isLive {
|
||||||
Color.clear
|
Color.clear
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ extension AppSettings {
|
|||||||
setNetCfg(val)
|
setNetCfg(val)
|
||||||
}
|
}
|
||||||
if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) }
|
if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) }
|
||||||
|
if let val = privacyAskToApproveRelays { privacyAskToApproveRelaysGroupDefault.set(val) }
|
||||||
if let val = privacyAcceptImages {
|
if let val = privacyAcceptImages {
|
||||||
privacyAcceptImagesGroupDefault.set(val)
|
privacyAcceptImagesGroupDefault.set(val)
|
||||||
def.setValue(val, forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
def.setValue(val, forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES)
|
||||||
@@ -50,6 +51,7 @@ extension AppSettings {
|
|||||||
var c = AppSettings.defaults
|
var c = AppSettings.defaults
|
||||||
c.networkConfig = getNetCfg()
|
c.networkConfig = getNetCfg()
|
||||||
c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get()
|
c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get()
|
||||||
|
c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get()
|
||||||
c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get()
|
c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get()
|
||||||
c.privacyLinkPreviews = def.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)
|
c.privacyLinkPreviews = def.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)
|
||||||
c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS)
|
c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import SimpleXChat
|
|||||||
struct DeveloperView: View {
|
struct DeveloperView: View {
|
||||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||||
@AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false
|
@AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false
|
||||||
@State private var appLogLevel = appLogLevelGroupDefault.get()
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -38,24 +37,6 @@ struct DeveloperView: View {
|
|||||||
settingsRow("chevron.left.forwardslash.chevron.right") {
|
settingsRow("chevron.left.forwardslash.chevron.right") {
|
||||||
Toggle("Show developer options", isOn: $developerTools)
|
Toggle("Show developer options", isOn: $developerTools)
|
||||||
}
|
}
|
||||||
settingsRow("text.justify") {
|
|
||||||
Picker("Log level", selection: $appLogLevel) {
|
|
||||||
ForEach(ChatLogLevel.allCases, id: \.self) { ll in
|
|
||||||
Text(ll.rawValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(height: 36)
|
|
||||||
.onChange(of: appLogLevel) { ll in
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
try await apiSetAppLogLevel(ll)
|
|
||||||
appLogLevelGroupDefault.set(ll)
|
|
||||||
} catch let e {
|
|
||||||
logger.error("apiSetAppLogLevel error: \(responseError(e))")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} header: {
|
} header: {
|
||||||
Text("")
|
Text("")
|
||||||
} footer: {
|
} footer: {
|
||||||
|
|||||||
@@ -82,29 +82,29 @@ struct NetworkAndServers: View {
|
|||||||
Text("Using .onion hosts requires compatible VPN provider.")
|
Text("Using .onion hosts requires compatible VPN provider.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Section {
|
Section {
|
||||||
// Picker("Private routing", selection: $proxyMode) {
|
Picker("Private routing", selection: $proxyMode) {
|
||||||
// ForEach(SMPProxyMode.values, id: \.self) { Text($0.text) }
|
ForEach(SMPProxyMode.values, id: \.self) { Text($0.text) }
|
||||||
// }
|
}
|
||||||
// .frame(height: 36)
|
.frame(height: 36)
|
||||||
//
|
|
||||||
// Picker("Allow downgrade", selection: $proxyFallback) {
|
Picker("Allow downgrade", selection: $proxyFallback) {
|
||||||
// ForEach(SMPProxyFallback.values, id: \.self) { Text($0.text) }
|
ForEach(SMPProxyFallback.values, id: \.self) { Text($0.text) }
|
||||||
// }
|
}
|
||||||
// .disabled(proxyMode == .never)
|
.disabled(proxyMode == .never)
|
||||||
// .frame(height: 36)
|
.frame(height: 36)
|
||||||
//
|
|
||||||
// Toggle("Show message status", isOn: $showSentViaProxy)
|
Toggle("Show message status", isOn: $showSentViaProxy)
|
||||||
// } header: {
|
} header: {
|
||||||
// Text("Private message routing")
|
Text("Private message routing")
|
||||||
// } footer: {
|
} footer: {
|
||||||
// VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
// Text("To protect your IP address, private routing uses your SMP servers to deliver messages.")
|
Text("To protect your IP address, private routing uses your SMP servers to deliver messages.")
|
||||||
// if showSentViaProxy {
|
if showSentViaProxy {
|
||||||
// Text("Show → on messages sent via private routing.")
|
Text("Show → on messages sent via private routing.")
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
Section("Calls") {
|
Section("Calls") {
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ struct PrivacySettings: View {
|
|||||||
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
|
@AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true
|
||||||
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
|
@AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
|
||||||
@AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true
|
@AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true
|
||||||
|
@AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true
|
||||||
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
|
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
|
||||||
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
||||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||||
@@ -64,18 +65,6 @@ struct PrivacySettings: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
settingsRow("lock.doc") {
|
|
||||||
Toggle("Encrypt local files", isOn: $encryptLocalFiles)
|
|
||||||
.onChange(of: encryptLocalFiles) {
|
|
||||||
setEncryptLocalFiles($0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
settingsRow("photo") {
|
|
||||||
Toggle("Auto-accept images", isOn: $autoAcceptImages)
|
|
||||||
.onChange(of: autoAcceptImages) {
|
|
||||||
privacyAcceptImagesGroupDefault.set($0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
settingsRow("network") {
|
settingsRow("network") {
|
||||||
Toggle("Send link previews", isOn: $useLinkPreviews)
|
Toggle("Send link previews", isOn: $useLinkPreviews)
|
||||||
}
|
}
|
||||||
@@ -108,6 +97,32 @@ struct PrivacySettings: View {
|
|||||||
Text("Chats")
|
Text("Chats")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
settingsRow("lock.doc") {
|
||||||
|
Toggle("Encrypt local files", isOn: $encryptLocalFiles)
|
||||||
|
.onChange(of: encryptLocalFiles) {
|
||||||
|
setEncryptLocalFiles($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settingsRow("photo") {
|
||||||
|
Toggle("Auto-accept images", isOn: $autoAcceptImages)
|
||||||
|
.onChange(of: autoAcceptImages) {
|
||||||
|
privacyAcceptImagesGroupDefault.set($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settingsRow("network.badge.shield.half.filled") {
|
||||||
|
Toggle("Protect IP address", isOn: $askToApproveRelays)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Files")
|
||||||
|
} footer: {
|
||||||
|
if askToApproveRelays {
|
||||||
|
Text("The app will ask to confirm downloads from unknown file servers (except .onion).")
|
||||||
|
} else {
|
||||||
|
Text("Without Tor or VPN, your IP address will be visible to file servers.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
settingsRow("person") {
|
settingsRow("person") {
|
||||||
Toggle("Contacts", isOn: $contactReceipts)
|
Toggle("Contacts", isOn: $contactReceipts)
|
||||||
|
|||||||
@@ -696,14 +696,16 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil) -> AChatItem? {
|
func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil) -> AChatItem? {
|
||||||
let r = sendSimpleXCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline))
|
let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get()
|
||||||
|
let r = sendSimpleXCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline))
|
||||||
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
|
if case let .rcvFileAccepted(_, chatItem) = r { return chatItem }
|
||||||
logger.error("receiveFile error: \(responseError(r))")
|
logger.error("receiveFile error: \(responseError(r))")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiSetFileToReceive(fileId: Int64, encrypted: Bool) {
|
func apiSetFileToReceive(fileId: Int64, encrypted: Bool) {
|
||||||
let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId, encrypted: encrypted))
|
let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get()
|
||||||
|
let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted))
|
||||||
if case .cmdOk = r { return }
|
if case .cmdOk = r { return }
|
||||||
logger.error("setFileToReceive error: \(responseError(r))")
|
logger.error("setFileToReceive error: \(responseError(r))")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,11 +76,11 @@
|
|||||||
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; };
|
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */; };
|
||||||
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; };
|
5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D13A2282187BB00AB8B43 /* WebRTC.swift */; };
|
||||||
5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */; };
|
5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */; };
|
||||||
5C9F3DCC2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DC72BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a */; };
|
5C9F3DD62BFBCDD90003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD12BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a */; };
|
||||||
5C9F3DCD2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DC82BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a */; };
|
5C9F3DD72BFBCDD90003B86B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD22BFBCDD80003B86B /* libffi.a */; };
|
||||||
5C9F3DCE2BF7A6900003B86B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DC92BF7A6900003B86B /* libgmp.a */; };
|
5C9F3DD82BFBCDD90003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD32BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a */; };
|
||||||
5C9F3DCF2BF7A6900003B86B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DCA2BF7A6900003B86B /* libgmpxx.a */; };
|
5C9F3DD92BFBCDD90003B86B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD42BFBCDD90003B86B /* libgmpxx.a */; };
|
||||||
5C9F3DD02BF7A6900003B86B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DCB2BF7A6900003B86B /* libffi.a */; };
|
5C9F3DDA2BFBCDD90003B86B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C9F3DD52BFBCDD90003B86B /* libgmp.a */; };
|
||||||
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; };
|
5C9FD96E27A5D6ED0075386C /* SendMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */; };
|
||||||
5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; };
|
5CA059DC279559F40002BEB4 /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DB279559F40002BEB4 /* Tests_iOS.swift */; };
|
||||||
5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; };
|
5CA059DE279559F40002BEB4 /* Tests_iOSLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CA059DD279559F40002BEB4 /* Tests_iOSLaunchTests.swift */; };
|
||||||
@@ -354,11 +354,11 @@
|
|||||||
5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = "<group>"; };
|
5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseEncryptionView.swift; sourceTree = "<group>"; };
|
||||||
5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; };
|
5C9D13A2282187BB00AB8B43 /* WebRTC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRTC.swift; sourceTree = "<group>"; };
|
||||||
5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFile.swift; sourceTree = "<group>"; };
|
5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoFile.swift; sourceTree = "<group>"; };
|
||||||
5C9F3DC72BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a"; sourceTree = "<group>"; };
|
5C9F3DD12BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||||
5C9F3DC82BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a"; sourceTree = "<group>"; };
|
5C9F3DD22BFBCDD80003B86B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||||
5C9F3DC92BF7A6900003B86B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
5C9F3DD32BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a"; sourceTree = "<group>"; };
|
||||||
5C9F3DCA2BF7A6900003B86B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
5C9F3DD42BFBCDD90003B86B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||||
5C9F3DCB2BF7A6900003B86B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
5C9F3DD52BFBCDD90003B86B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||||
5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
|
5C9FD96A27A56D4D0075386C /* JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
|
||||||
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; };
|
5C9FD96D27A5D6ED0075386C /* SendMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageView.swift; sourceTree = "<group>"; };
|
||||||
5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = "<group>"; };
|
5CA059C3279559F40002BEB4 /* SimpleXApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleXApp.swift; sourceTree = "<group>"; };
|
||||||
@@ -529,13 +529,13 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
5C9F3DCF2BF7A6900003B86B /* libgmpxx.a in Frameworks */,
|
5C9F3DD92BFBCDD90003B86B /* libgmpxx.a in Frameworks */,
|
||||||
|
5C9F3DDA2BFBCDD90003B86B /* libgmp.a in Frameworks */,
|
||||||
|
5C9F3DD82BFBCDD90003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a in Frameworks */,
|
||||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||||
5C9F3DCE2BF7A6900003B86B /* libgmp.a in Frameworks */,
|
5C9F3DD62BFBCDD90003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a in Frameworks */,
|
||||||
5C9F3DCC2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a in Frameworks */,
|
|
||||||
5C9F3DD02BF7A6900003B86B /* libffi.a in Frameworks */,
|
|
||||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||||
5C9F3DCD2BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a in Frameworks */,
|
5C9F3DD72BFBCDD90003B86B /* libffi.a in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -601,11 +601,11 @@
|
|||||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5C9F3DCB2BF7A6900003B86B /* libffi.a */,
|
5C9F3DD22BFBCDD80003B86B /* libffi.a */,
|
||||||
5C9F3DC92BF7A6900003B86B /* libgmp.a */,
|
5C9F3DD52BFBCDD90003B86B /* libgmp.a */,
|
||||||
5C9F3DCA2BF7A6900003B86B /* libgmpxx.a */,
|
5C9F3DD42BFBCDD90003B86B /* libgmpxx.a */,
|
||||||
5C9F3DC82BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g-ghc9.6.3.a */,
|
5C9F3DD12BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf-ghc9.6.3.a */,
|
||||||
5C9F3DC72BF7A6900003B86B /* libHSsimplex-chat-5.8.0.1-BrjXjAnJqNV7yWXU89n05g.a */,
|
5C9F3DD32BFBCDD80003B86B /* libHSsimplex-chat-5.8.0.2-8RkLdmy05dJBsgWrGV50Uf.a */,
|
||||||
);
|
);
|
||||||
path = Libraries;
|
path = Libraries;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1552,7 +1552,7 @@
|
|||||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 217;
|
CURRENT_PROJECT_VERSION = 218;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -1601,7 +1601,7 @@
|
|||||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 217;
|
CURRENT_PROJECT_VERSION = 218;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -1687,7 +1687,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 217;
|
CURRENT_PROJECT_VERSION = 218;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
GCC_OPTIMIZATION_LEVEL = s;
|
GCC_OPTIMIZATION_LEVEL = s;
|
||||||
@@ -1724,7 +1724,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 217;
|
CURRENT_PROJECT_VERSION = 218;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_CODE_COVERAGE = NO;
|
ENABLE_CODE_COVERAGE = NO;
|
||||||
@@ -1761,7 +1761,7 @@
|
|||||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 217;
|
CURRENT_PROJECT_VERSION = 218;
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
@@ -1812,7 +1812,7 @@
|
|||||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 217;
|
CURRENT_PROJECT_VERSION = 218;
|
||||||
DEFINES_MODULE = YES;
|
DEFINES_MODULE = YES;
|
||||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
|
|||||||
@@ -123,8 +123,8 @@ public enum ChatCommand {
|
|||||||
case apiGetNetworkStatuses
|
case apiGetNetworkStatuses
|
||||||
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
|
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
|
||||||
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
|
case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool)
|
||||||
case receiveFile(fileId: Int64, encrypted: Bool?, inline: Bool?)
|
case receiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?, inline: Bool?)
|
||||||
case setFileToReceive(fileId: Int64, encrypted: Bool?)
|
case setFileToReceive(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?)
|
||||||
case cancelFile(fileId: Int64)
|
case cancelFile(fileId: Int64)
|
||||||
// remote desktop commands
|
// remote desktop commands
|
||||||
case setLocalDeviceName(displayName: String)
|
case setLocalDeviceName(displayName: String)
|
||||||
@@ -140,7 +140,6 @@ public enum ChatCommand {
|
|||||||
case apiStandaloneFileInfo(url: String)
|
case apiStandaloneFileInfo(url: String)
|
||||||
// misc
|
// misc
|
||||||
case showVersion
|
case showVersion
|
||||||
case setAppLogLevel(appLogLevel: ChatLogLevel)
|
|
||||||
case string(String)
|
case string(String)
|
||||||
|
|
||||||
public var cmdString: String {
|
public var cmdString: String {
|
||||||
@@ -283,8 +282,8 @@ public enum ChatCommand {
|
|||||||
case .apiGetNetworkStatuses: return "/_network_statuses"
|
case .apiGetNetworkStatuses: return "/_network_statuses"
|
||||||
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
|
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
|
||||||
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
|
case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))"
|
||||||
case let .receiveFile(fileId, encrypt, inline): return "/freceive \(fileId)\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))"
|
case let .receiveFile(fileId, userApprovedRelays, encrypt, inline): return "/freceive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))"
|
||||||
case let .setFileToReceive(fileId, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("encrypt", encrypt))"
|
case let .setFileToReceive(fileId, userApprovedRelays, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))"
|
||||||
case let .cancelFile(fileId): return "/fcancel \(fileId)"
|
case let .cancelFile(fileId): return "/fcancel \(fileId)"
|
||||||
case let .setLocalDeviceName(displayName): return "/set device name \(displayName)"
|
case let .setLocalDeviceName(displayName): return "/set device name \(displayName)"
|
||||||
case let .connectRemoteCtrl(xrcpInv): return "/connect remote ctrl \(xrcpInv)"
|
case let .connectRemoteCtrl(xrcpInv): return "/connect remote ctrl \(xrcpInv)"
|
||||||
@@ -298,7 +297,6 @@ public enum ChatCommand {
|
|||||||
case let .apiDownloadStandaloneFile(userId, link, file): return "/_download \(userId) \(link) \(file.filePath)"
|
case let .apiDownloadStandaloneFile(userId, link, file): return "/_download \(userId) \(link) \(file.filePath)"
|
||||||
case let .apiStandaloneFileInfo(link): return "/_download info \(link)"
|
case let .apiStandaloneFileInfo(link): return "/_download info \(link)"
|
||||||
case .showVersion: return "/version"
|
case .showVersion: return "/version"
|
||||||
case let .setAppLogLevel(ll): return "/log \(ll.rawValue)"
|
|
||||||
case let .string(str): return str
|
case let .string(str): return str
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -431,7 +429,6 @@ public enum ChatCommand {
|
|||||||
case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile"
|
case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile"
|
||||||
case .apiStandaloneFileInfo: return "apiStandaloneFileInfo"
|
case .apiStandaloneFileInfo: return "apiStandaloneFileInfo"
|
||||||
case .showVersion: return "showVersion"
|
case .showVersion: return "showVersion"
|
||||||
case .setAppLogLevel: return "setAppLogLevel"
|
|
||||||
case .string: return "console command"
|
case .string: return "console command"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1763,6 +1760,7 @@ public enum ChatErrorType: Decodable {
|
|||||||
case fileImageType(filePath: String)
|
case fileImageType(filePath: String)
|
||||||
case fileImageSize(filePath: String)
|
case fileImageSize(filePath: String)
|
||||||
case fileNotReceived(fileId: Int64)
|
case fileNotReceived(fileId: Int64)
|
||||||
|
case fileNotApproved(fileId: Int64, unknownServers: [String])
|
||||||
// case xFTPRcvFile
|
// case xFTPRcvFile
|
||||||
// case xFTPSndFile
|
// case xFTPSndFile
|
||||||
case fallbackToSMPProhibited(fileId: Int64)
|
case fallbackToSMPProhibited(fileId: Int64)
|
||||||
@@ -2041,6 +2039,7 @@ public struct MigrationFileLinkData: Codable {
|
|||||||
public struct AppSettings: Codable, Equatable {
|
public struct AppSettings: Codable, Equatable {
|
||||||
public var networkConfig: NetCfg? = nil
|
public var networkConfig: NetCfg? = nil
|
||||||
public var privacyEncryptLocalFiles: Bool? = nil
|
public var privacyEncryptLocalFiles: Bool? = nil
|
||||||
|
public var privacyAskToApproveRelays: Bool? = nil
|
||||||
public var privacyAcceptImages: Bool? = nil
|
public var privacyAcceptImages: Bool? = nil
|
||||||
public var privacyLinkPreviews: Bool? = nil
|
public var privacyLinkPreviews: Bool? = nil
|
||||||
public var privacyShowChatPreviews: Bool? = nil
|
public var privacyShowChatPreviews: Bool? = nil
|
||||||
@@ -2064,6 +2063,7 @@ public struct AppSettings: Codable, Equatable {
|
|||||||
let def = AppSettings.defaults
|
let def = AppSettings.defaults
|
||||||
if networkConfig != def.networkConfig { empty.networkConfig = networkConfig }
|
if networkConfig != def.networkConfig { empty.networkConfig = networkConfig }
|
||||||
if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
|
if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
|
||||||
|
if privacyAskToApproveRelays != def.privacyAskToApproveRelays { empty.privacyAskToApproveRelays = privacyAskToApproveRelays }
|
||||||
if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages }
|
if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages }
|
||||||
if privacyLinkPreviews != def.privacyLinkPreviews { empty.privacyLinkPreviews = privacyLinkPreviews }
|
if privacyLinkPreviews != def.privacyLinkPreviews { empty.privacyLinkPreviews = privacyLinkPreviews }
|
||||||
if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews }
|
if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews }
|
||||||
@@ -2088,6 +2088,7 @@ public struct AppSettings: Codable, Equatable {
|
|||||||
AppSettings (
|
AppSettings (
|
||||||
networkConfig: NetCfg.defaults,
|
networkConfig: NetCfg.defaults,
|
||||||
privacyEncryptLocalFiles: true,
|
privacyEncryptLocalFiles: true,
|
||||||
|
privacyAskToApproveRelays: true,
|
||||||
privacyAcceptImages: true,
|
privacyAcceptImages: true,
|
||||||
privacyLinkPreviews: true,
|
privacyLinkPreviews: true,
|
||||||
privacyShowChatPreviews: true,
|
privacyShowChatPreviews: true,
|
||||||
@@ -2170,11 +2171,3 @@ public enum UserNetworkType: String, Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ChatLogLevel: String, Codable, CaseIterable {
|
|
||||||
case debug
|
|
||||||
case info
|
|
||||||
case warn
|
|
||||||
case error
|
|
||||||
case important
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic" // no longer
|
|||||||
let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
|
let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages"
|
||||||
public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used
|
public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used
|
||||||
public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles"
|
public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles"
|
||||||
|
public let GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS = "privacyAskToApproveRelays"
|
||||||
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
|
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
|
||||||
let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
|
let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
|
||||||
let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode"
|
let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode"
|
||||||
@@ -44,7 +45,6 @@ public let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphra
|
|||||||
public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades"
|
public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades"
|
||||||
public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled"
|
public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled"
|
||||||
public let GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED = "pqExperimentalEnabled" // no longer used
|
public let GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED = "pqExperimentalEnabled" // no longer used
|
||||||
public let GROUP_DEFAULT_APP_LOG_LEVEL = "appLogLevel"
|
|
||||||
|
|
||||||
public let APP_GROUP_NAME = "group.chat.simplex.app"
|
public let APP_GROUP_NAME = "group.chat.simplex.app"
|
||||||
|
|
||||||
@@ -74,10 +74,10 @@ public func registerGroupDefaults() {
|
|||||||
GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
|
GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true,
|
||||||
GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false,
|
GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false,
|
||||||
GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true,
|
GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true,
|
||||||
|
GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS: true,
|
||||||
GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false,
|
GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false,
|
||||||
GROUP_DEFAULT_CALL_KIT_ENABLED: true,
|
GROUP_DEFAULT_CALL_KIT_ENABLED: true,
|
||||||
GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED: false,
|
GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED: false,
|
||||||
GROUP_DEFAULT_APP_LOG_LEVEL: ChatLogLevel.important.rawValue,
|
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +183,8 @@ public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults
|
|||||||
|
|
||||||
public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES)
|
public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES)
|
||||||
|
|
||||||
|
public let privacyAskToApproveRelaysGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS)
|
||||||
|
|
||||||
public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT)
|
public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT)
|
||||||
|
|
||||||
public let networkUseOnionHostsGroupDefault = EnumDefault<OnionHosts>(
|
public let networkUseOnionHostsGroupDefault = EnumDefault<OnionHosts>(
|
||||||
@@ -217,12 +219,6 @@ public let confirmDBUpgradesGroupDefault = BoolDefault(defaults: groupDefaults,
|
|||||||
|
|
||||||
public let callKitEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CALL_KIT_ENABLED)
|
public let callKitEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_CALL_KIT_ENABLED)
|
||||||
|
|
||||||
public let appLogLevelGroupDefault = EnumDefault<ChatLogLevel>(
|
|
||||||
defaults: groupDefaults,
|
|
||||||
forKey: GROUP_DEFAULT_APP_LOG_LEVEL,
|
|
||||||
withDefault: .important
|
|
||||||
)
|
|
||||||
|
|
||||||
public class DateDefault {
|
public class DateDefault {
|
||||||
var defaults: UserDefaults
|
var defaults: UserDefaults
|
||||||
var key: String
|
var key: String
|
||||||
|
|||||||
@@ -3174,6 +3174,7 @@ public struct CIFile: Decodable {
|
|||||||
case .rcvInvitation: return false
|
case .rcvInvitation: return false
|
||||||
case .rcvAccepted: return false
|
case .rcvAccepted: return false
|
||||||
case .rcvTransfer: return false
|
case .rcvTransfer: return false
|
||||||
|
case .rcvAborted: return false
|
||||||
case .rcvCancelled: return false
|
case .rcvCancelled: return false
|
||||||
case .rcvComplete: return true
|
case .rcvComplete: return true
|
||||||
case .rcvError: return false
|
case .rcvError: return false
|
||||||
@@ -3198,6 +3199,7 @@ public struct CIFile: Decodable {
|
|||||||
case .rcvInvitation: return nil
|
case .rcvInvitation: return nil
|
||||||
case .rcvAccepted: return rcvCancelAction
|
case .rcvAccepted: return rcvCancelAction
|
||||||
case .rcvTransfer: return rcvCancelAction
|
case .rcvTransfer: return rcvCancelAction
|
||||||
|
case .rcvAborted: return nil
|
||||||
case .rcvCancelled: return nil
|
case .rcvCancelled: return nil
|
||||||
case .rcvComplete: return nil
|
case .rcvComplete: return nil
|
||||||
case .rcvError: return nil
|
case .rcvError: return nil
|
||||||
@@ -3312,6 +3314,7 @@ public enum CIFileStatus: Decodable, Equatable {
|
|||||||
case rcvInvitation
|
case rcvInvitation
|
||||||
case rcvAccepted
|
case rcvAccepted
|
||||||
case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64)
|
case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64)
|
||||||
|
case rcvAborted
|
||||||
case rcvComplete
|
case rcvComplete
|
||||||
case rcvCancelled
|
case rcvCancelled
|
||||||
case rcvError
|
case rcvError
|
||||||
@@ -3327,6 +3330,7 @@ public enum CIFileStatus: Decodable, Equatable {
|
|||||||
case .rcvInvitation: return "rcvInvitation"
|
case .rcvInvitation: return "rcvInvitation"
|
||||||
case .rcvAccepted: return "rcvAccepted"
|
case .rcvAccepted: return "rcvAccepted"
|
||||||
case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)"
|
case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)"
|
||||||
|
case .rcvAborted: return "rcvAborted"
|
||||||
case .rcvComplete: return "rcvComplete"
|
case .rcvComplete: return "rcvComplete"
|
||||||
case .rcvCancelled: return "rcvCancelled"
|
case .rcvCancelled: return "rcvCancelled"
|
||||||
case .rcvError: return "rcvError"
|
case .rcvError: return "rcvError"
|
||||||
|
|||||||
@@ -2644,6 +2644,7 @@ data class CIFile(
|
|||||||
is CIFileStatus.RcvInvitation -> false
|
is CIFileStatus.RcvInvitation -> false
|
||||||
is CIFileStatus.RcvAccepted -> false
|
is CIFileStatus.RcvAccepted -> false
|
||||||
is CIFileStatus.RcvTransfer -> false
|
is CIFileStatus.RcvTransfer -> false
|
||||||
|
is CIFileStatus.RcvAborted -> false
|
||||||
is CIFileStatus.RcvCancelled -> false
|
is CIFileStatus.RcvCancelled -> false
|
||||||
is CIFileStatus.RcvComplete -> true
|
is CIFileStatus.RcvComplete -> true
|
||||||
is CIFileStatus.RcvError -> false
|
is CIFileStatus.RcvError -> false
|
||||||
@@ -2665,6 +2666,7 @@ data class CIFile(
|
|||||||
is CIFileStatus.RcvInvitation -> null
|
is CIFileStatus.RcvInvitation -> null
|
||||||
is CIFileStatus.RcvAccepted -> rcvCancelAction
|
is CIFileStatus.RcvAccepted -> rcvCancelAction
|
||||||
is CIFileStatus.RcvTransfer -> rcvCancelAction
|
is CIFileStatus.RcvTransfer -> rcvCancelAction
|
||||||
|
is CIFileStatus.RcvAborted -> null
|
||||||
is CIFileStatus.RcvCancelled -> null
|
is CIFileStatus.RcvCancelled -> null
|
||||||
is CIFileStatus.RcvComplete -> null
|
is CIFileStatus.RcvComplete -> null
|
||||||
is CIFileStatus.RcvError -> null
|
is CIFileStatus.RcvError -> null
|
||||||
@@ -2845,6 +2847,7 @@ sealed class CIFileStatus {
|
|||||||
@Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus()
|
@Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus()
|
||||||
@Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus()
|
@Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus()
|
||||||
@Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus()
|
@Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus()
|
||||||
|
@Serializable @SerialName("rcvAborted") object RcvAborted: CIFileStatus()
|
||||||
@Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus()
|
@Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus()
|
||||||
@Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus()
|
@Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus()
|
||||||
@Serializable @SerialName("rcvError") object RcvError: CIFileStatus()
|
@Serializable @SerialName("rcvError") object RcvError: CIFileStatus()
|
||||||
@@ -2859,6 +2862,7 @@ sealed class CIFileStatus {
|
|||||||
is RcvInvitation -> false
|
is RcvInvitation -> false
|
||||||
is RcvAccepted -> false
|
is RcvAccepted -> false
|
||||||
is RcvTransfer -> false
|
is RcvTransfer -> false
|
||||||
|
is RcvAborted -> false
|
||||||
is RcvComplete -> false
|
is RcvComplete -> false
|
||||||
is RcvCancelled -> false
|
is RcvCancelled -> false
|
||||||
is RcvError -> false
|
is RcvError -> false
|
||||||
|
|||||||
+40
-8
@@ -12,6 +12,7 @@ import dev.icerock.moko.resources.compose.painterResource
|
|||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
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.chat.group.toggleShowMemberMessages
|
||||||
import chat.simplex.common.views.migration.MigrationFileLinkData
|
import chat.simplex.common.views.migration.MigrationFileLinkData
|
||||||
import chat.simplex.common.views.onboarding.OnboardingStage
|
import chat.simplex.common.views.onboarding.OnboardingStage
|
||||||
import chat.simplex.common.views.usersettings.*
|
import chat.simplex.common.views.usersettings.*
|
||||||
@@ -106,6 +107,7 @@ class AppPreferences {
|
|||||||
val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true)
|
val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true)
|
||||||
val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false)
|
val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false)
|
||||||
val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true)
|
val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true)
|
||||||
|
val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true)
|
||||||
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
|
val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false)
|
||||||
val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false)
|
val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false)
|
||||||
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
|
val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null)
|
||||||
@@ -292,6 +294,7 @@ class AppPreferences {
|
|||||||
private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft"
|
private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft"
|
||||||
private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet"
|
private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet"
|
||||||
private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles"
|
private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles"
|
||||||
|
private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays"
|
||||||
const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
|
const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup"
|
||||||
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
|
private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls"
|
||||||
private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites"
|
private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites"
|
||||||
@@ -1337,9 +1340,9 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun apiReceiveFile(rh: Long?, fileId: Long, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? {
|
suspend fun apiReceiveFile(rh: Long?, fileId: Long, userApprovedRelays: Boolean, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? {
|
||||||
// -1 here is to override default behavior of providing current remote host id because file can be asked by local device while remote is connected
|
// -1 here is to override default behavior of providing current remote host id because file can be asked by local device while remote is connected
|
||||||
val r = sendCmd(rh, CC.ReceiveFile(fileId, encrypted, inline))
|
val r = sendCmd(rh, CC.ReceiveFile(fileId, userApprovedRelays = userApprovedRelays, encrypt = encrypted, inline = inline))
|
||||||
return when (r) {
|
return when (r) {
|
||||||
is CR.RcvFileAccepted -> r.chatItem
|
is CR.RcvFileAccepted -> r.chatItem
|
||||||
is CR.RcvFileAcceptedSndCancelled -> {
|
is CR.RcvFileAcceptedSndCancelled -> {
|
||||||
@@ -1358,7 +1361,23 @@ object ChatController {
|
|||||||
val maybeChatError = chatError(r)
|
val maybeChatError = chatError(r)
|
||||||
if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) {
|
if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) {
|
||||||
Log.d(TAG, "apiReceiveFile ignoring FileCancelled or FileAlreadyReceiving error")
|
Log.d(TAG, "apiReceiveFile ignoring FileCancelled or FileAlreadyReceiving error")
|
||||||
} else {
|
} else if (maybeChatError is ChatErrorType.FileNotApproved) {
|
||||||
|
Log.d(TAG, "apiReceiveFile FileNotApproved error")
|
||||||
|
if (!auto) {
|
||||||
|
val srvs = maybeChatError.unknownServers.map{ serverHostname(it) }
|
||||||
|
AlertManager.shared.showAlertDialog(
|
||||||
|
title = generalGetString(MR.strings.file_not_approved_title),
|
||||||
|
text = generalGetString(MR.strings.file_not_approved_descr).format(srvs.sorted().joinToString(separator = ", ")),
|
||||||
|
confirmText = generalGetString(MR.strings.download_file),
|
||||||
|
onConfirm = {
|
||||||
|
val user = chatModel.currentUser.value
|
||||||
|
if (user != null) {
|
||||||
|
withBGApi { chatModel.controller.receiveFile(rh, user, fileId, userApprovedRelays = true) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (!auto) {
|
||||||
apiErrorAlert("apiReceiveFile", generalGetString(MR.strings.error_receiving_file), r)
|
apiErrorAlert("apiReceiveFile", generalGetString(MR.strings.error_receiving_file), r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2216,9 +2235,14 @@ object ChatController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, auto: Boolean = false) {
|
suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, userApprovedRelays: Boolean = false, auto: Boolean = false) {
|
||||||
val encrypted = appPrefs.privacyEncryptLocalFiles.get()
|
val chatItem = apiReceiveFile(
|
||||||
val chatItem = apiReceiveFile(rhId, fileId, encrypted = encrypted, auto = auto)
|
rhId,
|
||||||
|
fileId,
|
||||||
|
userApprovedRelays = userApprovedRelays || !appPrefs.privacyAskToApproveRelays.get(),
|
||||||
|
encrypted = appPrefs.privacyEncryptLocalFiles.get(),
|
||||||
|
auto = auto
|
||||||
|
)
|
||||||
if (chatItem != null) {
|
if (chatItem != null) {
|
||||||
chatItemSimpleUpdate(rhId, user, chatItem)
|
chatItemSimpleUpdate(rhId, user, chatItem)
|
||||||
}
|
}
|
||||||
@@ -2501,7 +2525,7 @@ sealed class CC {
|
|||||||
class ApiRejectContact(val contactReqId: Long): CC()
|
class ApiRejectContact(val contactReqId: Long): CC()
|
||||||
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
|
class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC()
|
||||||
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
|
class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC()
|
||||||
class ReceiveFile(val fileId: Long, val encrypt: Boolean, val inline: Boolean?): CC()
|
class ReceiveFile(val fileId: Long, val userApprovedRelays: Boolean, val encrypt: Boolean, val inline: Boolean?): CC()
|
||||||
class CancelFile(val fileId: Long): CC()
|
class CancelFile(val fileId: Long): CC()
|
||||||
// Remote control
|
// Remote control
|
||||||
class SetLocalDeviceName(val displayName: String): CC()
|
class SetLocalDeviceName(val displayName: String): CC()
|
||||||
@@ -2652,6 +2676,7 @@ sealed class CC {
|
|||||||
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
|
is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}"
|
||||||
is ReceiveFile ->
|
is ReceiveFile ->
|
||||||
"/freceive $fileId" +
|
"/freceive $fileId" +
|
||||||
|
(" approved_relays=${onOff(userApprovedRelays)}") +
|
||||||
(if (encrypt == null) "" else " encrypt=${onOff(encrypt)}") +
|
(if (encrypt == null) "" else " encrypt=${onOff(encrypt)}") +
|
||||||
(if (inline == null) "" else " inline=${onOff(inline)}")
|
(if (inline == null) "" else " inline=${onOff(inline)}")
|
||||||
is CancelFile -> "/fcancel $fileId"
|
is CancelFile -> "/fcancel $fileId"
|
||||||
@@ -4892,13 +4917,14 @@ sealed class ChatErrorType {
|
|||||||
is FileCancel -> "fileCancel"
|
is FileCancel -> "fileCancel"
|
||||||
is FileAlreadyExists -> "fileAlreadyExists"
|
is FileAlreadyExists -> "fileAlreadyExists"
|
||||||
is FileRead -> "fileRead"
|
is FileRead -> "fileRead"
|
||||||
is FileWrite -> "fileWrite"
|
is FileWrite -> "fileWrite $message"
|
||||||
is FileSend -> "fileSend"
|
is FileSend -> "fileSend"
|
||||||
is FileRcvChunk -> "fileRcvChunk"
|
is FileRcvChunk -> "fileRcvChunk"
|
||||||
is FileInternal -> "fileInternal"
|
is FileInternal -> "fileInternal"
|
||||||
is FileImageType -> "fileImageType"
|
is FileImageType -> "fileImageType"
|
||||||
is FileImageSize -> "fileImageSize"
|
is FileImageSize -> "fileImageSize"
|
||||||
is FileNotReceived -> "fileNotReceived"
|
is FileNotReceived -> "fileNotReceived"
|
||||||
|
is FileNotApproved -> "fileNotApproved"
|
||||||
// is XFTPRcvFile -> "xftpRcvFile"
|
// is XFTPRcvFile -> "xftpRcvFile"
|
||||||
// is XFTPSndFile -> "xftpSndFile"
|
// is XFTPSndFile -> "xftpSndFile"
|
||||||
is FallbackToSMPProhibited -> "fallbackToSMPProhibited"
|
is FallbackToSMPProhibited -> "fallbackToSMPProhibited"
|
||||||
@@ -4978,6 +5004,7 @@ sealed class ChatErrorType {
|
|||||||
@Serializable @SerialName("fileImageType") class FileImageType(val filePath: String): ChatErrorType()
|
@Serializable @SerialName("fileImageType") class FileImageType(val filePath: String): ChatErrorType()
|
||||||
@Serializable @SerialName("fileImageSize") class FileImageSize(val filePath: String): ChatErrorType()
|
@Serializable @SerialName("fileImageSize") class FileImageSize(val filePath: String): ChatErrorType()
|
||||||
@Serializable @SerialName("fileNotReceived") class FileNotReceived(val fileId: Long): ChatErrorType()
|
@Serializable @SerialName("fileNotReceived") class FileNotReceived(val fileId: Long): ChatErrorType()
|
||||||
|
@Serializable @SerialName("fileNotApproved") class FileNotApproved(val fileId: Long, val unknownServers: List<String>): ChatErrorType()
|
||||||
// @Serializable @SerialName("xFTPRcvFile") object XFTPRcvFile: ChatErrorType()
|
// @Serializable @SerialName("xFTPRcvFile") object XFTPRcvFile: ChatErrorType()
|
||||||
// @Serializable @SerialName("xFTPSndFile") object XFTPSndFile: ChatErrorType()
|
// @Serializable @SerialName("xFTPSndFile") object XFTPSndFile: ChatErrorType()
|
||||||
@Serializable @SerialName("fallbackToSMPProhibited") class FallbackToSMPProhibited(val fileId: Long): ChatErrorType()
|
@Serializable @SerialName("fallbackToSMPProhibited") class FallbackToSMPProhibited(val fileId: Long): ChatErrorType()
|
||||||
@@ -5476,6 +5503,7 @@ enum class NotificationsMode() {
|
|||||||
data class AppSettings(
|
data class AppSettings(
|
||||||
var networkConfig: NetCfg? = null,
|
var networkConfig: NetCfg? = null,
|
||||||
var privacyEncryptLocalFiles: Boolean? = null,
|
var privacyEncryptLocalFiles: Boolean? = null,
|
||||||
|
var privacyAskToApproveRelays: Boolean? = null,
|
||||||
var privacyAcceptImages: Boolean? = null,
|
var privacyAcceptImages: Boolean? = null,
|
||||||
var privacyLinkPreviews: Boolean? = null,
|
var privacyLinkPreviews: Boolean? = null,
|
||||||
var privacyShowChatPreviews: Boolean? = null,
|
var privacyShowChatPreviews: Boolean? = null,
|
||||||
@@ -5499,6 +5527,7 @@ data class AppSettings(
|
|||||||
val def = defaults
|
val def = defaults
|
||||||
if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig }
|
if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig }
|
||||||
if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
|
if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
|
||||||
|
if (privacyAskToApproveRelays != def.privacyAskToApproveRelays) { empty.privacyAskToApproveRelays = privacyAskToApproveRelays }
|
||||||
if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages }
|
if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages }
|
||||||
if (privacyLinkPreviews != def.privacyLinkPreviews) { empty.privacyLinkPreviews = privacyLinkPreviews }
|
if (privacyLinkPreviews != def.privacyLinkPreviews) { empty.privacyLinkPreviews = privacyLinkPreviews }
|
||||||
if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews }
|
if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews }
|
||||||
@@ -5530,6 +5559,7 @@ data class AppSettings(
|
|||||||
setNetCfg(net)
|
setNetCfg(net)
|
||||||
}
|
}
|
||||||
privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) }
|
privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) }
|
||||||
|
privacyAskToApproveRelays?.let { def.privacyAskToApproveRelays.set(it) }
|
||||||
privacyAcceptImages?.let { def.privacyAcceptImages.set(it) }
|
privacyAcceptImages?.let { def.privacyAcceptImages.set(it) }
|
||||||
privacyLinkPreviews?.let { def.privacyLinkPreviews.set(it) }
|
privacyLinkPreviews?.let { def.privacyLinkPreviews.set(it) }
|
||||||
privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) }
|
privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) }
|
||||||
@@ -5554,6 +5584,7 @@ data class AppSettings(
|
|||||||
get() = AppSettings(
|
get() = AppSettings(
|
||||||
networkConfig = NetCfg.defaults,
|
networkConfig = NetCfg.defaults,
|
||||||
privacyEncryptLocalFiles = true,
|
privacyEncryptLocalFiles = true,
|
||||||
|
privacyAskToApproveRelays = true,
|
||||||
privacyAcceptImages = true,
|
privacyAcceptImages = true,
|
||||||
privacyLinkPreviews = true,
|
privacyLinkPreviews = true,
|
||||||
privacyShowChatPreviews = true,
|
privacyShowChatPreviews = true,
|
||||||
@@ -5579,6 +5610,7 @@ data class AppSettings(
|
|||||||
return defaults.copy(
|
return defaults.copy(
|
||||||
networkConfig = getNetCfg(),
|
networkConfig = getNetCfg(),
|
||||||
privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(),
|
privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(),
|
||||||
|
privacyAskToApproveRelays = def.privacyAskToApproveRelays.get(),
|
||||||
privacyAcceptImages = def.privacyAcceptImages.get(),
|
privacyAcceptImages = def.privacyAcceptImages.get(),
|
||||||
privacyLinkPreviews = def.privacyLinkPreviews.get(),
|
privacyLinkPreviews = def.privacyLinkPreviews.get(),
|
||||||
privacyShowChatPreviews = def.privacyShowChatPreviews.get(),
|
privacyShowChatPreviews = def.privacyShowChatPreviews.get(),
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,5 @@
|
|||||||
package chat.simplex.common.views.chat.item
|
package chat.simplex.common.views.chat.item
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.CornerSize
|
import androidx.compose.foundation.shape.CornerSize
|
||||||
@@ -64,7 +63,7 @@ fun CIFileView(
|
|||||||
fun fileAction() {
|
fun fileAction() {
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
when {
|
when {
|
||||||
file.fileStatus is CIFileStatus.RcvInvitation -> {
|
file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted -> {
|
||||||
if (fileSizeValid(file)) {
|
if (fileSizeValid(file)) {
|
||||||
receiveFile(file.fileId)
|
receiveFile(file.fileId)
|
||||||
} else {
|
} else {
|
||||||
@@ -176,6 +175,8 @@ fun CIFileView(
|
|||||||
} else {
|
} else {
|
||||||
progressIndicator()
|
progressIndicator()
|
||||||
}
|
}
|
||||||
|
is CIFileStatus.RcvAborted ->
|
||||||
|
fileIcon(innerIcon = painterResource(MR.images.ic_sync_problem), color = MaterialTheme.colors.primary)
|
||||||
is CIFileStatus.RcvComplete -> fileIcon()
|
is CIFileStatus.RcvComplete -> fileIcon()
|
||||||
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||||
is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close))
|
||||||
|
|||||||
+3
-7
@@ -21,17 +21,12 @@ import chat.simplex.common.platform.*
|
|||||||
import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH
|
import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
import dev.icerock.moko.resources.StringResource
|
import dev.icerock.moko.resources.StringResource
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import java.io.File
|
|
||||||
import java.net.URI
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CIImageView(
|
fun CIImageView(
|
||||||
image: String,
|
image: String,
|
||||||
file: CIFile?,
|
file: CIFile?,
|
||||||
metaColor: Color,
|
|
||||||
imageProvider: () -> ImageGalleryProvider,
|
imageProvider: () -> ImageGalleryProvider,
|
||||||
showMenu: MutableState<Boolean>,
|
showMenu: MutableState<Boolean>,
|
||||||
receiveFile: (Long) -> Unit
|
receiveFile: (Long) -> Unit
|
||||||
@@ -51,7 +46,7 @@ fun CIImageView(
|
|||||||
icon,
|
icon,
|
||||||
stringResource(stringId),
|
stringResource(stringId),
|
||||||
Modifier.fillMaxSize(),
|
Modifier.fillMaxSize(),
|
||||||
tint = metaColor
|
tint = Color.White
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,6 +73,7 @@ fun CIImageView(
|
|||||||
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive)
|
is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive)
|
||||||
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image)
|
is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image)
|
||||||
is CIFileStatus.RcvTransfer -> progressIndicator()
|
is CIFileStatus.RcvTransfer -> progressIndicator()
|
||||||
|
is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file)
|
||||||
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||||
is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||||
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)
|
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)
|
||||||
@@ -206,7 +202,7 @@ fun CIImageView(
|
|||||||
imageView(base64ToBitmap(image), onClick = {
|
imageView(base64ToBitmap(image), onClick = {
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
when (file.fileStatus) {
|
when (file.fileStatus) {
|
||||||
CIFileStatus.RcvInvitation ->
|
CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted ->
|
||||||
if (fileSizeValid()) {
|
if (fileSizeValid()) {
|
||||||
receiveFile(file.fileId)
|
receiveFile(file.fileId)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+3
-2
@@ -73,7 +73,7 @@ fun CIVideoView(
|
|||||||
VideoPreviewImageView(preview, onClick = {
|
VideoPreviewImageView(preview, onClick = {
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
when (file.fileStatus) {
|
when (file.fileStatus) {
|
||||||
CIFileStatus.RcvInvitation ->
|
CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted ->
|
||||||
receiveFileIfValidSize(file, receiveFile)
|
receiveFileIfValidSize(file, receiveFile)
|
||||||
CIFileStatus.RcvAccepted ->
|
CIFileStatus.RcvAccepted ->
|
||||||
when (file.fileProtocol) {
|
when (file.fileProtocol) {
|
||||||
@@ -102,7 +102,7 @@ fun CIVideoView(
|
|||||||
if (file != null) {
|
if (file != null) {
|
||||||
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
|
DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/)
|
||||||
}
|
}
|
||||||
if (file?.fileStatus is CIFileStatus.RcvInvitation) {
|
if (file?.fileStatus is CIFileStatus.RcvInvitation || file?.fileStatus is CIFileStatus.RcvAborted) {
|
||||||
PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) }
|
PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -396,6 +396,7 @@ private fun loadingIndicator(file: CIFile?) {
|
|||||||
} else {
|
} else {
|
||||||
progressIndicator()
|
progressIndicator()
|
||||||
}
|
}
|
||||||
|
is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file)
|
||||||
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||||
is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file)
|
||||||
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)
|
is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file)
|
||||||
|
|||||||
+6
-2
@@ -22,6 +22,7 @@ import chat.simplex.common.views.helpers.*
|
|||||||
import chat.simplex.common.model.*
|
import chat.simplex.common.model.*
|
||||||
import chat.simplex.common.platform.*
|
import chat.simplex.common.platform.*
|
||||||
import chat.simplex.res.MR
|
import chat.simplex.res.MR
|
||||||
|
import dev.icerock.moko.resources.ImageResource
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
// TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901
|
// TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901
|
||||||
@@ -220,7 +221,8 @@ private fun PlayPauseButton(
|
|||||||
error: Boolean,
|
error: Boolean,
|
||||||
play: () -> Unit,
|
play: () -> Unit,
|
||||||
pause: () -> Unit,
|
pause: () -> Unit,
|
||||||
longClick: () -> Unit
|
longClick: () -> Unit,
|
||||||
|
icon: ImageResource = MR.images.ic_play_arrow_filled,
|
||||||
) {
|
) {
|
||||||
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage
|
||||||
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage
|
||||||
@@ -241,7 +243,7 @@ private fun PlayPauseButton(
|
|||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled),
|
if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(icon),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
Modifier.size(36.dp),
|
Modifier.size(36.dp),
|
||||||
tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||||
@@ -294,6 +296,8 @@ private fun VoiceMsgIndicator(
|
|||||||
) {
|
) {
|
||||||
ProgressIndicator()
|
ProgressIndicator()
|
||||||
}
|
}
|
||||||
|
} else if (file?.fileStatus is CIFileStatus.RcvAborted) {
|
||||||
|
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick, icon = MR.images.ic_sync_problem)
|
||||||
} else {
|
} else {
|
||||||
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick)
|
PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick)
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -241,7 +241,7 @@ fun FramedItemView(
|
|||||||
} else {
|
} else {
|
||||||
when (val mc = ci.content.msgContent) {
|
when (val mc = ci.content.msgContent) {
|
||||||
is MsgContent.MCImage -> {
|
is MsgContent.MCImage -> {
|
||||||
CIImageView(image = mc.image, file = ci.file, metaColor = metaColor, imageProvider ?: return@PriorityLayout, showMenu, receiveFile)
|
CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile)
|
||||||
if (mc.text == "" && !ci.meta.isLive) {
|
if (mc.text == "" && !ci.meta.isLive) {
|
||||||
metaColor = Color.White
|
metaColor = Color.White
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+11
-11
@@ -250,17 +250,17 @@ fun NetworkAndServersView() {
|
|||||||
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
|
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (currentRemoteHost == null) {
|
if (currentRemoteHost == null) {
|
||||||
// SectionView(generalGetString(MR.strings.settings_section_title_private_message_routing)) {
|
SectionView(generalGetString(MR.strings.settings_section_title_private_message_routing)) {
|
||||||
// SMPProxyModePicker(smpProxyMode, showModal, updateSMPProxyMode)
|
SMPProxyModePicker(smpProxyMode, showModal, updateSMPProxyMode)
|
||||||
// SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { mutableStateOf(smpProxyMode.value != SMPProxyMode.Never) })
|
SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { mutableStateOf(smpProxyMode.value != SMPProxyMode.Never) })
|
||||||
// SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy)
|
SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy)
|
||||||
// }
|
}
|
||||||
// SectionCustomFooter {
|
SectionCustomFooter {
|
||||||
// Text(stringResource(MR.strings.private_routing_explanation))
|
Text(stringResource(MR.strings.private_routing_explanation))
|
||||||
// }
|
}
|
||||||
// Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
|
Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp))
|
||||||
// }
|
}
|
||||||
|
|
||||||
SectionView(generalGetString(MR.strings.settings_section_title_calls)) {
|
SectionView(generalGetString(MR.strings.settings_section_title_calls)) {
|
||||||
SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } })
|
SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } })
|
||||||
|
|||||||
+18
-5
@@ -1,6 +1,7 @@
|
|||||||
package chat.simplex.common.views.usersettings
|
package chat.simplex.common.views.usersettings
|
||||||
|
|
||||||
import SectionBottomSpacer
|
import SectionBottomSpacer
|
||||||
|
import SectionCustomFooter
|
||||||
import SectionDividerSpaced
|
import SectionDividerSpaced
|
||||||
import SectionItemView
|
import SectionItemView
|
||||||
import SectionTextFooter
|
import SectionTextFooter
|
||||||
@@ -63,10 +64,6 @@ fun PrivacySettingsView(
|
|||||||
SectionDividerSpaced()
|
SectionDividerSpaced()
|
||||||
|
|
||||||
SectionView(stringResource(MR.strings.settings_section_title_chats)) {
|
SectionView(stringResource(MR.strings.settings_section_title_chats)) {
|
||||||
SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable ->
|
|
||||||
withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) }
|
|
||||||
})
|
|
||||||
SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
|
|
||||||
SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
|
SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews)
|
||||||
SettingsPreferenceItem(
|
SettingsPreferenceItem(
|
||||||
painterResource(MR.images.ic_chat_bubble),
|
painterResource(MR.images.ic_chat_bubble),
|
||||||
@@ -91,6 +88,22 @@ fun PrivacySettingsView(
|
|||||||
chatModel.simplexLinkMode.value = it
|
chatModel.simplexLinkMode.value = it
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
SectionDividerSpaced()
|
||||||
|
|
||||||
|
SectionView(stringResource(MR.strings.settings_section_title_files)) {
|
||||||
|
SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable ->
|
||||||
|
withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) }
|
||||||
|
})
|
||||||
|
SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages)
|
||||||
|
SettingsPreferenceItem(painterResource(MR.images.ic_security), stringResource(MR.strings.protect_ip_address), chatModel.controller.appPrefs.privacyAskToApproveRelays)
|
||||||
|
}
|
||||||
|
SectionCustomFooter {
|
||||||
|
if (chatModel.controller.appPrefs.privacyAskToApproveRelays.state.value) {
|
||||||
|
Text(stringResource(MR.strings.app_will_ask_to_confirm_unknown_file_servers))
|
||||||
|
} else {
|
||||||
|
Text(stringResource(MR.strings.without_tor_or_vpn_ip_address_will_be_visible_to_file_servers))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val currentUser = chatModel.currentUser.value
|
val currentUser = chatModel.currentUser.value
|
||||||
if (currentUser != null) {
|
if (currentUser != null) {
|
||||||
@@ -141,7 +154,7 @@ fun PrivacySettingsView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!chatModel.desktopNoUserNoRemote) {
|
if (!chatModel.desktopNoUserNoRemote) {
|
||||||
SectionDividerSpaced()
|
SectionDividerSpaced(maxTopPadding = true)
|
||||||
DeliveryReceiptsSection(
|
DeliveryReceiptsSection(
|
||||||
currentUser = currentUser,
|
currentUser = currentUser,
|
||||||
setOrAskSendReceiptsContacts = { enable ->
|
setOrAskSendReceiptsContacts = { enable ->
|
||||||
|
|||||||
@@ -119,6 +119,8 @@
|
|||||||
<string name="error_joining_group">Error joining group</string>
|
<string name="error_joining_group">Error joining group</string>
|
||||||
<string name="cannot_receive_file">Cannot receive file</string>
|
<string name="cannot_receive_file">Cannot receive file</string>
|
||||||
<string name="sender_cancelled_file_transfer">Sender cancelled file transfer.</string>
|
<string name="sender_cancelled_file_transfer">Sender cancelled file transfer.</string>
|
||||||
|
<string name="file_not_approved_title">Unknown servers!</string>
|
||||||
|
<string name="file_not_approved_descr">Without Tor or VPN, your IP address will be visible to these XFTP relays:\n%1$s.</string>
|
||||||
<string name="error_receiving_file">Error receiving file</string>
|
<string name="error_receiving_file">Error receiving file</string>
|
||||||
<string name="error_creating_address">Error creating address</string>
|
<string name="error_creating_address">Error creating address</string>
|
||||||
<string name="contact_already_exists">Contact already exists</string>
|
<string name="contact_already_exists">Contact already exists</string>
|
||||||
@@ -992,6 +994,9 @@
|
|||||||
<string name="protect_app_screen">Protect app screen</string>
|
<string name="protect_app_screen">Protect app screen</string>
|
||||||
<string name="encrypt_local_files">Encrypt local files</string>
|
<string name="encrypt_local_files">Encrypt local files</string>
|
||||||
<string name="auto_accept_images">Auto-accept images</string>
|
<string name="auto_accept_images">Auto-accept images</string>
|
||||||
|
<string name="protect_ip_address">Protect IP address</string>
|
||||||
|
<string name="app_will_ask_to_confirm_unknown_file_servers">The app will ask to confirm downloads from unknown file servers (except .onion or when SOCKS proxy is enabled).</string>
|
||||||
|
<string name="without_tor_or_vpn_ip_address_will_be_visible_to_file_servers">Without Tor or VPN, your IP address will be visible to file servers.</string>
|
||||||
<string name="send_link_previews">Send link previews</string>
|
<string name="send_link_previews">Send link previews</string>
|
||||||
<string name="privacy_show_last_messages">Show last messages</string>
|
<string name="privacy_show_last_messages">Show last messages</string>
|
||||||
<string name="privacy_message_draft">Message draft</string>
|
<string name="privacy_message_draft">Message draft</string>
|
||||||
@@ -1056,6 +1061,7 @@
|
|||||||
<string name="settings_section_title_app">APP</string>
|
<string name="settings_section_title_app">APP</string>
|
||||||
<string name="settings_section_title_device">DEVICE</string>
|
<string name="settings_section_title_device">DEVICE</string>
|
||||||
<string name="settings_section_title_chats">CHATS</string>
|
<string name="settings_section_title_chats">CHATS</string>
|
||||||
|
<string name="settings_section_title_files">FILES</string>
|
||||||
<string name="settings_section_title_delivery_receipts">SEND DELIVERY RECEIPTS TO</string>
|
<string name="settings_section_title_delivery_receipts">SEND DELIVERY RECEIPTS TO</string>
|
||||||
<string name="settings_restart_app">Restart</string>
|
<string name="settings_restart_app">Restart</string>
|
||||||
<string name="settings_shutdown">Shutdown</string>
|
<string name="settings_shutdown">Shutdown</string>
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ android.enableJetifier=true
|
|||||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||||
kotlin.jvm.target=11
|
kotlin.jvm.target=11
|
||||||
|
|
||||||
android.version_name=5.8-beta.1
|
android.version_name=5.8-beta.2
|
||||||
android.version_code=209
|
android.version_code=210
|
||||||
|
|
||||||
desktop.version_name=5.8-beta.1
|
desktop.version_name=5.8-beta.2
|
||||||
desktop.version_code=46
|
desktop.version_code=47
|
||||||
|
|
||||||
kotlin.version=1.9.23
|
kotlin.version=1.9.23
|
||||||
gradle.plugin.version=8.2.0
|
gradle.plugin.version=8.2.0
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import Directory.Search
|
|||||||
import Directory.Store
|
import Directory.Store
|
||||||
import Simplex.Chat.Bot
|
import Simplex.Chat.Bot
|
||||||
import Simplex.Chat.Bot.KnownContacts
|
import Simplex.Chat.Bot.KnownContacts
|
||||||
import Simplex.Chat.Controller hiding (logError, logInfo)
|
import Simplex.Chat.Controller
|
||||||
import Simplex.Chat.Core
|
import Simplex.Chat.Core
|
||||||
import Simplex.Chat.Messages
|
import Simplex.Chat.Messages
|
||||||
import Simplex.Chat.Options
|
import Simplex.Chat.Options
|
||||||
@@ -586,8 +586,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi
|
|||||||
sendChatCmdStr cc cmdStr >>= \r -> do
|
sendChatCmdStr cc cmdStr >>= \r -> do
|
||||||
ts <- getCurrentTime
|
ts <- getCurrentTime
|
||||||
tz <- getCurrentTimeZone
|
tz <- getCurrentTimeZone
|
||||||
ll <- readTVarIO $ appLogLevel cc
|
sendReply $ serializeChatResponse (Nothing, Just user) ts tz Nothing r
|
||||||
sendReply $ serializeChatResponse (Nothing, Just user) ll ts tz Nothing r
|
|
||||||
DCCommandError tag -> sendReply $ "Command error: " <> show tag
|
DCCommandError tag -> sendReply $ "Command error: " <> show tag
|
||||||
| otherwise = sendReply "You are not allowed to use this command"
|
| otherwise = sendReply "You are not allowed to use this command"
|
||||||
where
|
where
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
|||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
location: https://github.com/simplex-chat/simplexmq.git
|
location: https://github.com/simplex-chat/simplexmq.git
|
||||||
tag: 71489fe6fca70f32f18137186bab2b77304f11b0
|
tag: e3f5d244c1a435593e33adc023bf1f920f379f8d
|
||||||
|
|
||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
name: simplex-chat
|
name: simplex-chat
|
||||||
version: 5.8.0.1
|
version: 5.8.0.2
|
||||||
#synopsis:
|
#synopsis:
|
||||||
#description:
|
#description:
|
||||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"https://github.com/simplex-chat/simplexmq.git"."71489fe6fca70f32f18137186bab2b77304f11b0" = "18inrqiab269w7dw1isjrijgnycv0dz0bp1y9sq5z9fykvwg5rlh";
|
"https://github.com/simplex-chat/simplexmq.git"."e3f5d244c1a435593e33adc023bf1f920f379f8d" = "1klin78kgvgzdvf64nahn3280m7hw5f8wzrca43cmyajm2qp3wfs";
|
||||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
|
||||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
|||||||
-- see: https://github.com/sol/hpack
|
-- see: https://github.com/sol/hpack
|
||||||
|
|
||||||
name: simplex-chat
|
name: simplex-chat
|
||||||
version: 5.8.0.1
|
version: 5.8.0.2
|
||||||
category: Web, System, Services, Cryptography
|
category: Web, System, Services, Cryptography
|
||||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||||
author: simplex.chat
|
author: simplex.chat
|
||||||
@@ -144,6 +144,7 @@ library
|
|||||||
Simplex.Chat.Migrations.M20240430_ui_theme
|
Simplex.Chat.Migrations.M20240430_ui_theme
|
||||||
Simplex.Chat.Migrations.M20240501_chat_deleted
|
Simplex.Chat.Migrations.M20240501_chat_deleted
|
||||||
Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
|
Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
|
||||||
|
Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays
|
||||||
Simplex.Chat.Mobile
|
Simplex.Chat.Mobile
|
||||||
Simplex.Chat.Mobile.File
|
Simplex.Chat.Mobile.File
|
||||||
Simplex.Chat.Mobile.Shared
|
Simplex.Chat.Mobile.Shared
|
||||||
|
|||||||
+92
-54
@@ -17,7 +17,7 @@ module Simplex.Chat where
|
|||||||
|
|
||||||
import Control.Applicative (optional, (<|>))
|
import Control.Applicative (optional, (<|>))
|
||||||
import Control.Concurrent.STM (retry)
|
import Control.Concurrent.STM (retry)
|
||||||
import Control.Logger.Simple (LogConfig (..))
|
import Control.Logger.Simple
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Control.Monad.Except
|
import Control.Monad.Except
|
||||||
import Control.Monad.IO.Unlift
|
import Control.Monad.IO.Unlift
|
||||||
@@ -47,6 +47,7 @@ import Data.Map.Strict (Map)
|
|||||||
import qualified Data.Map.Strict as M
|
import qualified Data.Map.Strict as M
|
||||||
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList)
|
import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList)
|
||||||
import Data.Ord (Down (..))
|
import Data.Ord (Down (..))
|
||||||
|
import qualified Data.Set as S
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||||
@@ -90,8 +91,9 @@ import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
|
|||||||
import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription)
|
import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription)
|
||||||
import qualified Simplex.FileTransfer.Description as FD
|
import qualified Simplex.FileTransfer.Description as FD
|
||||||
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
|
import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI)
|
||||||
|
import qualified Simplex.FileTransfer.Transport as XFTP
|
||||||
import Simplex.Messaging.Agent as Agent
|
import Simplex.Messaging.Agent as Agent
|
||||||
import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, temporaryAgentError, withLockMap)
|
import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, ipAddressProtected, temporaryAgentError, withLockMap)
|
||||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
|
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig)
|
||||||
import Simplex.Messaging.Agent.Lock (withLock)
|
import Simplex.Messaging.Agent.Lock (withLock)
|
||||||
import Simplex.Messaging.Agent.Protocol
|
import Simplex.Messaging.Agent.Protocol
|
||||||
@@ -109,7 +111,7 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR
|
|||||||
import Simplex.Messaging.Encoding
|
import Simplex.Messaging.Encoding
|
||||||
import Simplex.Messaging.Encoding.String
|
import Simplex.Messaging.Encoding.String
|
||||||
import Simplex.Messaging.Parsers (base64P)
|
import Simplex.Messaging.Parsers (base64P)
|
||||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol)
|
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol)
|
||||||
import qualified Simplex.Messaging.Protocol as SMP
|
import qualified Simplex.Messaging.Protocol as SMP
|
||||||
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
|
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
|
||||||
import qualified Simplex.Messaging.TMap as TM
|
import qualified Simplex.Messaging.TMap as TM
|
||||||
@@ -155,6 +157,7 @@ defaultChatConfig =
|
|||||||
autoAcceptFileSize = 0,
|
autoAcceptFileSize = 0,
|
||||||
showReactions = False,
|
showReactions = False,
|
||||||
showReceipts = False,
|
showReceipts = False,
|
||||||
|
logLevel = CLLImportant,
|
||||||
subscriptionEvents = False,
|
subscriptionEvents = False,
|
||||||
hostEvents = False,
|
hostEvents = False,
|
||||||
testView = False,
|
testView = False,
|
||||||
@@ -217,7 +220,7 @@ newChatController
|
|||||||
ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize}
|
ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize}
|
||||||
backgroundMode = do
|
backgroundMode = do
|
||||||
let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
|
let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False}
|
||||||
config = cfg {showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable}
|
config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable}
|
||||||
firstTime = dbNew chatStore
|
firstTime = dbNew chatStore
|
||||||
currentUser <- newTVarIO user
|
currentUser <- newTVarIO user
|
||||||
currentRemoteHost <- newTVarIO Nothing
|
currentRemoteHost <- newTVarIO Nothing
|
||||||
@@ -240,7 +243,6 @@ newChatController
|
|||||||
remoteHostSessions <- atomically TM.empty
|
remoteHostSessions <- atomically TM.empty
|
||||||
remoteHostsFolder <- newTVarIO Nothing
|
remoteHostsFolder <- newTVarIO Nothing
|
||||||
remoteCtrlSession <- newTVarIO Nothing
|
remoteCtrlSession <- newTVarIO Nothing
|
||||||
appLogLevel <- newTVarIO logLevel
|
|
||||||
filesFolder <- newTVarIO optFilesFolder
|
filesFolder <- newTVarIO optFilesFolder
|
||||||
chatStoreChanged <- newTVarIO False
|
chatStoreChanged <- newTVarIO False
|
||||||
expireCIThreads <- newTVarIO M.empty
|
expireCIThreads <- newTVarIO M.empty
|
||||||
@@ -279,7 +281,6 @@ newChatController
|
|||||||
remoteHostsFolder,
|
remoteHostsFolder,
|
||||||
remoteCtrlSession,
|
remoteCtrlSession,
|
||||||
config,
|
config,
|
||||||
appLogLevel,
|
|
||||||
filesFolder,
|
filesFolder,
|
||||||
expireCIThreads,
|
expireCIThreads,
|
||||||
expireCIFlags,
|
expireCIFlags,
|
||||||
@@ -428,7 +429,7 @@ startReceiveUserFiles user = do
|
|||||||
filesToReceive <- withStore' (`getRcvFilesToReceive` user)
|
filesToReceive <- withStore' (`getRcvFilesToReceive` user)
|
||||||
forM_ filesToReceive $ \ft ->
|
forM_ filesToReceive $ \ft ->
|
||||||
flip catchChatError (toView . CRChatError (Just user)) $
|
flip catchChatError (toView . CRChatError (Just user)) $
|
||||||
toView =<< receiveFile' user ft Nothing Nothing
|
toView =<< receiveFile' user ft False Nothing Nothing
|
||||||
|
|
||||||
restoreCalls :: CM' ()
|
restoreCalls :: CM' ()
|
||||||
restoreCalls = do
|
restoreCalls = do
|
||||||
@@ -437,19 +438,18 @@ restoreCalls = do
|
|||||||
calls <- asks currentCalls
|
calls <- asks currentCalls
|
||||||
atomically $ writeTVar calls callsMap
|
atomically $ writeTVar calls callsMap
|
||||||
|
|
||||||
stopChatController :: ChatController -> CM' ()
|
stopChatController :: ChatController -> IO ()
|
||||||
stopChatController ChatController {smpAgent, agentAsync = s, sndFiles, rcvFiles, expireCIFlags, remoteHostSessions, remoteCtrlSession} = do
|
stopChatController ChatController {smpAgent, agentAsync = s, sndFiles, rcvFiles, expireCIFlags, remoteHostSessions, remoteCtrlSession} = do
|
||||||
readTVarIO remoteHostSessions >>= mapM_ (cancelRemoteHost False . snd)
|
readTVarIO remoteHostSessions >>= mapM_ (cancelRemoteHost False . snd)
|
||||||
liftIO $ do
|
atomically (stateTVar remoteCtrlSession (,Nothing)) >>= mapM_ (cancelRemoteCtrl False . snd)
|
||||||
atomically (stateTVar remoteCtrlSession (,Nothing)) >>= mapM_ (cancelRemoteCtrl False . snd)
|
disconnectAgentClient smpAgent
|
||||||
disconnectAgentClient smpAgent
|
readTVarIO s >>= mapM_ (\(a1, a2) -> uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2)
|
||||||
readTVarIO s >>= mapM_ (\(a1, a2) -> uninterruptibleCancel a1 >> mapM_ uninterruptibleCancel a2)
|
closeFiles sndFiles
|
||||||
closeFiles sndFiles
|
closeFiles rcvFiles
|
||||||
closeFiles rcvFiles
|
atomically $ do
|
||||||
atomically $ do
|
keys <- M.keys <$> readTVar expireCIFlags
|
||||||
keys <- M.keys <$> readTVar expireCIFlags
|
forM_ keys $ \k -> TM.insert k False expireCIFlags
|
||||||
forM_ keys $ \k -> TM.insert k False expireCIFlags
|
writeTVar s Nothing
|
||||||
writeTVar s Nothing
|
|
||||||
where
|
where
|
||||||
closeFiles :: TVar (Map Int64 Handle) -> IO ()
|
closeFiles :: TVar (Map Int64 Handle) -> IO ()
|
||||||
closeFiles files = do
|
closeFiles files = do
|
||||||
@@ -603,7 +603,7 @@ processChatCommand' vr = \case
|
|||||||
Just _ -> pure CRChatRunning
|
Just _ -> pure CRChatRunning
|
||||||
_ -> checkStoreNotChanged . lift $ startChatController mainApp $> CRChatStarted
|
_ -> checkStoreNotChanged . lift $ startChatController mainApp $> CRChatStarted
|
||||||
APIStopChat -> do
|
APIStopChat -> do
|
||||||
ask >>= lift . stopChatController
|
ask >>= liftIO . stopChatController
|
||||||
pure CRChatStopped
|
pure CRChatStopped
|
||||||
APIActivateChat restoreChat -> withUser $ \_ -> do
|
APIActivateChat restoreChat -> withUser $ \_ -> do
|
||||||
lift $ when restoreChat restoreCalls
|
lift $ when restoreChat restoreCalls
|
||||||
@@ -2057,17 +2057,17 @@ processChatCommand' vr = \case
|
|||||||
ForwardFile chatName fileId -> forwardFile chatName fileId SendFile
|
ForwardFile chatName fileId -> forwardFile chatName fileId SendFile
|
||||||
ForwardImage chatName fileId -> forwardFile chatName fileId SendImage
|
ForwardImage chatName fileId -> forwardFile chatName fileId SendImage
|
||||||
SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO"
|
SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO"
|
||||||
ReceiveFile fileId encrypted_ rcvInline_ filePath_ -> withUser $ \_ ->
|
ReceiveFile fileId userApprovedRelays encrypted_ rcvInline_ filePath_ -> withUser $ \_ ->
|
||||||
withFileLock "receiveFile" fileId . procCmd $ do
|
withFileLock "receiveFile" fileId . procCmd $ do
|
||||||
(user, ft@RcvFileTransfer {fileStatus}) <- withStore (`getRcvFileTransferById` fileId)
|
(user, ft@RcvFileTransfer {fileStatus}) <- withStore (`getRcvFileTransferById` fileId)
|
||||||
encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
|
encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
|
||||||
ft' <- (if encrypt && fileStatus == RFSNew then setFileToEncrypt else pure) ft
|
ft' <- (if encrypt && fileStatus == RFSNew then setFileToEncrypt else pure) ft
|
||||||
receiveFile' user ft' rcvInline_ filePath_
|
receiveFile' user ft' userApprovedRelays rcvInline_ filePath_
|
||||||
SetFileToReceive fileId encrypted_ -> withUser $ \_ -> do
|
SetFileToReceive fileId userApprovedRelays encrypted_ -> withUser $ \_ -> do
|
||||||
withFileLock "setFileToReceive" fileId . procCmd $ do
|
withFileLock "setFileToReceive" fileId . procCmd $ do
|
||||||
encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
|
encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles
|
||||||
cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing
|
cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing
|
||||||
withStore' $ \db -> setRcvFileToReceive db fileId cfArgs
|
withStore' $ \db -> setRcvFileToReceive db fileId userApprovedRelays cfArgs
|
||||||
ok_
|
ok_
|
||||||
CancelFile fileId -> withUser $ \user@User {userId} ->
|
CancelFile fileId -> withUser $ \user@User {userId} ->
|
||||||
withFileLock "cancelFile" fileId . procCmd $
|
withFileLock "cancelFile" fileId . procCmd $
|
||||||
@@ -2107,13 +2107,8 @@ processChatCommand' vr = \case
|
|||||||
liftIO $ removeFile fsFilePath `catchAll_` pure ()
|
liftIO $ removeFile fsFilePath `catchAll_` pure ()
|
||||||
lift . forM_ agentRcvFileId $ \(AgentRcvFileId aFileId) ->
|
lift . forM_ agentRcvFileId $ \(AgentRcvFileId aFileId) ->
|
||||||
withAgent' (`xftpDeleteRcvFile` aFileId)
|
withAgent' (`xftpDeleteRcvFile` aFileId)
|
||||||
ci <- withStore $ \db -> do
|
aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation
|
||||||
liftIO $ do
|
pure $ CRRcvFileCancelled user aci_ ftr
|
||||||
updateCIFileStatus db user fileId CIFSRcvInvitation
|
|
||||||
updateRcvFileStatus db fileId FSNew
|
|
||||||
updateRcvFileAgentId db fileId Nothing
|
|
||||||
lookupChatItemByFileId db vr user fileId
|
|
||||||
pure $ CRRcvFileCancelled user ci ftr
|
|
||||||
FileStatus fileId -> withUser $ \user -> do
|
FileStatus fileId -> withUser $ \user -> do
|
||||||
withStore (\db -> lookupChatItemByFileId db vr user fileId) >>= \case
|
withStore (\db -> lookupChatItemByFileId db vr user fileId) >>= \case
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
@@ -2201,10 +2196,6 @@ processChatCommand' vr = \case
|
|||||||
chatMigrations <- map upMigration <$> withStore' (Migrations.getCurrent . DB.conn)
|
chatMigrations <- map upMigration <$> withStore' (Migrations.getCurrent . DB.conn)
|
||||||
agentMigrations <- withAgent getAgentMigrations
|
agentMigrations <- withAgent getAgentMigrations
|
||||||
pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations}
|
pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations}
|
||||||
SetAppLogLevel ll -> do
|
|
||||||
chatWriteVar appLogLevel ll
|
|
||||||
lift $ withAgent' (`setAgentLogLevel` toLogLevel ll)
|
|
||||||
ok_
|
|
||||||
DebugLocks -> lift $ do
|
DebugLocks -> lift $ do
|
||||||
chatLockName <- atomically . tryReadTMVar =<< asks chatLock
|
chatLockName <- atomically . tryReadTMVar =<< asks chatLock
|
||||||
chatEntityLocks <- getLocks =<< asks entityLocks
|
chatEntityLocks <- getLocks =<< asks entityLocks
|
||||||
@@ -3058,9 +3049,9 @@ setFileToEncrypt ft@RcvFileTransfer {fileId} = do
|
|||||||
withStore' $ \db -> setFileCryptoArgs db fileId cfArgs
|
withStore' $ \db -> setFileCryptoArgs db fileId cfArgs
|
||||||
pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs}
|
pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs}
|
||||||
|
|
||||||
receiveFile' :: User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> CM ChatResponse
|
receiveFile' :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM ChatResponse
|
||||||
receiveFile' user ft rcvInline_ filePath_ = do
|
receiveFile' user ft userApprovedRelays rcvInline_ filePath_ = do
|
||||||
(CRRcvFileAccepted user <$> acceptFileReceive user ft rcvInline_ filePath_) `catchChatError` processError
|
(CRRcvFileAccepted user <$> acceptFileReceive user ft userApprovedRelays rcvInline_ filePath_) `catchChatError` processError
|
||||||
where
|
where
|
||||||
processError = \case
|
processError = \case
|
||||||
-- TODO AChatItem in Cancelled events
|
-- TODO AChatItem in Cancelled events
|
||||||
@@ -3068,8 +3059,8 @@ receiveFile' user ft rcvInline_ filePath_ = do
|
|||||||
ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft
|
ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft
|
||||||
e -> throwError e
|
e -> throwError e
|
||||||
|
|
||||||
acceptFileReceive :: User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> CM AChatItem
|
acceptFileReceive :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM AChatItem
|
||||||
acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId, cryptoArgs} rcvInline_ filePath_ = do
|
acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId, cryptoArgs} userApprovedRelays rcvInline_ filePath_ = do
|
||||||
unless (fileStatus == RFSNew) $ case fileStatus of
|
unless (fileStatus == RFSNew) $ case fileStatus of
|
||||||
RFSCancelled _ -> throwChatError $ CEFileCancelled fName
|
RFSCancelled _ -> throwChatError $ CEFileCancelled fName
|
||||||
_ -> throwChatError $ CEFileAlreadyReceiving fName
|
_ -> throwChatError $ CEFileAlreadyReceiving fName
|
||||||
@@ -3083,15 +3074,16 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
|
|||||||
filePath <- getRcvFilePath fileId filePath_ fName True
|
filePath <- getRcvFilePath fileId filePath_ fName True
|
||||||
withStore $ \db -> acceptRcvFileTransfer db vr user fileId connIds ConnJoined filePath subMode
|
withStore $ \db -> acceptRcvFileTransfer db vr user fileId connIds ConnJoined filePath subMode
|
||||||
-- XFTP
|
-- XFTP
|
||||||
(Just XFTPRcvFile {}, _) -> do
|
(Just XFTPRcvFile {userApprovedRelays = approvedBeforeReady}, _) -> do
|
||||||
|
let userApproved = approvedBeforeReady || userApprovedRelays
|
||||||
filePath <- getRcvFilePath fileId filePath_ fName False
|
filePath <- getRcvFilePath fileId filePath_ fName False
|
||||||
(ci, rfd) <- withStore $ \db -> do
|
(ci, rfd) <- withStore $ \db -> do
|
||||||
-- marking file as accepted and reading description in the same transaction
|
-- marking file as accepted and reading description in the same transaction
|
||||||
-- to prevent race condition with appending description
|
-- to prevent race condition with appending description
|
||||||
ci <- xftpAcceptRcvFT db vr user fileId filePath
|
ci <- xftpAcceptRcvFT db vr user fileId filePath userApproved
|
||||||
rfd <- getRcvFileDescrByRcvFileId db fileId
|
rfd <- getRcvFileDescrByRcvFileId db fileId
|
||||||
pure (ci, rfd)
|
pure (ci, rfd)
|
||||||
receiveViaCompleteFD user fileId rfd cryptoArgs
|
receiveViaCompleteFD user fileId rfd userApproved cryptoArgs
|
||||||
pure ci
|
pure ci
|
||||||
-- group & direct file protocol
|
-- group & direct file protocol
|
||||||
_ -> do
|
_ -> do
|
||||||
@@ -3136,18 +3128,61 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI
|
|||||||
|| (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks)
|
|| (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks)
|
||||||
)
|
)
|
||||||
|
|
||||||
receiveViaCompleteFD :: User -> FileTransferId -> RcvFileDescr -> Maybe CryptoFileArgs -> CM ()
|
receiveViaCompleteFD :: User -> FileTransferId -> RcvFileDescr -> Bool -> Maybe CryptoFileArgs -> CM ()
|
||||||
receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} cfArgs =
|
receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} userApprovedRelays cfArgs =
|
||||||
when fileDescrComplete $ do
|
when fileDescrComplete $ do
|
||||||
rd <- parseFileDescription fileDescrText
|
rd <- parseFileDescription fileDescrText
|
||||||
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs
|
if userApprovedRelays
|
||||||
startReceivingFile user fileId
|
then receive' rd True
|
||||||
withStore' $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId)
|
else do
|
||||||
|
let srvs = fileServers rd
|
||||||
|
unknownSrvs <- getUnknownSrvs srvs
|
||||||
|
let approved = null unknownSrvs
|
||||||
|
ifM
|
||||||
|
((approved ||) <$> ipProtectedForSrvs srvs)
|
||||||
|
(receive' rd approved)
|
||||||
|
(relaysNotApproved unknownSrvs)
|
||||||
|
where
|
||||||
|
receive' :: ValidFileDescription 'FRecipient -> Bool -> CM ()
|
||||||
|
receive' rd approved = do
|
||||||
|
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs approved
|
||||||
|
startReceivingFile user fileId
|
||||||
|
withStore' $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId)
|
||||||
|
fileServers :: ValidFileDescription 'FRecipient -> [XFTPServer]
|
||||||
|
fileServers (FD.ValidFileDescription FD.FileDescription {chunks}) =
|
||||||
|
S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks
|
||||||
|
getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer]
|
||||||
|
getUnknownSrvs srvs = do
|
||||||
|
ChatConfig {defaultServers = DefaultAgentServers {xftp = defXftp}} <- asks config
|
||||||
|
storedSrvs <- map (\ServerCfg {server} -> protoServer server) <$> withStore' (`getProtocolServers` user)
|
||||||
|
let defXftp' = L.map protoServer defXftp
|
||||||
|
knownSrvs = fromMaybe defXftp' $ nonEmpty storedSrvs
|
||||||
|
pure $ filter (`notElem` knownSrvs) srvs
|
||||||
|
ipProtectedForSrvs :: [XFTPServer] -> CM Bool
|
||||||
|
ipProtectedForSrvs srvs = do
|
||||||
|
netCfg <- lift $ withAgent' getNetworkConfig
|
||||||
|
pure $ all (ipAddressProtected netCfg) srvs
|
||||||
|
relaysNotApproved :: [XFTPServer] -> CM ()
|
||||||
|
relaysNotApproved unknownSrvs = do
|
||||||
|
aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation
|
||||||
|
forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci
|
||||||
|
throwChatError $ CEFileNotApproved fileId unknownSrvs
|
||||||
|
|
||||||
|
resetRcvCIFileStatus :: User -> FileTransferId -> CIFileStatus 'MDRcv -> CM (Maybe AChatItem)
|
||||||
|
resetRcvCIFileStatus user fileId ciFileStatus = do
|
||||||
|
vr <- chatVersionRange
|
||||||
|
withStore $ \db -> do
|
||||||
|
liftIO $ do
|
||||||
|
updateCIFileStatus db user fileId ciFileStatus
|
||||||
|
updateRcvFileStatus db fileId FSNew
|
||||||
|
updateRcvFileAgentId db fileId Nothing
|
||||||
|
lookupChatItemByFileId db vr user fileId
|
||||||
|
|
||||||
receiveViaURI :: User -> FileDescriptionURI -> CryptoFile -> CM RcvFileTransfer
|
receiveViaURI :: User -> FileDescriptionURI -> CryptoFile -> CM RcvFileTransfer
|
||||||
receiveViaURI user@User {userId} FileDescriptionURI {description} cf@CryptoFile {cryptoArgs} = do
|
receiveViaURI user@User {userId} FileDescriptionURI {description} cf@CryptoFile {cryptoArgs} = do
|
||||||
fileId <- withStore $ \db -> createRcvStandaloneFileTransfer db userId cf fileSize chunkSize
|
fileId <- withStore $ \db -> createRcvStandaloneFileTransfer db userId cf fileSize chunkSize
|
||||||
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) description cryptoArgs
|
-- currently the only use case is user migrating via their configured servers, so we pass approvedRelays = True
|
||||||
|
aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) description cryptoArgs True
|
||||||
withStore $ \db -> do
|
withStore $ \db -> do
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
updateRcvFileStatus db fileId FSConnected
|
updateRcvFileStatus db fileId FSConnected
|
||||||
@@ -3635,7 +3670,6 @@ processAgentMessageNoConn = \case
|
|||||||
UP srv conns -> serverEvent srv conns NSConnected CRContactsSubscribed
|
UP srv conns -> serverEvent srv conns NSConnected CRContactsSubscribed
|
||||||
SUSPENDED -> toView CRChatSuspended
|
SUSPENDED -> toView CRChatSuspended
|
||||||
DEL_USER agentUserId -> toView $ CRAgentUserDeleted agentUserId
|
DEL_USER agentUserId -> toView $ CRAgentUserDeleted agentUserId
|
||||||
LOG ll s -> toView $ CRAgentLog ll s
|
|
||||||
where
|
where
|
||||||
hostEvent :: ChatResponse -> CM ()
|
hostEvent :: ChatResponse -> CM ()
|
||||||
hostEvent = whenM (asks $ hostEvents . config) . toView
|
hostEvent = whenM (asks $ hostEvents . config) . toView
|
||||||
@@ -3818,6 +3852,10 @@ processAgentMsgRcvFile _corrId aFileId msg = do
|
|||||||
RFERR e
|
RFERR e
|
||||||
| temporaryAgentError e ->
|
| temporaryAgentError e ->
|
||||||
throwChatError $ CEXFTPRcvFile fileId (AgentRcvFileId aFileId) e
|
throwChatError $ CEXFTPRcvFile fileId (AgentRcvFileId aFileId) e
|
||||||
|
| e == XFTP "" XFTP.NOT_APPROVED -> do
|
||||||
|
aci_ <- resetRcvCIFileStatus user fileId CIFSRcvAborted
|
||||||
|
agentXFTPDeleteRcvFile aFileId fileId
|
||||||
|
forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci
|
||||||
| otherwise -> do
|
| otherwise -> do
|
||||||
ci <- withStore $ \db -> do
|
ci <- withStore $ \db -> do
|
||||||
liftIO $ updateFileCancelled db user fileId CIFSRcvError
|
liftIO $ updateFileCancelled db user fileId CIFSRcvError
|
||||||
@@ -4869,8 +4907,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
|
|
||||||
autoAcceptFile :: Maybe (RcvFileTransfer, CIFile 'MDRcv) -> CM ()
|
autoAcceptFile :: Maybe (RcvFileTransfer, CIFile 'MDRcv) -> CM ()
|
||||||
autoAcceptFile = mapM_ $ \(ft, CIFile {fileSize}) -> do
|
autoAcceptFile = mapM_ $ \(ft, CIFile {fileSize}) -> do
|
||||||
|
-- ! autoAcceptFileSize is only used in tests
|
||||||
ChatConfig {autoAcceptFileSize = sz} <- asks config
|
ChatConfig {autoAcceptFileSize = sz} <- asks config
|
||||||
when (sz > fileSize) $ receiveFile' user ft Nothing Nothing >>= toView
|
when (sz > fileSize) $ receiveFile' user ft False Nothing Nothing >>= toView
|
||||||
|
|
||||||
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> CM ()
|
messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> CM ()
|
||||||
messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr = do
|
messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr = do
|
||||||
@@ -4896,7 +4935,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
|||||||
ci <- withStore $ \db -> getAChatItemBySharedMsgId db user cd sharedMsgId
|
ci <- withStore $ \db -> getAChatItemBySharedMsgId db user cd sharedMsgId
|
||||||
toView $ CRRcvFileDescrReady user ci ft' rfd
|
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 {userApprovedRelays}) -> receiveViaCompleteFD user fileId rfd userApprovedRelays cryptoArgs
|
||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
|
|
||||||
processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> CM (Maybe (RcvFileTransfer, CIFile 'MDRcv))
|
processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> CM (Maybe (RcvFileTransfer, CIFile 'MDRcv))
|
||||||
@@ -7322,8 +7361,8 @@ chatCommandP =
|
|||||||
("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal),
|
("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal),
|
||||||
("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal),
|
("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal),
|
||||||
("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath),
|
("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath),
|
||||||
("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> optional (" encrypt=" *> onOffP) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)),
|
("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> (" approved_relays=" *> onOffP <|> pure False) <*> optional (" encrypt=" *> onOffP) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)),
|
||||||
"/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> optional (" encrypt=" *> onOffP)),
|
"/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> (" approved_relays=" *> onOffP <|> pure False) <*> optional (" encrypt=" *> onOffP)),
|
||||||
("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal),
|
("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal),
|
||||||
("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal),
|
("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal),
|
||||||
"/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal),
|
"/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal),
|
||||||
@@ -7384,7 +7423,6 @@ chatCommandP =
|
|||||||
"/_download " *> (APIDownloadStandaloneFile <$> A.decimal <* A.space <*> strP_ <*> cryptoFileP),
|
"/_download " *> (APIDownloadStandaloneFile <$> A.decimal <* A.space <*> strP_ <*> cryptoFileP),
|
||||||
("/quit" <|> "/q" <|> "/exit") $> QuitChat,
|
("/quit" <|> "/q" <|> "/exit") $> QuitChat,
|
||||||
("/version" <|> "/v") $> ShowVersion,
|
("/version" <|> "/v") $> ShowVersion,
|
||||||
"/log " *> (SetAppLogLevel <$> strP),
|
|
||||||
"/debug locks" $> DebugLocks,
|
"/debug locks" $> DebugLocks,
|
||||||
"/debug event " *> (DebugEvent <$> jsonP),
|
"/debug event " *> (DebugEvent <$> jsonP),
|
||||||
"/get stats" $> GetAgentStats,
|
"/get stats" $> GetAgentStats,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ data AppSettings = AppSettings
|
|||||||
{ appPlatform :: Maybe AppPlatform,
|
{ appPlatform :: Maybe AppPlatform,
|
||||||
networkConfig :: Maybe NetworkConfig,
|
networkConfig :: Maybe NetworkConfig,
|
||||||
privacyEncryptLocalFiles :: Maybe Bool,
|
privacyEncryptLocalFiles :: Maybe Bool,
|
||||||
|
privacyAskToApproveRelays :: Maybe Bool,
|
||||||
privacyAcceptImages :: Maybe Bool,
|
privacyAcceptImages :: Maybe Bool,
|
||||||
privacyLinkPreviews :: Maybe Bool,
|
privacyLinkPreviews :: Maybe Bool,
|
||||||
privacyShowChatPreviews :: Maybe Bool,
|
privacyShowChatPreviews :: Maybe Bool,
|
||||||
@@ -61,6 +62,7 @@ defaultAppSettings =
|
|||||||
{ appPlatform = Nothing,
|
{ appPlatform = Nothing,
|
||||||
networkConfig = Just defaultNetworkConfig,
|
networkConfig = Just defaultNetworkConfig,
|
||||||
privacyEncryptLocalFiles = Just True,
|
privacyEncryptLocalFiles = Just True,
|
||||||
|
privacyAskToApproveRelays = Just True,
|
||||||
privacyAcceptImages = Just True,
|
privacyAcceptImages = Just True,
|
||||||
privacyLinkPreviews = Just True,
|
privacyLinkPreviews = Just True,
|
||||||
privacyShowChatPreviews = Just True,
|
privacyShowChatPreviews = Just True,
|
||||||
@@ -92,6 +94,7 @@ defaultParseAppSettings =
|
|||||||
{ appPlatform = Nothing,
|
{ appPlatform = Nothing,
|
||||||
networkConfig = Nothing,
|
networkConfig = Nothing,
|
||||||
privacyEncryptLocalFiles = Nothing,
|
privacyEncryptLocalFiles = Nothing,
|
||||||
|
privacyAskToApproveRelays = Nothing,
|
||||||
privacyAcceptImages = Nothing,
|
privacyAcceptImages = Nothing,
|
||||||
privacyLinkPreviews = Nothing,
|
privacyLinkPreviews = Nothing,
|
||||||
privacyShowChatPreviews = Nothing,
|
privacyShowChatPreviews = Nothing,
|
||||||
@@ -123,6 +126,7 @@ combineAppSettings platformDefaults storedSettings =
|
|||||||
{ appPlatform = p appPlatform,
|
{ appPlatform = p appPlatform,
|
||||||
networkConfig = p networkConfig,
|
networkConfig = p networkConfig,
|
||||||
privacyEncryptLocalFiles = p privacyEncryptLocalFiles,
|
privacyEncryptLocalFiles = p privacyEncryptLocalFiles,
|
||||||
|
privacyAskToApproveRelays = p privacyAskToApproveRelays,
|
||||||
privacyAcceptImages = p privacyAcceptImages,
|
privacyAcceptImages = p privacyAcceptImages,
|
||||||
privacyLinkPreviews = p privacyLinkPreviews,
|
privacyLinkPreviews = p privacyLinkPreviews,
|
||||||
privacyShowChatPreviews = p privacyShowChatPreviews,
|
privacyShowChatPreviews = p privacyShowChatPreviews,
|
||||||
@@ -166,6 +170,7 @@ instance FromJSON AppSettings where
|
|||||||
appPlatform <- p "appPlatform"
|
appPlatform <- p "appPlatform"
|
||||||
networkConfig <- p "networkConfig"
|
networkConfig <- p "networkConfig"
|
||||||
privacyEncryptLocalFiles <- p "privacyEncryptLocalFiles"
|
privacyEncryptLocalFiles <- p "privacyEncryptLocalFiles"
|
||||||
|
privacyAskToApproveRelays <- p "privacyAskToApproveRelays"
|
||||||
privacyAcceptImages <- p "privacyAcceptImages"
|
privacyAcceptImages <- p "privacyAcceptImages"
|
||||||
privacyLinkPreviews <- p "privacyLinkPreviews"
|
privacyLinkPreviews <- p "privacyLinkPreviews"
|
||||||
privacyShowChatPreviews <- p "privacyShowChatPreviews"
|
privacyShowChatPreviews <- p "privacyShowChatPreviews"
|
||||||
@@ -194,6 +199,7 @@ instance FromJSON AppSettings where
|
|||||||
{ appPlatform,
|
{ appPlatform,
|
||||||
networkConfig,
|
networkConfig,
|
||||||
privacyEncryptLocalFiles,
|
privacyEncryptLocalFiles,
|
||||||
|
privacyAskToApproveRelays,
|
||||||
privacyAcceptImages,
|
privacyAcceptImages,
|
||||||
privacyLinkPreviews,
|
privacyLinkPreviews,
|
||||||
privacyShowChatPreviews,
|
privacyShowChatPreviews,
|
||||||
|
|||||||
@@ -85,9 +85,9 @@ textMsgContent :: String -> MsgContent
|
|||||||
textMsgContent = MCText . T.pack
|
textMsgContent = MCText . T.pack
|
||||||
|
|
||||||
printLog :: ChatController -> ChatLogLevel -> String -> IO ()
|
printLog :: ChatController -> ChatLogLevel -> String -> IO ()
|
||||||
printLog cc level s = do
|
printLog cc level s
|
||||||
ll <- readTVarIO $ appLogLevel cc
|
| logLevel (config cc) <= level = putStrLn s
|
||||||
when (ll <= level) $ putStrLn s
|
| otherwise = pure ()
|
||||||
|
|
||||||
contactInfo :: Contact -> String
|
contactInfo :: Contact -> String
|
||||||
contactInfo Contact {contactId, localDisplayName} = T.unpack localDisplayName <> " (" <> show contactId <> ")"
|
contactInfo Contact {contactId, localDisplayName} = T.unpack localDisplayName <> " (" <> show contactId <> ")"
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ module Simplex.Chat.Controller where
|
|||||||
import Control.Concurrent (ThreadId)
|
import Control.Concurrent (ThreadId)
|
||||||
import Control.Concurrent.Async (Async)
|
import Control.Concurrent.Async (Async)
|
||||||
import Control.Exception
|
import Control.Exception
|
||||||
import qualified Control.Logger.Simple as Logger
|
|
||||||
import Control.Monad (when)
|
|
||||||
import Control.Monad.Except
|
import Control.Monad.Except
|
||||||
import Control.Monad.IO.Unlift
|
import Control.Monad.IO.Unlift
|
||||||
import Control.Monad.Reader
|
import Control.Monad.Reader
|
||||||
@@ -83,7 +81,7 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption)
|
|||||||
import Simplex.Messaging.Encoding.String
|
import Simplex.Messaging.Encoding.String
|
||||||
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus)
|
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus)
|
||||||
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON)
|
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON)
|
||||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServerWithAuth, userProtocol)
|
import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServer, XFTPServerWithAuth, userProtocol)
|
||||||
import Simplex.Messaging.TMap (TMap)
|
import Simplex.Messaging.TMap (TMap)
|
||||||
import Simplex.Messaging.Transport (TLS, simplexMQVersion)
|
import Simplex.Messaging.Transport (TLS, simplexMQVersion)
|
||||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||||
@@ -141,6 +139,7 @@ data ChatConfig = ChatConfig
|
|||||||
showReceipts :: Bool,
|
showReceipts :: Bool,
|
||||||
subscriptionEvents :: Bool,
|
subscriptionEvents :: Bool,
|
||||||
hostEvents :: Bool,
|
hostEvents :: Bool,
|
||||||
|
logLevel :: ChatLogLevel,
|
||||||
testView :: Bool,
|
testView :: Bool,
|
||||||
initialCleanupManagerDelay :: Int64,
|
initialCleanupManagerDelay :: Int64,
|
||||||
cleanupManagerInterval :: NominalDiffTime,
|
cleanupManagerInterval :: NominalDiffTime,
|
||||||
@@ -222,7 +221,6 @@ data ChatController = ChatController
|
|||||||
remoteHostsFolder :: TVar (Maybe FilePath), -- folder for remote hosts data
|
remoteHostsFolder :: TVar (Maybe FilePath), -- folder for remote hosts data
|
||||||
remoteCtrlSession :: TVar (Maybe (SessionSeq, RemoteCtrlSession)), -- Supervisor process for hosted controllers
|
remoteCtrlSession :: TVar (Maybe (SessionSeq, RemoteCtrlSession)), -- Supervisor process for hosted controllers
|
||||||
config :: ChatConfig,
|
config :: ChatConfig,
|
||||||
appLogLevel :: TVar ChatLogLevel,
|
|
||||||
filesFolder :: TVar (Maybe FilePath), -- path to files folder for mobile apps,
|
filesFolder :: TVar (Maybe FilePath), -- path to files folder for mobile apps,
|
||||||
expireCIThreads :: TMap UserId (Maybe (Async ())),
|
expireCIThreads :: TMap UserId (Maybe (Async ())),
|
||||||
expireCIFlags :: TMap UserId Bool,
|
expireCIFlags :: TMap UserId Bool,
|
||||||
@@ -460,8 +458,8 @@ data ChatCommand
|
|||||||
| ForwardFile ChatName FileTransferId
|
| ForwardFile ChatName FileTransferId
|
||||||
| ForwardImage ChatName FileTransferId
|
| ForwardImage ChatName FileTransferId
|
||||||
| SendFileDescription ChatName FilePath
|
| SendFileDescription ChatName FilePath
|
||||||
| ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath}
|
| ReceiveFile {fileId :: FileTransferId, userApprovedRelays :: Bool, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath}
|
||||||
| SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Maybe Bool}
|
| SetFileToReceive {fileId :: FileTransferId, userApprovedRelays :: Bool, storeEncrypted :: Maybe Bool}
|
||||||
| CancelFile FileTransferId
|
| CancelFile FileTransferId
|
||||||
| FileStatus FileTransferId
|
| FileStatus FileTransferId
|
||||||
| ShowProfile -- UserId (not used in UI)
|
| ShowProfile -- UserId (not used in UI)
|
||||||
@@ -495,7 +493,6 @@ data ChatCommand
|
|||||||
| APIStandaloneFileInfo FileDescriptionURI
|
| APIStandaloneFileInfo FileDescriptionURI
|
||||||
| QuitChat
|
| QuitChat
|
||||||
| ShowVersion
|
| ShowVersion
|
||||||
| SetAppLogLevel ChatLogLevel
|
|
||||||
| DebugLocks
|
| DebugLocks
|
||||||
| DebugEvent ChatResponse
|
| DebugEvent ChatResponse
|
||||||
| GetAgentStats
|
| GetAgentStats
|
||||||
@@ -760,8 +757,6 @@ data ChatResponse
|
|||||||
| CRChatCmdError {user_ :: Maybe User, chatError :: ChatError}
|
| CRChatCmdError {user_ :: Maybe User, chatError :: ChatError}
|
||||||
| CRChatError {user_ :: Maybe User, chatError :: ChatError}
|
| CRChatError {user_ :: Maybe User, chatError :: ChatError}
|
||||||
| CRChatErrors {user_ :: Maybe User, chatErrors :: [ChatError]}
|
| CRChatErrors {user_ :: Maybe User, chatErrors :: [ChatError]}
|
||||||
| CRAgentLog {agentLogLevel :: AgentLogLevel, errorMessage :: Text}
|
|
||||||
| CRChatLog {chatLogLevel :: ChatLogLevel, errorMessage :: Text}
|
|
||||||
| CRArchiveImported {archiveErrors :: [ArchiveError]}
|
| CRArchiveImported {archiveErrors :: [ArchiveError]}
|
||||||
| CRAppSettings {appSettings :: AppSettings}
|
| CRAppSettings {appSettings :: AppSettings}
|
||||||
| CRTimedAction {action :: String, durationMilliseconds :: Int64}
|
| CRTimedAction {action :: String, durationMilliseconds :: Int64}
|
||||||
@@ -1058,30 +1053,6 @@ tmeToPref currentTTL tme = uncurry TimedMessagesPreference $ case tme of
|
|||||||
data ChatLogLevel = CLLDebug | CLLInfo | CLLWarning | CLLError | CLLImportant
|
data ChatLogLevel = CLLDebug | CLLInfo | CLLWarning | CLLError | CLLImportant
|
||||||
deriving (Eq, Ord, Show)
|
deriving (Eq, Ord, Show)
|
||||||
|
|
||||||
instance StrEncoding ChatLogLevel where
|
|
||||||
strEncode = \case
|
|
||||||
CLLDebug -> "debug"
|
|
||||||
CLLInfo -> "info"
|
|
||||||
CLLWarning -> "warn"
|
|
||||||
CLLError -> "error"
|
|
||||||
CLLImportant -> "important"
|
|
||||||
strP =
|
|
||||||
A.takeTill (== ' ')
|
|
||||||
>>= \case
|
|
||||||
"debug" -> pure CLLDebug
|
|
||||||
"info" -> pure CLLInfo
|
|
||||||
"warn" -> pure CLLWarning
|
|
||||||
"error" -> pure CLLError
|
|
||||||
"important" -> pure CLLImportant
|
|
||||||
_ -> fail "Invalid log level"
|
|
||||||
|
|
||||||
instance ToJSON ChatLogLevel where
|
|
||||||
toJSON = strToJSON
|
|
||||||
toEncoding = strToJEncoding
|
|
||||||
|
|
||||||
instance FromJSON ChatLogLevel where
|
|
||||||
parseJSON = strParseJSON "ChatLogLevel"
|
|
||||||
|
|
||||||
data CoreVersionInfo = CoreVersionInfo
|
data CoreVersionInfo = CoreVersionInfo
|
||||||
{ version :: String,
|
{ version :: String,
|
||||||
simplexmqVersion :: String,
|
simplexmqVersion :: String,
|
||||||
@@ -1161,6 +1132,7 @@ data ChatErrorType
|
|||||||
| CEFileImageType {filePath :: FilePath}
|
| CEFileImageType {filePath :: FilePath}
|
||||||
| CEFileImageSize {filePath :: FilePath}
|
| CEFileImageSize {filePath :: FilePath}
|
||||||
| CEFileNotReceived {fileId :: FileTransferId}
|
| CEFileNotReceived {fileId :: FileTransferId}
|
||||||
|
| CEFileNotApproved {fileId :: FileTransferId, unknownServers :: [XFTPServer]}
|
||||||
| CEXFTPRcvFile {fileId :: FileTransferId, agentRcvFileId :: AgentRcvFileId, agentError :: AgentErrorType}
|
| CEXFTPRcvFile {fileId :: FileTransferId, agentRcvFileId :: AgentRcvFileId, agentError :: AgentErrorType}
|
||||||
| CEXFTPSndFile {fileId :: FileTransferId, agentSndFileId :: AgentSndFileId, agentError :: AgentErrorType}
|
| CEXFTPSndFile {fileId :: FileTransferId, agentSndFileId :: AgentSndFileId, agentError :: AgentErrorType}
|
||||||
| CEFallbackToSMPProhibited {fileId :: FileTransferId}
|
| CEFallbackToSMPProhibited {fileId :: FileTransferId}
|
||||||
@@ -1426,34 +1398,6 @@ withAgent action =
|
|||||||
withAgent' :: (AgentClient -> IO a) -> CM' a
|
withAgent' :: (AgentClient -> IO a) -> CM' a
|
||||||
withAgent' action = asks smpAgent >>= liftIO . action
|
withAgent' action = asks smpAgent >>= liftIO . action
|
||||||
|
|
||||||
logDebug :: Text -> CM ()
|
|
||||||
logDebug = lift . logDebug'
|
|
||||||
{-# INLINE logDebug #-}
|
|
||||||
|
|
||||||
logDebug' :: Text -> CM' ()
|
|
||||||
logDebug' s = logToView CLLDebug s >> Logger.logDebug s
|
|
||||||
|
|
||||||
logInfo :: Text -> CM ()
|
|
||||||
logInfo s = lift (logToView CLLInfo s) >> Logger.logInfo s
|
|
||||||
|
|
||||||
logWarn :: Text -> CM ()
|
|
||||||
logWarn s = lift (logToView CLLWarning s) >> Logger.logWarn s
|
|
||||||
|
|
||||||
logError :: Text -> CM ()
|
|
||||||
logError = lift . logError'
|
|
||||||
{-# INLINE logError #-}
|
|
||||||
|
|
||||||
logError' :: Text -> CM' ()
|
|
||||||
logError' s = logToView CLLError s >> Logger.logError s
|
|
||||||
|
|
||||||
logImportant :: Text -> CM ()
|
|
||||||
logImportant s = lift (logToView CLLImportant s) >> Logger.logError s
|
|
||||||
|
|
||||||
logToView :: ChatLogLevel -> Text -> CM' ()
|
|
||||||
logToView ll' s = do
|
|
||||||
ll <- chatReadVar' appLogLevel
|
|
||||||
when (ll' >= ll) $ toView' $ CRChatLog ll s
|
|
||||||
|
|
||||||
$(JQ.deriveJSON (enumJSON $ dropPrefix "HS") ''HelpSection)
|
$(JQ.deriveJSON (enumJSON $ dropPrefix "HS") ''HelpSection)
|
||||||
|
|
||||||
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CLQ") ''ChatListQuery)
|
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CLQ") ''ChatListQuery)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ module Simplex.Chat.Core
|
|||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import Control.Concurrent.STM
|
|
||||||
import Control.Logger.Simple
|
import Control.Logger.Simple
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Control.Monad.Reader
|
import Control.Monad.Reader
|
||||||
@@ -111,8 +110,7 @@ createActiveUser cc = do
|
|||||||
r -> do
|
r -> do
|
||||||
ts <- getCurrentTime
|
ts <- getCurrentTime
|
||||||
tz <- getCurrentTimeZone
|
tz <- getCurrentTimeZone
|
||||||
ll <- readTVarIO $ appLogLevel cc
|
putStrLn $ serializeChatResponse (Nothing, Nothing) ts tz Nothing r
|
||||||
putStrLn $ serializeChatResponse (Nothing, Nothing) ll ts tz Nothing r
|
|
||||||
loop
|
loop
|
||||||
|
|
||||||
getWithPrompt :: String -> IO String
|
getWithPrompt :: String -> IO String
|
||||||
|
|||||||
@@ -539,6 +539,7 @@ data CIFileStatus (d :: MsgDirection) where
|
|||||||
CIFSRcvInvitation :: CIFileStatus 'MDRcv
|
CIFSRcvInvitation :: CIFileStatus 'MDRcv
|
||||||
CIFSRcvAccepted :: CIFileStatus 'MDRcv
|
CIFSRcvAccepted :: CIFileStatus 'MDRcv
|
||||||
CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv
|
CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv
|
||||||
|
CIFSRcvAborted :: CIFileStatus 'MDRcv
|
||||||
CIFSRcvComplete :: CIFileStatus 'MDRcv
|
CIFSRcvComplete :: CIFileStatus 'MDRcv
|
||||||
CIFSRcvCancelled :: CIFileStatus 'MDRcv
|
CIFSRcvCancelled :: CIFileStatus 'MDRcv
|
||||||
CIFSRcvError :: CIFileStatus 'MDRcv
|
CIFSRcvError :: CIFileStatus 'MDRcv
|
||||||
@@ -558,6 +559,7 @@ ciFileEnded = \case
|
|||||||
CIFSRcvInvitation -> False
|
CIFSRcvInvitation -> False
|
||||||
CIFSRcvAccepted -> False
|
CIFSRcvAccepted -> False
|
||||||
CIFSRcvTransfer {} -> False
|
CIFSRcvTransfer {} -> False
|
||||||
|
CIFSRcvAborted -> True
|
||||||
CIFSRcvCancelled -> True
|
CIFSRcvCancelled -> True
|
||||||
CIFSRcvComplete -> True
|
CIFSRcvComplete -> True
|
||||||
CIFSRcvError -> True
|
CIFSRcvError -> True
|
||||||
@@ -573,6 +575,7 @@ ciFileLoaded = \case
|
|||||||
CIFSRcvInvitation -> False
|
CIFSRcvInvitation -> False
|
||||||
CIFSRcvAccepted -> False
|
CIFSRcvAccepted -> False
|
||||||
CIFSRcvTransfer {} -> False
|
CIFSRcvTransfer {} -> False
|
||||||
|
CIFSRcvAborted -> False
|
||||||
CIFSRcvCancelled -> False
|
CIFSRcvCancelled -> False
|
||||||
CIFSRcvComplete -> True
|
CIFSRcvComplete -> True
|
||||||
CIFSRcvError -> False
|
CIFSRcvError -> False
|
||||||
@@ -592,6 +595,7 @@ instance MsgDirectionI d => StrEncoding (CIFileStatus d) where
|
|||||||
CIFSRcvInvitation -> "rcv_invitation"
|
CIFSRcvInvitation -> "rcv_invitation"
|
||||||
CIFSRcvAccepted -> "rcv_accepted"
|
CIFSRcvAccepted -> "rcv_accepted"
|
||||||
CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total)
|
CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total)
|
||||||
|
CIFSRcvAborted -> "rcv_aborted"
|
||||||
CIFSRcvComplete -> "rcv_complete"
|
CIFSRcvComplete -> "rcv_complete"
|
||||||
CIFSRcvCancelled -> "rcv_cancelled"
|
CIFSRcvCancelled -> "rcv_cancelled"
|
||||||
CIFSRcvError -> "rcv_error"
|
CIFSRcvError -> "rcv_error"
|
||||||
@@ -614,6 +618,7 @@ instance StrEncoding ACIFileStatus where
|
|||||||
"rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation
|
"rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation
|
||||||
"rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted
|
"rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted
|
||||||
"rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer
|
"rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer
|
||||||
|
"rcv_aborted" -> pure $ AFS SMDRcv CIFSRcvAborted
|
||||||
"rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete
|
"rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete
|
||||||
"rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled
|
"rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled
|
||||||
"rcv_error" -> pure $ AFS SMDRcv CIFSRcvError
|
"rcv_error" -> pure $ AFS SMDRcv CIFSRcvError
|
||||||
@@ -631,6 +636,7 @@ data JSONCIFileStatus
|
|||||||
| JCIFSRcvInvitation
|
| JCIFSRcvInvitation
|
||||||
| JCIFSRcvAccepted
|
| JCIFSRcvAccepted
|
||||||
| JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64}
|
| JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64}
|
||||||
|
| JCIFSRcvAborted
|
||||||
| JCIFSRcvComplete
|
| JCIFSRcvComplete
|
||||||
| JCIFSRcvCancelled
|
| JCIFSRcvCancelled
|
||||||
| JCIFSRcvError
|
| JCIFSRcvError
|
||||||
@@ -646,6 +652,7 @@ jsonCIFileStatus = \case
|
|||||||
CIFSRcvInvitation -> JCIFSRcvInvitation
|
CIFSRcvInvitation -> JCIFSRcvInvitation
|
||||||
CIFSRcvAccepted -> JCIFSRcvAccepted
|
CIFSRcvAccepted -> JCIFSRcvAccepted
|
||||||
CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total
|
CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total
|
||||||
|
CIFSRcvAborted -> JCIFSRcvAborted
|
||||||
CIFSRcvComplete -> JCIFSRcvComplete
|
CIFSRcvComplete -> JCIFSRcvComplete
|
||||||
CIFSRcvCancelled -> JCIFSRcvCancelled
|
CIFSRcvCancelled -> JCIFSRcvCancelled
|
||||||
CIFSRcvError -> JCIFSRcvError
|
CIFSRcvError -> JCIFSRcvError
|
||||||
@@ -661,6 +668,7 @@ aciFileStatusJSON = \case
|
|||||||
JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation
|
JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation
|
||||||
JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted
|
JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted
|
||||||
JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total
|
JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total
|
||||||
|
JCIFSRcvAborted -> AFS SMDRcv CIFSRcvAborted
|
||||||
JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete
|
JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete
|
||||||
JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled
|
JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled
|
||||||
JCIFSRcvError -> AFS SMDRcv CIFSRcvError
|
JCIFSRcvError -> AFS SMDRcv CIFSRcvError
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
|
module Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays where
|
||||||
|
|
||||||
|
import Database.SQLite.Simple (Query)
|
||||||
|
import Database.SQLite.Simple.QQ (sql)
|
||||||
|
|
||||||
|
m20240515_rcv_files_user_approved_relays :: Query
|
||||||
|
m20240515_rcv_files_user_approved_relays =
|
||||||
|
[sql|
|
||||||
|
ALTER TABLE rcv_files ADD COLUMN user_approved_relays INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|]
|
||||||
|
|
||||||
|
down_m20240515_rcv_files_user_approved_relays :: Query
|
||||||
|
down_m20240515_rcv_files_user_approved_relays =
|
||||||
|
[sql|
|
||||||
|
ALTER TABLE rcv_files DROP COLUMN user_approved_relays;
|
||||||
|
|]
|
||||||
@@ -229,7 +229,8 @@ CREATE TABLE rcv_files(
|
|||||||
REFERENCES xftp_file_descriptions ON DELETE SET NULL,
|
REFERENCES xftp_file_descriptions ON DELETE SET NULL,
|
||||||
agent_rcv_file_id BLOB NULL,
|
agent_rcv_file_id BLOB NULL,
|
||||||
agent_rcv_file_deleted INTEGER DEFAULT 0 CHECK(agent_rcv_file_deleted NOT NULL),
|
agent_rcv_file_deleted INTEGER DEFAULT 0 CHECK(agent_rcv_file_deleted NOT NULL),
|
||||||
to_receive INTEGER
|
to_receive INTEGER,
|
||||||
|
user_approved_relays INTEGER NOT NULL DEFAULT 0
|
||||||
);
|
);
|
||||||
CREATE TABLE snd_file_chunks(
|
CREATE TABLE snd_file_chunks(
|
||||||
file_id INTEGER NOT NULL,
|
file_id INTEGER NOT NULL,
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ mobileChatOpts dbFilePrefix =
|
|||||||
smpServers = [],
|
smpServers = [],
|
||||||
xftpServers = [],
|
xftpServers = [],
|
||||||
networkConfig = defaultNetworkConfig,
|
networkConfig = defaultNetworkConfig,
|
||||||
logLevel = CLLError,
|
logLevel = CLLImportant,
|
||||||
logConnections = False,
|
logConnections = False,
|
||||||
logServerHosts = True,
|
logServerHosts = True,
|
||||||
logAgent = Nothing,
|
logAgent = Nothing,
|
||||||
@@ -220,6 +220,7 @@ defaultMobileConfig :: ChatConfig
|
|||||||
defaultMobileConfig =
|
defaultMobileConfig =
|
||||||
defaultChatConfig
|
defaultChatConfig
|
||||||
{ confirmMigrations = MCYesUp,
|
{ confirmMigrations = MCYesUp,
|
||||||
|
logLevel = CLLError,
|
||||||
coreApi = True,
|
coreApi = True,
|
||||||
deviceNameForRemote = "Mobile"
|
deviceNameForRemote = "Mobile"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ module Simplex.Chat.Options
|
|||||||
getChatOpts,
|
getChatOpts,
|
||||||
protocolServersP,
|
protocolServersP,
|
||||||
fullNetworkConfig,
|
fullNetworkConfig,
|
||||||
toLogLevel,
|
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
@@ -69,13 +68,13 @@ data CoreChatOpts = CoreChatOpts
|
|||||||
data ChatCmdLog = CCLAll | CCLMessages | CCLNone
|
data ChatCmdLog = CCLAll | CCLMessages | CCLNone
|
||||||
deriving (Eq)
|
deriving (Eq)
|
||||||
|
|
||||||
toLogLevel :: ChatLogLevel -> LogLevel
|
agentLogLevel :: ChatLogLevel -> LogLevel
|
||||||
toLogLevel = \case
|
agentLogLevel = \case
|
||||||
CLLDebug -> LogDebug
|
CLLDebug -> LogDebug
|
||||||
CLLInfo -> LogInfo
|
CLLInfo -> LogInfo
|
||||||
CLLWarning -> LogWarn
|
CLLWarning -> LogWarn
|
||||||
CLLError -> LogError
|
CLLError -> LogError
|
||||||
CLLImportant -> LogError
|
CLLImportant -> LogInfo
|
||||||
|
|
||||||
coreChatOptsP :: FilePath -> FilePath -> Parser CoreChatOpts
|
coreChatOptsP :: FilePath -> FilePath -> Parser CoreChatOpts
|
||||||
coreChatOptsP appDir defaultDbFileName = do
|
coreChatOptsP appDir defaultDbFileName = do
|
||||||
@@ -195,11 +194,11 @@ coreChatOptsP appDir defaultDbFileName = do
|
|||||||
dbKey,
|
dbKey,
|
||||||
smpServers,
|
smpServers,
|
||||||
xftpServers,
|
xftpServers,
|
||||||
networkConfig = fullNetworkConfig socksProxy (useTcpTimeout socksProxy t) (logTLSErrors || logLevel <= CLLDebug),
|
networkConfig = fullNetworkConfig socksProxy (useTcpTimeout socksProxy t) (logTLSErrors || logLevel == CLLDebug),
|
||||||
logLevel,
|
logLevel,
|
||||||
logConnections = logConnections || logLevel <= CLLInfo,
|
logConnections = logConnections || logLevel <= CLLInfo,
|
||||||
logServerHosts = logServerHosts || logLevel <= CLLInfo,
|
logServerHosts = logServerHosts || logLevel <= CLLInfo,
|
||||||
logAgent = if logAgent || logLevel <= CLLDebug then Just $ toLogLevel logLevel else Nothing,
|
logAgent = if logAgent || logLevel == CLLDebug then Just $ agentLogLevel logLevel else Nothing,
|
||||||
logFile,
|
logFile,
|
||||||
tbqSize,
|
tbqSize,
|
||||||
highlyAvailable
|
highlyAvailable
|
||||||
@@ -343,7 +342,13 @@ protocolServersP :: ProtocolTypeI p => A.Parser [ProtoServerWithAuth p]
|
|||||||
protocolServersP = strP `A.sepBy1` A.char ' '
|
protocolServersP = strP `A.sepBy1` A.char ' '
|
||||||
|
|
||||||
parseLogLevel :: ReadM ChatLogLevel
|
parseLogLevel :: ReadM ChatLogLevel
|
||||||
parseLogLevel = eitherReader $ strDecode . B.pack
|
parseLogLevel = eitherReader $ \case
|
||||||
|
"debug" -> Right CLLDebug
|
||||||
|
"info" -> Right CLLInfo
|
||||||
|
"warn" -> Right CLLWarning
|
||||||
|
"error" -> Right CLLError
|
||||||
|
"important" -> Right CLLImportant
|
||||||
|
_ -> Left "Invalid log level"
|
||||||
|
|
||||||
parseChatCmdLog :: ReadM ChatCmdLog
|
parseChatCmdLog :: ReadM ChatCmdLog
|
||||||
parseChatCmdLog = eitherReader $ \case
|
parseChatCmdLog = eitherReader $ \case
|
||||||
|
|||||||
+16
-16
@@ -13,6 +13,7 @@
|
|||||||
module Simplex.Chat.Remote where
|
module Simplex.Chat.Remote where
|
||||||
|
|
||||||
import Control.Applicative ((<|>))
|
import Control.Applicative ((<|>))
|
||||||
|
import Control.Logger.Simple
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Control.Monad.Except
|
import Control.Monad.Except
|
||||||
import Control.Monad.IO.Class
|
import Control.Monad.IO.Class
|
||||||
@@ -248,7 +249,7 @@ startRemoteHostSession rhKey = do
|
|||||||
|
|
||||||
closeRemoteHost :: RHKey -> CM ()
|
closeRemoteHost :: RHKey -> CM ()
|
||||||
closeRemoteHost rhKey = do
|
closeRemoteHost rhKey = do
|
||||||
logInfo $ "Closing remote host session for " <> tshow rhKey
|
logNote $ "Closing remote host session for " <> tshow rhKey
|
||||||
cancelRemoteHostSession Nothing rhKey
|
cancelRemoteHostSession Nothing rhKey
|
||||||
|
|
||||||
cancelRemoteHostSession :: Maybe (SessionSeq, RemoteHostStopReason) -> RHKey -> CM ()
|
cancelRemoteHostSession :: Maybe (SessionSeq, RemoteHostStopReason) -> RHKey -> CM ()
|
||||||
@@ -265,7 +266,7 @@ cancelRemoteHostSession handlerInfo_ rhKey = do
|
|||||||
modifyTVar' crh $ \cur -> if (RHId <$> cur) == Just rhKey then Nothing else cur -- only wipe the closing RH
|
modifyTVar' crh $ \cur -> if (RHId <$> cur) == Just rhKey then Nothing else cur -- only wipe the closing RH
|
||||||
pure $ Just rhs
|
pure $ Just rhs
|
||||||
forM_ deregistered $ \session -> do
|
forM_ deregistered $ \session -> do
|
||||||
lift (cancelRemoteHost handlingError session) `catchAny` (logError . tshow)
|
liftIO $ cancelRemoteHost handlingError session `catchAny` (logError . tshow)
|
||||||
forM_ (snd <$> handlerInfo_) $ \rhStopReason ->
|
forM_ (snd <$> handlerInfo_) $ \rhStopReason ->
|
||||||
toView CRRemoteHostStopped {remoteHostId_, rhsState = rhsSessionState session, rhStopReason}
|
toView CRRemoteHostStopped {remoteHostId_, rhsState = rhsSessionState session, rhStopReason}
|
||||||
where
|
where
|
||||||
@@ -274,26 +275,25 @@ cancelRemoteHostSession handlerInfo_ rhKey = do
|
|||||||
RHNew -> Nothing
|
RHNew -> Nothing
|
||||||
RHId rhId -> Just rhId
|
RHId rhId -> Just rhId
|
||||||
|
|
||||||
cancelRemoteHost :: Bool -> RemoteHostSession -> CM' ()
|
cancelRemoteHost :: Bool -> RemoteHostSession -> IO ()
|
||||||
cancelRemoteHost handlingError = \case
|
cancelRemoteHost handlingError = \case
|
||||||
RHSessionStarting -> pure ()
|
RHSessionStarting -> pure ()
|
||||||
RHSessionConnecting _inv rhs -> cancelPendingSession rhs
|
RHSessionConnecting _inv rhs -> cancelPendingSession rhs
|
||||||
RHSessionPendingConfirmation _sessCode tls rhs -> do
|
RHSessionPendingConfirmation _sessCode tls rhs -> do
|
||||||
cancelPendingSession rhs
|
cancelPendingSession rhs
|
||||||
closeConn tls
|
closeConnection tls
|
||||||
RHSessionConfirmed tls rhs -> do
|
RHSessionConfirmed tls rhs -> do
|
||||||
cancelPendingSession rhs
|
cancelPendingSession rhs
|
||||||
closeConn tls
|
closeConnection tls
|
||||||
RHSessionConnected {rchClient, tls, rhClient = RemoteHostClient {httpClient}, pollAction} -> do
|
RHSessionConnected {rchClient, tls, rhClient = RemoteHostClient {httpClient}, pollAction} -> do
|
||||||
uninterruptibleCancel pollAction
|
uninterruptibleCancel pollAction
|
||||||
liftIO (cancelHostClient rchClient) `catchAny` (logError' . tshow)
|
cancelHostClient rchClient `catchAny` (logError . tshow)
|
||||||
closeConn tls
|
closeConnection tls `catchAny` (logError . tshow)
|
||||||
unless handlingError $ liftIO (closeHTTP2Client httpClient) `catchAny` (logError' . tshow)
|
unless handlingError $ closeHTTP2Client httpClient `catchAny` (logError . tshow)
|
||||||
where
|
where
|
||||||
closeConn tls = liftIO (closeConnection tls) `catchAny` (logError' . tshow)
|
|
||||||
cancelPendingSession RHPendingSession {rchClient, rhsWaitSession} = do
|
cancelPendingSession RHPendingSession {rchClient, rhsWaitSession} = do
|
||||||
unless handlingError $ uninterruptibleCancel rhsWaitSession `catchAny` (logError' . tshow)
|
unless handlingError $ uninterruptibleCancel rhsWaitSession `catchAny` (logError . tshow)
|
||||||
liftIO (cancelHostClient rchClient) `catchAny` (logError' . tshow)
|
cancelHostClient rchClient `catchAny` (logError . tshow)
|
||||||
|
|
||||||
-- | Generate a random 16-char filepath without / in it by using base64url encoding.
|
-- | Generate a random 16-char filepath without / in it by using base64url encoding.
|
||||||
randomStorePath :: IO FilePath
|
randomStorePath :: IO FilePath
|
||||||
@@ -495,7 +495,7 @@ parseCtrlAppInfo ctrlAppInfo = do
|
|||||||
|
|
||||||
handleRemoteCommand :: (ByteString -> CM' ChatResponse) -> RemoteCrypto -> TBQueue ChatResponse -> HTTP2Request -> CM' ()
|
handleRemoteCommand :: (ByteString -> CM' ChatResponse) -> RemoteCrypto -> TBQueue ChatResponse -> HTTP2Request -> CM' ()
|
||||||
handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do
|
handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {request, reqBody, sendResponse} = do
|
||||||
logDebug' "handleRemoteCommand"
|
logDebug "handleRemoteCommand"
|
||||||
liftIO (tryRemoteError' parseRequest) >>= \case
|
liftIO (tryRemoteError' parseRequest) >>= \case
|
||||||
Right (getNext, rc) -> do
|
Right (getNext, rc) -> do
|
||||||
chatReadVar' currentUser >>= \case
|
chatReadVar' currentUser >>= \case
|
||||||
@@ -511,7 +511,7 @@ handleRemoteCommand execChatCommand encryption remoteOutputQ HTTP2Request {reque
|
|||||||
processCommand :: User -> GetChunk -> RemoteCommand -> CM ()
|
processCommand :: User -> GetChunk -> RemoteCommand -> CM ()
|
||||||
processCommand user getNext = \case
|
processCommand user getNext = \case
|
||||||
RCSend {command} -> lift $ handleSend execChatCommand command >>= reply
|
RCSend {command} -> lift $ handleSend execChatCommand command >>= reply
|
||||||
RCRecv {wait = time} -> lift $ handleRecv time remoteOutputQ >>= reply
|
RCRecv {wait = time} -> lift $ liftIO (handleRecv time remoteOutputQ) >>= reply
|
||||||
RCStoreFile {fileName, fileSize, fileDigest} -> lift $ handleStoreFile encryption fileName fileSize fileDigest getNext >>= reply
|
RCStoreFile {fileName, fileSize, fileDigest} -> lift $ handleStoreFile encryption fileName fileSize fileDigest getNext >>= reply
|
||||||
RCGetFile {file} -> handleGetFile encryption user file replyWith
|
RCGetFile {file} -> handleGetFile encryption user file replyWith
|
||||||
reply :: RemoteResponse -> CM' ()
|
reply :: RemoteResponse -> CM' ()
|
||||||
@@ -547,14 +547,14 @@ tryRemoteError' = tryAllErrors' (RPEException . tshow)
|
|||||||
|
|
||||||
handleSend :: (ByteString -> CM' ChatResponse) -> Text -> CM' RemoteResponse
|
handleSend :: (ByteString -> CM' ChatResponse) -> Text -> CM' RemoteResponse
|
||||||
handleSend execChatCommand command = do
|
handleSend execChatCommand command = do
|
||||||
logDebug' $ "Send: " <> tshow command
|
logDebug $ "Send: " <> tshow command
|
||||||
-- execChatCommand checks for remote-allowed commands
|
-- execChatCommand checks for remote-allowed commands
|
||||||
-- convert errors thrown in execChatCommand into error responses to prevent aborting the protocol wrapper
|
-- convert errors thrown in execChatCommand into error responses to prevent aborting the protocol wrapper
|
||||||
RRChatResponse <$> execChatCommand (encodeUtf8 command)
|
RRChatResponse <$> execChatCommand (encodeUtf8 command)
|
||||||
|
|
||||||
handleRecv :: Int -> TBQueue ChatResponse -> CM' RemoteResponse
|
handleRecv :: Int -> TBQueue ChatResponse -> IO RemoteResponse
|
||||||
handleRecv time events = do
|
handleRecv time events = do
|
||||||
logDebug' $ "Recv: " <> tshow time
|
logDebug $ "Recv: " <> tshow time
|
||||||
RRChatEvent <$> (timeout time . atomically $ readTBQueue events)
|
RRChatEvent <$> (timeout time . atomically $ readTBQueue events)
|
||||||
|
|
||||||
-- TODO this command could remember stored files and return IDs to allow removing files that are not needed.
|
-- TODO this command could remember stored files and return IDs to allow removing files that are not needed.
|
||||||
|
|||||||
@@ -514,7 +514,7 @@ createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@File
|
|||||||
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
|
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
|
||||||
let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_
|
let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_
|
||||||
-- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it
|
-- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it
|
||||||
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_
|
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False, userApprovedRelays = False}) <$> rfd_
|
||||||
fileProtocol = if isJust rfd_ then FPXFTP else FPSMP
|
fileProtocol = if isJust rfd_ then FPXFTP else FPSMP
|
||||||
fileId <- liftIO $ do
|
fileId <- liftIO $ do
|
||||||
DB.execute
|
DB.execute
|
||||||
@@ -535,7 +535,7 @@ createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localD
|
|||||||
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
|
rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr
|
||||||
let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_
|
let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_
|
||||||
-- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it
|
-- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it
|
||||||
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_
|
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False, userApprovedRelays = False}) <$> rfd_
|
||||||
fileProtocol = if isJust rfd_ then FPXFTP else FPSMP
|
fileProtocol = if isJust rfd_ then FPXFTP else FPSMP
|
||||||
fileId <- liftIO $ do
|
fileId <- liftIO $ do
|
||||||
DB.execute
|
DB.execute
|
||||||
@@ -676,7 +676,9 @@ getRcvFileTransfer_ db userId fileId = do
|
|||||||
[sql|
|
[sql|
|
||||||
SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name,
|
SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name,
|
||||||
f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name,
|
f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name,
|
||||||
f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, r.agent_rcv_file_id, r.agent_rcv_file_deleted, c.connection_id, c.agent_conn_id
|
f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline,
|
||||||
|
r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays,
|
||||||
|
c.connection_id, c.agent_conn_id
|
||||||
FROM rcv_files r
|
FROM rcv_files r
|
||||||
JOIN files f USING (file_id)
|
JOIN files f USING (file_id)
|
||||||
LEFT JOIN connections c ON r.file_id = c.rcv_file_id
|
LEFT JOIN connections c ON r.file_id = c.rcv_file_id
|
||||||
@@ -690,9 +692,9 @@ getRcvFileTransfer_ db userId fileId = do
|
|||||||
where
|
where
|
||||||
rcvFileTransfer ::
|
rcvFileTransfer ::
|
||||||
Maybe RcvFileDescr ->
|
Maybe RcvFileDescr ->
|
||||||
(FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool) :. (Maybe Int64, Maybe AgentConnId) ->
|
(FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool, Bool) :. (Maybe Int64, Maybe AgentConnId) ->
|
||||||
ExceptT StoreError IO RcvFileTransfer
|
ExceptT StoreError IO RcvFileTransfer
|
||||||
rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted) :. (connId_, agentConnId_)) =
|
rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted, userApprovedRelays) :. (connId_, agentConnId_)) =
|
||||||
case contactName_ <|> memberName_ <|> standaloneName_ of
|
case contactName_ <|> memberName_ <|> standaloneName_ of
|
||||||
Nothing -> throwError $ SERcvFileInvalid fileId
|
Nothing -> throwError $ SERcvFileInvalid fileId
|
||||||
Just name ->
|
Just name ->
|
||||||
@@ -709,7 +711,7 @@ getRcvFileTransfer_ db userId fileId = do
|
|||||||
ft senderDisplayName fileStatus =
|
ft senderDisplayName fileStatus =
|
||||||
let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing}
|
let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing}
|
||||||
cryptoArgs = CFArgs <$> fileKey <*> fileNonce
|
cryptoArgs = CFArgs <$> fileKey <*> fileNonce
|
||||||
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted}) <$> rfd_
|
xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted, userApprovedRelays}) <$> rfd_
|
||||||
in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId, cryptoArgs}
|
in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId, cryptoArgs}
|
||||||
rfi = maybe (throwError $ SERcvFileInvalid fileId) pure =<< rfi_
|
rfi = maybe (throwError $ SERcvFileInvalid fileId) pure =<< rfi_
|
||||||
rfi_ = case (filePath_, connId_, agentConnId_) of
|
rfi_ = case (filePath_, connId_, agentConnId_) of
|
||||||
@@ -720,7 +722,7 @@ getRcvFileTransfer_ db userId fileId = do
|
|||||||
acceptRcvFileTransfer :: DB.Connection -> VersionRangeChat -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> SubscriptionMode -> ExceptT StoreError IO AChatItem
|
acceptRcvFileTransfer :: DB.Connection -> VersionRangeChat -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> SubscriptionMode -> ExceptT StoreError IO AChatItem
|
||||||
acceptRcvFileTransfer db vr user@User {userId} fileId (cmdId, acId) connStatus filePath subMode = ExceptT $ do
|
acceptRcvFileTransfer db vr user@User {userId} fileId (cmdId, acId) connStatus filePath subMode = ExceptT $ do
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
acceptRcvFT_ db user fileId filePath Nothing currentTs
|
acceptRcvFT_ db user fileId filePath False Nothing currentTs
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
"INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at, to_subscribe) VALUES (?,?,?,?,?,?,?,?)"
|
"INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at, to_subscribe) VALUES (?,?,?,?,?,?,?,?)"
|
||||||
@@ -740,33 +742,40 @@ getContactByFileId db vr user@User {userId} fileId = do
|
|||||||
|
|
||||||
acceptRcvInlineFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem
|
acceptRcvInlineFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem
|
||||||
acceptRcvInlineFT db vr user fileId filePath = do
|
acceptRcvInlineFT db vr user fileId filePath = do
|
||||||
liftIO $ acceptRcvFT_ db user fileId filePath (Just IFMOffer) =<< getCurrentTime
|
liftIO $ acceptRcvFT_ db user fileId filePath False (Just IFMOffer) =<< getCurrentTime
|
||||||
getChatItemByFileId db vr user fileId
|
getChatItemByFileId db vr user fileId
|
||||||
|
|
||||||
startRcvInlineFT :: DB.Connection -> User -> RcvFileTransfer -> FilePath -> Maybe InlineFileMode -> IO ()
|
startRcvInlineFT :: DB.Connection -> User -> RcvFileTransfer -> FilePath -> Maybe InlineFileMode -> IO ()
|
||||||
startRcvInlineFT db user RcvFileTransfer {fileId} filePath rcvFileInline =
|
startRcvInlineFT db user RcvFileTransfer {fileId} filePath rcvFileInline =
|
||||||
acceptRcvFT_ db user fileId filePath rcvFileInline =<< getCurrentTime
|
acceptRcvFT_ db user fileId filePath False rcvFileInline =<< getCurrentTime
|
||||||
|
|
||||||
xftpAcceptRcvFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem
|
xftpAcceptRcvFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> Bool -> ExceptT StoreError IO AChatItem
|
||||||
xftpAcceptRcvFT db vr user fileId filePath = do
|
xftpAcceptRcvFT db vr user fileId filePath userApprovedRelays = do
|
||||||
liftIO $ acceptRcvFT_ db user fileId filePath Nothing =<< getCurrentTime
|
liftIO $ acceptRcvFT_ db user fileId filePath userApprovedRelays Nothing =<< getCurrentTime
|
||||||
getChatItemByFileId db vr user fileId
|
getChatItemByFileId db vr user fileId
|
||||||
|
|
||||||
acceptRcvFT_ :: DB.Connection -> User -> FileTransferId -> FilePath -> Maybe InlineFileMode -> UTCTime -> IO ()
|
acceptRcvFT_ :: DB.Connection -> User -> FileTransferId -> FilePath -> Bool -> Maybe InlineFileMode -> UTCTime -> IO ()
|
||||||
acceptRcvFT_ db User {userId} fileId filePath rcvFileInline currentTs = do
|
acceptRcvFT_ db User {userId} fileId filePath userApprovedRelays rcvFileInline currentTs = do
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
"UPDATE files SET file_path = ?, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?"
|
"UPDATE files SET file_path = ?, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?"
|
||||||
(filePath, CIFSRcvAccepted, currentTs, userId, fileId)
|
(filePath, CIFSRcvAccepted, currentTs, userId, fileId)
|
||||||
DB.execute
|
DB.execute
|
||||||
db
|
db
|
||||||
"UPDATE rcv_files SET rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?"
|
"UPDATE rcv_files SET user_approved_relays = ?, rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?"
|
||||||
(rcvFileInline, FSAccepted, currentTs, fileId)
|
(userApprovedRelays, rcvFileInline, FSAccepted, currentTs, fileId)
|
||||||
|
|
||||||
setRcvFileToReceive :: DB.Connection -> FileTransferId -> Maybe CryptoFileArgs -> IO ()
|
setRcvFileToReceive :: DB.Connection -> FileTransferId -> Bool -> Maybe CryptoFileArgs -> IO ()
|
||||||
setRcvFileToReceive db fileId cfArgs_ = do
|
setRcvFileToReceive db fileId userApprovedRelays cfArgs_ = do
|
||||||
currentTs <- getCurrentTime
|
currentTs <- getCurrentTime
|
||||||
DB.execute db "UPDATE rcv_files SET to_receive = 1, updated_at = ? WHERE file_id = ?" (currentTs, fileId)
|
DB.execute
|
||||||
|
db
|
||||||
|
[sql|
|
||||||
|
UPDATE rcv_files
|
||||||
|
SET to_receive = 1, user_approved_relays = ?, updated_at = ?
|
||||||
|
WHERE file_id = ?
|
||||||
|
|]
|
||||||
|
(userApprovedRelays, currentTs, fileId)
|
||||||
forM_ cfArgs_ $ \cfArgs -> setFileCryptoArgs_ db fileId cfArgs currentTs
|
forM_ cfArgs_ $ \cfArgs -> setFileCryptoArgs_ db fileId cfArgs currentTs
|
||||||
|
|
||||||
setFileCryptoArgs :: DB.Connection -> FileTransferId -> CryptoFileArgs -> IO ()
|
setFileCryptoArgs :: DB.Connection -> FileTransferId -> CryptoFileArgs -> IO ()
|
||||||
@@ -950,7 +959,7 @@ getFileTransferMeta_ db userId fileId =
|
|||||||
fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileKey, fileNonce, fileInline, aSndFileId_, agentSndFileDeleted, privateSndFileDescr, cancelled_, xftpRedirectFor) =
|
fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileKey, fileNonce, fileInline, aSndFileId_, agentSndFileDeleted, privateSndFileDescr, cancelled_, xftpRedirectFor) =
|
||||||
let cryptoArgs = CFArgs <$> fileKey <*> fileNonce
|
let cryptoArgs = CFArgs <$> fileKey <*> fileNonce
|
||||||
xftpSndFile = (\fId -> XFTPSndFile {agentSndFileId = fId, privateSndFileDescr, agentSndFileDeleted, cryptoArgs}) <$> aSndFileId_
|
xftpSndFile = (\fId -> XFTPSndFile {agentSndFileId = fId, privateSndFileDescr, agentSndFileDeleted, cryptoArgs}) <$> aSndFileId_
|
||||||
in FileTransferMeta {fileId, xftpSndFile, xftpRedirectFor, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_}
|
in FileTransferMeta {fileId, xftpSndFile, xftpRedirectFor, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_}
|
||||||
|
|
||||||
lookupFileTransferRedirectMeta :: DB.Connection -> User -> Int64 -> IO [FileTransferMeta]
|
lookupFileTransferRedirectMeta :: DB.Connection -> User -> Int64 -> IO [FileTransferMeta]
|
||||||
lookupFileTransferRedirectMeta db User {userId} fileId = do
|
lookupFileTransferRedirectMeta db User {userId} fileId = do
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ import Simplex.Chat.Migrations.M20240402_item_forwarded
|
|||||||
import Simplex.Chat.Migrations.M20240430_ui_theme
|
import Simplex.Chat.Migrations.M20240430_ui_theme
|
||||||
import Simplex.Chat.Migrations.M20240501_chat_deleted
|
import Simplex.Chat.Migrations.M20240501_chat_deleted
|
||||||
import Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
|
import Simplex.Chat.Migrations.M20240510_chat_items_via_proxy
|
||||||
|
import Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays
|
||||||
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)]
|
||||||
@@ -215,7 +216,8 @@ schemaMigrations =
|
|||||||
("20240402_item_forwarded", m20240402_item_forwarded, Just down_m20240402_item_forwarded),
|
("20240402_item_forwarded", m20240402_item_forwarded, Just down_m20240402_item_forwarded),
|
||||||
("20240430_ui_theme", m20240430_ui_theme, Just down_m20240430_ui_theme),
|
("20240430_ui_theme", m20240430_ui_theme, Just down_m20240430_ui_theme),
|
||||||
("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted),
|
("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted),
|
||||||
("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy)
|
("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy),
|
||||||
|
("20240515_rcv_files_user_approved_relays", m20240515_rcv_files_user_approved_relays, Just down_m20240515_rcv_files_user_approved_relays)
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | The list of migrations in ascending order by date
|
-- | The list of migrations in ascending order by date
|
||||||
|
|||||||
@@ -48,8 +48,7 @@ simplexChatCLI cfg server_ = do
|
|||||||
ts <- getCurrentTime
|
ts <- getCurrentTime
|
||||||
tz <- getCurrentTimeZone
|
tz <- getCurrentTimeZone
|
||||||
rh <- readTVarIO $ currentRemoteHost cc
|
rh <- readTVarIO $ currentRemoteHost cc
|
||||||
ll <- readTVarIO $ appLogLevel cc
|
putStrLn $ serializeChatResponse (rh, Just user) ts tz rh r
|
||||||
putStrLn $ serializeChatResponse (rh, Just user) ll ts tz rh r
|
|
||||||
|
|
||||||
welcome :: ChatOpts -> IO ()
|
welcome :: ChatOpts -> IO ()
|
||||||
welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, networkConfig}} =
|
welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, networkConfig}} =
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
module Simplex.Chat.Terminal.Output where
|
module Simplex.Chat.Terminal.Output where
|
||||||
|
|
||||||
import Control.Concurrent (ThreadId)
|
import Control.Concurrent (ThreadId)
|
||||||
|
import Control.Logger.Simple
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Control.Monad.Catch (MonadMask)
|
import Control.Monad.Catch (MonadMask)
|
||||||
import Control.Monad.Except
|
import Control.Monad.Except
|
||||||
@@ -167,10 +168,9 @@ runTerminalOutput ct cc@ChatController {outputQ, showLiveItems, logFilePath} Cha
|
|||||||
_ -> pure ()
|
_ -> pure ()
|
||||||
logResponse path s = withFile path AppendMode $ \h -> mapM_ (hPutStrLn h . unStyle) s
|
logResponse path s = withFile path AppendMode $ \h -> mapM_ (hPutStrLn h . unStyle) s
|
||||||
getRemoteUser rhId =
|
getRemoteUser rhId =
|
||||||
flip runReaderT cc $
|
runReaderT (execChatCommand (Just rhId) "/user") cc >>= \case
|
||||||
execChatCommand (Just rhId) "/user" >>= \case
|
CRActiveUser {user} -> updateRemoteUser ct user rhId
|
||||||
CRActiveUser {user} -> liftIO $ updateRemoteUser ct user rhId
|
cr -> logError $ "Unexpected reply while getting remote user: " <> tshow cr
|
||||||
cr -> logError' $ "Unexpected reply while getting remote user: " <> tshow cr
|
|
||||||
removeRemoteUser rhId = atomically $ TM.delete rhId (currentRemoteUsers ct)
|
removeRemoteUser rhId = atomically $ TM.delete rhId (currentRemoteUsers ct)
|
||||||
|
|
||||||
responseNotification :: ChatTerminal -> ChatController -> ChatResponse -> IO ()
|
responseNotification :: ChatTerminal -> ChatController -> ChatResponse -> IO ()
|
||||||
@@ -275,8 +275,7 @@ responseString ct cc liveItems outputRH r = do
|
|||||||
cu <- getCurrentUser ct cc
|
cu <- getCurrentUser ct cc
|
||||||
ts <- getCurrentTime
|
ts <- getCurrentTime
|
||||||
tz <- getCurrentTimeZone
|
tz <- getCurrentTimeZone
|
||||||
ll <- readTVarIO $ appLogLevel cc
|
pure $ responseToView cu (config cc) liveItems ts tz outputRH r
|
||||||
pure $ responseToView cu (config cc) ll liveItems ts tz outputRH r
|
|
||||||
|
|
||||||
updateRemoteUser :: ChatTerminal -> User -> RemoteHostId -> IO ()
|
updateRemoteUser :: ChatTerminal -> User -> RemoteHostId -> IO ()
|
||||||
updateRemoteUser ct user rhId = atomically $ TM.insert rhId user (currentRemoteUsers ct)
|
updateRemoteUser ct user rhId = atomically $ TM.insert rhId user (currentRemoteUsers ct)
|
||||||
|
|||||||
@@ -1072,7 +1072,8 @@ data RcvFileTransfer = RcvFileTransfer
|
|||||||
data XFTPRcvFile = XFTPRcvFile
|
data XFTPRcvFile = XFTPRcvFile
|
||||||
{ rcvFileDescription :: RcvFileDescr,
|
{ rcvFileDescription :: RcvFileDescr,
|
||||||
agentRcvFileId :: Maybe AgentRcvFileId,
|
agentRcvFileId :: Maybe AgentRcvFileId,
|
||||||
agentRcvFileDeleted :: Bool
|
agentRcvFileDeleted :: Bool,
|
||||||
|
userApprovedRelays :: Bool
|
||||||
}
|
}
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
|||||||
@@ -79,11 +79,11 @@ data WCallCommand
|
|||||||
|
|
||||||
$(JQ.deriveToJSON (taggedObjectJSON $ dropPrefix "WCCall") ''WCallCommand)
|
$(JQ.deriveToJSON (taggedObjectJSON $ dropPrefix "WCCall") ''WCallCommand)
|
||||||
|
|
||||||
serializeChatResponse :: (Maybe RemoteHostId, Maybe User) -> ChatLogLevel -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> String
|
serializeChatResponse :: (Maybe RemoteHostId, Maybe User) -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> String
|
||||||
serializeChatResponse user_ logLevel ts tz remoteHost_ = unlines . map unStyle . responseToView user_ defaultChatConfig logLevel False ts tz remoteHost_
|
serializeChatResponse user_ ts tz remoteHost_ = unlines . map unStyle . responseToView user_ defaultChatConfig False ts tz remoteHost_
|
||||||
|
|
||||||
responseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> ChatLogLevel -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> [StyledString]
|
responseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> [StyledString]
|
||||||
responseToView hu@(currentRH, user_) ChatConfig {showReactions, showReceipts, testView} logLevel liveItems ts tz outputRH = \case
|
responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showReceipts, testView} liveItems ts tz outputRH = \case
|
||||||
CRActiveUser User {profile, uiThemes} -> viewUserProfile (fromLocalProfile profile) <> viewUITheme uiThemes
|
CRActiveUser User {profile, uiThemes} -> viewUserProfile (fromLocalProfile profile) <> viewUITheme uiThemes
|
||||||
CRUsersList users -> viewUsersList users
|
CRUsersList users -> viewUsersList users
|
||||||
CRChatStarted -> ["chat started"]
|
CRChatStarted -> ["chat started"]
|
||||||
@@ -391,8 +391,6 @@ responseToView hu@(currentRH, user_) ChatConfig {showReactions, showReceipts, te
|
|||||||
CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError logLevel testView e
|
CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError logLevel testView e
|
||||||
CRChatError u e -> ttyUser' u $ viewChatError logLevel testView e
|
CRChatError u e -> ttyUser' u $ viewChatError logLevel testView e
|
||||||
CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError logLevel testView) errs
|
CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError logLevel testView) errs
|
||||||
CRAgentLog {} -> []
|
|
||||||
CRChatLog {} -> []
|
|
||||||
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 _ _ -> []
|
||||||
@@ -1768,6 +1766,7 @@ viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId
|
|||||||
CIFSRcvInvitation -> ["receiving " <> fstr <> " not accepted yet, use " <> highlight ("/fr " <> show fileId) <> " to receive file"]
|
CIFSRcvInvitation -> ["receiving " <> fstr <> " not accepted yet, use " <> highlight ("/fr " <> show fileId) <> " to receive file"]
|
||||||
CIFSRcvAccepted -> ["receiving " <> fstr <> " just started"]
|
CIFSRcvAccepted -> ["receiving " <> fstr <> " just started"]
|
||||||
CIFSRcvTransfer progress total -> ["receiving " <> fstr <> " progress " <> fileProgressXFTP progress total fileSize]
|
CIFSRcvTransfer progress total -> ["receiving " <> fstr <> " progress " <> fileProgressXFTP progress total fileSize]
|
||||||
|
CIFSRcvAborted -> ["receiving " <> fstr <> " aborted, use " <> highlight ("/fr " <> show fileId) <> " to receive file"]
|
||||||
CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\(CryptoFile fp _) -> ", path: " <> plain fp) fileSource]
|
CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\(CryptoFile fp _) -> ", path: " <> plain fp) fileSource]
|
||||||
CIFSRcvCancelled -> ["receiving " <> fstr <> " cancelled"]
|
CIFSRcvCancelled -> ["receiving " <> fstr <> " cancelled"]
|
||||||
CIFSRcvError -> ["receiving " <> fstr <> " error"]
|
CIFSRcvError -> ["receiving " <> fstr <> " error"]
|
||||||
@@ -1971,8 +1970,9 @@ viewChatError logLevel testView = \case
|
|||||||
CEFileImageType _ -> ["image type must be jpg, send as a file using " <> highlight' "/f"]
|
CEFileImageType _ -> ["image type must be jpg, send as a file using " <> highlight' "/f"]
|
||||||
CEFileImageSize _ -> ["max image size: " <> sShow maxImageSize <> " bytes, resize it or send as a file using " <> highlight' "/f"]
|
CEFileImageSize _ -> ["max image size: " <> sShow maxImageSize <> " bytes, resize it or send as a file using " <> highlight' "/f"]
|
||||||
CEFileNotReceived fileId -> ["file " <> sShow fileId <> " not received"]
|
CEFileNotReceived fileId -> ["file " <> sShow fileId <> " not received"]
|
||||||
CEXFTPRcvFile fileId aFileId e -> ["error receiving XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel <= CLLError]
|
CEFileNotApproved fileId unknownSrvs -> ["file " <> sShow fileId <> " aborted, unknwon XFTP servers:"] <> map (plain . show) unknownSrvs
|
||||||
CEXFTPSndFile fileId aFileId e -> ["error sending XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel <= CLLError]
|
CEXFTPRcvFile fileId aFileId e -> ["error receiving XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel == CLLError]
|
||||||
|
CEXFTPSndFile fileId aFileId e -> ["error sending XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel == CLLError]
|
||||||
CEFallbackToSMPProhibited fileId -> ["recipient tried to accept file " <> sShow fileId <> " via old protocol, prohibited"]
|
CEFallbackToSMPProhibited fileId -> ["recipient tried to accept file " <> sShow fileId <> " via old protocol, prohibited"]
|
||||||
CEInlineFileProhibited _ -> ["A small file sent without acceptance - you can enable receiving such files with -f option."]
|
CEInlineFileProhibited _ -> ["A small file sent without acceptance - you can enable receiving such files with -f option."]
|
||||||
CEInvalidQuote -> ["cannot reply to this message"]
|
CEInvalidQuote -> ["cannot reply to this message"]
|
||||||
@@ -2034,7 +2034,7 @@ viewChatError logLevel testView = \case
|
|||||||
<> "error: connection authorization failed - this could happen if connection was deleted,\
|
<> "error: connection authorization failed - this could happen if connection was deleted,\
|
||||||
\ secured with different credentials, or due to a bug - please re-create the connection"
|
\ secured with different credentials, or due to a bug - please re-create the connection"
|
||||||
]
|
]
|
||||||
AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel <= CLLDebug]
|
AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug]
|
||||||
AGENT A_PROHIBITED -> [withConnEntity <> "error: AGENT A_PROHIBITED" | logLevel <= CLLWarning]
|
AGENT A_PROHIBITED -> [withConnEntity <> "error: AGENT A_PROHIBITED" | logLevel <= CLLWarning]
|
||||||
CONN NOT_FOUND -> [withConnEntity <> "error: CONN NOT_FOUND" | logLevel <= CLLWarning]
|
CONN NOT_FOUND -> [withConnEntity <> "error: CONN NOT_FOUND" | logLevel <= CLLWarning]
|
||||||
CRITICAL restart e -> [plain $ "critical error: " <> e] <> ["please restart the app" | restart]
|
CRITICAL restart e -> [plain $ "critical error: " <> e] <> ["please restart the app" | restart]
|
||||||
|
|||||||
Reference in New Issue
Block a user