add conversation screens

This commit is contained in:
Woit 2024-11-24 00:22:07 +01:00
parent 57793daf3d
commit 9d6f610b63
16 changed files with 1441 additions and 0 deletions

View file

@ -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 = "<group>"; };
7E71758A2CECC5C70059F30B /* server_features.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = server_features.plist; sourceTree = "<group>"; };
7E71758B2CECC5C70059F30B /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = "<group>"; };
7E84429F2CF297E5001CEBD2 /* CameraCellPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraCellPreview.swift; sourceTree = "<group>"; };
7E8442A02CF297E5001CEBD2 /* CameraPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPicker.swift; sourceTree = "<group>"; };
7E8442A12CF297E5001CEBD2 /* CameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
7E8442A22CF297E5001CEBD2 /* GalleryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryView.swift; sourceTree = "<group>"; };
7E8442A42CF297E5001CEBD2 /* AttachmentPickerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentPickerScreen.swift; sourceTree = "<group>"; };
7E8442A52CF297E5001CEBD2 /* ContactsPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsPickerView.swift; sourceTree = "<group>"; };
7E8442A62CF297E5001CEBD2 /* FilesPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesPickerView.swift; sourceTree = "<group>"; };
7E8442A72CF297E5001CEBD2 /* LocationPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerView.swift; sourceTree = "<group>"; };
7E8442A82CF297E5001CEBD2 /* MediaPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerView.swift; sourceTree = "<group>"; };
7E8442AA2CF297E5001CEBD2 /* ConversationMessageContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMessageContainer.swift; sourceTree = "<group>"; };
7E8442AB2CF297E5001CEBD2 /* ConversationMessageRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationMessageRow.swift; sourceTree = "<group>"; };
7E8442AC2CF297E5001CEBD2 /* ConversationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationScreen.swift; sourceTree = "<group>"; };
7E8442AD2CF297E5001CEBD2 /* ConversationSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSettingsScreen.swift; sourceTree = "<group>"; };
7E8442AE2CF297E5001CEBD2 /* ConversationTextInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationTextInput.swift; sourceTree = "<group>"; };
7E8D7AEE2CECEB30009AD3DF /* Colors+Generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Colors+Generated.swift"; sourceTree = "<group>"; };
7E8D7AEF2CECEB30009AD3DF /* Images+Generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Images+Generated.swift"; sourceTree = "<group>"; };
7E8D7AF02CECEB30009AD3DF /* Strings+Generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Strings+Generated.swift"; sourceTree = "<group>"; };
@ -1489,10 +1517,48 @@
path = Strings;
sourceTree = "<group>";
};
7E8442A32CF297E5001CEBD2 /* MediaPickerComponents */ = {
isa = PBXGroup;
children = (
7E84429F2CF297E5001CEBD2 /* CameraCellPreview.swift */,
7E8442A02CF297E5001CEBD2 /* CameraPicker.swift */,
7E8442A12CF297E5001CEBD2 /* CameraView.swift */,
7E8442A22CF297E5001CEBD2 /* GalleryView.swift */,
);
path = MediaPickerComponents;
sourceTree = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
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 */,

View file

@ -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
}
}
}

View file

@ -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
// }
// }
// }

View file

@ -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<DocumentPicker>) -> UIDocumentPickerViewController {
let picker: UIDocumentPickerViewController
picker = UIDocumentPickerViewController(forOpeningContentTypes: [.item], asCopy: true)
picker.delegate = context.coordinator
picker.allowsMultipleSelection = true
return picker
}
func updateUIViewController(_: UIDocumentPickerViewController, context _: UIViewControllerRepresentableContext<DocumentPicker>) {}
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()
}
}
}

View file

@ -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)
)
}
}
}

View file

@ -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()
// }
}
}

View file

@ -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)
// }
// }
}
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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()
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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)
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}