package com.fireworksdk.bridge.reactnative.manager import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.annotations.ReactProp import com.facebook.react.common.MapBuilder import com.firework.error.videofeed.VideoFeedError import com.firework.videofeed.FeedItemClickListener import com.firework.videofeed.FeedViewState import com.fireworksdk.bridge.components.videofeed.FWVideoFeed import com.fireworksdk.bridge.models.FWAdConfigurationDeserializer import com.fireworksdk.bridge.models.FWVideoFeedConfigModelDeserializer import com.fireworksdk.bridge.models.FWVideoFeedItemDetailsModel import com.fireworksdk.bridge.models.FWVideoPlayerConfigModelDeserializer import com.fireworksdk.bridge.models.enums.FWErrorAction import com.fireworksdk.bridge.models.enums.FWFeedViewEventName import com.fireworksdk.bridge.models.enums.FWVideoFeedMode import com.fireworksdk.bridge.models.enums.FWVideoFeedSource import com.fireworksdk.bridge.reactnative.utils.FWEventUtils import com.fireworksdk.bridge.utils.FWModelUtils.internalLiveStreamStatus import com.fireworksdk.bridge.utils.FWModelUtils.internalVideoType import org.json.JSONObject class FWVideoFeedManager : SimpleViewManager() { override fun getName(): String { return "FWVideoFeed" } override fun createViewInstance(reactContext: ThemedReactContext): FWVideoFeed { // MS-1250: Fix potential crash when reactContext.currentActivity is null val videoFeed = FWVideoFeed(reactContext.currentActivity ?: reactContext) videoFeed.themedReactContext = reactContext addVideoFeedListener(reactContext, videoFeed) return videoFeed } override fun onDropViewInstance(view: FWVideoFeed) { super.onDropViewInstance(view) view.dispose() } override fun onAfterUpdateTransaction(view: FWVideoFeed) { super.onAfterUpdateTransaction(view) view.initVideoFeedView() val reactContext = view.themedReactContext as? ThemedReactContext ?: return FWEventUtils.receiveVideoFeedGetFeedIdEvent( reactContext, view.id, view.getFeedId() ) } @ReactProp(name = "source") fun setSource(view: FWVideoFeed, source: String?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy( source = if (!source.isNullOrBlank()) FWVideoFeedSource.deserialize(source) else null ) } @ReactProp(name = "channel") fun setChannel(view: FWVideoFeed, channel: String?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy( channel = channel ) } @ReactProp(name = "playlist") fun setPlaylist(view: FWVideoFeed, playlist: String?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy( playlist = playlist ) } @ReactProp(name = "playlistGroup") fun setPlaylistGroup(view: FWVideoFeed, playlistGroup: String?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy( playlistGroup = playlistGroup ) } @ReactProp(name = "hashtagFilterExpression") fun setHashtagFilterExpression(view: FWVideoFeed, hashtagFilterExpression: String?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy( hashtagFilterExpression = hashtagFilterExpression ) } @Suppress("UNCHECKED_CAST") @ReactProp(name = "productIds") fun setProductIds(view: FWVideoFeed, productIdArray: ReadableArray?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy( productIds = productIdArray?.toArrayList() as ArrayList? ) } @ReactProp(name = "contentId") fun setContentId(view: FWVideoFeed, contentId: String?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy( contentId = contentId ) } @ReactProp(name = "shareUrl") fun setShareUrl(view: FWVideoFeed, shareUrl: String?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy( shareUrl = shareUrl ) } @ReactProp(name = "mode") fun setMode(view: FWVideoFeed, mode: String?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy( mode = if (!mode.isNullOrBlank()) FWVideoFeedMode.deserialize(mode) else null ) } @ReactProp(name = "autosizing") fun setAutosizing(view: FWVideoFeed, autosizing: Boolean) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy( autosizing = autosizing ) } /** * `0` is both React Native's value for a missing prop and the JS layer's "no cap", so it maps to * `null`; JS only ever sends a positive integer. */ @ReactProp(name = "maxVideos", defaultInt = 0) fun setMaxVideos(view: FWVideoFeed, maxVideos: Int) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy( maxVideos = maxVideos.takeIf { it > 0 } ) } @Suppress("UNCHECKED_CAST") @ReactProp(name = "dynamicContentParameters") fun setDynamicContentParameters(view: FWVideoFeed, parameters: ReadableMap?) { view.videoFeedPropsModel = view.videoFeedPropsModel.copy( dynamicContentParameters = parameters?.toHashMap() as? HashMap> ) } @ReactProp(name = "videoFeedConfiguration") fun setVideoFeedConfig(view: FWVideoFeed, config: ReadableMap?) { val configMap = config?.toHashMap() ?: hashMapOf() val jsonObject = JSONObject(configMap) val videoFeedConfigModel = FWVideoFeedConfigModelDeserializer.deserialize(jsonObject) view.videoFeedPropsModel = view.videoFeedPropsModel.copy( videoFeedConfiguration = videoFeedConfigModel ) } @ReactProp(name = "videoPlayerConfiguration") fun setVideoPlayerConfig(view: FWVideoFeed, config: ReadableMap?) { val configMap = config?.toHashMap() ?: hashMapOf() val jsonObject = JSONObject(configMap) val videoPlayerConfigModel = FWVideoPlayerConfigModelDeserializer.deserialize(jsonObject) view.videoFeedPropsModel = view.videoFeedPropsModel.copy( videoPlayerConfiguration = videoPlayerConfigModel ) } @ReactProp(name = "adConfiguration") fun setAdConfiguration(view: FWVideoFeed, config: ReadableMap?) { val configMap = config?.toHashMap() ?: hashMapOf() val jsonObject = JSONObject(configMap) val adConfigurationModel = FWAdConfigurationDeserializer.deserialize(jsonObject) view.videoFeedPropsModel = view.videoFeedPropsModel.copy( adConfiguration = adConfigurationModel ) } private fun addVideoFeedListener(reactContext: ThemedReactContext, videoFeed: FWVideoFeed?) { val videoFeedView = videoFeed?.videoFeedView videoFeedView ?: return videoFeedView.setOnFeedItemClickListener { feedItem -> val index = feedItem.indexInTheList val id = feedItem.id val duration = feedItem.duration val title = feedItem.title val fwVideoFeedItemDetailsModel = FWVideoFeedItemDetailsModel( videoFeedView.feedId, index, id, duration.toInt(), videoFeed.videoFeedPropsModel.source?.rawValue, title, videoFeed.videoFeedPropsModel.playlistGroup, videoFeed.videoFeedPropsModel.playlist, videoFeed.videoFeedPropsModel.channel, feedItem.videoInfo.internalVideoType, feedItem.videoInfo.internalLiveStreamStatus, videoFeed.videoFeedPropsModel.dynamicContentParameters ) FWEventUtils.sendVideoFeedClickEvent(reactContext, fwVideoFeedItemDetailsModel) } videoFeed.setOnFeedViewStateListener { feedViewState -> // Every load-state transition can change the feed's content height: the loading indicator // gives way to the first row, a page is appended, an empty feed collapses. videoFeed.invalidateContentHeight() when (feedViewState) { is FeedViewState.Loading -> { videoFeed.setContentEmpty(false) } is FeedViewState.LoadData -> { videoFeed.setContentEmpty(false) FWEventUtils.receiveVideoFeedLoadFinishedSuccessEvent(reactContext, videoFeed.id) } is FeedViewState.EmptyFeed -> { videoFeed.setContentEmpty(true) FWEventUtils.receiveVideoFeedEmptyEvent(reactContext, videoFeed.id) } is FeedViewState.EndOfFeed -> { FWEventUtils.receiveVideoFeedLoadFinishedSuccessEvent(reactContext, videoFeed.id) } else -> {} }} videoFeed.onContentHeightChanged = { height -> FWEventUtils.receiveVideoFeedContentSizeChangeEvent(reactContext, videoFeed.id, height) } videoFeed.setOnFeedLoadListener { videos -> // A full-screen player can also update the shared repository after an empty feed. if (videos.isNotEmpty()) videoFeed.setContentEmpty(false) videoFeed.invalidateContentHeight() FWEventUtils.receiveVideoFeedVideosLoadedEvent(reactContext, videoFeed.id, videos) } videoFeed.setOnErrorListener { error -> when (error) { is VideoFeedError.LoadingFirstPageFailed -> { FWEventUtils.receiveVideoFeedLoadFinishedFailedEvent( reactContext, videoFeed.id, error.message, error.message, FWErrorAction.LoadingFirstPage.rawValue ) } is VideoFeedError.LoadingNextPageFailed -> { FWEventUtils.receiveVideoFeedLoadFinishedFailedEvent( reactContext, videoFeed.id, error.message, error.message, FWErrorAction.LoadingNextPage.rawValue ) } else -> { } } } } override fun getCommandsMap(): MutableMap { val map: MutableMap = HashMap() map["refresh"] = COMMAND_REFRESH return map } override fun receiveCommand(root: FWVideoFeed, commandId: Int, args: ReadableArray?) { super.receiveCommand(root, commandId, args) when (commandId) { COMMAND_REFRESH -> { root.refresh() } else -> {} } } override fun receiveCommand(root: FWVideoFeed, commandId: String?, args: ReadableArray?) { super.receiveCommand(root, commandId, args) when (commandId?.toInt()) { COMMAND_REFRESH -> { root.refresh() } else -> {} } } /** * To map the "onVideoFeedLoadFinished" event name to the "onVideoFeedLoadFinished" callback prop in JavaScript * first onVideoFeedLoadFinished is event name * second onVideoFeedLoadFinished is callback prop in JavaScript */ override fun getExportedCustomBubblingEventTypeConstants(): Map? { return MapBuilder.builder() .put(FWFeedViewEventName.VideoFeedLoadFinished.rawValue, MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", FWFeedViewEventName.VideoFeedLoadFinished.rawValue))) .put(FWFeedViewEventName.VideoFeedEmpty.rawValue, MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", FWFeedViewEventName.VideoFeedEmpty.rawValue))) .put(FWFeedViewEventName.VideoFeedGetFeedId.rawValue, MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", FWFeedViewEventName.VideoFeedGetFeedId.rawValue))) .put(FWFeedViewEventName.VideoFeedVideosLoaded.rawValue, MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", FWFeedViewEventName.VideoFeedVideosLoaded.rawValue))) .build() } /** * Autosizing height feedback (native -> JS). A direct event: it carries this one view's size and * nothing up the tree listens for it. * * Merged onto the base map rather than replacing it, so `BaseViewManager`'s own direct events * (`onAccessibilityAction`) keep working on this view. */ override fun getExportedCustomDirectEventTypeConstants(): MutableMap { val constants: MutableMap = super.getExportedCustomDirectEventTypeConstants() ?: mutableMapOf() constants[FWFeedViewEventName.VideoFeedContentSizeChange.rawValue] = MapBuilder.of("registrationName", FWFeedViewEventName.VideoFeedContentSizeChange.rawValue) return constants } companion object { private const val COMMAND_REFRESH = 1000001 } }