add conversation screens
This commit is contained in:
parent
57793daf3d
commit
9d6f610b63
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
// }
|
||||
// }
|
||||
// }
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
// }
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
126
Monal/another.im/Views/Conversation/ConversationScreen.swift
Normal file
126
Monal/another.im/Views/Conversation/ConversationScreen.swift
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
104
Monal/another.im/Views/Conversation/ConversationTextInput.swift
Normal file
104
Monal/another.im/Views/Conversation/ConversationTextInput.swift
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue