2022-03-17 09:42:59 +00:00
|
|
|
//
|
|
|
|
// ComposeView.swift
|
|
|
|
// SimpleX
|
|
|
|
//
|
|
|
|
// Created by Evgeny on 13/03/2022.
|
|
|
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import SwiftUI
|
2022-05-31 07:55:13 +01:00
|
|
|
import SimpleXChat
|
2022-03-17 09:42:59 +00:00
|
|
|
|
2022-04-25 12:44:24 +04:00
|
|
|
enum ComposePreview {
|
|
|
|
case noPreview
|
2022-05-30 08:59:04 +01:00
|
|
|
case linkPreview(linkPreview: LinkPreview?)
|
2022-10-10 10:40:30 +01:00
|
|
|
case imagePreviews(imagePreviews: [String])
|
2022-11-24 21:18:28 +04:00
|
|
|
case voicePreview(recordingFileName: String, duration: Int)
|
2022-05-06 21:10:32 +04:00
|
|
|
case filePreview(fileName: String)
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
2022-04-19 12:29:03 +04:00
|
|
|
|
2022-04-25 12:44:24 +04:00
|
|
|
enum ComposeContextItem {
|
|
|
|
case noContextItem
|
|
|
|
case quotedItem(chatItem: ChatItem)
|
|
|
|
case editingItem(chatItem: ChatItem)
|
|
|
|
}
|
2022-04-08 18:17:10 +01:00
|
|
|
|
2022-11-24 21:18:28 +04:00
|
|
|
enum VoiceMessageRecordingState {
|
|
|
|
case noRecording
|
|
|
|
case recording
|
|
|
|
case finished
|
|
|
|
}
|
|
|
|
|
2022-04-25 12:44:24 +04:00
|
|
|
struct ComposeState {
|
|
|
|
var message: String
|
|
|
|
var preview: ComposePreview
|
|
|
|
var contextItem: ComposeContextItem
|
2022-11-24 21:18:28 +04:00
|
|
|
var voiceMessageRecordingState: VoiceMessageRecordingState
|
|
|
|
var voiceMessageAllowed: Bool
|
2022-10-10 10:40:30 +01:00
|
|
|
var inProgress = false
|
|
|
|
var disabled = false
|
2022-05-30 08:59:04 +01:00
|
|
|
var useLinkPreviews: Bool = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)
|
2022-04-25 12:44:24 +04:00
|
|
|
|
|
|
|
init(
|
|
|
|
message: String = "",
|
|
|
|
preview: ComposePreview = .noPreview,
|
2022-11-24 21:18:28 +04:00
|
|
|
contextItem: ComposeContextItem = .noContextItem,
|
|
|
|
voiceMessageRecordingState: VoiceMessageRecordingState = .noRecording,
|
|
|
|
voiceMessageAllowed: Bool = true // TODO based on preference
|
2022-04-25 12:44:24 +04:00
|
|
|
) {
|
|
|
|
self.message = message
|
|
|
|
self.preview = preview
|
|
|
|
self.contextItem = contextItem
|
2022-11-24 21:18:28 +04:00
|
|
|
self.voiceMessageRecordingState = voiceMessageRecordingState
|
|
|
|
self.voiceMessageAllowed = voiceMessageAllowed
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
init(editingItem: ChatItem) {
|
|
|
|
self.message = editingItem.content.text
|
|
|
|
self.preview = chatItemPreview(chatItem: editingItem)
|
|
|
|
self.contextItem = .editingItem(chatItem: editingItem)
|
2022-11-24 21:18:28 +04:00
|
|
|
if let emc = editingItem.content.msgContent,
|
|
|
|
case .voice = emc {
|
|
|
|
self.voiceMessageRecordingState = .finished
|
|
|
|
} else {
|
|
|
|
self.voiceMessageRecordingState = .noRecording
|
|
|
|
}
|
|
|
|
self.voiceMessageAllowed = false
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
func copy(
|
|
|
|
message: String? = nil,
|
|
|
|
preview: ComposePreview? = nil,
|
2022-11-24 21:18:28 +04:00
|
|
|
contextItem: ComposeContextItem? = nil,
|
|
|
|
voiceMessageRecordingState: VoiceMessageRecordingState? = nil
|
2022-04-25 12:44:24 +04:00
|
|
|
) -> ComposeState {
|
|
|
|
ComposeState(
|
|
|
|
message: message ?? self.message,
|
|
|
|
preview: preview ?? self.preview,
|
2022-11-24 21:18:28 +04:00
|
|
|
contextItem: contextItem ?? self.contextItem,
|
|
|
|
voiceMessageRecordingState: voiceMessageRecordingState ?? self.voiceMessageRecordingState
|
2022-04-25 12:44:24 +04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-11-24 21:18:28 +04:00
|
|
|
var editing: Bool {
|
2022-04-25 12:44:24 +04:00
|
|
|
switch contextItem {
|
|
|
|
case .editingItem: return true
|
|
|
|
default: return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-24 21:18:28 +04:00
|
|
|
var sendEnabled: Bool {
|
2022-04-25 12:44:24 +04:00
|
|
|
switch preview {
|
2022-11-24 21:18:28 +04:00
|
|
|
case .imagePreviews: return true
|
|
|
|
case .voicePreview: return voiceMessageRecordingState == .finished
|
|
|
|
case .filePreview: return true
|
|
|
|
default: return !message.isEmpty
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-24 21:18:28 +04:00
|
|
|
var linkPreviewAllowed: Bool {
|
2022-04-25 12:44:24 +04:00
|
|
|
switch preview {
|
2022-11-24 21:18:28 +04:00
|
|
|
case .imagePreviews: return false
|
|
|
|
case .voicePreview: return false
|
|
|
|
case .filePreview: return false
|
|
|
|
default: return useLinkPreviews
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-24 21:18:28 +04:00
|
|
|
var linkPreview: LinkPreview? {
|
2022-04-25 12:44:24 +04:00
|
|
|
switch preview {
|
2022-11-24 21:18:28 +04:00
|
|
|
case let .linkPreview(linkPreview): return linkPreview
|
|
|
|
default: return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var voiceMessageRecordingFileName: String? {
|
|
|
|
switch preview {
|
|
|
|
case let .voicePreview(recordingFileName: recordingFileName, _): return recordingFileName
|
|
|
|
default: return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var noPreview: Bool {
|
|
|
|
switch preview {
|
|
|
|
case .noPreview: return true
|
|
|
|
default: return false
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func chatItemPreview(chatItem: ChatItem) -> ComposePreview {
|
|
|
|
let chatItemPreview: ComposePreview
|
|
|
|
switch chatItem.content.msgContent {
|
2022-05-06 21:10:32 +04:00
|
|
|
case .text:
|
|
|
|
chatItemPreview = .noPreview
|
2022-04-25 12:44:24 +04:00
|
|
|
case let .link(_, preview: preview):
|
|
|
|
chatItemPreview = .linkPreview(linkPreview: preview)
|
2022-11-24 21:18:28 +04:00
|
|
|
case let .image(_, image):
|
2022-10-10 10:40:30 +01:00
|
|
|
chatItemPreview = .imagePreviews(imagePreviews: [image])
|
2022-11-24 21:18:28 +04:00
|
|
|
case let .voice(_, duration):
|
|
|
|
chatItemPreview = .voicePreview(recordingFileName: chatItem.file?.fileName ?? "", duration: duration)
|
2022-05-06 21:10:32 +04:00
|
|
|
case .file:
|
|
|
|
chatItemPreview = .filePreview(fileName: chatItem.file?.fileName ?? "")
|
2022-04-25 12:44:24 +04:00
|
|
|
default:
|
|
|
|
chatItemPreview = .noPreview
|
|
|
|
}
|
|
|
|
return chatItemPreview
|
|
|
|
}
|
|
|
|
|
|
|
|
struct ComposeView: View {
|
|
|
|
@EnvironmentObject var chatModel: ChatModel
|
|
|
|
let chat: Chat
|
|
|
|
@Binding var composeState: ComposeState
|
2022-03-17 09:42:59 +00:00
|
|
|
@FocusState.Binding var keyboardVisible: Bool
|
2022-04-25 12:44:24 +04:00
|
|
|
|
2022-04-08 18:17:10 +01:00
|
|
|
@State var linkUrl: URL? = nil
|
|
|
|
@State var prevLinkUrl: URL? = nil
|
|
|
|
@State var pendingLinkUrl: URL? = nil
|
|
|
|
@State var cancelledLinks: Set<String> = []
|
|
|
|
|
2022-04-19 12:29:03 +04:00
|
|
|
@State private var showChooseSource = false
|
|
|
|
@State private var showImagePicker = false
|
2022-05-18 21:32:30 +04:00
|
|
|
@State private var showTakePhoto = false
|
2022-10-10 10:40:30 +01:00
|
|
|
@State var chosenImages: [UIImage] = []
|
2022-05-06 21:10:32 +04:00
|
|
|
@State private var showFileImporter = false
|
|
|
|
@State var chosenFile: URL? = nil
|
2022-11-24 21:18:28 +04:00
|
|
|
|
|
|
|
@State private var audioRecorder: AudioRecorder?
|
|
|
|
@State private var voiceMessageRecordingTime: TimeInterval?
|
|
|
|
@State private var startingRecording: Bool = false
|
2022-04-08 18:17:10 +01:00
|
|
|
|
2022-03-17 09:42:59 +00:00
|
|
|
var body: some View {
|
|
|
|
VStack(spacing: 0) {
|
2022-04-25 12:44:24 +04:00
|
|
|
contextItemView()
|
2022-11-24 21:18:28 +04:00
|
|
|
switch (composeState.editing, composeState.preview) {
|
2022-05-06 21:10:32 +04:00
|
|
|
case (true, .filePreview): EmptyView()
|
2022-11-24 21:18:28 +04:00
|
|
|
case (true, .voicePreview): EmptyView() // ? we may allow playback when editing is allowed
|
2022-05-06 21:10:32 +04:00
|
|
|
default: previewView()
|
|
|
|
}
|
2022-04-25 12:44:24 +04:00
|
|
|
HStack (alignment: .bottom) {
|
|
|
|
Button {
|
|
|
|
showChooseSource = true
|
|
|
|
} label: {
|
|
|
|
Image(systemName: "paperclip")
|
|
|
|
.resizable()
|
|
|
|
}
|
2022-11-24 21:18:28 +04:00
|
|
|
.disabled(composeState.editing || composeState.voiceMessageRecordingState != .noRecording)
|
2022-04-25 12:44:24 +04:00
|
|
|
.frame(width: 25, height: 25)
|
|
|
|
.padding(.bottom, 12)
|
|
|
|
.padding(.leading, 12)
|
2022-04-19 12:29:03 +04:00
|
|
|
SendMessageView(
|
2022-04-25 12:44:24 +04:00
|
|
|
composeState: $composeState,
|
2022-04-27 20:54:21 +04:00
|
|
|
sendMessage: {
|
|
|
|
sendMessage()
|
2022-04-19 12:29:03 +04:00
|
|
|
resetLinkPreview()
|
|
|
|
},
|
2022-11-24 21:18:28 +04:00
|
|
|
startVoiceMessageRecording: {
|
|
|
|
Task {
|
|
|
|
await startVoiceMessageRecording()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
finishVoiceMessageRecording: { finishVoiceMessageRecording() },
|
2022-04-25 12:44:24 +04:00
|
|
|
keyboardVisible: $keyboardVisible
|
2022-04-19 12:29:03 +04:00
|
|
|
)
|
2022-04-25 12:44:24 +04:00
|
|
|
.padding(.trailing, 12)
|
2022-04-19 12:29:03 +04:00
|
|
|
.background(.background)
|
|
|
|
}
|
2022-03-17 09:42:59 +00:00
|
|
|
}
|
2022-04-25 12:44:24 +04:00
|
|
|
.onChange(of: composeState.message) { _ in
|
2022-11-24 21:18:28 +04:00
|
|
|
if composeState.linkPreviewAllowed {
|
2022-04-25 12:44:24 +04:00
|
|
|
if composeState.message.count > 0 {
|
|
|
|
showLinkPreview(composeState.message)
|
|
|
|
} else {
|
|
|
|
resetLinkPreview()
|
|
|
|
}
|
2022-04-08 18:17:10 +01:00
|
|
|
}
|
2022-03-25 22:26:05 +04:00
|
|
|
}
|
2022-04-19 12:29:03 +04:00
|
|
|
.confirmationDialog("Attach", isPresented: $showChooseSource, titleVisibility: .visible) {
|
|
|
|
Button("Take picture") {
|
2022-05-18 21:32:30 +04:00
|
|
|
showTakePhoto = true
|
2022-04-19 12:29:03 +04:00
|
|
|
}
|
|
|
|
Button("Choose from library") {
|
|
|
|
showImagePicker = true
|
|
|
|
}
|
2022-05-30 09:03:56 +01:00
|
|
|
if UIPasteboard.general.hasImages {
|
|
|
|
Button("Paste image") {
|
2022-10-10 10:40:30 +01:00
|
|
|
chosenImages = imageList(UIPasteboard.general.image)
|
2022-05-30 09:03:56 +01:00
|
|
|
}
|
|
|
|
}
|
2022-05-06 21:10:32 +04:00
|
|
|
Button("Choose file") {
|
|
|
|
showFileImporter = true
|
|
|
|
}
|
2022-04-19 12:29:03 +04:00
|
|
|
}
|
2022-05-18 21:32:30 +04:00
|
|
|
.fullScreenCover(isPresented: $showTakePhoto) {
|
|
|
|
ZStack {
|
|
|
|
Color.black.edgesIgnoringSafeArea(.all)
|
2022-10-10 10:40:30 +01:00
|
|
|
CameraImageListPicker(images: $chosenImages)
|
2022-04-19 12:29:03 +04:00
|
|
|
}
|
|
|
|
}
|
2022-05-18 21:32:30 +04:00
|
|
|
.sheet(isPresented: $showImagePicker) {
|
2022-10-10 10:40:30 +01:00
|
|
|
LibraryImageListPicker(images: $chosenImages, selectionLimit: 10) { itemsSelected in
|
|
|
|
showImagePicker = false
|
|
|
|
if itemsSelected {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
composeState = composeState.copy(preview: .imagePreviews(imagePreviews: []))
|
|
|
|
}
|
|
|
|
}
|
2022-05-18 21:32:30 +04:00
|
|
|
}
|
|
|
|
}
|
2022-10-10 10:40:30 +01:00
|
|
|
.onChange(of: chosenImages) { images in
|
|
|
|
Task {
|
|
|
|
var imgs: [String] = []
|
|
|
|
for image in images {
|
|
|
|
if let img = resizeImageToStrSize(image, maxDataSize: 14000) {
|
|
|
|
imgs.append(img)
|
|
|
|
await MainActor.run {
|
|
|
|
composeState = composeState.copy(preview: .imagePreviews(imagePreviews: imgs))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if imgs.count == 0 {
|
|
|
|
await MainActor.run {
|
|
|
|
composeState = composeState.copy(preview: .noPreview)
|
|
|
|
}
|
|
|
|
}
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
}
|
2022-05-06 21:10:32 +04:00
|
|
|
.fileImporter(
|
|
|
|
isPresented: $showFileImporter,
|
|
|
|
allowedContentTypes: [.data],
|
|
|
|
allowsMultipleSelection: false
|
|
|
|
) { result in
|
2022-06-24 13:52:20 +01:00
|
|
|
if case let .success(files) = result, let fileURL = files.first {
|
2022-05-06 21:10:32 +04:00
|
|
|
do {
|
|
|
|
var fileSize: Int? = nil
|
|
|
|
if fileURL.startAccessingSecurityScopedResource() {
|
|
|
|
let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey])
|
|
|
|
fileSize = resourceValues.fileSize
|
|
|
|
}
|
|
|
|
fileURL.stopAccessingSecurityScopedResource()
|
|
|
|
if let fileSize = fileSize,
|
|
|
|
fileSize <= maxFileSize {
|
|
|
|
chosenFile = fileURL
|
|
|
|
composeState = composeState.copy(preview: .filePreview(fileName: fileURL.lastPathComponent))
|
|
|
|
} else {
|
|
|
|
let prettyMaxFileSize = ByteCountFormatter().string(fromByteCount: maxFileSize)
|
|
|
|
AlertManager.shared.showAlertMsg(
|
|
|
|
title: "Large file!",
|
|
|
|
message: "Currently maximum supported file size is \(prettyMaxFileSize)."
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} catch {
|
|
|
|
logger.error("ComposeView fileImporter error \(error.localizedDescription)")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-11-24 21:18:28 +04:00
|
|
|
.onDisappear {
|
|
|
|
audioRecorder?.stop()
|
|
|
|
if let fileName = composeState.voiceMessageRecordingFileName {
|
|
|
|
cancelVoiceMessageRecording(fileName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.onChange(of: chatModel.stopPreviousRecPlay) { _ in
|
|
|
|
if !startingRecording {
|
|
|
|
if composeState.voiceMessageRecordingState == .recording {
|
|
|
|
finishVoiceMessageRecording()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
startingRecording = false
|
|
|
|
}
|
|
|
|
}
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
@ViewBuilder func previewView() -> some View {
|
|
|
|
switch composeState.preview {
|
|
|
|
case .noPreview:
|
|
|
|
EmptyView()
|
|
|
|
case let .linkPreview(linkPreview: preview):
|
|
|
|
ComposeLinkView(linkPreview: preview, cancelPreview: cancelLinkPreview)
|
2022-10-10 10:40:30 +01:00
|
|
|
case let .imagePreviews(imagePreviews: images):
|
2022-04-25 12:44:24 +04:00
|
|
|
ComposeImageView(
|
2022-10-10 10:40:30 +01:00
|
|
|
images: images,
|
2022-05-06 21:10:32 +04:00
|
|
|
cancelImage: {
|
|
|
|
composeState = composeState.copy(preview: .noPreview)
|
2022-10-10 10:40:30 +01:00
|
|
|
chosenImages = []
|
2022-05-06 21:10:32 +04:00
|
|
|
},
|
2022-11-24 21:18:28 +04:00
|
|
|
cancelEnabled: !composeState.editing)
|
|
|
|
case let .voicePreview(recordingFileName, _):
|
|
|
|
ComposeVoiceView(
|
|
|
|
recordingFileName: recordingFileName,
|
|
|
|
recordingTime: $voiceMessageRecordingTime,
|
|
|
|
recordingState: $composeState.voiceMessageRecordingState,
|
|
|
|
cancelVoiceMessage: { cancelVoiceMessageRecording($0) },
|
|
|
|
cancelEnabled: !composeState.editing
|
|
|
|
)
|
2022-05-06 21:10:32 +04:00
|
|
|
case let .filePreview(fileName: fileName):
|
|
|
|
ComposeFileView(
|
|
|
|
fileName: fileName,
|
|
|
|
cancelFile: {
|
|
|
|
composeState = composeState.copy(preview: .noPreview)
|
|
|
|
chosenFile = nil
|
|
|
|
},
|
2022-11-24 21:18:28 +04:00
|
|
|
cancelEnabled: !composeState.editing)
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@ViewBuilder private func contextItemView() -> some View {
|
|
|
|
switch composeState.contextItem {
|
|
|
|
case .noContextItem:
|
|
|
|
EmptyView()
|
|
|
|
case let .quotedItem(chatItem: quotedItem):
|
2022-05-06 21:10:32 +04:00
|
|
|
ContextItemView(
|
|
|
|
contextItem: quotedItem,
|
2022-05-09 09:12:32 +01:00
|
|
|
contextIcon: "arrowshape.turn.up.left",
|
2022-05-06 21:10:32 +04:00
|
|
|
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }
|
|
|
|
)
|
2022-04-25 12:44:24 +04:00
|
|
|
case let .editingItem(chatItem: editingItem):
|
2022-05-06 21:10:32 +04:00
|
|
|
ContextItemView(
|
|
|
|
contextItem: editingItem,
|
2022-05-09 09:12:32 +01:00
|
|
|
contextIcon: "pencil",
|
2022-05-06 21:10:32 +04:00
|
|
|
cancelContextItem: { clearState() }
|
|
|
|
)
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-27 20:54:21 +04:00
|
|
|
private func sendMessage() {
|
2022-04-25 12:44:24 +04:00
|
|
|
logger.debug("ChatView sendMessage")
|
|
|
|
Task {
|
|
|
|
logger.debug("ChatView sendMessage: in Task")
|
2022-09-21 17:18:48 +04:00
|
|
|
switch composeState.contextItem {
|
|
|
|
case let .editingItem(chatItem: ei):
|
|
|
|
if let oldMsgContent = ei.content.msgContent {
|
|
|
|
do {
|
2022-10-10 10:40:30 +01:00
|
|
|
await sending()
|
2022-09-24 22:20:56 +01:00
|
|
|
let mc = updateMsgContent(oldMsgContent)
|
2022-04-25 12:44:24 +04:00
|
|
|
let chatItem = try await apiUpdateChatItem(
|
|
|
|
type: chat.chatInfo.chatType,
|
|
|
|
id: chat.chatInfo.apiId,
|
|
|
|
itemId: ei.id,
|
2022-09-24 22:20:56 +01:00
|
|
|
msg: mc
|
2022-04-25 12:44:24 +04:00
|
|
|
)
|
2022-10-10 10:40:30 +01:00
|
|
|
await MainActor.run {
|
|
|
|
clearState()
|
2022-04-25 12:44:24 +04:00
|
|
|
let _ = self.chatModel.upsertChatItem(self.chat.chatInfo, chatItem)
|
|
|
|
}
|
2022-09-21 17:18:48 +04:00
|
|
|
} catch {
|
|
|
|
logger.error("ChatView.sendMessage error: \(error.localizedDescription)")
|
2022-10-10 10:40:30 +01:00
|
|
|
await MainActor.run {
|
|
|
|
composeState.disabled = false
|
|
|
|
composeState.inProgress = false
|
|
|
|
}
|
2022-09-21 17:18:48 +04:00
|
|
|
AlertManager.shared.showAlertMsg(title: "Error updating message", message: "Error: \(responseError(error))")
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
2022-09-24 22:20:56 +01:00
|
|
|
} else {
|
|
|
|
await MainActor.run { clearState() }
|
2022-09-21 17:18:48 +04:00
|
|
|
}
|
|
|
|
default:
|
2022-10-10 10:40:30 +01:00
|
|
|
await sending()
|
|
|
|
var quoted: Int64? = nil
|
|
|
|
if case let .quotedItem(chatItem: quotedItem) = composeState.contextItem {
|
|
|
|
quoted = quotedItem.id
|
|
|
|
}
|
|
|
|
|
2022-09-21 17:18:48 +04:00
|
|
|
switch (composeState.preview) {
|
|
|
|
case .noPreview:
|
2022-10-10 10:40:30 +01:00
|
|
|
await send(.text(composeState.message), quoted: quoted)
|
2022-09-21 17:18:48 +04:00
|
|
|
case .linkPreview:
|
2022-10-10 10:40:30 +01:00
|
|
|
await send(checkLinkPreview(), quoted: quoted)
|
|
|
|
case let .imagePreviews(imagePreviews: images):
|
|
|
|
var text = composeState.message
|
|
|
|
var sent = false
|
|
|
|
for i in 0..<min(chosenImages.count, images.count) {
|
|
|
|
if i > 0 { _ = try? await Task.sleep(nanoseconds: 100_000000) }
|
|
|
|
if let savedFile = saveImage(chosenImages[i]) {
|
|
|
|
await send(.image(text: text, image: images[i]), quoted: quoted, file: savedFile)
|
|
|
|
text = ""
|
|
|
|
quoted = nil
|
|
|
|
sent = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !sent {
|
|
|
|
await send(.text(composeState.message), quoted: quoted)
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
2022-11-24 21:18:28 +04:00
|
|
|
case let .voicePreview(recordingFileName, duration):
|
|
|
|
await send(.voice(text: composeState.message, duration: duration), quoted: quoted, file: recordingFileName)
|
2022-09-21 17:18:48 +04:00
|
|
|
case .filePreview:
|
|
|
|
if let fileURL = chosenFile,
|
|
|
|
let savedFile = saveFileFromURL(fileURL) {
|
2022-10-10 10:40:30 +01:00
|
|
|
await send(.file(composeState.message), quoted: quoted, file: savedFile)
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
}
|
2022-10-10 10:40:30 +01:00
|
|
|
}
|
|
|
|
await MainActor.run { clearState() }
|
|
|
|
}
|
|
|
|
|
|
|
|
func sending() async {
|
|
|
|
await MainActor.run { composeState.disabled = true }
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
|
|
if composeState.disabled { composeState.inProgress = true }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func send(_ mc: MsgContent, quoted: Int64?, file: String? = nil) async {
|
|
|
|
if let chatItem = await apiSendMessage(
|
|
|
|
type: chat.chatInfo.chatType,
|
|
|
|
id: chat.chatInfo.apiId,
|
|
|
|
file: file,
|
|
|
|
quotedItemId: quoted,
|
|
|
|
msg: mc
|
|
|
|
) {
|
|
|
|
await MainActor.run {
|
2022-09-21 17:18:48 +04:00
|
|
|
chatModel.addChatItem(chat.chatInfo, chatItem)
|
|
|
|
}
|
2022-04-19 12:29:03 +04:00
|
|
|
}
|
|
|
|
}
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
|
2022-11-24 21:18:28 +04:00
|
|
|
private func startVoiceMessageRecording() async {
|
|
|
|
startingRecording = true
|
|
|
|
chatModel.stopPreviousRecPlay.toggle()
|
|
|
|
let fileName = generateNewFileName("voice", "m4a")
|
|
|
|
audioRecorder = AudioRecorder(
|
|
|
|
onTimer: { voiceMessageRecordingTime = $0 },
|
|
|
|
onFinishRecording: {
|
|
|
|
updateComposeVMRFinished()
|
|
|
|
if let fileSize = fileSize(getAppFilePath(fileName)) {
|
|
|
|
logger.debug("onFinishRecording recording file size = \(fileSize)")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
2022-11-25 12:30:24 +04:00
|
|
|
if let recStartError = await audioRecorder?.start(fileName: fileName) {
|
|
|
|
switch recStartError {
|
|
|
|
case .permission:
|
|
|
|
AlertManager.shared.showAlertMsg(
|
|
|
|
title: "No permission to record voice message",
|
|
|
|
message: "To record voice message please grant permission to use Microphone."
|
|
|
|
)
|
|
|
|
case let .error(error):
|
|
|
|
AlertManager.shared.showAlertMsg(
|
|
|
|
title: "Unable to record voice message",
|
|
|
|
message: "Error: \(error)"
|
|
|
|
)
|
|
|
|
}
|
2022-11-24 21:18:28 +04:00
|
|
|
} else {
|
|
|
|
composeState = composeState.copy(
|
|
|
|
preview: .voicePreview(recordingFileName: fileName, duration: 0),
|
|
|
|
voiceMessageRecordingState: .recording
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func finishVoiceMessageRecording() {
|
|
|
|
audioRecorder?.stop()
|
|
|
|
audioRecorder = nil
|
|
|
|
updateComposeVMRFinished()
|
|
|
|
if let fileName = composeState.voiceMessageRecordingFileName,
|
|
|
|
let fileSize = fileSize(getAppFilePath(fileName)) {
|
|
|
|
logger.debug("finishVoiceMessageRecording recording file size = \(fileSize)")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// ? maybe we shouldn't have duration in ComposePreview.voicePreview
|
|
|
|
private func updateComposeVMRFinished() {
|
|
|
|
var preview = composeState.preview
|
|
|
|
if let recordingFileName = composeState.voiceMessageRecordingFileName,
|
|
|
|
let recordingTime = voiceMessageRecordingTime {
|
|
|
|
preview = .voicePreview(recordingFileName: recordingFileName, duration: Int(recordingTime.rounded()))
|
|
|
|
}
|
|
|
|
composeState = composeState.copy(
|
|
|
|
preview: preview,
|
|
|
|
voiceMessageRecordingState: .finished
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func cancelVoiceMessageRecording(_ fileName: String) {
|
|
|
|
removeFile(fileName)
|
|
|
|
clearState()
|
|
|
|
}
|
|
|
|
|
2022-05-06 21:10:32 +04:00
|
|
|
private func clearState() {
|
|
|
|
composeState = ComposeState()
|
|
|
|
linkUrl = nil
|
|
|
|
prevLinkUrl = nil
|
|
|
|
pendingLinkUrl = nil
|
|
|
|
cancelledLinks = []
|
2022-10-10 10:40:30 +01:00
|
|
|
chosenImages = []
|
2022-05-06 21:10:32 +04:00
|
|
|
chosenFile = nil
|
2022-11-24 21:18:28 +04:00
|
|
|
audioRecorder?.stop()
|
|
|
|
audioRecorder = nil
|
|
|
|
voiceMessageRecordingTime = nil
|
2022-05-06 21:10:32 +04:00
|
|
|
}
|
|
|
|
|
2022-04-25 12:44:24 +04:00
|
|
|
private func updateMsgContent(_ msgContent: MsgContent) -> MsgContent {
|
|
|
|
switch msgContent {
|
|
|
|
case .text:
|
|
|
|
return checkLinkPreview()
|
|
|
|
case .link:
|
|
|
|
return checkLinkPreview()
|
|
|
|
case .image(_, let image):
|
|
|
|
return .image(text: composeState.message, image: image)
|
2022-11-24 21:18:28 +04:00
|
|
|
case .voice(_, let duration):
|
|
|
|
return .voice(text: composeState.message, duration: duration)
|
2022-05-04 09:10:36 +04:00
|
|
|
case .file:
|
|
|
|
return .file(composeState.message)
|
2022-04-25 12:44:24 +04:00
|
|
|
case .unknown(let type, _):
|
|
|
|
return .unknown(type: type, text: composeState.message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-11 09:39:04 +01:00
|
|
|
private func showLinkPreview(_ s: String) {
|
|
|
|
prevLinkUrl = linkUrl
|
|
|
|
linkUrl = parseMessage(s)
|
|
|
|
if let url = linkUrl {
|
2022-11-24 21:18:28 +04:00
|
|
|
if url != composeState.linkPreview?.uri && url != pendingLinkUrl {
|
2022-04-11 09:39:04 +01:00
|
|
|
pendingLinkUrl = url
|
|
|
|
if prevLinkUrl == url {
|
|
|
|
loadLinkPreview(url)
|
|
|
|
} else {
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
|
|
|
|
loadLinkPreview(url)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2022-04-25 12:44:24 +04:00
|
|
|
composeState = composeState.copy(preview: .noPreview)
|
2022-04-11 09:39:04 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func parseMessage(_ msg: String) -> URL? {
|
2022-06-24 13:52:20 +01:00
|
|
|
let parsedMsg = parseSimpleXMarkdown(msg)
|
|
|
|
let uri = parsedMsg?.first(where: { ft in
|
|
|
|
ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text)
|
|
|
|
})
|
|
|
|
if let uri = uri { return URL(string: uri.text) }
|
|
|
|
else { return nil }
|
2022-04-11 09:39:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private func isSimplexLink(_ link: String) -> Bool {
|
|
|
|
link.starts(with: "https://simplex.chat") || link.starts(with: "http://simplex.chat")
|
|
|
|
}
|
|
|
|
|
2022-04-25 12:44:24 +04:00
|
|
|
private func cancelLinkPreview() {
|
2022-11-24 21:18:28 +04:00
|
|
|
if let uri = composeState.linkPreview?.uri.absoluteString {
|
2022-04-11 09:39:04 +01:00
|
|
|
cancelledLinks.insert(uri)
|
|
|
|
}
|
2022-05-30 08:59:04 +01:00
|
|
|
pendingLinkUrl = nil
|
2022-04-25 12:44:24 +04:00
|
|
|
composeState = composeState.copy(preview: .noPreview)
|
2022-04-11 09:39:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private func loadLinkPreview(_ url: URL) {
|
|
|
|
if pendingLinkUrl == url {
|
2022-05-30 08:59:04 +01:00
|
|
|
composeState = composeState.copy(preview: .linkPreview(linkPreview: nil))
|
2022-04-25 12:44:24 +04:00
|
|
|
getLinkPreview(url: url) { linkPreview in
|
|
|
|
if let linkPreview = linkPreview,
|
|
|
|
pendingLinkUrl == url {
|
|
|
|
composeState = composeState.copy(preview: .linkPreview(linkPreview: linkPreview))
|
2022-04-08 18:17:10 +01:00
|
|
|
pendingLinkUrl = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-11 09:39:04 +01:00
|
|
|
private func resetLinkPreview() {
|
2022-04-08 18:17:10 +01:00
|
|
|
linkUrl = nil
|
|
|
|
prevLinkUrl = nil
|
|
|
|
pendingLinkUrl = nil
|
|
|
|
cancelledLinks = []
|
|
|
|
}
|
2022-04-25 12:44:24 +04:00
|
|
|
|
|
|
|
private func checkLinkPreview() -> MsgContent {
|
|
|
|
switch (composeState.preview) {
|
|
|
|
case let .linkPreview(linkPreview: linkPreview):
|
|
|
|
if let url = parseMessage(composeState.message),
|
2022-05-30 08:59:04 +01:00
|
|
|
let linkPreview = linkPreview,
|
2022-04-25 12:44:24 +04:00
|
|
|
url == linkPreview.uri {
|
|
|
|
return .link(text: composeState.message, preview: linkPreview)
|
|
|
|
} else {
|
|
|
|
return .text(composeState.message)
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return .text(composeState.message)
|
|
|
|
}
|
|
|
|
}
|
2022-03-17 09:42:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
struct ComposeView_Previews: PreviewProvider {
|
|
|
|
static var previews: some View {
|
2022-04-25 12:44:24 +04:00
|
|
|
let chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])
|
|
|
|
@State var composeState = ComposeState(message: "hello")
|
2022-03-17 09:42:59 +00:00
|
|
|
@FocusState var keyboardVisible: Bool
|
|
|
|
|
2022-03-25 22:26:05 +04:00
|
|
|
return Group {
|
|
|
|
ComposeView(
|
2022-04-25 12:44:24 +04:00
|
|
|
chat: chat,
|
|
|
|
composeState: $composeState,
|
|
|
|
keyboardVisible: $keyboardVisible
|
2022-03-25 22:26:05 +04:00
|
|
|
)
|
2022-11-24 21:18:28 +04:00
|
|
|
.environmentObject(ChatModel())
|
2022-03-25 22:26:05 +04:00
|
|
|
ComposeView(
|
2022-04-25 12:44:24 +04:00
|
|
|
chat: chat,
|
|
|
|
composeState: $composeState,
|
|
|
|
keyboardVisible: $keyboardVisible
|
2022-03-25 22:26:05 +04:00
|
|
|
)
|
2022-11-24 21:18:28 +04:00
|
|
|
.environmentObject(ChatModel())
|
2022-03-25 22:26:05 +04:00
|
|
|
}
|
2022-03-17 09:42:59 +00:00
|
|
|
}
|
|
|
|
}
|