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.
209 lines
7.1 KiB
TypeScript
209 lines
7.1 KiB
TypeScript
import { Notification, NotificationStatus, getNotificationHtmlUrl, isNotificationBuiltFromTask } from "../notification";
|
|
import { Action, ActionPanel, Icon, getPreferenceValues, showToast, Toast } from "@raycast/api";
|
|
import { CreateTaskFromNotification } from "./CreateTaskFromNotification";
|
|
import { LinkNotificationToTask } from "./LinkNotificationToTask";
|
|
import { Page, UniversalInboxPreferences } from "../types";
|
|
import { default as dayjs, extend } from "dayjs";
|
|
import { MutatePromise } from "@raycast/utils";
|
|
import { useMemo, ReactNode } from "react";
|
|
import { handleErrors } from "../api";
|
|
import { TaskStatus } from "../task";
|
|
import utc from "dayjs/plugin/utc";
|
|
import fetch from "node-fetch";
|
|
|
|
extend(utc);
|
|
|
|
interface NotificationActionsProps {
|
|
notification: Notification;
|
|
detailsTarget: ReactNode;
|
|
mutate: MutatePromise<Page<Notification> | undefined>;
|
|
}
|
|
|
|
export function NotificationActions({ notification, detailsTarget, mutate }: NotificationActionsProps) {
|
|
const notificationHtmlUrl = useMemo(() => {
|
|
return getNotificationHtmlUrl(notification);
|
|
}, [notification]);
|
|
|
|
return (
|
|
<ActionPanel>
|
|
<Action.Push title="Show Details" icon={Icon.AppWindowSidebarRight} target={detailsTarget} />
|
|
<Action.OpenInBrowser url={notificationHtmlUrl} shortcut={{ modifiers: ["cmd"], key: "enter" }} />
|
|
<Action
|
|
title="Delete Notification"
|
|
icon={Icon.Trash}
|
|
shortcut={{ modifiers: ["ctrl"], key: "d" }}
|
|
onAction={() => deleteNotification(notification, mutate)}
|
|
/>
|
|
<Action
|
|
title="Unsubscribe from Notification"
|
|
icon={Icon.BellDisabled}
|
|
shortcut={{ modifiers: ["ctrl"], key: "u" }}
|
|
onAction={() => unsubscribeFromNotification(notification, mutate)}
|
|
/>
|
|
<Action
|
|
title="Snooze"
|
|
icon={Icon.Clock}
|
|
shortcut={{ modifiers: ["ctrl"], key: "s" }}
|
|
onAction={() => snoozeNotification(notification, mutate)}
|
|
/>
|
|
<Action.Push
|
|
title="Create Task…"
|
|
icon={Icon.Calendar}
|
|
shortcut={{ modifiers: ["ctrl"], key: "t" }}
|
|
target={<CreateTaskFromNotification notification={notification} mutate={mutate} />}
|
|
/>
|
|
<Action.Push
|
|
title="Link to Task…"
|
|
icon={Icon.Link}
|
|
shortcut={{ modifiers: ["ctrl"], key: "l" }}
|
|
target={<LinkNotificationToTask notification={notification} mutate={mutate} />}
|
|
/>
|
|
</ActionPanel>
|
|
);
|
|
}
|
|
|
|
export async function deleteNotification(
|
|
notification: Notification,
|
|
mutate: MutatePromise<Page<Notification> | undefined>,
|
|
) {
|
|
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
|
const toast = await showToast({ style: Toast.Style.Animated, title: "Deleting notification" });
|
|
try {
|
|
if (isNotificationBuiltFromTask(notification) && notification.task) {
|
|
await mutate(
|
|
handleErrors(
|
|
fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/tasks/${notification.task.id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ status: TaskStatus.Deleted }),
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${preferences.apiKey}`,
|
|
},
|
|
}),
|
|
),
|
|
{
|
|
optimisticUpdate(page) {
|
|
if (page) {
|
|
page.content = page.content.filter((n) => n.id !== notification.id);
|
|
}
|
|
return page;
|
|
},
|
|
},
|
|
);
|
|
} else {
|
|
await mutate(
|
|
handleErrors(
|
|
fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/notifications/${notification.id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ status: NotificationStatus.Deleted }),
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${preferences.apiKey}`,
|
|
},
|
|
}),
|
|
),
|
|
{
|
|
optimisticUpdate(page) {
|
|
if (page) {
|
|
page.content = page.content.filter((n) => n.id !== notification.id);
|
|
}
|
|
return page;
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
toast.style = Toast.Style.Success;
|
|
toast.title = "Notification successfully deleted";
|
|
} catch (error) {
|
|
toast.style = Toast.Style.Failure;
|
|
toast.title = "Failed to delete notification";
|
|
toast.message = (error as Error).message;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function unsubscribeFromNotification(
|
|
notification: Notification,
|
|
mutate: MutatePromise<Page<Notification> | undefined>,
|
|
) {
|
|
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
|
const toast = await showToast({ style: Toast.Style.Animated, title: "Unsubscribing from notification" });
|
|
try {
|
|
await mutate(
|
|
handleErrors(
|
|
fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/notifications/${notification.id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ status: NotificationStatus.Unsubscribed }),
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${preferences.apiKey}`,
|
|
},
|
|
}),
|
|
),
|
|
{
|
|
optimisticUpdate(page) {
|
|
if (page) {
|
|
page.content = page.content.filter((n) => n.id !== notification.id);
|
|
}
|
|
return page;
|
|
},
|
|
},
|
|
);
|
|
|
|
toast.style = Toast.Style.Success;
|
|
toast.title = "Notification successfully unsubscribed";
|
|
} catch (error) {
|
|
toast.style = Toast.Style.Failure;
|
|
toast.title = "Failed to unsubscribe from notification";
|
|
toast.message = (error as Error).message;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function snoozeNotification(
|
|
notification: Notification,
|
|
mutate: MutatePromise<Page<Notification> | undefined>,
|
|
) {
|
|
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
|
const toast = await showToast({ style: Toast.Style.Animated, title: "Snoozing notification" });
|
|
try {
|
|
const snoozeTime = computeSnoozedUntil(new Date(), 1, 6);
|
|
await mutate(
|
|
handleErrors(
|
|
fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/notifications/${notification.id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ snoozed_until: snoozeTime }),
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${preferences.apiKey}`,
|
|
},
|
|
}),
|
|
),
|
|
{
|
|
optimisticUpdate(page) {
|
|
if (page) {
|
|
page.content = page.content.filter((n) => n.id !== notification.id);
|
|
}
|
|
return page;
|
|
},
|
|
},
|
|
);
|
|
|
|
toast.style = Toast.Style.Success;
|
|
toast.title = "Notification successfully snoozed";
|
|
} catch (error) {
|
|
toast.style = Toast.Style.Failure;
|
|
toast.title = "Failed to snooze notification";
|
|
toast.message = (error as Error).message;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
function computeSnoozedUntil(fromDate: Date, daysOffset: number, resetHour: number): Date {
|
|
const result = dayjs(fromDate)
|
|
.utc()
|
|
.add(fromDate.getHours() < resetHour ? daysOffset - 1 : daysOffset, "day");
|
|
return result.hour(resetHour).minute(0).second(0).millisecond(0).toDate();
|
|
}
|