diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts
index eeb5ae15..03d68524 100644
--- a/__tests__/lib/string.test.ts
+++ b/__tests__/lib/string.test.ts
@@ -459,6 +459,12 @@ describe('parseEmbedPlayerFromUrl', () => {
'https://tenor.com/view',
'https://tenor.com/view/gifId.gif',
'https://tenor.com/intl/view/gifId.gif',
+
+ 'https://media.tenor.com/someID_AAAAC/someName.gif?hh=100&ww=100',
+ 'https://media.tenor.com/someID_AAAAC/someName.gif',
+ 'https://media.tenor.com/someID/someName.gif',
+ 'https://media.tenor.com/someID',
+ 'https://media.tenor.com',
]
const outputs = [
@@ -628,137 +634,129 @@ describe('parseEmbedPlayerFromUrl', () => {
},
undefined,
undefined,
-
{
type: 'giphy_gif',
source: 'giphy',
isGif: true,
hideDetails: true,
metaUri: 'https://giphy.com/gifs/39248209509382934029',
- playerUri: 'https://i.giphy.com/media/39248209509382934029/200.mp4',
+ playerUri: 'https://i.giphy.com/media/39248209509382934029/200.webp',
+ },
+ {
+ type: 'giphy_gif',
+ source: 'giphy',
+ isGif: true,
+ hideDetails: true,
+ metaUri: 'https://giphy.com/gifs/gifId',
+ playerUri: 'https://i.giphy.com/media/gifId/200.webp',
+ },
+ {
+ type: 'giphy_gif',
+ source: 'giphy',
+ isGif: true,
+ hideDetails: true,
+ metaUri: 'https://giphy.com/gifs/gifId',
+ playerUri: 'https://i.giphy.com/media/gifId/200.webp',
+ },
+ {
+ type: 'giphy_gif',
+ source: 'giphy',
+ isGif: true,
+ hideDetails: true,
+ metaUri: 'https://giphy.com/gifs/gifId',
+ playerUri: 'https://i.giphy.com/media/gifId/200.webp',
+ },
+ {
+ type: 'giphy_gif',
+ source: 'giphy',
+ isGif: true,
+ hideDetails: true,
+ metaUri: 'https://giphy.com/gifs/gifId',
+ playerUri: 'https://i.giphy.com/media/gifId/200.webp',
+ },
+ {
+ type: 'giphy_gif',
+ source: 'giphy',
+ isGif: true,
+ hideDetails: true,
+ metaUri: 'https://giphy.com/gifs/gifId',
+ playerUri: 'https://i.giphy.com/media/gifId/200.webp',
+ },
+ {
+ type: 'giphy_gif',
+ source: 'giphy',
+ isGif: true,
+ hideDetails: true,
+ metaUri: 'https://giphy.com/gifs/gifId',
+ playerUri: 'https://i.giphy.com/media/gifId/200.webp',
+ },
+ undefined,
+ undefined,
+ undefined,
+
+ {
+ type: 'giphy_gif',
+ source: 'giphy',
+ isGif: true,
+ hideDetails: true,
+ metaUri: 'https://giphy.com/gifs/gifId',
+ playerUri: 'https://i.giphy.com/media/gifId/200.webp',
+ },
+
+ {
+ type: 'giphy_gif',
+ source: 'giphy',
+ isGif: true,
+ hideDetails: true,
+ metaUri: 'https://giphy.com/gifs/gifId',
+ playerUri: 'https://i.giphy.com/media/gifId/200.webp',
+ },
+ {
+ type: 'giphy_gif',
+ source: 'giphy',
+ isGif: true,
+ hideDetails: true,
+ metaUri: 'https://giphy.com/gifs/gifId',
+ playerUri: 'https://i.giphy.com/media/gifId/200.webp',
+ },
+ {
+ type: 'giphy_gif',
+ source: 'giphy',
+ isGif: true,
+ hideDetails: true,
+ metaUri: 'https://giphy.com/gifs/gifId',
+ playerUri: 'https://i.giphy.com/media/gifId/200.webp',
+ },
+ {
+ type: 'giphy_gif',
+ source: 'giphy',
+ isGif: true,
+ hideDetails: true,
+ metaUri: 'https://giphy.com/gifs/gifId',
+ playerUri: 'https://i.giphy.com/media/gifId/200.webp',
+ },
+
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+
+ {
+ type: 'tenor_gif',
+ source: 'tenor',
+ isGif: true,
+ hideDetails: true,
+ playerUri: 'https://t.gifs.bsky.app/someID_AAAAM/someName.gif',
dimensions: {
width: 100,
height: 100,
},
},
-
- {
- type: 'giphy_gif',
- source: 'giphy',
- isGif: true,
- hideDetails: true,
- metaUri: 'https://giphy.com/gifs/gifId',
- playerUri: 'https://i.giphy.com/media/gifId/200.webp',
- },
- {
- type: 'giphy_gif',
- source: 'giphy',
- isGif: true,
- hideDetails: true,
- metaUri: 'https://giphy.com/gifs/gifId',
- playerUri: 'https://i.giphy.com/media/gifId/200.webp',
- },
- {
- type: 'giphy_gif',
- source: 'giphy',
- isGif: true,
- hideDetails: true,
- metaUri: 'https://giphy.com/gifs/gifId',
- playerUri: 'https://i.giphy.com/media/gifId/200.webp',
- },
- {
- type: 'giphy_gif',
- source: 'giphy',
- isGif: true,
- hideDetails: true,
- metaUri: 'https://giphy.com/gifs/gifId',
- playerUri: 'https://i.giphy.com/media/gifId/200.webp',
- },
- {
- type: 'giphy_gif',
- source: 'giphy',
- isGif: true,
- hideDetails: true,
- metaUri: 'https://giphy.com/gifs/gifId',
- playerUri: 'https://i.giphy.com/media/gifId/200.webp',
- },
- {
- type: 'giphy_gif',
- source: 'giphy',
- isGif: true,
- hideDetails: true,
- metaUri: 'https://giphy.com/gifs/gifId',
- playerUri: 'https://i.giphy.com/media/gifId/200.webp',
- },
undefined,
undefined,
undefined,
-
- {
- type: 'giphy_gif',
- source: 'giphy',
- isGif: true,
- hideDetails: true,
- metaUri: 'https://giphy.com/gifs/gifId',
- playerUri: 'https://i.giphy.com/media/gifId/200.webp',
- },
-
- {
- type: 'giphy_gif',
- source: 'giphy',
- isGif: true,
- hideDetails: true,
- metaUri: 'https://giphy.com/gifs/gifId',
- playerUri: 'https://i.giphy.com/media/gifId/200.webp',
- },
- {
- type: 'giphy_gif',
- source: 'giphy',
- isGif: true,
- hideDetails: true,
- metaUri: 'https://giphy.com/gifs/gifId',
- playerUri: 'https://i.giphy.com/media/gifId/200.webp',
- },
- {
- type: 'giphy_gif',
- source: 'giphy',
- isGif: true,
- hideDetails: true,
- metaUri: 'https://giphy.com/gifs/gifId',
- playerUri: 'https://i.giphy.com/media/gifId/200.webp',
- },
- {
- type: 'giphy_gif',
- source: 'giphy',
- isGif: true,
- hideDetails: true,
- metaUri: 'https://giphy.com/gifs/gifId',
- playerUri: 'https://i.giphy.com/media/gifId/200.webp',
- },
-
- {
- type: 'tenor_gif',
- source: 'tenor',
- isGif: true,
- hideDetails: true,
- playerUri: 'https://tenor.com/view/gifId.gif',
- },
undefined,
- undefined,
- {
- type: 'tenor_gif',
- source: 'tenor',
- isGif: true,
- hideDetails: true,
- playerUri: 'https://tenor.com/view/gifId.gif',
- },
- {
- type: 'tenor_gif',
- source: 'tenor',
- isGif: true,
- hideDetails: true,
- playerUri: 'https://tenor.com/intl/view/gifId.gif',
- },
]
it('correctly grabs the correct id from uri', () => {
diff --git a/modules/expo-bluesky-gif-view/android/build.gradle b/modules/expo-bluesky-gif-view/android/build.gradle
new file mode 100644
index 00000000..c209a35a
--- /dev/null
+++ b/modules/expo-bluesky-gif-view/android/build.gradle
@@ -0,0 +1,98 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'maven-publish'
+
+group = 'expo.modules.blueskygifview'
+version = '0.5.0'
+
+buildscript {
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
+ if (expoModulesCorePlugin.exists()) {
+ apply from: expoModulesCorePlugin
+ applyKotlinExpoModulesCorePlugin()
+ }
+
+ // Simple helper that allows the root project to override versions declared by this library.
+ ext.safeExtGet = { prop, fallback ->
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
+ }
+
+ // Ensures backward compatibility
+ ext.getKotlinVersion = {
+ if (ext.has("kotlinVersion")) {
+ ext.kotlinVersion()
+ } else {
+ ext.safeExtGet("kotlinVersion", "1.8.10")
+ }
+ }
+
+ repositories {
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
+ }
+}
+
+afterEvaluate {
+ publishing {
+ publications {
+ release(MavenPublication) {
+ from components.release
+ }
+ }
+ repositories {
+ maven {
+ url = mavenLocal().url
+ }
+ }
+ }
+}
+
+android {
+ compileSdkVersion safeExtGet("compileSdkVersion", 33)
+
+ def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
+ if (agpVersion.tokenize('.')[0].toInteger() < 8) {
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_11
+ targetCompatibility JavaVersion.VERSION_11
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_11.majorVersion
+ }
+ }
+
+ namespace "expo.modules.blueskygifview"
+ defaultConfig {
+ minSdkVersion safeExtGet("minSdkVersion", 21)
+ targetSdkVersion safeExtGet("targetSdkVersion", 34)
+ versionCode 1
+ versionName "0.5.0"
+ }
+ lintOptions {
+ abortOnError false
+ }
+ publishing {
+ singleVariant("release") {
+ withSourcesJar()
+ }
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ def GLIDE_VERSION = "4.13.2"
+
+ implementation project(':expo-modules-core')
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
+
+ // Keep glide version up to date with expo-image so that we don't have duplicate deps
+ implementation 'com.github.bumptech.glide:glide:4.13.2'
+}
diff --git a/modules/expo-bluesky-gif-view/android/src/main/AndroidManifest.xml b/modules/expo-bluesky-gif-view/android/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..bdae66c8
--- /dev/null
+++ b/modules/expo-bluesky-gif-view/android/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt
new file mode 100644
index 00000000..5d208484
--- /dev/null
+++ b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/AppCompatImageViewExtended.kt
@@ -0,0 +1,37 @@
+package expo.modules.blueskygifview
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.drawable.Animatable
+import androidx.appcompat.widget.AppCompatImageView
+
+class AppCompatImageViewExtended(context: Context, private val parent: GifView): AppCompatImageView(context) {
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+
+ if (this.drawable is Animatable) {
+ if (!parent.isLoaded) {
+ parent.isLoaded = true
+ parent.firePlayerStateChange()
+ }
+
+ if (!parent.isPlaying) {
+ this.pause()
+ }
+ }
+ }
+
+ fun pause() {
+ val drawable = this.drawable
+ if (drawable is Animatable) {
+ drawable.stop()
+ }
+ }
+
+ fun play() {
+ val drawable = this.drawable
+ if (drawable is Animatable) {
+ drawable.start()
+ }
+ }
+}
\ No newline at end of file
diff --git a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt
new file mode 100644
index 00000000..625e1d45
--- /dev/null
+++ b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/ExpoBlueskyGifViewModule.kt
@@ -0,0 +1,54 @@
+package expo.modules.blueskygifview
+
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import expo.modules.kotlin.modules.Module
+import expo.modules.kotlin.modules.ModuleDefinition
+
+class ExpoBlueskyGifViewModule : Module() {
+ override fun definition() = ModuleDefinition {
+ Name("ExpoBlueskyGifView")
+
+ AsyncFunction("prefetchAsync") { sources: List ->
+ val activity = appContext.currentActivity ?: return@AsyncFunction
+ val glide = Glide.with(activity)
+
+ sources.forEach { source ->
+ glide
+ .download(source)
+ .diskCacheStrategy(DiskCacheStrategy.DATA)
+ .submit()
+ }
+ }
+
+ View(GifView::class) {
+ Events(
+ "onPlayerStateChange"
+ )
+
+ Prop("source") { view: GifView, source: String ->
+ view.source = source
+ }
+
+ Prop("placeholderSource") { view: GifView, source: String ->
+ view.placeholderSource = source
+ }
+
+ Prop("autoplay") { view: GifView, autoplay: Boolean ->
+ view.autoplay = autoplay
+ }
+
+ AsyncFunction("playAsync") { view: GifView ->
+ view.play()
+ }
+
+ AsyncFunction("pauseAsync") { view: GifView ->
+ view.pause()
+ }
+
+ AsyncFunction("toggleAsync") { view: GifView ->
+ view.toggle()
+ }
+ }
+ }
+}
diff --git a/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt
new file mode 100644
index 00000000..be5830df
--- /dev/null
+++ b/modules/expo-bluesky-gif-view/android/src/main/java/expo/modules/blueskygifview/GifView.kt
@@ -0,0 +1,180 @@
+package expo.modules.blueskygifview
+
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.drawable.Animatable
+import android.graphics.drawable.Drawable
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import com.bumptech.glide.load.engine.GlideException
+import com.bumptech.glide.request.RequestListener
+import com.bumptech.glide.request.target.Target
+import expo.modules.kotlin.AppContext
+import expo.modules.kotlin.exception.Exceptions
+import expo.modules.kotlin.viewevent.EventDispatcher
+import expo.modules.kotlin.views.ExpoView
+
+class GifView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
+ // Events
+ private val onPlayerStateChange by EventDispatcher()
+
+ // Glide
+ private val activity = appContext.currentActivity ?: throw Exceptions.MissingActivity()
+ private val glide = Glide.with(activity)
+ val imageView = AppCompatImageViewExtended(context, this)
+ var isPlaying = true
+ var isLoaded = false
+
+ // Requests
+ private var placeholderRequest: Target? = null
+ private var webpRequest: Target? = null
+
+ // Props
+ var placeholderSource: String? = null
+ var source: String? = null
+ var autoplay: Boolean = true
+ set(value) {
+ field = value
+
+ if (value) {
+ this.play()
+ } else {
+ this.pause()
+ }
+ }
+
+
+ //
+
+ init {
+ this.setBackgroundColor(Color.TRANSPARENT)
+
+ this.imageView.setBackgroundColor(Color.TRANSPARENT)
+ this.imageView.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
+
+ this.addView(this.imageView)
+ }
+
+ override fun onAttachedToWindow() {
+ if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) {
+ this.load()
+ } else if (this.isPlaying) {
+ this.imageView.play()
+ }
+ super.onAttachedToWindow()
+ }
+
+ override fun onDetachedFromWindow() {
+ this.imageView.pause()
+ super.onDetachedFromWindow()
+ }
+
+ //
+
+ //
+
+ private fun load() {
+ if (placeholderSource == null || source == null) {
+ return
+ }
+
+ this.webpRequest = glide.load(source)
+ .diskCacheStrategy(DiskCacheStrategy.DATA)
+ .skipMemoryCache(false)
+ .listener(object: RequestListener {
+ override fun onResourceReady(
+ resource: Drawable?,
+ model: Any?,
+ target: Target?,
+ dataSource: com.bumptech.glide.load.DataSource?,
+ isFirstResource: Boolean
+ ): Boolean {
+ if (placeholderRequest != null) {
+ glide.clear(placeholderRequest)
+ }
+ return false
+ }
+
+ override fun onLoadFailed(
+ e: GlideException?,
+ model: Any?,
+ target: Target?,
+ isFirstResource: Boolean
+ ): Boolean {
+ return true
+ }
+ })
+ .into(this.imageView)
+
+ if (this.imageView.drawable == null || this.imageView.drawable !is Animatable) {
+ this.placeholderRequest = glide.load(placeholderSource)
+ .diskCacheStrategy(DiskCacheStrategy.DATA)
+ // Let's not bloat the memory cache with placeholders
+ .skipMemoryCache(true)
+ .listener(object: RequestListener {
+ override fun onResourceReady(
+ resource: Drawable?,
+ model: Any?,
+ target: Target?,
+ dataSource: com.bumptech.glide.load.DataSource?,
+ isFirstResource: Boolean
+ ): Boolean {
+ // Incase this request finishes after the webp, let's just not set
+ // the drawable. This shouldn't happen because the request should get cancelled
+ if (imageView.drawable == null) {
+ imageView.setImageDrawable(resource)
+ }
+ return true
+ }
+
+ override fun onLoadFailed(
+ e: GlideException?,
+ model: Any?,
+ target: Target?,
+ isFirstResource: Boolean
+ ): Boolean {
+ return true
+ }
+ })
+ .submit()
+ }
+ }
+
+ //
+
+ //
+
+ fun play() {
+ this.imageView.play()
+ this.isPlaying = true
+ this.firePlayerStateChange()
+ }
+
+ fun pause() {
+ this.imageView.pause()
+ this.isPlaying = false
+ this.firePlayerStateChange()
+ }
+
+ fun toggle() {
+ if (this.isPlaying) {
+ this.pause()
+ } else {
+ this.play()
+ }
+ }
+
+ //
+
+ //
+
+ fun firePlayerStateChange() {
+ onPlayerStateChange(mapOf(
+ "isPlaying" to this.isPlaying,
+ "isLoaded" to this.isLoaded,
+ ))
+ }
+
+ //
+}
diff --git a/modules/expo-bluesky-gif-view/expo-module.config.json b/modules/expo-bluesky-gif-view/expo-module.config.json
new file mode 100644
index 00000000..0756c8e2
--- /dev/null
+++ b/modules/expo-bluesky-gif-view/expo-module.config.json
@@ -0,0 +1,9 @@
+{
+ "platforms": ["ios", "android", "web"],
+ "ios": {
+ "modules": ["ExpoBlueskyGifViewModule"]
+ },
+ "android": {
+ "modules": ["expo.modules.blueskygifview.ExpoBlueskyGifViewModule"]
+ }
+}
diff --git a/modules/expo-bluesky-gif-view/index.ts b/modules/expo-bluesky-gif-view/index.ts
new file mode 100644
index 00000000..0244a549
--- /dev/null
+++ b/modules/expo-bluesky-gif-view/index.ts
@@ -0,0 +1 @@
+export {GifView} from './src/GifView'
diff --git a/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifView.podspec b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifView.podspec
new file mode 100644
index 00000000..ddd0877b
--- /dev/null
+++ b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifView.podspec
@@ -0,0 +1,23 @@
+Pod::Spec.new do |s|
+ s.name = 'ExpoBlueskyGifView'
+ s.version = '1.0.0'
+ s.summary = 'A simple GIF player for Bluesky'
+ s.description = 'A simple GIF player for Bluesky'
+ s.author = ''
+ s.homepage = 'https://github.com/bluesky-social/social-app'
+ s.platforms = { :ios => '13.4', :tvos => '13.4' }
+ s.source = { git: '' }
+ s.static_framework = true
+
+ s.dependency 'ExpoModulesCore'
+ s.dependency 'SDWebImage', '~> 5.17.0'
+ s.dependency 'SDWebImageWebPCoder', '~> 0.13.0'
+
+ # Swift/Objective-C compatibility
+ s.pod_target_xcconfig = {
+ 'DEFINES_MODULE' => 'YES',
+ 'SWIFT_COMPILATION_MODE' => 'wholemodule'
+ }
+
+ s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
+end
diff --git a/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift
new file mode 100644
index 00000000..7c713229
--- /dev/null
+++ b/modules/expo-bluesky-gif-view/ios/ExpoBlueskyGifViewModule.swift
@@ -0,0 +1,47 @@
+import ExpoModulesCore
+import SDWebImage
+import SDWebImageWebPCoder
+
+public class ExpoBlueskyGifViewModule: Module {
+ public func definition() -> ModuleDefinition {
+ Name("ExpoBlueskyGifView")
+
+ OnCreate {
+ SDImageCodersManager.shared.addCoder(SDImageGIFCoder.shared)
+ }
+
+ AsyncFunction("prefetchAsync") { (sources: [URL]) in
+ SDWebImagePrefetcher.shared.prefetchURLs(sources, context: Util.createContext(), progress: nil)
+ }
+
+ View(GifView.self) {
+ Events(
+ "onPlayerStateChange"
+ )
+
+ Prop("source") { (view: GifView, prop: String) in
+ view.source = prop
+ }
+
+ Prop("placeholderSource") { (view: GifView, prop: String) in
+ view.placeholderSource = prop
+ }
+
+ Prop("autoplay") { (view: GifView, prop: Bool) in
+ view.autoplay = prop
+ }
+
+ AsyncFunction("toggleAsync") { (view: GifView) in
+ view.toggle()
+ }
+
+ AsyncFunction("playAsync") { (view: GifView) in
+ view.play()
+ }
+
+ AsyncFunction("pauseAsync") { (view: GifView) in
+ view.pause()
+ }
+ }
+ }
+}
diff --git a/modules/expo-bluesky-gif-view/ios/GifView.swift b/modules/expo-bluesky-gif-view/ios/GifView.swift
new file mode 100644
index 00000000..de722d7a
--- /dev/null
+++ b/modules/expo-bluesky-gif-view/ios/GifView.swift
@@ -0,0 +1,185 @@
+import ExpoModulesCore
+import SDWebImage
+import SDWebImageWebPCoder
+
+typealias SDWebImageContext = [SDWebImageContextOption: Any]
+
+public class GifView: ExpoView, AVPlayerViewControllerDelegate {
+ // Events
+ private let onPlayerStateChange = EventDispatcher()
+
+ // SDWebImage
+ private let imageView = SDAnimatedImageView(frame: .zero)
+ private let imageManager = SDWebImageManager(
+ cache: SDImageCache.shared,
+ loader: SDImageLoadersManager.shared
+ )
+ private var isPlaying = true
+ private var isLoaded = false
+
+ // Requests
+ private var webpOperation: SDWebImageCombinedOperation?
+ private var placeholderOperation: SDWebImageCombinedOperation?
+
+ // Props
+ var source: String? = nil
+ var placeholderSource: String? = nil
+ var autoplay = true {
+ didSet {
+ if !autoplay {
+ self.pause()
+ } else {
+ self.play()
+ }
+ }
+ }
+
+ // MARK: - Lifecycle
+
+ public required init(appContext: AppContext? = nil) {
+ super.init(appContext: appContext)
+ self.clipsToBounds = true
+
+ self.imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+ self.imageView.layer.masksToBounds = false
+ self.imageView.backgroundColor = .clear
+ self.imageView.contentMode = .scaleToFill
+
+ // We have to explicitly set this to false. If we don't, every time
+ // the view comes into the viewport, it will start animating again
+ self.imageView.autoPlayAnimatedImage = false
+
+ self.addSubview(self.imageView)
+ }
+
+ public override func willMove(toWindow newWindow: UIWindow?) {
+ if newWindow == nil {
+ // Don't cancel the placeholder operation, because we really want that to complete for
+ // when we scroll back up
+ self.webpOperation?.cancel()
+ self.placeholderOperation?.cancel()
+ } else if self.imageView.image == nil {
+ self.load()
+ }
+ }
+
+ // MARK: - Loading
+
+ private func load() {
+ guard let source = self.source, let placeholderSource = self.placeholderSource else {
+ return
+ }
+
+ self.webpOperation?.cancel()
+ self.placeholderOperation?.cancel()
+
+ // We only need to start an operation for the placeholder if it doesn't exist
+ // in the cache already. Cache key is by default the absolute URL of the image.
+ // See:
+ // https://github.com/SDWebImage/SDWebImage/blob/master/Docs/HowToUse.md#using-asynchronous-image-caching-independently
+ if !SDImageCache.shared.diskImageDataExists(withKey: source),
+ let url = URL(string: placeholderSource)
+ {
+ self.placeholderOperation = imageManager.loadImage(
+ with: url,
+ options: [.retryFailed],
+ context: Util.createContext(),
+ progress: onProgress(_:_:_:),
+ completed: onLoaded(_:_:_:_:_:_:)
+ )
+ }
+
+ if let url = URL(string: source) {
+ self.webpOperation = imageManager.loadImage(
+ with: url,
+ options: [.retryFailed],
+ context: Util.createContext(),
+ progress: onProgress(_:_:_:),
+ completed: onLoaded(_:_:_:_:_:_:)
+ )
+ }
+ }
+
+ private func setImage(_ image: UIImage) {
+ if self.imageView.image == nil || image.sd_isAnimated {
+ self.imageView.image = image
+ }
+
+ if image.sd_isAnimated {
+ self.firePlayerStateChange()
+ if isPlaying {
+ self.imageView.startAnimating()
+ }
+ }
+ }
+
+ // MARK: - Loading blocks
+
+ private func onProgress(_ receivedSize: Int, _ expectedSize: Int, _ imageUrl: URL?) {}
+
+ private func onLoaded(
+ _ image: UIImage?,
+ _ data: Data?,
+ _ error: Error?,
+ _ cacheType: SDImageCacheType,
+ _ finished: Bool,
+ _ imageUrl: URL?
+ ) {
+ guard finished else {
+ return
+ }
+
+ if let placeholderSource = self.placeholderSource,
+ imageUrl?.absoluteString == placeholderSource,
+ self.imageView.image == nil,
+ let image = image
+ {
+ self.setImage(image)
+ return
+ }
+
+ if let source = self.source,
+ imageUrl?.absoluteString == source,
+ // UIImage perf suckssss if the image is animated
+ let data = data,
+ let animatedImage = SDAnimatedImage(data: data)
+ {
+ self.placeholderOperation?.cancel()
+ self.isPlaying = self.autoplay
+ self.isLoaded = true
+ self.setImage(animatedImage)
+ self.firePlayerStateChange()
+ }
+ }
+
+ // MARK: - Playback Controls
+
+ func play() {
+ self.imageView.startAnimating()
+ self.isPlaying = true
+ self.firePlayerStateChange()
+ }
+
+ func pause() {
+ self.imageView.stopAnimating()
+ self.isPlaying = false
+ self.firePlayerStateChange()
+ }
+
+ func toggle() {
+ if self.isPlaying {
+ self.pause()
+ } else {
+ self.play()
+ }
+ }
+
+ // MARK: - Util
+
+ private func firePlayerStateChange() {
+ onPlayerStateChange([
+ "isPlaying": self.isPlaying,
+ "isLoaded": self.isLoaded
+ ])
+ }
+}
diff --git a/modules/expo-bluesky-gif-view/ios/Util.swift b/modules/expo-bluesky-gif-view/ios/Util.swift
new file mode 100644
index 00000000..55ed4152
--- /dev/null
+++ b/modules/expo-bluesky-gif-view/ios/Util.swift
@@ -0,0 +1,17 @@
+import SDWebImage
+
+class Util {
+ static func createContext() -> SDWebImageContext {
+ var context = SDWebImageContext()
+
+ // SDAnimatedImage for some reason has issues whenever loaded from memory. Instead, we
+ // will just use the disk. SDWebImage will manage this cache for us, so we don't need
+ // to worry about clearing it.
+ context[.originalQueryCacheType] = SDImageCacheType.disk.rawValue
+ context[.originalStoreCacheType] = SDImageCacheType.disk.rawValue
+ context[.queryCacheType] = SDImageCacheType.disk.rawValue
+ context[.storeCacheType] = SDImageCacheType.disk.rawValue
+
+ return context
+ }
+}
diff --git a/modules/expo-bluesky-gif-view/src/GifView.tsx b/modules/expo-bluesky-gif-view/src/GifView.tsx
new file mode 100644
index 00000000..87258de1
--- /dev/null
+++ b/modules/expo-bluesky-gif-view/src/GifView.tsx
@@ -0,0 +1,39 @@
+import React from 'react'
+import {requireNativeModule} from 'expo'
+import {requireNativeViewManager} from 'expo-modules-core'
+
+import {GifViewProps} from './GifView.types'
+
+const NativeModule = requireNativeModule('ExpoBlueskyGifView')
+const NativeView: React.ComponentType<
+ GifViewProps & {ref: React.RefObject}
+> = requireNativeViewManager('ExpoBlueskyGifView')
+
+export class GifView extends React.PureComponent {
+ // TODO native types, should all be the same as those in this class
+ private nativeRef: React.RefObject = React.createRef()
+
+ constructor(props: GifViewProps | Readonly) {
+ super(props)
+ }
+
+ static async prefetchAsync(sources: string[]): Promise {
+ return await NativeModule.prefetchAsync(sources)
+ }
+
+ async playAsync(): Promise {
+ await this.nativeRef.current.playAsync()
+ }
+
+ async pauseAsync(): Promise {
+ await this.nativeRef.current.pauseAsync()
+ }
+
+ async toggleAsync(): Promise {
+ await this.nativeRef.current.toggleAsync()
+ }
+
+ render() {
+ return
+ }
+}
diff --git a/modules/expo-bluesky-gif-view/src/GifView.types.ts b/modules/expo-bluesky-gif-view/src/GifView.types.ts
new file mode 100644
index 00000000..29ec277f
--- /dev/null
+++ b/modules/expo-bluesky-gif-view/src/GifView.types.ts
@@ -0,0 +1,15 @@
+import {ViewProps} from 'react-native'
+
+export interface GifViewStateChangeEvent {
+ nativeEvent: {
+ isPlaying: boolean
+ isLoaded: boolean
+ }
+}
+
+export interface GifViewProps extends ViewProps {
+ autoplay?: boolean
+ source?: string
+ placeholderSource?: string
+ onPlayerStateChange?: (event: GifViewStateChangeEvent) => void
+}
diff --git a/modules/expo-bluesky-gif-view/src/GifView.web.tsx b/modules/expo-bluesky-gif-view/src/GifView.web.tsx
new file mode 100644
index 00000000..c197e01a
--- /dev/null
+++ b/modules/expo-bluesky-gif-view/src/GifView.web.tsx
@@ -0,0 +1,82 @@
+import * as React from 'react'
+import {StyleSheet} from 'react-native'
+
+import {GifViewProps} from './GifView.types'
+
+export class GifView extends React.PureComponent {
+ private readonly videoPlayerRef: React.RefObject =
+ React.createRef()
+ private isLoaded = false
+
+ constructor(props: GifViewProps | Readonly) {
+ super(props)
+ }
+
+ componentDidUpdate(prevProps: Readonly) {
+ if (prevProps.autoplay !== this.props.autoplay) {
+ if (this.props.autoplay) {
+ this.playAsync()
+ } else {
+ this.pauseAsync()
+ }
+ }
+ }
+
+ static async prefetchAsync(_: string[]): Promise {
+ console.warn('prefetchAsync is not supported on web')
+ }
+
+ private firePlayerStateChangeEvent = () => {
+ this.props.onPlayerStateChange?.({
+ nativeEvent: {
+ isPlaying: !this.videoPlayerRef.current?.paused,
+ isLoaded: this.isLoaded,
+ },
+ })
+ }
+
+ private onLoad = () => {
+ // Prevent multiple calls to onLoad because onCanPlay will fire after each loop
+ if (this.isLoaded) {
+ return
+ }
+
+ this.isLoaded = true
+ this.firePlayerStateChangeEvent()
+ }
+
+ async playAsync(): Promise {
+ this.videoPlayerRef.current?.play()
+ }
+
+ async pauseAsync(): Promise {
+ this.videoPlayerRef.current?.pause()
+ }
+
+ async toggleAsync(): Promise {
+ if (this.videoPlayerRef.current?.paused) {
+ await this.playAsync()
+ } else {
+ await this.pauseAsync()
+ }
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts
index c41083af..84183c1d 100644
--- a/src/lib/statsig/gates.ts
+++ b/src/lib/statsig/gates.ts
@@ -4,7 +4,6 @@ export type Gate =
| 'disable_min_shell_on_foregrounding_v2'
| 'disable_poll_on_discover_v2'
| 'hide_vertical_scroll_indicators'
- | 'new_gif_player'
| 'show_follow_back_label_v2'
| 'start_session_with_following_v2'
| 'use_new_suggestions_endpoint'
diff --git a/src/lib/strings/embed-player.ts b/src/lib/strings/embed-player.ts
index bbc58a20..b1fc75b8 100644
--- a/src/lib/strings/embed-player.ts
+++ b/src/lib/strings/embed-player.ts
@@ -1,4 +1,4 @@
-import {Dimensions} from 'react-native'
+import {Dimensions, Platform} from 'react-native'
import {isWeb} from 'platform/detection'
const {height: SCREEN_HEIGHT} = Dimensions.get('window')
@@ -255,16 +255,6 @@ export function parseEmbedPlayerFromUrl(
if (urlp.hostname === 'giphy.com' || urlp.hostname === 'www.giphy.com') {
const [_, gifs, nameAndId] = urlp.pathname.split('/')
- const h = urlp.searchParams.get('hh')
- const w = urlp.searchParams.get('ww')
- let dimensions
- if (h && w) {
- dimensions = {
- height: Number(h),
- width: Number(w),
- }
- }
-
/*
* nameAndId is a string that consists of the name (dash separated) and the id of the gif (the last part of the name)
* We want to get the id of the gif, then direct to media.giphy.com/media/{id}/giphy.webp so we can
@@ -281,10 +271,7 @@ export function parseEmbedPlayerFromUrl(
isGif: true,
hideDetails: true,
metaUri: `https://giphy.com/gifs/${gifId}`,
- playerUri: `https://i.giphy.com/media/${gifId}/${
- dimensions ? '200.mp4' : '200.webp'
- }`,
- dimensions,
+ playerUri: `https://i.giphy.com/media/${gifId}/200.webp`,
}
}
}
@@ -350,21 +337,34 @@ export function parseEmbedPlayerFromUrl(
}
}
- if (urlp.hostname === 'tenor.com' || urlp.hostname === 'www.tenor.com') {
- const [_, pathOrIntl, pathOrFilename, intlFilename] =
- urlp.pathname.split('/')
- const isIntl = pathOrFilename === 'view'
- const filename = isIntl ? intlFilename : pathOrFilename
+ if (urlp.hostname === 'media.tenor.com') {
+ let [_, id, filename] = urlp.pathname.split('/')
- if ((pathOrIntl === 'view' || pathOrFilename === 'view') && filename) {
- const includesExt = filename.split('.').pop() === 'gif'
+ const h = urlp.searchParams.get('hh')
+ const w = urlp.searchParams.get('ww')
+ let dimensions
+ if (h && w) {
+ dimensions = {
+ height: Number(h),
+ width: Number(w),
+ }
+ }
+
+ if (id && filename && dimensions && id.includes('AAAAC')) {
+ if (Platform.OS === 'web') {
+ id = id.replace('AAAAC', 'AAAP3')
+ filename = filename.replace('.gif', '.webm')
+ } else {
+ id = id.replace('AAAAC', 'AAAAM')
+ }
return {
type: 'tenor_gif',
source: 'tenor',
isGif: true,
hideDetails: true,
- playerUri: `${url}${!includesExt ? '.gif' : ''}`,
+ playerUri: `https://t.gifs.bsky.app/${id}/${filename}`,
+ dimensions,
}
}
}
diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx
index 93e2dc6b..8d14c16e 100644
--- a/src/view/com/composer/Composer.tsx
+++ b/src/view/com/composer/Composer.tsx
@@ -121,6 +121,7 @@ export const ComposePost = observer(function ComposePost({
initQuote,
)
const {extLink, setExtLink} = useExternalLinkFetch({setQuote})
+ const [extGif, setExtGif] = useState()
const [labels, setLabels] = useState([])
const [threadgate, setThreadgate] = useState([])
const gallery = useMemo(
@@ -318,7 +319,7 @@ export const ComposePost = observer(function ComposePost({
const onSelectGif = useCallback(
(gif: Gif) => {
setExtLink({
- uri: `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[0]}&ww=${gif.media_formats.gif.dims[1]}`,
+ uri: `${gif.media_formats.gif.url}?hh=${gif.media_formats.gif.dims[1]}&ww=${gif.media_formats.gif.dims[0]}`,
isLoading: true,
meta: {
url: gif.media_formats.gif.url,
@@ -328,6 +329,7 @@ export const ComposePost = observer(function ComposePost({
description: `ALT: ${gif.content_description}`,
},
})
+ setExtGif(gif)
},
[setExtLink],
)
@@ -473,7 +475,11 @@ export const ComposePost = observer(function ComposePost({
{gallery.isEmpty && extLink && (
setExtLink(undefined)}
+ gif={extGif}
+ onRemove={() => {
+ setExtLink(undefined)
+ setExtGif(undefined)
+ }}
/>
)}
{quote ? (
diff --git a/src/view/com/composer/ExternalEmbed.tsx b/src/view/com/composer/ExternalEmbed.tsx
index 3c2bf762..321e29b3 100644
--- a/src/view/com/composer/ExternalEmbed.tsx
+++ b/src/view/com/composer/ExternalEmbed.tsx
@@ -1,11 +1,12 @@
import React from 'react'
-import {TouchableOpacity, View} from 'react-native'
+import {StyleProp, TouchableOpacity, View, ViewStyle} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {ExternalEmbedDraft} from 'lib/api/index'
import {s} from 'lib/styles'
+import {Gif} from 'state/queries/tenor'
import {ExternalLinkEmbed} from 'view/com/util/post-embeds/ExternalLinkEmbed'
import {atoms as a, useTheme} from '#/alf'
import {Loader} from '#/components/Loader'
@@ -14,9 +15,11 @@ import {Text} from '#/components/Typography'
export const ExternalEmbed = ({
link,
onRemove,
+ gif,
}: {
link?: ExternalEmbedDraft
onRemove: () => void
+ gif?: Gif
}) => {
const t = useTheme()
const {_} = useLingui()
@@ -34,45 +37,38 @@ export const ExternalEmbed = ({
if (!link) return null
+ const loadingStyle: ViewStyle | undefined = gif
+ ? {
+ aspectRatio:
+ gif.media_formats.gif.dims[0] / gif.media_formats.gif.dims[1],
+ width: '100%',
+ }
+ : undefined
+
return (
-
+
{link.isLoading ? (
-
+
-
+
) : link.meta?.error ? (
-
+
{link.uri}
- {link.meta.error}
+ {link.meta?.error}
-
+
) : linkInfo ? (
-
+
) : null}
)
}
+
+function Container({
+ style,
+ children,
+}: {
+ style?: StyleProp
+ children: React.ReactNode
+}) {
+ const t = useTheme()
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx
index ff7c643f..1fe75c44 100644
--- a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx
+++ b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx
@@ -1,27 +1,32 @@
-import React from 'react'
-import {StyleSheet, View} from 'react-native'
+import React, {useCallback} from 'react'
+import {StyleProp, View, ViewStyle} from 'react-native'
import {Image} from 'expo-image'
import {AppBskyEmbedExternal} from '@atproto/api'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {useGate} from 'lib/statsig/statsig'
+import {shareUrl} from 'lib/sharing'
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
import {toNiceDomain} from 'lib/strings/url-helpers'
+import {isNative} from 'platform/detection'
import {useExternalEmbedsPrefs} from 'state/preferences'
+import {Link} from 'view/com/util/Link'
import {ExternalGifEmbed} from 'view/com/util/post-embeds/ExternalGifEmbed'
import {ExternalPlayer} from 'view/com/util/post-embeds/ExternalPlayerEmbed'
+import {GifEmbed} from 'view/com/util/post-embeds/GifEmbed'
+import {atoms as a, useTheme} from '#/alf'
import {Text} from '../text/Text'
export const ExternalLinkEmbed = ({
link,
+ style,
}: {
link: AppBskyEmbedExternal.ViewExternal
+ style?: StyleProp
}) => {
const pal = usePalette('default')
const {isMobile} = useWebMediaQueries()
const externalEmbedPrefs = useExternalEmbedsPrefs()
- const gate = useGate()
const embedPlayerParams = React.useMemo(() => {
const params = parseEmbedPlayerFromUrl(link.uri)
@@ -30,71 +35,96 @@ export const ExternalLinkEmbed = ({
return params
}
}, [link.uri, externalEmbedPrefs])
- const isCompatibleGiphy =
- embedPlayerParams?.source === 'giphy' &&
- embedPlayerParams.dimensions &&
- gate('new_gif_player')
+
+ if (embedPlayerParams?.source === 'tenor') {
+ return
+ }
return (
-
- {link.thumb && !embedPlayerParams ? (
-
- ) : undefined}
- {isCompatibleGiphy ? (
-
- ) : embedPlayerParams?.isGif ? (
-
- ) : embedPlayerParams ? (
-
- ) : undefined}
-
- {!isCompatibleGiphy && (
+
+
+ {link.thumb && !embedPlayerParams ? (
+
+ ) : undefined}
+ {embedPlayerParams?.isGif ? (
+
+ ) : embedPlayerParams ? (
+
+ ) : undefined}
+
+ style={[pal.textLight, {marginVertical: 2}]}>
{toNiceDomain(link.uri)}
- )}
- {!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && (
-
- {link.title || link.uri}
-
- )}
- {link.description && !embedPlayerParams?.hideDetails ? (
-
- {link.description}
-
- ) : undefined}
-
+ {!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && (
+
+ {link.title || link.uri}
+
+ )}
+ {link.description ? (
+
+ {link.description}
+
+ ) : undefined}
+
+
)
}
-const styles = StyleSheet.create({
- container: {
- flexDirection: 'column',
- borderRadius: 6,
- overflow: 'hidden',
- },
- info: {
- width: '100%',
- bottom: 0,
- paddingTop: 8,
- paddingBottom: 10,
- },
- extUri: {
- marginTop: 2,
- },
- extDescription: {
- marginTop: 4,
- },
-})
+function LinkWrapper({
+ link,
+ style,
+ children,
+}: {
+ link: AppBskyEmbedExternal.ViewExternal
+ style?: StyleProp
+ children: React.ReactNode
+}) {
+ const t = useTheme()
+
+ const onShareExternal = useCallback(() => {
+ if (link.uri && isNative) {
+ shareUrl(link.uri)
+ }
+ }, [link.uri])
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/view/com/util/post-embeds/GifEmbed.tsx b/src/view/com/util/post-embeds/GifEmbed.tsx
new file mode 100644
index 00000000..32bd75df
--- /dev/null
+++ b/src/view/com/util/post-embeds/GifEmbed.tsx
@@ -0,0 +1,140 @@
+import React from 'react'
+import {Pressable, View} from 'react-native'
+import {AppBskyEmbedExternal} from '@atproto/api'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {EmbedPlayerParams} from 'lib/strings/embed-player'
+import {useAutoplayDisabled} from 'state/preferences'
+import {atoms as a, useTheme} from '#/alf'
+import {Loader} from '#/components/Loader'
+import {GifView} from '../../../../../modules/expo-bluesky-gif-view'
+import {GifViewStateChangeEvent} from '../../../../../modules/expo-bluesky-gif-view/src/GifView.types'
+
+function PlaybackControls({
+ onPress,
+ isPlaying,
+ isLoaded,
+}: {
+ onPress: () => void
+ isPlaying: boolean
+ isLoaded: boolean
+}) {
+ const {_} = useLingui()
+ const t = useTheme()
+
+ return (
+
+ {!isLoaded ? (
+
+
+
+
+
+ ) : !isPlaying ? (
+
+
+
+ ) : undefined}
+
+ )
+}
+
+export function GifEmbed({
+ params,
+ link,
+}: {
+ params: EmbedPlayerParams
+ link: AppBskyEmbedExternal.ViewExternal
+}) {
+ const {_} = useLingui()
+ const autoplayDisabled = useAutoplayDisabled()
+
+ const playerRef = React.useRef(null)
+
+ const [playerState, setPlayerState] = React.useState<{
+ isPlaying: boolean
+ isLoaded: boolean
+ }>({
+ isPlaying: !autoplayDisabled,
+ isLoaded: false,
+ })
+
+ const onPlayerStateChange = React.useCallback(
+ (e: GifViewStateChangeEvent) => {
+ setPlayerState(e.nativeEvent)
+ },
+ [],
+ )
+
+ const onPress = React.useCallback(() => {
+ playerRef.current?.toggleAsync()
+ }, [])
+
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx
index 47091fbb..7ea5b55c 100644
--- a/src/view/com/util/post-embeds/index.tsx
+++ b/src/view/com/util/post-embeds/index.tsx
@@ -1,34 +1,32 @@
-import React, {useCallback} from 'react'
+import React from 'react'
import {
- StyleSheet,
+ InteractionManager,
StyleProp,
+ StyleSheet,
+ Text,
View,
ViewStyle,
- Text,
- InteractionManager,
} from 'react-native'
import {Image} from 'expo-image'
import {
- AppBskyEmbedImages,
AppBskyEmbedExternal,
+ AppBskyEmbedImages,
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
AppBskyGraphDefs,
ModerationDecision,
} from '@atproto/api'
-import {Link} from '../Link'
-import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
-import {useLightboxControls, ImagesLightbox} from '#/state/lightbox'
+
+import {ImagesLightbox, useLightboxControls} from '#/state/lightbox'
import {usePalette} from 'lib/hooks/usePalette'
-import {ExternalLinkEmbed} from './ExternalLinkEmbed'
-import {MaybeQuoteEmbed} from './QuoteEmbed'
-import {AutoSizedImage} from '../images/AutoSizedImage'
-import {ListEmbed} from './ListEmbed'
import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
import {ContentHider} from '../../../../components/moderation/ContentHider'
-import {isNative} from '#/platform/detection'
-import {shareUrl} from '#/lib/sharing'
+import {AutoSizedImage} from '../images/AutoSizedImage'
+import {ImageLayoutGrid} from '../images/ImageLayoutGrid'
+import {ExternalLinkEmbed} from './ExternalLinkEmbed'
+import {ListEmbed} from './ListEmbed'
+import {MaybeQuoteEmbed} from './QuoteEmbed'
type Embed =
| AppBskyEmbedRecord.View
@@ -49,16 +47,6 @@ export function PostEmbeds({
const pal = usePalette('default')
const {openLightbox} = useLightboxControls()
- const externalUri = AppBskyEmbedExternal.isView(embed)
- ? embed.external.uri
- : null
-
- const onShareExternal = useCallback(() => {
- if (externalUri && isNative) {
- shareUrl(externalUri)
- }
- }, [externalUri])
-
// quote post with media
// =
if (AppBskyEmbedRecordWithMedia.isView(embed)) {
@@ -161,18 +149,9 @@ export function PostEmbeds({
// =
if (AppBskyEmbedExternal.isView(embed)) {
const link = embed.external
-
return (
-
-
-
+
)
}
@@ -187,11 +166,6 @@ const styles = StyleSheet.create({
singleImage: {
borderRadius: 8,
},
- extOuter: {
- borderWidth: 1,
- borderRadius: 8,
- marginTop: 4,
- },
altContainer: {
backgroundColor: 'rgba(0, 0, 0, 0.75)',
borderRadius: 6,