feat: List Github notifications

This commit is contained in:
2024-01-24 08:53:36 +01:00
parent 6775863ed3
commit 2d2d47f55f
41 changed files with 3538 additions and 13 deletions

View File

@@ -0,0 +1,35 @@
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;
}
}

View File

@@ -0,0 +1,76 @@
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";
interface GithubDiscussionNotificationListItemProps {
icon: string;
notification: Notification;
githubDiscussion: GithubDiscussion;
}
export function GithubDiscussionNotificationListItem({
icon,
notification,
githubDiscussion,
}: GithubDiscussionNotificationListItemProps) {
const subtitle = `${githubDiscussion.repository.name_with_owner}`;
const author = getGithubActorAccessory(githubDiscussion.author);
const discussionStatus = getGithubDiscussionStatusAccessory(githubDiscussion.state_reason);
const accessories: List.Item.Accessory[] = [
author,
{ date: new Date(githubDiscussion.updated_at), tooltip: `Updated at ${githubDiscussion.updated_at}` },
];
if (discussionStatus) {
accessories.unshift(discussionStatus);
}
if (githubDiscussion.comments_count > 0) {
accessories.unshift({
text: githubDiscussion.comments_count.toString(),
icon: Icon.Bubble,
tooltip: `${githubDiscussion.comments_count} comments`,
});
}
return (
<List.Item
key={notification.id}
title={notification.title}
icon={icon}
subtitle={subtitle}
accessories={accessories}
actions={
<NotificationActions
notification={notification}
detailsTarget={<GithubDiscussionPreview notification={notification} githubDiscussion={githubDiscussion} />}
/>
}
/>
);
}
function getGithubDiscussionStatusAccessory(stateReason?: GithubDiscussionStateReason): List.Item.Accessory {
switch (stateReason) {
case GithubDiscussionStateReason.Duplicate:
return {
icon: { source: "github-discussion-duplicate.svg", tintColor: Color.SecondaryText },
tooltip: "Answered",
};
case GithubDiscussionStateReason.Outdated:
return {
icon: { source: "github-discussion-outdated.svg", tintColor: Color.SecondaryText },
tooltip: "Answered",
};
case GithubDiscussionStateReason.Reopened:
return { icon: { source: "github-discussion-opened.svg", tintColor: Color.Green }, tooltip: "Answered" };
case GithubDiscussionStateReason.Resolved:
return { icon: { source: "github-discussion-closed.svg", tintColor: Color.Magenta }, tooltip: "Answered" };
default:
return { icon: { source: "github-discussion-opened.svg", tintColor: Color.Green }, tooltip: "Answered" };
}
}

View File

@@ -0,0 +1,196 @@
import { Color, Icon, List } from "@raycast/api";
import { NotificationActions } from "../../../NotificationActions";
import { Notification } from "../../../types";
import { GithubPullRequestPreview } from "../preview/GithubPullRequestPreview";
import {
GithubPullRequestState,
GithubPullRequestReviewDecision,
GithubCheckConclusionState,
GithubPullRequest,
GithubCommitChecks,
GithubCheckStatusState,
GithubCheckSuite,
} from "../types";
import { getGithubActorAccessory } from "../misc";
interface GithubPullRequestNotificationListItemProps {
icon: string;
notification: Notification;
githubPullRequest: GithubPullRequest;
}
export function GithubPullRequestNotificationListItem({
icon,
notification,
githubPullRequest,
}: GithubPullRequestNotificationListItemProps) {
const subtitle = `${githubPullRequest.head_repository?.name_with_owner} #${githubPullRequest.number}`;
const author = getGithubActorAccessory(githubPullRequest.author);
const prChecks = getGithubPullRequestChecksAccessory(githubPullRequest.latest_commit);
const review = getGithubPullRequestReviewAccessory(githubPullRequest.review_decision);
const prStatus = getGithubPullRequestStateAccessory(githubPullRequest);
const accessories: List.Item.Accessory[] = [
author,
{
date: new Date(githubPullRequest.updated_at),
tooltip: `Updated at ${githubPullRequest.updated_at}`,
},
];
if (prStatus) {
accessories.unshift(prStatus);
}
if (githubPullRequest.comments_count > 0) {
accessories.unshift({
text: githubPullRequest.comments_count.toString(),
icon: Icon.Bubble,
tooltip: `${githubPullRequest.comments_count} comments`,
});
}
if (review) {
accessories.unshift(review);
}
if (prChecks) {
accessories.unshift(prChecks);
}
return (
<List.Item
key={notification.id}
title={notification.title}
icon={icon}
accessories={accessories}
subtitle={subtitle}
actions={
<NotificationActions
notification={notification}
detailsTarget={<GithubPullRequestPreview notification={notification} githubPullRequest={githubPullRequest} />}
/>
}
/>
);
}
function getGithubPullRequestChecksAccessory(latestCommit: GithubCommitChecks): List.Item.Accessory | null {
const progress = computePullRequestChecksProgress(latestCommit.check_suites);
if (!progress) {
return null;
}
switch (progress.status()) {
case GithubCheckStatusState.Pending:
return { icon: Icon.Pause, tooltip: "Pending" };
case GithubCheckStatusState.InProgress:
return { icon: Icon.Pause, tooltip: "In progress" }; // TODO Spinner
case GithubCheckStatusState.Completed:
switch (progress.conclusion()) {
case GithubCheckConclusionState.Success:
return { icon: Icon.CheckCircle, tooltip: "Success" };
case GithubCheckConclusionState.Failure:
return { icon: Icon.XMarkCircle, tooltip: "Failure" };
default:
return { icon: Icon.QuestionMarkCircle, tooltip: "Neutral" };
}
default:
return { icon: Icon.QuestionMarkCircle, tooltip: "Neutral" };
}
}
class GithubChecksProgress {
checksCount = 0;
completedChecksCount = 0;
failedChecksCount = 0;
status(): GithubCheckStatusState {
if (this.completedChecksCount === 0) {
return GithubCheckStatusState.Pending;
}
if (this.completedChecksCount === this.checksCount) {
return GithubCheckStatusState.Completed;
}
return GithubCheckStatusState.InProgress;
}
conclusion(): GithubCheckConclusionState {
if (this.status() === GithubCheckStatusState.InProgress) {
return GithubCheckConclusionState.Neutral;
}
if (this.failedChecksCount > 0) {
return GithubCheckConclusionState.Failure;
}
return GithubCheckConclusionState.Success;
}
}
function computePullRequestChecksProgress(checkSuites?: Array<GithubCheckSuite>): GithubChecksProgress | null {
if (checkSuites) {
const progress = new GithubChecksProgress();
for (const checkSuite of checkSuites) {
if (checkSuite.status !== GithubCheckStatusState.Queued) {
for (const checkRun of checkSuite.check_runs) {
progress.checksCount += 1;
if (checkRun.status === GithubCheckStatusState.Completed) {
progress.completedChecksCount += 1;
if (checkRun.conclusion && checkRun.conclusion !== GithubCheckConclusionState.Success) {
progress.failedChecksCount += 1;
}
}
}
}
}
if (progress.checksCount === 0) {
return null;
}
return progress;
}
return null;
}
function getGithubPullRequestReviewAccessory(
reviewDecision?: GithubPullRequestReviewDecision,
): List.Item.Accessory | null {
switch (reviewDecision) {
case GithubPullRequestReviewDecision.Approved:
return { tag: { value: "Approved", color: Color.Green } };
case GithubPullRequestReviewDecision.ChangesRequested:
return { tag: { value: "Changes requested", color: Color.Red } };
case GithubPullRequestReviewDecision.ReviewRequired:
return { tag: { value: "Review required", color: Color.Orange } };
default:
return null;
}
}
function getGithubPullRequestStateAccessory(githubPullRequest: GithubPullRequest): List.Item.Accessory | null {
switch (githubPullRequest.state) {
case GithubPullRequestState.Open:
if (githubPullRequest.is_draft) {
return {
icon: {
source: "github-pullrequest-draft.svg",
tintColor: Color.SecondaryText,
},
};
}
return {
icon: { source: "github-pullrequest.svg", tintColor: Color.Green },
};
case GithubPullRequestState.Closed:
return {
icon: {
source: "github-pullrequest-closed.svg",
tintColor: Color.SecondaryText,
},
};
case GithubPullRequestState.Merged:
return {
icon: { source: "github-pullrequest.svg", tintColor: Color.Magenta },
};
default:
return null;
}
}

View File

@@ -0,0 +1,12 @@
import { Icon, Image, List } from "@raycast/api";
import { GithubActor, getGithubActorName } from "./types";
export function getGithubActorAccessory(actor?: GithubActor): List.Item.Accessory {
if (actor) {
return {
icon: actor.content.avatar_url ? { source: actor.content.avatar_url, mask: Image.Mask.Circle } : Icon.Person,
tooltip: getGithubActorName(actor),
};
}
return { icon: Icon.Person, tooltip: "Unknown" };
}

View File

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

View File

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

View File

@@ -0,0 +1,221 @@
export interface GithubPullRequest {
id: string;
number: number;
url: string;
title: string;
body: string;
state: GithubPullRequestState;
is_draft: boolean;
closed_at?: Date;
created_at: Date;
updated_at: Date;
merged_at?: Date;
mergeable_state: GithubMergeableState;
merge_state_status: GithubMergeStateStatus;
merged_by?: GithubActor;
deletions: number;
additions: number;
changed_files: number;
labels: Array<GithubLabel>;
comments_count: number;
comments: Array<GithubIssueComment>;
latest_commit: GithubCommitChecks;
base_ref_name: string;
base_repository?: GithubRepositorySummary;
head_ref_name: string;
head_repository?: GithubRepositorySummary;
author?: GithubActor;
assignees: Array<GithubActor>;
review_decision?: GithubPullRequestReviewDecision;
reviews: Array<GithubPullRequestReview>;
review_requests: Array<GithubReviewer>;
}
export enum GithubPullRequestState {
Open = "Open",
Closed = "Closed",
Merged = "Merged",
}
export enum GithubMergeableState {
Unknown = "Unknown",
Mergeable = "Mergeable",
Conflicting = "Conflicting",
}
export enum GithubMergeStateStatus {
Behind = "Behind",
Blocked = "Blocked",
Clean = "Clean",
Dirty = "Dirty",
Draft = "Draft",
HasHooks = "HasHooks",
Unknown = "Unknown",
Unstable = "Unstable",
}
export type GithubActor =
| { type: "GithubUserSummary"; content: GithubUserSummary }
| { type: "GithubBotSummary"; content: GithubBotSummary };
export function getGithubActorName(actor?: GithubActor): string {
if (!actor) {
return "Unknown";
}
switch (actor.type) {
case "GithubUserSummary":
return actor.content.name ? actor.content.name : actor.content.login;
case "GithubBotSummary":
return actor.content.login;
}
}
export interface GithubUserSummary {
name?: string;
login: string;
avatar_url: string;
}
export interface GithubBotSummary {
login: string;
avatar_url: string;
}
export interface GithubTeamSummary {
avatar_url?: string;
name: string;
}
export interface GithubMannequinSummary {
avatar_url: string;
login: string;
}
export interface GithubLabel {
name: string;
color: string;
description?: string;
}
export interface GithubIssueComment {
url: string;
body: string;
created_at: Date;
author?: GithubActor;
}
export interface GithubCommitChecks {
git_commit_id: string;
check_suites?: Array<GithubCheckSuite>;
}
export interface GithubCheckSuite {
check_runs: Array<GithubCheckRun>;
conclusion?: GithubCheckConclusionState;
status: GithubCheckStatusState;
workflow?: GithubWorkflow;
app: GithubCheckSuiteApp;
}
export interface GithubCheckRun {
name: string;
conclusion?: GithubCheckConclusionState;
status: GithubCheckStatusState;
url?: string;
}
export enum GithubCheckConclusionState {
ActionRequired = "ActionRequired",
Cancelled = "Cancelled",
Failure = "Failure",
Neutral = "Neutral",
Skipped = "Skipped",
Stale = "Stale",
StartupFailure = "StartupFailure",
Success = "Success",
TimedOut = "TimedOut",
}
export enum GithubCheckStatusState {
Completed = "Completed",
InProgress = "InProgress",
Pending = "Pending",
Queued = "Queued",
Requested = "Requested",
Waiting = "Waiting",
}
export interface GithubWorkflow {
name: string;
url: string;
}
export interface GithubCheckSuiteApp {
name: string;
logo_url?: string;
url: string;
}
export interface GithubRepositorySummary {
name_with_owner: string;
url: string;
}
export enum GithubPullRequestReviewDecision {
Approved = "Approved",
ChangesRequested = "ChangesRequested",
ReviewRequired = "ReviewRequired",
}
export interface GithubPullRequestReview {
author?: GithubActor;
body: string;
state: GithubPullRequestReviewState;
}
export enum GithubPullRequestReviewState {
Approved = "Approved",
ChangesRequested = "ChangesRequested",
Commented = "Commented",
Dismissed = "Dismissed",
Pending = "Pending",
}
export type GithubReviewer =
| { type: "GithubUserSummary"; content: GithubUserSummary }
| { type: "GithubTeamSummary"; content: GithubTeamSummary }
| { type: "GithubBotSummary"; content: GithubBotSummary }
| { type: "GithubMannequinSummary"; content: GithubMannequinSummary };
export interface GithubDiscussion {
id: string;
number: number;
url: string;
title: string;
body: string;
repository: GithubRepositorySummary;
state_reason?: GithubDiscussionStateReason;
closed_at?: Date;
created_at: Date;
updated_at: Date;
labels: Array<GithubLabel>;
comments_count: number;
author?: GithubActor;
answer_chosen_at?: Date;
answer_chosen_by?: GithubActor;
answer?: GithubDiscussionComment;
}
export enum GithubDiscussionStateReason {
Duplicate = "Duplicate",
Outdated = "Outdated",
Reopened = "Reopened",
Resolved = "Resolved",
}
export interface GithubDiscussionComment {
url: string;
body: string;
created_at: Date;
author?: GithubActor;
}

View File

@@ -0,0 +1,25 @@
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 👋" />} />
}
/>
);
}

View File

@@ -0,0 +1,25 @@
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 👋" />} />
}
/>
);
}

View File

@@ -0,0 +1,28 @@
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 👋" />}
/>
}
/>
);
}