diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 31ee686413..1ca049a84a 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -57,6 +57,8 @@ final class ChatModel: ObservableObject { @Published var connReqInv: String? // audio recording and playback @Published var stopPreviousRecPlay: Bool = false // value is not taken into account, only the fact it switches + @Published var draft: ComposeState? + @Published var draftChatId: String? var callWebView: WKWebView? var messageDelivery: Dictionary Void> = [:] diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 9da2c6a7d6..96d0874f0e 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -64,6 +64,9 @@ struct ChatView: View { .navigationTitle(cInfo.chatViewName) .navigationBarTitleDisplayMode(.inline) .onAppear { + if chatModel.draftChatId == cInfo.id, let draft = chatModel.draft { + composeState = draft + } if chat.chatStats.unreadChat { Task { await markChatUnread(chat, unreadChat: false) @@ -73,9 +76,21 @@ struct ChatView: View { .onChange(of: chatModel.chatId) { _ in if chatModel.chatId == nil { dismiss() } } + .onChange(of: "\(composeState.empty) \(composeState.noPreview) \(composeState.message)") { _ in + if !composeState.empty { + chatModel.draft = composeState + chatModel.draftChatId = chat.id + } else if chatModel.draftChatId == chat.id { + chatModel.draft = nil + chatModel.draftChatId = nil + } + } .onDisappear { if chatModel.chatId == cInfo.id { chatModel.chatId = nil + if chatModel.draftChatId == cInfo.id { + chatModel.draft = composeState + } DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { if chatModel.chatId == nil { chatModel.reversedChatItems = [] @@ -175,7 +190,7 @@ struct ChatView: View { } } } - + private func searchToolbar() -> some View { HStack { HStack { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 5ca4627917..9d4b714164 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -161,6 +161,10 @@ struct ComposeState { default: return true } } + + var empty: Bool { + message == "" && liveMessage == nil && noPreview + } } func chatItemPreview(chatItem: ChatItem) -> ComposePreview { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index b5f8e9b5fd..8088f82fa3 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -76,45 +76,20 @@ struct SendMessageView: View { } } - if (composeState.inProgress) { + if composeState.inProgress { 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, - case .noContextItem = composeState.contextItem { - startLiveMessageButton(send: send, update: update) - } + VStack(alignment: .trailing) { + if teHeight > 100 { + deleteTextButton() + Spacer() } - } else if vmrs == .recording && !holdingVMR { - finishVoiceMessageRecordingButton() - } else if composeState.liveMessage != nil && composeState.liveMessage?.sentMsg == nil && composeState.message.isEmpty { - cancelLiveMessageButton { - cancelLiveMessage?() - } - } else { - sendMessageButton() + composeActionButtons() } + .frame(height: teHeight, alignment: .bottom) } } @@ -125,6 +100,52 @@ struct SendMessageView: View { .padding(.vertical, 8) } + @ViewBuilder private func composeActionButtons() -> some View { + 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, + case .noContextItem = composeState.contextItem { + startLiveMessageButton(send: send, update: update) + } + } + } else if vmrs == .recording && !holdingVMR { + finishVoiceMessageRecordingButton() + } else if composeState.liveMessage != nil && composeState.liveMessage?.sentMsg == nil && composeState.message.isEmpty { + cancelLiveMessageButton { + cancelLiveMessage?() + } + } else { + sendMessageButton() + } + } + + private func deleteTextButton() -> some View { + Button { + composeState.message = "" + } label: { + Image(systemName: "multiply.circle.fill") + } + .foregroundColor(Color(uiColor: .tertiaryLabel)) + .padding([.top, .trailing], 4) + } + @ViewBuilder private func sendMessageButton() -> some View { let v = Button(action: sendMessage) { Image(systemName: composeState.editing || composeState.liveMessage != nil diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index e85eeaa345..f006ebf13c 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -106,31 +106,57 @@ struct ChatPreviewView: View { .kerning(-2) } + private func chatPreviewLayout(_ text: Text) -> some View { + ZStack(alignment: .topTrailing) { + text + .lineLimit(2) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .topLeading) + .padding(.leading, 8) + .padding(.trailing, 36) + let s = chat.chatStats + if s.unreadCount > 0 || s.unreadChat { + unreadCountText(s.unreadCount) + .font(.caption) + .foregroundColor(.white) + .padding(.horizontal, 4) + .frame(minWidth: 18, minHeight: 18) + .background(chat.chatInfo.ntfsEnabled ? Color.accentColor : Color.secondary) + .cornerRadius(10) + } else if !chat.chatInfo.ntfsEnabled { + Image(systemName: "speaker.slash.fill") + .foregroundColor(.secondary) + } + } + } + + private func messageDraft(_ draft: ComposeState) -> Text { + let msg = draft.message + return image("rectangle.and.pencil.and.ellipsis", color: .accentColor) + + attachment() + + messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true) + + func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text { + Text(Image(systemName: s)).foregroundColor(color) + Text(" ") + } + + func attachment() -> Text { + switch draft.preview { + case .filePreview: return image("doc.fill") + case .imagePreviews: return image("photo") + case let .voicePreview(_, duration): return image("play.fill") + Text(voiceMessageTime(TimeInterval(duration))) + default: return Text("") + } + } + } + @ViewBuilder private func chatPreviewText(_ cItem: ChatItem?) -> some View { - if let cItem = cItem { + if chatModel.draftChatId == chat.id, let draft = chatModel.draft { + chatPreviewLayout(messageDraft(draft)) + } else if let cItem = cItem { let itemText = !cItem.meta.itemDeleted ? cItem.text : NSLocalizedString("marked deleted", comment: "marked deleted chat item preview text") let itemFormattedText = !cItem.meta.itemDeleted ? cItem.formattedText : nil - ZStack(alignment: .topTrailing) { - (itemStatusMark(cItem) + messageText(itemText, itemFormattedText, cItem.memberDisplayName, preview: true)) - .lineLimit(2) - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, alignment: .topLeading) - .padding(.leading, 8) - .padding(.trailing, 36) - let s = chat.chatStats - if s.unreadCount > 0 || s.unreadChat { - unreadCountText(s.unreadCount) - .font(.caption) - .foregroundColor(.white) - .padding(.horizontal, 4) - .frame(minWidth: 18, minHeight: 18) - .background(chat.chatInfo.ntfsEnabled ? Color.accentColor : Color.secondary) - .cornerRadius(10) - } else if !chat.chatInfo.ntfsEnabled { - Image(systemName: "speaker.slash.fill") - .foregroundColor(.secondary) - } - } + chatPreviewLayout(itemStatusMark(cItem) + messageText(itemText, itemFormattedText, cItem.memberDisplayName, preview: true)) } else { switch (chat.chatInfo) { case let .direct(contact):