ios: update chat previews, show filename in drafts (#1847)

* ios: update chat previews, show filename in drafts

* save and restore images/file/voice for draft

* refactor image

* it was a wrong value

* use param label

* proper stop of voice recording

* safe draft logic

* different way of finishing recording

* keep condition

* refactor

* fix live

* fix

* refactor

* fix

* simplify

* add space after filename in draft

---------

Co-authored-by: Avently <7953703+avently@users.noreply.github.com>
This commit is contained in:
Evgeny Poberezkin
2023-01-27 22:09:39 +00:00
committed by GitHub
co-authored by Avently
parent 6e131e0bad
commit 3e560278b6
6 changed files with 102 additions and 44 deletions
+4
View File
@@ -77,6 +77,10 @@ class AppDelegate: NSObject, UIApplicationDelegate {
func applicationWillTerminate(_ application: UIApplication) {
logger.debug("AppDelegate: applicationWillTerminate")
ChatModel.shared.filesToDelete.forEach {
removeFile($0)
}
ChatModel.shared.filesToDelete = []
terminateChat()
}
+2
View File
@@ -63,6 +63,8 @@ final class ChatModel: ObservableObject {
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
var filesToDelete: [String] = []
static let shared = ChatModel()
static var ok: Bool { ChatModel.shared.chatDbStatus == .ok }
@@ -84,7 +84,7 @@ struct MsgContentView: View {
}
}
func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, preview: Bool = false) -> Text {
func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, icon: String? = nil, preview: Bool = false) -> Text {
let s = text
var res: Text
if let ft = formattedText, ft.count > 0 {
@@ -98,6 +98,10 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: St
res = Text(s)
}
if let i = icon {
res = Text(Image(systemName: i)).foregroundColor(Color(uiColor: .tertiaryLabel)) + Text(" ") + res
}
if let s = sender {
let t = Text(s)
return (preview ? t : t.fontWeight(.medium)) + Text(": ") + res
+1 -5
View File
@@ -52,7 +52,7 @@ struct ChatView: View {
}
Spacer(minLength: 0)
ComposeView(
chat: chat,
composeState: $composeState,
@@ -79,10 +79,6 @@ struct ChatView: View {
.onDisappear {
if chatModel.chatId == cInfo.id {
chatModel.chatId = nil
if !composeState.empty {
chatModel.draft = composeState
chatModel.draftChatId = chat.id
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
if chatModel.chatId == nil {
chatModel.reversedChatItems = []
@@ -14,9 +14,9 @@ import PhotosUI
enum ComposePreview {
case noPreview
case linkPreview(linkPreview: LinkPreview?)
case imagePreviews(imagePreviews: [String])
case imagePreviews(imagePreviews: [(String, UploadContent?)])
case voicePreview(recordingFileName: String, duration: Int)
case filePreview(fileName: String)
case filePreview(fileName: String, file: URL)
}
enum ComposeContextItem {
@@ -163,7 +163,7 @@ struct ComposeState {
}
var empty: Bool {
message == "" && liveMessage == nil && noPreview
message == "" && noPreview
}
}
@@ -175,11 +175,12 @@ func chatItemPreview(chatItem: ChatItem) -> ComposePreview {
case let .link(_, preview: preview):
chatItemPreview = .linkPreview(linkPreview: preview)
case let .image(_, image):
chatItemPreview = .imagePreviews(imagePreviews: [image])
chatItemPreview = .imagePreviews(imagePreviews: [(image, nil)])
case let .voice(_, duration):
chatItemPreview = .voicePreview(recordingFileName: chatItem.file?.fileName ?? "", duration: duration)
case .file:
chatItemPreview = .filePreview(fileName: chatItem.file?.fileName ?? "")
let fileName = chatItem.file?.fileName ?? ""
chatItemPreview = .filePreview(fileName: fileName, file: getAppFilePath(fileName))
default:
chatItemPreview = .noPreview
}
@@ -233,7 +234,6 @@ struct ComposeView: View {
@State private var showTakePhoto = false
@State var chosenImages: [UploadContent] = []
@State private var showFileImporter = false
@State var chosenFile: URL? = nil
@State private var audioRecorder: AudioRecorder?
@State private var voiceMessageRecordingTime: TimeInterval?
@@ -341,10 +341,10 @@ struct ComposeView: View {
}
.onChange(of: chosenImages) { images in
Task {
var imgs: [String] = []
var imgs: [(String, UploadContent)] = []
for image in images {
if let img = resizeImageToStrSize(image.uiImage, maxDataSize: 14000) {
imgs.append(img)
imgs.append((img, image))
await MainActor.run {
composeState = composeState.copy(preview: .imagePreviews(imagePreviews: imgs))
}
@@ -371,9 +371,8 @@ struct ComposeView: View {
}
fileURL.stopAccessingSecurityScopedResource()
if let fileSize = fileSize,
fileSize <= MAX_FILE_SIZE {
chosenFile = fileURL
composeState = composeState.copy(preview: .filePreview(fileName: fileURL.lastPathComponent))
fileSize <= MAX_FILE_SIZE {
composeState = composeState.copy(preview: .filePreview(fileName: fileURL.lastPathComponent, file: fileURL))
} else {
let prettyMaxFileSize = ByteCountFormatter().string(fromByteCount: MAX_FILE_SIZE)
AlertManager.shared.showAlertMsg(
@@ -387,12 +386,18 @@ struct ComposeView: View {
}
}
.onDisappear {
if let fileName = composeState.voiceMessageRecordingFileName {
cancelVoiceMessageRecording(fileName)
}
if composeState.liveMessage != nil && (!composeState.message.isEmpty || composeState.liveMessage?.sentMsg != nil) {
if composeState.liveMessage != nil
&& (!composeState.message.isEmpty || composeState.liveMessage?.sentMsg != nil) {
cancelCurrentVoiceRecording()
clearCurrentDraft()
sendMessage()
resetLinkPreview()
} else if !composeState.empty {
saveCurrentDraft()
} else {
cancelCurrentVoiceRecording()
clearCurrentDraft()
clearState()
}
chatModel.removeLiveDummy(animated: false)
}
@@ -409,6 +414,12 @@ struct ComposeView: View {
if !vmAllowed && composeState.voicePreview,
let fileName = composeState.voiceMessageRecordingFileName {
cancelVoiceMessageRecording(fileName)
clearState()
}
}
.onAppear {
if case let .voicePreview(_, duration) = composeState.preview {
voiceMessageRecordingTime = TimeInterval(duration)
}
}
}
@@ -475,7 +486,7 @@ struct ComposeView: View {
ComposeLinkView(linkPreview: preview, cancelPreview: cancelLinkPreview)
case let .imagePreviews(imagePreviews: images):
ComposeImageView(
images: images,
images: images.map { (img, _) in img },
cancelImage: {
composeState = composeState.copy(preview: .noPreview)
chosenImages = []
@@ -486,16 +497,18 @@ struct ComposeView: View {
recordingFileName: recordingFileName,
recordingTime: $voiceMessageRecordingTime,
recordingState: $composeState.voiceMessageRecordingState,
cancelVoiceMessage: { cancelVoiceMessageRecording($0) },
cancelVoiceMessage: {
cancelVoiceMessageRecording($0)
clearState()
},
cancelEnabled: !composeState.editing,
stopPlayback: $stopPlayback
)
case let .filePreview(fileName: fileName):
case let .filePreview(fileName, _):
ComposeFileView(
fileName: fileName,
cancelFile: {
composeState = composeState.copy(preview: .noPreview)
chosenFile = nil
},
cancelEnabled: !composeState.editing)
}
@@ -552,27 +565,23 @@ struct ComposeView: View {
case .linkPreview:
sent = await send(checkLinkPreview(), quoted: quoted, live: live)
case let .imagePreviews(imagePreviews: images):
let last = min(chosenImages.count, images.count) - 1
let last = images.count - 1
if last >= 0 {
for i in 0..<last {
if let savedFile = saveAnyImage(chosenImages[i]) {
_ = await send(.image(text: "", image: images[i]), quoted: nil, file: savedFile)
}
sent = await sendImage(images[i])
_ = try? await Task.sleep(nanoseconds: 100_000000)
}
if let savedFile = saveAnyImage(chosenImages[last]) {
sent = await send(.image(text: msgText, image: images[last]), quoted: quoted, file: savedFile, live: live)
}
sent = await sendImage(images[last], text: msgText, quoted: quoted, live: live)
}
if sent == nil {
sent = await send(.text(msgText), quoted: quoted, live: live)
}
case let .voicePreview(recordingFileName, duration):
stopPlayback.toggle()
chatModel.filesToDelete.removeAll { $0 == recordingFileName }
sent = await send(.voice(text: msgText, duration: duration), quoted: quoted, file: recordingFileName)
case .filePreview:
if let fileURL = chosenFile,
let savedFile = saveFileFromURL(fileURL) {
case let .filePreview(_, file):
if let savedFile = saveFileFromURL(file) {
sent = await send(.file(msgText), quoted: quoted, file: savedFile, live: live)
}
}
@@ -627,6 +636,14 @@ struct ComposeView: View {
}
}
func sendImage(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false) async -> ChatItem? {
let (image, data) = imageData
if let data = data, let savedFile = saveAnyImage(data) {
return await send(.image(text: text, image: image), quoted: quoted, file: savedFile, live: live)
}
return nil
}
func send(_ mc: MsgContent, quoted: Int64?, file: String? = nil, live: Bool = false) async -> ChatItem? {
if let chatItem = await apiSendMessage(
type: chat.chatInfo.chatType,
@@ -737,11 +754,16 @@ struct ComposeView: View {
)
}
private func cancelCurrentVoiceRecording() {
if let fileName = composeState.voiceMessageRecordingFileName {
cancelVoiceMessageRecording(fileName)
}
}
private func cancelVoiceMessageRecording(_ fileName: String) {
stopPlayback.toggle()
audioRecorder?.stop()
removeFile(fileName)
clearState()
}
private func clearState(live: Bool = false) {
@@ -753,12 +775,29 @@ struct ComposeView: View {
resetLinkPreview()
}
chosenImages = []
chosenFile = nil
audioRecorder = nil
voiceMessageRecordingTime = nil
startingRecording = false
}
private func saveCurrentDraft() {
if case .recording = composeState.voiceMessageRecordingState {
finishVoiceMessageRecording()
if let fileName = composeState.voiceMessageRecordingFileName {
chatModel.filesToDelete.append(fileName)
}
}
chatModel.draft = composeState
chatModel.draftChatId = chat.id
}
private func clearCurrentDraft() {
if chatModel.draftChatId == chat.id {
chatModel.draft = nil
chatModel.draftChatId = nil
}
}
private func showLinkPreview(_ s: String) {
prevLinkUrl = linkUrl
linkUrl = parseMessage(s)
@@ -40,7 +40,7 @@ struct ChatPreviewView: View {
.padding(.horizontal, 8)
ZStack(alignment: .topTrailing) {
chatPreviewText(cItem)
chatMessagePreview(cItem)
if case .direct = chat.chatInfo {
chatStatusImage()
.padding(.top, 24)
@@ -142,21 +142,34 @@ struct ChatPreviewView: View {
func attachment() -> Text {
switch draft.preview {
case .filePreview: return image("doc.fill")
case let .filePreview(fileName, _): return image("doc.fill") + Text(fileName) + Text(" ")
case .imagePreviews: return image("photo")
case let .voicePreview(_, duration): return image("play.fill") + Text(voiceMessageTime(TimeInterval(duration)))
case let .voicePreview(_, duration): return image("play.fill") + Text(durationText(duration))
default: return Text("")
}
}
}
@ViewBuilder private func chatPreviewText(_ cItem: ChatItem?) -> some View {
func chatItemPreview(_ cItem: ChatItem) -> Text {
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
return messageText(itemText, itemFormattedText, cItem.memberDisplayName, icon: attachment(), preview: true)
func attachment() -> String? {
switch cItem.content.msgContent {
case .file: return "doc.fill"
case .image: return "photo"
case .voice: return "play.fill"
default: return nil
}
}
}
@ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?) -> some View {
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
chatPreviewLayout(itemStatusMark(cItem) + messageText(itemText, itemFormattedText, cItem.memberDisplayName, preview: true))
chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem))
} else {
switch (chat.chatInfo) {
case let .direct(contact):