Compare commits

..

1 Commits

Author SHA1 Message Date
63d265bac9 chore: Prepare for publishing the extension 2024-01-30 09:42:09 +01:00
10 changed files with 50 additions and 303 deletions

View File

@@ -2,23 +2,7 @@
## [Unreleased] ## [Unreleased]
## [0.1.2] - 2024-02-01 ## [Initial Version] - 2024-01-29
### Added
- Add "Plan task" action for notification created from task
### Fixed
- Fix "Complete task" action
## [0.1.1] - 2024-02-01
### Added
- Add filter per notification kind
## [0.1.0] - 2024-01-29
### Added ### Added

View File

@@ -1,6 +1,6 @@
# Universal Inbox # Universal Inbox
Manage your notifications in a single [Universal Inbox](https://www.universal-inbox.com) Manage your notifications in a single Universal Inbox
## Setup ## Setup
@@ -15,4 +15,3 @@ Get the URL from the Universal Inbox instance you are usually connecting to.
You can get an API Key from your Universal Inbox user profile page: You can get an API Key from your Universal Inbox user profile page:
![user profile page](media/ui-user-profile-page.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

2
package-lock.json generated
View File

@@ -1,12 +1,10 @@
{ {
"name": "universal-inbox", "name": "universal-inbox",
"version": "0.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "universal-inbox", "name": "universal-inbox",
"version": "0.1.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@raycast/api": "^1.65.1", "@raycast/api": "^1.65.1",

View File

@@ -4,8 +4,8 @@
"title": "Universal Inbox", "title": "Universal Inbox",
"description": "Manage your notifications in a single Universal Inbox", "description": "Manage your notifications in a single Universal Inbox",
"icon": "ui-logo-transparent.png", "icon": "ui-logo-transparent.png",
"author": "dax42", "author": "dax",
"version": "0.1.1", "owner": "universal-inbox",
"categories": [ "categories": [
"Productivity" "Productivity"
], ],

View File

@@ -62,16 +62,12 @@ export function NotificationActions({ notification, detailsTarget, mutate }: Not
); );
} }
export async function deleteNotification( async function deleteNotification(notification: Notification, mutate: MutatePromise<Page<Notification> | undefined>) {
notification: Notification,
mutate: MutatePromise<Page<Notification> | undefined>,
) {
const preferences = getPreferenceValues<UniversalInboxPreferences>(); const preferences = getPreferenceValues<UniversalInboxPreferences>();
const toast = await showToast({ style: Toast.Style.Animated, title: "Deleting notification" }); const toast = await showToast({ style: Toast.Style.Animated, title: "Deleting notification" });
try { try {
if (isNotificationBuiltFromTask(notification) && notification.task) { if (isNotificationBuiltFromTask(notification) && notification.task) {
await mutate( await mutate(
handleErrors(
fetch(`${preferences.universalInboxBaseUrl}/api/tasks/${notification.task.id}`, { fetch(`${preferences.universalInboxBaseUrl}/api/tasks/${notification.task.id}`, {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ status: TaskStatus.Deleted }), body: JSON.stringify({ status: TaskStatus.Deleted }),
@@ -88,7 +84,6 @@ export async function deleteNotification(
return page; return page;
}, },
}, },
),
); );
} else { } else {
await mutate( await mutate(
@@ -123,7 +118,7 @@ export async function deleteNotification(
} }
} }
export async function unsubscribeFromNotification( async function unsubscribeFromNotification(
notification: Notification, notification: Notification,
mutate: MutatePromise<Page<Notification> | undefined>, mutate: MutatePromise<Page<Notification> | undefined>,
) { ) {
@@ -161,10 +156,7 @@ export async function unsubscribeFromNotification(
} }
} }
export async function snoozeNotification( async function snoozeNotification(notification: Notification, mutate: MutatePromise<Page<Notification> | undefined>) {
notification: Notification,
mutate: MutatePromise<Page<Notification> | undefined>,
) {
const preferences = getPreferenceValues<UniversalInboxPreferences>(); const preferences = getPreferenceValues<UniversalInboxPreferences>();
const toast = await showToast({ style: Toast.Style.Animated, title: "Snoozing notification" }); const toast = await showToast({ style: Toast.Style.Animated, title: "Snoozing notification" });
try { try {

View File

@@ -1,13 +1,24 @@
import { deleteNotification, snoozeNotification, unsubscribeFromNotification } from "./NotificationActions"; import { Notification, getNotificationHtmlUrl } from "../notification";
import { Notification, getNotificationHtmlUrl, isNotificationBuiltFromTask } from "../notification"; import { Action, ActionPanel, Icon } from "@raycast/api";
import { Action, ActionPanel, Icon, Toast, getPreferenceValues, showToast } from "@raycast/api";
import { MutatePromise } from "@raycast/utils"; import { MutatePromise } from "@raycast/utils";
import { useMemo, ReactElement } from "react"; import { useMemo, ReactElement } from "react";
import { PlanTask } from "./PlanTask";
import { handleErrors } from "../api";
import { TaskStatus } from "../task";
import { Page } from "../types"; import { Page } from "../types";
import fetch from "node-fetch";
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 completeTask(notification: Notification) {
console.log(`Completing task ${notification.id}`);
}
interface NotificationTaskActionsProps { interface NotificationTaskActionsProps {
notification: Notification; notification: Notification;
@@ -15,7 +26,7 @@ interface NotificationTaskActionsProps {
mutate: MutatePromise<Page<Notification> | undefined>; mutate: MutatePromise<Page<Notification> | undefined>;
} }
export function NotificationTaskActions({ notification, detailsTarget, mutate }: NotificationTaskActionsProps) { export function NotificationTaskActions({ notification, detailsTarget }: NotificationTaskActionsProps) {
const notificationHtmlUrl = useMemo(() => { const notificationHtmlUrl = useMemo(() => {
return getNotificationHtmlUrl(notification); return getNotificationHtmlUrl(notification);
}, [notification]); }, [notification]);
@@ -28,71 +39,26 @@ export function NotificationTaskActions({ notification, detailsTarget, mutate }:
title="Delete Notification" title="Delete Notification"
icon={Icon.Trash} icon={Icon.Trash}
shortcut={{ modifiers: ["ctrl"], key: "d" }} shortcut={{ modifiers: ["ctrl"], key: "d" }}
onAction={() => deleteNotification(notification, mutate)} onAction={() => deleteNotification(notification)}
/> />
<Action <Action
title="Unsubscribe From Notification" title="Unsubscribe From Notification"
icon={Icon.BellDisabled} icon={Icon.BellDisabled}
shortcut={{ modifiers: ["ctrl"], key: "u" }} shortcut={{ modifiers: ["ctrl"], key: "u" }}
onAction={() => unsubscribeFromNotification(notification, mutate)} onAction={() => unsubscribeFromNotification(notification)}
/> />
<Action <Action
title="Snooze" title="Snooze"
icon={Icon.Clock} icon={Icon.Clock}
shortcut={{ modifiers: ["ctrl"], key: "s" }} shortcut={{ modifiers: ["ctrl"], key: "s" }}
onAction={() => snoozeNotification(notification, mutate)} onAction={() => snoozeNotification(notification)}
/> />
<Action <Action
title="Complete Task" title="Complete Task"
icon={Icon.Calendar} icon={Icon.Calendar}
shortcut={{ modifiers: ["ctrl"], key: "c" }} shortcut={{ modifiers: ["ctrl"], key: "c" }}
onAction={() => completeTask(notification, mutate)} onAction={() => completeTask(notification)}
/>
<Action.Push
title="Plan Task..."
icon={Icon.Calendar}
shortcut={{ modifiers: ["ctrl"], key: "t" }}
target={<PlanTask notification={notification} mutate={mutate} />}
/> />
</ActionPanel> </ActionPanel>
); );
} }
async function completeTask(notification: Notification, mutate: MutatePromise<Page<Notification> | undefined>) {
if (!isNotificationBuiltFromTask(notification) || !notification.task) {
return;
}
const preferences = getPreferenceValues<UniversalInboxPreferences>();
const toast = await showToast({ style: Toast.Style.Animated, title: "Marking task as Done" });
try {
await mutate(
handleErrors(
fetch(`${preferences.universalInboxBaseUrl}/api/tasks/${notification.task.id}`, {
method: "PATCH",
body: JSON.stringify({ status: TaskStatus.Done }),
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 marked as Done";
} catch (error) {
toast.style = Toast.Style.Failure;
toast.title = "Failed to mark task as Done";
toast.message = (error as Error).message;
throw error;
}
}

View File

@@ -1,155 +0,0 @@
import { Action, useNavigation, ActionPanel, Form, Icon, getPreferenceValues, showToast, Toast } from "@raycast/api";
import { MutatePromise, useForm, FormValidation, useFetch } from "@raycast/utils";
import { Notification, isNotificationBuiltFromTask } from "../notification";
import { Page, UniversalInboxPreferences } from "../types";
import { default as dayjs, extend } from "dayjs";
import { TaskPriority } from "../task";
import { handleErrors } from "../api";
import utc from "dayjs/plugin/utc";
import { useState } from "react";
import fetch from "node-fetch";
extend(utc);
interface PlanTaskProps {
notification: Notification;
mutate: MutatePromise<Page<Notification> | undefined>;
}
interface TaskPlanningFormValues {
project: string;
dueAt: Date | null;
priority: string;
}
interface ProjectSummary {
name: string;
source_id: string;
}
interface TaskPlanning {
project: ProjectSummary;
due_at?: { type: "DateTimeWithTz"; content: string };
priority: TaskPriority;
}
export function PlanTask({ notification, mutate }: PlanTaskProps) {
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<TaskPlanningFormValues>({
initialValues: {
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 taskPlanning: TaskPlanning = {
project: project,
due_at: values.dueAt ? { type: "DateTimeWithTz", content: dayjs(values.dueAt).utc().format() } : undefined,
priority: parseInt(values.priority) as TaskPriority,
};
await planTask(taskPlanning, notification, mutate);
pop();
},
validation: {
project: FormValidation.Required,
priority: FormValidation.Required,
},
});
return (
<Form
navigationTitle="Plan task"
actions={
<ActionPanel>
<Action.SubmitForm title="Plan Task" icon={Icon.Calendar} onSubmit={handleSubmit} />
</ActionPanel>
}
>
<Form.Description title="Task title" text={notification.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 planTask(
taskPlanning: TaskPlanning,
notification: Notification,
mutate: MutatePromise<Page<Notification> | undefined>,
) {
if (!isNotificationBuiltFromTask(notification) || !notification.task) {
return;
}
const preferences = getPreferenceValues<UniversalInboxPreferences>();
const toast = await showToast({ style: Toast.Style.Animated, title: "Planning task" });
try {
await mutate(
handleErrors(
fetch(`${preferences.universalInboxBaseUrl}/api/tasks/${notification.task.id}`, {
method: "PATCH",
body: JSON.stringify({
project: taskPlanning.project.name,
due_at: taskPlanning.due_at,
priority: taskPlanning.priority,
}),
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 planned";
} catch (error) {
toast.style = Toast.Style.Failure;
toast.title = "Failed to plan task";
toast.message = (error as Error).message;
throw error;
}
}

View File

@@ -7,7 +7,6 @@ import { Notification, NotificationListItemProps } from "./notification";
import { NotificationActions } from "./action/NotificationActions"; import { NotificationActions } from "./action/NotificationActions";
import { Page, UniversalInboxPreferences } from "./types"; import { Page, UniversalInboxPreferences } from "./types";
import { useFetch } from "@raycast/utils"; import { useFetch } from "@raycast/utils";
import { useState } from "react";
export default function Command() { export default function Command() {
const preferences = getPreferenceValues<UniversalInboxPreferences>(); const preferences = getPreferenceValues<UniversalInboxPreferences>();
@@ -30,11 +29,8 @@ export default function Command() {
); );
} }
const [notificationKind, setNotificationKind] = useState("");
const { isLoading, data, mutate } = 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`,
notificationKind ? "&notification_kind=" + notificationKind : ""
}`,
{ {
headers: { headers: {
Authorization: `Bearer ${preferences.apiKey}`, Authorization: `Bearer ${preferences.apiKey}`,
@@ -43,24 +39,10 @@ export default function Command() {
); );
return ( return (
<List <List isLoading={isLoading}>
isLoading={isLoading} {data?.content.map((notification: Notification) => {
searchBarPlaceholder="Filter notifications..."
searchBarAccessory={
<NotificationKindDropdown value={notificationKind} onNotificationKindChange={setNotificationKind} />
}
>
{data?.content.length === 0 ? (
<List.EmptyView
icon={{ source: "ui-logo-transparent.png" }}
title="Congrats! You have reach zero inbox 🎉"
description="You don't have any new notifications."
/>
) : (
data?.content.map((notification: Notification) => {
return <NotificationListItem key={notification.id} notification={notification} mutate={mutate} />; return <NotificationListItem key={notification.id} notification={notification} mutate={mutate} />;
}) })}
)}
</List> </List>
); );
} }
@@ -96,22 +78,3 @@ function DefaultNotificationListItem({ notification, mutate }: NotificationListI
/> />
); );
} }
interface NotificationKindDropdownProps {
value: string;
onNotificationKindChange: (newValue: string) => void;
}
function NotificationKindDropdown({ value, onNotificationKindChange }: NotificationKindDropdownProps) {
return (
<List.Dropdown tooltip="Select Notification Kind" value={value} onChange={onNotificationKindChange}>
<List.Dropdown.Section title="Notification kind">
<List.Dropdown.Item key="0" title="" value="" />
<List.Dropdown.Item key="Github" title="Github" value="Github" />
<List.Dropdown.Item key="Linear" title="Linear" value="Linear" />
<List.Dropdown.Item key="GoogleMail" title="Google Mail" value="GoogleMail" />
<List.Dropdown.Item key="Todoist" title="Todoist" value="Todoist" />
</List.Dropdown.Section>
</List.Dropdown>
);
}