feat: NIP-24
This commit is contained in:
59
packages/app/src/Cache/GiftWrapCache.ts
Normal file
59
packages/app/src/Cache/GiftWrapCache.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { FeedCache } from "@snort/shared";
|
||||
import { EventKind, EventPublisher, TaggedNostrEvent } from "@snort/system";
|
||||
import { UnwrappedGift, db } from "Db";
|
||||
import { findTag, unwrap } from "SnortUtils";
|
||||
|
||||
export class GiftWrapCache extends FeedCache<UnwrappedGift> {
|
||||
constructor() {
|
||||
super("GiftWrapCache", db.gifts);
|
||||
}
|
||||
|
||||
key(of: UnwrappedGift): string {
|
||||
return of.id;
|
||||
}
|
||||
|
||||
override async preload(): Promise<void> {
|
||||
await super.preload();
|
||||
await this.buffer([...this.onTable]);
|
||||
}
|
||||
|
||||
newest(): number {
|
||||
let ret = 0;
|
||||
this.cache.forEach(v => (ret = v.created_at > ret ? v.created_at : ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
takeSnapshot(): Array<UnwrappedGift> {
|
||||
return [...this.cache.values()];
|
||||
}
|
||||
|
||||
async onEvent(evs: Array<TaggedNostrEvent>, pub: EventPublisher) {
|
||||
const unwrapped = (
|
||||
await Promise.all(
|
||||
evs.map(async v => {
|
||||
try {
|
||||
return {
|
||||
id: v.id,
|
||||
to: findTag(v, "p"),
|
||||
created_at: v.created_at,
|
||||
inner: await pub.unwrapGift(v),
|
||||
} as UnwrappedGift;
|
||||
} catch (e) {
|
||||
console.debug(e, v);
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
.filter(a => a !== undefined)
|
||||
.map(unwrap);
|
||||
|
||||
// HACK: unseal to get p tags
|
||||
for (const u of unwrapped) {
|
||||
if (u.inner.kind === EventKind.SealedRumor) {
|
||||
const unsealed = await pub.unsealRumor(u.inner);
|
||||
u.tags = unsealed.tags;
|
||||
}
|
||||
}
|
||||
await this.bulkSet(unwrapped);
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ import { UserProfileCache, UserRelaysCache, RelayMetricCache } from "@snort/syst
|
||||
import { EventInteractionCache } from "./EventInteractionCache";
|
||||
import { ChatCache } from "./ChatCache";
|
||||
import { Payments } from "./PaymentsCache";
|
||||
import { GiftWrapCache } from "./GiftWrapCache";
|
||||
|
||||
export const UserCache = new UserProfileCache();
|
||||
export const UserRelays = new UserRelaysCache();
|
||||
@ -9,6 +10,7 @@ export const RelayMetrics = new RelayMetricCache();
|
||||
export const Chats = new ChatCache();
|
||||
export const PaymentsCache = new Payments();
|
||||
export const InteractionCache = new EventInteractionCache();
|
||||
export const GiftsCache = new GiftWrapCache();
|
||||
|
||||
export async function preload(follows?: Array<string>) {
|
||||
const preloads = [
|
||||
@ -17,6 +19,7 @@ export async function preload(follows?: Array<string>) {
|
||||
InteractionCache.preload(),
|
||||
UserRelays.preload(follows),
|
||||
RelayMetrics.preload(),
|
||||
GiftsCache.preload(),
|
||||
];
|
||||
await Promise.all(preloads);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import Dexie, { Table } from "dexie";
|
||||
import { HexKey, NostrEvent, u256 } from "@snort/system";
|
||||
|
||||
export const NAME = "snortDB";
|
||||
export const VERSION = 11;
|
||||
export const VERSION = 12;
|
||||
|
||||
export interface SubCache {
|
||||
id: string;
|
||||
@ -27,10 +27,19 @@ export interface Payment {
|
||||
macaroon: string;
|
||||
}
|
||||
|
||||
export interface UnwrappedGift {
|
||||
id: string;
|
||||
to: string;
|
||||
created_at: number;
|
||||
inner: NostrEvent;
|
||||
tags?: Array<Array<string>>; // some tags extracted
|
||||
}
|
||||
|
||||
const STORES = {
|
||||
chats: "++id",
|
||||
eventInteraction: "++id",
|
||||
payments: "++url",
|
||||
gifts: "++id",
|
||||
};
|
||||
|
||||
export class SnortDB extends Dexie {
|
||||
@ -38,6 +47,7 @@ export class SnortDB extends Dexie {
|
||||
chats!: Table<NostrEvent>;
|
||||
eventInteraction!: Table<EventInteraction>;
|
||||
payments!: Table<Payment>;
|
||||
gifts!: Table<UnwrappedGift>;
|
||||
|
||||
constructor() {
|
||||
super(NAME);
|
||||
|
@ -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");
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>;
|
||||
}
|
||||
|
||||
|
@ -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[];
|
||||
}
|
||||
|
||||
|
@ -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 [];
|
||||
}
|
||||
|
@ -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" />;
|
||||
|
@ -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} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { HexKey, TaggedRawEvent, EventKind, NoteCollection, RequestBuilder } from "@snort/system";
|
||||
import { HexKey, TaggedNostrEvent, EventKind, NoteCollection, RequestBuilder } from "@snort/system";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
|
||||
import useLogin from "Hooks/useLogin";
|
||||
@ -26,7 +26,7 @@ export default function useFollowsFeed(pubkey?: HexKey) {
|
||||
}, [contactFeed, follows, pubkey]);
|
||||
}
|
||||
|
||||
export function getFollowing(notes: readonly TaggedRawEvent[], pubkey?: HexKey) {
|
||||
export function getFollowing(notes: readonly TaggedNostrEvent[], pubkey?: HexKey) {
|
||||
const contactLists = notes.filter(a => a.kind === EventKind.ContactList && a.pubkey === pubkey);
|
||||
const pTags = contactLists?.map(a => a.tags.filter(b => b[0] === "p").map(c => c[1]));
|
||||
return [...new Set(pTags?.flat())];
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { TaggedRawEvent, Lists, EventKind, FlatNoteStore, RequestBuilder } from "@snort/system";
|
||||
import { TaggedNostrEvent, Lists, EventKind, FlatNoteStore, RequestBuilder } from "@snort/system";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
|
||||
import { bech32ToHex, getNewest, getNewestEventTagsByKey, unwrap } from "SnortUtils";
|
||||
@ -12,7 +12,7 @@ import { addSubscription, setBlocked, setBookmarked, setFollows, setMuted, setPi
|
||||
import { SnortPubKey } from "Const";
|
||||
import { SubscriptionEvent } from "Subscription";
|
||||
import useRelaysFeedFollows from "./RelaysFeedFollows";
|
||||
import { UserRelays } from "Cache";
|
||||
import { GiftsCache, UserRelays } from "Cache";
|
||||
import { System } from "index";
|
||||
import { Nip29Chats, Nip4Chats } from "chat";
|
||||
|
||||
@ -39,9 +39,9 @@ export default function useLoginFeed() {
|
||||
.authors([bech32ToHex(SnortPubKey)])
|
||||
.tag("p", [pubKey])
|
||||
.limit(1);
|
||||
b.withFilter().kinds([EventKind.GiftWrap]).tag("p", [pubKey]).since(GiftsCache.newest());
|
||||
|
||||
b.add(Nip4Chats.subscription(pubKey));
|
||||
b.add(Nip29Chats.subscription("n29.nostr.com/"));
|
||||
|
||||
return b;
|
||||
}, [pubKey]);
|
||||
@ -83,6 +83,9 @@ export default function useLoginFeed() {
|
||||
);
|
||||
Nip29Chats.onEvent(nip29Messages);
|
||||
|
||||
const giftWraps = loginFeed.data.filter(a => a.kind === EventKind.GiftWrap);
|
||||
GiftsCache.onEvent(giftWraps, publisher);
|
||||
|
||||
const subs = loginFeed.data.filter(
|
||||
a => a.kind === EventKind.SnortSubscriptions && a.pubkey === bech32ToHex(SnortPubKey)
|
||||
);
|
||||
@ -116,7 +119,7 @@ export default function useLoginFeed() {
|
||||
}
|
||||
}, [loginFeed, readNotifications]);
|
||||
|
||||
function handleMutedFeed(mutedFeed: TaggedRawEvent[]) {
|
||||
function handleMutedFeed(mutedFeed: TaggedNostrEvent[]) {
|
||||
const muted = getMutedKeys(mutedFeed);
|
||||
setMuted(login, muted.keys, muted.createdAt * 1000);
|
||||
|
||||
@ -136,21 +139,21 @@ export default function useLoginFeed() {
|
||||
}
|
||||
}
|
||||
|
||||
function handlePinnedFeed(pinnedFeed: TaggedRawEvent[]) {
|
||||
function handlePinnedFeed(pinnedFeed: TaggedNostrEvent[]) {
|
||||
const newest = getNewestEventTagsByKey(pinnedFeed, "e");
|
||||
if (newest) {
|
||||
setPinned(login, newest.keys, newest.createdAt * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function handleTagFeed(tagFeed: TaggedRawEvent[]) {
|
||||
function handleTagFeed(tagFeed: TaggedNostrEvent[]) {
|
||||
const newest = getNewestEventTagsByKey(tagFeed, "t");
|
||||
if (newest) {
|
||||
setTags(login, newest.keys, newest.createdAt * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function handleBookmarkFeed(bookmarkFeed: TaggedRawEvent[]) {
|
||||
function handleBookmarkFeed(bookmarkFeed: TaggedNostrEvent[]) {
|
||||
const newest = getNewestEventTagsByKey(bookmarkFeed, "e");
|
||||
if (newest) {
|
||||
setBookmarked(login, newest.keys, newest.createdAt * 1000);
|
||||
@ -161,7 +164,7 @@ export default function useLoginFeed() {
|
||||
|
||||
useEffect(() => {
|
||||
if (listsFeed.data) {
|
||||
const getList = (evs: readonly TaggedRawEvent[], list: Lists) =>
|
||||
const getList = (evs: readonly TaggedNostrEvent[], list: Lists) =>
|
||||
evs.filter(a => unwrap(a.tags.find(b => b[0] === "d"))[1] === list);
|
||||
|
||||
const mutedFeed = getList(listsFeed.data, Lists.Muted);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { HexKey, TaggedRawEvent, Lists, EventKind, NoteCollection, RequestBuilder } from "@snort/system";
|
||||
import { HexKey, TaggedNostrEvent, Lists, EventKind, NoteCollection, RequestBuilder } from "@snort/system";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
|
||||
import { getNewest } from "SnortUtils";
|
||||
@ -29,10 +29,10 @@ export default function useMutedFeed(pubkey?: HexKey) {
|
||||
return isMe ? muted.item : mutedList;
|
||||
}
|
||||
|
||||
export function getMutedKeys(rawNotes: TaggedRawEvent[]): {
|
||||
export function getMutedKeys(rawNotes: TaggedNostrEvent[]): {
|
||||
createdAt: number;
|
||||
keys: HexKey[];
|
||||
raw?: TaggedRawEvent;
|
||||
raw?: TaggedNostrEvent;
|
||||
} {
|
||||
const newest = getNewest(rawNotes);
|
||||
if (newest) {
|
||||
@ -47,7 +47,7 @@ export function getMutedKeys(rawNotes: TaggedRawEvent[]): {
|
||||
return { createdAt: 0, keys: [] };
|
||||
}
|
||||
|
||||
export function getMuted(feed: readonly TaggedRawEvent[], pubkey: HexKey): HexKey[] {
|
||||
export function getMuted(feed: readonly TaggedNostrEvent[], pubkey: HexKey): HexKey[] {
|
||||
const lists = feed.filter(a => a.kind === EventKind.PubkeyLists && a.pubkey === pubkey);
|
||||
return getMutedKeys(lists).keys;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { useMemo } from "react";
|
||||
import {
|
||||
HexKey,
|
||||
FullRelaySettings,
|
||||
TaggedRawEvent,
|
||||
TaggedNostrEvent,
|
||||
RelaySettings,
|
||||
EventKind,
|
||||
NoteCollection,
|
||||
@ -30,7 +30,7 @@ export default function useRelaysFeedFollows(pubkeys: HexKey[]): Array<RelayList
|
||||
return b;
|
||||
}, [pubkeys]);
|
||||
|
||||
function mapFromRelays(notes: Array<TaggedRawEvent>): Array<RelayList> {
|
||||
function mapFromRelays(notes: Array<TaggedNostrEvent>): Array<RelayList> {
|
||||
return notes.map(ev => {
|
||||
return {
|
||||
pubkey: ev.pubkey,
|
||||
@ -51,7 +51,7 @@ export default function useRelaysFeedFollows(pubkeys: HexKey[]): Array<RelayList
|
||||
}
|
||||
|
||||
// instead of discarding the follow list we should also use it for follow graph
|
||||
function mapFromContactList(notes: Array<TaggedRawEvent>): Array<RelayList> {
|
||||
function mapFromContactList(notes: Array<TaggedNostrEvent>): Array<RelayList> {
|
||||
return notes.map(ev => {
|
||||
if (ev.content !== "" && ev.content !== "{}" && ev.content.startsWith("{") && ev.content.endsWith("}")) {
|
||||
try {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Nostrich from "public/logo_256.png";
|
||||
|
||||
import { TaggedRawEvent, EventKind, MetadataCache } from "@snort/system";
|
||||
import { TaggedNostrEvent, EventKind, MetadataCache } from "@snort/system";
|
||||
import { getDisplayName } from "Element/ProfileImage";
|
||||
import { MentionRegex } from "Const";
|
||||
import { tagFilterOfTextRepost, unwrap } from "SnortUtils";
|
||||
@ -14,7 +14,7 @@ export interface NotificationRequest {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export async function makeNotification(ev: TaggedRawEvent): Promise<NotificationRequest | null> {
|
||||
export async function makeNotification(ev: TaggedNostrEvent): Promise<NotificationRequest | null> {
|
||||
switch (ev.kind) {
|
||||
case EventKind.TextNote: {
|
||||
if (ev.tags.some(tagFilterOfTextRepost(ev))) {
|
||||
@ -40,7 +40,7 @@ export async function makeNotification(ev: TaggedRawEvent): Promise<Notification
|
||||
return null;
|
||||
}
|
||||
|
||||
function replaceTagsWithUser(ev: TaggedRawEvent, users: MetadataCache[]) {
|
||||
function replaceTagsWithUser(ev: TaggedNostrEvent, users: MetadataCache[]) {
|
||||
return ev.content
|
||||
.split(MentionRegex)
|
||||
.map(match => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { NostrPrefix } from "@snort/system";
|
||||
import { NostrPrefix, TLVEntryType, decodeTLV } from "@snort/system";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
|
||||
import UnreadCount from "Element/UnreadCount";
|
||||
@ -44,15 +44,7 @@ export default function MessagesPage() {
|
||||
function openChat(e: React.MouseEvent<HTMLDivElement>, type: ChatType, id: string) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (type === ChatType.DirectMessage) {
|
||||
navigate(`/messages/${hexToBech32(NostrPrefix.PublicKey, id)}`, {
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
navigate(`/messages/${encodeURIComponent(id)}`, {
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
navigate(`/messages/${encodeURIComponent(id)}`);
|
||||
}
|
||||
|
||||
function noteToSelf(chat: Chat) {
|
||||
@ -63,16 +55,34 @@ export default function MessagesPage() {
|
||||
);
|
||||
}
|
||||
|
||||
function person(chat: Chat) {
|
||||
function conversationIdent(chat: Chat) {
|
||||
if (chat.participants.length === 1) {
|
||||
const p = chat.participants[0];
|
||||
|
||||
if (p.type === "pubkey") {
|
||||
return <ProfileImage pubkey={p.id} className="f-grow" link="" />;
|
||||
} else {
|
||||
return <ProfileImage pubkey={""} overrideUsername={p.id} className="f-grow" link="" />;
|
||||
}
|
||||
} else {
|
||||
return (
|
||||
<div className="flex f-grow pfp-overlap">
|
||||
{chat.participants.map(v => (
|
||||
<ProfileImage pubkey={v.id} link="" showUsername={false} />
|
||||
))}
|
||||
<div className="f-grow">{chat.title}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function conversation(chat: Chat) {
|
||||
if (!login.publicKey) return null;
|
||||
if (chat.id === login.publicKey) return noteToSelf(chat);
|
||||
const participants = chat.participants.map(a => a.id);
|
||||
if (participants.length === 1 && participants[0] === login.publicKey) return noteToSelf(chat);
|
||||
return (
|
||||
<div className="flex mb10" key={chat.id} onClick={e => openChat(e, chat.type, chat.id)}>
|
||||
{chat.type === ChatType.DirectMessage ? (
|
||||
<ProfileImage pubkey={chat.id} className="f-grow" link="" />
|
||||
) : (
|
||||
<ProfileImage pubkey={chat.id} overrideUsername={chat.id} className="f-grow" link="" />
|
||||
)}
|
||||
{conversationIdent(chat)}
|
||||
<div className="nowrap">
|
||||
<small>
|
||||
<NoteTime from={chat.lastMessage * 1000} fallback={formatMessage({ defaultMessage: "Just now" })} />
|
||||
@ -97,22 +107,31 @@ export default function MessagesPage() {
|
||||
</div>
|
||||
{chats
|
||||
.sort((a, b) => {
|
||||
return a.id === login.publicKey ? -1 : b.id === login.publicKey ? 1 : b.lastMessage - a.lastMessage;
|
||||
const aSelf = a.participants.length === 1 && a.participants[0].id === login.publicKey;
|
||||
const bSelf = b.participants.length === 1 && b.participants[0].id === login.publicKey;
|
||||
if (aSelf || bSelf) {
|
||||
return aSelf ? -1 : 1;
|
||||
}
|
||||
return b.lastMessage > a.lastMessage ? 1 : -1;
|
||||
})
|
||||
.map(person)}
|
||||
.map(conversation)}
|
||||
</div>
|
||||
)}
|
||||
{chat && <DmWindow id={chat} />}
|
||||
{pageWidth >= ThreeCol && chat && (
|
||||
<div>
|
||||
<ProfileDmActions pubkey={chat} />
|
||||
<ProfileDmActions id={chat} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ProfileDmActions({ pubkey }: { pubkey: string }) {
|
||||
function ProfileDmActions({ id }: { id: string }) {
|
||||
const authors = decodeTLV(id)
|
||||
.filter(a => a.type === TLVEntryType.Author)
|
||||
.map(a => a.value as string);
|
||||
const pubkey = authors[0];
|
||||
const profile = useUserProfile(System, pubkey);
|
||||
const { block, unblock, isBlocked } = useModeration();
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { bytesToHex } from "@noble/hashes/utils";
|
||||
import { bech32, base32hex } from "@scure/base";
|
||||
import {
|
||||
HexKey,
|
||||
TaggedRawEvent,
|
||||
TaggedNostrEvent,
|
||||
u256,
|
||||
EventKind,
|
||||
encodeTLV,
|
||||
@ -149,11 +149,11 @@ export function normalizeReaction(content: string) {
|
||||
/**
|
||||
* Get reactions to a specific event (#e + kind filter)
|
||||
*/
|
||||
export function getReactions(notes: readonly TaggedRawEvent[] | undefined, id: u256, kind?: EventKind) {
|
||||
export function getReactions(notes: readonly TaggedNostrEvent[] | undefined, id: u256, kind?: EventKind) {
|
||||
return notes?.filter(a => a.kind === (kind ?? a.kind) && a.tags.some(a => a[0] === "e" && a[1] === id)) || [];
|
||||
}
|
||||
|
||||
export function getAllReactions(notes: readonly TaggedRawEvent[] | undefined, ids: Array<u256>, kind?: EventKind) {
|
||||
export function getAllReactions(notes: readonly TaggedNostrEvent[] | undefined, ids: Array<u256>, kind?: EventKind) {
|
||||
return notes?.filter(a => a.kind === (kind ?? a.kind) && a.tags.some(a => a[0] === "e" && ids.includes(a[1]))) || [];
|
||||
}
|
||||
|
||||
@ -189,9 +189,9 @@ export function debounce(timeout: number, fn: () => void) {
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
|
||||
export function dedupeByPubkey(events: TaggedRawEvent[]) {
|
||||
export function dedupeByPubkey(events: TaggedNostrEvent[]) {
|
||||
const deduped = events.reduce(
|
||||
({ list, seen }: { list: TaggedRawEvent[]; seen: Set<HexKey> }, ev) => {
|
||||
({ list, seen }: { list: TaggedNostrEvent[]; seen: Set<HexKey> }, ev) => {
|
||||
if (seen.has(ev.pubkey)) {
|
||||
return { list, seen };
|
||||
}
|
||||
@ -203,7 +203,7 @@ export function dedupeByPubkey(events: TaggedRawEvent[]) {
|
||||
},
|
||||
{ list: [], seen: new Set([]) }
|
||||
);
|
||||
return deduped.list as TaggedRawEvent[];
|
||||
return deduped.list as TaggedNostrEvent[];
|
||||
}
|
||||
|
||||
export function dedupeById<T extends { id: string }>(events: Array<T>) {
|
||||
@ -228,8 +228,8 @@ export function dedupeById<T extends { id: string }>(events: Array<T>) {
|
||||
* @param events List of all notes to filter from
|
||||
* @returns
|
||||
*/
|
||||
export function getLatestByPubkey(events: TaggedRawEvent[]): Map<HexKey, TaggedRawEvent> {
|
||||
const deduped = events.reduce((results: Map<HexKey, TaggedRawEvent>, ev) => {
|
||||
export function getLatestByPubkey(events: TaggedNostrEvent[]): Map<HexKey, TaggedNostrEvent> {
|
||||
const deduped = events.reduce((results: Map<HexKey, TaggedNostrEvent>, ev) => {
|
||||
if (!results.has(ev.pubkey)) {
|
||||
const latest = getNewest(events.filter(a => a.pubkey === ev.pubkey));
|
||||
if (latest) {
|
||||
@ -237,7 +237,7 @@ export function getLatestByPubkey(events: TaggedRawEvent[]): Map<HexKey, TaggedR
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}, new Map<HexKey, TaggedRawEvent>());
|
||||
}, new Map<HexKey, TaggedNostrEvent>());
|
||||
return deduped;
|
||||
}
|
||||
|
||||
@ -274,7 +274,7 @@ export function randomSample<T>(coll: T[], size: number) {
|
||||
return random.sort(() => (Math.random() >= 0.5 ? 1 : -1)).slice(0, size);
|
||||
}
|
||||
|
||||
export function getNewest(rawNotes: readonly TaggedRawEvent[]) {
|
||||
export function getNewest(rawNotes: readonly TaggedNostrEvent[]) {
|
||||
const notes = [...rawNotes];
|
||||
notes.sort((a, b) => b.created_at - a.created_at);
|
||||
if (notes.length > 0) {
|
||||
@ -290,7 +290,7 @@ export function getNewestProfile(rawNotes: MetadataCache[]) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getNewestEventTagsByKey(evs: TaggedRawEvent[], tag: string) {
|
||||
export function getNewestEventTagsByKey(evs: TaggedNostrEvent[], tag: string) {
|
||||
const newest = getNewest(evs);
|
||||
if (newest) {
|
||||
const keys = newest.tags.filter(p => p && p.length === 2 && p[0] === tag).map(p => p[1]);
|
||||
@ -301,7 +301,7 @@ export function getNewestEventTagsByKey(evs: TaggedRawEvent[], tag: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function tagFilterOfTextRepost(note: TaggedRawEvent, id?: u256): (tag: string[], i: number) => boolean {
|
||||
export function tagFilterOfTextRepost(note: TaggedNostrEvent, id?: u256): (tag: string[], i: number) => boolean {
|
||||
return (tag, i) =>
|
||||
tag[0] === "e" && tag[3] === "mention" && note.content === `#[${i}]` && (id ? tag[1] === id : true);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { NostrEvent, TaggedRawEvent } from "@snort/system";
|
||||
import { NostrEvent, TaggedNostrEvent } from "@snort/system";
|
||||
|
||||
interface NoteCreatorStore {
|
||||
show: boolean;
|
||||
@ -7,7 +7,7 @@ interface NoteCreatorStore {
|
||||
error: string;
|
||||
active: boolean;
|
||||
preview?: NostrEvent;
|
||||
replyTo?: TaggedRawEvent;
|
||||
replyTo?: TaggedNostrEvent;
|
||||
showAdvanced: boolean;
|
||||
selectedCustomRelays: false | Array<string>;
|
||||
zapForward: string;
|
||||
@ -47,7 +47,7 @@ const NoteCreatorSlice = createSlice({
|
||||
setPreview: (state, action: PayloadAction<NostrEvent | undefined>) => {
|
||||
state.preview = action.payload;
|
||||
},
|
||||
setReplyTo: (state, action: PayloadAction<TaggedRawEvent | undefined>) => {
|
||||
setReplyTo: (state, action: PayloadAction<TaggedNostrEvent | undefined>) => {
|
||||
state.replyTo = action.payload;
|
||||
},
|
||||
setShowAdvanced: (state, action: PayloadAction<boolean>) => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Connection, EventKind, NostrEvent, EventBuilder, EventExt } from "@snort/system";
|
||||
import { Connection, EventKind, NostrEvent, EventBuilder, EventExt, PrivateKeySigner } from "@snort/system";
|
||||
import { LNWallet, WalletError, WalletErrorCode, WalletInfo, WalletInvoice, WalletInvoiceState } from "Wallet";
|
||||
import debug from "debug";
|
||||
|
||||
@ -163,9 +163,10 @@ export class NostrConnectWallet implements LNWallet {
|
||||
method,
|
||||
params,
|
||||
});
|
||||
const signer = new PrivateKeySigner(this.#config.secret);
|
||||
const eb = new EventBuilder();
|
||||
eb.kind(23194 as EventKind)
|
||||
.content(await EventExt.encryptDm(payload, this.#config.secret, this.#config.walletPubkey))
|
||||
.content(await signer.nip4Encrypt(payload, this.#config.walletPubkey))
|
||||
.tag(["p", this.#config.walletPubkey]);
|
||||
|
||||
const evCommand = await eb.buildAndSign(this.#config.secret);
|
||||
@ -187,7 +188,7 @@ export class NostrConnectWallet implements LNWallet {
|
||||
return await new Promise<T>((resolve, reject) => {
|
||||
this.#commandQueue.set(evCommand.id, {
|
||||
resolve: async (o: string) => {
|
||||
const reply = JSON.parse(await EventExt.decryptDm(o, this.#config.secret, this.#config.walletPubkey));
|
||||
const reply = JSON.parse(await signer.nip4Decrypt(o, this.#config.walletPubkey));
|
||||
debug("NWC")("%o", reply);
|
||||
resolve(reply);
|
||||
},
|
||||
|
@ -2,27 +2,46 @@ import { useSyncExternalStore } from "react";
|
||||
import { Nip4ChatSystem } from "./nip4";
|
||||
import { EventKind, EventPublisher, NostrEvent, RequestBuilder, SystemInterface, UserMetadata } from "@snort/system";
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { Chats } from "Cache";
|
||||
import { Chats, GiftsCache } from "Cache";
|
||||
import { findTag, unixNow } from "SnortUtils";
|
||||
import { Nip29ChatSystem } from "./nip29";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { Nip24ChatSystem } from "./nip24";
|
||||
|
||||
export enum ChatType {
|
||||
DirectMessage = 1,
|
||||
PublicGroupChat = 2,
|
||||
PrivateGroupChat = 3,
|
||||
PrivateDirectMessage = 4,
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string;
|
||||
from: string;
|
||||
created_at: number;
|
||||
tags: Array<Array<string>>;
|
||||
needsDecryption: boolean;
|
||||
content: string;
|
||||
decrypt: (pub: EventPublisher) => Promise<string>;
|
||||
}
|
||||
|
||||
export interface ChatParticipant {
|
||||
type: "pubkey" | "generic";
|
||||
id: string;
|
||||
profile?: UserMetadata;
|
||||
}
|
||||
|
||||
export interface Chat {
|
||||
type: ChatType;
|
||||
id: string;
|
||||
title?: string;
|
||||
unread: number;
|
||||
lastMessage: number;
|
||||
messages: Array<NostrEvent>;
|
||||
profile?: UserMetadata;
|
||||
createMessage(msg: string, pub: EventPublisher): Promise<NostrEvent>;
|
||||
sendMessage(ev: NostrEvent, system: SystemInterface): void | Promise<void>;
|
||||
participants: Array<ChatParticipant>;
|
||||
messages: Array<ChatMessage>;
|
||||
createMessage(msg: string, pub: EventPublisher): Promise<Array<NostrEvent>>;
|
||||
sendMessage(ev: Array<NostrEvent>, system: SystemInterface): void | Promise<void>;
|
||||
}
|
||||
|
||||
export interface ChatSystem {
|
||||
@ -37,6 +56,7 @@ export interface ChatSystem {
|
||||
|
||||
export const Nip4Chats = new Nip4ChatSystem(Chats);
|
||||
export const Nip29Chats = new Nip29ChatSystem(Chats);
|
||||
export const Nip24Chats = new Nip24ChatSystem(GiftsCache);
|
||||
|
||||
/**
|
||||
* Extract the P tag of the event
|
||||
@ -89,10 +109,18 @@ export function useNip29Chat() {
|
||||
);
|
||||
}
|
||||
|
||||
export function useNip24Chat() {
|
||||
const { publicKey } = useLogin();
|
||||
return useSyncExternalStore(
|
||||
c => Nip24Chats.hook(c),
|
||||
() => Nip24Chats.snapshot(publicKey)
|
||||
);
|
||||
}
|
||||
|
||||
export function useChatSystem() {
|
||||
const nip4 = useNip4Chat();
|
||||
const nip29 = useNip29Chat();
|
||||
const nip24 = useNip24Chat();
|
||||
const { muted, blocked } = useModeration();
|
||||
|
||||
return [...nip4, ...nip29].filter(a => !(muted.includes(a.id) || blocked.includes(a.id)));
|
||||
return [...nip4, ...nip24].filter(a => !(muted.includes(a.id) || blocked.includes(a.id)));
|
||||
}
|
||||
|
129
packages/app/src/chat/nip24.ts
Normal file
129
packages/app/src/chat/nip24.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import { ExternalStore, dedupe } from "@snort/shared";
|
||||
import {
|
||||
EventKind,
|
||||
SystemInterface,
|
||||
NostrPrefix,
|
||||
encodeTLVEntries,
|
||||
TLVEntryType,
|
||||
TLVEntry,
|
||||
decodeTLV,
|
||||
} from "@snort/system";
|
||||
import { GiftWrapCache } from "Cache/GiftWrapCache";
|
||||
import { UnwrappedGift } from "Db";
|
||||
import { Chat, ChatSystem, ChatType, lastReadInChat } from "chat";
|
||||
|
||||
export class Nip24ChatSystem extends ExternalStore<Array<Chat>> implements ChatSystem {
|
||||
#cache: GiftWrapCache;
|
||||
|
||||
constructor(cache: GiftWrapCache) {
|
||||
super();
|
||||
this.#cache = cache;
|
||||
this.#cache.hook(() => this.notifyChange(), "*");
|
||||
}
|
||||
|
||||
subscription() {
|
||||
// ignored
|
||||
return undefined;
|
||||
}
|
||||
|
||||
onEvent() {
|
||||
// ignored
|
||||
}
|
||||
|
||||
listChats(pk: string): Chat[] {
|
||||
const evs = this.#nip24Events();
|
||||
const messages = evs.filter(a => a.to === pk);
|
||||
const chatId = (u: UnwrappedGift) => {
|
||||
const pTags = dedupe([...(u.tags ?? []).filter(a => a[0] === "p").map(a => a[1]), u.inner.pubkey])
|
||||
.sort()
|
||||
.filter(a => a !== pk);
|
||||
|
||||
return encodeTLVEntries(
|
||||
"chat24" as NostrPrefix,
|
||||
...pTags.map(
|
||||
v =>
|
||||
({
|
||||
value: v,
|
||||
type: TLVEntryType.Author,
|
||||
length: v.length,
|
||||
} as TLVEntry)
|
||||
)
|
||||
);
|
||||
};
|
||||
return dedupe(messages.map(a => chatId(a))).map(a => {
|
||||
const chatMessages = messages.filter(b => chatId(b) === a);
|
||||
return Nip24ChatSystem.createChatObj(a, chatMessages);
|
||||
});
|
||||
}
|
||||
|
||||
static createChatObj(id: string, messages: Array<UnwrappedGift>) {
|
||||
const last = lastReadInChat(id);
|
||||
const participants = decodeTLV(id)
|
||||
.filter(v => v.type === TLVEntryType.Author)
|
||||
.map(v => ({
|
||||
type: "pubkey",
|
||||
id: v.value as string,
|
||||
}));
|
||||
const title = messages.reduce(
|
||||
(acc, v) => {
|
||||
const sbj = v.tags?.find(a => a[0] === "subject")?.[1];
|
||||
if (v.created_at > acc.t && sbj) {
|
||||
acc.title = sbj;
|
||||
acc.t = v.created_at;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
t: 0,
|
||||
title: "",
|
||||
}
|
||||
);
|
||||
return {
|
||||
type: ChatType.PrivateDirectMessage,
|
||||
id,
|
||||
title: title.title,
|
||||
unread: messages.reduce((acc, v) => (v.created_at > last ? acc++ : acc), 0),
|
||||
lastMessage: messages.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0),
|
||||
participants,
|
||||
messages: messages.map(m => ({
|
||||
id: m.id,
|
||||
created_at: m.created_at,
|
||||
from: m.inner.pubkey,
|
||||
tags: m.tags,
|
||||
content: "",
|
||||
needsDecryption: true,
|
||||
decrypt: async pub => {
|
||||
return await pub.decryptDm(m.inner);
|
||||
},
|
||||
})),
|
||||
createMessage: async (msg, pub) => {
|
||||
const gossip = pub.createUnsigned(EventKind.ChatRumor, msg, eb => {
|
||||
for (const pt of participants) {
|
||||
eb.tag(["p", pt.id]);
|
||||
}
|
||||
return eb;
|
||||
});
|
||||
const messages = [];
|
||||
for (const pt of participants) {
|
||||
const recvSealedN = await pub.giftWrap(await pub.sealRumor(gossip, pt.id), pt.id);
|
||||
messages.push(recvSealedN);
|
||||
}
|
||||
const sendSealed = await pub.giftWrap(await pub.sealRumor(gossip, pub.pubKey), pub.pubKey);
|
||||
return [...messages, sendSealed];
|
||||
},
|
||||
sendMessage: (ev, system: SystemInterface) => {
|
||||
console.debug(ev);
|
||||
ev.forEach(a => system.BroadcastEvent(a));
|
||||
},
|
||||
} as Chat;
|
||||
}
|
||||
|
||||
takeSnapshot(p: string): Chat[] {
|
||||
return this.listChats(p);
|
||||
}
|
||||
|
||||
#nip24Events() {
|
||||
const sn = this.#cache.takeSnapshot();
|
||||
return sn.filter(a => a.inner.kind === EventKind.SealedRumor);
|
||||
}
|
||||
}
|
0
packages/app/src/chat/nip28.ts
Normal file
0
packages/app/src/chat/nip28.ts
Normal file
@ -57,19 +57,41 @@ export class Nip29ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
|
||||
return {
|
||||
type: ChatType.PublicGroupChat,
|
||||
id: g,
|
||||
title: `${relay}/${channel}`,
|
||||
unread: messages.reduce((acc, v) => (v.created_at > lastRead ? acc++ : acc), 0),
|
||||
lastMessage: messages.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0),
|
||||
messages,
|
||||
createMessage: (msg, pub) => {
|
||||
return pub.generic(eb => {
|
||||
return eb
|
||||
.kind(EventKind.SimpleChatMessage)
|
||||
.tag(["g", `/${channel}`, relay])
|
||||
.content(msg);
|
||||
});
|
||||
messages: messages.map(m => ({
|
||||
id: m.id,
|
||||
created_at: m.created_at,
|
||||
from: m.pubkey,
|
||||
tags: m.tags,
|
||||
needsDecryption: false,
|
||||
content: m.content,
|
||||
decrypt: async () => {
|
||||
return m.content;
|
||||
},
|
||||
})),
|
||||
participants: [
|
||||
{
|
||||
type: "generic",
|
||||
id: "",
|
||||
profile: {
|
||||
name: `${relay}/${channel}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
createMessage: async (msg, pub) => {
|
||||
return [
|
||||
await pub.generic(eb => {
|
||||
return eb
|
||||
.kind(EventKind.SimpleChatMessage)
|
||||
.tag(["g", `/${channel}`, relay])
|
||||
.content(msg);
|
||||
}),
|
||||
];
|
||||
},
|
||||
sendMessage: async (ev: NostrEvent, system: SystemInterface) => {
|
||||
await system.WriteOnceToRelay(`wss://${relay}`, ev);
|
||||
sendMessage: async (ev, system: SystemInterface) => {
|
||||
ev.forEach(async a => await system.WriteOnceToRelay(`wss://${relay}`, a));
|
||||
},
|
||||
} as Chat;
|
||||
});
|
||||
|
@ -1,5 +1,14 @@
|
||||
import { ExternalStore, FeedCache, dedupe } from "@snort/shared";
|
||||
import { EventKind, NostrEvent, RequestBuilder, SystemInterface } from "@snort/system";
|
||||
import {
|
||||
EventKind,
|
||||
NostrEvent,
|
||||
NostrPrefix,
|
||||
RequestBuilder,
|
||||
SystemInterface,
|
||||
TLVEntryType,
|
||||
decodeTLV,
|
||||
encodeTLVEntries,
|
||||
} from "@snort/system";
|
||||
import { Chat, ChatSystem, ChatType, inChatWith, lastReadInChat, selfChat } from "chat";
|
||||
import { debug } from "debug";
|
||||
|
||||
@ -40,27 +49,50 @@ export class Nip4ChatSystem extends ExternalStore<Array<Chat>> implements ChatSy
|
||||
|
||||
listChats(pk: string): Chat[] {
|
||||
const myDms = this.#nip4Events();
|
||||
return dedupe(myDms.map(a => inChatWith(a, pk))).map(a => {
|
||||
const messages = myDms.filter(
|
||||
b => (a === pk && selfChat(b, pk)) || (!selfChat(b, pk) && inChatWith(b, pk) === a)
|
||||
);
|
||||
const chatId = (a: NostrEvent) => {
|
||||
return encodeTLVEntries("chat4" as NostrPrefix, {
|
||||
type: TLVEntryType.Author,
|
||||
value: inChatWith(a, pk),
|
||||
length: 0,
|
||||
});
|
||||
};
|
||||
|
||||
return dedupe(myDms.map(chatId)).map(a => {
|
||||
const messages = myDms.filter(b => chatId(b) === a);
|
||||
return Nip4ChatSystem.createChatObj(a, messages);
|
||||
});
|
||||
}
|
||||
|
||||
static createChatObj(id: string, messages: Array<NostrEvent>) {
|
||||
const last = lastReadInChat(id);
|
||||
const pk = decodeTLV(id).find(a => a.type === TLVEntryType.Author)?.value as string;
|
||||
return {
|
||||
type: ChatType.DirectMessage,
|
||||
id,
|
||||
unread: messages.reduce((acc, v) => (v.created_at > last ? acc++ : acc), 0),
|
||||
lastMessage: messages.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0),
|
||||
messages,
|
||||
createMessage: (msg, pub) => {
|
||||
return pub.sendDm(msg, id);
|
||||
participants: [
|
||||
{
|
||||
type: "pubkey",
|
||||
id: pk,
|
||||
},
|
||||
],
|
||||
messages: messages.map(m => ({
|
||||
id: m.id,
|
||||
created_at: m.created_at,
|
||||
from: m.pubkey,
|
||||
tags: m.tags,
|
||||
content: "",
|
||||
needsDecryption: true,
|
||||
decrypt: async pub => {
|
||||
return await pub.decryptDm(m);
|
||||
},
|
||||
})),
|
||||
createMessage: async (msg, pub) => {
|
||||
return [await pub.sendDm(msg, pk)];
|
||||
},
|
||||
sendMessage: (ev: NostrEvent, system: SystemInterface) => {
|
||||
system.BroadcastEvent(ev);
|
||||
sendMessage: (ev, system: SystemInterface) => {
|
||||
ev.forEach(a => system.BroadcastEvent(a));
|
||||
},
|
||||
} as Chat;
|
||||
}
|
||||
|
Reference in New Issue
Block a user