mirror of
https://gitlab.linphone.org/BC/public/linphone-iphone.git
synced 2026-01-17 11:08:06 +00:00
Add photo picker to conversation view
This commit is contained in:
parent
0682489645
commit
472bf46938
4 changed files with 342 additions and 42 deletions
|
|
@ -161,7 +161,7 @@ struct ChatBubbleView: View {
|
|||
func messageAttachments() -> some View {
|
||||
if message.attachments.count == 1 {
|
||||
if message.attachments.first!.type == .image || message.attachments.first!.type == .gif || message.attachments.first!.type == .video {
|
||||
let result = imageDimensions(url: message.attachments.first!.full.absoluteString)
|
||||
let result = imageDimensions(url: message.attachments.first!.thumbnail.absoluteString)
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(Color(.white))
|
||||
|
|
@ -186,31 +186,59 @@ struct ChatBubbleView: View {
|
|||
}
|
||||
|
||||
if message.attachments.first!.type == .image || message.attachments.first!.type == .video {
|
||||
AsyncImage(url: message.attachments.first!.full) { image in
|
||||
ZStack {
|
||||
image
|
||||
.resizable()
|
||||
.interpolation(.medium)
|
||||
.aspectRatio(contentMode: .fill)
|
||||
|
||||
if message.attachments.first!.type == .video {
|
||||
Image("play-fill")
|
||||
.renderingMode(.template)
|
||||
if #available(iOS 16.0, *) {
|
||||
AsyncImage(url: message.attachments.first!.thumbnail) { image in
|
||||
ZStack {
|
||||
image
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 40, height: 40, alignment: .leading)
|
||||
.interpolation(.medium)
|
||||
.aspectRatio(contentMode: .fill)
|
||||
|
||||
if message.attachments.first!.type == .video {
|
||||
Image("play-fill")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 40, height: 40, alignment: .leading)
|
||||
}
|
||||
}
|
||||
} placeholder: {
|
||||
ProgressView()
|
||||
}
|
||||
.layoutPriority(-1)
|
||||
} else {
|
||||
AsyncImage(url: message.attachments.first!.thumbnail) { image in
|
||||
ZStack {
|
||||
image
|
||||
.resizable()
|
||||
.interpolation(.medium)
|
||||
.aspectRatio(contentMode: .fill)
|
||||
|
||||
if message.attachments.first!.type == .video {
|
||||
Image("play-fill")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 40, height: 40, alignment: .leading)
|
||||
}
|
||||
}
|
||||
} placeholder: {
|
||||
ProgressView()
|
||||
}
|
||||
} placeholder: {
|
||||
ProgressView()
|
||||
}
|
||||
.id(UUID())
|
||||
.layoutPriority(-1)
|
||||
} else if message.attachments.first!.type == .gif {
|
||||
GifImageView(message.attachments.first!.full)
|
||||
.id(UUID())
|
||||
.layoutPriority(-1)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
}
|
||||
} else if message.attachments.first!.type == .gif {
|
||||
if #available(iOS 16.0, *) {
|
||||
GifImageView(message.attachments.first!.thumbnail)
|
||||
.layoutPriority(-1)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
} else {
|
||||
GifImageView(message.attachments.first!.thumbnail)
|
||||
.id(UUID())
|
||||
.layoutPriority(-1)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
}
|
||||
}
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
|
|
@ -227,29 +255,51 @@ struct ChatBubbleView: View {
|
|||
.fill(Color(.white))
|
||||
.frame(width: 120, height: 120)
|
||||
|
||||
AsyncImage(url: attachment.full) { image in
|
||||
ZStack {
|
||||
image
|
||||
.resizable()
|
||||
.interpolation(.medium)
|
||||
.aspectRatio(contentMode: .fill)
|
||||
|
||||
if attachment.type == .video {
|
||||
Image("play-fill")
|
||||
.renderingMode(.template)
|
||||
if #available(iOS 16.0, *) {
|
||||
AsyncImage(url: attachment.thumbnail) { image in
|
||||
ZStack {
|
||||
image
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 40, height: 40, alignment: .leading)
|
||||
.interpolation(.medium)
|
||||
.aspectRatio(contentMode: .fill)
|
||||
|
||||
if attachment.type == .video {
|
||||
Image("play-fill")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 40, height: 40, alignment: .leading)
|
||||
}
|
||||
}
|
||||
} placeholder: {
|
||||
ProgressView()
|
||||
}
|
||||
} placeholder: {
|
||||
ProgressView()
|
||||
.layoutPriority(-1)
|
||||
} else {
|
||||
AsyncImage(url: attachment.thumbnail) { image in
|
||||
ZStack {
|
||||
image
|
||||
.resizable()
|
||||
.interpolation(.medium)
|
||||
.aspectRatio(contentMode: .fill)
|
||||
|
||||
if attachment.type == .video {
|
||||
Image("play-fill")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 40, height: 40, alignment: .leading)
|
||||
}
|
||||
}
|
||||
} placeholder: {
|
||||
ProgressView()
|
||||
}
|
||||
.id(UUID())
|
||||
.layoutPriority(-1)
|
||||
}
|
||||
.id(UUID())
|
||||
.layoutPriority(-1)
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.clipped()
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
.frame(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,13 @@ struct ConversationFragment: View {
|
|||
|
||||
@State private var displayFloatingButton = false
|
||||
|
||||
@State private var isShowPhotoLibrary = false
|
||||
@State private var isShowCamera = false
|
||||
|
||||
@State private var mediasToSend: [Attachment] = []
|
||||
@State private var mediasIsLoading = false
|
||||
@State private var maxMediaCount = 12
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
GeometryReader { geometry in
|
||||
|
|
@ -313,6 +320,93 @@ struct ConversationFragment: View {
|
|||
}
|
||||
}
|
||||
|
||||
if !mediasToSend.isEmpty || mediasIsLoading {
|
||||
ZStack(alignment: .top) {
|
||||
HStack {
|
||||
if mediasIsLoading {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
ProgressView()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(height: 120)
|
||||
}
|
||||
|
||||
LazyVGrid(columns: [
|
||||
GridItem(.adaptive(minimum: 100), spacing: 1)
|
||||
], spacing: 3) {
|
||||
ForEach(mediasToSend, id: \.id) { attachment in
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(Color(.white))
|
||||
.frame(width: 100, height: 100)
|
||||
|
||||
AsyncImage(url: attachment.thumbnail) { image in
|
||||
ZStack {
|
||||
image
|
||||
.resizable()
|
||||
.interpolation(.medium)
|
||||
.aspectRatio(contentMode: .fill)
|
||||
|
||||
if attachment.type == .video {
|
||||
Image("play-fill")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 40, height: 40, alignment: .leading)
|
||||
}
|
||||
}
|
||||
} placeholder: {
|
||||
ProgressView()
|
||||
}
|
||||
.layoutPriority(-1)
|
||||
.onTapGesture {
|
||||
if mediasToSend.count == 1 {
|
||||
withAnimation {
|
||||
mediasToSend = []
|
||||
}
|
||||
} else {
|
||||
guard let index = self.mediasToSend.firstIndex(of: attachment) else { return }
|
||||
self.mediasToSend.remove(at: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
.frame(
|
||||
width: geometry.size.width > 0 && CGFloat(102 * mediasToSend.count) > geometry.size.width - 20
|
||||
? 102 * floor(CGFloat(geometry.size.width - 20) / 102)
|
||||
: CGFloat(102 * mediasToSend.count)
|
||||
)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.all, mediasToSend.isEmpty ? 0 : 10)
|
||||
.background(Color.gray100)
|
||||
|
||||
if !mediasIsLoading {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
mediasToSend = []
|
||||
}
|
||||
}, label: {
|
||||
Image("x")
|
||||
.resizable()
|
||||
.frame(width: 30, height: 30, alignment: .leading)
|
||||
.padding(.all, 10)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
.transition(.move(edge: .bottom))
|
||||
}
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Button {
|
||||
} label: {
|
||||
|
|
@ -327,26 +421,31 @@ struct ConversationFragment: View {
|
|||
.padding(.horizontal, isMessageTextFocused ? 0 : 2)
|
||||
|
||||
Button {
|
||||
self.isShowPhotoLibrary = true
|
||||
self.mediasIsLoading = true
|
||||
} label: {
|
||||
Image("paperclip")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.foregroundStyle(maxMediaCount <= mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500)
|
||||
.frame(width: isMessageTextFocused ? 0 : 28, height: isMessageTextFocused ? 0 : 28, alignment: .leading)
|
||||
.padding(.all, isMessageTextFocused ? 0 : 6)
|
||||
.padding(.top, 4)
|
||||
.disabled(maxMediaCount <= mediasToSend.count || mediasIsLoading)
|
||||
}
|
||||
.padding(.horizontal, isMessageTextFocused ? 0 : 2)
|
||||
|
||||
Button {
|
||||
self.isShowCamera = true
|
||||
} label: {
|
||||
Image("camera")
|
||||
.renderingMode(.template)
|
||||
.resizable()
|
||||
.foregroundStyle(Color.grayMain2c500)
|
||||
.foregroundStyle(maxMediaCount <= mediasToSend.count || mediasIsLoading ? Color.grayMain2c300 : Color.grayMain2c500)
|
||||
.frame(width: isMessageTextFocused ? 0 : 28, height: isMessageTextFocused ? 0 : 28, alignment: .leading)
|
||||
.padding(.all, isMessageTextFocused ? 0 : 6)
|
||||
.padding(.top, 4)
|
||||
.disabled(maxMediaCount <= mediasToSend.count || mediasIsLoading)
|
||||
}
|
||||
.padding(.horizontal, isMessageTextFocused ? 0 : 2)
|
||||
|
||||
|
|
@ -437,6 +536,28 @@ struct ConversationFragment: View {
|
|||
.onDisappear {
|
||||
conversationViewModel.removeConversationDelegate()
|
||||
}
|
||||
.sheet(isPresented: $isShowPhotoLibrary) {
|
||||
PhotoPicker(filter: nil, limit: maxMediaCount - mediasToSend.count) { results in
|
||||
PhotoPicker.convertToAttachmentArray(fromResults: results) { mediasOrNil, errorOrNil in
|
||||
if let error = errorOrNil {
|
||||
print(error)
|
||||
}
|
||||
|
||||
if let medias = mediasOrNil {
|
||||
mediasToSend.append(contentsOf: medias)
|
||||
}
|
||||
|
||||
self.mediasIsLoading = false
|
||||
}
|
||||
}
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
/*
|
||||
.fullScreenCover(isPresented: $isShowCamera) {
|
||||
ImagePicker(selectedImage: self.$image, sourceType: .camera)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
|
|
@ -456,6 +577,48 @@ extension UIApplication {
|
|||
}
|
||||
}
|
||||
|
||||
struct ImagePicker: UIViewControllerRepresentable {
|
||||
@Binding var selectedImage: UIImage
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
|
||||
var sourceType: UIImagePickerController.SourceType = .photoLibrary
|
||||
|
||||
final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
||||
|
||||
var parent: ImagePicker
|
||||
|
||||
init(_ parent: ImagePicker) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
||||
|
||||
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
|
||||
parent.selectedImage = image
|
||||
}
|
||||
|
||||
parent.presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
|
||||
let imagePicker = UIImagePickerController()
|
||||
imagePicker.allowsEditing = false
|
||||
imagePicker.sourceType = sourceType
|
||||
imagePicker.delegate = context.coordinator
|
||||
|
||||
return imagePicker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
|
||||
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#Preview {
|
||||
ConversationFragment(conversationViewModel: ConversationViewModel(), conversationsListViewModel: ConversationsListViewModel(), sections: [MessagesSection], ids: [""])
|
||||
|
|
|
|||
|
|
@ -32,4 +32,6 @@ extension View {
|
|||
self
|
||||
}
|
||||
}
|
||||
|
||||
func apply<V: View>(@ViewBuilder _ block: (Self) -> V) -> V { block(self) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,14 +23,16 @@ import PhotosUI
|
|||
struct PhotoPicker: UIViewControllerRepresentable {
|
||||
typealias UIViewControllerType = PHPickerViewController
|
||||
|
||||
let filter: PHPickerFilter
|
||||
let filter: PHPickerFilter?
|
||||
var limit: Int = 0
|
||||
let onComplete: ([PHPickerResult]) -> Void
|
||||
|
||||
func makeUIViewController(context: Context) -> PHPickerViewController {
|
||||
|
||||
var configuration = PHPickerConfiguration()
|
||||
configuration.filter = filter
|
||||
if filter != nil {
|
||||
configuration.filter = filter
|
||||
}
|
||||
configuration.selectionLimit = limit
|
||||
|
||||
let controller = PHPickerViewController(configuration: configuration)
|
||||
|
|
@ -63,6 +65,89 @@ struct PhotoPicker: UIViewControllerRepresentable {
|
|||
}
|
||||
}
|
||||
|
||||
static func convertToAttachmentArray(fromResults results: [PHPickerResult], onComplete: @escaping ([Attachment]?, Error?) -> Void) {
|
||||
var medias = [Attachment]()
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
for result in results {
|
||||
dispatchGroup.enter()
|
||||
let itemProvider = result.itemProvider
|
||||
if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
||||
itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { urlFile, error in
|
||||
if urlFile != nil {
|
||||
do {
|
||||
let dataResult = try Data(contentsOf: urlFile!)
|
||||
let urlImage = self.saveMedia(name: urlFile!.lastPathComponent, data: dataResult, type: .image)
|
||||
if urlImage != nil {
|
||||
let attachment = Attachment(id: UUID().uuidString, url: urlImage!, type: .image)
|
||||
medias.append(attachment)
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
} else if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
|
||||
itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { urlFile, error in
|
||||
if urlFile != nil {
|
||||
do {
|
||||
let dataResult = try Data(contentsOf: urlFile!)
|
||||
let urlImage = self.saveMedia(name: urlFile!.lastPathComponent, data: dataResult, type: .video)
|
||||
let urlThumbnail = getURLThumbnail(name: urlFile!.lastPathComponent)
|
||||
|
||||
if urlImage != nil {
|
||||
let attachment = Attachment(id: UUID().uuidString, thumbnail: urlThumbnail, full: urlImage!, type: .video)
|
||||
medias.append(attachment)
|
||||
}
|
||||
} catch {
|
||||
|
||||
}
|
||||
}
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
onComplete(medias, nil)
|
||||
}
|
||||
}
|
||||
|
||||
static func saveMedia(name: String, data: Data, type: AttachmentType) -> URL? {
|
||||
do {
|
||||
let path = FileManager.default.temporaryDirectory.appendingPathComponent((name.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? ""))
|
||||
|
||||
let decodedData: () = try data.write(to: path)
|
||||
|
||||
if type == .video {
|
||||
let asset = AVURLAsset(url: path, options: nil)
|
||||
let imgGenerator = AVAssetImageGenerator(asset: asset)
|
||||
imgGenerator.appliesPreferredTrackTransform = true
|
||||
let cgImage = try imgGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil)
|
||||
let thumbnail = UIImage(cgImage: cgImage)
|
||||
|
||||
guard let data = thumbnail.jpegData(compressionQuality: 1) ?? thumbnail.pngData() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let urlName = FileManager.default.temporaryDirectory.appendingPathComponent("preview_" + (name.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "") + ".png")
|
||||
|
||||
let decodedData: () = try data.write(to: urlName)
|
||||
}
|
||||
|
||||
return path
|
||||
} catch let error {
|
||||
print("*** Error generating thumbnail: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
static func getURLThumbnail(name: String) -> URL {
|
||||
return FileManager.default.temporaryDirectory.appendingPathComponent("preview_" + (name.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? "") + ".png")
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue