mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-15 11:20:36 +08:00
Add reactions to patch revisions
- Also adapts to the new reactions API schema everywhere.
This commit is contained in:
@@ -6,7 +6,7 @@ import type {
|
||||
Tree,
|
||||
DiffResponse,
|
||||
} from "./lib/project.js";
|
||||
import type { SuccessResponse } from "./lib/shared.js";
|
||||
import type { SuccessResponse, CodeLocation, Range } from "./lib/shared.js";
|
||||
import type { Comment, Embed } from "./lib/project/comment.js";
|
||||
import type {
|
||||
Commit,
|
||||
@@ -20,13 +20,11 @@ import type {
|
||||
} from "./lib/project/commit.js";
|
||||
import type { Issue, IssueState } from "./lib/project/issue.js";
|
||||
import type {
|
||||
CodeLocation,
|
||||
LifecycleState,
|
||||
Merge,
|
||||
Patch,
|
||||
PatchState,
|
||||
PatchUpdateAction,
|
||||
Range,
|
||||
Review,
|
||||
Revision,
|
||||
Verdict,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { z } from "zod";
|
||||
import { array, number, object, string, tuple } from "zod";
|
||||
import { array, boolean, number, object, string } from "zod";
|
||||
import { codeLocationSchema } from "../shared";
|
||||
|
||||
export type Comment = z.infer<typeof commentSchema>;
|
||||
export type Embed = z.infer<typeof commentSchema>["embeds"][0];
|
||||
@@ -17,7 +18,9 @@ export const commentSchema = object({
|
||||
}),
|
||||
),
|
||||
embeds: array(object({ name: string(), content: string() })),
|
||||
reactions: array(tuple([string(), string()])),
|
||||
reactions: array(object({ emoji: string(), authors: array(string()) })),
|
||||
timestamp: number(),
|
||||
location: codeLocationSchema.nullable().optional(),
|
||||
resolved: boolean(),
|
||||
replyTo: string().nullable(),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Comment, Embed } from "./comment.js";
|
||||
import type { ZodSchema } from "zod";
|
||||
import type { Embed } from "./comment.js";
|
||||
import type { ZodSchema, z } from "zod";
|
||||
import { array, boolean, literal, object, string, union } from "zod";
|
||||
|
||||
import { commentSchema } from "./comment.js";
|
||||
@@ -16,16 +16,6 @@ const issueStateSchema = union([
|
||||
}),
|
||||
]) satisfies ZodSchema<IssueState>;
|
||||
|
||||
export interface Issue {
|
||||
id: string;
|
||||
author: { id: string; alias?: string };
|
||||
title: string;
|
||||
state: IssueState;
|
||||
discussion: Comment[];
|
||||
labels: string[];
|
||||
assignees: string[];
|
||||
}
|
||||
|
||||
export const issueSchema = object({
|
||||
id: string(),
|
||||
author: object({ id: string(), alias: string().optional() }),
|
||||
@@ -34,7 +24,9 @@ export const issueSchema = object({
|
||||
discussion: array(commentSchema),
|
||||
labels: array(string()),
|
||||
assignees: array(string()),
|
||||
}) satisfies ZodSchema<Issue>;
|
||||
});
|
||||
|
||||
export type Issue = z.infer<typeof issueSchema>;
|
||||
|
||||
export interface IssueCreated {
|
||||
success: boolean;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Embed } from "./comment.js";
|
||||
import type { ZodSchema, z } from "zod";
|
||||
import type { CodeLocation } from "../shared.js";
|
||||
|
||||
import { commentSchema } from "./comment.js";
|
||||
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
tuple,
|
||||
union,
|
||||
} from "zod";
|
||||
import { codeLocationSchema } from "../shared.js";
|
||||
|
||||
export type PatchState =
|
||||
| { status: "draft" }
|
||||
@@ -55,23 +57,15 @@ const mergeSchema = object({
|
||||
|
||||
export type Verdict = "accept" | "reject";
|
||||
|
||||
export interface Review {
|
||||
author: { id: string; alias?: string };
|
||||
verdict?: Verdict | null;
|
||||
summary: string | null;
|
||||
comments: string[];
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
const reviewSchema = object({
|
||||
author: object({ id: string(), alias: string().optional() }),
|
||||
verdict: optional(union([literal("accept"), literal("reject")]).nullable()),
|
||||
comments: array(string()),
|
||||
comments: array(commentSchema),
|
||||
summary: string().nullable(),
|
||||
timestamp: number(),
|
||||
}) satisfies ZodSchema<Review>;
|
||||
});
|
||||
|
||||
export type Revision = z.infer<typeof revisionSchema>;
|
||||
export type Review = z.infer<typeof reviewSchema>;
|
||||
|
||||
const revisionSchema = object({
|
||||
id: string(),
|
||||
@@ -85,6 +79,13 @@ const revisionSchema = object({
|
||||
timestamp: number(),
|
||||
}),
|
||||
),
|
||||
reactions: array(
|
||||
object({
|
||||
emoji: string(),
|
||||
location: codeLocationSchema.nullable(),
|
||||
authors: array(string()),
|
||||
}),
|
||||
),
|
||||
base: string(),
|
||||
oid: string(),
|
||||
refs: array(string()),
|
||||
@@ -93,17 +94,7 @@ const revisionSchema = object({
|
||||
timestamp: number(),
|
||||
});
|
||||
|
||||
export interface Patch {
|
||||
id: string;
|
||||
author: { id: string; alias?: string };
|
||||
title: string;
|
||||
state: PatchState;
|
||||
target: string;
|
||||
labels: string[];
|
||||
merges: Merge[];
|
||||
assignees: string[];
|
||||
revisions: Revision[];
|
||||
}
|
||||
export type Revision = z.infer<typeof revisionSchema>;
|
||||
|
||||
export const patchSchema = object({
|
||||
id: string(),
|
||||
@@ -115,7 +106,9 @@ export const patchSchema = object({
|
||||
merges: array(mergeSchema),
|
||||
assignees: array(string()),
|
||||
revisions: array(revisionSchema),
|
||||
}) satisfies ZodSchema<Patch>;
|
||||
});
|
||||
|
||||
export type Patch = z.infer<typeof patchSchema>;
|
||||
|
||||
export const patchesSchema = array(patchSchema) satisfies ZodSchema<Patch[]>;
|
||||
|
||||
@@ -124,23 +117,6 @@ export type LifecycleState =
|
||||
| { status: "open" }
|
||||
| { status: "archived" };
|
||||
|
||||
export type Range =
|
||||
| {
|
||||
type: "lines";
|
||||
range: { start: number; end: number };
|
||||
}
|
||||
| {
|
||||
type: "chars";
|
||||
line: number;
|
||||
range: { start: number; end: number };
|
||||
};
|
||||
|
||||
export type CodeLocation = {
|
||||
path: string;
|
||||
old?: Range;
|
||||
new?: Range;
|
||||
};
|
||||
|
||||
export type PatchUpdateAction =
|
||||
| { type: "edit"; title: string; target: "delegates" }
|
||||
| { type: "label"; labels: string[] }
|
||||
@@ -189,6 +165,13 @@ export type PatchUpdateAction =
|
||||
description: string;
|
||||
embeds?: Embed[];
|
||||
}
|
||||
| {
|
||||
type: "revision.react";
|
||||
revision: string;
|
||||
reaction: string;
|
||||
location?: CodeLocation;
|
||||
active: boolean;
|
||||
}
|
||||
| { type: "revision.redact"; revision: string }
|
||||
| {
|
||||
type: "revision.comment";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ZodSchema } from "zod";
|
||||
import type { ZodSchema, z } from "zod";
|
||||
|
||||
import { array, boolean, literal, number, object, string, union } from "zod";
|
||||
|
||||
@@ -41,3 +41,25 @@ export const nodeConfigSchema = object({
|
||||
policy: union([literal("allow"), literal("block")]),
|
||||
scope: union([literal("followed"), literal("all")]),
|
||||
});
|
||||
|
||||
export const rangeSchema = union([
|
||||
object({
|
||||
type: literal("lines"),
|
||||
range: object({ start: number(), end: number() }),
|
||||
}),
|
||||
object({
|
||||
type: literal("chars"),
|
||||
line: number(),
|
||||
range: object({ start: number(), end: number() }),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type Range = z.infer<typeof rangeSchema>;
|
||||
|
||||
export const codeLocationSchema = object({
|
||||
path: string(),
|
||||
old: rangeSchema.optional(),
|
||||
new: rangeSchema.optional(),
|
||||
});
|
||||
|
||||
export type CodeLocation = z.infer<typeof codeLocationSchema>;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts" strictEvents>
|
||||
import type { Comment, Embed } from "@httpd-client";
|
||||
import type { GroupedReactions } from "@app/lib/reactions";
|
||||
|
||||
import { tick } from "svelte";
|
||||
|
||||
@@ -20,7 +19,7 @@
|
||||
export let authorAlias: string | undefined = undefined;
|
||||
export let body: string;
|
||||
export let enableAttachments: boolean = false;
|
||||
export let reactions: GroupedReactions | undefined = undefined;
|
||||
export let reactions: Comment["reactions"] | undefined = undefined;
|
||||
export let embeds: Map<string, Embed> | undefined = undefined;
|
||||
export let caption = "commented";
|
||||
export let rawPath: string;
|
||||
@@ -34,7 +33,7 @@
|
||||
export let editComment:
|
||||
| ((body: string, embeds: Embed[]) => Promise<void>)
|
||||
| undefined = undefined;
|
||||
export let handleReaction:
|
||||
export let reactOnComment:
|
||||
| ((nids: string[], reaction: string) => Promise<void>)
|
||||
| undefined = undefined;
|
||||
</script>
|
||||
@@ -175,22 +174,22 @@
|
||||
<Markdown {rawPath} content={body} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if (id && handleReaction) || (id && reactions && reactions.size > 0)}
|
||||
{#if (id && reactOnComment) || (id && reactions && reactions.length > 0)}
|
||||
<div class="actions">
|
||||
{#if id && handleReaction}
|
||||
{@const handleReaction_ = handleReaction}
|
||||
{#if id && reactOnComment}
|
||||
{@const reactOnComment_ = reactOnComment}
|
||||
<ReactionSelector
|
||||
{reactions}
|
||||
on:select={async ({ detail: { nids, reaction } }) => {
|
||||
on:select={async ({ detail: { authors, emoji } }) => {
|
||||
try {
|
||||
await handleReaction_(nids, reaction);
|
||||
await reactOnComment_(authors, emoji);
|
||||
} finally {
|
||||
closeFocused();
|
||||
}
|
||||
}} />
|
||||
{/if}
|
||||
{#if id && reactions && reactions.size > 0}
|
||||
<Reactions {handleReaction} {reactions} />
|
||||
{#if id && reactions && reactions.length > 0}
|
||||
<Reactions handleReaction={reactOnComment} {reactions} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { GroupedReactions } from "@app/lib/reactions";
|
||||
import type { Comment } from "@httpd-client";
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
import IconSmall from "./IconSmall.svelte";
|
||||
import Popover from "./Popover.svelte";
|
||||
|
||||
export let reactions: GroupedReactions | undefined;
|
||||
export let reactions: Comment["reactions"] | undefined = undefined;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
select: { nids: string[]; reaction: string };
|
||||
select: { emoji: string; authors: string[] };
|
||||
}>();
|
||||
</script>
|
||||
|
||||
@@ -56,13 +56,17 @@
|
||||
|
||||
<div class="selector" slot="popover">
|
||||
{#each config.reactions as reaction}
|
||||
{@const lookedUpReaction = reactions?.find(
|
||||
({ emoji }) => emoji === reaction,
|
||||
)}
|
||||
<button
|
||||
class:active={Boolean(reactions?.get(reaction)?.self)}
|
||||
on:click={() =>
|
||||
dispatch("select", {
|
||||
nids: reactions?.get(reaction)?.all ?? [],
|
||||
reaction,
|
||||
})}>
|
||||
class:active={Boolean(lookedUpReaction)}
|
||||
on:click={() => {
|
||||
dispatch(
|
||||
"select",
|
||||
lookedUpReaction || { emoji: reaction, authors: [] },
|
||||
);
|
||||
}}>
|
||||
{reaction}
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { GroupedReactions } from "@app/lib/reactions";
|
||||
import type { Comment } from "@httpd-client";
|
||||
|
||||
import IconButton from "./IconButton.svelte";
|
||||
|
||||
export let reactions: GroupedReactions;
|
||||
export let reactions: Comment["reactions"];
|
||||
export let handleReaction:
|
||||
| ((nids: string[], reaction: string) => Promise<void>)
|
||||
| ((authors: string[], reaction: string) => Promise<void>)
|
||||
| undefined;
|
||||
</script>
|
||||
|
||||
@@ -24,16 +24,16 @@
|
||||
</style>
|
||||
|
||||
<div class="reactions">
|
||||
{#each reactions as [reaction, { all: nids }]}
|
||||
{#each reactions as { emoji, authors }}
|
||||
<IconButton
|
||||
on:click={async () => {
|
||||
if (handleReaction) {
|
||||
await handleReaction(nids, reaction);
|
||||
await handleReaction(authors, emoji);
|
||||
}
|
||||
}}>
|
||||
<div class="reaction txt-tiny">
|
||||
<span>{reaction}</span>
|
||||
<span title={nids.join("\n")}>{nids.length}</span>
|
||||
<span>{emoji}</span>
|
||||
<span title={authors.join("\n")}>{authors.length}</span>
|
||||
</div>
|
||||
</IconButton>
|
||||
{/each}
|
||||
|
||||
@@ -1,26 +1,6 @@
|
||||
<script lang="ts" context="module">
|
||||
import type { Comment } from "@httpd-client";
|
||||
import { groupReactions } from "@app/lib/reactions";
|
||||
|
||||
function groupReactionsInThread(thread: {
|
||||
root: Comment;
|
||||
replies: Comment[];
|
||||
}) {
|
||||
return {
|
||||
root: {
|
||||
...thread.root,
|
||||
reactions: groupReactions(thread.root.reactions),
|
||||
},
|
||||
replies: thread.replies.map(reply => ({
|
||||
...reply,
|
||||
reactions: groupReactions(reply.reactions),
|
||||
})),
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts" strictEvents>
|
||||
import type { Embed } from "@httpd-client";
|
||||
import type { Comment } from "@httpd-client";
|
||||
|
||||
import * as utils from "@app/lib/utils";
|
||||
import partial from "lodash/partial";
|
||||
@@ -44,7 +24,7 @@
|
||||
export let createReply:
|
||||
| ((commentId: string, comment: string, embeds: Embed[]) => Promise<void>)
|
||||
| undefined;
|
||||
export let handleReaction:
|
||||
export let reactOnComment:
|
||||
| ((commentId: string, nids: string[], reaction: string) => Promise<void>)
|
||||
| undefined;
|
||||
|
||||
@@ -57,9 +37,8 @@
|
||||
});
|
||||
}
|
||||
|
||||
$: threadWithReactions = groupReactionsInThread(thread);
|
||||
$: root = threadWithReactions.root;
|
||||
$: replies = threadWithReactions.replies;
|
||||
$: root = thread.root;
|
||||
$: replies = thread.replies;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -102,7 +81,7 @@
|
||||
editComment={editComment &&
|
||||
canEditComment(root.author.id) &&
|
||||
partial(editComment, root.id)}
|
||||
handleReaction={handleReaction && partial(handleReaction, root.id)}>
|
||||
reactOnComment={reactOnComment && partial(reactOnComment, root.id)}>
|
||||
<IconSmall name="chat" slot="icon" />
|
||||
</CommentComponent>
|
||||
</div>
|
||||
@@ -126,8 +105,8 @@
|
||||
editComment={editComment &&
|
||||
canEditComment(reply.author.id) &&
|
||||
partial(editComment, reply.id)}
|
||||
handleReaction={handleReaction &&
|
||||
partial(handleReaction, reply.id)} />
|
||||
reactOnComment={reactOnComment &&
|
||||
partial(reactOnComment, reply.id)} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
export type GroupedReactions = Map<string, { all: string[]; self: boolean }>;
|
||||
|
||||
// Takes reactions from a comment and groups them by emoji
|
||||
// and if the current user has reacted with that emoji.
|
||||
export function groupReactions(
|
||||
reactions: [string, string][],
|
||||
publicKey?: string,
|
||||
) {
|
||||
return reactions.reduce(
|
||||
(acc, [nid, emoji]) =>
|
||||
acc.set(emoji, {
|
||||
all: [...(acc.get(emoji)?.all ?? []), nid],
|
||||
self: publicKey === nid,
|
||||
}),
|
||||
new Map<string, { all: string[]; self: boolean }>(),
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type {
|
||||
BaseUrl,
|
||||
Comment,
|
||||
DiffResponse,
|
||||
Embed,
|
||||
PatchState,
|
||||
@@ -11,6 +12,7 @@
|
||||
|
||||
import * as utils from "@app/lib/utils";
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
import { closeFocused } from "@app/components/Popover.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { parseEmbedIntoMap } from "@app/lib/file";
|
||||
|
||||
@@ -29,10 +31,12 @@
|
||||
import Markdown from "@app/components/Markdown.svelte";
|
||||
import NodeId from "@app/components/NodeId.svelte";
|
||||
import Popover from "@app/components/Popover.svelte";
|
||||
import ReactionSelector from "@app/components/ReactionSelector.svelte";
|
||||
import Reactions from "@app/components/Reactions.svelte";
|
||||
import Thread from "@app/components/Thread.svelte";
|
||||
|
||||
export let baseUrl: BaseUrl;
|
||||
export let initialExpanded: boolean = false;
|
||||
export let initiallyExpanded: boolean = false;
|
||||
export let rawPath: (commit?: string) => string;
|
||||
export let patchId: string;
|
||||
export let patchState: PatchState;
|
||||
@@ -44,6 +48,7 @@
|
||||
export let revisionEdits: Revision["edits"];
|
||||
export let revisionOid: string;
|
||||
export let revisionTimestamp: number;
|
||||
export let revisionReactions: Comment["reactions"];
|
||||
export let revisionAuthor: { id: string; alias?: string | undefined };
|
||||
export let revisionDescription: string;
|
||||
export let timelines: Timeline[];
|
||||
@@ -57,14 +62,21 @@
|
||||
export let editComment:
|
||||
| ((commentId: string, body: string, embeds: Embed[]) => Promise<void>)
|
||||
| undefined;
|
||||
export let handleReaction:
|
||||
| ((commentId: string, nids: string[], reaction: string) => Promise<void>)
|
||||
export let reactOnRevision:
|
||||
| ((authors: string[], reaction: string) => Promise<void>)
|
||||
| undefined;
|
||||
export let reactOnComment:
|
||||
| ((
|
||||
commentId: string,
|
||||
authors: string[],
|
||||
reaction: string,
|
||||
) => Promise<void>)
|
||||
| undefined;
|
||||
export let createReply:
|
||||
| ((commentId: string, comment: string, embeds: Embed[]) => Promise<void>)
|
||||
| undefined;
|
||||
|
||||
let expanded = initialExpanded;
|
||||
let expanded = initiallyExpanded;
|
||||
const api = new HttpdClient(baseUrl);
|
||||
const latestEdit = revisionEdits.pop();
|
||||
|
||||
@@ -184,7 +196,6 @@
|
||||
color: var(--color-foreground-dim);
|
||||
}
|
||||
.revision-description {
|
||||
margin-bottom: 1rem;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
.compare-dropdown-item {
|
||||
@@ -209,6 +220,14 @@
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--color-fill-gray);
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: 2rem;
|
||||
gap: 0.5rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
.commits {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -431,6 +450,27 @@
|
||||
content={revisionDescription} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if reactOnRevision || revisionReactions.length > 0}
|
||||
<div class="actions">
|
||||
{#if reactOnRevision}
|
||||
{@const reactOnRevision_ = reactOnRevision}
|
||||
<ReactionSelector
|
||||
reactions={revisionReactions}
|
||||
on:select={async ({ detail: { emoji, authors } }) => {
|
||||
try {
|
||||
await reactOnRevision_(authors, emoji);
|
||||
} finally {
|
||||
closeFocused();
|
||||
}
|
||||
}} />
|
||||
{/if}
|
||||
{#if revisionReactions && revisionReactions.length > 0}
|
||||
<Reactions
|
||||
handleReaction={reactOnRevision}
|
||||
reactions={revisionReactions} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if loading}
|
||||
<div style:height="3.5rem">
|
||||
@@ -471,7 +511,7 @@
|
||||
canEditComment={canEdit}
|
||||
{editComment}
|
||||
{createReply}
|
||||
{handleReaction} />
|
||||
{reactOnComment} />
|
||||
{:else if element.type === "merge"}
|
||||
<div class="connector" />
|
||||
<div class="action merge">
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import * as utils from "@app/lib/utils";
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
import { closeFocused } from "@app/components/Popover.svelte";
|
||||
import { groupReactions } from "@app/lib/reactions";
|
||||
import { httpdStore } from "@app/lib/httpd";
|
||||
import { parseEmbedIntoMap } from "@app/lib/file";
|
||||
|
||||
@@ -164,7 +163,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReaction(
|
||||
async function reactOnComment(
|
||||
session: Session,
|
||||
commentId: string,
|
||||
nids: string[],
|
||||
@@ -569,11 +568,11 @@
|
||||
<div class="reactions">
|
||||
{#if session}
|
||||
<ReactionSelector
|
||||
reactions={groupReactions(issue.discussion[0].reactions)}
|
||||
on:select={async ({ detail: { nids, reaction } }) => {
|
||||
reactions={issue.discussion[0].reactions}
|
||||
on:select={async ({ detail: { authors, emoji } }) => {
|
||||
try {
|
||||
if (session) {
|
||||
await handleReaction(session, issue.id, nids, reaction);
|
||||
await reactOnComment(session, issue.id, authors, emoji);
|
||||
}
|
||||
} finally {
|
||||
closeFocused();
|
||||
@@ -582,9 +581,9 @@
|
||||
{/if}
|
||||
{#if issue.discussion[0].reactions.length > 0}
|
||||
<Reactions
|
||||
reactions={groupReactions(issue.discussion[0].reactions)}
|
||||
reactions={issue.discussion[0].reactions}
|
||||
handleReaction={session &&
|
||||
partial(handleReaction, session, issue.id)} />
|
||||
partial(reactOnComment, session, issue.id)} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -621,7 +620,7 @@
|
||||
)}
|
||||
editComment={session && partial(editComment, session.id)}
|
||||
createReply={session && partial(createReply, session.id)}
|
||||
handleReaction={session && partial(handleReaction, session)} />
|
||||
reactOnComment={session && partial(reactOnComment, session)} />
|
||||
<div class="connector" />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
import type { PatchView } from "./router";
|
||||
import type { Route } from "@app/lib/router";
|
||||
import type { ComponentProps } from "svelte";
|
||||
import type { Session } from "@app/lib/httpd";
|
||||
|
||||
import * as modal from "@app/lib/modal";
|
||||
import * as role from "@app/lib/roles";
|
||||
@@ -50,7 +51,7 @@
|
||||
import partial from "lodash/partial";
|
||||
import uniqBy from "lodash/uniqBy";
|
||||
import { HttpdClient } from "@httpd-client";
|
||||
import { httpdStore, type Session } from "@app/lib/httpd";
|
||||
import { httpdStore } from "@app/lib/httpd";
|
||||
import { parseEmbedIntoMap } from "@app/lib/file";
|
||||
|
||||
import Badge from "@app/components/Badge.svelte";
|
||||
@@ -75,6 +76,8 @@
|
||||
import Placeholder from "@app/components/Placeholder.svelte";
|
||||
import Popover, { closeFocused } from "@app/components/Popover.svelte";
|
||||
import Radio from "@app/components/Radio.svelte";
|
||||
import ReactionSelector from "@app/components/ReactionSelector.svelte";
|
||||
import Reactions from "@app/components/Reactions.svelte";
|
||||
import RevisionComponent from "@app/views/projects/Cob/Revision.svelte";
|
||||
import TextInput from "@app/components/TextInput.svelte";
|
||||
import Share from "./Share.svelte";
|
||||
@@ -203,11 +206,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReaction(
|
||||
async function reactOnComment(
|
||||
session: Session,
|
||||
revisionId: string,
|
||||
commentId: string,
|
||||
nids: string[],
|
||||
authors: string[],
|
||||
reaction: string,
|
||||
) {
|
||||
try {
|
||||
@@ -219,7 +222,7 @@
|
||||
revision: revisionId,
|
||||
comment: commentId,
|
||||
reaction,
|
||||
active: nids.includes(session.publicKey) ? false : true,
|
||||
active: authors.includes(session.publicKey) ? false : true,
|
||||
},
|
||||
session.id,
|
||||
);
|
||||
@@ -363,6 +366,46 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function reactOnRevision(
|
||||
session: Session,
|
||||
revisionId: string,
|
||||
authors: string[],
|
||||
reaction: string,
|
||||
) {
|
||||
try {
|
||||
await api.project.updatePatch(
|
||||
project.id,
|
||||
patch.id,
|
||||
{
|
||||
type: "revision.react",
|
||||
revision: revisionId,
|
||||
reaction,
|
||||
active: authors.includes(session.publicKey) ? false : true,
|
||||
},
|
||||
session.id,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
modal.show({
|
||||
component: ErrorModal,
|
||||
props: {
|
||||
title: "Reacting on revision failed",
|
||||
subtitle: [
|
||||
"There was an error while trying to react to a revision.",
|
||||
"Check your radicle-httpd logs for details.",
|
||||
],
|
||||
error: {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await refreshPatch();
|
||||
}
|
||||
}
|
||||
|
||||
async function saveLabels(sessionId: string, labels: string[]) {
|
||||
try {
|
||||
await api.project.updatePatch(
|
||||
@@ -511,6 +554,7 @@
|
||||
revisionBase: string;
|
||||
revisionOid: string;
|
||||
revisionEdits: Revision["edits"];
|
||||
revisionReactions: Revision["reactions"];
|
||||
revisionAuthor: { id: string; alias?: string | undefined };
|
||||
revisionDescription: string;
|
||||
},
|
||||
@@ -523,6 +567,7 @@
|
||||
revisionBase: rev.base,
|
||||
revisionOid: rev.oid,
|
||||
revisionEdits: rev.edits,
|
||||
revisionReactions: rev.reactions,
|
||||
revisionAuthor: rev.author,
|
||||
revisionDescription: rev.description,
|
||||
},
|
||||
@@ -553,6 +598,7 @@
|
||||
})),
|
||||
].sort((a, b) => a.timestamp - b.timestamp),
|
||||
]);
|
||||
$: firstRevision = timelineTuple[0][0];
|
||||
$: session =
|
||||
$httpdStore.state === "authenticated" && utils.isLocal(baseUrl.hostname)
|
||||
? $httpdStore.session
|
||||
@@ -597,6 +643,12 @@
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.tabs {
|
||||
font-size: var(--font-size-tiny);
|
||||
display: flex;
|
||||
@@ -640,6 +692,7 @@
|
||||
}
|
||||
.revision-description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
.diff-button-range {
|
||||
@@ -736,12 +789,38 @@
|
||||
}
|
||||
}} />
|
||||
{:else if description}
|
||||
<Markdown
|
||||
content={description}
|
||||
rawPath={rawPath(patch.revisions[0].id)} />
|
||||
<Markdown content={description} rawPath={rawPath(patch.id)} />
|
||||
{:else}
|
||||
<span class="txt-missing">No description available</span>
|
||||
{/if}
|
||||
{#if session || (firstRevision.revisionReactions && firstRevision.revisionReactions.length > 0)}
|
||||
<div class="actions">
|
||||
{#if session}
|
||||
<ReactionSelector
|
||||
reactions={firstRevision.revisionReactions}
|
||||
on:select={async ({ detail: { emoji, authors } }) => {
|
||||
if (session) {
|
||||
try {
|
||||
await reactOnRevision(
|
||||
session,
|
||||
patch.id,
|
||||
authors,
|
||||
emoji,
|
||||
);
|
||||
} finally {
|
||||
closeFocused();
|
||||
}
|
||||
}
|
||||
}} />
|
||||
{/if}
|
||||
{#if firstRevision.revisionReactions.length > 0}
|
||||
<Reactions
|
||||
handleReaction={session &&
|
||||
partial(reactOnRevision, session, patch.id)}
|
||||
reactions={firstRevision.revisionReactions} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<div class="author" slot="author">
|
||||
@@ -882,13 +961,15 @@
|
||||
partial(editRevision, session.id, revision.revisionId)}
|
||||
editComment={session &&
|
||||
partial(editComment, session.id, revision.revisionId)}
|
||||
handleReaction={session &&
|
||||
partial(handleReaction, session, revision.revisionId)}
|
||||
reactOnComment={session &&
|
||||
partial(reactOnComment, session, revision.revisionId)}
|
||||
reactOnRevision={session &&
|
||||
partial(reactOnRevision, session, revision.revisionId)}
|
||||
createReply={session &&
|
||||
partial(createReply, session.id, revision.revisionId)}
|
||||
patchId={patch.id}
|
||||
patchState={patch.state}
|
||||
initialExpanded={index === patch.revisions.length - 1}
|
||||
initiallyExpanded={index === patch.revisions.length - 1}
|
||||
previousRevId={previousRevision?.id}
|
||||
previousRevOid={previousRevision?.oid}>
|
||||
{#if index === patch.revisions.length - 1}
|
||||
|
||||
@@ -126,7 +126,8 @@ test("add and remove reactions", async ({ page, authenticatedPeer }) => {
|
||||
await page
|
||||
.getByRole("link", { name: "This is an issue to test reactions" })
|
||||
.click();
|
||||
await expectReactionsToWork(page);
|
||||
const reactionsLocator = page.locator(".actions").first();
|
||||
await expectReactionsToWork(page, reactionsLocator);
|
||||
});
|
||||
|
||||
test("handling embeds", async ({ page, authenticatedPeer }) => {
|
||||
|
||||
@@ -67,7 +67,8 @@ test("add and remove reactions", async ({ page, authenticatedPeer }) => {
|
||||
}),
|
||||
);
|
||||
await page.goto(`${authenticatedPeer.uiUrl()}/${rid}/patches/${patchId}`);
|
||||
await expectReactionsToWork(page);
|
||||
const reactionsLocator = page.locator(".actions").first();
|
||||
await expectReactionsToWork(page, reactionsLocator);
|
||||
});
|
||||
|
||||
test("handling embeds", async ({ page, authenticatedPeer }) => {
|
||||
|
||||
+32
-17
@@ -1,4 +1,4 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import type { Locator, Page } from "@playwright/test";
|
||||
import type { RadiclePeer } from "@tests/support/peerManager";
|
||||
import type { ExecaReturnValue } from "execa";
|
||||
|
||||
@@ -120,28 +120,43 @@ export async function expectLabelEditingToWork(page: Page) {
|
||||
).toBeHidden();
|
||||
}
|
||||
|
||||
export async function expectReactionsToWork(page: Page) {
|
||||
export async function expectReactionsToWork(
|
||||
page: Page,
|
||||
reactionsLocator: Locator,
|
||||
) {
|
||||
await page.getByRole("button", { name: "Leave your comment" }).click();
|
||||
await page.getByPlaceholder("Leave your comment").fill("This is a comment");
|
||||
await page.getByRole("button", { name: "Comment" }).click();
|
||||
const commentReactionToggle = page.getByTitle("toggle-reaction").first();
|
||||
await commentReactionToggle.click();
|
||||
await page.getByRole("button", { name: "👍" }).click();
|
||||
await expect(page.getByRole("button", { name: "👍 1" })).toBeVisible();
|
||||
const reactionsToggleBtn = reactionsLocator.getByRole("button", {
|
||||
name: "toggle-reaction-popover",
|
||||
});
|
||||
await reactionsToggleBtn.click();
|
||||
await reactionsLocator.getByRole("button", { name: "👍" }).click();
|
||||
await expect(
|
||||
reactionsLocator.getByRole("button", { name: "👍 1" }),
|
||||
).toBeVisible();
|
||||
|
||||
await commentReactionToggle.click();
|
||||
await page.getByRole("button", { name: "🎉" }).click();
|
||||
await expect(page.getByRole("button", { name: "🎉 1" })).toBeVisible();
|
||||
await expect(page.locator(".reaction")).toHaveCount(2);
|
||||
await reactionsToggleBtn.click();
|
||||
await reactionsLocator.getByRole("button", { name: "🎉" }).click();
|
||||
await expect(
|
||||
reactionsLocator.getByRole("button", { name: "🎉 1" }),
|
||||
).toBeVisible();
|
||||
await expect(reactionsLocator.locator(".reaction")).toHaveCount(2);
|
||||
|
||||
await page.getByRole("button", { name: "👍" }).click();
|
||||
await expect(page.locator("span").filter({ hasText: "👍 1" })).toBeHidden();
|
||||
await expect(page.locator(".reaction")).toHaveCount(1);
|
||||
await reactionsLocator.getByRole("button", { name: "👍" }).click();
|
||||
await expect(
|
||||
reactionsLocator.locator("span").filter({ hasText: "👍 1" }),
|
||||
).toBeHidden();
|
||||
await expect(reactionsLocator.locator(".reaction")).toHaveCount(1);
|
||||
|
||||
await commentReactionToggle.click();
|
||||
await page.getByRole("button", { name: "🎉" }).first().click();
|
||||
await expect(page.getByRole("button", { name: "🎉 1" })).toBeHidden();
|
||||
await expect(page.locator(".reaction")).toHaveCount(0);
|
||||
await reactionsToggleBtn.click();
|
||||
await reactionsLocator
|
||||
.getByRole("button", { name: "🎉", exact: true })
|
||||
.click();
|
||||
await expect(
|
||||
reactionsLocator.getByRole("button", { name: "🎉 1" }),
|
||||
).toBeHidden();
|
||||
await expect(reactionsLocator.locator(".reaction")).toHaveCount(0);
|
||||
}
|
||||
|
||||
export async function addEmbed(
|
||||
|
||||
Reference in New Issue
Block a user