Skip to content

Commit

Permalink
Merge branch 'main' into Improve-notification-localization
Browse files Browse the repository at this point in the history
  • Loading branch information
quiple authored Sep 28, 2024
2 parents 2c4a349 + 587c0c6 commit 19a7c85
Show file tree
Hide file tree
Showing 80 changed files with 803 additions and 2,243 deletions.
15 changes: 0 additions & 15 deletions jest/jestSetup.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,6 @@ jest.mock('expo-image-manipulator', () => ({
SaveFormat: jest.requireActual('expo-image-manipulator').SaveFormat,
}))

jest.mock('@segment/analytics-react-native', () => ({
createClient: () => ({
add: jest.fn(),
}),
useAnalytics: () => ({
track: jest.fn(),
identify: jest.fn(),
reset: jest.fn(),
group: jest.fn(),
screen: jest.fn(),
alias: jest.fn(),
flush: jest.fn(),
}),
}))

jest.mock('expo-camera', () => ({
Camera: {
useCameraPermissions: jest.fn(() => [true]),
Expand Down
42 changes: 39 additions & 3 deletions modules/Share-with-Bluesky/ShareViewController.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import UIKit

let IMAGE_EXTENSIONS: [String] = ["png", "jpg", "jpeg", "gif", "heic"]
let MOVIE_EXTENSIONS: [String] = ["mov", "mp4", "m4v"]

enum URLType: String, CaseIterable {
case image
case movie
case other
}

class ShareViewController: UIViewController {
// This allows other forks to use this extension while also changing their
// scheme.
Expand Down Expand Up @@ -43,9 +52,18 @@ class ShareViewController: UIViewController {

private func handleUrl(item: NSItemProvider) async {
if let data = try? await item.loadItem(forTypeIdentifier: "public.url") as? URL {
if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") {
_ = self.openURL(url)
switch data.type {
case .image:
await handleImages(items: [item])
return
case .movie:
await handleVideos(items: [item])
return
case .other:
if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") {
_ = self.openURL(url)
}
}
}
self.completeRequest()
Expand Down Expand Up @@ -158,3 +176,21 @@ class ShareViewController: UIViewController {
return false
}
}

extension URL {
var type: URLType {
get {
guard self.absoluteString.starts(with: "file://"),
let ext = self.pathComponents.last?.split(separator: ".").last?.lowercased() else {
return .other
}

if IMAGE_EXTENSIONS.contains(ext) {
return .image
} else if MOVIE_EXTENSIONS.contains(ext) {
return .movie
}
return .other
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import java.io.File
import java.io.FileOutputStream
import java.net.URLEncoder

enum class AttachmentType {
IMAGE,
VIDEO,
}

class ExpoReceiveAndroidIntentsModule : Module() {
override fun definition() =
ModuleDefinition {
Expand All @@ -23,17 +28,26 @@ class ExpoReceiveAndroidIntentsModule : Module() {
}

private fun handleIntent(intent: Intent?) {
if (appContext.currentActivity == null || intent == null) return

if (intent.action == Intent.ACTION_SEND) {
if (intent.type == "text/plain") {
handleTextIntent(intent)
} else if (intent.type.toString().startsWith("image/")) {
handleImageIntent(intent)
if (appContext.currentActivity == null) return
intent?.let {
if (it.action == Intent.ACTION_SEND && it.type == "text/plain") {
handleTextIntent(it)
return
}
} else if (intent.action == Intent.ACTION_SEND_MULTIPLE) {
if (intent.type.toString().startsWith("image/")) {
handleImagesIntent(intent)

val type =
if (it.type.toString().startsWith("image/")) {
AttachmentType.IMAGE
} else if (it.type.toString().startsWith("video/")) {
AttachmentType.VIDEO
} else {
return
}

if (it.action == Intent.ACTION_SEND) {
handleAttachmentIntent(it, type)
} else if (it.action == Intent.ACTION_SEND_MULTIPLE) {
handleAttachmentsIntent(it, type)
}
}
}
Expand All @@ -48,26 +62,46 @@ class ExpoReceiveAndroidIntentsModule : Module() {
}
}

private fun handleImageIntent(intent: Intent) {
private fun handleAttachmentIntent(
intent: Intent,
type: AttachmentType,
) {
val uri =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
} else {
intent.getParcelableExtra(Intent.EXTRA_STREAM)
}
if (uri == null) return

handleImageIntents(listOf(uri))
uri?.let {
when (type) {
AttachmentType.IMAGE -> handleImageIntents(listOf(it))
AttachmentType.VIDEO -> handleVideoIntents(listOf(it))
}
}
}

private fun handleImagesIntent(intent: Intent) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)?.let {
handleImageIntents(it.filterIsInstance<Uri>().take(4))
private fun handleAttachmentsIntent(
intent: Intent,
type: AttachmentType,
) {
val uris =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent
.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)
?.filterIsInstance<Uri>()
?.take(4)
} else {
intent
.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
?.filterIsInstance<Uri>()
?.take(4)
}
} else {
intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)?.let {
handleImageIntents(it.filterIsInstance<Uri>().take(4))

uris?.let {
when (type) {
AttachmentType.IMAGE -> handleImageIntents(it)
else -> return
}
}
}
Expand All @@ -93,11 +127,33 @@ class ExpoReceiveAndroidIntentsModule : Module() {
}
}

private fun handleVideoIntents(uris: List<Uri>) {
val uri = uris[0]
// If there is no extension for the file, substringAfterLast returns the original string - not
// null, so we check for that below
// It doesn't actually matter what the extension is, so defaulting to mp4 is fine, even if the
// video isn't actually an mp4
var extension = uri.path?.substringAfterLast(".")
if (extension == null || extension == uri.path) {
extension = "mp4"
}
val file = createFile(extension)

val out = FileOutputStream(file)
appContext.currentActivity?.contentResolver?.openInputStream(uri)?.use {
it.copyTo(out)
}
"bluesky://intent/compose?videoUri=${URLEncoder.encode(file.path, "UTF-8")}".toUri().let {
val newIntent = Intent(Intent.ACTION_VIEW, it)
appContext.currentActivity?.startActivity(newIntent)
}
}

private fun getImageInfo(uri: Uri): Map<String, Any> {
val bitmap = MediaStore.Images.Media.getBitmap(appContext.currentActivity?.contentResolver, uri)
// We have to save this so that we can access it later when uploading the image.
// createTempFile will automatically place a unique string between "img" and "temp.jpeg"
val file = File.createTempFile("img", "temp.jpeg", appContext.currentActivity?.cacheDir)
val file = createFile("jpeg")
val out = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
out.flush()
Expand All @@ -110,6 +166,8 @@ class ExpoReceiveAndroidIntentsModule : Module() {
)
}

private fun createFile(extension: String): File = File.createTempFile(extension, "temp.$extension", appContext.currentActivity?.cacheDir)

// We will pas the width and height to the app here, since getting measurements
// on the RN side is a bit more involved, and we already have them here anyway.
private fun buildUriData(info: Map<String, Any>): String {
Expand Down
6 changes: 1 addition & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"icons:optimize": "svgo -f ./assets/icons"
},
"dependencies": {
"@atproto/api": "^0.13.7",
"@atproto/api": "^0.13.8",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
"@emoji-mart/react": "^1.1.1",
Expand Down Expand Up @@ -81,10 +81,6 @@
"@react-navigation/drawer": "^6.6.15",
"@react-navigation/native": "^6.1.17",
"@react-navigation/native-stack": "^6.9.26",
"@segment/analytics-next": "^1.51.3",
"@segment/analytics-react": "^1.0.0-rc1",
"@segment/analytics-react-native": "^2.10.1",
"@segment/sovran-react-native": "^0.4.5",
"@sentry/react-native": "5.32.0",
"@tamagui/focus-scope": "^1.84.1",
"@tanstack/query-async-storage-persister": "^5.25.0",
Expand Down
23 changes: 23 additions & 0 deletions plugins/shareExtension/withIntentFilters.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,29 @@ const withIntentFilters = config => {
},
],
},
{
action: [
{
$: {
'android:name': 'android.intent.action.SEND',
},
},
],
category: [
{
$: {
'android:name': 'android.intent.category.DEFAULT',
},
},
],
data: [
{
$: {
'android:mimeType': 'video/*',
},
},
],
},
{
action: [
{
Expand Down
3 changes: 0 additions & 3 deletions src/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
StackActions,
} from '@react-navigation/native'

import {init as initAnalytics} from '#/lib/analytics/analytics'
import {timeout} from '#/lib/async/timeout'
import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle'
import {usePalette} from '#/lib/hooks/usePalette'
Expand Down Expand Up @@ -647,8 +646,6 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {

function onReady() {
prevLoggedRouteName.current = getCurrentRouteName()
initAnalytics(currentAccount)

if (currentAccount && shouldRequestEmailConfirmation(currentAccount)) {
openModal({name: 'verify-email', showReminder: true})
snoozeEmailConfirmationPrompt()
Expand Down
17 changes: 11 additions & 6 deletions src/components/StarterPack/QrCode.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import React from 'react'
import {View} from 'react-native'
import QRCode from 'react-native-qrcode-styled'
import ViewShot from 'react-native-view-shot'
import type ViewShot from 'react-native-view-shot'
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
import {Trans} from '@lingui/macro'

import {isWeb} from 'platform/detection'
import {Logo} from 'view/icons/Logo'
import {Logotype} from 'view/icons/Logotype'
import {isWeb} from '#/platform/detection'
import {Logo} from '#/view/icons/Logo'
import {Logotype} from '#/view/icons/Logotype'
import {useTheme} from '#/alf'
import {atoms as a} from '#/alf'
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
import {Text} from '#/components/Typography'

const LazyViewShot = React.lazy(
// @ts-expect-error dynamic import
() => import('react-native-view-shot/src/index'),
)

interface Props {
starterPack: AppBskyGraphDefs.StarterPackView
link: string
Expand All @@ -29,7 +34,7 @@ export const QrCode = React.forwardRef<ViewShot, Props>(function QrCode(
}

return (
<ViewShot ref={ref}>
<LazyViewShot ref={ref}>
<LinearGradientBackground
style={[
{width: 300, minHeight: 390},
Expand Down Expand Up @@ -79,7 +84,7 @@ export const QrCode = React.forwardRef<ViewShot, Props>(function QrCode(
</Text>
</View>
</LinearGradientBackground>
</ViewShot>
</LazyViewShot>
)
})

Expand Down
Loading

0 comments on commit 19a7c85

Please sign in to comment.