Compare commits
17 Commits
2d2d47f55f
...
contributi
| Author | SHA1 | Date | |
|---|---|---|---|
|
98cbbf4814
|
|||
|
a8cf51ec4a
|
|||
|
4afa06a1c8
|
|||
|
8d49154be3
|
|||
|
751ee38d32
|
|||
|
8a7eda4f3c
|
|||
|
1084559688
|
|||
|
84f4d0bd53
|
|||
|
a17b259e50
|
|||
|
ad43bf3c86
|
|||
|
cfc0f0ed5e
|
|||
|
9fb5af87ed
|
|||
|
26f346f8b8
|
|||
|
ea40258f48
|
|||
|
e0f90b0c42
|
|||
|
3df827ebd7
|
|||
|
0507722ecf
|
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"printWidth": 120,
|
"printWidth": 120,
|
||||||
"singleQuote": false
|
"singleQuote": false,
|
||||||
|
"plugins": ["./node_modules/prettier-plugin-sort-imports/dist/index.js"]
|
||||||
}
|
}
|
||||||
57
CHANGELOG.md
@@ -1,3 +1,56 @@
|
|||||||
# Universal Inbox Changelog
|
# Universal Inbox extension for Raycast Changelog
|
||||||
|
|
||||||
## [Initial Version] - 2024-01-19
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [0.1.4] - 2024-03-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add Linear Project and Team icons
|
||||||
|
- Add Slack notifications (ie. save for later) support
|
||||||
|
- Set `Inbox` project as default when creating or planning a task
|
||||||
|
|
||||||
|
## [0.1.3] - 2024-02-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Display Linear notification reason
|
||||||
|
- Display Linear project on notification item
|
||||||
|
- Add missing action icons
|
||||||
|
|
||||||
|
## [0.1.2] - 2024-02-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add "Plan task" action for notification created from task
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix "Complete task" action
|
||||||
|
|
||||||
|
## [0.1.1] - 2024-02-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add filter per notification kind
|
||||||
|
|
||||||
|
## [0.1.0] - 2024-01-29
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Support listing notifications from
|
||||||
|
- Github Pull Requests
|
||||||
|
- Github Discussions
|
||||||
|
- Linear Issues
|
||||||
|
- Linear Projects
|
||||||
|
- Google Mail
|
||||||
|
- Todoist tasks
|
||||||
|
- Act on notifications
|
||||||
|
- Open in Browser
|
||||||
|
- Delete notification
|
||||||
|
- Unsubscribe from notification
|
||||||
|
- Snooze notification
|
||||||
|
- Create a task from notification
|
||||||
|
- Link notification to an existing task
|
||||||
|
- Act on tasks in the notification list
|
||||||
|
- Complete task
|
||||||
|
|||||||
17
README.md
@@ -1,3 +1,18 @@
|
|||||||
# Universal Inbox
|
# Universal Inbox
|
||||||
|
|
||||||
Manage Universal Inbox notifications from Raycast
|
Manage your notifications in a single [Universal Inbox](https://www.universal-inbox.com)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
To use the Universal Inbox extension for Raycast, you will need to configure an instance URL and an API key.
|
||||||
|
|
||||||
|
### Instance URL
|
||||||
|
|
||||||
|
The default instance URL is: https://app.universal-inbox.com
|
||||||
|
Get the URL from the Universal Inbox instance you are usually connecting to.
|
||||||
|
|
||||||
|
### API Key
|
||||||
|
|
||||||
|
You can get an API Key from your Universal Inbox user profile page:
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
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 |
3
assets/slack-logo-dark.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3.362 10.11c0 .926-.756 1.681-1.681 1.681S0 11.036 0 10.111C0 9.186.756 8.43 1.68 8.43h1.682v1.68zm.846 0c0-.924.756-1.68 1.681-1.68s1.681.756 1.681 1.68v4.21c0 .924-.756 1.68-1.68 1.68a1.685 1.685 0 0 1-1.682-1.68v-4.21zM5.89 3.362c-.926 0-1.682-.756-1.682-1.681S4.964 0 5.89 0s1.68.756 1.68 1.68v1.682H5.89zm0 .846c.924 0 1.68.756 1.68 1.681S6.814 7.57 5.89 7.57H1.68C.757 7.57 0 6.814 0 5.89c0-.926.756-1.682 1.68-1.682h4.21zm6.749 1.682c0-.926.755-1.682 1.68-1.682.925 0 1.681.756 1.681 1.681s-.756 1.681-1.68 1.681h-1.681V5.89zm-.848 0c0 .924-.755 1.68-1.68 1.68A1.685 1.685 0 0 1 8.43 5.89V1.68C8.43.757 9.186 0 10.11 0c.926 0 1.681.756 1.681 1.68v4.21zm-1.681 6.748c.926 0 1.682.756 1.682 1.681S11.036 16 10.11 16s-1.681-.756-1.681-1.68v-1.682h1.68zm0-.847c-.924 0-1.68-.755-1.68-1.68 0-.925.756-1.681 1.68-1.681h4.21c.924 0 1.68.756 1.68 1.68 0 .926-.756 1.681-1.68 1.681h-4.21z"></path>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
2
assets/slack-logo-light.svg
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path d="M3.362 10.11c0 .926-.756 1.681-1.681 1.681S0 11.036 0 10.111C0 9.186.756 8.43 1.68 8.43h1.682v1.68zm.846 0c0-.924.756-1.68 1.681-1.68s1.681.756 1.681 1.68v4.21c0 .924-.756 1.68-1.68 1.68a1.685 1.685 0 0 1-1.682-1.68v-4.21zM5.89 3.362c-.926 0-1.682-.756-1.682-1.681S4.964 0 5.89 0s1.68.756 1.68 1.68v1.682H5.89zm0 .846c.924 0 1.68.756 1.68 1.681S6.814 7.57 5.89 7.57H1.68C.757 7.57 0 6.814 0 5.89c0-.926.756-1.682 1.68-1.682h4.21zm6.749 1.682c0-.926.755-1.682 1.68-1.682.925 0 1.681.756 1.681 1.681s-.756 1.681-1.68 1.681h-1.681V5.89zm-.848 0c0 .924-.755 1.68-1.68 1.68A1.685 1.685 0 0 1 8.43 5.89V1.68C8.43.757 9.186 0 10.11 0c.926 0 1.681.756 1.681 1.68v4.21zm-1.681 6.748c.926 0 1.682.756 1.682 1.681S11.036 16 10.11 16s-1.681-.756-1.681-1.68v-1.682h1.68zm0-.847c-.924 0-1.68-.755-1.68-1.68 0-.925.756-1.681 1.68-1.681h4.21c.924 0 1.68.756 1.68 1.68 0 .926-.756 1.681-1.68 1.681h-4.21z"></path></svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 28 KiB |
8
justfile
@@ -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
|
||||||
@@ -16,3 +20,7 @@ fix:
|
|||||||
# Run extension in development mode
|
# Run extension in development mode
|
||||||
run:
|
run:
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
|
# Publish a new version of the extension
|
||||||
|
publish:
|
||||||
|
npm run publish
|
||||||
|
|||||||
BIN
media/ui-user-profile-page.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
metadata/universal-inbox-1.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
metadata/universal-inbox-2.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
metadata/universal-inbox-3.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
1944
package-lock.json
generated
20
package.json
@@ -2,12 +2,11 @@
|
|||||||
"$schema": "https://www.raycast.com/schemas/extension.json",
|
"$schema": "https://www.raycast.com/schemas/extension.json",
|
||||||
"name": "universal-inbox",
|
"name": "universal-inbox",
|
||||||
"title": "Universal Inbox",
|
"title": "Universal Inbox",
|
||||||
"description": "Manage Universal Inbox notifications from Raycast",
|
"description": "Manage your notifications in a single Universal Inbox",
|
||||||
"icon": "ui-logo-transparent.png",
|
"icon": "ui-logo-transparent.png",
|
||||||
"author": "david_rousselie",
|
"author": "dax42",
|
||||||
"owner": "universal-inbox",
|
"version": "0.1.4",
|
||||||
"categories": [
|
"categories": [
|
||||||
"Applications",
|
|
||||||
"Productivity"
|
"Productivity"
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -40,18 +39,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": "^7.2.0",
|
||||||
"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",
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,63 +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 completeTask(notification: Notification) {
|
|
||||||
console.log(`Completing task ${notification.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NotificationTaskActions({
|
|
||||||
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="Complete Task"
|
|
||||||
icon={Icon.Calendar}
|
|
||||||
shortcut={{ modifiers: ["ctrl"], key: "c" }}
|
|
||||||
onAction={() => completeTask(notification)}
|
|
||||||
/>
|
|
||||||
</ActionPanel>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
153
src/action/CreateTaskFromNotification.tsx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
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,
|
||||||
|
project: projects?.find((p) => p.name === "Inbox")?.source_id,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src/action/LinkNotificationToTask.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
208
src/action/NotificationActions.tsx
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
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" icon={Icon.AppWindowSidebarRight} 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: "t" }}
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteNotification(
|
||||||
|
notification: Notification,
|
||||||
|
mutate: MutatePromise<Page<Notification> | undefined>,
|
||||||
|
) {
|
||||||
|
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||||
|
const toast = await showToast({ style: Toast.Style.Animated, title: "Deleting notification" });
|
||||||
|
try {
|
||||||
|
if (isNotificationBuiltFromTask(notification) && notification.task) {
|
||||||
|
await mutate(
|
||||||
|
handleErrors(
|
||||||
|
fetch(`${preferences.universalInboxBaseUrl}/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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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();
|
||||||
|
}
|
||||||
98
src/action/NotificationTaskActions.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { deleteNotification, snoozeNotification, unsubscribeFromNotification } from "./NotificationActions";
|
||||||
|
import { Notification, getNotificationHtmlUrl, isNotificationBuiltFromTask } from "../notification";
|
||||||
|
import { Action, ActionPanel, Icon, Toast, getPreferenceValues, showToast } from "@raycast/api";
|
||||||
|
import { Page, UniversalInboxPreferences } from "../types";
|
||||||
|
import { MutatePromise } from "@raycast/utils";
|
||||||
|
import { useMemo, ReactElement } from "react";
|
||||||
|
import { PlanTask } from "./PlanTask";
|
||||||
|
import { handleErrors } from "../api";
|
||||||
|
import { TaskStatus } from "../task";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
|
||||||
|
interface NotificationTaskActionsProps {
|
||||||
|
notification: Notification;
|
||||||
|
detailsTarget: ReactElement;
|
||||||
|
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotificationTaskActions({ notification, detailsTarget, mutate }: NotificationTaskActionsProps) {
|
||||||
|
const notificationHtmlUrl = useMemo(() => {
|
||||||
|
return getNotificationHtmlUrl(notification);
|
||||||
|
}, [notification]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionPanel>
|
||||||
|
<Action.OpenInBrowser url={notificationHtmlUrl} />
|
||||||
|
<Action.Push title="Show Details" icon={Icon.AppWindowSidebarRight} 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
|
||||||
|
title="Complete Task"
|
||||||
|
icon={Icon.CheckCircle}
|
||||||
|
shortcut={{ modifiers: ["ctrl"], key: "c" }}
|
||||||
|
onAction={() => completeTask(notification, mutate)}
|
||||||
|
/>
|
||||||
|
<Action.Push
|
||||||
|
title="Plan Task..."
|
||||||
|
icon={Icon.Calendar}
|
||||||
|
shortcut={{ modifiers: ["ctrl"], key: "t" }}
|
||||||
|
target={<PlanTask notification={notification} mutate={mutate} />}
|
||||||
|
/>
|
||||||
|
</ActionPanel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeTask(notification: Notification, mutate: MutatePromise<Page<Notification> | undefined>) {
|
||||||
|
if (!isNotificationBuiltFromTask(notification) || !notification.task) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||||
|
const toast = await showToast({ style: Toast.Style.Animated, title: "Marking task as Done" });
|
||||||
|
try {
|
||||||
|
await mutate(
|
||||||
|
handleErrors(
|
||||||
|
fetch(`${preferences.universalInboxBaseUrl}/api/tasks/${notification.task.id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ status: TaskStatus.Done }),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${preferences.apiKey}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
optimisticUpdate(page) {
|
||||||
|
if (page) {
|
||||||
|
page.content = page.content.filter((n) => n.id !== notification.id);
|
||||||
|
}
|
||||||
|
return page;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
toast.style = Toast.Style.Success;
|
||||||
|
toast.title = "Task successfully marked as Done";
|
||||||
|
} catch (error) {
|
||||||
|
toast.style = Toast.Style.Failure;
|
||||||
|
toast.title = "Failed to mark task as Done";
|
||||||
|
toast.message = (error as Error).message;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
156
src/action/PlanTask.tsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { Action, useNavigation, ActionPanel, Form, Icon, getPreferenceValues, showToast, Toast } from "@raycast/api";
|
||||||
|
import { MutatePromise, useForm, FormValidation, useFetch } from "@raycast/utils";
|
||||||
|
import { Notification, isNotificationBuiltFromTask } from "../notification";
|
||||||
|
import { Page, UniversalInboxPreferences } from "../types";
|
||||||
|
import { default as dayjs, extend } from "dayjs";
|
||||||
|
import { TaskPriority } from "../task";
|
||||||
|
import { handleErrors } from "../api";
|
||||||
|
import utc from "dayjs/plugin/utc";
|
||||||
|
import { useState } from "react";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
|
||||||
|
extend(utc);
|
||||||
|
|
||||||
|
interface PlanTaskProps {
|
||||||
|
notification: Notification;
|
||||||
|
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TaskPlanningFormValues {
|
||||||
|
project: string;
|
||||||
|
dueAt: Date | null;
|
||||||
|
priority: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectSummary {
|
||||||
|
name: string;
|
||||||
|
source_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TaskPlanning {
|
||||||
|
project: ProjectSummary;
|
||||||
|
due_at?: { type: "DateTimeWithTz"; content: string };
|
||||||
|
priority: TaskPriority;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PlanTask({ notification, mutate }: PlanTaskProps) {
|
||||||
|
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||||
|
const { pop } = useNavigation();
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
|
||||||
|
const { isLoading, data: projects } = useFetch<Array<ProjectSummary>>(
|
||||||
|
`${preferences.universalInboxBaseUrl}/api/tasks/projects/search?matches=${searchText}`,
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${preferences.apiKey}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { handleSubmit, itemProps } = useForm<TaskPlanningFormValues>({
|
||||||
|
initialValues: {
|
||||||
|
dueAt: new Date(),
|
||||||
|
project: projects?.find((p) => p.name === "Inbox")?.source_id,
|
||||||
|
priority: `${TaskPriority.P4 as number}`,
|
||||||
|
},
|
||||||
|
async onSubmit(values) {
|
||||||
|
const project = projects?.find((p) => p.source_id === values.project);
|
||||||
|
if (!project) {
|
||||||
|
throw new Error("Project not found");
|
||||||
|
}
|
||||||
|
const taskPlanning: TaskPlanning = {
|
||||||
|
project: project,
|
||||||
|
due_at: values.dueAt ? { type: "DateTimeWithTz", content: dayjs(values.dueAt).utc().format() } : undefined,
|
||||||
|
priority: parseInt(values.priority) as TaskPriority,
|
||||||
|
};
|
||||||
|
await planTask(taskPlanning, notification, mutate);
|
||||||
|
pop();
|
||||||
|
},
|
||||||
|
validation: {
|
||||||
|
project: FormValidation.Required,
|
||||||
|
priority: FormValidation.Required,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form
|
||||||
|
navigationTitle="Plan task"
|
||||||
|
actions={
|
||||||
|
<ActionPanel>
|
||||||
|
<Action.SubmitForm title="Plan Task" icon={Icon.Calendar} onSubmit={handleSubmit} />
|
||||||
|
</ActionPanel>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Form.Description title="Task title" text={notification.title} />
|
||||||
|
<Form.Dropdown
|
||||||
|
title="Project"
|
||||||
|
placeholder="Search project..."
|
||||||
|
filtering={true}
|
||||||
|
throttle={true}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onSearchTextChange={setSearchText}
|
||||||
|
{...itemProps.project}
|
||||||
|
>
|
||||||
|
<Form.Dropdown.Item value="" title="" key={0} />
|
||||||
|
{projects?.map((project) => {
|
||||||
|
return <Form.Dropdown.Item title={project.name} value={project.source_id} key={project.source_id} />;
|
||||||
|
})}
|
||||||
|
</Form.Dropdown>
|
||||||
|
<Form.DatePicker title="Due at" min={new Date()} type={Form.DatePicker.Type.Date} {...itemProps.dueAt} />
|
||||||
|
<Form.Dropdown title="Priority" {...itemProps.priority}>
|
||||||
|
<Form.Dropdown.Item title="Priority 1" value={`${TaskPriority.P1 as number}`} key={TaskPriority.P1} />
|
||||||
|
<Form.Dropdown.Item title="Priority 2" value={`${TaskPriority.P2 as number}`} key={TaskPriority.P2} />
|
||||||
|
<Form.Dropdown.Item title="Priority 3" value={`${TaskPriority.P3 as number}`} key={TaskPriority.P3} />
|
||||||
|
<Form.Dropdown.Item title="Priority 4" value={`${TaskPriority.P4 as number}`} key={TaskPriority.P4} />
|
||||||
|
</Form.Dropdown>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function planTask(
|
||||||
|
taskPlanning: TaskPlanning,
|
||||||
|
notification: Notification,
|
||||||
|
mutate: MutatePromise<Page<Notification> | undefined>,
|
||||||
|
) {
|
||||||
|
if (!isNotificationBuiltFromTask(notification) || !notification.task) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||||
|
const toast = await showToast({ style: Toast.Style.Animated, title: "Planning task" });
|
||||||
|
try {
|
||||||
|
await mutate(
|
||||||
|
handleErrors(
|
||||||
|
fetch(`${preferences.universalInboxBaseUrl}/api/tasks/${notification.task.id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({
|
||||||
|
project: taskPlanning.project.name,
|
||||||
|
due_at: taskPlanning.due_at,
|
||||||
|
priority: taskPlanning.priority,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${preferences.apiKey}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
optimisticUpdate(page) {
|
||||||
|
if (page) {
|
||||||
|
page.content = page.content.filter((n) => n.id !== notification.id);
|
||||||
|
}
|
||||||
|
return page;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
toast.style = Toast.Style.Success;
|
||||||
|
toast.title = "Task successfully planned";
|
||||||
|
} catch (error) {
|
||||||
|
toast.style = Toast.Style.Failure;
|
||||||
|
toast.title = "Failed to plan task";
|
||||||
|
toast.message = (error as Error).message;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
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,35 +1,41 @@
|
|||||||
import { Action, ActionPanel, Detail, List, getPreferenceValues, openExtensionPreferences } from "@raycast/api";
|
import { Action, ActionPanel, Detail, Icon, List, getPreferenceValues, openExtensionPreferences } from "@raycast/api";
|
||||||
|
import { GoogleMailNotificationListItem } from "./integrations/google-mail/listitem/GoogleMailNotificationListItem";
|
||||||
|
import { TodoistNotificationListItem } from "./integrations/todoist/listitem/TodoistNotificationListItem";
|
||||||
|
import { GithubNotificationListItem } from "./integrations/github/listitem/GithubNotificationListItem";
|
||||||
|
import { LinearNotificationListItem } from "./integrations/linear/listitem/LinearNotificationListItem";
|
||||||
|
import { SlackNotificationListItem } from "./integrations/slack/listitem/SlackNotificationListItem";
|
||||||
|
import { Notification, NotificationListItemProps } 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 { useState } from "react";
|
||||||
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
|
||||||
markdown={"API key incorrect. Please update it in extension preferences and try again."}
|
markdown={"API key incorrect. Please update it in extension preferences and try again."}
|
||||||
actions={
|
actions={
|
||||||
<ActionPanel>
|
<ActionPanel>
|
||||||
<Action title="Open Extension Preferences" onAction={openExtensionPreferences} />
|
<Action title="Open Extension Preferences" icon={Icon.Gear} onAction={openExtensionPreferences} />
|
||||||
</ActionPanel>
|
</ActionPanel>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isLoading, data } = useFetch<Page<Notification>>(
|
const [notificationKind, setNotificationKind] = useState("");
|
||||||
`${preferences.universalInboxBaseUrl}/api/notifications?status=Unread,Read&with_tasks=true`,
|
const { isLoading, data, mutate } = useFetch<Page<Notification>>(
|
||||||
|
`${preferences.universalInboxBaseUrl}/api/notifications?status=Unread,Read&with_tasks=true${
|
||||||
|
notificationKind ? "¬ification_kind=" + notificationKind : ""
|
||||||
|
}`,
|
||||||
{
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${preferences.apiKey}`,
|
Authorization: `Bearer ${preferences.apiKey}`,
|
||||||
@@ -38,38 +44,78 @@ export default function Command() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List isLoading={isLoading}>
|
<List
|
||||||
{data?.content.map((notification: Notification) => {
|
isLoading={isLoading}
|
||||||
return <NotificationListItem key={notification.id} notification={notification} />;
|
searchBarPlaceholder="Filter notifications..."
|
||||||
})}
|
searchBarAccessory={
|
||||||
|
<NotificationKindDropdown value={notificationKind} onNotificationKindChange={setNotificationKind} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data?.content.length === 0 ? (
|
||||||
|
<List.EmptyView
|
||||||
|
icon={{ source: "ui-logo-transparent.png" }}
|
||||||
|
title="Congrats! You have reach zero inbox 🎉"
|
||||||
|
description="You don't have any new notifications."
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
data?.content.map((notification: Notification) => {
|
||||||
|
return <NotificationListItem key={notification.id} notification={notification} mutate={mutate} />;
|
||||||
|
})
|
||||||
|
)}
|
||||||
</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 "Slack":
|
||||||
|
return <SlackNotificationListItem 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}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NotificationKindDropdownProps {
|
||||||
|
value: string;
|
||||||
|
onNotificationKindChange: (newValue: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotificationKindDropdown({ value, onNotificationKindChange }: NotificationKindDropdownProps) {
|
||||||
|
return (
|
||||||
|
<List.Dropdown tooltip="Select Notification Kind" value={value} onChange={onNotificationKindChange}>
|
||||||
|
<List.Dropdown.Section title="Notification kind">
|
||||||
|
<List.Dropdown.Item key="0" title="" value="" />
|
||||||
|
<List.Dropdown.Item key="Github" title="Github" value="Github" />
|
||||||
|
<List.Dropdown.Item key="Linear" title="Linear" value="Linear" />
|
||||||
|
<List.Dropdown.Item key="GoogleMail" title="Google Mail" value="GoogleMail" />
|
||||||
|
<List.Dropdown.Item key="Slack" title="Slack" value="Slack" />
|
||||||
|
<List.Dropdown.Item key="Todoist" title="Todoist" value="Todoist" />
|
||||||
|
</List.Dropdown.Section>
|
||||||
|
</List.Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 { 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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 {
|
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:
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 👋" />} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
37
src/integrations/linear/accessories.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Icon, Image, List } from "@raycast/api";
|
||||||
|
import { getAvatarIcon } from "@raycast/utils";
|
||||||
|
import { LinearUser } from "./types";
|
||||||
|
import { match } from "ts-pattern";
|
||||||
|
|
||||||
|
export function getLinearUserAccessory(user?: LinearUser): List.Item.Accessory {
|
||||||
|
if (user) {
|
||||||
|
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" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLinearNotificationReasonAccessory(notification_type: string): List.Item.Accessory {
|
||||||
|
const reason = match(notification_type)
|
||||||
|
.with("issueAddedToTriage", () => "Added To Triage")
|
||||||
|
.with("issueAddedToView", () => "Added To View")
|
||||||
|
.with("issueAssignedToYou", () => "Assigned To You")
|
||||||
|
.with("issueBlocking", () => "Blocked")
|
||||||
|
.with("issueCommentMention", () => "Comment Mention")
|
||||||
|
.with("issueCommentReaction", () => "Comment Reaction")
|
||||||
|
.with("issueCreated", () => "Created")
|
||||||
|
.with("issueDue", () => "Due")
|
||||||
|
.with("issueEmojiReaction", () => "Reaction")
|
||||||
|
.with("issueMention", () => "Mention")
|
||||||
|
.with("issueNewComment", () => "New Comment")
|
||||||
|
.with("issueStatusChanged", () => "Status Changed")
|
||||||
|
.with("issueUnassignedFromYou", () => "Unassigned From You")
|
||||||
|
.with("projectAddedAsLead", () => "Added As Lead")
|
||||||
|
.with("projectAddedAsMember", () => "Added As Member")
|
||||||
|
.with("projectUpdateCreated", () => "Update Created")
|
||||||
|
.with("projectUpdateMentionPrompt", () => "Update Mention")
|
||||||
|
.otherwise(() => notification_type);
|
||||||
|
return { tag: { value: reason } };
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { LinearWorkflowStateType, LinearIssueNotification, LinearWorkflowState } from "../types";
|
||||||
|
import { getLinearNotificationReasonAccessory, getLinearUserAccessory } from "../accessories";
|
||||||
|
import { NotificationActions } from "../../../action/NotificationActions";
|
||||||
|
import { LinearIssuePreview } from "../preview/LinearIssuePreview";
|
||||||
|
import { Notification } from "../../../notification";
|
||||||
|
import { MutatePromise } from "@raycast/utils";
|
||||||
|
import { Page } from "../../../types";
|
||||||
|
import { match, P } from "ts-pattern";
|
||||||
|
import { List } from "@raycast/api";
|
||||||
|
|
||||||
|
interface LinearIssueNotificationListItemProps {
|
||||||
|
notification: Notification;
|
||||||
|
linearIssueNotification: LinearIssueNotification;
|
||||||
|
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LinearIssueNotificationListItem({
|
||||||
|
notification,
|
||||||
|
linearIssueNotification,
|
||||||
|
mutate,
|
||||||
|
}: LinearIssueNotificationListItemProps) {
|
||||||
|
const projectSubtitle = match(linearIssueNotification.issue.project)
|
||||||
|
.with({ name: P.select(), icon: P.nullish }, (project_name) => `/ ${project_name}`)
|
||||||
|
.with(
|
||||||
|
{ name: P.select("project_name"), icon: P.select("icon") },
|
||||||
|
({ project_name, icon }) => `/ ${icon} ${project_name}`,
|
||||||
|
)
|
||||||
|
.otherwise(() => "");
|
||||||
|
const teamSubtitle = match(linearIssueNotification.issue.team)
|
||||||
|
.with({ name: P.select(), icon: P.nullish }, (team_name) => `${team_name}`)
|
||||||
|
.with({ name: P.select("team_name"), icon: P.select("icon") }, ({ team_name, icon }) => `${icon} ${team_name}`)
|
||||||
|
.otherwise(() => "");
|
||||||
|
const subtitle = `${teamSubtitle} ${projectSubtitle} #${linearIssueNotification.issue.identifier}`;
|
||||||
|
|
||||||
|
const state = getLinearIssueStateAccessory(linearIssueNotification.issue.state);
|
||||||
|
const assignee = getLinearUserAccessory(linearIssueNotification.issue.assignee);
|
||||||
|
const reason = getLinearNotificationReasonAccessory(linearIssueNotification.type);
|
||||||
|
|
||||||
|
const accessories: List.Item.Accessory[] = [
|
||||||
|
reason,
|
||||||
|
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,88 @@
|
|||||||
|
import { getLinearNotificationReasonAccessory, getLinearUserAccessory } from "../accessories";
|
||||||
|
import { LinearProjectNotification, LinearProjectState, LinearProject } from "../types";
|
||||||
|
import { NotificationActions } from "../../../action/NotificationActions";
|
||||||
|
import { LinearProjectPreview } from "../preview/LinearProjectPreview";
|
||||||
|
import { Notification } from "../../../notification";
|
||||||
|
import { MutatePromise } from "@raycast/utils";
|
||||||
|
import { List, Color } from "@raycast/api";
|
||||||
|
import { Page } from "../../../types";
|
||||||
|
import { match, P } from "ts-pattern";
|
||||||
|
|
||||||
|
interface LinearProjectNotificationListItemProps {
|
||||||
|
notification: Notification;
|
||||||
|
linearProjectNotification: LinearProjectNotification;
|
||||||
|
mutate: MutatePromise<Page<Notification> | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LinearProjectNotificationListItem({
|
||||||
|
notification,
|
||||||
|
linearProjectNotification,
|
||||||
|
mutate,
|
||||||
|
}: LinearProjectNotificationListItemProps) {
|
||||||
|
const subtitle = match(linearProjectNotification.project)
|
||||||
|
.with({ name: P.select(), icon: P.nullish }, (project_name) => `${project_name}`)
|
||||||
|
.with(
|
||||||
|
{ name: P.select("project_name"), icon: P.select("icon") },
|
||||||
|
({ project_name, icon }) => `${icon} ${project_name}`,
|
||||||
|
)
|
||||||
|
.otherwise(() => "");
|
||||||
|
|
||||||
|
const state = getLinearProjectStateAccessory(linearProjectNotification.project);
|
||||||
|
const lead = getLinearUserAccessory(linearProjectNotification.project.lead);
|
||||||
|
const reason = getLinearNotificationReasonAccessory(linearProjectNotification.type);
|
||||||
|
|
||||||
|
const accessories: List.Item.Accessory[] = [
|
||||||
|
reason,
|
||||||
|
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;
|
||||||
|
}
|
||||||
147
src/integrations/slack/listitem/SlackNotificationListItem.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { NotificationDetails, NotificationListItemProps } from "../../../notification";
|
||||||
|
import { NotificationActions } from "../../../action/NotificationActions";
|
||||||
|
import { SlackStarPreview } from "../preview/SlackStarPreview";
|
||||||
|
import { SlackBotInfo, SlackIcon, SlackUser } from "../types";
|
||||||
|
/* import { NotificationActions } from "../../../action/NotificationActions"; */
|
||||||
|
import { Icon, Image, List } from "@raycast/api";
|
||||||
|
import { getAvatarIcon } from "@raycast/utils";
|
||||||
|
import { match, P } from "ts-pattern";
|
||||||
|
|
||||||
|
export function SlackNotificationListItem({ notification, mutate }: NotificationListItemProps) {
|
||||||
|
const subtitle = getSlackNotificationSubtitle(notification.details);
|
||||||
|
|
||||||
|
const author = getSlackAuthorAccessory(notification.details);
|
||||||
|
const team = getSlackTeamAccessory(notification.details);
|
||||||
|
const updated_at = "2023-01-01"; // TODO
|
||||||
|
|
||||||
|
const accessories: List.Item.Accessory[] = [{ date: new Date(updated_at), tooltip: `Updated at ${updated_at}` }];
|
||||||
|
|
||||||
|
if (author) {
|
||||||
|
accessories.unshift(author);
|
||||||
|
}
|
||||||
|
if (team) {
|
||||||
|
accessories.unshift(team);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
key={notification.id}
|
||||||
|
title={notification.title}
|
||||||
|
icon={{ source: { light: "slack-logo-dark.svg", dark: "slack-logo-light.svg" } }}
|
||||||
|
subtitle={subtitle}
|
||||||
|
accessories={accessories}
|
||||||
|
actions={
|
||||||
|
<NotificationActions
|
||||||
|
notification={notification}
|
||||||
|
detailsTarget={<SlackStarPreview notification={notification} />}
|
||||||
|
mutate={mutate}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSlackNotificationSubtitle(notificationDetails?: NotificationDetails): string {
|
||||||
|
return match(notificationDetails)
|
||||||
|
.with(
|
||||||
|
{
|
||||||
|
type: P.union("SlackMessage", "SlackFile", "SlackFileComment", "SlackChannel", "SlackIm", "SlackGroup"),
|
||||||
|
content: P.select(),
|
||||||
|
},
|
||||||
|
(slackNotificationDetails) => {
|
||||||
|
const channelName = slackNotificationDetails.channel?.name;
|
||||||
|
return channelName ? `#${channelName}` : "";
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.otherwise(() => "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSlackAuthorAccessory(notificationDetails?: NotificationDetails): List.Item.Accessory | null {
|
||||||
|
return match(notificationDetails)
|
||||||
|
.with(
|
||||||
|
{
|
||||||
|
type: "SlackMessage",
|
||||||
|
content: P.select(),
|
||||||
|
},
|
||||||
|
(slackNotificationDetails) => {
|
||||||
|
return match(slackNotificationDetails.sender)
|
||||||
|
.with({ type: "User", content: P.select() }, (slackUser: SlackUser) => {
|
||||||
|
const userAvatarUrl = getSlackUserAvatarUrl(slackUser);
|
||||||
|
const userName = slackUser.real_name || "Unknown";
|
||||||
|
return {
|
||||||
|
icon: userAvatarUrl ? { source: userAvatarUrl, mask: Image.Mask.Circle } : getAvatarIcon(userName),
|
||||||
|
tooltip: userName,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.with({ type: "Bot", content: P.select() }, (slackBot: SlackBotInfo) => {
|
||||||
|
const botAvatarUrl = getSlackIconUrl(slackBot.icons);
|
||||||
|
return {
|
||||||
|
icon: botAvatarUrl ? { source: botAvatarUrl, mask: Image.Mask.Circle } : getAvatarIcon(slackBot.name),
|
||||||
|
tooltip: slackBot.name,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.otherwise(() => ({ icon: Icon.Person, tooltip: "Unknown" }));
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.otherwise(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSlackTeamAccessory(notificationDetails?: NotificationDetails): List.Item.Accessory | null {
|
||||||
|
return match(notificationDetails)
|
||||||
|
.with(
|
||||||
|
{
|
||||||
|
type: P.union("SlackMessage", "SlackFile", "SlackFileComment", "SlackChannel", "SlackIm", "SlackGroup"),
|
||||||
|
content: P.select(),
|
||||||
|
},
|
||||||
|
(slackNotificationDetails) => {
|
||||||
|
const teamName = slackNotificationDetails.team.name;
|
||||||
|
const teamIconUrl = getSlackIconUrl(slackNotificationDetails.team.icon);
|
||||||
|
if (!teamName || !teamIconUrl) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { icon: { source: teamIconUrl, mask: Image.Mask.Circle }, tooltip: teamName };
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.otherwise(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSlackUserAvatarUrl(slackUser: SlackUser): string | null {
|
||||||
|
if (!slackUser.profile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (slackUser.profile.image_24) {
|
||||||
|
return slackUser.profile.image_24;
|
||||||
|
}
|
||||||
|
if (slackUser.profile.image_32) {
|
||||||
|
return slackUser.profile.image_32;
|
||||||
|
}
|
||||||
|
if (slackUser.profile.image_34) {
|
||||||
|
return slackUser.profile.image_34;
|
||||||
|
}
|
||||||
|
if (slackUser.profile.image_44) {
|
||||||
|
return slackUser.profile.image_44;
|
||||||
|
}
|
||||||
|
if (slackUser.profile.image_48) {
|
||||||
|
return slackUser.profile.image_48;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSlackIconUrl(slackIcon?: SlackIcon): string | null {
|
||||||
|
if (slackIcon?.image_24) {
|
||||||
|
return slackIcon.image_24;
|
||||||
|
}
|
||||||
|
if (slackIcon?.image_32) {
|
||||||
|
return slackIcon.image_32;
|
||||||
|
}
|
||||||
|
if (slackIcon?.image_34) {
|
||||||
|
return slackIcon.image_34;
|
||||||
|
}
|
||||||
|
if (slackIcon?.image_44) {
|
||||||
|
return slackIcon.image_44;
|
||||||
|
}
|
||||||
|
if (slackIcon?.image_48) {
|
||||||
|
return slackIcon.image_48;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
24
src/integrations/slack/preview/SlackStarPreview.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Notification, getNotificationHtmlUrl } from "../../../notification";
|
||||||
|
import { Detail, ActionPanel, Action } from "@raycast/api";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
interface SlackStarPreviewProps {
|
||||||
|
notification: Notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SlackStarPreview({ notification }: SlackStarPreviewProps) {
|
||||||
|
const notificationHtmlUrl = useMemo(() => {
|
||||||
|
return getNotificationHtmlUrl(notification);
|
||||||
|
}, [notification]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Detail
|
||||||
|
markdown={`# ${notification.title}`}
|
||||||
|
actions={
|
||||||
|
<ActionPanel>
|
||||||
|
<Action.OpenInBrowser url={notificationHtmlUrl} />
|
||||||
|
</ActionPanel>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
400
src/integrations/slack/types.ts
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
export interface SlackPushEventCallback {
|
||||||
|
team_id: string;
|
||||||
|
api_app_id: string;
|
||||||
|
event: SlackEventCallbackBody;
|
||||||
|
event_id: string;
|
||||||
|
event_time: Date;
|
||||||
|
event_context?: string;
|
||||||
|
authed_users?: Array<string>;
|
||||||
|
authorizations?: Array<SlackEventAuthorization>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackEventAuthorization {
|
||||||
|
team_id: string;
|
||||||
|
user_id: string;
|
||||||
|
is_bot: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SlackEventCallbackBody =
|
||||||
|
| { type: "SarAdded"; content: SlackStarAddedEvent }
|
||||||
|
| { type: "StarRemoved"; content: SlackStarRemovedEvent };
|
||||||
|
|
||||||
|
export interface SlackStarAddedEvent {
|
||||||
|
user: string;
|
||||||
|
item: SlackStarsItem;
|
||||||
|
event_ts: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackStarRemovedEvent {
|
||||||
|
user: string;
|
||||||
|
item: SlackStarsItem;
|
||||||
|
event_ts: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SlackStarsItem =
|
||||||
|
| { type: "Message"; content: SlackStarsItemMessage }
|
||||||
|
| { type: "File"; content: SlackStarsItemFile }
|
||||||
|
| { type: "FileComment"; content: SlackStarsItemFileComment }
|
||||||
|
| { type: "Channel"; content: SlackStarsItemChannel }
|
||||||
|
| { type: "Im"; content: SlackStarsItemIm }
|
||||||
|
| { type: "Group"; content: SlackStarsItemGroup };
|
||||||
|
|
||||||
|
export interface SlackStarsItemMessage {
|
||||||
|
message: SlackHistoryMessage;
|
||||||
|
channel: string;
|
||||||
|
date_create: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackHistoryMessage {
|
||||||
|
ts: string;
|
||||||
|
channel?: string;
|
||||||
|
channel_type?: string;
|
||||||
|
thread_ts?: string;
|
||||||
|
client_msg_id?: string;
|
||||||
|
|
||||||
|
text?: string;
|
||||||
|
blocks?: Array<SlackBlock>;
|
||||||
|
attachments?: Array<SlackMessageAttachment>;
|
||||||
|
upload?: boolean;
|
||||||
|
files?: Array<SlackFile>;
|
||||||
|
reactions?: Array<SlackReaction>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackReaction {
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
users: Array<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackMessageAttachment {
|
||||||
|
id?: number;
|
||||||
|
color?: string;
|
||||||
|
fallback?: string;
|
||||||
|
title?: string;
|
||||||
|
fields?: Array<SlackMessageAttachmentFieldObject>;
|
||||||
|
mrkdwn_in?: Array<string>;
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackMessageAttachmentFieldObject {
|
||||||
|
title?: string;
|
||||||
|
value?: string;
|
||||||
|
short?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SlackBlock =
|
||||||
|
| SlackSectionBlock
|
||||||
|
| SlackHeaderBlock
|
||||||
|
| SlackDividerBlock
|
||||||
|
| SlackImageBlock
|
||||||
|
| SlackActionsBlock
|
||||||
|
| SlackContextBlock
|
||||||
|
| SlackInputBlock
|
||||||
|
| SlackFileBlock
|
||||||
|
| { type: "rich_text" }
|
||||||
|
| { type: "event" };
|
||||||
|
|
||||||
|
export interface SlackSectionBlock {
|
||||||
|
type: "section";
|
||||||
|
block_id?: string;
|
||||||
|
text?: SlackBlockText;
|
||||||
|
fields?: Array<SlackBlockText>;
|
||||||
|
// To be specified
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
accessory?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SlackBlockText =
|
||||||
|
| { type: "plain_text"; value: string }
|
||||||
|
| { type: "mrkdwn"; text: string; verbatim?: boolean };
|
||||||
|
|
||||||
|
export interface SlackHeaderBlock {
|
||||||
|
type: "header";
|
||||||
|
block_id?: string;
|
||||||
|
text: SlackBlockText;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackDividerBlock {
|
||||||
|
type: "divider";
|
||||||
|
block_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackImageBlock {
|
||||||
|
type: "image";
|
||||||
|
block_id?: string;
|
||||||
|
image_url: string;
|
||||||
|
alt_text: string;
|
||||||
|
title?: SlackBlockText;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackActionsBlock {
|
||||||
|
type: "actions";
|
||||||
|
block_id?: string;
|
||||||
|
// To be specified
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
elements: Array<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackContextBlock {
|
||||||
|
type: "context";
|
||||||
|
block_id?: string;
|
||||||
|
// To be specified
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
elements: Array<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackInputBlock {
|
||||||
|
type: "input";
|
||||||
|
block_id?: string;
|
||||||
|
label: SlackBlockText;
|
||||||
|
// To be specified
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
element: any;
|
||||||
|
hint?: SlackBlockText;
|
||||||
|
optional?: boolean;
|
||||||
|
dispatch_action?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackFileBlock {
|
||||||
|
type: "file";
|
||||||
|
block_id?: string;
|
||||||
|
external_id: string;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackStarsItemFile {
|
||||||
|
file: SlackFile;
|
||||||
|
channel: string;
|
||||||
|
date_create: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackFile {
|
||||||
|
id: string;
|
||||||
|
created?: Date;
|
||||||
|
timestamp?: Date;
|
||||||
|
name?: string;
|
||||||
|
title?: string;
|
||||||
|
mimetype?: string;
|
||||||
|
filetype?: string;
|
||||||
|
pretty_type?: string;
|
||||||
|
external_type?: string;
|
||||||
|
user?: string;
|
||||||
|
username?: string;
|
||||||
|
url_private?: string;
|
||||||
|
url_private_download?: string;
|
||||||
|
permalink?: string;
|
||||||
|
permalink_public?: string;
|
||||||
|
reactions?: Array<SlackReaction>;
|
||||||
|
editable?: boolean;
|
||||||
|
is_external?: boolean;
|
||||||
|
is_public?: boolean;
|
||||||
|
public_url_shared?: boolean;
|
||||||
|
display_as_bot?: boolean;
|
||||||
|
is_starred?: boolean;
|
||||||
|
has_rich_preview?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackStarsItemFileComment {
|
||||||
|
file: SlackFile;
|
||||||
|
file_comment: string;
|
||||||
|
channel: string;
|
||||||
|
date_create: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackStarsItemChannel {
|
||||||
|
channel: string;
|
||||||
|
date_create: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackStarsItemIm {
|
||||||
|
channel: string;
|
||||||
|
date_create: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackStarsItemGroup {
|
||||||
|
group: string;
|
||||||
|
date_create: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackMessageDetails {
|
||||||
|
url: string;
|
||||||
|
message: SlackHistoryMessage;
|
||||||
|
channel: SlackChannelInfo;
|
||||||
|
sender: SlackMessageSenderDetails;
|
||||||
|
team: SlackTeamInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SlackMessageSenderDetails = { type: "User"; content: SlackUser } | { type: "Bot"; content: SlackBotInfo };
|
||||||
|
|
||||||
|
export interface SlackUser {
|
||||||
|
id: string;
|
||||||
|
team_id?: string;
|
||||||
|
name?: string;
|
||||||
|
locale?: string;
|
||||||
|
profile?: SlackUserProfile;
|
||||||
|
is_admin?: boolean;
|
||||||
|
is_app_user?: boolean;
|
||||||
|
is_bot?: boolean;
|
||||||
|
is_invited_user?: boolean;
|
||||||
|
is_owner?: boolean;
|
||||||
|
is_primary_owner?: boolean;
|
||||||
|
is_restricted?: boolean;
|
||||||
|
is_stranger?: boolean;
|
||||||
|
is_ultra_restricted?: boolean;
|
||||||
|
has_2fa?: boolean;
|
||||||
|
tz?: string;
|
||||||
|
tz_label?: string;
|
||||||
|
tz_offset?: number;
|
||||||
|
updated?: Date;
|
||||||
|
deleted?: boolean;
|
||||||
|
color?: string;
|
||||||
|
real_name?: string;
|
||||||
|
enterprise_user?: SlackEnterpriseUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackUserProfile {
|
||||||
|
id?: string;
|
||||||
|
display_name?: string;
|
||||||
|
real_name?: string;
|
||||||
|
real_name_normalized?: string;
|
||||||
|
avatar_hash?: string;
|
||||||
|
status_text?: string;
|
||||||
|
status_expiration?: Date;
|
||||||
|
status_emoji?: string;
|
||||||
|
display_name_normalized?: string;
|
||||||
|
email?: string;
|
||||||
|
team?: string;
|
||||||
|
image_original?: string;
|
||||||
|
image_default?: boolean;
|
||||||
|
image_24?: string;
|
||||||
|
image_32?: string;
|
||||||
|
image_34?: string;
|
||||||
|
image_44?: string;
|
||||||
|
image_48?: string;
|
||||||
|
image_68?: string;
|
||||||
|
image_72?: string;
|
||||||
|
image_88?: string;
|
||||||
|
image_102?: string;
|
||||||
|
image_132?: string;
|
||||||
|
image_192?: string;
|
||||||
|
image_230?: string;
|
||||||
|
image_512?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackEnterpriseUser {
|
||||||
|
id: string;
|
||||||
|
enterprise_id: string;
|
||||||
|
enterprise_name?: string;
|
||||||
|
teams?: Array<string>;
|
||||||
|
is_admin?: boolean;
|
||||||
|
is_app_user?: boolean;
|
||||||
|
is_bot?: boolean;
|
||||||
|
is_invited_user?: boolean;
|
||||||
|
is_owner?: boolean;
|
||||||
|
is_primary_owner?: boolean;
|
||||||
|
is_restricted?: boolean;
|
||||||
|
is_stranger?: boolean;
|
||||||
|
is_ultra_restricted?: boolean;
|
||||||
|
has_2fa?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackBotInfo {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
updated?: Date;
|
||||||
|
app_id: string;
|
||||||
|
user_id: string;
|
||||||
|
icons?: SlackIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackTeamInfo {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
domain?: string;
|
||||||
|
email_domain?: string;
|
||||||
|
icon?: SlackIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackIcon {
|
||||||
|
image_original?: string;
|
||||||
|
image_default?: boolean;
|
||||||
|
image_24?: string;
|
||||||
|
image_32?: string;
|
||||||
|
image_34?: string;
|
||||||
|
image_44?: string;
|
||||||
|
image_48?: string;
|
||||||
|
image_68?: string;
|
||||||
|
image_72?: string;
|
||||||
|
image_88?: string;
|
||||||
|
image_102?: string;
|
||||||
|
image_132?: string;
|
||||||
|
image_192?: string;
|
||||||
|
image_230?: string;
|
||||||
|
image_512?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackChannelInfo {
|
||||||
|
id: string;
|
||||||
|
created: Date;
|
||||||
|
creator?: string;
|
||||||
|
name?: string;
|
||||||
|
name_normalized?: string;
|
||||||
|
topic?: SlackChannelTopicInfo;
|
||||||
|
purpose?: SlackChannelPurposeInfo;
|
||||||
|
previous_names?: Array<string>;
|
||||||
|
priority?: number;
|
||||||
|
num_members?: number;
|
||||||
|
locale?: string;
|
||||||
|
is_channel?: boolean;
|
||||||
|
is_group?: boolean;
|
||||||
|
is_im?: boolean;
|
||||||
|
is_archived?: boolean;
|
||||||
|
is_general?: boolean;
|
||||||
|
is_shared?: boolean;
|
||||||
|
is_org_shared?: boolean;
|
||||||
|
is_member?: boolean;
|
||||||
|
is_private?: boolean;
|
||||||
|
is_mpim?: boolean;
|
||||||
|
is_user_deleted?: boolean;
|
||||||
|
last_read?: string;
|
||||||
|
unread_count?: number;
|
||||||
|
unread_count_display?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackChannelTopicInfo {
|
||||||
|
value: string;
|
||||||
|
creator?: string;
|
||||||
|
last_set?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackChannelPurposeInfo {
|
||||||
|
value: string;
|
||||||
|
creator?: string;
|
||||||
|
last_set?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackChannelDetails {
|
||||||
|
channel: SlackChannelInfo;
|
||||||
|
team: SlackTeamInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackFileDetails {
|
||||||
|
channel: SlackChannelInfo;
|
||||||
|
sender?: SlackUser;
|
||||||
|
team: SlackTeamInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackFileCommentDetails {
|
||||||
|
channel: SlackChannelInfo;
|
||||||
|
team: SlackTeamInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackImDetails {
|
||||||
|
channel: SlackChannelInfo;
|
||||||
|
team: SlackTeamInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SlackGroupDetails {
|
||||||
|
channel: SlackChannelInfo;
|
||||||
|
team: SlackTeamInfo;
|
||||||
|
}
|
||||||
@@ -1,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,102 @@
|
|||||||
import { Notification } from "./types";
|
import {
|
||||||
|
SlackChannelDetails,
|
||||||
|
SlackFileCommentDetails,
|
||||||
|
SlackFileDetails,
|
||||||
|
SlackGroupDetails,
|
||||||
|
SlackImDetails,
|
||||||
|
SlackMessageDetails,
|
||||||
|
SlackPushEventCallback,
|
||||||
|
} from "./integrations/slack/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 }
|
||||||
|
| { type: "Slack"; content: SlackPushEventCallback };
|
||||||
|
|
||||||
|
export type NotificationDetails =
|
||||||
|
| { type: "GithubPullRequest"; content: GithubPullRequest }
|
||||||
|
| { type: "GithubDiscussion"; content: GithubDiscussion }
|
||||||
|
| { type: "SlackMessage"; content: SlackMessageDetails }
|
||||||
|
| { type: "SlackFile"; content: SlackFileDetails }
|
||||||
|
| { type: "SlackFileComment"; content: SlackFileCommentDetails }
|
||||||
|
| { type: "SlackChannel"; content: SlackChannelDetails }
|
||||||
|
| { type: "SlackIm"; content: SlackImDetails }
|
||||||
|
| { type: "SlackGroup"; content: SlackGroupDetails };
|
||||||
|
|
||||||
|
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({ details: { type: "SlackMessage", content: P.select() } }, (notificationDetails) => notificationDetails.url)
|
||||||
return notification.details.content.url;
|
.with(
|
||||||
case "GithubDiscussion":
|
{
|
||||||
return notification.details.content.url;
|
details: {
|
||||||
default: {
|
type: P.union("SlackChannel", "SlackFile", "SlackFileComment", "SlackGroup", "SlackIm"),
|
||||||
// TODO
|
content: P.select(),
|
||||||
return "https://github.com";
|
},
|
||||||
}
|
},
|
||||||
}
|
(notificationDetails) =>
|
||||||
|
`https://app.slack.com/client/${notificationDetails.team.id}/${notificationDetails.channel.id}`,
|
||||||
|
)
|
||||||
|
.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")
|
||||||
|
.with({ metadata: { type: "Slack" } }, () => "https://app.slack.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 {
|
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;
|
|
||||||
};
|
|
||||||
|
|||||||