Skip to content

Commit

Permalink
Merge pull request #13949 from wordpress-mobile/feature/13803-tenor_i…
Browse files Browse the repository at this point in the history
…ntegration

Tenor integration
  • Loading branch information
ScoutHarris authored May 12, 2020
2 parents 28210f9 + d35ce72 commit bdf30ae
Show file tree
Hide file tree
Showing 46 changed files with 1,840 additions and 13 deletions.
17 changes: 16 additions & 1 deletion WordPress/Classes/Services/MediaCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,23 @@ class MediaCoordinator: NSObject {
}

private func trackUploadOf(_ media: Media, analyticsInfo: MediaAnalyticsInfo?) {
guard let info = analyticsInfo else {
return
}

guard let event = info.eventForMediaType(media.mediaType) else {
// Fall back to the WPShared event tracking
trackUploadViaWPSharedOf(media, analyticsInfo: analyticsInfo)
return
}

let properties = info.properties(for: media)
WPAnalytics.track(event, properties: properties, blog: media.blog)
}

private func trackUploadViaWPSharedOf(_ media: Media, analyticsInfo: MediaAnalyticsInfo?) {
guard let info = analyticsInfo,
let event = info.eventForMediaType(media.mediaType) else {
let event = info.wpsharedEventForMediaType(media.mediaType) else {
return
}

Expand Down
3 changes: 3 additions & 0 deletions WordPress/Classes/Services/MediaImportService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ open class MediaImportService: LocalCoreDataService {
case let stockPhotosMedia as StockPhotosMedia:
let exporter = MediaExternalExporter(externalAsset: stockPhotosMedia)
return exporter
case let tenorMedia as TenorMedia:
let exporter = MediaExternalExporter(externalAsset: tenorMedia)
return exporter
default:
return nil
}
Expand Down
42 changes: 39 additions & 3 deletions WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ import Foundation

// WPiOS-only events
@objc enum WPAnalyticsEvent: Int {
// Media Editor
case mediaEditorShown
case mediaEditorUsed
case editorCreatedPage
case createSheetShown
// Tenor
case tenorAccessed
case tenorSearched
case tenorUploaded
case mediaLibraryAddedPhotoViaTenor
case editorAddedPhotoViaTenor

// Settings and Prepublishing Nudges
case editorPostPublishTap
Expand All @@ -25,6 +32,7 @@ import Foundation
/// A String that represents the event
var value: String {
switch self {
// Media Editor
case .mediaEditorShown:
return "media_editor_shown"
case .mediaEditorUsed:
Expand All @@ -33,6 +41,18 @@ import Foundation
return "editor_page_created"
case .createSheetShown:
return "create_sheet_shown"
// Tenor
case .tenorAccessed:
return "tenor_accessed"
case .tenorSearched:
return "tenor_searched"
case .tenorUploaded:
return "tenor_uploaded"
case .mediaLibraryAddedPhotoViaTenor:
return "media_library_photo_added"
case .editorAddedPhotoViaTenor:
return "editor_photo_added"
// Editor
case .editorPostPublishTap:
return "editor_post_publish_tapped"
case .editorPostScheduled:
Expand Down Expand Up @@ -73,6 +93,10 @@ import Foundation
*/
var defaultProperties: [AnyHashable: Any]? {
switch self {
case .mediaLibraryAddedPhotoViaTenor:
return ["via": "tenor"]
case .editorAddedPhotoViaTenor:
return ["via": "tenor"]
default:
return nil
}
Expand All @@ -83,11 +107,11 @@ extension WPAnalytics {

/// Track a event
///
/// This will call each registered tracker and fire the given event
/// This will call each registered tracker and fire the given event.
/// - Parameter event: a `String` that represents the event name
///
/// - Note: If an event has its default properties, it will be passed through
static func track(_ event: WPAnalyticsEvent) {
WPAnalytics.trackString(event.value)
WPAnalytics.trackString(event.value, withProperties: event.defaultProperties ?? [:])
}

/// Track a event
Expand All @@ -103,6 +127,18 @@ extension WPAnalytics {
WPAnalytics.trackString(event.value, withProperties: mergedProperties)
}


/// This will call each registered tracker and fire the given event.
/// - Parameters:
/// - event: a `String` that represents the event name
/// - properties: a `Hash` that represents the properties
/// - blog: a `Blog` asssociated with the event
static func track(_ event: WPAnalyticsEvent, properties: [AnyHashable: Any], blog: Blog) {
var props = properties
props[WPAppAnalyticsKeyBlogID] = blog.dotComID
WPAnalytics.track(event, properties: props)
}

/// Track a event in Obj-C
///
/// This will call each registered tracker and fire the given event
Expand Down
29 changes: 27 additions & 2 deletions WordPress/Classes/Utility/Analytics/WPAppAnalytics+Media.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,15 @@ public struct MediaAnalyticsInfo {
self.selectionMethod = selectionMethod
}

func eventForMediaType(_ mediaType: MediaType) -> WPAnalyticsStat? {
func eventForMediaType(_ mediaType: MediaType) -> WPAnalyticsEvent? {
return origin.eventForMediaType(mediaType)
}

// Old tracking events via WPShared
func wpsharedEventForMediaType(_ mediaType: MediaType) -> WPAnalyticsStat? {
return origin.wpsharedEventForMediaType(mediaType)
}

var retryEvent: WPAnalyticsStat? {
switch origin {
case .mediaLibrary:
Expand Down Expand Up @@ -107,8 +112,26 @@ enum MediaUploadOrigin {
case mediaLibrary(MediaSource)
case editor(MediaSource)

func eventForMediaType(_ mediaType: MediaType) -> WPAnalyticsStat? {
// All new media tracking events will be added into WPAnalyticsEvent
func eventForMediaType(_ mediaType: MediaType) -> WPAnalyticsEvent? {
switch (self, mediaType) {
// Media Library
case (.mediaLibrary(let source), .image) where source == .tenor:
return .mediaLibraryAddedPhotoViaTenor

// Editor
case (.editor(let source), .image) where source == .tenor:
return .editorAddedPhotoViaTenor

default:
return nil
}
}

// This is for the previous events created within WordPressShared
func wpsharedEventForMediaType(_ mediaType: MediaType) -> WPAnalyticsStat? {
switch (self, mediaType) {
// Media Library
case (.mediaLibrary(let source), .image) where source == .deviceLibrary:
return .mediaLibraryAddedPhotoViaDeviceLibrary
case (.mediaLibrary(let source), .image) where source == .giphy:
Expand All @@ -125,6 +148,7 @@ enum MediaUploadOrigin {
return .mediaLibraryAddedVideoViaOtherApps
case (.mediaLibrary(let source), .video) where source == .camera:
return .mediaLibraryAddedVideoViaCamera
// Editor
case (.editor(let source), .image) where source == .giphy :
return .editorAddedPhotoViaGiphy
case (.editor(let source), .image) where source == .deviceLibrary:
Expand Down Expand Up @@ -159,4 +183,5 @@ enum MediaSource {
case camera
case giphy
case mediaEditor
case tenor
}
8 changes: 6 additions & 2 deletions WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ enum FeatureFlag: Int, CaseIterable {
case meMove
case floatingCreateButton
case newReaderNavigation
case tenor

/// Returns a boolean indicating if the feature is enabled
var enabled: Bool {
Expand All @@ -20,8 +21,7 @@ enum FeatureFlag: Int, CaseIterable {
case .jetpackDisconnect:
return BuildConfiguration.current == .localDeveloper
case .debugMenu:
return BuildConfiguration.current ~= [.localDeveloper,
.a8cBranchTest]
return BuildConfiguration.current ~= [.localDeveloper, .a8cBranchTest]
case .unifiedAuth:
return BuildConfiguration.current == .localDeveloper
case .quickActions:
Expand All @@ -32,6 +32,8 @@ enum FeatureFlag: Int, CaseIterable {
return BuildConfiguration.current ~= [.localDeveloper, .a8cBranchTest, .a8cPrereleaseTesting]
case .newReaderNavigation:
return BuildConfiguration.current ~= [.localDeveloper, .a8cBranchTest]
case .tenor:
return BuildConfiguration.current ~= [.localDeveloper, .a8cBranchTest]
}
}
}
Expand Down Expand Up @@ -64,6 +66,8 @@ extension FeatureFlag: OverrideableFlag {
return "Floating Create Button"
case .newReaderNavigation:
return "New Reader Navigation"
case .tenor:
return "Tenor GIF media source"
}
}

Expand Down
9 changes: 9 additions & 0 deletions WordPress/Classes/Utility/Media/GIFPlaybackStrategy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation

@objc
public enum GIFStrategy: Int {
case tinyGIFs
case smallGIFs
case mediumGIFs
case largeGIFs
Expand All @@ -10,6 +11,8 @@ public enum GIFStrategy: Int {
///
var playbackStrategy: GIFPlaybackStrategy {
switch self {
case .tinyGIFs:
return TinyGIFPlaybackStrategy()
case .smallGIFs:
return SmallGIFPlaybackStrategy()
case .mediumGIFs:
Expand Down Expand Up @@ -51,6 +54,12 @@ extension GIFPlaybackStrategy {
return true
}
}
// This is good for thumbnail GIFs used in a collection view
class TinyGIFPlaybackStrategy: GIFPlaybackStrategy {
var maxSize = 2_000_000 // in MB
var frameBufferCount = 5
var gifStrategy: GIFStrategy = .tinyGIFs
}

class SmallGIFPlaybackStrategy: GIFPlaybackStrategy {
var maxSize = 8_000_000 // in MB
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2395,7 +2395,7 @@ extension AztecPostViewController {
/// Sets the badge title of `attachment` to "GIF" if either the media is being imported from Giphy,
/// or if it's a PHAsset with an animated playback style.
private func setGifBadgeIfNecessary(for attachment: MediaAttachment, asset: ExportableAsset, source: MediaSource) {
var isGif = (source == .giphy)
var isGif = [.giphy, .tenor].contains(source)

if let asset = asset as? PHAsset,
asset.playbackStyle == .imageAnimated {
Expand Down Expand Up @@ -3315,6 +3315,14 @@ extension AztecPostViewController: GiphyPickerDelegate {
}
}

extension AztecPostViewController: TenorPickerDelegate {
func tenorPicker(_ picker: TenorPicker, didFinishPicking assets: [TenorMedia]) {
assets.forEach {
insert(exportableAsset: $0, source: .tenor)
}
}
}

// MARK: - Constants
//
extension AztecPostViewController {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class GutenbergViewController: UIViewController, PostEditor {
private lazy var filesAppMediaPicker: GutenbergFilesAppMediaSource = {
return GutenbergFilesAppMediaSource(gutenberg: gutenberg, mediaInserter: mediaInserterHelper)
}()
private lazy var tenorMediaPicker: GutenbergTenorMediaPicker = {
return GutenbergTenorMediaPicker(gutenberg: gutenberg, mediaInserter: mediaInserterHelper)
}()

// MARK: - Aztec

Expand Down Expand Up @@ -378,8 +381,14 @@ extension GutenbergViewController: GutenbergBridgeDelegate {
gutenbergDidRequestMediaFromDevicePicker(filter: flags, allowMultipleSelection: allowMultipleSelection, with: callback)
case .deviceCamera:
gutenbergDidRequestMediaFromCameraPicker(filter: flags, with: callback)

case .stockPhotos:
stockPhotos.presentPicker(origin: self, post: post, multipleSelection: allowMultipleSelection, callback: callback)
case .tenor:
tenorMediaPicker.presentPicker(origin: self,
post: post,
multipleSelection: allowMultipleSelection,
callback: callback)
case .filesApp:
filesAppMediaPicker.presentPicker(origin: self, filters: filter, multipleSelection: allowMultipleSelection, callback: callback)
default: break
Expand Down Expand Up @@ -637,6 +646,7 @@ extension GutenbergViewController: GutenbergBridgeDataSource {
func gutenbergMediaSources() -> [Gutenberg.MediaSource] {
return [
post.blog.supports(.stockPhotos) ? .stockPhotos : nil,
FeatureFlag.tenor.enabled ? .tenor : nil,
.filesApp,
].compactMap { $0 }
}
Expand Down Expand Up @@ -738,6 +748,7 @@ extension GutenbergViewController: PostEditorNavigationBarManagerDelegate {
extension Gutenberg.MediaSource {
static let stockPhotos = Gutenberg.MediaSource(id: "wpios-stock-photo-library", label: .freePhotosLibrary, types: [.image])
static let filesApp = Gutenberg.MediaSource(id: "wpios-files-app", label: .files, types: [.image, .video, .audio, .other])
static let tenor = Gutenberg.MediaSource(id: "wpios-tenor", label: .tenor, types: [.image])
}

private extension GutenbergViewController {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import Gutenberg

class GutenbergTenorMediaPicker {
private var tenor: TenorPicker?
private var mediaPickerCallback: MediaPickerDidPickMediaCallback?
private let mediaInserter: GutenbergMediaInserterHelper
private unowned var gutenberg: Gutenberg
private var multipleSelection = false

init(gutenberg: Gutenberg, mediaInserter: GutenbergMediaInserterHelper) {
self.mediaInserter = mediaInserter
self.gutenberg = gutenberg
}

func presentPicker(origin: UIViewController, post: AbstractPost, multipleSelection: Bool, callback: @escaping MediaPickerDidPickMediaCallback) {
let picker = TenorPicker()
tenor = picker
picker.allowMultipleSelection = true
picker.delegate = self
mediaPickerCallback = callback
picker.presentPicker(origin: origin, blog: post.blog)
self.multipleSelection = multipleSelection
}
}

extension GutenbergTenorMediaPicker: TenorPickerDelegate {
func tenorPicker(_ picker: TenorPicker, didFinishPicking assets: [TenorMedia]) {
defer {
mediaPickerCallback = nil
tenor = nil
}
guard assets.isEmpty == false else {
mediaPickerCallback?(nil)
return
}

// For blocks that support multiple uploads this will upload all images.
// If multiple uploads are not supported then it will seperate them out to Image Blocks.
multipleSelection ? insertOnBlock(with: assets) : insertSingleImages(assets)
}

/// Adds the given image object to the requesting block and seperates multiple images to seperate image blocks
/// - Parameter asset: Tenor Media object to add.
func insertSingleImages(_ assets: [TenorMedia]) {
// Append the first item via callback given by Gutenberg.
if let firstItem = assets.first {
insertOnBlock(with: [firstItem])
}
// Append the rest of images via `.appendMedia` event.
// Ideally we would send all picked images via the given callback, but that seems to not be possible yet.
appendOnNewBlocks(assets: assets.dropFirst())
}

/// Adds the given images to the requesting block
/// - Parameter assets: Tenor Media objects to add.
func insertOnBlock(with assets: [TenorMedia]) {
guard let callback = mediaPickerCallback else {
return assertionFailure("Image picked without callback")
}

let mediaInfo = assets.compactMap { (asset) -> MediaInfo? in
guard let media = self.mediaInserter.insert(exportableAsset: asset, source: .tenor) else {
return nil
}
let mediaUploadID = media.gutenbergUploadID
return MediaInfo(id: mediaUploadID, url: asset.URL.absoluteString, type: media.mediaTypeString)
}

callback(mediaInfo)
}

/// Create a new image block for each of the image objects in the slice.
/// - Parameter assets: Tenor Media objects to append.
func appendOnNewBlocks(assets: ArraySlice<TenorMedia>) {
assets.forEach {
if let media = self.mediaInserter.insert(exportableAsset: $0, source: .tenor) {
self.gutenberg.appendMedia(id: media.gutenbergUploadID, url: $0.URL, type: .image)
}
}
}
}
Loading

0 comments on commit bdf30ae

Please sign in to comment.