feat: Add Todoist notification list item

This commit is contained in:
2024-01-27 00:09:50 +01:00
parent e0f90b0c42
commit ea40258f48
29 changed files with 146 additions and 106 deletions

View File

@@ -1,15 +1,15 @@
import { Action, useNavigation, ActionPanel, Form, Icon, getPreferenceValues, showToast, Toast } from "@raycast/api";
import { MutatePromise, useForm, FormValidation, useFetch } from "@raycast/utils";
import { Page, UniversalInboxPreferences } from "../types";
import { default as dayjs, extend } from "dayjs";
import { Notification } from "../notification";
import { TaskPriority } from "../task";
import { handleErrors } from "../api";
import utc from "dayjs/plugin/utc";
import { useState } from "react";
import fetch from "node-fetch";
import dayjs from "dayjs";
dayjs.extend(utc);
extend(utc);
interface CreateTaskFromNotificationProps {
notification: Notification;

View File

@@ -3,15 +3,15 @@ import { Action, ActionPanel, Icon, getPreferenceValues, showToast, Toast } from
import { CreateTaskFromNotification } from "./CreateTaskFromNotification";
import { LinkNotificationToTask } from "./LinkNotificationToTask";
import { Page, UniversalInboxPreferences } from "../types";
import { default as dayjs, extend } from "dayjs";
import { MutatePromise } from "@raycast/utils";
import { useMemo, ReactElement } from "react";
import { handleErrors } from "../api";
import { TaskStatus } from "../task";
import utc from "dayjs/plugin/utc";
import fetch from "node-fetch";
import dayjs from "dayjs";
dayjs.extend(utc);
extend(utc);
interface NotificationActionsProps {
notification: Notification;
@@ -20,14 +20,13 @@ interface NotificationActionsProps {
}
export function NotificationActions({ notification, detailsTarget, mutate }: NotificationActionsProps) {
const notification_html_url = useMemo(() => {
const notificationHtmlUrl = useMemo(() => {
return getNotificationHtmlUrl(notification);
}, [notification]);
console.log("notification_html_url", notification_html_url, notification);
return (
<ActionPanel>
<Action.OpenInBrowser url={notification_html_url} />
<Action.OpenInBrowser url={notificationHtmlUrl} />
<Action.Push title="Show Details" target={detailsTarget} />
<Action
title="Delete Notification"

View File

@@ -25,14 +25,15 @@ interface NotificationTaskActionsProps {
detailsTarget: ReactElement;
mutate: MutatePromise<Page<Notification> | undefined>;
}
export function NotificationTaskActions({ notification, detailsTarget }: NotificationTaskActionsProps) {
const notification_html_url = useMemo(() => {
const notificationHtmlUrl = useMemo(() => {
return getNotificationHtmlUrl(notification);
}, [notification]);
return (
<ActionPanel>
<Action.OpenInBrowser url={notification_html_url} />
<Action.OpenInBrowser url={notificationHtmlUrl} />
<Action.Push title="Show Details" target={detailsTarget} />
<Action
title="Delete Notification"

View File

@@ -1,8 +1,8 @@
import { GoogleMailNotificationListItem } from "./integrations/google-mail/listitem/GoogleMailNotificationListItem";
import { Action, ActionPanel, Detail, List, getPreferenceValues, openExtensionPreferences } from "@raycast/api";
import { TodoistNotificationListItem } from "./integrations/todoist/listitem/TodoistNotificationListItem";
import { GithubNotificationListItem } from "./integrations/github/listitem/GithubNotificationListItem";
import { LinearNotificationListItem } from "./integrations/linear/listitem/LinearNotificationListItem";
import { TodoistNotificationListItem } from "./integrations/todoist/TodoistNotificationListItem";
import { Notification, NotificationListItemProps } from "./notification";
import { NotificationActions } from "./action/NotificationActions";
import { Page, UniversalInboxPreferences } from "./types";

View File

@@ -1,10 +1,13 @@
import { GithubActor, getGithubActorName } from "./types";
import { Icon, Image, List } from "@raycast/api";
import { getAvatarIcon } from "@raycast/utils";
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,
icon: actor.content.avatar_url
? { source: actor.content.avatar_url, mask: Image.Mask.Circle }
: getAvatarIcon(getGithubActorName(actor)),
tooltip: getGithubActorName(actor),
};
}

View File

@@ -8,14 +8,12 @@ import { MutatePromise } from "@raycast/utils";
import { Page } from "../../../types";
interface GithubDiscussionNotificationListItemProps {
icon: string;
notification: Notification;
githubDiscussion: GithubDiscussion;
mutate: MutatePromise<Page<Notification> | undefined>;
}
export function GithubDiscussionNotificationListItem({
icon,
notification,
githubDiscussion,
mutate,
@@ -45,7 +43,7 @@ export function GithubDiscussionNotificationListItem({
<List.Item
key={notification.id}
title={notification.title}
icon={icon}
icon={{ source: { light: "github-logo-dark.svg", dark: "github-logo-light.svg" } }}
subtitle={subtitle}
accessories={accessories}
actions={

View File

@@ -1,22 +1,12 @@
import { GithubPullRequestNotificationListItem } from "./GithubPullRequestNotificationListItem";
import { GithubDiscussionNotificationListItem } from "./GithubDiscussionNotificationListItem";
import { NotificationListItemProps } from "../../../notification";
import { environment } from "@raycast/api";
import { useMemo } from "react";
export function GithubNotificationListItem({ notification, mutate }: 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}
mutate={mutate}
@@ -25,7 +15,6 @@ export function GithubNotificationListItem({ notification, mutate }: Notificatio
case "GithubDiscussion":
return (
<GithubDiscussionNotificationListItem
icon={icon}
notification={notification}
githubDiscussion={notification.details.content}
mutate={mutate}

View File

@@ -16,14 +16,12 @@ import { MutatePromise } from "@raycast/utils";
import { Page } from "../../../types";
interface GithubPullRequestNotificationListItemProps {
icon: string;
notification: Notification;
githubPullRequest: GithubPullRequest;
mutate: MutatePromise<Page<Notification> | undefined>;
}
export function GithubPullRequestNotificationListItem({
icon,
notification,
githubPullRequest,
mutate,
@@ -64,7 +62,7 @@ export function GithubPullRequestNotificationListItem({
<List.Item
key={notification.id}
title={notification.title}
icon={icon}
icon={{ source: { light: "github-logo-dark.svg", dark: "github-logo-light.svg" } }}
accessories={accessories}
subtitle={subtitle}
actions={

View File

@@ -9,7 +9,7 @@ interface GithubDiscussionPreviewProps {
}
export function GithubDiscussionPreview({ notification, githubDiscussion }: GithubDiscussionPreviewProps) {
const notification_html_url = useMemo(() => {
const notificationHtmlUrl = useMemo(() => {
return getNotificationHtmlUrl(notification);
}, [notification]);
@@ -18,7 +18,7 @@ export function GithubDiscussionPreview({ notification, githubDiscussion }: Gith
markdown={`# ${githubDiscussion.title}`}
actions={
<ActionPanel>
<Action.OpenInBrowser url={notification_html_url} />
<Action.OpenInBrowser url={notificationHtmlUrl} />
</ActionPanel>
}
/>

View File

@@ -9,7 +9,7 @@ interface GithubPullRequestPreviewProps {
}
export function GithubPullRequestPreview({ notification, githubPullRequest }: GithubPullRequestPreviewProps) {
const notification_html_url = useMemo(() => {
const notificationHtmlUrl = useMemo(() => {
return getNotificationHtmlUrl(notification);
}, [notification]);
@@ -18,7 +18,7 @@ export function GithubPullRequestPreview({ notification, githubPullRequest }: Gi
markdown={`# ${githubPullRequest.title}`}
actions={
<ActionPanel>
<Action.OpenInBrowser url={notification_html_url} />
<Action.OpenInBrowser url={notificationHtmlUrl} />
</ActionPanel>
}
/>

View File

@@ -1,21 +1,11 @@
import { GoogleMailThreadListItem } from "./GoogleMailThreadListItem";
import { NotificationListItemProps } from "../../../notification";
import { environment } from "@raycast/api";
import { useMemo } from "react";
export function GoogleMailNotificationListItem({ notification, mutate }: NotificationListItemProps) {
const icon = useMemo(() => {
if (environment.appearance === "dark") {
return "google-mail-logo-light.svg";
}
return "google-mail-logo-dark.svg";
}, [environment]);
if (notification.metadata.type !== "GoogleMail") return null;
return (
<GoogleMailThreadListItem
icon={icon}
notification={notification}
googleMailThread={notification.metadata.content}
mutate={mutate}

View File

@@ -8,18 +8,12 @@ import { GoogleMailThread } from "../types";
import { Page } from "../../../types";
interface GoogleMailThreadListItemProps {
icon: string;
notification: Notification;
googleMailThread: GoogleMailThread;
mutate: MutatePromise<Page<Notification> | undefined>;
}
export function GoogleMailThreadListItem({
icon,
notification,
googleMailThread,
mutate,
}: GoogleMailThreadListItemProps) {
export function GoogleMailThreadListItem({ notification, googleMailThread, mutate }: GoogleMailThreadListItemProps) {
const isStarred = googleMailThread.messages.some((message) => message.labelIds?.includes("STARRED"));
const isImportant = googleMailThread.messages.some((message) => message.labelIds?.includes("IMPORTANT"));
const fromAddress = googleMailThread.messages[0].payload.headers.find((header) => header.name === "From")?.value;
@@ -50,7 +44,7 @@ export function GoogleMailThreadListItem({
<List.Item
key={notification.id}
title={notification.title}
icon={icon}
icon={{ source: { light: "google-mail-logo-dark.svg", dark: "google-mail-logo-light.svg" } }}
accessories={accessories}
subtitle={subtitle}
actions={

View File

@@ -9,7 +9,7 @@ interface GoogleMailThreadPreviewProps {
}
export function GoogleMailThreadPreview({ notification }: GoogleMailThreadPreviewProps) {
const notification_html_url = useMemo(() => {
const notificationHtmlUrl = useMemo(() => {
return getNotificationHtmlUrl(notification);
}, [notification]);
@@ -18,7 +18,7 @@ export function GoogleMailThreadPreview({ notification }: GoogleMailThreadPrevie
markdown={`# ${notification.title}`}
actions={
<ActionPanel>
<Action.OpenInBrowser url={notification_html_url} />
<Action.OpenInBrowser url={notificationHtmlUrl} />
</ActionPanel>
}
/>

View File

@@ -1,10 +1,11 @@
import { Icon, Image, List } from "@raycast/api";
import { getAvatarIcon } from "@raycast/utils";
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,
icon: user.avatar_url ? { source: user.avatar_url, mask: Image.Mask.Circle } : getAvatarIcon(user.name),
tooltip: user.name,
};
}

View File

@@ -9,14 +9,12 @@ import { List } from "@raycast/api";
import { match } from "ts-pattern";
interface LinearIssueNotificationListItemProps {
icon: string;
notification: Notification;
linearIssueNotification: LinearIssueNotification;
mutate: MutatePromise<Page<Notification> | undefined>;
}
export function LinearIssueNotificationListItem({
icon,
notification,
linearIssueNotification,
mutate,
@@ -39,7 +37,7 @@ export function LinearIssueNotificationListItem({
<List.Item
key={notification.id}
title={notification.title}
icon={icon}
icon={{ source: { light: "linear-logo-dark.svg", dark: "linear-logo-light.svg" } }}
accessories={accessories}
subtitle={subtitle}
actions={

View File

@@ -1,24 +1,14 @@
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}
@@ -27,7 +17,6 @@ export function LinearNotificationListItem({ notification, mutate }: Notificatio
case "ProjectNotification":
return (
<LinearProjectNotificationListItem
icon={icon}
notification={notification}
linearProjectNotification={notification.metadata.content.content}
mutate={mutate}

View File

@@ -1,30 +1,31 @@
import { LinearProjectNotification, LinearProjectState, LinearProject } from "../types";
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 { List, Color } from "@raycast/api";
import { Page } from "../../../types";
import { List } from "@raycast/api";
import { match } from "ts-pattern";
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 state = getLinearProjectStateAccessory(linearProjectNotification.project);
const lead = getLinearUserAccessory(linearProjectNotification.project.lead);
const accessories: List.Item.Accessory[] = [
state,
lead,
{
date: new Date(linearProjectNotification.updated_at),
@@ -36,7 +37,7 @@ export function LinearProjectNotificationListItem({
<List.Item
key={notification.id}
title={notification.title}
icon={icon}
icon={{ source: { light: "linear-logo-dark.svg", dark: "linear-logo-light.svg" } }}
accessories={accessories}
subtitle={subtitle}
actions={
@@ -51,3 +52,29 @@ export function LinearProjectNotificationListItem({
/>
);
}
export function getLinearProjectStateAccessory(project: LinearProject): List.Item.Accessory {
return {
icon: match(project)
.with({ state: LinearProjectState.Planned }, () => {
return { source: "linear-project-planned.svg", tintColor: Color.SecondaryText };
})
.with({ state: LinearProjectState.Backlog }, () => {
return { source: "linear-project-backlog.svg", tintColor: Color.PrimaryText };
})
.with({ state: LinearProjectState.Started }, () => {
return { source: "linear-project-started.svg", tintColor: Color.Blue };
})
.with({ state: LinearProjectState.Paused }, () => {
return { source: "linear-project-paused.svg", tintColor: Color.PrimaryText };
})
.with({ state: LinearProjectState.Completed }, () => {
return { source: "linear-project-completed.svg", tintColor: Color.Magenta };
})
.with({ state: LinearProjectState.Canceled }, () => {
return { source: "linear-project-canceled.svg", tintColor: Color.SecondaryText };
})
.exhaustive(),
tooltip: project.state,
};
}

View File

@@ -9,7 +9,7 @@ interface LinearIssuePreviewProps {
}
export function LinearIssuePreview({ notification, linearIssue }: LinearIssuePreviewProps) {
const notification_html_url = useMemo(() => {
const notificationHtmlUrl = useMemo(() => {
return getNotificationHtmlUrl(notification);
}, [notification]);
@@ -18,7 +18,7 @@ export function LinearIssuePreview({ notification, linearIssue }: LinearIssuePre
markdown={`# ${linearIssue.title}`}
actions={
<ActionPanel>
<Action.OpenInBrowser url={notification_html_url} />
<Action.OpenInBrowser url={notificationHtmlUrl} />
</ActionPanel>
}
/>

View File

@@ -9,7 +9,7 @@ interface LinearProjectPreviewProps {
}
export function LinearProjectPreview({ notification, linearProject }: LinearProjectPreviewProps) {
const notification_html_url = useMemo(() => {
const notificationHtmlUrl = useMemo(() => {
return getNotificationHtmlUrl(notification);
}, [notification]);
@@ -18,7 +18,7 @@ export function LinearProjectPreview({ notification, linearProject }: LinearProj
markdown={`# ${linearProject.name}`}
actions={
<ActionPanel>
<Action.OpenInBrowser url={notification_html_url} />
<Action.OpenInBrowser url={notificationHtmlUrl} />
</ActionPanel>
}
/>

View File

@@ -1,29 +0,0 @@
import { NotificationTaskActions } from "../../action/NotificationTaskActions";
import { NotificationListItemProps } from "../../notification";
import { Detail, List, environment } from "@raycast/api";
import { useMemo } from "react";
export function TodoistNotificationListItem({ notification, mutate }: 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 👋" />}
mutate={mutate}
/>
}
/>
);
}

View File

@@ -0,0 +1,52 @@
import { NotificationTaskActions } from "../../../action/NotificationTaskActions";
import { TodoistTaskPreview } from "../preview/TodoistTaskPreview";
import { NotificationListItemProps } from "../../../notification";
import { Icon, List, Color } from "@raycast/api";
import { TaskPriority } from "../../../task";
import { match } from "ts-pattern";
import dayjs from "dayjs";
export function TodoistNotificationListItem({ notification, mutate }: NotificationListItemProps) {
const dueAt = notification.task?.due_at?.content;
const subtitle = dueAt ? dayjs(dueAt).format("YYYY-MM-DD") : undefined;
const color = match(notification)
.with({ task: { priority: TaskPriority.P1 } }, () => Color.Red)
.with({ task: { priority: TaskPriority.P2 } }, () => Color.Orange)
.with({ task: { priority: TaskPriority.P3 } }, () => Color.Yellow)
.otherwise(() => null);
const accessories: List.Item.Accessory[] = [
{
icon: { source: Icon.Circle, tintColor: color },
},
{
date: new Date(notification.updated_at),
tooltip: `Updated at ${notification.updated_at}`,
},
];
const task = notification.task;
if (task) {
for (const tag of task.tags) {
accessories.unshift({ tag: { value: tag } });
}
}
return (
<List.Item
key={notification.id}
title={notification.title}
icon={{ source: { light: "todoist-logo-dark.svg", dark: "todoist-logo-light.svg" } }}
subtitle={subtitle}
accessories={accessories}
actions={
<NotificationTaskActions
notification={notification}
detailsTarget={<TodoistTaskPreview notification={notification} />}
mutate={mutate}
/>
}
/>
);
}

View File

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

View File

@@ -64,7 +64,7 @@ export function getNotificationHtmlUrl(notification: Notification) {
(googleMailThread) =>
`https://mail.google.com/mail/u/${googleMailThread.user_email_address}/#inbox/${googleMailThread.id}`,
)
.with({ metadata: { type: "Todoist", content: P.select() } }, () => "")
.with({ metadata: { type: "Todoist" } }, () => `https://todoist.com/showTask?id=${notification.source_id}`)
.with({ metadata: { type: "Github" } }, () => "https://github.com")
.exhaustive();
}