feat: Add notification actions

This commit is contained in:
2024-01-26 08:48:27 +01:00
parent 2d2d47f55f
commit 0507722ecf
24 changed files with 1967 additions and 172 deletions

View File

@@ -1,4 +1,10 @@
{ {
"root": true, "root": true,
"extends": ["@raycast"] "extends": ["@raycast", "plugin:import/recommended", "plugin:import/typescript"],
"settings": {
"import/resolver": {
"typescript": true,
"node": true
}
}
} }

View File

@@ -1,4 +1,5 @@
{ {
"printWidth": 120, "printWidth": 120,
"singleQuote": false "singleQuote": false,
} "plugins": ["./node_modules/prettier-plugin-sort-imports/dist/index.js"]
}

View File

@@ -5,6 +5,10 @@ default:
build: build:
npm run build npm run build
# Format code
format:
npm run format
# Lint extension code # Lint extension code
lint: lint:
npm run lint npm run lint

1300
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,18 +40,25 @@
"dependencies": { "dependencies": {
"@raycast/api": "^1.65.1", "@raycast/api": "^1.65.1",
"@raycast/utils": "^1.10.1", "@raycast/utils": "^1.10.1",
"node-fetch": "^3.3.2" "dayjs": "^1.11.10",
"node-fetch": "^3.3.2",
"ts-pattern": "^5.0.6"
}, },
"devDependencies": { "devDependencies": {
"@raycast/eslint-config": "^1.0.6", "@raycast/eslint-config": "^1.0.6",
"@types/node": "20.8.10", "@types/node": "20.8.10",
"@types/react": "18.2.27", "@types/react": "18.2.27",
"@typescript-eslint/parser": "^6.19.1",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"prettier-plugin-sort-imports": "^1.8.3",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"scripts": { "scripts": {
"build": "ray build -e dist", "build": "ray build -e dist",
"format": "prettier --write --list-different --ignore-unknown src",
"dev": "ray develop", "dev": "ray develop",
"fix-lint": "ray lint --fix", "fix-lint": "ray lint --fix",
"lint": "ray lint", "lint": "ray lint",

View File

@@ -1,73 +0,0 @@
import { Action, ActionPanel, Icon } from "@raycast/api";
import { useMemo, ReactElement } from "react";
import { getNotificationHtmlUrl } from "./notification";
import { Notification } from "./types";
function deleteNotification(notification: Notification) {
console.log(`Deleting notification ${notification.id}`);
}
function unsubscribeFromNotification(notification: Notification) {
console.log(`Unsubcribing from notification ${notification.id}`);
}
function snoozeNotification(notification: Notification) {
console.log(`Snoozing notification ${notification.id}`);
}
function createTaskFromNotification(notification: Notification) {
console.log(`Creating task notification ${notification.id}`);
}
function linkNotificationToTask(notification: Notification) {
console.log(`Linking notification ${notification.id}`);
}
export function NotificationActions({
notification,
detailsTarget,
}: {
notification: Notification;
detailsTarget: ReactElement;
}) {
const notification_html_url = useMemo(() => {
return getNotificationHtmlUrl(notification);
}, [notification]);
return (
<ActionPanel>
<Action.Push title="Show Details" target={detailsTarget} />
<Action.OpenInBrowser url={notification_html_url} />
<Action
title="Delete Notification"
icon={Icon.Trash}
shortcut={{ modifiers: ["ctrl"], key: "d" }}
onAction={() => deleteNotification(notification)}
/>
<Action
title="Unsubscribe From Notification"
icon={Icon.BellDisabled}
shortcut={{ modifiers: ["ctrl"], key: "u" }}
onAction={() => unsubscribeFromNotification(notification)}
/>
<Action
title="Snooze"
icon={Icon.Clock}
shortcut={{ modifiers: ["ctrl"], key: "s" }}
onAction={() => snoozeNotification(notification)}
/>
<Action
title="Create Task"
icon={Icon.Calendar}
shortcut={{ modifiers: ["ctrl"], key: "d" }}
onAction={() => createTaskFromNotification(notification)}
/>
<Action
title="Link to Task"
icon={Icon.Link}
shortcut={{ modifiers: ["ctrl"], key: "d" }}
onAction={() => linkNotificationToTask(notification)}
/>
</ActionPanel>
);
}

View File

@@ -0,0 +1,152 @@
import { Action, useNavigation, ActionPanel, Form, Icon, getPreferenceValues, showToast, Toast } from "@raycast/api";
import { MutatePromise, useForm, FormValidation, useFetch } from "@raycast/utils";
import { Page, UniversalInboxPreferences } from "../types";
import { Notification } from "../notification";
import { TaskPriority } from "../task";
import { handleErrors } from "../api";
import utc from "dayjs/plugin/utc";
import { useState } from "react";
import fetch from "node-fetch";
import dayjs from "dayjs";
dayjs.extend(utc);
interface CreateTaskFromNotificationProps {
notification: Notification;
mutate: MutatePromise<Page<Notification> | undefined>;
}
interface TaskCreationFormValues {
title: string;
project: string;
dueAt: Date | null;
priority: string;
}
interface ProjectSummary {
name: string;
source_id: string;
}
interface TaskCreation {
title: string;
project: ProjectSummary;
due_at?: { type: "DateTimeWithTz"; content: string };
priority: TaskPriority;
}
export function CreateTaskFromNotification({ notification, mutate }: CreateTaskFromNotificationProps) {
const preferences = getPreferenceValues<UniversalInboxPreferences>();
const { pop } = useNavigation();
const [searchText, setSearchText] = useState("");
const { isLoading, data: projects } = useFetch<Array<ProjectSummary>>(
`${preferences.universalInboxBaseUrl}/api/tasks/projects/search?matches=${searchText}`,
{
keepPreviousData: true,
headers: {
Authorization: `Bearer ${preferences.apiKey}`,
},
},
);
const { handleSubmit, itemProps } = useForm<TaskCreationFormValues>({
initialValues: {
title: notification.title,
dueAt: new Date(),
priority: `${TaskPriority.P4 as number}`,
},
async onSubmit(values) {
const project = projects?.find((p) => p.source_id === values.project);
if (!project) {
throw new Error("Project not found");
}
const taskCreation: TaskCreation = {
title: values.title,
project: project,
due_at: values.dueAt ? { type: "DateTimeWithTz", content: dayjs(values.dueAt).utc().format() } : undefined,
priority: parseInt(values.priority) as TaskPriority,
};
await createTaskFromNotification(taskCreation, notification, mutate);
pop();
},
validation: {
title: FormValidation.Required,
project: FormValidation.Required,
priority: FormValidation.Required,
},
});
return (
<Form
navigationTitle="Create task from notification"
actions={
<ActionPanel>
<Action.SubmitForm title="Create Task" icon={Icon.Calendar} onSubmit={handleSubmit} />
</ActionPanel>
}
>
<Form.TextField title="Task title" placeholder="Enter task title" {...itemProps.title} />
<Form.Dropdown
title="Project"
placeholder="Search project..."
filtering={true}
throttle={true}
isLoading={isLoading}
onSearchTextChange={setSearchText}
{...itemProps.project}
>
<Form.Dropdown.Item value="" title="" key={0} />
{projects?.map((project) => {
return <Form.Dropdown.Item title={project.name} value={project.source_id} key={project.source_id} />;
})}
</Form.Dropdown>
<Form.DatePicker title="Due at" min={new Date()} type={Form.DatePicker.Type.Date} {...itemProps.dueAt} />
<Form.Dropdown title="Priority" {...itemProps.priority}>
<Form.Dropdown.Item title="Priority 1" value={`${TaskPriority.P1 as number}`} key={TaskPriority.P1} />
<Form.Dropdown.Item title="Priority 2" value={`${TaskPriority.P2 as number}`} key={TaskPriority.P2} />
<Form.Dropdown.Item title="Priority 3" value={`${TaskPriority.P3 as number}`} key={TaskPriority.P3} />
<Form.Dropdown.Item title="Priority 4" value={`${TaskPriority.P4 as number}`} key={TaskPriority.P4} />
</Form.Dropdown>
</Form>
);
}
async function createTaskFromNotification(
taskCreation: TaskCreation,
notification: Notification,
mutate: MutatePromise<Page<Notification> | undefined>,
) {
const preferences = getPreferenceValues<UniversalInboxPreferences>();
const toast = await showToast({ style: Toast.Style.Animated, title: "Creating task from notification" });
try {
await mutate(
handleErrors(
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}/task`, {
method: "POST",
body: JSON.stringify(taskCreation),
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 = "Task successfully created";
} catch (error) {
toast.style = Toast.Style.Failure;
toast.title = "Failed to create task from notification";
toast.message = (error as Error).message;
throw error;
}
}

View File

@@ -0,0 +1,109 @@
import { Action, ActionPanel, useNavigation, Form, Icon, getPreferenceValues, showToast, Toast } from "@raycast/api";
import { MutatePromise, useForm, FormValidation, useFetch } from "@raycast/utils";
import { Page, UniversalInboxPreferences } from "../types";
import { Notification } from "../notification";
import { Task, TaskStatus } from "../task";
import { handleErrors } from "../api";
import { useState } from "react";
import fetch from "node-fetch";
interface LinkNotificationToTaskProps {
notification: Notification;
mutate: MutatePromise<Page<Notification> | undefined>;
}
interface TaskLinkFormValues {
taskId: string;
}
export function LinkNotificationToTask({ notification, mutate }: LinkNotificationToTaskProps) {
const preferences = getPreferenceValues<UniversalInboxPreferences>();
const { pop } = useNavigation();
const [searchText, setSearchText] = useState("");
const { isLoading, data: tasks } = useFetch<Array<Task>>(
`${preferences.universalInboxBaseUrl}/api/tasks/search?matches=${searchText}`,
{
keepPreviousData: true,
headers: {
Authorization: `Bearer ${preferences.apiKey}`,
},
},
);
const { handleSubmit, itemProps } = useForm<TaskLinkFormValues>({
async onSubmit(values) {
await linkNotificationToTask(notification, values.taskId, mutate);
pop();
},
validation: {
taskId: FormValidation.Required,
},
});
return (
<Form
navigationTitle="Link notification with task"
actions={
<ActionPanel>
<Action.SubmitForm title="Link to Task" icon={Icon.Link} onSubmit={handleSubmit} />
</ActionPanel>
}
>
<Form.Dropdown
title="Task"
placeholder="Search task..."
filtering={true}
throttle={true}
isLoading={isLoading}
onSearchTextChange={setSearchText}
{...itemProps.taskId}
>
<Form.Dropdown.Item value="" title="" key={0} />
{tasks?.map((task) => {
return <Form.Dropdown.Item title={task.title} value={task.id} key={task.id} />;
})}
</Form.Dropdown>
</Form>
);
}
async function linkNotificationToTask(
notification: Notification,
taskId: string,
mutate: MutatePromise<Page<Notification> | undefined>,
) {
const preferences = getPreferenceValues<UniversalInboxPreferences>();
const toast = await showToast({ style: Toast.Style.Animated, title: "Linking notification to task" });
try {
await mutate(
handleErrors(
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}`, {
method: "PATCH",
body: JSON.stringify({ status: TaskStatus.Deleted, task_id: taskId }),
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${preferences.apiKey}`,
},
}),
),
{
optimisticUpdate(page) {
if (page) {
page.content = page.content.filter((n) => n.id !== notification.id);
}
console.log(`page(link): ${notification.id}`, page);
return page;
},
},
);
toast.style = Toast.Style.Success;
toast.title = "Notification successfully linked to task";
} catch (error) {
toast.style = Toast.Style.Failure;
toast.title = "Failed to link notification to task";
toast.message = (error as Error).message;
throw error;
}
}

View File

@@ -0,0 +1,200 @@
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 { MutatePromise } from "@raycast/utils";
import { useMemo, ReactElement } from "react";
import { handleErrors } from "../api";
import { TaskStatus } from "../task";
import utc from "dayjs/plugin/utc";
import fetch from "node-fetch";
import dayjs from "dayjs";
dayjs.extend(utc);
interface NotificationActionsProps {
notification: Notification;
detailsTarget: ReactElement;
mutate: MutatePromise<Page<Notification> | undefined>;
}
export function NotificationActions({ notification, detailsTarget, mutate }: NotificationActionsProps) {
const notification_html_url = useMemo(() => {
return getNotificationHtmlUrl(notification);
}, [notification]);
return (
<ActionPanel>
<Action.OpenInBrowser url={notification_html_url} />
<Action.Push title="Show Details" target={detailsTarget} />
<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: "p" }}
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>
);
}
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(
fetch(`${preferences.universalInboxBaseUrl}/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}/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;
}
}
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}/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;
}
}
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}/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();
}

View File

@@ -1,7 +1,8 @@
import { Notification, getNotificationHtmlUrl } from "../notification";
import { Action, ActionPanel, Icon } from "@raycast/api"; import { Action, ActionPanel, Icon } from "@raycast/api";
import { MutatePromise } from "@raycast/utils";
import { useMemo, ReactElement } from "react"; import { useMemo, ReactElement } from "react";
import { getNotificationHtmlUrl } from "./notification"; import { Page } from "../types";
import { Notification } from "./types";
function deleteNotification(notification: Notification) { function deleteNotification(notification: Notification) {
console.log(`Deleting notification ${notification.id}`); console.log(`Deleting notification ${notification.id}`);
@@ -19,21 +20,20 @@ function completeTask(notification: Notification) {
console.log(`Completing task ${notification.id}`); console.log(`Completing task ${notification.id}`);
} }
export function NotificationTaskActions({ interface NotificationTaskActionsProps {
notification,
detailsTarget,
}: {
notification: Notification; notification: Notification;
detailsTarget: ReactElement; detailsTarget: ReactElement;
}) { mutate: MutatePromise<Page<Notification> | undefined>;
}
export function NotificationTaskActions({ notification, detailsTarget }: NotificationTaskActionsProps) {
const notification_html_url = useMemo(() => { const notification_html_url = useMemo(() => {
return getNotificationHtmlUrl(notification); return getNotificationHtmlUrl(notification);
}, [notification]); }, [notification]);
return ( return (
<ActionPanel> <ActionPanel>
<Action.Push title="Show Details" target={detailsTarget} />
<Action.OpenInBrowser url={notification_html_url} /> <Action.OpenInBrowser url={notification_html_url} />
<Action.Push title="Show Details" target={detailsTarget} />
<Action <Action
title="Delete Notification" title="Delete Notification"
icon={Icon.Trash} icon={Icon.Trash}

20
src/api.ts Normal file
View File

@@ -0,0 +1,20 @@
import { Response } from "node-fetch";
import { match } from "ts-pattern";
interface ResponseError {
message: string;
}
export async function handleErrors(response: Promise<Response>) {
return match(await response)
.with({ status: 400 }, async (r) => {
throw new Error(((await r.json()) as ResponseError).message);
})
.with({ status: 401 }, async (r) => {
throw new Error(((await r.json()) as ResponseError).message);
})
.with({ status: 500 }, async (r) => {
throw new Error(((await r.json()) as ResponseError).message);
})
.otherwise((resp) => resp);
}

View File

@@ -1,20 +1,21 @@
import { Action, ActionPanel, Detail, List, getPreferenceValues, openExtensionPreferences } from "@raycast/api"; import { Action, ActionPanel, Detail, List, getPreferenceValues, openExtensionPreferences } from "@raycast/api";
import { useFetch } from "@raycast/utils";
import { NotificationActions } from "./NotificationActions";
import { GithubNotificationListItem } from "./integrations/github/GithubNotificationListItem";
import { GoogleMailNotificationListItem } from "./integrations/google-mail/GoogleMailNotificationListItem"; import { GoogleMailNotificationListItem } from "./integrations/google-mail/GoogleMailNotificationListItem";
import { LinearNotificationListItem } from "./integrations/linear/LinearNotificationListItem"; import { GithubNotificationListItem } from "./integrations/github/listitem/GithubNotificationListItem";
import { TodoistNotificationListItem } from "./integrations/todoist/TodoistNotificationListItem"; import { TodoistNotificationListItem } from "./integrations/todoist/TodoistNotificationListItem";
import { Notification, NotificationListItemProps, Page, UniversalInboxPreferences } from "./types"; import { LinearNotificationListItem } from "./integrations/linear/LinearNotificationListItem";
import { Notification, NotificationListItemProps } from "./notification";
import { NotificationActions } from "./action/NotificationActions";
import { Page, UniversalInboxPreferences } from "./types";
import { useFetch } from "@raycast/utils";
export default function Command() { export default function Command() {
const preferences = getPreferenceValues<UniversalInboxPreferences>(); const preferences = getPreferenceValues<UniversalInboxPreferences>();
if ( if (
preferences.apiKey === undefined || preferences.apiKey === undefined ||
preferences.apiKey === "" preferences.apiKey === "" ||
/* preferences.universalInboxBaseUrl === undefined || preferences.universalInboxBaseUrl === undefined ||
* preferences.universalInboxBaseUrl === "" */ preferences.universalInboxBaseUrl === ""
) { ) {
return ( return (
<Detail <Detail
@@ -28,7 +29,7 @@ export default function Command() {
); );
} }
const { isLoading, data } = useFetch<Page<Notification>>( const { isLoading, data, mutate } = useFetch<Page<Notification>>(
`${preferences.universalInboxBaseUrl}/api/notifications?status=Unread,Read&with_tasks=true`, `${preferences.universalInboxBaseUrl}/api/notifications?status=Unread,Read&with_tasks=true`,
{ {
headers: { headers: {
@@ -40,35 +41,39 @@ export default function Command() {
return ( return (
<List isLoading={isLoading}> <List isLoading={isLoading}>
{data?.content.map((notification: Notification) => { {data?.content.map((notification: Notification) => {
return <NotificationListItem key={notification.id} notification={notification} />; return <NotificationListItem key={notification.id} notification={notification} mutate={mutate} />;
})} })}
</List> </List>
); );
} }
function NotificationListItem({ notification }: NotificationListItemProps) { function NotificationListItem({ notification, mutate }: NotificationListItemProps) {
switch (notification.metadata.type) { switch (notification.metadata.type) {
case "Github": case "Github":
return <GithubNotificationListItem notification={notification} />; return <GithubNotificationListItem notification={notification} mutate={mutate} />;
case "Linear": case "Linear":
return <LinearNotificationListItem notification={notification} />; return <LinearNotificationListItem notification={notification} mutate={mutate} />;
case "GoogleMail": case "GoogleMail":
return <GoogleMailNotificationListItem notification={notification} />; return <GoogleMailNotificationListItem notification={notification} mutate={mutate} />;
case "Todoist": case "Todoist":
return <TodoistNotificationListItem notification={notification} />; return <TodoistNotificationListItem notification={notification} mutate={mutate} />;
default: default:
return <DefaultNotificationListItem notification={notification} />; return <DefaultNotificationListItem notification={notification} mutate={mutate} />;
} }
} }
function DefaultNotificationListItem({ notification }: NotificationListItemProps) { function DefaultNotificationListItem({ notification, mutate }: NotificationListItemProps) {
return ( return (
<List.Item <List.Item
key={notification.id} key={notification.id}
title={notification.title} title={notification.title}
subtitle={`#${notification.source_id}`} subtitle={`#${notification.source_id}`}
actions={ actions={
<NotificationActions notification={notification} detailsTarget={<Detail markdown="# To be implemented 👋" />} /> <NotificationActions
notification={notification}
detailsTarget={<Detail markdown="# To be implemented 👋" />}
mutate={mutate}
/>
} }
/> />
); );

View File

@@ -1,5 +1,5 @@
import { Icon, Image, List } from "@raycast/api";
import { GithubActor, getGithubActorName } from "./types"; import { GithubActor, getGithubActorName } from "./types";
import { Icon, Image, List } from "@raycast/api";
export function getGithubActorAccessory(actor?: GithubActor): List.Item.Accessory { export function getGithubActorAccessory(actor?: GithubActor): List.Item.Accessory {
if (actor) { if (actor) {

View File

@@ -1,20 +1,24 @@
import { Color, Icon, List } from "@raycast/api";
import { NotificationActions } from "../../../NotificationActions";
import { Notification } from "../../../types";
import { getGithubActorAccessory } from "../misc";
import { GithubDiscussion, GithubDiscussionStateReason } from "../types";
import { GithubDiscussionPreview } from "../preview/GithubDiscussionPreview"; import { GithubDiscussionPreview } from "../preview/GithubDiscussionPreview";
import { NotificationActions } from "../../../action/NotificationActions";
import { GithubDiscussion, GithubDiscussionStateReason } from "../types";
import { getGithubActorAccessory } from "../accessories";
import { Notification } from "../../../notification";
import { Color, Icon, List } from "@raycast/api";
import { MutatePromise } from "@raycast/utils";
import { Page } from "../../../types";
interface GithubDiscussionNotificationListItemProps { interface GithubDiscussionNotificationListItemProps {
icon: string; icon: string;
notification: Notification; notification: Notification;
githubDiscussion: GithubDiscussion; githubDiscussion: GithubDiscussion;
mutate: MutatePromise<Page<Notification> | undefined>;
} }
export function GithubDiscussionNotificationListItem({ export function GithubDiscussionNotificationListItem({
icon, icon,
notification, notification,
githubDiscussion, githubDiscussion,
mutate,
}: GithubDiscussionNotificationListItemProps) { }: GithubDiscussionNotificationListItemProps) {
const subtitle = `${githubDiscussion.repository.name_with_owner}`; const subtitle = `${githubDiscussion.repository.name_with_owner}`;
@@ -48,6 +52,7 @@ export function GithubDiscussionNotificationListItem({
<NotificationActions <NotificationActions
notification={notification} notification={notification}
detailsTarget={<GithubDiscussionPreview notification={notification} githubDiscussion={githubDiscussion} />} detailsTarget={<GithubDiscussionPreview notification={notification} githubDiscussion={githubDiscussion} />}
mutate={mutate}
/> />
} }
/> />

View File

@@ -1,10 +1,10 @@
import { GithubPullRequestNotificationListItem } from "./GithubPullRequestNotificationListItem";
import { GithubDiscussionNotificationListItem } from "./GithubDiscussionNotificationListItem";
import { NotificationListItemProps } from "../../../notification";
import { environment } from "@raycast/api"; import { environment } from "@raycast/api";
import { useMemo } from "react"; import { useMemo } from "react";
import { NotificationListItemProps } from "../../types";
import { GithubDiscussionNotificationListItem } from "./listitem/GithubDiscussionNotificationListItem";
import { GithubPullRequestNotificationListItem } from "./listitem/GithubPullRequestNotificationListItem";
export function GithubNotificationListItem({ notification }: NotificationListItemProps) { export function GithubNotificationListItem({ notification, mutate }: NotificationListItemProps) {
const icon = useMemo(() => { const icon = useMemo(() => {
if (environment.appearance === "dark") { if (environment.appearance === "dark") {
return "github-logo-light.svg"; return "github-logo-light.svg";
@@ -19,6 +19,7 @@ export function GithubNotificationListItem({ notification }: NotificationListIte
icon={icon} icon={icon}
notification={notification} notification={notification}
githubPullRequest={notification.details.content} githubPullRequest={notification.details.content}
mutate={mutate}
/> />
); );
case "GithubDiscussion": case "GithubDiscussion":
@@ -27,6 +28,7 @@ export function GithubNotificationListItem({ notification }: NotificationListIte
icon={icon} icon={icon}
notification={notification} notification={notification}
githubDiscussion={notification.details.content} githubDiscussion={notification.details.content}
mutate={mutate}
/> />
); );
default: default:

View File

@@ -1,7 +1,3 @@
import { Color, Icon, List } from "@raycast/api";
import { NotificationActions } from "../../../NotificationActions";
import { Notification } from "../../../types";
import { GithubPullRequestPreview } from "../preview/GithubPullRequestPreview";
import { import {
GithubPullRequestState, GithubPullRequestState,
GithubPullRequestReviewDecision, GithubPullRequestReviewDecision,
@@ -11,18 +7,26 @@ import {
GithubCheckStatusState, GithubCheckStatusState,
GithubCheckSuite, GithubCheckSuite,
} from "../types"; } from "../types";
import { getGithubActorAccessory } from "../misc"; import { GithubPullRequestPreview } from "../preview/GithubPullRequestPreview";
import { NotificationActions } from "../../../action/NotificationActions";
import { getGithubActorAccessory } from "../accessories";
import { Notification } from "../../../notification";
import { Color, Icon, List } from "@raycast/api";
import { MutatePromise } from "@raycast/utils";
import { Page } from "../../../types";
interface GithubPullRequestNotificationListItemProps { interface GithubPullRequestNotificationListItemProps {
icon: string; icon: string;
notification: Notification; notification: Notification;
githubPullRequest: GithubPullRequest; githubPullRequest: GithubPullRequest;
mutate: MutatePromise<Page<Notification> | undefined>;
} }
export function GithubPullRequestNotificationListItem({ export function GithubPullRequestNotificationListItem({
icon, icon,
notification, notification,
githubPullRequest, githubPullRequest,
mutate,
}: GithubPullRequestNotificationListItemProps) { }: GithubPullRequestNotificationListItemProps) {
const subtitle = `${githubPullRequest.head_repository?.name_with_owner} #${githubPullRequest.number}`; const subtitle = `${githubPullRequest.head_repository?.name_with_owner} #${githubPullRequest.number}`;
@@ -67,6 +71,7 @@ export function GithubPullRequestNotificationListItem({
<NotificationActions <NotificationActions
notification={notification} notification={notification}
detailsTarget={<GithubPullRequestPreview notification={notification} githubPullRequest={githubPullRequest} />} detailsTarget={<GithubPullRequestPreview notification={notification} githubPullRequest={githubPullRequest} />}
mutate={mutate}
/> />
} }
/> />

View File

@@ -1,8 +1,7 @@
import { Notification, getNotificationHtmlUrl } from "../../../notification";
import { Detail, ActionPanel, Action } from "@raycast/api"; import { Detail, ActionPanel, Action } from "@raycast/api";
import { Notification } from "../../../types";
import { GithubDiscussion } from "../types"; import { GithubDiscussion } from "../types";
import { useMemo } from "react"; import { useMemo } from "react";
import { getNotificationHtmlUrl } from "../../../notification";
interface GithubDiscussionPreviewProps { interface GithubDiscussionPreviewProps {
notification: Notification; notification: Notification;

View File

@@ -1,8 +1,7 @@
import { Notification, getNotificationHtmlUrl } from "../../../notification";
import { Detail, ActionPanel, Action } from "@raycast/api"; import { Detail, ActionPanel, Action } from "@raycast/api";
import { Notification } from "../../../types";
import { GithubPullRequest } from "../types"; import { GithubPullRequest } from "../types";
import { useMemo } from "react"; import { useMemo } from "react";
import { getNotificationHtmlUrl } from "../../../notification";
interface GithubPullRequestPreviewProps { interface GithubPullRequestPreviewProps {
notification: Notification; notification: Notification;

View File

@@ -1,9 +1,9 @@
import { NotificationActions } from "../../action/NotificationActions";
import { NotificationListItemProps } from "../../notification";
import { Detail, List, environment } from "@raycast/api"; import { Detail, List, environment } from "@raycast/api";
import { useMemo } from "react"; import { useMemo } from "react";
import { NotificationActions } from "../../NotificationActions";
import { NotificationListItemProps } from "../../types";
export function GoogleMailNotificationListItem({ notification }: NotificationListItemProps) { export function GoogleMailNotificationListItem({ notification, mutate }: NotificationListItemProps) {
const icon = useMemo(() => { const icon = useMemo(() => {
if (environment.appearance === "dark") { if (environment.appearance === "dark") {
return "google-mail-logo-light.svg"; return "google-mail-logo-light.svg";
@@ -18,7 +18,11 @@ export function GoogleMailNotificationListItem({ notification }: NotificationLis
icon={icon} icon={icon}
subtitle={`#${notification.source_id}`} subtitle={`#${notification.source_id}`}
actions={ actions={
<NotificationActions notification={notification} detailsTarget={<Detail markdown="# To be implemented 👋" />} /> <NotificationActions
notification={notification}
detailsTarget={<Detail markdown="# To be implemented 👋" />}
mutate={mutate}
/>
} }
/> />
); );

View File

@@ -1,9 +1,9 @@
import { NotificationActions } from "../../action/NotificationActions";
import { NotificationListItemProps } from "../../notification";
import { Detail, List, environment } from "@raycast/api"; import { Detail, List, environment } from "@raycast/api";
import { useMemo } from "react"; import { useMemo } from "react";
import { NotificationActions } from "../../NotificationActions";
import { NotificationListItemProps } from "../../types";
export function LinearNotificationListItem({ notification }: NotificationListItemProps) { export function LinearNotificationListItem({ notification, mutate }: NotificationListItemProps) {
const icon = useMemo(() => { const icon = useMemo(() => {
if (environment.appearance === "dark") { if (environment.appearance === "dark") {
return "linear-logo-light.svg"; return "linear-logo-light.svg";
@@ -18,7 +18,11 @@ export function LinearNotificationListItem({ notification }: NotificationListIte
icon={icon} icon={icon}
subtitle={`#${notification.source_id}`} subtitle={`#${notification.source_id}`}
actions={ actions={
<NotificationActions notification={notification} detailsTarget={<Detail markdown="# To be implemented 👋" />} /> <NotificationActions
notification={notification}
detailsTarget={<Detail markdown="# To be implemented 👋" />}
mutate={mutate}
/>
} }
/> />
); );

View File

@@ -1,9 +1,9 @@
import { NotificationTaskActions } from "../../action/NotificationTaskActions";
import { NotificationListItemProps } from "../../notification";
import { Detail, List, environment } from "@raycast/api"; import { Detail, List, environment } from "@raycast/api";
import { useMemo } from "react"; import { useMemo } from "react";
import { NotificationTaskActions } from "../../NotificationTaskActions";
import { NotificationListItemProps } from "../../types";
export function TodoistNotificationListItem({ notification }: NotificationListItemProps) { export function TodoistNotificationListItem({ notification, mutate }: NotificationListItemProps) {
const icon = useMemo(() => { const icon = useMemo(() => {
if (environment.appearance === "dark") { if (environment.appearance === "dark") {
return "todoist-icon-light.svg"; return "todoist-icon-light.svg";
@@ -21,6 +21,7 @@ export function TodoistNotificationListItem({ notification }: NotificationListIt
<NotificationTaskActions <NotificationTaskActions
notification={notification} notification={notification}
detailsTarget={<Detail markdown="# To be implemented 👋" />} detailsTarget={<Detail markdown="# To be implemented 👋" />}
mutate={mutate}
/> />
} }
/> />

View File

@@ -1,4 +1,43 @@
import { Notification } from "./types"; import { GithubDiscussion, GithubPullRequest } from "./integrations/github/types";
import { MutatePromise } from "@raycast/utils";
import { Page } from "./types";
import { Task } from "./task";
export interface Notification {
id: string;
title: string;
source_id: string;
status: NotificationStatus;
metadata: NotificationMetadata;
updated_at: Date;
last_read_at?: Date;
snoozed_until?: Date;
user_id: string;
task?: Task;
details?: NotificationDetails;
}
export type NotificationMetadata = {
type: "Github" | "Linear" | "GoogleMail" | "Todoist";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
content: any;
};
export type NotificationDetails =
| { type: "GithubPullRequest"; content: GithubPullRequest }
| { type: "GithubDiscussion"; content: GithubDiscussion };
export enum NotificationStatus {
Unread = "Unread",
Read = "Read",
Deleted = "Deleted",
Unsubscribed = "Unsubscribed",
}
export type NotificationListItemProps = {
notification: Notification;
mutate: MutatePromise<Page<Notification> | undefined>;
};
export function getNotificationHtmlUrl(notification: Notification) { export function getNotificationHtmlUrl(notification: Notification) {
switch (notification.details?.type) { switch (notification.details?.type) {
@@ -12,3 +51,7 @@ export function getNotificationHtmlUrl(notification: Notification) {
} }
} }
} }
export function isNotificationBuiltFromTask(notification: Notification) {
return notification.metadata.type === "Todoist";
}

41
src/task.ts Normal file
View File

@@ -0,0 +1,41 @@
export interface Task {
id: string;
source_id: string;
title: string;
body: string;
status: TaskStatus;
completed_at?: Date;
priority: TaskPriority;
due_at?: DueDate;
tags: Array<string>;
parent_id?: string;
project: string;
is_recurring: boolean;
created_at: Date;
metadata: TaskMetadata;
user_id: string;
}
export type TaskMetadata = {
type: "Todoist";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
content: any;
};
export enum TaskStatus {
Active = "Active",
Done = "Done",
Deleted = "Deleted",
}
export enum TaskPriority {
P1 = 1,
P2 = 2,
P3 = 3,
P4 = 4,
}
export type DueDate =
| { type: "Date"; content: Date }
| { type: "DateTime"; content: Date }
| { type: "DateTimeWithTz"; content: Date };

View File

@@ -1,5 +1,3 @@
import { GithubDiscussion, GithubPullRequest } from "./integrations/github/types";
export interface UniversalInboxPreferences { export interface UniversalInboxPreferences {
apiKey: string; apiKey: string;
universalInboxBaseUrl: string; universalInboxBaseUrl: string;
@@ -11,38 +9,3 @@ export interface Page<T> {
total: number; total: number;
content: Array<T>; content: Array<T>;
} }
export interface Notification {
id: string;
title: string;
source_id: string;
status: NotificationStatus;
metadata: NotificationMetadata;
updated_at: Date;
last_read_at?: Date;
snoozed_until?: Date;
user_id: string;
task_id?: string;
details?: NotificationDetails;
}
export type NotificationMetadata = {
type: "Github" | "Linear" | "GoogleMail" | "Todoist";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
content: any;
};
export type NotificationDetails =
| { type: "GithubPullRequest"; content: GithubPullRequest }
| { type: "GithubDiscussion"; content: GithubDiscussion };
export enum NotificationStatus {
Unread = "Unread",
Read = "Read",
Deleted = "Deleted",
Unsubscribed = "Unsubscribed",
}
export type NotificationListItemProps = {
notification: Notification;
};