feat: NIP-24

This commit is contained in:
2023-08-17 19:54:14 +01:00
parent 8500dee24f
commit f6a46e3523
51 changed files with 792 additions and 319 deletions

View File

@ -3,7 +3,7 @@ import "./BadgeList.css";
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import { TaggedRawEvent } from "@snort/system";
import { TaggedNostrEvent } from "@snort/system";
import { ProxyImg } from "Element/ProxyImg";
import Icon from "Icons/Icon";
@ -11,7 +11,7 @@ import Modal from "Element/Modal";
import Username from "Element/Username";
import { findTag } from "SnortUtils";
export default function BadgeList({ badges }: { badges: TaggedRawEvent[] }) {
export default function BadgeList({ badges }: { badges: TaggedNostrEvent[] }) {
const [showModal, setShowModal] = useState(false);
const badgeMetadata = badges.map(b => {
const thumb = findTag(b, "thumb");

View File

@ -1,6 +1,6 @@
import { useState, useMemo, ChangeEvent } from "react";
import { FormattedMessage } from "react-intl";
import { HexKey, TaggedRawEvent } from "@snort/system";
import { HexKey, TaggedNostrEvent } from "@snort/system";
import Note from "Element/Note";
import useLogin from "Hooks/useLogin";
@ -10,8 +10,8 @@ import messages from "./messages";
interface BookmarksProps {
pubkey: HexKey;
bookmarks: readonly TaggedRawEvent[];
related: readonly TaggedRawEvent[];
bookmarks: readonly TaggedNostrEvent[];
related: readonly TaggedNostrEvent[];
}
const Bookmarks = ({ pubkey, bookmarks, related }: BookmarksProps) => {

View File

@ -2,56 +2,54 @@ import "./DM.css";
import { useEffect, useState } from "react";
import { useIntl } from "react-intl";
import { useInView } from "react-intersection-observer";
import { EventKind, TaggedRawEvent } from "@snort/system";
import useEventPublisher from "Feed/EventPublisher";
import NoteTime from "Element/NoteTime";
import Text from "Element/Text";
import useLogin from "Hooks/useLogin";
import { Chat, ChatType, chatTo, setLastReadIn } from "chat";
import { Chat, ChatMessage, ChatType, setLastReadIn } from "chat";
import messages from "./messages";
import ProfileImage from "./ProfileImage";
export interface DMProps {
chat: Chat;
data: TaggedRawEvent;
data: ChatMessage;
}
export default function DM(props: DMProps) {
const pubKey = useLogin().publicKey;
const publisher = useEventPublisher();
const ev = props.data;
const needsDecryption = ev.kind === EventKind.DirectMessage;
const [content, setContent] = useState(needsDecryption ? "Loading..." : ev.content);
const msg = props.data;
const [content, setContent] = useState(msg.needsDecryption ? "Loading..." : msg.content);
const [decrypted, setDecrypted] = useState(false);
const { ref, inView } = useInView();
const { formatMessage } = useIntl();
const isMe = ev.pubkey === pubKey;
const otherPubkey = isMe ? pubKey : chatTo(ev);
const isMe = msg.from === pubKey;
const otherPubkey = isMe ? pubKey : msg.from;
async function decrypt() {
if (publisher) {
const decrypted = await publisher.decryptDm(ev);
const decrypted = await msg.decrypt(publisher);
setContent(decrypted || "<ERROR>");
if (!isMe) {
setLastReadIn(ev.pubkey);
setLastReadIn(msg.id);
}
}
}
function sender() {
if (props.chat.type !== ChatType.DirectMessage && !isMe) {
return <ProfileImage pubkey={ev.pubkey} />;
return <ProfileImage pubkey={msg.from} />;
}
}
useEffect(() => {
if (!decrypted && inView && needsDecryption) {
if (!decrypted && inView && msg.needsDecryption) {
setDecrypted(true);
decrypt().catch(console.error);
}
}, [inView, ev]);
}, [inView, msg]);
return (
<div className={isMe ? "dm me" : "dm other"} ref={ref}>
@ -60,7 +58,7 @@ export default function DM(props: DMProps) {
<Text content={content} tags={[]} creator={otherPubkey} />
</div>
<div>
<NoteTime from={ev.created_at * 1000} fallback={formatMessage(messages.JustNow)} />
<NoteTime from={msg.created_at * 1000} fallback={formatMessage(messages.JustNow)} />
</div>
</div>
);

View File

@ -19,3 +19,12 @@
gap: 10px;
padding: 5px 10px;
}
.pfp-overlap .pfp:not(:last-of-type) {
margin-right: -20px;
}
.pfp-overlap .avatar {
width: 32px;
height: 32px;
}

View File

@ -1,31 +1,46 @@
import "./DmWindow.css";
import { useMemo } from "react";
import { TaggedRawEvent } from "@snort/system";
import ProfileImage from "Element/ProfileImage";
import DM from "Element/DM";
import NoteToSelf from "Element/NoteToSelf";
import useLogin from "Hooks/useLogin";
import WriteMessage from "Element/WriteMessage";
import { Chat, ChatType, useChatSystem } from "chat";
import { Chat, ChatParticipant, ChatType, useChatSystem } from "chat";
import { Nip4ChatSystem } from "chat/nip4";
import { FormattedMessage } from "react-intl";
export default function DmWindow({ id }: { id: string }) {
const pubKey = useLogin().publicKey;
const dms = useChatSystem();
const chat = dms.find(a => a.id === id) ?? Nip4ChatSystem.createChatObj(id, []);
function participant(p: ChatParticipant) {
if (p.id === pubKey) {
return <NoteToSelf className="f-grow mb-10" pubkey={p.id} />;
}
if (p.type === "pubkey") {
return <ProfileImage pubkey={p.id} className="f-grow mb10" />;
}
if (p?.profile) {
return <ProfileImage pubkey={p.id} className="f-grow mb10" profile={p.profile} />;
}
return <ProfileImage pubkey={p.id} className="f-grow mb10" overrideUsername={p.id} />;
}
function sender() {
if (id === pubKey) {
return <NoteToSelf className="f-grow mb-10" pubkey={id} />;
if (chat.participants.length === 1) {
return participant(chat.participants[0]);
} else {
return (
<div className="flex pfp-overlap mb10">
{chat.participants.map(v => (
<ProfileImage pubkey={v.id} showUsername={false} />
))}
{chat.title ?? <FormattedMessage defaultMessage="Group Chat" />}
</div>
);
}
if (chat?.type === ChatType.DirectMessage) {
return <ProfileImage pubkey={id} className="f-grow mb10" />;
}
if (chat?.profile) {
return <ProfileImage pubkey={id} className="f-grow mb10" profile={chat.profile} />;
}
return <ProfileImage pubkey={id ?? ""} className="f-grow mb10" overrideUsername={chat?.id} />;
}
return (
@ -55,7 +70,7 @@ function DmChatSelected({ chat }: { chat: Chat }) {
return (
<>
{sortedDms.map(a => (
<DM data={a as TaggedRawEvent} key={a.id} chat={chat} />
<DM data={a} key={a.id} chat={chat} />
))}
</>
);

View File

@ -1,5 +1,5 @@
import "./LiveChat.css";
import { EventKind, NostrLink, TaggedRawEvent } from "@snort/system";
import { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
@ -15,7 +15,7 @@ import Text from "Element/Text";
import { System } from "index";
import { profileLink } from "SnortUtils";
export function LiveChat({ ev, link }: { ev: TaggedRawEvent; link: NostrLink }) {
export function LiveChat({ ev, link }: { ev: TaggedNostrEvent; link: NostrLink }) {
const [chat, setChat] = useState("");
const messages = useLiveChatFeed(link);
const pub = useEventPublisher();
@ -74,7 +74,7 @@ export function LiveChat({ ev, link }: { ev: TaggedRawEvent; link: NostrLink })
);
}
function ChatMessage({ ev }: { ev: TaggedRawEvent }) {
function ChatMessage({ ev }: { ev: TaggedNostrEvent }) {
const profile = useUserProfile(System, ev.pubkey);
const navigate = useNavigate();

View File

@ -3,7 +3,7 @@ import React, { useMemo, useState, useLayoutEffect, ReactNode } from "react";
import { useNavigate, Link } from "react-router-dom";
import { useInView } from "react-intersection-observer";
import { useIntl, FormattedMessage } from "react-intl";
import { TaggedRawEvent, HexKey, EventKind, NostrPrefix, Lists, EventExt, parseZap } from "@snort/system";
import { TaggedNostrEvent, HexKey, EventKind, NostrPrefix, Lists, EventExt, parseZap } from "@snort/system";
import { System } from "index";
import useEventPublisher from "Feed/EventPublisher";
@ -36,12 +36,12 @@ import { LiveEvent } from "Element/LiveEvent";
import messages from "./messages";
export interface NoteProps {
data: TaggedRawEvent;
data: TaggedNostrEvent;
className?: string;
related: readonly TaggedRawEvent[];
related: readonly TaggedNostrEvent[];
highlight?: boolean;
ignoreModeration?: boolean;
onClick?: (e: TaggedRawEvent) => void;
onClick?: (e: TaggedNostrEvent) => void;
depth?: number;
options?: {
showHeader?: boolean;
@ -113,8 +113,8 @@ export default function Note(props: NoteProps) {
return { ...acc, [kind]: [...rs, reaction] };
},
{
[Reaction.Positive]: [] as TaggedRawEvent[],
[Reaction.Negative]: [] as TaggedRawEvent[],
[Reaction.Positive]: [] as TaggedNostrEvent[],
[Reaction.Negative]: [] as TaggedNostrEvent[],
}
);
return {
@ -219,7 +219,7 @@ export default function Note(props: NoteProps) {
function goToEvent(
e: React.MouseEvent,
eTarget: TaggedRawEvent,
eTarget: TaggedNostrEvent,
isTargetAllowed: boolean = e.target === e.currentTarget
) {
if (!isTargetAllowed || opt?.canClick === false) {

View File

@ -1,7 +1,7 @@
import "./NoteCreator.css";
import { FormattedMessage, useIntl } from "react-intl";
import { useDispatch, useSelector } from "react-redux";
import { encodeTLV, EventKind, NostrPrefix, TaggedRawEvent, EventBuilder } from "@snort/system";
import { encodeTLV, EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder } from "@snort/system";
import { LNURL } from "@snort/shared";
import Icon from "Icons/Icon";
@ -38,7 +38,7 @@ import useLogin from "Hooks/useLogin";
import { System } from "index";
interface NotePreviewProps {
note: TaggedRawEvent;
note: TaggedNostrEvent;
}
function NotePreview({ note }: NotePreviewProps) {
@ -194,7 +194,7 @@ export function NoteCreator() {
if (preview) {
return (
<Note
data={preview as TaggedRawEvent}
data={preview as TaggedNostrEvent}
related={[]}
options={{
showFooter: false,

View File

@ -3,7 +3,7 @@ import { useSelector, useDispatch } from "react-redux";
import { useIntl, FormattedMessage } from "react-intl";
import { Menu, MenuItem } from "@szhsin/react-menu";
import { useLongPress } from "use-long-press";
import { TaggedRawEvent, HexKey, u256, encodeTLV, NostrPrefix, Lists, ParsedZap } from "@snort/system";
import { TaggedNostrEvent, HexKey, u256, encodeTLV, NostrPrefix, Lists, ParsedZap } from "@snort/system";
import { LNURL } from "@snort/shared";
import { useUserProfile } from "@snort/system-react";
@ -56,13 +56,13 @@ export interface Translation {
}
export interface NoteFooterProps {
reposts: TaggedRawEvent[];
reposts: TaggedNostrEvent[];
zaps: ParsedZap[];
positive: TaggedRawEvent[];
negative: TaggedRawEvent[];
positive: TaggedNostrEvent[];
negative: TaggedNostrEvent[];
showReactions: boolean;
setShowReactions(b: boolean): void;
ev: TaggedRawEvent;
ev: TaggedNostrEvent;
onTranslated?: (content: Translation) => void;
}

View File

@ -1,7 +1,7 @@
import "./NoteReaction.css";
import { Link } from "react-router-dom";
import { useMemo } from "react";
import { EventKind, NostrEvent, TaggedRawEvent, NostrPrefix, EventExt } from "@snort/system";
import { EventKind, NostrEvent, TaggedNostrEvent, NostrPrefix, EventExt } from "@snort/system";
import Note from "Element/Note";
import ProfileImage from "Element/ProfileImage";
@ -10,8 +10,8 @@ import NoteTime from "Element/NoteTime";
import useModeration from "Hooks/useModeration";
export interface NoteReactionProps {
data: TaggedRawEvent;
root?: TaggedRawEvent;
data: TaggedNostrEvent;
root?: TaggedNostrEvent;
}
export default function NoteReaction(props: NoteReactionProps) {
const { data: ev } = props;
@ -43,7 +43,7 @@ export default function NoteReaction(props: NoteReactionProps) {
if (ev?.kind === EventKind.Repost && ev.content.length > 0 && ev.content !== "#[0]") {
try {
const r: NostrEvent = JSON.parse(ev.content);
return r as TaggedRawEvent;
return r as TaggedNostrEvent;
} catch (e) {
console.error("Could not load reposted content", e);
}

View File

@ -1,4 +1,4 @@
import { TaggedRawEvent, ParsedZap } from "@snort/system";
import { TaggedNostrEvent, ParsedZap } from "@snort/system";
import { LNURL } from "@snort/shared";
import { useState } from "react";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
@ -15,7 +15,7 @@ import useLogin from "Hooks/useLogin";
import { System } from "index";
interface PollProps {
ev: TaggedRawEvent;
ev: TaggedNostrEvent;
zaps: Array<ParsedZap>;
}

View File

@ -2,7 +2,7 @@ import "./Reactions.css";
import { useState, useMemo, useEffect } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { TaggedRawEvent, ParsedZap } from "@snort/system";
import { TaggedNostrEvent, ParsedZap } from "@snort/system";
import { formatShort } from "Number";
import Icon from "Icons/Icon";
@ -16,9 +16,9 @@ import messages from "./messages";
interface ReactionsProps {
show: boolean;
setShow(b: boolean): void;
positive: TaggedRawEvent[];
negative: TaggedRawEvent[];
reposts: TaggedRawEvent[];
positive: TaggedNostrEvent[];
negative: TaggedNostrEvent[];
reposts: TaggedNostrEvent[];
zaps: ParsedZap[];
}

View File

@ -3,7 +3,7 @@ import { useMemo, useState, ReactNode } from "react";
import { useIntl } from "react-intl";
import { useNavigate, useLocation, Link, useParams } from "react-router-dom";
import {
TaggedRawEvent,
TaggedNostrEvent,
u256,
EventKind,
NostrPrefix,
@ -37,14 +37,14 @@ const Divider = ({ variant = "regular" }: DividerProps) => {
interface SubthreadProps {
isLastSubthread?: boolean;
active: u256;
notes: readonly TaggedRawEvent[];
related: readonly TaggedRawEvent[];
chains: Map<u256, Array<TaggedRawEvent>>;
onNavigate: (e: TaggedRawEvent) => void;
notes: readonly TaggedNostrEvent[];
related: readonly TaggedNostrEvent[];
chains: Map<u256, Array<TaggedNostrEvent>>;
onNavigate: (e: TaggedNostrEvent) => void;
}
const Subthread = ({ active, notes, related, chains, onNavigate }: SubthreadProps) => {
const renderSubthread = (a: TaggedRawEvent, idx: number) => {
const renderSubthread = (a: TaggedNostrEvent, idx: number) => {
const isLastSubthread = idx === notes.length - 1;
const replies = getReplies(a.id, chains);
return (
@ -79,7 +79,7 @@ const Subthread = ({ active, notes, related, chains, onNavigate }: SubthreadProp
};
interface ThreadNoteProps extends Omit<SubthreadProps, "notes"> {
note: TaggedRawEvent;
note: TaggedNostrEvent;
isLast: boolean;
}
@ -136,7 +136,7 @@ const TierTwo = ({ active, isLastSubthread, notes, related, chains, onNavigate }
isLast={rest.length === 0}
/>
{rest.map((r: TaggedRawEvent, idx: number) => {
{rest.map((r: TaggedNostrEvent, idx: number) => {
const lastReply = idx === rest.length - 1;
return (
<ThreadNote
@ -187,7 +187,7 @@ const TierThree = ({ active, isLastSubthread, notes, related, chains, onNavigate
/>
)}
{rest.map((r: TaggedRawEvent, idx: number) => {
{rest.map((r: TaggedNostrEvent, idx: number) => {
const lastReply = idx === rest.length - 1;
const lastNote = isLastSubthread && lastReply;
return (
@ -226,13 +226,13 @@ export default function Thread() {
const isSingleNote = thread.data?.filter(a => a.kind === EventKind.TextNote).length === 1;
const { formatMessage } = useIntl();
function navigateThread(e: TaggedRawEvent) {
function navigateThread(e: TaggedNostrEvent) {
setCurrentId(e.id);
//const link = encodeTLV(e.id, NostrPrefix.Event, e.relays);
}
const chains = useMemo(() => {
const chains = new Map<u256, Array<TaggedRawEvent>>();
const chains = new Map<u256, Array<TaggedNostrEvent>>();
if (thread.data) {
thread.data
?.filter(a => a.kind === EventKind.TextNote)
@ -265,7 +265,7 @@ export default function Thread() {
ne =>
ne.id === currentId ||
(link.type === NostrPrefix.Address && findTag(ne, "d") === currentId && ne.pubkey === link.author)
) ?? (location.state && "sig" in location.state ? (location.state as TaggedRawEvent) : undefined);
) ?? (location.state && "sig" in location.state ? (location.state as TaggedNostrEvent) : undefined);
if (currentNote) {
const currentThread = EventExt.extractThread(currentNote);
const isRoot = (ne?: ThreadInfo) => ne === undefined;
@ -318,7 +318,7 @@ export default function Thread() {
const brokenChains = Array.from(chains?.keys()).filter(a => !thread.data?.some(b => b.id === a));
function renderRoot(note: TaggedRawEvent) {
function renderRoot(note: TaggedNostrEvent) {
const className = `thread-root${isSingleNote ? " thread-root-single" : ""}`;
if (note) {
return (
@ -396,7 +396,7 @@ export default function Thread() {
);
}
function getReplies(from: u256, chains?: Map<u256, Array<TaggedRawEvent>>): Array<TaggedRawEvent> {
function getReplies(from: u256, chains?: Map<u256, Array<TaggedNostrEvent>>): Array<TaggedNostrEvent> {
if (!from || !chains) {
return [];
}

View File

@ -2,7 +2,7 @@ import "./Timeline.css";
import { FormattedMessage } from "react-intl";
import { useCallback, useMemo } from "react";
import { useInView } from "react-intersection-observer";
import { TaggedRawEvent, EventKind, u256, parseZap } from "@snort/system";
import { TaggedNostrEvent, EventKind, u256, parseZap } from "@snort/system";
import Icon from "Icons/Icon";
import { dedupeByPubkey, findTag, tagFilterOfTextRepost } from "SnortUtils";
@ -43,7 +43,7 @@ const Timeline = (props: TimelineProps) => {
const { ref, inView } = useInView();
const filterPosts = useCallback(
(nts: readonly TaggedRawEvent[]) => {
(nts: readonly TaggedNostrEvent[]) => {
const a = [...nts];
props.noSort || a.sort((a, b) => b.created_at - a.created_at);
return a
@ -76,7 +76,7 @@ const Timeline = (props: TimelineProps) => {
return dedupeByPubkey(latestFeed).map(e => e.pubkey);
}, [latestFeed]);
function eventElement(e: TaggedRawEvent) {
function eventElement(e: TaggedNostrEvent) {
switch (e.kind) {
case EventKind.SetMetadata: {
return <ProfilePreview actions={<></>} pubkey={e.pubkey} className="card" />;

View File

@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { NostrEvent, TaggedRawEvent } from "@snort/system";
import { NostrEvent, TaggedNostrEvent } from "@snort/system";
import { FormattedMessage } from "react-intl";
import PageSpinner from "Element/PageSpinner";
@ -27,7 +27,7 @@ export default function TrendingNotes() {
<FormattedMessage defaultMessage="Trending Notes" />
</h3>
{posts.map(e => (
<Note key={e.id} data={e as TaggedRawEvent} related={[]} depth={0} />
<Note key={e.id} data={e as TaggedNostrEvent} related={[]} depth={0} />
))}
</>
);