feat: Add Linear notification list item

This commit is contained in:
2024-01-26 21:22:47 +01:00
parent 0507722ecf
commit 3df827ebd7
17 changed files with 383 additions and 41 deletions

View File

@@ -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";

View File

@@ -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:

View File

@@ -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 (
<List.Item
key={notification.id}
title={notification.title}
icon={icon}
subtitle={`#${notification.source_id}`}
actions={
<NotificationActions
notification={notification}
detailsTarget={<Detail markdown="# To be implemented 👋" />}
mutate={mutate}
/>
}
/>
);
}

View File

@@ -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" };
}

View File

@@ -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<Page<Notification> | 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 (
<List.Item
key={notification.id}
title={notification.title}
icon={icon}
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 {
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,
};
}
}

View File

@@ -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 (
<LinearIssueNotificationListItem
icon={icon}
notification={notification}
linearIssueNotification={notification.metadata.content.content}
mutate={mutate}
/>
);
case "ProjectNotification":
return (
<LinearProjectNotificationListItem
icon={icon}
notification={notification}
LinearProjectNotification={notification.metadata.content.content}
mutate={mutate}
/>
);
default:
return null;
}
}

View File

@@ -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<Page<Notification> | 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 (
<List.Item
key={notification.id}
title={notification.title}
icon={icon}
accessories={accessories}
subtitle={subtitle}
actions={
<NotificationActions
notification={notification}
detailsTarget={
<LinearProjectPreview notification={notification} linearProject={linearProjectNotification.issue} />
}
mutate={mutate}
/>
}
/>
);
}

View File

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

View File

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

View File

@@ -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<LinearLabel>;
description: string;
team: LinearTeam;
}
export enum LinearIssuePriority {
NoPriority = 0,
Urgent = 1,
High = 2,
Normal = 3,
Low = 4,
}
export interface LinearProject {
id: string;
name: string;
url: string;
description: string;
icon?: string;
color: string;
state: LinearProjectState;
progress: number; // percentage between 0 and 100
start_date?: Date;
target_date?: Date;
lead?: LinearUser;
}
export enum LinearProjectState {
Planned = "Planned",
Backlog = "Backlog",
Started = "Started",
Paused = "Paused",
Completed = "Completed",
Canceled = "Canceled",
}
export interface LinearProjectMilestone {
name: string;
description?: string;
}
export interface LinearUser {
name: string;
avatar_url?: string;
url: string;
}
export interface LinearWorkflowState {
name: string;
description?: string;
color: string;
type: LinearWorkflowStateType;
}
export enum LinearWorkflowStateType {
Triage = "Triage",
Backlog = "Backlog",
Unstarted = "Unstarted",
Started = "Started",
Completed = "Completed",
Canceled = "Canceled",
}
export interface LinearLabel {
name: string;
description?: string;
color: string;
}
export interface LinearTeam {
id: string;
key: string;
name: string;
}
export interface LinearProjectNotification {
id: string;
type: string;
read_at?: Date;
updated_at: Date;
snoozed_until_at?: Date;
organization: LinearOrganization;
project: LinearProject;
}

View File

@@ -1,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) {