diff --git a/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt b/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt index e4ab65ceba1..eac65e52db2 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt +++ b/android/app/src/main/java/chat/rocket/reactnative/MainActivity.kt @@ -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] @@ -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) } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11yModule.java b/android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11yModule.java new file mode 100644 index 00000000000..68b4506ba44 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11yModule.java @@ -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); + } +} + diff --git a/android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11ySpec.java b/android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11ySpec.java new file mode 100644 index 00000000000..eb0bc0430f2 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/a11y/KeyboardA11ySpec.java @@ -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); +} + diff --git a/android/app/src/main/java/chat/rocket/reactnative/scroll/FocusUtils.java b/android/app/src/main/java/chat/rocket/reactnative/scroll/FocusUtils.java new file mode 100644 index 00000000000..ec17d8e7cf5 --- /dev/null +++ b/android/app/src/main/java/chat/rocket/reactnative/scroll/FocusUtils.java @@ -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; + } +} + diff --git a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java b/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java index a4acb0c1e13..02003559c6d 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java +++ b/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java @@ -1,9 +1,11 @@ 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 @@ -11,13 +13,67 @@ */ 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 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 views, int direction, int focusableMode) { + super.addFocusables(views, direction, focusableMode); + if (mIsInvertedContent) { + // Find indices of focusables that are children of this view + ArrayList 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); + } + } } } + diff --git a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentViewManager.java b/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentViewManager.java index d30f9fc84c2..095c2463424 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentViewManager.java +++ b/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentViewManager.java @@ -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; /** @@ -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); + } } diff --git a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java b/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java index def585a7511..281dc32f87f 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java +++ b/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java @@ -1,36 +1,137 @@ package chat.rocket.reactnative.scroll; +import android.content.Context; +import android.view.FocusFinder; +import android.view.KeyEvent; import android.view.View; -import com.facebook.react.bridge.ReactContext; +import android.view.ViewGroup; +import android.view.ViewParent; +import androidx.annotation.Nullable; +import com.facebook.react.uimanager.util.ReactFindViewUtil; import com.facebook.react.views.scroll.ReactScrollView; -import java.util.ArrayList; -import java.util.Collections; - -// When a FlatList is inverted (inverted={true}), React Native uses scaleY: -1 transform which -// visually inverts the list but Android still reports children in array order. This view overrides -// addChildrenForAccessibility to reverse the order so TalkBack matches the visual order. +/** + * Custom ScrollView for inverted FlatLists that corrects keyboard navigation so it follows + * the visual order instead of the inverted view-tree order. + * + * Both Tab/Shift+Tab and DPAD arrows navigate between FlatList cells (direct children of the + * content view) to avoid loops caused by inner focusable elements within a single message. + * Boundary exit uses ReactFindViewUtil to find a tagged exit-target view by nativeID. + */ public class InvertedScrollView extends ReactScrollView { - private boolean mIsInvertedVirtualizedList = false; + private boolean mKeyConsumed = false; + private @Nullable String mExitFocusNativeId; - public InvertedScrollView(ReactContext context) { + public InvertedScrollView(Context context) { super(context); } - - // Set whether this ScrollView is used for an inverted virtualized list. When true, we reverse the - // accessibility traversal order to match the visual order. - - public void setIsInvertedVirtualizedList(boolean isInverted) { - mIsInvertedVirtualizedList = isInverted; + public void setExitFocusNativeId(@Nullable String nativeId) { + mExitFocusNativeId = nativeId; } @Override - public void addChildrenForAccessibility(ArrayList outChildren) { - super.addChildrenForAccessibility(outChildren); - if (mIsInvertedVirtualizedList) { - Collections.reverse(outChildren); + public boolean dispatchKeyEvent(KeyEvent event) { + int keyCode = event.getKeyCode(); + + if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || keyCode == KeyEvent.KEYCODE_DPAD_UP) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + boolean isForward = (keyCode == KeyEvent.KEYCODE_DPAD_DOWN); + mKeyConsumed = handleCellNavigation(isForward); + return mKeyConsumed; + } + return mKeyConsumed; + } + + if (keyCode == KeyEvent.KEYCODE_TAB) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + boolean isForward = !event.isShiftPressed(); + mKeyConsumed = handleCellNavigation(isForward); + return mKeyConsumed; + } + return mKeyConsumed; + } + + return super.dispatchKeyEvent(event); + } + + /** + * Shared navigation logic for Tab and DPAD. + * @param isForward true = visual down (Tab / DPAD_DOWN), false = visual up (Shift+Tab / DPAD_UP) + */ + private boolean handleCellNavigation(boolean isForward) { + View focused = findFocus(); + if (focused == null || getChildCount() == 0) { + return false; + } + + ViewGroup contentView = (ViewGroup) getChildAt(0); + int cellIndex = findContainingCellIndex(contentView, focused); + if (cellIndex < 0) { + return false; + } + + int step = isForward ? -1 : 1; + int focusDir = isForward ? View.FOCUS_UP : View.FOCUS_DOWN; + + for (int i = cellIndex + step; i >= 0 && i < contentView.getChildCount(); i += step) { + View cell = contentView.getChildAt(i); + if (cell != null && cell.getVisibility() == VISIBLE && cell.requestFocus(focusDir)) { + return true; + } + } + + int exitDir = isForward ? View.FOCUS_DOWN : View.FOCUS_UP; + View exitTarget = findExitTarget(exitDir); + if (exitTarget != null) { + exitTarget.requestFocus(); + return true; + } + + return true; + } + + private int findContainingCellIndex(ViewGroup contentView, View focused) { + View current = focused; + while (current != null && current.getParent() != contentView) { + ViewParent p = current.getParent(); + if (p instanceof View) { + current = (View) p; + } else { + return -1; + } + } + return current != null ? contentView.indexOfChild(current) : -1; + } + + private View findExitTarget(int direction) { + if (mExitFocusNativeId != null) { + View target = ReactFindViewUtil.findView(getRootView(), mExitFocusNativeId); + if (target != null) { + return target; + } + } + View rootView = getRootView(); + if (!(rootView instanceof ViewGroup)) { + return null; + } + View target = FocusFinder.getInstance() + .findNextFocus((ViewGroup) rootView, this, direction); + if (target != null && !isDescendantOf(target, this)) { + return target; + } + return null; + } + + private static boolean isDescendantOf(View view, ViewGroup ancestor) { + ViewParent parent = view.getParent(); + while (parent != null) { + if (parent == ancestor) { + return true; + } + parent = parent.getParent(); } + return false; } } diff --git a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollViewManager.java b/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollViewManager.java index 453dd009ec0..30ad13084d7 100644 --- a/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollViewManager.java +++ b/android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollViewManager.java @@ -1,7 +1,9 @@ package chat.rocket.reactnative.scroll; +import androidx.annotation.Nullable; 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.scroll.ReactScrollViewManager; /** @@ -23,4 +25,9 @@ public String getName() { public InvertedScrollView createViewInstance(ThemedReactContext context) { return new InvertedScrollView(context); } + + @ReactProp(name = "exitFocusNativeId") + public void setExitFocusNativeId(InvertedScrollView view, @Nullable String nativeId) { + view.setExitFocusNativeId(nativeId); + } } diff --git a/android/app/src/main/res/values/ids.xml b/android/app/src/main/res/values/ids.xml new file mode 100644 index 00000000000..bab9bf66bef --- /dev/null +++ b/android/app/src/main/res/values/ids.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/containers/Button/Button.test.tsx b/app/containers/Button/Button.test.tsx index 1f63f7d4976..5c98db24f85 100644 --- a/app/containers/Button/Button.test.tsx +++ b/app/containers/Button/Button.test.tsx @@ -64,8 +64,6 @@ describe('ButtonTests', () => { test('disabled button is in disabled state', async () => { const { findByTestId } = render(); const button = await findByTestId(testProps.testID); - // In the test environment, RNGH Pressable may still invoke onPress when disabled, - // so we assert the button is in a disabled state (enabled={false}). expect(button.props.enabled).toBe(false); }); diff --git a/app/containers/Button/__snapshots__/Button.test.tsx.snap b/app/containers/Button/__snapshots__/Button.test.tsx.snap index 4c0bdb49306..c514b1ae90b 100644 --- a/app/containers/Button/__snapshots__/Button.test.tsx.snap +++ b/app/containers/Button/__snapshots__/Button.test.tsx.snap @@ -4,43 +4,56 @@ exports[`Story Snapshots: CustomButton should match snapshot 1`] = ` + + + + + + + { title: string; onPress: () => void; type?: 'primary' | 'secondary'; @@ -17,6 +17,7 @@ interface IButtonProps extends PressableProps { style?: StyleProp | StyleProp[]; styleText?: StyleProp | StyleProp[]; small?: boolean; + disabled?: boolean; } const styles = StyleSheet.create({ @@ -87,15 +88,19 @@ const Button: React.FC = ({ ]; return ( - {loading ? : {title}} - + ); }; diff --git a/app/containers/LoginServices/__snapshots__/LoginServices.test.tsx.snap b/app/containers/LoginServices/__snapshots__/LoginServices.test.tsx.snap index e36536132bb..29e20c77aef 100644 --- a/app/containers/LoginServices/__snapshots__/LoginServices.test.tsx.snap +++ b/app/containers/LoginServices/__snapshots__/LoginServices.test.tsx.snap @@ -5,42 +5,54 @@ exports[`Story Snapshots: Separators should match snapshot 1`] = ` + + void; getText: () => string; setInput: TSetInput; + focus: () => void; } export interface IMessageComposerContainerProps { diff --git a/app/containers/RoomHeader/RoomHeader.tsx b/app/containers/RoomHeader/RoomHeader.tsx index 35a44f5181f..68c319a0963 100644 --- a/app/containers/RoomHeader/RoomHeader.tsx +++ b/app/containers/RoomHeader/RoomHeader.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { StyleSheet, Text, useWindowDimensions, View } from 'react-native'; +import { AccessibilityInfo, findNodeHandle, StyleSheet, Text, useWindowDimensions, View } from 'react-native'; import { TouchableOpacity } from 'react-native-gesture-handler'; import { useResponsiveLayout } from '../../lib/hooks/useResponsiveLayout/useResponsiveLayout'; @@ -83,6 +83,10 @@ interface IRoomHeader { abacAttributes?: ISubscription['abacAttributes']; } +export interface IRoomHeaderRef { + focus: () => void; +} + const SubTitle = React.memo(({ usersTyping, subtitle, renderFunc, scale }: TRoomHeaderSubTitle) => { const { colors } = useTheme(); const fontSize = getSubTitleSize(scale); @@ -131,27 +135,44 @@ const HeaderTitle = React.memo(({ title, tmid, prid, scale, testID }: TRoomHeade return ; }); -const Header = React.memo( - ({ - title, - subtitle, - parentTitle, - type, - status, - width, - height, - roomUserId, - prid, - tmid, - onPress, - isGroupChat, - teamMain, - testID, - usersTyping = [], - sourceType, - disabled, - abacAttributes - }: IRoomHeader) => { +const Header = React.forwardRef( + ( + { + title, + subtitle, + parentTitle, + type, + status, + width, + height, + roomUserId, + prid, + tmid, + onPress, + isGroupChat, + teamMain, + testID, + usersTyping = [], + sourceType, + disabled, + abacAttributes + }: IRoomHeader, + ref + ) => { + const headerRef = React.useRef(null); + React.useImperativeHandle( + ref, + () => ({ + focus: () => { + const nodeHandle = headerRef.current ? findNodeHandle(headerRef.current) : null; + if (nodeHandle) { + AccessibilityInfo.setAccessibilityFocus(nodeHandle); + } + } + }), + [] + ); + const statusAccessibilityLabel = useStatusAccessibilityLabel({ isGroupChat, prid, @@ -197,6 +218,7 @@ const Header = React.memo( return ( { - let subtitle: string | undefined; - let statusVisitor: TUserStatus | undefined; - let statusText: string | undefined; - const { width, height } = useResponsiveLayout(); + React.forwardRef( + ( + { + isGroupChat, + onPress, + parentTitle, + prid, + roomUserId, + subtitle: subtitleProp, + teamMain, + testID, + title, + tmid, + type, + sourceType, + visitor, + disabled, + abacAttributes + }: IRoomHeaderContainerProps, + ref + ) => { + let subtitle: string | undefined; + let statusVisitor: TUserStatus | undefined; + let statusText: string | undefined; + const { width, height } = useResponsiveLayout(); - const connecting = useSelector((state: IApplicationState) => state.meteor.connecting || state.server.loading); - const usersTyping = useSelector((state: IApplicationState) => state.usersTyping, shallowEqual); - const connected = useSelector((state: IApplicationState) => state.meteor.connected); - const activeUser = useSelector( - (state: IApplicationState) => (roomUserId ? state.activeUsers?.[roomUserId] : undefined), - shallowEqual - ); + const connecting = useSelector((state: IApplicationState) => state.meteor.connecting || state.server.loading); + const usersTyping = useSelector((state: IApplicationState) => state.usersTyping, shallowEqual); + const connected = useSelector((state: IApplicationState) => state.meteor.connected); + const activeUser = useSelector( + (state: IApplicationState) => (roomUserId ? state.activeUsers?.[roomUserId] : undefined), + shallowEqual + ); - if (connecting) { - subtitle = I18n.t('Connecting'); - } else if (!connected) { - subtitle = I18n.t('Waiting_for_network'); - } else { - subtitle = subtitleProp; - } + if (connecting) { + subtitle = I18n.t('Connecting'); + } else if (!connected) { + subtitle = I18n.t('Waiting_for_network'); + } else { + subtitle = subtitleProp; + } - if (connected) { - if ((type === 'd' || (tmid && roomUserId)) && activeUser) { - const { statusText: statusTextActiveUser } = activeUser; - statusText = statusTextActiveUser; - } else if (type === 'l' && visitor?.status) { - ({ status: statusVisitor } = visitor); + if (connected) { + if ((type === 'd' || (tmid && roomUserId)) && activeUser) { + const { statusText: statusTextActiveUser } = activeUser; + statusText = statusTextActiveUser; + } else if (type === 'l' && visitor?.status) { + ({ status: statusVisitor } = visitor); + } } - } - return ( - - ); - } + return ( + + ); + } + ) ); export default RoomHeaderContainer; +export type { IRoomHeaderRef }; diff --git a/app/containers/UIKit/__snapshots__/UiKitMessage.test.tsx.snap b/app/containers/UIKit/__snapshots__/UiKitMessage.test.tsx.snap index 7afc636e519..5e2e329aa8b 100644 --- a/app/containers/UIKit/__snapshots__/UiKitMessage.test.tsx.snap +++ b/app/containers/UIKit/__snapshots__/UiKitMessage.test.tsx.snap @@ -6,42 +6,54 @@ exports[`Story Snapshots: ActionButton should match snapshot 1`] = ` + + + + + + + + + + + + + + + + + + + + + + { onPress={reactionInit} key='message-add-reaction' testID='message-add-reaction' + accessibilityRole='button' + accessibilityLabel='Add reaction' style={[styles.reactionButton, { backgroundColor: themes[theme].surfaceRoom }]} hitSlop={BUTTON_HIT_SLOP}> @@ -61,6 +63,9 @@ const Reaction = React.memo(({ reaction, getCustomEmoji, theme }: IMessageReacti onLongPress={onReactionLongPress} key={reaction.emoji} testID={`message-reaction-${reaction.emoji}`} + accessibilityRole='button' + accessibilityLabel={`${reaction.emoji}, ${reaction.usernames.length}`} + accessibilityState={{ selected: reacted }} style={[styles.reactionButton, { backgroundColor: reacted ? themes[theme].surfaceNeutral : themes[theme].surfaceRoom }]} hitSlop={BUTTON_HIT_SLOP}> { 'use memo'; const { theme, colors } = useTheme(); - const { threadBadgeColor, toggleFollowThread, user, replies } = useContext(MessageContext); + const { threadBadgeColor, toggleFollowThread, user, replies, onThreadPress } = useContext(MessageContext); const backgroundColor = threadBadgeColor ? colors.badgeBackgroundLevel2 : colors.buttonBackgroundSecondaryDefault; const textColor = threadBadgeColor || theme !== 'light' ? colors.fontWhite : colors.fontPureBlack; @@ -24,9 +25,14 @@ const Thread = React.memo( return ( - + {I18n.t('View_Thread')} - + void; } +const KeyboardPressable = withKeyboardFocus(Pressable); + const RCTouchable: React.FC = React.memo(({ children, ...props }) => { 'use memo'; const { onLongPress } = useContext(MessageContext); + const { onHoverIn, onHoverOut, ...rest } = props; return ( - + {children} - + ); }); diff --git a/app/containers/message/__snapshots__/Message.test.tsx.snap b/app/containers/message/__snapshots__/Message.test.tsx.snap index eb164c03554..c498fed247d 100644 --- a/app/containers/message/__snapshots__/Message.test.tsx.snap +++ b/app/containers/message/__snapshots__/Message.test.tsx.snap @@ -1264,181 +1264,206 @@ exports[`Story Snapshots: AttachmentWithTextAndLink should match snapshot 1`] = } } > - - - Rocket.Chat - - - - + Rocket.Chat + + + - Rocket.Chat - - - , the best open source chat + + Rocket.Chat + + + , the best open source chat + - + - + - - + - + - + -  - - - +  + + + + /> + - + @@ -2004,181 +2054,206 @@ exports[`Story Snapshots: AttachmentWithTextAndLinkLargeFont should match snapsh } } > - - - Rocket.Chat - - - - + Rocket.Chat + + + - Rocket.Chat - - - , the best open source chat + + Rocket.Chat + + + , the best open source chat + - + - + - - + - + - + -  - - - +  + + + + /> + - + @@ -8658,117 +8758,142 @@ exports[`Story Snapshots: Broadcast should match snapshot 1`] = ` } } > - - -  - - + - Reply - + +  + + + Reply + + - + @@ -9219,117 +9344,142 @@ exports[`Story Snapshots: BroadcastLargeFont should match snapshot 1`] = ` } } > - - -  - - + - Reply - + +  + + + Reply + + - + @@ -9779,150 +9929,175 @@ exports[`Story Snapshots: CollapsedAttachments should match snapshot 1`] = ` } } > - + + + Title collapsed + + + + - Title collapsed +  - - -  - - - + @@ -10307,178 +10482,132 @@ exports[`Story Snapshots: CollapsedAttachments should match snapshot 1`] = ` } } > - - - Title collapsed - - - - - - Title collapsed - + Title collapsed - - - + - Field 1 + + + Title collapsed + + + + + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - + - + + + Title collapsed + + + + - Title collapsed +  - - -  - - - + @@ -11682,178 +11907,132 @@ exports[`Story Snapshots: CollapsedAttachmentsLargeFont should match snapshot 1` } } > - - - Title collapsed - - - - - - Title collapsed - + Title collapsed - - - + - Field 1 + + + Title collapsed + + + + + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - + - + + + Collapsed attachment block + + + + - Collapsed attachment block +  - - -  - - - + @@ -13059,178 +13334,132 @@ exports[`Story Snapshots: CollapsibleAttachmentWithText should match snapshot 1` } } > - - - Collapsed attachment block - - - - - - This attachment text should NOT appear as plain text above the message or duplicate before the block. - + Collapsed attachment block - - - + - Field 1 + + + This attachment text should NOT appear as plain text above the message or duplicate before the block. + + + + + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - - - - Long field - - + Long field + + - This field value could also contribute to duplicate text when expanded. + + This field value could also contribute to duplicate text when expanded. + - + - + - + + + Collapsed attachment block + + + + - Collapsed attachment block +  - - -  - - - + @@ -14577,178 +14902,132 @@ exports[`Story Snapshots: CollapsibleAttachmentWithTextLargeFont should match sn } } > - - - Collapsed attachment block - - - - - - This attachment text should NOT appear as plain text above the message or duplicate before the block. - + Collapsed attachment block - - - + - Field 1 + + + This attachment text should NOT appear as plain text above the message or duplicate before the block. + + + + + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - - - - Long field - - + Long field + + - This field value could also contribute to duplicate text when expanded. + + This field value could also contribute to duplicate text when expanded. + - + - + - - + - - Field 1 - - + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - + - - + - - Field 1 - - + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - + - - + - - Field 1 - - + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - + @@ -16716,129 +17141,128 @@ exports[`Story Snapshots: ColoredAttachmentsLargeFont should match snapshot 1`] } } > - - + - - Field 1 - - + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - + - - + - - Field 1 - - + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - + - - + - - Field 1 - - + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - + @@ -17919,179 +18419,133 @@ exports[`Story Snapshots: CustomFields should match snapshot 1`] = ` } } > - - - rocket.cat - - - - - - Custom fields - + rocket.cat - - - + - Field 1 + + + Custom fields + + + + + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - + - - - rocket.cat - - - - - - Custom fields - + rocket.cat - - - + - Field 1 + + + Custom fields + + + + + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - - - - Field 3 - - + Field 3 + + - Value 3 + + Value 3 + - + - - - - Field 4 - - + Field 4 + + - Value 4 + + Value 4 + - + - - - - Field 5 - - + Field 5 + + - Value 5 + + Value 5 + - + - + - - - rocket.cat - - - - - - Custom fields - + rocket.cat - - - + - Field 1 + + + Custom fields + + + + + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - + - - - rocket.cat - - - - + rocket.cat + + + - Custom fields + + Custom fields + - - - + - - Field 1 - - + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - - - - Field 3 - - + Field 3 + + - Value 3 + + Value 3 + - + - - - - Field 4 - - + Field 4 + + - Value 4 + + Value 4 + - + - - - - Field 5 - - + Field 5 + + - Value 5 + + Value 5 + - + - + - - -  - - + - No messages yet - + +  + + + No messages yet + + - + - - -  - - + - 1 message - + +  + + + 1 message + + - + - - -  - - + - 10 messages - + +  + + + 10 messages + + - + - - -  - - + - +999 messages - + +  + + + +999 messages + + - + - - -  - - + - No messages yet - + +  + + + No messages yet + + - + - - -  - - + - 1 message - + +  + + + 1 message + + - + - - -  - - + - 10 messages - + +  + + + 10 messages + + - + - - -  - - + - +999 messages - + +  + + + +999 messages + + - + - + - -  - - + +  + + + - - -  - - + +  + + + @@ -38700,77 +39550,102 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` } } > - + - -  - - + +  + + + - - - 😂 - - - 1 - + + 😂 + + + 1 + + - - + - - + - 1 - + + + 1 + + - - + - - 🤔 - - - 1 - + + 🤔 + + + 1 + + - - + - -  - + +  + + - + @@ -40013,77 +40996,102 @@ exports[`Story Snapshots: Encrypted should match snapshot 1`] = ` } } > - + - -  - - + +  + + + - + - -  - - + +  + + + - - -  - - - +  + + + + - -  - - + +  + + + - + - -  - - + +  + + + - - -  - - + +  + + + - + - -  - - + +  + + + - - -  - - + +  + + + @@ -43116,77 +44299,102 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` } } > - + - -  - - + +  + + + - - - 😂 - - - 1 - + + 😂 + + + 1 + + - - + - - + - 1 - + + + 1 + + - - + - - 🤔 - - - 1 - + + 🤔 + + + 1 + + - - + - -  - + +  + + - + @@ -44429,77 +45745,102 @@ exports[`Story Snapshots: EncryptedLargeFont should match snapshot 1`] = ` } } > - + - -  - - + +  + + + - + - -  - - + +  + + + - - -  - - - +  + + + + - -  - - + +  + + + - + - -  - - + +  + + + - - -  - - + +  + + + - - -  - - + +  + + + - + - -  - - + +  + + + @@ -47396,77 +48912,102 @@ exports[`Story Snapshots: ErrorLargeFont should match snapshot 1`] = ` } } > - - -  - - + +  + + + - + - -  - - + +  + + + @@ -48155,129 +49721,154 @@ exports[`Story Snapshots: FileAttachmentsWithFilenames should match snapshot 1`] } } > - + + + test.py + + test.py - - test.py - - + - + + + Component.tsx + + Component.tsx - - Component.tsx - - + - + + + config.json + + config.json - - config.json - - + - - - main.go - - - - + main.go + + + - - This is the - - main + This is the - - - entry point for the application + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontWeight": "700", + "textAlign": "left", + } + } + > + + main + + + + entry point for the application + - + - + - + + + document.pdf + + document.pdf - - document.pdf - - + - + + + image.png + + image.png - - image.png - - + @@ -50917,129 +52633,154 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna } } > - + + + test.py + + test.py - - test.py - - + - + + + Component.tsx + + Component.tsx - - Component.tsx - - + - + + + config.json + + config.json - - config.json - - + - - - main.go - - - - + main.go + + + - - This is the - - main + This is the - - - entry point for the application + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontWeight": "700", + "textAlign": "left", + } + } + > + + main + + + + entry point for the application + - + - + - + + + document.pdf + + document.pdf - - document.pdf - - + - + + + image.png + + image.png - - image.png - - + @@ -53666,141 +55532,166 @@ exports[`Story Snapshots: FileAttachmentsWithFilenamesLargeFont should match sna } } > - - - + - File.pdf + + File.pdf + - + - + - - - + - File.pdf + + File.pdf + - + - + - + - -  - - + +  + + + - - -  - - + +  + + + - + - -  - - + +  + + + - - -  - - + +  + + + - - -  - - + +  + + + - + - -  - - + +  + + + - + - -  - - + +  + + + - + - -  - - + +  + + + - + - -  - - + +  + + + - + - -  - - + +  + + + - - -  - - + +  + + + - + - -  - - + +  + + + - + + +  + + + + -  - - - - -  - - - - -  +  + + + +  + + + - + - -  - - + +  + + + - + - -  - - + +  + + + - + + +  + + + + -  +  - - -  - - - - -  - - + +  + + + - - - rocket.cat - - - 10:00 AM - + + rocket.cat + + + 10:00 AM + + - + @@ -78171,111 +80562,128 @@ exports[`Story Snapshots: MessageWithNestedReplyAndFile should match snapshot 1` } } > - - - rocket.cat - - - + + rocket.cat + + - - Nested image from forwarded message + + Nested image from forwarded message + - - - - + - - - + + + + +  + + + -  - + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + - - + - + @@ -81658,162 +84099,187 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` } } > - - - I'm a very long long title and I'll break - - - - + I'm a very long long title and I'll break + + + - How are you? + + How are you? + - + - + - - - rocket.cat - - - - + rocket.cat + + + - How are you? - - - + How are you? + + - + > + + + - + - + - - - rocket.cat - - - - + rocket.cat + + + - How are you? - - - + How are you? + + - + > + + + - + - + - - - rocket.cat - - - + + rocket.cat + + - - What you think about this one? + + What you think about this one? + - - - - + - - - + + + + +  + + + -  - + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + - - + - + @@ -83999,111 +86565,128 @@ exports[`Story Snapshots: MessageWithReply should match snapshot 1`] = ` } } > - - - rocket.cat - - - + + rocket.cat + + - - Are you seeing this mario - - - + Are you seeing this mario + + - - - ? + > + + + + ? + - - - - + - - - + + + + +  + + + -  - + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + - - + - + @@ -84789,149 +87405,174 @@ exports[`Story Snapshots: MessageWithReplyAndFile should match snapshot 1`] = ` } } > - - - rocket.cat - + + rocket.cat + + + script.sh + + script.sh - - script.sh - - + - - - rocket.cat - - - config.yaml - - - - + rocket.cat + + + config.yaml + + + - - This is a configuration file with - - important + This is a configuration file with - - - settings + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontWeight": "700", + "textAlign": "left", + } + } + > + + important + + + + settings + - + - + - - - rocket.cat - + + rocket.cat + + + index.ts + + index.ts - - index.ts - - + - - - rocket.cat - + + rocket.cat + + + styles.css + + styles.css - - styles.css - - + - - - rocket.cat - + + rocket.cat + + + script.sh + + script.sh - - script.sh - - + - - - rocket.cat - - - config.yaml - - - - + rocket.cat + + + config.yaml + + + - - This is a configuration file with - - important + This is a configuration file with - - - settings + "backgroundColor": "transparent", + "fontFamily": "Inter", + "fontWeight": "700", + "textAlign": "left", + } + } + > + + important + + + + settings + - + - + - - - rocket.cat - + + rocket.cat + + + index.ts + + index.ts - - index.ts - - + - - - rocket.cat - + + rocket.cat + + + styles.css + + styles.css - - styles.css - - + - - - I'm a very long long title and I'll break - - - - + I'm a very long long title and I'll break + + + - How are you? + + How are you? + - + - + - - - rocket.cat - - - - + rocket.cat + + + - How are you? - - - + How are you? + + - + > + + + - + - + - - - rocket.cat - - - + + rocket.cat + + - - What you think about this one? + + What you think about this one? + - - - - + - - - + + + + +  + + + -  - + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + - - + - + @@ -90871,111 +93787,128 @@ exports[`Story Snapshots: MessageWithReplyLargeFont should match snapshot 1`] = } } > - - - rocket.cat - - - + + rocket.cat + + - - Are you seeing this mario - - - + Are you seeing this mario + + - - - ? + > + + + + ? + - - - - + - - - + + + + +  + + + -  - + "bottom": 8, + "flexDirection": "row", + "gap": 8, + "position": "absolute", + "right": 8, + } + } + /> + - - + - + @@ -91714,44 +94680,100 @@ exports[`Story Snapshots: MessageWithThread should match snapshot 1`] = ` } } > - - - View thread - - + + View thread + + + - - - View thread - - + + View thread + + + - - - 😂 - - - 1 - + + 😂 + + + 1 + + - - + - - + - 99 - + + + 99 + + - - + - - 🤔 - - - 999 - + + 🤔 + + + 999 + + - - + - - 🤔 - - - 9999 - + + 🤔 + + + 9999 + + - - + - -  - + +  + + - + @@ -100049,978 +103262,1221 @@ exports[`Story Snapshots: Reactions should match snapshot 1`] = ` } } > - - - + - 1 - + + + 1 + + - - + - - + - 1 - + + + 1 + + - - + - - + - 1 - + + + 1 + + - - + - - ❤️ - - - 1 - + + ❤️ + + + 1 + + - - + - - 🐶 - - - 1 - + + 🐶 + + + 1 + + - - + - - 😀 - - - 1 - + + 😀 + + + 1 + + - - + - - 😬 - - - 1 - + + 😬 + + + 1 + + - - + - - 😁 - - - 1 - + + 😁 + + + 1 + + - - + - -  - + +  + + - + @@ -101472,530 +104928,665 @@ exports[`Story Snapshots: ReactionsLargeFont should match snapshot 1`] = ` } } > - - - 😂 - - - 1 - + + 😂 + + + 1 + + - - + - - + - 99 - + + + 99 + + - - + - - 🤔 - - - 999 - + + 🤔 + + + 999 + + - - + - - 🤔 - - - 9999 - + + 🤔 + + + 9999 + + - - + - -  - + +  + + - + @@ -102434,978 +106025,1221 @@ exports[`Story Snapshots: ReactionsLargeFont should match snapshot 1`] = ` } } > - - - + - 1 - + + + 1 + + - - + - - + - 1 - + + + 1 + + - - + + - - - + - 1 - + + + 1 + + - - + - - ❤️ - - - 1 - + + ❤️ + + + 1 + + - - + - - 🐶 - - - 1 - + + 🐶 + + + 1 + + - - + - - 😀 - - - 1 - + + 😀 + + + 1 + + - - + - - 😬 - - - 1 - + + 😬 + + + 1 + + - - + - - 😁 - - - 1 - + + 😁 + + + 1 + + - - + - -  - + +  + + - + @@ -103856,44 +107690,100 @@ exports[`Story Snapshots: SequentialThreadMessagesFollowingThreadButton should m } } > - - - View thread - - + + View thread + + + - - - View thread - - + + View thread + + + + + + + - - - Title - - - - + Title + + + - Image text + + Image text + - + - - + transition={null} + width={80} + /> + - + - - - Title - - - - + Title + + + - Image text + + Image text + - + - - + transition={null} + width={80} + /> + - + - - - rocket.cat - - - - - - Custom fields - + rocket.cat - - - + - Field 1 + + + Custom fields + + + + + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - + - - - rocket.cat - - - - - - Custom fields 2 - + rocket.cat - - - + - Field 1 + + + Custom fields 2 + + + + + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - + - - - rocket.cat - - - - - - Custom fields - + rocket.cat - - - + - Field 1 + + + Custom fields + + + + + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - + - - - rocket.cat - - - - - - Custom fields 2 - + rocket.cat - - - + - Field 1 + + + Custom fields 2 + + + + + Field 1 + + - Value 1 + + Value 1 + - + - - - - Field 2 - - + Field 2 + + - Value 2 + + Value 2 + - + - + - - - Rocket.Chat - Free, Open Source, Enterprise Team Chat - - + - Rocket.Chat is the leading open source team chat software solution. Free, unlimited and completely customizable with on-premises and SaaS cloud hosting. - + + Rocket.Chat - Free, Open Source, Enterprise Team Chat + + + Rocket.Chat is the leading open source team chat software solution. Free, unlimited and completely customizable with on-premises and SaaS cloud hosting. + + - - + - - Google - - + - Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. - + + Google + + + Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. + + - + @@ -130500,7 +134702,7 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={451} + handlerTag={443} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -130627,7 +134829,7 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={452} + handlerTag={444} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -130847,104 +135049,129 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` - - - Google - - + - Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. - + + Google + + + Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. + + - + @@ -131072,104 +135299,129 @@ exports[`Story Snapshots: URL should match snapshot 1`] = ` } } > - - - Google - - + - Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. - + + Google + + + Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. + + - + @@ -131315,7 +135567,7 @@ exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={453} + handlerTag={445} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -131442,7 +135694,7 @@ exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={454} + handlerTag={446} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -131559,104 +135811,129 @@ exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` } } > - - - Google - - + - Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. - + + Google + + + Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. + + - + @@ -131782,7 +136059,7 @@ exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={455} + handlerTag={447} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -131909,7 +136186,7 @@ exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={456} + handlerTag={448} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -132026,47 +136303,72 @@ exports[`Story Snapshots: URLImagePreview should match snapshot 1`] = ` } } > - + > + + @@ -132205,7 +136507,7 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={457} + handlerTag={449} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -132332,7 +136634,7 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={458} + handlerTag={450} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -132449,104 +136751,129 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` } } > - - - Google - - + - Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. - + + Google + + + Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. + + - + @@ -132672,7 +136999,7 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={459} + handlerTag={451} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -132799,7 +137126,7 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={460} + handlerTag={452} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -132916,47 +137243,72 @@ exports[`Story Snapshots: URLImagePreviewLargeFont should match snapshot 1`] = ` } } > - + > + + @@ -133095,7 +137447,7 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={461} + handlerTag={453} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -133222,7 +137574,7 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={462} + handlerTag={454} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -133339,202 +137691,252 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` } } > - - - Rocket.Chat - Free, Open Source, Enterprise Team Chat - - + - Rocket.Chat is the leading open source team chat software solution. Free, unlimited and completely customizable with on-premises and SaaS cloud hosting. - + + Rocket.Chat - Free, Open Source, Enterprise Team Chat + + + Rocket.Chat is the leading open source team chat software solution. Free, unlimited and completely customizable with on-premises and SaaS cloud hosting. + + - - + - - Google - - + - Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. - + + Google + + + Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. + + - + @@ -133660,7 +138062,7 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={463} + handlerTag={455} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -133787,7 +138189,7 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={464} + handlerTag={456} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -134007,104 +138409,129 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` - - - Google - - + - Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. - + + Google + + + Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. + + - + @@ -134232,104 +138659,129 @@ exports[`Story Snapshots: URLLargeFont should match snapshot 1`] = ` } } > - - - Google - - + - Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. - + + Google + + + Search the world's information, including webpages, images, videos and more. Google has many special features to help you find exactly what you're looking for. + + - + @@ -134475,7 +138927,7 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={465} + handlerTag={457} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -134602,7 +139054,7 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={466} + handlerTag={458} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -134922,7 +139374,7 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={467} + handlerTag={459} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -135049,7 +139501,7 @@ exports[`Story Snapshots: WithAlias should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={468} + handlerTag={460} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -135382,7 +139834,7 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={469} + handlerTag={461} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -135509,7 +139961,7 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={470} + handlerTag={462} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -135829,7 +140281,7 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={471} + handlerTag={463} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -135956,7 +140408,7 @@ exports[`Story Snapshots: WithAliasLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={472} + handlerTag={464} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -136289,7 +140741,7 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={473} + handlerTag={465} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -136416,7 +140868,7 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={474} + handlerTag={466} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -136850,7 +141302,7 @@ exports[`Story Snapshots: WithAudio should match snapshot 1`] = ` - - - + - File.pdf + + File.pdf + - + - + - - - + - File.pdf + + File.pdf + - + - + - - - + - File.pdf + + File.pdf + - + - + - - - + - File.pdf + + File.pdf + - + - + - - - + - File.pdf + + File.pdf + - + - + - - + - + - + -  - - - +  + + + + /> + - + @@ -144918,7 +149520,7 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={496} + handlerTag={488} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -145045,7 +149647,7 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={497} + handlerTag={489} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -145334,72 +149936,77 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` - - + - + - + -  - - - +  + + + + /> + - + + + + + + + + +  + + + + + + + + - - - - - - - -  - - - - - + @@ -146045,7 +150722,7 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={498} + handlerTag={490} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -146172,7 +150849,7 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={499} + handlerTag={491} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -146355,72 +151032,77 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` } } > - - + - + - + -  - - - +  + + + + /> + - - + - + - + - + -  - - - +  + + + + /> + - + @@ -146799,7 +151526,7 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={500} + handlerTag={492} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -146926,7 +151653,7 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={501} + handlerTag={493} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -147156,72 +151883,77 @@ exports[`Story Snapshots: WithImage should match snapshot 1`] = ` - - + - + - + + + + - - + @@ -147442,7 +152194,7 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={502} + handlerTag={494} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -147569,7 +152321,7 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={503} + handlerTag={495} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -147755,72 +152507,77 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` - - + - + - + -  - - - +  + + + + /> + - + @@ -148112,72 +152889,77 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` - - + - + - + -  - - - +  + + + + /> + - + @@ -148405,7 +153207,7 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={504} + handlerTag={496} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -148532,7 +153334,7 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={505} + handlerTag={497} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -148715,72 +153517,77 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` } } > - - + - + - + -  - - - +  + + + + /> + - - + - + - + - + -  - - - +  + + + + /> + - + @@ -149159,7 +154011,7 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={506} + handlerTag={498} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -149286,7 +154138,7 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={507} + handlerTag={499} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -149516,72 +154368,77 @@ exports[`Story Snapshots: WithImageLargeFont should match snapshot 1`] = ` - - + - + - + + + + - - + @@ -149802,7 +154679,7 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={508} + handlerTag={500} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -149929,7 +154806,7 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={509} + handlerTag={501} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -150159,125 +155036,150 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` - - + - + -  - + +  + + - + @@ -150476,125 +155378,150 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` - - + - + -  - + +  + + - + @@ -150729,7 +155656,7 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={510} + handlerTag={502} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -150856,7 +155783,7 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={511} + handlerTag={503} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -151046,125 +155973,150 @@ exports[`Story Snapshots: WithVideo should match snapshot 1`] = ` } } > - - + - + -  - + +  + + - + - - + - + -  - + +  + + - + @@ -151432,7 +156409,7 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={512} + handlerTag={504} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -151559,7 +156536,7 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={513} + handlerTag={505} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -151789,125 +156766,150 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` - - + - + -  - + +  + + - + @@ -152035,7 +157037,7 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={514} + handlerTag={506} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -152162,7 +157164,7 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={515} + handlerTag={507} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -152293,125 +157295,150 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` } } > - - + - + -  - + +  + + - + @@ -152539,7 +157566,7 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={516} + handlerTag={508} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -152666,7 +157693,7 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` collapsable={false} delayLongPress={600} enabled={true} - handlerTag={517} + handlerTag={509} handlerType="NativeViewGestureHandler" innerRef={null} onActiveStateChange={[Function]} @@ -152856,125 +157883,150 @@ exports[`Story Snapshots: WithVideoLargeFont should match snapshot 1`] = ` } } > - - + - + -  - + +  + + - + - - + - + -  - + +  + + - + diff --git a/app/containers/message/index.tsx b/app/containers/message/index.tsx index 31522bd7f5c..a352240850a 100644 --- a/app/containers/message/index.tsx +++ b/app/containers/message/index.tsx @@ -441,6 +441,7 @@ class MessageContainer extends React.Component { +const navigate = ({ + item, + isMasterDetail, + ...props +}: { + item: TGoRoomItem; + isMasterDetail: boolean; + focusHeaderOnOpen?: boolean; +}) => { const routeParams = { rid: item.rid, name: getRoomTitle(item), @@ -34,7 +42,8 @@ const navigate = ({ item, isMasterDetail, ...props }: { item: TGoRoomItem; isMas room: item, visitor: item.visitor, roomUserId: getUidDirectMessage(item), - ...props + ...props, + ...(isMasterDetail && props.focusHeaderOnOpen ? { focusHeaderOnOpen: true } : {}) }; const currentRoute = Navigation.getCurrentRoute() as any; @@ -91,6 +100,7 @@ export const goRoom = async ({ isMasterDetail: boolean; jumpToMessageId?: string; usedCannedResponse?: string; + focusHeaderOnOpen?: boolean; }): Promise => { if (!('id' in item) && item.t === SubscriptionType.DIRECT && item?.search) { // if user is using the search we need first to join/create room diff --git a/app/lib/native/KeyboardInversionA11yAndroid.ts b/app/lib/native/KeyboardInversionA11yAndroid.ts new file mode 100644 index 00000000000..6f26387272f --- /dev/null +++ b/app/lib/native/KeyboardInversionA11yAndroid.ts @@ -0,0 +1,25 @@ +import type { TurboModule } from 'react-native'; +import { TurboModuleRegistry } from 'react-native'; + +export interface KeyboardInversionState { + enabled: boolean; + scope: 'room-view' | null; +} + +interface Spec extends TurboModule { + enable(scope: 'room-view'): void; + disable(): void; + getState(): Promise; +} + +const NativeModule = TurboModuleRegistry.getEnforcing('KeyboardA11y'); + +export const enableRoomViewKeyboardA11y = (scope: 'room-view' = 'room-view') => { + NativeModule.enable(scope); +}; + +export const disableKeyboardA11y = () => { + NativeModule.disable(); +}; + +export const getKeyboardA11yState = () => NativeModule.getState(); diff --git a/app/stacks/types.ts b/app/stacks/types.ts index c8afff63863..fbda5ef6b66 100644 --- a/app/stacks/types.ts +++ b/app/stacks/types.ts @@ -41,6 +41,7 @@ export type ChatsStackParamList = { jumpToThreadId?: string; roomUserId?: string | null; usedCannedResponse?: string; + focusHeaderOnOpen?: boolean; status?: string; } | undefined; // Navigates back to RoomView already on stack diff --git a/app/views/CannedResponsesListView/__snapshots__/CannedResponseItem.test.tsx.snap b/app/views/CannedResponsesListView/__snapshots__/CannedResponseItem.test.tsx.snap index 660a41df1fd..267b24753aa 100644 --- a/app/views/CannedResponsesListView/__snapshots__/CannedResponseItem.test.tsx.snap +++ b/app/views/CannedResponsesListView/__snapshots__/CannedResponseItem.test.tsx.snap @@ -127,49 +127,57 @@ exports[`Story Snapshots: Itens should match snapshot 1`] = ` + + { title='Avatars' testID='display-pref-view-avatars' right={() => renderAvatarSwitch(showAvatar)} + onPress={toggleAvatar} additionalAccessibilityLabel={showAvatar} accessibilityRole='switch' /> diff --git a/app/views/RoomView/List/components/InvertedScrollView.tsx b/app/views/RoomView/List/components/InvertedScrollView.tsx index a441c25893c..b1a68c23896 100644 --- a/app/views/RoomView/List/components/InvertedScrollView.tsx +++ b/app/views/RoomView/List/components/InvertedScrollView.tsx @@ -1,24 +1,10 @@ -import React, { forwardRef } from 'react'; -import { ScrollView, requireNativeComponent, type ScrollViewProps, type ViewProps } from 'react-native'; +import type { ComponentType } from 'react'; +import { type ScrollViewProps } from 'react-native'; -const NativeInvertedScrollContentView = requireNativeComponent('InvertedScrollContentView'); +import RNLikeInvertedScrollView from './RNLikeInvertedScrollView'; -/** - * Android-only scroll component that wraps the standard ScrollView but uses a native content view - * that reverses accessibility traversal order. This fixes TalkBack reading inverted FlatList items - * in the wrong order, while preserving all ScrollView JS-side behavior (responder handling, - * momentum events, touch coordination). - */ -const InvertedScrollView = forwardRef((props, ref) => { - const { children, ...rest } = props; +interface InvertedScrollViewProps extends ScrollViewProps { + exitFocusNativeId?: string; +} - return ( - - {children} - - ); -}); - -InvertedScrollView.displayName = 'InvertedScrollView'; - -export default InvertedScrollView; +export default RNLikeInvertedScrollView as ComponentType; diff --git a/app/views/RoomView/List/components/List.tsx b/app/views/RoomView/List/components/List.tsx index 7ffe0135587..df39d0f3d0c 100644 --- a/app/views/RoomView/List/components/List.tsx +++ b/app/views/RoomView/List/components/List.tsx @@ -44,7 +44,9 @@ const List = ({ listRef, jumpToBottom, ...props }: IListProps) => { contentContainerStyle={styles.contentContainer} style={styles.list} inverted - renderScrollComponent={isIOS ? undefined : props => } + renderScrollComponent={ + isIOS ? undefined : props => + } removeClippedSubviews={isIOS} initialNumToRender={7} onEndReachedThreshold={0.5} diff --git a/app/views/RoomView/List/components/RNLikeInvertedScrollView.tsx b/app/views/RoomView/List/components/RNLikeInvertedScrollView.tsx new file mode 100644 index 00000000000..c402ac1d0b5 --- /dev/null +++ b/app/views/RoomView/List/components/RNLikeInvertedScrollView.tsx @@ -0,0 +1,228 @@ +import React from 'react'; +import { + Keyboard, + Platform, + StyleSheet, + TextInput, + type GestureResponderEvent, + type LayoutChangeEvent, + requireNativeComponent, + type ScrollViewProps, + type ViewProps +} from 'react-native'; + +interface InvertedScrollContentViewProps extends ViewProps { + isInvertedContent?: boolean; +} + +interface InvertedScrollViewNativeProps extends ScrollViewProps { + exitFocusNativeId?: string; +} + +interface Props extends ScrollViewProps { + exitFocusNativeId?: string; + scrollViewRef?: React.Ref; +} + +interface State { + layoutHeight: number | null; +} + +const NativeInvertedScrollView = requireNativeComponent('InvertedScrollView'); +const NativeInvertedScrollContentView = requireNativeComponent('InvertedScrollContentView'); + +const IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16; + +class RNLikeInvertedScrollView extends React.Component { + private scrollRef = React.createRef(); + private _keyboardMetrics: { height: number } | null = null; + private _isTouching = false; + private _lastMomentumScrollBeginTime = 0; + private _lastMomentumScrollEndTime = 0; + private _observedScrollSinceBecomingResponder = false; + private _subscriptionKeyboardDidShow?: { remove: () => void }; + private _subscriptionKeyboardDidHide?: { remove: () => void }; + + state: State = { + layoutHeight: null + }; + + componentDidMount() { + this._subscriptionKeyboardDidShow = Keyboard.addListener('keyboardDidShow', this.onKeyboardDidShow); + this._subscriptionKeyboardDidHide = Keyboard.addListener('keyboardDidHide', this.onKeyboardDidHide); + } + + componentWillUnmount() { + this._subscriptionKeyboardDidShow?.remove(); + this._subscriptionKeyboardDidHide?.remove(); + } + + private onKeyboardDidShow = (e: any) => { + this._keyboardMetrics = e?.endCoordinates ?? { height: 0 }; + }; + + private onKeyboardDidHide = (_e: any) => { + this._keyboardMetrics = null; + }; + + private isAnimating = () => { + const now = global.performance.now(); + const timeSinceLastMomentumScrollEnd = now - this._lastMomentumScrollEndTime; + return ( + timeSinceLastMomentumScrollEnd < IS_ANIMATING_TOUCH_START_THRESHOLD_MS || + this._lastMomentumScrollEndTime < this._lastMomentumScrollBeginTime + ); + }; + + private keyboardEventsAreUnreliable = () => Platform.OS === 'android' && Platform.Version < 30; + + private keyboardIsDismissible = () => { + const currentlyFocusedInput = TextInput.State.currentlyFocusedInput?.(); + const hasFocusedTextInput = currentlyFocusedInput != null; + const softKeyboardMayBeOpen = this._keyboardMetrics != null || this.keyboardEventsAreUnreliable(); + return hasFocusedTextInput && softKeyboardMayBeOpen; + }; + + private handleLayout = (e: LayoutChangeEvent) => { + if (this.props.invertStickyHeaders === true) { + this.setState({ layoutHeight: e.nativeEvent.layout.height }); + } + this.props.onLayout?.(e); + }; + + private handleContentOnLayout = (e: LayoutChangeEvent) => { + const { width, height } = e.nativeEvent.layout; + this.props.onContentSizeChange?.(width, height); + }; + + private handleScroll = (e: any) => { + this._observedScrollSinceBecomingResponder = true; + this.props.onScroll?.(e); + }; + + private handleMomentumScrollBegin = (e: any) => { + this._lastMomentumScrollBeginTime = global.performance.now(); + this.props.onMomentumScrollBegin?.(e); + }; + + private handleMomentumScrollEnd = (e: any) => { + this._lastMomentumScrollEndTime = global.performance.now(); + this.props.onMomentumScrollEnd?.(e); + }; + + private handleResponderGrant = (e: GestureResponderEvent) => { + this._observedScrollSinceBecomingResponder = false; + this.props.onResponderGrant?.(e); + }; + + private handleResponderRelease = (e: GestureResponderEvent) => { + this._isTouching = e.nativeEvent.touches.length !== 0; + this.props.onResponderRelease?.(e); + }; + + private handleResponderTerminationRequest = () => !this._observedScrollSinceBecomingResponder; + + private handleScrollShouldSetResponder = () => { + if (this.props.disableScrollViewPanResponder === true) { + return false; + } + return this._isTouching; + }; + + private handleStartShouldSetResponder = (e: GestureResponderEvent) => { + if (this.props.disableScrollViewPanResponder === true) { + return false; + } + const currentlyFocusedInput = TextInput.State.currentlyFocusedInput?.(); + if ( + this.props.keyboardShouldPersistTaps === 'handled' && + this.keyboardIsDismissible() && + e.target !== currentlyFocusedInput + ) { + return true; + } + return false; + }; + + private handleStartShouldSetResponderCapture = (e: GestureResponderEvent) => { + if (this.isAnimating()) { + return true; + } + if (this.props.disableScrollViewPanResponder === true) { + return false; + } + const { keyboardShouldPersistTaps } = this.props; + const keyboardNeverPersistTaps = !keyboardShouldPersistTaps || keyboardShouldPersistTaps === 'never'; + return keyboardNeverPersistTaps && this.keyboardIsDismissible() && e.target != null; + }; + + private setRefs = (instance: any) => { + this.scrollRef.current = instance; + const { scrollViewRef } = this.props; + if (!scrollViewRef) { + return; + } + if (typeof scrollViewRef === 'function') { + scrollViewRef(instance); + return; + } + (scrollViewRef as React.MutableRefObject).current = instance; + }; + + render() { + const { horizontal, children, style, contentContainerStyle, onContentSizeChange, ...rest } = this.props; + const contentStyle = [horizontal ? styles.contentContainerHorizontal : null, contentContainerStyle]; + const baseStyle = horizontal ? styles.baseHorizontal : styles.baseVertical; + const ScrollContainer = NativeInvertedScrollView as any; + + return ( + + + {children} + + + ); + } +} + +const styles = StyleSheet.create({ + baseVertical: { + flexGrow: 1, + flexShrink: 1, + flexDirection: 'column', + overflow: 'scroll' + }, + baseHorizontal: { + flexGrow: 1, + flexShrink: 1, + flexDirection: 'row', + overflow: 'scroll' + }, + contentContainerHorizontal: { + flexDirection: 'row' + } +}); + +const Wrapper = React.forwardRef((props, ref) => ); + +Wrapper.displayName = 'RNLikeInvertedScrollView'; + +export default Wrapper; diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 2622040f562..e00c2514783 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -29,7 +29,7 @@ import MessageErrorActions, { type IMessageErrorActions } from '../../containers import log, { events, logEvent } from '../../lib/methods/helpers/log'; import EventEmitter from '../../lib/methods/helpers/events'; import I18n from '../../i18n'; -import RoomHeader from '../../containers/RoomHeader'; +import RoomHeader, { type IRoomHeaderRef } from '../../containers/RoomHeader'; import ReactionsList from '../../containers/ReactionsList'; import { LISTENER } from '../../containers/Toast'; import { getBadgeColor, isBlocked, makeThreadName } from '../../lib/methods/helpers/room'; @@ -117,6 +117,7 @@ class RoomView extends React.Component { private jumpToMessageId?: string; private jumpToThreadId?: string; private messageComposerRef: React.RefObject; + private roomHeaderRef: React.RefObject; private joinCode: React.RefObject; // ListContainer component private list: React.RefObject; @@ -201,6 +202,7 @@ class RoomView extends React.Component { this.updateE2EEState(); this.messageComposerRef = React.createRef(); + this.roomHeaderRef = React.createRef(); this.list = React.createRef(); this.flatList = React.createRef(); this.joinCode = React.createRef(); @@ -217,7 +219,7 @@ class RoomView extends React.Component { } componentDidMount() { - const { navigation, dispatch } = this.props; + const { navigation, dispatch, isMasterDetail, route } = this.props; const { selectedMessages } = this.state; dispatch(clearInAppFeedback()); this.mounted = true; @@ -253,6 +255,16 @@ class RoomView extends React.Component { this.unsubscribeBlur = navigation.addListener('blur', () => { AudioManager.pauseAudio(); }); + this.unsubscribeFocus = navigation.addListener('focus', () => { + InteractionManager.runAfterInteractions(() => { + if (isMasterDetail && route?.params?.focusHeaderOnOpen) { + this.roomHeaderRef.current?.focus(); + navigation.setParams({ focusHeaderOnOpen: undefined }); + return; + } + this.messageComposerRef.current?.focus?.(); + }); + }); } shouldComponentUpdate(nextProps: IRoomViewProps, nextState: IRoomViewState) { @@ -534,6 +546,7 @@ class RoomView extends React.Component { ), headerTitle: () => ( { } } - return ; + return ( + + + + ); }; renderActions = () => { diff --git a/app/views/RoomsListView/index.tsx b/app/views/RoomsListView/index.tsx index 0ec091befb3..308ab92e9fe 100644 --- a/app/views/RoomsListView/index.tsx +++ b/app/views/RoomsListView/index.tsx @@ -73,7 +73,7 @@ const RoomsListView = memo(function RoomsListView() { logEvent(events.RL_GO_ROOM); stopSearch(); - goRoom({ item, isMasterDetail }); + goRoom({ item, isMasterDetail, focusHeaderOnOpen: true }); }; const renderItem = ({ item }: { item: IRoomItem }) => { diff --git a/app/views/SecurityPrivacyView.tsx b/app/views/SecurityPrivacyView.tsx index d87ac03a26c..94a5a0dbc31 100644 --- a/app/views/SecurityPrivacyView.tsx +++ b/app/views/SecurityPrivacyView.tsx @@ -95,14 +95,18 @@ const SecurityPrivacyView = ({ navigation }: ISecurityPrivacyViewProps): JSX.Ele title='Log_analytics_events' testID='security-privacy-view-analytics-events' right={() => } + onPress={() => toggleAnalyticsEvents(!analyticsEventsState)} additionalAccessibilityLabel={analyticsEventsState} + accessibilityRole='switch' /> } - additionalAccessibilityLabel={analyticsEventsState} + onPress={() => toggleCrashReport(!crashReportState)} + additionalAccessibilityLabel={crashReportState} + accessibilityRole='switch' /> diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8122f18684a..843148c6d24 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1640,6 +1640,30 @@ PODS: - Yoga - react-native-cookies (6.2.1): - React-Core + - react-native-external-keyboard (0.8.2): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - react-native-keyboard-controller (1.17.1): - DoubleConversion - glog @@ -2722,6 +2746,7 @@ DEPENDENCIES: - react-native-background-timer (from `../node_modules/react-native-background-timer`) - "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)" - "react-native-cookies (from `../node_modules/@react-native-cookies/cookies`)" + - react-native-external-keyboard (from `../node_modules/react-native-external-keyboard`) - react-native-keyboard-controller (from `../node_modules/react-native-keyboard-controller`) - react-native-mmkv (from `../node_modules/react-native-mmkv`) - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" @@ -2943,6 +2968,8 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-camera-roll/camera-roll" react-native-cookies: :path: "../node_modules/@react-native-cookies/cookies" + react-native-external-keyboard: + :path: "../node_modules/react-native-external-keyboard" react-native-keyboard-controller: :path: "../node_modules/react-native-keyboard-controller" react-native-mmkv: @@ -3148,6 +3175,7 @@ SPEC CHECKSUMS: react-native-background-timer: 4638ae3bee00320753647900b21260b10587b6f7 react-native-cameraroll: 23d28040c32ca8b20661e0c41b56ab041779244b react-native-cookies: d648ab7025833b977c0b19e142503034f5f29411 + react-native-external-keyboard: cfbd95f3c5623b008efe01c583f7787a86a1aee1 react-native-keyboard-controller: 9ec7ee23328c30251a399cffd8b54324a00343bf react-native-mmkv: ec96a16cd90e0d994d486c3993abf712186f7262 react-native-netinfo: 2e3c27627db7d49ba412bfab25834e679db41e21 diff --git a/package.json b/package.json index 4b3e53b0dd1..38e0bc2738d 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "react-native-device-info": "11.1.0", "react-native-easy-grid": "0.2.2", "react-native-easy-toast": "2.3.0", + "react-native-external-keyboard": "^0.8.2", "react-native-file-viewer": "2.1.4", "react-native-gesture-handler": "2.24.0", "react-native-image-crop-picker": "RocketChat/react-native-image-crop-picker#f028aac24373d05166747ef6d9e59bb037fe3224", diff --git a/patches/react-native-gesture-handler+2.24.0.patch b/patches/react-native-gesture-handler+2.24.0.patch new file mode 100644 index 00000000000..309449cc7c5 --- /dev/null +++ b/patches/react-native-gesture-handler+2.24.0.patch @@ -0,0 +1,57 @@ +diff --git a/node_modules/react-native-gesture-handler/apple/RNGestureHandlerButton.mm b/node_modules/react-native-gesture-handler/apple/RNGestureHandlerButton.mm +index 2296c39..0a872ba 100644 +--- a/node_modules/react-native-gesture-handler/apple/RNGestureHandlerButton.mm ++++ b/node_modules/react-native-gesture-handler/apple/RNGestureHandlerButton.mm +@@ -89,6 +89,52 @@ + } + + #if !TARGET_OS_OSX ++- (BOOL)canBecomeFocused ++{ ++ return self.userEnabled && self.enabled && self.userInteractionEnabled; ++} ++ ++- (BOOL)_isActivationKeyPress:(NSSet *)presses ++{ ++ if (@available(iOS 13.4, *)) { ++ for (UIPress *press in presses) { ++ if (press.key != nil && ++ (press.key.keyCode == UIKeyboardHIDUsageKeyboardSpacebar || ++ press.key.keyCode == UIKeyboardHIDUsageKeyboardReturnOrEnter)) { ++ return YES; ++ } ++ } ++ } ++ return NO; ++} ++ ++- (void)pressesBegan:(NSSet *)presses withEvent:(UIPressesEvent *)event ++{ ++ if ([self _isActivationKeyPress:presses]) { ++ [self sendActionsForControlEvents:UIControlEventTouchDown]; ++ } else { ++ [super pressesBegan:presses withEvent:event]; ++ } ++} ++ ++- (void)pressesEnded:(NSSet *)presses withEvent:(UIPressesEvent *)event ++{ ++ if ([self _isActivationKeyPress:presses]) { ++ [self sendActionsForControlEvents:UIControlEventTouchUpInside]; ++ } else { ++ [super pressesEnded:presses withEvent:event]; ++ } ++} ++ ++- (void)pressesCancelled:(NSSet *)presses withEvent:(UIPressesEvent *)event ++{ ++ if ([self _isActivationKeyPress:presses]) { ++ [self sendActionsForControlEvents:UIControlEventTouchCancel]; ++ } else { ++ [super pressesCancelled:presses withEvent:event]; ++ } ++} ++ + - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event + { + if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) { diff --git a/yarn.lock b/yarn.lock index 6d8d740fa76..a3915e4d825 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12062,6 +12062,11 @@ react-native-edge-to-edge@1.6.0: resolved "https://registry.yarnpkg.com/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz#2ba63b941704a7f713e298185c26cde4d9e4b973" integrity sha512-2WCNdE3Qd6Fwg9+4BpbATUxCLcouF6YRY7K+J36KJ4l3y+tWN6XCqAC4DuoGblAAbb2sLkhEDp4FOlbOIot2Og== +react-native-external-keyboard@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/react-native-external-keyboard/-/react-native-external-keyboard-0.8.2.tgz#2929460223f1ed3a1153e631ee28b062c66dac75" + integrity sha512-UMchkfHwC6p9hQOoMSO+OwX31VC4Pzjyul0XArH/eZLZS+GDDDpIn6q15QUwYUudrt+rXB+DtwSszTyxZbIY2g== + react-native-file-viewer@2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/react-native-file-viewer/-/react-native-file-viewer-2.1.4.tgz#987b2902f0f0ac87b42f3ac3d3037c8ae98f17a6"