mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-24 21:45:38 +00:00
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:
committed by
GitHub
parent
2679bc2e94
commit
e27013071b
@@ -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> = [:]
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user