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,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>
);
}

View 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>
);
}

View File

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

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 👋" />}
/>
}
/>
);
}

14
src/notification.ts Normal file
View 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
View 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;
};