Compare commits
1 Commits
main
...
63d265bac9
| Author | SHA1 | Date | |
|---|---|---|---|
|
63d265bac9
|
41
CHANGELOG.md
@@ -2,46 +2,7 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.2.0] - 2024-12-16
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for Slack reaction notifications
|
||||
- Add support for Slack message notifications
|
||||
|
||||
## [0.1.4] - 2024-03-13
|
||||
|
||||
### Added
|
||||
|
||||
- Add Linear Project and Team icons
|
||||
- Add Slack notifications (ie. save for later) support
|
||||
- Set `Inbox` project as default when creating or planning a task
|
||||
|
||||
## [0.1.3] - 2024-02-05
|
||||
|
||||
### Added
|
||||
|
||||
- Display Linear notification reason
|
||||
- Display Linear project on notification item
|
||||
- Add missing action icons
|
||||
|
||||
## [0.1.2] - 2024-02-01
|
||||
|
||||
### 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
|
||||
## [Initial Version] - 2024-01-29
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Universal Inbox
|
||||
|
||||
Manage your notifications in a single [Universal Inbox](https://www.universal-inbox.com)
|
||||
Manage your notifications in a single Universal Inbox
|
||||
|
||||
## 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:
|
||||
|
||||

|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.362 10.11c0 .926-.756 1.681-1.681 1.681S0 11.036 0 10.111C0 9.186.756 8.43 1.68 8.43h1.682v1.68zm.846 0c0-.924.756-1.68 1.681-1.68s1.681.756 1.681 1.68v4.21c0 .924-.756 1.68-1.68 1.68a1.685 1.685 0 0 1-1.682-1.68v-4.21zM5.89 3.362c-.926 0-1.682-.756-1.682-1.681S4.964 0 5.89 0s1.68.756 1.68 1.68v1.682H5.89zm0 .846c.924 0 1.68.756 1.68 1.681S6.814 7.57 5.89 7.57H1.68C.757 7.57 0 6.814 0 5.89c0-.926.756-1.682 1.68-1.682h4.21zm6.749 1.682c0-.926.755-1.682 1.68-1.682.925 0 1.681.756 1.681 1.681s-.756 1.681-1.68 1.681h-1.681V5.89zm-.848 0c0 .924-.755 1.68-1.68 1.68A1.685 1.685 0 0 1 8.43 5.89V1.68C8.43.757 9.186 0 10.11 0c.926 0 1.681.756 1.681 1.68v4.21zm-1.681 6.748c.926 0 1.682.756 1.682 1.681S11.036 16 10.11 16s-1.681-.756-1.681-1.68v-1.682h1.68zm0-.847c-.924 0-1.68-.755-1.68-1.68 0-.925.756-1.681 1.68-1.681h4.21c.924 0 1.68.756 1.68 1.68 0 .926-.756 1.681-1.68 1.681h-4.21z"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
@@ -1,2 +0,0 @@
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path d="M3.362 10.11c0 .926-.756 1.681-1.681 1.681S0 11.036 0 10.111C0 9.186.756 8.43 1.68 8.43h1.682v1.68zm.846 0c0-.924.756-1.68 1.681-1.68s1.681.756 1.681 1.68v4.21c0 .924-.756 1.68-1.68 1.68a1.685 1.685 0 0 1-1.682-1.68v-4.21zM5.89 3.362c-.926 0-1.682-.756-1.682-1.681S4.964 0 5.89 0s1.68.756 1.68 1.68v1.682H5.89zm0 .846c.924 0 1.68.756 1.68 1.681S6.814 7.57 5.89 7.57H1.68C.757 7.57 0 6.814 0 5.89c0-.926.756-1.682 1.68-1.682h4.21zm6.749 1.682c0-.926.755-1.682 1.68-1.682.925 0 1.681.756 1.681 1.681s-.756 1.681-1.68 1.681h-1.681V5.89zm-.848 0c0 .924-.755 1.68-1.68 1.68A1.685 1.685 0 0 1 8.43 5.89V1.68C8.43.757 9.186 0 10.11 0c.926 0 1.681.756 1.681 1.68v4.21zm-1.681 6.748c.926 0 1.682.756 1.682 1.681S11.036 16 10.11 16s-1.681-.756-1.681-1.68v-1.682h1.68zm0-.847c-.924 0-1.68-.755-1.68-1.68 0-.925.756-1.681 1.68-1.681h4.21c.924 0 1.68.756 1.68 1.68 0 .926-.756 1.681-1.68 1.681h-4.21z"></path></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 11 KiB |
@@ -43,7 +43,6 @@
|
||||
},
|
||||
"nodejs@latest": {
|
||||
"last_modified": "2024-01-14T03:55:27Z",
|
||||
"plugin_version": "0.0.2",
|
||||
"resolved": "github:NixOS/nixpkgs/dd5621df6dcb90122b50da5ec31c411a0de3e538#nodejs_21",
|
||||
"source": "devbox-search",
|
||||
"version": "21.5.0",
|
||||
|
||||
4
justfile
@@ -20,7 +20,3 @@ fix:
|
||||
# Run extension in development mode
|
||||
run:
|
||||
npm run dev
|
||||
|
||||
# Publish a new version of the extension
|
||||
publish:
|
||||
npm run publish
|
||||
|
||||
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 2.1 MiB |
3782
package-lock.json
generated
14
package.json
@@ -4,9 +4,11 @@
|
||||
"title": "Universal Inbox",
|
||||
"description": "Manage your notifications in a single Universal Inbox",
|
||||
"icon": "ui-logo-transparent.png",
|
||||
"author": "dax42",
|
||||
"version": "0.2.0",
|
||||
"categories": ["Productivity"],
|
||||
"author": "dax",
|
||||
"owner": "universal-inbox",
|
||||
"categories": [
|
||||
"Productivity"
|
||||
],
|
||||
"license": "MIT",
|
||||
"preferences": [
|
||||
{
|
||||
@@ -43,9 +45,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@raycast/eslint-config": "^1.0.6",
|
||||
"@types/node": "^20.8.10",
|
||||
"@types/react": "^18.2.27",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
"@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",
|
||||
|
||||
@@ -41,7 +41,7 @@ export function CreateTaskFromNotification({ notification, mutate }: CreateTaskF
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const { isLoading, data: projects } = useFetch<Array<ProjectSummary>>(
|
||||
`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/tasks/projects/search?matches=${searchText}`,
|
||||
`${preferences.universalInboxBaseUrl}/api/tasks/projects/search?matches=${searchText}`,
|
||||
{
|
||||
keepPreviousData: true,
|
||||
headers: {
|
||||
@@ -53,7 +53,6 @@ export function CreateTaskFromNotification({ notification, mutate }: CreateTaskF
|
||||
const { handleSubmit, itemProps } = useForm<TaskCreationFormValues>({
|
||||
initialValues: {
|
||||
title: notification.title,
|
||||
project: projects?.find((p) => p.name === "Inbox")?.source_id,
|
||||
dueAt: new Date(),
|
||||
priority: `${TaskPriority.P4 as number}`,
|
||||
},
|
||||
@@ -123,7 +122,7 @@ async function createTaskFromNotification(
|
||||
try {
|
||||
await mutate(
|
||||
handleErrors(
|
||||
fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/notifications/${notification.id}/task`, {
|
||||
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}/task`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(taskCreation),
|
||||
headers: {
|
||||
|
||||
@@ -22,7 +22,7 @@ export function LinkNotificationToTask({ notification, mutate }: LinkNotificatio
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const { isLoading, data: tasks } = useFetch<Array<Task>>(
|
||||
`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/tasks/search?matches=${searchText}`,
|
||||
`${preferences.universalInboxBaseUrl}/api/tasks/search?matches=${searchText}`,
|
||||
{
|
||||
keepPreviousData: true,
|
||||
headers: {
|
||||
@@ -78,7 +78,7 @@ async function linkNotificationToTask(
|
||||
try {
|
||||
await mutate(
|
||||
handleErrors(
|
||||
fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/notifications/${notification.id}`, {
|
||||
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status: TaskStatus.Deleted, task_id: taskId }),
|
||||
headers: {
|
||||
@@ -92,6 +92,7 @@ async function linkNotificationToTask(
|
||||
if (page) {
|
||||
page.content = page.content.filter((n) => n.id !== notification.id);
|
||||
}
|
||||
console.log(`page(link): ${notification.id}`, page);
|
||||
return page;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ export function NotificationActions({ notification, detailsTarget, mutate }: Not
|
||||
return (
|
||||
<ActionPanel>
|
||||
<Action.OpenInBrowser url={notificationHtmlUrl} />
|
||||
<Action.Push title="Show Details" icon={Icon.AppWindowSidebarRight} target={detailsTarget} />
|
||||
<Action.Push title="Show Details" target={detailsTarget} />
|
||||
<Action
|
||||
title="Delete Notification"
|
||||
icon={Icon.Trash}
|
||||
@@ -35,7 +35,7 @@ export function NotificationActions({ notification, detailsTarget, mutate }: Not
|
||||
onAction={() => deleteNotification(notification, mutate)}
|
||||
/>
|
||||
<Action
|
||||
title="Unsubscribe from Notification"
|
||||
title="Unsubscribe From Notification"
|
||||
icon={Icon.BellDisabled}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "u" }}
|
||||
onAction={() => unsubscribeFromNotification(notification, mutate)}
|
||||
@@ -47,13 +47,13 @@ export function NotificationActions({ notification, detailsTarget, mutate }: Not
|
||||
onAction={() => snoozeNotification(notification, mutate)}
|
||||
/>
|
||||
<Action.Push
|
||||
title="Create Task…"
|
||||
title="Create Task..."
|
||||
icon={Icon.Calendar}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "t" }}
|
||||
target={<CreateTaskFromNotification notification={notification} mutate={mutate} />}
|
||||
/>
|
||||
<Action.Push
|
||||
title="Link to Task…"
|
||||
title="Link to Task..."
|
||||
icon={Icon.Link}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "l" }}
|
||||
target={<LinkNotificationToTask notification={notification} mutate={mutate} />}
|
||||
@@ -62,25 +62,20 @@ export function NotificationActions({ notification, detailsTarget, mutate }: Not
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteNotification(
|
||||
notification: Notification,
|
||||
mutate: MutatePromise<Page<Notification> | undefined>,
|
||||
) {
|
||||
async function deleteNotification(notification: Notification, mutate: MutatePromise<Page<Notification> | undefined>) {
|
||||
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||
const toast = await showToast({ style: Toast.Style.Animated, title: "Deleting notification" });
|
||||
try {
|
||||
if (isNotificationBuiltFromTask(notification) && notification.task) {
|
||||
await mutate(
|
||||
handleErrors(
|
||||
fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/tasks/${notification.task.id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status: TaskStatus.Deleted }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${preferences.apiKey}`,
|
||||
},
|
||||
}),
|
||||
),
|
||||
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) {
|
||||
@@ -93,7 +88,7 @@ export async function deleteNotification(
|
||||
} else {
|
||||
await mutate(
|
||||
handleErrors(
|
||||
fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/notifications/${notification.id}`, {
|
||||
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status: NotificationStatus.Deleted }),
|
||||
headers: {
|
||||
@@ -123,7 +118,7 @@ export async function deleteNotification(
|
||||
}
|
||||
}
|
||||
|
||||
export async function unsubscribeFromNotification(
|
||||
async function unsubscribeFromNotification(
|
||||
notification: Notification,
|
||||
mutate: MutatePromise<Page<Notification> | undefined>,
|
||||
) {
|
||||
@@ -132,7 +127,7 @@ export async function unsubscribeFromNotification(
|
||||
try {
|
||||
await mutate(
|
||||
handleErrors(
|
||||
fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/notifications/${notification.id}`, {
|
||||
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status: NotificationStatus.Unsubscribed }),
|
||||
headers: {
|
||||
@@ -161,17 +156,14 @@ export async function unsubscribeFromNotification(
|
||||
}
|
||||
}
|
||||
|
||||
export async function snoozeNotification(
|
||||
notification: Notification,
|
||||
mutate: MutatePromise<Page<Notification> | undefined>,
|
||||
) {
|
||||
async function snoozeNotification(notification: Notification, mutate: MutatePromise<Page<Notification> | undefined>) {
|
||||
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||
const toast = await showToast({ style: Toast.Style.Animated, title: "Snoozing notification" });
|
||||
try {
|
||||
const snoozeTime = computeSnoozedUntil(new Date(), 1, 6);
|
||||
await mutate(
|
||||
handleErrors(
|
||||
fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/notifications/${notification.id}`, {
|
||||
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ snoozed_until: snoozeTime }),
|
||||
headers: {
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import { deleteNotification, snoozeNotification, unsubscribeFromNotification } from "./NotificationActions";
|
||||
import { Notification, getNotificationHtmlUrl, isNotificationBuiltFromTask } from "../notification";
|
||||
import { Action, ActionPanel, Icon, Toast, getPreferenceValues, showToast } from "@raycast/api";
|
||||
import { Page, UniversalInboxPreferences } from "../types";
|
||||
import { Notification, getNotificationHtmlUrl } from "../notification";
|
||||
import { Action, ActionPanel, Icon } from "@raycast/api";
|
||||
import { MutatePromise } from "@raycast/utils";
|
||||
import { useMemo, ReactElement } from "react";
|
||||
import { PlanTask } from "./PlanTask";
|
||||
import { handleErrors } from "../api";
|
||||
import { TaskStatus } from "../task";
|
||||
import fetch from "node-fetch";
|
||||
import { Page } 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 completeTask(notification: Notification) {
|
||||
console.log(`Completing task ${notification.id}`);
|
||||
}
|
||||
|
||||
interface NotificationTaskActionsProps {
|
||||
notification: Notification;
|
||||
@@ -15,7 +26,7 @@ interface NotificationTaskActionsProps {
|
||||
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||
}
|
||||
|
||||
export function NotificationTaskActions({ notification, detailsTarget, mutate }: NotificationTaskActionsProps) {
|
||||
export function NotificationTaskActions({ notification, detailsTarget }: NotificationTaskActionsProps) {
|
||||
const notificationHtmlUrl = useMemo(() => {
|
||||
return getNotificationHtmlUrl(notification);
|
||||
}, [notification]);
|
||||
@@ -23,76 +34,31 @@ export function NotificationTaskActions({ notification, detailsTarget, mutate }:
|
||||
return (
|
||||
<ActionPanel>
|
||||
<Action.OpenInBrowser url={notificationHtmlUrl} />
|
||||
<Action.Push title="Show Details" icon={Icon.AppWindowSidebarRight} target={detailsTarget} />
|
||||
<Action.Push title="Show Details" target={detailsTarget} />
|
||||
<Action
|
||||
title="Delete Notification"
|
||||
icon={Icon.Trash}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "d" }}
|
||||
onAction={() => deleteNotification(notification, mutate)}
|
||||
onAction={() => deleteNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Unsubscribe from Notification"
|
||||
title="Unsubscribe From Notification"
|
||||
icon={Icon.BellDisabled}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "u" }}
|
||||
onAction={() => unsubscribeFromNotification(notification, mutate)}
|
||||
onAction={() => unsubscribeFromNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Snooze"
|
||||
icon={Icon.Clock}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "s" }}
|
||||
onAction={() => snoozeNotification(notification, mutate)}
|
||||
onAction={() => snoozeNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Complete Task"
|
||||
icon={Icon.CheckCircle}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "c" }}
|
||||
onAction={() => completeTask(notification, mutate)}
|
||||
/>
|
||||
<Action.Push
|
||||
title="Plan Task…"
|
||||
icon={Icon.Calendar}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "t" }}
|
||||
target={<PlanTask notification={notification} mutate={mutate} />}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "c" }}
|
||||
onAction={() => completeTask(notification)}
|
||||
/>
|
||||
</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.replace(/\/$/, "")}/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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,156 +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.replace(/\/$/, "")}/api/tasks/projects/search?matches=${searchText}`,
|
||||
{
|
||||
keepPreviousData: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${preferences.apiKey}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { handleSubmit, itemProps } = useForm<TaskPlanningFormValues>({
|
||||
initialValues: {
|
||||
dueAt: new Date(),
|
||||
project: projects?.find((p) => p.name === "Inbox")?.source_id,
|
||||
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.replace(/\/$/, "")}/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;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
import { Action, ActionPanel, Detail, Icon, List, getPreferenceValues, openExtensionPreferences } from "@raycast/api";
|
||||
import { GoogleMailNotificationListItem } from "./integrations/google-mail/listitem/GoogleMailNotificationListItem";
|
||||
import { Action, ActionPanel, Detail, List, getPreferenceValues, openExtensionPreferences } from "@raycast/api";
|
||||
import { TodoistNotificationListItem } from "./integrations/todoist/listitem/TodoistNotificationListItem";
|
||||
import { GithubNotificationListItem } from "./integrations/github/listitem/GithubNotificationListItem";
|
||||
import { LinearNotificationListItem } from "./integrations/linear/listitem/LinearNotificationListItem";
|
||||
import { SlackNotificationListItem } from "./integrations/slack/listitem/SlackNotificationListItem";
|
||||
import { Notification, NotificationListItemProps, NotificationSourceKind } from "./notification";
|
||||
import { Notification, NotificationListItemProps } from "./notification";
|
||||
import { NotificationActions } from "./action/NotificationActions";
|
||||
import { Page, UniversalInboxPreferences } from "./types";
|
||||
import { useFetch } from "@raycast/utils";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function Command() {
|
||||
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||
@@ -24,18 +22,15 @@ export default function Command() {
|
||||
markdown={"API key incorrect. Please update it in extension preferences and try again."}
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<Action title="Open Extension Preferences" icon={Icon.Gear} onAction={openExtensionPreferences} />
|
||||
<Action title="Open Extension Preferences" onAction={openExtensionPreferences} />
|
||||
</ActionPanel>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const [notificationKind, setNotificationKind] = useState("");
|
||||
const { isLoading, data, mutate } = useFetch<Page<Notification>>(
|
||||
`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/notifications?status=Unread,Read&with_tasks=true${
|
||||
notificationKind ? "¬ification_kind=" + notificationKind : ""
|
||||
}`,
|
||||
`${preferences.universalInboxBaseUrl}/api/notifications?status=Unread,Read&with_tasks=true`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${preferences.apiKey}`,
|
||||
@@ -44,39 +39,23 @@ export default function Command() {
|
||||
);
|
||||
|
||||
return (
|
||||
<List
|
||||
isLoading={isLoading}
|
||||
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} />;
|
||||
})
|
||||
)}
|
||||
<List isLoading={isLoading}>
|
||||
{data?.content.map((notification: Notification) => {
|
||||
return <NotificationListItem key={notification.id} notification={notification} mutate={mutate} />;
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationListItem({ notification, mutate }: NotificationListItemProps) {
|
||||
switch (notification.kind) {
|
||||
case NotificationSourceKind.Github:
|
||||
switch (notification.metadata.type) {
|
||||
case "Github":
|
||||
return <GithubNotificationListItem notification={notification} mutate={mutate} />;
|
||||
case NotificationSourceKind.Linear:
|
||||
case "Linear":
|
||||
return <LinearNotificationListItem notification={notification} mutate={mutate} />;
|
||||
case NotificationSourceKind.GoogleMail:
|
||||
case "GoogleMail":
|
||||
return <GoogleMailNotificationListItem notification={notification} mutate={mutate} />;
|
||||
case NotificationSourceKind.Slack:
|
||||
return <SlackNotificationListItem notification={notification} mutate={mutate} />;
|
||||
case NotificationSourceKind.Todoist:
|
||||
case "Todoist":
|
||||
return <TodoistNotificationListItem notification={notification} mutate={mutate} />;
|
||||
default:
|
||||
return <DefaultNotificationListItem notification={notification} mutate={mutate} />;
|
||||
@@ -88,7 +67,7 @@ function DefaultNotificationListItem({ notification, mutate }: NotificationListI
|
||||
<List.Item
|
||||
key={notification.id}
|
||||
title={notification.title}
|
||||
subtitle={`#${notification.source_item.source_id}`}
|
||||
subtitle={`#${notification.source_id}`}
|
||||
actions={
|
||||
<NotificationActions
|
||||
notification={notification}
|
||||
@@ -99,23 +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="Slack" title="Slack" value="Slack" />
|
||||
<List.Dropdown.Item key="Todoist" title="Todoist" value="Todoist" />
|
||||
</List.Dropdown.Section>
|
||||
</List.Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,14 +3,12 @@ import { GithubDiscussionNotificationListItem } from "./GithubDiscussionNotifica
|
||||
import { NotificationListItemProps } from "../../../notification";
|
||||
|
||||
export function GithubNotificationListItem({ notification, mutate }: NotificationListItemProps) {
|
||||
if (notification.source_item.data.type !== "GithubNotification") return null;
|
||||
|
||||
switch (notification.source_item.data.content.item?.type) {
|
||||
switch (notification.details?.type) {
|
||||
case "GithubPullRequest":
|
||||
return (
|
||||
<GithubPullRequestNotificationListItem
|
||||
notification={notification}
|
||||
githubPullRequest={notification.source_item.data.content.item.content}
|
||||
githubPullRequest={notification.details.content}
|
||||
mutate={mutate}
|
||||
/>
|
||||
);
|
||||
@@ -18,7 +16,7 @@ export function GithubNotificationListItem({ notification, mutate }: Notificatio
|
||||
return (
|
||||
<GithubDiscussionNotificationListItem
|
||||
notification={notification}
|
||||
githubDiscussion={notification.source_item.data.content.item.content}
|
||||
githubDiscussion={notification.details.content}
|
||||
mutate={mutate}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getNotificationHtmlUrl, Notification } from "../../../notification";
|
||||
import { Notification, getNotificationHtmlUrl } from "../../../notification";
|
||||
import { Detail, ActionPanel, Action } from "@raycast/api";
|
||||
import { GithubDiscussion } from "../types";
|
||||
import { useMemo } from "react";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getNotificationHtmlUrl, Notification } from "../../../notification";
|
||||
import { Notification, getNotificationHtmlUrl } from "../../../notification";
|
||||
import { Detail, ActionPanel, Action } from "@raycast/api";
|
||||
import { GithubPullRequest } from "../types";
|
||||
import { useMemo } from "react";
|
||||
|
||||
@@ -1,60 +1,3 @@
|
||||
import { match, P } from "ts-pattern";
|
||||
|
||||
export interface GithubNotification {
|
||||
id: string;
|
||||
repository: GithubRepositorySummary;
|
||||
subject: GithubNotificationSubject;
|
||||
reason: string;
|
||||
unread: boolean;
|
||||
updated_at: Date;
|
||||
last_read_at?: Date;
|
||||
url: string;
|
||||
subscription_url: string;
|
||||
item?: GithubNotificationItem;
|
||||
}
|
||||
|
||||
export function getGithubNotificationHtmlUrl(notification: GithubNotification): string {
|
||||
return match(notification.item)
|
||||
.with({ type: "GithubPullRequest", content: P.select() }, (pr) => pr.url)
|
||||
.with({ type: "GithubDiscussion", content: P.select() }, (discussion) => discussion.url)
|
||||
.otherwise(() => getHtmlUrlFromApiUrl(notification.subject.url) ?? getHtmlUrlFromMetadata(notification));
|
||||
}
|
||||
|
||||
function getHtmlUrlFromApiUrl(apiUrl: string | undefined): string | undefined {
|
||||
if (!apiUrl) {
|
||||
return undefined;
|
||||
}
|
||||
const url = new URL(apiUrl);
|
||||
if (url.host === "api.github.com" && url.pathname.startsWith("/repos")) {
|
||||
const result = new URL(apiUrl);
|
||||
result.host = "github.com";
|
||||
result.pathname = url.pathname.replace("/repos", "").replace("/pulls/", "/pull/");
|
||||
return result.toString();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getHtmlUrlFromMetadata(notification: GithubNotification): string {
|
||||
return match(notification.subject.type)
|
||||
.with("CheckSuite", () => {
|
||||
const result = new URL(notification.repository.url);
|
||||
result.pathname = `${result.pathname}/actions`;
|
||||
return result.toString();
|
||||
})
|
||||
.otherwise(() => notification.repository.url);
|
||||
}
|
||||
|
||||
export interface GithubNotificationSubject {
|
||||
title: string;
|
||||
url?: string;
|
||||
latest_comment_url?: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export type GithubNotificationItem =
|
||||
| { type: "GithubPullRequest"; content: GithubPullRequest }
|
||||
| { type: "GithubDiscussion"; content: GithubDiscussion };
|
||||
|
||||
export interface GithubPullRequest {
|
||||
id: string;
|
||||
number: number;
|
||||
|
||||
@@ -2,12 +2,12 @@ import { GoogleMailThreadListItem } from "./GoogleMailThreadListItem";
|
||||
import { NotificationListItemProps } from "../../../notification";
|
||||
|
||||
export function GoogleMailNotificationListItem({ notification, mutate }: NotificationListItemProps) {
|
||||
if (notification.source_item.data.type !== "GoogleMailThread") return null;
|
||||
if (notification.metadata.type !== "GoogleMail") return null;
|
||||
|
||||
return (
|
||||
<GoogleMailThreadListItem
|
||||
notification={notification}
|
||||
googleMailThread={notification.source_item.data.content}
|
||||
googleMailThread={notification.metadata.content}
|
||||
mutate={mutate}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { GoogleMailThreadPreview } from "../preview/GoogleMailThreadPreview";
|
||||
import { NotificationActions } from "../../../action/NotificationActions";
|
||||
//import { getLinearUserAccessory } from "../accessories";
|
||||
import { Notification } from "../../../notification";
|
||||
import { Icon, Color, List } from "@raycast/api";
|
||||
import { MutatePromise } from "@raycast/utils";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getNotificationHtmlUrl, Notification } from "../../../notification";
|
||||
import { Notification, getNotificationHtmlUrl } from "../../../notification";
|
||||
import { Detail, ActionPanel, Action } from "@raycast/api";
|
||||
import { GoogleMailThread } from "../types";
|
||||
import { useMemo } from "react";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Icon, Image, List } from "@raycast/api";
|
||||
import { getAvatarIcon } from "@raycast/utils";
|
||||
import { LinearUser } from "./types";
|
||||
import { match } from "ts-pattern";
|
||||
|
||||
export function getLinearUserAccessory(user?: LinearUser): List.Item.Accessory {
|
||||
if (user) {
|
||||
@@ -12,26 +11,3 @@ export function getLinearUserAccessory(user?: LinearUser): List.Item.Accessory {
|
||||
}
|
||||
return { icon: Icon.Person, tooltip: "Unknown" };
|
||||
}
|
||||
|
||||
export function getLinearNotificationReasonAccessory(notification_type: string): List.Item.Accessory {
|
||||
const reason = match(notification_type)
|
||||
.with("issueAddedToTriage", () => "Added To Triage")
|
||||
.with("issueAddedToView", () => "Added To View")
|
||||
.with("issueAssignedToYou", () => "Assigned To You")
|
||||
.with("issueBlocking", () => "Blocked")
|
||||
.with("issueCommentMention", () => "Comment Mention")
|
||||
.with("issueCommentReaction", () => "Comment Reaction")
|
||||
.with("issueCreated", () => "Created")
|
||||
.with("issueDue", () => "Due")
|
||||
.with("issueEmojiReaction", () => "Reaction")
|
||||
.with("issueMention", () => "Mention")
|
||||
.with("issueNewComment", () => "New Comment")
|
||||
.with("issueStatusChanged", () => "Status Changed")
|
||||
.with("issueUnassignedFromYou", () => "Unassigned From You")
|
||||
.with("projectAddedAsLead", () => "Added As Lead")
|
||||
.with("projectAddedAsMember", () => "Added As Member")
|
||||
.with("projectUpdateCreated", () => "Update Created")
|
||||
.with("projectUpdateMentionPrompt", () => "Update Mention")
|
||||
.otherwise(() => notification_type);
|
||||
return { tag: { value: reason } };
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { LinearWorkflowStateType, LinearIssueNotification, LinearWorkflowState } from "../types";
|
||||
import { getLinearNotificationReasonAccessory, getLinearUserAccessory } from "../accessories";
|
||||
import { NotificationActions } from "../../../action/NotificationActions";
|
||||
import { LinearIssuePreview } from "../preview/LinearIssuePreview";
|
||||
import { getLinearUserAccessory } from "../accessories";
|
||||
import { Notification } from "../../../notification";
|
||||
import { MutatePromise } from "@raycast/utils";
|
||||
import { Page } from "../../../types";
|
||||
import { match, P } from "ts-pattern";
|
||||
import { List } from "@raycast/api";
|
||||
import { match } from "ts-pattern";
|
||||
|
||||
interface LinearIssueNotificationListItemProps {
|
||||
notification: Notification;
|
||||
@@ -19,25 +19,12 @@ export function LinearIssueNotificationListItem({
|
||||
linearIssueNotification,
|
||||
mutate,
|
||||
}: LinearIssueNotificationListItemProps) {
|
||||
const projectSubtitle = match(linearIssueNotification.issue.project)
|
||||
.with({ name: P.select(), icon: P.nullish }, (project_name) => `/ ${project_name}`)
|
||||
.with(
|
||||
{ name: P.select("project_name"), icon: P.select("icon") },
|
||||
({ project_name, icon }) => `/ ${icon} ${project_name}`,
|
||||
)
|
||||
.otherwise(() => "");
|
||||
const teamSubtitle = match(linearIssueNotification.issue.team)
|
||||
.with({ name: P.select(), icon: P.nullish }, (team_name) => `${team_name}`)
|
||||
.with({ name: P.select("team_name"), icon: P.select("icon") }, ({ team_name, icon }) => `${icon} ${team_name}`)
|
||||
.otherwise(() => "");
|
||||
const subtitle = `${teamSubtitle} ${projectSubtitle} #${linearIssueNotification.issue.identifier}`;
|
||||
const subtitle = `${linearIssueNotification.issue.team.name} #${linearIssueNotification.issue.identifier}`;
|
||||
|
||||
const state = getLinearIssueStateAccessory(linearIssueNotification.issue.state);
|
||||
const assignee = getLinearUserAccessory(linearIssueNotification.issue.assignee);
|
||||
const reason = getLinearNotificationReasonAccessory(linearIssueNotification.type);
|
||||
|
||||
const accessories: List.Item.Accessory[] = [
|
||||
reason,
|
||||
state,
|
||||
assignee,
|
||||
{
|
||||
|
||||
@@ -3,14 +3,14 @@ import { LinearIssueNotificationListItem } from "./LinearIssueNotificationListIt
|
||||
import { NotificationListItemProps } from "../../../notification";
|
||||
|
||||
export function LinearNotificationListItem({ notification, mutate }: NotificationListItemProps) {
|
||||
if (notification.source_item.data.type !== "LinearNotification") return null;
|
||||
if (notification.metadata.type !== "Linear") return null;
|
||||
|
||||
switch (notification.source_item.data.content.type) {
|
||||
switch (notification.metadata.content.type) {
|
||||
case "IssueNotification":
|
||||
return (
|
||||
<LinearIssueNotificationListItem
|
||||
notification={notification}
|
||||
linearIssueNotification={notification.source_item.data.content.content}
|
||||
linearIssueNotification={notification.metadata.content.content}
|
||||
mutate={mutate}
|
||||
/>
|
||||
);
|
||||
@@ -18,7 +18,7 @@ export function LinearNotificationListItem({ notification, mutate }: Notificatio
|
||||
return (
|
||||
<LinearProjectNotificationListItem
|
||||
notification={notification}
|
||||
linearProjectNotification={notification.source_item.data.content.content}
|
||||
linearProjectNotification={notification.metadata.content.content}
|
||||
mutate={mutate}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { getLinearNotificationReasonAccessory, getLinearUserAccessory } from "../accessories";
|
||||
import { LinearProjectNotification, LinearProjectState, LinearProject } from "../types";
|
||||
import { NotificationActions } from "../../../action/NotificationActions";
|
||||
import { LinearProjectPreview } from "../preview/LinearProjectPreview";
|
||||
import { getLinearUserAccessory } from "../accessories";
|
||||
import { Notification } from "../../../notification";
|
||||
import { MutatePromise } from "@raycast/utils";
|
||||
import { List, Color } from "@raycast/api";
|
||||
import { Page } from "../../../types";
|
||||
import { match, P } from "ts-pattern";
|
||||
import { match } from "ts-pattern";
|
||||
|
||||
interface LinearProjectNotificationListItemProps {
|
||||
notification: Notification;
|
||||
@@ -19,20 +19,12 @@ export function LinearProjectNotificationListItem({
|
||||
linearProjectNotification,
|
||||
mutate,
|
||||
}: LinearProjectNotificationListItemProps) {
|
||||
const subtitle = match(linearProjectNotification.project)
|
||||
.with({ name: P.select(), icon: P.nullish }, (project_name) => `${project_name}`)
|
||||
.with(
|
||||
{ name: P.select("project_name"), icon: P.select("icon") },
|
||||
({ project_name, icon }) => `${icon} ${project_name}`,
|
||||
)
|
||||
.otherwise(() => "");
|
||||
const subtitle = linearProjectNotification.project.name;
|
||||
|
||||
const state = getLinearProjectStateAccessory(linearProjectNotification.project);
|
||||
const lead = getLinearUserAccessory(linearProjectNotification.project.lead);
|
||||
const reason = getLinearNotificationReasonAccessory(linearProjectNotification.type);
|
||||
|
||||
const accessories: List.Item.Accessory[] = [
|
||||
reason,
|
||||
state,
|
||||
lead,
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getNotificationHtmlUrl, Notification } from "../../../notification";
|
||||
import { Notification, getNotificationHtmlUrl } from "../../../notification";
|
||||
import { Detail, ActionPanel, Action } from "@raycast/api";
|
||||
import { LinearIssue } from "../types";
|
||||
import { useMemo } from "react";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getNotificationHtmlUrl, Notification } from "../../../notification";
|
||||
import { Notification, getNotificationHtmlUrl } from "../../../notification";
|
||||
import { Detail, ActionPanel, Action } from "@raycast/api";
|
||||
import { LinearProject } from "../types";
|
||||
import { useMemo } from "react";
|
||||
|
||||
@@ -2,15 +2,6 @@ export type LinearNotification =
|
||||
| { type: "IssueNotification"; content: LinearIssueNotification }
|
||||
| { type: "ProjectNotification"; content: LinearProjectNotification };
|
||||
|
||||
export function getLinearNotificationHtmlUrl(notification: LinearNotification): string {
|
||||
switch (notification.type) {
|
||||
case "IssueNotification":
|
||||
return notification.content.issue.url;
|
||||
case "ProjectNotification":
|
||||
return notification.content.project.url;
|
||||
}
|
||||
}
|
||||
|
||||
export interface LinearIssueNotification {
|
||||
id: string;
|
||||
type: string;
|
||||
@@ -48,10 +39,6 @@ export interface LinearIssue {
|
||||
team: LinearTeam;
|
||||
}
|
||||
|
||||
export function getLinearIssueHtmlUrl(linearIssue: LinearIssue): string {
|
||||
return linearIssue.url;
|
||||
}
|
||||
|
||||
export enum LinearIssuePriority {
|
||||
NoPriority = 0,
|
||||
Urgent = 1,
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { SlackIcon, SlackUser } from "./types";
|
||||
|
||||
export function getSlackUserAvatarUrl(slackUser: SlackUser): string | null {
|
||||
if (!slackUser.profile) {
|
||||
return null;
|
||||
}
|
||||
if (slackUser.profile.image_24) {
|
||||
return slackUser.profile.image_24;
|
||||
}
|
||||
if (slackUser.profile.image_32) {
|
||||
return slackUser.profile.image_32;
|
||||
}
|
||||
if (slackUser.profile.image_34) {
|
||||
return slackUser.profile.image_34;
|
||||
}
|
||||
if (slackUser.profile.image_44) {
|
||||
return slackUser.profile.image_44;
|
||||
}
|
||||
if (slackUser.profile.image_48) {
|
||||
return slackUser.profile.image_48;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getSlackIconUrl(slackIcon?: SlackIcon): string | null {
|
||||
if (slackIcon?.image_24) {
|
||||
return slackIcon.image_24;
|
||||
}
|
||||
if (slackIcon?.image_32) {
|
||||
return slackIcon.image_32;
|
||||
}
|
||||
if (slackIcon?.image_34) {
|
||||
return slackIcon.image_34;
|
||||
}
|
||||
if (slackIcon?.image_44) {
|
||||
return slackIcon.image_44;
|
||||
}
|
||||
if (slackIcon?.image_48) {
|
||||
return slackIcon.image_48;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { SlackReactionNotificationListItem } from "./SlackReactionNotificationListItem";
|
||||
import { SlackThreadNotificationListItem } from "./SlackThreadNotificationListItem";
|
||||
import { SlackStarNotificationListItem } from "./SlackStarNotificationListItem";
|
||||
import { NotificationListItemProps } from "../../../notification";
|
||||
|
||||
export function SlackNotificationListItem({ notification, mutate }: NotificationListItemProps) {
|
||||
switch (notification.source_item.data.type) {
|
||||
case "SlackStar":
|
||||
return (
|
||||
<SlackStarNotificationListItem
|
||||
notification={notification}
|
||||
slack_star={notification.source_item.data.content}
|
||||
mutate={mutate}
|
||||
/>
|
||||
);
|
||||
case "SlackReaction":
|
||||
return (
|
||||
<SlackReactionNotificationListItem
|
||||
notification={notification}
|
||||
slack_reaction={notification.source_item.data.content}
|
||||
mutate={mutate}
|
||||
/>
|
||||
);
|
||||
case "SlackThread":
|
||||
return (
|
||||
<SlackThreadNotificationListItem
|
||||
notification={notification}
|
||||
slack_thread={notification.source_item.data.content}
|
||||
mutate={mutate}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import { NotificationActions } from "../../../action/NotificationActions";
|
||||
import { SlackReactionPreview } from "../preview/SlackReactionPreview";
|
||||
import { SlackBotInfo, SlackReaction, SlackUser } from "../types";
|
||||
import { getAvatarIcon, MutatePromise } from "@raycast/utils";
|
||||
import { getSlackIconUrl, getSlackUserAvatarUrl } from "..";
|
||||
import { Notification } from "../../../notification";
|
||||
import { Icon, Image, List } from "@raycast/api";
|
||||
import { Page } from "../../../types";
|
||||
import { match, P } from "ts-pattern";
|
||||
|
||||
export type SlackReactionNotificationListItemProps = {
|
||||
notification: Notification;
|
||||
slack_reaction: SlackReaction;
|
||||
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||
};
|
||||
|
||||
export function SlackReactionNotificationListItem({
|
||||
notification,
|
||||
slack_reaction,
|
||||
mutate,
|
||||
}: SlackReactionNotificationListItemProps) {
|
||||
const subtitle = getSlackReactionNotificationSubtitle(slack_reaction);
|
||||
|
||||
const author = getSlackReactionAuthorAccessory(slack_reaction);
|
||||
const team = getSlackReactionTeamAccessory(slack_reaction);
|
||||
const updated_at = "2023-01-01"; // TODO
|
||||
|
||||
const accessories: List.Item.Accessory[] = [{ date: new Date(updated_at), tooltip: `Updated at ${updated_at}` }];
|
||||
|
||||
if (author) {
|
||||
accessories.unshift(author);
|
||||
}
|
||||
if (team) {
|
||||
accessories.unshift(team);
|
||||
}
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
key={notification.id}
|
||||
title={notification.title}
|
||||
icon={{ source: { light: "slack-logo-dark.svg", dark: "slack-logo-light.svg" } }}
|
||||
subtitle={subtitle}
|
||||
accessories={accessories}
|
||||
actions={
|
||||
<NotificationActions
|
||||
notification={notification}
|
||||
detailsTarget={<SlackReactionPreview notification={notification} slack_reaction={slack_reaction} />}
|
||||
mutate={mutate}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getSlackReactionNotificationSubtitle(slack_reaction: SlackReaction): string {
|
||||
return match(slack_reaction.item)
|
||||
.with(
|
||||
{
|
||||
type: P.union("Message", "File"),
|
||||
content: P.select(),
|
||||
},
|
||||
(slack_reaction_item) => {
|
||||
const channelName = slack_reaction_item.channel?.name;
|
||||
return channelName ? `#${channelName}` : "";
|
||||
},
|
||||
)
|
||||
.otherwise(() => "");
|
||||
}
|
||||
|
||||
function getSlackReactionAuthorAccessory(slack_reaction: SlackReaction): List.Item.Accessory | null {
|
||||
return match(slack_reaction.item)
|
||||
.with(
|
||||
{
|
||||
type: "Message",
|
||||
content: P.select(),
|
||||
},
|
||||
(slackMessageDetails) => {
|
||||
return match(slackMessageDetails.sender)
|
||||
.with({ type: "User", content: P.select() }, (slackUser: SlackUser) => {
|
||||
const userAvatarUrl = getSlackUserAvatarUrl(slackUser);
|
||||
const userName = slackUser.real_name || "Unknown";
|
||||
return {
|
||||
icon: userAvatarUrl ? { source: userAvatarUrl, mask: Image.Mask.Circle } : getAvatarIcon(userName),
|
||||
tooltip: userName,
|
||||
};
|
||||
})
|
||||
.with({ type: "Bot", content: P.select() }, (slackBot: SlackBotInfo) => {
|
||||
const botAvatarUrl = getSlackIconUrl(slackBot.icons);
|
||||
return {
|
||||
icon: botAvatarUrl ? { source: botAvatarUrl, mask: Image.Mask.Circle } : getAvatarIcon(slackBot.name),
|
||||
tooltip: slackBot.name,
|
||||
};
|
||||
})
|
||||
.otherwise(() => ({ icon: Icon.Person, tooltip: "Unknown" }));
|
||||
},
|
||||
)
|
||||
.otherwise(() => null);
|
||||
}
|
||||
|
||||
function getSlackReactionTeamAccessory(slack_reaction: SlackReaction): List.Item.Accessory | null {
|
||||
return match(slack_reaction.item)
|
||||
.with(
|
||||
{
|
||||
type: P.union("Message", "File"),
|
||||
content: P.select(),
|
||||
},
|
||||
(slack_reaction_item) => {
|
||||
const teamName = slack_reaction_item.team.name;
|
||||
const teamIconUrl = getSlackIconUrl(slack_reaction_item.team.icon);
|
||||
if (!teamName || !teamIconUrl) {
|
||||
return null;
|
||||
}
|
||||
return { icon: { source: teamIconUrl, mask: Image.Mask.Circle }, tooltip: teamName };
|
||||
},
|
||||
)
|
||||
.otherwise(() => null);
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import { NotificationActions } from "../../../action/NotificationActions";
|
||||
import { SlackStarPreview } from "../preview/SlackStarPreview";
|
||||
import { getAvatarIcon, MutatePromise } from "@raycast/utils";
|
||||
import { SlackBotInfo, SlackStar, SlackUser } from "../types";
|
||||
import { getSlackIconUrl, getSlackUserAvatarUrl } from "..";
|
||||
import { Notification } from "../../../notification";
|
||||
import { Icon, Image, List } from "@raycast/api";
|
||||
import { Page } from "../../../types";
|
||||
import { match, P } from "ts-pattern";
|
||||
|
||||
export type SlackStarNotificationListItemProps = {
|
||||
notification: Notification;
|
||||
slack_star: SlackStar;
|
||||
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||
};
|
||||
|
||||
export function SlackStarNotificationListItem({
|
||||
notification,
|
||||
slack_star,
|
||||
mutate,
|
||||
}: SlackStarNotificationListItemProps) {
|
||||
const subtitle = getSlackStarNotificationSubtitle(slack_star);
|
||||
|
||||
const author = getSlackStarAuthorAccessory(slack_star);
|
||||
const team = getSlackStarTeamAccessory(slack_star);
|
||||
const updated_at = "2023-01-01"; // TODO
|
||||
|
||||
const accessories: List.Item.Accessory[] = [{ date: new Date(updated_at), tooltip: `Updated at ${updated_at}` }];
|
||||
|
||||
if (author) {
|
||||
accessories.unshift(author);
|
||||
}
|
||||
if (team) {
|
||||
accessories.unshift(team);
|
||||
}
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
key={notification.id}
|
||||
title={notification.title}
|
||||
icon={{ source: { light: "slack-logo-dark.svg", dark: "slack-logo-light.svg" } }}
|
||||
subtitle={subtitle}
|
||||
accessories={accessories}
|
||||
actions={
|
||||
<NotificationActions
|
||||
notification={notification}
|
||||
detailsTarget={<SlackStarPreview notification={notification} slack_star={slack_star} />}
|
||||
mutate={mutate}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getSlackStarNotificationSubtitle(slack_star: SlackStar): string {
|
||||
return match(slack_star.item)
|
||||
.with(
|
||||
{
|
||||
type: P.union("Message", "File", "FileComment", "Channel", "Im", "Group"),
|
||||
content: P.select(),
|
||||
},
|
||||
(slack_star_item) => {
|
||||
const channelName = slack_star_item.channel?.name;
|
||||
return channelName ? `#${channelName}` : "";
|
||||
},
|
||||
)
|
||||
.otherwise(() => "");
|
||||
}
|
||||
|
||||
function getSlackStarAuthorAccessory(slack_star: SlackStar): List.Item.Accessory | null {
|
||||
return match(slack_star.item)
|
||||
.with(
|
||||
{
|
||||
type: "Message",
|
||||
content: P.select(),
|
||||
},
|
||||
(slackMessageDetails) => {
|
||||
return match(slackMessageDetails.sender)
|
||||
.with({ type: "User", content: P.select() }, (slackUser: SlackUser) => {
|
||||
const userAvatarUrl = getSlackUserAvatarUrl(slackUser);
|
||||
const userName = slackUser.real_name || "Unknown";
|
||||
return {
|
||||
icon: userAvatarUrl ? { source: userAvatarUrl, mask: Image.Mask.Circle } : getAvatarIcon(userName),
|
||||
tooltip: userName,
|
||||
};
|
||||
})
|
||||
.with({ type: "Bot", content: P.select() }, (slackBot: SlackBotInfo) => {
|
||||
const botAvatarUrl = getSlackIconUrl(slackBot.icons);
|
||||
return {
|
||||
icon: botAvatarUrl ? { source: botAvatarUrl, mask: Image.Mask.Circle } : getAvatarIcon(slackBot.name),
|
||||
tooltip: slackBot.name,
|
||||
};
|
||||
})
|
||||
.otherwise(() => ({ icon: Icon.Person, tooltip: "Unknown" }));
|
||||
},
|
||||
)
|
||||
.otherwise(() => null);
|
||||
}
|
||||
|
||||
function getSlackStarTeamAccessory(slack_star: SlackStar): List.Item.Accessory | null {
|
||||
return match(slack_star.item)
|
||||
.with(
|
||||
{
|
||||
type: P.union("Message", "File", "FileComment", "Channel", "Im", "Group"),
|
||||
content: P.select(),
|
||||
},
|
||||
(slack_star_item) => {
|
||||
const teamName = slack_star_item.team.name;
|
||||
const teamIconUrl = getSlackIconUrl(slack_star_item.team.icon);
|
||||
if (!teamName || !teamIconUrl) {
|
||||
return null;
|
||||
}
|
||||
return { icon: { source: teamIconUrl, mask: Image.Mask.Circle }, tooltip: teamName };
|
||||
},
|
||||
)
|
||||
.otherwise(() => null);
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
import { NotificationActions } from "../../../action/NotificationActions";
|
||||
import { SlackThreadPreview } from "../preview/SlackThreadPreview";
|
||||
import { getAvatarIcon, MutatePromise } from "@raycast/utils";
|
||||
import { getSlackIconUrl, getSlackUserAvatarUrl } from "..";
|
||||
import { Notification } from "../../../notification";
|
||||
import { Icon, Image, List } from "@raycast/api";
|
||||
import { SlackThread } from "../types";
|
||||
import { Page } from "../../../types";
|
||||
|
||||
export type SlackThreadNotificationListItemProps = {
|
||||
notification: Notification;
|
||||
slack_thread: SlackThread;
|
||||
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||
};
|
||||
|
||||
export function SlackThreadNotificationListItem({
|
||||
notification,
|
||||
slack_thread,
|
||||
mutate,
|
||||
}: SlackThreadNotificationListItemProps) {
|
||||
const subtitle = getSlackThreadNotificationSubtitle(slack_thread);
|
||||
|
||||
const author = getSlackThreadAuthorAccessory(slack_thread);
|
||||
const team = getSlackThreadTeamAccessory(slack_thread);
|
||||
const updated_at = "2023-01-01"; // TODO
|
||||
|
||||
const accessories: List.Item.Accessory[] = [{ date: new Date(updated_at), tooltip: `Updated at ${updated_at}` }];
|
||||
|
||||
if (author) {
|
||||
accessories.unshift(author);
|
||||
}
|
||||
if (team) {
|
||||
accessories.unshift(team);
|
||||
}
|
||||
|
||||
return (
|
||||
<List.Item
|
||||
key={notification.id}
|
||||
title={notification.title}
|
||||
icon={{ source: { light: "slack-logo-dark.svg", dark: "slack-logo-light.svg" } }}
|
||||
subtitle={subtitle}
|
||||
accessories={accessories}
|
||||
actions={
|
||||
<NotificationActions
|
||||
notification={notification}
|
||||
detailsTarget={<SlackThreadPreview notification={notification} slack_thread={slack_thread} />}
|
||||
mutate={mutate}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function getSlackThreadNotificationSubtitle(slack_thread: SlackThread): string {
|
||||
const channelName = slack_thread.channel?.name;
|
||||
return channelName ? `in #${channelName}` : "";
|
||||
}
|
||||
|
||||
function getSlackThreadAuthorAccessory(slack_thread: SlackThread): List.Item.Accessory | null {
|
||||
const firstUnreadMessage = slack_thread.messages[0];
|
||||
|
||||
if (firstUnreadMessage.user) {
|
||||
const profile = slack_thread.sender_profiles[firstUnreadMessage.user];
|
||||
|
||||
if (profile && profile.type === "User") {
|
||||
const slackUser = profile.content;
|
||||
const userAvatarUrl = getSlackUserAvatarUrl(slackUser);
|
||||
const userName = slackUser.real_name || "Unknown";
|
||||
return {
|
||||
icon: userAvatarUrl ? { source: userAvatarUrl, mask: Image.Mask.Circle } : getAvatarIcon(userName),
|
||||
tooltip: userName,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (firstUnreadMessage.bot_id) {
|
||||
const profile = slack_thread.sender_profiles[firstUnreadMessage.bot_id];
|
||||
|
||||
if (profile && profile.type === "Bot") {
|
||||
const slackBot = profile.content;
|
||||
const botAvatarUrl = getSlackIconUrl(slackBot.icons);
|
||||
return {
|
||||
icon: botAvatarUrl ? { source: botAvatarUrl, mask: Image.Mask.Circle } : getAvatarIcon(slackBot.name),
|
||||
tooltip: slackBot.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { icon: Icon.Person, tooltip: "Unknown" };
|
||||
}
|
||||
|
||||
function getSlackThreadTeamAccessory(slack_thread: SlackThread): List.Item.Accessory | null {
|
||||
const teamName = slack_thread.team.name;
|
||||
const teamIconUrl = getSlackIconUrl(slack_thread.team.icon);
|
||||
if (!teamName || !teamIconUrl) {
|
||||
return null;
|
||||
}
|
||||
return { icon: { source: teamIconUrl, mask: Image.Mask.Circle }, tooltip: teamName };
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { getNotificationHtmlUrl, Notification } from "../../../notification";
|
||||
import { Detail, ActionPanel, Action } from "@raycast/api";
|
||||
import { SlackReaction } from "../types";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface SlackReactionPreviewProps {
|
||||
notification: Notification;
|
||||
slack_reaction: SlackReaction;
|
||||
}
|
||||
|
||||
export function SlackReactionPreview({ notification }: SlackReactionPreviewProps) {
|
||||
const notificationHtmlUrl = useMemo(() => {
|
||||
return getNotificationHtmlUrl(notification);
|
||||
}, [notification]);
|
||||
|
||||
return (
|
||||
<Detail
|
||||
markdown={`# ${notification.title}`}
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<Action.OpenInBrowser url={notificationHtmlUrl} />
|
||||
</ActionPanel>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { getNotificationHtmlUrl, Notification } from "../../../notification";
|
||||
import { Detail, ActionPanel, Action } from "@raycast/api";
|
||||
import { SlackStar } from "../types";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface SlackStarPreviewProps {
|
||||
notification: Notification;
|
||||
slack_star: SlackStar;
|
||||
}
|
||||
|
||||
export function SlackStarPreview({ notification }: SlackStarPreviewProps) {
|
||||
const notificationHtmlUrl = useMemo(() => {
|
||||
return getNotificationHtmlUrl(notification);
|
||||
}, [notification]);
|
||||
|
||||
return (
|
||||
<Detail
|
||||
markdown={`# ${notification.title}`}
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<Action.OpenInBrowser url={notificationHtmlUrl} />
|
||||
</ActionPanel>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { getNotificationHtmlUrl, Notification } from "../../../notification";
|
||||
import { Detail, ActionPanel, Action } from "@raycast/api";
|
||||
import { SlackThread } from "../types";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface SlackThreadPreviewProps {
|
||||
notification: Notification;
|
||||
slack_thread: SlackThread;
|
||||
}
|
||||
|
||||
export function SlackThreadPreview({ notification }: SlackThreadPreviewProps) {
|
||||
const notificationHtmlUrl = useMemo(() => {
|
||||
return getNotificationHtmlUrl(notification);
|
||||
}, [notification]);
|
||||
|
||||
return (
|
||||
<Detail
|
||||
markdown={`# ${notification.title}`}
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<Action.OpenInBrowser url={notificationHtmlUrl} />
|
||||
</ActionPanel>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,493 +0,0 @@
|
||||
import { match, P } from "ts-pattern";
|
||||
|
||||
export interface SlackStar {
|
||||
state: SlackStarState;
|
||||
created_at: Date;
|
||||
item: SlackStarItem;
|
||||
}
|
||||
|
||||
export function getSlackStarHtmlUrl(slack_star: SlackStar): string {
|
||||
return match(slack_star.item)
|
||||
.with({ type: "Message", content: P.select() }, (message) => message.url)
|
||||
.with(
|
||||
{ type: "File", content: P.select() },
|
||||
(file) => `https://app.slack.com/client/${file.team.id}/${file.channel.id}`,
|
||||
)
|
||||
.with(
|
||||
{ type: "FileComment", content: P.select() },
|
||||
(fileComment) => `https://app.slack.com/client/${fileComment.team.id}/${fileComment.channel.id}`,
|
||||
)
|
||||
.with(
|
||||
{ type: "Channel", content: P.select() },
|
||||
(channel) => `https://app.slack.com/client/${channel.team.id}/${channel.channel.id}`,
|
||||
)
|
||||
.with({ type: "Im", content: P.select() }, (im) => `https://app.slack.com/client/${im.team.id}/${im.channel.id}`)
|
||||
.with(
|
||||
{ type: "Group", content: P.select() },
|
||||
(group) => `https://app.slack.com/client/${group.team.id}/${group.channel.id}`,
|
||||
)
|
||||
.otherwise(() => "");
|
||||
}
|
||||
|
||||
export enum SlackStarState {
|
||||
StarAdded = "StarAdded",
|
||||
StarRemoved = "StarRemoved",
|
||||
}
|
||||
|
||||
export type SlackStarItem =
|
||||
| { type: "Message"; content: SlackMessageDetails }
|
||||
| { type: "File"; content: SlackFileDetails }
|
||||
| { type: "FileComment"; content: SlackFileCommentDetails }
|
||||
| { type: "Channel"; content: SlackChannelDetails }
|
||||
| { type: "Im"; content: SlackImDetails }
|
||||
| { type: "Group"; content: SlackGroupDetails };
|
||||
|
||||
export interface SlackReaction {
|
||||
name: string;
|
||||
state: SlackReactionState;
|
||||
created_at: Date;
|
||||
item: SlackReactionItem;
|
||||
}
|
||||
|
||||
export function getSlackReactionHtmlUrl(slack_reaction: SlackReaction): string {
|
||||
return match(slack_reaction.item)
|
||||
.with({ type: "Message", content: P.select() }, (message) => message.url)
|
||||
.with(
|
||||
{ type: "File", content: P.select() },
|
||||
(file) => `https://app.slack.com/client/${file.team.id}/${file.channel.id}`,
|
||||
)
|
||||
.otherwise(() => "");
|
||||
}
|
||||
|
||||
export enum SlackReactionState {
|
||||
ReactionAdded = "ReactionAdded",
|
||||
ReactionRemoved = "ReactionRemoved",
|
||||
}
|
||||
|
||||
export type SlackReactionItem =
|
||||
| { type: "Message"; content: SlackMessageDetails }
|
||||
| { type: "File"; content: SlackFileDetails };
|
||||
|
||||
export interface SlackThread {
|
||||
url: string;
|
||||
messages: Array<SlackHistoryMessage>;
|
||||
sender_profiles: Record<string, SlackMessageSenderDetails>;
|
||||
subscribed: boolean;
|
||||
last_read?: string;
|
||||
channel: SlackChannelInfo;
|
||||
team: SlackTeamInfo;
|
||||
references?: SlackReferences;
|
||||
}
|
||||
|
||||
export function getSlackThreadHtmlUrl(slack_thread: SlackThread): string {
|
||||
return slack_thread.url;
|
||||
}
|
||||
|
||||
export interface SlackReferences {
|
||||
channels: Record<string, string | null>;
|
||||
users: Record<string, string | null>;
|
||||
usergroups: Record<string, string | null>;
|
||||
}
|
||||
|
||||
export interface SlackPushEventCallback {
|
||||
team_id: string;
|
||||
api_app_id: string;
|
||||
event: SlackEventCallbackBody;
|
||||
event_id: string;
|
||||
event_time: Date;
|
||||
event_context?: string;
|
||||
authed_users?: Array<string>;
|
||||
authorizations?: Array<SlackEventAuthorization>;
|
||||
}
|
||||
|
||||
export interface SlackEventAuthorization {
|
||||
team_id: string;
|
||||
user_id: string;
|
||||
is_bot: boolean;
|
||||
}
|
||||
|
||||
export type SlackEventCallbackBody =
|
||||
| { type: "SarAdded"; content: SlackStarAddedEvent }
|
||||
| { type: "StarRemoved"; content: SlackStarRemovedEvent };
|
||||
|
||||
export interface SlackStarAddedEvent {
|
||||
user: string;
|
||||
item: SlackStarsItem;
|
||||
event_ts: Date;
|
||||
}
|
||||
|
||||
export interface SlackStarRemovedEvent {
|
||||
user: string;
|
||||
item: SlackStarsItem;
|
||||
event_ts: Date;
|
||||
}
|
||||
|
||||
export type SlackStarsItem =
|
||||
| { type: "Message"; content: SlackStarsItemMessage }
|
||||
| { type: "File"; content: SlackStarsItemFile }
|
||||
| { type: "FileComment"; content: SlackStarsItemFileComment }
|
||||
| { type: "Channel"; content: SlackStarsItemChannel }
|
||||
| { type: "Im"; content: SlackStarsItemIm }
|
||||
| { type: "Group"; content: SlackStarsItemGroup };
|
||||
|
||||
export interface SlackStarsItemMessage {
|
||||
message: SlackHistoryMessage;
|
||||
channel: string;
|
||||
date_create: Date;
|
||||
}
|
||||
|
||||
export interface SlackHistoryMessage {
|
||||
ts: string;
|
||||
channel?: string;
|
||||
channel_type?: string;
|
||||
thread_ts?: string;
|
||||
client_msg_id?: string;
|
||||
user?: string;
|
||||
bot_id?: string;
|
||||
|
||||
text?: string;
|
||||
blocks?: Array<SlackBlock>;
|
||||
attachments?: Array<SlackMessageAttachment>;
|
||||
upload?: boolean;
|
||||
files?: Array<SlackFile>;
|
||||
reactions?: Array<SlackReactionDetails>;
|
||||
}
|
||||
|
||||
export interface SlackReactionDetails {
|
||||
name: string;
|
||||
count: number;
|
||||
users: Array<string>;
|
||||
}
|
||||
|
||||
export interface SlackMessageAttachment {
|
||||
id?: number;
|
||||
color?: string;
|
||||
fallback?: string;
|
||||
title?: string;
|
||||
fields?: Array<SlackMessageAttachmentFieldObject>;
|
||||
mrkdwn_in?: Array<string>;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export interface SlackMessageAttachmentFieldObject {
|
||||
title?: string;
|
||||
value?: string;
|
||||
short?: boolean;
|
||||
}
|
||||
|
||||
export type SlackBlock =
|
||||
| SlackSectionBlock
|
||||
| SlackHeaderBlock
|
||||
| SlackDividerBlock
|
||||
| SlackImageBlock
|
||||
| SlackActionsBlock
|
||||
| SlackContextBlock
|
||||
| SlackInputBlock
|
||||
| SlackFileBlock
|
||||
| { type: "rich_text" }
|
||||
| { type: "event" };
|
||||
|
||||
export interface SlackSectionBlock {
|
||||
type: "section";
|
||||
block_id?: string;
|
||||
text?: SlackBlockText;
|
||||
fields?: Array<SlackBlockText>;
|
||||
// To be specified
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
accessory?: any;
|
||||
}
|
||||
|
||||
export type SlackBlockText =
|
||||
| { type: "plain_text"; value: string }
|
||||
| { type: "mrkdwn"; text: string; verbatim?: boolean };
|
||||
|
||||
export interface SlackHeaderBlock {
|
||||
type: "header";
|
||||
block_id?: string;
|
||||
text: SlackBlockText;
|
||||
}
|
||||
|
||||
export interface SlackDividerBlock {
|
||||
type: "divider";
|
||||
block_id?: string;
|
||||
}
|
||||
|
||||
export interface SlackImageBlock {
|
||||
type: "image";
|
||||
block_id?: string;
|
||||
image_url: string;
|
||||
alt_text: string;
|
||||
title?: SlackBlockText;
|
||||
}
|
||||
|
||||
export interface SlackActionsBlock {
|
||||
type: "actions";
|
||||
block_id?: string;
|
||||
// To be specified
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
elements: Array<any>;
|
||||
}
|
||||
|
||||
export interface SlackContextBlock {
|
||||
type: "context";
|
||||
block_id?: string;
|
||||
// To be specified
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
elements: Array<any>;
|
||||
}
|
||||
|
||||
export interface SlackInputBlock {
|
||||
type: "input";
|
||||
block_id?: string;
|
||||
label: SlackBlockText;
|
||||
// To be specified
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
element: any;
|
||||
hint?: SlackBlockText;
|
||||
optional?: boolean;
|
||||
dispatch_action?: boolean;
|
||||
}
|
||||
|
||||
export interface SlackFileBlock {
|
||||
type: "file";
|
||||
block_id?: string;
|
||||
external_id: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface SlackStarsItemFile {
|
||||
file: SlackFile;
|
||||
channel: string;
|
||||
date_create: Date;
|
||||
}
|
||||
|
||||
export interface SlackFile {
|
||||
id: string;
|
||||
created?: Date;
|
||||
timestamp?: Date;
|
||||
name?: string;
|
||||
title?: string;
|
||||
mimetype?: string;
|
||||
filetype?: string;
|
||||
pretty_type?: string;
|
||||
external_type?: string;
|
||||
user?: string;
|
||||
username?: string;
|
||||
url_private?: string;
|
||||
url_private_download?: string;
|
||||
permalink?: string;
|
||||
permalink_public?: string;
|
||||
reactions?: Array<SlackReactionDetails>;
|
||||
editable?: boolean;
|
||||
is_external?: boolean;
|
||||
is_public?: boolean;
|
||||
public_url_shared?: boolean;
|
||||
display_as_bot?: boolean;
|
||||
is_starred?: boolean;
|
||||
has_rich_preview?: boolean;
|
||||
}
|
||||
|
||||
export interface SlackStarsItemFileComment {
|
||||
file: SlackFile;
|
||||
file_comment: string;
|
||||
channel: string;
|
||||
date_create: Date;
|
||||
}
|
||||
|
||||
export interface SlackStarsItemChannel {
|
||||
channel: string;
|
||||
date_create: Date;
|
||||
}
|
||||
|
||||
export interface SlackStarsItemIm {
|
||||
channel: string;
|
||||
date_create: Date;
|
||||
}
|
||||
|
||||
export interface SlackStarsItemGroup {
|
||||
group: string;
|
||||
date_create: Date;
|
||||
}
|
||||
|
||||
export interface SlackMessageDetails {
|
||||
url: string;
|
||||
message: SlackHistoryMessage;
|
||||
channel: SlackChannelInfo;
|
||||
sender: SlackMessageSenderDetails;
|
||||
team: SlackTeamInfo;
|
||||
}
|
||||
|
||||
export type SlackMessageSenderDetails = { type: "User"; content: SlackUser } | { type: "Bot"; content: SlackBotInfo };
|
||||
|
||||
export interface SlackUser {
|
||||
id: string;
|
||||
team_id?: string;
|
||||
name?: string;
|
||||
locale?: string;
|
||||
profile?: SlackUserProfile;
|
||||
is_admin?: boolean;
|
||||
is_app_user?: boolean;
|
||||
is_bot?: boolean;
|
||||
is_invited_user?: boolean;
|
||||
is_owner?: boolean;
|
||||
is_primary_owner?: boolean;
|
||||
is_restricted?: boolean;
|
||||
is_stranger?: boolean;
|
||||
is_ultra_restricted?: boolean;
|
||||
has_2fa?: boolean;
|
||||
tz?: string;
|
||||
tz_label?: string;
|
||||
tz_offset?: number;
|
||||
updated?: Date;
|
||||
deleted?: boolean;
|
||||
color?: string;
|
||||
real_name?: string;
|
||||
enterprise_user?: SlackEnterpriseUser;
|
||||
}
|
||||
|
||||
export interface SlackUserProfile {
|
||||
id?: string;
|
||||
display_name?: string;
|
||||
real_name?: string;
|
||||
real_name_normalized?: string;
|
||||
avatar_hash?: string;
|
||||
status_text?: string;
|
||||
status_expiration?: Date;
|
||||
status_emoji?: string;
|
||||
display_name_normalized?: string;
|
||||
email?: string;
|
||||
team?: string;
|
||||
image_original?: string;
|
||||
image_default?: boolean;
|
||||
image_24?: string;
|
||||
image_32?: string;
|
||||
image_34?: string;
|
||||
image_44?: string;
|
||||
image_48?: string;
|
||||
image_68?: string;
|
||||
image_72?: string;
|
||||
image_88?: string;
|
||||
image_102?: string;
|
||||
image_132?: string;
|
||||
image_192?: string;
|
||||
image_230?: string;
|
||||
image_512?: string;
|
||||
}
|
||||
|
||||
export interface SlackEnterpriseUser {
|
||||
id: string;
|
||||
enterprise_id: string;
|
||||
enterprise_name?: string;
|
||||
teams?: Array<string>;
|
||||
is_admin?: boolean;
|
||||
is_app_user?: boolean;
|
||||
is_bot?: boolean;
|
||||
is_invited_user?: boolean;
|
||||
is_owner?: boolean;
|
||||
is_primary_owner?: boolean;
|
||||
is_restricted?: boolean;
|
||||
is_stranger?: boolean;
|
||||
is_ultra_restricted?: boolean;
|
||||
has_2fa?: boolean;
|
||||
}
|
||||
|
||||
export interface SlackBotInfo {
|
||||
id?: string;
|
||||
name: string;
|
||||
updated?: Date;
|
||||
app_id: string;
|
||||
user_id: string;
|
||||
icons?: SlackIcon;
|
||||
}
|
||||
|
||||
export interface SlackTeamInfo {
|
||||
id: string;
|
||||
name?: string;
|
||||
domain?: string;
|
||||
email_domain?: string;
|
||||
icon?: SlackIcon;
|
||||
}
|
||||
|
||||
export interface SlackIcon {
|
||||
image_original?: string;
|
||||
image_default?: boolean;
|
||||
image_24?: string;
|
||||
image_32?: string;
|
||||
image_34?: string;
|
||||
image_44?: string;
|
||||
image_48?: string;
|
||||
image_68?: string;
|
||||
image_72?: string;
|
||||
image_88?: string;
|
||||
image_102?: string;
|
||||
image_132?: string;
|
||||
image_192?: string;
|
||||
image_230?: string;
|
||||
image_512?: string;
|
||||
}
|
||||
|
||||
export interface SlackChannelInfo {
|
||||
id: string;
|
||||
created: Date;
|
||||
creator?: string;
|
||||
name?: string;
|
||||
name_normalized?: string;
|
||||
topic?: SlackChannelTopicInfo;
|
||||
purpose?: SlackChannelPurposeInfo;
|
||||
previous_names?: Array<string>;
|
||||
priority?: number;
|
||||
num_members?: number;
|
||||
locale?: string;
|
||||
is_channel?: boolean;
|
||||
is_group?: boolean;
|
||||
is_im?: boolean;
|
||||
is_archived?: boolean;
|
||||
is_general?: boolean;
|
||||
is_shared?: boolean;
|
||||
is_org_shared?: boolean;
|
||||
is_member?: boolean;
|
||||
is_private?: boolean;
|
||||
is_mpim?: boolean;
|
||||
is_user_deleted?: boolean;
|
||||
last_read?: string;
|
||||
unread_count?: number;
|
||||
unread_count_display?: number;
|
||||
}
|
||||
|
||||
export interface SlackChannelTopicInfo {
|
||||
value: string;
|
||||
creator?: string;
|
||||
last_set?: Date;
|
||||
}
|
||||
|
||||
export interface SlackChannelPurposeInfo {
|
||||
value: string;
|
||||
creator?: string;
|
||||
last_set?: Date;
|
||||
}
|
||||
|
||||
export interface SlackChannelDetails {
|
||||
channel: SlackChannelInfo;
|
||||
team: SlackTeamInfo;
|
||||
}
|
||||
|
||||
export interface SlackFileDetails {
|
||||
channel: SlackChannelInfo;
|
||||
sender?: SlackUser;
|
||||
team: SlackTeamInfo;
|
||||
}
|
||||
|
||||
export interface SlackFileCommentDetails {
|
||||
channel: SlackChannelInfo;
|
||||
team: SlackTeamInfo;
|
||||
}
|
||||
|
||||
export interface SlackImDetails {
|
||||
channel: SlackChannelInfo;
|
||||
team: SlackTeamInfo;
|
||||
}
|
||||
|
||||
export interface SlackGroupDetails {
|
||||
channel: SlackChannelInfo;
|
||||
team: SlackTeamInfo;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getNotificationHtmlUrl, Notification } from "../../../notification";
|
||||
import { Notification, getNotificationHtmlUrl } from "../../../notification";
|
||||
import { Detail, ActionPanel, Action } from "@raycast/api";
|
||||
import { useMemo } from "react";
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export interface TodoistItem {
|
||||
id: string;
|
||||
}
|
||||
@@ -1,29 +1,37 @@
|
||||
import { getThirdPartyItemHtmlUrl, ThirdPartyItem } from "./third_party_item";
|
||||
import { GithubDiscussion, GithubPullRequest } from "./integrations/github/types";
|
||||
import { GoogleMailThread } from "./integrations/google-mail/types";
|
||||
import { LinearNotification } from "./integrations/linear/types";
|
||||
import { MutatePromise } from "@raycast/utils";
|
||||
import { match, P } from "ts-pattern";
|
||||
import { Page } from "./types";
|
||||
import { Task } from "./task";
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
title: string;
|
||||
source_id: string;
|
||||
status: NotificationStatus;
|
||||
created_at: Date;
|
||||
metadata: NotificationMetadata;
|
||||
updated_at: Date;
|
||||
last_read_at?: Date;
|
||||
snoozed_until?: Date;
|
||||
user_id: string;
|
||||
task?: Task;
|
||||
kind: NotificationSourceKind;
|
||||
source_item: ThirdPartyItem;
|
||||
details?: NotificationDetails;
|
||||
}
|
||||
|
||||
export enum NotificationSourceKind {
|
||||
Github = "Github",
|
||||
Todoist = "Todoist",
|
||||
Linear = "Linear",
|
||||
GoogleMail = "GoogleMail",
|
||||
Slack = "Slack",
|
||||
}
|
||||
export type NotificationMetadata =
|
||||
| {
|
||||
type: "Github" | "Todoist";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
content: any;
|
||||
}
|
||||
| { type: "Linear"; content: LinearNotification }
|
||||
| { type: "GoogleMail"; content: GoogleMailThread };
|
||||
|
||||
export type NotificationDetails =
|
||||
| { type: "GithubPullRequest"; content: GithubPullRequest }
|
||||
| { type: "GithubDiscussion"; content: GithubDiscussion };
|
||||
|
||||
export enum NotificationStatus {
|
||||
Unread = "Unread",
|
||||
@@ -37,10 +45,30 @@ export type NotificationListItemProps = {
|
||||
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||
};
|
||||
|
||||
export function getNotificationHtmlUrl(notification: Notification): string {
|
||||
return getThirdPartyItemHtmlUrl(notification.source_item);
|
||||
export function getNotificationHtmlUrl(notification: Notification) {
|
||||
return match(notification)
|
||||
.with(
|
||||
{ details: { type: P.union("GithubPullRequest", "GithubDiscussion"), content: P.select() } },
|
||||
(notificationDetails) => notificationDetails.url,
|
||||
)
|
||||
.with(
|
||||
{ metadata: { type: "Linear", content: { type: "IssueNotification", content: P.select() } } },
|
||||
(linearIssueNotification) => linearIssueNotification.issue.url,
|
||||
)
|
||||
.with(
|
||||
{ metadata: { type: "Linear", content: { type: "ProjectNotification", content: P.select() } } },
|
||||
(linearProjectNotification) => linearProjectNotification.project.url,
|
||||
)
|
||||
.with(
|
||||
{ metadata: { type: "GoogleMail", content: P.select() } },
|
||||
(googleMailThread) =>
|
||||
`https://mail.google.com/mail/u/${googleMailThread.user_email_address}/#inbox/${googleMailThread.id}`,
|
||||
)
|
||||
.with({ metadata: { type: "Todoist" } }, () => `https://todoist.com/showTask?id=${notification.source_id}`)
|
||||
.with({ metadata: { type: "Github" } }, () => "https://github.com")
|
||||
.exhaustive();
|
||||
}
|
||||
|
||||
export function isNotificationBuiltFromTask(notification: Notification) {
|
||||
return notification.kind === NotificationSourceKind.Todoist;
|
||||
return notification.metadata.type === "Todoist";
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import {
|
||||
getSlackReactionHtmlUrl,
|
||||
getSlackStarHtmlUrl,
|
||||
getSlackThreadHtmlUrl,
|
||||
SlackReaction,
|
||||
SlackStar,
|
||||
SlackThread,
|
||||
} from "./integrations/slack/types";
|
||||
import {
|
||||
getLinearIssueHtmlUrl,
|
||||
getLinearNotificationHtmlUrl,
|
||||
LinearIssue,
|
||||
LinearNotification,
|
||||
} from "./integrations/linear/types";
|
||||
import { getGithubNotificationHtmlUrl, GithubNotification } from "./integrations/github/types";
|
||||
import { GoogleMailThread } from "./integrations/google-mail/types";
|
||||
import { TodoistItem } from "./integrations/todoist/types";
|
||||
|
||||
export interface ThirdPartyItem {
|
||||
id: string;
|
||||
source_id: string;
|
||||
data: ThirdPartyItemData;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
user_id: string;
|
||||
integration_connection_id: string;
|
||||
}
|
||||
|
||||
export type ThirdPartyItemData =
|
||||
| { type: "TodoistItem"; content: TodoistItem }
|
||||
| { type: "SlackStar"; content: SlackStar }
|
||||
| { type: "SlackReaction"; content: SlackReaction }
|
||||
| { type: "SlackThread"; content: SlackThread }
|
||||
| { type: "LinearIssue"; content: LinearIssue }
|
||||
| { type: "LinearNotification"; content: LinearNotification }
|
||||
| { type: "GithubNotification"; content: GithubNotification }
|
||||
| { type: "GoogleMailThread"; content: GoogleMailThread };
|
||||
|
||||
export function getThirdPartyItemHtmlUrl(thirdPartyItem: ThirdPartyItem): string {
|
||||
switch (thirdPartyItem.data.type) {
|
||||
case "TodoistItem":
|
||||
return `https://todoist.com/showTask?id=${thirdPartyItem.data.content.id}`;
|
||||
case "SlackStar":
|
||||
return getSlackStarHtmlUrl(thirdPartyItem.data.content);
|
||||
case "SlackReaction":
|
||||
return getSlackReactionHtmlUrl(thirdPartyItem.data.content);
|
||||
case "SlackThread":
|
||||
return getSlackThreadHtmlUrl(thirdPartyItem.data.content);
|
||||
case "LinearIssue":
|
||||
return getLinearIssueHtmlUrl(thirdPartyItem.data.content);
|
||||
case "LinearNotification":
|
||||
return getLinearNotificationHtmlUrl(thirdPartyItem.data.content);
|
||||
case "GithubNotification":
|
||||
return getGithubNotificationHtmlUrl(thirdPartyItem.data.content);
|
||||
case "GoogleMailThread":
|
||||
return `https://mail.google.com/mail/u/0/#inbox/${thirdPartyItem.data.content.id}`;
|
||||
}
|
||||
}
|
||||