feat: add missing integrations and rich notification previews

Add TickTick, Google Calendar, Google Drive and API (WebPage) notification
types, which the backend already supported but the extension ignored.

Fill the previously empty notification previews with content modeled on the
web app: a metadata sidebar (status, priority, assignee, labels, dates,
channel, etc.) plus a markdown body and comment/message threads. Add shared
helpers: PreviewDetail wrapper, TaskMetadata, Slack mrkdwn renderer, GitHub
check/review emoji, and date/HTML utils (cleanHtml strips raw HTML from
GitHub bodies).

The preview metadata "Type" row shows the source item type (Linear Issue,
GitHub Pull Request, Slack Thread, etc.).

Swap list-screen shortcuts: Enter shows details, Cmd+Enter opens in browser.
This commit is contained in:
2026-06-08 11:23:06 +02:00
parent fc5b290c5e
commit 9e51e0df6c
32 changed files with 1287 additions and 162 deletions
@@ -1,26 +1,85 @@
import { getNotificationHtmlUrl, Notification } from "../../../notification";
import { Detail, ActionPanel, Action } from "@raycast/api";
import { LinearIssue } from "../types";
import { useMemo } from "react";
import { PreviewDetail } from "../../../preview/PreviewDetail";
import { formatDate, formatElapsedTime } from "../../../utils";
import { LinearIssue, LinearIssuePriority } from "../types";
import { Notification } from "../../../notification";
import { Color, Detail, Image } from "@raycast/api";
interface LinearIssuePreviewProps {
notification: Notification;
linearIssue: LinearIssue;
}
export function LinearIssuePreview({ notification, linearIssue }: LinearIssuePreviewProps) {
const notificationHtmlUrl = useMemo(() => {
return getNotificationHtmlUrl(notification);
}, [notification]);
const PRIORITY_LABEL: Record<LinearIssuePriority, string> = {
[LinearIssuePriority.NoPriority]: "No priority",
[LinearIssuePriority.Urgent]: "Urgent",
[LinearIssuePriority.High]: "High",
[LinearIssuePriority.Normal]: "Normal",
[LinearIssuePriority.Low]: "Low",
};
return (
<Detail
markdown={`# ${linearIssue.title}`}
actions={
<ActionPanel>
<Action.OpenInBrowser url={notificationHtmlUrl} />
</ActionPanel>
}
/>
);
const PRIORITY_COLOR: Record<LinearIssuePriority, Color> = {
[LinearIssuePriority.NoPriority]: Color.SecondaryText,
[LinearIssuePriority.Urgent]: Color.Red,
[LinearIssuePriority.High]: Color.Orange,
[LinearIssuePriority.Normal]: Color.Yellow,
[LinearIssuePriority.Low]: Color.Blue,
};
function userIcon(avatarUrl?: string): Image.ImageLike | undefined {
return avatarUrl ? { source: avatarUrl, mask: Image.Mask.Circle } : undefined;
}
export function LinearIssuePreview({ notification, linearIssue }: LinearIssuePreviewProps) {
let markdown = `# ${linearIssue.identifier} ${linearIssue.title}`;
if (linearIssue.creator) {
markdown += `\n\n_Opened by ${linearIssue.creator.name} · ${formatElapsedTime(linearIssue.created_at)}_`;
}
if (linearIssue.description) {
markdown += `\n\n${linearIssue.description}`;
}
const metadata = (
<>
<Detail.Metadata.TagList title="Status">
<Detail.Metadata.TagList.Item text={linearIssue.state.name} color={linearIssue.state.color} />
</Detail.Metadata.TagList>
<Detail.Metadata.TagList title="Priority">
<Detail.Metadata.TagList.Item
text={PRIORITY_LABEL[linearIssue.priority]}
color={PRIORITY_COLOR[linearIssue.priority]}
/>
</Detail.Metadata.TagList>
{linearIssue.assignee ? (
<Detail.Metadata.Label
title="Assignee"
text={linearIssue.assignee.name}
icon={userIcon(linearIssue.assignee.avatar_url)}
/>
) : null}
{linearIssue.creator ? (
<Detail.Metadata.Label
title="Creator"
text={linearIssue.creator.name}
icon={userIcon(linearIssue.creator.avatar_url)}
/>
) : null}
{linearIssue.project ? (
<Detail.Metadata.Link title="Project" target={linearIssue.project.url} text={linearIssue.project.name} />
) : null}
{linearIssue.project_milestone ? (
<Detail.Metadata.Label title="Milestone" text={linearIssue.project_milestone.name} />
) : null}
<Detail.Metadata.Label title="Team" text={linearIssue.team.name} />
{linearIssue.due_date ? <Detail.Metadata.Label title="Due" text={formatDate(linearIssue.due_date)} /> : null}
{linearIssue.labels.length > 0 ? (
<Detail.Metadata.TagList title="Labels">
{linearIssue.labels.map((label) => (
<Detail.Metadata.TagList.Item key={label.name} text={label.name} color={label.color} />
))}
</Detail.Metadata.TagList>
) : null}
</>
);
return <PreviewDetail notification={notification} markdown={markdown} metadata={metadata} />;
}