From e8e65c71cd3df1ae648ac216504d5703b6de7071 Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 24 Aug 2023 15:25:54 +0100 Subject: [PATCH] DM styles update --- packages/app/src/Element/CashuNuts.tsx | 3 +- packages/app/src/Element/DmWindow.css | 4 +- packages/app/src/Element/DmWindow.tsx | 7 +- packages/app/src/Element/FollowListBase.tsx | 4 +- packages/app/src/Element/Mention.tsx | 3 +- packages/app/src/Element/Nip05.tsx | 3 +- packages/app/src/Element/Nip5Service.tsx | 2 +- packages/app/src/Element/Note.css | 2 +- packages/app/src/Element/NoteFooter.tsx | 2 +- packages/app/src/Element/NoteReaction.tsx | 3 +- packages/app/src/Element/Poll.tsx | 3 +- packages/app/src/Element/ProfileImage.tsx | 12 +- packages/app/src/Element/ProfilePreview.tsx | 16 +- packages/app/src/Element/Username.tsx | 3 +- packages/app/src/Element/ZapButton.tsx | 3 +- packages/app/src/Feed/BadgesFeed.ts | 7 +- packages/app/src/Feed/EventFeed.ts | 3 +- packages/app/src/Feed/FollowersFeed.ts | 3 +- packages/app/src/Feed/FollowsFeed.ts | 3 +- packages/app/src/Feed/LiveChatFeed.tsx | 3 +- packages/app/src/Feed/LoginFeed.ts | 4 +- packages/app/src/Feed/MuteList.ts | 3 +- packages/app/src/Feed/RelaysFeed.tsx | 3 +- packages/app/src/Feed/RelaysFeedFollows.tsx | 3 +- packages/app/src/Feed/ThreadFeed.ts | 3 +- packages/app/src/Feed/TimelineFeed.ts | 7 +- packages/app/src/Feed/ZapsFeed.ts | 4 +- .../app/src/Hooks/useNotelistSubscription.ts | 5 +- packages/app/src/Pages/Layout.css | 2 +- packages/app/src/Pages/Layout.tsx | 2 +- packages/app/src/Pages/MessagesPage.css | 59 ++++++- packages/app/src/Pages/MessagesPage.tsx | 158 ++++++++++++++---- packages/app/src/Pages/Notifications.tsx | 21 ++- packages/app/src/Pages/ProfilePage.tsx | 3 +- packages/app/src/Pages/ZapPool.tsx | 2 +- packages/app/src/Pages/new/GetVerified.tsx | 3 +- packages/app/src/Pages/new/ProfileSetup.tsx | 2 +- packages/app/src/Pages/settings/Profile.tsx | 2 +- packages/app/src/Tasks/TaskList.tsx | 3 +- packages/app/src/chat/index.ts | 55 ++++++ packages/app/src/chat/nip24.ts | 5 +- packages/app/src/index.css | 14 +- packages/app/src/index.tsx | 5 +- packages/shared/src/utils.ts | 2 +- packages/system-react/example/example.tsx | 12 +- packages/system-react/package.json | 8 +- packages/system-react/src/context.tsx | 4 + packages/system-react/src/index.ts | 2 + .../system-react/src/useRequestBuilder.tsx | 9 +- packages/system-react/src/useUserProfile.ts | 8 +- packages/system-react/src/useUserSearch.tsx | 37 ++++ packages/system/package.json | 6 +- packages/system/src/index.ts | 2 + packages/system/src/system-worker.ts | 6 +- 54 files changed, 411 insertions(+), 142 deletions(-) create mode 100644 packages/system-react/src/context.tsx create mode 100644 packages/system-react/src/useUserSearch.tsx diff --git a/packages/app/src/Element/CashuNuts.tsx b/packages/app/src/Element/CashuNuts.tsx index e2332810..8674657b 100644 --- a/packages/app/src/Element/CashuNuts.tsx +++ b/packages/app/src/Element/CashuNuts.tsx @@ -3,7 +3,6 @@ import { FormattedMessage } from "react-intl"; import useLogin from "Hooks/useLogin"; import { useUserProfile } from "@snort/system-react"; -import { System } from "index"; interface Token { token: Array<{ @@ -17,7 +16,7 @@ interface Token { export default function CashuNuts({ token }: { token: string }) { const login = useLogin(); - const profile = useUserProfile(System, login.publicKey); + const profile = useUserProfile(login.publicKey); async function copyToken(e: React.MouseEvent, token: string) { e.stopPropagation(); diff --git a/packages/app/src/Element/DmWindow.css b/packages/app/src/Element/DmWindow.css index 9d31a306..1fbefc88 100644 --- a/packages/app/src/Element/DmWindow.css +++ b/packages/app/src/Element/DmWindow.css @@ -3,6 +3,9 @@ flex-direction: column; height: 100%; } +.dm-window > div:nth-child(1) { + padding: 12px 0; +} .dm-window > div:nth-child(2) { overflow-y: auto; @@ -15,7 +18,6 @@ .dm-window > div:nth-child(3) { display: flex; align-items: center; - background-color: var(--bg-color); gap: 10px; padding: 5px 10px; } diff --git a/packages/app/src/Element/DmWindow.tsx b/packages/app/src/Element/DmWindow.tsx index 87df9017..5b270747 100644 --- a/packages/app/src/Element/DmWindow.tsx +++ b/packages/app/src/Element/DmWindow.tsx @@ -6,14 +6,13 @@ import DM from "Element/DM"; import NoteToSelf from "Element/NoteToSelf"; import useLogin from "Hooks/useLogin"; import WriteMessage from "Element/WriteMessage"; -import { Chat, ChatParticipant, useChatSystem } from "chat"; -import { Nip4ChatSystem } from "chat/nip4"; +import { Chat, ChatParticipant, createEmptyChatObject, useChatSystem } from "chat"; 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, []); + const chat = dms.find(a => a.id === id) ?? createEmptyChatObject(id); function participant(p: ChatParticipant) { if (p.id === pubKey) { @@ -37,7 +36,7 @@ export default function DmWindow({ id }: { id: string }) { {chat.participants.map(v => ( ))} - {chat.title ?? } + {chat.title ?? } ); } diff --git a/packages/app/src/Element/FollowListBase.tsx b/packages/app/src/Element/FollowListBase.tsx index bcc13c3f..c10eaa5a 100644 --- a/packages/app/src/Element/FollowListBase.tsx +++ b/packages/app/src/Element/FollowListBase.tsx @@ -16,6 +16,7 @@ export interface FollowListBaseProps { showAbout?: boolean; className?: string; actions?: ReactNode; + profileActions?: (pk: string) => ReactNode; } export default function FollowListBase({ @@ -25,6 +26,7 @@ export default function FollowListBase({ showAbout, className, actions, + profileActions, }: FollowListBaseProps) { const publisher = useEventPublisher(); const { follows, relays } = useLogin(); @@ -48,7 +50,7 @@ export default function FollowListBase({ )} {pubkeys?.map(a => ( - + ))} ); diff --git a/packages/app/src/Element/Mention.tsx b/packages/app/src/Element/Mention.tsx index 8e53f26d..216774a6 100644 --- a/packages/app/src/Element/Mention.tsx +++ b/packages/app/src/Element/Mention.tsx @@ -5,10 +5,9 @@ import { HexKey } from "@snort/system"; import { useUserProfile } from "@snort/system-react"; import { profileLink } from "SnortUtils"; import { getDisplayName } from "Element/ProfileImage"; -import { System } from "index"; export default function Mention({ pubkey, relays }: { pubkey: HexKey; relays?: Array | string }) { - const user = useUserProfile(System, pubkey); + const user = useUserProfile(pubkey); const name = useMemo(() => { return getDisplayName(user, pubkey); diff --git a/packages/app/src/Element/Nip05.tsx b/packages/app/src/Element/Nip05.tsx index 546d6c65..847627fb 100644 --- a/packages/app/src/Element/Nip05.tsx +++ b/packages/app/src/Element/Nip05.tsx @@ -3,10 +3,9 @@ import { HexKey } from "@snort/system"; import Icon from "Icons/Icon"; import { useUserProfile } from "@snort/system-react"; -import { System } from "index"; export function useIsVerified(pubkey: HexKey, bypassCheck?: boolean) { - const profile = useUserProfile(System, pubkey); + const profile = useUserProfile(pubkey); return { isVerified: bypassCheck || profile?.isNostrAddressValid }; } diff --git a/packages/app/src/Element/Nip5Service.tsx b/packages/app/src/Element/Nip5Service.tsx index 0185e870..19318901 100644 --- a/packages/app/src/Element/Nip5Service.tsx +++ b/packages/app/src/Element/Nip5Service.tsx @@ -44,7 +44,7 @@ export default function Nip5Service(props: Nip05ServiceProps) { const { helpText = true } = props; const { formatMessage } = useIntl(); const pubkey = useLogin().publicKey; - const user = useUserProfile(System, pubkey); + const user = useUserProfile(pubkey); const publisher = useEventPublisher(); const svc = useMemo(() => new ServiceProvider(props.service), [props.service]); const [serviceConfig, setServiceConfig] = useState(); diff --git a/packages/app/src/Element/Note.css b/packages/app/src/Element/Note.css index 256eb6da..47f19b7f 100644 --- a/packages/app/src/Element/Note.css +++ b/packages/app/src/Element/Note.css @@ -2,7 +2,7 @@ min-height: 110px; display: flex; flex-direction: column; - gap: 8px; + gap: 12px; } .note:hover { diff --git a/packages/app/src/Element/NoteFooter.tsx b/packages/app/src/Element/NoteFooter.tsx index 9fed46ad..6e74f1f1 100644 --- a/packages/app/src/Element/NoteFooter.tsx +++ b/packages/app/src/Element/NoteFooter.tsx @@ -50,7 +50,7 @@ export default function NoteFooter(props: NoteFooterProps) { const { formatMessage } = useIntl(); const login = useLogin(); const { publicKey, preferences: prefs, relays } = login; - const author = useUserProfile(System, ev.pubkey); + const author = useUserProfile(ev.pubkey); const interactionCache = useInteractionCache(publicKey, ev.id); const publisher = useEventPublisher(); const showNoteCreatorModal = useSelector((s: RootState) => s.noteCreator.show); diff --git a/packages/app/src/Element/NoteReaction.tsx b/packages/app/src/Element/NoteReaction.tsx index 808dfca2..060c7f1c 100644 --- a/packages/app/src/Element/NoteReaction.tsx +++ b/packages/app/src/Element/NoteReaction.tsx @@ -10,7 +10,6 @@ import useModeration from "Hooks/useModeration"; import { FormattedMessage } from "react-intl"; import Icon from "Icons/Icon"; import { useUserProfile } from "@snort/system-react"; -import { System } from "index"; export interface NoteReactionProps { data: TaggedNostrEvent; @@ -19,7 +18,7 @@ export interface NoteReactionProps { export default function NoteReaction(props: NoteReactionProps) { const { data: ev } = props; const { isMuted } = useModeration(); - const profile = useUserProfile(System, ev.pubkey); + const profile = useUserProfile(ev.pubkey); const refEvent = useMemo(() => { if (ev) { diff --git a/packages/app/src/Element/Poll.tsx b/packages/app/src/Element/Poll.tsx index cd9464dc..128077a0 100644 --- a/packages/app/src/Element/Poll.tsx +++ b/packages/app/src/Element/Poll.tsx @@ -12,7 +12,6 @@ import { formatShort } from "Number"; import Spinner from "Icons/Spinner"; import SendSats from "Element/SendSats"; import useLogin from "Hooks/useLogin"; -import { System } from "index"; interface PollProps { ev: TaggedNostrEvent; @@ -24,7 +23,7 @@ export default function Poll(props: PollProps) { const publisher = useEventPublisher(); const { wallet } = useWallet(); const { preferences: prefs, publicKey: myPubKey, relays } = useLogin(); - const pollerProfile = useUserProfile(System, props.ev.pubkey); + const pollerProfile = useUserProfile(props.ev.pubkey); const [error, setError] = useState(""); const [invoice, setInvoice] = useState(""); const [voting, setVoting] = useState(); diff --git a/packages/app/src/Element/ProfileImage.tsx b/packages/app/src/Element/ProfileImage.tsx index f4456045..6342e8cb 100644 --- a/packages/app/src/Element/ProfileImage.tsx +++ b/packages/app/src/Element/ProfileImage.tsx @@ -8,7 +8,6 @@ import { useUserProfile } from "@snort/system-react"; import { hexToBech32, profileLink } from "SnortUtils"; import Avatar from "Element/Avatar"; import Nip05 from "Element/Nip05"; -import { System } from "index"; export interface ProfileImageProps { pubkey: HexKey; @@ -21,6 +20,7 @@ export interface ProfileImageProps { overrideUsername?: string; profile?: UserMetadata; size?: number; + onClick?: (e: React.MouseEvent) => void; } export default function ProfileImage({ @@ -34,8 +34,9 @@ export default function ProfileImage({ overrideUsername, profile, size, + onClick, }: ProfileImageProps) { - const user = profile ?? useUserProfile(System, pubkey); + const user = useUserProfile(profile ? "" : pubkey) ?? profile; const nip05 = defaultNip ? defaultNip : user?.nip05; const name = useMemo(() => { @@ -45,6 +46,7 @@ export default function ProfileImage({ function handleClick(e: React.MouseEvent) { if (link === "") { e.preventDefault(); + onClick?.(e); } } @@ -68,7 +70,11 @@ export default function ProfileImage({ } if (link === "") { - return
{inner()}
; + return ( +
+ {inner()} +
+ ); } else { return ( ) => void; } export default function ProfilePreview(props: ProfilePreviewProps) { const pubkey = props.pubkey; const { ref, inView } = useInView({ triggerOnce: true }); - const user = useUserProfile(System, inView ? pubkey : undefined); + const user = useUserProfile(inView ? pubkey : undefined); const options = { about: true, ...props.options, }; + function handleClick(e: React.MouseEvent) { + if (props.onClick) { + e.stopPropagation(); + e.preventDefault(); + props.onClick(e); + } + } + return ( <> -
+
{inView && ( <> {user?.about}
: undefined} /> {props.actions ?? ( diff --git a/packages/app/src/Element/Username.tsx b/packages/app/src/Element/Username.tsx index 1e22d02e..262c14f1 100644 --- a/packages/app/src/Element/Username.tsx +++ b/packages/app/src/Element/Username.tsx @@ -5,10 +5,9 @@ import { HexKey } from "@snort/system"; import { useUserProfile } from "@snort/system-react"; import { profileLink } from "SnortUtils"; -import { System } from "index"; export default function Username({ pubkey, onLinkVisit }: { pubkey: HexKey; onLinkVisit(): void }) { - const user = useUserProfile(System, pubkey); + const user = useUserProfile(pubkey); const navigate = useNavigate(); function onClick(ev: MouseEvent) { diff --git a/packages/app/src/Element/ZapButton.tsx b/packages/app/src/Element/ZapButton.tsx index 279b6e47..d5ea6111 100644 --- a/packages/app/src/Element/ZapButton.tsx +++ b/packages/app/src/Element/ZapButton.tsx @@ -5,7 +5,6 @@ import { useUserProfile } from "@snort/system-react"; import SendSats from "Element/SendSats"; import Icon from "Icons/Icon"; -import { System } from "index"; const ZapButton = ({ pubkey, @@ -18,7 +17,7 @@ const ZapButton = ({ children?: React.ReactNode; event?: string; }) => { - const profile = useUserProfile(System, pubkey); + const profile = useUserProfile(pubkey); const [zap, setZap] = useState(false); const service = lnurl ?? (profile?.lud16 || profile?.lud06); if (!service) return null; diff --git a/packages/app/src/Feed/BadgesFeed.ts b/packages/app/src/Feed/BadgesFeed.ts index 5cbcd393..d6905c5d 100644 --- a/packages/app/src/Feed/BadgesFeed.ts +++ b/packages/app/src/Feed/BadgesFeed.ts @@ -1,9 +1,8 @@ import { useMemo } from "react"; -import { EventKind, HexKey, Lists, RequestBuilder, FlatNoteStore, ReplaceableNoteStore } from "@snort/system"; +import { EventKind, HexKey, Lists, RequestBuilder, ReplaceableNoteStore, NoteCollection } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; import { unwrap, findTag, chunks } from "SnortUtils"; -import { System } from "index"; type BadgeAwards = { pubkeys: string[]; @@ -18,7 +17,7 @@ export default function useProfileBadges(pubkey?: HexKey) { return b; }, [pubkey]); - const profileBadges = useRequestBuilder(System, ReplaceableNoteStore, sub); + const profileBadges = useRequestBuilder(ReplaceableNoteStore, sub); const profile = useMemo(() => { if (profileBadges.data) { @@ -58,7 +57,7 @@ export default function useProfileBadges(pubkey?: HexKey) { return b; }, [profile, ds]); - const awards = useRequestBuilder(System, FlatNoteStore, awardsSub); + const awards = useRequestBuilder(NoteCollection, awardsSub); const result = useMemo(() => { if (awards.data) { diff --git a/packages/app/src/Feed/EventFeed.ts b/packages/app/src/Feed/EventFeed.ts index e06ada00..23532025 100644 --- a/packages/app/src/Feed/EventFeed.ts +++ b/packages/app/src/Feed/EventFeed.ts @@ -3,7 +3,6 @@ import { NostrPrefix, RequestBuilder, ReplaceableNoteStore, NostrLink } from "@s import { useRequestBuilder } from "@snort/system-react"; import { unwrap } from "SnortUtils"; -import { System } from "index"; export default function useEventFeed(link: NostrLink) { const sub = useMemo(() => { @@ -28,5 +27,5 @@ export default function useEventFeed(link: NostrLink) { return b; }, [link]); - return useRequestBuilder(System, ReplaceableNoteStore, sub); + return useRequestBuilder(ReplaceableNoteStore, sub); } diff --git a/packages/app/src/Feed/FollowersFeed.ts b/packages/app/src/Feed/FollowersFeed.ts index b7552857..84544a29 100644 --- a/packages/app/src/Feed/FollowersFeed.ts +++ b/packages/app/src/Feed/FollowersFeed.ts @@ -1,7 +1,6 @@ import { useMemo } from "react"; import { HexKey, EventKind, NoteCollection, RequestBuilder } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; -import { System } from "index"; export default function useFollowersFeed(pubkey?: HexKey) { const sub = useMemo(() => { @@ -11,7 +10,7 @@ export default function useFollowersFeed(pubkey?: HexKey) { return b; }, [pubkey]); - const followersFeed = useRequestBuilder(System, NoteCollection, sub); + const followersFeed = useRequestBuilder(NoteCollection, sub); const followers = useMemo(() => { const contactLists = followersFeed.data?.filter( diff --git a/packages/app/src/Feed/FollowsFeed.ts b/packages/app/src/Feed/FollowsFeed.ts index af5a2fca..d8f838aa 100644 --- a/packages/app/src/Feed/FollowsFeed.ts +++ b/packages/app/src/Feed/FollowsFeed.ts @@ -3,7 +3,6 @@ import { HexKey, TaggedNostrEvent, EventKind, NoteCollection, RequestBuilder } f import { useRequestBuilder } from "@snort/system-react"; import useLogin from "Hooks/useLogin"; -import { System } from "index"; export default function useFollowsFeed(pubkey?: HexKey) { const { publicKey, follows } = useLogin(); @@ -16,7 +15,7 @@ export default function useFollowsFeed(pubkey?: HexKey) { return b; }, [isMe, pubkey]); - const contactFeed = useRequestBuilder(System, NoteCollection, sub); + const contactFeed = useRequestBuilder(NoteCollection, sub); return useMemo(() => { if (isMe) { return follows.item; diff --git a/packages/app/src/Feed/LiveChatFeed.tsx b/packages/app/src/Feed/LiveChatFeed.tsx index b7f154ec..e4d16e82 100644 --- a/packages/app/src/Feed/LiveChatFeed.tsx +++ b/packages/app/src/Feed/LiveChatFeed.tsx @@ -1,6 +1,5 @@ import { EventKind, FlatNoteStore, NostrLink, RequestBuilder } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; -import { System } from "index"; import { useMemo } from "react"; export function useLiveChatFeed(link: NostrLink) { @@ -16,5 +15,5 @@ export function useLiveChatFeed(link: NostrLink) { return rb; }, [link]); - return useRequestBuilder(System, FlatNoteStore, sub); + return useRequestBuilder(FlatNoteStore, sub); } diff --git a/packages/app/src/Feed/LoginFeed.ts b/packages/app/src/Feed/LoginFeed.ts index 9dc386a7..0fa698d1 100644 --- a/packages/app/src/Feed/LoginFeed.ts +++ b/packages/app/src/Feed/LoginFeed.ts @@ -60,7 +60,7 @@ export default function useLoginFeed() { return b; }, [pubKey]); - const loginFeed = useRequestBuilder(System, NoteCollection, subLogin); + const loginFeed = useRequestBuilder(NoteCollection, subLogin); // update relays and follow lists useEffect(() => { @@ -156,7 +156,7 @@ export default function useLoginFeed() { } } - const listsFeed = useRequestBuilder(System, FlatNoteStore, subLists); + const listsFeed = useRequestBuilder(FlatNoteStore, subLists); useEffect(() => { if (listsFeed.data) { diff --git a/packages/app/src/Feed/MuteList.ts b/packages/app/src/Feed/MuteList.ts index 719b79d0..6abf9bcd 100644 --- a/packages/app/src/Feed/MuteList.ts +++ b/packages/app/src/Feed/MuteList.ts @@ -4,7 +4,6 @@ import { useRequestBuilder } from "@snort/system-react"; import { getNewest } from "SnortUtils"; import useLogin from "Hooks/useLogin"; -import { System } from "index"; export default function useMutedFeed(pubkey?: HexKey) { const { publicKey, muted } = useLogin(); @@ -17,7 +16,7 @@ export default function useMutedFeed(pubkey?: HexKey) { return b; }, [pubkey]); - const mutedFeed = useRequestBuilder(System, NoteCollection, sub); + const mutedFeed = useRequestBuilder(NoteCollection, sub); const mutedList = useMemo(() => { if (pubkey && mutedFeed.data) { diff --git a/packages/app/src/Feed/RelaysFeed.tsx b/packages/app/src/Feed/RelaysFeed.tsx index d710e62a..4764862e 100644 --- a/packages/app/src/Feed/RelaysFeed.tsx +++ b/packages/app/src/Feed/RelaysFeed.tsx @@ -1,7 +1,6 @@ import { useMemo } from "react"; import { HexKey, FullRelaySettings, EventKind, RequestBuilder, ReplaceableNoteStore } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; -import { System } from "index"; export default function useRelaysFeed(pubkey?: HexKey) { const sub = useMemo(() => { @@ -11,7 +10,7 @@ export default function useRelaysFeed(pubkey?: HexKey) { return b; }, [pubkey]); - const relays = useRequestBuilder(System, ReplaceableNoteStore, sub); + const relays = useRequestBuilder(ReplaceableNoteStore, sub); if (!relays.data?.content) { return [] as FullRelaySettings[]; diff --git a/packages/app/src/Feed/RelaysFeedFollows.tsx b/packages/app/src/Feed/RelaysFeedFollows.tsx index fcb776a0..363b0c30 100644 --- a/packages/app/src/Feed/RelaysFeedFollows.tsx +++ b/packages/app/src/Feed/RelaysFeedFollows.tsx @@ -13,7 +13,6 @@ import debug from "debug"; import { sanitizeRelayUrl } from "SnortUtils"; import { UserRelays } from "Cache"; -import { System } from "index"; interface RelayList { pubkey: string; @@ -80,7 +79,7 @@ export default function useRelaysFeedFollows(pubkeys: HexKey[]): Array(System, NoteCollection, sub); + const relays = useRequestBuilder(NoteCollection, sub); const notesRelays = relays.data?.filter(a => a.kind === EventKind.Relays) ?? []; const notesContactLists = relays.data?.filter(a => a.kind === EventKind.ContactList) ?? []; return useMemo(() => { diff --git a/packages/app/src/Feed/ThreadFeed.ts b/packages/app/src/Feed/ThreadFeed.ts index f06ffc97..6603bd2d 100644 --- a/packages/app/src/Feed/ThreadFeed.ts +++ b/packages/app/src/Feed/ThreadFeed.ts @@ -4,7 +4,6 @@ import { useRequestBuilder } from "@snort/system-react"; import { appendDedupe } from "SnortUtils"; import useLogin from "Hooks/useLogin"; -import { System } from "index"; interface RelayTaggedEventId { id: u256; @@ -58,7 +57,7 @@ export default function useThreadFeed(link: NostrLink) { return sub; }, [trackingEvents, trackingATags, allEvents, pref]); - const store = useRequestBuilder(System, FlatNoteStore, sub); + const store = useRequestBuilder(FlatNoteStore, sub); useEffect(() => { if (link.type === NostrPrefix.Address) { diff --git a/packages/app/src/Feed/TimelineFeed.ts b/packages/app/src/Feed/TimelineFeed.ts index 8a35b778..ae31cf34 100644 --- a/packages/app/src/Feed/TimelineFeed.ts +++ b/packages/app/src/Feed/TimelineFeed.ts @@ -5,7 +5,6 @@ import { useRequestBuilder } from "@snort/system-react"; import { unixNow, unwrap, tagFilterOfTextRepost } from "SnortUtils"; import useTimelineWindow from "Hooks/useTimelineWindow"; import useLogin from "Hooks/useLogin"; -import { System } from "index"; import { SearchRelays } from "Const"; export interface TimelineFeedOptions { @@ -117,7 +116,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel return rb?.builder ?? null; }, [until, since, options.method, pref, createBuilder]); - const main = useRequestBuilder(System, NoteCollection, sub); + const main = useRequestBuilder(NoteCollection, sub); const subRealtime = useMemo(() => { const rb = createBuilder(); @@ -131,7 +130,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel return rb?.builder ?? null; }, [pref.autoShowLatest, createBuilder]); - const latest = useRequestBuilder(System, NoteCollection, subRealtime); + const latest = useRequestBuilder(NoteCollection, subRealtime); useEffect(() => { // clear store if changing relays @@ -177,7 +176,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel return rb.numFilters > 0 ? rb : null; }, [main.data, pref, subject.type]); - const related = useRequestBuilder(System, NoteCollection, subNext); + const related = useRequestBuilder(NoteCollection, subNext); return { main: main.data, diff --git a/packages/app/src/Feed/ZapsFeed.ts b/packages/app/src/Feed/ZapsFeed.ts index 40df5177..e82bc188 100644 --- a/packages/app/src/Feed/ZapsFeed.ts +++ b/packages/app/src/Feed/ZapsFeed.ts @@ -1,8 +1,6 @@ import { useMemo } from "react"; import { EventKind, RequestBuilder, parseZap, NostrLink, NostrPrefix, NoteCollection } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; - -import { System } from "index"; import { UserCache } from "Cache"; export default function useZapsFeed(link?: NostrLink) { @@ -17,7 +15,7 @@ export default function useZapsFeed(link?: NostrLink) { return b; }, [link]); - const zapsFeed = useRequestBuilder(System, NoteCollection, sub); + const zapsFeed = useRequestBuilder(NoteCollection, sub); const zaps = useMemo(() => { if (zapsFeed.data) { diff --git a/packages/app/src/Hooks/useNotelistSubscription.ts b/packages/app/src/Hooks/useNotelistSubscription.ts index 40537246..83002d1f 100644 --- a/packages/app/src/Hooks/useNotelistSubscription.ts +++ b/packages/app/src/Hooks/useNotelistSubscription.ts @@ -3,7 +3,6 @@ import { HexKey, Lists, EventKind, FlatNoteStore, NoteCollection, RequestBuilder import { useRequestBuilder } from "@snort/system-react"; import useLogin from "Hooks/useLogin"; -import { System } from "index"; export default function useNotelistSubscription(pubkey: HexKey | undefined, l: Lists, defaultIds: HexKey[]) { const { preferences, publicKey } = useLogin(); @@ -17,7 +16,7 @@ export default function useNotelistSubscription(pubkey: HexKey | undefined, l: L return rb; }, [pubkey]); - const listStore = useRequestBuilder(System, NoteCollection, sub); + const listStore = useRequestBuilder(NoteCollection, sub); const etags = useMemo(() => { if (isMe) return defaultIds; // there should only be a single event here because we only load 1 pubkey @@ -39,7 +38,7 @@ export default function useNotelistSubscription(pubkey: HexKey | undefined, l: L return s; }, [etags, pubkey, preferences]); - const store = useRequestBuilder(System, FlatNoteStore, esub); + const store = useRequestBuilder(FlatNoteStore, esub); return store.data ?? []; } diff --git a/packages/app/src/Pages/Layout.css b/packages/app/src/Pages/Layout.css index f3ccf873..a4243922 100644 --- a/packages/app/src/Pages/Layout.css +++ b/packages/app/src/Pages/Layout.css @@ -16,7 +16,7 @@ header { display: flex; - padding: 10px 16px; + padding: var(--header-padding-tb) 16px; justify-content: space-between; align-items: center; align-self: stretch; diff --git a/packages/app/src/Pages/Layout.tsx b/packages/app/src/Pages/Layout.tsx index 11a7418f..62d130dd 100644 --- a/packages/app/src/Pages/Layout.tsx +++ b/packages/app/src/Pages/Layout.tsx @@ -142,7 +142,7 @@ const AccountHeader = () => { const { formatMessage } = useIntl(); const { publicKey, latestNotification, readNotifications } = useLogin(); - const profile = useUserProfile(System, publicKey); + const profile = useUserProfile(publicKey); const hasNotifications = useMemo( () => latestNotification > readNotifications, diff --git a/packages/app/src/Pages/MessagesPage.css b/packages/app/src/Pages/MessagesPage.css index 51f21e7a..b7ddfd9f 100644 --- a/packages/app/src/Pages/MessagesPage.css +++ b/packages/app/src/Pages/MessagesPage.css @@ -1,7 +1,7 @@ .dm-page { display: grid; grid-template-columns: 350px auto; - height: calc(100vh - 57px); + height: calc(100vh - 42px - var(--header-padding-tb) - var(--header-padding-tb) - 16px); /* 100vh - header - padding */ overflow: hidden; } @@ -26,13 +26,14 @@ /* User list */ .dm-page > div:nth-child(1) { overflow-y: auto; - margin: 0 10px; - padding: 0 10px 0 0; + padding: 0 5px; } /* Chat window */ .dm-page > div:nth-child(2) { - height: calc(100vh - 57px); + padding: 0 12px; + background-color: var(--gray-superdark); + border-radius: 16px; } /* Profile pannel */ @@ -48,3 +49,53 @@ .dm-page > div:nth-child(3) .card { cursor: pointer; } + +.dm-page .new-chat { + min-width: 100px; +} + +.dm-page .chat-list > div.active { + background-color: var(--gray-superdark); + border-radius: 16px; +} + +.new-chat-modal .user-list { + max-height: 50vh; + overflow-y: auto; +} + +.new-chat-modal .user-list > div { + padding: 8px 12px; + cursor: pointer; +} + +/* user in list selected */ +.new-chat-modal .user-list > div.active { + background-color: var(--gray-dark); + border-radius: 16px; +} + +.new-chat-modal .modal-body { + padding: 24px 32px; +} + +.new-chat-modal h2, +.new-chat-modal h3, +.new-chat-modal p { + font-weight: 600; + margin: 0; +} + +.new-chat-modal h2 { + font-size: 21px; +} + +.new-chat-modal h3 { + font-size: 16px; +} + +.new-chat-modal p { + font-size: 11px; + letter-spacing: 1.21px; + text-transform: uppercase; +} diff --git a/packages/app/src/Pages/MessagesPage.tsx b/packages/app/src/Pages/MessagesPage.tsx index 2010379d..3c398431 100644 --- a/packages/app/src/Pages/MessagesPage.tsx +++ b/packages/app/src/Pages/MessagesPage.tsx @@ -1,12 +1,14 @@ +import "./MessagesPage.css"; + import React, { useEffect, useMemo, useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { useNavigate, useParams } from "react-router-dom"; import { TLVEntryType, decodeTLV } from "@snort/system"; -import { useUserProfile } from "@snort/system-react"; +import { useUserProfile, useUserSearch } from "@snort/system-react"; import UnreadCount from "Element/UnreadCount"; import ProfileImage, { getDisplayName } from "Element/ProfileImage"; -import { parseId } from "SnortUtils"; +import { appendDedupe, debounce, parseId } from "SnortUtils"; import NoteToSelf from "Element/NoteToSelf"; import useModeration from "Hooks/useModeration"; import useLogin from "Hooks/useLogin"; @@ -16,11 +18,9 @@ import DmWindow from "Element/DmWindow"; import Avatar from "Element/Avatar"; import Icon from "Icons/Icon"; import Text from "Element/Text"; -import { System } from "index"; -import { Chat, ChatType, useChatSystem } from "chat"; - -import "./MessagesPage.css"; -import messages from "./messages"; +import { Chat, ChatType, createChatLink, useChatSystem } from "chat"; +import Modal from "Element/Modal"; +import ProfilePreview from "Element/ProfilePreview"; const TwoCol = 768; const ThreeCol = 1500; @@ -49,15 +49,15 @@ export default function MessagesPage() { function noteToSelf(chat: Chat) { return ( -
openChat(e, chat.type, chat.id)}> +
openChat(e, chat.type, chat.id)}>
); } - function conversationIdent(chat: Chat) { - if (chat.participants.length === 1) { - const p = chat.participants[0]; + function conversationIdent(cx: Chat) { + if (cx.participants.length === 1) { + const p = cx.participants[0]; if (p.type === "pubkey") { return ; @@ -67,27 +67,29 @@ export default function MessagesPage() { } else { return (
- {chat.participants.map(v => ( + {cx.participants.map(v => ( ))} -
{chat.title}
+ {cx.title ?? }
); } } - function conversation(chat: Chat) { + function conversation(cx: Chat) { if (!login.publicKey) return null; - const participants = chat.participants.map(a => a.id); - if (participants.length === 1 && participants[0] === login.publicKey) return noteToSelf(chat); + const participants = cx.participants.map(a => a.id); + if (participants.length === 1 && participants[0] === login.publicKey) return noteToSelf(cx); + + const isActive = cx.id === chat; return ( -
openChat(e, chat.type, chat.id)}> - {conversationIdent(chat)} +
openChat(e, cx.type, cx.id)}> + {conversationIdent(cx)}
- + - {chat.unread > 0 && } + {cx.unread > 0 && }
); @@ -96,14 +98,12 @@ export default function MessagesPage() { return (
{(pageWidth >= TwoCol || !chat) && ( -
-
-

- -

+
+
+
{chats .sort((a, b) => { @@ -117,7 +117,7 @@ export default function MessagesPage() { .map(conversation)}
)} - {chat && } + {chat ? : pageWidth >= TwoCol &&
} {pageWidth >= ThreeCol && chat && (
@@ -132,7 +132,7 @@ function ProfileDmActions({ id }: { id: string }) { .filter(a => a.type === TLVEntryType.Author) .map(a => a.value as string); const pubkey = authors[0]; - const profile = useUserProfile(System, pubkey); + const profile = useUserProfile(pubkey); const { block, unblock, isBlocked } = useModeration(); function truncAbout(s?: string) { @@ -158,3 +158,105 @@ function ProfileDmActions({ id }: { id: string }) { ); } + +function NewChatWindow() { + const [show, setShow] = useState(false); + const [newChat, setNewChat] = useState([]); + const [results, setResults] = useState([]); + const [term, setSearchTerm] = useState(""); + const navigate = useNavigate(); + const search = useUserSearch(); + const { follows } = useLogin(); + + useEffect(() => { + setNewChat([]); + setSearchTerm(""); + setResults(follows.item); + }, [show]); + + useEffect(() => { + return debounce(500, () => { + if (term) { + search(term).then(setResults); + } else { + setResults(follows.item); + } + }); + }, [term]); + + function togglePubkey(a: string) { + setNewChat(c => (c.includes(a) ? c.filter(v => v !== a) : appendDedupe(c, [a]))); + } + + function startChat() { + setShow(false); + if (newChat.length === 1) { + navigate(createChatLink(ChatType.DirectMessage, newChat[0])); + } else { + navigate(createChatLink(ChatType.PrivateGroupChat, ...newChat)); + } + } + + return ( + <> + + {show && ( + setShow(false)} className="new-chat-modal"> +
+
+

+ +

+ +
+
+

+ +

+ setSearchTerm(e.target.value)} + /> +
+
+ {newChat.map(a => ( + togglePubkey(a)} + /> + ))} +
+
+

+ +

+
+ {results.map(a => { + return ( + } + onClick={() => togglePubkey(a)} + className={newChat.includes(a) ? "active" : undefined} + /> + ); + })} +
+
+
+
+ )} + + ); +} diff --git a/packages/app/src/Pages/Notifications.tsx b/packages/app/src/Pages/Notifications.tsx index 13633a57..f510b7b5 100644 --- a/packages/app/src/Pages/Notifications.tsx +++ b/packages/app/src/Pages/Notifications.tsx @@ -6,7 +6,7 @@ import { NostrEvent, NostrLink, NostrPrefix, - TaggedRawEvent, + TaggedNostrEvent, createNostrLink, parseZap, } from "@snort/system"; @@ -22,13 +22,12 @@ import { dedupe, findTag, orderDescending } from "SnortUtils"; import Icon from "Icons/Icon"; import ProfileImage, { getDisplayName } from "Element/ProfileImage"; import useModeration from "Hooks/useModeration"; -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) { +function notificationContext(ev: TaggedNostrEvent) { switch (ev.kind) { case EventKind.ZapReceipt: { const aTag = findTag(ev, "a"); @@ -88,14 +87,14 @@ export default function NotificationsPage() { return orderDescending([...notifications]) .filter(a => !isMuted(a.pubkey) && findTag(a, "p") === login.publicKey) .reduce((acc, v) => { - const key = `${timeKey(v)}:${notificationContext(v as TaggedRawEvent)?.encode()}:${v.kind}`; + const key = `${timeKey(v)}:${notificationContext(v as TaggedNostrEvent)?.encode()}:${v.kind}`; if (acc.has(key)) { - unwrap(acc.get(key)).push(v as TaggedRawEvent); + unwrap(acc.get(key)).push(v as TaggedNostrEvent); } else { - acc.set(key, [v as TaggedRawEvent]); + acc.set(key, [v as TaggedNostrEvent]); } return acc; - }, new Map>()); + }, new Map>()); }, [notifications]); return ( @@ -105,7 +104,7 @@ export default function NotificationsPage() { ); } -function NotificationGroup({ evs }: { evs: Array }) { +function NotificationGroup({ evs }: { evs: Array }) { const { ref, inView } = useInView({ triggerOnce: true }); const { formatMessage } = useIntl(); const kind = evs[0].kind; @@ -123,7 +122,7 @@ function NotificationGroup({ evs }: { evs: Array }) { }) ); const firstPubkey = pubkeys[0]; - const firstPubkeyProfile = useUserProfile(System, inView ? (firstPubkey === "anon" ? "" : firstPubkey) : ""); + const firstPubkeyProfile = useUserProfile(inView ? (firstPubkey === "anon" ? "" : firstPubkey) : ""); const context = notificationContext(evs[0]); const totalZaps = zaps.reduce((acc, v) => acc + v.amount, 0); @@ -187,13 +186,13 @@ function NotificationGroup({ evs }: { evs: Array }) {
{inView && ( <> -
+
{kind === EventKind.ZapReceipt && formatShort(totalZaps)}
-
+
{pubkeys .filter(a => a !== "anon") diff --git a/packages/app/src/Pages/ProfilePage.tsx b/packages/app/src/Pages/ProfilePage.tsx index 718bfe1e..3f7bc31f 100644 --- a/packages/app/src/Pages/ProfilePage.tsx +++ b/packages/app/src/Pages/ProfilePage.tsx @@ -56,7 +56,6 @@ import { getNip05PubKey } from "Pages/LoginPage"; import useLogin from "Hooks/useLogin"; import messages from "./messages"; -import { System } from "index"; const NOTES = 0; const REACTIONS = 1; @@ -113,7 +112,7 @@ export default function ProfilePage() { const params = useParams(); const navigate = useNavigate(); const [id, setId] = useState(); - const user = useUserProfile(System, id); + const user = useUserProfile(id); const loginPubKey = useLogin().publicKey; const isMe = loginPubKey === id; const [showLnQr, setShowLnQr] = useState(false); diff --git a/packages/app/src/Pages/ZapPool.tsx b/packages/app/src/Pages/ZapPool.tsx index 53ae5fd1..f6703ee3 100644 --- a/packages/app/src/Pages/ZapPool.tsx +++ b/packages/app/src/Pages/ZapPool.tsx @@ -35,7 +35,7 @@ const DataProviders = [ function ZapTarget({ target }: { target: ZapPoolRecipient }) { const login = useLogin(); - const profile = useUserProfile(System, target.pubkey); + const profile = useUserProfile(target.pubkey); const hasAddress = profile?.lud16 || profile?.lud06; const defaultZapMount = Math.ceil(login.preferences.defaultZapAmount * (target.split / 100)); return ( diff --git a/packages/app/src/Pages/new/GetVerified.tsx b/packages/app/src/Pages/new/GetVerified.tsx index 5f317bfc..273de69e 100644 --- a/packages/app/src/Pages/new/GetVerified.tsx +++ b/packages/app/src/Pages/new/GetVerified.tsx @@ -10,12 +10,11 @@ import ProfileImage from "Element/ProfileImage"; import useLogin from "Hooks/useLogin"; import messages from "./messages"; -import { System } from "index"; export default function GetVerified() { const navigate = useNavigate(); const { publicKey } = useLogin(); - const user = useUserProfile(System, publicKey); + const user = useUserProfile(publicKey); const [isVerified, setIsVerified] = useState(false); const name = user?.name || "nostrich"; const [nip05, setNip05] = useState(`${name}@snort.social`); diff --git a/packages/app/src/Pages/new/ProfileSetup.tsx b/packages/app/src/Pages/new/ProfileSetup.tsx index 5ff09f03..3d23df0e 100644 --- a/packages/app/src/Pages/new/ProfileSetup.tsx +++ b/packages/app/src/Pages/new/ProfileSetup.tsx @@ -16,7 +16,7 @@ import messages from "./messages"; export default function ProfileSetup() { const login = useLogin(); - const myProfile = useUserProfile(System, login.publicKey); + const myProfile = useUserProfile(login.publicKey); const [username, setUsername] = useState(""); const [picture, setPicture] = useState(""); const { formatMessage } = useIntl(); diff --git a/packages/app/src/Pages/settings/Profile.tsx b/packages/app/src/Pages/settings/Profile.tsx index 227ce059..13759d39 100644 --- a/packages/app/src/Pages/settings/Profile.tsx +++ b/packages/app/src/Pages/settings/Profile.tsx @@ -24,7 +24,7 @@ export interface ProfileSettingsProps { export default function ProfileSettings(props: ProfileSettingsProps) { const navigate = useNavigate(); const { publicKey: id } = useLogin(); - const user = useUserProfile(System, id ?? ""); + const user = useUserProfile(id ?? ""); const publisher = useEventPublisher(); const uploader = useFileUpload(); diff --git a/packages/app/src/Tasks/TaskList.tsx b/packages/app/src/Tasks/TaskList.tsx index b4993679..11ecf4ef 100644 --- a/packages/app/src/Tasks/TaskList.tsx +++ b/packages/app/src/Tasks/TaskList.tsx @@ -6,14 +6,13 @@ import Icon from "Icons/Icon"; import { UITask } from "Tasks"; import { DonateTask } from "./DonateTask"; import { Nip5Task } from "./Nip5Task"; -import { System } from "index"; const AllTasks: Array = [new Nip5Task(), new DonateTask()]; AllTasks.forEach(a => a.load()); export const TaskList = () => { const publicKey = useLogin().publicKey; - const user = useUserProfile(System, publicKey); + const user = useUserProfile(publicKey); const [, setTick] = useState(0); function muteTask(t: UITask) { diff --git a/packages/app/src/chat/index.ts b/packages/app/src/chat/index.ts index fd1747af..327b88b1 100644 --- a/packages/app/src/chat/index.ts +++ b/packages/app/src/chat/index.ts @@ -4,10 +4,14 @@ import { EventKind, EventPublisher, NostrEvent, + NostrPrefix, RequestBuilder, SystemInterface, + TLVEntry, + TLVEntryType, TaggedRawEvent, UserMetadata, + encodeTLVEntries, } from "@snort/system"; import { unwrap } from "@snort/shared"; import { Chats, GiftsCache } from "Cache"; @@ -102,6 +106,57 @@ export function setLastReadIn(id: string) { window.localStorage.setItem(k, now.toString()); } +export function createChatLink(type: ChatType, ...params: Array) { + switch (type) { + case ChatType.DirectMessage: { + if (params.length > 1) throw new Error("Must only contain one pubkey"); + return `/messages/${encodeTLVEntries( + "chat4" as NostrPrefix, + { + type: TLVEntryType.Author, + length: params[0].length, + value: params[0], + } as TLVEntry + )}`; + } + case ChatType.PrivateDirectMessage: { + if (params.length > 1) throw new Error("Must only contain one pubkey"); + return `/messages/${encodeTLVEntries( + "chat24" as NostrPrefix, + { + type: TLVEntryType.Author, + length: params[0].length, + value: params[0], + } as TLVEntry + )}`; + } + case ChatType.PrivateGroupChat: { + return `/messages/${encodeTLVEntries( + "chat24" as NostrPrefix, + ...params.map( + a => + ({ + type: TLVEntryType.Author, + length: a.length, + value: a, + } as TLVEntry) + ) + )}`; + } + } + throw new Error("Unknown chat type"); +} + +export function createEmptyChatObject(id: string) { + if (id.startsWith("chat4")) { + return Nip4ChatSystem.createChatObj(id, []); + } + if (id.startsWith("chat24")) { + return Nip24ChatSystem.createChatObj(id, []); + } + throw new Error("Cant create new empty chat, unknown id"); +} + export function useNip4Chat() { const { publicKey } = useLogin(); return useSyncExternalStore( diff --git a/packages/app/src/chat/nip24.ts b/packages/app/src/chat/nip24.ts index c90f63cf..9344e6fb 100644 --- a/packages/app/src/chat/nip24.ts +++ b/packages/app/src/chat/nip24.ts @@ -68,7 +68,10 @@ export class Nip24ChatSystem extends ExternalStore> implements ChatS }, { t: 0, - title: "", + title: undefined, + } as { + t: number; + title: string | undefined; } ); return { diff --git a/packages/app/src/index.css b/packages/app/src/index.css index 0636d11f..318a783d 100644 --- a/packages/app/src/index.css +++ b/packages/app/src/index.css @@ -42,6 +42,8 @@ ); --expired-invoice-gradient: linear-gradient(45deg, var(--gray-superdark) 50%, var(--gray), var(--gray-superdark)); --strike-army-gradient: linear-gradient(to bottom right, #ccff00, #a1c900); + + --header-padding-tb: 10px; } ::-webkit-scrollbar { @@ -115,10 +117,6 @@ code { margin-right: auto; } -body #root > div:not(.page) header { - padding: 2px 10px; -} - @media (min-width: 768px) { .page { width: 640px; @@ -411,6 +409,10 @@ input:disabled { gap: 12px; } +.g16 { + gap: 16px; +} + .g24 { gap: 24px; } @@ -681,8 +683,8 @@ button.tall { } .rta__textarea { - /* Fix width calculation to account for 12px padding on input */ - width: calc(100% - 24px) !important; + /* Fix width calculation to account for 32px padding on input */ + width: calc(100% - 32px) !important; } .ctx-menu { diff --git a/packages/app/src/index.tsx b/packages/app/src/index.tsx index 9b74fe54..8f493844 100644 --- a/packages/app/src/index.tsx +++ b/packages/app/src/index.tsx @@ -34,6 +34,7 @@ import DebugPage from "Pages/Debug"; import { db } from "Db"; import { preload, RelayMetrics, UserCache, UserRelays } from "Cache"; import { LoginStore } from "Login"; +import { SnortContext } from "@snort/system-react"; /** * Singleton nostr system @@ -164,7 +165,9 @@ root.render( - + + + diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts index 099a0fd4..6ecfc05c 100644 --- a/packages/shared/src/utils.ts +++ b/packages/shared/src/utils.ts @@ -164,7 +164,7 @@ export function bech32ToText(str: string) { } } -export async function fetchNip05Pubkey(name: string, domain: string, timeout = 2_000) { +export async function fetchNip05Pubkey(name: string, domain: string, timeout = 2_000): Promise { interface NostrJson { names: Record; } diff --git a/packages/system-react/example/example.tsx b/packages/system-react/example/example.tsx index 75226582..5b3807a1 100644 --- a/packages/system-react/example/example.tsx +++ b/packages/system-react/example/example.tsx @@ -1,7 +1,7 @@ import { useMemo } from "react"; -import { useRequestBuilder, useUserProfile } from "../src"; +import { SnortContext, useRequestBuilder, useUserProfile } from "../src"; -import { FlatNoteStore, NostrSystem, RequestBuilder, TaggedNostrEvent } from "@snort/system"; +import { NostrSystem, NoteCollection, RequestBuilder, TaggedNostrEvent } from "@snort/system"; const System = new NostrSystem({}); @@ -9,7 +9,7 @@ const System = new NostrSystem({}); ["wss://relay.snort.social", "wss://nos.lol"].forEach(r => System.ConnectToRelay(r, { read: true, write: false })); export function Note({ ev }: { ev: TaggedNostrEvent }) { - const profile = useUserProfile(System, ev.pubkey); + const profile = useUserProfile(ev.pubkey); return (
@@ -27,7 +27,7 @@ export function UserPosts(props: { pubkey: string }) { return rb; }, [props.pubkey]); - const data = useRequestBuilder(System, FlatNoteStore, sub); + const data = useRequestBuilder(NoteCollection, sub); return ( <> {data.data.map(a => ( @@ -38,5 +38,7 @@ export function UserPosts(props: { pubkey: string }) { } export function MyApp() { - return ; + return + + ; } diff --git a/packages/system-react/package.json b/packages/system-react/package.json index a9ad29c3..819dbc50 100644 --- a/packages/system-react/package.json +++ b/packages/system-react/package.json @@ -1,6 +1,6 @@ { "name": "@snort/system-react", - "version": "1.0.11", + "version": "1.0.12", "description": "React hooks for @snort/system", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -15,10 +15,12 @@ "dist" ], "dependencies": { - "@snort/shared": "^1.0.4", - "@snort/system": "^1.0.16", "react": "^18.2.0" }, + "peerDependencies": { + "@snort/shared": "^1.0.4", + "@snort/system": "^1.0.17" + }, "devDependencies": { "@types/react": "^18.2.14", "typescript": "^5.1.6" diff --git a/packages/system-react/src/context.tsx b/packages/system-react/src/context.tsx new file mode 100644 index 00000000..74254ef2 --- /dev/null +++ b/packages/system-react/src/context.tsx @@ -0,0 +1,4 @@ +import { createContext } from "react"; +import { NostrSystem, SystemInterface } from "@snort/system"; + +export const SnortContext = createContext(new NostrSystem({})); \ No newline at end of file diff --git a/packages/system-react/src/index.ts b/packages/system-react/src/index.ts index 354bf0c5..a8f4fbfd 100644 --- a/packages/system-react/src/index.ts +++ b/packages/system-react/src/index.ts @@ -1,3 +1,5 @@ export * from "./useRequestBuilder"; export * from "./useSystemState"; export * from "./useUserProfile"; +export * from "./context"; +export * from "./useUserSearch"; \ No newline at end of file diff --git a/packages/system-react/src/useRequestBuilder.tsx b/packages/system-react/src/useRequestBuilder.tsx index e5c09385..24c48acc 100644 --- a/packages/system-react/src/useRequestBuilder.tsx +++ b/packages/system-react/src/useRequestBuilder.tsx @@ -1,15 +1,16 @@ -import { useSyncExternalStore } from "react"; -import { RequestBuilder, EmptySnapshot, NoteStore, StoreSnapshot, SystemInterface } from "@snort/system"; +import { useContext, useSyncExternalStore } from "react"; +import { RequestBuilder, EmptySnapshot, NoteStore, StoreSnapshot } from "@snort/system"; import { unwrap } from "@snort/shared"; +import { SnortContext } from "./context"; /** * Send a query to the relays and wait for data */ const useRequestBuilder = >( - system: SystemInterface, - type: { new (): TStore }, + type: { new(): TStore }, rb: RequestBuilder | null, ) => { + const system = useContext(SnortContext); const subscribe = (onChanged: () => void) => { if (rb) { const q = system.Query(type, rb); diff --git a/packages/system-react/src/useUserProfile.ts b/packages/system-react/src/useUserProfile.ts index 5e748639..33df9f98 100644 --- a/packages/system-react/src/useUserProfile.ts +++ b/packages/system-react/src/useUserProfile.ts @@ -1,10 +1,12 @@ -import { useSyncExternalStore } from "react"; -import { HexKey, MetadataCache, NostrSystem } from "@snort/system"; +import { useContext, useSyncExternalStore } from "react"; +import { HexKey, MetadataCache } from "@snort/system"; +import { SnortContext } from "./context"; /** * Gets a profile from cache or requests it from the relays */ -export function useUserProfile(system: NostrSystem, pubKey?: HexKey): MetadataCache | undefined { +export function useUserProfile(pubKey?: HexKey): MetadataCache | undefined { + const system = useContext(SnortContext); return useSyncExternalStore( h => { if (pubKey) { diff --git a/packages/system-react/src/useUserSearch.tsx b/packages/system-react/src/useUserSearch.tsx new file mode 100644 index 00000000..1ba4b687 --- /dev/null +++ b/packages/system-react/src/useUserSearch.tsx @@ -0,0 +1,37 @@ +import { useContext } from "react"; +import { NostrPrefix, UserProfileCache, tryParseNostrLink } from "@snort/system"; +import { fetchNip05Pubkey } from "@snort/shared"; +import { SnortContext } from "./context"; + +export function useUserSearch() { + const system = useContext(SnortContext); + const cache = system.ProfileLoader.Cache as UserProfileCache; + + async function search(input: string): Promise> { + // try exact match first + if (input.length === 64 && [...input].every(c => !isNaN(parseInt(c, 16)))) { + return [input]; + } + + if (input.startsWith(NostrPrefix.PublicKey) || input.startsWith(NostrPrefix.Profile)) { + const link = tryParseNostrLink(input); + if (link) { + return [link.id] + } + } + + if (input.includes("@")) { + const [name, domain] = input.split("@"); + const pk = await fetchNip05Pubkey(name, domain); + if (pk) { + return [pk]; + } + } + + // search cache + const cacheResults = await cache.search(input); + return cacheResults.map(v => v.pubkey); + } + + return search; +} \ No newline at end of file diff --git a/packages/system/package.json b/packages/system/package.json index 5a0f130c..b1ecbaee 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -1,6 +1,6 @@ { "name": "@snort/system", - "version": "1.0.16", + "version": "1.0.17", "description": "Snort nostr system package", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -30,10 +30,12 @@ "@noble/curves": "^1.0.0", "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.1", - "@snort/shared": "^1.0.4", "@stablelib/xchacha20": "^1.0.1", "debug": "^4.3.4", "dexie": "^3.2.4", "uuid": "^9.0.0" + }, + "peerDependencies": { + "@snort/shared": "^1.0.4" } } diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts index 30bb16b7..ec5605b1 100644 --- a/packages/system/src/index.ts +++ b/packages/system/src/index.ts @@ -3,6 +3,7 @@ import { RequestBuilder } from "./request-builder"; import { NoteStore } from "./note-collection"; import { Query } from "./query"; import { NostrEvent, ReqFilter } from "./nostr"; +import { ProfileLoaderService } from "./profile-cache"; export * from "./nostr-system"; export { default as EventKind } from "./event-kind"; @@ -46,6 +47,7 @@ export interface SystemInterface { DisconnectRelay(address: string): void; BroadcastEvent(ev: NostrEvent): void; WriteOnceToRelay(relay: string, ev: NostrEvent): Promise; + get ProfileLoader(): ProfileLoaderService } export interface SystemSnapshot { diff --git a/packages/system/src/system-worker.ts b/packages/system/src/system-worker.ts index eff6df76..d42cd08f 100644 --- a/packages/system/src/system-worker.ts +++ b/packages/system/src/system-worker.ts @@ -1,6 +1,6 @@ import { ExternalStore } from "@snort/shared"; -import { SystemSnapshot, SystemInterface } from "."; +import { SystemSnapshot, SystemInterface, ProfileLoaderService } from "."; import { AuthHandler, ConnectionStateSnapshot, RelaySettings } from "./connection"; import { NostrEvent } from "./nostr"; import { NoteStore } from "./note-collection"; @@ -20,6 +20,10 @@ export class SystemWorker extends ExternalStore implements Syste throw new Error("SharedWorker is not supported"); } } + + get ProfileLoader(): ProfileLoaderService { + throw new Error("Method not implemented."); + } HandleAuth?: AuthHandler;