Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions apps/desktop/src/electron/ElectronProtocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ describe("ElectronProtocol", () => {
);
assert.include(
response.headers.get("content-security-policy") ?? "",
"connect-src 'self' http: https: ws: wss:",
"connect-src 'self' blob: http: https: ws: wss:",
);
assert.include(
response.headers.get("content-security-policy") ?? "",
Expand Down Expand Up @@ -255,7 +255,14 @@ describe("ElectronProtocol", () => {
"https://clerk.t3.codes",
"https://challenges.cloudflare.com",
]);
assert.deepEqual(directives["connect-src"], ["'self'", "http:", "https:", "ws:", "wss:"]);
assert.deepEqual(directives["connect-src"], [
"'self'",
"blob:",
"http:",
"https:",
"ws:",
"wss:",
]);
assert.deepEqual(directives["img-src"], [
"'self'",
"t3code:",
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/electron/ElectronProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ export function makeDesktopContentSecurityPolicy(input: DesktopProtocolRegistrat
// the build-configured Clerk, relay, and OTLP endpoints. Those environment
// origins are not known when this response policy is created, so restrict
// connections by the network schemes the client supports instead of by host.
const connectSources = ["'self'", "http:", "https:", "ws:", "wss:"];
// GLTFLoader fetches embedded textures through blob URLs after parsing the model.
const connectSources = ["'self'", "blob:", "http:", "https:", "ws:", "wss:"];

return [
"default-src 'self'",
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const MENU_ACTION_CHANNEL = "desktop:menu-action";
export const PASTE_AS_TEXT_CHANNEL = "desktop:paste-as-text";
export const SNAP_SHOT_EVENT_CHANNEL = "desktop:snap-shot-event";
export const QUIT_SHORTCUT_CHANNEL = "desktop:quit-shortcut";
export const TRACKPAD_SCROLL_END_CHANNEL = "desktop:trackpad-scroll-end";
export const GET_WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:get-window-fullscreen-state";
export const WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:window-fullscreen-state";
export const DESKTOP_APP_ACTIVATION_READY_CHANNEL = "desktop:app-activation-ready";
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ contextBridge.exposeInMainWorld("desktopBridge", {
ipcRenderer.on(IpcChannels.SET_NOTIFICATION_BADGE_CHANNEL, handler);
return () => ipcRenderer.removeListener(IpcChannels.SET_NOTIFICATION_BADGE_CHANNEL, handler);
},
onTrackpadScrollEnd: (listener) => {
const handler = () => listener();
ipcRenderer.on(IpcChannels.TRACKPAD_SCROLL_END_CHANNEL, handler);
return () => ipcRenderer.removeListener(IpcChannels.TRACKPAD_SCROLL_END_CHANNEL, handler);
},
getSystemLocale: () => {
const result = ipcRenderer.sendSync(IpcChannels.GET_SYSTEM_LOCALE_CHANNEL);
return typeof result === "string" ? result : null;
Expand Down
25 changes: 25 additions & 0 deletions apps/desktop/src/window/DesktopWindow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import * as ElectronWindow from "../electron/ElectronWindow.ts";
import {
MENU_ACTION_CHANNEL,
SNAP_SHOT_EVENT_CHANNEL,
TRACKPAD_SCROLL_END_CHANNEL,
WINDOW_FULLSCREEN_STATE_CHANNEL,
} from "../ipc/channels.ts";
import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts";
Expand Down Expand Up @@ -709,6 +710,30 @@ describe("DesktopWindow", () => {
}),
);

it.effect("forwards native trackpad release to the renderer", () =>
Effect.gen(function* () {
const fakeWindow = makeFakeBrowserWindow();
const send = vi.spyOn(fakeWindow.window.webContents, "send");
const createCount = yield* Ref.make(0);
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
const layer = makeTestLayer({ window: fakeWindow.window, createCount, mainWindow });

yield* Effect.gen(function* () {
const desktopWindow = yield* DesktopWindow.DesktopWindow;
yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
const onInput = fakeWindow.webContentsListeners.get("input-event");
if (!onInput) return yield* Effect.die("input-event listener was not registered");
onInput({}, { type: "gestureScrollUpdate" });
assert.notInclude(
send.mock.calls.map(([channel]) => channel),
TRACKPAD_SCROLL_END_CHANNEL,
);
onInput({}, { type: "gestureScrollEnd" });
assert.isTrue(send.mock.calls.some(([channel]) => channel === TRACKPAD_SCROLL_END_CHANNEL));
}).pipe(Effect.provide(layer));
}),
);

// Chromium hands the main window's zoom level down to embedded preview
// guests, so every app zoom has to put the preview browser back at its own
// zoom or zooming the UI drags the previewed page with it.
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/window/DesktopWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
MENU_ACTION_CHANNEL,
QUIT_SHORTCUT_CHANNEL,
SNAP_SHOT_EVENT_CHANNEL,
TRACKPAD_SCROLL_END_CHANNEL,
WINDOW_FULLSCREEN_STATE_CHANNEL,
} from "../ipc/channels.ts";
import * as PreviewManager from "../preview/Manager.ts";
Expand Down Expand Up @@ -661,6 +662,9 @@ export const make = Effect.gen(function* () {
event.preventDefault();
}
});
window.webContents.on("input-event", (_event, input) => {
if (input.type === "gestureScrollEnd") window.webContents.send(TRACKPAD_SCROLL_END_CHANNEL);
});

window.on("page-title-updated", (event) => {
event.preventDefault();
Expand Down
37 changes: 36 additions & 1 deletion apps/web/src/components/RightPanelTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import { ScrollArea } from "~/components/ui/scroll-area";
import { PanelTabCloseButton } from "~/components/ui/panel-tab-close-button";
import { faviconUrlForOrigin } from "~/lib/favicon";
import { useTheme } from "~/hooks/useTheme";
import { useDeviceState } from "~/state/device";
import {
newestPullRequestSummary,
pullRequestEnvironment,
Expand Down Expand Up @@ -1244,7 +1245,17 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
</button>
}
/>
<TooltipPopup>{title}</TooltipPopup>
<TooltipPopup>
{surface.kind === "device" ? (
<DeviceTabTooltip
surface={surface}
environmentId={props.environmentId}
title={title}
/>
) : (
title
)}
</TooltipPopup>
</Tooltip>
)}
</div>
Expand Down Expand Up @@ -1421,3 +1432,27 @@ export function RightPanelTabs(props: RightPanelTabsProps) {
</PreviewPanelShell>
);
}

function DeviceTabTooltip(props: {
surface: Extract<RightPanelSurface, { kind: "device" }>;
environmentId: EnvironmentId | null;
title: string;
}) {
const target = props.surface.target;
const { state } = useDeviceState(target ? props.environmentId : null);
const device = target
? state.devices.find((entry) => entry.hostId === target.hostId && entry.id === target.deviceId)
: undefined;
const host = target ? state.hosts.find((entry) => entry.id === target.hostId) : undefined;
return (
<div className="flex flex-col gap-0.5">
<span>{props.title}</span>
{target ? (
<span className="text-muted-foreground">
{host?.label ?? "Device host"} ·{" "}
{device?.version ?? (target.platform === "ios" ? "iOS" : "Android")}
</span>
) : null}
</div>
);
}
Loading
Loading