diff --git a/packages/app/public/icons.svg b/packages/app/public/icons.svg index 1aa8ce5bb..7bc70081f 100644 --- a/packages/app/public/icons.svg +++ b/packages/app/public/icons.svg @@ -287,6 +287,11 @@ + + + + + \ No newline at end of file diff --git a/packages/app/src/Element/ProfileImage.css b/packages/app/src/Element/ProfileImage.css index 4f3c3031e..cf66f0f16 100644 --- a/packages/app/src/Element/ProfileImage.css +++ b/packages/app/src/Element/ProfileImage.css @@ -5,7 +5,7 @@ text-decoration: none; user-select: none; min-width: 0; - gap: 12px; + gap: 8px; } .pfp .avatar { diff --git a/packages/app/src/Pages/Notifications.css b/packages/app/src/Pages/Notifications.css index 11910a567..168437fc0 100644 --- a/packages/app/src/Pages/Notifications.css +++ b/packages/app/src/Pages/Notifications.css @@ -1,7 +1,28 @@ .notification-group { + display: flex; + padding: 16px 24px 26px 0; + min-height: 100px; +} + +.notification-group > div:first-of-type { + width: 64px; + min-width: 64px; + max-width: 64px; + align-items: center; +} + +.notification-group > div:first-of-type > div:first-of-type { + height: 40px; display: flex; flex-direction: column; - gap: 12px; + justify-content: center; +} + +.notification-group > div:first-of-type > div { + align-self: center; + font-size: 16px; + font-weight: 600; + line-height: 1em; } .notification-group .avatar { @@ -9,10 +30,6 @@ height: 40px; } -.notification-group .pfp { - gap: unset; -} - .notification-group .names { display: flex; gap: 24px; @@ -21,13 +38,7 @@ line-height: 1em; } -.notification-group .names > div:first-of-type { - width: 24px; - text-align: center; -} - .notification-group .content { - margin-left: 48px; font-size: 14px; line-height: 22px; color: var(--font-secondary-color); diff --git a/packages/app/src/Pages/Notifications.tsx b/packages/app/src/Pages/Notifications.tsx index 5e4efbd24..1bd77d0b4 100644 --- a/packages/app/src/Pages/Notifications.tsx +++ b/packages/app/src/Pages/Notifications.tsx @@ -13,13 +13,12 @@ import { import { unwrap } from "@snort/shared"; import { useUserProfile } from "@snort/system-react"; import { useInView } from "react-intersection-observer"; -import { FormattedMessage } from "react-intl"; +import { FormattedMessage, useIntl } from "react-intl"; import useLogin from "Hooks/useLogin"; import { markNotificationsRead } from "Login"; import { Notifications, UserCache } from "Cache"; import { dedupe, findTag, orderDescending } from "SnortUtils"; -import Note from "Element/Note"; import Icon from "Icons/Icon"; import ProfileImage, { getDisplayName } from "Element/ProfileImage"; import useModeration from "Hooks/useModeration"; @@ -27,6 +26,7 @@ import { System } from "index"; import useEventFeed from "Feed/EventFeed"; import Text from "Element/Text"; import { formatShort } from "Number"; +import { useNavigate } from "react-router-dom"; function notificationContext(ev: TaggedRawEvent) { switch (ev.kind) { @@ -47,11 +47,20 @@ function notificationContext(ev: TaggedRawEvent) { break; } case EventKind.Repost: - case EventKind.TextNote: case EventKind.Reaction: { const thread = EventExt.extractThread(ev); - const id = unwrap(thread?.replyTo?.value ?? thread?.root?.value ?? ev.id); - return createNostrLink(NostrPrefix.Event, id); + const tag = unwrap(thread?.replyTo ?? thread?.root ?? { value: ev.id, key: "e" }); + if (tag.key === "e") { + return createNostrLink(NostrPrefix.Event, unwrap(tag.value)); + } else if (tag.key === "a") { + const [kind, author, d] = unwrap(tag.value).split(":"); + return createNostrLink(NostrPrefix.Address, d, undefined, Number(kind), author); + } else { + throw new Error("Unknown thread context"); + } + } + case EventKind.TextNote: { + return createNostrLink(NostrPrefix.Note, ev.id); } } } @@ -98,8 +107,26 @@ export default function NotificationsPage() { function NotificationGroup({ evs }: { evs: Array }) { const { ref, inView } = useInView({ triggerOnce: true }); + const { formatMessage } = useIntl(); const kind = evs[0].kind; + const zaps = useMemo(() => { + return evs.filter(a => a.kind === EventKind.ZapReceipt).map(a => parseZap(a, UserCache)); + }, [evs]); + const pubkeys = dedupe( + evs.map(a => { + if (a.kind === EventKind.ZapReceipt) { + const zap = unwrap(zaps.find(b => b.id === a.id)); + return zap.anonZap ? "anon" : zap.sender ?? a.pubkey; + } + return a.pubkey; + }) + ); + const firstPubkey = pubkeys[0]; + const firstPubkeyProfile = useUserProfile(System, inView ? (firstPubkey === "anon" ? "" : firstPubkey) : ""); + const context = notificationContext(evs[0]); + const totalZaps = zaps.reduce((acc, v) => acc + v.amount, 0); + const iconName = () => { switch (kind) { case EventKind.Reaction: @@ -108,12 +135,17 @@ function NotificationGroup({ evs }: { evs: Array }) { return "zap-solid"; case EventKind.Repost: return "repeat"; + case EventKind.TextNote: + return "reverse-left"; } return ""; }; const actionName = (n: number, name: string) => { switch (kind) { + case EventKind.TextNote: { + return ""; + } case EventKind.Reaction: { return ( }) { return `${kind}'d your post`; }; - if (kind === EventKind.TextNote) { - return ( - <> - {evs.map(v => ( - - ))} - - ); - } - - const zaps = useMemo(() => { - return evs.filter(a => a.kind === EventKind.ZapReceipt).map(a => parseZap(a, UserCache)); - }, [evs]); - const pubkeys = dedupe( - evs.map(a => { - if (a.kind === EventKind.ZapReceipt) { - const zap = zaps.find(a => a.id === a.id); - return zap?.sender ?? a.pubkey; - } - return a.pubkey; - }) - ); - const firstPubkey = pubkeys[0]; - const firstPubkeyProfile = useUserProfile(System, inView ? firstPubkey : ""); - const context = notificationContext(evs[0]); - const totalZaps = zaps.reduce((acc, v) => acc + v.amount, 0); - return (
-
- -
- {pubkeys.map(v => ( - - ))} -
-
-
-
{kind === EventKind.ZapReceipt && formatShort(totalZaps)}
- {actionName(pubkeys.length - 1, getDisplayName(firstPubkeyProfile, firstPubkey))} -
-
{context && }
+ {inView && ( + <> +
+
+ +
+
{kind === EventKind.ZapReceipt && formatShort(totalZaps)}
+
+
+
+ {pubkeys + .filter(a => a !== "anon") + .slice(0, 12) + .map(v => ( + + ))} +
+ {kind !== EventKind.TextNote && ( +
+ {actionName( + pubkeys.length - 1, + firstPubkey === "anon" + ? formatMessage({ defaultMessage: "Anon" }) + : getDisplayName(firstPubkeyProfile, firstPubkey) + )} +
+ )} +
{context && }
+
+ + )}
); } function NotificationContext({ link }: { link: NostrLink }) { const { data: ev } = useEventFeed(link); + const navigate = useNavigate(); const content = ev?.content ?? ""; return ( -
+
navigate(`/${link.encode()}`)} className="pointer"> 120 ? `${content.substring(0, 120)}...` : content} tags={ev?.tags ?? []} diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 76186209b..6ecb0cfab 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -14,6 +14,8 @@ --success: #2ad544; --warning: #ff8800; --live: #f83838; + --heart: #ef4444; + --zap: #ff710a; --gray-superlight: #eee; --gray-light: #999; @@ -739,3 +741,11 @@ button.tall { background: transparent !important; position: absolute !important; } + +svg.heart-solid { + color: var(--heart); +} + +svg.zap-solid { + color: var(--zap); +}