From b8566eba5ef936f5455830d761eec44d55447a14 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Tue, 24 Feb 2026 14:23:23 +0100 Subject: [PATCH 01/15] feat: proof of concept of postMessage API --- .../android/example/ReactNativeConstants.kt | 3 +- .../android/example/MainActivity.kt | 61 +---- .../example/components/GreetingCard.kt | 51 ++++ .../example/components/MaterialCard.kt | 22 ++ .../example/components/PostMessageCard.kt | 96 ++++++++ .../android/example/ReactNativeConstants.kt | 1 + .../BrownfieldAppleApp.swift | 22 +- .../Brownfield Apple App/ContentView.swift | 105 +++++++- apps/ExpoApp/app/(tabs)/_layout.tsx | 9 + apps/ExpoApp/app/(tabs)/index.tsx | 140 +++++------ apps/ExpoApp/app/(tabs)/postMessage.tsx | 105 ++++++++ .../ExpoApp/components/postMessage/Message.ts | 6 + .../components/postMessage/MessageBubble.tsx | 74 ++++++ apps/ExpoApp/components/ui/icon-symbol.tsx | 1 + apps/ExpoApp/package.json | 8 +- apps/RNApp/ios/Podfile.lock | 10 +- apps/RNApp/src/HomeScreen.tsx | 226 ++++++++++++++++-- .../callstack/kotlinexample/MainActivity.kt | 128 +++++++++- apps/TesterIntegrated/swift/App.swift | 132 ++++++++-- .../react-native-brownfield/java.mdx | 55 +++++ .../react-native-brownfield/javascript.mdx | 86 ++++++- .../react-native-brownfield/kotlin.mdx | 55 +++++ .../react-native-brownfield/objective-c.mdx | 39 +++ .../react-native-brownfield/swift.mdx | 51 +++- .../ReactNativeBrownfield.kt | 34 +++ .../newarch/ReactNativeBrownfieldModule.kt | 11 + .../oldarch/ReactNativeBrownfieldModule.kt | 11 + .../ios/Notification+Brownfield.swift | 8 + .../ios/ReactNativeBrownfield.swift | 35 +++ .../ios/ReactNativeBrownfieldModule.mm | 33 +++ .../ios/ReactNativeBrownfieldModule.swift | 7 + .../src/NativeReactNativeBrownfieldModule.ts | 15 ++ packages/react-native-brownfield/src/index.ts | 29 ++- 33 files changed, 1458 insertions(+), 211 deletions(-) create mode 100644 apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/GreetingCard.kt create mode 100644 apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/MaterialCard.kt create mode 100644 apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/PostMessageCard.kt create mode 100644 apps/ExpoApp/app/(tabs)/postMessage.tsx create mode 100644 apps/ExpoApp/components/postMessage/Message.ts create mode 100644 apps/ExpoApp/components/postMessage/MessageBubble.tsx diff --git a/apps/AndroidApp/app/src/expo/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt b/apps/AndroidApp/app/src/expo/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt index 2d9aba0b..da2e6d93 100644 --- a/apps/AndroidApp/app/src/expo/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt +++ b/apps/AndroidApp/app/src/expo/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt @@ -1,5 +1,6 @@ package com.callstack.brownfield.android.example object ReactNativeConstants { - const val MAIN_MODULE_NAME = "RNApp" + const val MAIN_MODULE_NAME = "main" + const val APP_NAME = "Android (Expo)" } diff --git a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt index 4143acf2..1d265bc0 100644 --- a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt +++ b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/MainActivity.kt @@ -9,28 +9,22 @@ import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.fragment.compose.AndroidFragment +import com.callstack.brownfield.android.example.components.GreetingCard +import com.callstack.brownfield.android.example.components.PostMessageCard import com.callstack.brownfield.android.example.ui.theme.AndroidBrownfieldAppTheme import com.callstack.reactnativebrownfield.ReactNativeFragment import com.callstack.reactnativebrownfield.constants.ReactNativeFragmentArgNames @@ -81,57 +75,22 @@ private fun MainScreen(modifier: Modifier = Modifier) { horizontalAlignment = Alignment.CenterHorizontally // center top bar content ) { GreetingCard( - name = "Android", - modifier = Modifier.fillMaxWidth() + name = ReactNativeConstants.APP_NAME, ) + PostMessageCard() + + Spacer(modifier = Modifier.height(1.dp)) + ReactNativeView( modifier = Modifier - .fillMaxWidth() + .fillMaxSize() .clip(RoundedCornerShape(16.dp)) .background(MaterialTheme.colorScheme.surface) ) } } -@Composable -fun GreetingCard( - name: String, - modifier: Modifier = Modifier -) { - var counter by rememberSaveable { mutableStateOf(0) } - - Card( - modifier = modifier, - shape = RoundedCornerShape(16.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) - ) { - Column( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - text = "Hello native $name 👋", - style = MaterialTheme.typography.titleMedium, - textAlign = TextAlign.Center - ) - - Text( - text = "You clicked the button $counter time${if (counter == 1) "" else "s"}", - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium - ) - - Button(onClick = { counter++ }) { - Text("Increment counter") - } - } - } -} - @Composable fun ReactNativeView( modifier: Modifier = Modifier diff --git a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/GreetingCard.kt b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/GreetingCard.kt new file mode 100644 index 00000000..06f4b81d --- /dev/null +++ b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/GreetingCard.kt @@ -0,0 +1,51 @@ +package com.callstack.brownfield.android.example.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +@Composable +fun GreetingCard( + name: String, +) { + var counter by rememberSaveable { mutableIntStateOf(0) } + + MaterialCard { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = "Hello native $name 👋", + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center + ) + + Text( + text = "You clicked the button $counter time${if (counter == 1) "" else "s"}", + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium + ) + + Button(onClick = { counter++ }) { + Text("Increment counter") + } + } + } +} \ No newline at end of file diff --git a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/MaterialCard.kt b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/MaterialCard.kt new file mode 100644 index 00000000..2dc660e7 --- /dev/null +++ b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/MaterialCard.kt @@ -0,0 +1,22 @@ +package com.callstack.brownfield.android.example.components + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun MaterialCard( + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Card( + modifier = modifier, + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) + ) { + content() + } +} diff --git a/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/PostMessageCard.kt b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/PostMessageCard.kt new file mode 100644 index 00000000..fe405e5d --- /dev/null +++ b/apps/AndroidApp/app/src/main/java/com/callstack/brownfield/android/example/components/PostMessageCard.kt @@ -0,0 +1,96 @@ +package com.callstack.brownfield.android.example.components + +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import com.callstack.reactnativebrownfield.OnMessageListener +import com.callstack.reactnativebrownfield.ReactNativeBrownfield +import org.json.JSONObject + +@Composable +fun PostMessageCard() { + var nextId by remember { mutableIntStateOf(0) } + var draft by remember { mutableStateOf("") } + val lastToast = remember { mutableStateOf(null) } + + val context = LocalContext.current + + DisposableEffect(Unit) { + val listener = OnMessageListener { raw -> + val text = try { + JSONObject(raw).optString("text", raw) + } catch (_: Exception) { + raw + } + val toast = Toast.makeText( + context, + "Received message from React Native: $text", + Toast.LENGTH_LONG + ) + lastToast.value?.cancel() // cancel previous toast if still visible + toast.show() + lastToast.value = toast + } + ReactNativeBrownfield.shared.addMessageListener(listener) + onDispose { ReactNativeBrownfield.shared.removeMessageListener(listener) } + } + + MaterialCard { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + "postMessage", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .padding(bottom = 2.dp) + .align(Alignment.CenterHorizontally) + ) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + + OutlinedTextField( + value = draft, + onValueChange = { draft = it }, + modifier = Modifier.weight(1f), + placeholder = { Text("Type a message...") }, + singleLine = true, + ) + Button(onClick = { + val text = draft.ifBlank { "Hello from Android! (#${nextId++})" } + val json = JSONObject().put("text", text).toString() + ReactNativeBrownfield.shared.postMessage(json) + draft = "" + }) { + Text("Send") + } + } + } + } +} diff --git a/apps/AndroidApp/app/src/vanilla/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt b/apps/AndroidApp/app/src/vanilla/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt index 2d9aba0b..a353f186 100644 --- a/apps/AndroidApp/app/src/vanilla/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt +++ b/apps/AndroidApp/app/src/vanilla/java/com/callstack/brownfield/android/example/ReactNativeConstants.kt @@ -2,4 +2,5 @@ package com.callstack.brownfield.android.example object ReactNativeConstants { const val MAIN_MODULE_NAME = "RNApp" + const val APP_NAME = "Android" } diff --git a/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift b/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift index f4d35f78..f4eab6a8 100644 --- a/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift +++ b/apps/AppleApp/Brownfield Apple App/BrownfieldAppleApp.swift @@ -1,36 +1,40 @@ import BrownfieldLib import Brownie -import SwiftUI import ReactBrownfield +import SwiftUI class AppDelegate: NSObject, UIApplicationDelegate { var window: UIWindow? - + func application( _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + didFinishLaunchingWithOptions launchOptions: [UIApplication + .LaunchOptionsKey: Any]? = nil ) -> Bool { - return ReactNativeBrownfield.shared.application(application, didFinishLaunchingWithOptions: launchOptions) + return ReactNativeBrownfield.shared.application( + application, + didFinishLaunchingWithOptions: launchOptions + ) } } @main struct BrownfieldAppleApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - + init() { ReactNativeBrownfield.shared.bundle = ReactNativeBundle ReactNativeBrownfield.shared.startReactNative { print("React Native has been loaded") } -#if USE_EXPO_HOST - ReactNativeBrownfield.shared.ensureExpoModulesProvider() -#endif + #if USE_EXPO_HOST + ReactNativeBrownfield.shared.ensureExpoModulesProvider() + #endif BrownfieldStore.register(initialState) } - + var body: some Scene { WindowGroup { ContentView() diff --git a/apps/AppleApp/Brownfield Apple App/ContentView.swift b/apps/AppleApp/Brownfield Apple App/ContentView.swift index 4be7c44c..e584ce47 100644 --- a/apps/AppleApp/Brownfield Apple App/ContentView.swift +++ b/apps/AppleApp/Brownfield Apple App/ContentView.swift @@ -1,6 +1,13 @@ import ReactBrownfield import Brownie import SwiftUI +import UIKit + +struct ChatMessage: Identifiable { + let id: Int + let text: String + let fromRN: Bool +} let initialState = BrownfieldStore( counter: 0, @@ -18,16 +25,21 @@ struct ContentView: View { struct MainScreen: View { var body: some View { - VStack(spacing: 16) { - GreetingCard(name: "iOS") - - ReactNativeView(moduleName: "RNApp") - .navigationBarHidden(true) - .clipShape(RoundedRectangle(cornerRadius: 16)) - .background(Color(UIColor.systemBackground)) + ScrollView { + VStack(spacing: 16) { + GreetingCard(name: "iOS") + + ReactNativeView(moduleName: "RNApp") + .navigationBarHidden(true) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .background(Color(UIColor.systemBackground)) + + Spacer(minLength: 16) + + MessagesView() + } + .frame(maxWidth: .infinity) } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding() } } @@ -60,6 +72,81 @@ struct GreetingCard: View { } } +struct MessagesView: View { + @State private var messages: [ChatMessage] = [] + @State private var draft: String = "" + @State private var nextId: Int = 0 + @State private var observer: NSObjectProtocol? + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text("postMessage") + .font(.headline) + .frame(maxWidth: .infinity, alignment: .center) + + HStack { + TextField("Type a message...", text: $draft) + .textFieldStyle(.roundedBorder) + + Button("Send") { + let text = draft.isEmpty ? "Hello from iOS! (#\(nextId))" : draft + let json = "{\"text\":\"\(text)\"}" + ReactNativeBrownfield.shared.postMessage(json) + withAnimation(.spring(response: 0.35, dampingFraction: 0.7)) { + messages.insert(ChatMessage(id: nextId, text: text, fromRN: false), at: 0) + nextId += 1 + } + draft = "" + } + .buttonStyle(.borderedProminent) + } + + ForEach(messages) { msg in + HStack { + if !msg.fromRN { Spacer() } + VStack(alignment: msg.fromRN ? .leading : .trailing, spacing: 2) { + Text(msg.fromRN ? "From React Native" : "Sent") + .font(.caption2) + .foregroundColor(.secondary) + Text(msg.text) + .font(.body) + } + .padding(10) + .background(msg.fromRN ? Color(.systemGray5) : Color.accentColor.opacity(0.15)) + .cornerRadius(12) + .frame(maxWidth: 260, alignment: msg.fromRN ? .leading : .trailing) + if msg.fromRN { Spacer() } + } + .transition(.asymmetric( + insertion: .move(edge: .top).combined(with: .opacity), + removal: .opacity + )) + } + } + .padding() + .onAppear { + observer = ReactNativeBrownfield.shared.onMessage { raw in + var text = raw + if let data = raw.data(using: .utf8), + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let t = json["text"] as? String { + text = t + } + withAnimation(.spring(response: 0.35, dampingFraction: 0.7)) { + messages.insert(ChatMessage(id: nextId, text: text, fromRN: true), at: 0) + nextId += 1 + } + } + } + .onDisappear { + if let obs = observer { + NotificationCenter.default.removeObserver(obs) + observer = nil + } + } + } +} + #Preview { ContentView() } diff --git a/apps/ExpoApp/app/(tabs)/_layout.tsx b/apps/ExpoApp/app/(tabs)/_layout.tsx index cd8f4050..ce946ec7 100644 --- a/apps/ExpoApp/app/(tabs)/_layout.tsx +++ b/apps/ExpoApp/app/(tabs)/_layout.tsx @@ -35,6 +35,15 @@ export default function TabLayout() { ), }} /> + ( + + ), + }} + /> ); } diff --git a/apps/ExpoApp/app/(tabs)/index.tsx b/apps/ExpoApp/app/(tabs)/index.tsx index 7242543e..ba27c8fd 100644 --- a/apps/ExpoApp/app/(tabs)/index.tsx +++ b/apps/ExpoApp/app/(tabs)/index.tsx @@ -1,6 +1,6 @@ import { Image } from 'expo-image'; import { Link } from 'expo-router'; -import { Platform, StyleSheet } from 'react-native'; +import { Platform, StyleSheet, View } from 'react-native'; import { HelloWave } from '@/components/hello-wave'; import ParallaxScrollView from '@/components/parallax-scroll-view'; @@ -9,77 +9,79 @@ import { ThemedView } from '@/components/themed-view'; export default function HomeScreen() { return ( - - } - > - - Welcome! - - - - Step 1: Try it - - Edit{' '} - app/(tabs)/index.tsx{' '} - to see changes. Press{' '} - - {Platform.select({ - ios: 'cmd + d', - android: 'cmd + m', - web: 'F12', - })} - {' '} - to open developer tools. - - - - - - Step 2: Explore - - - - alert('Action pressed')} - /> - alert('Share pressed')} - /> - + + + } + > + + Welcome! + + + + Step 1: Try it + + Edit{' '} + app/(tabs)/index.tsx{' '} + to see changes. Press{' '} + + {Platform.select({ + ios: 'cmd + d', + android: 'cmd + m', + web: 'F12', + })} + {' '} + to open developer tools. + + + + + + Step 2: Explore + + + alert('Delete pressed')} + title="Action" + icon="cube" + onPress={() => alert('Action pressed')} /> + alert('Share pressed')} + /> + + alert('Delete pressed')} + /> + - - - - - Step 3: Get a fresh start - - {`When you're ready, run `} - - npm run reset-project - {' '} - to get a fresh app{' '} - directory. This will move the current{' '} - app to{' '} - app-example. - - - + + + + Step 3: Get a fresh start + + {`When you're ready, run `} + + npm run reset-project + {' '} + to get a fresh app{' '} + directory. This will move the current{' '} + app to{' '} + app-example. + + + + ); } diff --git a/apps/ExpoApp/app/(tabs)/postMessage.tsx b/apps/ExpoApp/app/(tabs)/postMessage.tsx new file mode 100644 index 00000000..4910c934 --- /dev/null +++ b/apps/ExpoApp/app/(tabs)/postMessage.tsx @@ -0,0 +1,105 @@ +import { StyleSheet, FlatList, TouchableOpacity } from 'react-native'; + +import { useCallback, useEffect, useRef, useState } from 'react'; +import ReactNativeBrownfield from '@callstack/react-native-brownfield'; +import type { MessageEvent } from '@callstack/react-native-brownfield'; + +import { ThemedView } from '@/components/themed-view'; +import { ThemedText } from '@/components/themed-text'; +import type { Message } from '@/components/postMessage/Message'; +import { MessageBubble } from '@/components/postMessage/MessageBubble'; + +export default function HomeScreen() { + const [messages, setMessages] = useState([]); + const flatListRef = useRef>(null); + + const messageCounterRef = useRef(0); + + useEffect(() => { + const sub = ReactNativeBrownfield.onMessage((event: MessageEvent) => { + const data = event.data as { text?: string }; + setMessages((prev) => [ + ...prev, + { + id: String(++messageCounterRef.current), + text: data?.text ?? JSON.stringify(event.data), + from: 'native', + timestamp: Date.now(), + }, + ]); + }); + return () => sub.remove(); + }, []); + + const sendMessage = useCallback(() => { + const msg = { + text: `Hello from Expo! (#${++messageCounterRef.current})`, + timestamp: Date.now(), + }; + ReactNativeBrownfield.postMessage(msg); + setMessages((prev) => [ + ...prev, + { + id: String(messageCounterRef.current), + text: msg.text, + from: 'rn', + timestamp: msg.timestamp, + }, + ]); + }, []); + + return ( + + + + Send message to Native + + + + `message-${item.id}`} + renderItem={({ item }) => } + style={styles.messageList} + contentContainerStyle={styles.messageListContent} + inverted={true} // ensure newest messages are at the top + onContentSizeChange={() => { + flatListRef.current?.scrollToEnd({ animated: true }); + }} + ref={flatListRef} + /> + + ); +} + +const styles = StyleSheet.create({ + messageSection: { + flex: 1, + width: '100%', + padding: 12, + paddingHorizontal: 20, + }, + sendButton: { + paddingVertical: 12, + paddingHorizontal: 20, + borderRadius: 10, + alignItems: 'center', + marginBottom: 10, + backgroundColor: '#4F8EF7', + }, + sendButtonText: { + fontWeight: '700', + fontSize: 15, + color: '#fff', + }, + messageList: { + flex: 1, + }, + messageListContent: { + paddingBottom: 8, + }, +}); diff --git a/apps/ExpoApp/components/postMessage/Message.ts b/apps/ExpoApp/components/postMessage/Message.ts new file mode 100644 index 00000000..35e5f1d1 --- /dev/null +++ b/apps/ExpoApp/components/postMessage/Message.ts @@ -0,0 +1,6 @@ +export interface Message { + id: string; + text: string; + from: 'native' | 'rn'; + timestamp: number; +} diff --git a/apps/ExpoApp/components/postMessage/MessageBubble.tsx b/apps/ExpoApp/components/postMessage/MessageBubble.tsx new file mode 100644 index 00000000..b9b86102 --- /dev/null +++ b/apps/ExpoApp/components/postMessage/MessageBubble.tsx @@ -0,0 +1,74 @@ +import { Animated, StyleSheet } from 'react-native'; +import { Message } from './Message'; +import { useEffect, useRef } from 'react'; +import { ThemedText } from '@/components/themed-text'; + +export function MessageBubble({ item }: { item: Message }) { + const opacity = useRef(new Animated.Value(0)).current; + const translateY = useRef(new Animated.Value(20)).current; + + useEffect(() => { + Animated.parallel([ + Animated.timing(opacity, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }), + Animated.spring(translateY, { + toValue: 0, + tension: 80, + friction: 10, + useNativeDriver: true, + }), + ]).start(); + }, [opacity, translateY]); + + const isFromNative = item.from === 'native'; + + return ( + + + {isFromNative ? 'From Native' : 'From RN'} + + {item.text} + + ); +} + +const styles = StyleSheet.create({ + bubble: { + padding: 10, + borderRadius: 10, + borderWidth: 1, + marginBottom: 6, + }, + bubbleNative: { + alignSelf: 'flex-start', + backgroundColor: 'rgba(79, 142, 247, 0.1)', + maxWidth: '80%', + }, + bubbleRN: { + alignSelf: 'flex-end', + backgroundColor: 'rgba(218, 165, 32, 0.1)', + maxWidth: '80%', + }, + bubbleLabel: { + fontSize: 10, + fontWeight: '600', + marginBottom: 2, + textTransform: 'uppercase', + }, + bubbleText: { + fontSize: 14, + }, +}); diff --git a/apps/ExpoApp/components/ui/icon-symbol.tsx b/apps/ExpoApp/components/ui/icon-symbol.tsx index 5aa32001..440fab60 100644 --- a/apps/ExpoApp/components/ui/icon-symbol.tsx +++ b/apps/ExpoApp/components/ui/icon-symbol.tsx @@ -22,6 +22,7 @@ const MAPPING = { 'chevron.left.forwardslash.chevron.right': 'code', 'chevron.right': 'chevron-right', plus: 'add', + 'message.fill': 'message', } as IconMapping; /** diff --git a/apps/ExpoApp/package.json b/apps/ExpoApp/package.json index 4b2a56e8..9244f052 100644 --- a/apps/ExpoApp/package.json +++ b/apps/ExpoApp/package.json @@ -19,13 +19,13 @@ "@callstack/brownie": "workspace:^", "@callstack/react-native-brownfield": "workspace:^", "@expo/vector-icons": "^15.0.3", - "expo": "~54.0.31", + "expo": "~54.0.33", "expo-constants": "~18.0.13", - "expo-font": "~14.0.10", + "expo-font": "~14.0.11", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", "expo-linking": "~8.0.11", - "expo-router": "~6.0.21", + "expo-router": "~6.0.23", "expo-splash-screen": "~31.0.13", "expo-status-bar": "~3.0.9", "expo-symbols": "~1.0.8", @@ -40,7 +40,7 @@ "react-native-worklets": "0.5.1" }, "devDependencies": { - "@types/react": "~19.1.0", + "@types/react": "~19.1.10", "eslint": "^9.25.0", "eslint-config-expo": "~10.0.0", "typescript": "~5.9.2" diff --git a/apps/RNApp/ios/Podfile.lock b/apps/RNApp/ios/Podfile.lock index 238047ad..00a30127 100644 --- a/apps/RNApp/ios/Podfile.lock +++ b/apps/RNApp/ios/Podfile.lock @@ -1,6 +1,6 @@ PODS: - boost (1.84.0) - - Brownie (0.0.5): + - Brownie (3.0.0-rc.2): - boost - DoubleConversion - fast_float @@ -2350,7 +2350,7 @@ PODS: - SocketRocket - ReactAppDependencyProvider (0.82.1): - ReactCodegen - - ReactBrownfield (3.0.0-rc.1): + - ReactBrownfield (3.0.0-rc.2): - boost - DoubleConversion - fast_float @@ -2772,7 +2772,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - Brownie: e4291b884a7157a2dbe6d60dd72e962078223791 + Brownie: 062c63b8aff0eedae128008f2375478fd86fd928 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 FBLazyVector: 0aa6183b9afe3c31fc65b5d1eeef1f3c19b63bfa @@ -2843,12 +2843,12 @@ SPEC CHECKSUMS: React-utils: f06ff240e06e2bd4b34e48f1b34cac00866e8979 React-webperformancenativemodule: b3398f8175fa96d992c071b1fa59bd6f9646b840 ReactAppDependencyProvider: a45ef34bb22dc1c9b2ac1f74167d9a28af961176 - ReactBrownfield: ce231a9060b34e1fe8f91ec8416f21dc6da8b4b5 + ReactBrownfield: 91f5dde821a05d473b97fba634a7d401a697b9d1 ReactCodegen: 0bce2d209e2e802589f4c5ff76d21618200e74cb ReactCommon: 801eff8cb9c940c04d3a89ce399c343ee3eff654 RNScreens: d6413aeb1878cdafd3c721e2c5218faf5d5d3b13 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - Yoga: 526f25666395d30c297d53154398ffd249eaf9e1 + Yoga: 46ff53afcbeda2bae19c85b65e17487c3e3984dd PODFILE CHECKSUM: 7c116a16dd0744063c8c6293dbfc638c9d447c19 diff --git a/apps/RNApp/src/HomeScreen.tsx b/apps/RNApp/src/HomeScreen.tsx index 99131782..75638c26 100644 --- a/apps/RNApp/src/HomeScreen.tsx +++ b/apps/RNApp/src/HomeScreen.tsx @@ -1,17 +1,79 @@ -import { useEffect } from 'react'; -import { StyleSheet, Text, View, Button } from 'react-native'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { + Animated, + Button, + FlatList, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import ReactNativeBrownfield from '@callstack/react-native-brownfield'; +import type { MessageEvent } from '@callstack/react-native-brownfield'; import { getRandomTheme } from './utils'; import type { RootStackParamList } from './navigation/RootStack'; import Counter from './components/counter'; +interface Message { + id: string; + text: string; + from: 'native' | 'rn'; + timestamp: number; +} + +function MessageBubble({ item, color }: { item: Message; color: string }) { + const opacity = useRef(new Animated.Value(0)).current; + const translateY = useRef(new Animated.Value(20)).current; + + useEffect(() => { + Animated.parallel([ + Animated.timing(opacity, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }), + Animated.spring(translateY, { + toValue: 0, + tension: 80, + friction: 10, + useNativeDriver: true, + }), + ]).start(); + }, [opacity, translateY]); + + const isFromNative = item.from === 'native'; + + return ( + + + {isFromNative ? 'From Native' : 'From RN'} + + {item.text} + + ); +} + +let messageCounter = 0; + export function HomeScreen({ navigation, route, }: NativeStackScreenProps) { const colors = route.params?.theme ? route.params.theme : getRandomTheme(); + const [messages, setMessages] = useState([]); + const flatListRef = useRef>(null); useEffect(() => { const unsubscribe = navigation.addListener('focus', () => { @@ -21,6 +83,39 @@ export function HomeScreen({ return unsubscribe; }, [navigation]); + useEffect(() => { + const sub = ReactNativeBrownfield.onMessage((event: MessageEvent) => { + const data = event.data as { text?: string }; + setMessages((prev) => [ + ...prev, + { + id: String(++messageCounter), + text: data?.text ?? JSON.stringify(event.data), + from: 'native', + timestamp: Date.now(), + }, + ]); + }); + return () => sub.remove(); + }, []); + + const sendMessage = useCallback(() => { + const msg = { + text: `Hello from React Native! (#${++messageCounter})`, + timestamp: Date.now(), + }; + ReactNativeBrownfield.postMessage(msg); + setMessages((prev) => [ + ...prev, + { + id: String(messageCounter), + text: msg.text, + from: 'rn', + timestamp: msg.timestamp, + }, + ]); + }, []); + return ( @@ -29,27 +124,53 @@ export function HomeScreen({ -