diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 6617c7f7dc..332438ce00 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -34,7 +34,7 @@ struct ChatView: View { // opening GroupMemberInfoView on member icon @State private var selectedMember: GroupMember? = nil @State private var memberConnectionStats: ConnectionStats? - + var body: some View { let cInfo = chat.chatInfo return VStack(spacing: 0) { @@ -48,9 +48,9 @@ struct ChatView: View { floatingButtons(proxy) } } - + Spacer(minLength: 0) - + ComposeView( chat: chat, composeState: $composeState, @@ -170,16 +170,16 @@ struct ChatView: View { } } } - + private func searchToolbar() -> some View { HStack { HStack { Image(systemName: "magnifyingglass") TextField("Search", text: $searchText) - .focused($searchFocussed) - .foregroundColor(.primary) - .frame(maxWidth: .infinity) - + .focused($searchFocussed) + .foregroundColor(.primary) + .frame(maxWidth: .infinity) + Button { searchText = "" } label: { @@ -190,7 +190,7 @@ struct ChatView: View { .foregroundColor(.secondary) .background(Color(.secondarySystemBackground)) .cornerRadius(10.0) - + Button ("Cancel") { searchText = "" searchMode = false @@ -204,37 +204,37 @@ struct ChatView: View { .padding(.horizontal) .padding(.vertical, 8) } - + private func chatItemsList() -> some View { let cInfo = chat.chatInfo return GeometryReader { g in ScrollViewReader { proxy in ScrollView { let maxWidth = - cInfo.chatType == .group - ? (g.size.width - 28) * 0.84 - 42 - : (g.size.width - 32) * 0.84 + cInfo.chatType == .group + ? (g.size.width - 28) * 0.84 - 42 + : (g.size.width - 32) * 0.84 LazyVStack(spacing: 5) { ForEach(chatModel.reversedChatItems, id: \.viewId) { ci in chatItemView(ci, maxWidth) - .scaleEffect(x: 1, y: -1, anchor: .center) - .onAppear { - itemsInView.insert(ci.viewId) - loadChatItems(cInfo, ci, proxy) - if ci.isRcvNew() { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { - if chatModel.chatId == cInfo.id && itemsInView.contains(ci.viewId) { - Task { - await apiMarkChatItemRead(cInfo, ci) - NtfManager.shared.decNtfBadgeCount() + .scaleEffect(x: 1, y: -1, anchor: .center) + .onAppear { + itemsInView.insert(ci.viewId) + loadChatItems(cInfo, ci, proxy) + if ci.isRcvNew() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { + if chatModel.chatId == cInfo.id && itemsInView.contains(ci.viewId) { + Task { + await apiMarkChatItemRead(cInfo, ci) + NtfManager.shared.decNtfBadgeCount() + } } } } } - } - .onDisappear { - itemsInView.remove(ci.viewId) - } + .onDisappear { + itemsInView.remove(ci.viewId) + } } } } @@ -258,7 +258,7 @@ struct ChatView: View { } .scaleEffect(x: 1, y: -1, anchor: .center) } - + private func floatingButtons(_ proxy: ScrollViewProxy) -> some View { let counts = chatModel.unreadChatItemCounts(itemsInView: itemsInView) return VStack { @@ -300,7 +300,7 @@ struct ChatView: View { } .padding() } - + private func circleButton(_ content: @escaping () -> Content) -> some View { ZStack { Circle() @@ -309,7 +309,7 @@ struct ChatView: View { content() } } - + private func callButton(_ contact: Contact, _ media: CallMediaType, imageName: String) -> some View { Button { CallController.shared.startCall(contact, media) @@ -317,7 +317,7 @@ struct ChatView: View { Image(systemName: imageName) } } - + private func searchButton() -> some View { Button { searchMode = true @@ -327,7 +327,7 @@ struct ChatView: View { Label("Search", systemImage: "magnifyingglass") } } - + private func addMembersButton() -> some View { Button { if case let .group(gInfo) = chat.chatInfo { @@ -343,7 +343,7 @@ struct ChatView: View { Image(systemName: "person.crop.circle.badge.plus") } } - + private func loadChatItems(_ cInfo: ChatInfo, _ ci: ChatItem, _ proxy: ScrollViewProxy) { if let firstItem = chatModel.reversedChatItems.last, firstItem.id == ci.id { if loadingItems || firstPage { return } @@ -371,7 +371,7 @@ struct ChatView: View { } } } - + @ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View { if case let .groupRcv(member) = ci.chatDir, case let .group(groupInfo) = chat.chatInfo { @@ -429,7 +429,7 @@ struct ChatView: View { ).padding(.horizontal) } } - + private struct ChatItemWithMenu: View { var chat: Chat var ci: ChatItem @@ -440,12 +440,12 @@ struct ChatView: View { @Binding var deletingItem: ChatItem? @Binding var composeState: ComposeState @Binding var showDeleteMessage: Bool - + @State private var revealed = false - + var body: some View { let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading - + ChatItemView(chatInfo: chat.chatInfo, chatItem: ci, showMember: showMember, maxWidth: maxWidth, scrollProxy: scrollProxy, revealed: $revealed) .uiKitContextMenu(actions: menu()) .confirmationDialog("Delete message?", isPresented: $showDeleteMessage, titleVisibility: .visible) { @@ -461,85 +461,24 @@ struct ChatView: View { .frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment) .frame(minWidth: 0, maxWidth: .infinity, alignment: alignment) } - + private func menu() -> [UIAction] { var menu: [UIAction] = [] if let mc = ci.content.msgContent, !ci.meta.itemDeleted || revealed { if !ci.meta.itemDeleted { - menu.append( - UIAction( - title: NSLocalizedString("Reply", comment: "chat item action"), - image: UIImage(systemName: "arrowshape.turn.up.left") - ) { _ in - withAnimation { - if composeState.editing { - composeState = ComposeState(contextItem: .quotedItem(chatItem: ci)) - } else { - composeState = composeState.copy(contextItem: .quotedItem(chatItem: ci)) - } - } - } - ) + menu.append(replyUIAction()) } - menu.append( - UIAction( - title: NSLocalizedString("Share", comment: "chat item action"), - image: UIImage(systemName: "square.and.arrow.up") - ) { _ in - var shareItems: [Any] = [ci.content.text] - if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) { - shareItems.append(image) - } - showShareSheet(items: shareItems) - } - ) - menu.append( - UIAction( - title: NSLocalizedString("Copy", comment: "chat item action"), - image: UIImage(systemName: "doc.on.doc") - ) { _ in - if case let .image(text, _) = ci.content.msgContent, - text == "", - let image = getLoadedImage(ci.file) { - UIPasteboard.general.image = image - } else { - UIPasteboard.general.string = ci.content.text - } - } - ) + menu.append(shareUIAction()) + menu.append(copyUIAction()) if let filePath = getLoadedFilePath(ci.file) { if case .image = ci.content.msgContent, let image = UIImage(contentsOfFile: filePath) { - menu.append( - UIAction( - title: NSLocalizedString("Save", comment: "chat item action"), - image: UIImage(systemName: "square.and.arrow.down") - ) { _ in - UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) - } - ) + menu.append(saveImageAction(image)) } else { - menu.append( - UIAction( - title: NSLocalizedString("Save", comment: "chat item action"), - image: UIImage(systemName: "square.and.arrow.down") - ) { _ in - let fileURL = URL(fileURLWithPath: filePath) - showShareSheet(items: [fileURL]) - } - ) + menu.append(saveFileAction(filePath)) } } if ci.meta.editable && !mc.isVoice { - menu.append( - UIAction( - title: NSLocalizedString("Edit", comment: "chat item action"), - image: UIImage(systemName: "square.and.pencil") - ) { _ in - withAnimation { - composeState = ComposeState(editingItem: ci) - } - } - ) + menu.append(editAction()) } menu.append(deleteUIAction()) } else if ci.meta.itemDeleted { @@ -551,6 +490,79 @@ struct ChatView: View { return menu } + private func replyUIAction() -> UIAction { + UIAction( + title: NSLocalizedString("Reply", comment: "chat item action"), + image: UIImage(systemName: "arrowshape.turn.up.left") + ) { _ in + withAnimation { + if composeState.editing { + composeState = ComposeState(contextItem: .quotedItem(chatItem: ci)) + } else { + composeState = composeState.copy(contextItem: .quotedItem(chatItem: ci)) + } + } + } + } + + private func shareUIAction() -> UIAction { + UIAction( + title: NSLocalizedString("Share", comment: "chat item action"), + image: UIImage(systemName: "square.and.arrow.up") + ) { _ in + var shareItems: [Any] = [ci.content.text] + if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) { + shareItems.append(image) + } + showShareSheet(items: shareItems) + } + } + + private func copyUIAction() -> UIAction { + UIAction( + title: NSLocalizedString("Copy", comment: "chat item action"), + image: UIImage(systemName: "doc.on.doc") + ) { _ in + if case let .image(text, _) = ci.content.msgContent, + text == "", + let image = getLoadedImage(ci.file) { + UIPasteboard.general.image = image + } else { + UIPasteboard.general.string = ci.content.text + } + } + } + + private func saveImageAction(_ image: UIImage) -> UIAction { + UIAction( + title: NSLocalizedString("Save", comment: "chat item action"), + image: UIImage(systemName: "square.and.arrow.down") + ) { _ in + UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) + } + } + + private func saveFileAction(_ filePath: String) -> UIAction { + UIAction( + title: NSLocalizedString("Save", comment: "chat item action"), + image: UIImage(systemName: "square.and.arrow.down") + ) { _ in + let fileURL = URL(fileURLWithPath: filePath) + showShareSheet(items: [fileURL]) + } + } + + private func editAction() -> UIAction { + UIAction( + title: NSLocalizedString("Edit", comment: "chat item action"), + image: UIImage(systemName: "square.and.pencil") + ) { _ in + withAnimation { + composeState = ComposeState(editingItem: ci) + } + } + } + private func deleteUIAction() -> UIAction { UIAction( title: NSLocalizedString("Delete", comment: "chat item action"), @@ -561,7 +573,7 @@ struct ChatView: View { deletingItem = ci } } - + private func revealUIAction() -> UIAction { UIAction( title: NSLocalizedString("Reveal", comment: "chat item action"), @@ -572,12 +584,12 @@ struct ChatView: View { } } } - + private var broadcastDeleteButtonText: LocalizedStringKey { chat.chatInfo.fullDeletionAllowed ? "Delete for everyone" : "Mark deleted for everyone" } } - + private func showMemberImage(_ member: GroupMember, _ prevItem: ChatItem?) -> Bool { switch (prevItem?.chatDir) { case .groupSnd: return true @@ -585,19 +597,19 @@ struct ChatView: View { default: return false } } - + private func scrollToBottom(_ proxy: ScrollViewProxy) { if let ci = chatModel.reversedChatItems.first { withAnimation { proxy.scrollTo(ci.viewId, anchor: .top) } } } - + private func scrollUp(_ proxy: ScrollViewProxy) { if let ci = chatModel.topItemInView(itemsInView: itemsInView) { withAnimation { proxy.scrollTo(ci.viewId, anchor: .top) } } } - + private func deleteMessage(_ mode: CIDeleteMode) { logger.debug("ChatView deleteMessage") Task {