authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-18 12:55:13-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-18 12:55:13-07:00
logdec0732281f5b7e787ad3a84e5cbe9a1607ea4ca
tree8938aa8a3f6c7d6ab94515e00ff84cee11df030a
parent429d66c13666c4e243065bbd02bee48e1eea2307
signature Signed by PGP key B5690EEEBB952194

refactor(mobile): isolate thread row appearance by platform (#12383)


3 files changed, 129 insertions(+), 124 deletions(-)

apps/mobile/src/features/threads/thread-list-v2-items.tsx+34-124
......@@ -1,3 +1,11 @@
1import {
2 THREAD_LIST_V2_MONO_FONT as MONO_FONT,
3 THREAD_LIST_V2_ROW_CONTENT_CLASS_NAME,
4 THREAD_LIST_V2_ROW_DIVIDERS,
5 selectedThreadRowColors,
6 getThreadListV2NewBranchMenuTitle,
7 getThreadListV2RowAppearance,
8} from "./thread-list-v2-row-appearance";
19import { RowPressable } from "../../components/RowPressable";
210import { CustomSnoozeSheet } from "./CustomSnoozeSheet";
311import { appAtomRegistry } from "../../state/atom-registry";
......@@ -13,7 +21,7 @@ import { canSnooze, resolveSnoozePresets } from "@t3tools/client-runtime/state/t
1321import { resolveSettledThreadTimestamp } from "@t3tools/client-runtime/state/thread-sort";
1422import type { MenuAction } from "@react-native-menu/menu";
1523import { memo, useCallback, useEffect, useMemo, useState, type ComponentProps } from "react";
16import { Alert, Platform, Pressable, useWindowDimensions, View } from "react-native";
24import { Alert, Pressable, useWindowDimensions, View } from "react-native";
1725import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable";
1826
1927import { SymbolView } from "../../components/AppSymbol";
......@@ -49,12 +57,6 @@ import { ThreadSearchMatchExcerpt } from "./thread-search-match";
4957 * hierarchy rather than card fills.
5058 */
5159
52const MONO_FONT = Platform.select({
53 ios: "Menlo",
54 android: "monospace",
55 default: "monospace",
56});
57
5860// Status hues follow the system-wide convention set by sidebar v1 and the
5961// Live Activity/widgets (amber approval, indigo input, sky working) so a
6062// thread reads the same color everywhere it surfaces.
......@@ -477,23 +479,9 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
477479 const pr = useThreadPr(thread);
478480
479481 const theme = useUniwindTheme();
480 const screenColor = theme["--color-screen"];
481 const drawerColor = theme["--color-drawer"];
482 const selectedBackgroundColor =
483 theme[Platform.OS === "android" ? "--color-thread-selected" : "--color-user-bubble"];
484482 const sidebarPane = props.pane === "sidebar";
485483 const selected = props.selected === true;
486 // The provider badge's border blends into the row's own surface, which
487 // differs by pane and (for the sidebar pane) selection: the sidebar row
488 // background becomes the selected fill or the drawer surface, while the
489 // flat "screen" pane rows always sit on the screen background.
490 const providerIconSurfaceColor = sidebarPane
491 ? selected
492 ? selectedBackgroundColor
493 : Platform.OS === "android"
494 ? screenColor
495 : drawerColor
496 : screenColor;
484 const rowAppearance = getThreadListV2RowAppearance(theme, sidebarPane, selected);
497485
498486 const status = resolveThreadListV2Status(thread);
499487 const statusLabel = STATUS_LABEL_BY_STATUS[status];
......@@ -788,11 +776,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
788776 <Text
789777 className={cn(
790778 "flex-1 text-sm font-t3-medium",
791 selected
792 ? Platform.OS === "android"
793 ? "text-thread-selected-foreground-muted"
794 : "text-user-bubble-foreground-muted"
795 : "text-foreground-muted",
779 selected ? selectedThreadRowColors.mutedForegroundClassName : "text-foreground-muted",
796780 )}
797781 numberOfLines={1}
798782 >
......@@ -811,9 +795,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
811795 className={cn(
812796 "text-xs tabular-nums",
813797 selected
814 ? Platform.OS === "android"
815 ? "text-thread-selected-foreground"
816 : "text-user-bubble-foreground"
798 ? selectedThreadRowColors.foregroundClassName
817799 : (statusLabel?.className ?? "text-foreground-tertiary"),
818800 )}
819801 >
......@@ -823,11 +805,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
823805 <Text
824806 className={cn(
825807 "mt-1 text-base font-t3-medium",
826 selected
827 ? Platform.OS === "android"
828 ? "text-thread-selected-foreground"
829 : "text-user-bubble-foreground"
830 : "text-foreground",
808 selected ? selectedThreadRowColors.foregroundClassName : "text-foreground",
831809 )}
832810 numberOfLines={2}
833811 >
......@@ -848,9 +826,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
848826 className={cn(
849827 "flex-1 text-xs",
850828 selected
851 ? Platform.OS === "android"
852 ? "text-thread-selected-foreground-muted"
853 : "text-user-bubble-foreground-muted"
829 ? selectedThreadRowColors.mutedForegroundClassName
854830 : "text-danger-foreground",
855831 )}
856832 numberOfLines={1}
......@@ -869,9 +845,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
869845 className={cn(
870846 "shrink text-xs",
871847 selected
872 ? Platform.OS === "android"
873 ? "text-thread-selected-foreground-muted"
874 : "text-user-bubble-foreground-muted"
848 ? selectedThreadRowColors.mutedForegroundClassName
875849 : "text-foreground-muted",
876850 )}
877851 numberOfLines={1}
......@@ -881,9 +855,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
881855 className={cn(
882856 "text-xs",
883857 selected
884 ? Platform.OS === "android"
885 ? "text-thread-selected-foreground-muted"
886 : "text-user-bubble-foreground-muted"
858 ? selectedThreadRowColors.mutedForegroundClassName
887859 : "text-foreground-muted",
888860 )}
889861 style={{ fontFamily: MONO_FONT }}
......@@ -897,9 +869,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
897869 className={cn(
898870 "text-xs",
899871 selected
900 ? Platform.OS === "android"
901 ? "text-thread-selected-foreground-muted"
902 : "text-user-bubble-foreground-muted"
872 ? selectedThreadRowColors.mutedForegroundClassName
903873 : "text-foreground-tertiary",
904874 )}
905875 >
......@@ -913,9 +883,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
913883 size={11}
914884 tintColorClassName={
915885 selected
916 ? Platform.OS === "android"
917 ? "accent-thread-selected-foreground-muted"
918 : "accent-user-bubble-foreground-muted"
886 ? selectedThreadRowColors.mutedIconTintClassName
919887 : "accent-foreground-tertiary"
920888 }
921889 />
......@@ -932,9 +900,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
932900 size={12}
933901 tintColorClassName={
934902 selected
935 ? Platform.OS === "android"
936 ? "accent-thread-selected-foreground"
937 : "accent-user-bubble-foreground"
903 ? selectedThreadRowColors.iconTintClassName
938904 : pr.state === null || pr.isDraft
939905 ? "accent-foreground-muted"
940906 : pr.state === "open"
......@@ -949,11 +915,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
949915 accessibilityLabel={pr.accessibilityLabel}
950916 className={cn(
951917 "text-xs",
952 selected
953 ? Platform.OS === "android"
954 ? "text-thread-selected-foreground"
955 : "text-user-bubble-foreground"
956 : pr.textClassName,
918 selected ? selectedThreadRowColors.foregroundClassName : pr.textClassName,
957919 )}
958920 style={{ fontFamily: MONO_FONT }}
959921 >
......@@ -968,7 +930,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
968930 displayName={props.providerInstance.displayName}
969931 accentColor={props.providerInstance.accentColor}
970932 showBadge={props.providerInstance.showBadge}
971 surfaceColor={providerIconSurfaceColor}
933 surfaceColor={rowAppearance.providerIconSurfaceColor}
972934 />
973935 ) : null}
974936 </View>
......@@ -979,14 +941,8 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
979941 variant === "card" ? (
980942 <RowPressable
981943 key={`${thread.environmentId}:${thread.id}`}
982 interactionClassName={
983 selected && (sidebarPane || Platform.OS === "android")
984 ? Platform.OS === "android"
985 ? "bg-thread-selected-foreground"
986 : "bg-user-bubble-foreground"
987 : "bg-primary"
988 }
989 className={sidebarPane || Platform.OS === "android" ? undefined : "bg-screen"}
944 interactionClassName={rowAppearance.interactionClassName}
945 className={rowAppearance.className}
990946 accessibilityHint={swipeAccessibilityHint}
991947 accessibilityLabel={
992948 props.hasQueuedMessages ? `${thread.title}, messages queued to send` : thread.title
......@@ -997,19 +953,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
997953 close();
998954 onSelectThread(thread);
999955 }}
1000 style={
1001 sidebarPane || Platform.OS === "android"
1002 ? {
1003 backgroundColor: selected
1004 ? selectedBackgroundColor
1005 : sidebarPane && Platform.OS !== "android"
1006 ? drawerColor
1007 : screenColor,
1008 borderRadius: Platform.OS === "android" ? 20 : SIDEBAR_V2_ROW_RADIUS,
1009 ...(sidebarPane ? { paddingHorizontal: 12, paddingVertical: 10 } : null),
1010 }
1011 : undefined
1012 }
956 style={rowAppearance.cardStyle}
1013957 >
1014958 {sidebarPane ? (
1015959 cardContent
......@@ -1019,10 +963,8 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
1019963 separates rows. The opaque screen background stays so swipe
1020964 actions reveal behind the row. */
1021965 <View>
1022 <View className={Platform.OS === "android" ? "px-3 py-2.5" : "px-5 py-2.5"}>
1023 {cardContent}
1024 </View>
1025 {Platform.OS !== "android" && props.showTrailingDivider !== false ? (
966 <View className={THREAD_LIST_V2_ROW_CONTENT_CLASS_NAME}>{cardContent}</View>
967 {THREAD_LIST_V2_ROW_DIVIDERS && props.showTrailingDivider !== false ? (
1026968 <View className="ml-5 h-px bg-border-subtle" />
1027969 ) : null}
1028970 </View>
......@@ -1031,36 +973,19 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
1031973 ) : (
1032974 <RowPressable
1033975 key={`${thread.environmentId}:${thread.id}`}
1034 interactionClassName={
1035 selected && (sidebarPane || Platform.OS === "android")
1036 ? Platform.OS === "android"
1037 ? "bg-thread-selected-foreground"
1038 : "bg-user-bubble-foreground"
1039 : "bg-primary"
1040 }
976 interactionClassName={rowAppearance.interactionClassName}
1041977 accessibilityHint={swipeAccessibilityHint}
1042978 accessibilityLabel={
1043979 props.hasQueuedMessages ? `${thread.title}, messages queued to send` : thread.title
1044980 }
1045981 accessibilityRole="button"
1046982 accessibilityState={{ selected }}
1047 className={sidebarPane || Platform.OS === "android" ? undefined : "bg-screen"}
983 className={rowAppearance.className}
1048984 onPress={() => {
1049985 close();
1050986 onSelectThread(thread);
1051987 }}
1052 style={
1053 sidebarPane || Platform.OS === "android"
1054 ? {
1055 backgroundColor: selected
1056 ? selectedBackgroundColor
1057 : sidebarPane && Platform.OS !== "android"
1058 ? drawerColor
1059 : screenColor,
1060 borderRadius: Platform.OS === "android" ? 20 : SIDEBAR_V2_ROW_RADIUS,
1061 }
1062 : undefined
1063 }
988 style={rowAppearance.style}
1064989 >
1065990 {/* Settled history recedes: dimmed favicon + muted title. */}
1066991 <View
......@@ -1084,11 +1009,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
10841009 <Text
10851010 className={cn(
10861011 "text-base",
1087 selected
1088 ? Platform.OS === "android"
1089 ? "text-thread-selected-foreground"
1090 : "text-user-bubble-foreground"
1091 : "text-foreground-muted",
1012 selected ? selectedThreadRowColors.foregroundClassName : "text-foreground-muted",
10921013 )}
10931014 numberOfLines={1}
10941015 >
......@@ -1107,9 +1028,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
11071028 className={cn(
11081029 "text-sm tabular-nums",
11091030 selected
1110 ? Platform.OS === "android"
1111 ? "text-thread-selected-foreground-muted"
1112 : "text-user-bubble-foreground-muted"
1031 ? selectedThreadRowColors.mutedForegroundClassName
11131032 : snoozedRow
11141033 ? "text-foreground-secondary"
11151034 : "text-foreground-tertiary",
......@@ -1131,15 +1050,9 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
11311050 )}
11321051 <ThreadSwipeable
11331052 threadKey={`${thread.environmentId}:${thread.id}`}
1134 backgroundColor={sidebarPane && Platform.OS !== "android" ? drawerColor : screenColor}
1053 backgroundColor={rowAppearance.swipeBackgroundColor}
11351054 compactActions={variant === "slim"}
1136 containerStyle={
1137 Platform.OS === "android"
1138 ? { borderRadius: 20, overflow: "hidden", marginHorizontal: 8, marginVertical: 2 }
1139 : sidebarPane
1140 ? { borderRadius: SIDEBAR_V2_ROW_RADIUS, overflow: "hidden" }
1141 : undefined
1142 }
1055 containerStyle={rowAppearance.swipeContainerStyle}
11431056 enableTrackpadSwipe
11441057 // Full swipe commits the advertised lifecycle action (Settle /
11451058 // Un-settle), never the secondary snooze action.
......@@ -1161,10 +1074,7 @@ export const ThreadListV2Row = memo(function ThreadListV2Row(props: {
11611074 ? [
11621075 {
11631076 id: "new-thread-on-branch",
1164 title:
1165 Platform.OS === "ios"
1166 ? "New thread on branch"
1167 : `New thread on ${thread.branch}`,
1077 title: getThreadListV2NewBranchMenuTitle(thread.branch),
11681078 image: "square.and.pencil",
11691079 },
11701080 ]
apps/mobile/src/features/threads/thread-list-v2-row-appearance.android.ts created+46
......@@ -0,0 +1,46 @@
1import type { ViewStyle } from "react-native";
2import type { MobileThemeVariables } from "../../lib/mobileTheme";
3
4export const THREAD_LIST_V2_MONO_FONT = "monospace";
5export const THREAD_LIST_V2_ROW_CONTENT_CLASS_NAME = "px-3 py-2.5";
6export const THREAD_LIST_V2_ROW_DIVIDERS = false;
7
8export const selectedThreadRowColors = {
9 foregroundClassName: "text-thread-selected-foreground",
10 mutedForegroundClassName: "text-thread-selected-foreground-muted",
11 iconTintClassName: "accent-thread-selected-foreground",
12 mutedIconTintClassName: "accent-thread-selected-foreground-muted",
13};
14
15export function getThreadListV2NewBranchMenuTitle(branch: string) {
16 return `New thread on ${branch}`;
17}
18
19export function getThreadListV2RowAppearance(
20 theme: MobileThemeVariables,
21 sidebarPane: boolean,
22 selected: boolean,
23) {
24 const selectedBackgroundColor = theme["--color-thread-selected"];
25 const style: ViewStyle = {
26 backgroundColor: selected ? selectedBackgroundColor : theme["--color-screen"],
27 borderRadius: 20,
28 };
29 const swipeContainerStyle: ViewStyle = {
30 borderRadius: 20,
31 overflow: "hidden",
32 marginHorizontal: 8,
33 marginVertical: 2,
34 };
35
36 return {
37 className: undefined,
38 interactionClassName: selected ? "bg-thread-selected-foreground" : "bg-primary",
39 style,
40 cardStyle: sidebarPane ? { ...style, paddingHorizontal: 12, paddingVertical: 10 } : style,
41 swipeContainerStyle,
42 swipeBackgroundColor: theme["--color-screen"],
43 providerIconSurfaceColor:
44 sidebarPane && selected ? selectedBackgroundColor : theme["--color-screen"],
45 };
46}
apps/mobile/src/features/threads/thread-list-v2-row-appearance.ts created+49
......@@ -0,0 +1,49 @@
1import type { ViewStyle } from "react-native";
2import type { MobileThemeVariables } from "../../lib/mobileTheme";
3
4export const THREAD_LIST_V2_MONO_FONT = "Menlo";
5export const THREAD_LIST_V2_ROW_CONTENT_CLASS_NAME = "px-5 py-2.5";
6export const THREAD_LIST_V2_ROW_DIVIDERS = true;
7
8export const selectedThreadRowColors = {
9 foregroundClassName: "text-user-bubble-foreground",
10 mutedForegroundClassName: "text-user-bubble-foreground-muted",
11 iconTintClassName: "accent-user-bubble-foreground",
12 mutedIconTintClassName: "accent-user-bubble-foreground-muted",
13};
14
15export function getThreadListV2NewBranchMenuTitle(_branch: string) {
16 return "New thread on branch";
17}
18
19export function getThreadListV2RowAppearance(
20 theme: MobileThemeVariables,
21 sidebarPane: boolean,
22 selected: boolean,
23) {
24 const selectedBackgroundColor = theme["--color-user-bubble"];
25 const style: ViewStyle | undefined = sidebarPane
26 ? {
27 backgroundColor: selected ? selectedBackgroundColor : theme["--color-drawer"],
28 borderRadius: 12,
29 }
30 : undefined;
31 const swipeContainerStyle: ViewStyle | undefined = sidebarPane
32 ? { borderRadius: 12, overflow: "hidden" }
33 : undefined;
34
35 return {
36 className: sidebarPane ? undefined : "bg-screen",
37 interactionClassName: selected && sidebarPane ? "bg-user-bubble-foreground" : "bg-primary",
38 style,
39 cardStyle: sidebarPane ? { ...style, paddingHorizontal: 12, paddingVertical: 10 } : undefined,
40 swipeContainerStyle,
41 swipeBackgroundColor: theme[sidebarPane ? "--color-drawer" : "--color-screen"],
42 // Provider badges blend into the surface beneath them.
43 providerIconSurfaceColor: sidebarPane
44 ? selected
45 ? selectedBackgroundColor
46 : theme["--color-drawer"]
47 : theme["--color-screen"],
48 };
49}