Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
62e3571
fix(media): preview host files and stream videos across clients
juliusmarminge Sep 1, 2026
e67d3b1
fix(media): add video icons and paused previews
juliusmarminge Sep 1, 2026
1dc4c2c
fix(media): secure file streams and renew preview capabilities
juliusmarminge Sep 1, 2026
b45ff76
fix(media): validate stream bounds and resolve network-path links
juliusmarminge Sep 1, 2026
f42882d
fix(media): keep preview states and error context consistent
juliusmarminge Sep 1, 2026
bde2af5
fix(media): avoid weak validators on mutable video streams
juliusmarminge Sep 1, 2026
af0ccf3
test(media): remove low-value rendering and wiring tests
juliusmarminge Sep 1, 2026
d84a531
fix(media): open workspace media links in the file viewer
juliusmarminge Sep 1, 2026
ef04cfe
fix(mobile): preserve UNC hosts in markdown file links
juliusmarminge Sep 1, 2026
d936826
fix(server): describe media file failures
juliusmarminge Sep 1, 2026
5538b06
feat(media): add source tooltips and preview actions
juliusmarminge Sep 1, 2026
50b1f5d
fix(web): use player controls for video downloads
juliusmarminge Sep 1, 2026
8757284
fix(web): retain native recovery for failed videos
juliusmarminge Sep 1, 2026
74b1818
chore: merge main into media preview branch
juliusmarminge Sep 1, 2026
38a0c15
fix(web): refresh attachment URLs before opening
juliusmarminge Sep 1, 2026
cc36e83
fix(web): resolve protocol-relative media on desktop
juliusmarminge Sep 1, 2026
0977bfc
fix(web): preserve expanded video aspect ratios
juliusmarminge Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion apps/desktop/src/electron/ElectronProtocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,71 @@ describe("ElectronProtocol", () => {
}).pipe(Effect.provide(ElectronProtocol.layer)),
);

it.effect("forwards video byte ranges and streams the response without buffering the file", () =>
Effect.gen(function* () {
let handler: ((request: Request) => Promise<Response>) | undefined;
let controller: ReadableStreamDefaultController<Uint8Array> | undefined;
handleMock.mockImplementation((_scheme, nextHandler) => {
handler = nextHandler;
});
netFetchMock.mockResolvedValue(
new Response(
new ReadableStream<Uint8Array>({
start(nextController) {
controller = nextController;
controller.enqueue(new Uint8Array([2, 3]));
},
}),
{
status: 206,
headers: {
"content-type": "video/mp4",
"content-range": "bytes 2-5/10",
"accept-ranges": "bytes",
},
},
),
);

yield* Effect.scoped(
Effect.gen(function* () {
const protocol = yield* ElectronProtocol.ElectronProtocol;
yield* protocol.registerDesktopProtocol({
scheme: "t3code",
targetOrigin: new URL("http://127.0.0.1:3773/"),
backendOrigin: new URL("http://127.0.0.1:3773/"),
clerkFrontendApiHostname: undefined,
});
const response = yield* Effect.promise(() =>
handler!(
new Request("t3code://app/api/assets/signed-token/recording.mp4", {
headers: { range: "bytes=2-5" },
}),
),
);
const forwardedHeaders = new Headers(netFetchMock.mock.calls[0]?.[1]?.headers);
assert.equal(forwardedHeaders.get("range"), "bytes=2-5");
assert.equal(response.status, 206);
assert.equal(response.headers.get("content-range"), "bytes 2-5/10");
assert.equal(response.headers.get("accept-ranges"), "bytes");
assert.equal(response.headers.get("content-type"), "video/mp4");
const reader = response.body!.getReader();
assert.deepEqual(yield* Effect.promise(() => reader.read()), {
done: false,
value: new Uint8Array([2, 3]),
});
controller!.enqueue(new Uint8Array([4, 5]));
controller!.close();
assert.deepEqual(yield* Effect.promise(() => reader.read()), {
done: false,
value: new Uint8Array([4, 5]),
});
assert.isTrue((yield* Effect.promise(() => reader.read())).done);
}),
);
}).pipe(Effect.provide(ElectronProtocol.layer)),
);

it.effect("retries transient renderer target failures", () =>
Effect.gen(function* () {
let handler: ((request: Request) => Promise<Response>) | undefined;
Expand Down Expand Up @@ -225,7 +290,7 @@ describe("ElectronProtocol", () => {
"http:",
"https:",
]);
assert.deepEqual(directives["media-src"], ["'self'", "t3code:", "blob:"]);
assert.deepEqual(directives["media-src"], ["'self'", "t3code:", "blob:", "http:", "https:"]);
assert.deepEqual(directives["font-src"], ["'self'", "t3code:", "data:"]);
});
});
4 changes: 3 additions & 1 deletion apps/desktop/src/electron/ElectronProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function makeDesktopContentSecurityPolicy(input: DesktopProtocolRegistrat
`script-src ${scriptSources.join(" ")}`,
`connect-src ${connectSources.join(" ")}`,
`img-src 'self' ${input.scheme}: blob: data: http: https:`,
`media-src 'self' ${input.scheme}: blob:`,
`media-src 'self' ${input.scheme}: blob: http: https:`,
"style-src 'self' 'unsafe-inline'",
`font-src 'self' ${input.scheme}: data:`,
"worker-src 'self' blob:",
Expand Down Expand Up @@ -118,6 +118,7 @@ export function registerDesktopSchemePrivilegesSync(): void {
secure: true,
supportFetchAPI: true,
corsEnabled: true,
stream: true,
},
},
{
Expand All @@ -127,6 +128,7 @@ export function registerDesktopSchemePrivilegesSync(): void {
secure: true,
supportFetchAPI: true,
corsEnabled: true,
stream: true,
},
},
]);
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions apps/mobile/modules/t3-markdown-text/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"./types": "./src/SelectableMarkdownText.types.ts"
},
"peerDependencies": {
"@t3tools/client-runtime": "*",
"@t3tools/shared": "*",
"expo-asset": "*",
"expo-clipboard": "*",
"expo-haptics": "*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ const colors = {
terraform: "#693acf",
text: "#84848a",
typescript: "#1a85d4",
video: "#a631be",
vite: "#a631be",
vscode: "#1a85d4",
vue: "#199f43",
Expand All @@ -83,6 +84,7 @@ const customIcons = {
pnpm: "t3-file-icon-pnpm",
readme: "t3-file-icon-readme",
tsconfig: "t3-file-icon-tsconfig",
video: "t3-file-icon-video",
};

function symbolFromSprite(sprite, id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const MARKDOWN_FILE_ICON_SOURCES = {
text: require("../assets/file-icons/pierre_text.png"),
tsconfig: require("../assets/file-icons/pierre_tsconfig.png"),
typescript: require("../assets/file-icons/pierre_typescript.png"),
video: require("../assets/file-icons/pierre_video.png"),
vite: require("../assets/file-icons/pierre_vite.png"),
vscode: require("../assets/file-icons/pierre_vscode.png"),
vue: require("../assets/file-icons/pierre_vue.png"),
Expand Down
25 changes: 24 additions & 1 deletion apps/mobile/modules/t3-markdown-text/src/markdownLinks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import {
inlineCodeFilePathCandidate,
isConventionalFilePosition,
} from "@t3tools/client-runtime/markdown-links";
import { videoMimeType } from "@t3tools/shared/video";

import type { MARKDOWN_FILE_ICON_SOURCES } from "./markdownFileIcons.generated";

const WINDOWS_DRIVE_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
Expand Down Expand Up @@ -253,6 +259,11 @@ function normalizeDestination(value: string): string {
return trimmed.startsWith("<") && trimmed.endsWith(">") ? trimmed.slice(1, -1) : trimmed;
}

/** Native link and media APIs have no document scheme to inherit from protocol-relative URLs. */
export function normalizeNativeMarkdownUrl(value: string): string {
return value.startsWith("//") ? `https:${value}` : value;
}

function fileUrlTarget(href: string): { readonly path: string; readonly hash: string } | null {
try {
const parsed = new URL(href);
Expand Down Expand Up @@ -327,6 +338,7 @@ function looksLikeFilePath(value: string): boolean {
if (FILE_ICON_BY_NAME[value.replace(POSITION_SUFFIX_PATTERN, "").toLowerCase()]) {
return true;
}
if (isConventionalFilePosition(value)) return true;
return RELATIVE_FILE_PATH_PATTERN.test(value) || RELATIVE_FILE_NAME_PATTERN.test(value);
}

Expand All @@ -338,6 +350,7 @@ function fileLabel(value: string): string {

export function resolveMarkdownFileIcon(value: string): MarkdownFileIcon {
const basename = fileLabel(value).replace(POSITION_SUFFIX_PATTERN, "").toLowerCase();
if (videoMimeType({ name: basename, mimeType: "" }) !== null) return "video";
const exactIcon = FILE_ICON_BY_NAME[basename];
if (exactIcon) return exactIcon;
if (basename.startsWith("tsconfig.") && basename.endsWith(".json")) {
Expand All @@ -354,7 +367,7 @@ export function resolveMarkdownFileIcon(value: string): MarkdownFileIcon {
export function resolveMarkdownLinkPresentation(href: string): MarkdownLinkPresentation {
const normalized = normalizeDestination(href);
try {
const parsed = new URL(normalized);
const parsed = new URL(normalizeNativeMarkdownUrl(normalized));
if (parsed.protocol === "http:" || parsed.protocol === "https:") {
return {
kind: "external",
Expand Down Expand Up @@ -399,3 +412,13 @@ export function resolveMarkdownLinkPresentation(href: string): MarkdownLinkPrese
href: /^(?:mailto|tel):/i.test(normalized) ? normalized : null,
};
}

/** Backticks become file references only when the shared path heuristic recognizes the whole span. */
export function resolveMarkdownInlineCodePresentation(
content: string,
): Extract<MarkdownLinkPresentation, { readonly kind: "file" }> | null {
const candidate = inlineCodeFilePathCandidate(content);
if (candidate === null) return null;
const presentation = resolveMarkdownLinkPresentation(candidate);
Comment thread
juliusmarminge marked this conversation as resolved.
return presentation.kind === "file" ? presentation : null;
}
19 changes: 16 additions & 3 deletions apps/mobile/modules/t3-markdown-text/src/nativeMarkdownText.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import type { MarkdownNode } from "react-native-nitro-markdown/headless";

import type { SelectableMarkdownSkill } from "./SelectableMarkdownText.types";
import { resolveMarkdownLinkPresentation, type MarkdownFileIcon } from "./markdownLinks";
import {
resolveMarkdownInlineCodePresentation,
resolveMarkdownLinkPresentation,
type MarkdownFileIcon,
} from "./markdownLinks";

export interface NativeMarkdownTextRun {
readonly text: string;
Expand Down Expand Up @@ -283,8 +287,17 @@ function appendNode(
return appendRun(runs, textNodeContent(nodeTextContent(node)), context);
case "html_inline":
return appendRun(runs, inlineHtmlText(nodeTextContent(node)), context);
case "code_inline":
return appendRun(runs, nodeTextContent(node), { ...context, code: true });
case "code_inline": {
const content = nodeTextContent(node);
const presentation = context.href ? null : resolveMarkdownInlineCodePresentation(content);
return presentation
? appendRun(runs, presentation.label, {
...context,
href: presentation.href,
fileIcon: presentation.icon,
})
: appendRun(runs, content, { ...context, code: true });
}
case "soft_break":
return appendRun(runs, " ", context);
case "line_break":
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@
"@effect/vitest": "catalog:",
"@pierre/trees": "1.0.0-beta.4",
"@types/react": "~19.2.0",
"@types/react-dom": "~19.2.3",
"babel-preset-expo": "~57.0.9",
"happy-dom": "^20.12.0",
"tailwindcss": "^4.0.0",
"typescript": "catalog:"
},
Expand Down
14 changes: 10 additions & 4 deletions apps/mobile/src/components/FilePreviewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface ResolvedFilePreviewSource {
readonly uri: string;
readonly name?: string;
readonly sourceIdentifier?: string;
readonly srcFragment?: string;
}

export type FilePreviewSource = Omit<ResolvedFilePreviewSource, "uri"> &
Expand All @@ -40,13 +41,18 @@ function ResolvedFilePreview(props: {
(connection._tag === "None" || asset._tag === "Failure");
useEffect(() => Keyboard.dismiss(), []);
useEffect(() => {
if (uri === null && asset._tag === "Success") setUri(asset.url);
}, [uri, asset]);
if (uri === null && asset._tag === "Success") setUri(asset.url + (source.srcFragment ?? ""));
}, [uri, asset, source.srcFragment]);
useEffect(() => {
if (!failed) return;
Alert.alert("Could not open preview", "Reconnect to this environment and try again.");
Alert.alert(
"Could not open preview",
connection._tag === "None"
? "Reconnect to this environment and try again."
: "The file could not be loaded. It may have been moved or deleted.",
);
onRequestClose();
}, [failed]);
}, [failed, connection._tag]);
useEffect(() => {
if (!("attachment" in source)) return;
const controller = new AbortController();
Expand Down
Loading
Loading