Skip to content

Commit 77be0d2

Browse files
Merge remote-tracking branch 'origin/main' into fix/issue-27915
2 parents db9c04a + 43915a4 commit 77be0d2

64 files changed

Lines changed: 2541 additions & 689 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/desktop/src/window/DesktopWindow.test.ts‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,55 @@ describe("DesktopWindow", () => {
697697
}),
698698
);
699699

700+
it.effect("ignores input that arrives after the window is destroyed", () =>
701+
Effect.gen(function* () {
702+
const fakeWindow = makeFakeBrowserWindow();
703+
const createCount = yield* Ref.make(0);
704+
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
705+
const layer = layerTest({
706+
window: fakeWindow.window,
707+
createCount,
708+
mainWindow,
709+
});
710+
711+
yield* Effect.gen(function* () {
712+
const desktopWindow = yield* DesktopWindow.DesktopWindow;
713+
yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
714+
715+
const beforeInput = fakeWindow.webContentsListeners.get("before-input-event");
716+
const inputEvent = fakeWindow.webContentsListeners.get("input-event");
717+
if (!beforeInput || !inputEvent) {
718+
return yield* Effect.die("input listeners were not registered");
719+
}
720+
721+
// Electron throws on any member access of a destroyed BrowserWindow.
722+
fakeWindow.isDestroyed.mockReturnValue(true);
723+
Object.defineProperty(fakeWindow.window, "webContents", {
724+
get: () => {
725+
throw new TypeError("Object has been destroyed");
726+
},
727+
});
728+
729+
// A held Cmd+Q quits from the grace timer, so the trailing Q keyUp lands here.
730+
assert.doesNotThrow(() =>
731+
beforeInput(
732+
{ preventDefault: () => {} },
733+
{
734+
type: "keyUp",
735+
isAutoRepeat: false,
736+
key: "q",
737+
meta: true,
738+
control: false,
739+
alt: false,
740+
shift: false,
741+
},
742+
),
743+
);
744+
assert.doesNotThrow(() => inputEvent({}, { type: "gestureScrollEnd" }));
745+
}).pipe(Effect.provide(layer));
746+
}),
747+
);
748+
700749
// Chromium hands the main window's zoom level down to embedded preview
701750
// guests, so every app zoom has to put the preview browser back at its own
702751
// zoom or zooming the UI drags the previewed page with it.

‎apps/desktop/src/window/DesktopWindow.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,6 +705,7 @@ export const make = Effect.gen(function* () {
705705
});
706706
window.webContents.on("before-input-event", (event, input) => {
707707
quitShortcutHandler(event, input);
708+
if (window.isDestroyed()) return;
708709
// Cmd/Ctrl+W reaches the renderer; DevTools uses the native menu shortcut.
709710
window.webContents.setIgnoreMenuShortcuts(
710711
(environment.platform === "darwin" ? input.meta : input.control) &&
@@ -720,6 +721,7 @@ export const make = Effect.gen(function* () {
720721
}
721722
});
722723
window.webContents.on("input-event", (_event, input) => {
724+
if (window.isDestroyed()) return;
723725
if (input.type === "gestureScrollBegin") {
724726
window.webContents.send(TRACKPAD_SCROLL_BEGIN_CHANNEL);
725727
} else if (input.type === "gestureScrollEnd") {

‎apps/mobile/src/components/ui/ListRow.tsx‎

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
1-
import { Platform } from "react-native";
1+
import { Platform, View } from "react-native";
22

33
import { useResolveClassNames } from "uniwind";
44
import { SymbolView } from "./AppSymbol";
55
import { ListRowLayout, type ListRowContentProps } from "./ListRowLayout";
6+
import { RadioIndicator } from "./RadioIndicator";
67
import { RowPressable } from "./RowPressable";
78

8-
export function ListRow(props: ListRowContentProps & { readonly onPress: () => void }) {
9+
export function ListRow(
10+
props: ListRowContentProps & {
11+
readonly onPress: () => void;
12+
/** Marks the row as the chosen option while it still navigates. */
13+
readonly selected?: boolean;
14+
},
15+
) {
916
const rippleStyle = useResolveClassNames("bg-subtle-strong");
1017
return (
1118
<RowPressable
1219
accessibilityRole="button"
1320
accessibilityLabel={[props.label, props.description, props.detail].filter(Boolean).join(", ")}
14-
accessibilityState={{ disabled: Boolean(props.disabled) }}
21+
accessibilityState={{
22+
disabled: Boolean(props.disabled),
23+
...(props.selected === undefined ? {} : { checked: props.selected }),
24+
}}
1525
disabled={props.disabled}
1626
onPress={props.onPress}
1727
android_ripple={{ color: rippleStyle.backgroundColor }}
@@ -21,13 +31,34 @@ export function ListRow(props: ListRowContentProps & { readonly onPress: () => v
2131
<ListRowLayout
2232
{...props}
2333
accessory={
24-
<SymbolView
25-
name="chevron.right"
26-
size={16}
27-
tintColorClassName="accent-chevron"
28-
type="monochrome"
29-
weight="semibold"
30-
/>
34+
<View className="flex-row items-center gap-3">
35+
{props.selected === undefined ? null : (
36+
<View
37+
accessibilityElementsHidden
38+
importantForAccessibility="no-hide-descendants"
39+
className="size-6 items-center justify-center"
40+
>
41+
{Platform.OS === "android" ? (
42+
<RadioIndicator selected={props.selected} />
43+
) : props.selected ? (
44+
<SymbolView
45+
name="checkmark"
46+
size={18}
47+
tintColorClassName="accent-primary-text"
48+
type="monochrome"
49+
weight="semibold"
50+
/>
51+
) : null}
52+
</View>
53+
)}
54+
<SymbolView
55+
name="chevron.right"
56+
size={16}
57+
tintColorClassName="accent-chevron"
58+
type="monochrome"
59+
weight="semibold"
60+
/>
61+
</View>
3162
}
3263
/>
3364
</RowPressable>

0 commit comments

Comments
 (0)