SimpleX-Chat/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift

334 lines
13 KiB
Swift
Raw Normal View History

//
// SendMessageView.swift
// SimpleX
//
// Created by Evgeny Poberezkin on 29/01/2022.
// Copyright © 2022 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct SendMessageView: View {
@Binding var composeState: ComposeState
2022-04-27 20:54:21 +04:00
var sendMessage: () -> Void
var sendLiveMessage: (() async -> Void)? = nil
var updateLiveMessage: (() async -> Void)? = nil
var showVoiceMessageButton: Bool = true
var voiceMessageAllowed: Bool = true
var showEnableVoiceMessagesAlert: ChatInfo.ShowEnableVoiceMessagesAlert = .other
var startVoiceMessageRecording: (() -> Void)? = nil
var finishVoiceMessageRecording: (() -> Void)? = nil
var allowVoiceMessagesToContact: (() -> Void)? = nil
var onImagesAdded: ([UploadContent]) -> Void
@State private var holdingVMR = false
2022-02-05 14:24:23 +00:00
@Namespace var namespace
@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
@State private var teUiFont: UIFont = UIFont.preferredFont(forTextStyle: .body)
@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
@AppStorage(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
var body: some View {
2022-02-05 14:24:23 +00:00
ZStack {
HStack(alignment: .bottom) {
ZStack(alignment: .leading) {
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)
// put text on top (after NativeTextEditor) and set color to precisely align it on changes
// .foregroundColor(.red)
.foregroundColor(.clear)
.padding(.horizontal, 10)
.padding(.top, 8)
.padding(.bottom, 6)
.matchedGeometryEffect(id: "te", in: namespace)
.background(GeometryReader(content: updateHeight))
NativeTextEditor(
text: $composeState.message,
height: teHeight,
font: teUiFont,
focused: $keyboardVisible,
alignment: alignment,
onImagesAdded: onImagesAdded
)
.allowsTightening(false)
.frame(height: teHeight)
}
2022-02-05 14:24:23 +00: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 {
let vmrs = composeState.voiceMessageRecordingState
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()
}
if let send = sendLiveMessage, let update = updateLiveMessage {
startLiveMessageButton(send: send, update: update)
}
}
} else if vmrs == .recording && !holdingVMR {
finishVoiceMessageRecordingButton()
} else {
sendMessageButton()
2022-02-05 14:24:23 +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-02-05 14:24:23 +00:00
.padding(.vertical, 8)
}
@ViewBuilder private func sendMessageButton() -> some View {
let v = Button(action: sendMessage) {
Image(systemName: composeState.editing || composeState.liveMessage != nil
? "checkmark.circle.fill"
: "arrow.up.circle.fill")
.resizable()
.foregroundColor(.accentColor)
.frame(width: sendButtonSize, height: sendButtonSize)
.opacity(sendButtonOpacity)
}
.disabled(
!composeState.sendEnabled ||
composeState.disabled ||
(!voiceMessageAllowed && composeState.voicePreview)
)
.frame(width: 29, height: 29)
if composeState.liveMessage == nil,
!composeState.voicePreview && !composeState.editing,
let send = sendLiveMessage,
let update = updateLiveMessage {
v.contextMenu{
Button {
startLiveMessage(send: send, update: update)
} label: {
Label("Send live message", systemImage: "ellipsis.circle")
}
}
.padding([.bottom, .trailing], 4)
} else {
v.padding([.bottom, .trailing], 4)
}
}
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 {
Button {
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."
)
}
} label: {
Image(systemName: "mic")
.foregroundColor(.secondary)
}
.disabled(composeState.disabled)
.frame(width: 29, height: 29)
.padding([.bottom, .trailing], 4)
}
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: {
Image(systemName: "ellipsis.circle.fill")
.resizable()
.foregroundColor(.accentColor)
}
.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
}
}
Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { t in
if composeState.liveMessage == nil { t.invalidate() }
Task { await update() }
}
}
}
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)
(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
}
}
struct SendMessageView_Previews: PreviewProvider {
static var previews: some View {
@State var composeStateNew = ComposeState()
let ci = ChatItem.getSample(1, .directSnd, .now, "hello")
@State var composeStateEditing = ComposeState(editingItem: ci)
@FocusState var keyboardVisible: Bool
mobile: support images (#536) * ios api * ios wip * android wip * ios files folder * ios get address on start * android app files folder * ios more backend * android more backend * translation * ios image without text, remove preview * android image without text, remove preview * fix translation * file name in previews and w/t text * Revert "file name in previews and w/t text" This reverts commit 0110570e55d23ecc361613f41aeadcaff07ac903. * ios filename in preview * android filename in preview * android wider images * ios determine width on image for correct quote width * ios images in previews wip * ios square image in quote * ios: update image layout * android images in quotes * android remove redundant modifier * android clip to bounds * android - image in right side of quote * android refactor image view * android - refactor, align quote text top * android fix emoji view * fix image layout * full screen image view, fix quote layout * android various size * android fixed image width * android meta on image * ios: add drag gesture to hide full-screen image * android: make image-only meta white * refactor file.stored * android: meta icon color * android: open chat scrolled to last unread item * copy/share image messages * android: full screen image * check file is loaded * terminal: refactor view for messages with files * android: change to onClick, only show stored file * android: remove close sheet bar * android: close image view on click * translation * android: pass showMenu to CIImageView to show menu on long click * increase DropDown width Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
2022-04-19 12:29:03 +04:00
@State var sendEnabled: Bool = true
return Group {
VStack {
Text("")
Spacer(minLength: 0)
SendMessageView(
composeState: $composeStateNew,
2022-04-27 20:54:21 +04:00
sendMessage: {},
onImagesAdded: { _ in },
keyboardVisible: $keyboardVisible
)
}
VStack {
Text("")
Spacer(minLength: 0)
SendMessageView(
composeState: $composeStateEditing,
2022-04-27 20:54:21 +04:00
sendMessage: {},
onImagesAdded: { _ in },
keyboardVisible: $keyboardVisible
)
}
2022-02-05 14:24:23 +00:00
}
}
}