ios: preserve message draft in the latest chat only (#1834)

* ios: preserve message draft in the latest chat only

* show attachment icon and formatting in draft

* button to remove message text

* show icon for active draft, refactor

* add voice message duration to draft
This commit is contained in:
Evgeny Poberezkin
2023-01-25 08:35:25 +00:00
committed by GitHub
parent 2679bc2e94
commit e27013071b
5 changed files with 123 additions and 55 deletions
+2
View File
@@ -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<Int64, () -> Void> = [:]
+16 -1
View File
@@ -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 {
@@ -161,6 +161,10 @@ struct ComposeState {
default: return true
}
}
var empty: Bool {
message == "" && liveMessage == nil && noPreview
}
}
func chatItemPreview(chatItem: ChatItem) -> ComposePreview {
@@ -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
@@ -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):