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
34 changes: 32 additions & 2 deletions desktop/src-tauri/src/commands/project_git_diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
})
}
Expand Down Expand Up @@ -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}"))?
Expand Down Expand Up @@ -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}"))?
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/forum/ui/ForumPostCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/forum/ui/ForumThreadPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/messages/lib/imetaMediaMarkdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/messages/lib/rowHeightEstimate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/messages/ui/MessageRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
3 changes: 3 additions & 0 deletions desktop/src/features/projects/projectIssues.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type ProjectIssueStatus =
export type ProjectIssueComment = {
id: string;
content: string;
tags: string[][];
author: string;
createdAt: number;
};
Expand All @@ -19,6 +20,7 @@ export type ProjectIssue = {
id: string;
title: string;
content: string;
tags: string[][];
author: string;
createdAt: number;
repoAddress: string | null;
Expand All @@ -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[],
Expand Down
6 changes: 6 additions & 0 deletions desktop/src/features/projects/projectIssues.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
}));
Expand All @@ -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,
Expand Down
26 changes: 26 additions & 0 deletions desktop/src/features/projects/projectIssues.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: "![Screenshot](https://relay.example/media/comment.png)",
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({
Expand Down
3 changes: 3 additions & 0 deletions desktop/src/features/projects/projectPullRequests.d.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -12,6 +13,7 @@ export type ProjectPullRequestUpdate = {
export type ProjectPullRequestComment = {
id: string;
content: string;
tags: string[][];
author: string;
createdAt: number;
commit: string | null;
Expand Down Expand Up @@ -70,6 +72,7 @@ export type ProjectPullRequest = {
id: string;
title: string;
content: string;
tags: string[][];
author: string;
createdAt: number;
repoAddress: string | null;
Expand Down
10 changes: 9 additions & 1 deletion desktop/src/features/projects/projectPullRequests.mjs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
38 changes: 38 additions & 0 deletions desktop/src/features/projects/projectPullRequests.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -98,6 +99,9 @@ export function ProjectCommitDetailPanel({
</div>
</div>
</div>
{diff?.commitBody ? (
<ProjectRichContent content={diff.commitBody} />
) : null}
</header>

<ProjectDiffFilesPanel
Expand Down
14 changes: 3 additions & 11 deletions desktop/src/features/projects/ui/ProjectIssuesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ 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,
ProjectFeedRowMonoCell,
} 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";
Expand Down Expand Up @@ -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>

Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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>
))}
Expand Down
Loading
Loading