diff --git a/ConversationsClassic/Resources/Strings/Localizable.strings b/ConversationsClassic/Resources/Strings/Localizable.strings index 8ae665d..5f97a53 100644 --- a/ConversationsClassic/Resources/Strings/Localizable.strings +++ b/ConversationsClassic/Resources/Strings/Localizable.strings @@ -50,7 +50,16 @@ "Conversation.startError" = "Error occurs in conversation starting"; "Chat.textfieldPrompt" = "Type a message"; - +// MARK: Attachments +"Attachment.Prompt.main" = "Select attachment"; +"Attachment.Tab.media" = "Media"; +"Attachment.Tab.files" = "Files"; +"Attachment.Tab.location" = "Location"; +"Attachment.Tab.contacts" = "Contacts"; +"Attachment.Send.media" = "Send media"; +"Attachment.Send.location" = "Send location"; +"Attachment.Send.contact" = "Send contact"; +"Attachment.Downloading.retry" = "Retry"; @@ -64,13 +73,3 @@ -// MARK: Attachments -//"Attachment.Prompt.main" = "Select attachment"; -//"Attachment.Tab.media" = "Media"; -//"Attachment.Tab.files" = "Files"; -//"Attachment.Tab.location" = "Location"; -//"Attachment.Tab.contacts" = "Contacts"; -//"Attachment.Send.media" = "Send media"; -//"Attachment.Send.location" = "Send location"; -//"Attachment.Send.contact" = "Send contact"; -//"Attachment.Downloading.retry" = "Retry"; diff --git a/ConversationsClassic/View/Main/Conversation/Attachments/AttachmentPickerScreen.swift b/ConversationsClassic/View/Main/Conversation/Attachments/AttachmentPickerScreen.swift new file mode 100644 index 0000000..4d6ca1a --- /dev/null +++ b/ConversationsClassic/View/Main/Conversation/Attachments/AttachmentPickerScreen.swift @@ -0,0 +1,134 @@ +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: + Color.blue + // SharingFilesPickerView() + + case .location: + Color.green + // SharingLocationPickerView() + + case .contacts: + Color.yellow + // SharingContactsPickerView() + } + + // Tab bar + AttachmentTabBar(selectedTab: $selectedTab) + } + } + } +} + +struct AttachmentTabBar: View { + @Binding var selectedTab: AttachmentTab + + var body: some View { + VStack(spacing: 0) { + Rectangle() + .frame(maxWidth: .infinity) + .frame(height: 0.2) + .foregroundColor(.Material.Shape.separator) + HStack(spacing: 0) { + AttachmentTabBarButton(tab: .media, selected: $selectedTab) + AttachmentTabBarButton(tab: .files, selected: $selectedTab) + AttachmentTabBarButton(tab: .location, selected: $selectedTab) + AttachmentTabBarButton(tab: .contacts, selected: $selectedTab) + } + .background(Color.Material.Background.dark) + } + .frame(height: 50) + } +} + +private struct AttachmentTabBarButton: View { + let tab: AttachmentTab + @Binding var selected: AttachmentTab + + var body: some View { + ZStack { + VStack(spacing: 2) { + buttonImg + .foregroundColor(selected == tab ? .Material.Elements.active : .Material.Elements.inactive) + .font(.system(size: 24, weight: .light)) + .symbolRenderingMode(.hierarchical) + Text(buttonTitle) + .font(.sub1) + .foregroundColor(selected == tab ? .Material.Text.main : .Material.Elements.inactive) + } + Rectangle() + .foregroundColor(.white.opacity(0.01)) + .onTapGesture { + selected = tab + } + } + } + + var buttonImg: Image { + switch tab { + case .media: + return Image(systemName: "photo.on.rectangle.angled") + + case .files: + return Image(systemName: "doc.on.doc") + + case .location: + return Image(systemName: "location.circle") + + case .contacts: + return Image(systemName: "person.crop.circle") + } + } + + var buttonTitle: String { + switch tab { + case .media: + return L10n.Attachment.Tab.media + + case .files: + return L10n.Attachment.Tab.files + + case .location: + return L10n.Attachment.Tab.location + + case .contacts: + return L10n.Attachment.Tab.contacts + } + } +} diff --git a/ConversationsClassic/View/Main/Conversation/Attachments/MediaPicker/MediaPickerView.swift b/ConversationsClassic/View/Main/Conversation/Attachments/MediaPicker/MediaPickerView.swift new file mode 100644 index 0000000..b265b1d --- /dev/null +++ b/ConversationsClassic/View/Main/Conversation/Attachments/MediaPicker/MediaPickerView.swift @@ -0,0 +1,289 @@ +import AVFoundation +import MobileCoreServices +import Photos +import SwiftUI + +struct MediaPickerView: View { + // @State private var showCameraPicker = false + // @State private var cameraReady = false + + // @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 + // if store.state.sharingState.isCameraAccessGranted { + // if cameraReady { + // 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 { + // showCameraPicker = true + // } + // } else { + // ProgressView() + // .frame(maxWidth: .infinity) + // .frame(height: 100) + // .onAppear { + // DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { + // cameraReady = true + // } + // } + // } + // } 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) + // } + // } + // } + + // For gallery + // if store.state.sharingState.isGalleryAccessGranted { + // ForEach(store.state.sharingState.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) + // } + // } + // } + } + } + // .fullScreenCover(isPresented: $showCameraPicker) { + // CameraPicker(sourceType: .camera) { data, type in + // store.dispatch(.sharingAction(.cameraCaptured(media: data, type: type))) + // showCameraPicker = false + // store.dispatch(.sharingAction(.showSharing(false))) + // } + // .edgesIgnoringSafeArea(.all) + // } + + // 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 { + // store.dispatch(.sharingAction(.shareMedia(ids: selectedItems))) + // store.dispatch(.sharingAction(.showSharing(false))) + } + } + // .onAppear { + // store.dispatch(.sharingAction(.checkCameraAccess)) + // store.dispatch(.sharingAction(.checkGalleryAccess)) + // } + // .onChange(of: store.state.sharingState.isGalleryAccessGranted) { granted in + // if granted { + // store.dispatch(.fileAction(.fetchItemsFromGallery)) + // } + // } + } +} + +private struct GridViewItem: View { + // let item: SharingGalleryItem + @Binding var selected: [String] + @State var isSelected = false + + var body: some View { + Text("Test") + // if let data = item.thumbnail { + // ZStack { + // Image(uiImage: UIImage(data: data) ?? UIImage()) + // .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 { + // isSelected.toggle() + // if isSelected { + // selected.append(item.id) + // } else { + // selected.removeAll { $0 == item.id } + // } + // } + // } else { + // ZStack { + // Rectangle() + // .fill(Color.Material.Background.light) + // .overlay { + // ProgressView() + // .foregroundColor(.Material.Elements.active) + // } + // .frame(width: Const.galleryGridSize, height: Const.galleryGridSize) + // } + // } + } +} + +// 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 +// +// captureSession.startRunning() +// +// return view +// } +// +// func updateUIView(_ uiView: CameraUIView, context _: Context) { +// uiView.previewLayer?.frame = uiView.bounds +// } +// } +// +// struct CameraPicker: UIViewControllerRepresentable { +// var sourceType: UIImagePickerController.SourceType +// var completionHandler: (Data, SharingCameraMediaType) -> Void +// +// func makeUIViewController(context: Context) -> UIImagePickerController { +// let picker = UIImagePickerController() +// picker.sourceType = sourceType +// picker.delegate = context.coordinator +// picker.mediaTypes = [UTType.movie.identifier, UTType.image.identifier] +// picker.videoQuality = .typeHigh +// picker.videoMaximumDuration = Const.videoDurationLimit +// picker.view.backgroundColor = .clear +// return picker +// } +// +// func updateUIViewController(_: UIImagePickerController, context _: Context) {} +// +// func makeCoordinator() -> Coordinator { +// Coordinator(self) +// } +// +// class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { +// let parent: CameraPicker +// +// init(_ parent: CameraPicker) { +// self.parent = parent +// } +// +// func imagePickerController(_: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { +// // swiftlint:disable:next force_cast +// let mediaType = info[.mediaType] as! String +// +// if mediaType == UTType.image.identifier { +// if let image = info[.originalImage] as? UIImage { +// let data = image.jpegData(compressionQuality: 1.0) ?? Data() +// parent.completionHandler(data, .photo) +// } +// } else if mediaType == UTType.movie.identifier { +// if let url = info[.mediaURL] as? URL { +// let data = try? Data(contentsOf: url) +// parent.completionHandler(data ?? Data(), .video) +// } +// } +// } +// } +// } diff --git a/ConversationsClassic/View/Main/Conversation/ConversationTextInput.swift b/ConversationsClassic/View/Main/Conversation/ConversationTextInput.swift index 4a477fb..14c4280 100644 --- a/ConversationsClassic/View/Main/Conversation/ConversationTextInput.swift +++ b/ConversationsClassic/View/Main/Conversation/ConversationTextInput.swift @@ -2,6 +2,7 @@ import SwiftUI import UIKit struct ConversationTextInput: View { + @Environment(\.router) var router @EnvironmentObject var conversation: ConversationStore @State private var messageStr = "" @@ -49,7 +50,9 @@ struct ConversationTextInput: View { .foregroundColor(.Material.Elements.active) .padding(.leading, 8) .tappablePadding(.symmetric(8)) { - // store.dispatch(.sharingAction(.showSharing(true))) + router.showScreen(.fullScreenCover) { _ in + AttachmentPickerScreen() + } } TextField("", text: $messageStr, prompt: Text(L10n.Chat.textfieldPrompt).foregroundColor(.Material.Shape.separator)) .font(.body1)