From 9d6f610b6365f51f1cb5b726d302929d247154c1 Mon Sep 17 00:00:00 2001 From: Woit Date: Sun, 24 Nov 2024 00:22:07 +0100 Subject: [PATCH] add conversation screens --- Monal/Monal.xcodeproj/project.pbxproj | 80 ++++++ .../Attachments/AttachmentPickerScreen.swift | 131 +++++++++ .../Attachments/ContactsPickerView.swift | 71 +++++ .../Attachments/FilesPickerView.swift | 66 +++++ .../Attachments/LocationPickerView.swift | 137 +++++++++ .../CameraCellPreview.swift | 61 ++++ .../MediaPickerComponents/CameraPicker.swift | 49 ++++ .../MediaPickerComponents/CameraView.swift | 38 +++ .../MediaPickerComponents/GalleryView.swift | 116 ++++++++ .../Attachments/MediaPickerView.swift | 52 ++++ .../ConversationMessageContainer.swift | 263 ++++++++++++++++++ .../Conversation/ConversationMessageRow.swift | 82 ++++++ .../Conversation/ConversationScreen.swift | 126 +++++++++ .../ConversationSettingsScreen.swift | 51 ++++ .../Conversation/ConversationTextInput.swift | 104 +++++++ Monal/another.im/XMPP/MonalXmppWrapper.swift | 14 + 16 files changed, 1441 insertions(+) create mode 100644 Monal/another.im/Views/Conversation/Attachments/AttachmentPickerScreen.swift create mode 100644 Monal/another.im/Views/Conversation/Attachments/ContactsPickerView.swift create mode 100644 Monal/another.im/Views/Conversation/Attachments/FilesPickerView.swift create mode 100644 Monal/another.im/Views/Conversation/Attachments/LocationPickerView.swift create mode 100644 Monal/another.im/Views/Conversation/Attachments/MediaPickerComponents/CameraCellPreview.swift create mode 100644 Monal/another.im/Views/Conversation/Attachments/MediaPickerComponents/CameraPicker.swift create mode 100644 Monal/another.im/Views/Conversation/Attachments/MediaPickerComponents/CameraView.swift create mode 100644 Monal/another.im/Views/Conversation/Attachments/MediaPickerComponents/GalleryView.swift create mode 100644 Monal/another.im/Views/Conversation/Attachments/MediaPickerView.swift create mode 100644 Monal/another.im/Views/Conversation/ConversationMessageContainer.swift create mode 100644 Monal/another.im/Views/Conversation/ConversationMessageRow.swift create mode 100644 Monal/another.im/Views/Conversation/ConversationScreen.swift create mode 100644 Monal/another.im/Views/Conversation/ConversationSettingsScreen.swift create mode 100644 Monal/another.im/Views/Conversation/ConversationTextInput.swift diff --git a/Monal/Monal.xcodeproj/project.pbxproj b/Monal/Monal.xcodeproj/project.pbxproj index 8327aa9..75322ba 100644 --- a/Monal/Monal.xcodeproj/project.pbxproj +++ b/Monal/Monal.xcodeproj/project.pbxproj @@ -142,6 +142,20 @@ 7E71758D2CECC5C70059F30B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7E71758B2CECC5C70059F30B /* Localizable.strings */; }; 7E71758E2CECC5C70059F30B /* server_features.plist in Resources */ = {isa = PBXBuildFile; fileRef = 7E71758A2CECC5C70059F30B /* server_features.plist */; }; 7E71758F2CECC5C70059F30B /* launchscreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7E7175892CECC5C70059F30B /* launchscreen.storyboard */; }; + 7E8442B02CF297E5001CEBD2 /* AttachmentPickerScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8442A42CF297E5001CEBD2 /* AttachmentPickerScreen.swift */; }; + 7E8442B12CF297E5001CEBD2 /* ConversationMessageRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8442AB2CF297E5001CEBD2 /* ConversationMessageRow.swift */; }; + 7E8442B22CF297E5001CEBD2 /* CameraCellPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E84429F2CF297E5001CEBD2 /* CameraCellPreview.swift */; }; + 7E8442B32CF297E5001CEBD2 /* ConversationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8442AC2CF297E5001CEBD2 /* ConversationScreen.swift */; }; + 7E8442B42CF297E5001CEBD2 /* MediaPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8442A82CF297E5001CEBD2 /* MediaPickerView.swift */; }; + 7E8442B52CF297E5001CEBD2 /* ConversationTextInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8442AE2CF297E5001CEBD2 /* ConversationTextInput.swift */; }; + 7E8442B62CF297E5001CEBD2 /* ConversationSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8442AD2CF297E5001CEBD2 /* ConversationSettingsScreen.swift */; }; + 7E8442B72CF297E5001CEBD2 /* CameraPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8442A02CF297E5001CEBD2 /* CameraPicker.swift */; }; + 7E8442B82CF297E5001CEBD2 /* ConversationMessageContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8442AA2CF297E5001CEBD2 /* ConversationMessageContainer.swift */; }; + 7E8442B92CF297E5001CEBD2 /* ContactsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8442A52CF297E5001CEBD2 /* ContactsPickerView.swift */; }; + 7E8442BA2CF297E5001CEBD2 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8442A12CF297E5001CEBD2 /* CameraView.swift */; }; + 7E8442BB2CF297E5001CEBD2 /* GalleryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8442A22CF297E5001CEBD2 /* GalleryView.swift */; }; + 7E8442BC2CF297E5001CEBD2 /* LocationPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8442A72CF297E5001CEBD2 /* LocationPickerView.swift */; }; + 7E8442BD2CF297E5001CEBD2 /* FilesPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8442A62CF297E5001CEBD2 /* FilesPickerView.swift */; }; 7E8D7AE32CECD011009AD3DF /* SwiftfulRouting in Frameworks */ = {isa = PBXBuildFile; productRef = 7E8D7AE22CECD011009AD3DF /* SwiftfulRouting */; }; 7E8D7AF12CECEB30009AD3DF /* Images+Generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7AEF2CECEB30009AD3DF /* Images+Generated.swift */; }; 7E8D7AF22CECEB30009AD3DF /* Strings+Generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E8D7AF02CECEB30009AD3DF /* Strings+Generated.swift */; }; @@ -650,6 +664,20 @@ 7E7175892CECC5C70059F30B /* launchscreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = launchscreen.storyboard; sourceTree = ""; }; 7E71758A2CECC5C70059F30B /* server_features.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = server_features.plist; sourceTree = ""; }; 7E71758B2CECC5C70059F30B /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; + 7E84429F2CF297E5001CEBD2 /* CameraCellPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraCellPreview.swift; sourceTree = ""; }; + 7E8442A02CF297E5001CEBD2 /* CameraPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPicker.swift; sourceTree = ""; }; + 7E8442A12CF297E5001CEBD2 /* CameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = ""; }; + 7E8442A22CF297E5001CEBD2 /* GalleryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryView.swift; sourceTree = ""; }; + 7E8442A42CF297E5001CEBD2 /* AttachmentPickerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentPickerScreen.swift; sourceTree = ""; }; + 7E8442A52CF297E5001CEBD2 /* ContactsPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsPickerView.swift; sourceTree = ""; }; + 7E8442A62CF297E5001CEBD2 /* FilesPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesPickerView.swift; sourceTree = ""; }; + 7E8442A72CF297E5001CEBD2 /* LocationPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerView.swift; sourceTree = ""; }; + 7E8442A82CF297E5001CEBD2 /* MediaPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerView.swift; sourceTree = ""; }; + 7E8442AA2CF297E5001CEBD2 /* ConversationMessageContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMessageContainer.swift; sourceTree = ""; }; + 7E8442AB2CF297E5001CEBD2 /* ConversationMessageRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMessageRow.swift; sourceTree = ""; }; + 7E8442AC2CF297E5001CEBD2 /* ConversationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationScreen.swift; sourceTree = ""; }; + 7E8442AD2CF297E5001CEBD2 /* ConversationSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSettingsScreen.swift; sourceTree = ""; }; + 7E8442AE2CF297E5001CEBD2 /* ConversationTextInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTextInput.swift; sourceTree = ""; }; 7E8D7AEE2CECEB30009AD3DF /* Colors+Generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Colors+Generated.swift"; sourceTree = ""; }; 7E8D7AEF2CECEB30009AD3DF /* Images+Generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Images+Generated.swift"; sourceTree = ""; }; 7E8D7AF02CECEB30009AD3DF /* Strings+Generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Generated.swift"; sourceTree = ""; }; @@ -1489,10 +1517,48 @@ path = Strings; sourceTree = ""; }; + 7E8442A32CF297E5001CEBD2 /* MediaPickerComponents */ = { + isa = PBXGroup; + children = ( + 7E84429F2CF297E5001CEBD2 /* CameraCellPreview.swift */, + 7E8442A02CF297E5001CEBD2 /* CameraPicker.swift */, + 7E8442A12CF297E5001CEBD2 /* CameraView.swift */, + 7E8442A22CF297E5001CEBD2 /* GalleryView.swift */, + ); + path = MediaPickerComponents; + sourceTree = ""; + }; + 7E8442A92CF297E5001CEBD2 /* Attachments */ = { + isa = PBXGroup; + children = ( + 7E8442A32CF297E5001CEBD2 /* MediaPickerComponents */, + 7E8442A42CF297E5001CEBD2 /* AttachmentPickerScreen.swift */, + 7E8442A52CF297E5001CEBD2 /* ContactsPickerView.swift */, + 7E8442A62CF297E5001CEBD2 /* FilesPickerView.swift */, + 7E8442A72CF297E5001CEBD2 /* LocationPickerView.swift */, + 7E8442A82CF297E5001CEBD2 /* MediaPickerView.swift */, + ); + path = Attachments; + sourceTree = ""; + }; + 7E8442AF2CF297E5001CEBD2 /* Conversation */ = { + isa = PBXGroup; + children = ( + 7E8442A92CF297E5001CEBD2 /* Attachments */, + 7E8442AA2CF297E5001CEBD2 /* ConversationMessageContainer.swift */, + 7E8442AB2CF297E5001CEBD2 /* ConversationMessageRow.swift */, + 7E8442AC2CF297E5001CEBD2 /* ConversationScreen.swift */, + 7E8442AD2CF297E5001CEBD2 /* ConversationSettingsScreen.swift */, + 7E8442AE2CF297E5001CEBD2 /* ConversationTextInput.swift */, + ); + path = Conversation; + sourceTree = ""; + }; 7E8D7AE42CECD037009AD3DF /* Views */ = { isa = PBXGroup; children = ( 7E995F222CEAC5D2005B30EE /* RootView.swift */, + 7E8442AF2CF297E5001CEBD2 /* Conversation */, 7E8D7AF92CECEDB3009AD3DF /* SharedComponents */, 54D8CBD8978DA29C88226FBB /* Enter */, D7FD95FF8F72ECE4DBEE1095 /* Main */, @@ -2656,6 +2722,20 @@ 7E8D7B212CECEE79009AD3DF /* Map+Extensions.swift in Sources */, 7E8D7B222CECEE79009AD3DF /* View+Flip.swift in Sources */, 7E8D7B232CECEE79009AD3DF /* View+TappableArea.swift in Sources */, + 7E8442B02CF297E5001CEBD2 /* AttachmentPickerScreen.swift in Sources */, + 7E8442B12CF297E5001CEBD2 /* ConversationMessageRow.swift in Sources */, + 7E8442B22CF297E5001CEBD2 /* CameraCellPreview.swift in Sources */, + 7E8442B32CF297E5001CEBD2 /* ConversationScreen.swift in Sources */, + 7E8442B42CF297E5001CEBD2 /* MediaPickerView.swift in Sources */, + 7E8442B52CF297E5001CEBD2 /* ConversationTextInput.swift in Sources */, + 7E8442B62CF297E5001CEBD2 /* ConversationSettingsScreen.swift in Sources */, + 7E8442B72CF297E5001CEBD2 /* CameraPicker.swift in Sources */, + 7E8442B82CF297E5001CEBD2 /* ConversationMessageContainer.swift in Sources */, + 7E8442B92CF297E5001CEBD2 /* ContactsPickerView.swift in Sources */, + 7E8442BA2CF297E5001CEBD2 /* CameraView.swift in Sources */, + 7E8442BB2CF297E5001CEBD2 /* GalleryView.swift in Sources */, + 7E8442BC2CF297E5001CEBD2 /* LocationPickerView.swift in Sources */, + 7E8442BD2CF297E5001CEBD2 /* FilesPickerView.swift in Sources */, 7E8D7B242CECEE79009AD3DF /* URL+Extensions.swift in Sources */, 7E8D7B252CECEE79009AD3DF /* PHImageManager+Fetch.swift in Sources */, 7E8D7B262CECEE79009AD3DF /* View+If.swift in Sources */, diff --git a/Monal/another.im/Views/Conversation/Attachments/AttachmentPickerScreen.swift b/Monal/another.im/Views/Conversation/Attachments/AttachmentPickerScreen.swift new file mode 100644 index 0000000..8c57bd4 --- /dev/null +++ b/Monal/another.im/Views/Conversation/Attachments/AttachmentPickerScreen.swift @@ -0,0 +1,131 @@ +import SwiftUI + +enum AttachmentTab: Int, CaseIterable { + case media + case files + case location + case contacts +} + +struct AttachmentPickerScreen: View { + @Environment(\.router) var router + + @State private var selectedTab: AttachmentTab = .media + + var body: some View { + ZStack { + // Background color + Color.Material.Background.light + .ignoresSafeArea() + + // Content + VStack(spacing: 0) { + // Header + SharedNavigationBar( + leftButton: .init( + image: Image(systemName: "xmark"), + action: { + router.dismissScreen() + } + ), + centerText: .init(text: L10n.Attachment.Prompt.main) + ) + + // Pickers + switch selectedTab { + case .media: + MediaPickerView() + + case .files: + FilesPickerView() + + case .location: + LocationPickerView() + + case .contacts: + ContactsPickerView() + } + + // Tab bar + AttachmentTabBar(selectedTab: $selectedTab) + } + } + } +} + +struct AttachmentTabBar: View { + @Binding var selectedTab: AttachmentTab + + var body: some View { + VStack(spacing: 0) { + Rectangle() + .frame(maxWidth: .infinity) + .frame(height: 0.2) + .foregroundColor(.Material.Shape.separator) + HStack(spacing: 0) { + AttachmentTabBarButton(tab: .media, selected: $selectedTab) + AttachmentTabBarButton(tab: .files, selected: $selectedTab) + AttachmentTabBarButton(tab: .location, selected: $selectedTab) + AttachmentTabBarButton(tab: .contacts, selected: $selectedTab) + } + .background(Color.Material.Background.dark) + } + .frame(height: 50) + } +} + +private struct AttachmentTabBarButton: View { + let tab: AttachmentTab + @Binding var selected: AttachmentTab + + var body: some View { + ZStack { + VStack(spacing: 2) { + buttonImg + .foregroundColor(selected == tab ? .Material.Elements.active : .Material.Elements.inactive) + .font(.system(size: 24, weight: .light)) + .symbolRenderingMode(.hierarchical) + Text(buttonTitle) + .font(.sub1) + .foregroundColor(selected == tab ? .Material.Text.main : .Material.Elements.inactive) + } + Rectangle() + .foregroundColor(.white.opacity(0.01)) + .onTapGesture { + selected = tab + } + } + } + + var buttonImg: Image { + switch tab { + case .media: + return Image(systemName: "photo.on.rectangle.angled") + + case .files: + return Image(systemName: "doc.on.doc") + + case .location: + return Image(systemName: "location.circle") + + case .contacts: + return Image(systemName: "person.crop.circle") + } + } + + var buttonTitle: String { + switch tab { + case .media: + return L10n.Attachment.Tab.media + + case .files: + return L10n.Attachment.Tab.files + + case .location: + return L10n.Attachment.Tab.location + + case .contacts: + return L10n.Attachment.Tab.contacts + } + } +} diff --git a/Monal/another.im/Views/Conversation/Attachments/ContactsPickerView.swift b/Monal/another.im/Views/Conversation/Attachments/ContactsPickerView.swift new file mode 100644 index 0000000..86b2087 --- /dev/null +++ b/Monal/another.im/Views/Conversation/Attachments/ContactsPickerView.swift @@ -0,0 +1,71 @@ +import SwiftUI + +struct ContactsPickerView: View { + @Environment(\.router) var router + // @EnvironmentObject var messages: MessagesStore + + // @State private var rosters: [Roster] = [] + // @State private var selectedContact: Roster? + + var body: some View { + Text("dumb") + // VStack(spacing: 0) { + // // Contacts list + // if !rosters.isEmpty { + // List { + // ForEach(rosters) { roster in + // ContactRow(roster: roster, selectedContact: $selectedContact) + // } + // } + // .listStyle(.plain) + // .background(Color.Material.Background.light) + // } else { + // Spacer() + // } + // + // // Send panel + // Rectangle() + // .foregroundColor(.Material.Shape.black) + // .frame(maxWidth: .infinity) + // .frame(height: selectedContact == nil ? 0 : 50) + // .overlay { + // HStack { + // Text(L10n.Attachment.Send.contact) + // .foregroundColor(.Material.Text.white) + // .font(.body1) + // Image(systemName: "arrow.up.circle") + // .foregroundColor(.Material.Text.white) + // .font(.body1) + // .padding(.leading, 8) + // } + // .padding() + // } + // .clipped() + // .onTapGesture { + // if let selectedContact = selectedContact { + // messages.sendContact(selectedContact.contactBareJid) + // router.dismissEnvironment() + // } + // } + // } + // .task { + // rosters = await Roster.allActive + // } + } +} + +// private struct ContactRow: View { +// var roster: Roster +// @Binding var selectedContact: Roster? +// +// var body: some View { +// SharedListRow( +// iconType: .charCircle(roster.name?.firstLetter ?? roster.contactBareJid.firstLetter), +// text: roster.contactBareJid, +// controlType: .none +// ) +// .onTapGesture { +// selectedContact = roster +// } +// } +// } diff --git a/Monal/another.im/Views/Conversation/Attachments/FilesPickerView.swift b/Monal/another.im/Views/Conversation/Attachments/FilesPickerView.swift new file mode 100644 index 0000000..b1062d8 --- /dev/null +++ b/Monal/another.im/Views/Conversation/Attachments/FilesPickerView.swift @@ -0,0 +1,66 @@ +import SwiftUI +import UIKit + +struct FilesPickerView: View { + @Environment(\.router) var router + // @EnvironmentObject var attachments: AttachmentsStore + + var body: some View { + Text("dumb") + // DocumentPicker( + // completion: { dataArray, extensionsArray in + // attachments.sendDocuments(dataArray, extensionsArray) + // router.dismissEnvironment() + // }, + // cancel: { + // router.dismissEnvironment() + // } + // ) + } +} + +struct DocumentPicker: UIViewControllerRepresentable { + let completion: ([Data], [String]) -> Void + let cancel: () -> Void + + func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIDocumentPickerViewController { + let picker: UIDocumentPickerViewController + picker = UIDocumentPickerViewController(forOpeningContentTypes: [.item], asCopy: true) + picker.delegate = context.coordinator + picker.allowsMultipleSelection = true + return picker + } + + func updateUIViewController(_: UIDocumentPickerViewController, context _: UIViewControllerRepresentableContext) {} + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, UIDocumentPickerDelegate { + var parent: DocumentPicker + + init(_ parent: DocumentPicker) { + self.parent = parent + } + + func documentPicker(_: UIDocumentPickerViewController, didPickDocumentsAt: [URL]) { + var dataArray = [Data]() + var extensionArray = [String]() + for url in didPickDocumentsAt { + do { + let data = try Data(contentsOf: url) + dataArray.append(data) + extensionArray.append(url.pathExtension) + } catch { + print("Unable to load data from \(url): \(error)") + } + } + parent.completion(dataArray, extensionArray) + } + + func documentPickerWasCancelled(_: UIDocumentPickerViewController) { + parent.cancel() + } + } +} diff --git a/Monal/another.im/Views/Conversation/Attachments/LocationPickerView.swift b/Monal/another.im/Views/Conversation/Attachments/LocationPickerView.swift new file mode 100644 index 0000000..c5159c1 --- /dev/null +++ b/Monal/another.im/Views/Conversation/Attachments/LocationPickerView.swift @@ -0,0 +1,137 @@ +import MapKit +import SwiftUI + +struct LocationPickerView: View { + @Environment(\.router) var router + // @EnvironmentObject var messages: MessagesStore + + @StateObject var locationManager = LocationManager() + @State private var region = MKCoordinateRegion() + + var body: some View { + VStack(spacing: 0) { + ZStack { + // MapView + MapView(region: $region) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.region = locationManager.region + } + } + .overlay { + Image(systemName: "mappin") + .foregroundColor(.Material.Elements.active) + .font(.system(size: 30)) + .shadow(color: .white, radius: 2) + } + + // Track button + VStack { + Spacer() + HStack { + Spacer() + Image(systemName: "location.circle") + .resizable() + .frame(width: 40, height: 40) + .foregroundColor(.Material.Elements.active) + .background(Color.Material.Shape.white) + .clipShape(Circle()) + .shadow(color: .white, radius: 2) + .padding(.trailing) + .padding(.bottom, 50) + .tappablePadding(.symmetric(10)) { + self.region = locationManager.region + } + } + } + } + + // Send panel + Rectangle() + .foregroundColor(.Material.Shape.black) + .frame(maxWidth: .infinity) + .frame(height: 50) + .overlay { + HStack { + Text(L10n.Attachment.Send.location) + .foregroundColor(.Material.Text.white) + .font(.body1) + Image(systemName: "arrow.up.circle") + .foregroundColor(.Material.Text.white) + .font(.body1) + .padding(.leading, 8) + } + .padding() + } + .clipped() + .onTapGesture { + // messages.sendLocation(region.center.latitude, region.center.longitude) + router.dismissEnvironment() + } + } + .onAppear { + locationManager.start() + } + } +} + +struct MapView: UIViewRepresentable { + @Binding var region: MKCoordinateRegion + + func makeUIView(context: Context) -> MKMapView { + let mapView = MKMapView() + mapView.delegate = context.coordinator + mapView.showsUserLocation = false + mapView.userTrackingMode = .none + + return mapView + } + + func updateUIView(_ uiView: MKMapView, context _: Context) { + if uiView.region != region { + uiView.setRegion(region, animated: true) + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, MKMapViewDelegate { + var parent: MapView + + init(_ parent: MapView) { + self.parent = parent + } + } +} + +class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate { + private let locationManager = CLLocationManager() + @Published var region: MKCoordinateRegion + + override init() { + region = MKCoordinateRegion() + super.init() + locationManager.delegate = self + locationManager.desiredAccuracy = kCLLocationAccuracyBest + } + + func start() { + locationManager.requestWhenInUseAuthorization() + locationManager.startUpdatingLocation() + } + + func stop() { + locationManager.stopUpdatingLocation() + } + + func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + if let loc = locations.first { + region = MKCoordinateRegion( + center: loc.coordinate, + span: MKCoordinateSpan(latitudeDelta: 0.002, longitudeDelta: 0.002) + ) + } + } +} diff --git a/Monal/another.im/Views/Conversation/Attachments/MediaPickerComponents/CameraCellPreview.swift b/Monal/another.im/Views/Conversation/Attachments/MediaPickerComponents/CameraCellPreview.swift new file mode 100644 index 0000000..73ca5ab --- /dev/null +++ b/Monal/another.im/Views/Conversation/Attachments/MediaPickerComponents/CameraCellPreview.swift @@ -0,0 +1,61 @@ +import AVFoundation +import SwiftUI + +struct CameraCellPreview: View { + @Environment(\.router) var router + // @EnvironmentObject var attachments: AttachmentsStore + + var body: some View { + Text("dumb") + // Group { + // if attachments.cameraAccessGranted { + // ZStack { + // CameraView() + // .aspectRatio(1, contentMode: .fit) + // .frame(maxWidth: .infinity) + // Image(systemName: "camera") + // .resizable() + // .aspectRatio(contentMode: .fit) + // .frame(width: 40, height: 40) + // .foregroundColor(.white) + // .padding(8) + // .background(Color.black.opacity(0.5)) + // .clipShape(Circle()) + // .padding(8) + // } + // .onTapGesture { + // router.showScreen(.fullScreenCover) { _ in + // CameraPicker { data, type in + // attachments.sendCaptured(data, type) + // router.dismissEnvironment() + // } + // .ignoresSafeArea(.all) + // } + // } + // } else { + // Button { + // openAppSettings() + // } label: { + // ZStack { + // Rectangle() + // .fill(Color.Material.Background.light) + // .overlay { + // VStack { + // Image(systemName: "camera") + // .foregroundColor(.Material.Elements.active) + // .font(.system(size: 30)) + // Text("Allow camera access") + // .foregroundColor(.Material.Text.main) + // .font(.body3) + // } + // } + // .frame(height: 100) + // } + // } + // } + // } + // .task { + // await attachments.checkCameraAuthorization() + // } + } +} diff --git a/Monal/another.im/Views/Conversation/Attachments/MediaPickerComponents/CameraPicker.swift b/Monal/another.im/Views/Conversation/Attachments/MediaPickerComponents/CameraPicker.swift new file mode 100644 index 0000000..eae692b --- /dev/null +++ b/Monal/another.im/Views/Conversation/Attachments/MediaPickerComponents/CameraPicker.swift @@ -0,0 +1,49 @@ +import Foundation +import Photos +import SwiftUI + +struct CameraPicker: UIViewControllerRepresentable { + // var completionHandler: (Data, GalleryMediaType) -> Void + + func makeUIViewController(context: Context) -> UIImagePickerController { + let picker = UIImagePickerController() + picker.sourceType = .camera + picker.delegate = context.coordinator + picker.mediaTypes = [UTType.movie.identifier, UTType.image.identifier] + picker.videoQuality = .typeHigh + picker.videoMaximumDuration = Const.videoDurationLimit + picker.view.backgroundColor = .clear + return picker + } + + func updateUIViewController(_: UIImagePickerController, context _: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { + let parent: CameraPicker + + init(_ parent: CameraPicker) { + self.parent = parent + } + + func imagePickerController(_: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + // swiftlint:disable:next force_cast + let mediaType = info[.mediaType] as! String + + // if mediaType == UTType.image.identifier { + // if let image = info[.originalImage] as? UIImage { + // let data = image.jpegData(compressionQuality: 1.0) ?? Data() + // parent.completionHandler(data, .photo) + // } + // } else if mediaType == UTType.movie.identifier { + // if let url = info[.mediaURL] as? URL { + // let data = try? Data(contentsOf: url) + // parent.completionHandler(data ?? Data(), .video) + // } + // } + } + } +} diff --git a/Monal/another.im/Views/Conversation/Attachments/MediaPickerComponents/CameraView.swift b/Monal/another.im/Views/Conversation/Attachments/MediaPickerComponents/CameraView.swift new file mode 100644 index 0000000..e7261fd --- /dev/null +++ b/Monal/another.im/Views/Conversation/Attachments/MediaPickerComponents/CameraView.swift @@ -0,0 +1,38 @@ +import AVFoundation +import SwiftUI +import UIKit + +class CameraUIView: UIView { + var previewLayer: AVCaptureVideoPreviewLayer? + + override func layoutSubviews() { + super.layoutSubviews() + previewLayer?.frame = bounds + } +} + +struct CameraView: UIViewRepresentable { + func makeUIView(context _: Context) -> CameraUIView { + let view = CameraUIView() + + let captureSession = AVCaptureSession() + guard let captureDevice = AVCaptureDevice.default(for: .video) else { return view } + guard let input = try? AVCaptureDeviceInput(device: captureDevice) else { return view } + captureSession.addInput(input) + + let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + previewLayer.videoGravity = .resizeAspectFill + view.layer.addSublayer(previewLayer) + view.previewLayer = previewLayer + + DispatchQueue.global(qos: .background).async { + captureSession.startRunning() + } + + return view + } + + func updateUIView(_ uiView: CameraUIView, context _: Context) { + uiView.previewLayer?.frame = uiView.bounds + } +} diff --git a/Monal/another.im/Views/Conversation/Attachments/MediaPickerComponents/GalleryView.swift b/Monal/another.im/Views/Conversation/Attachments/MediaPickerComponents/GalleryView.swift new file mode 100644 index 0000000..eaaa60b --- /dev/null +++ b/Monal/another.im/Views/Conversation/Attachments/MediaPickerComponents/GalleryView.swift @@ -0,0 +1,116 @@ +import SwiftUI + +struct GalleryView: View { + // @EnvironmentObject var attachments: AttachmentsStore + @Binding var selectedItems: [String] + + var body: some View { + Text("dumb") + // Group { + // if attachments.galleryAccessGranted { + // ForEach(attachments.galleryItems) { item in + // GridViewItem(item: item, selected: $selectedItems) + // } + // } else { + // Button { + // openAppSettings() + // } label: { + // ZStack { + // Rectangle() + // .fill(Color.Material.Background.light) + // .overlay { + // VStack { + // Image(systemName: "photo") + // .foregroundColor(.Material.Elements.active) + // .font(.system(size: 30)) + // Text("Allow gallery access") + // .foregroundColor(.Material.Text.main) + // .font(.body3) + // } + // } + // .frame(height: 100) + // } + // } + // } + // } + // .task { + // await attachments.checkGalleryAuthorization() + // } + } +} + +private struct GridViewItem: View { + // @State var item: GalleryItem + // @Binding var selected: [String] + + var body: some View { + Text("dumb") + // if let img = item.thumbnail { + // ZStack { + // img + // .resizable() + // .aspectRatio(contentMode: .fill) + // .frame(width: Const.galleryGridSize, height: Const.galleryGridSize) + // .clipped() + // if let duration = item.duration { + // VStack { + // Spacer() + // HStack { + // Spacer() + // Text(duration) + // .foregroundColor(.Material.Text.white) + // .font(.sub1) + // .shadow(color: .black, radius: 2) + // .padding(4) + // } + // } + // } + // if isSelected { + // VStack { + // HStack { + // Spacer() + // Circle() + // .frame(width: 30, height: 30) + // .shadow(color: .black, radius: 2) + // .foregroundColor(.Material.Shape.white) + // .overlay { + // Image(systemName: "checkmark") + // .foregroundColor(.Material.Elements.active) + // .font(.body3) + // } + // .padding(4) + // } + // Spacer() + // } + // } + // } + // .onTapGesture { + // if isSelected { + // selected.removeAll { $0 == item.id } + // } else { + // selected.append(item.id) + // } + // } + // } else { + // ZStack { + // Rectangle() + // .fill(Color.Material.Background.light) + // .overlay { + // ProgressView() + // .foregroundColor(.Material.Elements.active) + // } + // .frame(width: Const.galleryGridSize, height: Const.galleryGridSize) + // } + // .task { + // if item.thumbnail == nil { + // try? await item.fetchThumbnail() + // } + // } + // } + } + + private var isSelected: Bool { + false + // selected.contains(item.id) + } +} diff --git a/Monal/another.im/Views/Conversation/Attachments/MediaPickerView.swift b/Monal/another.im/Views/Conversation/Attachments/MediaPickerView.swift new file mode 100644 index 0000000..624a3ee --- /dev/null +++ b/Monal/another.im/Views/Conversation/Attachments/MediaPickerView.swift @@ -0,0 +1,52 @@ +import AVFoundation +import MobileCoreServices +import Photos +import SwiftUI + +struct MediaPickerView: View { + @Environment(\.router) var router + // @EnvironmentObject var attachments: AttachmentsStore + + @State private var selectedItems: [String] = [] + + var body: some View { + let columns = Array(repeating: GridItem(.flexible(), spacing: 0), count: 3) + + VStack(spacing: 0) { + // List of media + ScrollView(showsIndicators: false) { + LazyVGrid(columns: columns, spacing: 0) { + // For camera + CameraCellPreview() + + // For gallery + GalleryView(selectedItems: $selectedItems) + } + } + + // Send panel + Rectangle() + .foregroundColor(.Material.Shape.black) + .frame(maxWidth: .infinity) + .frame(height: self.selectedItems.isEmpty ? 0 : 50) + .overlay { + HStack { + Text(L10n.Attachment.Send.media) + .foregroundColor(.Material.Text.white) + .font(.body1) + Image(systemName: "arrow.up.circle") + .foregroundColor(.Material.Text.white) + .font(.body1) + .padding(.leading, 8) + } + .padding() + } + .clipped() + .onTapGesture { + // let items = attachments.galleryItems.filter { selectedItems.contains($0.id) } + // attachments.sendMedia(items) + router.dismissEnvironment() + } + } + } +} diff --git a/Monal/another.im/Views/Conversation/ConversationMessageContainer.swift b/Monal/another.im/Views/Conversation/ConversationMessageContainer.swift new file mode 100644 index 0000000..9ffc3ac --- /dev/null +++ b/Monal/another.im/Views/Conversation/ConversationMessageContainer.swift @@ -0,0 +1,263 @@ +import AVKit +import MapKit +import QuickLook +import SwiftUI + +struct ConversationMessageContainer: View { + // let message: Message + let isOutgoing: Bool + + var body: some View { + Text("dumb") + // if let msgText = message.body, msgText.isLocation { + // EmbededMapView(location: msgText.getLatLon) + // } else if let msgText = message.body, msgText.isContact { + // ContactView(message: message) + // } else if case .attachment(let attachment) = message.contentType { + // AttachmentView(message: message, attachment: attachment) + // } else { + // Text(message.body ?? "...") + // .font(.body2) + // .foregroundColor(.Material.Text.main) + // .multilineTextAlignment(.leading) + // .padding(10) + // } + } +} + +struct MessageAttr: View { + // let message: Message + + var body: some View { + // VStack(alignment: .leading, spacing: 0) { + // Text(message.date, style: .time) + // .font(.sub2) + // .foregroundColor(.Material.Shape.separator) + // Spacer() + // if message.status == .error { + // Image(systemName: "exclamationmark.circle") + // .font(.body3) + // .foregroundColor(.Rainbow.red500) + // } else if message.status == .pending { + // Image(systemName: "clock") + // .font(.body3) + // .foregroundColor(.Material.Shape.separator) + // } else if message.secure { + // Image(systemName: "lock") + // .font(.body3) + // .foregroundColor(.Material.Shape.separator) + // } + // } + Text("dumb") + } +} + +private struct EmbededMapView: View { + let location: CLLocationCoordinate2D + + var body: some View { + Map( + coordinateRegion: .constant(MKCoordinateRegion(center: location, span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))), + interactionModes: [], + showsUserLocation: false, + userTrackingMode: .none, + annotationItems: [location], + annotationContent: { _ in + MapMarker(coordinate: location, tint: .blue) + } + ) + .frame(width: Const.mapPreviewSize, height: Const.mapPreviewSize) + .onTapGesture { + let mapItem = MKMapItem(placemark: MKPlacemark(coordinate: location)) + mapItem.name = "Location" + mapItem.openInMaps(launchOptions: [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving]) + } + } +} + +private struct ContactView: View { + // let message: Message + + var body: some View { + VStack { + ZStack { + Circle() + .frame(width: 44, height: 44) + .foregroundColor(contactName.firstLetterColor) + Text(contactName.firstLetter) + .foregroundColor(.white) + .font(.body1) + } + // Text(message.body?.getContactJid ?? "...") + // .font(.body2) + // .foregroundColor(.Material.Text.main) + // .multilineTextAlignment(.leading) + } + .padding() + .onTapGesture { + // TODO: Jump to add roster from here + } + } + + private var contactName: String { + "dumb" + // message.body?.getContactJid ?? "?" + } +} + +// private struct AttachmentView: View { +// @EnvironmentObject var attachments: AttachmentsStore +// +// let message: Message +// let attachment: Attachment +// +// var body: some View { +// if message.status == .error { +// failed +// } else { +// switch attachment.type { +// case .image: +// AsyncImage(url: attachment.thumbnailPath) { image in +// image +// .resizable() +// .aspectRatio(contentMode: .fit) +// .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) +// } placeholder: { +// placeholder +// } +// .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) +// +// case .video: +// if let file = attachment.localPath { +// VideoPlayerView(url: file) +// .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) +// } else { +// placeholder +// } +// +// case .file: +// if let file = attachment.localPath { +// DocumentPreview(url: file) +// .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) +// } else { +// placeholder +// } +// +// default: +// placeholder +// } +// } +// } +// +// @ViewBuilder private var placeholder: some View { +// Rectangle() +// .foregroundColor(.Material.Background.dark) +// .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) +// .overlay { +// ZStack { +// ProgressView() +// .scaleEffect(1.5) +// .progressViewStyle(CircularProgressViewStyle(tint: .Material.Elements.active)) +// let imageName = progressImageName(attachment.type) +// Image(systemName: imageName) +// .font(.body1) +// .foregroundColor(.Material.Elements.active) +// } +// } +// } +// +// @ViewBuilder private var failed: some View { +// Rectangle() +// .foregroundColor(.Material.Background.dark) +// .frame(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize) +// .overlay { +// ZStack { +// VStack { +// Text(L10n.Attachment.Downloading.retry) +// .font(.body3) +// .foregroundColor(.Rainbow.red500) +// Image(systemName: "exclamationmark.arrow.triangle.2.circlepath") +// .font(.body1) +// .foregroundColor(.Rainbow.red500) +// } +// } +// } +// .onTapGesture { +// Task { +// try? await message.setStatus(.pending) +// } +// } +// } +// +// private func progressImageName(_ type: AttachmentType) -> String { +// switch type { +// case .image: +// return "photo" +// +// case .audio: +// return "music.note" +// +// case .video: +// return "film" +// +// case .file: +// return "doc" +// } +// } +// +// private func thumbnail() -> Image? { +// guard let thumbnailPath = attachment.thumbnailPath else { return nil } +// guard let uiImage = UIImage(contentsOfFile: thumbnailPath.path()) else { return nil } +// return Image(uiImage: uiImage) +// } +// } + +// TODO: Make video player better! +private struct VideoPlayerView: UIViewControllerRepresentable { + let url: URL + + func makeUIViewController(context _: Context) -> AVPlayerViewController { + let controller = AVPlayerViewController() + controller.player = AVPlayer(url: url) + controller.allowsPictureInPicturePlayback = true + return controller + } + + func updateUIViewController(_: AVPlayerViewController, context _: Context) { + // Update the controller if needed. + } +} + +struct DocumentPreview: UIViewControllerRepresentable { + var url: URL + + func makeUIViewController(context: Context) -> QLPreviewController { + let controller = QLPreviewController() + controller.dataSource = context.coordinator + return controller + } + + func updateUIViewController(_: QLPreviewController, context _: Context) { + // Update the controller if needed. + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, QLPreviewControllerDataSource { + var parent: DocumentPreview + + init(_ parent: DocumentPreview) { + self.parent = parent + } + + func numberOfPreviewItems(in _: QLPreviewController) -> Int { + 1 + } + + func previewController(_: QLPreviewController, previewItemAt _: Int) -> QLPreviewItem { + parent.url as QLPreviewItem + } + } +} diff --git a/Monal/another.im/Views/Conversation/ConversationMessageRow.swift b/Monal/another.im/Views/Conversation/ConversationMessageRow.swift new file mode 100644 index 0000000..a229fa3 --- /dev/null +++ b/Monal/another.im/Views/Conversation/ConversationMessageRow.swift @@ -0,0 +1,82 @@ +import Foundation +import SwiftUI + +struct ConversationMessageRow: View { + // @EnvironmentObject var messages: MessagesStore + // let message: Message + @State private var offset: CGSize = .zero + + var body: some View { + VStack(spacing: 0) { + HStack(spacing: 0) { + if isOutgoing() { + Spacer() + // MessageAttr(message: message) + // .padding(.trailing, 4) + } + // ConversationMessageContainer(message: message, isOutgoing: isOutgoing()) + // .background(isOutgoing() ? Color.Material.Shape.alternate : Color.Material.Shape.white) + // .clipShape(ConversationMessageBubble(isOutgoing: isOutgoing())) + if !isOutgoing() { + // MessageAttr(message: message) + // .padding(.leading, 4) + Spacer() + } + } + .padding(.vertical, 10) + .padding(.horizontal, 16) + .background(Color.clearTappable) + .offset(offset) + .gesture( + DragGesture(minimumDistance: 30, coordinateSpace: .local) + .onChanged { value in + var width = value.translation.width + width = width > 0 ? 0 : width + offset = CGSize(width: width, height: 0) + } + .onEnded { value in + let targetWidth: CGFloat = -90 + withAnimation(.easeOut(duration: 0.1)) { + if value.translation.width <= targetWidth { + Vibration.success.vibrate() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + withAnimation(.easeOut(duration: 0.1)) { + offset = .zero + } + } + } else { + offset = .zero + } + } + if value.translation.width <= targetWidth { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.02) { + // messages.replyText = message.body ?? "" + } + } + } + ) + } + .listRowInsets(.zero) + .listRowSeparator(.hidden) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.Material.Background.light) + } + + private func isOutgoing() -> Bool { + true + // message.from == messages.roster.bareJid + } +} + +struct ConversationMessageBubble: Shape { + let isOutgoing: Bool + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: isOutgoing ? [.topLeft, .bottomLeft, .bottomRight] : [.topRight, .bottomLeft, .bottomRight], + cornerRadii: CGSize(width: 8, height: 10) + ) + return Path(path.cgPath) + } +} diff --git a/Monal/another.im/Views/Conversation/ConversationScreen.swift b/Monal/another.im/Views/Conversation/ConversationScreen.swift new file mode 100644 index 0000000..ee40b30 --- /dev/null +++ b/Monal/another.im/Views/Conversation/ConversationScreen.swift @@ -0,0 +1,126 @@ +import Combine +import Foundation +import SwiftUI + +struct ConversationScreen: View { + @Environment(\.router) var router + @EnvironmentObject var chatModel: ChatModel + // @StateObject var messagesStore: MessagesStore + // @StateObject var attachments: AttachmentsStore + // @StateObject var settings: ChatSettingsStore + + @State private var autoScroll = true + @State private var firstIsVisible = true + + var body: some View { + ZStack { + // Background color + Color.Material.Background.light + .ignoresSafeArea() + + // Content + VStack(spacing: 0) { + // Header + SharedNavigationBar( + leftButton: .init( + image: Image(systemName: "chevron.left"), + action: { + router.dismissScreen() + } + ), + centerText: .init(text: centerText()), + rightButton: .init( + image: Image(systemName: "gear"), + action: { + router.showScreen(.push) { _ in + ConversationSettingsScreen() + // .environmentObject(settings) + .navigationBarHidden(true) + } + } + ) + ) + + // Msg list + // let messages = messagesStore.messages + // if !messages.isEmpty { + // ScrollViewReader { proxy in + // ScrollView { + // LazyVStack(spacing: 0) { + // ForEach(messages) { message in + // ConversationMessageRow(message: message) + // .id(message.id) + // .flip() + // .onAppear { + // if message.id == messages.first?.id { + // firstIsVisible = true + // autoScroll = true + // } + // messagesStore.scrolledMessage(message.id) + // } + // .onDisappear { + // if message.id == messages.first?.id { + // firstIsVisible = false + // autoScroll = false + // } + // } + // } + // } + // } + // .flip() + // .scrollDismissesKeyboard(.immediately) + // .onChange(of: autoScroll) { new in + // if new, !firstIsVisible { + // withAnimation { + // proxy.scrollTo(messages.first?.id, anchor: .top) + // } + // } + // } + // } + // } else { + // Spacer() + // } + Spacer() + } + .onTapGesture { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } + + // Jump to last button + if !autoScroll { + VStack { + Spacer() + HStack { + Spacer() + Button { + autoScroll = true + } label: { + ZStack { + Circle() + .fill(Color.Material.Shape.white) + Image(systemName: "arrow.down") + .foregroundColor(.Material.Elements.active) + } + .frame(width: 40, height: 40) + .shadow(color: .black.opacity(0.2), radius: 4) + .padding(.trailing, 8) + .padding(.bottom, 8) + } + } + } + } + } + // .environmentObject(messagesStore) + // .environmentObject(attachments) + // .safeAreaInset(edge: .bottom, spacing: 0) { + // ConversationTextInput(autoScroll: $autoScroll) + // .environmentObject(messagesStore) + // .environmentObject(attachments) + // .environmentObject(settings) + // } + } + + private func centerText() -> String { + chatModel.contact.name ?? chatModel.contact.contactJid + } +} diff --git a/Monal/another.im/Views/Conversation/ConversationSettingsScreen.swift b/Monal/another.im/Views/Conversation/ConversationSettingsScreen.swift new file mode 100644 index 0000000..4c1837c --- /dev/null +++ b/Monal/another.im/Views/Conversation/ConversationSettingsScreen.swift @@ -0,0 +1,51 @@ +import Combine +import Foundation +import SwiftUI + +struct ConversationSettingsScreen: View { + @Environment(\.router) var router + // @EnvironmentObject var settingsStore: ChatSettingsStore + + var body: some View { + ZStack { + // Background color + Color.Material.Background.light + .ignoresSafeArea() + + // Content + VStack(spacing: 0) { + // Header + SharedNavigationBar( + leftButton: .init( + image: Image(systemName: "chevron.left"), + action: { + router.dismissScreen() + } + ), + centerText: .init(text: centerText()) + ) + + // Settings list + // ScrollView { + // LazyVStack(spacing: 0) { + // SharedListRow( + // iconType: .none, + // text: L10n.Conversation.Settings.enableOmemo, + // controlType: .switcher(isOn: Binding( + // get: { settingsStore.chat?.encrypted ?? false }, + // set: { new in + // settingsStore.setSecured(new) + // } + // )) + // ) + // } + // } + } + } + } + + private func centerText() -> String { + // TODO: make center text depend on conversation type in future (chat, group chat, channel, etc.) + L10n.Conversation.Settings.Title.chat + } +} diff --git a/Monal/another.im/Views/Conversation/ConversationTextInput.swift b/Monal/another.im/Views/Conversation/ConversationTextInput.swift new file mode 100644 index 0000000..f8c539f --- /dev/null +++ b/Monal/another.im/Views/Conversation/ConversationTextInput.swift @@ -0,0 +1,104 @@ +import SwiftUI +import UIKit + +struct ConversationTextInput: View { + @Environment(\.router) var router + // @EnvironmentObject var messages: MessagesStore + // @EnvironmentObject var attachments: AttachmentsStore + + @State private var messageStr = "" + @FocusState private var isFocused: Bool + @Binding var autoScroll: Bool + + var body: some View { + VStack(spacing: 0) { + Rectangle() + .foregroundColor(.Material.Shape.separator) + .frame(height: 0.5) + .padding(.bottom, 8) + // if !messages.replyText.isEmpty { + // VStack(spacing: 0) { + // HStack(alignment: .top) { + // Text(messages.replyText) + // .font(.body3) + // .foregroundColor(Color.Material.Text.main) + // .multilineTextAlignment(.leading) + // .lineLimit(3) + // .padding(8) + // Spacer() + // Image(systemName: "xmark") + // .font(.title2) + // .foregroundColor(.Material.Elements.active) + // .padding(.leading, 8) + // .tappablePadding(.symmetric(8)) { + // messages.replyText = "" + // } + // .padding(8) + // } + // .frame(maxWidth: .infinity) + // .background(RoundedRectangle(cornerRadius: 4) + // .foregroundColor(.Material.Background.light) + // .shadow(radius: 0.5) + // ) + // .padding(.bottom, 8) + // .padding(.horizontal, 8) + // } + // .padding(.horizontal, 8) + // } + HStack { + Image(systemName: "paperclip") + .font(.title2) + .foregroundColor(.Material.Elements.active) + .padding(.leading, 8) + .tappablePadding(.symmetric(8)) { + router.showScreen(.fullScreenCover) { _ in + AttachmentPickerScreen() + // .environmentObject(messages) + // .environmentObject(attachments) + } + } + TextField("", text: $messageStr, prompt: Text(L10n.Chat.textfieldPrompt).foregroundColor(.Material.Shape.separator), axis: .vertical) + .font(.body1) + .foregroundColor(Color.Material.Text.main) + .accentColor(.Material.Shape.black) + .focused($isFocused) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.Material.Shape.white) + .clipShape(RoundedRectangle(cornerRadius: 8)) + .padding(.vertical, 4) + let img = messageStr.isEmpty ? "paperplane" : "paperplane.fill" + Image(systemName: img) + .font(.title2) + .foregroundColor(messageStr.isEmpty ? .Material.Elements.inactive : .Material.Elements.active) + .padding(.trailing, 8) + .tappablePadding(.symmetric(8)) { + if !messageStr.isEmpty { + // messages.sendMessage(composedMessage) + // messageStr = "" + // autoScroll = true + // if !messages.replyText.isEmpty { + // messages.replyText = "" + // } + } + } + } + } + .padding(.bottom, 8) + .background(Color.Material.Background.dark) + // .onChange(of: messages.replyText) { new in + // if !new.isEmpty { + // isFocused = true + // } + // } + } + + private var composedMessage: String { + var result = "" + // if !messages.replyText.isEmpty { + // result += messages.replyText.makeReply + "\n\n" + // } + // result += messageStr + return result + } +} diff --git a/Monal/another.im/XMPP/MonalXmppWrapper.swift b/Monal/another.im/XMPP/MonalXmppWrapper.swift index 9f1f1f1..b8968a6 100644 --- a/Monal/another.im/XMPP/MonalXmppWrapper.swift +++ b/Monal/another.im/XMPP/MonalXmppWrapper.swift @@ -54,6 +54,11 @@ extension MonalXmppWrapper { throw AimErrors.contactRemoveError } } + + func chat(with: Contact) -> ChatModel { + let chatModel = ChatModel(contact: with) + return chatModel + } } // MARK: - Try login from Login screen @@ -160,3 +165,12 @@ private extension MonalXmppWrapper { } } } + +// MARK: - Chat object +final class ChatModel: ObservableObject { + let contact: Contact + + init(contact: Contact) { + self.contact = contact + } +}