Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
59e4a04
fix: inverted navigation
OtavioStasiak Feb 26, 2026
4eb8bf2
fix: just invert list content
OtavioStasiak Feb 26, 2026
9b639da
fix: lint
OtavioStasiak Feb 26, 2026
cf4cb85
test: inverted just in the inverted
OtavioStasiak Feb 27, 2026
3f38334
fix: code
OtavioStasiak Feb 27, 2026
a33f49d
fix: invert just the flatlist content
OtavioStasiak Feb 27, 2026
4e86354
fix: switch tab
OtavioStasiak Mar 2, 2026
e3dccec
Merge branch 'develop' into fix.android-inverted-keyboard-navigation
OtavioStasiak Mar 2, 2026
32d0b75
try to reverse just the flatlist content
OtavioStasiak Mar 2, 2026
48609f2
Merge branch 'develop' into fix.android-inverted-keyboard-navigation
OtavioStasiak Mar 2, 2026
037d5c6
fix: inverted list direction
OtavioStasiak Mar 4, 2026
8886ec3
chore: format code and fix lint issues
OtavioStasiak Mar 4, 2026
c1bbde2
fix: list issues
OtavioStasiak Mar 5, 2026
4db3a04
rollback List
OtavioStasiak Mar 5, 2026
846b8ba
revert unused changes on module
OtavioStasiak Mar 5, 2026
2341565
try native module approach
OtavioStasiak Mar 5, 2026
98ea3b5
test: invert navigation
OtavioStasiak Mar 12, 2026
083f4d7
chore: format code and fix lint issues
OtavioStasiak Mar 12, 2026
bde3b23
space
OtavioStasiak Mar 12, 2026
bb3556d
fix: build
OtavioStasiak Mar 13, 2026
cdd5ef8
fix: keyboard navigation still working on different rooms
OtavioStasiak Mar 13, 2026
b07d06b
fix: last item focus
OtavioStasiak Mar 13, 2026
425187b
chore: format code and fix lint issues
OtavioStasiak Mar 13, 2026
4731276
fix: change focus
OtavioStasiak Mar 13, 2026
311cddc
Merge branch 'develop' into fix.android-inverted-keyboard-navigation
OtavioStasiak Mar 14, 2026
0c311b2
fix: lint
OtavioStasiak Mar 16, 2026
b591ecf
fix: merge issues
OtavioStasiak Mar 18, 2026
ec27cc9
Merge branch 'develop' into fix.android-inverted-keyboard-navigation
OtavioStasiak Mar 18, 2026
8064b66
fix: lint
OtavioStasiak Mar 18, 2026
f4b730e
chore: format code and fix lint issues
OtavioStasiak Mar 18, 2026
4b51b7c
rollback focus change
OtavioStasiak Mar 19, 2026
b87b186
focus inside of roomView!
OtavioStasiak Mar 19, 2026
f4c87c2
Merge branch 'develop' into fix.android-inverted-keyboard-navigation
OtavioStasiak Mar 19, 2026
e546f31
rollback: useScrollContainer
OtavioStasiak Mar 20, 2026
4d25dd3
test: use a RNLike scrolview
OtavioStasiak Mar 20, 2026
2ecf157
Merge branch 'develop' into fix.android-inverted-keyboard-navigation
OtavioStasiak Mar 25, 2026
a2abc14
fix: Pressable issues
OtavioStasiak Mar 19, 2026
0b051b1
fix: tests
OtavioStasiak Mar 19, 2026
3fee8cf
use rectButton
OtavioStasiak Mar 24, 2026
e728b66
update snapshot
OtavioStasiak Mar 24, 2026
4098eed
fix: unit tests
OtavioStasiak Mar 25, 2026
5fae357
fix: use Pressable to able to focus on iOS and keep it working on tru…
OtavioStasiak Mar 25, 2026
0907cc1
chore: install react-native-external-keyboard
OtavioStasiak Mar 25, 2026
e45b714
fix: focus on thread and reactions
OtavioStasiak Mar 25, 2026
9cf9ad0
feat: onThreadPress to open threads on keyboard
OtavioStasiak Mar 25, 2026
506ee19
feat: usePressable gh
OtavioStasiak Mar 26, 2026
7f3c952
fix: gesture handler iOS focusable on keyboard
OtavioStasiak Mar 26, 2026
ac854d3
fix: patch package cleanup
OtavioStasiak Mar 26, 2026
ef439b8
fix: improve switch navigation
OtavioStasiak Mar 26, 2026
6f21bba
fix: tests
OtavioStasiak Mar 26, 2026
bfb8ed9
fix: test
OtavioStasiak Mar 26, 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
63 changes: 51 additions & 12 deletions android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
package chat.rocket.reactnative


import android.os.Bundle
import android.content.Intent
import android.view.KeyEvent
import android.view.View
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate

import android.os.Bundle
import com.zoontek.rnbootsplash.RNBootSplash
import android.content.Intent
import android.content.res.Configuration
import chat.rocket.reactnative.notification.NotificationIntentHandler

import chat.rocket.reactnative.a11y.KeyboardA11yModule
import chat.rocket.reactnative.scroll.FocusUtils

class MainActivity : ReactActivity() {

/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "RocketChatRN"

/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
Expand All @@ -29,20 +31,57 @@ class MainActivity : ReactActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
RNBootSplash.init(this, R.style.BootTheme)
super.onCreate(null)

// Handle notification intents
intent?.let { NotificationIntentHandler.handleIntent(this, it) }
}

public override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)

// Handle notification intents when activity is already running
NotificationIntentHandler.handleIntent(this, intent)
}

override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (KeyboardA11yModule.isEnabled()) {
val current: View? = currentFocus
if (current != null && FocusUtils.hasInvertedParent(current)) {
if (event.action == KeyEvent.ACTION_DOWN) {
val keyCode = event.keyCode
val isShiftPressed = event.isShiftPressed
val mapped = when (keyCode) {
// Invert DPAD vertical arrows for inverted lists
KeyEvent.KEYCODE_DPAD_DOWN -> KeyEvent.KEYCODE_DPAD_UP
KeyEvent.KEYCODE_DPAD_UP -> KeyEvent.KEYCODE_DPAD_DOWN
// Map Tab / Shift+Tab to vertical navigation as well
KeyEvent.KEYCODE_TAB ->
if (isShiftPressed) KeyEvent.KEYCODE_DPAD_UP else KeyEvent.KEYCODE_DPAD_DOWN
else -> keyCode
}
if (mapped != keyCode) {
val invertedEvent = KeyEvent(
event.downTime,
event.eventTime,
event.action,
mapped,
event.repeatCount,
event.metaState,
event.deviceId,
event.scanCode,
event.flags,
event.source
)
return super.dispatchKeyEvent(invertedEvent)
}
}
}
}
return super.dispatchKeyEvent(event)
}

override fun invokeDefaultOnBackPressed() {
moveTaskToBack(true)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package chat.rocket.reactnative.a11y;

import androidx.annotation.Nullable;

import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.annotations.ReactModule;

@ReactModule(name = KeyboardA11ySpec.NAME)
public class KeyboardA11yModule extends KeyboardA11ySpec {

private static volatile boolean sEnabled = false;
@Nullable
private static volatile String sScope = null;

public KeyboardA11yModule(ReactApplicationContext reactContext) {
super(reactContext);
}

public static boolean isEnabled() {
return sEnabled;
}

@Nullable
public static String getScope() {
return sScope;
}

@Override
public void enable(String scope) {
sEnabled = true;
sScope = scope;
}

@Override
public void disable() {
sEnabled = false;
sScope = null;
}

@Override
public void getState(Promise promise) {
WritableMap state = com.facebook.react.bridge.Arguments.createMap();
state.putBoolean("enabled", sEnabled);
state.putString("scope", sScope);
promise.resolve(state);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package chat.rocket.reactnative.a11y;

import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.turbomodule.core.interfaces.TurboModule;

public abstract class KeyboardA11ySpec extends ReactContextBaseJavaModule implements TurboModule {

public static final String NAME = "KeyboardA11y";

public KeyboardA11ySpec(ReactApplicationContext reactContext) {
super(reactContext);
}

@Override
public String getName() {
return NAME;
}

@ReactMethod
public abstract void enable(String scope);

@ReactMethod
public abstract void disable();

@ReactMethod
public abstract void getState(Promise promise);
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package chat.rocket.reactnative.scroll;

import android.view.View;
import android.view.ViewParent;
import chat.rocket.reactnative.R;

/**
* Utilities for focus-related queries inside custom scroll views.
*/
public final class FocusUtils {

private FocusUtils() {}

public static boolean hasInvertedParent(View view) {
if (view == null) {
return false;
}
ViewParent parent = view.getParent();
while (parent instanceof View) {
View parentView = (View) parent;
Object tag = parentView.getTag(R.id.tag_inverted_list);
if (tag instanceof Boolean && (Boolean) tag) {
return true;
}
parent = parentView.getParent();
}
return false;
}
}

Original file line number Diff line number Diff line change
@@ -1,23 +1,79 @@
package chat.rocket.reactnative.scroll;

import android.graphics.Rect;
import android.view.View;
import com.facebook.react.views.view.ReactViewGroup;
import java.util.ArrayList;
import java.util.Collections;
import chat.rocket.reactnative.R;

/**
* Content view for inverted FlatLists. Reports its children to accessibility in reversed order so
* TalkBack traversal matches the visual order (newest-first) when used inside InvertedScrollView.
*/
public class InvertedScrollContentView extends ReactViewGroup {

private boolean mIsInvertedContent = false;

public InvertedScrollContentView(android.content.Context context) {
super(context);
}

public void setIsInvertedContent(boolean isInverted) {
mIsInvertedContent = isInverted;
if (isInverted) {
setTag(R.id.tag_inverted_list, true);
} else {
setTag(R.id.tag_inverted_list, null);
}
}

@Override
public void addChildrenForAccessibility(ArrayList<View> outChildren) {
super.addChildrenForAccessibility(outChildren);
Collections.reverse(outChildren);
if (mIsInvertedContent) {
Collections.reverse(outChildren);
}
}

@Override
protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
if (mIsInvertedContent) {
for (int i = getChildCount() - 1; i >= 0; i--) {
View child = getChildAt(i);
if (child.getVisibility() == VISIBLE) {
if (child.requestFocus(direction, previouslyFocusedRect)) {
return true;
}
}
}
return false;
}
return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
}

@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
super.addFocusables(views, direction, focusableMode);
if (mIsInvertedContent) {
// Find indices of focusables that are children of this view
ArrayList<Integer> childIndices = new ArrayList<>();
for (int i = 0; i < views.size(); i++) {
View v = views.get(i);
if (v.getParent() == this) {
childIndices.add(i);
}
}
// Reverse only the sublist of children focusables
int n = childIndices.size();
for (int i = 0; i < n / 2; i++) {
int idx1 = childIndices.get(i);
int idx2 = childIndices.get(n - 1 - i);
View temp = views.get(idx1);
views.set(idx1, views.get(idx2));
views.set(idx2, temp);
}
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.views.view.ReactViewManager;

/**
Expand All @@ -22,4 +23,9 @@ public String getName() {
public InvertedScrollContentView createViewInstance(ThemedReactContext context) {
return new InvertedScrollContentView(context);
}

@ReactProp(name = "isInvertedContent")
public void setIsInvertedContent(InvertedScrollContentView view, boolean isInverted) {
view.setIsInvertedContent(isInverted);
}
}
Loading
Loading