mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): render rich project work item content (#3100)
## Summary Project issues, pull requests, reviews, and commit details no longer flatten rich content into inert or plain text. They now share the message markdown and media pipeline, preserving NIP-92 `imeta` metadata so links, images, and videos render consistently. Commit bodies are fetched only when a single-commit detail view is opened, keeping list queries lightweight while exposing full context beside the diff. ### Related issue None found. ### Testing - Pre-push suite: `desktop-check`, `desktop-test`, `desktop-tauri-test`, `rust-tests`, and `mobile-test` - Project issue and pull request regression tests cover preserving attachment metadata on roots, updates, and comments - The project commit detail smoke scenario verifies linked text, images, and video in commit bodies Signed-off-by: Thomas Petersen <thomasp@squareup.com>
This commit is contained in:
@@ -25,6 +25,7 @@ pub struct ProjectRepoDiffInfo {
|
||||
pub files: Vec<ProjectRepoDiffFileInfo>,
|
||||
pub additions: usize,
|
||||
pub deletions: usize,
|
||||
pub commit_body: Option<String>,
|
||||
}
|
||||
|
||||
fn clean_target_ref(value: Option<String>) -> Option<String> {
|
||||
@@ -340,7 +341,25 @@ fn diff_from_repo(
|
||||
repo_dir: &std::path::Path,
|
||||
auth: &GitAuthConfig,
|
||||
range: &str,
|
||||
target_commit: Option<&str>,
|
||||
) -> Result<ProjectRepoDiffInfo, String> {
|
||||
let commit_body = target_commit
|
||||
.map(|commit| {
|
||||
run_git(
|
||||
&[
|
||||
"show",
|
||||
"--no-patch",
|
||||
"--format=%b",
|
||||
"--end-of-options",
|
||||
commit,
|
||||
],
|
||||
Some(repo_dir),
|
||||
auth,
|
||||
)
|
||||
.map(|body| body.trim_end().to_string())
|
||||
})
|
||||
.transpose()?
|
||||
.filter(|body| !body.is_empty());
|
||||
let numstat = run_git(&["diff", "--numstat", range], Some(repo_dir), auth)?;
|
||||
let files = parse_numstat(&numstat)
|
||||
.into_iter()
|
||||
@@ -375,6 +394,7 @@ fn diff_from_repo(
|
||||
Ok(ProjectRepoDiffInfo {
|
||||
additions: files.iter().map(|file| file.additions).sum(),
|
||||
deletions: files.iter().map(|file| file.deletions).sum(),
|
||||
commit_body,
|
||||
files,
|
||||
})
|
||||
}
|
||||
@@ -430,7 +450,12 @@ pub async fn get_project_repo_diff(
|
||||
diff_base_ref(&repo_dir, &auth, base_branch.as_deref()),
|
||||
),
|
||||
};
|
||||
diff_from_repo(&repo_dir, &auth, &range)
|
||||
let commit_body_ref = if target_ref.is_none() && base_branch.is_none() {
|
||||
target_commit.as_deref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
diff_from_repo(&repo_dir, &auth, &range, commit_body_ref)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("repo diff task failed: {error}"))?
|
||||
@@ -468,7 +493,12 @@ pub async fn get_project_local_repo_diff(
|
||||
base_commit.as_deref(),
|
||||
target_commit.as_deref(),
|
||||
);
|
||||
diff_from_repo(&repo_dir, &auth, &range).map(Some)
|
||||
let commit_body_ref = if base_commit.is_none() && base_branch.is_none() {
|
||||
target_commit.as_deref()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
diff_from_repo(&repo_dir, &auth, &range, commit_body_ref).map(Some)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("local repo diff task failed: {error}"))?
|
||||
|
||||
@@ -9,9 +9,9 @@ import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
|
||||
import { UserAvatar } from "@/shared/ui/UserAvatar";
|
||||
import type { ForumPost } from "@/shared/api/types";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { parseImetaTags } from "@/features/messages/lib/parseImeta";
|
||||
import { resolveMentionProps } from "@/shared/lib/resolveMentionNames";
|
||||
import { Markdown } from "@/shared/ui/markdown";
|
||||
import { parseImetaTags } from "@/shared/ui/markdown/parseImeta";
|
||||
|
||||
import { formatRelativeTime } from "../lib/time";
|
||||
import { DeleteActionMenu } from "./DeleteActionMenu";
|
||||
|
||||
@@ -11,9 +11,9 @@ import type { ForumThreadResponse, ThreadReply } from "@/shared/api/types";
|
||||
import { channelChrome } from "@/shared/layout/chromeLayout";
|
||||
import { cn } from "@/shared/lib/cn";
|
||||
import { useChannelNavigation } from "@/shared/context/ChannelNavigationContext";
|
||||
import { parseImetaTags } from "@/features/messages/lib/parseImeta";
|
||||
import { resolveMentionProps } from "@/shared/lib/resolveMentionNames";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { parseImetaTags } from "@/shared/ui/markdown/parseImeta";
|
||||
import { Markdown } from "@/shared/ui/markdown";
|
||||
import { Skeleton } from "@/shared/ui/skeleton";
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
*/
|
||||
|
||||
import type { BlobDescriptor } from "@/shared/api/tauri";
|
||||
import { parseImetaTags } from "./parseImeta";
|
||||
import { parseImetaTags } from "@/shared/ui/markdown/parseImeta";
|
||||
|
||||
export type ImetaMedia = BlobDescriptor & {
|
||||
/** Composer-only label used for attachment links; not emitted in imeta. */
|
||||
|
||||
@@ -3,7 +3,7 @@ import type * as React from "react";
|
||||
import { dimensionsFromDim } from "@/shared/ui/markdown/utils";
|
||||
import type { TimelineItem } from "./timelineItems";
|
||||
import type { TimelineMessage } from "../types";
|
||||
import { parseImetaTags } from "./parseImeta";
|
||||
import { parseImetaTags } from "@/shared/ui/markdown/parseImeta";
|
||||
|
||||
/**
|
||||
* Estimate a timeline row's rendered height so its `content-visibility`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { rewriteRelayUrl } from "@/shared/lib/mediaUrl";
|
||||
import { parseImetaTags } from "@/shared/ui/markdown/parseImeta";
|
||||
import type { TimelineMessage } from "../types";
|
||||
import { parseImetaTags } from "./parseImeta";
|
||||
|
||||
/**
|
||||
* Return non-message-media image URLs worth warming before a virtualized row
|
||||
|
||||
@@ -32,7 +32,7 @@ import { cn } from "@/shared/lib/cn";
|
||||
import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
import { UserAvatar } from "@/shared/ui/UserAvatar";
|
||||
import { useChannelNavigation } from "@/shared/context/ChannelNavigationContext";
|
||||
import { parseImetaTags } from "@/features/messages/lib/parseImeta";
|
||||
import { parseImetaTags } from "@/shared/ui/markdown/parseImeta";
|
||||
import { useMessageEmoji } from "@/features/messages/lib/useMessageEmoji";
|
||||
import { parseWaveMessageContent } from "@/features/messages/lib/waveMessage";
|
||||
import { resolveSnapshotSharedBy } from "@/features/messages/lib/snapshotSharedBy";
|
||||
|
||||
@@ -11,6 +11,7 @@ export type ProjectIssueStatus =
|
||||
export type ProjectIssueComment = {
|
||||
id: string;
|
||||
content: string;
|
||||
tags: string[][];
|
||||
author: string;
|
||||
createdAt: number;
|
||||
};
|
||||
@@ -19,6 +20,7 @@ export type ProjectIssue = {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
tags: string[][];
|
||||
author: string;
|
||||
createdAt: number;
|
||||
repoAddress: string | null;
|
||||
@@ -41,6 +43,7 @@ export const PROJECT_ISSUE_STATUS: {
|
||||
|
||||
export function getTag(event: RelayEvent, name: string): string | undefined;
|
||||
export function getAllTags(event: RelayEvent, name: string): string[];
|
||||
export function getImetaTags(event: RelayEvent): string[][];
|
||||
export function eventToProjectIssue(
|
||||
issue: RelayEvent,
|
||||
statusEvents?: RelayEvent[],
|
||||
|
||||
@@ -22,6 +22,10 @@ export function getAllTags(event, name) {
|
||||
.map((tag) => tag[1]);
|
||||
}
|
||||
|
||||
export function getImetaTags(event) {
|
||||
return event.tags.filter((tag) => tag[0] === "imeta");
|
||||
}
|
||||
|
||||
function repoOwnerFromAddress(repoAddress) {
|
||||
const owner = (repoAddress ?? "").split(":")[1] ?? "";
|
||||
return /^[a-fA-F0-9]{64}$/.test(owner) ? owner.toLowerCase() : null;
|
||||
@@ -80,6 +84,7 @@ function commentsForIssue(issueId, commentEvents) {
|
||||
.map((event) => ({
|
||||
id: event.id,
|
||||
content: event.content,
|
||||
tags: getImetaTags(event),
|
||||
author: event.pubkey,
|
||||
createdAt: event.created_at,
|
||||
}));
|
||||
@@ -101,6 +106,7 @@ export function eventToProjectIssue(
|
||||
id: issue.id,
|
||||
title,
|
||||
content: issue.content,
|
||||
tags: getImetaTags(issue),
|
||||
author: issue.pubkey,
|
||||
createdAt: issue.created_at,
|
||||
repoAddress: getTag(issue, "a") ?? null,
|
||||
|
||||
@@ -99,6 +99,32 @@ test("tag helpers drop malformed value-less tags", () => {
|
||||
assert.equal(issue.title, "Something is broken");
|
||||
});
|
||||
|
||||
test("preserves root and comment tags for rich content rendering", () => {
|
||||
const root = issueEvent({
|
||||
tags: [
|
||||
["a", REPO_ADDRESS],
|
||||
["subject", "Something is broken"],
|
||||
["imeta", "url https://relay.example/media/root.png", "m image/png"],
|
||||
],
|
||||
});
|
||||
const comment = {
|
||||
id: "comment-rich-content",
|
||||
kind: 1,
|
||||
pubkey: ATTACKER,
|
||||
created_at: 200,
|
||||
content: "",
|
||||
tags: [
|
||||
["e", root.id, "", "root"],
|
||||
["imeta", "url https://relay.example/media/comment.png", "m image/png"],
|
||||
],
|
||||
};
|
||||
|
||||
const issue = eventToProjectIssue(root, [], [comment]);
|
||||
|
||||
assert.deepEqual(issue.tags, [root.tags[2]]);
|
||||
assert.deepEqual(issue.comments[0].tags, [comment.tags[1]]);
|
||||
});
|
||||
|
||||
test("builds repository-scoped issue creation tags", () => {
|
||||
assert.deepEqual(
|
||||
buildGitIssueTags({
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { RelayEvent } from "@/shared/api/types";
|
||||
export type ProjectPullRequestUpdate = {
|
||||
id: string;
|
||||
content: string;
|
||||
tags: string[][];
|
||||
author: string;
|
||||
createdAt: number;
|
||||
commit: string | null;
|
||||
@@ -12,6 +13,7 @@ export type ProjectPullRequestUpdate = {
|
||||
export type ProjectPullRequestComment = {
|
||||
id: string;
|
||||
content: string;
|
||||
tags: string[][];
|
||||
author: string;
|
||||
createdAt: number;
|
||||
commit: string | null;
|
||||
@@ -70,6 +72,7 @@ export type ProjectPullRequest = {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
tags: string[][];
|
||||
author: string;
|
||||
createdAt: number;
|
||||
repoAddress: string | null;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { allowedActorsForRoot, getAllTags, getTag } from "./projectIssues.mjs";
|
||||
import {
|
||||
allowedActorsForRoot,
|
||||
getAllTags,
|
||||
getImetaTags,
|
||||
getTag,
|
||||
} from "./projectIssues.mjs";
|
||||
|
||||
// Updates and status changes rewrite the PR's tip commit, clone URLs, and
|
||||
// lifecycle state, so they are only honored when signed by the PR author or
|
||||
@@ -135,6 +140,7 @@ function eventToPullRequestUpdate(event) {
|
||||
return {
|
||||
id: event.id,
|
||||
content: event.content,
|
||||
tags: getImetaTags(event),
|
||||
author: event.pubkey,
|
||||
createdAt: event.created_at,
|
||||
commit: getTag(event, "c") ?? null,
|
||||
@@ -190,6 +196,7 @@ function eventToPullRequestComment(event) {
|
||||
return {
|
||||
id: event.id,
|
||||
content: event.content,
|
||||
tags: getImetaTags(event),
|
||||
author: event.pubkey,
|
||||
createdAt: event.created_at,
|
||||
commit: getTag(event, "c") ?? null,
|
||||
@@ -352,6 +359,7 @@ export function eventToProjectPullRequest(
|
||||
id: pullRequest.id,
|
||||
title,
|
||||
content: pullRequest.content,
|
||||
tags: getImetaTags(pullRequest),
|
||||
author: pullRequest.pubkey,
|
||||
createdAt: pullRequest.created_at,
|
||||
repoAddress: getTag(pullRequest, "a") ?? null,
|
||||
|
||||
@@ -96,6 +96,44 @@ test("accepts updates signed by the PR author", () => {
|
||||
assert.equal(pullRequest.updateCount, 1);
|
||||
});
|
||||
|
||||
test("preserves root, update, and comment tags for rich content rendering", () => {
|
||||
const root = pullRequestEvent({
|
||||
tags: [
|
||||
["a", REPO_ADDRESS],
|
||||
["subject", "Add feature"],
|
||||
["c", "1111111111111111111111111111111111111111"],
|
||||
["imeta", "url https://relay.example/media/root.png", "m image/png"],
|
||||
],
|
||||
});
|
||||
const update = updateEvent({
|
||||
pubkey: AUTHOR,
|
||||
createdAt: 200,
|
||||
commit: "2222222222222222222222222222222222222222",
|
||||
});
|
||||
update.tags.push([
|
||||
"imeta",
|
||||
"url https://relay.example/media/update.mp4",
|
||||
"m video/mp4",
|
||||
]);
|
||||
const comment = {
|
||||
id: "comment-rich-content",
|
||||
kind: 1,
|
||||
pubkey: ATTACKER,
|
||||
created_at: 250,
|
||||
content: "[Demo](https://relay.example/media/comment.png)",
|
||||
tags: [
|
||||
["e", root.id, "", "root"],
|
||||
["imeta", "url https://relay.example/media/comment.png", "m image/png"],
|
||||
],
|
||||
};
|
||||
|
||||
const pullRequest = eventToProjectPullRequest(root, [update], [comment]);
|
||||
|
||||
assert.deepEqual(pullRequest.tags, [root.tags[3]]);
|
||||
assert.deepEqual(pullRequest.updates[0].tags, [update.tags[3]]);
|
||||
assert.deepEqual(pullRequest.comments[0].tags, [comment.tags[1]]);
|
||||
});
|
||||
|
||||
test("accepts updates signed by the repo owner", () => {
|
||||
const update = updateEvent({
|
||||
pubkey: OWNER,
|
||||
|
||||
@@ -12,6 +12,7 @@ import type { ProjectRepoCommit, ProjectRepoDiff } from "@/shared/api/types";
|
||||
import { CopyCommitHashButton } from "./ProjectCommitCopyButton";
|
||||
import { ProfileIdentityButton } from "./ProjectProfileIdentity";
|
||||
import { ProjectDiffFilesPanel } from "./ProjectPullRequestFilesChangedPanel";
|
||||
import { ProjectRichContent } from "./ProjectRichContent";
|
||||
|
||||
function commitDateLabel(timestamp: number) {
|
||||
return new Date(timestamp * 1_000).toLocaleString(undefined, {
|
||||
@@ -98,6 +99,9 @@ export function ProjectCommitDetailPanel({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{diff?.commitBody ? (
|
||||
<ProjectRichContent content={diff.commitBody} />
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
<ProjectDiffFilesPanel
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { relativeTime } from "@/features/projects/lib/projectsViewHelpers";
|
||||
import type { ChannelMember } from "@/shared/api/types";
|
||||
import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
import { Markdown } from "@/shared/ui/markdown";
|
||||
import {
|
||||
ProjectFeedRow,
|
||||
ProjectFeedRowCluster,
|
||||
@@ -24,6 +23,7 @@ import {
|
||||
} from "./ProjectFeedRow";
|
||||
import { OverviewRailSection } from "./ProjectOverviewPanel";
|
||||
import { ProfileIdentityButton } from "./ProjectProfileIdentity";
|
||||
import { ProjectRichContent } from "./ProjectRichContent";
|
||||
|
||||
export function issueStatusClassName(status: ProjectIssue["status"]) {
|
||||
if (status === "Done") return "text-purple-400";
|
||||
@@ -214,11 +214,7 @@ function IssueDetail({
|
||||
</h3>
|
||||
</div>
|
||||
{issue.content ? (
|
||||
<Markdown
|
||||
className="text-sm"
|
||||
content={issue.content}
|
||||
interactive={false}
|
||||
/>
|
||||
<ProjectRichContent content={issue.content} tags={issue.tags} />
|
||||
) : null}
|
||||
</header>
|
||||
|
||||
@@ -238,11 +234,7 @@ function IssueDetail({
|
||||
role={relativeTime(item.createdAt)}
|
||||
/>
|
||||
</div>
|
||||
<Markdown
|
||||
className="text-sm"
|
||||
content={item.content}
|
||||
interactive={false}
|
||||
/>
|
||||
<ProjectRichContent content={item.content} tags={item.tags} />
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
import { relativeTime } from "@/features/projects/lib/projectsViewHelpers";
|
||||
import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import { normalizePubkey, truncatePubkey } from "@/shared/lib/pubkey";
|
||||
import { Markdown } from "@/shared/ui/markdown";
|
||||
import { ProjectRichContent } from "./ProjectRichContent";
|
||||
|
||||
function commentAuthor(
|
||||
pubkey: string,
|
||||
@@ -68,10 +68,9 @@ export function ProjectPullRequestInlineCommentThread({
|
||||
{relativeTime(comment.createdAt)}
|
||||
</span>
|
||||
</div>
|
||||
<Markdown
|
||||
className="text-sm"
|
||||
<ProjectRichContent
|
||||
content={comment.content}
|
||||
interactive={false}
|
||||
tags={comment.tags}
|
||||
/>
|
||||
</article>
|
||||
))}
|
||||
|
||||
@@ -36,7 +36,6 @@ import type { UserProfileLookup } from "@/features/profile/lib/identity";
|
||||
import { useIdentityQuery } from "@/shared/api/hooks";
|
||||
import type { ChannelMember } from "@/shared/api/types";
|
||||
import { normalizePubkey, truncatePubkey } from "@/shared/lib/pubkey";
|
||||
import { Markdown } from "@/shared/ui/markdown";
|
||||
import {
|
||||
ProjectFeedRow,
|
||||
ProjectFeedRowCluster,
|
||||
@@ -49,6 +48,7 @@ import {
|
||||
ProfileAuthorName,
|
||||
ProfileIdentityButton,
|
||||
} from "./ProjectProfileIdentity";
|
||||
import { ProjectRichContent } from "./ProjectRichContent";
|
||||
import { PullRequestReviewersRow } from "./PullRequestReviewersRow";
|
||||
import { PullRequestReviewCard } from "./PullRequestReviewCard";
|
||||
|
||||
@@ -639,10 +639,9 @@ function PullRequestDetail({
|
||||
<div>
|
||||
{pullRequest.content ? (
|
||||
<header className="p-4">
|
||||
<Markdown
|
||||
className="text-sm"
|
||||
<ProjectRichContent
|
||||
content={pullRequest.content}
|
||||
interactive={false}
|
||||
tags={pullRequest.tags}
|
||||
/>
|
||||
</header>
|
||||
) : null}
|
||||
@@ -670,9 +669,11 @@ function PullRequestDetail({
|
||||
) : null}
|
||||
</div>
|
||||
{update.content ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{update.content}
|
||||
</p>
|
||||
<ProjectRichContent
|
||||
className="text-sm text-muted-foreground"
|
||||
content={update.content}
|
||||
tags={update.tags}
|
||||
/>
|
||||
) : null}
|
||||
</article>
|
||||
))}
|
||||
@@ -829,10 +830,10 @@ function PullRequestDetail({
|
||||
</span>
|
||||
</div>
|
||||
{activityContent ? (
|
||||
<Markdown
|
||||
<ProjectRichContent
|
||||
className="mt-1 text-sm text-foreground/90"
|
||||
content={activityContent}
|
||||
interactive={false}
|
||||
tags={item.tags}
|
||||
/>
|
||||
) : null}
|
||||
{item.anchor ? (
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { Markdown } from "@/shared/ui/markdown";
|
||||
import { parseImetaTags } from "@/shared/ui/markdown/parseImeta";
|
||||
|
||||
/**
|
||||
* Renders project event content with the same link and media support as
|
||||
* messages while retaining NIP-92 attachment metadata from the source event.
|
||||
*/
|
||||
export function ProjectRichContent({
|
||||
className = "text-sm",
|
||||
content,
|
||||
tags,
|
||||
}: {
|
||||
className?: string;
|
||||
content: string;
|
||||
tags?: string[][];
|
||||
}) {
|
||||
const imetaByUrl = React.useMemo(
|
||||
() => (tags ? parseImetaTags(tags) : undefined),
|
||||
[tags],
|
||||
);
|
||||
|
||||
return (
|
||||
<Markdown className={className} content={content} imetaByUrl={imetaByUrl} />
|
||||
);
|
||||
}
|
||||
@@ -116,6 +116,7 @@ type RawProjectRepoDiff = {
|
||||
files: RawProjectRepoDiffFile[];
|
||||
additions: number;
|
||||
deletions: number;
|
||||
commit_body: string | null;
|
||||
};
|
||||
|
||||
function fromRawProjectRepoSnapshot(
|
||||
@@ -192,6 +193,7 @@ export async function getProjectRepoDiff(input: {
|
||||
return {
|
||||
additions: diff.additions,
|
||||
deletions: diff.deletions,
|
||||
commitBody: diff.commit_body,
|
||||
files: diff.files.map((file) => ({
|
||||
path: file.path,
|
||||
additions: file.additions,
|
||||
@@ -227,6 +229,7 @@ export async function getProjectLocalRepoDiff(input: {
|
||||
return {
|
||||
additions: diff.additions,
|
||||
deletions: diff.deletions,
|
||||
commitBody: diff.commit_body,
|
||||
files: diff.files.map((file) => ({
|
||||
path: file.path,
|
||||
additions: file.additions,
|
||||
|
||||
@@ -43,6 +43,7 @@ export type ProjectRepoDiff = {
|
||||
files: ProjectRepoDiffFile[];
|
||||
additions: number;
|
||||
deletions: number;
|
||||
commitBody: string | null;
|
||||
};
|
||||
|
||||
export type ProjectLocalRepoSnapshot = {
|
||||
|
||||
+9
-10
@@ -1,22 +1,21 @@
|
||||
export type ImetaEntry = {
|
||||
import type { ImetaEntry } from "./types";
|
||||
|
||||
export type ParsedImetaEntry = ImetaEntry & {
|
||||
url: string;
|
||||
m: string;
|
||||
x: string;
|
||||
size: number;
|
||||
dim?: string;
|
||||
blurhash?: string;
|
||||
alt?: string;
|
||||
thumb?: string;
|
||||
duration?: number;
|
||||
image?: string;
|
||||
filename?: string;
|
||||
};
|
||||
|
||||
export function parseImetaTags(tags: string[][]): Map<string, ImetaEntry> {
|
||||
const map = new Map<string, ImetaEntry>();
|
||||
export function parseImetaTags(
|
||||
tags: string[][],
|
||||
): Map<string, ParsedImetaEntry> {
|
||||
const map = new Map<string, ParsedImetaEntry>();
|
||||
for (const tag of tags) {
|
||||
if (tag[0] !== "imeta") continue;
|
||||
const entry: Partial<ImetaEntry> = {};
|
||||
const entry: Partial<ParsedImetaEntry> = {};
|
||||
for (const part of tag.slice(1)) {
|
||||
const spaceIdx = part.indexOf(" ");
|
||||
if (spaceIdx === -1) continue;
|
||||
@@ -58,7 +57,7 @@ export function parseImetaTags(tags: string[][]): Map<string, ImetaEntry> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (entry.url) map.set(entry.url, entry as ImetaEntry);
|
||||
if (entry.url) map.set(entry.url, entry as ParsedImetaEntry);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
@@ -9660,6 +9660,13 @@ export function maybeInstallE2eTauriMocks() {
|
||||
return {
|
||||
additions: 27,
|
||||
deletions: 4,
|
||||
commit_body: [
|
||||
"See the [project guide](https://example.com/project-guide).",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
].join("\n"),
|
||||
files: [
|
||||
{
|
||||
path: "desktop/src/features/projects/ui/ProjectDetailScreen.tsx",
|
||||
|
||||
@@ -187,6 +187,16 @@ test("commit detail opens from the commits feed with a diff", async ({
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Copy commit hash" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("link", { name: "project guide" }),
|
||||
).toHaveAttribute("href", "https://example.com/project-guide");
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Architecture" }),
|
||||
).toBeVisible();
|
||||
await expect(page.locator("video")).toHaveAttribute(
|
||||
"src",
|
||||
"https://example.com/project-demo.mp4",
|
||||
);
|
||||
|
||||
// Diff from the mocked get_project_repo_diff renders changed files.
|
||||
await expect(page.getByText("2 changed files")).toBeVisible({
|
||||
|
||||
Reference in New Issue
Block a user