mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-03 10:40:30 +08:00
When we land on a repo via notification inbox the repository wouldn't be visible if the section was collapsed. Now we expand it and scroll into view so we see which repository we're browsing.
238 lines
6.4 KiB
Svelte
238 lines
6.4 KiB
Svelte
<script lang="ts">
|
|
import type { RepoRoute } from "@app/views/repo/router";
|
|
import type { ActionWithAuthor } from "@bindings/cob/inbox/ActionWithAuthor";
|
|
import type { NotificationItem } from "@bindings/cob/inbox/NotificationItem";
|
|
import type { Action as IssueAction } from "@bindings/cob/issue/Action";
|
|
import type { Action as PatchAction } from "@bindings/cob/patch/Action";
|
|
import type { ComponentProps } from "svelte";
|
|
|
|
import uniqWith from "lodash/uniqWith";
|
|
|
|
import { compressActions } from "@app/lib/notification";
|
|
import { push } from "@app/lib/router";
|
|
import {
|
|
authorForNodeId,
|
|
formatTimestamp,
|
|
issueStatusBackgroundColor,
|
|
issueStatusColor,
|
|
patchStatusBackgroundColor,
|
|
patchStatusColor,
|
|
} from "@app/lib/utils";
|
|
|
|
import Button from "@app/components/Button.svelte";
|
|
import Icon from "@app/components/Icon.svelte";
|
|
import InlineTitle from "@app/components/InlineTitle.svelte";
|
|
import NodeId from "@app/components/NodeId.svelte";
|
|
import { closeFocused } from "@app/components/Popover.svelte";
|
|
import { revealRepoInSidebar } from "@app/components/SidebarRepoList.svelte";
|
|
|
|
interface Props {
|
|
rid: string;
|
|
kind: "issue" | "patch";
|
|
oid: string;
|
|
clearByIds: (ids: string[]) => Promise<void>;
|
|
notificationItems: NotificationItem[];
|
|
selected?: boolean;
|
|
}
|
|
|
|
const {
|
|
clearByIds,
|
|
notificationItems,
|
|
rid,
|
|
oid,
|
|
kind,
|
|
selected = false,
|
|
}: Props = $props();
|
|
|
|
type Action = ActionWithAuthor<IssueAction> | ActionWithAuthor<PatchAction>;
|
|
|
|
const uniqueActions = $derived.by(() => {
|
|
return compressActions(
|
|
uniqWith(
|
|
notificationItems.flatMap<Action>(n => n.actions),
|
|
(a, b) =>
|
|
Boolean(
|
|
a.oid === b.oid &&
|
|
a.type === b.type &&
|
|
a.author.did &&
|
|
b.author.did,
|
|
),
|
|
).sort((a, b) => b.timestamp - a.timestamp),
|
|
kind,
|
|
oid,
|
|
);
|
|
});
|
|
|
|
const title = $derived.by(() => {
|
|
const lastDetail = notificationItems.at(-1);
|
|
if (lastDetail && "title" in lastDetail) {
|
|
return lastDetail.title;
|
|
}
|
|
});
|
|
|
|
const icon: ComponentProps<typeof Icon>["name"] = $derived.by(() => {
|
|
const lastDetail = notificationItems.at(-1);
|
|
if (lastDetail?.type === "issue" && lastDetail.status.status !== "open") {
|
|
return `issue-${lastDetail.status.status}` as const;
|
|
} else if (lastDetail?.type === "issue") {
|
|
return "issue" as const;
|
|
} else if (
|
|
lastDetail?.type === "patch" &&
|
|
lastDetail.status.status !== "open"
|
|
) {
|
|
return `patch-${lastDetail.status.status}` as const;
|
|
} else {
|
|
return "patch" as const;
|
|
}
|
|
});
|
|
|
|
const statusColor = $derived.by(() => {
|
|
const lastDetail = notificationItems.at(-1);
|
|
if (lastDetail?.type === "patch") {
|
|
return {
|
|
color: patchStatusColor[lastDetail.status.status],
|
|
background: patchStatusBackgroundColor[lastDetail.status.status],
|
|
};
|
|
} else if (lastDetail?.type === "issue") {
|
|
return {
|
|
color: issueStatusColor[lastDetail.status.status],
|
|
background: issueStatusBackgroundColor[lastDetail.status.status],
|
|
};
|
|
} else {
|
|
return {
|
|
color: "var(--color-text-secondary)",
|
|
background: "var(--color-surface-subtle)",
|
|
};
|
|
}
|
|
});
|
|
|
|
const route = $derived.by(() => {
|
|
const lastDetail = notificationItems.at(-1);
|
|
switch (lastDetail?.type) {
|
|
case "patch":
|
|
return {
|
|
resource: "repo.patch",
|
|
rid,
|
|
patch: lastDetail.id,
|
|
status: undefined,
|
|
} as RepoRoute;
|
|
case "issue":
|
|
return {
|
|
resource: "repo.issue",
|
|
rid,
|
|
issue: lastDetail.id,
|
|
status: "all",
|
|
} as RepoRoute;
|
|
}
|
|
|
|
return undefined;
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.notification-teaser {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 0.25rem;
|
|
min-height: 5rem;
|
|
background-color: var(--color-surface-subtle);
|
|
padding: 1rem;
|
|
cursor: pointer;
|
|
font: var(--txt-body-l-regular);
|
|
word-break: break-word;
|
|
}
|
|
.clear-icon {
|
|
position: absolute;
|
|
top: 0.5rem;
|
|
right: 0.5rem;
|
|
visibility: hidden;
|
|
color: var(--color-text-tertiary);
|
|
}
|
|
.notification-teaser:hover .clear-icon {
|
|
visibility: visible;
|
|
}
|
|
.selected {
|
|
background-color: var(--color-surface-mid);
|
|
}
|
|
.notification-teaser:hover {
|
|
background-color: var(--color-surface-mid);
|
|
}
|
|
.status {
|
|
padding: 0;
|
|
margin-right: 1rem;
|
|
}
|
|
.notification-teaser:first-of-type {
|
|
border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0;
|
|
}
|
|
.notification-teaser:last-of-type {
|
|
border-radius: 0 0 var(--border-radius-sm) var(--border-radius-sm);
|
|
}
|
|
.notification-teaser:only-of-type {
|
|
border-radius: var(--border-radius-sm);
|
|
}
|
|
</style>
|
|
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<div
|
|
tabindex="0"
|
|
role="button"
|
|
class:selected
|
|
class="notification-teaser"
|
|
onclick={() => {
|
|
if (route) {
|
|
revealRepoInSidebar(rid);
|
|
void push(route);
|
|
closeFocused();
|
|
}
|
|
}}>
|
|
<div class="global-flex" style:width="100%">
|
|
<div
|
|
class="global-chip status"
|
|
style:align-self="flex-start"
|
|
style:color={statusColor.color}
|
|
style:background-color={statusColor.background}>
|
|
<Icon name={icon} />
|
|
</div>
|
|
<div
|
|
class="global-flex"
|
|
style:flex-direction="column"
|
|
style:align-items="flex-start"
|
|
style:width="100%">
|
|
{#if title}
|
|
<InlineTitle content={title} />
|
|
{/if}
|
|
<div class="txt-body-m-regular" style:width="100%">
|
|
{#each uniqueActions as action}
|
|
<div
|
|
class="global-flex"
|
|
style:gap="0.25rem"
|
|
style:min-height="2rem"
|
|
style:flex-wrap="wrap"
|
|
style:width="100%">
|
|
<NodeId {...authorForNodeId(action.items[0].author)} />
|
|
<span>{@html action.summary}</span>
|
|
<span
|
|
style:margin-left="auto"
|
|
style:color="var(--color-text-tertiary)">
|
|
{formatTimestamp(action.items[0].timestamp)}
|
|
</span>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="clear-icon">
|
|
<Button
|
|
variant="naked"
|
|
stylePadding="0 0.25rem"
|
|
onclick={e => {
|
|
e.stopPropagation();
|
|
void clearByIds(notificationItems.map(n => n.rowId));
|
|
}}>
|
|
<Icon name="trash" />
|
|
</Button>
|
|
</div>
|
|
</div>
|