Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
6fed722
fix(mobile): unify Material You pane framing on foldables
PixPMusic Sep 15, 2026
8af949f
fix(mobile): preserve contrast and inspector spacing in Material frames
PixPMusic Sep 15, 2026
d062650
feat(mobile): prototype search FAB toolbar
PixPMusic Sep 15, 2026
279b696
feat(mobile): prototype extended new-thread FAB
PixPMusic Sep 15, 2026
764fd80
fix(mobile): tighten extended preview FAB spacing
PixPMusic Sep 15, 2026
ed80f0a
fix(mobile): size extended preview FABs to 56dp
PixPMusic Sep 15, 2026
673d3fe
fix(mobile): use native Material controls and adaptive settings
juliusmarminge Sep 16, 2026
daf2d01
fix(mobile): extend Material controls across home and tool panes
juliusmarminge Sep 16, 2026
40615b7
fix(mobile): finish Material pane styling and themed inputs
PixPMusic Sep 16, 2026
9db32f0
Merge remote-tracking branch 'fork/fix/android-foldable-material-pari…
PixPMusic Sep 16, 2026
9b39138
Merge main into Material You foldable layout
PixPMusic Sep 16, 2026
c2e1ec9
fix(mobile): align Material search with native headers
PixPMusic Sep 16, 2026
fec0a81
style(mobile): format selection handle tint helpers
PixPMusic Sep 16, 2026
f4483ee
fix(mobile): reserve space for Material FABs in thread list v2
PixPMusic Sep 16, 2026
905062e
Merge remote-tracking branch 'origin/main' into t3code/pr-11841/fix/a…
juliusmarminge Sep 16, 2026
df430b4
fix(mobile): align Material navigation and accessible native actions
juliusmarminge Sep 16, 2026
790e338
fix(mobile): integrate shared Material panes with native controls
juliusmarminge Sep 16, 2026
df1ffb3
fix(mobile): show Material pane state and match provider badge surfaces
juliusmarminge Sep 16, 2026
5f462e8
fix(mobile): preserve native dialog guards and action states
juliusmarminge Sep 16, 2026
102afb5
fix(mobile): restore Material brand inset and FAB hierarchy
PixPMusic Sep 16, 2026
146d2e3
fix(mobile): bound Material project selection on tablets
juliusmarminge Sep 16, 2026
de3e08e
fix(mobile): close Files from the active Android header action
juliusmarminge Sep 16, 2026
d41e19d
fix(mobile): expose selected state in icon button fallbacks
juliusmarminge Sep 16, 2026
90162dc
fix(mobile): show the Material terminal keyboard icon
juliusmarminge Sep 16, 2026
7e4c6fc
fix(mobile): dismiss the native Android terminal keyboard
juliusmarminge Sep 16, 2026
08e60da
fix(mobile): expose wide Android workspace pane controls
juliusmarminge Sep 16, 2026
0cd7373
fix(mobile): preserve Material You colors in native code previews
juliusmarminge Sep 16, 2026
ddca508
fix(mobile): theme the Android custom snooze dialog
juliusmarminge Sep 16, 2026
87058c2
fix(mobile): align Android thread settings with Material
juliusmarminge Sep 16, 2026
8ee90b9
fix(mobile): keep picker selection colors on custom themes
juliusmarminge Sep 16, 2026
96e9486
fix(mobile): respect Android clock format in custom snooze
PixPMusic Sep 16, 2026
7ad1567
fix(mobile): simplify the Material empty environment state
PixPMusic Sep 16, 2026
c55bf52
fix(mobile): align Material header insets and avoid duplicate compose…
PixPMusic Sep 16, 2026
e7965c2
fix(mobile): theme environment actions in Material layouts
PixPMusic Sep 16, 2026
38ac145
fix(mobile): preserve Files header height while searching
PixPMusic Sep 16, 2026
b08ad56
fix(mobile): restore Android creation and picker navigation
juliusmarminge Sep 16, 2026
133fc6b
fix(mobile): use native Material dropdown menus
juliusmarminge Sep 16, 2026
ed81c4a
fix(mobile): retain integrated Material controls after rebase
PixPMusic Sep 16, 2026
3d8135f
fix(mobile): dismiss the new-thread flow when cancelling on Android
PixPMusic Sep 16, 2026
e961ffd
fix(mobile): unify Material rows and settings controls
juliusmarminge Sep 16, 2026
82b6d2c
fix(mobile): align scaled toolbars and collapse compose FAB on scroll
PixPMusic Sep 16, 2026
e80f8f5
fix(mobile): streamline native custom snooze dialogs
juliusmarminge Sep 16, 2026
6696c5b
refactor(mobile): make Material the only Android layout
juliusmarminge Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.os.Build
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.Spanned
Expand Down Expand Up @@ -164,6 +165,17 @@ private class SanitizingSelectionActionModeCallback(
}
}

internal fun applySelectionHandleColor(textView: TextView, color: Int) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return
textView.textSelectHandle?.mutate()?.apply { setTint(color) }?.let(textView::setTextSelectHandle)
textView.textSelectHandleLeft?.mutate()?.apply {
setTint(color)
}?.let(textView::setTextSelectHandleLeft)
textView.textSelectHandleRight?.mutate()?.apply {
setTint(color)
}?.let(textView::setTextSelectHandleRight)
}

class T3MarkdownTextSelectionModule : Module() {
private val chipImages = LruCache<String, Map<String, Any>>(128)

Expand All @@ -183,9 +195,23 @@ class T3MarkdownTextSelectionModule : Module() {
}
}.fontMetricsInt

private fun setSelectionHandleColor(reactTag: Int, color: Int) {
val reactContext = appContext.reactContext as? ReactContext ?: return
reactContext.runOnUiQueueThread {
val textView = runCatching {
UIManagerHelper.getUIManagerForReactTag(reactContext, reactTag)?.resolveView(reactTag)
}.getOrNull() as? TextView ?: return@runOnUiQueueThread
applySelectionHandleColor(textView, color)
}
}

override fun definition() = ModuleDefinition {
Name("T3MarkdownTextSelection")

Function("setSelectionHandleColor") { reactTag: Int, color: Int ->
setSelectionHandleColor(reactTag, color)
}

Function("renderContextChip") { payloadJson: String ->
val resources = appContext.reactContext?.resources ?: return@Function null
val metrics = resources.displayMetrics
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package expo.modules.t3markdowntext

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.widget.TextView
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [36], manifest = Config.NONE)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class MarkdownSelectionColorTest {
private fun textView() = TextView(RuntimeEnvironment.getApplication()).apply {
setTextSelectHandle(ColorDrawable(Color.WHITE))
setTextSelectHandleLeft(ColorDrawable(Color.WHITE))
setTextSelectHandleRight(ColorDrawable(Color.WHITE))
}

private fun renderedColor(drawable: Drawable?): Int {
requireNotNull(drawable)
val bitmap = Bitmap.createBitmap(4, 4, Bitmap.Config.ARGB_8888)
drawable.setBounds(0, 0, 4, 4)
drawable.draw(Canvas(bitmap))
val color = bitmap.getPixel(2, 2)
bitmap.recycle()
return color
}

@Test
fun retintsAllHandlesWhenTheThemeChangesWithoutChangingTheHighlight() {
val text = textView()
val highlight = 0x52FF0088
text.highlightColor = highlight

for (color in listOf(Color.MAGENTA, Color.GREEN, Color.MAGENTA)) {
applySelectionHandleColor(text, color)
assertEquals(color, renderedColor(text.textSelectHandle))
assertEquals(color, renderedColor(text.textSelectHandleLeft))
assertEquals(color, renderedColor(text.textSelectHandleRight))
assertEquals(highlight, text.highlightColor)
}
}

@Test
fun doesNotTintOtherTextViewsSharingDrawableState() {
val original = ColorDrawable(Color.WHITE)
val first = textView().apply {
setTextSelectHandleLeft(original.constantState!!.newDrawable())
}
val second = textView().apply {
setTextSelectHandleLeft(original.constantState!!.newDrawable())
}

applySelectionHandleColor(first, Color.MAGENTA)

assertEquals(Color.MAGENTA, renderedColor(first.textSelectHandleLeft))
assertEquals(Color.WHITE, renderedColor(second.textSelectHandleLeft))
}
}
53 changes: 51 additions & 2 deletions apps/mobile/modules/t3-markdown-text/src/MarkdownTextPrimitive.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import React, { type Ref } from "react";
import { Platform, StyleSheet, Text as RNText, type TextProps, type ViewStyle } from "react-native";
import {
findNodeHandle,
Platform,
processColor,
StyleSheet,
Text as RNText,
type ColorValue,
type TextProps,
type ViewStyle,
} from "react-native";
import { setMarkdownSelectionHandleColor } from "./T3MarkdownTextSelectionModule";
import T3MarkdownTextRunNativeComponent from "./T3MarkdownTextRunNativeComponent";
import T3MarkdownTextNativeComponent from "./T3MarkdownTextNativeComponent";
import { flattenStyles } from "./util";
Expand Down Expand Up @@ -34,6 +44,7 @@ export type ContextMenuActionEvent = {
*/
export type MarkdownTextPrimitiveProps = Omit<TextProps, "onTextLayout"> & {
nativeTextRef?: Ref<RNText>;
selectionHandleColor?: ColorValue;
uiTextView?: boolean;
contextMenuConfig?: string;
contextClipboardConfig?: string;
Expand Down Expand Up @@ -116,7 +127,45 @@ function MarkdownTextPrimitiveInner({ nativeTextRef, ...props }: MarkdownTextPri
return <MarkdownTextPrimitiveChild {...props} />;
}

export function MarkdownTextPrimitive(props: MarkdownTextPrimitiveProps) {
function AndroidMarkdownText({
nativeTextRef,
selectionHandleColor,
onLayout,
contextClipboardConfig: _contextClipboardConfig,
...props
}: MarkdownTextPrimitiveProps) {
const textRef = React.useRef<RNText | null>(null);
React.useImperativeHandle<RNText | null, RNText | null>(nativeTextRef, () => textRef.current, []);
const color = processColor(selectionHandleColor);
const applyHandleColor = React.useCallback(() => {
if (!textRef.current || typeof color !== "number") return;
const reactTag = findNodeHandle(textRef.current);
if (reactTag !== null) setMarkdownSelectionHandleColor(reactTag, color);
}, [color]);

// RN's selectionColor only sets the highlight. Retint mounted handles when
// the theme changes, and after layout when the native view first exists.
React.useEffect(applyHandleColor, [applyHandleColor]);

return (
<RNText
ref={textRef}
{...props}
onLayout={(event) => {
applyHandleColor();
onLayout?.(event);
}}
/>
);
}

export function MarkdownTextPrimitive({
selectionHandleColor,
...props
}: MarkdownTextPrimitiveProps) {
if (Platform.OS === "android" && selectionHandleColor !== undefined) {
return <AndroidMarkdownText {...props} selectionHandleColor={selectionHandleColor} />;
}
if (Platform.OS !== "ios") {
const { nativeTextRef, contextClipboardConfig: _contextClipboardConfig, ...textProps } = props;
return <RNText ref={nativeTextRef} {...textProps} />;
Expand Down
20 changes: 15 additions & 5 deletions apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,13 @@ function HighlightedCodeText(props: {
}
}
return (
<MarkdownTextPrimitive uiTextView selectable style={style}>
<MarkdownTextPrimitive
uiTextView
selectable
selectionColor={props.textStyle.selectionColor}
selectionHandleColor={props.textStyle.selectionHandleColor}
style={style}
>
{props.highlighted ? lines : props.content}
</MarkdownTextPrimitive>
);
Expand Down Expand Up @@ -174,8 +180,10 @@ function NativeCodeBlock(props: {
justifyContent: "space-between",
}}
>
<Text
<MarkdownTextPrimitive
selectable
selectionColor={props.textStyle.selectionColor}
selectionHandleColor={props.textStyle.selectionHandleColor}
style={{
flex: 1,
color: props.textStyle.mutedColor,
Expand All @@ -184,7 +192,7 @@ function NativeCodeBlock(props: {
}}
>
{languageLabel}
</Text>
</MarkdownTextPrimitive>
<CopyTextButton
accessibilityLabel={`Copy ${languageLabel.toLowerCase()} code`}
text={content}
Expand Down Expand Up @@ -332,8 +340,10 @@ function NativeMarkdownImage(props: {
}}
/>
{props.node.alt ? (
<Text
<MarkdownTextPrimitive
selectable
selectionColor={props.textStyle.selectionColor}
selectionHandleColor={props.textStyle.selectionHandleColor}
style={{
color: props.textStyle.mutedColor,
fontFamily: props.textStyle.fontFamily,
Expand All @@ -342,7 +352,7 @@ function NativeMarkdownImage(props: {
}}
>
{props.node.alt}
</Text>
</MarkdownTextPrimitive>
) : null}
</View>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,8 @@ export function NativeMarkdownSelectableText(props: {
}
uiTextView
selectable
selectionColor={props.textStyle.selectionColor}
selectionHandleColor={props.textStyle.selectionHandleColor}
style={{
flexShrink: 1,
minWidth: 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export interface NativeMarkdownTextStyle {
readonly selectionColor?: string;
readonly selectionHandleColor?: string;
readonly color: string;
readonly strongColor: string;
readonly mutedColor: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { requireOptionalNativeModule } from "expo";

interface T3MarkdownTextSelectionNativeModule {
readonly setSelectionHandleColor?: (reactTag: number, color: number) => void;
readonly installCopySanitizer: (reactTag: number, contextClipboardConfig: string) => void;
readonly renderContextChip?: (payloadJson: string) => {
readonly uri: string;
Expand All @@ -20,6 +21,10 @@ export function installMarkdownCopySanitizer(reactTag: number, contextClipboardC
nativeModule?.installCopySanitizer(reactTag, contextClipboardConfig);
}

export function setMarkdownSelectionHandleColor(reactTag: number, color: number): void {
nativeModule?.setSelectionHandleColor?.(reactTag, color);
}

export function renderAndroidContextChip(payloadJson: string) {
return nativeModule?.renderContextChip?.(payloadJson) ?? null;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package expo.modules.t3nativecontrols

import android.content.Intent
import android.text.format.DateFormat
import androidx.core.content.FileProvider
import expo.modules.kotlin.Promise
import expo.modules.kotlin.modules.Module
Expand All @@ -15,6 +16,11 @@ class T3NativeControlsModule : Module() {
override fun definition() = ModuleDefinition {
Name("T3NativeControls")

Function("is24HourFormat") {
val context = appContext.reactContext ?: error("The app is not active.")
DateFormat.is24HourFormat(context)
}

AsyncFunction("openFile") { uri: String, mimeType: String, promise: Promise ->
check(filePreviewPromise == null) { "A document viewer is already open." }
val activity = appContext.currentActivity ?: error("The app is not active.")
Expand Down
10 changes: 3 additions & 7 deletions apps/mobile/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { BlurTargetView } from "expo-blur";
import * as Linking from "expo-linking";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import { StatusBar } from "react-native";
import { StatusBar, View } from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { KeyboardProvider } from "react-native-keyboard-controller";
import { SafeAreaProvider } from "react-native-safe-area-context";
Expand All @@ -21,10 +20,8 @@ import {
import { RootStack } from "./Stack";
import { appAtomRegistry } from "./state/atom-registry";
import { OverlayPortalHost } from "./components/OverlayPortal";
import { appBlurTargetRef } from "./lib/appBlurTarget";
import { shouldHandleAppLink } from "./lib/appLinking";
import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme";

import { SubscriptionUsageCoordinator } from "./widgets/SubscriptionUsageCoordinator";

import "../global.css";
Expand Down Expand Up @@ -88,14 +85,13 @@ function AppContent() {
this, React Navigation defaults to its light theme and every native
header (glass buttons, title, materials) is forced light even when
the system is in dark mode. */}
{/* Blur target for Android dropdown backdrops — see appBlurTarget.ts. */}
<BlurTargetView ref={appBlurTargetRef} style={{ flex: 1 }}>
<View style={{ flex: 1 }}>
<IncomingShareProvider>
<Navigation linking={appLinking} theme={navigationTheme} />
</IncomingShareProvider>
<ConfirmDialogHost />
<ThreadArrangementHost />
</BlurTargetView>
</View>
{/* Anchored-menu overlays render here — in-window, so the
keyboard stays up while a dropdown is open. */}
<OverlayPortalHost />
Expand Down
Loading
Loading