diff --git a/assets/linear-issue-backlog.svg b/assets/linear-issue-backlog.svg
new file mode 100644
index 0000000..063bbf3
--- /dev/null
+++ b/assets/linear-issue-backlog.svg
@@ -0,0 +1 @@
+
diff --git a/assets/linear-issue-canceled.svg b/assets/linear-issue-canceled.svg
new file mode 100644
index 0000000..b883ea9
--- /dev/null
+++ b/assets/linear-issue-canceled.svg
@@ -0,0 +1 @@
+
diff --git a/assets/linear-issue-completed.svg b/assets/linear-issue-completed.svg
new file mode 100644
index 0000000..6082069
--- /dev/null
+++ b/assets/linear-issue-completed.svg
@@ -0,0 +1 @@
+
diff --git a/assets/linear-issue-started.svg b/assets/linear-issue-started.svg
new file mode 100644
index 0000000..4f943ca
--- /dev/null
+++ b/assets/linear-issue-started.svg
@@ -0,0 +1 @@
+
diff --git a/assets/linear-issue-triage.svg b/assets/linear-issue-triage.svg
new file mode 100644
index 0000000..22811be
--- /dev/null
+++ b/assets/linear-issue-triage.svg
@@ -0,0 +1 @@
+
diff --git a/assets/linear-issue-unstarted.svg b/assets/linear-issue-unstarted.svg
new file mode 100644
index 0000000..2848c90
--- /dev/null
+++ b/assets/linear-issue-unstarted.svg
@@ -0,0 +1 @@
+
diff --git a/src/index.tsx b/src/index.tsx
index c9a14a4..e4df547 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,8 +1,8 @@
import { Action, ActionPanel, Detail, List, getPreferenceValues, openExtensionPreferences } from "@raycast/api";
import { GoogleMailNotificationListItem } from "./integrations/google-mail/GoogleMailNotificationListItem";
import { GithubNotificationListItem } from "./integrations/github/listitem/GithubNotificationListItem";
+import { LinearNotificationListItem } from "./integrations/linear/listitem/LinearNotificationListItem";
import { TodoistNotificationListItem } from "./integrations/todoist/TodoistNotificationListItem";
-import { LinearNotificationListItem } from "./integrations/linear/LinearNotificationListItem";
import { Notification, NotificationListItemProps } from "./notification";
import { NotificationActions } from "./action/NotificationActions";
import { Page, UniversalInboxPreferences } from "./types";
diff --git a/src/integrations/github/listitem/GithubPullRequestNotificationListItem.tsx b/src/integrations/github/listitem/GithubPullRequestNotificationListItem.tsx
index 47c0f1c..1b96540 100644
--- a/src/integrations/github/listitem/GithubPullRequestNotificationListItem.tsx
+++ b/src/integrations/github/listitem/GithubPullRequestNotificationListItem.tsx
@@ -87,7 +87,7 @@ function getGithubPullRequestChecksAccessory(latestCommit: GithubCommitChecks):
case GithubCheckStatusState.Pending:
return { icon: Icon.Pause, tooltip: "Pending" };
case GithubCheckStatusState.InProgress:
- return { icon: Icon.Pause, tooltip: "In progress" }; // TODO Spinner
+ return { icon: Icon.CircleProgress, tooltip: "In progress" };
case GithubCheckStatusState.Completed:
switch (progress.conclusion()) {
case GithubCheckConclusionState.Success:
diff --git a/src/integrations/linear/LinearNotificationListItem.tsx b/src/integrations/linear/LinearNotificationListItem.tsx
deleted file mode 100644
index 1ce4cd0..0000000
--- a/src/integrations/linear/LinearNotificationListItem.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { NotificationActions } from "../../action/NotificationActions";
-import { NotificationListItemProps } from "../../notification";
-import { Detail, List, environment } from "@raycast/api";
-import { useMemo } from "react";
-
-export function LinearNotificationListItem({ notification, mutate }: NotificationListItemProps) {
- const icon = useMemo(() => {
- if (environment.appearance === "dark") {
- return "linear-logo-light.svg";
- }
- return "linear-logo-dark.svg";
- }, [environment]);
-
- return (
- }
- mutate={mutate}
- />
- }
- />
- );
-}
diff --git a/src/integrations/linear/accessories.ts b/src/integrations/linear/accessories.ts
new file mode 100644
index 0000000..9bdffbf
--- /dev/null
+++ b/src/integrations/linear/accessories.ts
@@ -0,0 +1,12 @@
+import { Icon, Image, List } from "@raycast/api";
+import { LinearUser } from "./types";
+
+export function getLinearUserAccessory(user?: LinearUser): List.Item.Accessory {
+ if (user) {
+ return {
+ icon: user.avatar_url ? { source: user.avatar_url, mask: Image.Mask.Circle } : Icon.Person,
+ tooltip: user.name,
+ };
+ }
+ return { icon: Icon.Person, tooltip: "Unknown" };
+}
diff --git a/src/integrations/linear/listitem/LinearIssueNotificationListItem.tsx b/src/integrations/linear/listitem/LinearIssueNotificationListItem.tsx
new file mode 100644
index 0000000..5fbc3b9
--- /dev/null
+++ b/src/integrations/linear/listitem/LinearIssueNotificationListItem.tsx
@@ -0,0 +1,88 @@
+import { NotificationActions } from "../../../action/NotificationActions";
+import { LinearIssuePreview } from "../preview/LinearIssuePreview";
+import { getLinearUserAccessory } from "../accessories";
+import { Notification } from "../../../notification";
+import { LinearIssueNotification } from "../types";
+import { MutatePromise } from "@raycast/utils";
+import { Page } from "../../../types";
+import { List } from "@raycast/api";
+
+interface LinearIssueNotificationListItemProps {
+ icon: string;
+ notification: Notification;
+ linearIssueNotification: LinearIssueNotification;
+ mutate: MutatePromise | undefined>;
+}
+
+export function LinearIssueNotificationListItem({
+ icon,
+ notification,
+ linearIssueNotification,
+ mutate,
+}: LinearIssueNotificationListItemProps) {
+ const subtitle = `${linearIssueNotification.issue.team.name} #${linearIssueNotification.issue.identifier}`;
+
+ const state = getLinearIssueStateAccessory(linearIssueNotification.issue.state);
+ const assignee = getLinearUserAccessory(linearIssueNotification.issue.assignee);
+
+ const accessories: List.Item.Accessory[] = [
+ state,
+ assignee,
+ {
+ date: new Date(linearIssueNotification.updated_at),
+ tooltip: `Updated at ${linearIssueNotification.updated_at}`,
+ },
+ ];
+
+ return (
+ }
+ mutate={mutate}
+ />
+ }
+ />
+ );
+}
+
+export function getLinearIssueStateAccessory(state: LinearWorkflowState): List.Item.Accessory {
+ switch (state.type) {
+ case "Triage":
+ return {
+ icon: { source: "linear-issue-triage.svg", tintColor: state.color },
+ tooltip: state.name,
+ };
+ case "Backlog":
+ return {
+ icon: { source: "linear-issue-backlog.svg", tintColor: state.color },
+ tooltip: state.name,
+ };
+ case "Unstarted":
+ return {
+ icon: { source: "linear-issue-unstarted.svg", tintColor: state.color },
+ tooltip: state.name,
+ };
+ case "Started":
+ return {
+ icon: { source: "linear-issue-started.svg", tintColor: state.color },
+ tooltip: state.name,
+ };
+ case "Completed":
+ return {
+ icon: { source: "linear-issue-completed.svg", tintColor: state.color },
+ tooltip: state.name,
+ };
+ case "Canceled":
+ return {
+ icon: { source: "linear-issue-canceled.svg", tintColor: state.color },
+ tooltip: state.name,
+ };
+ }
+}
diff --git a/src/integrations/linear/listitem/LinearNotificationListItem.tsx b/src/integrations/linear/listitem/LinearNotificationListItem.tsx
new file mode 100644
index 0000000..6a1aebb
--- /dev/null
+++ b/src/integrations/linear/listitem/LinearNotificationListItem.tsx
@@ -0,0 +1,39 @@
+import { LinearProjectNotificationListItem } from "./LinearProjectNotificationListItem";
+import { LinearIssueNotificationListItem } from "./LinearIssueNotificationListItem";
+import { NotificationListItemProps } from "../../../notification";
+import { environment } from "@raycast/api";
+import { useMemo } from "react";
+
+export function LinearNotificationListItem({ notification, mutate }: NotificationListItemProps) {
+ const icon = useMemo(() => {
+ if (environment.appearance === "dark") {
+ return "linear-logo-light.svg";
+ }
+ return "linear-logo-dark.svg";
+ }, [environment]);
+
+ if (notification.metadata.type !== "Linear") return null;
+
+ switch (notification.metadata.content.type) {
+ case "IssueNotification":
+ return (
+
+ );
+ case "ProjectNotification":
+ return (
+
+ );
+ default:
+ return null;
+ }
+}
diff --git a/src/integrations/linear/listitem/LinearProjectNotificationListItem.tsx b/src/integrations/linear/listitem/LinearProjectNotificationListItem.tsx
new file mode 100644
index 0000000..304871b
--- /dev/null
+++ b/src/integrations/linear/listitem/LinearProjectNotificationListItem.tsx
@@ -0,0 +1,53 @@
+import { NotificationActions } from "../../../action/NotificationActions";
+import { LinearProjectPreview } from "../preview/LinearProjectPreview";
+import { getLinearUserAccessory } from "../accessories";
+import { Notification } from "../../../notification";
+import { LinearProjectNotification } from "../types";
+import { MutatePromise } from "@raycast/utils";
+import { Page } from "../../../types";
+import { List } from "@raycast/api";
+
+interface LinearProjectNotificationListItemProps {
+ icon: string;
+ notification: Notification;
+ linearProjectNotification: LinearProjectNotification;
+ mutate: MutatePromise | undefined>;
+}
+
+export function LinearProjectNotificationListItem({
+ icon,
+ notification,
+ linearProjectNotification,
+ mutate,
+}: LinearProjectNotificationListItemProps) {
+ const subtitle = linearProjectNotification.project.name;
+
+ const lead = getLinearUserAccessory(linearProjectNotification.project.lead);
+
+ const accessories: List.Item.Accessory[] = [
+ lead,
+ {
+ date: new Date(linearProjectNotification.updated_at),
+ tooltip: `Updated at ${linearProjectNotification.updated_at}`,
+ },
+ ];
+
+ return (
+
+ }
+ mutate={mutate}
+ />
+ }
+ />
+ );
+}
diff --git a/src/integrations/linear/preview/LinearIssuePreview.tsx b/src/integrations/linear/preview/LinearIssuePreview.tsx
new file mode 100644
index 0000000..136ea92
--- /dev/null
+++ b/src/integrations/linear/preview/LinearIssuePreview.tsx
@@ -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 notification_html_url = useMemo(() => {
+ return getNotificationHtmlUrl(notification);
+ }, [notification]);
+
+ return (
+
+
+
+ }
+ />
+ );
+}
diff --git a/src/integrations/linear/preview/LinearProjectPreview.tsx b/src/integrations/linear/preview/LinearProjectPreview.tsx
new file mode 100644
index 0000000..9be85e1
--- /dev/null
+++ b/src/integrations/linear/preview/LinearProjectPreview.tsx
@@ -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 notification_html_url = useMemo(() => {
+ return getNotificationHtmlUrl(notification);
+ }, [notification]);
+
+ return (
+
+
+
+ }
+ />
+ );
+}
diff --git a/src/integrations/linear/types.ts b/src/integrations/linear/types.ts
new file mode 100644
index 0000000..14f1591
--- /dev/null
+++ b/src/integrations/linear/types.ts
@@ -0,0 +1,116 @@
+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;
+ 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;
+}
diff --git a/src/notification.ts b/src/notification.ts
index 281bc61..05e219a 100644
--- a/src/notification.ts
+++ b/src/notification.ts
@@ -1,5 +1,6 @@
import { GithubDiscussion, GithubPullRequest } from "./integrations/github/types";
import { MutatePromise } from "@raycast/utils";
+import { match, P } from "ts-pattern";
import { Page } from "./types";
import { Task } from "./task";
@@ -40,16 +41,20 @@ export type NotificationListItemProps = {
};
export function getNotificationHtmlUrl(notification: Notification) {
- switch (notification.details?.type) {
- case "GithubPullRequest":
- return notification.details.content.url;
- case "GithubDiscussion":
- return notification.details.content.url;
- default: {
- // TODO
- return "https://github.com";
- }
- }
+ return match(notification)
+ .with(
+ { details: { type: P.union("GithubPullRequest", "GithubDiscussion") } },
+ () => notification.details.content.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,
+ )
+ .exhaustive();
}
export function isNotificationBuiltFromTask(notification: Notification) {