2022-01-29 23:37:02 +00:00
|
|
|
//
|
|
|
|
// SendMessageView.swift
|
|
|
|
// SimpleX
|
|
|
|
//
|
|
|
|
// Created by Evgeny Poberezkin on 29/01/2022.
|
|
|
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import SwiftUI
|
2022-05-31 07:55:13 +01:00
|
|
|
import SimpleXChat
|
2022-01-29 23:37:02 +00:00
|
|
|
|
2023-01-10 19:12:48 +00:00
|
|
|
private let liveMsgInterval: UInt64 = 3000_000000
|
|
|
|
|
2022-01-29 23:37:02 +00:00
|
|
|
struct SendMessageView: View {
|
2022-04-25 12:44:24 +04:00
|
|
|
@Binding var composeState: ComposeState
|
2022-04-27 20:54:21 +04:00
|
|
|
var sendMessage: () -> Void
|
2022-12-17 14:02:07 +00:00
|
|
|
var sendLiveMessage: (() async -> Void)? = nil
|
|
|
|
var updateLiveMessage: (() async -> Void)? = nil
|
2023-01-10 19:12:48 +00:00
|
|
|
var cancelLiveMessage: (() -> Void)? = nil
|
2022-11-26 18:43:49 +04:00
|
|
|
var showVoiceMessageButton: Bool = true
|
2022-11-25 15:16:37 +04:00
|
|
|
var voiceMessageAllowed: Bool = true
|
2022-11-25 21:05:14 +04:00
|
|
|
var showEnableVoiceMessagesAlert: ChatInfo.ShowEnableVoiceMessagesAlert = .other
|
2022-11-24 21:18:28 +04:00
|
|
|
var startVoiceMessageRecording: (() -> Void)? = nil
|
|
|
|
var finishVoiceMessageRecording: (() -> Void)? = nil
|
2022-11-25 21:05:14 +04:00
|
|
|
var allowVoiceMessagesToContact: (() -> Void)? = nil
|
2022-12-24 00:22:12 +03:00
|
|
|
var onImagesAdded: ([UploadContent]) -> Void
|
2022-11-25 21:05:14 +04:00
|
|
|
@State private var holdingVMR = false
|
2022-02-05 14:24:23 +00:00
|
|
|
@Namespace var namespace
|
2022-02-11 07:42:00 +00:00
|
|
|
@FocusState.Binding var keyboardVisible: Bool
|
2022-02-05 14:24:23 +00:00
|
|
|
@State private var teHeight: CGFloat = 42
|
|
|
|
@State private var teFont: Font = .body
|
2022-12-17 12:01:49 +03:00
|
|
|
@State private var teUiFont: UIFont = UIFont.preferredFont(forTextStyle: .body)
|
2022-12-17 14:02:07 +00:00
|
|
|
@State private var sendButtonSize: CGFloat = 29
|
|
|
|
@State private var sendButtonOpacity: CGFloat = 1
|
2022-02-05 14:24:23 +00:00
|
|
|
var maxHeight: CGFloat = 360
|
|
|
|
var minHeight: CGFloat = 37
|
2022-12-17 14:02:07 +00:00
|
|
|
@AppStorage(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
|
2022-01-29 23:37:02 +00:00
|
|
|
|
|
|
|
var body: some View {
|
2022-02-05 14:24:23 +00:00
|
|
|
ZStack {
|
|
|
|
HStack(alignment: .bottom) {
|
|
|
|
ZStack(alignment: .leading) {
|
2022-11-24 21:18:28 +04:00
|
|
|
if case .voicePreview = composeState.preview {
|
|
|
|
Text("Voice message…")
|
|
|
|
.font(teFont.italic())
|
|
|
|
.multilineTextAlignment(.leading)
|
|
|
|
.foregroundColor(.secondary)
|
|
|
|
.padding(.horizontal, 10)
|
|
|
|
.padding(.vertical, 8)
|
|
|
|
.frame(maxWidth: .infinity)
|
|
|
|
} else {
|
|
|
|
let alignment: TextAlignment = isRightToLeft(composeState.message) ? .trailing : .leading
|
|
|
|
Text(composeState.message)
|
|
|
|
.lineLimit(10)
|
|
|
|
.font(teFont)
|
|
|
|
.multilineTextAlignment(alignment)
|
2022-12-17 12:01:49 +03:00
|
|
|
// put text on top (after NativeTextEditor) and set color to precisely align it on changes
|
|
|
|
// .foregroundColor(.red)
|
2022-11-24 21:18:28 +04:00
|
|
|
.foregroundColor(.clear)
|
|
|
|
.padding(.horizontal, 10)
|
2022-12-17 12:01:49 +03:00
|
|
|
.padding(.top, 8)
|
|
|
|
.padding(.bottom, 6)
|
2022-11-24 21:18:28 +04:00
|
|
|
.matchedGeometryEffect(id: "te", in: namespace)
|
|
|
|
.background(GeometryReader(content: updateHeight))
|
2022-12-17 12:01:49 +03:00
|
|
|
|
|
|
|
NativeTextEditor(
|
|
|
|
text: $composeState.message,
|
|
|
|
height: teHeight,
|
|
|
|
font: teUiFont,
|
|
|
|
focused: $keyboardVisible,
|
|
|
|
alignment: alignment,
|
2022-12-24 00:22:12 +03:00
|
|
|
onImagesAdded: onImagesAdded
|
2022-12-17 12:01:49 +03:00
|
|
|
)
|
|
|
|
.allowsTightening(false)
|
|
|
|
.frame(height: teHeight)
|
2022-11-24 21:18:28 +04:00
|
|
|
}
|
2022-02-05 14:24:23 +00:00
|
|
|
}
|
|
|
|
|
2022-04-25 12:44:24 +04:00
|
|
|
if (composeState.inProgress) {
|
2022-02-05 14:24:23 +00:00
|
|
|
ProgressView()
|
|
|
|
.scaleEffect(1.4)
|
|
|
|
.frame(width: 31, height: 31, alignment: .center)
|
|
|
|
.padding([.bottom, .trailing], 3)
|
|
|
|
} else {
|
2022-11-24 21:18:28 +04:00
|
|
|
let vmrs = composeState.voiceMessageRecordingState
|
2022-12-17 14:02:07 +00:00
|
|
|
if showVoiceMessageButton
|
|
|
|
&& composeState.message.isEmpty
|
|
|
|
&& !composeState.editing
|
|
|
|
&& composeState.liveMessage == nil
|
|
|
|
&& ((composeState.noPreview && vmrs == .noRecording)
|
|
|
|
|| (vmrs == .recording && holdingVMR)) {
|
|
|
|
HStack {
|
|
|
|
if voiceMessageAllowed {
|
|
|
|
RecordVoiceMessageButton(
|
|
|
|
startVoiceMessageRecording: startVoiceMessageRecording,
|
|
|
|
finishVoiceMessageRecording: finishVoiceMessageRecording,
|
|
|
|
holdingVMR: $holdingVMR,
|
|
|
|
disabled: composeState.disabled
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
voiceMessageNotAllowedButton()
|
|
|
|
}
|
2023-01-11 12:01:02 +00:00
|
|
|
if let send = sendLiveMessage,
|
|
|
|
let update = updateLiveMessage,
|
|
|
|
case .noContextItem = composeState.contextItem {
|
2022-12-17 14:02:07 +00:00
|
|
|
startLiveMessageButton(send: send, update: update)
|
|
|
|
}
|
2022-11-25 21:05:14 +04:00
|
|
|
}
|
|
|
|
} else if vmrs == .recording && !holdingVMR {
|
2022-11-24 21:18:28 +04:00
|
|
|
finishVoiceMessageRecordingButton()
|
2023-01-10 19:12:48 +00:00
|
|
|
} else if composeState.liveMessage != nil && composeState.liveMessage?.sentMsg == nil && composeState.message.isEmpty {
|
|
|
|
cancelLiveMessageButton {
|
|
|
|
cancelLiveMessage?()
|
|
|
|
}
|
2022-11-24 21:18:28 +04:00
|
|
|
} else {
|
2022-12-21 12:55:59 +00:00
|
|
|
sendMessageButton()
|
2022-02-05 14:24:23 +00:00
|
|
|
}
|
|
|
|
}
|
2022-01-29 23:37:02 +00:00
|
|
|
}
|
2022-02-05 14:24:23 +00:00
|
|
|
|
|
|
|
RoundedRectangle(cornerSize: CGSize(width: 20, height: 20))
|
|
|
|
.strokeBorder(.secondary, lineWidth: 0.3, antialiased: true)
|
|
|
|
.frame(height: teHeight)
|
2022-01-29 23:37:02 +00:00
|
|
|
}
|
2022-02-05 14:24:23 +00:00
|
|
|
.padding(.vertical, 8)
|
2022-01-29 23:37:02 +00:00
|
|
|
}
|
|
|
|
|
2022-12-21 12:55:59 +00:00
|
|
|
@ViewBuilder private func sendMessageButton() -> some View {
|
|
|
|
let v = Button(action: sendMessage) {
|
2022-12-17 14:02:07 +00:00
|
|
|
Image(systemName: composeState.editing || composeState.liveMessage != nil
|
|
|
|
? "checkmark.circle.fill"
|
|
|
|
: "arrow.up.circle.fill")
|
2022-11-24 21:18:28 +04:00
|
|
|
.resizable()
|
|
|
|
.foregroundColor(.accentColor)
|
2022-12-17 14:02:07 +00:00
|
|
|
.frame(width: sendButtonSize, height: sendButtonSize)
|
|
|
|
.opacity(sendButtonOpacity)
|
2022-11-24 21:18:28 +04:00
|
|
|
}
|
2022-11-25 15:16:37 +04:00
|
|
|
.disabled(
|
|
|
|
!composeState.sendEnabled ||
|
|
|
|
composeState.disabled ||
|
2023-01-10 19:12:48 +00:00
|
|
|
(!voiceMessageAllowed && composeState.voicePreview) ||
|
2023-01-11 12:01:02 +00:00
|
|
|
composeState.endLiveDisabled
|
2022-11-25 15:16:37 +04:00
|
|
|
)
|
2022-11-24 21:18:28 +04:00
|
|
|
.frame(width: 29, height: 29)
|
2022-12-21 12:55:59 +00:00
|
|
|
|
|
|
|
if composeState.liveMessage == nil,
|
2023-01-11 12:01:02 +00:00
|
|
|
case .noContextItem = composeState.contextItem,
|
2022-12-21 12:55:59 +00:00
|
|
|
!composeState.voicePreview && !composeState.editing,
|
|
|
|
let send = sendLiveMessage,
|
|
|
|
let update = updateLiveMessage {
|
|
|
|
v.contextMenu{
|
|
|
|
Button {
|
|
|
|
startLiveMessage(send: send, update: update)
|
|
|
|
} label: {
|
2022-12-27 09:18:20 +00:00
|
|
|
Label("Send live message", systemImage: "bolt.fill")
|
2022-12-21 12:55:59 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
.padding([.bottom, .trailing], 4)
|
|
|
|
} else {
|
|
|
|
v.padding([.bottom, .trailing], 4)
|
|
|
|
}
|
2022-11-24 21:18:28 +04:00
|
|
|
}
|
|
|
|
|
2022-11-25 21:05:14 +04:00
|
|
|
private struct RecordVoiceMessageButton: View {
|
|
|
|
var startVoiceMessageRecording: (() -> Void)?
|
|
|
|
var finishVoiceMessageRecording: (() -> Void)?
|
|
|
|
@Binding var holdingVMR: Bool
|
|
|
|
var disabled: Bool
|
|
|
|
@State private var pressed: TimeInterval? = nil
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
Button(action: {}) {
|
|
|
|
Image(systemName: "mic.fill")
|
|
|
|
.foregroundColor(.accentColor)
|
|
|
|
}
|
|
|
|
.disabled(disabled)
|
|
|
|
.frame(width: 29, height: 29)
|
|
|
|
.padding([.bottom, .trailing], 4)
|
|
|
|
._onButtonGesture { down in
|
|
|
|
if down {
|
|
|
|
holdingVMR = true
|
|
|
|
pressed = ProcessInfo.processInfo.systemUptime
|
|
|
|
startVoiceMessageRecording?()
|
|
|
|
} else {
|
|
|
|
let now = ProcessInfo.processInfo.systemUptime
|
|
|
|
if let pressed = pressed,
|
|
|
|
now - pressed >= 1 {
|
|
|
|
finishVoiceMessageRecording?()
|
|
|
|
}
|
|
|
|
holdingVMR = false
|
|
|
|
pressed = nil
|
|
|
|
}
|
|
|
|
} perform: {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private func voiceMessageNotAllowedButton() -> some View {
|
2022-12-17 14:02:07 +00:00
|
|
|
Button {
|
2022-11-25 21:05:14 +04:00
|
|
|
switch showEnableVoiceMessagesAlert {
|
|
|
|
case .userEnable:
|
|
|
|
AlertManager.shared.showAlert(Alert(
|
|
|
|
title: Text("Allow voice messages?"),
|
|
|
|
message: Text("You need to allow your contact to send voice messages to be able to send them."),
|
|
|
|
primaryButton: .default(Text("Allow")) {
|
|
|
|
allowVoiceMessagesToContact?()
|
|
|
|
},
|
|
|
|
secondaryButton: .cancel()
|
|
|
|
))
|
|
|
|
case .askContact:
|
|
|
|
AlertManager.shared.showAlertMsg(
|
|
|
|
title: "Voice messages prohibited!",
|
|
|
|
message: "Please ask your contact to enable sending voice messages."
|
|
|
|
)
|
|
|
|
case .groupOwnerCan:
|
|
|
|
AlertManager.shared.showAlertMsg(
|
|
|
|
title: "Voice messages prohibited!",
|
|
|
|
message: "Only group owners can enable voice messages."
|
|
|
|
)
|
|
|
|
case .other:
|
|
|
|
AlertManager.shared.showAlertMsg(
|
|
|
|
title: "Voice messages prohibited!",
|
|
|
|
message: "Please check yours and your contact preferences."
|
|
|
|
)
|
2022-11-24 21:18:28 +04:00
|
|
|
}
|
2022-12-17 14:02:07 +00:00
|
|
|
} label: {
|
2022-11-24 21:18:28 +04:00
|
|
|
Image(systemName: "mic")
|
|
|
|
.foregroundColor(.secondary)
|
|
|
|
}
|
|
|
|
.disabled(composeState.disabled)
|
|
|
|
.frame(width: 29, height: 29)
|
|
|
|
.padding([.bottom, .trailing], 4)
|
|
|
|
}
|
|
|
|
|
2023-01-10 19:12:48 +00:00
|
|
|
private func cancelLiveMessageButton(cancel: @escaping () -> Void) -> some View {
|
|
|
|
return Button {
|
|
|
|
cancel()
|
|
|
|
} label: {
|
|
|
|
Image(systemName: "multiply")
|
|
|
|
.resizable()
|
|
|
|
.scaledToFit()
|
|
|
|
.foregroundColor(.accentColor)
|
|
|
|
.frame(width: 15, height: 15)
|
|
|
|
}
|
|
|
|
.frame(width: 29, height: 29)
|
|
|
|
.padding([.bottom, .horizontal], 4)
|
|
|
|
}
|
|
|
|
|
2022-12-17 14:02:07 +00:00
|
|
|
private func startLiveMessageButton(send: @escaping () async -> Void, update: @escaping () async -> Void) -> some View {
|
|
|
|
return Button {
|
|
|
|
switch composeState.preview {
|
|
|
|
case .noPreview: startLiveMessage(send: send, update: update)
|
|
|
|
default: ()
|
|
|
|
}
|
|
|
|
} label: {
|
2022-12-27 09:18:20 +00:00
|
|
|
Image(systemName: "bolt.fill")
|
2022-12-18 21:20:39 +00:00
|
|
|
.resizable()
|
2022-12-27 09:18:20 +00:00
|
|
|
.scaledToFit()
|
2022-12-18 21:20:39 +00:00
|
|
|
.foregroundColor(.accentColor)
|
2022-12-27 09:18:20 +00:00
|
|
|
.frame(width: 20, height: 20)
|
2022-12-17 14:02:07 +00:00
|
|
|
}
|
|
|
|
.frame(width: 29, height: 29)
|
|
|
|
.padding([.bottom, .horizontal], 4)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func startLiveMessage(send: @escaping () async -> Void, update: @escaping () async -> Void) {
|
|
|
|
if liveMessageAlertShown {
|
|
|
|
start()
|
|
|
|
} else {
|
|
|
|
AlertManager.shared.showAlert(Alert(
|
|
|
|
title: Text("Live message!"),
|
|
|
|
message: Text("Send a live message - it will update for the recipient(s) as you type it"),
|
|
|
|
primaryButton: .default(Text("Send")) {
|
|
|
|
liveMessageAlertShown = true
|
|
|
|
start()
|
|
|
|
},
|
|
|
|
secondaryButton: .cancel()
|
|
|
|
))
|
|
|
|
}
|
|
|
|
|
|
|
|
func start() {
|
|
|
|
Task {
|
|
|
|
await send()
|
|
|
|
await MainActor.run { run() }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Sendable func run() {
|
|
|
|
Timer.scheduledTimer(withTimeInterval: 0.75, repeats: true) { t in
|
|
|
|
withAnimation(.easeInOut(duration: 0.7)) {
|
|
|
|
sendButtonSize = sendButtonSize == 29 ? 26 : 29
|
|
|
|
sendButtonOpacity = sendButtonOpacity == 1 ? 0.75 : 1
|
|
|
|
}
|
|
|
|
if composeState.liveMessage == nil {
|
|
|
|
t.invalidate()
|
|
|
|
sendButtonSize = 29
|
|
|
|
sendButtonOpacity = 1
|
|
|
|
}
|
|
|
|
}
|
2023-01-10 19:12:48 +00:00
|
|
|
Task {
|
|
|
|
_ = try? await Task.sleep(nanoseconds: liveMsgInterval)
|
|
|
|
while composeState.liveMessage != nil {
|
|
|
|
await update()
|
|
|
|
_ = try? await Task.sleep(nanoseconds: liveMsgInterval)
|
|
|
|
}
|
2022-12-17 14:02:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-24 21:18:28 +04:00
|
|
|
private func finishVoiceMessageRecordingButton() -> some View {
|
|
|
|
Button(action: { finishVoiceMessageRecording?() }) {
|
|
|
|
Image(systemName: "stop.fill")
|
|
|
|
.foregroundColor(.accentColor)
|
|
|
|
}
|
|
|
|
.disabled(composeState.disabled)
|
|
|
|
.frame(width: 29, height: 29)
|
|
|
|
.padding([.bottom, .trailing], 4)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateHeight(_ g: GeometryProxy) -> Color {
|
2022-02-05 14:24:23 +00:00
|
|
|
DispatchQueue.main.async {
|
|
|
|
teHeight = min(max(g.frame(in: .local).size.height, minHeight), maxHeight)
|
2022-12-17 12:01:49 +03:00
|
|
|
(teFont, teUiFont) = isShortEmoji(composeState.message)
|
|
|
|
? composeState.message.count < 4
|
|
|
|
? (largeEmojiFont, largeEmojiUIFont)
|
|
|
|
: (mediumEmojiFont, mediumEmojiUIFont)
|
|
|
|
: (.body, UIFont.preferredFont(forTextStyle: .body))
|
2022-02-05 14:24:23 +00:00
|
|
|
}
|
|
|
|
return Color.clear
|
2022-01-29 23:37:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct SendMessageView_Previews: PreviewProvider {
|
|
|
|
static var previews: some View {
|
2022-04-25 12:44:24 +04:00
|
|
|
@State var composeStateNew = ComposeState()
|
|
|
|
let ci = ChatItem.getSample(1, .directSnd, .now, "hello")
|
|
|
|
@State var composeStateEditing = ComposeState(editingItem: ci)
|
2022-02-11 07:42:00 +00:00
|
|
|
@FocusState var keyboardVisible: Bool
|
2022-04-19 12:29:03 +04:00
|
|
|
@State var sendEnabled: Bool = true
|
2022-02-11 07:42:00 +00:00
|
|
|
|
2022-03-25 22:26:05 +04:00
|
|
|
return Group {
|
|
|
|
VStack {
|
|
|
|
Text("")
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
SendMessageView(
|
2022-04-25 12:44:24 +04:00
|
|
|
composeState: $composeStateNew,
|
2022-04-27 20:54:21 +04:00
|
|
|
sendMessage: {},
|
2022-12-24 00:22:12 +03:00
|
|
|
onImagesAdded: { _ in },
|
2022-04-25 12:44:24 +04:00
|
|
|
keyboardVisible: $keyboardVisible
|
2022-03-25 22:26:05 +04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
VStack {
|
|
|
|
Text("")
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
SendMessageView(
|
2022-04-25 12:44:24 +04:00
|
|
|
composeState: $composeStateEditing,
|
2022-04-27 20:54:21 +04:00
|
|
|
sendMessage: {},
|
2022-12-24 00:22:12 +03:00
|
|
|
onImagesAdded: { _ in },
|
2022-04-25 12:44:24 +04:00
|
|
|
keyboardVisible: $keyboardVisible
|
2022-03-25 22:26:05 +04:00
|
|
|
)
|
|
|
|
}
|
2022-02-05 14:24:23 +00:00
|
|
|
}
|
2022-01-29 23:37:02 +00:00
|
|
|
}
|
|
|
|
}
|