mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-30 14:00:21 +00:00
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:
co-authored by
Avently
parent
6e131e0bad
commit
3e560278b6
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user