import { isDid } from "@atcute/lexicons/syntax";
import type { Component } from "svelte";
import type { SvelteHTMLElements } from "svelte/elements";
import AtSign from "$icon/at-sign";
import Ban from "$icon/ban";
import CircleDot from "$icon/circle-dot";
import GitMerge from "$icon/git-merge";
import GitPullRequestClosed from "$icon/git-pull-request-closed";
import GitPullRequestCreate from "$icon/git-pull-request-create";
import MessageSquare from "$icon/message-square";
import Star from "$icon/star";
import UserPlus from "$icon/user-plus";
import UserRoundMinus from "$icon/user-round-minus";
import UserRoundArrowForward from "#lib/components/icons/UserRoundArrowForward.svelte";
import { profileIdentifier } from "#lib/api/identity.ts";
import type {
NotificationCategory,
NotificationSummary,
NotificationType
} from "#lib/components/notifications/types.ts";
type IconComponent = Component<SvelteHTMLElements["svg"]>;
export const SOCIAL_TYPES: NotificationType[] = ["repo_starred", "followed"];
const CATEGORIES: Record<NotificationType, NotificationCategory> = {
repo_starred: "social",
followed: "social",
issue_created: "work",
issue_commented: "work",
issue_closed: "work",
issue_reopen: "work",
issue_assigned: "work",
issue_unassigned: "work",
pull_created: "work",
pull_commented: "work",
pull_merged: "work",
pull_closed: "work",
pull_reopen: "work",
pull_assigned: "work",
pull_unassigned: "work",
user_mentioned: "work"
};
const ICONS: Record<NotificationType, IconComponent> = {
repo_starred: Star,
followed: UserPlus,
issue_created: CircleDot,
issue_commented: MessageSquare,
issue_closed: Ban,
issue_reopen: CircleDot,
issue_assigned: UserRoundArrowForward,
issue_unassigned: UserRoundMinus,
pull_created: GitPullRequestCreate,
pull_commented: MessageSquare,
pull_merged: GitMerge,
pull_closed: GitPullRequestClosed,
pull_reopen: GitPullRequestCreate,
pull_assigned: UserRoundArrowForward,
pull_unassigned: UserRoundMinus,
user_mentioned: AtSign
};
// the design system has no purple, and a merged pull already reads as info on
// TicketStatePill, so merges land on the same token here.
const TONES: Record<NotificationType, string> = {
repo_starred: "text-foreground-warning",
followed: "text-foreground-muted",
issue_created: "text-foreground-success",
issue_commented: "text-foreground-muted",
issue_closed: "text-foreground-muted",
issue_reopen: "text-foreground-success",
issue_assigned: "text-foreground-info",
issue_unassigned: "text-foreground-muted",
pull_created: "text-foreground-success",
pull_commented: "text-foreground-muted",
pull_merged: "text-foreground-info",
pull_closed: "text-foreground-danger",
pull_reopen: "text-foreground-success",
pull_assigned: "text-foreground-info",
pull_unassigned: "text-foreground-muted",
user_mentioned: "text-foreground-info"
};
const PHRASES: Record<NotificationType, string> = {
repo_starred: "starred",
followed: "followed you",
issue_created: "opened an issue",
issue_commented: "commented on an issue",
issue_closed: "closed an issue",
issue_reopen: "reopened an issue",
issue_assigned: "assigned you to an issue",
issue_unassigned: "unassigned you from an issue",
pull_created: "created a PR",
pull_commented: "commented on a PR",
pull_merged: "merged a PR",
pull_closed: "closed a PR",
pull_reopen: "reopened a PR",
pull_assigned: "assigned you to a PR",
pull_unassigned: "unassigned you from a PR",
user_mentioned: "mentioned you"
};
export const notificationCategory = (type: NotificationType): NotificationCategory =>
CATEGORIES[type];
export const notificationIcon = (type: NotificationType): IconComponent => ICONS[type];
export const notificationTone = (type: NotificationType): string => TONES[type];
const basePhrase = (notification: NotificationSummary): string => {
if (notification.type === "user_mentioned" && notification.subject) {
return notification.subject.kind === "issue"
? "mentioned you on an issue"
: "mentioned you on a pull request";
}
return PHRASES[notification.type];
};
const preposition = (type: NotificationType): string => {
if (type === "repo_starred") return "";
return type === "user_mentioned" ? " in" : " on";
};
export const notificationPhrase = (
notification: NotificationSummary,
leadingRepo = false
): string => {
const phrase = basePhrase(notification);
return leadingRepo && notification.repo ? phrase + preposition(notification.type) : phrase;
};
export const notificationTarget = (
notification: NotificationSummary,
includingOwner = true
): string => {
const repo = notification.repo;
if (!repo) return "";
return includingOwner ? `${repo.ownerHandle}/${repo.name}` : repo.name;
};
export const notificationHref = (notification: NotificationSummary): string => {
if (notification.type === "followed")
return `/${profileIdentifier(notification.actor.handle, notification.actor.did)}`;
const repo = notification.repo;
if (!repo) return `/${profileIdentifier(notification.actor.handle, notification.actor.did)}`;
// did-addressed repos have no name to route on; the did is their address, and
// issues/pulls under them can't be addressed either
if (isDid(repo.name)) return `/${repo.name}`;
const base = `/${repo.ownerHandle}/${repo.name}`;
const subject = notification.subject;
if (!subject) return base;
return `${base}/${subject.kind === "issue" ? "issues" : "pulls"}/${subject.number}`;
};