Add support for GIPHY + rework loading of the media in the editor

This commit is contained in:
Thomas Ricouard 2023-12-17 10:27:01 +01:00
parent 4985e69200
commit 1fa54afc3a
7 changed files with 174 additions and 54 deletions

View file

@ -88,9 +88,10 @@
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = 9FBFE64D292A72BD00C250E9 /* Network */; };
9FD34823293D06E800DB0EE9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9FD34822293D06E800DB0EE9 /* Assets.xcassets */; };
9FD542E72962D2FF0045321A /* Lists in Frameworks */ = {isa = PBXBuildFile; productRef = 9FD542E62962D2FF0045321A /* Lists */; };
9FE0346C2ADD5C2100529EA8 /* MediaUI in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE0346B2ADD5C2100529EA8 /* MediaUI */; };
9FE151A6293C90F900E9683D /* IconSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FE151A5293C90F900E9683D /* IconSelectorView.swift */; };
9FE3DB57296FEFCA00628CB0 /* AppAccount in Frameworks */ = {isa = PBXBuildFile; productRef = 9FE3DB56296FEFCA00628CB0 /* AppAccount */; };
9FF614472B2EDCE500F7B0E6 /* GiphyUISDK in Frameworks */ = {isa = PBXBuildFile; productRef = 9FF614462B2EDCE500F7B0E6 /* GiphyUISDK */; };
9FF614492B2EDCEC00F7B0E6 /* GiphyUISDK in Frameworks */ = {isa = PBXBuildFile; productRef = 9FF614482B2EDCEC00F7B0E6 /* GiphyUISDK */; };
9FFF677C299B7B2C00FE700A /* Notifications in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677B299B7B2C00FE700A /* Notifications */; };
9FFF677E299B7D2800FE700A /* Status in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677D299B7D2800FE700A /* Status */; };
9FFF6780299B7D2B00FE700A /* DesignSystem in Frameworks */ = {isa = PBXBuildFile; productRef = 9FFF677F299B7D2B00FE700A /* DesignSystem */; };
@ -228,7 +229,6 @@
9FCBB3D429859615009B77EE /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/InfoPlist.strings; sourceTree = "<group>"; };
9FD34822293D06E800DB0EE9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
9FD542E52962D2CE0045321A /* Lists */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Lists; path = Packages/Lists; sourceTree = "<group>"; };
9FE034692ADD597100529EA8 /* MediaUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MediaUI; path = Packages/MediaUI; sourceTree = "<group>"; };
9FE0346A2ADD59AC00529EA8 /* MediaUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MediaUI; path = Packages/MediaUI; sourceTree = "<group>"; };
9FE151A5293C90F900E9683D /* IconSelectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconSelectorView.swift; sourceTree = "<group>"; };
9FE3DB55296FEF5800628CB0 /* AppAccount */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = AppAccount; path = Packages/AppAccount; sourceTree = "<group>"; };
@ -275,6 +275,7 @@
9FAD85A2297456A400496AB1 /* Env in Frameworks */,
9FAD85A0297456A100496AB1 /* Models in Frameworks */,
9FAD85A4297456A800496AB1 /* DesignSystem in Frameworks */,
9FF614492B2EDCEC00F7B0E6 /* GiphyUISDK in Frameworks */,
9FAD859E2974569B00496AB1 /* Account in Frameworks */,
9FAD859C2974422700496AB1 /* AppAccount in Frameworks */,
9FAD859A297440CB00496AB1 /* KeychainSwift in Frameworks */,
@ -296,13 +297,13 @@
9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */,
9F7335EA2966B3F800AFF0BA /* Conversations in Frameworks */,
9FE3DB57296FEFCA00628CB0 /* AppAccount in Frameworks */,
9FE0346C2ADD5C2100529EA8 /* MediaUI in Frameworks */,
9F398AA92935FFDB00A889F2 /* Account in Frameworks */,
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */,
9FD542E72962D2FF0045321A /* Lists in Frameworks */,
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */,
9F5E581929545BE700A53960 /* Env in Frameworks */,
9F2A540A29699705009B2D7C /* ReceiptParser in Frameworks */,
9FF614472B2EDCE500F7B0E6 /* GiphyUISDK in Frameworks */,
9F35DB44294F9A7D00B3281A /* Status in Frameworks */,
DA0B24FB2A6876D50045BDD7 /* SFSafeSymbols in Frameworks */,
9F295540292B6C3400E0E81B /* Timeline in Frameworks */,
@ -493,7 +494,6 @@
9FBFE64C292A72BD00C250E9 /* Frameworks */ = {
isa = PBXGroup;
children = (
9FE034692ADD597100529EA8 /* MediaUI */,
9F2A540D2969A0B0009B2D7C /* StoreKit.framework */,
9F2A5404296995FB009B2D7C /* QuickLookUI.framework */,
9F7335EE29674F7100AFF0BA /* QuickLook.framework */,
@ -597,6 +597,7 @@
9FAD85A1297456A400496AB1 /* Env */,
9FAD85A3297456A800496AB1 /* DesignSystem */,
065FA209298675BA00012EA0 /* LRUCache */,
9FF614482B2EDCEC00F7B0E6 /* GiphyUISDK */,
);
productName = IceCubesShareExtension;
productReference = 9FAD858829743F7400496AB1 /* IceCubesShareExtension.appex */;
@ -636,7 +637,7 @@
9FE3DB56296FEFCA00628CB0 /* AppAccount */,
065FA1FD29866CD600012EA0 /* LRUCache */,
DA0B24FA2A6876D50045BDD7 /* SFSafeSymbols */,
9FE0346B2ADD5C2100529EA8 /* MediaUI */,
9FF614462B2EDCE500F7B0E6 /* GiphyUISDK */,
);
productName = IceCubesApp;
productReference = 9FBFE639292A715500C250E9 /* Ice Cubes.app */;
@ -719,6 +720,7 @@
9F2A540829699705009B2D7C /* XCRemoteSwiftPackageReference "purchases-ios" */,
065FA1FC29866CD600012EA0 /* XCRemoteSwiftPackageReference "LRUCache" */,
DA0B24F92A6876D40045BDD7 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
9FF614452B2EDCE500F7B0E6 /* XCRemoteSwiftPackageReference "giphy-ios-sdk" */,
);
productRefGroup = 9FBFE63A292A715500C250E9 /* Products */;
projectDirPath = "";
@ -1410,6 +1412,14 @@
kind = branch;
};
};
9FF614452B2EDCE500F7B0E6 /* XCRemoteSwiftPackageReference "giphy-ios-sdk" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Giphy/giphy-ios-sdk";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.2.7;
};
};
DA0B24F92A6876D40045BDD7 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols";
@ -1532,14 +1542,20 @@
isa = XCSwiftPackageProductDependency;
productName = Lists;
};
9FE0346B2ADD5C2100529EA8 /* MediaUI */ = {
isa = XCSwiftPackageProductDependency;
productName = MediaUI;
};
9FE3DB56296FEFCA00628CB0 /* AppAccount */ = {
isa = XCSwiftPackageProductDependency;
productName = AppAccount;
};
9FF614462B2EDCE500F7B0E6 /* GiphyUISDK */ = {
isa = XCSwiftPackageProductDependency;
package = 9FF614452B2EDCE500F7B0E6 /* XCRemoteSwiftPackageReference "giphy-ios-sdk" */;
productName = GiphyUISDK;
};
9FF614482B2EDCEC00F7B0E6 /* GiphyUISDK */ = {
isa = XCSwiftPackageProductDependency;
package = 9FF614452B2EDCE500F7B0E6 /* XCRemoteSwiftPackageReference "giphy-ios-sdk" */;
productName = GiphyUISDK;
};
9FFF677B299B7B2C00FE700A /* Notifications */ = {
isa = XCSwiftPackageProductDependency;
productName = Notifications;

View file

@ -18,6 +18,15 @@
"version" : "3.2.0"
}
},
{
"identity" : "giphy-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/Giphy/giphy-ios-sdk",
"state" : {
"revision" : "95c32b862185e76107841b49bfff8ff7230f3b68",
"version" : "2.2.7"
}
},
{
"identity" : "keychain-swift",
"kind" : "remoteSourceControl",
@ -27,6 +36,15 @@
"revision" : "f38cb0ada97847ac5068b915b8d2793b35435668"
}
},
{
"identity" : "libwebp-xcode",
"kind" : "remoteSourceControl",
"location" : "https://github.com/SDWebImage/libwebp-Xcode",
"state" : {
"revision" : "b2b1d20a90b14d11f6ef4241da6b81c1d3f171e4",
"version" : "1.3.2"
}
},
{
"identity" : "lrucache",
"kind" : "remoteSourceControl",

View file

@ -2,7 +2,7 @@ import Foundation
public enum Media: Endpoint {
case medias
case media(id: String, json: MediaDescriptionData)
case media(id: String, json: MediaDescriptionData?)
public func path() -> String {
switch self {
@ -20,9 +20,12 @@ public enum Media: Endpoint {
public var jsonValue: Encodable? {
switch self {
case let .media(_, json):
json
if let json {
return json
}
return nil
default:
nil
return nil
}
}
}

View file

@ -0,0 +1,51 @@
import SwiftUI
import GiphyUISDK
import UIKit
import DesignSystem
struct GifPickerView: UIViewControllerRepresentable {
@Environment(Theme.self) private var theme
var completion: ((String) -> Void)
var onShouldDismissGifPicker: () -> Void
func makeUIViewController(context: Context) -> GiphyViewController {
Giphy.configure(apiKey: "MIylJkNX57vcUNZxmSODKU9dQKBgXCkV")
let controller = GiphyViewController()
controller.swiftUIEnabled = true
controller.mediaTypeConfig = [.gifs, .stickers, .recents]
controller.delegate = context.coordinator
controller.navigationController?.isNavigationBarHidden = true
controller.navigationController?.setNavigationBarHidden(true, animated: false)
GiphyViewController.trayHeightMultiplier = 1.0
controller.theme = GPHTheme(type: theme.selectedScheme == .dark ? .darkBlur : .lightBlur)
return controller
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
func makeCoordinator() -> Coordinator {
GifPickerView.Coordinator(parent: self)
}
class Coordinator: NSObject, GiphyDelegate {
var parent: GifPickerView
init(parent: GifPickerView) {
self.parent = parent
}
func didDismiss(controller: GiphyViewController?) {
self.parent.onShouldDismissGifPicker()
}
func didSelectMedia(giphyViewController: GiphyViewController, media: GPHMedia) {
let url = media.url(rendition: .fixedWidth, fileType: .gif)
parent.completion(url ?? "")
}
}
}

View file

@ -4,6 +4,7 @@ import Models
import NukeUI
import PhotosUI
import SwiftUI
import GiphyUISDK
@MainActor
struct StatusEditorAccessoryView: View {
@ -24,6 +25,7 @@ struct StatusEditorAccessoryView: View {
@State private var isPhotosPickerPresented: Bool = false
@State private var isFileImporterPresented: Bool = false
@State private var isCameraPickerPresented: Bool = false
@State private var isGIFPickerPresented: Bool = false
var body: some View {
@Bindable var viewModel = focusedSEVM
@ -51,6 +53,12 @@ struct StatusEditorAccessoryView: View {
} label: {
Label("status.editor.browse-file", systemImage: "folder")
}
Button {
isGIFPickerPresented = true
} label: {
Label("GIPHY", systemImage: "party.popper")
}
} label: {
if viewModel.isMediasLoading {
ProgressView()
@ -82,6 +90,18 @@ struct StatusEditorAccessoryView: View {
}))
.background(.black)
})
.sheet(isPresented: $isGIFPickerPresented, content: {
GifPickerView { url in
GPHCache.shared.downloadAssetData(url) { data, error in
guard let data else { return }
viewModel.processGIFData(data: data)
}
isGIFPickerPresented = false
} onShouldDismissGifPicker: {
isGIFPickerPresented = false
}
.presentationDetents([.medium, .large])
})
.accessibilityLabel("accessibility.editor.button.attach-photo")
.disabled(viewModel.showPoll)

View file

@ -4,6 +4,7 @@ import Env
import Models
import NukeUI
import SwiftUI
import MediaUI
@MainActor
struct StatusEditorMediaView: View {
@ -93,13 +94,13 @@ struct StatusEditorMediaView: View {
RoundedRectangle(cornerRadius: 8).fill(.clear)
.overlay {
if let attachement = container.mediaAttachment {
makeLazyImage(mediaAttachement: attachement)
makeRemoteMediaView(mediaAttachement: attachement)
} else if container.image != nil {
makeLocalImage(container: container)
} else if container.movieTransferable != nil || container.gifTransferable != nil {
makeVideoAttachement(container: container)
makeLocalImageView(container: container)
} else if let error = container.error as? ServerError {
makeErrorView(error: error)
} else {
placeholderView
}
}
}
@ -116,17 +117,7 @@ struct StatusEditorMediaView: View {
.matchedGeometryEffect(id: index, in: mediaSpace, anchor: .leading)
}
private func makeVideoAttachement(container: StatusEditorMediaContainer) -> some View {
ZStack(alignment: .center) {
placeholderView
if container.mediaAttachment == nil {
ProgressView()
}
}
.cornerRadius(8)
}
private func makeLocalImage(container: StatusEditorMediaContainer) -> some View {
private func makeLocalImageView(container: StatusEditorMediaContainer) -> some View {
ZStack(alignment: .center) {
Image(uiImage: container.image!)
.resizable()
@ -141,30 +132,29 @@ struct StatusEditorMediaView: View {
}
}
private func makeLazyImage(mediaAttachement: MediaAttachment) -> some View {
private func makeRemoteMediaView(mediaAttachement: MediaAttachment) -> some View {
ZStack(alignment: .center) {
if let url = mediaAttachement.url ?? mediaAttachement.previewUrl {
LazyImage(url: url) { state in
if let image = state.image {
image
.resizable()
.scaledToFill()
} else {
placeholderView
switch mediaAttachement.supportedType {
case .gifv, .video, .audio:
if let url = mediaAttachement.url {
MediaUIAttachmentVideoView(viewModel: .init(url: url, forceAutoPlay: true))
} else {
placeholderView
}
case .image:
if let url = mediaAttachement.url ?? mediaAttachement.previewUrl {
LazyImage(url: url) { state in
if let image = state.image {
image
.resizable()
.scaledToFill()
} else {
placeholderView
}
}
}
} else {
placeholderView
}
if mediaAttachement.url == nil {
ProgressView()
}
if mediaAttachement.url != nil,
mediaAttachement.supportedType == .video || mediaAttachement.supportedType == .gifv
{
Image(systemName: "play.fill")
.font(.headline)
.tint(.white)
case .none:
EmptyView()
}
}
.cornerRadius(8)
@ -242,11 +232,18 @@ struct StatusEditorMediaView: View {
}
return false
})
viewModel.mediaContainers.removeAll {
$0.id == container.id
}
}
private var placeholderView: some View {
Rectangle()
.foregroundColor(theme.secondaryBackgroundColor)
.accessibilityHidden(true)
ZStack(alignment: .center) {
Rectangle()
.foregroundColor(theme.secondaryBackgroundColor)
.accessibilityHidden(true)
ProgressView()
}
.cornerRadius(8)
}
}

View file

@ -109,7 +109,7 @@ import SwiftUI
var isMediasLoading: Bool = false
private(set) var mediaContainers: [StatusEditorMediaContainer] = []
var mediaContainers: [StatusEditorMediaContainer] = []
var replyToStatus: Status?
var embeddedStatus: Status?
@ -384,6 +384,19 @@ import SwiftUI
.compactMap { NSItemProvider(contentsOf: $0) }
processItemsProvider(items: items)
}
func processGIFData(data: Data) {
isMediasLoading = true
let url = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).gif")
try? data.write(to: url)
let container = StatusEditorMediaContainer(id: UUID().uuidString,
image: nil,
movieTransferable: nil,
gifTransferable: .init(url: url),
mediaAttachment: nil,
error: nil)
prepareToPost(for: container)
}
func processCameraPhoto(image: UIImage) {
let container = StatusEditorMediaContainer(
@ -725,7 +738,7 @@ import SwiftUI
}
do {
let newAttachement: MediaAttachment = try await client.get(endpoint: Media.media(id: mediaAttachement.id,
json: .init(description: nil)))
json: nil))
if newAttachement.url != nil {
let oldContainer = mediaContainers[index]
mediaContainers[index] = StatusEditorMediaContainer(
@ -736,7 +749,9 @@ import SwiftUI
mediaAttachment: newAttachement,
error: nil)
}
} catch {}
} catch {
print(error.localizedDescription)
}
}
try? await Task.sleep(for: .seconds(5))
} while !Task.isCancelled