Compare commits

..

5 Commits

34 changed files with 2820 additions and 869 deletions

View File

@@ -2,6 +2,13 @@
## [Unreleased] ## [Unreleased]
## [0.2.0] - 2024-12-16
### Added
- Add support for Slack reaction notifications
- Add support for Slack message notifications
## [0.1.4] - 2024-03-13 ## [0.1.4] - 2024-03-13
### Added ### Added

View File

@@ -43,6 +43,7 @@
}, },
"nodejs@latest": { "nodejs@latest": {
"last_modified": "2024-01-14T03:55:27Z", "last_modified": "2024-01-14T03:55:27Z",
"plugin_version": "0.0.2",
"resolved": "github:NixOS/nixpkgs/dd5621df6dcb90122b50da5ec31c411a0de3e538#nodejs_21", "resolved": "github:NixOS/nixpkgs/dd5621df6dcb90122b50da5ec31c411a0de3e538#nodejs_21",
"source": "devbox-search", "source": "devbox-search",
"version": "21.5.0", "version": "21.5.0",

2677
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,10 +5,8 @@
"description": "Manage your notifications in a single Universal Inbox", "description": "Manage your notifications in a single Universal Inbox",
"icon": "ui-logo-transparent.png", "icon": "ui-logo-transparent.png",
"author": "dax42", "author": "dax42",
"version": "0.1.4", "version": "0.2.0",
"categories": [ "categories": ["Productivity"],
"Productivity"
],
"license": "MIT", "license": "MIT",
"preferences": [ "preferences": [
{ {

View File

@@ -41,7 +41,7 @@ export function CreateTaskFromNotification({ notification, mutate }: CreateTaskF
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const { isLoading, data: projects } = useFetch<Array<ProjectSummary>>( const { isLoading, data: projects } = useFetch<Array<ProjectSummary>>(
`${preferences.universalInboxBaseUrl}/api/tasks/projects/search?matches=${searchText}`, `${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/tasks/projects/search?matches=${searchText}`,
{ {
keepPreviousData: true, keepPreviousData: true,
headers: { headers: {
@@ -123,7 +123,7 @@ async function createTaskFromNotification(
try { try {
await mutate( await mutate(
handleErrors( handleErrors(
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}/task`, { fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/notifications/${notification.id}/task`, {
method: "POST", method: "POST",
body: JSON.stringify(taskCreation), body: JSON.stringify(taskCreation),
headers: { headers: {

View File

@@ -22,7 +22,7 @@ export function LinkNotificationToTask({ notification, mutate }: LinkNotificatio
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const { isLoading, data: tasks } = useFetch<Array<Task>>( const { isLoading, data: tasks } = useFetch<Array<Task>>(
`${preferences.universalInboxBaseUrl}/api/tasks/search?matches=${searchText}`, `${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/tasks/search?matches=${searchText}`,
{ {
keepPreviousData: true, keepPreviousData: true,
headers: { headers: {
@@ -78,7 +78,7 @@ async function linkNotificationToTask(
try { try {
await mutate( await mutate(
handleErrors( handleErrors(
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}`, { fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/notifications/${notification.id}`, {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ status: TaskStatus.Deleted, task_id: taskId }), body: JSON.stringify({ status: TaskStatus.Deleted, task_id: taskId }),
headers: { headers: {

View File

@@ -35,7 +35,7 @@ export function NotificationActions({ notification, detailsTarget, mutate }: Not
onAction={() => deleteNotification(notification, mutate)} onAction={() => deleteNotification(notification, mutate)}
/> />
<Action <Action
title="Unsubscribe From Notification" title="Unsubscribe from Notification"
icon={Icon.BellDisabled} icon={Icon.BellDisabled}
shortcut={{ modifiers: ["ctrl"], key: "u" }} shortcut={{ modifiers: ["ctrl"], key: "u" }}
onAction={() => unsubscribeFromNotification(notification, mutate)} onAction={() => unsubscribeFromNotification(notification, mutate)}
@@ -47,13 +47,13 @@ export function NotificationActions({ notification, detailsTarget, mutate }: Not
onAction={() => snoozeNotification(notification, mutate)} onAction={() => snoozeNotification(notification, mutate)}
/> />
<Action.Push <Action.Push
title="Create Task..." title="Create Task"
icon={Icon.Calendar} icon={Icon.Calendar}
shortcut={{ modifiers: ["ctrl"], key: "t" }} shortcut={{ modifiers: ["ctrl"], key: "t" }}
target={<CreateTaskFromNotification notification={notification} mutate={mutate} />} target={<CreateTaskFromNotification notification={notification} mutate={mutate} />}
/> />
<Action.Push <Action.Push
title="Link to Task..." title="Link to Task"
icon={Icon.Link} icon={Icon.Link}
shortcut={{ modifiers: ["ctrl"], key: "l" }} shortcut={{ modifiers: ["ctrl"], key: "l" }}
target={<LinkNotificationToTask notification={notification} mutate={mutate} />} target={<LinkNotificationToTask notification={notification} mutate={mutate} />}
@@ -72,7 +72,7 @@ export async function deleteNotification(
if (isNotificationBuiltFromTask(notification) && notification.task) { if (isNotificationBuiltFromTask(notification) && notification.task) {
await mutate( await mutate(
handleErrors( handleErrors(
fetch(`${preferences.universalInboxBaseUrl}/api/tasks/${notification.task.id}`, { fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/tasks/${notification.task.id}`, {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ status: TaskStatus.Deleted }), body: JSON.stringify({ status: TaskStatus.Deleted }),
headers: { headers: {
@@ -93,7 +93,7 @@ export async function deleteNotification(
} else { } else {
await mutate( await mutate(
handleErrors( handleErrors(
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}`, { fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/notifications/${notification.id}`, {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ status: NotificationStatus.Deleted }), body: JSON.stringify({ status: NotificationStatus.Deleted }),
headers: { headers: {
@@ -132,7 +132,7 @@ export async function unsubscribeFromNotification(
try { try {
await mutate( await mutate(
handleErrors( handleErrors(
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}`, { fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/notifications/${notification.id}`, {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ status: NotificationStatus.Unsubscribed }), body: JSON.stringify({ status: NotificationStatus.Unsubscribed }),
headers: { headers: {
@@ -171,7 +171,7 @@ export async function snoozeNotification(
const snoozeTime = computeSnoozedUntil(new Date(), 1, 6); const snoozeTime = computeSnoozedUntil(new Date(), 1, 6);
await mutate( await mutate(
handleErrors( handleErrors(
fetch(`${preferences.universalInboxBaseUrl}/api/notifications/${notification.id}`, { fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/notifications/${notification.id}`, {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ snoozed_until: snoozeTime }), body: JSON.stringify({ snoozed_until: snoozeTime }),
headers: { headers: {

View File

@@ -31,7 +31,7 @@ export function NotificationTaskActions({ notification, detailsTarget, mutate }:
onAction={() => deleteNotification(notification, mutate)} onAction={() => deleteNotification(notification, mutate)}
/> />
<Action <Action
title="Unsubscribe From Notification" title="Unsubscribe from Notification"
icon={Icon.BellDisabled} icon={Icon.BellDisabled}
shortcut={{ modifiers: ["ctrl"], key: "u" }} shortcut={{ modifiers: ["ctrl"], key: "u" }}
onAction={() => unsubscribeFromNotification(notification, mutate)} onAction={() => unsubscribeFromNotification(notification, mutate)}
@@ -49,7 +49,7 @@ export function NotificationTaskActions({ notification, detailsTarget, mutate }:
onAction={() => completeTask(notification, mutate)} onAction={() => completeTask(notification, mutate)}
/> />
<Action.Push <Action.Push
title="Plan Task..." title="Plan Task"
icon={Icon.Calendar} icon={Icon.Calendar}
shortcut={{ modifiers: ["ctrl"], key: "t" }} shortcut={{ modifiers: ["ctrl"], key: "t" }}
target={<PlanTask notification={notification} mutate={mutate} />} target={<PlanTask notification={notification} mutate={mutate} />}
@@ -68,7 +68,7 @@ async function completeTask(notification: Notification, mutate: MutatePromise<Pa
try { try {
await mutate( await mutate(
handleErrors( handleErrors(
fetch(`${preferences.universalInboxBaseUrl}/api/tasks/${notification.task.id}`, { fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/tasks/${notification.task.id}`, {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ status: TaskStatus.Done }), body: JSON.stringify({ status: TaskStatus.Done }),
headers: { headers: {

View File

@@ -39,7 +39,7 @@ export function PlanTask({ notification, mutate }: PlanTaskProps) {
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
const { isLoading, data: projects } = useFetch<Array<ProjectSummary>>( const { isLoading, data: projects } = useFetch<Array<ProjectSummary>>(
`${preferences.universalInboxBaseUrl}/api/tasks/projects/search?matches=${searchText}`, `${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/tasks/projects/search?matches=${searchText}`,
{ {
keepPreviousData: true, keepPreviousData: true,
headers: { headers: {
@@ -122,7 +122,7 @@ async function planTask(
try { try {
await mutate( await mutate(
handleErrors( handleErrors(
fetch(`${preferences.universalInboxBaseUrl}/api/tasks/${notification.task.id}`, { fetch(`${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/tasks/${notification.task.id}`, {
method: "PATCH", method: "PATCH",
body: JSON.stringify({ body: JSON.stringify({
project: taskPlanning.project.name, project: taskPlanning.project.name,

View File

@@ -4,7 +4,7 @@ import { TodoistNotificationListItem } from "./integrations/todoist/listitem/Tod
import { GithubNotificationListItem } from "./integrations/github/listitem/GithubNotificationListItem"; import { GithubNotificationListItem } from "./integrations/github/listitem/GithubNotificationListItem";
import { LinearNotificationListItem } from "./integrations/linear/listitem/LinearNotificationListItem"; import { LinearNotificationListItem } from "./integrations/linear/listitem/LinearNotificationListItem";
import { SlackNotificationListItem } from "./integrations/slack/listitem/SlackNotificationListItem"; import { SlackNotificationListItem } from "./integrations/slack/listitem/SlackNotificationListItem";
import { Notification, NotificationListItemProps } from "./notification"; import { Notification, NotificationListItemProps, NotificationSourceKind } from "./notification";
import { NotificationActions } from "./action/NotificationActions"; import { NotificationActions } from "./action/NotificationActions";
import { Page, UniversalInboxPreferences } from "./types"; import { Page, UniversalInboxPreferences } from "./types";
import { useFetch } from "@raycast/utils"; import { useFetch } from "@raycast/utils";
@@ -33,7 +33,7 @@ export default function Command() {
const [notificationKind, setNotificationKind] = useState(""); const [notificationKind, setNotificationKind] = useState("");
const { isLoading, data, mutate } = useFetch<Page<Notification>>( const { isLoading, data, mutate } = useFetch<Page<Notification>>(
`${preferences.universalInboxBaseUrl}/api/notifications?status=Unread,Read&with_tasks=true${ `${preferences.universalInboxBaseUrl.replace(/\/$/, "")}/api/notifications?status=Unread,Read&with_tasks=true${
notificationKind ? "&notification_kind=" + notificationKind : "" notificationKind ? "&notification_kind=" + notificationKind : ""
}`, }`,
{ {
@@ -67,16 +67,16 @@ export default function Command() {
} }
function NotificationListItem({ notification, mutate }: NotificationListItemProps) { function NotificationListItem({ notification, mutate }: NotificationListItemProps) {
switch (notification.metadata.type) { switch (notification.kind) {
case "Github": case NotificationSourceKind.Github:
return <GithubNotificationListItem notification={notification} mutate={mutate} />; return <GithubNotificationListItem notification={notification} mutate={mutate} />;
case "Linear": case NotificationSourceKind.Linear:
return <LinearNotificationListItem notification={notification} mutate={mutate} />; return <LinearNotificationListItem notification={notification} mutate={mutate} />;
case "GoogleMail": case NotificationSourceKind.GoogleMail:
return <GoogleMailNotificationListItem notification={notification} mutate={mutate} />; return <GoogleMailNotificationListItem notification={notification} mutate={mutate} />;
case "Slack": case NotificationSourceKind.Slack:
return <SlackNotificationListItem notification={notification} mutate={mutate} />; return <SlackNotificationListItem notification={notification} mutate={mutate} />;
case "Todoist": case NotificationSourceKind.Todoist:
return <TodoistNotificationListItem notification={notification} mutate={mutate} />; return <TodoistNotificationListItem notification={notification} mutate={mutate} />;
default: default:
return <DefaultNotificationListItem notification={notification} mutate={mutate} />; return <DefaultNotificationListItem notification={notification} mutate={mutate} />;
@@ -88,7 +88,7 @@ function DefaultNotificationListItem({ notification, mutate }: NotificationListI
<List.Item <List.Item
key={notification.id} key={notification.id}
title={notification.title} title={notification.title}
subtitle={`#${notification.source_id}`} subtitle={`#${notification.source_item.source_id}`}
actions={ actions={
<NotificationActions <NotificationActions
notification={notification} notification={notification}

View File

@@ -3,12 +3,14 @@ import { GithubDiscussionNotificationListItem } from "./GithubDiscussionNotifica
import { NotificationListItemProps } from "../../../notification"; import { NotificationListItemProps } from "../../../notification";
export function GithubNotificationListItem({ notification, mutate }: NotificationListItemProps) { export function GithubNotificationListItem({ notification, mutate }: NotificationListItemProps) {
switch (notification.details?.type) { if (notification.source_item.data.type !== "GithubNotification") return null;
switch (notification.source_item.data.content.item?.type) {
case "GithubPullRequest": case "GithubPullRequest":
return ( return (
<GithubPullRequestNotificationListItem <GithubPullRequestNotificationListItem
notification={notification} notification={notification}
githubPullRequest={notification.details.content} githubPullRequest={notification.source_item.data.content.item.content}
mutate={mutate} mutate={mutate}
/> />
); );
@@ -16,7 +18,7 @@ export function GithubNotificationListItem({ notification, mutate }: Notificatio
return ( return (
<GithubDiscussionNotificationListItem <GithubDiscussionNotificationListItem
notification={notification} notification={notification}
githubDiscussion={notification.details.content} githubDiscussion={notification.source_item.data.content.item.content}
mutate={mutate} mutate={mutate}
/> />
); );

View File

@@ -1,4 +1,4 @@
import { Notification, getNotificationHtmlUrl } from "../../../notification"; import { getNotificationHtmlUrl, Notification } from "../../../notification";
import { Detail, ActionPanel, Action } from "@raycast/api"; import { Detail, ActionPanel, Action } from "@raycast/api";
import { GithubDiscussion } from "../types"; import { GithubDiscussion } from "../types";
import { useMemo } from "react"; import { useMemo } from "react";

View File

@@ -1,4 +1,4 @@
import { Notification, getNotificationHtmlUrl } from "../../../notification"; import { getNotificationHtmlUrl, Notification } from "../../../notification";
import { Detail, ActionPanel, Action } from "@raycast/api"; import { Detail, ActionPanel, Action } from "@raycast/api";
import { GithubPullRequest } from "../types"; import { GithubPullRequest } from "../types";
import { useMemo } from "react"; import { useMemo } from "react";

View File

@@ -1,3 +1,60 @@
import { match, P } from "ts-pattern";
export interface GithubNotification {
id: string;
repository: GithubRepositorySummary;
subject: GithubNotificationSubject;
reason: string;
unread: boolean;
updated_at: Date;
last_read_at?: Date;
url: string;
subscription_url: string;
item?: GithubNotificationItem;
}
export function getGithubNotificationHtmlUrl(notification: GithubNotification): string {
return match(notification.item)
.with({ type: "GithubPullRequest", content: P.select() }, (pr) => pr.url)
.with({ type: "GithubDiscussion", content: P.select() }, (discussion) => discussion.url)
.otherwise(() => getHtmlUrlFromApiUrl(notification.subject.url) ?? getHtmlUrlFromMetadata(notification));
}
function getHtmlUrlFromApiUrl(apiUrl: string | undefined): string | undefined {
if (!apiUrl) {
return undefined;
}
const url = new URL(apiUrl);
if (url.host === "api.github.com" && url.pathname.startsWith("/repos")) {
const result = new URL(apiUrl);
result.host = "github.com";
result.pathname = url.pathname.replace("/repos", "").replace("/pulls/", "/pull/");
return result.toString();
}
return undefined;
}
function getHtmlUrlFromMetadata(notification: GithubNotification): string {
return match(notification.subject.type)
.with("CheckSuite", () => {
const result = new URL(notification.repository.url);
result.pathname = `${result.pathname}/actions`;
return result.toString();
})
.otherwise(() => notification.repository.url);
}
export interface GithubNotificationSubject {
title: string;
url?: string;
latest_comment_url?: string;
type: string;
}
export type GithubNotificationItem =
| { type: "GithubPullRequest"; content: GithubPullRequest }
| { type: "GithubDiscussion"; content: GithubDiscussion };
export interface GithubPullRequest { export interface GithubPullRequest {
id: string; id: string;
number: number; number: number;

View File

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

View File

@@ -1,6 +1,5 @@
import { GoogleMailThreadPreview } from "../preview/GoogleMailThreadPreview"; import { GoogleMailThreadPreview } from "../preview/GoogleMailThreadPreview";
import { NotificationActions } from "../../../action/NotificationActions"; import { NotificationActions } from "../../../action/NotificationActions";
//import { getLinearUserAccessory } from "../accessories";
import { Notification } from "../../../notification"; import { Notification } from "../../../notification";
import { Icon, Color, List } from "@raycast/api"; import { Icon, Color, List } from "@raycast/api";
import { MutatePromise } from "@raycast/utils"; import { MutatePromise } from "@raycast/utils";

View File

@@ -1,4 +1,4 @@
import { Notification, getNotificationHtmlUrl } from "../../../notification"; import { getNotificationHtmlUrl, Notification } from "../../../notification";
import { Detail, ActionPanel, Action } from "@raycast/api"; import { Detail, ActionPanel, Action } from "@raycast/api";
import { GoogleMailThread } from "../types"; import { GoogleMailThread } from "../types";
import { useMemo } from "react"; import { useMemo } from "react";

View File

@@ -3,14 +3,14 @@ import { LinearIssueNotificationListItem } from "./LinearIssueNotificationListIt
import { NotificationListItemProps } from "../../../notification"; import { NotificationListItemProps } from "../../../notification";
export function LinearNotificationListItem({ notification, mutate }: NotificationListItemProps) { export function LinearNotificationListItem({ notification, mutate }: NotificationListItemProps) {
if (notification.metadata.type !== "Linear") return null; if (notification.source_item.data.type !== "LinearNotification") return null;
switch (notification.metadata.content.type) { switch (notification.source_item.data.content.type) {
case "IssueNotification": case "IssueNotification":
return ( return (
<LinearIssueNotificationListItem <LinearIssueNotificationListItem
notification={notification} notification={notification}
linearIssueNotification={notification.metadata.content.content} linearIssueNotification={notification.source_item.data.content.content}
mutate={mutate} mutate={mutate}
/> />
); );
@@ -18,7 +18,7 @@ export function LinearNotificationListItem({ notification, mutate }: Notificatio
return ( return (
<LinearProjectNotificationListItem <LinearProjectNotificationListItem
notification={notification} notification={notification}
linearProjectNotification={notification.metadata.content.content} linearProjectNotification={notification.source_item.data.content.content}
mutate={mutate} mutate={mutate}
/> />
); );

View File

@@ -1,4 +1,4 @@
import { Notification, getNotificationHtmlUrl } from "../../../notification"; import { getNotificationHtmlUrl, Notification } from "../../../notification";
import { Detail, ActionPanel, Action } from "@raycast/api"; import { Detail, ActionPanel, Action } from "@raycast/api";
import { LinearIssue } from "../types"; import { LinearIssue } from "../types";
import { useMemo } from "react"; import { useMemo } from "react";

View File

@@ -1,4 +1,4 @@
import { Notification, getNotificationHtmlUrl } from "../../../notification"; import { getNotificationHtmlUrl, Notification } from "../../../notification";
import { Detail, ActionPanel, Action } from "@raycast/api"; import { Detail, ActionPanel, Action } from "@raycast/api";
import { LinearProject } from "../types"; import { LinearProject } from "../types";
import { useMemo } from "react"; import { useMemo } from "react";

View File

@@ -2,6 +2,15 @@ export type LinearNotification =
| { type: "IssueNotification"; content: LinearIssueNotification } | { type: "IssueNotification"; content: LinearIssueNotification }
| { type: "ProjectNotification"; content: LinearProjectNotification }; | { type: "ProjectNotification"; content: LinearProjectNotification };
export function getLinearNotificationHtmlUrl(notification: LinearNotification): string {
switch (notification.type) {
case "IssueNotification":
return notification.content.issue.url;
case "ProjectNotification":
return notification.content.project.url;
}
}
export interface LinearIssueNotification { export interface LinearIssueNotification {
id: string; id: string;
type: string; type: string;
@@ -39,6 +48,10 @@ export interface LinearIssue {
team: LinearTeam; team: LinearTeam;
} }
export function getLinearIssueHtmlUrl(linearIssue: LinearIssue): string {
return linearIssue.url;
}
export enum LinearIssuePriority { export enum LinearIssuePriority {
NoPriority = 0, NoPriority = 0,
Urgent = 1, Urgent = 1,

View File

@@ -0,0 +1,42 @@
import { SlackIcon, SlackUser } from "./types";
export function getSlackUserAvatarUrl(slackUser: SlackUser): string | null {
if (!slackUser.profile) {
return null;
}
if (slackUser.profile.image_24) {
return slackUser.profile.image_24;
}
if (slackUser.profile.image_32) {
return slackUser.profile.image_32;
}
if (slackUser.profile.image_34) {
return slackUser.profile.image_34;
}
if (slackUser.profile.image_44) {
return slackUser.profile.image_44;
}
if (slackUser.profile.image_48) {
return slackUser.profile.image_48;
}
return null;
}
export function getSlackIconUrl(slackIcon?: SlackIcon): string | null {
if (slackIcon?.image_24) {
return slackIcon.image_24;
}
if (slackIcon?.image_32) {
return slackIcon.image_32;
}
if (slackIcon?.image_34) {
return slackIcon.image_34;
}
if (slackIcon?.image_44) {
return slackIcon.image_44;
}
if (slackIcon?.image_48) {
return slackIcon.image_48;
}
return null;
}

View File

@@ -1,147 +1,35 @@
import { NotificationDetails, NotificationListItemProps } from "../../../notification"; import { SlackReactionNotificationListItem } from "./SlackReactionNotificationListItem";
import { NotificationActions } from "../../../action/NotificationActions"; import { SlackThreadNotificationListItem } from "./SlackThreadNotificationListItem";
import { SlackStarPreview } from "../preview/SlackStarPreview"; import { SlackStarNotificationListItem } from "./SlackStarNotificationListItem";
import { SlackBotInfo, SlackIcon, SlackUser } from "../types"; import { NotificationListItemProps } from "../../../notification";
/* 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) { export function SlackNotificationListItem({ notification, mutate }: NotificationListItemProps) {
const subtitle = getSlackNotificationSubtitle(notification.details); switch (notification.source_item.data.type) {
case "SlackStar":
const author = getSlackAuthorAccessory(notification.details); return (
const team = getSlackTeamAccessory(notification.details); <SlackStarNotificationListItem
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} notification={notification}
detailsTarget={<SlackStarPreview notification={notification} />} slack_star={notification.source_item.data.content}
mutate={mutate} mutate={mutate}
/> />
} );
/> case "SlackReaction":
); return (
} <SlackReactionNotificationListItem
notification={notification}
function getSlackNotificationSubtitle(notificationDetails?: NotificationDetails): string { slack_reaction={notification.source_item.data.content}
return match(notificationDetails) mutate={mutate}
.with( />
{ );
type: P.union("SlackMessage", "SlackFile", "SlackFileComment", "SlackChannel", "SlackIm", "SlackGroup"), case "SlackThread":
content: P.select(), return (
}, <SlackThreadNotificationListItem
(slackNotificationDetails) => { notification={notification}
const channelName = slackNotificationDetails.channel?.name; slack_thread={notification.source_item.data.content}
return channelName ? `#${channelName}` : ""; mutate={mutate}
}, />
) );
.otherwise(() => ""); default:
} return null;
}
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;
} }

View File

@@ -0,0 +1,117 @@
import { NotificationActions } from "../../../action/NotificationActions";
import { SlackReactionPreview } from "../preview/SlackReactionPreview";
import { SlackBotInfo, SlackReaction, SlackUser } from "../types";
import { getAvatarIcon, MutatePromise } from "@raycast/utils";
import { getSlackIconUrl, getSlackUserAvatarUrl } from "..";
import { Notification } from "../../../notification";
import { Icon, Image, List } from "@raycast/api";
import { Page } from "../../../types";
import { match, P } from "ts-pattern";
export type SlackReactionNotificationListItemProps = {
notification: Notification;
slack_reaction: SlackReaction;
mutate: MutatePromise<Page<Notification> | undefined>;
};
export function SlackReactionNotificationListItem({
notification,
slack_reaction,
mutate,
}: SlackReactionNotificationListItemProps) {
const subtitle = getSlackReactionNotificationSubtitle(slack_reaction);
const author = getSlackReactionAuthorAccessory(slack_reaction);
const team = getSlackReactionTeamAccessory(slack_reaction);
const updated_at = "2023-01-01"; // TODO
const accessories: List.Item.Accessory[] = [{ date: new Date(updated_at), tooltip: `Updated at ${updated_at}` }];
if (author) {
accessories.unshift(author);
}
if (team) {
accessories.unshift(team);
}
return (
<List.Item
key={notification.id}
title={notification.title}
icon={{ source: { light: "slack-logo-dark.svg", dark: "slack-logo-light.svg" } }}
subtitle={subtitle}
accessories={accessories}
actions={
<NotificationActions
notification={notification}
detailsTarget={<SlackReactionPreview notification={notification} slack_reaction={slack_reaction} />}
mutate={mutate}
/>
}
/>
);
}
function getSlackReactionNotificationSubtitle(slack_reaction: SlackReaction): string {
return match(slack_reaction.item)
.with(
{
type: P.union("Message", "File"),
content: P.select(),
},
(slack_reaction_item) => {
const channelName = slack_reaction_item.channel?.name;
return channelName ? `#${channelName}` : "";
},
)
.otherwise(() => "");
}
function getSlackReactionAuthorAccessory(slack_reaction: SlackReaction): List.Item.Accessory | null {
return match(slack_reaction.item)
.with(
{
type: "Message",
content: P.select(),
},
(slackMessageDetails) => {
return match(slackMessageDetails.sender)
.with({ type: "User", content: P.select() }, (slackUser: SlackUser) => {
const userAvatarUrl = getSlackUserAvatarUrl(slackUser);
const userName = slackUser.real_name || "Unknown";
return {
icon: userAvatarUrl ? { source: userAvatarUrl, mask: Image.Mask.Circle } : getAvatarIcon(userName),
tooltip: userName,
};
})
.with({ type: "Bot", content: P.select() }, (slackBot: SlackBotInfo) => {
const botAvatarUrl = getSlackIconUrl(slackBot.icons);
return {
icon: botAvatarUrl ? { source: botAvatarUrl, mask: Image.Mask.Circle } : getAvatarIcon(slackBot.name),
tooltip: slackBot.name,
};
})
.otherwise(() => ({ icon: Icon.Person, tooltip: "Unknown" }));
},
)
.otherwise(() => null);
}
function getSlackReactionTeamAccessory(slack_reaction: SlackReaction): List.Item.Accessory | null {
return match(slack_reaction.item)
.with(
{
type: P.union("Message", "File"),
content: P.select(),
},
(slack_reaction_item) => {
const teamName = slack_reaction_item.team.name;
const teamIconUrl = getSlackIconUrl(slack_reaction_item.team.icon);
if (!teamName || !teamIconUrl) {
return null;
}
return { icon: { source: teamIconUrl, mask: Image.Mask.Circle }, tooltip: teamName };
},
)
.otherwise(() => null);
}

View File

@@ -0,0 +1,117 @@
import { NotificationActions } from "../../../action/NotificationActions";
import { SlackStarPreview } from "../preview/SlackStarPreview";
import { getAvatarIcon, MutatePromise } from "@raycast/utils";
import { SlackBotInfo, SlackStar, SlackUser } from "../types";
import { getSlackIconUrl, getSlackUserAvatarUrl } from "..";
import { Notification } from "../../../notification";
import { Icon, Image, List } from "@raycast/api";
import { Page } from "../../../types";
import { match, P } from "ts-pattern";
export type SlackStarNotificationListItemProps = {
notification: Notification;
slack_star: SlackStar;
mutate: MutatePromise<Page<Notification> | undefined>;
};
export function SlackStarNotificationListItem({
notification,
slack_star,
mutate,
}: SlackStarNotificationListItemProps) {
const subtitle = getSlackStarNotificationSubtitle(slack_star);
const author = getSlackStarAuthorAccessory(slack_star);
const team = getSlackStarTeamAccessory(slack_star);
const updated_at = "2023-01-01"; // TODO
const accessories: List.Item.Accessory[] = [{ date: new Date(updated_at), tooltip: `Updated at ${updated_at}` }];
if (author) {
accessories.unshift(author);
}
if (team) {
accessories.unshift(team);
}
return (
<List.Item
key={notification.id}
title={notification.title}
icon={{ source: { light: "slack-logo-dark.svg", dark: "slack-logo-light.svg" } }}
subtitle={subtitle}
accessories={accessories}
actions={
<NotificationActions
notification={notification}
detailsTarget={<SlackStarPreview notification={notification} slack_star={slack_star} />}
mutate={mutate}
/>
}
/>
);
}
function getSlackStarNotificationSubtitle(slack_star: SlackStar): string {
return match(slack_star.item)
.with(
{
type: P.union("Message", "File", "FileComment", "Channel", "Im", "Group"),
content: P.select(),
},
(slack_star_item) => {
const channelName = slack_star_item.channel?.name;
return channelName ? `#${channelName}` : "";
},
)
.otherwise(() => "");
}
function getSlackStarAuthorAccessory(slack_star: SlackStar): List.Item.Accessory | null {
return match(slack_star.item)
.with(
{
type: "Message",
content: P.select(),
},
(slackMessageDetails) => {
return match(slackMessageDetails.sender)
.with({ type: "User", content: P.select() }, (slackUser: SlackUser) => {
const userAvatarUrl = getSlackUserAvatarUrl(slackUser);
const userName = slackUser.real_name || "Unknown";
return {
icon: userAvatarUrl ? { source: userAvatarUrl, mask: Image.Mask.Circle } : getAvatarIcon(userName),
tooltip: userName,
};
})
.with({ type: "Bot", content: P.select() }, (slackBot: SlackBotInfo) => {
const botAvatarUrl = getSlackIconUrl(slackBot.icons);
return {
icon: botAvatarUrl ? { source: botAvatarUrl, mask: Image.Mask.Circle } : getAvatarIcon(slackBot.name),
tooltip: slackBot.name,
};
})
.otherwise(() => ({ icon: Icon.Person, tooltip: "Unknown" }));
},
)
.otherwise(() => null);
}
function getSlackStarTeamAccessory(slack_star: SlackStar): List.Item.Accessory | null {
return match(slack_star.item)
.with(
{
type: P.union("Message", "File", "FileComment", "Channel", "Im", "Group"),
content: P.select(),
},
(slack_star_item) => {
const teamName = slack_star_item.team.name;
const teamIconUrl = getSlackIconUrl(slack_star_item.team.icon);
if (!teamName || !teamIconUrl) {
return null;
}
return { icon: { source: teamIconUrl, mask: Image.Mask.Circle }, tooltip: teamName };
},
)
.otherwise(() => null);
}

View File

@@ -0,0 +1,99 @@
import { NotificationActions } from "../../../action/NotificationActions";
import { SlackThreadPreview } from "../preview/SlackThreadPreview";
import { getAvatarIcon, MutatePromise } from "@raycast/utils";
import { getSlackIconUrl, getSlackUserAvatarUrl } from "..";
import { Notification } from "../../../notification";
import { Icon, Image, List } from "@raycast/api";
import { SlackThread } from "../types";
import { Page } from "../../../types";
export type SlackThreadNotificationListItemProps = {
notification: Notification;
slack_thread: SlackThread;
mutate: MutatePromise<Page<Notification> | undefined>;
};
export function SlackThreadNotificationListItem({
notification,
slack_thread,
mutate,
}: SlackThreadNotificationListItemProps) {
const subtitle = getSlackThreadNotificationSubtitle(slack_thread);
const author = getSlackThreadAuthorAccessory(slack_thread);
const team = getSlackThreadTeamAccessory(slack_thread);
const updated_at = "2023-01-01"; // TODO
const accessories: List.Item.Accessory[] = [{ date: new Date(updated_at), tooltip: `Updated at ${updated_at}` }];
if (author) {
accessories.unshift(author);
}
if (team) {
accessories.unshift(team);
}
return (
<List.Item
key={notification.id}
title={notification.title}
icon={{ source: { light: "slack-logo-dark.svg", dark: "slack-logo-light.svg" } }}
subtitle={subtitle}
accessories={accessories}
actions={
<NotificationActions
notification={notification}
detailsTarget={<SlackThreadPreview notification={notification} slack_thread={slack_thread} />}
mutate={mutate}
/>
}
/>
);
}
function getSlackThreadNotificationSubtitle(slack_thread: SlackThread): string {
const channelName = slack_thread.channel?.name;
return channelName ? `in #${channelName}` : "";
}
function getSlackThreadAuthorAccessory(slack_thread: SlackThread): List.Item.Accessory | null {
const firstUnreadMessage = slack_thread.messages[0];
if (firstUnreadMessage.user) {
const profile = slack_thread.sender_profiles[firstUnreadMessage.user];
if (profile && profile.type === "User") {
const slackUser = profile.content;
const userAvatarUrl = getSlackUserAvatarUrl(slackUser);
const userName = slackUser.real_name || "Unknown";
return {
icon: userAvatarUrl ? { source: userAvatarUrl, mask: Image.Mask.Circle } : getAvatarIcon(userName),
tooltip: userName,
};
}
}
if (firstUnreadMessage.bot_id) {
const profile = slack_thread.sender_profiles[firstUnreadMessage.bot_id];
if (profile && profile.type === "Bot") {
const slackBot = profile.content;
const botAvatarUrl = getSlackIconUrl(slackBot.icons);
return {
icon: botAvatarUrl ? { source: botAvatarUrl, mask: Image.Mask.Circle } : getAvatarIcon(slackBot.name),
tooltip: slackBot.name,
};
}
}
return { icon: Icon.Person, tooltip: "Unknown" };
}
function getSlackThreadTeamAccessory(slack_thread: SlackThread): List.Item.Accessory | null {
const teamName = slack_thread.team.name;
const teamIconUrl = getSlackIconUrl(slack_thread.team.icon);
if (!teamName || !teamIconUrl) {
return null;
}
return { icon: { source: teamIconUrl, mask: Image.Mask.Circle }, tooltip: teamName };
}

View File

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

View File

@@ -1,9 +1,11 @@
import { Notification, getNotificationHtmlUrl } from "../../../notification"; import { getNotificationHtmlUrl, Notification } from "../../../notification";
import { Detail, ActionPanel, Action } from "@raycast/api"; import { Detail, ActionPanel, Action } from "@raycast/api";
import { SlackStar } from "../types";
import { useMemo } from "react"; import { useMemo } from "react";
interface SlackStarPreviewProps { interface SlackStarPreviewProps {
notification: Notification; notification: Notification;
slack_star: SlackStar;
} }
export function SlackStarPreview({ notification }: SlackStarPreviewProps) { export function SlackStarPreview({ notification }: SlackStarPreviewProps) {

View File

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

View File

@@ -1,3 +1,94 @@
import { match, P } from "ts-pattern";
export interface SlackStar {
state: SlackStarState;
created_at: Date;
item: SlackStarItem;
}
export function getSlackStarHtmlUrl(slack_star: SlackStar): string {
return match(slack_star.item)
.with({ type: "Message", content: P.select() }, (message) => message.url)
.with(
{ type: "File", content: P.select() },
(file) => `https://app.slack.com/client/${file.team.id}/${file.channel.id}`,
)
.with(
{ type: "FileComment", content: P.select() },
(fileComment) => `https://app.slack.com/client/${fileComment.team.id}/${fileComment.channel.id}`,
)
.with(
{ type: "Channel", content: P.select() },
(channel) => `https://app.slack.com/client/${channel.team.id}/${channel.channel.id}`,
)
.with({ type: "Im", content: P.select() }, (im) => `https://app.slack.com/client/${im.team.id}/${im.channel.id}`)
.with(
{ type: "Group", content: P.select() },
(group) => `https://app.slack.com/client/${group.team.id}/${group.channel.id}`,
)
.otherwise(() => "");
}
export enum SlackStarState {
StarAdded = "StarAdded",
StarRemoved = "StarRemoved",
}
export type SlackStarItem =
| { type: "Message"; content: SlackMessageDetails }
| { type: "File"; content: SlackFileDetails }
| { type: "FileComment"; content: SlackFileCommentDetails }
| { type: "Channel"; content: SlackChannelDetails }
| { type: "Im"; content: SlackImDetails }
| { type: "Group"; content: SlackGroupDetails };
export interface SlackReaction {
name: string;
state: SlackReactionState;
created_at: Date;
item: SlackReactionItem;
}
export function getSlackReactionHtmlUrl(slack_reaction: SlackReaction): string {
return match(slack_reaction.item)
.with({ type: "Message", content: P.select() }, (message) => message.url)
.with(
{ type: "File", content: P.select() },
(file) => `https://app.slack.com/client/${file.team.id}/${file.channel.id}`,
)
.otherwise(() => "");
}
export enum SlackReactionState {
ReactionAdded = "ReactionAdded",
ReactionRemoved = "ReactionRemoved",
}
export type SlackReactionItem =
| { type: "Message"; content: SlackMessageDetails }
| { type: "File"; content: SlackFileDetails };
export interface SlackThread {
url: string;
messages: Array<SlackHistoryMessage>;
sender_profiles: Record<string, SlackMessageSenderDetails>;
subscribed: boolean;
last_read?: string;
channel: SlackChannelInfo;
team: SlackTeamInfo;
references?: SlackReferences;
}
export function getSlackThreadHtmlUrl(slack_thread: SlackThread): string {
return slack_thread.url;
}
export interface SlackReferences {
channels: Record<string, string | null>;
users: Record<string, string | null>;
usergroups: Record<string, string | null>;
}
export interface SlackPushEventCallback { export interface SlackPushEventCallback {
team_id: string; team_id: string;
api_app_id: string; api_app_id: string;
@@ -51,16 +142,18 @@ export interface SlackHistoryMessage {
channel_type?: string; channel_type?: string;
thread_ts?: string; thread_ts?: string;
client_msg_id?: string; client_msg_id?: string;
user?: string;
bot_id?: string;
text?: string; text?: string;
blocks?: Array<SlackBlock>; blocks?: Array<SlackBlock>;
attachments?: Array<SlackMessageAttachment>; attachments?: Array<SlackMessageAttachment>;
upload?: boolean; upload?: boolean;
files?: Array<SlackFile>; files?: Array<SlackFile>;
reactions?: Array<SlackReaction>; reactions?: Array<SlackReactionDetails>;
} }
export interface SlackReaction { export interface SlackReactionDetails {
name: string; name: string;
count: number; count: number;
users: Array<string>; users: Array<string>;
@@ -184,7 +277,7 @@ export interface SlackFile {
url_private_download?: string; url_private_download?: string;
permalink?: string; permalink?: string;
permalink_public?: string; permalink_public?: string;
reactions?: Array<SlackReaction>; reactions?: Array<SlackReactionDetails>;
editable?: boolean; editable?: boolean;
is_external?: boolean; is_external?: boolean;
is_public?: boolean; is_public?: boolean;

View File

@@ -1,4 +1,4 @@
import { Notification, getNotificationHtmlUrl } from "../../../notification"; import { getNotificationHtmlUrl, Notification } from "../../../notification";
import { Detail, ActionPanel, Action } from "@raycast/api"; import { Detail, ActionPanel, Action } from "@raycast/api";
import { useMemo } from "react"; import { useMemo } from "react";

View File

@@ -0,0 +1,3 @@
export interface TodoistItem {
id: string;
}

View File

@@ -1,53 +1,29 @@
import { import { getThirdPartyItemHtmlUrl, ThirdPartyItem } from "./third_party_item";
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 { MutatePromise } from "@raycast/utils";
import { match, P } from "ts-pattern";
import { Page } from "./types"; import { Page } from "./types";
import { Task } from "./task"; import { Task } from "./task";
export interface Notification { export interface Notification {
id: string; id: string;
title: string; title: string;
source_id: string;
status: NotificationStatus; status: NotificationStatus;
metadata: NotificationMetadata; created_at: Date;
updated_at: Date; updated_at: Date;
last_read_at?: Date; last_read_at?: Date;
snoozed_until?: Date; snoozed_until?: Date;
user_id: string; user_id: string;
task?: Task; task?: Task;
details?: NotificationDetails; kind: NotificationSourceKind;
source_item: ThirdPartyItem;
} }
export type NotificationMetadata = export enum NotificationSourceKind {
| { Github = "Github",
type: "Github" | "Todoist"; Todoist = "Todoist",
// eslint-disable-next-line @typescript-eslint/no-explicit-any Linear = "Linear",
content: any; GoogleMail = "GoogleMail",
} Slack = "Slack",
| { 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 { export enum NotificationStatus {
Unread = "Unread", Unread = "Unread",
@@ -61,42 +37,10 @@ export type NotificationListItemProps = {
mutate: MutatePromise<Page<Notification> | undefined>; mutate: MutatePromise<Page<Notification> | undefined>;
}; };
export function getNotificationHtmlUrl(notification: Notification) { export function getNotificationHtmlUrl(notification: Notification): string {
return match(notification) return getThirdPartyItemHtmlUrl(notification.source_item);
.with({ details: { type: "SlackMessage", content: P.select() } }, (notificationDetails) => notificationDetails.url)
.with(
{
details: {
type: P.union("SlackChannel", "SlackFile", "SlackFileComment", "SlackGroup", "SlackIm"),
content: P.select(),
},
},
(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) { export function isNotificationBuiltFromTask(notification: Notification) {
return notification.metadata.type === "Todoist"; return notification.kind === NotificationSourceKind.Todoist;
} }

58
src/third_party_item.ts Normal file
View File

@@ -0,0 +1,58 @@
import {
getSlackReactionHtmlUrl,
getSlackStarHtmlUrl,
getSlackThreadHtmlUrl,
SlackReaction,
SlackStar,
SlackThread,
} from "./integrations/slack/types";
import {
getLinearIssueHtmlUrl,
getLinearNotificationHtmlUrl,
LinearIssue,
LinearNotification,
} from "./integrations/linear/types";
import { getGithubNotificationHtmlUrl, GithubNotification } from "./integrations/github/types";
import { GoogleMailThread } from "./integrations/google-mail/types";
import { TodoistItem } from "./integrations/todoist/types";
export interface ThirdPartyItem {
id: string;
source_id: string;
data: ThirdPartyItemData;
created_at: Date;
updated_at: Date;
user_id: string;
integration_connection_id: string;
}
export type ThirdPartyItemData =
| { type: "TodoistItem"; content: TodoistItem }
| { type: "SlackStar"; content: SlackStar }
| { type: "SlackReaction"; content: SlackReaction }
| { type: "SlackThread"; content: SlackThread }
| { type: "LinearIssue"; content: LinearIssue }
| { type: "LinearNotification"; content: LinearNotification }
| { type: "GithubNotification"; content: GithubNotification }
| { type: "GoogleMailThread"; content: GoogleMailThread };
export function getThirdPartyItemHtmlUrl(thirdPartyItem: ThirdPartyItem): string {
switch (thirdPartyItem.data.type) {
case "TodoistItem":
return `https://todoist.com/showTask?id=${thirdPartyItem.data.content.id}`;
case "SlackStar":
return getSlackStarHtmlUrl(thirdPartyItem.data.content);
case "SlackReaction":
return getSlackReactionHtmlUrl(thirdPartyItem.data.content);
case "SlackThread":
return getSlackThreadHtmlUrl(thirdPartyItem.data.content);
case "LinearIssue":
return getLinearIssueHtmlUrl(thirdPartyItem.data.content);
case "LinearNotification":
return getLinearNotificationHtmlUrl(thirdPartyItem.data.content);
case "GithubNotification":
return getGithubNotificationHtmlUrl(thirdPartyItem.data.content);
case "GoogleMailThread":
return `https://mail.google.com/mail/u/0/#inbox/${thirdPartyItem.data.content.id}`;
}
}