Compare commits

...

4 Commits

50 changed files with 2584 additions and 294 deletions

View File

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

View File

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

View File

@@ -0,0 +1 @@
<svg width="14" height="14" viewBox="0 0 14 14" aria-label="Backlog" fill="#D2D3E0"><path d="M13.9408 7.91426L11.9576 7.65557C11.9855 7.4419 12 7.22314 12 7C12 6.77686 11.9855 6.5581 11.9576 6.34443L13.9408 6.08573C13.9799 6.38496 14 6.69013 14 7C14 7.30987 13.9799 7.61504 13.9408 7.91426ZM13.4688 4.32049C13.2328 3.7514 12.9239 3.22019 12.5538 2.73851L10.968 3.95716C11.2328 4.30185 11.4533 4.68119 11.6214 5.08659L13.4688 4.32049ZM11.2615 1.4462L10.0428 3.03204C9.69815 2.76716 9.31881 2.54673 8.91341 2.37862L9.67951 0.531163C10.2486 0.767153 10.7798 1.07605 11.2615 1.4462ZM7.91426 0.0591659L7.65557 2.04237C7.4419 2.01449 7.22314 2 7 2C6.77686 2 6.5581 2.01449 6.34443 2.04237L6.08574 0.059166C6.38496 0.0201343 6.69013 0 7 0C7.30987 0 7.61504 0.0201343 7.91426 0.0591659ZM4.32049 0.531164L5.08659 2.37862C4.68119 2.54673 4.30185 2.76716 3.95716 3.03204L2.73851 1.4462C3.22019 1.07605 3.7514 0.767153 4.32049 0.531164ZM1.4462 2.73851L3.03204 3.95716C2.76716 4.30185 2.54673 4.68119 2.37862 5.08659L0.531164 4.32049C0.767153 3.7514 1.07605 3.22019 1.4462 2.73851ZM0.0591659 6.08574C0.0201343 6.38496 0 6.69013 0 7C0 7.30987 0.0201343 7.61504 0.059166 7.91426L2.04237 7.65557C2.01449 7.4419 2 7.22314 2 7C2 6.77686 2.01449 6.5581 2.04237 6.34443L0.0591659 6.08574ZM0.531164 9.67951L2.37862 8.91341C2.54673 9.31881 2.76716 9.69815 3.03204 10.0428L1.4462 11.2615C1.07605 10.7798 0.767153 10.2486 0.531164 9.67951ZM2.73851 12.5538L3.95716 10.968C4.30185 11.2328 4.68119 11.4533 5.08659 11.6214L4.32049 13.4688C3.7514 13.2328 3.22019 12.9239 2.73851 12.5538ZM6.08574 13.9408L6.34443 11.9576C6.5581 11.9855 6.77686 12 7 12C7.22314 12 7.4419 11.9855 7.65557 11.9576L7.91427 13.9408C7.61504 13.9799 7.30987 14 7 14C6.69013 14 6.38496 13.9799 6.08574 13.9408ZM9.67951 13.4688L8.91341 11.6214C9.31881 11.4533 9.69815 11.2328 10.0428 10.968L11.2615 12.5538C10.7798 12.9239 10.2486 13.2328 9.67951 13.4688ZM12.5538 11.2615L10.968 10.0428C11.2328 9.69815 11.4533 9.31881 11.6214 8.91341L13.4688 9.67951C13.2328 10.2486 12.924 10.7798 12.5538 11.2615Z" stroke="none"></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1 @@
<svg width="14" height="14" fill="none"><circle cx="7" cy="7" r="6" fill="none" stroke="#95a2b3" stroke-width="2" stroke-dasharray="3.14 0" stroke-dashoffset="-0.7"></circle><circle class="progress" cx="7" cy="7" r="3" fill="none" stroke="#95a2b3" rotate="20" stroke-width="6" stroke-dasharray="18.84955592153876 100" transform="rotate(-90 7 7)"></circle><path class="icon" stroke="none" d="M4.21967 4.21967C4.51256 3.92678 4.98743 3.92678 5.28032 4.21967L6.26516 5.2045L7.25 6.18934L9.21966 4.21967C9.51255 3.92678 9.98743 3.92678 10.2803 4.21967C10.5732 4.51256 10.5732 4.98744 10.2803 5.28033L8.31065 7.25L10.2803 9.21967C10.5732 9.51257 10.5732 9.98744 10.2803 10.2803C9.98743 10.5732 9.51255 10.5732 9.21966 10.2803L7.25 8.31066L5.28032 10.2803C4.98743 10.5732 4.51256 10.5732 4.21967 10.2803C3.92678 9.98744 3.92678 9.51257 4.21967 9.21967L5.2045 8.23484L6.18934 7.25L4.21967 5.28033C3.92678 4.98744 3.92678 4.51256 4.21967 4.21967Z"></path></svg>

After

Width:  |  Height:  |  Size: 954 B

View File

@@ -0,0 +1 @@
<svg width="14" height="14" viewBox="0 0 14 14" aria-label="Done" fill="#5e6ad2"><path fill-rule="evenodd" clip-rule="evenodd" d="M7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0ZM11.101 5.10104C11.433 4.76909 11.433 4.23091 11.101 3.89896C10.7691 3.56701 10.2309 3.56701 9.89896 3.89896L5.5 8.29792L4.10104 6.89896C3.7691 6.56701 3.2309 6.56701 2.89896 6.89896C2.56701 7.2309 2.56701 7.7691 2.89896 8.10104L4.89896 10.101C5.2309 10.433 5.7691 10.433 6.10104 10.101L11.101 5.10104Z"></path></svg>

After

Width:  |  Height:  |  Size: 551 B

View File

@@ -0,0 +1 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1" y="1" width="12" height="12" rx="6" stroke="#F2C94C" stroke-width="2" fill="none"></rect><path fill="#F2C94C" stroke="none" d="M 3.5,3.5 L3.5,0 A3.5,3.5 0 1,1 0, 3.5 z" transform="translate(3.5,3.5)"></path></svg>

After

Width:  |  Height:  |  Size: 286 B

View File

@@ -0,0 +1 @@
<svg width="14" height="14" fill="none"><circle cx="7" cy="7" r="3.5" fill="none" stroke="#fc7839" stroke-width="7" stroke-dasharray="2 0" stroke-dashoffset="3.2"></circle><circle class="progress" cx="6" cy="7" r="2" fill="none" stroke="#fc7840" rotate="20" stroke-width="4" stroke-dasharray="0 100" transform="rotate(-90 7 7)"></circle><path class="icon" stroke="none" d="M8.0126 7.98223V9.50781C8.0126 9.92901 8.52329 10.1548 8.85102 9.87854L11.8258 7.37066C12.0581 7.17486 12.0581 6.82507 11.8258 6.62927L8.85102 4.12139C8.52329 3.84509 8.0126 4.07092 8.0126 4.49212V6.01763H5.98739V4.49218C5.98739 4.07098 5.4767 3.84515 5.14897 4.12146L2.17419 6.62933C1.94194 6.82513 1.94194 7.17492 2.17419 7.37072L5.14897 9.8786C5.4767 10.1549 5.98739 9.92907 5.98739 9.50787V7.98223H8.0126Z"></path></svg>

After

Width:  |  Height:  |  Size: 798 B

View File

@@ -0,0 +1 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><rect x="1" y="1" width="12" height="12" rx="6" stroke="#858699" stroke-width="2" fill="none"></rect><path fill="#858699" stroke="none" d="M 3.5,3.5 L3.5,0 A3.5,3.5 0 0,1 3.5, 0 z" transform="translate(3.5,3.5)"></path></svg>

After

Width:  |  Height:  |  Size: 286 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="1 1 14 14" fill="#fb773f"><path d="M2 4.74695C2 4.68722 2.01039 4.62899 2.02989 4.57451L2.11601 4.42269C2.15266 4.37819 2.19711 4.33975 2.24806 4.30966L3.16473 3.76824L3.92054 5.08013L3.5 5.32852V5.8313H2V4.74695Z"></path><path d="M4.8372 4.53871L4.0814 3.22682L5.91473 2.14398L6.67054 3.45588L4.8372 4.53871Z"></path><path d="M7.5872 2.91446L6.8314 1.60257L7.74806 1.06115C7.7997 1.03065 7.85539 1.01027 7.91244 1H8.08756C8.14461 1.01027 8.2003 1.03065 8.25194 1.06115L9.1686 1.60257L8.4128 2.91446L8 2.67065L7.5872 2.91446Z"></path><path d="M9.32946 3.45588L10.0853 2.14398L11.9186 3.22682L11.1628 4.53871L9.32946 3.45588Z"></path><path d="M12.0795 5.08013L12.8353 3.76824L13.7519 4.30966C13.8029 4.33975 13.8473 4.37819 13.884 4.42269L13.9701 4.57451C13.9896 4.62899 14 4.68722 14 4.74695V5.8313H12.5V5.32852L12.0795 5.08013Z"></path><path d="M12.5 6.91565H14V9.08435H12.5V6.91565Z"></path><path d="M12.5 10.1687H14V11.253C14 11.3128 13.9896 11.371 13.9701 11.4255L13.884 11.5773C13.8473 11.6218 13.8029 11.6602 13.7519 11.6903L12.8353 12.2318L12.0795 10.9199L12.5 10.6715V10.1687Z"></path><path d="M11.1628 11.4613L11.9186 12.7732L10.0853 13.856L9.32946 12.5441L11.1628 11.4613Z"></path><path d="M8.4128 13.0855L9.1686 14.3974L8.25194 14.9389C8.2003 14.9694 8.14461 14.9897 8.08756 15H7.91244C7.85539 14.9897 7.7997 14.9694 7.74806 14.9389L6.8314 14.3974L7.5872 13.0855L8 13.3294L8.4128 13.0855Z"></path><path d="M6.67054 12.5441L5.91473 13.856L4.0814 12.7732L4.8372 11.4613L6.67054 12.5441Z"></path><path d="M3.92054 10.9199L3.16473 12.2318L2.24806 11.6903C2.19711 11.6602 2.15266 11.6218 2.11601 11.5773L2.02989 11.4255C2.01039 11.371 2 11.3128 2 11.253V10.1687H3.5V10.6715L3.92054 10.9199Z"></path><path d="M3.5 9.08435H2V6.91565H3.5V9.08435Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="1 1 14 14" fill="#DCD8FE93"><path d="M5.96967 5.96967C6.26256 5.67678 6.73744 5.67678 7.03033 5.96967L8 6.93934L8.96967 5.96967C9.26256 5.67678 9.73744 5.67678 10.0303 5.96967C10.3232 6.26256 10.3232 6.73744 10.0303 7.03033L9.06066 8L10.0303 8.96967C10.3232 9.26256 10.3232 9.73744 10.0303 10.0303C9.73744 10.3232 9.26256 10.3232 8.96967 10.0303L8 9.06066L7.03033 10.0303C6.73744 10.3232 6.26256 10.3232 5.96967 10.0303C5.67678 9.73744 5.67678 9.26256 5.96967 8.96967L6.93934 8L5.96967 7.03033C5.67678 6.73744 5.67678 6.26256 5.96967 5.96967Z"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M8.75581 1.21148C8.28876 0.929507 7.71124 0.929507 7.24419 1.21148L2.74419 3.92829C2.28337 4.20651 2 4.71711 2 5.26927V10.7307C2 11.2829 2.28337 11.7935 2.74419 12.0717L7.24419 14.7885C7.71124 15.0705 8.28876 15.0705 8.75581 14.7885L13.2558 12.0717C13.7166 11.7935 14 11.2829 14 10.7307V5.26927C14 4.71711 13.7166 4.20651 13.2558 3.92829L8.75581 1.21148ZM12.5 5.26928L8 2.55246L3.5 5.26927L3.5 10.7307L8 13.4475L12.5 10.7307L12.5 5.26928Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="1 1 14 14" fill="#5e6ad2"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.5 5.125L8 2.5L3.5 5.125L3.5 10.4019L8 13.0269L12.5 10.4019L12.5 5.125ZM8.75581 1.20433C8.28876 0.93189 7.71124 0.931889 7.24419 1.20433L2.74419 3.82933C2.28337 4.09815 2 4.5915 2 5.125V10.4019C2 10.9354 2.28337 11.4287 2.74419 11.6976L7.24419 14.3226C7.71124 14.595 8.28876 14.595 8.75581 14.3226L13.2558 11.6976C13.7166 11.4287 14 10.9354 14 10.4019V5.125C14 4.5915 13.7166 4.09815 13.2558 3.82933L8.75581 1.20433Z"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M10.7381 5.69424C11.0526 5.96381 11.089 6.43728 10.8194 6.75178L7.81944 10.2518C7.68349 10.4104 7.48754 10.5051 7.27878 10.5131C7.07003 10.5212 6.86739 10.4417 6.71967 10.294L5.21967 8.79402C4.92678 8.50112 4.92678 8.02625 5.21967 7.73336C5.51256 7.44046 5.98744 7.44046 6.28033 7.73336L7.20764 8.66066L9.68056 5.77559C9.95012 5.4611 10.4236 5.42468 10.7381 5.69424Z"></path></svg>

After

Width:  |  Height:  |  Size: 971 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="1 1 14 14" fill="#DCD8FE93"><path fill-rule="evenodd" clip-rule="evenodd" d="M8.75581 1.21148C8.28876 0.929507 7.71124 0.929507 7.24419 1.21148L2.74419 3.92829C2.28337 4.20651 2 4.71711 2 5.26927V10.7307C2 11.2829 2.28337 11.7935 2.74419 12.0717L7.24419 14.7885C7.71124 15.0705 8.28876 15.0705 8.75581 14.7885L13.2558 12.0717C13.7166 11.7935 14 11.2829 14 10.7307V5.26927C14 4.71711 13.7166 4.20651 13.2558 3.92829L8.75581 1.21148ZM12.5 5.26928L8 2.55246L3.5 5.26927L3.5 10.7307L8 13.4475L12.5 10.7307L12.5 5.26928Z"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 5.75C6.91421 5.75 7.25 6.08579 7.25 6.5V9.5C7.25 9.91421 6.91421 10.25 6.5 10.25C6.08579 10.25 5.75 9.91421 5.75 9.5V6.5C5.75 6.08579 6.08579 5.75 6.5 5.75Z"></path><path fill-rule="evenodd" clip-rule="evenodd" d="M9.5 5.75C9.91421 5.75 10.25 6.08579 10.25 6.5V9.5C10.25 9.91421 9.91421 10.25 9.5 10.25C9.08579 10.25 8.75 9.91421 8.75 9.5V6.5C8.75 6.08579 9.08579 5.75 9.5 5.75Z"></path></svg>

After

Width:  |  Height:  |  Size: 1009 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" viewBox="1 1 14 14" fill="#e2e2e2"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.5 5.36133L8 2.73633L3.5 5.36133L3.5 10.6382L8 13.2632L12.5 10.6382L12.5 5.36133ZM8.75581 1.44066C8.28876 1.16822 7.71124 1.16822 7.24419 1.44066L2.74419 4.06566C2.28337 4.33448 2 4.82783 2 5.36133V10.6382C2 11.1717 2.28337 11.6651 2.74419 11.9339L7.24419 14.5589C7.71124 14.8313 8.28876 14.8313 8.75581 14.5589L13.2558 11.9339C13.7166 11.6651 14 11.1717 14 10.6382V5.36133C14 4.82783 13.7166 4.33448 13.2558 4.06566L8.75581 1.44066Z"></path></svg>

After

Width:  |  Height:  |  Size: 564 B

View File

@@ -0,0 +1 @@
<svg width="16" height="16" fill="#f2be00" viewBox="1 1 14 14"><path d="M8.3779 4.74233C8.14438 4.60607 7.85562 4.60607 7.6221 4.74233L5.37209 6.05513C5.14168 6.18957 5 6.4363 5 6.70311V9.34216C5 9.60897 5.14168 9.85573 5.37209 9.99016L7.6221 11.303C7.85562 11.4392 8.14438 11.4392 8.3779 11.303L10.6279 9.99016C10.8583 9.85573 11 9.60897 11 9.34216V6.70311C11 6.4363 10.8583 6.18957 10.6279 6.05513L8.3779 4.74233Z" mask="url(#hole-50)"></path><mask id="hole-50"><rect width="100%" height="100%" fill="white"></rect><circle r="4" cx="7.5" cy="8" fill="black" stroke="white" stroke-width="8" stroke-dasharray="calc(12.56) 25.12" transform="rotate(-90) translate(-16)"></circle></mask><path fill-rule="evenodd" clip-rule="evenodd" d="M7.24419 1.44066C7.71124 1.16822 8.28876 1.16822 8.75581 1.44066L13.2558 4.06566C13.7166 4.33448 14 4.82783 14 5.36133V10.6382C14 11.1717 13.7166 11.6651 13.2558 11.9339L8.75581 14.5589C8.28876 14.8313 7.71124 14.8313 7.24419 14.5589L2.74419 11.9339C2.28337 11.6651 2 11.1717 2 10.6382V5.36133C2 4.82783 2.28337 4.33448 2.74419 4.06566L7.24419 1.44066ZM8 2.73633L12.5 5.36133V10.6382L8 13.2632L3.5 10.6382V5.36133L8 2.73633Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

1300
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

20
src/api.ts Normal file
View File

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

View File

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

View File

@@ -1,35 +0,0 @@
import { environment } from "@raycast/api";
import { useMemo } from "react";
import { NotificationListItemProps } from "../../types";
import { GithubDiscussionNotificationListItem } from "./listitem/GithubDiscussionNotificationListItem";
import { GithubPullRequestNotificationListItem } from "./listitem/GithubPullRequestNotificationListItem";
export function GithubNotificationListItem({ notification }: NotificationListItemProps) {
const icon = useMemo(() => {
if (environment.appearance === "dark") {
return "github-logo-light.svg";
}
return "github-logo-dark.svg";
}, [environment]);
switch (notification.details?.type) {
case "GithubPullRequest":
return (
<GithubPullRequestNotificationListItem
icon={icon}
notification={notification}
githubPullRequest={notification.details.content}
/>
);
case "GithubDiscussion":
return (
<GithubDiscussionNotificationListItem
icon={icon}
notification={notification}
githubDiscussion={notification.details.content}
/>
);
default:
return null;
}
}

View File

@@ -1,10 +1,13 @@
import { Icon, Image, List } from "@raycast/api";
import { GithubActor, getGithubActorName } from "./types"; import { GithubActor, getGithubActorName } from "./types";
import { Icon, Image, List } from "@raycast/api";
import { getAvatarIcon } from "@raycast/utils";
export function getGithubActorAccessory(actor?: GithubActor): List.Item.Accessory { export function getGithubActorAccessory(actor?: GithubActor): List.Item.Accessory {
if (actor) { if (actor) {
return { return {
icon: actor.content.avatar_url ? { source: actor.content.avatar_url, mask: Image.Mask.Circle } : Icon.Person, icon: actor.content.avatar_url
? { source: actor.content.avatar_url, mask: Image.Mask.Circle }
: getAvatarIcon(getGithubActorName(actor)),
tooltip: getGithubActorName(actor), tooltip: getGithubActorName(actor),
}; };
} }

View File

@@ -1,20 +1,22 @@
import { Color, Icon, List } from "@raycast/api";
import { NotificationActions } from "../../../NotificationActions";
import { Notification } from "../../../types";
import { getGithubActorAccessory } from "../misc";
import { GithubDiscussion, GithubDiscussionStateReason } from "../types";
import { GithubDiscussionPreview } from "../preview/GithubDiscussionPreview"; import { GithubDiscussionPreview } from "../preview/GithubDiscussionPreview";
import { NotificationActions } from "../../../action/NotificationActions";
import { GithubDiscussion, GithubDiscussionStateReason } from "../types";
import { getGithubActorAccessory } from "../accessories";
import { Notification } from "../../../notification";
import { Color, Icon, List } from "@raycast/api";
import { MutatePromise } from "@raycast/utils";
import { Page } from "../../../types";
interface GithubDiscussionNotificationListItemProps { interface GithubDiscussionNotificationListItemProps {
icon: string;
notification: Notification; notification: Notification;
githubDiscussion: GithubDiscussion; githubDiscussion: GithubDiscussion;
mutate: MutatePromise<Page<Notification> | undefined>;
} }
export function GithubDiscussionNotificationListItem({ export function GithubDiscussionNotificationListItem({
icon,
notification, notification,
githubDiscussion, githubDiscussion,
mutate,
}: GithubDiscussionNotificationListItemProps) { }: GithubDiscussionNotificationListItemProps) {
const subtitle = `${githubDiscussion.repository.name_with_owner}`; const subtitle = `${githubDiscussion.repository.name_with_owner}`;
@@ -41,13 +43,14 @@ export function GithubDiscussionNotificationListItem({
<List.Item <List.Item
key={notification.id} key={notification.id}
title={notification.title} title={notification.title}
icon={icon} icon={{ source: { light: "github-logo-dark.svg", dark: "github-logo-light.svg" } }}
subtitle={subtitle} subtitle={subtitle}
accessories={accessories} accessories={accessories}
actions={ actions={
<NotificationActions <NotificationActions
notification={notification} notification={notification}
detailsTarget={<GithubDiscussionPreview notification={notification} githubDiscussion={githubDiscussion} />} detailsTarget={<GithubDiscussionPreview notification={notification} githubDiscussion={githubDiscussion} />}
mutate={mutate}
/> />
} }
/> />

View File

@@ -0,0 +1,26 @@
import { GithubPullRequestNotificationListItem } from "./GithubPullRequestNotificationListItem";
import { GithubDiscussionNotificationListItem } from "./GithubDiscussionNotificationListItem";
import { NotificationListItemProps } from "../../../notification";
export function GithubNotificationListItem({ notification, mutate }: NotificationListItemProps) {
switch (notification.details?.type) {
case "GithubPullRequest":
return (
<GithubPullRequestNotificationListItem
notification={notification}
githubPullRequest={notification.details.content}
mutate={mutate}
/>
);
case "GithubDiscussion":
return (
<GithubDiscussionNotificationListItem
notification={notification}
githubDiscussion={notification.details.content}
mutate={mutate}
/>
);
default:
return null;
}
}

View File

@@ -1,7 +1,3 @@
import { Color, Icon, List } from "@raycast/api";
import { NotificationActions } from "../../../NotificationActions";
import { Notification } from "../../../types";
import { GithubPullRequestPreview } from "../preview/GithubPullRequestPreview";
import { import {
GithubPullRequestState, GithubPullRequestState,
GithubPullRequestReviewDecision, GithubPullRequestReviewDecision,
@@ -11,18 +7,24 @@ import {
GithubCheckStatusState, GithubCheckStatusState,
GithubCheckSuite, GithubCheckSuite,
} from "../types"; } from "../types";
import { getGithubActorAccessory } from "../misc"; import { GithubPullRequestPreview } from "../preview/GithubPullRequestPreview";
import { NotificationActions } from "../../../action/NotificationActions";
import { getGithubActorAccessory } from "../accessories";
import { Notification } from "../../../notification";
import { Color, Icon, List } from "@raycast/api";
import { MutatePromise } from "@raycast/utils";
import { Page } from "../../../types";
interface GithubPullRequestNotificationListItemProps { interface GithubPullRequestNotificationListItemProps {
icon: string;
notification: Notification; notification: Notification;
githubPullRequest: GithubPullRequest; githubPullRequest: GithubPullRequest;
mutate: MutatePromise<Page<Notification> | undefined>;
} }
export function GithubPullRequestNotificationListItem({ export function GithubPullRequestNotificationListItem({
icon,
notification, notification,
githubPullRequest, githubPullRequest,
mutate,
}: GithubPullRequestNotificationListItemProps) { }: GithubPullRequestNotificationListItemProps) {
const subtitle = `${githubPullRequest.head_repository?.name_with_owner} #${githubPullRequest.number}`; const subtitle = `${githubPullRequest.head_repository?.name_with_owner} #${githubPullRequest.number}`;
@@ -60,13 +62,14 @@ export function GithubPullRequestNotificationListItem({
<List.Item <List.Item
key={notification.id} key={notification.id}
title={notification.title} title={notification.title}
icon={icon} icon={{ source: { light: "github-logo-dark.svg", dark: "github-logo-light.svg" } }}
accessories={accessories} accessories={accessories}
subtitle={subtitle} subtitle={subtitle}
actions={ actions={
<NotificationActions <NotificationActions
notification={notification} notification={notification}
detailsTarget={<GithubPullRequestPreview notification={notification} githubPullRequest={githubPullRequest} />} detailsTarget={<GithubPullRequestPreview notification={notification} githubPullRequest={githubPullRequest} />}
mutate={mutate}
/> />
} }
/> />
@@ -82,7 +85,7 @@ function getGithubPullRequestChecksAccessory(latestCommit: GithubCommitChecks):
case GithubCheckStatusState.Pending: case GithubCheckStatusState.Pending:
return { icon: Icon.Pause, tooltip: "Pending" }; return { icon: Icon.Pause, tooltip: "Pending" };
case GithubCheckStatusState.InProgress: case GithubCheckStatusState.InProgress:
return { icon: Icon.Pause, tooltip: "In progress" }; // TODO Spinner return { icon: Icon.CircleProgress, tooltip: "In progress" };
case GithubCheckStatusState.Completed: case GithubCheckStatusState.Completed:
switch (progress.conclusion()) { switch (progress.conclusion()) {
case GithubCheckConclusionState.Success: case GithubCheckConclusionState.Success:

View File

@@ -1,8 +1,7 @@
import { Notification, getNotificationHtmlUrl } from "../../../notification";
import { Detail, ActionPanel, Action } from "@raycast/api"; import { Detail, ActionPanel, Action } from "@raycast/api";
import { Notification } from "../../../types";
import { GithubDiscussion } from "../types"; import { GithubDiscussion } from "../types";
import { useMemo } from "react"; import { useMemo } from "react";
import { getNotificationHtmlUrl } from "../../../notification";
interface GithubDiscussionPreviewProps { interface GithubDiscussionPreviewProps {
notification: Notification; notification: Notification;
@@ -10,7 +9,7 @@ interface GithubDiscussionPreviewProps {
} }
export function GithubDiscussionPreview({ notification, githubDiscussion }: GithubDiscussionPreviewProps) { export function GithubDiscussionPreview({ notification, githubDiscussion }: GithubDiscussionPreviewProps) {
const notification_html_url = useMemo(() => { const notificationHtmlUrl = useMemo(() => {
return getNotificationHtmlUrl(notification); return getNotificationHtmlUrl(notification);
}, [notification]); }, [notification]);
@@ -19,7 +18,7 @@ export function GithubDiscussionPreview({ notification, githubDiscussion }: Gith
markdown={`# ${githubDiscussion.title}`} markdown={`# ${githubDiscussion.title}`}
actions={ actions={
<ActionPanel> <ActionPanel>
<Action.OpenInBrowser url={notification_html_url} /> <Action.OpenInBrowser url={notificationHtmlUrl} />
</ActionPanel> </ActionPanel>
} }
/> />

View File

@@ -1,8 +1,7 @@
import { Notification, getNotificationHtmlUrl } from "../../../notification";
import { Detail, ActionPanel, Action } from "@raycast/api"; import { Detail, ActionPanel, Action } from "@raycast/api";
import { Notification } from "../../../types";
import { GithubPullRequest } from "../types"; import { GithubPullRequest } from "../types";
import { useMemo } from "react"; import { useMemo } from "react";
import { getNotificationHtmlUrl } from "../../../notification";
interface GithubPullRequestPreviewProps { interface GithubPullRequestPreviewProps {
notification: Notification; notification: Notification;
@@ -10,7 +9,7 @@ interface GithubPullRequestPreviewProps {
} }
export function GithubPullRequestPreview({ notification, githubPullRequest }: GithubPullRequestPreviewProps) { export function GithubPullRequestPreview({ notification, githubPullRequest }: GithubPullRequestPreviewProps) {
const notification_html_url = useMemo(() => { const notificationHtmlUrl = useMemo(() => {
return getNotificationHtmlUrl(notification); return getNotificationHtmlUrl(notification);
}, [notification]); }, [notification]);
@@ -19,7 +18,7 @@ export function GithubPullRequestPreview({ notification, githubPullRequest }: Gi
markdown={`# ${githubPullRequest.title}`} markdown={`# ${githubPullRequest.title}`}
actions={ actions={
<ActionPanel> <ActionPanel>
<Action.OpenInBrowser url={notification_html_url} /> <Action.OpenInBrowser url={notificationHtmlUrl} />
</ActionPanel> </ActionPanel>
} }
/> />

View File

@@ -1,25 +0,0 @@
import { Detail, List, environment } from "@raycast/api";
import { useMemo } from "react";
import { NotificationActions } from "../../NotificationActions";
import { NotificationListItemProps } from "../../types";
export function GoogleMailNotificationListItem({ notification }: NotificationListItemProps) {
const icon = useMemo(() => {
if (environment.appearance === "dark") {
return "google-mail-logo-light.svg";
}
return "google-mail-logo-dark.svg";
}, [environment]);
return (
<List.Item
key={notification.id}
title={notification.title}
icon={icon}
subtitle={`#${notification.source_id}`}
actions={
<NotificationActions notification={notification} detailsTarget={<Detail markdown="# To be implemented 👋" />} />
}
/>
);
}

View File

@@ -0,0 +1,14 @@
import { GoogleMailThreadListItem } from "./GoogleMailThreadListItem";
import { NotificationListItemProps } from "../../../notification";
export function GoogleMailNotificationListItem({ notification, mutate }: NotificationListItemProps) {
if (notification.metadata.type !== "GoogleMail") return null;
return (
<GoogleMailThreadListItem
notification={notification}
googleMailThread={notification.metadata.content}
mutate={mutate}
/>
);
}

View File

@@ -0,0 +1,59 @@
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";
import { GoogleMailThread } from "../types";
import { Page } from "../../../types";
interface GoogleMailThreadListItemProps {
notification: Notification;
googleMailThread: GoogleMailThread;
mutate: MutatePromise<Page<Notification> | undefined>;
}
export function GoogleMailThreadListItem({ notification, googleMailThread, mutate }: GoogleMailThreadListItemProps) {
const isStarred = googleMailThread.messages.some((message) => message.labelIds?.includes("STARRED"));
const isImportant = googleMailThread.messages.some((message) => message.labelIds?.includes("IMPORTANT"));
const fromAddress = googleMailThread.messages[0].payload.headers.find((header) => header.name === "From")?.value;
const subtitle = fromAddress;
const accessories: List.Item.Accessory[] = [
{
date: new Date(notification.updated_at),
tooltip: `Updated at ${notification.updated_at}`,
},
];
if (isStarred) {
accessories.unshift({
icon: { source: Icon.Star, tintColor: Color.Yellow },
tooltip: "Starred",
});
}
if (isImportant) {
accessories.unshift({
icon: { source: Icon.Exclamationmark, tintColor: Color.Red },
tooltip: "Important",
});
}
return (
<List.Item
key={notification.id}
title={notification.title}
icon={{ source: { light: "google-mail-logo-dark.svg", dark: "google-mail-logo-light.svg" } }}
accessories={accessories}
subtitle={subtitle}
actions={
<NotificationActions
notification={notification}
detailsTarget={<GoogleMailThreadPreview notification={notification} googleMailThread={googleMailThread} />}
mutate={mutate}
/>
}
/>
);
}

View File

@@ -0,0 +1,26 @@
import { Notification, getNotificationHtmlUrl } from "../../../notification";
import { Detail, ActionPanel, Action } from "@raycast/api";
import { GoogleMailThread } from "../types";
import { useMemo } from "react";
interface GoogleMailThreadPreviewProps {
notification: Notification;
googleMailThread: GoogleMailThread;
}
export function GoogleMailThreadPreview({ notification }: GoogleMailThreadPreviewProps) {
const notificationHtmlUrl = useMemo(() => {
return getNotificationHtmlUrl(notification);
}, [notification]);
return (
<Detail
markdown={`# ${notification.title}`}
actions={
<ActionPanel>
<Action.OpenInBrowser url={notificationHtmlUrl} />
</ActionPanel>
}
/>
);
}

View File

@@ -0,0 +1,27 @@
export interface GoogleMailThread {
id: string;
user_email_address: string;
history_id: string;
messages: Array<GoogleMailMessage>;
}
export interface GoogleMailMessage {
id: string;
threadId: string;
labelIds?: Array<string>;
snippet: string;
payload: GoogleMailMessagePayload;
sizeEstimate: number;
historyId: string;
internalDate: Date;
}
export interface GoogleMailMessagePayload {
mimeType: string;
headers: Array<GoogleMailMessageHeader>;
}
export interface GoogleMailMessageHeader {
name: string;
value: string;
}

View File

@@ -1,25 +0,0 @@
import { Detail, List, environment } from "@raycast/api";
import { useMemo } from "react";
import { NotificationActions } from "../../NotificationActions";
import { NotificationListItemProps } from "../../types";
export function LinearNotificationListItem({ notification }: NotificationListItemProps) {
const icon = useMemo(() => {
if (environment.appearance === "dark") {
return "linear-logo-light.svg";
}
return "linear-logo-dark.svg";
}, [environment]);
return (
<List.Item
key={notification.id}
title={notification.title}
icon={icon}
subtitle={`#${notification.source_id}`}
actions={
<NotificationActions notification={notification} detailsTarget={<Detail markdown="# To be implemented 👋" />} />
}
/>
);
}

View File

@@ -0,0 +1,13 @@
import { Icon, Image, List } from "@raycast/api";
import { getAvatarIcon } from "@raycast/utils";
import { LinearUser } from "./types";
export function getLinearUserAccessory(user?: LinearUser): List.Item.Accessory {
if (user) {
return {
icon: user.avatar_url ? { source: user.avatar_url, mask: Image.Mask.Circle } : getAvatarIcon(user.name),
tooltip: user.name,
};
}
return { icon: Icon.Person, tooltip: "Unknown" };
}

View File

@@ -0,0 +1,69 @@
import { LinearWorkflowStateType, LinearIssueNotification, LinearWorkflowState } from "../types";
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 { List } from "@raycast/api";
import { match } from "ts-pattern";
interface LinearIssueNotificationListItemProps {
notification: Notification;
linearIssueNotification: LinearIssueNotification;
mutate: MutatePromise<Page<Notification> | undefined>;
}
export function LinearIssueNotificationListItem({
notification,
linearIssueNotification,
mutate,
}: LinearIssueNotificationListItemProps) {
const subtitle = `${linearIssueNotification.issue.team.name} #${linearIssueNotification.issue.identifier}`;
const state = getLinearIssueStateAccessory(linearIssueNotification.issue.state);
const assignee = getLinearUserAccessory(linearIssueNotification.issue.assignee);
const accessories: List.Item.Accessory[] = [
state,
assignee,
{
date: new Date(linearIssueNotification.updated_at),
tooltip: `Updated at ${linearIssueNotification.updated_at}`,
},
];
return (
<List.Item
key={notification.id}
title={notification.title}
icon={{ source: { light: "linear-logo-dark.svg", dark: "linear-logo-light.svg" } }}
accessories={accessories}
subtitle={subtitle}
actions={
<NotificationActions
notification={notification}
detailsTarget={<LinearIssuePreview notification={notification} linearIssue={linearIssueNotification.issue} />}
mutate={mutate}
/>
}
/>
);
}
export function getLinearIssueStateAccessory(state: LinearWorkflowState): List.Item.Accessory {
return {
icon: {
source: match(state)
.with({ type: LinearWorkflowStateType.Triage }, () => "linear-issue-triage.svg")
.with({ type: LinearWorkflowStateType.Backlog }, () => "linear-issue-backlog.svg")
.with({ type: LinearWorkflowStateType.Unstarted }, () => "linear-issue-unstarted.svg")
.with({ type: LinearWorkflowStateType.Started }, () => "linear-issue-started.svg")
.with({ type: LinearWorkflowStateType.Completed }, () => "linear-issue-completed.svg")
.with({ type: LinearWorkflowStateType.Canceled }, () => "linear-issue-canceled.svg")
.exhaustive(),
tintColor: state.color,
},
tooltip: state.name,
};
}

View File

@@ -0,0 +1,28 @@
import { LinearProjectNotificationListItem } from "./LinearProjectNotificationListItem";
import { LinearIssueNotificationListItem } from "./LinearIssueNotificationListItem";
import { NotificationListItemProps } from "../../../notification";
export function LinearNotificationListItem({ notification, mutate }: NotificationListItemProps) {
if (notification.metadata.type !== "Linear") return null;
switch (notification.metadata.content.type) {
case "IssueNotification":
return (
<LinearIssueNotificationListItem
notification={notification}
linearIssueNotification={notification.metadata.content.content}
mutate={mutate}
/>
);
case "ProjectNotification":
return (
<LinearProjectNotificationListItem
notification={notification}
linearProjectNotification={notification.metadata.content.content}
mutate={mutate}
/>
);
default:
return null;
}
}

View File

@@ -0,0 +1,80 @@
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 } from "ts-pattern";
interface LinearProjectNotificationListItemProps {
notification: Notification;
linearProjectNotification: LinearProjectNotification;
mutate: MutatePromise<Page<Notification> | undefined>;
}
export function LinearProjectNotificationListItem({
notification,
linearProjectNotification,
mutate,
}: LinearProjectNotificationListItemProps) {
const subtitle = linearProjectNotification.project.name;
const state = getLinearProjectStateAccessory(linearProjectNotification.project);
const lead = getLinearUserAccessory(linearProjectNotification.project.lead);
const accessories: List.Item.Accessory[] = [
state,
lead,
{
date: new Date(linearProjectNotification.updated_at),
tooltip: `Updated at ${linearProjectNotification.updated_at}`,
},
];
return (
<List.Item
key={notification.id}
title={notification.title}
icon={{ source: { light: "linear-logo-dark.svg", dark: "linear-logo-light.svg" } }}
accessories={accessories}
subtitle={subtitle}
actions={
<NotificationActions
notification={notification}
detailsTarget={
<LinearProjectPreview notification={notification} linearProject={linearProjectNotification.project} />
}
mutate={mutate}
/>
}
/>
);
}
export function getLinearProjectStateAccessory(project: LinearProject): List.Item.Accessory {
return {
icon: match(project)
.with({ state: LinearProjectState.Planned }, () => {
return { source: "linear-project-planned.svg", tintColor: Color.SecondaryText };
})
.with({ state: LinearProjectState.Backlog }, () => {
return { source: "linear-project-backlog.svg", tintColor: Color.PrimaryText };
})
.with({ state: LinearProjectState.Started }, () => {
return { source: "linear-project-started.svg", tintColor: Color.Blue };
})
.with({ state: LinearProjectState.Paused }, () => {
return { source: "linear-project-paused.svg", tintColor: Color.PrimaryText };
})
.with({ state: LinearProjectState.Completed }, () => {
return { source: "linear-project-completed.svg", tintColor: Color.Magenta };
})
.with({ state: LinearProjectState.Canceled }, () => {
return { source: "linear-project-canceled.svg", tintColor: Color.SecondaryText };
})
.exhaustive(),
tooltip: project.state,
};
}

View File

@@ -0,0 +1,26 @@
import { Notification, getNotificationHtmlUrl } from "../../../notification";
import { Detail, ActionPanel, Action } from "@raycast/api";
import { LinearIssue } from "../types";
import { useMemo } from "react";
interface LinearIssuePreviewProps {
notification: Notification;
linearIssue: LinearIssue;
}
export function LinearIssuePreview({ notification, linearIssue }: LinearIssuePreviewProps) {
const notificationHtmlUrl = useMemo(() => {
return getNotificationHtmlUrl(notification);
}, [notification]);
return (
<Detail
markdown={`# ${linearIssue.title}`}
actions={
<ActionPanel>
<Action.OpenInBrowser url={notificationHtmlUrl} />
</ActionPanel>
}
/>
);
}

View File

@@ -0,0 +1,26 @@
import { Notification, getNotificationHtmlUrl } from "../../../notification";
import { Detail, ActionPanel, Action } from "@raycast/api";
import { LinearProject } from "../types";
import { useMemo } from "react";
interface LinearProjectPreviewProps {
notification: Notification;
linearProject: LinearProject;
}
export function LinearProjectPreview({ notification, linearProject }: LinearProjectPreviewProps) {
const notificationHtmlUrl = useMemo(() => {
return getNotificationHtmlUrl(notification);
}, [notification]);
return (
<Detail
markdown={`# ${linearProject.name}`}
actions={
<ActionPanel>
<Action.OpenInBrowser url={notificationHtmlUrl} />
</ActionPanel>
}
/>
);
}

View File

@@ -0,0 +1,120 @@
export type LinearNotification =
| { type: "IssueNotification"; content: LinearIssueNotification }
| { type: "ProjectNotification"; content: LinearProjectNotification };
export interface LinearIssueNotification {
id: string;
type: string;
read_at?: Date;
updated_at: Date;
snoozed_until_at?: Date;
organization: LinearOrganization;
issue: LinearIssue;
}
export interface LinearOrganization {
name: string;
key: string;
logo_url?: string;
}
export interface LinearIssue {
id: string;
created_at: Date;
updated_at: Date;
completed_at?: Date;
canceled_at?: Date;
due_date?: Date;
identifier: string;
title: string;
url: string;
priority: LinearIssuePriority;
project?: LinearProject;
project_milestone?: LinearProjectMilestone;
creator?: LinearUser;
assignee?: LinearUser;
state: LinearWorkflowState;
labels: Array<LinearLabel>;
description: string;
team: LinearTeam;
}
export enum LinearIssuePriority {
NoPriority = 0,
Urgent = 1,
High = 2,
Normal = 3,
Low = 4,
}
export interface LinearProject {
id: string;
name: string;
url: string;
description: string;
icon?: string;
color: string;
state: LinearProjectState;
progress: number; // percentage between 0 and 100
start_date?: Date;
target_date?: Date;
lead?: LinearUser;
}
export enum LinearProjectState {
Planned = "Planned",
Backlog = "Backlog",
Started = "Started",
Paused = "Paused",
Completed = "Completed",
Canceled = "Canceled",
}
export interface LinearProjectMilestone {
name: string;
description?: string;
}
export interface LinearUser {
name: string;
avatar_url?: string;
url: string;
}
export interface LinearWorkflowState {
name: string;
description?: string;
color: string;
type: LinearWorkflowStateType;
}
export enum LinearWorkflowStateType {
Triage = "Triage",
Backlog = "Backlog",
Unstarted = "Unstarted",
Started = "Started",
Completed = "Completed",
Canceled = "Canceled",
}
export interface LinearLabel {
name: string;
description?: string;
color: string;
}
export interface LinearTeam {
id: string;
key: string;
name: string;
}
export interface LinearProjectNotification {
id: string;
type: string;
read_at?: Date;
updated_at: Date;
snoozed_until_at?: Date;
organization: LinearOrganization;
project: LinearProject;
}

View File

@@ -1,28 +0,0 @@
import { Detail, List, environment } from "@raycast/api";
import { useMemo } from "react";
import { NotificationTaskActions } from "../../NotificationTaskActions";
import { NotificationListItemProps } from "../../types";
export function TodoistNotificationListItem({ notification }: NotificationListItemProps) {
const icon = useMemo(() => {
if (environment.appearance === "dark") {
return "todoist-icon-light.svg";
}
return "todoist-icon-dark.svg";
}, [environment]);
return (
<List.Item
key={notification.id}
title={notification.title}
icon={icon}
subtitle={`#${notification.source_id}`}
actions={
<NotificationTaskActions
notification={notification}
detailsTarget={<Detail markdown="# To be implemented 👋" />}
/>
}
/>
);
}

View File

@@ -0,0 +1,52 @@
import { NotificationTaskActions } from "../../../action/NotificationTaskActions";
import { TodoistTaskPreview } from "../preview/TodoistTaskPreview";
import { NotificationListItemProps } from "../../../notification";
import { Icon, List, Color } from "@raycast/api";
import { TaskPriority } from "../../../task";
import { match } from "ts-pattern";
import dayjs from "dayjs";
export function TodoistNotificationListItem({ notification, mutate }: NotificationListItemProps) {
const dueAt = notification.task?.due_at?.content;
const subtitle = dueAt ? dayjs(dueAt).format("YYYY-MM-DD") : undefined;
const color = match(notification)
.with({ task: { priority: TaskPriority.P1 } }, () => Color.Red)
.with({ task: { priority: TaskPriority.P2 } }, () => Color.Orange)
.with({ task: { priority: TaskPriority.P3 } }, () => Color.Yellow)
.otherwise(() => null);
const accessories: List.Item.Accessory[] = [
{
icon: { source: Icon.Circle, tintColor: color },
},
{
date: new Date(notification.updated_at),
tooltip: `Updated at ${notification.updated_at}`,
},
];
const task = notification.task;
if (task) {
for (const tag of task.tags) {
accessories.unshift({ tag: { value: tag } });
}
}
return (
<List.Item
key={notification.id}
title={notification.title}
icon={{ source: { light: "todoist-logo-dark.svg", dark: "todoist-logo-light.svg" } }}
subtitle={subtitle}
accessories={accessories}
actions={
<NotificationTaskActions
notification={notification}
detailsTarget={<TodoistTaskPreview notification={notification} />}
mutate={mutate}
/>
}
/>
);
}

View File

@@ -0,0 +1,24 @@
import { Notification, getNotificationHtmlUrl } from "../../../notification";
import { Detail, ActionPanel, Action } from "@raycast/api";
import { useMemo } from "react";
interface TodoistTaskPreviewProps {
notification: Notification;
}
export function TodoistTaskPreview({ notification }: TodoistTaskPreviewProps) {
const notificationHtmlUrl = useMemo(() => {
return getNotificationHtmlUrl(notification);
}, [notification]);
return (
<Detail
markdown={`# ${notification.title}`}
actions={
<ActionPanel>
<Action.OpenInBrowser url={notificationHtmlUrl} />
</ActionPanel>
}
/>
);
}

View File

@@ -1,14 +1,74 @@
import { Notification } from "./types"; 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;
metadata: NotificationMetadata;
updated_at: Date;
last_read_at?: Date;
snoozed_until?: Date;
user_id: string;
task?: Task;
details?: NotificationDetails;
}
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",
Read = "Read",
Deleted = "Deleted",
Unsubscribed = "Unsubscribed",
}
export type NotificationListItemProps = {
notification: Notification;
mutate: MutatePromise<Page<Notification> | undefined>;
};
export function getNotificationHtmlUrl(notification: Notification) { export function getNotificationHtmlUrl(notification: Notification) {
switch (notification.details?.type) { return match(notification)
case "GithubPullRequest": .with(
return notification.details.content.url; { details: { type: P.union("GithubPullRequest", "GithubDiscussion"), content: P.select() } },
case "GithubDiscussion": (notificationDetails) => notificationDetails.url,
return notification.details.content.url; )
default: { .with(
// TODO { metadata: { type: "Linear", content: { type: "IssueNotification", content: P.select() } } },
return "https://github.com"; (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.metadata.type === "Todoist";
} }

41
src/task.ts Normal file
View File

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

View File

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