feat: Add notification actions
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["@raycast"]
|
||||
"extends": ["@raycast", "plugin:import/recommended", "plugin:import/typescript"],
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"typescript": true,
|
||||
"node": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"singleQuote": false
|
||||
"singleQuote": false,
|
||||
"plugins": ["./node_modules/prettier-plugin-sort-imports/dist/index.js"]
|
||||
}
|
||||
4
justfile
4
justfile
@@ -5,6 +5,10 @@ default:
|
||||
build:
|
||||
npm run build
|
||||
|
||||
# Format code
|
||||
format:
|
||||
npm run format
|
||||
|
||||
# Lint extension code
|
||||
lint:
|
||||
npm run lint
|
||||
|
||||
1300
package-lock.json
generated
1300
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -40,18 +40,25 @@
|
||||
"dependencies": {
|
||||
"@raycast/api": "^1.65.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": {
|
||||
"@raycast/eslint-config": "^1.0.6",
|
||||
"@types/node": "20.8.10",
|
||||
"@types/react": "18.2.27",
|
||||
"@typescript-eslint/parser": "^6.19.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-sort-imports": "^1.8.3",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "ray build -e dist",
|
||||
"format": "prettier --write --list-different --ignore-unknown src",
|
||||
"dev": "ray develop",
|
||||
"fix-lint": "ray lint --fix",
|
||||
"lint": "ray lint",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
152
src/action/CreateTaskFromNotification.tsx
Normal file
152
src/action/CreateTaskFromNotification.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
109
src/action/LinkNotificationToTask.tsx
Normal file
109
src/action/LinkNotificationToTask.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
200
src/action/NotificationActions.tsx
Normal file
200
src/action/NotificationActions.tsx
Normal 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();
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Notification, getNotificationHtmlUrl } from "../notification";
|
||||
import { Action, ActionPanel, Icon } from "@raycast/api";
|
||||
import { MutatePromise } from "@raycast/utils";
|
||||
import { useMemo, ReactElement } from "react";
|
||||
import { getNotificationHtmlUrl } from "./notification";
|
||||
import { Notification } from "./types";
|
||||
import { Page } from "../types";
|
||||
|
||||
function deleteNotification(notification: Notification) {
|
||||
console.log(`Deleting notification ${notification.id}`);
|
||||
@@ -19,21 +20,20 @@ function completeTask(notification: Notification) {
|
||||
console.log(`Completing task ${notification.id}`);
|
||||
}
|
||||
|
||||
export function NotificationTaskActions({
|
||||
notification,
|
||||
detailsTarget,
|
||||
}: {
|
||||
interface NotificationTaskActionsProps {
|
||||
notification: Notification;
|
||||
detailsTarget: ReactElement;
|
||||
}) {
|
||||
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||
}
|
||||
export function NotificationTaskActions({ notification, detailsTarget }: NotificationTaskActionsProps) {
|
||||
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.Push title="Show Details" target={detailsTarget} />
|
||||
<Action
|
||||
title="Delete Notification"
|
||||
icon={Icon.Trash}
|
||||
20
src/api.ts
Normal file
20
src/api.ts
Normal 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);
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
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 { LinearNotificationListItem } from "./integrations/linear/LinearNotificationListItem";
|
||||
import { GithubNotificationListItem } from "./integrations/github/listitem/GithubNotificationListItem";
|
||||
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() {
|
||||
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||
|
||||
if (
|
||||
preferences.apiKey === undefined ||
|
||||
preferences.apiKey === ""
|
||||
/* preferences.universalInboxBaseUrl === undefined ||
|
||||
* preferences.universalInboxBaseUrl === "" */
|
||||
preferences.apiKey === "" ||
|
||||
preferences.universalInboxBaseUrl === undefined ||
|
||||
preferences.universalInboxBaseUrl === ""
|
||||
) {
|
||||
return (
|
||||
<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`,
|
||||
{
|
||||
headers: {
|
||||
@@ -40,35 +41,39 @@ export default function Command() {
|
||||
return (
|
||||
<List isLoading={isLoading}>
|
||||
{data?.content.map((notification: Notification) => {
|
||||
return <NotificationListItem key={notification.id} notification={notification} />;
|
||||
return <NotificationListItem key={notification.id} notification={notification} mutate={mutate} />;
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationListItem({ notification }: NotificationListItemProps) {
|
||||
function NotificationListItem({ notification, mutate }: NotificationListItemProps) {
|
||||
switch (notification.metadata.type) {
|
||||
case "Github":
|
||||
return <GithubNotificationListItem notification={notification} />;
|
||||
return <GithubNotificationListItem notification={notification} mutate={mutate} />;
|
||||
case "Linear":
|
||||
return <LinearNotificationListItem notification={notification} />;
|
||||
return <LinearNotificationListItem notification={notification} mutate={mutate} />;
|
||||
case "GoogleMail":
|
||||
return <GoogleMailNotificationListItem notification={notification} />;
|
||||
return <GoogleMailNotificationListItem notification={notification} mutate={mutate} />;
|
||||
case "Todoist":
|
||||
return <TodoistNotificationListItem notification={notification} />;
|
||||
return <TodoistNotificationListItem notification={notification} mutate={mutate} />;
|
||||
default:
|
||||
return <DefaultNotificationListItem notification={notification} />;
|
||||
return <DefaultNotificationListItem notification={notification} mutate={mutate} />;
|
||||
}
|
||||
}
|
||||
|
||||
function DefaultNotificationListItem({ notification }: NotificationListItemProps) {
|
||||
function DefaultNotificationListItem({ notification, mutate }: NotificationListItemProps) {
|
||||
return (
|
||||
<List.Item
|
||||
key={notification.id}
|
||||
title={notification.title}
|
||||
subtitle={`#${notification.source_id}`}
|
||||
actions={
|
||||
<NotificationActions notification={notification} detailsTarget={<Detail markdown="# To be implemented 👋" />} />
|
||||
<NotificationActions
|
||||
notification={notification}
|
||||
detailsTarget={<Detail markdown="# To be implemented 👋" />}
|
||||
mutate={mutate}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Icon, Image, List } from "@raycast/api";
|
||||
import { GithubActor, getGithubActorName } from "./types";
|
||||
import { Icon, Image, List } from "@raycast/api";
|
||||
|
||||
export function getGithubActorAccessory(actor?: GithubActor): List.Item.Accessory {
|
||||
if (actor) {
|
||||
@@ -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 { 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 {
|
||||
icon: string;
|
||||
notification: Notification;
|
||||
githubDiscussion: GithubDiscussion;
|
||||
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||
}
|
||||
|
||||
export function GithubDiscussionNotificationListItem({
|
||||
icon,
|
||||
notification,
|
||||
githubDiscussion,
|
||||
mutate,
|
||||
}: GithubDiscussionNotificationListItemProps) {
|
||||
const subtitle = `${githubDiscussion.repository.name_with_owner}`;
|
||||
|
||||
@@ -48,6 +52,7 @@ export function GithubDiscussionNotificationListItem({
|
||||
<NotificationActions
|
||||
notification={notification}
|
||||
detailsTarget={<GithubDiscussionPreview notification={notification} githubDiscussion={githubDiscussion} />}
|
||||
mutate={mutate}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { GithubPullRequestNotificationListItem } from "./GithubPullRequestNotificationListItem";
|
||||
import { GithubDiscussionNotificationListItem } from "./GithubDiscussionNotificationListItem";
|
||||
import { NotificationListItemProps } from "../../../notification";
|
||||
import { environment } from "@raycast/api";
|
||||
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(() => {
|
||||
if (environment.appearance === "dark") {
|
||||
return "github-logo-light.svg";
|
||||
@@ -19,6 +19,7 @@ export function GithubNotificationListItem({ notification }: NotificationListIte
|
||||
icon={icon}
|
||||
notification={notification}
|
||||
githubPullRequest={notification.details.content}
|
||||
mutate={mutate}
|
||||
/>
|
||||
);
|
||||
case "GithubDiscussion":
|
||||
@@ -27,6 +28,7 @@ export function GithubNotificationListItem({ notification }: NotificationListIte
|
||||
icon={icon}
|
||||
notification={notification}
|
||||
githubDiscussion={notification.details.content}
|
||||
mutate={mutate}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
@@ -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 {
|
||||
GithubPullRequestState,
|
||||
GithubPullRequestReviewDecision,
|
||||
@@ -11,18 +7,26 @@ import {
|
||||
GithubCheckStatusState,
|
||||
GithubCheckSuite,
|
||||
} 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 {
|
||||
icon: string;
|
||||
notification: Notification;
|
||||
githubPullRequest: GithubPullRequest;
|
||||
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||
}
|
||||
|
||||
export function GithubPullRequestNotificationListItem({
|
||||
icon,
|
||||
notification,
|
||||
githubPullRequest,
|
||||
mutate,
|
||||
}: GithubPullRequestNotificationListItemProps) {
|
||||
const subtitle = `${githubPullRequest.head_repository?.name_with_owner} #${githubPullRequest.number}`;
|
||||
|
||||
@@ -67,6 +71,7 @@ export function GithubPullRequestNotificationListItem({
|
||||
<NotificationActions
|
||||
notification={notification}
|
||||
detailsTarget={<GithubPullRequestPreview notification={notification} githubPullRequest={githubPullRequest} />}
|
||||
mutate={mutate}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Notification, getNotificationHtmlUrl } from "../../../notification";
|
||||
import { Detail, ActionPanel, Action } from "@raycast/api";
|
||||
import { Notification } from "../../../types";
|
||||
import { GithubDiscussion } from "../types";
|
||||
import { useMemo } from "react";
|
||||
import { getNotificationHtmlUrl } from "../../../notification";
|
||||
|
||||
interface GithubDiscussionPreviewProps {
|
||||
notification: Notification;
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Notification, getNotificationHtmlUrl } from "../../../notification";
|
||||
import { Detail, ActionPanel, Action } from "@raycast/api";
|
||||
import { Notification } from "../../../types";
|
||||
import { GithubPullRequest } from "../types";
|
||||
import { useMemo } from "react";
|
||||
import { getNotificationHtmlUrl } from "../../../notification";
|
||||
|
||||
interface GithubPullRequestPreviewProps {
|
||||
notification: Notification;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NotificationActions } from "../../action/NotificationActions";
|
||||
import { NotificationListItemProps } from "../../notification";
|
||||
import { Detail, List, environment } from "@raycast/api";
|
||||
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(() => {
|
||||
if (environment.appearance === "dark") {
|
||||
return "google-mail-logo-light.svg";
|
||||
@@ -18,7 +18,11 @@ export function GoogleMailNotificationListItem({ notification }: NotificationLis
|
||||
icon={icon}
|
||||
subtitle={`#${notification.source_id}`}
|
||||
actions={
|
||||
<NotificationActions notification={notification} detailsTarget={<Detail markdown="# To be implemented 👋" />} />
|
||||
<NotificationActions
|
||||
notification={notification}
|
||||
detailsTarget={<Detail markdown="# To be implemented 👋" />}
|
||||
mutate={mutate}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NotificationActions } from "../../action/NotificationActions";
|
||||
import { NotificationListItemProps } from "../../notification";
|
||||
import { Detail, List, environment } from "@raycast/api";
|
||||
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(() => {
|
||||
if (environment.appearance === "dark") {
|
||||
return "linear-logo-light.svg";
|
||||
@@ -18,7 +18,11 @@ export function LinearNotificationListItem({ notification }: NotificationListIte
|
||||
icon={icon}
|
||||
subtitle={`#${notification.source_id}`}
|
||||
actions={
|
||||
<NotificationActions notification={notification} detailsTarget={<Detail markdown="# To be implemented 👋" />} />
|
||||
<NotificationActions
|
||||
notification={notification}
|
||||
detailsTarget={<Detail markdown="# To be implemented 👋" />}
|
||||
mutate={mutate}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NotificationTaskActions } from "../../action/NotificationTaskActions";
|
||||
import { NotificationListItemProps } from "../../notification";
|
||||
import { Detail, List, environment } from "@raycast/api";
|
||||
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(() => {
|
||||
if (environment.appearance === "dark") {
|
||||
return "todoist-icon-light.svg";
|
||||
@@ -21,6 +21,7 @@ export function TodoistNotificationListItem({ notification }: NotificationListIt
|
||||
<NotificationTaskActions
|
||||
notification={notification}
|
||||
detailsTarget={<Detail markdown="# To be implemented 👋" />}
|
||||
mutate={mutate}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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) {
|
||||
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
41
src/task.ts
Normal 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 };
|
||||
37
src/types.ts
37
src/types.ts
@@ -1,5 +1,3 @@
|
||||
import { GithubDiscussion, GithubPullRequest } from "./integrations/github/types";
|
||||
|
||||
export interface UniversalInboxPreferences {
|
||||
apiKey: string;
|
||||
universalInboxBaseUrl: string;
|
||||
@@ -11,38 +9,3 @@ export interface Page<T> {
|
||||
total: number;
|
||||
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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user