diff --git a/.circleci/config.yml b/.circleci/config.yml index 8dd656f556..8f2db51e3f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -383,8 +383,6 @@ jobs: docker: - image: cimg/node:lts resource_class: large - environment: - CODECOV_TOKEN: caa771ab-3d45-4756-8e2a-e1f25996fef6 steps: - checkout @@ -403,11 +401,6 @@ jobs: command: | yarn test --runInBand - - run: - name: Codecov - command: | - yarn codecov - - save_cache: *save-npm-cache-linux # Android builds diff --git a/android/app/build.gradle b/android/app/build.gradle index f2976d8064..b634e7607f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -93,7 +93,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode VERSIONCODE as Integer - versionName "4.51.0" + versionName "4.52.0" vectorDrawables.useSupportLibrary = true if (!isFoss) { manifestPlaceholders = [BugsnagAPIKey: BugsnagAPIKey as String] diff --git a/android/app/src/main/java/chat/rocket/reactnative/share/ShareActivity.java b/android/app/src/main/java/chat/rocket/reactnative/share/ShareActivity.java deleted file mode 100644 index 366efc3b8a..0000000000 --- a/android/app/src/main/java/chat/rocket/reactnative/share/ShareActivity.java +++ /dev/null @@ -1,10 +0,0 @@ -package chat.rocket.reactnative.share; - -import com.facebook.react.ReactActivity; - -public class ShareActivity extends ReactActivity { - @Override - protected String getMainComponentName() { - return "ShareRocketChatRN"; - } -} \ No newline at end of file diff --git a/android/app/src/main/java/chat/rocket/reactnative/share/ShareActivity.kt b/android/app/src/main/java/chat/rocket/reactnative/share/ShareActivity.kt new file mode 100644 index 0000000000..40dc45832c --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/share/ShareActivity.kt @@ -0,0 +1,137 @@ +package chat.rocket.reactnative.share + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import java.io.File +import java.io.FileOutputStream +import java.util.* + +class ShareActivity : AppCompatActivity() { + + private val appScheme = "rocketchat" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + handleIntent(intent) + } + + private fun handleIntent(intent: Intent?) { + // Check if the intent contains shared content + if (intent?.action == Intent.ACTION_SEND || intent?.action == Intent.ACTION_SEND_MULTIPLE) { + when { + intent.type?.startsWith("text/") == true -> handleText(intent) + intent.type?.startsWith("image/") == true -> handleMedia(intent, "data") + intent.type?.startsWith("video/") == true -> handleMedia(intent, "data") + intent.type?.startsWith("application/") == true -> handleMedia(intent, "data") + intent.type == "*/*" -> handleMedia(intent, "data") + intent.type == "text/plain" -> handleText(intent) + else -> completeRequest() // No matching type, complete the request + } + } else { + completeRequest() // No relevant intent action, complete the request + } + } + + private fun handleText(intent: Intent) { + // Handle sharing text + val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT) + if (sharedText != null) { + val encoded = Uri.encode(sharedText) + val url = Uri.parse("$appScheme://shareextension?text=$encoded") + openURL(url) + } + completeRequest() + } + + private fun handleMedia(intent: Intent, type: String) { + val mediaUris = StringBuilder() + var valid = true + + val uris = when (intent.action) { + Intent.ACTION_SEND -> listOf(intent.getParcelableExtra(Intent.EXTRA_STREAM) as Uri?) + Intent.ACTION_SEND_MULTIPLE -> intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM) + else -> null + } + + uris?.forEachIndexed { index, uri -> + val mediaUri = uri?.let { handleMediaUri(it, type) } + if (mediaUri != null) { + mediaUris.append(mediaUri) + if (index < uris.size - 1) { + mediaUris.append(",") + } + } else { + valid = false + } + } + + if (valid) { + val encoded = Uri.encode(mediaUris.toString()) + val url = Uri.parse("$appScheme://shareextension?mediaUris=$encoded") + openURL(url) + } + completeRequest() + } + + private fun handleMediaUri(uri: Uri, type: String): String? { + return try { + val inputStream = contentResolver.openInputStream(uri) + val originalFilename = getFileName(uri) + val filename = originalFilename ?: UUID.randomUUID().toString() + getFileExtension(uri, type) + val fileUri = saveDataToCacheDir(inputStream?.readBytes(), filename) + fileUri?.toString() + } catch (e: Exception) { + Log.e("ShareRocketChat", "Failed to process media", e) + null + } + } + + private fun getFileName(uri: Uri): String? { + // Attempt to get the original filename from the Uri + val cursor = contentResolver.query(uri, null, null, null, null) + return cursor?.use { + if (it.moveToFirst()) { + val nameIndex = it.getColumnIndex("_display_name") + if (nameIndex != -1) it.getString(nameIndex) else null + } else null + } + } + + private fun getFileExtension(uri: Uri, type: String): String { + // Determine the file extension based on the mime type, with fallbacks + val mimeType = contentResolver.getType(uri) + return when { + mimeType?.startsWith("image/") == true -> ".jpeg" + mimeType?.startsWith("video/") == true -> ".mp4" + else -> "" // Ignore the file if the type is not recognized + } + } + + private fun saveDataToCacheDir(data: ByteArray?, filename: String): Uri? { + // Save the shared data to the app's cache directory and return the file URI + return try { + val file = File(cacheDir, filename) + FileOutputStream(file).use { it.write(data) } + Uri.fromFile(file) // Return the file URI with file:// scheme + } catch (e: Exception) { + Log.e("ShareRocketChat", "Failed to save data", e) + null + } + } + + private fun openURL(uri: Uri) { + // Open the custom URI in the associated app + val intent = Intent(Intent.ACTION_VIEW, uri) + if (intent.resolveActivity(packageManager) != null) { + startActivity(intent) + } + } + + private fun completeRequest() { + // Finish the share activity + finish() + } +} diff --git a/android/app/src/main/java/chat/rocket/reactnative/share/ShareApplication.java b/android/app/src/main/java/chat/rocket/reactnative/share/ShareApplication.java deleted file mode 100644 index 94cd8b8610..0000000000 --- a/android/app/src/main/java/chat/rocket/reactnative/share/ShareApplication.java +++ /dev/null @@ -1,38 +0,0 @@ -package chat.rocket.reactnative.share; - -import chat.rocket.reactnative.BuildConfig; - -import chat.rocket.SharePackage; - -import android.app.Application; - -import com.facebook.react.shell.MainReactPackage; -import com.facebook.react.ReactNativeHost; -import com.facebook.react.ReactApplication; -import com.facebook.react.ReactPackage; - -import java.util.Arrays; -import java.util.List; - - -public class ShareApplication extends Application implements ReactApplication { - private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { - @Override - public boolean getUseDeveloperSupport() { - return BuildConfig.DEBUG; - } - - @Override - protected List getPackages() { - return Arrays.asList( - new MainReactPackage(), - new SharePackage() - ); - } - }; - - @Override - public ReactNativeHost getReactNativeHost() { - return mReactNativeHost; - } -} \ No newline at end of file diff --git a/app.json b/app.json index 5b802fd60f..5d8e300850 100644 --- a/app.json +++ b/app.json @@ -1,5 +1,3 @@ { - "name": "RocketChatRN", - "share": "ShareRocketChatRN", - "displayName": "RocketChatRN" + "name": "RocketChatRN" } diff --git a/app/AppContainer.tsx b/app/AppContainer.tsx index a1e99822c8..bfd1a8cc66 100644 --- a/app/AppContainer.tsx +++ b/app/AppContainer.tsx @@ -14,6 +14,7 @@ import SetUsernameView from './views/SetUsernameView'; import OutsideStack from './stacks/OutsideStack'; import InsideStack from './stacks/InsideStack'; import MasterDetailStack from './stacks/MasterDetailStack'; +import ShareExtensionStack from './stacks/ShareExtensionStack'; import { ThemeContext } from './theme'; import { setCurrentScreen } from './lib/methods/helpers/log'; @@ -57,13 +58,18 @@ const App = memo(({ root, isMasterDetail }: { root: string; isMasterDetail: bool Navigation.routeNameRef.current = currentRouteName; }}> - {root === RootEnum.ROOT_LOADING ? : null} + {root === RootEnum.ROOT_LOADING || root === RootEnum.ROOT_LOADING_SHARE_EXTENSION ? ( + + ) : null} {root === RootEnum.ROOT_OUTSIDE ? : null} {root === RootEnum.ROOT_INSIDE && isMasterDetail ? ( ) : null} {root === RootEnum.ROOT_INSIDE && !isMasterDetail ? : null} {root === RootEnum.ROOT_SET_USERNAME ? : null} + {root === RootEnum.ROOT_SHARE_EXTENSION ? ( + + ) : null} ); diff --git a/app/actions/actionsTypes.ts b/app/actions/actionsTypes.ts index fe6fb521e8..af1f261b3e 100644 --- a/app/actions/actionsTypes.ts +++ b/app/actions/actionsTypes.ts @@ -10,7 +10,7 @@ function createRequestTypes(base = {}, types = defaultTypes): Record { - const server = useSelector((state: IApplicationState) => state.share.server.server || state.server.server); - const serverVersion = useSelector((state: IApplicationState) => state.share.server.version || state.server.version); + const server = useSelector((state: IApplicationState) => state.server.server); + const serverVersion = useSelector((state: IApplicationState) => state.server.version); const { id, token, username } = useSelector( (state: IApplicationState) => ({ id: getUserSelector(state).id, @@ -38,11 +38,8 @@ const AvatarContainer = ({ cdnPrefix: state.settings.CDN_PREFIX as string })); const blockUnauthenticatedAccess = useSelector( - (state: IApplicationState) => - (state.share.settings?.Accounts_AvatarBlockUnauthenticatedAccess as boolean) ?? - state.settings.Accounts_AvatarBlockUnauthenticatedAccess ?? - true - ); + (state: IApplicationState) => state.settings.Accounts_AvatarBlockUnauthenticatedAccess ?? true + ) as boolean; const { avatarETag } = useAvatarETag({ username, text, type, rid, id }); diff --git a/app/containers/Button/Button.stories.tsx b/app/containers/Button/Button.stories.tsx index d58fcdbe14..2ecf791037 100644 --- a/app/containers/Button/Button.stories.tsx +++ b/app/containers/Button/Button.stories.tsx @@ -4,7 +4,7 @@ import Button from '.'; const buttonProps = { title: 'Press me!', - type: 'primary', + type: 'primary' as const, onPress: () => {}, testID: 'testButton' }; diff --git a/app/containers/Button/Button.test.tsx b/app/containers/Button/Button.test.tsx index 6d89faa452..9fd76940fa 100644 --- a/app/containers/Button/Button.test.tsx +++ b/app/containers/Button/Button.test.tsx @@ -8,7 +8,7 @@ const onPressMock = jest.fn(); const testProps = { title: 'Press me!', - type: 'primary', + type: 'primary' as const, onPress: onPressMock, testID: 'testButton', initialText: 'Initial text', @@ -19,7 +19,7 @@ const TestButton = ({ loading = false, disabled = false }) => (