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-12-24 00:22:12 +03:00
|
|
|
import SwiftyGif
|
|
|
|
import PhotosUI
|
2022-03-17 09:42:59 +00:00
|
|
|
|
2025-02-03 20:47:32 +00:00
|
|
|
let MAX_NUMBER_OF_MENTIONS = 3
|
|
|
|
|
2022-04-25 12:44:24 +04:00
|
|
|
enum ComposePreview {
|
|
|
|
case noPreview
|
2022-05-30 08:59:04 +01:00
|
|
|
case linkPreview(linkPreview: LinkPreview?)
|
2023-04-06 20:26:48 +03:00
|
|
|
case mediaPreviews(mediaPreviews: [(String, UploadContent?)])
|
2022-11-24 21:18:28 +04:00
|
|
|
case voicePreview(recordingFileName: String, duration: Int)
|
2023-01-27 22:09:39 +00:00
|
|
|
case filePreview(fileName: String, file: URL)
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
2022-04-19 12:29:03 +04:00
|
|
|
|
2025-02-03 20:47:32 +00:00
|
|
|
enum ComposeContextItem: Equatable {
|
2022-04-25 12:44:24 +04:00
|
|
|
case noContextItem
|
|
|
|
case quotedItem(chatItem: ChatItem)
|
|
|
|
case editingItem(chatItem: ChatItem)
|
2024-09-19 10:04:19 +03:00
|
|
|
case forwardingItems(chatItems: [ChatItem], fromChatInfo: ChatInfo)
|
2025-01-08 18:28:45 +00:00
|
|
|
case reportedItem(chatItem: ChatItem, reason: ReportReason)
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
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-12-17 14:02:07 +00:00
|
|
|
struct LiveMessage {
|
|
|
|
var chatItem: ChatItem
|
|
|
|
var typedMsg: String
|
2023-01-10 19:12:48 +00:00
|
|
|
var sentMsg: String?
|
2022-12-17 14:02:07 +00:00
|
|
|
}
|
|
|
|
|
2025-02-03 20:47:32 +00:00
|
|
|
typealias MentionedMembers = [String: CIMention]
|
|
|
|
|
2022-04-25 12:44:24 +04:00
|
|
|
struct ComposeState {
|
|
|
|
var message: String
|
2025-02-03 20:47:32 +00:00
|
|
|
var parsedMessage: [FormattedText]
|
2022-12-17 14:02:07 +00:00
|
|
|
var liveMessage: LiveMessage? = nil
|
2022-04-25 12:44:24 +04:00
|
|
|
var preview: ComposePreview
|
|
|
|
var contextItem: ComposeContextItem
|
2022-11-24 21:18:28 +04:00
|
|
|
var voiceMessageRecordingState: VoiceMessageRecordingState
|
2022-10-10 10:40:30 +01:00
|
|
|
var inProgress = false
|
2022-05-30 08:59:04 +01:00
|
|
|
var useLinkPreviews: Bool = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)
|
2025-02-03 20:47:32 +00:00
|
|
|
var mentions: MentionedMembers = [:]
|
2022-04-25 12:44:24 +04:00
|
|
|
|
|
|
|
init(
|
|
|
|
message: String = "",
|
2025-02-03 20:47:32 +00:00
|
|
|
parsedMessage: [FormattedText] = [],
|
2022-12-17 14:02:07 +00:00
|
|
|
liveMessage: LiveMessage? = nil,
|
2022-04-25 12:44:24 +04:00
|
|
|
preview: ComposePreview = .noPreview,
|
2022-11-24 21:18:28 +04:00
|
|
|
contextItem: ComposeContextItem = .noContextItem,
|
2025-02-03 20:47:32 +00:00
|
|
|
voiceMessageRecordingState: VoiceMessageRecordingState = .noRecording,
|
|
|
|
mentions: MentionedMembers = [:]
|
2022-04-25 12:44:24 +04:00
|
|
|
) {
|
|
|
|
self.message = message
|
2025-02-03 20:47:32 +00:00
|
|
|
self.parsedMessage = parsedMessage
|
2022-12-17 14:02:07 +00:00
|
|
|
self.liveMessage = liveMessage
|
2022-04-25 12:44:24 +04:00
|
|
|
self.preview = preview
|
|
|
|
self.contextItem = contextItem
|
2022-11-24 21:18:28 +04:00
|
|
|
self.voiceMessageRecordingState = voiceMessageRecordingState
|
2025-02-03 20:47:32 +00:00
|
|
|
self.mentions = mentions
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
init(editingItem: ChatItem) {
|
2025-02-03 20:47:32 +00:00
|
|
|
let text = editingItem.content.text
|
|
|
|
self.message = text
|
|
|
|
self.parsedMessage = editingItem.formattedText ?? FormattedText.plain(text)
|
2022-04-25 12:44:24 +04:00
|
|
|
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
|
|
|
|
}
|
2025-02-03 20:47:32 +00:00
|
|
|
self.mentions = editingItem.mentions ?? [:]
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
|
2024-09-19 10:04:19 +03:00
|
|
|
init(forwardingItems: [ChatItem], fromChatInfo: ChatInfo) {
|
2024-04-16 12:28:39 +04:00
|
|
|
self.message = ""
|
2025-02-03 20:47:32 +00:00
|
|
|
self.parsedMessage = []
|
2024-04-16 12:28:39 +04:00
|
|
|
self.preview = .noPreview
|
2024-09-19 10:04:19 +03:00
|
|
|
self.contextItem = .forwardingItems(chatItems: forwardingItems, fromChatInfo: fromChatInfo)
|
2024-04-16 12:28:39 +04:00
|
|
|
self.voiceMessageRecordingState = .noRecording
|
|
|
|
}
|
|
|
|
|
2022-04-25 12:44:24 +04:00
|
|
|
func copy(
|
|
|
|
message: String? = nil,
|
2025-02-03 20:47:32 +00:00
|
|
|
parsedMessage: [FormattedText]? = nil,
|
2022-12-17 14:02:07 +00:00
|
|
|
liveMessage: LiveMessage? = nil,
|
2022-04-25 12:44:24 +04:00
|
|
|
preview: ComposePreview? = nil,
|
2022-11-24 21:18:28 +04:00
|
|
|
contextItem: ComposeContextItem? = nil,
|
2025-02-03 20:47:32 +00:00
|
|
|
voiceMessageRecordingState: VoiceMessageRecordingState? = nil,
|
|
|
|
mentions: MentionedMembers? = nil
|
2022-04-25 12:44:24 +04:00
|
|
|
) -> ComposeState {
|
|
|
|
ComposeState(
|
|
|
|
message: message ?? self.message,
|
2025-02-03 20:47:32 +00:00
|
|
|
parsedMessage: parsedMessage ?? self.parsedMessage,
|
2022-12-17 14:02:07 +00:00
|
|
|
liveMessage: liveMessage ?? self.liveMessage,
|
2022-04-25 12:44:24 +04:00
|
|
|
preview: preview ?? self.preview,
|
2022-11-24 21:18:28 +04:00
|
|
|
contextItem: contextItem ?? self.contextItem,
|
2025-02-03 20:47:32 +00:00
|
|
|
voiceMessageRecordingState: voiceMessageRecordingState ?? self.voiceMessageRecordingState,
|
|
|
|
mentions: mentions ?? self.mentions
|
2022-04-25 12:44:24 +04:00
|
|
|
)
|
|
|
|
}
|
2025-02-03 20:47:32 +00:00
|
|
|
|
|
|
|
func mentionMemberName(_ name: String) -> String {
|
|
|
|
var n = 0
|
|
|
|
var tryName = name
|
|
|
|
while mentions[tryName] != nil {
|
|
|
|
n += 1
|
|
|
|
tryName = "\(name)_\(n)"
|
|
|
|
}
|
|
|
|
return tryName
|
|
|
|
}
|
|
|
|
|
|
|
|
var memberMentions: [String: Int64] {
|
|
|
|
self.mentions.compactMapValues { $0.memberRef?.groupMemberId }
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-11 12:01:02 +00:00
|
|
|
var quoting: Bool {
|
|
|
|
switch contextItem {
|
|
|
|
case .quotedItem: return true
|
|
|
|
default: return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-16 12:28:39 +04:00
|
|
|
var forwarding: Bool {
|
|
|
|
switch contextItem {
|
2024-09-19 10:04:19 +03:00
|
|
|
case .forwardingItems: return true
|
2024-04-16 12:28:39 +04:00
|
|
|
default: return false
|
|
|
|
}
|
|
|
|
}
|
2025-01-08 18:28:45 +00:00
|
|
|
|
|
|
|
var reporting: Bool {
|
|
|
|
switch contextItem {
|
|
|
|
case .reportedItem: return true
|
|
|
|
default: return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var submittingValidReport: Bool {
|
|
|
|
switch contextItem {
|
|
|
|
case let .reportedItem(_, reason):
|
|
|
|
switch reason {
|
|
|
|
case .other: return !message.isEmpty
|
|
|
|
default: 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 {
|
2023-12-27 18:23:46 +07:00
|
|
|
case let .mediaPreviews(media): return !media.isEmpty
|
2022-11-24 21:18:28 +04:00
|
|
|
case .voicePreview: return voiceMessageRecordingState == .finished
|
|
|
|
case .filePreview: return true
|
2025-01-08 18:28:45 +00:00
|
|
|
default: return !message.isEmpty || forwarding || liveMessage != nil || submittingValidReport
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-11 12:01:02 +00:00
|
|
|
var endLiveDisabled: Bool {
|
|
|
|
liveMessage != nil && message.isEmpty && noPreview && !quoting
|
|
|
|
}
|
|
|
|
|
2022-11-24 21:18:28 +04:00
|
|
|
var linkPreviewAllowed: Bool {
|
2022-04-25 12:44:24 +04:00
|
|
|
switch preview {
|
2023-04-06 20:26:48 +03:00
|
|
|
case .mediaPreviews: return false
|
2022-11-24 21:18:28 +04:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
2022-11-25 15:16:37 +04:00
|
|
|
|
|
|
|
var voicePreview: Bool {
|
|
|
|
switch preview {
|
|
|
|
case .voicePreview: return true
|
|
|
|
default: return false
|
|
|
|
}
|
|
|
|
}
|
2022-12-17 14:02:07 +00:00
|
|
|
|
2024-09-19 10:04:19 +03:00
|
|
|
var manyMediaPreviews: Bool {
|
|
|
|
switch preview {
|
|
|
|
case let .mediaPreviews(mediaPreviews): return mediaPreviews.count > 1
|
|
|
|
default: return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-17 14:02:07 +00:00
|
|
|
var attachmentDisabled: Bool {
|
2025-01-08 18:28:45 +00:00
|
|
|
if editing || forwarding || liveMessage != nil || inProgress || reporting { return true }
|
2022-12-17 14:02:07 +00:00
|
|
|
switch preview {
|
|
|
|
case .noPreview: return false
|
|
|
|
case .linkPreview: return false
|
|
|
|
default: return true
|
|
|
|
}
|
|
|
|
}
|
2023-01-25 08:35:25 +00:00
|
|
|
|
2024-04-12 11:29:58 +01:00
|
|
|
var attachmentPreview: Bool {
|
|
|
|
switch preview {
|
|
|
|
case .noPreview: false
|
|
|
|
case .linkPreview: false
|
|
|
|
case let .mediaPreviews(mediaPreviews): !mediaPreviews.isEmpty
|
|
|
|
case .voicePreview: false
|
|
|
|
case .filePreview: true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-08 18:28:45 +00:00
|
|
|
var placeholder: String? {
|
|
|
|
switch contextItem {
|
|
|
|
case let .reportedItem(_, reason):
|
|
|
|
return reason.text
|
|
|
|
default:
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-25 08:35:25 +00:00
|
|
|
var empty: Bool {
|
2023-01-27 22:09:39 +00:00
|
|
|
message == "" && noPreview
|
2023-01-25 08:35:25 +00:00
|
|
|
}
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
func chatItemPreview(chatItem: ChatItem) -> ComposePreview {
|
|
|
|
switch chatItem.content.msgContent {
|
2022-05-06 21:10:32 +04:00
|
|
|
case .text:
|
2023-09-07 11:28:37 +01:00
|
|
|
return .noPreview
|
2022-04-25 12:44:24 +04:00
|
|
|
case let .link(_, preview: preview):
|
2023-09-07 11:28:37 +01:00
|
|
|
return .linkPreview(linkPreview: preview)
|
2022-11-24 21:18:28 +04:00
|
|
|
case let .image(_, image):
|
2023-09-07 11:28:37 +01:00
|
|
|
return .mediaPreviews(mediaPreviews: [(image, nil)])
|
2023-04-06 20:26:48 +03:00
|
|
|
case let .video(_, image, _):
|
2023-09-07 11:28:37 +01:00
|
|
|
return .mediaPreviews(mediaPreviews: [(image, nil)])
|
2022-11-24 21:18:28 +04:00
|
|
|
case let .voice(_, duration):
|
2023-09-07 11:28:37 +01:00
|
|
|
return .voicePreview(recordingFileName: chatItem.file?.fileName ?? "", duration: duration)
|
2022-05-06 21:10:32 +04:00
|
|
|
case .file:
|
2023-01-27 22:09:39 +00:00
|
|
|
let fileName = chatItem.file?.fileName ?? ""
|
2023-09-07 11:28:37 +01:00
|
|
|
return .filePreview(fileName: fileName, file: getAppFilePath(fileName))
|
2022-04-25 12:44:24 +04:00
|
|
|
default:
|
2023-09-07 11:28:37 +01:00
|
|
|
return .noPreview
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-24 00:22:12 +03:00
|
|
|
enum UploadContent: Equatable {
|
|
|
|
case simpleImage(image: UIImage)
|
|
|
|
case animatedImage(image: UIImage)
|
2023-04-06 20:26:48 +03:00
|
|
|
case video(image: UIImage, url: URL, duration: Int)
|
2022-12-24 00:22:12 +03:00
|
|
|
|
|
|
|
var uiImage: UIImage {
|
|
|
|
switch self {
|
|
|
|
case let .simpleImage(image): return image
|
|
|
|
case let .animatedImage(image): return image
|
2023-04-06 20:26:48 +03:00
|
|
|
case let .video(image, _, _): return image
|
2022-12-24 00:22:12 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static func loadFromURL(url: URL) -> UploadContent? {
|
|
|
|
do {
|
|
|
|
let data = try Data(contentsOf: url)
|
|
|
|
if let image = UIImage(data: data) {
|
|
|
|
try image.setGifFromData(data, levelOfIntegrity: 1.0)
|
|
|
|
logger.log("UploadContent: added animated image")
|
|
|
|
return .animatedImage(image: image)
|
|
|
|
} else { return nil }
|
|
|
|
} catch {
|
|
|
|
do {
|
|
|
|
if let image = try UIImage(data: Data(contentsOf: url)) {
|
|
|
|
logger.log("UploadContent: added simple image")
|
|
|
|
return .simpleImage(image: image)
|
|
|
|
}
|
|
|
|
} catch {}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2023-04-06 20:26:48 +03:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2022-12-24 00:22:12 +03:00
|
|
|
}
|
|
|
|
|
2022-04-25 12:44:24 +04:00
|
|
|
struct ComposeView: View {
|
|
|
|
@EnvironmentObject var chatModel: ChatModel
|
2024-07-03 22:42:13 +01:00
|
|
|
@EnvironmentObject var theme: AppTheme
|
2022-11-25 15:16:37 +04:00
|
|
|
@ObservedObject var chat: Chat
|
2022-04-25 12:44:24 +04:00
|
|
|
@Binding var composeState: ComposeState
|
2023-07-10 13:53:46 +01:00
|
|
|
@Binding var keyboardVisible: Bool
|
2025-02-28 21:45:24 +07:00
|
|
|
@Binding var keyboardHiddenDate: Date
|
2025-02-03 20:47:32 +00:00
|
|
|
@Binding var selectedRange: NSRange
|
2022-04-25 12:44:24 +04:00
|
|
|
|
2022-04-08 18:17:10 +01:00
|
|
|
@State var linkUrl: URL? = nil
|
2024-04-12 11:29:58 +01:00
|
|
|
@State var hasSimplexLink: Bool = false
|
2022-04-08 18:17:10 +01:00
|
|
|
@State var prevLinkUrl: URL? = nil
|
|
|
|
@State var pendingLinkUrl: URL? = nil
|
|
|
|
@State var cancelledLinks: Set<String> = []
|
|
|
|
|
2023-08-14 22:55:18 +01:00
|
|
|
@Environment(\.colorScheme) private var colorScheme
|
2022-04-19 12:29:03 +04:00
|
|
|
@State private var showChooseSource = false
|
2023-04-06 20:26:48 +03:00
|
|
|
@State private var showMediaPicker = false
|
2022-05-18 21:32:30 +04:00
|
|
|
@State private var showTakePhoto = false
|
2023-04-06 20:26:48 +03:00
|
|
|
@State var chosenMedia: [UploadContent] = []
|
2022-05-06 21:10:32 +04:00
|
|
|
@State private var showFileImporter = false
|
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-11-29 15:23:54 +04:00
|
|
|
// for some reason voice message preview playback occasionally
|
|
|
|
// fails to stop on ComposeVoiceView.playbackMode().onDisappear,
|
|
|
|
// this is a workaround to fire an explicit event in certain cases
|
|
|
|
@State private var stopPlayback: Bool = false
|
2022-12-17 14:02:07 +00:00
|
|
|
|
2024-10-18 14:35:27 +03:00
|
|
|
@UserDefault(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true
|
|
|
|
@UserDefault(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial
|
2023-08-17 15:04:17 +04:00
|
|
|
|
2022-03-17 09:42:59 +00:00
|
|
|
var body: some View {
|
|
|
|
VStack(spacing: 0) {
|
2024-07-15 13:14:14 +01:00
|
|
|
Divider()
|
2023-09-20 12:26:16 +04:00
|
|
|
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
|
|
|
|
ContextInvitingContactMemberView()
|
2024-07-15 13:14:14 +01:00
|
|
|
Divider()
|
2023-09-20 12:26:16 +04:00
|
|
|
}
|
2025-01-08 18:28:45 +00:00
|
|
|
|
|
|
|
if case let .reportedItem(_, reason) = composeState.contextItem {
|
|
|
|
reportReasonView(reason)
|
|
|
|
Divider()
|
|
|
|
}
|
2024-06-19 13:49:44 +04:00
|
|
|
// preference checks should match checks in forwarding list
|
2024-04-12 11:29:58 +01:00
|
|
|
let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks)
|
|
|
|
let fileProhibited = composeState.attachmentPreview && !chat.groupFeatureEnabled(.files)
|
|
|
|
let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice)
|
|
|
|
if simplexLinkProhibited {
|
|
|
|
msgNotAllowedView("SimpleX links not allowed", icon: "link")
|
2024-07-15 13:14:14 +01:00
|
|
|
Divider()
|
2024-04-12 11:29:58 +01:00
|
|
|
} else if fileProhibited {
|
|
|
|
msgNotAllowedView("Files and media not allowed", icon: "doc")
|
2024-07-15 13:14:14 +01:00
|
|
|
Divider()
|
2024-04-12 11:29:58 +01:00
|
|
|
} else if voiceProhibited {
|
|
|
|
msgNotAllowedView("Voice messages not allowed", icon: "mic")
|
2024-07-15 13:14:14 +01:00
|
|
|
Divider()
|
2024-04-12 11:29:58 +01:00
|
|
|
}
|
2022-04-25 12:44:24 +04:00
|
|
|
contextItemView()
|
2022-11-24 21:18:28 +04:00
|
|
|
switch (composeState.editing, composeState.preview) {
|
2023-01-10 19:12:48 +00:00
|
|
|
case (true, .filePreview): EmptyView()
|
|
|
|
case (true, .voicePreview): EmptyView() // ? we may allow playback when editing is allowed
|
|
|
|
default: previewView()
|
2022-05-06 21:10:32 +04:00
|
|
|
}
|
2022-04-25 12:44:24 +04:00
|
|
|
HStack (alignment: .bottom) {
|
2023-06-27 07:55:33 +01:00
|
|
|
let b = Button {
|
2022-04-25 12:44:24 +04:00
|
|
|
showChooseSource = true
|
|
|
|
} label: {
|
|
|
|
Image(systemName: "paperclip")
|
|
|
|
.resizable()
|
|
|
|
}
|
2023-09-20 12:26:16 +04:00
|
|
|
.disabled(composeState.attachmentDisabled || !chat.userCanSend || (chat.chatInfo.contact?.nextSendGrpInv ?? false))
|
2022-04-25 12:44:24 +04:00
|
|
|
.frame(width: 25, height: 25)
|
2025-02-22 19:15:33 +07:00
|
|
|
.padding(.bottom, 16)
|
2022-04-25 12:44:24 +04:00
|
|
|
.padding(.leading, 12)
|
2024-07-03 22:42:13 +01:00
|
|
|
.tint(theme.colors.primary)
|
2023-06-27 07:55:33 +01:00
|
|
|
if case let .group(g) = chat.chatInfo,
|
2024-04-12 11:29:58 +01:00
|
|
|
!g.fullGroupPreferences.files.on(for: g.membership) {
|
2023-06-27 07:55:33 +01:00
|
|
|
b.disabled(true).onTapGesture {
|
|
|
|
AlertManager.shared.showAlertMsg(
|
|
|
|
title: "Files and media prohibited!",
|
|
|
|
message: "Only group owners can enable files and media."
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
b
|
|
|
|
}
|
2023-03-06 13:54:43 +00:00
|
|
|
ZStack(alignment: .leading) {
|
|
|
|
SendMessageView(
|
|
|
|
composeState: $composeState,
|
2025-02-03 20:47:32 +00:00
|
|
|
selectedRange: $selectedRange,
|
2023-05-15 16:07:55 +04:00
|
|
|
sendMessage: { ttl in
|
|
|
|
sendMessage(ttl: ttl)
|
2023-03-06 13:54:43 +00:00
|
|
|
resetLinkPreview()
|
|
|
|
},
|
2024-01-18 22:57:14 +07:00
|
|
|
sendLiveMessage: chat.chatInfo.chatType != .local ? sendLiveMessage : nil,
|
2023-03-06 13:54:43 +00:00
|
|
|
updateLiveMessage: updateLiveMessage,
|
|
|
|
cancelLiveMessage: {
|
|
|
|
composeState.liveMessage = nil
|
|
|
|
chatModel.removeLiveDummy()
|
|
|
|
},
|
2023-09-20 12:26:16 +04:00
|
|
|
nextSendGrpInv: chat.chatInfo.contact?.nextSendGrpInv ?? false,
|
2023-03-06 13:54:43 +00:00
|
|
|
voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice),
|
2024-04-12 11:29:58 +01:00
|
|
|
disableSendButton: simplexLinkProhibited || fileProhibited || voiceProhibited,
|
2023-03-06 13:54:43 +00:00
|
|
|
showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert,
|
|
|
|
startVoiceMessageRecording: {
|
|
|
|
Task {
|
|
|
|
await startVoiceMessageRecording()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
finishVoiceMessageRecording: finishVoiceMessageRecording,
|
|
|
|
allowVoiceMessagesToContact: allowVoiceMessagesToContact,
|
2023-06-20 10:13:16 +04:00
|
|
|
timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages),
|
2023-04-06 20:26:48 +03:00
|
|
|
onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }},
|
2023-08-14 22:55:18 +01:00
|
|
|
keyboardVisible: $keyboardVisible,
|
2025-02-28 21:45:24 +07:00
|
|
|
keyboardHiddenDate: $keyboardHiddenDate,
|
2023-08-14 22:55:18 +01:00
|
|
|
sendButtonColor: chat.chatInfo.incognito
|
|
|
|
? .indigo.opacity(colorScheme == .dark ? 1 : 0.7)
|
2024-07-03 22:42:13 +01:00
|
|
|
: theme.colors.primary
|
2023-03-06 13:54:43 +00:00
|
|
|
)
|
|
|
|
.padding(.trailing, 12)
|
|
|
|
.disabled(!chat.userCanSend)
|
|
|
|
|
2023-03-16 23:29:47 +00:00
|
|
|
if chat.userIsObserver {
|
2023-03-06 13:54:43 +00:00
|
|
|
Text("you are observer")
|
|
|
|
.italic()
|
2024-07-03 22:42:13 +01:00
|
|
|
.foregroundColor(theme.colors.secondary)
|
2023-03-06 13:54:43 +00:00
|
|
|
.padding(.horizontal, 12)
|
|
|
|
.onTapGesture {
|
|
|
|
AlertManager.shared.showAlertMsg(
|
|
|
|
title: "You can't send messages!",
|
|
|
|
message: "Please contact group admin."
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-04-19 12:29:03 +04:00
|
|
|
}
|
2022-03-17 09:42:59 +00:00
|
|
|
}
|
2024-08-15 20:43:30 +03:00
|
|
|
.background {
|
|
|
|
Color.clear
|
|
|
|
.overlay(ToolbarMaterial.material(toolbarMaterial))
|
|
|
|
.ignoresSafeArea(.all, edges: .bottom)
|
|
|
|
}
|
2024-04-12 11:29:58 +01:00
|
|
|
.onChange(of: composeState.message) { msg in
|
2025-02-03 20:47:32 +00:00
|
|
|
let parsedMsg = parseSimpleXMarkdown(msg)
|
|
|
|
composeState = composeState.copy(parsedMessage: parsedMsg ?? FormattedText.plain(msg))
|
2022-11-24 21:18:28 +04:00
|
|
|
if composeState.linkPreviewAllowed {
|
2024-04-12 11:29:58 +01:00
|
|
|
if msg.count > 0 {
|
2025-02-03 20:47:32 +00:00
|
|
|
showLinkPreview(parsedMsg)
|
2022-04-25 12:44:24 +04:00
|
|
|
} else {
|
|
|
|
resetLinkPreview()
|
2024-04-12 11:29:58 +01:00
|
|
|
hasSimplexLink = false
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
2024-04-12 11:29:58 +01:00
|
|
|
} else if msg.count > 0 && !chat.groupFeatureEnabled(.simplexLinks) {
|
2025-02-03 20:47:32 +00:00
|
|
|
(_, hasSimplexLink) = getSimplexLink(parsedMsg)
|
2024-04-12 11:29:58 +01:00
|
|
|
} else {
|
|
|
|
hasSimplexLink = false
|
2022-04-08 18:17:10 +01:00
|
|
|
}
|
2022-03-25 22:26:05 +04:00
|
|
|
}
|
2023-03-06 13:54:43 +00:00
|
|
|
.onChange(of: chat.userCanSend) { canSend in
|
|
|
|
if !canSend {
|
|
|
|
cancelCurrentVoiceRecording()
|
|
|
|
clearCurrentDraft()
|
|
|
|
clearState()
|
|
|
|
}
|
|
|
|
}
|
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") {
|
2023-04-06 20:26:48 +03:00
|
|
|
showMediaPicker = true
|
2022-04-19 12:29:03 +04:00
|
|
|
}
|
2022-05-30 09:03:56 +01:00
|
|
|
if UIPasteboard.general.hasImages {
|
|
|
|
Button("Paste image") {
|
2022-12-24 00:22:12 +03:00
|
|
|
UIPasteboard.general.itemProviders.forEach { p in
|
|
|
|
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) {
|
2023-04-06 20:26:48 +03:00
|
|
|
chosenMedia.append(image)
|
2022-12-24 00:22:12 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
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)
|
2023-04-06 20:26:48 +03:00
|
|
|
CameraImageListPicker(images: $chosenMedia)
|
2022-04-19 12:29:03 +04:00
|
|
|
}
|
|
|
|
}
|
2023-04-06 20:26:48 +03:00
|
|
|
.sheet(isPresented: $showMediaPicker) {
|
2023-12-27 18:23:46 +07:00
|
|
|
LibraryMediaListPicker(addMedia: addMediaContent, selectionLimit: 10, finishedPreprocessing: finishedPreprocessingMediaContent) { itemsSelected in
|
2023-12-12 09:04:48 +00:00
|
|
|
await MainActor.run {
|
|
|
|
showMediaPicker = false
|
|
|
|
if itemsSelected {
|
2023-04-06 20:26:48 +03:00
|
|
|
composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: []))
|
2022-10-10 10:40:30 +01:00
|
|
|
}
|
|
|
|
}
|
2022-05-18 21:32:30 +04:00
|
|
|
}
|
|
|
|
}
|
2023-04-06 20:26:48 +03:00
|
|
|
.onChange(of: chosenMedia) { selected in
|
2022-10-10 10:40:30 +01:00
|
|
|
Task {
|
2023-04-06 20:26:48 +03:00
|
|
|
var media: [(String, UploadContent)] = []
|
|
|
|
for content in selected {
|
2024-10-05 22:11:57 +03:00
|
|
|
if let img = await resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
|
2023-04-06 20:26:48 +03:00
|
|
|
media.append((img, content))
|
2022-10-10 10:40:30 +01:00
|
|
|
await MainActor.run {
|
2023-04-06 20:26:48 +03:00
|
|
|
composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: media))
|
2022-10-10 10:40:30 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-04-06 20:26:48 +03:00
|
|
|
if media.count == 0 {
|
2022-10-10 10:40:30 +01:00
|
|
|
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,
|
2023-03-28 22:20:06 +04:00
|
|
|
fileSize <= maxFileSize {
|
2023-01-27 22:09:39 +00:00
|
|
|
composeState = composeState.copy(preview: .filePreview(fileName: fileURL.lastPathComponent, file: fileURL))
|
2022-05-06 21:10:32 +04:00
|
|
|
} else {
|
2023-03-28 22:20:06 +04:00
|
|
|
let prettyMaxFileSize = ByteCountFormatter.string(fromByteCount: maxFileSize, countStyle: .binary)
|
2022-05-06 21:10:32 +04:00
|
|
|
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 {
|
2023-01-27 22:09:39 +00:00
|
|
|
if composeState.liveMessage != nil
|
|
|
|
&& (!composeState.message.isEmpty || composeState.liveMessage?.sentMsg != nil) {
|
|
|
|
cancelCurrentVoiceRecording()
|
|
|
|
clearCurrentDraft()
|
2023-05-15 16:07:55 +04:00
|
|
|
sendMessage(ttl: nil)
|
2022-12-17 14:02:07 +00:00
|
|
|
resetLinkPreview()
|
2023-05-03 15:57:10 +03:00
|
|
|
} else if (composeState.inProgress) {
|
|
|
|
clearCurrentDraft()
|
2023-01-27 22:09:39 +00:00
|
|
|
} else if !composeState.empty {
|
2023-08-17 15:04:17 +04:00
|
|
|
if case .recording = composeState.voiceMessageRecordingState {
|
|
|
|
finishVoiceMessageRecording()
|
|
|
|
if let fileName = composeState.voiceMessageRecordingFileName {
|
|
|
|
chatModel.filesToDelete.insert(getAppFilePath(fileName))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if saveLastDraft {
|
|
|
|
saveCurrentDraft()
|
|
|
|
}
|
2023-01-27 22:09:39 +00:00
|
|
|
} else {
|
|
|
|
cancelCurrentVoiceRecording()
|
|
|
|
clearCurrentDraft()
|
|
|
|
clearState()
|
2022-12-17 14:02:07 +00:00
|
|
|
}
|
2023-01-10 19:12:48 +00:00
|
|
|
chatModel.removeLiveDummy(animated: false)
|
2022-11-24 21:18:28 +04:00
|
|
|
}
|
|
|
|
.onChange(of: chatModel.stopPreviousRecPlay) { _ in
|
|
|
|
if !startingRecording {
|
|
|
|
if composeState.voiceMessageRecordingState == .recording {
|
|
|
|
finishVoiceMessageRecording()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
startingRecording = false
|
|
|
|
}
|
|
|
|
}
|
2022-12-22 21:01:29 +00:00
|
|
|
.onChange(of: chat.chatInfo.featureEnabled(.voice)) { vmAllowed in
|
2022-11-29 15:23:54 +04:00
|
|
|
if !vmAllowed && composeState.voicePreview,
|
|
|
|
let fileName = composeState.voiceMessageRecordingFileName {
|
|
|
|
cancelVoiceMessageRecording(fileName)
|
2023-01-27 22:09:39 +00:00
|
|
|
clearState()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.onAppear {
|
|
|
|
if case let .voicePreview(_, duration) = composeState.preview {
|
|
|
|
voiceMessageRecordingTime = TimeInterval(duration)
|
2022-11-29 15:23:54 +04:00
|
|
|
}
|
|
|
|
}
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
|
2023-12-12 09:04:48 +00:00
|
|
|
private func addMediaContent(_ content: UploadContent) async {
|
2024-10-05 22:11:57 +03:00
|
|
|
if let img = await resizeImageToStrSize(content.uiImage, maxDataSize: 14000) {
|
2023-12-12 09:04:48 +00:00
|
|
|
var newMedia: [(String, UploadContent?)] = []
|
|
|
|
if case var .mediaPreviews(media) = composeState.preview {
|
|
|
|
media.append((img, content))
|
|
|
|
newMedia = media
|
|
|
|
} else {
|
|
|
|
newMedia = [(img, content)]
|
|
|
|
}
|
|
|
|
await MainActor.run {
|
|
|
|
composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: newMedia))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-27 18:23:46 +07:00
|
|
|
// When error occurs while converting video, remove media preview
|
|
|
|
private func finishedPreprocessingMediaContent() {
|
|
|
|
if case let .mediaPreviews(media) = composeState.preview, media.isEmpty {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
composeState = composeState.copy(preview: .noPreview)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-28 22:20:06 +04:00
|
|
|
private var maxFileSize: Int64 {
|
2023-04-19 10:41:01 +02:00
|
|
|
getMaxFileSize(.xftp)
|
2023-03-28 22:20:06 +04:00
|
|
|
}
|
|
|
|
|
2022-12-17 14:02:07 +00:00
|
|
|
private func sendLiveMessage() async {
|
|
|
|
let typedMsg = composeState.message
|
2023-01-11 12:01:02 +00:00
|
|
|
let lm = composeState.liveMessage
|
|
|
|
if (composeState.sendEnabled || composeState.quoting)
|
|
|
|
&& (lm == nil || lm?.sentMsg == nil),
|
2023-05-15 16:07:55 +04:00
|
|
|
let ci = await sendMessageAsync(typedMsg, live: true, ttl: nil) {
|
2022-12-17 14:02:07 +00:00
|
|
|
await MainActor.run {
|
2023-01-11 12:01:02 +00:00
|
|
|
composeState = composeState.copy(liveMessage: LiveMessage(chatItem: ci, typedMsg: typedMsg, sentMsg: typedMsg))
|
2023-01-10 19:12:48 +00:00
|
|
|
}
|
2023-01-11 12:01:02 +00:00
|
|
|
} else if lm == nil {
|
|
|
|
let cItem = chatModel.addLiveDummy(chat.chatInfo)
|
2023-01-10 19:12:48 +00:00
|
|
|
await MainActor.run {
|
|
|
|
composeState = composeState.copy(liveMessage: LiveMessage(chatItem: cItem, typedMsg: typedMsg, sentMsg: nil))
|
|
|
|
}
|
2022-12-17 14:02:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateLiveMessage() async {
|
|
|
|
let typedMsg = composeState.message
|
|
|
|
if let liveMessage = composeState.liveMessage {
|
|
|
|
if let sentMsg = liveMessageToSend(liveMessage, typedMsg),
|
2023-05-15 16:07:55 +04:00
|
|
|
let ci = await sendMessageAsync(sentMsg, live: true, ttl: nil) {
|
2022-12-17 14:02:07 +00:00
|
|
|
await MainActor.run {
|
|
|
|
composeState = composeState.copy(liveMessage: LiveMessage(chatItem: ci, typedMsg: typedMsg, sentMsg: sentMsg))
|
|
|
|
}
|
|
|
|
} else if liveMessage.typedMsg != typedMsg {
|
|
|
|
await MainActor.run {
|
|
|
|
var lm = liveMessage
|
|
|
|
lm.typedMsg = typedMsg
|
|
|
|
composeState = composeState.copy(liveMessage: lm)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-18 21:20:39 +00:00
|
|
|
private func liveMessageToSend(_ lm: LiveMessage, _ t: String) -> String? {
|
|
|
|
let s = t != lm.typedMsg ? truncateToWords(t) : t
|
2023-01-10 19:12:48 +00:00
|
|
|
return s != lm.sentMsg && (lm.sentMsg != nil || !s.isEmpty) ? s : nil
|
2022-12-17 14:02:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private func truncateToWords(_ s: String) -> String {
|
2022-12-18 21:20:39 +00:00
|
|
|
var acc = ""
|
|
|
|
var word = ""
|
|
|
|
for c in s {
|
|
|
|
if c.isLetter || c.isNumber {
|
|
|
|
word = word + String(c)
|
|
|
|
} else {
|
|
|
|
acc = acc + word + String(c)
|
|
|
|
word = ""
|
2022-12-17 14:02:07 +00:00
|
|
|
}
|
|
|
|
}
|
2022-12-18 21:20:39 +00:00
|
|
|
return acc
|
2022-12-17 14:02:07 +00:00
|
|
|
}
|
|
|
|
|
2022-04-25 12:44:24 +04:00
|
|
|
@ViewBuilder func previewView() -> some View {
|
|
|
|
switch composeState.preview {
|
|
|
|
case .noPreview:
|
|
|
|
EmptyView()
|
|
|
|
case let .linkPreview(linkPreview: preview):
|
2023-05-03 15:57:10 +03:00
|
|
|
ComposeLinkView(
|
|
|
|
linkPreview: preview,
|
|
|
|
cancelPreview: cancelLinkPreview,
|
|
|
|
cancelEnabled: !composeState.inProgress
|
|
|
|
)
|
2024-07-15 13:14:14 +01:00
|
|
|
Divider()
|
2023-04-06 20:26:48 +03:00
|
|
|
case let .mediaPreviews(mediaPreviews: media):
|
2022-04-25 12:44:24 +04:00
|
|
|
ComposeImageView(
|
2023-04-06 20:26:48 +03:00
|
|
|
images: media.map { (img, _) in img },
|
2022-05-06 21:10:32 +04:00
|
|
|
cancelImage: {
|
|
|
|
composeState = composeState.copy(preview: .noPreview)
|
2023-04-06 20:26:48 +03:00
|
|
|
chosenMedia = []
|
2022-05-06 21:10:32 +04:00
|
|
|
},
|
2023-05-03 15:57:10 +03:00
|
|
|
cancelEnabled: !composeState.editing && !composeState.inProgress)
|
2024-07-15 13:14:14 +01:00
|
|
|
Divider()
|
2022-11-24 21:18:28 +04:00
|
|
|
case let .voicePreview(recordingFileName, _):
|
|
|
|
ComposeVoiceView(
|
|
|
|
recordingFileName: recordingFileName,
|
|
|
|
recordingTime: $voiceMessageRecordingTime,
|
|
|
|
recordingState: $composeState.voiceMessageRecordingState,
|
2023-01-27 22:09:39 +00:00
|
|
|
cancelVoiceMessage: {
|
|
|
|
cancelVoiceMessageRecording($0)
|
|
|
|
clearState()
|
|
|
|
},
|
2023-05-03 15:57:10 +03:00
|
|
|
cancelEnabled: !composeState.editing && !composeState.inProgress,
|
2022-11-29 15:23:54 +04:00
|
|
|
stopPlayback: $stopPlayback
|
2022-11-24 21:18:28 +04:00
|
|
|
)
|
2024-07-15 13:14:14 +01:00
|
|
|
Divider()
|
2023-01-27 22:09:39 +00:00
|
|
|
case let .filePreview(fileName, _):
|
2022-05-06 21:10:32 +04:00
|
|
|
ComposeFileView(
|
|
|
|
fileName: fileName,
|
|
|
|
cancelFile: {
|
|
|
|
composeState = composeState.copy(preview: .noPreview)
|
|
|
|
},
|
2023-05-03 15:57:10 +03:00
|
|
|
cancelEnabled: !composeState.editing && !composeState.inProgress)
|
2024-07-15 13:14:14 +01:00
|
|
|
Divider()
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-12 11:29:58 +01:00
|
|
|
private func msgNotAllowedView(_ reason: LocalizedStringKey, icon: String) -> some View {
|
|
|
|
HStack {
|
2024-07-03 22:42:13 +01:00
|
|
|
Image(systemName: icon).foregroundColor(theme.colors.secondary)
|
2024-04-12 11:29:58 +01:00
|
|
|
Text(reason).italic()
|
|
|
|
}
|
|
|
|
.padding(12)
|
2024-07-15 13:14:14 +01:00
|
|
|
.frame(minHeight: 54)
|
2024-04-12 11:29:58 +01:00
|
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
2024-07-15 13:14:14 +01:00
|
|
|
.background(.thinMaterial)
|
2024-04-12 11:29:58 +01:00
|
|
|
}
|
2025-01-08 18:28:45 +00:00
|
|
|
|
|
|
|
|
|
|
|
private func reportReasonView(_ reason: ReportReason) -> some View {
|
|
|
|
let reportText = switch reason {
|
|
|
|
case .spam: NSLocalizedString("Report spam: only group moderators will see it.", comment: "report reason")
|
|
|
|
case .profile: NSLocalizedString("Report member profile: only group moderators will see it.", comment: "report reason")
|
|
|
|
case .community: NSLocalizedString("Report violation: only group moderators will see it.", comment: "report reason")
|
|
|
|
case .illegal: NSLocalizedString("Report content: only group moderators will see it.", comment: "report reason")
|
|
|
|
case .other: NSLocalizedString("Report other: only group moderators will see it.", comment: "report reason")
|
|
|
|
case .unknown: "" // Should never happen
|
|
|
|
}
|
|
|
|
|
|
|
|
return Text(reportText)
|
|
|
|
.italic()
|
|
|
|
.font(.caption)
|
|
|
|
.padding(12)
|
|
|
|
.frame(minHeight: 44)
|
|
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
|
|
.background(.thinMaterial)
|
|
|
|
}
|
|
|
|
|
2024-04-12 11:29:58 +01:00
|
|
|
|
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(
|
2023-10-31 09:44:57 +00:00
|
|
|
chat: chat,
|
2024-09-19 10:04:19 +03:00
|
|
|
contextItems: [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) }
|
|
|
|
)
|
2024-07-15 13:14:14 +01:00
|
|
|
Divider()
|
2022-04-25 12:44:24 +04:00
|
|
|
case let .editingItem(chatItem: editingItem):
|
2022-05-06 21:10:32 +04:00
|
|
|
ContextItemView(
|
2023-10-31 09:44:57 +00:00
|
|
|
chat: chat,
|
2024-09-19 10:04:19 +03:00
|
|
|
contextItems: [editingItem],
|
2022-05-09 09:12:32 +01:00
|
|
|
contextIcon: "pencil",
|
2022-05-06 21:10:32 +04:00
|
|
|
cancelContextItem: { clearState() }
|
|
|
|
)
|
2024-07-15 13:14:14 +01:00
|
|
|
Divider()
|
2024-09-19 10:04:19 +03:00
|
|
|
case let .forwardingItems(chatItems, _):
|
2024-04-16 12:28:39 +04:00
|
|
|
ContextItemView(
|
|
|
|
chat: chat,
|
2024-09-19 10:04:19 +03:00
|
|
|
contextItems: chatItems,
|
2024-04-16 12:28:39 +04:00
|
|
|
contextIcon: "arrowshape.turn.up.forward",
|
2024-09-19 10:04:19 +03:00
|
|
|
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }
|
2024-04-16 12:28:39 +04:00
|
|
|
)
|
2024-07-15 13:14:14 +01:00
|
|
|
Divider()
|
2025-01-08 18:28:45 +00:00
|
|
|
case let .reportedItem(chatItem: reportedItem, _):
|
|
|
|
ContextItemView(
|
|
|
|
chat: chat,
|
|
|
|
contextItems: [reportedItem],
|
|
|
|
contextIcon: "flag",
|
|
|
|
cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) },
|
|
|
|
contextIconForeground: Color.red
|
|
|
|
)
|
|
|
|
Divider()
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-15 16:07:55 +04:00
|
|
|
private func sendMessage(ttl: Int?) {
|
2022-04-25 12:44:24 +04:00
|
|
|
logger.debug("ChatView sendMessage")
|
|
|
|
Task {
|
|
|
|
logger.debug("ChatView sendMessage: in Task")
|
2023-05-15 16:07:55 +04:00
|
|
|
_ = await sendMessageAsync(nil, live: false, ttl: ttl)
|
2022-12-17 14:02:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-15 16:07:55 +04:00
|
|
|
private func sendMessageAsync(_ text: String?, live: Bool, ttl: Int?) async -> ChatItem? {
|
2022-12-17 14:02:07 +00:00
|
|
|
var sent: ChatItem?
|
|
|
|
let msgText = text ?? composeState.message
|
2022-12-31 10:25:32 +00:00
|
|
|
let liveMessage = composeState.liveMessage
|
2025-02-03 20:47:32 +00:00
|
|
|
let mentions = composeState.memberMentions
|
2022-12-31 10:25:32 +00:00
|
|
|
if !live {
|
|
|
|
if liveMessage != nil { composeState = composeState.copy(liveMessage: nil) }
|
|
|
|
await sending()
|
|
|
|
}
|
2023-09-20 12:26:16 +04:00
|
|
|
if chat.chatInfo.contact?.nextSendGrpInv ?? false {
|
|
|
|
await sendMemberContactInvitation()
|
2024-09-19 10:04:19 +03:00
|
|
|
} else if case let .forwardingItems(chatItems, fromChatInfo) = composeState.contextItem {
|
|
|
|
// Composed text is send as a reply to the last forwarded item
|
|
|
|
sent = await forwardItems(chatItems, fromChatInfo, ttl).last
|
2024-04-16 12:28:39 +04:00
|
|
|
if !composeState.message.isEmpty {
|
2025-02-03 20:47:32 +00:00
|
|
|
_ = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl, mentions: mentions)
|
2024-04-16 12:28:39 +04:00
|
|
|
}
|
2023-09-20 12:26:16 +04:00
|
|
|
} else if case let .editingItem(ci) = composeState.contextItem {
|
2022-12-17 14:02:07 +00:00
|
|
|
sent = await updateMessage(ci, live: live)
|
2023-01-10 19:12:48 +00:00
|
|
|
} else if let liveMessage = liveMessage, liveMessage.sentMsg != nil {
|
2022-12-17 14:02:07 +00:00
|
|
|
sent = await updateMessage(liveMessage.chatItem, live: live)
|
2025-01-08 18:28:45 +00:00
|
|
|
} else if case let .reportedItem(chatItem, reason) = composeState.contextItem {
|
|
|
|
sent = await send(reason, chatItemId: chatItem.id)
|
2022-12-17 14:02:07 +00:00
|
|
|
} else {
|
|
|
|
var quoted: Int64? = nil
|
|
|
|
if case let .quotedItem(chatItem: quotedItem) = composeState.contextItem {
|
|
|
|
quoted = quotedItem.id
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (composeState.preview) {
|
|
|
|
case .noPreview:
|
2025-02-03 20:47:32 +00:00
|
|
|
sent = await send(.text(msgText), quoted: quoted, live: live, ttl: ttl, mentions: mentions)
|
2022-12-17 14:02:07 +00:00
|
|
|
case .linkPreview:
|
2025-02-03 20:47:32 +00:00
|
|
|
sent = await send(checkLinkPreview(), quoted: quoted, live: live, ttl: ttl, mentions: mentions)
|
2024-09-19 10:04:19 +03:00
|
|
|
case let .mediaPreviews(media):
|
2025-02-03 20:47:32 +00:00
|
|
|
// TODO: CHECK THIS
|
2023-04-06 20:26:48 +03:00
|
|
|
let last = media.count - 1
|
2024-09-19 10:04:19 +03:00
|
|
|
var msgs: [ComposedMessage] = []
|
2023-01-25 11:48:54 +00:00
|
|
|
if last >= 0 {
|
|
|
|
for i in 0..<last {
|
2024-09-19 10:04:19 +03:00
|
|
|
if i > 0 {
|
|
|
|
// Sleep to allow `progressByTimeout` update be rendered
|
|
|
|
try? await Task.sleep(nanoseconds: 100_000000)
|
|
|
|
}
|
|
|
|
if let (fileSource, msgContent) = mediaContent(media[i], text: "") {
|
|
|
|
msgs.append(ComposedMessage(fileSource: fileSource, msgContent: msgContent))
|
2023-04-06 20:26:48 +03:00
|
|
|
}
|
2023-01-25 11:48:54 +00:00
|
|
|
}
|
2024-09-19 10:04:19 +03:00
|
|
|
if let (fileSource, msgContent) = mediaContent(media[last], text: msgText) {
|
|
|
|
msgs.append(ComposedMessage(fileSource: fileSource, quotedItemId: quoted, msgContent: msgContent))
|
2023-04-06 20:26:48 +03:00
|
|
|
}
|
2022-10-10 10:40:30 +01:00
|
|
|
}
|
2024-09-19 10:04:19 +03:00
|
|
|
if msgs.isEmpty {
|
|
|
|
msgs = [ComposedMessage(quotedItemId: quoted, msgContent: .text(msgText))]
|
2022-12-17 14:02:07 +00:00
|
|
|
}
|
2024-09-19 10:04:19 +03:00
|
|
|
sent = await send(msgs, live: live, ttl: ttl).last
|
|
|
|
|
2022-12-17 14:02:07 +00:00
|
|
|
case let .voicePreview(recordingFileName, duration):
|
|
|
|
stopPlayback.toggle()
|
2023-09-07 11:28:37 +01:00
|
|
|
let file = voiceCryptoFile(recordingFileName)
|
2025-02-03 20:47:32 +00:00
|
|
|
sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: file, ttl: ttl, mentions: mentions)
|
2023-01-27 22:09:39 +00:00
|
|
|
case let .filePreview(_, file):
|
2024-01-16 18:49:44 +07:00
|
|
|
if let savedFile = saveFileFromURL(file) {
|
2025-02-03 20:47:32 +00:00
|
|
|
sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live, ttl: ttl, mentions: mentions)
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
2022-10-10 10:40:30 +01:00
|
|
|
}
|
|
|
|
}
|
2024-04-16 12:28:39 +04:00
|
|
|
await MainActor.run {
|
|
|
|
let wasForwarding = composeState.forwarding
|
|
|
|
clearState(live: live)
|
|
|
|
if wasForwarding,
|
|
|
|
chatModel.draftChatId == chat.chatInfo.id,
|
|
|
|
let draft = chatModel.draft {
|
|
|
|
composeState = draft
|
|
|
|
}
|
|
|
|
}
|
2022-12-17 14:02:07 +00:00
|
|
|
return sent
|
2022-10-10 10:40:30 +01:00
|
|
|
|
2024-09-19 10:04:19 +03:00
|
|
|
func mediaContent(_ media: (String, UploadContent?), text: String) -> (CryptoFile?, MsgContent)? {
|
|
|
|
let (previewImage, uploadContent) = media
|
|
|
|
return switch uploadContent {
|
|
|
|
case let .simpleImage(image):
|
|
|
|
(saveImage(image), .image(text: text, image: previewImage))
|
|
|
|
case let .animatedImage(image):
|
|
|
|
(saveAnimImage(image), .image(text: text, image: previewImage))
|
|
|
|
case let .video(_, url, duration):
|
|
|
|
(moveTempFileFromURL(url), .video(text: text, image: previewImage, duration: duration))
|
|
|
|
case .none:
|
|
|
|
nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-10 10:40:30 +01:00
|
|
|
func sending() async {
|
2023-08-10 23:38:11 +03:00
|
|
|
await MainActor.run { composeState.inProgress = true }
|
2022-10-10 10:40:30 +01:00
|
|
|
}
|
|
|
|
|
2023-09-20 12:26:16 +04:00
|
|
|
func sendMemberContactInvitation() async {
|
|
|
|
do {
|
|
|
|
let mc = checkLinkPreview()
|
|
|
|
let contact = try await apiSendMemberContactInvitation(chat.chatInfo.apiId, mc)
|
|
|
|
await MainActor.run {
|
|
|
|
self.chatModel.updateContact(contact)
|
|
|
|
}
|
|
|
|
} catch {
|
|
|
|
logger.error("ChatView.sendMemberContactInvitation error: \(error.localizedDescription)")
|
|
|
|
AlertManager.shared.showAlertMsg(title: "Error sending member contact invitation", message: "Error: \(responseError(error))")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-17 14:02:07 +00:00
|
|
|
func updateMessage(_ ei: ChatItem, live: Bool) async -> ChatItem? {
|
|
|
|
if let oldMsgContent = ei.content.msgContent {
|
|
|
|
do {
|
|
|
|
let mc = updateMsgContent(oldMsgContent)
|
2023-07-17 16:55:19 +04:00
|
|
|
if mc != oldMsgContent || (ei.meta.itemLive ?? false) {
|
|
|
|
let chatItem = try await apiUpdateChatItem(
|
|
|
|
type: chat.chatInfo.chatType,
|
|
|
|
id: chat.chatInfo.apiId,
|
|
|
|
itemId: ei.id,
|
2025-02-03 20:47:32 +00:00
|
|
|
updatedMessage: UpdatedMessage(msgContent: mc, mentions: composeState.memberMentions),
|
2023-07-17 16:55:19 +04:00
|
|
|
live: live
|
|
|
|
)
|
|
|
|
await MainActor.run {
|
|
|
|
_ = self.chatModel.upsertChatItem(self.chat.chatInfo, chatItem)
|
|
|
|
}
|
|
|
|
return chatItem
|
|
|
|
} else {
|
|
|
|
return nil
|
2022-12-17 14:02:07 +00:00
|
|
|
}
|
|
|
|
} catch {
|
|
|
|
logger.error("ChatView.sendMessage error: \(error.localizedDescription)")
|
|
|
|
AlertManager.shared.showAlertMsg(title: "Error updating message", message: "Error: \(responseError(error))")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateMsgContent(_ msgContent: MsgContent) -> MsgContent {
|
|
|
|
switch msgContent {
|
|
|
|
case .text:
|
|
|
|
return checkLinkPreview()
|
|
|
|
case .link:
|
|
|
|
return checkLinkPreview()
|
|
|
|
case .image(_, let image):
|
|
|
|
return .image(text: msgText, image: image)
|
2023-04-06 20:26:48 +03:00
|
|
|
case .video(_, let image, let duration):
|
|
|
|
return .video(text: msgText, image: image, duration: duration)
|
2022-12-17 14:02:07 +00:00
|
|
|
case .voice(_, let duration):
|
|
|
|
return .voice(text: msgText, duration: duration)
|
|
|
|
case .file:
|
|
|
|
return .file(msgText)
|
2025-01-08 18:28:45 +00:00
|
|
|
case .report(_, let reason):
|
|
|
|
return .report(text: msgText, reason: reason)
|
2022-12-17 14:02:07 +00:00
|
|
|
case .unknown(let type, _):
|
|
|
|
return .unknown(type: type, text: msgText)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-07 11:28:37 +01:00
|
|
|
func voiceCryptoFile(_ fileName: String) -> CryptoFile? {
|
|
|
|
if !privacyEncryptLocalFilesGroupDefault.get() {
|
|
|
|
return CryptoFile.plain(fileName)
|
|
|
|
}
|
|
|
|
let url = getAppFilePath(fileName)
|
|
|
|
let toFile = generateNewFileName("voice", "m4a")
|
|
|
|
let toUrl = getAppFilePath(toFile)
|
|
|
|
if let cfArgs = try? encryptCryptoFile(fromPath: url.path, toPath: toUrl.path) {
|
|
|
|
removeFile(url)
|
|
|
|
return CryptoFile(filePath: toFile, cryptoArgs: cfArgs)
|
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
2025-01-08 18:28:45 +00:00
|
|
|
|
|
|
|
func send(_ reportReason: ReportReason, chatItemId: Int64) async -> ChatItem? {
|
|
|
|
if let chatItems = await apiReportMessage(
|
|
|
|
groupId: chat.chatInfo.apiId,
|
|
|
|
chatItemId: chatItemId,
|
|
|
|
reportReason: reportReason,
|
|
|
|
reportText: msgText
|
|
|
|
) {
|
|
|
|
await MainActor.run {
|
|
|
|
for chatItem in chatItems {
|
|
|
|
chatModel.addChatItem(chat.chatInfo, chatItem)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return chatItems.first
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2025-02-03 20:47:32 +00:00
|
|
|
func send(_ mc: MsgContent, quoted: Int64?, file: CryptoFile? = nil, live: Bool = false, ttl: Int?, mentions: [String: Int64]) async -> ChatItem? {
|
2024-09-19 10:04:19 +03:00
|
|
|
await send(
|
2025-02-03 20:47:32 +00:00
|
|
|
[ComposedMessage(fileSource: file, quotedItemId: quoted, msgContent: mc, mentions: mentions)],
|
2024-09-19 10:04:19 +03:00
|
|
|
live: live,
|
|
|
|
ttl: ttl
|
|
|
|
).first
|
|
|
|
}
|
|
|
|
|
|
|
|
func send(_ msgs: [ComposedMessage], live: Bool, ttl: Int?) async -> [ChatItem] {
|
2024-08-22 21:38:22 +04:00
|
|
|
if let chatItems = chat.chatInfo.chatType == .local
|
2024-09-19 10:04:19 +03:00
|
|
|
? await apiCreateChatItems(noteFolderId: chat.chatInfo.apiId, composedMessages: msgs)
|
2024-08-22 21:38:22 +04:00
|
|
|
: await apiSendMessages(
|
2024-01-18 22:57:14 +07:00
|
|
|
type: chat.chatInfo.chatType,
|
|
|
|
id: chat.chatInfo.apiId,
|
|
|
|
live: live,
|
2024-08-22 21:38:22 +04:00
|
|
|
ttl: ttl,
|
2024-09-19 10:04:19 +03:00
|
|
|
composedMessages: msgs
|
2024-01-18 22:57:14 +07:00
|
|
|
) {
|
2022-10-10 10:40:30 +01:00
|
|
|
await MainActor.run {
|
2023-01-10 19:12:48 +00:00
|
|
|
chatModel.removeLiveDummy(animated: false)
|
2024-08-22 21:38:22 +04:00
|
|
|
for chatItem in chatItems {
|
|
|
|
chatModel.addChatItem(chat.chatInfo, chatItem)
|
|
|
|
}
|
2022-09-21 17:18:48 +04:00
|
|
|
}
|
2024-09-19 10:04:19 +03:00
|
|
|
return chatItems
|
2022-04-19 12:29:03 +04:00
|
|
|
}
|
2024-09-19 10:04:19 +03:00
|
|
|
for msg in msgs {
|
|
|
|
if let file = msg.fileSource {
|
|
|
|
removeFile(file.filePath)
|
|
|
|
}
|
2023-05-23 12:43:18 +04:00
|
|
|
}
|
2024-09-19 10:04:19 +03:00
|
|
|
return []
|
2022-04-19 12:29:03 +04:00
|
|
|
}
|
2022-12-18 21:20:39 +00:00
|
|
|
|
2024-09-19 10:04:19 +03:00
|
|
|
func forwardItems(_ forwardedItems: [ChatItem], _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> [ChatItem] {
|
2024-08-22 21:38:22 +04:00
|
|
|
if let chatItems = await apiForwardChatItems(
|
2024-04-16 12:28:39 +04:00
|
|
|
toChatType: chat.chatInfo.chatType,
|
|
|
|
toChatId: chat.chatInfo.apiId,
|
|
|
|
fromChatType: fromChatInfo.chatType,
|
|
|
|
fromChatId: fromChatInfo.apiId,
|
2024-09-19 10:04:19 +03:00
|
|
|
itemIds: forwardedItems.map { $0.id },
|
2024-05-16 14:12:58 +04:00
|
|
|
ttl: ttl
|
2024-04-16 12:28:39 +04:00
|
|
|
) {
|
|
|
|
await MainActor.run {
|
2024-08-22 21:38:22 +04:00
|
|
|
for chatItem in chatItems {
|
|
|
|
chatModel.addChatItem(chat.chatInfo, chatItem)
|
|
|
|
}
|
2024-09-19 10:04:19 +03:00
|
|
|
if forwardedItems.count != chatItems.count {
|
|
|
|
showAlert(
|
|
|
|
String.localizedStringWithFormat(
|
|
|
|
NSLocalizedString("%d messages not forwarded", comment: "alert title"),
|
|
|
|
forwardedItems.count - chatItems.count
|
|
|
|
),
|
|
|
|
message: NSLocalizedString("Messages were deleted after you selected them.", comment: "alert message")
|
|
|
|
)
|
|
|
|
}
|
2024-04-16 12:28:39 +04:00
|
|
|
}
|
2024-09-19 10:04:19 +03:00
|
|
|
return chatItems
|
|
|
|
} else {
|
|
|
|
return []
|
2024-04-16 12:28:39 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-18 21:20:39 +00:00
|
|
|
func checkLinkPreview() -> MsgContent {
|
|
|
|
switch (composeState.preview) {
|
|
|
|
case let .linkPreview(linkPreview: linkPreview):
|
2025-02-03 20:47:32 +00:00
|
|
|
if let parsedMsg = parseSimpleXMarkdown(msgText),
|
|
|
|
let url = getSimplexLink(parsedMsg).url,
|
2022-12-18 21:20:39 +00:00
|
|
|
let linkPreview = linkPreview,
|
|
|
|
url == linkPreview.uri {
|
|
|
|
return .link(text: msgText, preview: linkPreview)
|
|
|
|
} else {
|
|
|
|
return .text(msgText)
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
return .text(msgText)
|
|
|
|
}
|
|
|
|
}
|
2022-04-25 12:44:24 +04:00
|
|
|
}
|
|
|
|
|
2022-11-24 21:18:28 +04:00
|
|
|
private func startVoiceMessageRecording() async {
|
|
|
|
startingRecording = true
|
|
|
|
let fileName = generateNewFileName("voice", "m4a")
|
2023-04-06 20:26:48 +03:00
|
|
|
chatModel.stopPreviousRecPlay = getAppFilePath(fileName)
|
2022-11-24 21:18:28 +04:00
|
|
|
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:
|
2022-11-29 12:41:48 +04:00
|
|
|
AlertManager.shared.showAlert(Alert(
|
|
|
|
title: Text("No permission to record voice message"),
|
|
|
|
message: Text("To record voice message please grant permission to use Microphone."),
|
|
|
|
primaryButton: .default(Text("Open Settings")) {
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
secondaryButton: .cancel()
|
|
|
|
))
|
2022-11-25 12:30:24 +04:00
|
|
|
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)")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-25 21:05:14 +04:00
|
|
|
private func allowVoiceMessagesToContact() {
|
|
|
|
if case let .direct(contact) = chat.chatInfo {
|
2022-12-22 21:01:29 +00:00
|
|
|
allowFeatureToContact(contact, .voice)
|
2022-11-25 21:05:14 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-24 21:18:28 +04:00
|
|
|
// ? 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
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-01-27 22:09:39 +00:00
|
|
|
private func cancelCurrentVoiceRecording() {
|
|
|
|
if let fileName = composeState.voiceMessageRecordingFileName {
|
|
|
|
cancelVoiceMessageRecording(fileName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-24 21:18:28 +04:00
|
|
|
private func cancelVoiceMessageRecording(_ fileName: String) {
|
2022-11-29 15:23:54 +04:00
|
|
|
stopPlayback.toggle()
|
|
|
|
audioRecorder?.stop()
|
2022-11-24 21:18:28 +04:00
|
|
|
removeFile(fileName)
|
|
|
|
}
|
|
|
|
|
2022-12-17 14:02:07 +00:00
|
|
|
private func clearState(live: Bool = false) {
|
|
|
|
if live {
|
|
|
|
composeState.inProgress = false
|
|
|
|
} else {
|
|
|
|
composeState = ComposeState()
|
|
|
|
resetLinkPreview()
|
|
|
|
}
|
2023-04-06 20:26:48 +03:00
|
|
|
chosenMedia = []
|
2022-11-24 21:18:28 +04:00
|
|
|
audioRecorder = nil
|
|
|
|
voiceMessageRecordingTime = nil
|
2022-11-29 15:23:54 +04:00
|
|
|
startingRecording = false
|
2022-05-06 21:10:32 +04:00
|
|
|
}
|
|
|
|
|
2023-01-27 22:09:39 +00:00
|
|
|
private func saveCurrentDraft() {
|
|
|
|
chatModel.draft = composeState
|
|
|
|
chatModel.draftChatId = chat.id
|
|
|
|
}
|
|
|
|
|
|
|
|
private func clearCurrentDraft() {
|
|
|
|
if chatModel.draftChatId == chat.id {
|
|
|
|
chatModel.draft = nil
|
|
|
|
chatModel.draftChatId = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-03 20:47:32 +00:00
|
|
|
private func showLinkPreview(_ parsedMsg: [FormattedText]?) {
|
2022-04-11 09:39:04 +01:00
|
|
|
prevLinkUrl = linkUrl
|
2025-02-03 20:47:32 +00:00
|
|
|
(linkUrl, hasSimplexLink) = getSimplexLink(parsedMsg)
|
2022-04-11 09:39:04 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-03 20:47:32 +00:00
|
|
|
private func getSimplexLink(_ parsedMsg: [FormattedText]?) -> (url: URL?, hasSimplexLink: Bool) {
|
|
|
|
guard let parsedMsg else { return (nil, false) }
|
2024-04-12 11:29:58 +01:00
|
|
|
let url: URL? = if let uri = parsedMsg.first(where: { ft in
|
2022-06-24 13:52:20 +01:00
|
|
|
ft.format == .uri && !cancelledLinks.contains(ft.text) && !isSimplexLink(ft.text)
|
2024-04-12 11:29:58 +01:00
|
|
|
}) {
|
|
|
|
URL(string: uri.text)
|
|
|
|
} else {
|
|
|
|
nil
|
|
|
|
}
|
2024-06-19 13:49:44 +04:00
|
|
|
let simplexLink = parsedMsgHasSimplexLink(parsedMsg)
|
2024-04-12 11:29:58 +01:00
|
|
|
return (url, simplexLink)
|
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() {
|
2024-01-25 14:58:39 +04:00
|
|
|
if let pendingLink = pendingLinkUrl?.absoluteString {
|
|
|
|
cancelledLinks.insert(pendingLink)
|
|
|
|
}
|
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
|
2025-05-11 15:42:09 +01:00
|
|
|
if let linkPreview, pendingLinkUrl == url {
|
2022-04-25 12:44:24 +04:00
|
|
|
composeState = composeState.copy(preview: .linkPreview(linkPreview: linkPreview))
|
2025-05-11 15:42:09 +01:00
|
|
|
} else {
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
|
|
composeState = composeState.copy(preview: .noPreview)
|
|
|
|
}
|
2022-04-08 18:17:10 +01:00
|
|
|
}
|
2025-05-11 15:42:09 +01:00
|
|
|
pendingLinkUrl = nil
|
2022-04-08 18:17:10 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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-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")
|
2025-02-03 20:47:32 +00:00
|
|
|
@State var selectedRange = NSRange()
|
2022-03-17 09:42:59 +00:00
|
|
|
|
2022-03-25 22:26:05 +04:00
|
|
|
return Group {
|
|
|
|
ComposeView(
|
2022-04-25 12:44:24 +04:00
|
|
|
chat: chat,
|
|
|
|
composeState: $composeState,
|
2025-02-03 20:47:32 +00:00
|
|
|
keyboardVisible: Binding.constant(true),
|
2025-02-28 21:45:24 +07:00
|
|
|
keyboardHiddenDate: Binding.constant(Date.now),
|
2025-02-03 20:47:32 +00:00
|
|
|
selectedRange: $selectedRange
|
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,
|
2025-02-03 20:47:32 +00:00
|
|
|
keyboardVisible: Binding.constant(true),
|
2025-02-28 21:45:24 +07:00
|
|
|
keyboardHiddenDate: Binding.constant(Date.now),
|
2025-02-03 20:47:32 +00:00
|
|
|
selectedRange: $selectedRange
|
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
|
|
|
}
|
|
|
|
}
|