Compare commits
4 Commits
2d2d47f55f
...
ea40258f48
| Author | SHA1 | Date | |
|---|---|---|---|
|
ea40258f48
|
|||
|
e0f90b0c42
|
|||
|
3df827ebd7
|
|||
|
0507722ecf
|
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["@raycast"]
|
||||
"extends": ["@raycast", "plugin:import/recommended", "plugin:import/typescript"],
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"typescript": true,
|
||||
"node": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"singleQuote": false
|
||||
"singleQuote": false,
|
||||
"plugins": ["./node_modules/prettier-plugin-sort-imports/dist/index.js"]
|
||||
}
|
||||
1
assets/linear-issue-backlog.svg
Normal 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 |
1
assets/linear-issue-canceled.svg
Normal 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 |
1
assets/linear-issue-completed.svg
Normal 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 |
1
assets/linear-issue-started.svg
Normal 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 |
1
assets/linear-issue-triage.svg
Normal 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 |
1
assets/linear-issue-unstarted.svg
Normal 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 |
1
assets/linear-project-backlog.svg
Normal 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 |
1
assets/linear-project-canceled.svg
Normal 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 |
1
assets/linear-project-completed.svg
Normal 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 |
1
assets/linear-project-paused.svg
Normal 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 |
1
assets/linear-project-planned.svg
Normal 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 |
1
assets/linear-project-started.svg
Normal 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 |
4
justfile
@@ -5,6 +5,10 @@ default:
|
||||
build:
|
||||
npm run build
|
||||
|
||||
# Format code
|
||||
format:
|
||||
npm run format
|
||||
|
||||
# Lint extension code
|
||||
lint:
|
||||
npm run lint
|
||||
|
||||
1300
package-lock.json
generated
@@ -40,18 +40,25 @@
|
||||
"dependencies": {
|
||||
"@raycast/api": "^1.65.1",
|
||||
"@raycast/utils": "^1.10.1",
|
||||
"node-fetch": "^3.3.2"
|
||||
"dayjs": "^1.11.10",
|
||||
"node-fetch": "^3.3.2",
|
||||
"ts-pattern": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@raycast/eslint-config": "^1.0.6",
|
||||
"@types/node": "20.8.10",
|
||||
"@types/react": "18.2.27",
|
||||
"@typescript-eslint/parser": "^6.19.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-sort-imports": "^1.8.3",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "ray build -e dist",
|
||||
"format": "prettier --write --list-different --ignore-unknown src",
|
||||
"dev": "ray develop",
|
||||
"fix-lint": "ray lint --fix",
|
||||
"lint": "ray lint",
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { Action, ActionPanel, Icon } from "@raycast/api";
|
||||
import { useMemo, ReactElement } from "react";
|
||||
import { getNotificationHtmlUrl } from "./notification";
|
||||
import { Notification } from "./types";
|
||||
|
||||
function deleteNotification(notification: Notification) {
|
||||
console.log(`Deleting notification ${notification.id}`);
|
||||
}
|
||||
|
||||
function unsubscribeFromNotification(notification: Notification) {
|
||||
console.log(`Unsubcribing from notification ${notification.id}`);
|
||||
}
|
||||
|
||||
function snoozeNotification(notification: Notification) {
|
||||
console.log(`Snoozing notification ${notification.id}`);
|
||||
}
|
||||
|
||||
function createTaskFromNotification(notification: Notification) {
|
||||
console.log(`Creating task notification ${notification.id}`);
|
||||
}
|
||||
|
||||
function linkNotificationToTask(notification: Notification) {
|
||||
console.log(`Linking notification ${notification.id}`);
|
||||
}
|
||||
|
||||
export function NotificationActions({
|
||||
notification,
|
||||
detailsTarget,
|
||||
}: {
|
||||
notification: Notification;
|
||||
detailsTarget: ReactElement;
|
||||
}) {
|
||||
const notification_html_url = useMemo(() => {
|
||||
return getNotificationHtmlUrl(notification);
|
||||
}, [notification]);
|
||||
|
||||
return (
|
||||
<ActionPanel>
|
||||
<Action.Push title="Show Details" target={detailsTarget} />
|
||||
<Action.OpenInBrowser url={notification_html_url} />
|
||||
<Action
|
||||
title="Delete Notification"
|
||||
icon={Icon.Trash}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "d" }}
|
||||
onAction={() => deleteNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Unsubscribe From Notification"
|
||||
icon={Icon.BellDisabled}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "u" }}
|
||||
onAction={() => unsubscribeFromNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Snooze"
|
||||
icon={Icon.Clock}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "s" }}
|
||||
onAction={() => snoozeNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Create Task"
|
||||
icon={Icon.Calendar}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "d" }}
|
||||
onAction={() => createTaskFromNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Link to Task"
|
||||
icon={Icon.Link}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "d" }}
|
||||
onAction={() => linkNotificationToTask(notification)}
|
||||
/>
|
||||
</ActionPanel>
|
||||
);
|
||||
}
|
||||
152
src/action/CreateTaskFromNotification.tsx
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
109
src/action/LinkNotificationToTask.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Action, ActionPanel, useNavigation, Form, Icon, getPreferenceValues, showToast, Toast } from "@raycast/api";
|
||||
import { MutatePromise, useForm, FormValidation, useFetch } from "@raycast/utils";
|
||||
import { Page, UniversalInboxPreferences } from "../types";
|
||||
import { Notification } from "../notification";
|
||||
import { Task, TaskStatus } from "../task";
|
||||
import { handleErrors } from "../api";
|
||||
import { useState } from "react";
|
||||
import fetch from "node-fetch";
|
||||
|
||||
interface LinkNotificationToTaskProps {
|
||||
notification: Notification;
|
||||
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||
}
|
||||
|
||||
interface TaskLinkFormValues {
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
export function LinkNotificationToTask({ notification, mutate }: LinkNotificationToTaskProps) {
|
||||
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||
const { pop } = useNavigation();
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const { isLoading, data: tasks } = useFetch<Array<Task>>(
|
||||
`${preferences.universalInboxBaseUrl}/api/tasks/search?matches=${searchText}`,
|
||||
{
|
||||
keepPreviousData: true,
|
||||
headers: {
|
||||
Authorization: `Bearer ${preferences.apiKey}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { handleSubmit, itemProps } = useForm<TaskLinkFormValues>({
|
||||
async onSubmit(values) {
|
||||
await linkNotificationToTask(notification, values.taskId, mutate);
|
||||
pop();
|
||||
},
|
||||
validation: {
|
||||
taskId: FormValidation.Required,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Form
|
||||
navigationTitle="Link notification with task"
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<Action.SubmitForm title="Link to Task" icon={Icon.Link} onSubmit={handleSubmit} />
|
||||
</ActionPanel>
|
||||
}
|
||||
>
|
||||
<Form.Dropdown
|
||||
title="Task"
|
||||
placeholder="Search task..."
|
||||
filtering={true}
|
||||
throttle={true}
|
||||
isLoading={isLoading}
|
||||
onSearchTextChange={setSearchText}
|
||||
{...itemProps.taskId}
|
||||
>
|
||||
<Form.Dropdown.Item value="" title="" key={0} />
|
||||
{tasks?.map((task) => {
|
||||
return <Form.Dropdown.Item title={task.title} value={task.id} key={task.id} />;
|
||||
})}
|
||||
</Form.Dropdown>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
async function linkNotificationToTask(
|
||||
notification: Notification,
|
||||
taskId: string,
|
||||
mutate: MutatePromise<Page<Notification> | undefined>,
|
||||
) {
|
||||
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||
const toast = await showToast({ style: Toast.Style.Animated, title: "Linking notification to task" });
|
||||
try {
|
||||
await mutate(
|
||||
handleErrors(
|
||||
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ status: TaskStatus.Deleted, task_id: taskId }),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${preferences.apiKey}`,
|
||||
},
|
||||
}),
|
||||
),
|
||||
{
|
||||
optimisticUpdate(page) {
|
||||
if (page) {
|
||||
page.content = page.content.filter((n) => n.id !== notification.id);
|
||||
}
|
||||
console.log(`page(link): ${notification.id}`, page);
|
||||
return page;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
toast.style = Toast.Style.Success;
|
||||
toast.title = "Notification successfully linked to task";
|
||||
} catch (error) {
|
||||
toast.style = Toast.Style.Failure;
|
||||
toast.title = "Failed to link notification to task";
|
||||
toast.message = (error as Error).message;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
200
src/action/NotificationActions.tsx
Normal file
@@ -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();
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Notification, getNotificationHtmlUrl } from "../notification";
|
||||
import { Action, ActionPanel, Icon } from "@raycast/api";
|
||||
import { MutatePromise } from "@raycast/utils";
|
||||
import { useMemo, ReactElement } from "react";
|
||||
import { getNotificationHtmlUrl } from "./notification";
|
||||
import { Notification } from "./types";
|
||||
import { Page } from "../types";
|
||||
|
||||
function deleteNotification(notification: Notification) {
|
||||
console.log(`Deleting notification ${notification.id}`);
|
||||
@@ -19,21 +20,21 @@ function completeTask(notification: Notification) {
|
||||
console.log(`Completing task ${notification.id}`);
|
||||
}
|
||||
|
||||
export function NotificationTaskActions({
|
||||
notification,
|
||||
detailsTarget,
|
||||
}: {
|
||||
interface NotificationTaskActionsProps {
|
||||
notification: Notification;
|
||||
detailsTarget: ReactElement;
|
||||
}) {
|
||||
const notification_html_url = useMemo(() => {
|
||||
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||
}
|
||||
|
||||
export function NotificationTaskActions({ notification, detailsTarget }: NotificationTaskActionsProps) {
|
||||
const notificationHtmlUrl = useMemo(() => {
|
||||
return getNotificationHtmlUrl(notification);
|
||||
}, [notification]);
|
||||
|
||||
return (
|
||||
<ActionPanel>
|
||||
<Action.OpenInBrowser url={notificationHtmlUrl} />
|
||||
<Action.Push title="Show Details" target={detailsTarget} />
|
||||
<Action.OpenInBrowser url={notification_html_url} />
|
||||
<Action
|
||||
title="Delete Notification"
|
||||
icon={Icon.Trash}
|
||||
20
src/api.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Response } from "node-fetch";
|
||||
import { match } from "ts-pattern";
|
||||
|
||||
interface ResponseError {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export async function handleErrors(response: Promise<Response>) {
|
||||
return match(await response)
|
||||
.with({ status: 400 }, async (r) => {
|
||||
throw new Error(((await r.json()) as ResponseError).message);
|
||||
})
|
||||
.with({ status: 401 }, async (r) => {
|
||||
throw new Error(((await r.json()) as ResponseError).message);
|
||||
})
|
||||
.with({ status: 500 }, async (r) => {
|
||||
throw new Error(((await r.json()) as ResponseError).message);
|
||||
})
|
||||
.otherwise((resp) => resp);
|
||||
}
|
||||
@@ -1,20 +1,21 @@
|
||||
import { 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 { Notification, NotificationListItemProps } from "./notification";
|
||||
import { NotificationActions } from "./action/NotificationActions";
|
||||
import { Page, UniversalInboxPreferences } from "./types";
|
||||
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() {
|
||||
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||
|
||||
if (
|
||||
preferences.apiKey === undefined ||
|
||||
preferences.apiKey === ""
|
||||
/* preferences.universalInboxBaseUrl === undefined ||
|
||||
* preferences.universalInboxBaseUrl === "" */
|
||||
preferences.apiKey === "" ||
|
||||
preferences.universalInboxBaseUrl === undefined ||
|
||||
preferences.universalInboxBaseUrl === ""
|
||||
) {
|
||||
return (
|
||||
<Detail
|
||||
@@ -28,7 +29,7 @@ export default function Command() {
|
||||
);
|
||||
}
|
||||
|
||||
const { isLoading, data } = useFetch<Page<Notification>>(
|
||||
const { isLoading, data, mutate } = useFetch<Page<Notification>>(
|
||||
`${preferences.universalInboxBaseUrl}/api/notifications?status=Unread,Read&with_tasks=true`,
|
||||
{
|
||||
headers: {
|
||||
@@ -40,35 +41,39 @@ export default function Command() {
|
||||
return (
|
||||
<List isLoading={isLoading}>
|
||||
{data?.content.map((notification: Notification) => {
|
||||
return <NotificationListItem key={notification.id} notification={notification} />;
|
||||
return <NotificationListItem key={notification.id} notification={notification} mutate={mutate} />;
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationListItem({ notification }: NotificationListItemProps) {
|
||||
function NotificationListItem({ notification, mutate }: NotificationListItemProps) {
|
||||
switch (notification.metadata.type) {
|
||||
case "Github":
|
||||
return <GithubNotificationListItem notification={notification} />;
|
||||
return <GithubNotificationListItem notification={notification} mutate={mutate} />;
|
||||
case "Linear":
|
||||
return <LinearNotificationListItem notification={notification} />;
|
||||
return <LinearNotificationListItem notification={notification} mutate={mutate} />;
|
||||
case "GoogleMail":
|
||||
return <GoogleMailNotificationListItem notification={notification} />;
|
||||
return <GoogleMailNotificationListItem notification={notification} mutate={mutate} />;
|
||||
case "Todoist":
|
||||
return <TodoistNotificationListItem notification={notification} />;
|
||||
return <TodoistNotificationListItem notification={notification} mutate={mutate} />;
|
||||
default:
|
||||
return <DefaultNotificationListItem notification={notification} />;
|
||||
return <DefaultNotificationListItem notification={notification} mutate={mutate} />;
|
||||
}
|
||||
}
|
||||
|
||||
function DefaultNotificationListItem({ notification }: NotificationListItemProps) {
|
||||
function DefaultNotificationListItem({ notification, mutate }: NotificationListItemProps) {
|
||||
return (
|
||||
<List.Item
|
||||
key={notification.id}
|
||||
title={notification.title}
|
||||
subtitle={`#${notification.source_id}`}
|
||||
actions={
|
||||
<NotificationActions notification={notification} detailsTarget={<Detail markdown="# To be implemented 👋" />} />
|
||||
<NotificationActions
|
||||
notification={notification}
|
||||
detailsTarget={<Detail markdown="# To be implemented 👋" />}
|
||||
mutate={mutate}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,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;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Icon, Image, List } from "@raycast/api";
|
||||
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 {
|
||||
if (actor) {
|
||||
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),
|
||||
};
|
||||
}
|
||||
@@ -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 { NotificationActions } from "../../../action/NotificationActions";
|
||||
import { GithubDiscussion, GithubDiscussionStateReason } from "../types";
|
||||
import { getGithubActorAccessory } from "../accessories";
|
||||
import { Notification } from "../../../notification";
|
||||
import { Color, Icon, List } from "@raycast/api";
|
||||
import { MutatePromise } from "@raycast/utils";
|
||||
import { Page } from "../../../types";
|
||||
|
||||
interface GithubDiscussionNotificationListItemProps {
|
||||
icon: string;
|
||||
notification: Notification;
|
||||
githubDiscussion: GithubDiscussion;
|
||||
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||
}
|
||||
|
||||
export function GithubDiscussionNotificationListItem({
|
||||
icon,
|
||||
notification,
|
||||
githubDiscussion,
|
||||
mutate,
|
||||
}: GithubDiscussionNotificationListItemProps) {
|
||||
const subtitle = `${githubDiscussion.repository.name_with_owner}`;
|
||||
|
||||
@@ -41,13 +43,14 @@ export function GithubDiscussionNotificationListItem({
|
||||
<List.Item
|
||||
key={notification.id}
|
||||
title={notification.title}
|
||||
icon={icon}
|
||||
icon={{ source: { light: "github-logo-dark.svg", dark: "github-logo-light.svg" } }}
|
||||
subtitle={subtitle}
|
||||
accessories={accessories}
|
||||
actions={
|
||||
<NotificationActions
|
||||
notification={notification}
|
||||
detailsTarget={<GithubDiscussionPreview notification={notification} githubDiscussion={githubDiscussion} />}
|
||||
mutate={mutate}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,3 @@
|
||||
import { Color, Icon, List } from "@raycast/api";
|
||||
import { NotificationActions } from "../../../NotificationActions";
|
||||
import { Notification } from "../../../types";
|
||||
import { GithubPullRequestPreview } from "../preview/GithubPullRequestPreview";
|
||||
import {
|
||||
GithubPullRequestState,
|
||||
GithubPullRequestReviewDecision,
|
||||
@@ -11,18 +7,24 @@ import {
|
||||
GithubCheckStatusState,
|
||||
GithubCheckSuite,
|
||||
} from "../types";
|
||||
import { getGithubActorAccessory } from "../misc";
|
||||
import { GithubPullRequestPreview } from "../preview/GithubPullRequestPreview";
|
||||
import { NotificationActions } from "../../../action/NotificationActions";
|
||||
import { getGithubActorAccessory } from "../accessories";
|
||||
import { Notification } from "../../../notification";
|
||||
import { Color, Icon, List } from "@raycast/api";
|
||||
import { MutatePromise } from "@raycast/utils";
|
||||
import { Page } from "../../../types";
|
||||
|
||||
interface GithubPullRequestNotificationListItemProps {
|
||||
icon: string;
|
||||
notification: Notification;
|
||||
githubPullRequest: GithubPullRequest;
|
||||
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||
}
|
||||
|
||||
export function GithubPullRequestNotificationListItem({
|
||||
icon,
|
||||
notification,
|
||||
githubPullRequest,
|
||||
mutate,
|
||||
}: GithubPullRequestNotificationListItemProps) {
|
||||
const subtitle = `${githubPullRequest.head_repository?.name_with_owner} #${githubPullRequest.number}`;
|
||||
|
||||
@@ -60,13 +62,14 @@ export function GithubPullRequestNotificationListItem({
|
||||
<List.Item
|
||||
key={notification.id}
|
||||
title={notification.title}
|
||||
icon={icon}
|
||||
icon={{ source: { light: "github-logo-dark.svg", dark: "github-logo-light.svg" } }}
|
||||
accessories={accessories}
|
||||
subtitle={subtitle}
|
||||
actions={
|
||||
<NotificationActions
|
||||
notification={notification}
|
||||
detailsTarget={<GithubPullRequestPreview notification={notification} githubPullRequest={githubPullRequest} />}
|
||||
mutate={mutate}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -82,7 +85,7 @@ function getGithubPullRequestChecksAccessory(latestCommit: GithubCommitChecks):
|
||||
case GithubCheckStatusState.Pending:
|
||||
return { icon: Icon.Pause, tooltip: "Pending" };
|
||||
case GithubCheckStatusState.InProgress:
|
||||
return { icon: Icon.Pause, tooltip: "In progress" }; // TODO Spinner
|
||||
return { icon: Icon.CircleProgress, tooltip: "In progress" };
|
||||
case GithubCheckStatusState.Completed:
|
||||
switch (progress.conclusion()) {
|
||||
case GithubCheckConclusionState.Success:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Notification, getNotificationHtmlUrl } from "../../../notification";
|
||||
import { Detail, ActionPanel, Action } from "@raycast/api";
|
||||
import { Notification } from "../../../types";
|
||||
import { GithubDiscussion } from "../types";
|
||||
import { useMemo } from "react";
|
||||
import { getNotificationHtmlUrl } from "../../../notification";
|
||||
|
||||
interface GithubDiscussionPreviewProps {
|
||||
notification: Notification;
|
||||
@@ -10,7 +9,7 @@ interface GithubDiscussionPreviewProps {
|
||||
}
|
||||
|
||||
export function GithubDiscussionPreview({ notification, githubDiscussion }: GithubDiscussionPreviewProps) {
|
||||
const notification_html_url = useMemo(() => {
|
||||
const notificationHtmlUrl = useMemo(() => {
|
||||
return getNotificationHtmlUrl(notification);
|
||||
}, [notification]);
|
||||
|
||||
@@ -19,7 +18,7 @@ export function GithubDiscussionPreview({ notification, githubDiscussion }: Gith
|
||||
markdown={`# ${githubDiscussion.title}`}
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<Action.OpenInBrowser url={notification_html_url} />
|
||||
<Action.OpenInBrowser url={notificationHtmlUrl} />
|
||||
</ActionPanel>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Notification, getNotificationHtmlUrl } from "../../../notification";
|
||||
import { Detail, ActionPanel, Action } from "@raycast/api";
|
||||
import { Notification } from "../../../types";
|
||||
import { GithubPullRequest } from "../types";
|
||||
import { useMemo } from "react";
|
||||
import { getNotificationHtmlUrl } from "../../../notification";
|
||||
|
||||
interface GithubPullRequestPreviewProps {
|
||||
notification: Notification;
|
||||
@@ -10,7 +9,7 @@ interface GithubPullRequestPreviewProps {
|
||||
}
|
||||
|
||||
export function GithubPullRequestPreview({ notification, githubPullRequest }: GithubPullRequestPreviewProps) {
|
||||
const notification_html_url = useMemo(() => {
|
||||
const notificationHtmlUrl = useMemo(() => {
|
||||
return getNotificationHtmlUrl(notification);
|
||||
}, [notification]);
|
||||
|
||||
@@ -19,7 +18,7 @@ export function GithubPullRequestPreview({ notification, githubPullRequest }: Gi
|
||||
markdown={`# ${githubPullRequest.title}`}
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<Action.OpenInBrowser url={notification_html_url} />
|
||||
<Action.OpenInBrowser url={notificationHtmlUrl} />
|
||||
</ActionPanel>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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 👋" />} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
27
src/integrations/google-mail/types.ts
Normal 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;
|
||||
}
|
||||
@@ -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 👋" />} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
13
src/integrations/linear/accessories.ts
Normal 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" };
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
26
src/integrations/linear/preview/LinearIssuePreview.tsx
Normal 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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
26
src/integrations/linear/preview/LinearProjectPreview.tsx
Normal 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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
120
src/integrations/linear/types.ts
Normal 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;
|
||||
}
|
||||
@@ -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 👋" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
24
src/integrations/todoist/preview/TodoistTaskPreview.tsx
Normal 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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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) {
|
||||
switch (notification.details?.type) {
|
||||
case "GithubPullRequest":
|
||||
return notification.details.content.url;
|
||||
case "GithubDiscussion":
|
||||
return notification.details.content.url;
|
||||
default: {
|
||||
// TODO
|
||||
return "https://github.com";
|
||||
}
|
||||
}
|
||||
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.metadata.type === "Todoist";
|
||||
}
|
||||
|
||||
41
src/task.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export interface Task {
|
||||
id: string;
|
||||
source_id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
status: TaskStatus;
|
||||
completed_at?: Date;
|
||||
priority: TaskPriority;
|
||||
due_at?: DueDate;
|
||||
tags: Array<string>;
|
||||
parent_id?: string;
|
||||
project: string;
|
||||
is_recurring: boolean;
|
||||
created_at: Date;
|
||||
metadata: TaskMetadata;
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
export type TaskMetadata = {
|
||||
type: "Todoist";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
content: any;
|
||||
};
|
||||
|
||||
export enum TaskStatus {
|
||||
Active = "Active",
|
||||
Done = "Done",
|
||||
Deleted = "Deleted",
|
||||
}
|
||||
|
||||
export enum TaskPriority {
|
||||
P1 = 1,
|
||||
P2 = 2,
|
||||
P3 = 3,
|
||||
P4 = 4,
|
||||
}
|
||||
|
||||
export type DueDate =
|
||||
| { type: "Date"; content: Date }
|
||||
| { type: "DateTime"; content: Date }
|
||||
| { type: "DateTimeWithTz"; content: Date };
|
||||
37
src/types.ts
@@ -1,5 +1,3 @@
|
||||
import { GithubDiscussion, GithubPullRequest } from "./integrations/github/types";
|
||||
|
||||
export interface UniversalInboxPreferences {
|
||||
apiKey: string;
|
||||
universalInboxBaseUrl: string;
|
||||
@@ -11,38 +9,3 @@ export interface Page<T> {
|
||||
total: number;
|
||||
content: Array<T>;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
title: string;
|
||||
source_id: string;
|
||||
status: NotificationStatus;
|
||||
metadata: NotificationMetadata;
|
||||
updated_at: Date;
|
||||
last_read_at?: Date;
|
||||
snoozed_until?: Date;
|
||||
user_id: string;
|
||||
task_id?: string;
|
||||
details?: NotificationDetails;
|
||||
}
|
||||
|
||||
export type NotificationMetadata = {
|
||||
type: "Github" | "Linear" | "GoogleMail" | "Todoist";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
content: any;
|
||||
};
|
||||
|
||||
export type NotificationDetails =
|
||||
| { type: "GithubPullRequest"; content: GithubPullRequest }
|
||||
| { type: "GithubDiscussion"; content: GithubDiscussion };
|
||||
|
||||
export enum NotificationStatus {
|
||||
Unread = "Unread",
|
||||
Read = "Read",
|
||||
Deleted = "Deleted",
|
||||
Unsubscribed = "Unsubscribed",
|
||||
}
|
||||
|
||||
export type NotificationListItemProps = {
|
||||
notification: Notification;
|
||||
};
|
||||
|
||||