mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7f5443920 | |||
| ccff79aa59 | |||
| ccb52e0acd | |||
| d84b30c071 | |||
| 5ae0afe1fe | |||
| d250e503b0 | |||
| afb0ae3d03 | |||
| 1a3f0bed47 | |||
| 1e280fb7e1 | |||
| 6feac55380 |
@@ -192,10 +192,13 @@ class ChatModel(val controller: ChatController) {
|
||||
// add to current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem)
|
||||
} else {
|
||||
chatItems.add(cItem)
|
||||
// Prevent situation when chat item already in the list received from backend
|
||||
if (chatItems.none { it.id == cItem.id }) {
|
||||
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
|
||||
chatItems.add(kotlin.math.max(0, chatItems.lastIndex), cItem)
|
||||
} else {
|
||||
chatItems.add(cItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,19 +225,19 @@ class ChatModel(val controller: ChatController) {
|
||||
res = true
|
||||
}
|
||||
// update current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
||||
if (itemIndex >= 0) {
|
||||
chatItems[itemIndex] = cItem
|
||||
return false
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
return if (chatId.value == cInfo.id) {
|
||||
withContext(Dispatchers.Main) {
|
||||
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
|
||||
if (itemIndex >= 0) {
|
||||
chatItems[itemIndex] = cItem
|
||||
false
|
||||
} else {
|
||||
chatItems.add(cItem)
|
||||
true
|
||||
}
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return res
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,8 @@ class AudioRecorder {
|
||||
AVEncoderBitRateKey: 12000,
|
||||
AVNumberOfChannelsKey: 1
|
||||
]
|
||||
audioRecorder = try AVAudioRecorder(url: getAppFilePath(fileName), settings: settings)
|
||||
let url = getAppFilePath(fileName)
|
||||
audioRecorder = try AVAudioRecorder(url: url, settings: settings)
|
||||
audioRecorder?.record(forDuration: MAX_VOICE_MESSAGE_LENGTH)
|
||||
|
||||
await MainActor.run {
|
||||
@@ -102,7 +103,8 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
|
||||
}
|
||||
|
||||
func start(fileName: String) {
|
||||
audioPlayer = try? AVAudioPlayer(contentsOf: getAppFilePath(fileName))
|
||||
let url = getAppFilePath(fileName)
|
||||
audioPlayer = try? AVAudioPlayer(contentsOf: url)
|
||||
audioPlayer?.delegate = self
|
||||
audioPlayer?.prepareToPlay()
|
||||
audioPlayer?.play()
|
||||
|
||||
@@ -33,8 +33,6 @@ final class ChatModel: ObservableObject {
|
||||
// items in the terminal view
|
||||
@Published var terminalItems: [TerminalItem] = []
|
||||
@Published var userAddress: UserContactLink?
|
||||
@Published var userSMPServers: [ServerCfg]?
|
||||
@Published var presetSMPServers: [String]?
|
||||
@Published var chatItemTTL: ChatItemTTL = .none
|
||||
@Published var appOpenUrl: URL?
|
||||
@Published var deviceToken: DeviceToken?
|
||||
@@ -55,13 +53,13 @@ final class ChatModel: ObservableObject {
|
||||
// currently showing QR code
|
||||
@Published var connReqInv: String?
|
||||
// audio recording and playback
|
||||
@Published var stopPreviousRecPlay: Bool = false // value is not taken into account, only the fact it switches
|
||||
@Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source
|
||||
@Published var draft: ComposeState?
|
||||
@Published var draftChatId: String?
|
||||
|
||||
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
|
||||
|
||||
var filesToDelete: [String] = []
|
||||
var filesToDelete: Set<URL> = []
|
||||
|
||||
static let shared = ChatModel()
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import SimpleXChat
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
|
||||
func getLoadedFilePath(_ file: CIFile?) -> String? {
|
||||
if let fileName = getLoadedFileName(file) {
|
||||
@@ -42,6 +43,17 @@ func getLoadedImage(_ file: CIFile?) -> UIImage? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLoadedVideo(_ file: CIFile?) -> URL? {
|
||||
let loadedFilePath = getLoadedFilePath(file)
|
||||
if loadedFilePath != nil, let fileName = file?.filePath {
|
||||
let filePath = getAppFilePath(fileName)
|
||||
if FileManager.default.fileExists(atPath: filePath.path) {
|
||||
return filePath
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveAnimImage(_ image: UIImage) -> String? {
|
||||
let fileName = generateNewFileName("IMG", "gif")
|
||||
guard let imageData = image.imageData else { return nil }
|
||||
@@ -164,6 +176,20 @@ func saveFileFromURL(_ url: URL) -> String? {
|
||||
return savedFile
|
||||
}
|
||||
|
||||
func saveFileFromURLWithoutLoad(_ url: URL) -> String? {
|
||||
let savedFile: String?
|
||||
do {
|
||||
let fileName = uniqueCombine(url.lastPathComponent)
|
||||
try FileManager.default.moveItem(at: url, to: getAppFilePath(fileName))
|
||||
ChatModel.shared.filesToDelete.remove(url)
|
||||
savedFile = fileName
|
||||
} catch {
|
||||
logger.error("FileUtils.saveFileFromURLWithoutLoad error: \(error.localizedDescription)")
|
||||
savedFile = nil
|
||||
}
|
||||
return savedFile
|
||||
}
|
||||
|
||||
func generateNewFileName(_ prefix: String, _ ext: String) -> String {
|
||||
uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)")
|
||||
}
|
||||
@@ -204,6 +230,18 @@ private func dropPrefix(_ s: String, _ prefix: String) -> String {
|
||||
s.hasPrefix(prefix) ? String(s.dropFirst(prefix.count)) : s
|
||||
}
|
||||
|
||||
extension AVAsset {
|
||||
func generatePreview() -> (UIImage, Int)? {
|
||||
let generator = AVAssetImageGenerator(asset: self)
|
||||
generator.appliesPreferredTrackTransform = true
|
||||
var actualTime = CMTimeMake(value: 0, timescale: 0)
|
||||
if let image = try? generator.copyCGImage(at: CMTimeMakeWithSeconds(0.0, preferredTimescale: 1), actualTime: &actualTime) {
|
||||
return (UIImage(cgImage: image), Int(duration.seconds))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension UIImage {
|
||||
func replaceColor(_ from: UIColor, _ to: UIColor) -> UIImage {
|
||||
if let cgImage = cgImage {
|
||||
|
||||
@@ -391,30 +391,22 @@ func apiDeleteToken(token: DeviceToken) async throws {
|
||||
try await sendCommandOkResp(.apiDeleteToken(token: token))
|
||||
}
|
||||
|
||||
func getUserSMPServers() throws -> ([ServerCfg], [String]) {
|
||||
let userId = try currentUserId("getUserSMPServers")
|
||||
return try userSMPServersResponse(chatSendCmdSync(.apiGetUserSMPServers(userId: userId)))
|
||||
}
|
||||
|
||||
func getUserSMPServersAsync() async throws -> ([ServerCfg], [String]) {
|
||||
let userId = try currentUserId("getUserSMPServersAsync")
|
||||
return try userSMPServersResponse(await chatSendCmd(.apiGetUserSMPServers(userId: userId)))
|
||||
}
|
||||
|
||||
private func userSMPServersResponse(_ r: ChatResponse) throws -> ([ServerCfg], [String]) {
|
||||
if case let .userSMPServers(_, smpServers, presetServers) = r { return (smpServers, presetServers) }
|
||||
func getUserProtoServers(_ serverProtocol: ServerProtocol) throws -> UserProtoServers {
|
||||
let userId = try currentUserId("getUserProtoServers")
|
||||
let r = chatSendCmdSync(.apiGetUserProtoServers(userId: userId, serverProtocol: serverProtocol))
|
||||
if case let .userProtoServers(_, servers) = r { return servers }
|
||||
throw r
|
||||
}
|
||||
|
||||
func setUserSMPServers(smpServers: [ServerCfg]) async throws {
|
||||
let userId = try currentUserId("setUserSMPServers")
|
||||
try await sendCommandOkResp(.apiSetUserSMPServers(userId: userId, smpServers: smpServers))
|
||||
func setUserProtoServers(_ serverProtocol: ServerProtocol, servers: [ServerCfg]) async throws {
|
||||
let userId = try currentUserId("setUserProtoServers")
|
||||
try await sendCommandOkResp(.apiSetUserProtoServers(userId: userId, serverProtocol: serverProtocol, servers: servers))
|
||||
}
|
||||
|
||||
func testSMPServer(smpServer: String) async throws -> Result<(), SMPTestFailure> {
|
||||
let userId = try currentUserId("testSMPServer")
|
||||
let r = await chatSendCmd(.apiTestSMPServer(userId: userId, smpServer: smpServer))
|
||||
if case let .smpTestResult(_, testFailure) = r {
|
||||
func testProtoServer(server: String) async throws -> Result<(), ProtocolTestFailure> {
|
||||
let userId = try currentUserId("testProtoServer")
|
||||
let r = await chatSendCmd(.apiTestProtoServer(userId: userId, server: server))
|
||||
if case let .serverTestResult(_, _, testFailure) = r {
|
||||
if let t = testFailure {
|
||||
return .failure(t)
|
||||
}
|
||||
@@ -1098,7 +1090,6 @@ func changeActiveUserAsync_(_ userId: Int64, viewPwd: String?) async throws {
|
||||
func getUserChatData() throws {
|
||||
let m = ChatModel.shared
|
||||
m.userAddress = try apiGetUserAddress()
|
||||
(m.userSMPServers, m.presetSMPServers) = try getUserSMPServers()
|
||||
m.chatItemTTL = try getChatItemTTL()
|
||||
let chats = try apiGetChats()
|
||||
m.chats = chats.map { Chat.init($0) }
|
||||
@@ -1106,13 +1097,11 @@ func getUserChatData() throws {
|
||||
|
||||
private func getUserChatDataAsync() async throws {
|
||||
let userAddress = try await apiGetUserAddressAsync()
|
||||
let servers = try await getUserSMPServersAsync()
|
||||
let chatItemTTL = try await getChatItemTTLAsync()
|
||||
let chats = try await apiGetChatsAsync()
|
||||
await MainActor.run {
|
||||
let m = ChatModel.shared
|
||||
m.userAddress = userAddress
|
||||
(m.userSMPServers, m.presetSMPServers) = servers
|
||||
m.chatItemTTL = chatItemTTL
|
||||
m.chats = chats.map { Chat.init($0) }
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ struct CIImageView: View {
|
||||
if let uiImage = getLoadedImage(file) {
|
||||
imageView(uiImage)
|
||||
.fullScreenCover(isPresented: $showFullScreenImage) {
|
||||
FullScreenImageView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage, scrollProxy: scrollProxy)
|
||||
FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage, scrollProxy: scrollProxy)
|
||||
}
|
||||
.onTapGesture { showFullScreenImage = true }
|
||||
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
//
|
||||
// CIVideoView.swift
|
||||
// SimpleX
|
||||
//
|
||||
// Created by Avently on 30/03/2023.
|
||||
// Copyright © 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
import SimpleXChat
|
||||
|
||||
struct CIVideoView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
private let chatItem: ChatItem
|
||||
private let image: String
|
||||
@State private var duration: Int
|
||||
@State private var progress: Int = 0
|
||||
@State private var videoPlaying: Bool = false
|
||||
private let maxWidth: CGFloat
|
||||
@Binding private var videoWidth: CGFloat?
|
||||
@State private var scrollProxy: ScrollViewProxy?
|
||||
@State private var preview: UIImage? = nil
|
||||
@State private var player: AVPlayer?
|
||||
@State private var url: URL?
|
||||
@State private var showFullScreenPlayer = false
|
||||
@State private var timeObserver: Any? = nil
|
||||
@State private var fullScreenTimeObserver: Any? = nil
|
||||
|
||||
init(chatItem: ChatItem, image: String, duration: Int, maxWidth: CGFloat, videoWidth: Binding<CGFloat?>, scrollProxy: ScrollViewProxy?) {
|
||||
self.chatItem = chatItem
|
||||
self.image = image
|
||||
self._duration = State(initialValue: duration)
|
||||
self.maxWidth = maxWidth
|
||||
self._videoWidth = videoWidth
|
||||
self.scrollProxy = scrollProxy
|
||||
if let url = getLoadedVideo(chatItem.file) {
|
||||
self._player = State(initialValue: VideoPlayerView.getOrCreatePlayer(url, false))
|
||||
self._url = State(initialValue: url)
|
||||
}
|
||||
if let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||
let uiImage = UIImage(data: data) {
|
||||
self._preview = State(initialValue: uiImage)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let file = chatItem.file
|
||||
ZStack {
|
||||
ZStack(alignment: .topLeading) {
|
||||
if let file = file, let preview = preview, let player = player, let url = url {
|
||||
videoView(player, url, file, preview, duration)
|
||||
} else if let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||
let uiImage = UIImage(data: data) {
|
||||
imageView(uiImage)
|
||||
.onTapGesture {
|
||||
if let file = file {
|
||||
switch file.fileStatus {
|
||||
case .rcvInvitation:
|
||||
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
||||
case .rcvAccepted:
|
||||
switch file.fileProtocol {
|
||||
case .xftp:
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Waiting for video",
|
||||
message: "Video will be received when your contact completes uploading it."
|
||||
)
|
||||
case .smp:
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title: "Waiting for video",
|
||||
message: "Video will be received when your contact is online, please wait or check later!"
|
||||
)
|
||||
}
|
||||
case .rcvTransfer: () // ?
|
||||
case .rcvComplete: () // ?
|
||||
case .rcvCancelled: () // TODO
|
||||
default: ()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
durationProgress()
|
||||
}
|
||||
if let file = file, case .rcvInvitation = file.fileStatus {
|
||||
Button {
|
||||
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
||||
} label: {
|
||||
playPauseIcon("play.fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func videoView(_ player: AVPlayer, _ url: URL, _ file: CIFile, _ preview: UIImage, _ duration: Int) -> some View {
|
||||
let w = preview.size.width <= preview.size.height ? maxWidth * 0.75 : maxWidth
|
||||
DispatchQueue.main.async { videoWidth = w }
|
||||
return ZStack(alignment: .topTrailing) {
|
||||
ZStack(alignment: .center) {
|
||||
VideoPlayerView(player: player, url: url, showControls: false)
|
||||
.frame(width: w, height: w * preview.size.height / preview.size.width)
|
||||
.onChange(of: ChatModel.shared.stopPreviousRecPlay) { playingUrl in
|
||||
if playingUrl != url {
|
||||
player.pause()
|
||||
videoPlaying = false
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showFullScreenPlayer) {
|
||||
fullScreenPlayer(url)
|
||||
}
|
||||
.onTapGesture {
|
||||
switch player.timeControlStatus {
|
||||
case .playing:
|
||||
player.pause()
|
||||
videoPlaying = false
|
||||
case .paused:
|
||||
showFullScreenPlayer = true
|
||||
default: ()
|
||||
}
|
||||
}
|
||||
if !videoPlaying {
|
||||
Button {
|
||||
ChatModel.shared.stopPreviousRecPlay = url
|
||||
player.play()
|
||||
} label: {
|
||||
playPauseIcon("play.fill")
|
||||
}
|
||||
}
|
||||
}
|
||||
loadingIndicator()
|
||||
}
|
||||
.onAppear {
|
||||
addObserver(player, url)
|
||||
}
|
||||
.onDisappear {
|
||||
removeObserver()
|
||||
player.pause()
|
||||
videoPlaying = false
|
||||
}
|
||||
}
|
||||
|
||||
private func playPauseIcon(_ image: String, _ color: Color = .white) -> some View {
|
||||
Image(systemName: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 12, height: 12)
|
||||
.foregroundColor(color)
|
||||
.padding(.leading, 4)
|
||||
.frame(width: 40, height: 40)
|
||||
.background(Color.black.opacity(0.35))
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
private func durationProgress() -> some View {
|
||||
HStack {
|
||||
Text("\(durationText(videoPlaying ? progress : duration))")
|
||||
.foregroundColor(.white)
|
||||
.font(.caption)
|
||||
.padding(.vertical, 3)
|
||||
.padding(.horizontal, 6)
|
||||
.background(Color.black.opacity(0.35))
|
||||
.cornerRadius(10)
|
||||
.padding([.top, .leading], 6)
|
||||
|
||||
if let file = chatItem.file, !videoPlaying {
|
||||
Text("\(ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary))")
|
||||
.foregroundColor(.white)
|
||||
.font(.caption)
|
||||
.padding(.vertical, 3)
|
||||
.padding(.horizontal, 6)
|
||||
.background(Color.black.opacity(0.35))
|
||||
.cornerRadius(10)
|
||||
.padding(.top, 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func imageView(_ img: UIImage) -> some View {
|
||||
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : .infinity
|
||||
DispatchQueue.main.async { videoWidth = w }
|
||||
return ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(maxWidth: w)
|
||||
loadingIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func loadingIndicator() -> some View {
|
||||
if let file = chatItem.file {
|
||||
switch file.fileStatus {
|
||||
case .sndStored:
|
||||
switch file.fileProtocol {
|
||||
case .xftp: progressView()
|
||||
case .smp: EmptyView()
|
||||
}
|
||||
case let .sndTransfer(sndProgress, sndTotal):
|
||||
switch file.fileProtocol {
|
||||
case .xftp: progressCircle(sndProgress, sndTotal)
|
||||
case .smp: progressView()
|
||||
}
|
||||
case .sndComplete:
|
||||
Image(systemName: "checkmark")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 10, height: 10)
|
||||
.foregroundColor(.white)
|
||||
.padding(13)
|
||||
case .rcvInvitation:
|
||||
Image(systemName: "arrow.down")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 14, height: 14)
|
||||
.foregroundColor(.white)
|
||||
.padding(11)
|
||||
case .rcvAccepted:
|
||||
Image(systemName: "ellipsis")
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 14, height: 14)
|
||||
.foregroundColor(.white)
|
||||
.padding(11)
|
||||
case let .rcvTransfer(rcvProgress, rcvTotal):
|
||||
if file.fileProtocol == .xftp && rcvProgress < rcvTotal {
|
||||
progressCircle(rcvProgress, rcvTotal)
|
||||
} else {
|
||||
progressView()
|
||||
}
|
||||
default: EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func progressView() -> some View {
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
.frame(width: 16, height: 16)
|
||||
.tint(.white)
|
||||
.padding(11)
|
||||
}
|
||||
|
||||
private func progressCircle(_ progress: Int64, _ total: Int64) -> some View {
|
||||
Circle()
|
||||
.trim(from: 0, to: Double(progress) / Double(total))
|
||||
.stroke(
|
||||
Color(uiColor: .white),
|
||||
style: StrokeStyle(lineWidth: 2)
|
||||
)
|
||||
.rotationEffect(.degrees(-90))
|
||||
.frame(width: 16, height: 16)
|
||||
.padding([.trailing, .top], 11)
|
||||
}
|
||||
|
||||
private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64) async -> Void) {
|
||||
Task {
|
||||
if let user = ChatModel.shared.currentUser {
|
||||
await receiveFile(user, file.fileId)
|
||||
}
|
||||
// TODO image accepted alert?
|
||||
}
|
||||
}
|
||||
|
||||
private func fullScreenPlayer(_ url: URL) -> some View {
|
||||
ZStack {
|
||||
Color.black.edgesIgnoringSafeArea(.all)
|
||||
VideoPlayer(player: createFullScreenPlayerAndPlay(url)) {
|
||||
}
|
||||
.overlay(alignment: .topLeading, content: {
|
||||
Button(action: { showFullScreenPlayer = false },
|
||||
label: {
|
||||
Image(systemName: "multiply")
|
||||
.resizable()
|
||||
.tint(.white)
|
||||
.frame(width: 15, height: 15)
|
||||
.padding(.leading, 15)
|
||||
.padding(.top, 13)
|
||||
}
|
||||
)
|
||||
})
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 80)
|
||||
.onChanged { gesture in
|
||||
let t = gesture.translation
|
||||
let w = abs(t.width)
|
||||
if t.height > 60 && t.height > w * 2 {
|
||||
showFullScreenPlayer = false
|
||||
}
|
||||
}
|
||||
)
|
||||
.onDisappear {
|
||||
if let fullScreenTimeObserver = fullScreenTimeObserver {
|
||||
NotificationCenter.default.removeObserver(fullScreenTimeObserver)
|
||||
}
|
||||
fullScreenTimeObserver = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createFullScreenPlayerAndPlay(_ url: URL) -> AVPlayer {
|
||||
let player = AVPlayer(url: url)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now()) {
|
||||
ChatModel.shared.stopPreviousRecPlay = url
|
||||
player.play()
|
||||
fullScreenTimeObserver = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main) { _ in
|
||||
player.seek(to: CMTime.zero)
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
return player
|
||||
}
|
||||
|
||||
private func addObserver(_ player: AVPlayer, _ url: URL) {
|
||||
timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.01, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), queue: .main) { time in
|
||||
if let item = player.currentItem {
|
||||
let dur = CMTimeGetSeconds(item.duration)
|
||||
if !dur.isInfinite && !dur.isNaN {
|
||||
duration = Int(dur)
|
||||
}
|
||||
progress = Int(CMTimeGetSeconds(player.currentTime()))
|
||||
// `if` prevents showing Play button while the playback seeks to start and then plays
|
||||
if player.currentTime() != player.currentItem?.duration && player.currentTime() != .zero {
|
||||
videoPlaying = player.timeControlStatus == .playing || player.timeControlStatus == .waitingToPlayAtSpecifiedRate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func removeObserver() {
|
||||
if let timeObserver = timeObserver {
|
||||
player?.removeTimeObserver(timeObserver)
|
||||
}
|
||||
timeObserver = nil
|
||||
}
|
||||
}
|
||||
@@ -203,7 +203,7 @@ struct VoiceMessagePlayer: View {
|
||||
|
||||
private func startPlayback(_ recordingFileName: String) {
|
||||
startingPlayback = true
|
||||
chatModel.stopPreviousRecPlay.toggle()
|
||||
chatModel.stopPreviousRecPlay = getAppFilePath(recordingFileName)
|
||||
audioPlayer = AudioPlayer(
|
||||
onTimer: { playbackTime = $0 },
|
||||
onFinishPlayback: {
|
||||
|
||||
@@ -23,6 +23,7 @@ struct FramedItemView: View {
|
||||
@State var scrollProxy: ScrollViewProxy? = nil
|
||||
@State var msgWidth: CGFloat = 0
|
||||
@State var imgWidth: CGFloat? = nil
|
||||
@State var videoWidth: CGFloat? = nil
|
||||
@State var metaColor = Color.secondary
|
||||
@State var showFullScreenImage = false
|
||||
|
||||
@@ -64,7 +65,7 @@ struct FramedItemView: View {
|
||||
.overlay(DetermineWidth())
|
||||
}
|
||||
}
|
||||
.background(chatItemFrameColorMaybeImage(chatItem, colorScheme))
|
||||
.background(chatItemFrameColorMaybeImageOrVideo(chatItem, colorScheme))
|
||||
.cornerRadius(18)
|
||||
.onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 }
|
||||
|
||||
@@ -103,6 +104,19 @@ struct FramedItemView: View {
|
||||
} else {
|
||||
ciMsgContentView (chatItem, showMember)
|
||||
}
|
||||
case let .video(text, image, duration):
|
||||
CIVideoView(chatItem: chatItem, image: image, duration: duration, maxWidth: maxWidth, videoWidth: $videoWidth, scrollProxy: scrollProxy)
|
||||
.overlay(DetermineWidth())
|
||||
if text == "" && !chatItem.meta.isLive {
|
||||
Color.clear
|
||||
.frame(width: 0, height: 0)
|
||||
.preference(
|
||||
key: MetaColorPreferenceKey.self,
|
||||
value: .white
|
||||
)
|
||||
} else {
|
||||
ciMsgContentView (chatItem, showMember)
|
||||
}
|
||||
case let .voice(text, duration):
|
||||
FramedCIVoiceView(chatItem: chatItem, recordingFile: chatItem.file, duration: duration)
|
||||
.overlay(DetermineWidth())
|
||||
@@ -152,8 +166,8 @@ struct FramedItemView: View {
|
||||
.overlay(DetermineWidth())
|
||||
.frame(minWidth: msgWidth, alignment: .leading)
|
||||
.background(chatItemFrameContextColor(chatItem, colorScheme))
|
||||
if let imgWidth = imgWidth, imgWidth < maxWidth {
|
||||
v.frame(maxWidth: imgWidth, alignment: .leading)
|
||||
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
|
||||
v.frame(maxWidth: mediaWidth, alignment: .leading)
|
||||
} else {
|
||||
v
|
||||
}
|
||||
@@ -175,6 +189,19 @@ struct FramedItemView: View {
|
||||
} else {
|
||||
ciQuotedMsgView(qi)
|
||||
}
|
||||
case let .video(_, image, _):
|
||||
if let data = Data(base64Encoded: dropImagePrefix(image)),
|
||||
let uiImage = UIImage(data: data) {
|
||||
ciQuotedMsgView(qi)
|
||||
.padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading)
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 68, height: 68)
|
||||
.clipped()
|
||||
} else {
|
||||
ciQuotedMsgView(qi)
|
||||
}
|
||||
case .file:
|
||||
ciQuotedMsgView(qi)
|
||||
.padding(.trailing, 20).frame(minWidth: msgWidth, alignment: .leading)
|
||||
@@ -190,9 +217,9 @@ struct FramedItemView: View {
|
||||
.overlay(DetermineWidth())
|
||||
.frame(minWidth: msgWidth, alignment: .leading)
|
||||
.background(chatItemFrameContextColor(chatItem, colorScheme))
|
||||
|
||||
if let imgWidth = imgWidth, imgWidth < maxWidth {
|
||||
v.frame(maxWidth: imgWidth, alignment: .leading)
|
||||
|
||||
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
|
||||
v.frame(maxWidth: mediaWidth, alignment: .leading)
|
||||
} else {
|
||||
v
|
||||
}
|
||||
@@ -243,9 +270,9 @@ struct FramedItemView: View {
|
||||
.overlay(DetermineWidth())
|
||||
.frame(minWidth: 0, alignment: .leading)
|
||||
.textSelection(.enabled)
|
||||
|
||||
if let imgWidth = imgWidth, imgWidth < maxWidth {
|
||||
v.frame(maxWidth: imgWidth, alignment: .leading)
|
||||
|
||||
if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth {
|
||||
v.frame(maxWidth: mediaWidth, alignment: .leading)
|
||||
} else {
|
||||
v
|
||||
}
|
||||
@@ -258,6 +285,16 @@ struct FramedItemView: View {
|
||||
ciMsgContentView (chatItem, showMember)
|
||||
}
|
||||
}
|
||||
|
||||
private func maxMediaWidth() -> CGFloat? {
|
||||
if let imgWidth = imgWidth, let videoWidth = videoWidth {
|
||||
return imgWidth > videoWidth ? imgWidth : videoWidth
|
||||
} else if let imgWidth = imgWidth {
|
||||
return imgWidth
|
||||
} else {
|
||||
return videoWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isRightToLeft(_ s: String) -> Bool {
|
||||
@@ -274,15 +311,17 @@ private struct MetaColorPreferenceKey: PreferenceKey {
|
||||
}
|
||||
}
|
||||
|
||||
func onlyImage(_ ci: ChatItem) -> Bool {
|
||||
func onlyImageOrVideo(_ ci: ChatItem) -> Bool {
|
||||
if case let .image(text, _) = ci.content.msgContent {
|
||||
return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && text == ""
|
||||
} else if case let .video(text, _, _) = ci.content.msgContent {
|
||||
return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && text == ""
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func chatItemFrameColorMaybeImage(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
|
||||
onlyImage(ci)
|
||||
func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color {
|
||||
onlyImageOrVideo(ci)
|
||||
? Color.clear
|
||||
: chatItemFrameColor(ci, colorScheme)
|
||||
}
|
||||
|
||||
+69
-11
@@ -9,36 +9,61 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
import SwiftyGif
|
||||
import AVKit
|
||||
|
||||
struct FullScreenImageView: View {
|
||||
struct FullScreenMediaView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@State var chatItem: ChatItem
|
||||
@State var image: UIImage
|
||||
@State var image: UIImage?
|
||||
@State var player: AVPlayer? = nil
|
||||
@State var url: URL? = nil
|
||||
@Binding var showView: Bool
|
||||
@State var scrollProxy: ScrollViewProxy?
|
||||
@State private var showNext = false
|
||||
@State private var nextImage: UIImage?
|
||||
@State private var nextPlayer: AVPlayer?
|
||||
@State private var nextURL: URL?
|
||||
@State private var scrolling = false
|
||||
@State private var offset: CGFloat = 0
|
||||
@State private var nextOffset: CGFloat = 0
|
||||
|
||||
var body: some View {
|
||||
GeometryReader(content: imageScrollView)
|
||||
GeometryReader(content: mediaScrollView)
|
||||
}
|
||||
|
||||
func imageScrollView(_ g: GeometryProxy) -> some View {
|
||||
func mediaScrollView(_ g: GeometryProxy) -> some View {
|
||||
ZStack {
|
||||
Color.black.edgesIgnoringSafeArea(.all)
|
||||
if showNext, let nextImage = nextImage {
|
||||
imageView(image).offset(x: offset)
|
||||
if let image = image {
|
||||
imageView(image).offset(x: offset)
|
||||
} else if let player = player, let url = url {
|
||||
videoView(player, url).offset(x: offset)
|
||||
}
|
||||
imageView(nextImage).offset(x: offset + nextOffset)
|
||||
} else if showNext, let nextPlayer = nextPlayer, let nextURL = nextURL {
|
||||
if let image = image {
|
||||
imageView(image).offset(x: offset)
|
||||
} else if let player = player, let url = url {
|
||||
videoView(player, url).offset(x: offset)
|
||||
}
|
||||
videoView(nextPlayer, nextURL).offset(x: offset + nextOffset)
|
||||
} else {
|
||||
ZoomableScrollView {
|
||||
imageView(image)
|
||||
if let image = image {
|
||||
imageView(image)
|
||||
} else if let player = player, let url = url {
|
||||
videoView(player, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture { showView = false }
|
||||
.onAppear {
|
||||
startPlayerAndNotify()
|
||||
}
|
||||
.onDisappear {
|
||||
player?.pause()
|
||||
}
|
||||
.gesture(
|
||||
DragGesture(minimumDistance: 80)
|
||||
.onChanged { gesture in
|
||||
@@ -53,9 +78,17 @@ struct FullScreenImageView: View {
|
||||
let previous = t.width > 0
|
||||
scrolling = true
|
||||
if let item = m.nextChatItemData(chatItem.id, previous: previous, map: chatItemImage) {
|
||||
var img: UIImage
|
||||
(chatItem, img) = item
|
||||
var img: UIImage?
|
||||
var url: URL?
|
||||
(chatItem, img, url) = item
|
||||
nextImage = img
|
||||
nextPlayer?.pause()
|
||||
if let url = url {
|
||||
nextPlayer = VideoPlayerView.getOrCreatePlayer(url, true)
|
||||
} else {
|
||||
nextPlayer = nil
|
||||
}
|
||||
nextURL = url
|
||||
let s = g.size.width
|
||||
var toOffset: CGFloat
|
||||
(toOffset, nextOffset) = previous ? (s, -s) : (-s, s)
|
||||
@@ -65,6 +98,14 @@ struct FullScreenImageView: View {
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
||||
image = img
|
||||
player?.pause()
|
||||
self.url = url
|
||||
if let url = url {
|
||||
player = VideoPlayerView.getOrCreatePlayer(url, true)
|
||||
startPlayerAndNotify()
|
||||
} else {
|
||||
player = nil
|
||||
}
|
||||
showNext = false
|
||||
offset = 0
|
||||
}
|
||||
@@ -87,13 +128,30 @@ struct FullScreenImageView: View {
|
||||
.scaledToFit()
|
||||
}
|
||||
}
|
||||
.onTapGesture { showView = false }
|
||||
}
|
||||
|
||||
private func chatItemImage(_ ci: ChatItem) -> (ChatItem, UIImage)? {
|
||||
private func videoView( _ player: AVPlayer, _ url: URL) -> some View {
|
||||
VideoPlayerView(player: player, url: url, showControls: true)
|
||||
}
|
||||
|
||||
private func chatItemImage(_ ci: ChatItem) -> (ChatItem, UIImage?, URL?)? {
|
||||
if case .image = ci.content.msgContent,
|
||||
let img = getLoadedImage(ci.file) {
|
||||
return (ci, img)
|
||||
return (ci, img, nil)
|
||||
}
|
||||
// Currently, video support in gallery is not enabled
|
||||
/*else if case .video = ci.content.msgContent,
|
||||
let url = getLoadedVideo(ci.file) {
|
||||
return (ci, nil, url)
|
||||
}*/
|
||||
return nil
|
||||
}
|
||||
|
||||
private func startPlayerAndNotify() {
|
||||
if let player = player {
|
||||
ChatModel.shared.stopPreviousRecPlay = url
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +78,7 @@ struct ChatView: View {
|
||||
if chatModel.chatId == nil { dismiss() }
|
||||
}
|
||||
.onDisappear {
|
||||
VideoPlayerView.players.removeAll()
|
||||
if chatModel.chatId == cInfo.id && !presentationMode.wrappedValue.isPresented {
|
||||
chatModel.chatId = nil
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import PhotosUI
|
||||
enum ComposePreview {
|
||||
case noPreview
|
||||
case linkPreview(linkPreview: LinkPreview?)
|
||||
case imagePreviews(imagePreviews: [(String, UploadContent?)])
|
||||
case mediaPreviews(mediaPreviews: [(String, UploadContent?)])
|
||||
case voicePreview(recordingFileName: String, duration: Int)
|
||||
case filePreview(fileName: String, file: URL)
|
||||
}
|
||||
@@ -105,7 +105,7 @@ struct ComposeState {
|
||||
|
||||
var sendEnabled: Bool {
|
||||
switch preview {
|
||||
case .imagePreviews: return true
|
||||
case .mediaPreviews: return true
|
||||
case .voicePreview: return voiceMessageRecordingState == .finished
|
||||
case .filePreview: return true
|
||||
default: return !message.isEmpty || liveMessage != nil
|
||||
@@ -118,7 +118,7 @@ struct ComposeState {
|
||||
|
||||
var linkPreviewAllowed: Bool {
|
||||
switch preview {
|
||||
case .imagePreviews: return false
|
||||
case .mediaPreviews: return false
|
||||
case .voicePreview: return false
|
||||
case .filePreview: return false
|
||||
default: return useLinkPreviews
|
||||
@@ -175,7 +175,9 @@ func chatItemPreview(chatItem: ChatItem) -> ComposePreview {
|
||||
case let .link(_, preview: preview):
|
||||
chatItemPreview = .linkPreview(linkPreview: preview)
|
||||
case let .image(_, image):
|
||||
chatItemPreview = .imagePreviews(imagePreviews: [(image, nil)])
|
||||
chatItemPreview = .mediaPreviews(mediaPreviews: [(image, nil)])
|
||||
case let .video(_, image, _):
|
||||
chatItemPreview = .mediaPreviews(mediaPreviews: [(image, nil)])
|
||||
case let .voice(_, duration):
|
||||
chatItemPreview = .voicePreview(recordingFileName: chatItem.file?.fileName ?? "", duration: duration)
|
||||
case .file:
|
||||
@@ -190,11 +192,13 @@ func chatItemPreview(chatItem: ChatItem) -> ComposePreview {
|
||||
enum UploadContent: Equatable {
|
||||
case simpleImage(image: UIImage)
|
||||
case animatedImage(image: UIImage)
|
||||
case video(image: UIImage, url: URL, duration: Int)
|
||||
|
||||
var uiImage: UIImage {
|
||||
switch self {
|
||||
case let .simpleImage(image): return image
|
||||
case let .animatedImage(image): return image
|
||||
case let .video(image, _, _): return image
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +220,14 @@ enum UploadContent: Equatable {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static func loadVideoFromURL(url: URL) -> UploadContent? {
|
||||
let asset = AVAsset(url: url)
|
||||
if let (image, duration) = asset.generatePreview() {
|
||||
return .video(image: image, url: url, duration: duration)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
struct ComposeView: View {
|
||||
@@ -232,9 +244,9 @@ struct ComposeView: View {
|
||||
@AppStorage(GROUP_DEFAULT_XFTP_SEND_ENABLED, store: groupDefaults) private var xftpSendEnabled = false
|
||||
|
||||
@State private var showChooseSource = false
|
||||
@State private var showImagePicker = false
|
||||
@State private var showMediaPicker = false
|
||||
@State private var showTakePhoto = false
|
||||
@State var chosenImages: [UploadContent] = []
|
||||
@State var chosenMedia: [UploadContent] = []
|
||||
@State private var showFileImporter = false
|
||||
|
||||
@State private var audioRecorder: AudioRecorder?
|
||||
@@ -286,7 +298,7 @@ struct ComposeView: View {
|
||||
},
|
||||
finishVoiceMessageRecording: finishVoiceMessageRecording,
|
||||
allowVoiceMessagesToContact: allowVoiceMessagesToContact,
|
||||
onImagesAdded: { images in if !images.isEmpty { chosenImages = images }},
|
||||
onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }},
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
.padding(.trailing, 12)
|
||||
@@ -329,7 +341,7 @@ struct ComposeView: View {
|
||||
showTakePhoto = true
|
||||
}
|
||||
Button("Choose from library") {
|
||||
showImagePicker = true
|
||||
showMediaPicker = true
|
||||
}
|
||||
if UIPasteboard.general.hasImages {
|
||||
Button("Paste image") {
|
||||
@@ -337,7 +349,7 @@ struct ComposeView: View {
|
||||
if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
|
||||
p.loadFileRepresentation(forTypeIdentifier: UTType.data.identifier) { url, error in
|
||||
if let url = url, let image = UploadContent.loadFromURL(url: url) {
|
||||
chosenImages.append(image)
|
||||
chosenMedia.append(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -351,31 +363,31 @@ struct ComposeView: View {
|
||||
.fullScreenCover(isPresented: $showTakePhoto) {
|
||||
ZStack {
|
||||
Color.black.edgesIgnoringSafeArea(.all)
|
||||
CameraImageListPicker(images: $chosenImages)
|
||||
CameraImageListPicker(images: $chosenMedia)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showImagePicker) {
|
||||
LibraryImageListPicker(images: $chosenImages, selectionLimit: 10) { itemsSelected in
|
||||
showImagePicker = false
|
||||
.sheet(isPresented: $showMediaPicker) {
|
||||
LibraryMediaListPicker(media: $chosenMedia, selectionLimit: 10) { itemsSelected in
|
||||
showMediaPicker = false
|
||||
if itemsSelected {
|
||||
DispatchQueue.main.async {
|
||||
composeState = composeState.copy(preview: .imagePreviews(imagePreviews: []))
|
||||
composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: []))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: chosenImages) { images in
|
||||
.onChange(of: chosenMedia) { selected in
|
||||
Task {
|
||||
var imgs: [(String, UploadContent)] = []
|
||||
for image in images {
|
||||
if let img = resizeImageToStrSize(image.uiImage, maxDataSize: 14000) {
|
||||
imgs.append((img, image))
|
||||
var media: [(String, UploadContent)] = []
|
||||
for content in selected {
|
||||
if let img = resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
|
||||
media.append((img, content))
|
||||
await MainActor.run {
|
||||
composeState = composeState.copy(preview: .imagePreviews(imagePreviews: imgs))
|
||||
composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: media))
|
||||
}
|
||||
}
|
||||
}
|
||||
if imgs.count == 0 {
|
||||
if media.count == 0 {
|
||||
await MainActor.run {
|
||||
composeState = composeState.copy(preview: .noPreview)
|
||||
}
|
||||
@@ -514,12 +526,12 @@ struct ComposeView: View {
|
||||
EmptyView()
|
||||
case let .linkPreview(linkPreview: preview):
|
||||
ComposeLinkView(linkPreview: preview, cancelPreview: cancelLinkPreview)
|
||||
case let .imagePreviews(imagePreviews: images):
|
||||
case let .mediaPreviews(mediaPreviews: media):
|
||||
ComposeImageView(
|
||||
images: images.map { (img, _) in img },
|
||||
images: media.map { (img, _) in img },
|
||||
cancelImage: {
|
||||
composeState = composeState.copy(preview: .noPreview)
|
||||
chosenImages = []
|
||||
chosenMedia = []
|
||||
},
|
||||
cancelEnabled: !composeState.editing)
|
||||
case let .voicePreview(recordingFileName, _):
|
||||
@@ -594,21 +606,29 @@ struct ComposeView: View {
|
||||
sent = await send(.text(msgText), quoted: quoted, live: live)
|
||||
case .linkPreview:
|
||||
sent = await send(checkLinkPreview(), quoted: quoted, live: live)
|
||||
case let .imagePreviews(imagePreviews: images):
|
||||
let last = images.count - 1
|
||||
case let .mediaPreviews(mediaPreviews: media):
|
||||
let last = media.count - 1
|
||||
if last >= 0 {
|
||||
for i in 0..<last {
|
||||
sent = await sendImage(images[i])
|
||||
if case (_, .video(_, _, _)) = media[i] {
|
||||
sent = await sendVideo(media[i])
|
||||
} else {
|
||||
sent = await sendImage(media[i])
|
||||
}
|
||||
_ = try? await Task.sleep(nanoseconds: 100_000000)
|
||||
}
|
||||
sent = await sendImage(images[last], text: msgText, quoted: quoted, live: live)
|
||||
if case (_, .video(_, _, _)) = media[last] {
|
||||
sent = await sendVideo(media[last], text: msgText, quoted: quoted, live: live)
|
||||
} else {
|
||||
sent = await sendImage(media[last], text: msgText, quoted: quoted, live: live)
|
||||
}
|
||||
}
|
||||
if sent == nil {
|
||||
sent = await send(.text(msgText), quoted: quoted, live: live)
|
||||
}
|
||||
case let .voicePreview(recordingFileName, duration):
|
||||
stopPlayback.toggle()
|
||||
chatModel.filesToDelete.removeAll { $0 == recordingFileName }
|
||||
chatModel.filesToDelete.remove(getAppFilePath(recordingFileName))
|
||||
sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: recordingFileName)
|
||||
case let .filePreview(_, file):
|
||||
if let savedFile = saveFileFromURL(file) {
|
||||
@@ -657,6 +677,8 @@ struct ComposeView: View {
|
||||
return checkLinkPreview()
|
||||
case .image(_, let image):
|
||||
return .image(text: msgText, image: image)
|
||||
case .video(_, let image, let duration):
|
||||
return .video(text: msgText, image: image, duration: duration)
|
||||
case .voice(_, let duration):
|
||||
return .voice(text: msgText, duration: duration)
|
||||
case .file:
|
||||
@@ -674,6 +696,14 @@ struct ComposeView: View {
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendVideo(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false) async -> ChatItem? {
|
||||
let (image, data) = imageData
|
||||
if case let .video(_, url, duration) = data, let savedFile = saveFileFromURLWithoutLoad(url) {
|
||||
return await send(.video(text: text, image: image, duration: duration), quoted: quoted, file: savedFile, live: live)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func send(_ mc: MsgContent, quoted: Int64?, file: String? = nil, live: Bool = false) async -> ChatItem? {
|
||||
if let chatItem = await apiSendMessage(
|
||||
type: chat.chatInfo.chatType,
|
||||
@@ -711,14 +741,15 @@ struct ComposeView: View {
|
||||
switch img {
|
||||
case let .simpleImage(image): return saveImage(image)
|
||||
case let .animatedImage(image): return saveAnimImage(image)
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func startVoiceMessageRecording() async {
|
||||
startingRecording = true
|
||||
chatModel.stopPreviousRecPlay.toggle()
|
||||
let fileName = generateNewFileName("voice", "m4a")
|
||||
chatModel.stopPreviousRecPlay = getAppFilePath(fileName)
|
||||
audioRecorder = AudioRecorder(
|
||||
onTimer: { voiceMessageRecordingTime = $0 },
|
||||
onFinishRecording: {
|
||||
@@ -804,7 +835,7 @@ struct ComposeView: View {
|
||||
composeState = ComposeState()
|
||||
resetLinkPreview()
|
||||
}
|
||||
chosenImages = []
|
||||
chosenMedia = []
|
||||
audioRecorder = nil
|
||||
voiceMessageRecordingTime = nil
|
||||
startingRecording = false
|
||||
@@ -814,7 +845,7 @@ struct ComposeView: View {
|
||||
if case .recording = composeState.voiceMessageRecordingState {
|
||||
finishVoiceMessageRecording()
|
||||
if let fileName = composeState.voiceMessageRecordingFileName {
|
||||
chatModel.filesToDelete.append(fileName)
|
||||
chatModel.filesToDelete.insert(getAppFilePath(fileName))
|
||||
}
|
||||
}
|
||||
chatModel.draft = composeState
|
||||
|
||||
@@ -164,7 +164,7 @@ struct ComposeVoiceView: View {
|
||||
|
||||
private func startPlayback() {
|
||||
startingPlayback = true
|
||||
chatModel.stopPreviousRecPlay.toggle()
|
||||
chatModel.stopPreviousRecPlay = getAppFilePath(recordingFileName)
|
||||
audioPlayer = AudioPlayer(
|
||||
onTimer: { playbackTime = $0 },
|
||||
onFinishPlayback: {
|
||||
|
||||
@@ -23,7 +23,7 @@ struct SendMessageView: View {
|
||||
var startVoiceMessageRecording: (() -> Void)? = nil
|
||||
var finishVoiceMessageRecording: (() -> Void)? = nil
|
||||
var allowVoiceMessagesToContact: (() -> Void)? = nil
|
||||
var onImagesAdded: ([UploadContent]) -> Void
|
||||
var onMediaAdded: ([UploadContent]) -> Void
|
||||
@State private var holdingVMR = false
|
||||
@Namespace var namespace
|
||||
@FocusState.Binding var keyboardVisible: Bool
|
||||
@@ -69,7 +69,7 @@ struct SendMessageView: View {
|
||||
font: teUiFont,
|
||||
focused: $keyboardVisible,
|
||||
alignment: alignment,
|
||||
onImagesAdded: onImagesAdded
|
||||
onImagesAdded: onMediaAdded
|
||||
)
|
||||
.allowsTightening(false)
|
||||
.frame(height: teHeight)
|
||||
@@ -365,7 +365,7 @@ struct SendMessageView_Previews: PreviewProvider {
|
||||
SendMessageView(
|
||||
composeState: $composeStateNew,
|
||||
sendMessage: {},
|
||||
onImagesAdded: { _ in },
|
||||
onMediaAdded: { _ in },
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
}
|
||||
@@ -375,7 +375,7 @@ struct SendMessageView_Previews: PreviewProvider {
|
||||
SendMessageView(
|
||||
composeState: $composeStateEditing,
|
||||
sendMessage: {},
|
||||
onImagesAdded: { _ in },
|
||||
onMediaAdded: { _ in },
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ struct ChatPreviewView: View {
|
||||
func attachment() -> Text {
|
||||
switch draft.preview {
|
||||
case let .filePreview(fileName, _): return image("doc.fill") + Text(fileName) + Text(" ")
|
||||
case .imagePreviews: return image("photo")
|
||||
case .mediaPreviews: return image("photo")
|
||||
case let .voicePreview(_, duration): return image("play.fill") + Text(durationText(duration))
|
||||
default: return Text("")
|
||||
}
|
||||
@@ -159,6 +159,7 @@ struct ChatPreviewView: View {
|
||||
switch cItem.content.msgContent {
|
||||
case .file: return "doc.fill"
|
||||
case .image: return "photo"
|
||||
case .video: return "video"
|
||||
case .voice: return "play.fill"
|
||||
default: return nil
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ struct LibraryImagePicker: View {
|
||||
@State var images: [UploadContent] = []
|
||||
|
||||
var body: some View {
|
||||
LibraryImageListPicker(images: $images, selectionLimit: 1, didFinishPicking: didFinishPicking)
|
||||
LibraryMediaListPicker(media: $images, selectionLimit: 1, didFinishPicking: didFinishPicking)
|
||||
.onChange(of: images) { _ in
|
||||
if let img = images.first {
|
||||
image = img.uiImage
|
||||
@@ -26,19 +26,20 @@ struct LibraryImagePicker: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct LibraryImageListPicker: UIViewControllerRepresentable {
|
||||
struct LibraryMediaListPicker: UIViewControllerRepresentable {
|
||||
typealias UIViewControllerType = PHPickerViewController
|
||||
@Binding var images: [UploadContent]
|
||||
@AppStorage(GROUP_DEFAULT_XFTP_SEND_ENABLED, store: groupDefaults) var xftpSendEnabled = false
|
||||
@Binding var media: [UploadContent]
|
||||
var selectionLimit: Int
|
||||
var didFinishPicking: (_ didSelectItems: Bool) -> Void
|
||||
|
||||
class Coordinator: PHPickerViewControllerDelegate {
|
||||
let parent: LibraryImageListPicker
|
||||
let dispatchQueue = DispatchQueue(label: "chat.simplex.app.LibraryImageListPicker")
|
||||
var images: [UploadContent] = []
|
||||
var imageCount: Int = 0
|
||||
let parent: LibraryMediaListPicker
|
||||
let dispatchQueue = DispatchQueue(label: "chat.simplex.app.LibraryMediaListPicker")
|
||||
var media: [UploadContent] = []
|
||||
var mediaCount: Int = 0
|
||||
|
||||
init(_ parent: LibraryImageListPicker) {
|
||||
init(_ parent: LibraryMediaListPicker) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
@@ -48,13 +49,23 @@ struct LibraryImageListPicker: UIViewControllerRepresentable {
|
||||
return
|
||||
}
|
||||
|
||||
parent.images = []
|
||||
images = []
|
||||
imageCount = results.count
|
||||
parent.media = []
|
||||
media = []
|
||||
mediaCount = results.count
|
||||
for result in results {
|
||||
logger.log("LibraryImageListPicker result")
|
||||
logger.log("LibraryMediaListPicker result")
|
||||
let p = result.itemProvider
|
||||
if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
|
||||
if p.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
|
||||
p.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in
|
||||
if let url = url {
|
||||
let tempUrl = URL(fileURLWithPath: getTempFilesDirectory().path + "/" + generateNewFileName("video", url.pathExtension))
|
||||
if ((try? FileManager.default.copyItem(at: url, to: tempUrl)) != nil) {
|
||||
ChatModel.shared.filesToDelete.insert(tempUrl)
|
||||
self.loadVideo(url: tempUrl, error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) {
|
||||
p.loadFileRepresentation(forTypeIdentifier: UTType.data.identifier) { url, error in
|
||||
self.loadImage(object: url, error: error)
|
||||
}
|
||||
@@ -65,14 +76,14 @@ struct LibraryImageListPicker: UIViewControllerRepresentable {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dispatchQueue.sync { self.imageCount -= 1}
|
||||
dispatchQueue.sync { self.mediaCount -= 1}
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
|
||||
self.dispatchQueue.sync {
|
||||
if self.parent.images.count == 0 {
|
||||
logger.log("LibraryImageListPicker: added \(self.images.count) images out of \(results.count)")
|
||||
self.parent.images = self.images
|
||||
if self.parent.media.count == 0 {
|
||||
logger.log("LibraryMediaListPicker: added \(self.media.count) images out of \(results.count)")
|
||||
self.parent.media = self.media
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,19 +91,35 @@ struct LibraryImageListPicker: UIViewControllerRepresentable {
|
||||
|
||||
func loadImage(object: Any?, error: Error? = nil) {
|
||||
if let error = error {
|
||||
logger.error("LibraryImageListPicker: couldn't load image with error: \(error.localizedDescription)")
|
||||
logger.error("LibraryMediaListPicker: couldn't load image with error: \(error.localizedDescription)")
|
||||
} else if let image = object as? UIImage {
|
||||
images.append(.simpleImage(image: image))
|
||||
logger.log("LibraryImageListPicker: added image")
|
||||
media.append(.simpleImage(image: image))
|
||||
logger.log("LibraryMediaListPicker: added image")
|
||||
} else if let url = object as? URL, let image = UploadContent.loadFromURL(url: url) {
|
||||
images.append(image)
|
||||
media.append(image)
|
||||
}
|
||||
dispatchQueue.sync {
|
||||
self.imageCount -= 1
|
||||
if self.imageCount == 0 && self.parent.images.count == 0 {
|
||||
logger.log("LibraryImageListPicker: added all images")
|
||||
self.parent.images = self.images
|
||||
self.images = []
|
||||
self.mediaCount -= 1
|
||||
if self.mediaCount == 0 && self.parent.media.count == 0 {
|
||||
logger.log("LibraryMediaListPicker: added all media")
|
||||
self.parent.media = self.media
|
||||
self.media = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func loadVideo(url: URL?, error: Error? = nil) {
|
||||
if let error = error {
|
||||
logger.error("LibraryMediaListPicker: couldn't load video with error: \(error.localizedDescription)")
|
||||
} else if let url = url as URL?, let video = UploadContent.loadVideoFromURL(url: url) {
|
||||
media.append(video)
|
||||
}
|
||||
dispatchQueue.sync {
|
||||
self.mediaCount -= 1
|
||||
if self.mediaCount == 0 && self.parent.media.count == 0 {
|
||||
logger.log("LibraryMediaListPicker: added all media")
|
||||
self.parent.media = self.media
|
||||
self.media = []
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,8 +131,15 @@ struct LibraryImageListPicker: UIViewControllerRepresentable {
|
||||
|
||||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||||
var config = PHPickerConfiguration()
|
||||
config.filter = .images
|
||||
let allowVideoAttachment = xftpSendEnabled
|
||||
if allowVideoAttachment {
|
||||
config.filter = .any(of: [.images, .videos])
|
||||
} else {
|
||||
config.filter = .images
|
||||
}
|
||||
config.selectionLimit = selectionLimit
|
||||
config.selection = .ordered
|
||||
//config.preferredAssetRepresentationMode = .current
|
||||
let controller = PHPickerViewController(configuration: config)
|
||||
controller.delegate = context.coordinator
|
||||
return controller
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// Created by Avently on 30.03.2023.
|
||||
// Copyright (c) 2023 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
|
||||
struct VideoPlayerView: UIViewRepresentable {
|
||||
|
||||
static var players: [String: AVPlayer] = [:]
|
||||
static func getOrCreatePlayer(_ url: URL, _ gallery: Bool) -> AVPlayer {
|
||||
if let player = players[url.absoluteString + gallery.description] {
|
||||
return player
|
||||
} else {
|
||||
let player = AVPlayer(url: url)
|
||||
players[url.absoluteString + gallery.description] = player
|
||||
return player
|
||||
}
|
||||
}
|
||||
|
||||
typealias UIViewType = UIView
|
||||
let player: AVPlayer
|
||||
let url: URL
|
||||
let showControls: Bool
|
||||
|
||||
func makeUIView(context: UIViewRepresentableContext<VideoPlayerView>) -> UIView {
|
||||
let controller = AVPlayerViewController()
|
||||
controller.showsPlaybackControls = showControls
|
||||
if #available(iOS 16.0, *) {
|
||||
controller.speeds = []
|
||||
}
|
||||
controller.player = player
|
||||
context.coordinator.controller = controller
|
||||
context.coordinator.timeObserver = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main) { _ in
|
||||
player.seek(to: CMTime.zero)
|
||||
player.play()
|
||||
}
|
||||
return controller.view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<VideoPlayerView>) {
|
||||
}
|
||||
|
||||
func makeCoordinator() -> VideoPlayerView.Coordinator {
|
||||
Coordinator()
|
||||
}
|
||||
|
||||
class Coordinator: NSObject {
|
||||
var controller: AVPlayerViewController?
|
||||
var timeObserver: Any? = nil
|
||||
|
||||
deinit {
|
||||
print("deinit coordinator of VideoPlayer")
|
||||
if let timeObserver = timeObserver {
|
||||
NotificationCenter.default.removeObserver(timeObserver)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,7 +74,7 @@ struct TerminalView: View {
|
||||
composeState: $composeState,
|
||||
sendMessage: sendMessage,
|
||||
showVoiceMessageButton: false,
|
||||
onImagesAdded: { _ in },
|
||||
onMediaAdded: { _ in },
|
||||
keyboardVisible: $keyboardVisible
|
||||
)
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
@@ -44,7 +44,7 @@ struct DeveloperView: View {
|
||||
|
||||
Section {
|
||||
settingsRow("arrow.up.doc") {
|
||||
Toggle("Send files via XFTP", isOn: $xftpSendEnabled)
|
||||
Toggle("Send videos and files via XFTP", isOn: $xftpSendEnabled)
|
||||
.onChange(of: xftpSendEnabled) { _ in
|
||||
do {
|
||||
try setXFTPConfig(getXFTPCfg())
|
||||
|
||||
@@ -16,7 +16,7 @@ struct ExperimentalFeaturesView: View {
|
||||
List {
|
||||
Section("") {
|
||||
settingsRow("arrow.up.doc") {
|
||||
Toggle("Send files via XFTP", isOn: $xftpSendEnabled)
|
||||
Toggle("Send videos and files via XFTP", isOn: $xftpSendEnabled)
|
||||
.onChange(of: xftpSendEnabled) { _ in
|
||||
do {
|
||||
try setXFTPConfig(getXFTPCfg())
|
||||
|
||||
@@ -25,6 +25,7 @@ private enum NetworkAlert: Identifiable {
|
||||
|
||||
struct NetworkAndServers: View {
|
||||
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
|
||||
@AppStorage(GROUP_DEFAULT_XFTP_SEND_ENABLED, store: groupDefaults) private var xftpSendEnabled = false
|
||||
@State private var cfgLoaded = false
|
||||
@State private var currentNetCfg = NetCfg.defaults
|
||||
@State private var netCfg = NetCfg.defaults
|
||||
@@ -37,12 +38,21 @@ struct NetworkAndServers: View {
|
||||
List {
|
||||
Section {
|
||||
NavigationLink {
|
||||
SMPServersView()
|
||||
ProtocolServersView(serverProtocol: .smp)
|
||||
.navigationTitle("Your SMP servers")
|
||||
} label: {
|
||||
Text("SMP servers")
|
||||
}
|
||||
|
||||
if xftpSendEnabled {
|
||||
NavigationLink {
|
||||
ProtocolServersView(serverProtocol: .xftp)
|
||||
.navigationTitle("Your XFTP servers")
|
||||
} label: {
|
||||
Text("XFTP servers")
|
||||
}
|
||||
}
|
||||
|
||||
Picker("Use .onion hosts", selection: $onionHosts) {
|
||||
ForEach(OnionHosts.values, id: \.self) { Text($0.text) }
|
||||
}
|
||||
@@ -62,7 +72,7 @@ struct NetworkAndServers: View {
|
||||
Text("Advanced network settings")
|
||||
}
|
||||
} header: {
|
||||
Text("Messages")
|
||||
Text("Messages & files")
|
||||
} footer: {
|
||||
Text("Using .onion hosts requires compatible VPN provider.")
|
||||
}
|
||||
|
||||
+24
-17
@@ -9,13 +9,16 @@
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct SMPServerView: View {
|
||||
struct ProtocolServerView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
let serverProtocol: ServerProtocol
|
||||
@Binding var server: ServerCfg
|
||||
@State var serverToEdit: ServerCfg
|
||||
@State private var showTestFailure = false
|
||||
@State private var testing = false
|
||||
@State private var testFailure: SMPTestFailure?
|
||||
@State private var testFailure: ProtocolTestFailure?
|
||||
|
||||
var proto: String { serverProtocol.rawValue.uppercased() }
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -28,7 +31,7 @@ struct SMPServerView: View {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
}
|
||||
.modifier(BackButton(label: "Your SMP servers") {
|
||||
.modifier(BackButton(label: "Your \(proto) servers") {
|
||||
server = serverToEdit
|
||||
dismiss()
|
||||
})
|
||||
@@ -57,7 +60,8 @@ struct SMPServerView: View {
|
||||
|
||||
private func customServer() -> some View {
|
||||
VStack {
|
||||
let valid = parseServerAddress(serverToEdit.server)?.valid == true
|
||||
let serverAddress = parseServerAddress(serverToEdit.server)
|
||||
let valid = serverAddress?.valid == true && serverAddress?.serverProtocol == serverProtocol
|
||||
List {
|
||||
Section {
|
||||
TextEditor(text: $serverToEdit.server)
|
||||
@@ -144,18 +148,17 @@ struct BackButton: ViewModifier {
|
||||
}
|
||||
}
|
||||
|
||||
func testServerConnection(server: Binding<ServerCfg>) async -> SMPTestFailure? {
|
||||
func testServerConnection(server: Binding<ServerCfg>) async -> ProtocolTestFailure? {
|
||||
do {
|
||||
let r = try await testSMPServer(smpServer: server.wrappedValue.server)
|
||||
|
||||
switch r {
|
||||
case .success:
|
||||
await MainActor.run { server.wrappedValue.tested = true }
|
||||
return nil
|
||||
case let .failure(f):
|
||||
await MainActor.run { server.wrappedValue.tested = false }
|
||||
return f
|
||||
}
|
||||
let r = try await testProtoServer(server: server.wrappedValue.server)
|
||||
switch r {
|
||||
case .success:
|
||||
await MainActor.run { server.wrappedValue.tested = true }
|
||||
return nil
|
||||
case let .failure(f):
|
||||
await MainActor.run { server.wrappedValue.tested = false }
|
||||
return f
|
||||
}
|
||||
} catch let error {
|
||||
logger.error("testServerConnection \(responseError(error))")
|
||||
await MainActor.run {
|
||||
@@ -169,8 +172,12 @@ func serverHostname(_ srv: String) -> String {
|
||||
parseServerAddress(srv)?.hostnames.first ?? srv
|
||||
}
|
||||
|
||||
struct SMPServerView_Previews: PreviewProvider {
|
||||
struct ProtocolServerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SMPServerView(server: Binding.constant(ServerCfg.sampleData.custom), serverToEdit: ServerCfg.sampleData.custom)
|
||||
ProtocolServerView(
|
||||
serverProtocol: .smp,
|
||||
server: Binding.constant(ServerCfg.sampleData.custom),
|
||||
serverToEdit: ServerCfg.sampleData.custom
|
||||
)
|
||||
}
|
||||
}
|
||||
+64
-36
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// SMPServersView.swift
|
||||
// ProtocolServersView.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 15/11/2022.
|
||||
@@ -11,29 +11,35 @@ import SimpleXChat
|
||||
|
||||
private let howToUrl = URL(string: "https://github.com/simplex-chat/simplex-chat/blob/stable/docs/SERVER.md")!
|
||||
|
||||
struct SMPServersView: View {
|
||||
struct ProtocolServersView: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@EnvironmentObject private var m: ChatModel
|
||||
@Environment(\.editMode) private var editMode
|
||||
@State private var servers = ChatModel.shared.userSMPServers ?? []
|
||||
let serverProtocol: ServerProtocol
|
||||
@State private var currServers: [ServerCfg] = []
|
||||
@State private var presetServers: [String] = []
|
||||
@State private var servers: [ServerCfg] = []
|
||||
@State private var selectedServer: String? = nil
|
||||
@State private var showAddServer = false
|
||||
@State private var showScanSMPServer = false
|
||||
@State private var showScanProtoServer = false
|
||||
@State private var justOpened = true
|
||||
@State private var testing = false
|
||||
@State private var alert: SMPServerAlert? = nil
|
||||
@State private var alert: ServerAlert? = nil
|
||||
@State private var showSaveDialog = false
|
||||
|
||||
var proto: String { serverProtocol.rawValue.uppercased() }
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
smpServersView()
|
||||
protocolServersView()
|
||||
if testing {
|
||||
ProgressView().scaleEffect(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum SMPServerAlert: Identifiable {
|
||||
case testsFailed(failures: [String: SMPTestFailure])
|
||||
enum ServerAlert: Identifiable {
|
||||
case testsFailed(failures: [String: ProtocolTestFailure])
|
||||
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
|
||||
|
||||
var id: String {
|
||||
@@ -44,11 +50,11 @@ struct SMPServersView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func smpServersView() -> some View {
|
||||
private func protocolServersView() -> some View {
|
||||
List {
|
||||
Section {
|
||||
ForEach($servers) { srv in
|
||||
smpServerView(srv)
|
||||
protocolServerView(srv)
|
||||
}
|
||||
.onMove { indexSet, offset in
|
||||
servers.move(fromOffsets: indexSet, toOffset: offset)
|
||||
@@ -60,18 +66,18 @@ struct SMPServersView: View {
|
||||
showAddServer = true
|
||||
}
|
||||
} header: {
|
||||
Text("SMP servers")
|
||||
Text("\(proto) servers")
|
||||
} footer: {
|
||||
Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.")
|
||||
.lineLimit(10)
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("Reset") { servers = m.userSMPServers ?? [] }
|
||||
.disabled(servers == m.userSMPServers || testing)
|
||||
Button("Reset") { servers = currServers }
|
||||
.disabled(servers == currServers || testing)
|
||||
Button("Test servers", action: testServers)
|
||||
.disabled(testing || allServersDisabled)
|
||||
Button("Save servers", action: saveSMPServers)
|
||||
Button("Save servers", action: saveServers)
|
||||
.disabled(saveDisabled)
|
||||
howToButton()
|
||||
}
|
||||
@@ -82,24 +88,26 @@ struct SMPServersView: View {
|
||||
servers.append(ServerCfg.empty)
|
||||
selectedServer = servers.last?.id
|
||||
}
|
||||
Button("Scan server QR code") { showScanSMPServer = true }
|
||||
Button("Scan server QR code") { showScanProtoServer = true }
|
||||
Button("Add preset servers", action: addAllPresets)
|
||||
.disabled(hasAllPresets())
|
||||
}
|
||||
.sheet(isPresented: $showScanSMPServer) {
|
||||
ScanSMPServer(servers: $servers)
|
||||
.sheet(isPresented: $showScanProtoServer) {
|
||||
ScanProtocolServer(servers: $servers)
|
||||
}
|
||||
.modifier(BackButton {
|
||||
if saveDisabled {
|
||||
dismiss()
|
||||
justOpened = false
|
||||
} else {
|
||||
showSaveDialog = true
|
||||
}
|
||||
})
|
||||
.confirmationDialog("Save servers?", isPresented: $showSaveDialog) {
|
||||
Button("Save") {
|
||||
saveSMPServers()
|
||||
saveServers()
|
||||
dismiss()
|
||||
justOpened = false
|
||||
}
|
||||
Button("Exit without saving") { dismiss() }
|
||||
}
|
||||
@@ -119,11 +127,27 @@ struct SMPServersView: View {
|
||||
)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
// this condition is needed to prevent re-setting the servers when exiting single server view
|
||||
if !justOpened { return }
|
||||
do {
|
||||
let r = try getUserProtoServers(serverProtocol)
|
||||
currServers = r.protoServers
|
||||
presetServers = r.presetServers
|
||||
servers = currServers
|
||||
} catch let error {
|
||||
alert = .error(
|
||||
title: "Error loading \(proto) servers",
|
||||
error: "Error: \(responseError(error))"
|
||||
)
|
||||
}
|
||||
justOpened = false
|
||||
}
|
||||
}
|
||||
|
||||
private var saveDisabled: Bool {
|
||||
servers.isEmpty ||
|
||||
servers == m.userSMPServers ||
|
||||
servers == currServers ||
|
||||
testing ||
|
||||
!servers.allSatisfy { srv in
|
||||
if let address = parseServerAddress(srv.server) {
|
||||
@@ -138,18 +162,22 @@ struct SMPServersView: View {
|
||||
servers.allSatisfy { !$0.enabled }
|
||||
}
|
||||
|
||||
private func smpServerView(_ server: Binding<ServerCfg>) -> some View {
|
||||
private func protocolServerView(_ server: Binding<ServerCfg>) -> some View {
|
||||
let srv = server.wrappedValue
|
||||
return NavigationLink(tag: srv.id, selection: $selectedServer) {
|
||||
SMPServerView(server: server, serverToEdit: srv)
|
||||
.navigationBarTitle(srv.preset ? "Preset server" : "Your server")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
ProtocolServerView(
|
||||
serverProtocol: serverProtocol,
|
||||
server: server,
|
||||
serverToEdit: srv
|
||||
)
|
||||
.navigationBarTitle(srv.preset ? "Preset server" : "Your server")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
} label: {
|
||||
let address = parseServerAddress(srv.server)
|
||||
HStack {
|
||||
Group {
|
||||
if let address = address {
|
||||
if !address.valid {
|
||||
if !address.valid || address.serverProtocol != serverProtocol {
|
||||
invalidServer()
|
||||
} else if !uniqueAddress(srv, address) {
|
||||
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
||||
@@ -201,11 +229,11 @@ struct SMPServersView: View {
|
||||
}
|
||||
|
||||
private func hasAllPresets() -> Bool {
|
||||
m.presetSMPServers?.allSatisfy { hasPreset($0) } ?? true
|
||||
presetServers.allSatisfy { hasPreset($0) }
|
||||
}
|
||||
|
||||
private func addAllPresets() {
|
||||
for srv in m.presetSMPServers ?? [] {
|
||||
for srv in presetServers {
|
||||
if !hasPreset(srv) {
|
||||
servers.append(ServerCfg(server: srv, preset: true, tested: nil, enabled: true))
|
||||
}
|
||||
@@ -238,8 +266,8 @@ struct SMPServersView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func runServersTest() async -> [String: SMPTestFailure] {
|
||||
var fs: [String: SMPTestFailure] = [:]
|
||||
private func runServersTest() async -> [String: ProtocolTestFailure] {
|
||||
var fs: [String: ProtocolTestFailure] = [:]
|
||||
for i in 0..<servers.count {
|
||||
if servers[i].enabled {
|
||||
if let f = await testServerConnection(server: $servers[i]) {
|
||||
@@ -250,21 +278,21 @@ struct SMPServersView: View {
|
||||
return fs
|
||||
}
|
||||
|
||||
func saveSMPServers() {
|
||||
func saveServers() {
|
||||
Task {
|
||||
do {
|
||||
try await setUserSMPServers(smpServers: servers)
|
||||
try await setUserProtoServers(serverProtocol, servers: servers)
|
||||
await MainActor.run {
|
||||
m.userSMPServers = servers
|
||||
currServers = servers
|
||||
editMode?.wrappedValue = .inactive
|
||||
}
|
||||
} catch let error {
|
||||
let err = responseError(error)
|
||||
logger.error("saveSMPServers setUserSMPServers error: \(err)")
|
||||
logger.error("saveServers setUserProtocolServers error: \(err)")
|
||||
await MainActor.run {
|
||||
alert = .error(
|
||||
title: "Error saving SMP servers",
|
||||
error: "Make sure SMP server addresses are in correct format, line separated and are not duplicated (\(responseError(error)))."
|
||||
title: "Error saving \(proto) servers",
|
||||
error: "Make sure \(proto) server addresses are in correct format, line separated and are not duplicated (\(responseError(error)))."
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -272,8 +300,8 @@ struct SMPServersView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct SMPServersView_Previews: PreviewProvider {
|
||||
struct ProtocolServersView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SMPServersView()
|
||||
ProtocolServersView(serverProtocol: .smp)
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// ScanSMPServer.swift
|
||||
// ScanProtocolServer.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Evgeny on 19/11/2022.
|
||||
@@ -10,7 +10,7 @@ import SwiftUI
|
||||
import SimpleXChat
|
||||
import CodeScanner
|
||||
|
||||
struct ScanSMPServer: View {
|
||||
struct ScanProtocolServer: View {
|
||||
@Environment(\.dismiss) var dismiss: DismissAction
|
||||
@Binding var servers: [ServerCfg]
|
||||
@State private var showAddressError = false
|
||||
@@ -47,14 +47,14 @@ struct ScanSMPServer: View {
|
||||
showAddressError = true
|
||||
}
|
||||
case let .failure(e):
|
||||
logger.error("ScanSMPServer.processQRCode QR code error: \(e.localizedDescription)")
|
||||
logger.error("ScanProtocolServer.processQRCode QR code error: \(e.localizedDescription)")
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ScanSMPServer_Previews: PreviewProvider {
|
||||
struct ScanProtocolServer_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ScanSMPServer(servers: Binding.constant([]))
|
||||
ScanProtocolServer(servers: Binding.constant([]))
|
||||
}
|
||||
}
|
||||
@@ -3081,8 +3081,8 @@ Wir werden Serverredundanzen hinzufügen, um verloren gegangene Nachrichten zu v
|
||||
<target>Direktnachricht senden</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send files via XFTP" xml:space="preserve">
|
||||
<source>Send files via XFTP</source>
|
||||
<trans-unit id="Send videos and files via XFTP" xml:space="preserve">
|
||||
<source>Send videos and files via XFTP</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send link previews" xml:space="preserve">
|
||||
|
||||
@@ -3084,9 +3084,9 @@ We will be adding server redundancy to prevent lost messages.</target>
|
||||
<target>Send direct message</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send files via XFTP" xml:space="preserve">
|
||||
<source>Send files via XFTP</source>
|
||||
<target>Send files via XFTP</target>
|
||||
<trans-unit id="Send videos and files via XFTP" xml:space="preserve">
|
||||
<source>Send videos and files via XFTP</source>
|
||||
<target>Send videos and files via XFTP</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send link previews" xml:space="preserve">
|
||||
|
||||
@@ -3082,8 +3082,8 @@ Añadiremos redundancia de servidores para evitar la pérdida de mensajes.</targ
|
||||
<target>Enviar mensaje directo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send files via XFTP" xml:space="preserve">
|
||||
<source>Send files via XFTP</source>
|
||||
<trans-unit id="Send videos and files via XFTP" xml:space="preserve">
|
||||
<source>Send videos and files via XFTP</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send link previews" xml:space="preserve">
|
||||
|
||||
@@ -3081,8 +3081,8 @@ Nous allons ajouter une redondance des serveurs pour éviter la perte de message
|
||||
<target>Envoi de message direct</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send files via XFTP" xml:space="preserve">
|
||||
<source>Send files via XFTP</source>
|
||||
<trans-unit id="Send videos and files via XFTP" xml:space="preserve">
|
||||
<source>Send videos and files via XFTP</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send link previews" xml:space="preserve">
|
||||
|
||||
@@ -3081,8 +3081,8 @@ Aggiungeremo la ridondanza del server per prevenire la perdita di messaggi.</tar
|
||||
<target>Invia messaggio diretto</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send files via XFTP" xml:space="preserve">
|
||||
<source>Send files via XFTP</source>
|
||||
<trans-unit id="Send videos and files via XFTP" xml:space="preserve">
|
||||
<source>Send videos and files via XFTP</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send link previews" xml:space="preserve">
|
||||
|
||||
@@ -3081,8 +3081,8 @@ We zullen serverredundantie toevoegen om verloren berichten te voorkomen.</targe
|
||||
<target>Direct bericht sturen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send files via XFTP" xml:space="preserve">
|
||||
<source>Send files via XFTP</source>
|
||||
<trans-unit id="Send videos and files via XFTP" xml:space="preserve">
|
||||
<source>Send videos and files via XFTP</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send link previews" xml:space="preserve">
|
||||
|
||||
@@ -3084,9 +3084,9 @@ We will be adding server redundancy to prevent lost messages.</source>
|
||||
<target>Отправить сообщение</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send files via XFTP" xml:space="preserve">
|
||||
<source>Send files via XFTP</source>
|
||||
<target>Отправлять файлы через XFTP</target>
|
||||
<trans-unit id="Send videos and files via XFTP" xml:space="preserve">
|
||||
<source>Send videos and files via XFTP</source>
|
||||
<target>Отправлять видео и файлы через XFTP</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send link previews" xml:space="preserve">
|
||||
|
||||
@@ -277,6 +277,12 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
|
||||
privacyAcceptImagesGroupDefault.get() {
|
||||
cItem = apiReceiveFile(fileId: file.fileId)?.chatItem ?? cItem
|
||||
}
|
||||
} else if case .video = cItem.content.msgContent {
|
||||
if let file = cItem.file,
|
||||
file.fileSize <= MAX_VIDEO_SIZE_AUTO_RCV,
|
||||
privacyAcceptImagesGroupDefault.get() {
|
||||
cItem = apiReceiveFile(fileId: file.fileId)?.chatItem ?? cItem
|
||||
}
|
||||
} else if case .voice = cItem.content.msgContent { // TODO check inlineFileMode != IFMSent
|
||||
if let file = cItem.file,
|
||||
file.fileSize <= MAX_IMAGE_SIZE,
|
||||
|
||||
@@ -10,9 +10,11 @@
|
||||
184152CEF68D2336FC2EBCB0 /* CallViewRenderers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415B08031E8FB0F7FC27F9 /* CallViewRenderers.swift */; };
|
||||
1841538E296606C74533367C /* UserPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415835CBD939A9ABDC108A /* UserPicker.swift */; };
|
||||
1841560FD1CD447955474C1D /* UserProfilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415845648CA4F5A8BCA272 /* UserProfilesView.swift */; };
|
||||
184158C131FDB829D8A117EA /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415DAAAD1ADBEDB0EDA852 /* VideoPlayerView.swift */; };
|
||||
1841594C978674A7B42EF0C0 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1841511920742C6E152E469F /* AnimatedImageView.swift */; };
|
||||
18415B0585EB5A9A0A7CA8CD /* PressedButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */; };
|
||||
18415C6C56DBCEC2CBBD2F11 /* WebRTCClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415323A4082FC92887F906 /* WebRTCClient.swift */; };
|
||||
18415F9A2D551F9757DA4654 /* CIVideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18415FD2E36F13F596A45BB4 /* CIVideoView.swift */; };
|
||||
18415FEFE153C5920BFB7828 /* GroupWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1841516F0CE5992B0EDFB377 /* GroupWelcomeView.swift */; };
|
||||
3C71477A281C0F6800CB4D4B /* www in Resources */ = {isa = PBXBuildFile; fileRef = 3C714779281C0F6800CB4D4B /* www */; };
|
||||
3C8C548928133C84000A3EC7 /* PasteToConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */; };
|
||||
@@ -25,7 +27,7 @@
|
||||
5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; };
|
||||
5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; };
|
||||
5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */; };
|
||||
5C10D88A28F187F300E58BF0 /* FullScreenImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88928F187F300E58BF0 /* FullScreenImageView.swift */; };
|
||||
5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */; };
|
||||
5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; };
|
||||
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C13730A28156D2700F43030 /* ContactConnectionView.swift */; };
|
||||
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1A4C1D27A715B700EAD5AD /* ChatItemView.swift */; };
|
||||
@@ -63,10 +65,10 @@
|
||||
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7505A727B6D34800BE3227 /* ChatInfoToolbar.swift */; };
|
||||
5C764E89279CBCB3000C6508 /* ChatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C764E88279CBCB3000C6508 /* ChatModel.swift */; };
|
||||
5C8F01CD27A6F0D8007D2C8D /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = 5C8F01CC27A6F0D8007D2C8D /* CodeScanner */; };
|
||||
5C93292F29239A170090FFF9 /* SMPServersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93292E29239A170090FFF9 /* SMPServersView.swift */; };
|
||||
5C93293129239BED0090FFF9 /* SMPServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293029239BED0090FFF9 /* SMPServerView.swift */; };
|
||||
5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93292E29239A170090FFF9 /* ProtocolServersView.swift */; };
|
||||
5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293029239BED0090FFF9 /* ProtocolServerView.swift */; };
|
||||
5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */; };
|
||||
5C9329412929248A0090FFF9 /* ScanSMPServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9329402929248A0090FFF9 /* ScanSMPServer.swift */; };
|
||||
5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */; };
|
||||
5C971E1D27AEBEF600C8A3CE /* ChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */; };
|
||||
5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */; };
|
||||
5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */; };
|
||||
@@ -97,11 +99,11 @@
|
||||
5CB346E52868AA7F001FD2EF /* SuspendChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E42868AA7F001FD2EF /* SuspendChat.swift */; };
|
||||
5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E62868D76D001FD2EF /* NotificationsView.swift */; };
|
||||
5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */; };
|
||||
5CB6348429DCD8130066AD6B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6347F29DCD8130066AD6B /* libgmpxx.a */; };
|
||||
5CB6348529DCD8130066AD6B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6348029DCD8130066AD6B /* libffi.a */; };
|
||||
5CB6348629DCD8130066AD6B /* libHSsimplex-chat-4.6.1.1-mpt0wL13f7GxVMIrCoj3B-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6348129DCD8130066AD6B /* libHSsimplex-chat-4.6.1.1-mpt0wL13f7GxVMIrCoj3B-ghc8.10.7.a */; };
|
||||
5CB6348729DCD8130066AD6B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6348229DCD8130066AD6B /* libgmp.a */; };
|
||||
5CB6348829DCD8130066AD6B /* libHSsimplex-chat-4.6.1.1-mpt0wL13f7GxVMIrCoj3B.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6348329DCD8130066AD6B /* libHSsimplex-chat-4.6.1.1-mpt0wL13f7GxVMIrCoj3B.a */; };
|
||||
5CB6349829DF7CF00066AD6B /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6349329DF7CF00066AD6B /* libgmpxx.a */; };
|
||||
5CB6349929DF7CF00066AD6B /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6349429DF7CF00066AD6B /* libffi.a */; };
|
||||
5CB6349A29DF7CF00066AD6B /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6349529DF7CF00066AD6B /* libgmp.a */; };
|
||||
5CB6349B29DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6349629DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD-ghc8.10.7.a */; };
|
||||
5CB6349C29DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CB6349729DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD.a */; };
|
||||
5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D627A8563F00ACCCDD /* SettingsView.swift */; };
|
||||
5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; };
|
||||
5CB924E427A8683A00ACCCDD /* UserAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E327A8683A00ACCCDD /* UserAddress.swift */; };
|
||||
@@ -240,6 +242,8 @@
|
||||
18415845648CA4F5A8BCA272 /* UserProfilesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserProfilesView.swift; sourceTree = "<group>"; };
|
||||
18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PressedButtonStyle.swift; sourceTree = "<group>"; };
|
||||
18415B08031E8FB0F7FC27F9 /* CallViewRenderers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallViewRenderers.swift; sourceTree = "<group>"; };
|
||||
18415DAAAD1ADBEDB0EDA852 /* VideoPlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = "<group>"; };
|
||||
18415FD2E36F13F596A45BB4 /* CIVideoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIVideoView.swift; sourceTree = "<group>"; };
|
||||
3C714779281C0F6800CB4D4B /* www */ = {isa = PBXFileReference; lastKnownFileType = folder; name = www; path = ../android/app/src/main/assets/www; sourceTree = "<group>"; };
|
||||
3C8C548828133C84000A3EC7 /* PasteToConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteToConnectView.swift; sourceTree = "<group>"; };
|
||||
3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = "<group>"; };
|
||||
@@ -251,7 +255,7 @@
|
||||
5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = "<group>"; };
|
||||
5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = "<group>"; };
|
||||
5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionInfo.swift; sourceTree = "<group>"; };
|
||||
5C10D88928F187F300E58BF0 /* FullScreenImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenImageView.swift; sourceTree = "<group>"; };
|
||||
5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMediaView.swift; sourceTree = "<group>"; };
|
||||
5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = "<group>"; };
|
||||
5C13730A28156D2700F43030 /* ContactConnectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionView.swift; sourceTree = "<group>"; };
|
||||
5C13730C2815740A00F43030 /* DebugJSON.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = DebugJSON.playground; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
@@ -307,10 +311,10 @@
|
||||
5C8B41CA29AF41BC00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
5C8B41CB29AF44CF00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = "cs.lproj/SimpleX--iOS--InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
5C8B41CC29AF44CF00888272 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
5C93292E29239A170090FFF9 /* SMPServersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServersView.swift; sourceTree = "<group>"; };
|
||||
5C93293029239BED0090FFF9 /* SMPServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMPServerView.swift; sourceTree = "<group>"; };
|
||||
5C93292E29239A170090FFF9 /* ProtocolServersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolServersView.swift; sourceTree = "<group>"; };
|
||||
5C93293029239BED0090FFF9 /* ProtocolServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtocolServerView.swift; sourceTree = "<group>"; };
|
||||
5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecPlay.swift; sourceTree = "<group>"; };
|
||||
5C9329402929248A0090FFF9 /* ScanSMPServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanSMPServer.swift; sourceTree = "<group>"; };
|
||||
5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanProtocolServer.swift; sourceTree = "<group>"; };
|
||||
5C971E1C27AEBEF600C8A3CE /* ChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoView.swift; sourceTree = "<group>"; };
|
||||
5C971E2027AEBF8300C8A3CE /* ChatInfoImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInfoImage.swift; sourceTree = "<group>"; };
|
||||
5C9A5BDA2871E05400A5B906 /* SetNotificationsMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetNotificationsMode.swift; sourceTree = "<group>"; };
|
||||
@@ -351,11 +355,11 @@
|
||||
5CB346E42868AA7F001FD2EF /* SuspendChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuspendChat.swift; sourceTree = "<group>"; };
|
||||
5CB346E62868D76D001FD2EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
|
||||
5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushEnvironment.swift; sourceTree = "<group>"; };
|
||||
5CB6347F29DCD8130066AD6B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CB6348029DCD8130066AD6B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CB6348129DCD8130066AD6B /* libHSsimplex-chat-4.6.1.1-mpt0wL13f7GxVMIrCoj3B-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.6.1.1-mpt0wL13f7GxVMIrCoj3B-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5CB6348229DCD8130066AD6B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CB6348329DCD8130066AD6B /* libHSsimplex-chat-4.6.1.1-mpt0wL13f7GxVMIrCoj3B.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.6.1.1-mpt0wL13f7GxVMIrCoj3B.a"; sourceTree = "<group>"; };
|
||||
5CB6349329DF7CF00066AD6B /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
5CB6349429DF7CF00066AD6B /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
5CB6349529DF7CF00066AD6B /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
5CB6349629DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD-ghc8.10.7.a"; sourceTree = "<group>"; };
|
||||
5CB6349729DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD.a"; sourceTree = "<group>"; };
|
||||
5CB924D627A8563F00ACCCDD /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
5CB924E027A867BA00ACCCDD /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = "<group>"; };
|
||||
5CB924E327A8683A00ACCCDD /* UserAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddress.swift; sourceTree = "<group>"; };
|
||||
@@ -467,13 +471,13 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5CB6348629DCD8130066AD6B /* libHSsimplex-chat-4.6.1.1-mpt0wL13f7GxVMIrCoj3B-ghc8.10.7.a in Frameworks */,
|
||||
5CB6348829DCD8130066AD6B /* libHSsimplex-chat-4.6.1.1-mpt0wL13f7GxVMIrCoj3B.a in Frameworks */,
|
||||
5CB6348429DCD8130066AD6B /* libgmpxx.a in Frameworks */,
|
||||
5CB6349C29DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD.a in Frameworks */,
|
||||
5CB6349B29DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD-ghc8.10.7.a in Frameworks */,
|
||||
5CB6349829DF7CF00066AD6B /* libgmpxx.a in Frameworks */,
|
||||
5CB6349A29DF7CF00066AD6B /* libgmp.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
5CB6348729DCD8130066AD6B /* libgmp.a in Frameworks */,
|
||||
5CB6348529DCD8130066AD6B /* libffi.a in Frameworks */,
|
||||
5CB6349929DF7CF00066AD6B /* libffi.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -532,11 +536,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CB6348029DCD8130066AD6B /* libffi.a */,
|
||||
5CB6348229DCD8130066AD6B /* libgmp.a */,
|
||||
5CB6347F29DCD8130066AD6B /* libgmpxx.a */,
|
||||
5CB6348129DCD8130066AD6B /* libHSsimplex-chat-4.6.1.1-mpt0wL13f7GxVMIrCoj3B-ghc8.10.7.a */,
|
||||
5CB6348329DCD8130066AD6B /* libHSsimplex-chat-4.6.1.1-mpt0wL13f7GxVMIrCoj3B.a */,
|
||||
5CB6349429DF7CF00066AD6B /* libffi.a */,
|
||||
5CB6349529DF7CF00066AD6B /* libgmp.a */,
|
||||
5CB6349329DF7CF00066AD6B /* libgmpxx.a */,
|
||||
5CB6349629DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD-ghc8.10.7.a */,
|
||||
5CB6349729DF7CF00066AD6B /* libHSsimplex-chat-4.6.1.1-KdibHfABD5wCx2UMHpStjD.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -584,6 +588,7 @@
|
||||
5CA7DFC229302AF000F7FDDE /* AppSheet.swift */,
|
||||
18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */,
|
||||
5CCB939B297EFCB100399E78 /* NavStackCompat.swift */,
|
||||
18415DAAAD1ADBEDB0EDA852 /* VideoPlayerView.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
@@ -685,9 +690,9 @@
|
||||
5CB924E027A867BA00ACCCDD /* UserProfile.swift */,
|
||||
5CC036DF29C488D500C0EF20 /* HiddenProfileView.swift */,
|
||||
5C577F7C27C83AA10006112D /* MarkdownHelp.swift */,
|
||||
5C93292E29239A170090FFF9 /* SMPServersView.swift */,
|
||||
5C93293029239BED0090FFF9 /* SMPServerView.swift */,
|
||||
5C9329402929248A0090FFF9 /* ScanSMPServer.swift */,
|
||||
5C93292E29239A170090FFF9 /* ProtocolServersView.swift */,
|
||||
5C93293029239BED0090FFF9 /* ProtocolServerView.swift */,
|
||||
5C9329402929248A0090FFF9 /* ScanProtocolServer.swift */,
|
||||
5CB2084E28DA4B4800D024EC /* RTCServers.swift */,
|
||||
5C3F1D592844B4DE00EC8A82 /* ExperimentalFeaturesView.swift */,
|
||||
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */,
|
||||
@@ -753,7 +758,7 @@
|
||||
5CEACCEC27DEA495000BD591 /* MsgContentView.swift */,
|
||||
5C3A88D027DF57800060F1C2 /* FramedItemView.swift */,
|
||||
649BCDA12805D6EF00C3A862 /* CIImageView.swift */,
|
||||
5C10D88928F187F300E58BF0 /* FullScreenImageView.swift */,
|
||||
5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */,
|
||||
648010AA281ADD15009009B9 /* CIFileView.swift */,
|
||||
644EFFDF292CFD7F00525D5B /* CIVoiceView.swift */,
|
||||
3CDBCF4727FF621E00354CDD /* CILinkView.swift */,
|
||||
@@ -768,6 +773,7 @@
|
||||
644EFFE32937BE9700525D5B /* MarkedDeletedItemView.swift */,
|
||||
1841511920742C6E152E469F /* AnimatedImageView.swift */,
|
||||
6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */,
|
||||
18415FD2E36F13F596A45BB4 /* CIVideoView.swift */,
|
||||
);
|
||||
path = ChatItem;
|
||||
sourceTree = "<group>";
|
||||
@@ -1022,7 +1028,7 @@
|
||||
5C6AD81327A834E300348BD7 /* NewChatButton.swift in Sources */,
|
||||
5C3F1D58284363C400EC8A82 /* PrivacySettings.swift in Sources */,
|
||||
5C55A923283CEDE600C4E99E /* SoundPlayer.swift in Sources */,
|
||||
5C93292F29239A170090FFF9 /* SMPServersView.swift in Sources */,
|
||||
5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */,
|
||||
5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */,
|
||||
5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */,
|
||||
5C65DAF929D0CC20003CEE45 /* DeveloperView.swift in Sources */,
|
||||
@@ -1032,7 +1038,7 @@
|
||||
5C13730B28156D2700F43030 /* ContactConnectionView.swift in Sources */,
|
||||
644EFFE0292CFD7F00525D5B /* CIVoiceView.swift in Sources */,
|
||||
6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */,
|
||||
5C93293129239BED0090FFF9 /* SMPServerView.swift in Sources */,
|
||||
5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */,
|
||||
5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */,
|
||||
5CBD285A295711D700EC2CF4 /* ImageUtils.swift in Sources */,
|
||||
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
|
||||
@@ -1052,7 +1058,7 @@
|
||||
3CDBCF4227FAE51000354CDD /* ComposeLinkView.swift in Sources */,
|
||||
3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */,
|
||||
5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */,
|
||||
5C10D88A28F187F300E58BF0 /* FullScreenImageView.swift in Sources */,
|
||||
5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */,
|
||||
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */,
|
||||
5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */,
|
||||
5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */,
|
||||
@@ -1122,7 +1128,7 @@
|
||||
6440CA00288857A10062C672 /* CIEventView.swift in Sources */,
|
||||
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
|
||||
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
|
||||
5C9329412929248A0090FFF9 /* ScanSMPServer.swift in Sources */,
|
||||
5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */,
|
||||
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
|
||||
5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */,
|
||||
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */,
|
||||
@@ -1143,6 +1149,8 @@
|
||||
18415C6C56DBCEC2CBBD2F11 /* WebRTCClient.swift in Sources */,
|
||||
184152CEF68D2336FC2EBCB0 /* CallViewRenderers.swift in Sources */,
|
||||
18415FEFE153C5920BFB7828 /* GroupWelcomeView.swift in Sources */,
|
||||
18415F9A2D551F9757DA4654 /* CIVideoView.swift in Sources */,
|
||||
184158C131FDB829D8A117EA /* VideoPlayerView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -57,9 +57,9 @@ public enum ChatCommand {
|
||||
case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole)
|
||||
case apiDeleteGroupLink(groupId: Int64)
|
||||
case apiGetGroupLink(groupId: Int64)
|
||||
case apiGetUserSMPServers(userId: Int64)
|
||||
case apiSetUserSMPServers(userId: Int64, smpServers: [ServerCfg])
|
||||
case apiTestSMPServer(userId: Int64, smpServer: String)
|
||||
case apiGetUserProtoServers(userId: Int64, serverProtocol: ServerProtocol)
|
||||
case apiSetUserProtoServers(userId: Int64, serverProtocol: ServerProtocol, servers: [ServerCfg])
|
||||
case apiTestProtoServer(userId: Int64, server: String)
|
||||
case apiSetChatItemTTL(userId: Int64, seconds: Int64?)
|
||||
case apiGetChatItemTTL(userId: Int64)
|
||||
case apiSetNetworkConfig(networkConfig: NetCfg)
|
||||
@@ -158,9 +158,9 @@ public enum ChatCommand {
|
||||
case let .apiGroupLinkMemberRole(groupId, memberRole): return "/_set link role #\(groupId) \(memberRole)"
|
||||
case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)"
|
||||
case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)"
|
||||
case let .apiGetUserSMPServers(userId): return "/_smp \(userId)"
|
||||
case let .apiSetUserSMPServers(userId, smpServers): return "/_smp \(userId) \(smpServersStr(smpServers: smpServers))"
|
||||
case let .apiTestSMPServer(userId, smpServer): return "/_smp test \(userId) \(smpServer)"
|
||||
case let .apiGetUserProtoServers(userId, serverProtocol): return "/_servers \(userId) \(serverProtocol)"
|
||||
case let .apiSetUserProtoServers(userId, serverProtocol, servers): return "/_servers \(userId) \(serverProtocol) \(protoServersStr(servers))"
|
||||
case let .apiTestProtoServer(userId, server): return "/_server test \(userId) \(server)"
|
||||
case let .apiSetChatItemTTL(userId, seconds): return "/_ttl \(userId) \(chatItemTTLStr(seconds: seconds))"
|
||||
case let .apiGetChatItemTTL(userId): return "/_ttl \(userId)"
|
||||
case let .apiSetNetworkConfig(networkConfig): return "/_network \(encodeJSON(networkConfig))"
|
||||
@@ -260,9 +260,9 @@ public enum ChatCommand {
|
||||
case .apiGroupLinkMemberRole: return "apiGroupLinkMemberRole"
|
||||
case .apiDeleteGroupLink: return "apiDeleteGroupLink"
|
||||
case .apiGetGroupLink: return "apiGetGroupLink"
|
||||
case .apiGetUserSMPServers: return "apiGetUserSMPServers"
|
||||
case .apiSetUserSMPServers: return "apiSetUserSMPServers"
|
||||
case .apiTestSMPServer: return "testSMPServer"
|
||||
case .apiGetUserProtoServers: return "apiGetUserProtoServers"
|
||||
case .apiSetUserProtoServers: return "apiSetUserProtoServers"
|
||||
case .apiTestProtoServer: return "apiTestProtoServer"
|
||||
case .apiSetChatItemTTL: return "apiSetChatItemTTL"
|
||||
case .apiGetChatItemTTL: return "apiGetChatItemTTL"
|
||||
case .apiSetNetworkConfig: return "apiSetNetworkConfig"
|
||||
@@ -313,8 +313,8 @@ public enum ChatCommand {
|
||||
"\(type.rawValue)\(id)"
|
||||
}
|
||||
|
||||
func smpServersStr(smpServers: [ServerCfg]) -> String {
|
||||
smpServers.isEmpty ? "default" : encodeJSON(SMPServersConfig(smpServers: smpServers))
|
||||
func protoServersStr(_ servers: [ServerCfg]) -> String {
|
||||
encodeJSON(ProtoServersConfig(servers: servers))
|
||||
}
|
||||
|
||||
func chatItemTTLStr(seconds: Int64?) -> String {
|
||||
@@ -375,8 +375,8 @@ public enum ChatResponse: Decodable, Error {
|
||||
case chatSuspended
|
||||
case apiChats(user: User, chats: [ChatData])
|
||||
case apiChat(user: User, chat: ChatData)
|
||||
case userSMPServers(user: User, smpServers: [ServerCfg], presetSMPServers: [String])
|
||||
case smpTestResult(user: User, smpTestFailure: SMPTestFailure?)
|
||||
case userProtoServers(user: User, servers: UserProtoServers)
|
||||
case serverTestResult(user: User, testServer: String, testFailure: ProtocolTestFailure?)
|
||||
case chatItemTTL(user: User, chatItemTTL: Int64?)
|
||||
case networkConfig(networkConfig: NetCfg)
|
||||
case contactInfo(user: User, contact: Contact, connectionStats: ConnectionStats, customUserProfile: Profile?)
|
||||
@@ -488,8 +488,8 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .chatSuspended: return "chatSuspended"
|
||||
case .apiChats: return "apiChats"
|
||||
case .apiChat: return "apiChat"
|
||||
case .userSMPServers: return "userSMPServers"
|
||||
case .smpTestResult: return "smpTestResult"
|
||||
case .userProtoServers: return "userProtoServers"
|
||||
case .serverTestResult: return "serverTestResult"
|
||||
case .chatItemTTL: return "chatItemTTL"
|
||||
case .networkConfig: return "networkConfig"
|
||||
case .contactInfo: return "contactInfo"
|
||||
@@ -601,8 +601,8 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .chatSuspended: return noDetails
|
||||
case let .apiChats(u, chats): return withUser(u, String(describing: chats))
|
||||
case let .apiChat(u, chat): return withUser(u, String(describing: chat))
|
||||
case let .userSMPServers(u, smpServers, presetServers): return withUser(u, "smpServers: \(String(describing: smpServers))\npresetServers: \(String(describing: presetServers))")
|
||||
case let .smpTestResult(u, smpTestFailure): return withUser(u, String(describing: smpTestFailure))
|
||||
case let .userProtoServers(u, servers): return withUser(u, "servers: \(String(describing: servers))")
|
||||
case let .serverTestResult(u, server, testFailure): return withUser(u, "server: \(server)\result: \(String(describing: testFailure))")
|
||||
case let .chatItemTTL(u, chatItemTTL): return withUser(u, String(describing: chatItemTTL))
|
||||
case let .networkConfig(networkConfig): return String(describing: networkConfig)
|
||||
case let .contactInfo(u, contact, connectionStats, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))\ncustomUserProfile: \(String(describing: customUserProfile))")
|
||||
@@ -760,6 +760,21 @@ struct SMPServersConfig: Encodable {
|
||||
var smpServers: [ServerCfg]
|
||||
}
|
||||
|
||||
public enum ServerProtocol: String, Decodable {
|
||||
case smp
|
||||
case xftp
|
||||
}
|
||||
|
||||
public struct ProtoServersConfig: Codable {
|
||||
public var servers: [ServerCfg]
|
||||
}
|
||||
|
||||
public struct UserProtoServers: Decodable {
|
||||
public var serverProtocol: ServerProtocol
|
||||
public var protoServers: [ServerCfg]
|
||||
public var presetServers: [String]
|
||||
}
|
||||
|
||||
public struct ServerCfg: Identifiable, Equatable, Codable {
|
||||
public var server: String
|
||||
public var preset: Bool
|
||||
@@ -824,29 +839,39 @@ public struct ServerCfg: Identifiable, Equatable, Codable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum SMPTestStep: String, Decodable, Equatable {
|
||||
public enum ProtocolTestStep: String, Decodable, Equatable {
|
||||
case connect
|
||||
case disconnect
|
||||
case createQueue
|
||||
case secureQueue
|
||||
case deleteQueue
|
||||
case disconnect
|
||||
case createFile
|
||||
case uploadFile
|
||||
case downloadFile
|
||||
case compareFile
|
||||
case deleteFile
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case .connect: return NSLocalizedString("Connect", comment: "server test step")
|
||||
case .disconnect: return NSLocalizedString("Disconnect", comment: "server test step")
|
||||
case .createQueue: return NSLocalizedString("Create queue", comment: "server test step")
|
||||
case .secureQueue: return NSLocalizedString("Secure queue", comment: "server test step")
|
||||
case .deleteQueue: return NSLocalizedString("Delete queue", comment: "server test step")
|
||||
case .disconnect: return NSLocalizedString("Disconnect", comment: "server test step")
|
||||
case .createFile: return NSLocalizedString("Create file", comment: "server test step")
|
||||
case .uploadFile: return NSLocalizedString("Upload file", comment: "server test step")
|
||||
case .downloadFile: return NSLocalizedString("Download file", comment: "server test step")
|
||||
case .compareFile: return NSLocalizedString("Compare file", comment: "server test step")
|
||||
case .deleteFile: return NSLocalizedString("Delete file", comment: "server test step")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct SMPTestFailure: Decodable, Error, Equatable {
|
||||
var testStep: SMPTestStep
|
||||
public struct ProtocolTestFailure: Decodable, Error, Equatable {
|
||||
var testStep: ProtocolTestStep
|
||||
var testError: AgentErrorType
|
||||
|
||||
public static func == (l: SMPTestFailure, r: SMPTestFailure) -> Bool {
|
||||
public static func == (l: ProtocolTestFailure, r: ProtocolTestFailure) -> Bool {
|
||||
l.testStep == r.testStep
|
||||
}
|
||||
|
||||
@@ -855,6 +880,8 @@ public struct SMPTestFailure: Decodable, Error, Equatable {
|
||||
switch testError {
|
||||
case .SMP(.AUTH):
|
||||
return err + " " + NSLocalizedString("Server requires authorization to create queues, check password", comment: "server test error")
|
||||
case .XFTP(.AUTH):
|
||||
return err + " " + NSLocalizedString("Server requires authorization to upload, check password", comment: "server test error")
|
||||
case .BROKER(_, .NETWORK):
|
||||
return err + " " + NSLocalizedString("Possibly, certificate fingerprint in server address is incorrect", comment: "server test error")
|
||||
default:
|
||||
@@ -864,12 +891,14 @@ public struct SMPTestFailure: Decodable, Error, Equatable {
|
||||
}
|
||||
|
||||
public struct ServerAddress: Decodable {
|
||||
public var serverProtocol: ServerProtocol
|
||||
public var hostnames: [String]
|
||||
public var port: String
|
||||
public var keyHash: String
|
||||
public var basicAuth: String
|
||||
|
||||
public init(hostnames: [String], port: String, keyHash: String, basicAuth: String = "") {
|
||||
public init(serverProtocol: ServerProtocol, hostnames: [String], port: String, keyHash: String, basicAuth: String = "") {
|
||||
self.serverProtocol = serverProtocol
|
||||
self.hostnames = hostnames
|
||||
self.port = port
|
||||
self.keyHash = keyHash
|
||||
@@ -877,21 +906,25 @@ public struct ServerAddress: Decodable {
|
||||
}
|
||||
|
||||
public var uri: String {
|
||||
"smp://\(keyHash)\(basicAuth == "" ? "" : ":" + basicAuth)@\(hostnames.joined(separator: ","))"
|
||||
"\(serverProtocol)://\(keyHash)\(basicAuth == "" ? "" : ":" + basicAuth)@\(hostnames.joined(separator: ","))"
|
||||
}
|
||||
|
||||
public var valid: Bool {
|
||||
hostnames.count > 0 && Set(hostnames).count == hostnames.count
|
||||
}
|
||||
|
||||
static public var empty = ServerAddress(
|
||||
hostnames: [],
|
||||
port: "",
|
||||
keyHash: "",
|
||||
basicAuth: ""
|
||||
)
|
||||
static func empty(_ serverProtocol: ServerProtocol) -> ServerAddress {
|
||||
ServerAddress(
|
||||
serverProtocol: serverProtocol,
|
||||
hostnames: [],
|
||||
port: "",
|
||||
keyHash: "",
|
||||
basicAuth: ""
|
||||
)
|
||||
}
|
||||
|
||||
static public var sampleData = ServerAddress(
|
||||
serverProtocol: .smp,
|
||||
hostnames: ["smp.simplex.im", "1234.onion"],
|
||||
port: "",
|
||||
keyHash: "LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=",
|
||||
@@ -1236,6 +1269,7 @@ public enum AgentErrorType: Decodable {
|
||||
case CMD(cmdErr: CommandErrorType)
|
||||
case CONN(connErr: ConnectionErrorType)
|
||||
case SMP(smpErr: ProtocolErrorType)
|
||||
case XFTP(xftpErr: XFTPErrorType)
|
||||
case NTF(ntfErr: ProtocolErrorType)
|
||||
case BROKER(brokerAddress: String, brokerErr: BrokerErrorType)
|
||||
case AGENT(agentErr: SMPAgentError)
|
||||
@@ -1277,6 +1311,21 @@ public enum ProtocolErrorType: Decodable {
|
||||
case INTERNAL
|
||||
}
|
||||
|
||||
public enum XFTPErrorType: Decodable {
|
||||
case BLOCK
|
||||
case SESSION
|
||||
case CMD(cmdErr: ProtocolCommandError)
|
||||
case AUTH
|
||||
case SIZE
|
||||
case QUOTA
|
||||
case DIGEST
|
||||
case CRYPTO
|
||||
case NO_FILE
|
||||
case HAS_FILE
|
||||
case FILE_IO
|
||||
case INTERNAL
|
||||
}
|
||||
|
||||
public enum ProtocolCommandError: Decodable {
|
||||
case UNKNOWN
|
||||
case SYNTAX
|
||||
|
||||
@@ -2289,6 +2289,7 @@ public enum MsgContent {
|
||||
case text(String)
|
||||
case link(text: String, preview: LinkPreview)
|
||||
case image(text: String, image: String)
|
||||
case video(text: String, image: String, duration: Int)
|
||||
case voice(text: String, duration: Int)
|
||||
case file(String)
|
||||
// TODO include original JSON, possibly using https://github.com/zoul/generic-json-swift
|
||||
@@ -2299,6 +2300,7 @@ public enum MsgContent {
|
||||
case let .text(text): return text
|
||||
case let .link(text, _): return text
|
||||
case let .image(text, _): return text
|
||||
case let .video(text, _, _): return text
|
||||
case let .voice(text, _): return text
|
||||
case let .file(text): return text
|
||||
case let .unknown(_, text): return text
|
||||
@@ -2326,6 +2328,13 @@ public enum MsgContent {
|
||||
}
|
||||
}
|
||||
|
||||
public var isVideo: Bool {
|
||||
switch self {
|
||||
case .video: return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
var cmdString: String {
|
||||
"json \(encodeJSON(self))"
|
||||
}
|
||||
@@ -2356,6 +2365,11 @@ extension MsgContent: Decodable {
|
||||
let text = try container.decode(String.self, forKey: CodingKeys.text)
|
||||
let image = try container.decode(String.self, forKey: CodingKeys.image)
|
||||
self = .image(text: text, image: image)
|
||||
case "video":
|
||||
let text = try container.decode(String.self, forKey: CodingKeys.text)
|
||||
let image = try container.decode(String.self, forKey: CodingKeys.image)
|
||||
let duration = try container.decode(Int.self, forKey: CodingKeys.duration)
|
||||
self = .video(text: text, image: image, duration: duration)
|
||||
case "voice":
|
||||
let text = try container.decode(String.self, forKey: CodingKeys.text)
|
||||
let duration = try container.decode(Int.self, forKey: CodingKeys.duration)
|
||||
@@ -2388,6 +2402,11 @@ extension MsgContent: Encodable {
|
||||
try container.encode("image", forKey: .type)
|
||||
try container.encode(text, forKey: .text)
|
||||
try container.encode(image, forKey: .image)
|
||||
case let .video(text, image, duration):
|
||||
try container.encode("video", forKey: .type)
|
||||
try container.encode(text, forKey: .text)
|
||||
try container.encode(image, forKey: .image)
|
||||
try container.encode(duration, forKey: .duration)
|
||||
case let .voice(text, duration):
|
||||
try container.encode("voice", forKey: .type)
|
||||
try container.encode(text, forKey: .text)
|
||||
|
||||
@@ -16,6 +16,8 @@ public let MAX_IMAGE_SIZE: Int64 = 236700
|
||||
|
||||
public let MAX_IMAGE_SIZE_AUTO_RCV: Int64 = MAX_IMAGE_SIZE * 2
|
||||
|
||||
public let MAX_VIDEO_SIZE_AUTO_RCV: Int64 = 8000000
|
||||
|
||||
public let MAX_FILE_SIZE_XFTP: Int64 = 1_073_741_824
|
||||
|
||||
public let MAX_FILE_SIZE_SMP: Int64 = 8000000
|
||||
@@ -182,6 +184,14 @@ public func saveFile(_ data: Data, _ fileName: String) -> String? {
|
||||
}
|
||||
}
|
||||
|
||||
public func removeFile(_ url: URL) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: url.path)
|
||||
} catch {
|
||||
logger.error("FileUtils.removeFile error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
public func removeFile(_ fileName: String) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: getAppFilePath(fileName).path)
|
||||
|
||||
@@ -2125,7 +2125,7 @@
|
||||
"Send direct message" = "Отправить сообщение";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Send files via XFTP" = "Отправлять файлы через XFTP";
|
||||
"Send videos and files via XFTP" = "Отправлять видео и файлы через XFTP";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Send link previews" = "Отправлять картинки ссылок";
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 44f0dd39f3d1536c979b09e268dbdf681f9b0bb8
|
||||
tag: 215d2414b7a76e3d501017b24fb6b301bf022546
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
@@ -17,7 +17,7 @@ source-repository-package
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/kazu-yamamoto/http2.git
|
||||
tag: 159417b413a684a9b754e10e4a5db4376aa8c6b9
|
||||
tag: b5a1b7200cf5bc7044af34ba325284271f6dff25
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
@@ -20,6 +20,7 @@ dependencies:
|
||||
- base64-bytestring >= 1.0 && < 1.3
|
||||
- bytestring == 0.10.*
|
||||
- composition == 1.0.*
|
||||
- constraints >= 0.12 && < 0.14
|
||||
- containers == 0.6.*
|
||||
- cryptonite >= 0.27 && < 0.30
|
||||
- directory == 1.3.*
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."44f0dd39f3d1536c979b09e268dbdf681f9b0bb8" = "0dh0q2vng374kkq8s1lnnv658xfv6q7b9cgshiqxs9hxij64kxav";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."215d2414b7a76e3d501017b24fb6b301bf022546" = "1gzq4zlpndlanvg9ryhyz29fd60xic6dnprsijy6wc78cf6rw3vb";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/kazu-yamamoto/http2.git"."159417b413a684a9b754e10e4a5db4376aa8c6b9" = "17jjw582f4ls1m14abym1p0xlpjx1viqsfcpl4fkykv0sksbxdg7";
|
||||
"https://github.com/kazu-yamamoto/http2.git"."b5a1b7200cf5bc7044af34ba325284271f6dff25" = "0dqb50j57an64nf4qcf5vcz4xkd1vzvghvf8bk529c1k30r9nfzb";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."34309410eb2069b029b8fc1872deb1e0db123294" = "0kwkmhyfsn2lixdlgl15smgr1h5gjk7fky6abzh8rng2h5ymnffd";
|
||||
"https://github.com/simplex-chat/sqlcipher-simple.git"."5e154a2aeccc33ead6c243ec07195ab673137221" = "1d1gc5wax4vqg0801ajsmx1sbwvd9y7p7b8mmskvqsmpbwgbh0m0";
|
||||
"https://github.com/simplex-chat/aeson.git"."3eb66f9a68f103b5f1489382aad89f5712a64db7" = "0kilkx59fl6c3qy3kjczqvm8c3f4n3p0bdk9biyflf51ljnzp4yp";
|
||||
|
||||
@@ -89,6 +89,7 @@ library
|
||||
Simplex.Chat.Migrations.M20230318_file_description
|
||||
Simplex.Chat.Migrations.M20230321_agent_file_deleted
|
||||
Simplex.Chat.Migrations.M20230328_files_protocol
|
||||
Simplex.Chat.Migrations.M20230402_protocol_servers
|
||||
Simplex.Chat.Mobile
|
||||
Simplex.Chat.Mobile.WebRTC
|
||||
Simplex.Chat.Options
|
||||
@@ -117,6 +118,7 @@ library
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, direct-sqlcipher ==2.3.*
|
||||
@@ -164,6 +166,7 @@ executable simplex-bot
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, direct-sqlcipher ==2.3.*
|
||||
@@ -212,6 +215,7 @@ executable simplex-bot-advanced
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, direct-sqlcipher ==2.3.*
|
||||
@@ -261,6 +265,7 @@ executable simplex-broadcast-bot
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, direct-sqlcipher ==2.3.*
|
||||
@@ -310,6 +315,7 @@ executable simplex-chat
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, direct-sqlcipher ==2.3.*
|
||||
@@ -372,6 +378,7 @@ test-suite simplex-chat-test
|
||||
, base64-bytestring >=1.0 && <1.3
|
||||
, bytestring ==0.10.*
|
||||
, composition ==1.0.*
|
||||
, constraints >=0.12 && <0.14
|
||||
, containers ==0.6.*
|
||||
, cryptonite >=0.27 && <0.30
|
||||
, deepseq ==1.4.*
|
||||
|
||||
+88
-72
@@ -29,6 +29,7 @@ import qualified Data.ByteString.Base64 as B64
|
||||
import Data.ByteString.Char8 (ByteString)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Char (isSpace)
|
||||
import Data.Constraint (Dict (..))
|
||||
import Data.Either (fromRight, rights)
|
||||
import Data.Fixed (div')
|
||||
import Data.Functor (($>))
|
||||
@@ -72,7 +73,7 @@ import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (base64P)
|
||||
import Simplex.Messaging.Protocol (EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolType (..), ProtocolTypeI)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI, SProtocolType (..), UserProtocol, userProtocol)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport.Client (defaultSocksProxy)
|
||||
@@ -182,27 +183,32 @@ newChatController ChatDatabase {chatStore, agentStore} user cfg@ChatConfig {agen
|
||||
agentServers :: ChatConfig -> IO InitialAgentServers
|
||||
agentServers config@ChatConfig {defaultServers = defServers@DefaultAgentServers {ntf, netCfg}} = do
|
||||
users <- withTransaction chatStore getUsers
|
||||
smp' <- getUserServers users smp
|
||||
xftp' <- getUserServers users xftp
|
||||
smp' <- getUserServers users SPSMP
|
||||
xftp' <- getUserServers users SPXFTP
|
||||
pure InitialAgentServers {smp = smp', xftp = xftp', ntf, netCfg}
|
||||
where
|
||||
getUserServers :: forall p. ProtocolTypeI p => [User] -> (DefaultAgentServers -> NonEmpty (ProtoServerWithAuth p)) -> IO (Map UserId (NonEmpty (ProtoServerWithAuth p)))
|
||||
getUserServers users srvSel = case users of
|
||||
[] -> pure $ M.fromList [(1, srvSel defServers)]
|
||||
getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => [User] -> SProtocolType p -> IO (Map UserId (NonEmpty (ProtoServerWithAuth p)))
|
||||
getUserServers users protocol = case users of
|
||||
[] -> pure $ M.fromList [(1, cfgServers protocol defServers)]
|
||||
_ -> M.fromList <$> initialServers
|
||||
where
|
||||
initialServers :: IO [(UserId, NonEmpty (ProtoServerWithAuth p))]
|
||||
initialServers = mapM (\u -> (aUserId u,) <$> userServers u) users
|
||||
userServers :: User -> IO (NonEmpty (ProtoServerWithAuth p))
|
||||
userServers user' = activeAgentServers config srvSel <$> withTransaction chatStore (`getProtocolServers` user')
|
||||
userServers user' = activeAgentServers config protocol <$> withTransaction chatStore (`getProtocolServers` user')
|
||||
|
||||
activeAgentServers :: ChatConfig -> (DefaultAgentServers -> NonEmpty (ProtoServerWithAuth p)) -> [ServerCfg p] -> NonEmpty (ProtoServerWithAuth p)
|
||||
activeAgentServers ChatConfig {defaultServers} srvSel =
|
||||
fromMaybe (srvSel defaultServers)
|
||||
activeAgentServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [ServerCfg p] -> NonEmpty (ProtoServerWithAuth p)
|
||||
activeAgentServers ChatConfig {defaultServers} p =
|
||||
fromMaybe (cfgServers p defaultServers)
|
||||
. nonEmpty
|
||||
. map (\ServerCfg {server} -> server)
|
||||
. filter (\ServerCfg {enabled} -> enabled)
|
||||
|
||||
cfgServers :: UserProtocol p => SProtocolType p -> (DefaultAgentServers -> NonEmpty (ProtoServerWithAuth p))
|
||||
cfgServers = \case
|
||||
SPSMP -> smp
|
||||
SPXFTP -> xftp
|
||||
|
||||
startChatController :: forall m. ChatMonad' m => Bool -> Bool -> m (Async ())
|
||||
startChatController subConns enableExpireCIs = do
|
||||
asks smpAgent >>= resumeAgentClient
|
||||
@@ -294,33 +300,37 @@ processChatCommand = \case
|
||||
ShowActiveUser -> withUser' $ pure . CRActiveUser
|
||||
CreateActiveUser p@Profile {displayName} sameServers -> do
|
||||
u <- asks currentUser
|
||||
(smp, smpServers) <- chooseServers
|
||||
(smp, smpServers) <- chooseServers SPSMP
|
||||
(xftp, xftpServers) <- chooseServers SPXFTP
|
||||
auId <-
|
||||
withStore' getUsers >>= \case
|
||||
[] -> pure 1
|
||||
users -> do
|
||||
when (any (\User {localDisplayName = n} -> n == displayName) users) $
|
||||
throwChatError $ CEUserExists displayName
|
||||
withAgent (`createUser` smp)
|
||||
withAgent (\a -> createUser a smp xftp)
|
||||
user <- withStore $ \db -> createUserRecord db (AgentUserId auId) p True
|
||||
unless (null smpServers) $
|
||||
withStore $ \db -> overwriteSMPServers db user smpServers
|
||||
storeServers user smpServers
|
||||
storeServers user xftpServers
|
||||
setActive ActiveNone
|
||||
atomically . writeTVar u $ Just user
|
||||
pure $ CRActiveUser user
|
||||
where
|
||||
chooseServers :: m (NonEmpty SMPServerWithAuth, [ServerCfg 'PSMP])
|
||||
chooseServers
|
||||
chooseServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> m (NonEmpty (ProtoServerWithAuth p), [ServerCfg p])
|
||||
chooseServers protocol
|
||||
| sameServers =
|
||||
asks currentUser >>= readTVarIO >>= \case
|
||||
Nothing -> throwChatError CENoActiveUser
|
||||
Just user -> do
|
||||
smpServers <- withStore' (`getSMPServers` user)
|
||||
smpServers <- withStore' (`getProtocolServers` user)
|
||||
cfg <- asks config
|
||||
pure (activeAgentServers cfg smp smpServers, smpServers)
|
||||
pure (activeAgentServers cfg protocol smpServers, smpServers)
|
||||
| otherwise = do
|
||||
DefaultAgentServers {smp} <- asks $ defaultServers . config
|
||||
pure (smp, [])
|
||||
defServers <- asks $ defaultServers . config
|
||||
pure (cfgServers protocol defServers, [])
|
||||
storeServers user servers =
|
||||
unless (null servers) $
|
||||
withStore $ \db -> overwriteProtocolServers db user servers
|
||||
ListUsers -> CRUsersList <$> withStore' getUsersInfo
|
||||
APISetActiveUser userId' viewPwd_ -> withUser $ \user -> do
|
||||
user' <- privateGetUser userId'
|
||||
@@ -902,30 +912,29 @@ processChatCommand = \case
|
||||
pure user_ $>>= \user ->
|
||||
withStore (\db -> Just <$> getConnectionEntity db user agentConnId) `catchError` (\e -> toView (CRChatError (Just user) e) $> Nothing)
|
||||
pure CRNtfMessages {user_, connEntity, msgTs = msgTs', ntfMessages}
|
||||
APIGetUserSMPServers userId -> withUserId userId $ \user -> do
|
||||
ChatConfig {defaultServers = DefaultAgentServers {smp = defaultSMPServers}} <- asks config
|
||||
smpServers <- withStore' (`getSMPServers` user)
|
||||
let smpServers' = fromMaybe (L.map toServerCfg defaultSMPServers) $ nonEmpty smpServers
|
||||
pure $ CRUserSMPServers user smpServers' defaultSMPServers
|
||||
APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do
|
||||
ChatConfig {defaultServers} <- asks config
|
||||
servers <- withStore' (`getProtocolServers` user)
|
||||
let defServers = cfgServers p defaultServers
|
||||
servers' = fromMaybe (L.map toServerCfg defServers) $ nonEmpty servers
|
||||
pure $ CRUserProtoServers user $ AUPS $ UserProtoServers p servers' defServers
|
||||
where
|
||||
toServerCfg server = ServerCfg {server, preset = True, tested = Nothing, enabled = True}
|
||||
GetUserSMPServers -> withUser $ \User {userId} ->
|
||||
processChatCommand $ APIGetUserSMPServers userId
|
||||
APISetUserSMPServers userId (SMPServersConfig smpServers) -> withUserId userId $ \user -> withChatLock "setUserSMPServers" $ do
|
||||
withStore $ \db -> overwriteSMPServers db user smpServers
|
||||
cfg <- asks config
|
||||
withAgent $ \a -> setSMPServers a (aUserId user) $ activeAgentServers cfg smp smpServers
|
||||
ok user
|
||||
SetUserSMPServers smpServersConfig -> withUser $ \User {userId} ->
|
||||
processChatCommand $ APISetUserSMPServers userId smpServersConfig
|
||||
APIGetUserServers _ -> pure $ chatCmdError Nothing "TODO"
|
||||
GetUserServers -> pure $ chatCmdError Nothing "TODO"
|
||||
APISetUserServers _ _ -> pure $ chatCmdError Nothing "TODO"
|
||||
SetUserServers _ -> pure $ chatCmdError Nothing "TODO"
|
||||
APITestSMPServer userId smpServer -> withUserId userId $ \user ->
|
||||
CRSmpTestResult user <$> withAgent (\a -> testSMPServerConnection a (aUserId user) smpServer)
|
||||
TestSMPServer smpServer -> withUser $ \User {userId} ->
|
||||
processChatCommand $ APITestSMPServer userId smpServer
|
||||
GetUserProtoServers aProtocol -> withUser $ \User {userId} ->
|
||||
processChatCommand $ APIGetUserProtoServers userId aProtocol
|
||||
APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) -> withUserId userId $ \user -> withServerProtocol p $
|
||||
withChatLock "setUserSMPServers" $ do
|
||||
withStore $ \db -> overwriteProtocolServers db user servers
|
||||
cfg <- asks config
|
||||
withAgent $ \a -> setProtocolServers a (aUserId user) $ activeAgentServers cfg p servers
|
||||
ok user
|
||||
SetUserProtoServers serversConfig -> withUser $ \User {userId} ->
|
||||
processChatCommand $ APISetUserProtoServers userId serversConfig
|
||||
APITestProtoServer userId srv@(AProtoServerWithAuth p server) -> withUserId userId $ \user ->
|
||||
withServerProtocol p $
|
||||
CRServerTestResult user srv <$> withAgent (\a -> testProtocolServer a (aUserId user) server)
|
||||
TestProtoServer srv -> withUser $ \User {userId} ->
|
||||
processChatCommand $ APITestProtoServer userId srv
|
||||
APISetChatItemTTL userId newTTL_ -> withUser' $ \user -> do
|
||||
checkSameUser userId user
|
||||
checkStoreNotChanged $
|
||||
@@ -1061,8 +1070,9 @@ processChatCommand = \case
|
||||
pure $ CRInvitation user cReq
|
||||
AddContact -> withUser $ \User {userId} ->
|
||||
processChatCommand $ APIAddContact userId
|
||||
APIConnect userId (Just (ACR SCMInvitation cReq)) -> withUserId userId $ \user -> withChatLock "connect" . procCmd $ do
|
||||
APIConnect userId (Just (ACR SCMInvitation cReq)) mc_ -> withUserId userId $ \user -> withChatLock "connect" . procCmd $ do
|
||||
-- [incognito] generate profile to send
|
||||
when (isJust mc_) $ throwChatError CEConnReqMessageProhibited
|
||||
incognito <- readTVarIO =<< asks incognitoMode
|
||||
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
|
||||
let profileToSend = userProfileToSend user incognitoProfile Nothing
|
||||
@@ -1070,13 +1080,11 @@ processChatCommand = \case
|
||||
conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined $ incognitoProfile $> profileToSend
|
||||
toView $ CRNewContactConnection user conn
|
||||
pure $ CRSentConfirmation user
|
||||
APIConnect userId (Just (ACR SCMContact cReq)) -> withUserId userId (`connectViaContact` cReq)
|
||||
APIConnect _ Nothing -> throwChatError CEInvalidConnReq
|
||||
Connect cReqUri -> withUser $ \User {userId} ->
|
||||
processChatCommand $ APIConnect userId cReqUri
|
||||
ConnectSimplex -> withUser $ \user ->
|
||||
-- [incognito] generate profile to send
|
||||
connectViaContact user adminContactReq
|
||||
APIConnect userId (Just (ACR SCMContact cReq)) mc_ -> withUserId userId $ \user -> connectViaContact user cReq mc_
|
||||
APIConnect _ Nothing _ -> throwChatError CEInvalidConnReq
|
||||
Connect cReqUri msg_ -> withUser $ \User {userId} ->
|
||||
processChatCommand $ APIConnect userId cReqUri $ MCText <$> msg_
|
||||
ConnectSimplex msg_ -> withUser $ \user -> connectViaContact user adminContactReq $ MCText <$> msg_
|
||||
DeleteContact cName -> withContactName cName $ APIDeleteChat . ChatRef CTDirect
|
||||
ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect
|
||||
APIListContacts userId -> withUserId userId $ \user ->
|
||||
@@ -1527,12 +1535,14 @@ processChatCommand = \case
|
||||
CTDirect -> withStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd msg
|
||||
CTGroup -> withStore $ \db -> getGroupChatItemIdByText db user cId (Just localDisplayName) msg
|
||||
_ -> throwChatError $ CECommandError "not supported"
|
||||
connectViaContact :: User -> ConnectionRequestUri 'CMContact -> m ChatResponse
|
||||
connectViaContact user@User {userId} cReq@(CRContactUri ConnReqUriData {crClientData}) = withChatLock "connectViaContact" $ do
|
||||
connectViaContact :: User -> ConnectionRequestUri 'CMContact -> Maybe MsgContent -> m ChatResponse
|
||||
connectViaContact user@User {userId} cReq@(CRContactUri ConnReqUriData {crClientData}) mc_ = withChatLock "connectViaContact" $ do
|
||||
let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
|
||||
withStore' (\db -> getConnReqContactXContactId db user cReqHash) >>= \case
|
||||
(Just contact, _) -> pure $ CRContactAlreadyExists user contact
|
||||
(_, xContactId_) -> procCmd $ do
|
||||
let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
|
||||
when (isJust groupLinkId && isJust mc_) $ throwChatError CEConnReqMessageProhibited
|
||||
let randomXContactId = XContactId <$> drgRandomBytes 16
|
||||
xContactId <- maybe randomXContactId pure xContactId_
|
||||
-- [incognito] generate profile to send
|
||||
@@ -1543,8 +1553,7 @@ processChatCommand = \case
|
||||
incognito <- readTVarIO =<< asks incognitoMode
|
||||
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
|
||||
let profileToSend = userProfileToSend user incognitoProfile Nothing
|
||||
connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq $ directMessage (XContact profileToSend $ Just xContactId)
|
||||
let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
|
||||
connId <- withAgent $ \a -> joinConnection a (aUserId user) True cReq $ directMessage (XContact profileToSend (Just xContactId) mc_)
|
||||
conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId
|
||||
toView $ CRNewContactConnection user conn
|
||||
pure $ CRSentInvitation user incognitoProfile
|
||||
@@ -1661,6 +1670,10 @@ processChatCommand = \case
|
||||
atomically $ TM.delete ctId calls
|
||||
ok user
|
||||
| otherwise -> throwChatError $ CECallContact contactId
|
||||
withServerProtocol :: ProtocolTypeI p => SProtocolType p -> (UserProtocol p => m a) -> m a
|
||||
withServerProtocol p action = case userProtocol p of
|
||||
Just Dict -> action
|
||||
_ -> throwChatError $ CEServerProtocol $ AProtocolType p
|
||||
forwardFile :: ChatName -> FileTransferId -> (ChatName -> FilePath -> ChatCommand) -> m ChatResponse
|
||||
forwardFile chatName fileId sendCommand = withUser $ \user -> do
|
||||
withStore (\db -> getFileTransfer db user fileId) >>= \case
|
||||
@@ -2908,8 +2921,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
REQ invId _ connInfo -> do
|
||||
ChatMessage {chatMsgEvent} <- parseChatMessage connInfo
|
||||
case chatMsgEvent of
|
||||
XContact p xContactId_ -> profileContactRequest invId p xContactId_
|
||||
XInfo p -> profileContactRequest invId p Nothing
|
||||
XContact p xContactId_ mc_ -> profileContactRequest invId p xContactId_ mc_
|
||||
XInfo p -> profileContactRequest invId p Nothing Nothing
|
||||
-- TODO show/log error, other events in contact request
|
||||
_ -> pure ()
|
||||
MERR _ err -> do
|
||||
@@ -2921,19 +2934,21 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do
|
||||
-- TODO add debugging output
|
||||
_ -> pure ()
|
||||
where
|
||||
profileContactRequest :: InvitationId -> Profile -> Maybe XContactId -> m ()
|
||||
profileContactRequest invId p xContactId_ = do
|
||||
profileContactRequest :: InvitationId -> Profile -> Maybe XContactId -> Maybe MsgContent -> m ()
|
||||
profileContactRequest invId p xContactId_ _mc_ = do
|
||||
withStore (\db -> createOrUpdateContactRequest db user userContactLinkId invId p xContactId_) >>= \case
|
||||
CORContact contact -> toView $ CRContactRequestAlreadyAccepted user contact
|
||||
CORRequest cReq@UserContactRequest {localDisplayName} -> do
|
||||
withStore' (\db -> getUserContactLinkById db userId userContactLinkId) >>= \case
|
||||
Just (UserContactLink {autoAccept}, groupId_, _) ->
|
||||
-- TODO add chat item for contact request
|
||||
case autoAccept of
|
||||
Just AutoAccept {acceptIncognito} -> case groupId_ of
|
||||
Nothing -> do
|
||||
-- [incognito] generate profile to send, create connection with incognito profile
|
||||
incognitoProfile <- if acceptIncognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing
|
||||
ct <- acceptContactRequestAsync user cReq incognitoProfile
|
||||
-- TODO move chat item to contact
|
||||
toView $ CRAcceptingContactRequest user ct
|
||||
Just groupId -> do
|
||||
gInfo@GroupInfo {membership = membership@GroupMember {memberProfile}} <- withStore $ \db -> getGroupInfo db user groupId
|
||||
@@ -4457,17 +4472,17 @@ chatCommandP =
|
||||
"/_remove #" *> (APIRemoveMember <$> A.decimal <* A.space <*> A.decimal),
|
||||
"/_leave #" *> (APILeaveGroup <$> A.decimal),
|
||||
"/_members #" *> (APIListMembers <$> A.decimal),
|
||||
-- /smp_servers is deprecated, use /smp and /_smp
|
||||
"/smp_servers default" $> SetUserSMPServers (SMPServersConfig []),
|
||||
"/smp_servers " *> (SetUserSMPServers . SMPServersConfig . map toServerCfg <$> smpServersP),
|
||||
"/smp_servers" $> GetUserSMPServers,
|
||||
"/smp default" $> SetUserSMPServers (SMPServersConfig []),
|
||||
"/_smp test " *> (APITestSMPServer <$> A.decimal <* A.space <*> strP),
|
||||
"/smp test " *> (TestSMPServer <$> strP),
|
||||
"/_smp " *> (APISetUserSMPServers <$> A.decimal <* A.space <*> jsonP),
|
||||
"/smp " *> (SetUserSMPServers . SMPServersConfig . map toServerCfg <$> smpServersP),
|
||||
"/_smp " *> (APIGetUserSMPServers <$> A.decimal),
|
||||
"/smp" $> GetUserSMPServers,
|
||||
"/_server test " *> (APITestProtoServer <$> A.decimal <* A.space <*> strP),
|
||||
"/smp test " *> (TestProtoServer . AProtoServerWithAuth SPSMP <$> strP),
|
||||
"/xftp test " *> (TestProtoServer . AProtoServerWithAuth SPXFTP <$> strP),
|
||||
"/_servers " *> (APISetUserProtoServers <$> A.decimal <* A.space <*> srvCfgP),
|
||||
"/smp " *> (SetUserProtoServers . APSC SPSMP . ProtoServersConfig . map toServerCfg <$> protocolServersP),
|
||||
"/smp default" $> SetUserProtoServers (APSC SPSMP $ ProtoServersConfig []),
|
||||
"/xftp " *> (SetUserProtoServers . APSC SPXFTP . ProtoServersConfig . map toServerCfg <$> protocolServersP),
|
||||
"/xftp default" $> SetUserProtoServers (APSC SPXFTP $ ProtoServersConfig []),
|
||||
"/_servers " *> (APIGetUserProtoServers <$> A.decimal <* A.space <*> strP),
|
||||
"/smp" $> GetUserProtoServers (AProtocolType SPSMP),
|
||||
"/xftp" $> GetUserProtoServers (AProtocolType SPXFTP),
|
||||
"/_ttl " *> (APISetChatItemTTL <$> A.decimal <* A.space <*> ciTTLDecimal),
|
||||
"/ttl " *> (SetChatItemTTL <$> ciTTL),
|
||||
"/_ttl " *> (APIGetChatItemTTL <$> A.decimal),
|
||||
@@ -4533,9 +4548,9 @@ chatCommandP =
|
||||
(">#" <|> "> #") *> (SendGroupMessageQuote <$> displayName <* A.space <* char_ '@' <*> (Just <$> displayName) <* A.space <*> quotedMsg <*> msgTextP),
|
||||
"/_contacts " *> (APIListContacts <$> A.decimal),
|
||||
"/contacts" $> ListContacts,
|
||||
"/_connect " *> (APIConnect <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeByteString $> Nothing)),
|
||||
"/_connect " *> (APIConnect <$> A.decimal <* A.space <*> ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing) <*> optional (A.space *> msgContentP)),
|
||||
"/_connect " *> (APIAddContact <$> A.decimal),
|
||||
("/connect " <|> "/c ") *> (Connect <$> ((Just <$> strP) <|> A.takeByteString $> Nothing)),
|
||||
("/connect " <|> "/c ") *> (Connect <$> ((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing) <*> optional (A.space *> msgTextP)),
|
||||
("/connect" <|> "/c") $> AddContact,
|
||||
SendMessage <$> chatNameP <* A.space <*> msgTextP,
|
||||
"/live " *> (SendLiveMessage <$> chatNameP <*> (A.space *> msgTextP <|> pure "")),
|
||||
@@ -4559,7 +4574,7 @@ chatCommandP =
|
||||
("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)),
|
||||
("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal),
|
||||
("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal),
|
||||
"/simplex" $> ConnectSimplex,
|
||||
"/simplex" *> (ConnectSimplex <$> optional (A.space *> msgTextP)),
|
||||
"/_address " *> (APICreateMyAddress <$> A.decimal),
|
||||
("/address" <|> "/ad") $> CreateMyAddress,
|
||||
"/_delete_address " *> (APIDeleteMyAddress <$> A.decimal),
|
||||
@@ -4686,6 +4701,7 @@ chatCommandP =
|
||||
onOffP
|
||||
(Just <$> (AutoAccept <$> (" incognito=" *> onOffP <|> pure False) <*> optional (A.space *> msgContentP)))
|
||||
(pure Nothing)
|
||||
srvCfgP = strP >>= \case AProtocolType p -> APSC p <$> (A.space *> jsonP)
|
||||
toServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = True}
|
||||
char_ = optional . A.char
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE StandaloneDeriving #-}
|
||||
{-# LANGUAGE StrictData #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
@@ -45,7 +47,7 @@ import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Store (AutoAccept, StoreError, UserContactLink)
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Messaging.Agent (AgentClient)
|
||||
import Simplex.Messaging.Agent.Client (AgentLocks, SMPTestFailure)
|
||||
import Simplex.Messaging.Agent.Client (AgentLocks, ProtocolTestFailure)
|
||||
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig)
|
||||
import Simplex.Messaging.Agent.Lock
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
@@ -54,7 +56,7 @@ import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus)
|
||||
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (AProtocolType, CorrId, MsgFlags, NtfServer, ProtocolType (..), QueueId, XFTPServerWithAuth)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType, CorrId, MsgFlags, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SProtocolType, UserProtocol, XFTPServerWithAuth)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import Simplex.Messaging.Transport (simplexMQVersion)
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
@@ -258,16 +260,12 @@ data ChatCommand
|
||||
| APIGroupLinkMemberRole GroupId GroupMemberRole
|
||||
| APIDeleteGroupLink GroupId
|
||||
| APIGetGroupLink GroupId
|
||||
| APIGetUserSMPServers UserId
|
||||
| GetUserSMPServers
|
||||
| APISetUserSMPServers UserId SMPServersConfig
|
||||
| SetUserSMPServers SMPServersConfig
|
||||
| APIGetUserServers UserId
|
||||
| GetUserServers
|
||||
| APISetUserServers UserId ServersConfig
|
||||
| SetUserServers ServersConfig
|
||||
| APITestSMPServer UserId SMPServerWithAuth
|
||||
| TestSMPServer SMPServerWithAuth
|
||||
| APIGetUserProtoServers UserId AProtocolType
|
||||
| GetUserProtoServers AProtocolType
|
||||
| APISetUserProtoServers UserId AProtoServersConfig
|
||||
| SetUserProtoServers AProtoServersConfig
|
||||
| APITestProtoServer UserId AProtoServerWithAuth
|
||||
| TestProtoServer AProtoServerWithAuth
|
||||
| APISetChatItemTTL UserId (Maybe Int64)
|
||||
| SetChatItemTTL (Maybe Int64)
|
||||
| APIGetChatItemTTL UserId
|
||||
@@ -300,9 +298,9 @@ data ChatCommand
|
||||
| Welcome
|
||||
| APIAddContact UserId
|
||||
| AddContact
|
||||
| APIConnect UserId (Maybe AConnectionRequestUri)
|
||||
| Connect (Maybe AConnectionRequestUri)
|
||||
| ConnectSimplex -- UserId (not used in UI)
|
||||
| APIConnect UserId (Maybe AConnectionRequestUri) (Maybe MsgContent)
|
||||
| Connect (Maybe AConnectionRequestUri) (Maybe Text)
|
||||
| ConnectSimplex (Maybe Text)
|
||||
| DeleteContact ContactName
|
||||
| ClearContact ContactName
|
||||
| APIListContacts UserId
|
||||
@@ -386,8 +384,8 @@ data ChatResponse
|
||||
| CRChatItems {user :: User, chatItems :: [AChatItem]}
|
||||
| CRChatItemId User (Maybe ChatItemId)
|
||||
| CRApiParsedMarkdown {formattedText :: Maybe MarkdownList}
|
||||
| CRUserSMPServers {user :: User, smpServers :: NonEmpty (ServerCfg 'PSMP), presetSMPServers :: NonEmpty SMPServerWithAuth}
|
||||
| CRSmpTestResult {user :: User, smpTestFailure :: Maybe SMPTestFailure}
|
||||
| CRUserProtoServers {user :: User, servers :: AUserProtoServers}
|
||||
| CRServerTestResult {user :: User, testServer :: AProtoServerWithAuth, testFailure :: Maybe ProtocolTestFailure}
|
||||
| CRChatItemTTL {user :: User, chatItemTTL :: Maybe Int64}
|
||||
| CRNetworkConfig {networkConfig :: NetworkConfig}
|
||||
| CRContactInfo {user :: User, contact :: Contact, connectionStats :: ConnectionStats, customUserProfile :: Maybe Profile}
|
||||
@@ -566,11 +564,31 @@ instance ToJSON AgentQueueId where
|
||||
toJSON = strToJSON
|
||||
toEncoding = strToJEncoding
|
||||
|
||||
data SMPServersConfig = SMPServersConfig {smpServers :: [ServerCfg 'PSMP]}
|
||||
data ProtoServersConfig p = ProtoServersConfig {servers :: [ServerCfg p]}
|
||||
deriving (Show, Generic, FromJSON)
|
||||
|
||||
data ServersConfig = ServersConfig {servers :: [AServerCfg]}
|
||||
deriving (Show, Generic, FromJSON)
|
||||
data AProtoServersConfig = forall p. ProtocolTypeI p => APSC (SProtocolType p) (ProtoServersConfig p)
|
||||
|
||||
deriving instance Show AProtoServersConfig
|
||||
|
||||
data UserProtoServers p = UserProtoServers
|
||||
{ serverProtocol :: SProtocolType p,
|
||||
protoServers :: NonEmpty (ServerCfg p),
|
||||
presetServers :: NonEmpty (ProtoServerWithAuth p)
|
||||
}
|
||||
deriving (Show, Generic)
|
||||
|
||||
instance ProtocolTypeI p => ToJSON (UserProtoServers p) where
|
||||
toJSON = J.genericToJSON J.defaultOptions
|
||||
toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
data AUserProtoServers = forall p. (ProtocolTypeI p, UserProtocol p) => AUPS (UserProtoServers p)
|
||||
|
||||
instance ToJSON AUserProtoServers where
|
||||
toJSON (AUPS s) = J.genericToJSON J.defaultOptions s
|
||||
toEncoding (AUPS s) = J.genericToEncoding J.defaultOptions s
|
||||
|
||||
deriving instance Show AUserProtoServers
|
||||
|
||||
data ArchiveConfig = ArchiveConfig {archivePath :: FilePath, disableCompression :: Maybe Bool, parentTempDirectory :: Maybe FilePath}
|
||||
deriving (Show, Generic, FromJSON)
|
||||
@@ -679,7 +697,8 @@ data ParsedServerAddress = ParsedServerAddress
|
||||
instance ToJSON ParsedServerAddress where toEncoding = J.genericToEncoding J.defaultOptions
|
||||
|
||||
data ServerAddress = ServerAddress
|
||||
{ hostnames :: NonEmpty String,
|
||||
{ serverProtocol :: AProtocolType,
|
||||
hostnames :: NonEmpty String,
|
||||
port :: String,
|
||||
keyHash :: String,
|
||||
basicAuth :: String
|
||||
@@ -750,6 +769,7 @@ data ChatErrorType
|
||||
| CEChatStoreChanged
|
||||
| CEInvalidConnReq
|
||||
| CEInvalidChatMessage {message :: String}
|
||||
| CEConnReqMessageProhibited
|
||||
| CEContactNotReady {contact :: Contact}
|
||||
| CEContactDisabled {contact :: Contact}
|
||||
| CEConnectionDisabled {connection :: Connection}
|
||||
@@ -794,6 +814,7 @@ data ChatErrorType
|
||||
| CEAgentVersion
|
||||
| CEAgentNoSubResult {agentConnId :: AgentConnId}
|
||||
| CECommandError {message :: String}
|
||||
| CEServerProtocol {serverProtocol :: AProtocolType}
|
||||
| CEAgentCommandError {message :: String}
|
||||
| CEInvalidFileDescription {message :: String}
|
||||
| CEInternalError {message :: String}
|
||||
|
||||
@@ -35,5 +35,5 @@ SELECT
|
||||
DROP TABLE smp_servers;
|
||||
ALTER TABLE new_smp_servers RENAME TO smp_servers;
|
||||
|
||||
CREATE INDEX idx_smp_servers_user_id ON smp_servers(user_id);
|
||||
CREATE INDEX idx_smp_servers_user_id ON "smp_servers"(user_id);
|
||||
|]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Simplex.Chat.Migrations.M20230402_protocol_servers where
|
||||
|
||||
import Database.SQLite.Simple (Query)
|
||||
import Database.SQLite.Simple.QQ (sql)
|
||||
|
||||
m20230402_protocol_servers :: Query
|
||||
m20230402_protocol_servers =
|
||||
[sql|
|
||||
ALTER TABLE smp_servers RENAME TO protocol_servers;
|
||||
ALTER TABLE protocol_servers ADD COLUMN protocol TEXT NOT NULL DEFAULT 'smp';
|
||||
|]
|
||||
|
||||
down_m20230402_protocol_servers :: Query
|
||||
down_m20230402_protocol_servers =
|
||||
[sql|
|
||||
ALTER TABLE protocol_servers DROP COLUMN protocol;
|
||||
ALTER TABLE protocol_servers RENAME TO smp_servers;
|
||||
|]
|
||||
@@ -547,7 +547,7 @@ CREATE INDEX idx_snd_file_chunks_file_id_connection_id ON snd_file_chunks(
|
||||
CREATE INDEX idx_snd_files_group_member_id ON snd_files(group_member_id);
|
||||
CREATE INDEX idx_snd_files_connection_id ON snd_files(connection_id);
|
||||
CREATE INDEX idx_snd_files_file_id ON snd_files(file_id);
|
||||
CREATE TABLE IF NOT EXISTS "smp_servers"(
|
||||
CREATE TABLE IF NOT EXISTS "protocol_servers"(
|
||||
smp_server_id INTEGER PRIMARY KEY,
|
||||
host TEXT NOT NULL,
|
||||
port TEXT NOT NULL,
|
||||
@@ -559,9 +559,10 @@ CREATE TABLE IF NOT EXISTS "smp_servers"(
|
||||
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
|
||||
created_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT(datetime('now')),
|
||||
protocol TEXT NOT NULL DEFAULT 'smp',
|
||||
UNIQUE(user_id, host, port)
|
||||
);
|
||||
CREATE INDEX idx_smp_servers_user_id ON smp_servers(user_id);
|
||||
CREATE INDEX idx_smp_servers_user_id ON "protocol_servers"(user_id);
|
||||
CREATE INDEX idx_chat_items_item_deleted_by_group_member_id ON chat_items(
|
||||
item_deleted_by_group_member_id
|
||||
);
|
||||
|
||||
@@ -44,7 +44,7 @@ import Simplex.Messaging.Client (defaultNetworkConfig)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (BasicAuth (..), CorrId (..), ProtoServerWithAuth (..), ProtocolServer (..), SMPServerWithAuth)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), BasicAuth (..), CorrId (..), ProtoServerWithAuth (..), ProtocolServer (..))
|
||||
import Simplex.Messaging.Util (catchAll, liftEitherWith, safeDecodeUtf8)
|
||||
import System.Timeout (timeout)
|
||||
|
||||
@@ -194,11 +194,11 @@ chatParseMarkdown = LB.unpack . J.encode . ParsedMarkdown . parseMaybeMarkdownLi
|
||||
chatParseServer :: String -> JSONString
|
||||
chatParseServer = LB.unpack . J.encode . toServerAddress . strDecode . B.pack
|
||||
where
|
||||
toServerAddress :: Either String SMPServerWithAuth -> ParsedServerAddress
|
||||
toServerAddress :: Either String AProtoServerWithAuth -> ParsedServerAddress
|
||||
toServerAddress = \case
|
||||
Right (ProtoServerWithAuth ProtocolServer {host, port, keyHash = C.KeyHash kh} auth) ->
|
||||
Right (AProtoServerWithAuth protocol (ProtoServerWithAuth ProtocolServer {host, port, keyHash = C.KeyHash kh} auth)) ->
|
||||
let basicAuth = maybe "" (\(BasicAuth a) -> enc a) auth
|
||||
in ParsedServerAddress (Just ServerAddress {hostnames = L.map enc host, port, keyHash = enc kh, basicAuth}) ""
|
||||
in ParsedServerAddress (Just ServerAddress {serverProtocol = AProtocolType protocol, hostnames = L.map enc host, port, keyHash = enc kh, basicAuth}) ""
|
||||
Left e -> ParsedServerAddress Nothing e
|
||||
enc :: StrEncoding a => a -> String
|
||||
enc = B.unpack . strEncode
|
||||
|
||||
@@ -11,7 +11,7 @@ module Simplex.Chat.Options
|
||||
chatOptsP,
|
||||
coreChatOptsP,
|
||||
getChatOpts,
|
||||
smpServersP,
|
||||
protocolServersP,
|
||||
fullNetworkConfig,
|
||||
)
|
||||
where
|
||||
@@ -25,7 +25,7 @@ import Simplex.Chat.Controller (ChatLogLevel (..), updateStr, versionNumber, ver
|
||||
import Simplex.Messaging.Client (NetworkConfig (..), defaultNetworkConfig)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Protocol (SMPServerWithAuth, XFTPServerWithAuth)
|
||||
import Simplex.Messaging.Protocol (ProtocolTypeI, ProtoServerWithAuth, SMPServerWithAuth, XFTPServerWithAuth)
|
||||
import Simplex.Messaging.Transport.Client (SocksProxy, defaultSocksProxy)
|
||||
import System.FilePath (combine)
|
||||
|
||||
@@ -82,7 +82,7 @@ coreChatOptsP appDir defaultDbFileName = do
|
||||
)
|
||||
smpServers <-
|
||||
option
|
||||
parseSMPServers
|
||||
parseProtocolServers
|
||||
( long "server"
|
||||
<> short 's'
|
||||
<> metavar "SERVER"
|
||||
@@ -91,7 +91,7 @@ coreChatOptsP appDir defaultDbFileName = do
|
||||
)
|
||||
xftpServers <-
|
||||
option
|
||||
parseXFTPServers
|
||||
parseProtocolServers
|
||||
( long "xftp-server"
|
||||
<> metavar "SERVER"
|
||||
<> help "Semicolon-separated list of XFTP server(s) to use (each server can have more than one hostname)"
|
||||
@@ -243,11 +243,8 @@ fullNetworkConfig socksProxy tcpTimeout logTLSErrors =
|
||||
let tcpConnectTimeout = (tcpTimeout * 3) `div` 2
|
||||
in defaultNetworkConfig {socksProxy, tcpTimeout, tcpConnectTimeout, logTLSErrors}
|
||||
|
||||
parseSMPServers :: ReadM [SMPServerWithAuth]
|
||||
parseSMPServers = eitherReader $ parseAll smpServersP . B.pack
|
||||
|
||||
parseXFTPServers :: ReadM [XFTPServerWithAuth]
|
||||
parseXFTPServers = eitherReader $ parseAll xftpServersP . B.pack
|
||||
parseProtocolServers :: ProtocolTypeI p => ReadM [ProtoServerWithAuth p]
|
||||
parseProtocolServers = eitherReader $ parseAll protocolServersP . B.pack
|
||||
|
||||
parseSocksProxy :: ReadM (Maybe SocksProxy)
|
||||
parseSocksProxy = eitherReader $ parseAll strP . B.pack
|
||||
@@ -258,11 +255,8 @@ parseServerPort = eitherReader $ parseAll serverPortP . B.pack
|
||||
serverPortP :: A.Parser (Maybe String)
|
||||
serverPortP = Just . B.unpack <$> A.takeWhile A.isDigit
|
||||
|
||||
smpServersP :: A.Parser [SMPServerWithAuth]
|
||||
smpServersP = strP `A.sepBy1` A.char ';'
|
||||
|
||||
xftpServersP :: A.Parser [XFTPServerWithAuth]
|
||||
xftpServersP = strP `A.sepBy1` A.char ';'
|
||||
protocolServersP :: ProtocolTypeI p => A.Parser [ProtoServerWithAuth p]
|
||||
protocolServersP = strP `A.sepBy1` A.char ';'
|
||||
|
||||
parseLogLevel :: ReadM ChatLogLevel
|
||||
parseLogLevel = eitherReader $ \case
|
||||
|
||||
@@ -189,7 +189,7 @@ data ChatMsgEvent (e :: MsgEncoding) where
|
||||
XFileAcptInv :: SharedMsgId -> Maybe ConnReqInvitation -> String -> ChatMsgEvent 'Json
|
||||
XFileCancel :: SharedMsgId -> ChatMsgEvent 'Json
|
||||
XInfo :: Profile -> ChatMsgEvent 'Json
|
||||
XContact :: Profile -> Maybe XContactId -> ChatMsgEvent 'Json
|
||||
XContact :: Profile -> Maybe XContactId -> Maybe MsgContent -> ChatMsgEvent 'Json
|
||||
XGrpInv :: GroupInvitation -> ChatMsgEvent 'Json
|
||||
XGrpAcpt :: MemberId -> ChatMsgEvent 'Json
|
||||
XGrpMemNew :: MemberInfo -> ChatMsgEvent 'Json
|
||||
@@ -605,7 +605,7 @@ toCMEventTag msg = case msg of
|
||||
XFileAcptInv {} -> XFileAcptInv_
|
||||
XFileCancel _ -> XFileCancel_
|
||||
XInfo _ -> XInfo_
|
||||
XContact _ _ -> XContact_
|
||||
XContact {} -> XContact_
|
||||
XGrpInv _ -> XGrpInv_
|
||||
XGrpAcpt _ -> XGrpAcpt_
|
||||
XGrpMemNew _ -> XGrpMemNew_
|
||||
@@ -692,7 +692,7 @@ appJsonToCM AppMessageJson {msgId, event, params} = do
|
||||
XFileAcptInv_ -> XFileAcptInv <$> p "msgId" <*> opt "fileConnReq" <*> p "fileName"
|
||||
XFileCancel_ -> XFileCancel <$> p "msgId"
|
||||
XInfo_ -> XInfo <$> p "profile"
|
||||
XContact_ -> XContact <$> p "profile" <*> opt "contactReqId"
|
||||
XContact_ -> XContact <$> p "profile" <*> opt "contactReqId" <*> opt "content"
|
||||
XGrpInv_ -> XGrpInv <$> p "groupInvitation"
|
||||
XGrpAcpt_ -> XGrpAcpt <$> p "memberId"
|
||||
XGrpMemNew_ -> XGrpMemNew <$> p "memberInfo"
|
||||
@@ -747,7 +747,7 @@ chatToAppMessage ChatMessage {msgId, chatMsgEvent} = case encoding @e of
|
||||
XFileAcptInv sharedMsgId fileConnReq fileName -> o $ ("fileConnReq" .=? fileConnReq) ["msgId" .= sharedMsgId, "fileName" .= fileName]
|
||||
XFileCancel sharedMsgId -> o ["msgId" .= sharedMsgId]
|
||||
XInfo profile -> o ["profile" .= profile]
|
||||
XContact profile xContactId -> o $ ("contactReqId" .=? xContactId) ["profile" .= profile]
|
||||
XContact profile xContactId content -> o $ ("contactReqId" .=? xContactId) $ ("content" .=? content) ["profile" .= profile]
|
||||
XGrpInv groupInv -> o ["groupInvitation" .= groupInv]
|
||||
XGrpAcpt memId -> o ["memberId" .= memId]
|
||||
XGrpMemNew memInfo -> o ["memberInfo" .= memInfo]
|
||||
|
||||
+23
-29
@@ -246,8 +246,6 @@ module Simplex.Chat.Store
|
||||
updateGroupChatItemsRead,
|
||||
getGroupUnreadTimedItems,
|
||||
setGroupChatItemDeleteAt,
|
||||
getSMPServers,
|
||||
overwriteSMPServers,
|
||||
getProtocolServers,
|
||||
overwriteProtocolServers,
|
||||
createCall,
|
||||
@@ -297,7 +295,7 @@ import Data.Maybe (fromMaybe, isJust, isNothing, listToMaybe, mapMaybe)
|
||||
import Data.Ord (Down (..))
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Text.Encoding (decodeLatin1, encodeUtf8)
|
||||
import Data.Time (addUTCTime)
|
||||
import Data.Time.Clock (UTCTime (..), getCurrentTime)
|
||||
import Data.Time.LocalTime (TimeZone, getCurrentTimeZone)
|
||||
@@ -365,6 +363,7 @@ import Simplex.Chat.Migrations.M20230317_hidden_profiles
|
||||
import Simplex.Chat.Migrations.M20230318_file_description
|
||||
import Simplex.Chat.Migrations.M20230321_agent_file_deleted
|
||||
import Simplex.Chat.Migrations.M20230328_files_protocol
|
||||
import Simplex.Chat.Migrations.M20230402_protocol_servers
|
||||
import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Chat.Util (week)
|
||||
@@ -372,8 +371,9 @@ import Simplex.Messaging.Agent.Protocol (ACorrId, AgentMsgId, ConnId, Invitation
|
||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, MigrationError, SQLiteStore (..), createSQLiteStore, firstRow, firstRow', maybeFirstRow, withTransaction)
|
||||
import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
|
||||
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), pattern SMPServer)
|
||||
import Simplex.Messaging.Protocol (BasicAuth (..), ProtoServerWithAuth (..), ProtocolServer (..), ProtocolTypeI (..))
|
||||
import Simplex.Messaging.Transport.Client (TransportHost)
|
||||
import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8)
|
||||
import UnliftIO.STM
|
||||
@@ -435,7 +435,8 @@ schemaMigrations =
|
||||
("20230317_hidden_profiles", m20230317_hidden_profiles, Just down_m20230317_hidden_profiles),
|
||||
("20230318_file_description", m20230318_file_description, Just down_m20230318_file_description),
|
||||
("20230321_agent_file_deleted", m20230321_agent_file_deleted, Just down_m20230321_agent_file_deleted),
|
||||
("20230328_files_protocol", m20230328_files_protocol, Just down_m20230328_files_protocol)
|
||||
("20230328_files_protocol", m20230328_files_protocol, Just down_m20230328_files_protocol),
|
||||
("20230402_protocol_servers", m20230402_protocol_servers, Just down_m20230402_protocol_servers)
|
||||
]
|
||||
|
||||
-- | The list of migrations in ascending order by date
|
||||
@@ -4849,49 +4850,42 @@ toGroupChatItemList tz currentTs userContactId (((Just itemId, Just itemTs, Just
|
||||
either (const []) (: []) $ toGroupChatItem tz currentTs userContactId (((itemId, itemTs, itemContent, itemText, itemStatus, sharedMsgId, itemDeleted, itemEdited, createdAt, updatedAt) :. (timedTTL, timedDeleteAt, itemLive) :. fileRow) :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_)
|
||||
toGroupChatItemList _ _ _ _ = []
|
||||
|
||||
getSMPServers :: DB.Connection -> User -> IO [ServerCfg 'PSMP]
|
||||
getSMPServers db User {userId} =
|
||||
getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> IO [ServerCfg p]
|
||||
getProtocolServers db User {userId} =
|
||||
map toServerCfg
|
||||
<$> DB.query
|
||||
db
|
||||
[sql|
|
||||
SELECT host, port, key_hash, basic_auth, preset, tested, enabled
|
||||
FROM smp_servers
|
||||
WHERE user_id = ?;
|
||||
FROM protocol_servers
|
||||
WHERE user_id = ? AND protocol = ?;
|
||||
|]
|
||||
(Only userId)
|
||||
(userId, decodeLatin1 $ strEncode protocol)
|
||||
where
|
||||
toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Bool) -> ServerCfg 'PSMP
|
||||
protocol = protocolTypeI @p
|
||||
toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Bool) -> ServerCfg p
|
||||
toServerCfg (host, port, keyHash, auth_, preset, tested, enabled) =
|
||||
let server = ProtoServerWithAuth (SMPServer host port keyHash) (BasicAuth . encodeUtf8 <$> auth_)
|
||||
let server = ProtoServerWithAuth (ProtocolServer protocol host port keyHash) (BasicAuth . encodeUtf8 <$> auth_)
|
||||
in ServerCfg {server, preset, tested, enabled}
|
||||
|
||||
overwriteSMPServers :: DB.Connection -> User -> [ServerCfg 'PSMP] -> ExceptT StoreError IO ()
|
||||
overwriteSMPServers db User {userId} servers =
|
||||
overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO ()
|
||||
overwriteProtocolServers db User {userId} servers =
|
||||
checkConstraint SEUniqueID . ExceptT $ do
|
||||
currentTs <- getCurrentTime
|
||||
DB.execute db "DELETE FROM smp_servers WHERE user_id = ?" (Only userId)
|
||||
DB.execute db "DELETE FROM protocol_servers WHERE user_id = ? AND protocol = ? " (userId, protocol)
|
||||
forM_ servers $ \ServerCfg {server, preset, tested, enabled} -> do
|
||||
let ProtoServerWithAuth ProtocolServer {host, port, keyHash} auth_ = server
|
||||
DB.execute
|
||||
db
|
||||
[sql|
|
||||
INSERT INTO smp_servers
|
||||
(host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?)
|
||||
INSERT INTO protocol_servers
|
||||
(protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||||
|]
|
||||
(host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_, preset, tested, enabled, userId, currentTs, currentTs)
|
||||
((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_) :. (preset, tested, enabled, userId, currentTs, currentTs))
|
||||
pure $ Right ()
|
||||
|
||||
getProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> IO [ServerCfg p]
|
||||
getProtocolServers db user = case protocolTypeI @p of
|
||||
SPSMP -> getSMPServers db user
|
||||
_ -> pure [] -- TODO read from the new table of all servers (alternatively, we could migrate data)
|
||||
|
||||
overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO ()
|
||||
overwriteProtocolServers db user servers = case protocolTypeI @p of
|
||||
SPSMP -> overwriteSMPServers db user servers
|
||||
_ -> pure () -- TODO write the new table of all servers
|
||||
where
|
||||
protocol = decodeLatin1 $ strEncode $ protocolTypeI @p
|
||||
|
||||
createCall :: DB.Connection -> User -> Call -> UTCTime -> IO ()
|
||||
createCall db user@User {userId} Call {contactId, callId, chatItemId, callState} callTs = do
|
||||
|
||||
@@ -52,7 +52,7 @@ import Simplex.FileTransfer.Description (FileDigest)
|
||||
import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, SAEntity (..), UserId)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, enumJSON, fromTextField_, sumTypeJSON, taggedObjectJSON)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth, ProtoServerWithAuth, ProtocolTypeI)
|
||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI)
|
||||
import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>))
|
||||
|
||||
class IsContact a where
|
||||
@@ -2100,21 +2100,3 @@ instance ProtocolTypeI p => ToJSON (ServerCfg p) where
|
||||
|
||||
instance ProtocolTypeI p => FromJSON (ServerCfg p) where
|
||||
parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True}
|
||||
|
||||
data AServerCfg = AServerCfg
|
||||
{ server :: AProtoServerWithAuth,
|
||||
preset :: Bool,
|
||||
tested :: Maybe Bool,
|
||||
enabled :: Bool
|
||||
}
|
||||
|
||||
deriving instance Show AServerCfg
|
||||
|
||||
deriving instance Generic AServerCfg
|
||||
|
||||
instance ToJSON AServerCfg where
|
||||
toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True}
|
||||
toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True}
|
||||
|
||||
instance FromJSON AServerCfg where
|
||||
parseJSON = J.genericParseJSON J.defaultOptions {J.omitNothingFields = True}
|
||||
|
||||
+36
-21
@@ -18,10 +18,12 @@ import Data.Char (toUpper)
|
||||
import Data.Function (on)
|
||||
import Data.Int (Int64)
|
||||
import Data.List (groupBy, intercalate, intersperse, partition, sortOn)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import qualified Data.List.NonEmpty as L
|
||||
import Data.Maybe (fromMaybe, isJust, isNothing, mapMaybe)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (decodeLatin1)
|
||||
import Data.Time.Clock (DiffTime, UTCTime)
|
||||
import Data.Time.Format (defaultTimeLocale, formatTime)
|
||||
import Data.Time.LocalTime (ZonedTime (..), localDay, localTimeOfDay, timeOfDayToTime, utcToZonedTime)
|
||||
@@ -38,14 +40,15 @@ import Simplex.Chat.Protocol
|
||||
import Simplex.Chat.Store (AutoAccept (..), StoreError (..), UserContactLink (..))
|
||||
import Simplex.Chat.Styled
|
||||
import Simplex.Chat.Types
|
||||
import Simplex.Messaging.Agent.Client (SMPTestFailure (..), SMPTestStep (..))
|
||||
import qualified Simplex.FileTransfer.Protocol as XFTP
|
||||
import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..))
|
||||
import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..))
|
||||
import Simplex.Messaging.Agent.Protocol
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Encoding
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON)
|
||||
import Simplex.Messaging.Protocol (AProtocolType, ProtocolServer (..), ProtocolTypeI)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtocolServer (..), ProtocolTypeI, SProtocolType (..))
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.Transport.Client (TransportHost (..))
|
||||
import Simplex.Messaging.Util (bshow, tshow)
|
||||
@@ -68,8 +71,8 @@ responseToView user_ ChatConfig {logLevel, testView} liveItems ts = \case
|
||||
CRChats chats -> viewChats ts chats
|
||||
CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [plain . bshow $ J.encode chat]
|
||||
CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft]
|
||||
CRUserSMPServers u smpServers _ -> ttyUser u $ viewSMPServers (L.toList smpServers) testView
|
||||
CRSmpTestResult u testFailure -> ttyUser u $ viewSMPTestResult testFailure
|
||||
CRUserProtoServers u userServers -> ttyUser u $ viewUserServers userServers testView
|
||||
CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure
|
||||
CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl
|
||||
CRNetworkConfig cfg -> viewNetworkConfig cfg
|
||||
CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile
|
||||
@@ -748,36 +751,46 @@ viewUserPrivacy User {userId} User {userId = userId', localDisplayName = n', sho
|
||||
]
|
||||
|
||||
-- TODO make more generic messages or split
|
||||
viewSMPServers :: ProtocolTypeI p => [ServerCfg p] -> Bool -> [StyledString]
|
||||
viewSMPServers servers testView =
|
||||
viewUserServers :: AUserProtoServers -> Bool -> [StyledString]
|
||||
viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers}) testView =
|
||||
if testView
|
||||
then [customServers]
|
||||
else
|
||||
[ customServers,
|
||||
"",
|
||||
"use " <> highlight' "/smp test <srv>" <> " to test SMP server connection",
|
||||
"use " <> highlight' "/smp set <srv1[,srv2,...]>" <> " to switch to custom SMP servers",
|
||||
"use " <> highlight' "/smp default" <> " to remove custom SMP servers and use default",
|
||||
"(chat option " <> highlight' "-s" <> " (" <> highlight' "--server" <> ") has precedence over saved SMP servers for chat session)"
|
||||
"use " <> highlight (srvCmd <> " test <srv>") <> " to test " <> pName <> " server connection",
|
||||
"use " <> highlight (srvCmd <> " set <srv1[,srv2,...]>") <> " to switch to custom " <> pName <> " servers",
|
||||
"use " <> highlight (srvCmd <> " default") <> " to remove custom " <> pName <> " servers and use default"
|
||||
]
|
||||
<> case p of
|
||||
SPSMP -> ["(chat option " <> highlight' "-s" <> " (" <> highlight' "--server" <> ") has precedence over saved SMP servers for chat session)"]
|
||||
SPXFTP -> ["(chat option " <> highlight' "-xftp-servers" <> " has precedence over saved XFTP servers for chat session)"]
|
||||
where
|
||||
srvCmd = "/" <> strEncode p
|
||||
pName = protocolName p
|
||||
customServers =
|
||||
if null servers
|
||||
if null protoServers
|
||||
then "no custom SMP servers saved"
|
||||
else viewServers servers
|
||||
else viewServers protoServers
|
||||
|
||||
viewSMPTestResult :: Maybe SMPTestFailure -> [StyledString]
|
||||
viewSMPTestResult = \case
|
||||
Just SMPTestFailure {testStep, testError} ->
|
||||
protocolName :: ProtocolTypeI p => SProtocolType p -> StyledString
|
||||
protocolName = plain . map toUpper . T.unpack . decodeLatin1 . strEncode
|
||||
|
||||
viewServerTestResult :: AProtoServerWithAuth -> Maybe ProtocolTestFailure -> [StyledString]
|
||||
viewServerTestResult (AProtoServerWithAuth p _) = \case
|
||||
Just ProtocolTestFailure {testStep, testError} ->
|
||||
result
|
||||
<> ["Server requires authorization to create queues, check password" | testStep == TSCreateQueue && testError == SMP SMP.AUTH]
|
||||
<> ["Possibly, certificate fingerprint in server address is incorrect" | testStep == TSConnect && brokerErr]
|
||||
<> [pName <> " server requires authorization to create queues, check password" | testStep == TSCreateQueue && testError == SMP SMP.AUTH]
|
||||
<> [pName <> " server requires authorization to upload files, check password" | testStep == TSCreateFile && testError == XFTP XFTP.AUTH]
|
||||
<> ["Possibly, certificate fingerprint in " <> pName <> " server address is incorrect" | testStep == TSConnect && brokerErr]
|
||||
where
|
||||
result = ["SMP server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> plain (strEncode testError)]
|
||||
result = [pName <> " server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> plain (strEncode testError)]
|
||||
brokerErr = case testError of
|
||||
BROKER _ NETWORK -> True
|
||||
_ -> False
|
||||
_ -> ["SMP server test passed"]
|
||||
_ -> [pName <> " server test passed"]
|
||||
where
|
||||
pName = protocolName p
|
||||
|
||||
viewChatItemTTL :: Maybe Int64 -> [StyledString]
|
||||
viewChatItemTTL = \case
|
||||
@@ -825,8 +838,8 @@ viewConnectionStats ConnectionStats {rcvServers, sndServers} =
|
||||
["receiving messages via: " <> viewServerHosts rcvServers | not $ null rcvServers]
|
||||
<> ["sending messages via: " <> viewServerHosts sndServers | not $ null sndServers]
|
||||
|
||||
viewServers :: ProtocolTypeI p => [ServerCfg p] -> StyledString
|
||||
viewServers = plain . intercalate ", " . map (B.unpack . strEncode . (\ServerCfg {server} -> server))
|
||||
viewServers :: ProtocolTypeI p => NonEmpty (ServerCfg p) -> StyledString
|
||||
viewServers = plain . intercalate ", " . map (B.unpack . strEncode . (\ServerCfg {server} -> server)) . L.toList
|
||||
|
||||
viewServerHosts :: [SMPServer] -> StyledString
|
||||
viewServerHosts = plain . intercalate ", " . map showSMPServer
|
||||
@@ -1258,6 +1271,7 @@ viewChatError logLevel = \case
|
||||
CEChatStoreChanged -> ["error: chat store changed, please restart chat"]
|
||||
CEInvalidConnReq -> viewInvalidConnReq
|
||||
CEInvalidChatMessage e -> ["chat message error: " <> sShow e]
|
||||
CEConnReqMessageProhibited -> ["message is not allowed with this connection link"]
|
||||
CEContactNotReady c -> [ttyContact' c <> ": not ready"]
|
||||
CEContactDisabled Contact {localDisplayName = c} -> [ttyContact c <> ": disabled, to enable: " <> highlight ("/enable " <> c) <> ", to delete: " <> highlight ("/d " <> c)]
|
||||
CEConnectionDisabled Connection {connId, connType} -> [plain $ "connection " <> textEncode connType <> " (" <> tshow connId <> ") is disabled" | logLevel <= CLLWarning]
|
||||
@@ -1304,6 +1318,7 @@ viewChatError logLevel = \case
|
||||
CEDirectMessagesProhibited dir ct -> viewDirectMessagesProhibited dir ct
|
||||
CEAgentVersion -> ["unsupported agent version"]
|
||||
CEAgentNoSubResult connId -> ["no subscription result for connection: " <> sShow connId]
|
||||
CEServerProtocol p -> [plain $ "Servers for protocol " <> strEncode p <> " cannot be configured by the users"]
|
||||
CECommandError e -> ["bad chat command: " <> plain e]
|
||||
CEAgentCommandError e -> ["agent command error: " <> plain e]
|
||||
CEInvalidFileDescription e -> ["invalid file description: " <> plain e]
|
||||
|
||||
+2
-2
@@ -49,9 +49,9 @@ extra-deps:
|
||||
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
||||
# - ../simplexmq
|
||||
- github: simplex-chat/simplexmq
|
||||
commit: 44f0dd39f3d1536c979b09e268dbdf681f9b0bb8
|
||||
commit: 215d2414b7a76e3d501017b24fb6b301bf022546
|
||||
- github: kazu-yamamoto/http2
|
||||
commit: 159417b413a684a9b754e10e4a5db4376aa8c6b9
|
||||
commit: b5a1b7200cf5bc7044af34ba325284271f6dff25
|
||||
# - ../direct-sqlcipher
|
||||
- github: simplex-chat/direct-sqlcipher
|
||||
commit: 34309410eb2069b029b8fc1872deb1e0db123294
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ testOpts =
|
||||
dbKey = "",
|
||||
-- dbKey = "this is a pass-phrase to encrypt the database",
|
||||
smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"],
|
||||
xftpServers = ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7002"],
|
||||
xftpServers = ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"],
|
||||
networkConfig = defaultNetworkConfig,
|
||||
logLevel = CLLImportant,
|
||||
logConnections = False,
|
||||
|
||||
@@ -36,6 +36,9 @@ chatDirectTests = do
|
||||
describe "SMP servers" $ do
|
||||
it "get and set SMP servers" testGetSetSMPServers
|
||||
it "test SMP server connection" testTestSMPServerConnection
|
||||
describe "XFTP servers" $ do
|
||||
it "get and set XFTP servers" testGetSetXFTPServers
|
||||
it "test XFTP server connection" testTestXFTPServer
|
||||
describe "async connection handshake" $ do
|
||||
it "connect when initiating client goes offline" testAsyncInitiatingOffline
|
||||
it "connect when accepting client goes offline" testAsyncAcceptingOffline
|
||||
@@ -393,7 +396,7 @@ testGetSetSMPServers :: HasCallStack => FilePath -> IO ()
|
||||
testGetSetSMPServers =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice _ -> do
|
||||
alice #$> ("/_smp 1", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001")
|
||||
alice #$> ("/_servers 1 smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001")
|
||||
alice #$> ("/smp smp://1234-w==@smp1.example.im", id, "ok")
|
||||
alice #$> ("/smp", id, "smp://1234-w==@smp1.example.im")
|
||||
alice #$> ("/smp smp://1234-w==:password@smp1.example.im", id, "ok")
|
||||
@@ -416,7 +419,36 @@ testTestSMPServerConnection =
|
||||
alice <## "SMP server test passed"
|
||||
alice ##> "/smp test smp://LcJU@localhost:7001"
|
||||
alice <## "SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:7001 NETWORK"
|
||||
alice <## "Possibly, certificate fingerprint in server address is incorrect"
|
||||
alice <## "Possibly, certificate fingerprint in SMP server address is incorrect"
|
||||
|
||||
testGetSetXFTPServers :: HasCallStack => FilePath -> IO ()
|
||||
testGetSetXFTPServers =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice _ -> withXFTPServer $ do
|
||||
alice #$> ("/_servers 1 xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002")
|
||||
alice #$> ("/xftp xftp://1234-w==@xftp1.example.im", id, "ok")
|
||||
alice #$> ("/xftp", id, "xftp://1234-w==@xftp1.example.im")
|
||||
alice #$> ("/xftp xftp://1234-w==:password@xftp1.example.im", id, "ok")
|
||||
alice #$> ("/xftp", id, "xftp://1234-w==:password@xftp1.example.im")
|
||||
alice #$> ("/xftp xftp://2345-w==@xftp2.example.im;xftp://3456-w==@xftp3.example.im:5224", id, "ok")
|
||||
alice #$> ("/xftp", id, "xftp://2345-w==@xftp2.example.im, xftp://3456-w==@xftp3.example.im:5224")
|
||||
alice #$> ("/xftp default", id, "ok")
|
||||
alice #$> ("/xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002")
|
||||
|
||||
testTestXFTPServer :: HasCallStack => FilePath -> IO ()
|
||||
testTestXFTPServer =
|
||||
testChat2 aliceProfile bobProfile $
|
||||
\alice _ -> withXFTPServer $ do
|
||||
alice ##> "/xftp test xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7002"
|
||||
alice <## "XFTP server test passed"
|
||||
-- to test with password:
|
||||
-- alice <## "XFTP server test failed at CreateFile, error: XFTP AUTH"
|
||||
-- alice <## "Server requires authorization to upload files, check password"
|
||||
alice ##> "/xftp test xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"
|
||||
alice <## "XFTP server test passed"
|
||||
alice ##> "/xftp test xftp://LcJU@localhost:7002"
|
||||
alice <## "XFTP server test failed at Connect, error: BROKER xftp://LcJU@localhost:7002 NETWORK"
|
||||
alice <## "Possibly, certificate fingerprint in XFTP server address is incorrect"
|
||||
|
||||
testAsyncInitiatingOffline :: HasCallStack => FilePath -> IO ()
|
||||
testAsyncInitiatingOffline tmp = do
|
||||
|
||||
@@ -201,16 +201,16 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
|
||||
#==# XInfo Profile {displayName = "alice", fullName = "", image = Nothing, preferences = testChatPreferences}
|
||||
it "x.contact with xContactId" $
|
||||
"{\"event\":\"x.contact\",\"params\":{\"contactReqId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
#==# XContact testProfile (Just $ XContactId "\1\2\3\4")
|
||||
#==# XContact testProfile (Just $ XContactId "\1\2\3\4") Nothing
|
||||
it "x.contact without XContactId" $
|
||||
"{\"event\":\"x.contact\",\"params\":{\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
#==# XContact testProfile Nothing
|
||||
#==# XContact testProfile Nothing Nothing
|
||||
it "x.contact with content null" $
|
||||
"{\"event\":\"x.contact\",\"params\":{\"content\":null,\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
==# XContact testProfile Nothing
|
||||
it "x.contact with content (ignored)" $
|
||||
==# XContact testProfile Nothing Nothing
|
||||
it "x.contact with content" $
|
||||
"{\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"voice\":{\"allow\":\"yes\"}}}}}"
|
||||
==# XContact testProfile Nothing
|
||||
#==# XContact testProfile Nothing (Just $ MCText "hello")
|
||||
it "x.grp.inv" $
|
||||
"{\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"https://simplex.chat/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D1-2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}"
|
||||
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Nothing}
|
||||
|
||||
Reference in New Issue
Block a user