feat: List Github notifications
This commit is contained in:
73
src/NotificationActions.tsx
Normal file
73
src/NotificationActions.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Action, ActionPanel, Icon } from "@raycast/api";
|
||||
import { useMemo, ReactElement } from "react";
|
||||
import { getNotificationHtmlUrl } from "./notification";
|
||||
import { Notification } from "./types";
|
||||
|
||||
function deleteNotification(notification: Notification) {
|
||||
console.log(`Deleting notification ${notification.id}`);
|
||||
}
|
||||
|
||||
function unsubscribeFromNotification(notification: Notification) {
|
||||
console.log(`Unsubcribing from notification ${notification.id}`);
|
||||
}
|
||||
|
||||
function snoozeNotification(notification: Notification) {
|
||||
console.log(`Snoozing notification ${notification.id}`);
|
||||
}
|
||||
|
||||
function createTaskFromNotification(notification: Notification) {
|
||||
console.log(`Creating task notification ${notification.id}`);
|
||||
}
|
||||
|
||||
function linkNotificationToTask(notification: Notification) {
|
||||
console.log(`Linking notification ${notification.id}`);
|
||||
}
|
||||
|
||||
export function NotificationActions({
|
||||
notification,
|
||||
detailsTarget,
|
||||
}: {
|
||||
notification: Notification;
|
||||
detailsTarget: ReactElement;
|
||||
}) {
|
||||
const notification_html_url = useMemo(() => {
|
||||
return getNotificationHtmlUrl(notification);
|
||||
}, [notification]);
|
||||
|
||||
return (
|
||||
<ActionPanel>
|
||||
<Action.Push title="Show Details" target={detailsTarget} />
|
||||
<Action.OpenInBrowser url={notification_html_url} />
|
||||
<Action
|
||||
title="Delete Notification"
|
||||
icon={Icon.Trash}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "d" }}
|
||||
onAction={() => deleteNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Unsubscribe From Notification"
|
||||
icon={Icon.BellDisabled}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "u" }}
|
||||
onAction={() => unsubscribeFromNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Snooze"
|
||||
icon={Icon.Clock}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "s" }}
|
||||
onAction={() => snoozeNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Create Task"
|
||||
icon={Icon.Calendar}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "d" }}
|
||||
onAction={() => createTaskFromNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Link to Task"
|
||||
icon={Icon.Link}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "d" }}
|
||||
onAction={() => linkNotificationToTask(notification)}
|
||||
/>
|
||||
</ActionPanel>
|
||||
);
|
||||
}
|
||||
63
src/NotificationTaskActions.tsx
Normal file
63
src/NotificationTaskActions.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Action, ActionPanel, Icon } from "@raycast/api";
|
||||
import { useMemo, ReactElement } from "react";
|
||||
import { getNotificationHtmlUrl } from "./notification";
|
||||
import { Notification } from "./types";
|
||||
|
||||
function deleteNotification(notification: Notification) {
|
||||
console.log(`Deleting notification ${notification.id}`);
|
||||
}
|
||||
|
||||
function unsubscribeFromNotification(notification: Notification) {
|
||||
console.log(`Unsubcribing from notification ${notification.id}`);
|
||||
}
|
||||
|
||||
function snoozeNotification(notification: Notification) {
|
||||
console.log(`Snoozing notification ${notification.id}`);
|
||||
}
|
||||
|
||||
function completeTask(notification: Notification) {
|
||||
console.log(`Completing task ${notification.id}`);
|
||||
}
|
||||
|
||||
export function NotificationTaskActions({
|
||||
notification,
|
||||
detailsTarget,
|
||||
}: {
|
||||
notification: Notification;
|
||||
detailsTarget: ReactElement;
|
||||
}) {
|
||||
const notification_html_url = useMemo(() => {
|
||||
return getNotificationHtmlUrl(notification);
|
||||
}, [notification]);
|
||||
|
||||
return (
|
||||
<ActionPanel>
|
||||
<Action.Push title="Show Details" target={detailsTarget} />
|
||||
<Action.OpenInBrowser url={notification_html_url} />
|
||||
<Action
|
||||
title="Delete Notification"
|
||||
icon={Icon.Trash}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "d" }}
|
||||
onAction={() => deleteNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Unsubscribe From Notification"
|
||||
icon={Icon.BellDisabled}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "u" }}
|
||||
onAction={() => unsubscribeFromNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Snooze"
|
||||
icon={Icon.Clock}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "s" }}
|
||||
onAction={() => snoozeNotification(notification)}
|
||||
/>
|
||||
<Action
|
||||
title="Complete Task"
|
||||
icon={Icon.Calendar}
|
||||
shortcut={{ modifiers: ["ctrl"], key: "c" }}
|
||||
onAction={() => completeTask(notification)}
|
||||
/>
|
||||
</ActionPanel>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,75 @@
|
||||
import { ActionPanel, Detail, List, Action } from "@raycast/api";
|
||||
import { Action, ActionPanel, Detail, List, getPreferenceValues, openExtensionPreferences } from "@raycast/api";
|
||||
import { useFetch } from "@raycast/utils";
|
||||
import { NotificationActions } from "./NotificationActions";
|
||||
import { GithubNotificationListItem } from "./integrations/github/GithubNotificationListItem";
|
||||
import { GoogleMailNotificationListItem } from "./integrations/google-mail/GoogleMailNotificationListItem";
|
||||
import { LinearNotificationListItem } from "./integrations/linear/LinearNotificationListItem";
|
||||
import { TodoistNotificationListItem } from "./integrations/todoist/TodoistNotificationListItem";
|
||||
import { Notification, NotificationListItemProps, Page, UniversalInboxPreferences } from "./types";
|
||||
|
||||
export default function Command() {
|
||||
return (
|
||||
<List>
|
||||
<List.Item
|
||||
icon="list-icon.png"
|
||||
title="Greeting"
|
||||
const preferences = getPreferenceValues<UniversalInboxPreferences>();
|
||||
|
||||
if (
|
||||
preferences.apiKey === undefined ||
|
||||
preferences.apiKey === ""
|
||||
/* preferences.universalInboxBaseUrl === undefined ||
|
||||
* preferences.universalInboxBaseUrl === "" */
|
||||
) {
|
||||
return (
|
||||
<Detail
|
||||
markdown={"API key incorrect. Please update it in extension preferences and try again."}
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<Action.Push title="Show Details" target={<Detail markdown="# Hey! 👋" />} />
|
||||
<Action title="Open Extension Preferences" onAction={openExtensionPreferences} />
|
||||
</ActionPanel>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const { isLoading, data } = useFetch<Page<Notification>>(
|
||||
`${preferences.universalInboxBaseUrl}/api/notifications?status=Unread,Read&with_tasks=true`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${preferences.apiKey}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<List isLoading={isLoading}>
|
||||
{data?.content.map((notification: Notification) => {
|
||||
return <NotificationListItem key={notification.id} notification={notification} />;
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationListItem({ notification }: NotificationListItemProps) {
|
||||
switch (notification.metadata.type) {
|
||||
case "Github":
|
||||
return <GithubNotificationListItem notification={notification} />;
|
||||
case "Linear":
|
||||
return <LinearNotificationListItem notification={notification} />;
|
||||
case "GoogleMail":
|
||||
return <GoogleMailNotificationListItem notification={notification} />;
|
||||
case "Todoist":
|
||||
return <TodoistNotificationListItem notification={notification} />;
|
||||
default:
|
||||
return <DefaultNotificationListItem notification={notification} />;
|
||||
}
|
||||
}
|
||||
|
||||
function DefaultNotificationListItem({ notification }: NotificationListItemProps) {
|
||||
return (
|
||||
<List.Item
|
||||
key={notification.id}
|
||||
title={notification.title}
|
||||
subtitle={`#${notification.source_id}`}
|
||||
actions={
|
||||
<NotificationActions notification={notification} detailsTarget={<Detail markdown="# To be implemented 👋" />} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
35
src/integrations/github/GithubNotificationListItem.tsx
Normal file
35
src/integrations/github/GithubNotificationListItem.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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" };
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
12
src/integrations/github/misc.tsx
Normal file
12
src/integrations/github/misc.tsx
Normal 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" };
|
||||
}
|
||||
27
src/integrations/github/preview/GithubDiscussionPreview.tsx
Normal file
27
src/integrations/github/preview/GithubDiscussionPreview.tsx
Normal 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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
27
src/integrations/github/preview/GithubPullRequestPreview.tsx
Normal file
27
src/integrations/github/preview/GithubPullRequestPreview.tsx
Normal 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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
221
src/integrations/github/types.ts
Normal file
221
src/integrations/github/types.ts
Normal 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;
|
||||
}
|
||||
@@ -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 👋" />} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
25
src/integrations/linear/LinearNotificationListItem.tsx
Normal file
25
src/integrations/linear/LinearNotificationListItem.tsx
Normal 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 👋" />} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
28
src/integrations/todoist/TodoistNotificationListItem.tsx
Normal file
28
src/integrations/todoist/TodoistNotificationListItem.tsx
Normal 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 👋" />}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
14
src/notification.ts
Normal file
14
src/notification.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Notification } from "./types";
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
48
src/types.ts
Normal file
48
src/types.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { GithubDiscussion, GithubPullRequest } from "./integrations/github/types";
|
||||
|
||||
export interface UniversalInboxPreferences {
|
||||
apiKey: string;
|
||||
universalInboxBaseUrl: string;
|
||||
}
|
||||
|
||||
export interface Page<T> {
|
||||
page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
content: Array<T>;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
title: string;
|
||||
source_id: string;
|
||||
status: NotificationStatus;
|
||||
metadata: NotificationMetadata;
|
||||
updated_at: Date;
|
||||
last_read_at?: Date;
|
||||
snoozed_until?: Date;
|
||||
user_id: string;
|
||||
task_id?: string;
|
||||
details?: NotificationDetails;
|
||||
}
|
||||
|
||||
export type NotificationMetadata = {
|
||||
type: "Github" | "Linear" | "GoogleMail" | "Todoist";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
content: any;
|
||||
};
|
||||
|
||||
export type NotificationDetails =
|
||||
| { type: "GithubPullRequest"; content: GithubPullRequest }
|
||||
| { type: "GithubDiscussion"; content: GithubDiscussion };
|
||||
|
||||
export enum NotificationStatus {
|
||||
Unread = "Unread",
|
||||
Read = "Read",
|
||||
Deleted = "Deleted",
|
||||
Unsubscribed = "Unsubscribed",
|
||||
}
|
||||
|
||||
export type NotificationListItemProps = {
|
||||
notification: Notification;
|
||||
};
|
||||
Reference in New Issue
Block a user