DM styles update
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing

This commit is contained in:
Kieran 2023-08-24 15:25:54 +01:00
parent 0e7aefb036
commit e8e65c71cd
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
54 changed files with 411 additions and 142 deletions

View File

@ -3,7 +3,6 @@ import { FormattedMessage } from "react-intl";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import { useUserProfile } from "@snort/system-react"; import { useUserProfile } from "@snort/system-react";
import { System } from "index";
interface Token { interface Token {
token: Array<{ token: Array<{
@ -17,7 +16,7 @@ interface Token {
export default function CashuNuts({ token }: { token: string }) { export default function CashuNuts({ token }: { token: string }) {
const login = useLogin(); const login = useLogin();
const profile = useUserProfile(System, login.publicKey); const profile = useUserProfile(login.publicKey);
async function copyToken(e: React.MouseEvent<HTMLButtonElement>, token: string) { async function copyToken(e: React.MouseEvent<HTMLButtonElement>, token: string) {
e.stopPropagation(); e.stopPropagation();

View File

@ -3,6 +3,9 @@
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
} }
.dm-window > div:nth-child(1) {
padding: 12px 0;
}
.dm-window > div:nth-child(2) { .dm-window > div:nth-child(2) {
overflow-y: auto; overflow-y: auto;
@ -15,7 +18,6 @@
.dm-window > div:nth-child(3) { .dm-window > div:nth-child(3) {
display: flex; display: flex;
align-items: center; align-items: center;
background-color: var(--bg-color);
gap: 10px; gap: 10px;
padding: 5px 10px; padding: 5px 10px;
} }

View File

@ -6,14 +6,13 @@ import DM from "Element/DM";
import NoteToSelf from "Element/NoteToSelf"; import NoteToSelf from "Element/NoteToSelf";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import WriteMessage from "Element/WriteMessage"; import WriteMessage from "Element/WriteMessage";
import { Chat, ChatParticipant, useChatSystem } from "chat"; import { Chat, ChatParticipant, createEmptyChatObject, useChatSystem } from "chat";
import { Nip4ChatSystem } from "chat/nip4";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
export default function DmWindow({ id }: { id: string }) { export default function DmWindow({ id }: { id: string }) {
const pubKey = useLogin().publicKey; const pubKey = useLogin().publicKey;
const dms = useChatSystem(); 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) { function participant(p: ChatParticipant) {
if (p.id === pubKey) { if (p.id === pubKey) {
@ -37,7 +36,7 @@ export default function DmWindow({ id }: { id: string }) {
{chat.participants.map(v => ( {chat.participants.map(v => (
<ProfileImage pubkey={v.id} showUsername={false} /> <ProfileImage pubkey={v.id} showUsername={false} />
))} ))}
{chat.title ?? <FormattedMessage defaultMessage="Group Chat" />} {chat.title ?? <FormattedMessage defaultMessage="Secret Group Chat" />}
</div> </div>
); );
} }

View File

@ -16,6 +16,7 @@ export interface FollowListBaseProps {
showAbout?: boolean; showAbout?: boolean;
className?: string; className?: string;
actions?: ReactNode; actions?: ReactNode;
profileActions?: (pk: string) => ReactNode;
} }
export default function FollowListBase({ export default function FollowListBase({
@ -25,6 +26,7 @@ export default function FollowListBase({
showAbout, showAbout,
className, className,
actions, actions,
profileActions,
}: FollowListBaseProps) { }: FollowListBaseProps) {
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const { follows, relays } = useLogin(); const { follows, relays } = useLogin();
@ -48,7 +50,7 @@ export default function FollowListBase({
</div> </div>
)} )}
{pubkeys?.map(a => ( {pubkeys?.map(a => (
<ProfilePreview pubkey={a} key={a} options={{ about: showAbout }} /> <ProfilePreview pubkey={a} key={a} options={{ about: showAbout }} actions={profileActions?.(a)} />
))} ))}
</div> </div>
); );

View File

@ -5,10 +5,9 @@ import { HexKey } from "@snort/system";
import { useUserProfile } from "@snort/system-react"; import { useUserProfile } from "@snort/system-react";
import { profileLink } from "SnortUtils"; import { profileLink } from "SnortUtils";
import { getDisplayName } from "Element/ProfileImage"; import { getDisplayName } from "Element/ProfileImage";
import { System } from "index";
export default function Mention({ pubkey, relays }: { pubkey: HexKey; relays?: Array<string> | string }) { export default function Mention({ pubkey, relays }: { pubkey: HexKey; relays?: Array<string> | string }) {
const user = useUserProfile(System, pubkey); const user = useUserProfile(pubkey);
const name = useMemo(() => { const name = useMemo(() => {
return getDisplayName(user, pubkey); return getDisplayName(user, pubkey);

View File

@ -3,10 +3,9 @@ import { HexKey } from "@snort/system";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
import { useUserProfile } from "@snort/system-react"; import { useUserProfile } from "@snort/system-react";
import { System } from "index";
export function useIsVerified(pubkey: HexKey, bypassCheck?: boolean) { export function useIsVerified(pubkey: HexKey, bypassCheck?: boolean) {
const profile = useUserProfile(System, pubkey); const profile = useUserProfile(pubkey);
return { isVerified: bypassCheck || profile?.isNostrAddressValid }; return { isVerified: bypassCheck || profile?.isNostrAddressValid };
} }

View File

@ -44,7 +44,7 @@ export default function Nip5Service(props: Nip05ServiceProps) {
const { helpText = true } = props; const { helpText = true } = props;
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const pubkey = useLogin().publicKey; const pubkey = useLogin().publicKey;
const user = useUserProfile(System, pubkey); const user = useUserProfile(pubkey);
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const svc = useMemo(() => new ServiceProvider(props.service), [props.service]); const svc = useMemo(() => new ServiceProvider(props.service), [props.service]);
const [serviceConfig, setServiceConfig] = useState<ServiceConfig>(); const [serviceConfig, setServiceConfig] = useState<ServiceConfig>();

View File

@ -2,7 +2,7 @@
min-height: 110px; min-height: 110px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 12px;
} }
.note:hover { .note:hover {

View File

@ -50,7 +50,7 @@ export default function NoteFooter(props: NoteFooterProps) {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const login = useLogin(); const login = useLogin();
const { publicKey, preferences: prefs, relays } = login; const { publicKey, preferences: prefs, relays } = login;
const author = useUserProfile(System, ev.pubkey); const author = useUserProfile(ev.pubkey);
const interactionCache = useInteractionCache(publicKey, ev.id); const interactionCache = useInteractionCache(publicKey, ev.id);
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const showNoteCreatorModal = useSelector((s: RootState) => s.noteCreator.show); const showNoteCreatorModal = useSelector((s: RootState) => s.noteCreator.show);

View File

@ -10,7 +10,6 @@ import useModeration from "Hooks/useModeration";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
import { useUserProfile } from "@snort/system-react"; import { useUserProfile } from "@snort/system-react";
import { System } from "index";
export interface NoteReactionProps { export interface NoteReactionProps {
data: TaggedNostrEvent; data: TaggedNostrEvent;
@ -19,7 +18,7 @@ export interface NoteReactionProps {
export default function NoteReaction(props: NoteReactionProps) { export default function NoteReaction(props: NoteReactionProps) {
const { data: ev } = props; const { data: ev } = props;
const { isMuted } = useModeration(); const { isMuted } = useModeration();
const profile = useUserProfile(System, ev.pubkey); const profile = useUserProfile(ev.pubkey);
const refEvent = useMemo(() => { const refEvent = useMemo(() => {
if (ev) { if (ev) {

View File

@ -12,7 +12,6 @@ import { formatShort } from "Number";
import Spinner from "Icons/Spinner"; import Spinner from "Icons/Spinner";
import SendSats from "Element/SendSats"; import SendSats from "Element/SendSats";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import { System } from "index";
interface PollProps { interface PollProps {
ev: TaggedNostrEvent; ev: TaggedNostrEvent;
@ -24,7 +23,7 @@ export default function Poll(props: PollProps) {
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const { wallet } = useWallet(); const { wallet } = useWallet();
const { preferences: prefs, publicKey: myPubKey, relays } = useLogin(); 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 [error, setError] = useState("");
const [invoice, setInvoice] = useState(""); const [invoice, setInvoice] = useState("");
const [voting, setVoting] = useState<number>(); const [voting, setVoting] = useState<number>();

View File

@ -8,7 +8,6 @@ import { useUserProfile } from "@snort/system-react";
import { hexToBech32, profileLink } from "SnortUtils"; import { hexToBech32, profileLink } from "SnortUtils";
import Avatar from "Element/Avatar"; import Avatar from "Element/Avatar";
import Nip05 from "Element/Nip05"; import Nip05 from "Element/Nip05";
import { System } from "index";
export interface ProfileImageProps { export interface ProfileImageProps {
pubkey: HexKey; pubkey: HexKey;
@ -21,6 +20,7 @@ export interface ProfileImageProps {
overrideUsername?: string; overrideUsername?: string;
profile?: UserMetadata; profile?: UserMetadata;
size?: number; size?: number;
onClick?: (e: React.MouseEvent) => void;
} }
export default function ProfileImage({ export default function ProfileImage({
@ -34,8 +34,9 @@ export default function ProfileImage({
overrideUsername, overrideUsername,
profile, profile,
size, size,
onClick,
}: ProfileImageProps) { }: ProfileImageProps) {
const user = profile ?? useUserProfile(System, pubkey); const user = useUserProfile(profile ? "" : pubkey) ?? profile;
const nip05 = defaultNip ? defaultNip : user?.nip05; const nip05 = defaultNip ? defaultNip : user?.nip05;
const name = useMemo(() => { const name = useMemo(() => {
@ -45,6 +46,7 @@ export default function ProfileImage({
function handleClick(e: React.MouseEvent) { function handleClick(e: React.MouseEvent) {
if (link === "") { if (link === "") {
e.preventDefault(); e.preventDefault();
onClick?.(e);
} }
} }
@ -68,7 +70,11 @@ export default function ProfileImage({
} }
if (link === "") { if (link === "") {
return <div className={`pfp${className ? ` ${className}` : ""}`}>{inner()}</div>; return (
<div className={`pfp${className ? ` ${className}` : ""}`} onClick={handleClick}>
{inner()}
</div>
);
} else { } else {
return ( return (
<Link <Link

View File

@ -6,32 +6,42 @@ import { useInView } from "react-intersection-observer";
import ProfileImage from "Element/ProfileImage"; import ProfileImage from "Element/ProfileImage";
import FollowButton from "Element/FollowButton"; import FollowButton from "Element/FollowButton";
import { System } from "index";
export interface ProfilePreviewProps { export interface ProfilePreviewProps {
pubkey: HexKey; pubkey: HexKey;
options?: { options?: {
about?: boolean; about?: boolean;
linkToProfile?: boolean;
}; };
actions?: ReactNode; actions?: ReactNode;
className?: string; className?: string;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
} }
export default function ProfilePreview(props: ProfilePreviewProps) { export default function ProfilePreview(props: ProfilePreviewProps) {
const pubkey = props.pubkey; const pubkey = props.pubkey;
const { ref, inView } = useInView({ triggerOnce: true }); const { ref, inView } = useInView({ triggerOnce: true });
const user = useUserProfile(System, inView ? pubkey : undefined); const user = useUserProfile(inView ? pubkey : undefined);
const options = { const options = {
about: true, about: true,
...props.options, ...props.options,
}; };
function handleClick(e: React.MouseEvent<HTMLDivElement>) {
if (props.onClick) {
e.stopPropagation();
e.preventDefault();
props.onClick(e);
}
}
return ( return (
<> <>
<div className={`profile-preview${props.className ? ` ${props.className}` : ""}`} ref={ref}> <div className={`profile-preview${props.className ? ` ${props.className}` : ""}`} ref={ref} onClick={handleClick}>
{inView && ( {inView && (
<> <>
<ProfileImage <ProfileImage
pubkey={pubkey} pubkey={pubkey}
link={options.linkToProfile ?? true ? undefined : ""}
subHeader={options.about ? <div className="about">{user?.about}</div> : undefined} subHeader={options.about ? <div className="about">{user?.about}</div> : undefined}
/> />
{props.actions ?? ( {props.actions ?? (

View File

@ -5,10 +5,9 @@ import { HexKey } from "@snort/system";
import { useUserProfile } from "@snort/system-react"; import { useUserProfile } from "@snort/system-react";
import { profileLink } from "SnortUtils"; import { profileLink } from "SnortUtils";
import { System } from "index";
export default function Username({ pubkey, onLinkVisit }: { pubkey: HexKey; onLinkVisit(): void }) { export default function Username({ pubkey, onLinkVisit }: { pubkey: HexKey; onLinkVisit(): void }) {
const user = useUserProfile(System, pubkey); const user = useUserProfile(pubkey);
const navigate = useNavigate(); const navigate = useNavigate();
function onClick(ev: MouseEvent) { function onClick(ev: MouseEvent) {

View File

@ -5,7 +5,6 @@ import { useUserProfile } from "@snort/system-react";
import SendSats from "Element/SendSats"; import SendSats from "Element/SendSats";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
import { System } from "index";
const ZapButton = ({ const ZapButton = ({
pubkey, pubkey,
@ -18,7 +17,7 @@ const ZapButton = ({
children?: React.ReactNode; children?: React.ReactNode;
event?: string; event?: string;
}) => { }) => {
const profile = useUserProfile(System, pubkey); const profile = useUserProfile(pubkey);
const [zap, setZap] = useState(false); const [zap, setZap] = useState(false);
const service = lnurl ?? (profile?.lud16 || profile?.lud06); const service = lnurl ?? (profile?.lud16 || profile?.lud06);
if (!service) return null; if (!service) return null;

View File

@ -1,9 +1,8 @@
import { useMemo } from "react"; 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 { useRequestBuilder } from "@snort/system-react";
import { unwrap, findTag, chunks } from "SnortUtils"; import { unwrap, findTag, chunks } from "SnortUtils";
import { System } from "index";
type BadgeAwards = { type BadgeAwards = {
pubkeys: string[]; pubkeys: string[];
@ -18,7 +17,7 @@ export default function useProfileBadges(pubkey?: HexKey) {
return b; return b;
}, [pubkey]); }, [pubkey]);
const profileBadges = useRequestBuilder<ReplaceableNoteStore>(System, ReplaceableNoteStore, sub); const profileBadges = useRequestBuilder(ReplaceableNoteStore, sub);
const profile = useMemo(() => { const profile = useMemo(() => {
if (profileBadges.data) { if (profileBadges.data) {
@ -58,7 +57,7 @@ export default function useProfileBadges(pubkey?: HexKey) {
return b; return b;
}, [profile, ds]); }, [profile, ds]);
const awards = useRequestBuilder<FlatNoteStore>(System, FlatNoteStore, awardsSub); const awards = useRequestBuilder(NoteCollection, awardsSub);
const result = useMemo(() => { const result = useMemo(() => {
if (awards.data) { if (awards.data) {

View File

@ -3,7 +3,6 @@ import { NostrPrefix, RequestBuilder, ReplaceableNoteStore, NostrLink } from "@s
import { useRequestBuilder } from "@snort/system-react"; import { useRequestBuilder } from "@snort/system-react";
import { unwrap } from "SnortUtils"; import { unwrap } from "SnortUtils";
import { System } from "index";
export default function useEventFeed(link: NostrLink) { export default function useEventFeed(link: NostrLink) {
const sub = useMemo(() => { const sub = useMemo(() => {
@ -28,5 +27,5 @@ export default function useEventFeed(link: NostrLink) {
return b; return b;
}, [link]); }, [link]);
return useRequestBuilder<ReplaceableNoteStore>(System, ReplaceableNoteStore, sub); return useRequestBuilder(ReplaceableNoteStore, sub);
} }

View File

@ -1,7 +1,6 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { HexKey, EventKind, NoteCollection, RequestBuilder } from "@snort/system"; import { HexKey, EventKind, NoteCollection, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react"; import { useRequestBuilder } from "@snort/system-react";
import { System } from "index";
export default function useFollowersFeed(pubkey?: HexKey) { export default function useFollowersFeed(pubkey?: HexKey) {
const sub = useMemo(() => { const sub = useMemo(() => {
@ -11,7 +10,7 @@ export default function useFollowersFeed(pubkey?: HexKey) {
return b; return b;
}, [pubkey]); }, [pubkey]);
const followersFeed = useRequestBuilder<NoteCollection>(System, NoteCollection, sub); const followersFeed = useRequestBuilder(NoteCollection, sub);
const followers = useMemo(() => { const followers = useMemo(() => {
const contactLists = followersFeed.data?.filter( const contactLists = followersFeed.data?.filter(

View File

@ -3,7 +3,6 @@ import { HexKey, TaggedNostrEvent, EventKind, NoteCollection, RequestBuilder } f
import { useRequestBuilder } from "@snort/system-react"; import { useRequestBuilder } from "@snort/system-react";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import { System } from "index";
export default function useFollowsFeed(pubkey?: HexKey) { export default function useFollowsFeed(pubkey?: HexKey) {
const { publicKey, follows } = useLogin(); const { publicKey, follows } = useLogin();
@ -16,7 +15,7 @@ export default function useFollowsFeed(pubkey?: HexKey) {
return b; return b;
}, [isMe, pubkey]); }, [isMe, pubkey]);
const contactFeed = useRequestBuilder<NoteCollection>(System, NoteCollection, sub); const contactFeed = useRequestBuilder(NoteCollection, sub);
return useMemo(() => { return useMemo(() => {
if (isMe) { if (isMe) {
return follows.item; return follows.item;

View File

@ -1,6 +1,5 @@
import { EventKind, FlatNoteStore, NostrLink, RequestBuilder } from "@snort/system"; import { EventKind, FlatNoteStore, NostrLink, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react"; import { useRequestBuilder } from "@snort/system-react";
import { System } from "index";
import { useMemo } from "react"; import { useMemo } from "react";
export function useLiveChatFeed(link: NostrLink) { export function useLiveChatFeed(link: NostrLink) {
@ -16,5 +15,5 @@ export function useLiveChatFeed(link: NostrLink) {
return rb; return rb;
}, [link]); }, [link]);
return useRequestBuilder<FlatNoteStore>(System, FlatNoteStore, sub); return useRequestBuilder(FlatNoteStore, sub);
} }

View File

@ -60,7 +60,7 @@ export default function useLoginFeed() {
return b; return b;
}, [pubKey]); }, [pubKey]);
const loginFeed = useRequestBuilder(System, NoteCollection, subLogin); const loginFeed = useRequestBuilder(NoteCollection, subLogin);
// update relays and follow lists // update relays and follow lists
useEffect(() => { useEffect(() => {
@ -156,7 +156,7 @@ export default function useLoginFeed() {
} }
} }
const listsFeed = useRequestBuilder<FlatNoteStore>(System, FlatNoteStore, subLists); const listsFeed = useRequestBuilder(FlatNoteStore, subLists);
useEffect(() => { useEffect(() => {
if (listsFeed.data) { if (listsFeed.data) {

View File

@ -4,7 +4,6 @@ import { useRequestBuilder } from "@snort/system-react";
import { getNewest } from "SnortUtils"; import { getNewest } from "SnortUtils";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import { System } from "index";
export default function useMutedFeed(pubkey?: HexKey) { export default function useMutedFeed(pubkey?: HexKey) {
const { publicKey, muted } = useLogin(); const { publicKey, muted } = useLogin();
@ -17,7 +16,7 @@ export default function useMutedFeed(pubkey?: HexKey) {
return b; return b;
}, [pubkey]); }, [pubkey]);
const mutedFeed = useRequestBuilder<NoteCollection>(System, NoteCollection, sub); const mutedFeed = useRequestBuilder(NoteCollection, sub);
const mutedList = useMemo(() => { const mutedList = useMemo(() => {
if (pubkey && mutedFeed.data) { if (pubkey && mutedFeed.data) {

View File

@ -1,7 +1,6 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { HexKey, FullRelaySettings, EventKind, RequestBuilder, ReplaceableNoteStore } from "@snort/system"; import { HexKey, FullRelaySettings, EventKind, RequestBuilder, ReplaceableNoteStore } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react"; import { useRequestBuilder } from "@snort/system-react";
import { System } from "index";
export default function useRelaysFeed(pubkey?: HexKey) { export default function useRelaysFeed(pubkey?: HexKey) {
const sub = useMemo(() => { const sub = useMemo(() => {
@ -11,7 +10,7 @@ export default function useRelaysFeed(pubkey?: HexKey) {
return b; return b;
}, [pubkey]); }, [pubkey]);
const relays = useRequestBuilder<ReplaceableNoteStore>(System, ReplaceableNoteStore, sub); const relays = useRequestBuilder(ReplaceableNoteStore, sub);
if (!relays.data?.content) { if (!relays.data?.content) {
return [] as FullRelaySettings[]; return [] as FullRelaySettings[];

View File

@ -13,7 +13,6 @@ import debug from "debug";
import { sanitizeRelayUrl } from "SnortUtils"; import { sanitizeRelayUrl } from "SnortUtils";
import { UserRelays } from "Cache"; import { UserRelays } from "Cache";
import { System } from "index";
interface RelayList { interface RelayList {
pubkey: string; pubkey: string;
@ -80,7 +79,7 @@ export default function useRelaysFeedFollows(pubkeys: HexKey[]): Array<RelayList
}); });
} }
const relays = useRequestBuilder<NoteCollection>(System, NoteCollection, sub); const relays = useRequestBuilder(NoteCollection, sub);
const notesRelays = relays.data?.filter(a => a.kind === EventKind.Relays) ?? []; const notesRelays = relays.data?.filter(a => a.kind === EventKind.Relays) ?? [];
const notesContactLists = relays.data?.filter(a => a.kind === EventKind.ContactList) ?? []; const notesContactLists = relays.data?.filter(a => a.kind === EventKind.ContactList) ?? [];
return useMemo(() => { return useMemo(() => {

View File

@ -4,7 +4,6 @@ import { useRequestBuilder } from "@snort/system-react";
import { appendDedupe } from "SnortUtils"; import { appendDedupe } from "SnortUtils";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import { System } from "index";
interface RelayTaggedEventId { interface RelayTaggedEventId {
id: u256; id: u256;
@ -58,7 +57,7 @@ export default function useThreadFeed(link: NostrLink) {
return sub; return sub;
}, [trackingEvents, trackingATags, allEvents, pref]); }, [trackingEvents, trackingATags, allEvents, pref]);
const store = useRequestBuilder<FlatNoteStore>(System, FlatNoteStore, sub); const store = useRequestBuilder(FlatNoteStore, sub);
useEffect(() => { useEffect(() => {
if (link.type === NostrPrefix.Address) { if (link.type === NostrPrefix.Address) {

View File

@ -5,7 +5,6 @@ import { useRequestBuilder } from "@snort/system-react";
import { unixNow, unwrap, tagFilterOfTextRepost } from "SnortUtils"; import { unixNow, unwrap, tagFilterOfTextRepost } from "SnortUtils";
import useTimelineWindow from "Hooks/useTimelineWindow"; import useTimelineWindow from "Hooks/useTimelineWindow";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import { System } from "index";
import { SearchRelays } from "Const"; import { SearchRelays } from "Const";
export interface TimelineFeedOptions { export interface TimelineFeedOptions {
@ -117,7 +116,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
return rb?.builder ?? null; return rb?.builder ?? null;
}, [until, since, options.method, pref, createBuilder]); }, [until, since, options.method, pref, createBuilder]);
const main = useRequestBuilder<NoteCollection>(System, NoteCollection, sub); const main = useRequestBuilder(NoteCollection, sub);
const subRealtime = useMemo(() => { const subRealtime = useMemo(() => {
const rb = createBuilder(); const rb = createBuilder();
@ -131,7 +130,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
return rb?.builder ?? null; return rb?.builder ?? null;
}, [pref.autoShowLatest, createBuilder]); }, [pref.autoShowLatest, createBuilder]);
const latest = useRequestBuilder<NoteCollection>(System, NoteCollection, subRealtime); const latest = useRequestBuilder(NoteCollection, subRealtime);
useEffect(() => { useEffect(() => {
// clear store if changing relays // clear store if changing relays
@ -177,7 +176,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
return rb.numFilters > 0 ? rb : null; return rb.numFilters > 0 ? rb : null;
}, [main.data, pref, subject.type]); }, [main.data, pref, subject.type]);
const related = useRequestBuilder<NoteCollection>(System, NoteCollection, subNext); const related = useRequestBuilder(NoteCollection, subNext);
return { return {
main: main.data, main: main.data,

View File

@ -1,8 +1,6 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { EventKind, RequestBuilder, parseZap, NostrLink, NostrPrefix, NoteCollection } from "@snort/system"; import { EventKind, RequestBuilder, parseZap, NostrLink, NostrPrefix, NoteCollection } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react"; import { useRequestBuilder } from "@snort/system-react";
import { System } from "index";
import { UserCache } from "Cache"; import { UserCache } from "Cache";
export default function useZapsFeed(link?: NostrLink) { export default function useZapsFeed(link?: NostrLink) {
@ -17,7 +15,7 @@ export default function useZapsFeed(link?: NostrLink) {
return b; return b;
}, [link]); }, [link]);
const zapsFeed = useRequestBuilder(System, NoteCollection, sub); const zapsFeed = useRequestBuilder(NoteCollection, sub);
const zaps = useMemo(() => { const zaps = useMemo(() => {
if (zapsFeed.data) { if (zapsFeed.data) {

View File

@ -3,7 +3,6 @@ import { HexKey, Lists, EventKind, FlatNoteStore, NoteCollection, RequestBuilder
import { useRequestBuilder } from "@snort/system-react"; import { useRequestBuilder } from "@snort/system-react";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import { System } from "index";
export default function useNotelistSubscription(pubkey: HexKey | undefined, l: Lists, defaultIds: HexKey[]) { export default function useNotelistSubscription(pubkey: HexKey | undefined, l: Lists, defaultIds: HexKey[]) {
const { preferences, publicKey } = useLogin(); const { preferences, publicKey } = useLogin();
@ -17,7 +16,7 @@ export default function useNotelistSubscription(pubkey: HexKey | undefined, l: L
return rb; return rb;
}, [pubkey]); }, [pubkey]);
const listStore = useRequestBuilder<NoteCollection>(System, NoteCollection, sub); const listStore = useRequestBuilder(NoteCollection, sub);
const etags = useMemo(() => { const etags = useMemo(() => {
if (isMe) return defaultIds; if (isMe) return defaultIds;
// there should only be a single event here because we only load 1 pubkey // 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; return s;
}, [etags, pubkey, preferences]); }, [etags, pubkey, preferences]);
const store = useRequestBuilder<FlatNoteStore>(System, FlatNoteStore, esub); const store = useRequestBuilder(FlatNoteStore, esub);
return store.data ?? []; return store.data ?? [];
} }

View File

@ -16,7 +16,7 @@
header { header {
display: flex; display: flex;
padding: 10px 16px; padding: var(--header-padding-tb) 16px;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
align-self: stretch; align-self: stretch;

View File

@ -142,7 +142,7 @@ const AccountHeader = () => {
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const { publicKey, latestNotification, readNotifications } = useLogin(); const { publicKey, latestNotification, readNotifications } = useLogin();
const profile = useUserProfile(System, publicKey); const profile = useUserProfile(publicKey);
const hasNotifications = useMemo( const hasNotifications = useMemo(
() => latestNotification > readNotifications, () => latestNotification > readNotifications,

View File

@ -1,7 +1,7 @@
.dm-page { .dm-page {
display: grid; display: grid;
grid-template-columns: 350px auto; 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 */ /* 100vh - header - padding */
overflow: hidden; overflow: hidden;
} }
@ -26,13 +26,14 @@
/* User list */ /* User list */
.dm-page > div:nth-child(1) { .dm-page > div:nth-child(1) {
overflow-y: auto; overflow-y: auto;
margin: 0 10px; padding: 0 5px;
padding: 0 10px 0 0;
} }
/* Chat window */ /* Chat window */
.dm-page > div:nth-child(2) { .dm-page > div:nth-child(2) {
height: calc(100vh - 57px); padding: 0 12px;
background-color: var(--gray-superdark);
border-radius: 16px;
} }
/* Profile pannel */ /* Profile pannel */
@ -48,3 +49,53 @@
.dm-page > div:nth-child(3) .card { .dm-page > div:nth-child(3) .card {
cursor: pointer; 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;
}

View File

@ -1,12 +1,14 @@
import "./MessagesPage.css";
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { TLVEntryType, decodeTLV } from "@snort/system"; 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 UnreadCount from "Element/UnreadCount";
import ProfileImage, { getDisplayName } from "Element/ProfileImage"; import ProfileImage, { getDisplayName } from "Element/ProfileImage";
import { parseId } from "SnortUtils"; import { appendDedupe, debounce, parseId } from "SnortUtils";
import NoteToSelf from "Element/NoteToSelf"; import NoteToSelf from "Element/NoteToSelf";
import useModeration from "Hooks/useModeration"; import useModeration from "Hooks/useModeration";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
@ -16,11 +18,9 @@ import DmWindow from "Element/DmWindow";
import Avatar from "Element/Avatar"; import Avatar from "Element/Avatar";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
import Text from "Element/Text"; import Text from "Element/Text";
import { System } from "index"; import { Chat, ChatType, createChatLink, useChatSystem } from "chat";
import { Chat, ChatType, useChatSystem } from "chat"; import Modal from "Element/Modal";
import ProfilePreview from "Element/ProfilePreview";
import "./MessagesPage.css";
import messages from "./messages";
const TwoCol = 768; const TwoCol = 768;
const ThreeCol = 1500; const ThreeCol = 1500;
@ -49,15 +49,15 @@ export default function MessagesPage() {
function noteToSelf(chat: Chat) { function noteToSelf(chat: Chat) {
return ( return (
<div className="flex mb10" key={chat.id} onClick={e => openChat(e, chat.type, chat.id)}> <div className="flex p" key={chat.id} onClick={e => openChat(e, chat.type, chat.id)}>
<NoteToSelf clickable={true} className="f-grow" link="" pubkey={chat.id} /> <NoteToSelf clickable={true} className="f-grow" link="" pubkey={chat.id} />
</div> </div>
); );
} }
function conversationIdent(chat: Chat) { function conversationIdent(cx: Chat) {
if (chat.participants.length === 1) { if (cx.participants.length === 1) {
const p = chat.participants[0]; const p = cx.participants[0];
if (p.type === "pubkey") { if (p.type === "pubkey") {
return <ProfileImage pubkey={p.id} className="f-grow" link="" />; return <ProfileImage pubkey={p.id} className="f-grow" link="" />;
@ -67,27 +67,29 @@ export default function MessagesPage() {
} else { } else {
return ( return (
<div className="flex f-grow pfp-overlap"> <div className="flex f-grow pfp-overlap">
{chat.participants.map(v => ( {cx.participants.map(v => (
<ProfileImage pubkey={v.id} link="" showUsername={false} /> <ProfileImage pubkey={v.id} link="" showUsername={false} />
))} ))}
<div className="f-grow">{chat.title}</div> {cx.title ?? <FormattedMessage defaultMessage="Group Chat" />}
</div> </div>
); );
} }
} }
function conversation(chat: Chat) { function conversation(cx: Chat) {
if (!login.publicKey) return null; if (!login.publicKey) return null;
const participants = chat.participants.map(a => a.id); const participants = cx.participants.map(a => a.id);
if (participants.length === 1 && participants[0] === login.publicKey) return noteToSelf(chat); if (participants.length === 1 && participants[0] === login.publicKey) return noteToSelf(cx);
const isActive = cx.id === chat;
return ( return (
<div className="flex mb10" key={chat.id} onClick={e => openChat(e, chat.type, chat.id)}> <div className={`flex p${isActive ? " active" : ""}`} key={cx.id} onClick={e => openChat(e, cx.type, cx.id)}>
{conversationIdent(chat)} {conversationIdent(cx)}
<div className="nowrap"> <div className="nowrap">
<small> <small>
<NoteTime from={chat.lastMessage * 1000} fallback={formatMessage({ defaultMessage: "Just now" })} /> <NoteTime from={cx.lastMessage * 1000} fallback={formatMessage({ defaultMessage: "Just now" })} />
</small> </small>
{chat.unread > 0 && <UnreadCount unread={chat.unread} />} {cx.unread > 0 && <UnreadCount unread={cx.unread} />}
</div> </div>
</div> </div>
); );
@ -96,14 +98,12 @@ export default function MessagesPage() {
return ( return (
<div className="dm-page"> <div className="dm-page">
{(pageWidth >= TwoCol || !chat) && ( {(pageWidth >= TwoCol || !chat) && (
<div> <div className="chat-list">
<div className="flex"> <div className="flex p f-space">
<h3 className="f-grow">
<FormattedMessage {...messages.Messages} />
</h3>
<button disabled={unreadCount <= 0} type="button"> <button disabled={unreadCount <= 0} type="button">
<FormattedMessage {...messages.MarkAllRead} /> <FormattedMessage defaultMessage="Mark all read" />
</button> </button>
<NewChatWindow />
</div> </div>
{chats {chats
.sort((a, b) => { .sort((a, b) => {
@ -117,7 +117,7 @@ export default function MessagesPage() {
.map(conversation)} .map(conversation)}
</div> </div>
)} )}
{chat && <DmWindow id={chat} />} {chat ? <DmWindow id={chat} /> : pageWidth >= TwoCol && <div></div>}
{pageWidth >= ThreeCol && chat && ( {pageWidth >= ThreeCol && chat && (
<div> <div>
<ProfileDmActions id={chat} /> <ProfileDmActions id={chat} />
@ -132,7 +132,7 @@ function ProfileDmActions({ id }: { id: string }) {
.filter(a => a.type === TLVEntryType.Author) .filter(a => a.type === TLVEntryType.Author)
.map(a => a.value as string); .map(a => a.value as string);
const pubkey = authors[0]; const pubkey = authors[0];
const profile = useUserProfile(System, pubkey); const profile = useUserProfile(pubkey);
const { block, unblock, isBlocked } = useModeration(); const { block, unblock, isBlocked } = useModeration();
function truncAbout(s?: string) { function truncAbout(s?: string) {
@ -158,3 +158,105 @@ function ProfileDmActions({ id }: { id: string }) {
</> </>
); );
} }
function NewChatWindow() {
const [show, setShow] = useState(false);
const [newChat, setNewChat] = useState<string[]>([]);
const [results, setResults] = useState<string[]>([]);
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 (
<>
<button type="button" className="new-chat" onClick={() => setShow(true)}>
<Icon name="plus" size={16} />
</button>
{show && (
<Modal onClose={() => setShow(false)} className="new-chat-modal">
<div className="flex-column g16">
<div className="flex f-space">
<h2>
<FormattedMessage defaultMessage="New Chat" />
</h2>
<button onClick={startChat}>
<FormattedMessage defaultMessage="Start chat" />
</button>
</div>
<div className="flex-column g8">
<h3>
<FormattedMessage defaultMessage="Search users" />
</h3>
<input
type="text"
placeholder="npub/nprofile/nostr address"
value={term}
onChange={e => setSearchTerm(e.target.value)}
/>
</div>
<div className="flex">
{newChat.map(a => (
<ProfileImage
key={`selected-${a}`}
pubkey={a}
showUsername={false}
link=""
onClick={() => togglePubkey(a)}
/>
))}
</div>
<div>
<p>
<FormattedMessage defaultMessage="People you follow" />
</p>
<div className="user-list flex-column g2">
{results.map(a => {
return (
<ProfilePreview
pubkey={a}
key={`option-${a}`}
options={{ about: false, linkToProfile: false }}
actions={<></>}
onClick={() => togglePubkey(a)}
className={newChat.includes(a) ? "active" : undefined}
/>
);
})}
</div>
</div>
</div>
</Modal>
)}
</>
);
}

View File

@ -6,7 +6,7 @@ import {
NostrEvent, NostrEvent,
NostrLink, NostrLink,
NostrPrefix, NostrPrefix,
TaggedRawEvent, TaggedNostrEvent,
createNostrLink, createNostrLink,
parseZap, parseZap,
} from "@snort/system"; } from "@snort/system";
@ -22,13 +22,12 @@ import { dedupe, findTag, orderDescending } from "SnortUtils";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
import ProfileImage, { getDisplayName } from "Element/ProfileImage"; import ProfileImage, { getDisplayName } from "Element/ProfileImage";
import useModeration from "Hooks/useModeration"; import useModeration from "Hooks/useModeration";
import { System } from "index";
import useEventFeed from "Feed/EventFeed"; import useEventFeed from "Feed/EventFeed";
import Text from "Element/Text"; import Text from "Element/Text";
import { formatShort } from "Number"; import { formatShort } from "Number";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
function notificationContext(ev: TaggedRawEvent) { function notificationContext(ev: TaggedNostrEvent) {
switch (ev.kind) { switch (ev.kind) {
case EventKind.ZapReceipt: { case EventKind.ZapReceipt: {
const aTag = findTag(ev, "a"); const aTag = findTag(ev, "a");
@ -88,14 +87,14 @@ export default function NotificationsPage() {
return orderDescending([...notifications]) return orderDescending([...notifications])
.filter(a => !isMuted(a.pubkey) && findTag(a, "p") === login.publicKey) .filter(a => !isMuted(a.pubkey) && findTag(a, "p") === login.publicKey)
.reduce((acc, v) => { .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)) { if (acc.has(key)) {
unwrap(acc.get(key)).push(v as TaggedRawEvent); unwrap(acc.get(key)).push(v as TaggedNostrEvent);
} else { } else {
acc.set(key, [v as TaggedRawEvent]); acc.set(key, [v as TaggedNostrEvent]);
} }
return acc; return acc;
}, new Map<string, Array<TaggedRawEvent>>()); }, new Map<string, Array<TaggedNostrEvent>>());
}, [notifications]); }, [notifications]);
return ( return (
@ -105,7 +104,7 @@ export default function NotificationsPage() {
); );
} }
function NotificationGroup({ evs }: { evs: Array<TaggedRawEvent> }) { function NotificationGroup({ evs }: { evs: Array<TaggedNostrEvent> }) {
const { ref, inView } = useInView({ triggerOnce: true }); const { ref, inView } = useInView({ triggerOnce: true });
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();
const kind = evs[0].kind; const kind = evs[0].kind;
@ -123,7 +122,7 @@ function NotificationGroup({ evs }: { evs: Array<TaggedRawEvent> }) {
}) })
); );
const firstPubkey = pubkeys[0]; 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 context = notificationContext(evs[0]);
const totalZaps = zaps.reduce((acc, v) => acc + v.amount, 0); const totalZaps = zaps.reduce((acc, v) => acc + v.amount, 0);
@ -187,13 +186,13 @@ function NotificationGroup({ evs }: { evs: Array<TaggedRawEvent> }) {
<div className="card notification-group" ref={ref}> <div className="card notification-group" ref={ref}>
{inView && ( {inView && (
<> <>
<div className="flex f-col g12"> <div className="flex-column g12">
<div> <div>
<Icon name={iconName()} size={24} className={iconName()} /> <Icon name={iconName()} size={24} className={iconName()} />
</div> </div>
<div>{kind === EventKind.ZapReceipt && formatShort(totalZaps)}</div> <div>{kind === EventKind.ZapReceipt && formatShort(totalZaps)}</div>
</div> </div>
<div className="flex f-col g12 w-max"> <div className="flex-column g12">
<div className="flex"> <div className="flex">
{pubkeys {pubkeys
.filter(a => a !== "anon") .filter(a => a !== "anon")

View File

@ -56,7 +56,6 @@ import { getNip05PubKey } from "Pages/LoginPage";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import messages from "./messages"; import messages from "./messages";
import { System } from "index";
const NOTES = 0; const NOTES = 0;
const REACTIONS = 1; const REACTIONS = 1;
@ -113,7 +112,7 @@ export default function ProfilePage() {
const params = useParams(); const params = useParams();
const navigate = useNavigate(); const navigate = useNavigate();
const [id, setId] = useState<string>(); const [id, setId] = useState<string>();
const user = useUserProfile(System, id); const user = useUserProfile(id);
const loginPubKey = useLogin().publicKey; const loginPubKey = useLogin().publicKey;
const isMe = loginPubKey === id; const isMe = loginPubKey === id;
const [showLnQr, setShowLnQr] = useState<boolean>(false); const [showLnQr, setShowLnQr] = useState<boolean>(false);

View File

@ -35,7 +35,7 @@ const DataProviders = [
function ZapTarget({ target }: { target: ZapPoolRecipient }) { function ZapTarget({ target }: { target: ZapPoolRecipient }) {
const login = useLogin(); const login = useLogin();
const profile = useUserProfile(System, target.pubkey); const profile = useUserProfile(target.pubkey);
const hasAddress = profile?.lud16 || profile?.lud06; const hasAddress = profile?.lud16 || profile?.lud06;
const defaultZapMount = Math.ceil(login.preferences.defaultZapAmount * (target.split / 100)); const defaultZapMount = Math.ceil(login.preferences.defaultZapAmount * (target.split / 100));
return ( return (

View File

@ -10,12 +10,11 @@ import ProfileImage from "Element/ProfileImage";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import messages from "./messages"; import messages from "./messages";
import { System } from "index";
export default function GetVerified() { export default function GetVerified() {
const navigate = useNavigate(); const navigate = useNavigate();
const { publicKey } = useLogin(); const { publicKey } = useLogin();
const user = useUserProfile(System, publicKey); const user = useUserProfile(publicKey);
const [isVerified, setIsVerified] = useState(false); const [isVerified, setIsVerified] = useState(false);
const name = user?.name || "nostrich"; const name = user?.name || "nostrich";
const [nip05, setNip05] = useState(`${name}@snort.social`); const [nip05, setNip05] = useState(`${name}@snort.social`);

View File

@ -16,7 +16,7 @@ import messages from "./messages";
export default function ProfileSetup() { export default function ProfileSetup() {
const login = useLogin(); const login = useLogin();
const myProfile = useUserProfile(System, login.publicKey); const myProfile = useUserProfile(login.publicKey);
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [picture, setPicture] = useState(""); const [picture, setPicture] = useState("");
const { formatMessage } = useIntl(); const { formatMessage } = useIntl();

View File

@ -24,7 +24,7 @@ export interface ProfileSettingsProps {
export default function ProfileSettings(props: ProfileSettingsProps) { export default function ProfileSettings(props: ProfileSettingsProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const { publicKey: id } = useLogin(); const { publicKey: id } = useLogin();
const user = useUserProfile(System, id ?? ""); const user = useUserProfile(id ?? "");
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const uploader = useFileUpload(); const uploader = useFileUpload();

View File

@ -6,14 +6,13 @@ import Icon from "Icons/Icon";
import { UITask } from "Tasks"; import { UITask } from "Tasks";
import { DonateTask } from "./DonateTask"; import { DonateTask } from "./DonateTask";
import { Nip5Task } from "./Nip5Task"; import { Nip5Task } from "./Nip5Task";
import { System } from "index";
const AllTasks: Array<UITask> = [new Nip5Task(), new DonateTask()]; const AllTasks: Array<UITask> = [new Nip5Task(), new DonateTask()];
AllTasks.forEach(a => a.load()); AllTasks.forEach(a => a.load());
export const TaskList = () => { export const TaskList = () => {
const publicKey = useLogin().publicKey; const publicKey = useLogin().publicKey;
const user = useUserProfile(System, publicKey); const user = useUserProfile(publicKey);
const [, setTick] = useState<number>(0); const [, setTick] = useState<number>(0);
function muteTask(t: UITask) { function muteTask(t: UITask) {

View File

@ -4,10 +4,14 @@ import {
EventKind, EventKind,
EventPublisher, EventPublisher,
NostrEvent, NostrEvent,
NostrPrefix,
RequestBuilder, RequestBuilder,
SystemInterface, SystemInterface,
TLVEntry,
TLVEntryType,
TaggedRawEvent, TaggedRawEvent,
UserMetadata, UserMetadata,
encodeTLVEntries,
} from "@snort/system"; } from "@snort/system";
import { unwrap } from "@snort/shared"; import { unwrap } from "@snort/shared";
import { Chats, GiftsCache } from "Cache"; import { Chats, GiftsCache } from "Cache";
@ -102,6 +106,57 @@ export function setLastReadIn(id: string) {
window.localStorage.setItem(k, now.toString()); window.localStorage.setItem(k, now.toString());
} }
export function createChatLink(type: ChatType, ...params: Array<string>) {
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() { export function useNip4Chat() {
const { publicKey } = useLogin(); const { publicKey } = useLogin();
return useSyncExternalStore( return useSyncExternalStore(

View File

@ -68,7 +68,10 @@ export class Nip24ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
}, },
{ {
t: 0, t: 0,
title: "", title: undefined,
} as {
t: number;
title: string | undefined;
} }
); );
return { return {

View File

@ -42,6 +42,8 @@
); );
--expired-invoice-gradient: linear-gradient(45deg, var(--gray-superdark) 50%, var(--gray), var(--gray-superdark)); --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); --strike-army-gradient: linear-gradient(to bottom right, #ccff00, #a1c900);
--header-padding-tb: 10px;
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
@ -115,10 +117,6 @@ code {
margin-right: auto; margin-right: auto;
} }
body #root > div:not(.page) header {
padding: 2px 10px;
}
@media (min-width: 768px) { @media (min-width: 768px) {
.page { .page {
width: 640px; width: 640px;
@ -411,6 +409,10 @@ input:disabled {
gap: 12px; gap: 12px;
} }
.g16 {
gap: 16px;
}
.g24 { .g24 {
gap: 24px; gap: 24px;
} }
@ -681,8 +683,8 @@ button.tall {
} }
.rta__textarea { .rta__textarea {
/* Fix width calculation to account for 12px padding on input */ /* Fix width calculation to account for 32px padding on input */
width: calc(100% - 24px) !important; width: calc(100% - 32px) !important;
} }
.ctx-menu { .ctx-menu {

View File

@ -34,6 +34,7 @@ import DebugPage from "Pages/Debug";
import { db } from "Db"; import { db } from "Db";
import { preload, RelayMetrics, UserCache, UserRelays } from "Cache"; import { preload, RelayMetrics, UserCache, UserRelays } from "Cache";
import { LoginStore } from "Login"; import { LoginStore } from "Login";
import { SnortContext } from "@snort/system-react";
/** /**
* Singleton nostr system * Singleton nostr system
@ -164,7 +165,9 @@ root.render(
<StrictMode> <StrictMode>
<Provider store={Store}> <Provider store={Store}>
<IntlProvider> <IntlProvider>
<RouterProvider router={router} /> <SnortContext.Provider value={System}>
<RouterProvider router={router} />
</SnortContext.Provider>
</IntlProvider> </IntlProvider>
</Provider> </Provider>
</StrictMode> </StrictMode>

View File

@ -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<string | undefined> {
interface NostrJson { interface NostrJson {
names: Record<string, string>; names: Record<string, string>;
} }

View File

@ -1,7 +1,7 @@
import { useMemo } from "react"; 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({}); 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 })); ["wss://relay.snort.social", "wss://nos.lol"].forEach(r => System.ConnectToRelay(r, { read: true, write: false }));
export function Note({ ev }: { ev: TaggedNostrEvent }) { export function Note({ ev }: { ev: TaggedNostrEvent }) {
const profile = useUserProfile(System, ev.pubkey); const profile = useUserProfile(ev.pubkey);
return ( return (
<div> <div>
@ -27,7 +27,7 @@ export function UserPosts(props: { pubkey: string }) {
return rb; return rb;
}, [props.pubkey]); }, [props.pubkey]);
const data = useRequestBuilder<FlatNoteStore>(System, FlatNoteStore, sub); const data = useRequestBuilder(NoteCollection, sub);
return ( return (
<> <>
{data.data.map(a => ( {data.data.map(a => (
@ -38,5 +38,7 @@ export function UserPosts(props: { pubkey: string }) {
} }
export function MyApp() { export function MyApp() {
return <UserPosts pubkey="63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed" />; return <SnortContext.Provider value={System}>
<UserPosts pubkey="63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed" />
</SnortContext.Provider>;
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@snort/system-react", "name": "@snort/system-react",
"version": "1.0.11", "version": "1.0.12",
"description": "React hooks for @snort/system", "description": "React hooks for @snort/system",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@ -15,10 +15,12 @@
"dist" "dist"
], ],
"dependencies": { "dependencies": {
"@snort/shared": "^1.0.4",
"@snort/system": "^1.0.16",
"react": "^18.2.0" "react": "^18.2.0"
}, },
"peerDependencies": {
"@snort/shared": "^1.0.4",
"@snort/system": "^1.0.17"
},
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.14", "@types/react": "^18.2.14",
"typescript": "^5.1.6" "typescript": "^5.1.6"

View File

@ -0,0 +1,4 @@
import { createContext } from "react";
import { NostrSystem, SystemInterface } from "@snort/system";
export const SnortContext = createContext<SystemInterface>(new NostrSystem({}));

View File

@ -1,3 +1,5 @@
export * from "./useRequestBuilder"; export * from "./useRequestBuilder";
export * from "./useSystemState"; export * from "./useSystemState";
export * from "./useUserProfile"; export * from "./useUserProfile";
export * from "./context";
export * from "./useUserSearch";

View File

@ -1,15 +1,16 @@
import { useSyncExternalStore } from "react"; import { useContext, useSyncExternalStore } from "react";
import { RequestBuilder, EmptySnapshot, NoteStore, StoreSnapshot, SystemInterface } from "@snort/system"; import { RequestBuilder, EmptySnapshot, NoteStore, StoreSnapshot } from "@snort/system";
import { unwrap } from "@snort/shared"; import { unwrap } from "@snort/shared";
import { SnortContext } from "./context";
/** /**
* Send a query to the relays and wait for data * Send a query to the relays and wait for data
*/ */
const useRequestBuilder = <TStore extends NoteStore, TSnapshot = ReturnType<TStore["getSnapshotData"]>>( const useRequestBuilder = <TStore extends NoteStore, TSnapshot = ReturnType<TStore["getSnapshotData"]>>(
system: SystemInterface, type: { new(): TStore },
type: { new (): TStore },
rb: RequestBuilder | null, rb: RequestBuilder | null,
) => { ) => {
const system = useContext(SnortContext);
const subscribe = (onChanged: () => void) => { const subscribe = (onChanged: () => void) => {
if (rb) { if (rb) {
const q = system.Query<TStore>(type, rb); const q = system.Query<TStore>(type, rb);

View File

@ -1,10 +1,12 @@
import { useSyncExternalStore } from "react"; import { useContext, useSyncExternalStore } from "react";
import { HexKey, MetadataCache, NostrSystem } from "@snort/system"; import { HexKey, MetadataCache } from "@snort/system";
import { SnortContext } from "./context";
/** /**
* Gets a profile from cache or requests it from the relays * 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<MetadataCache | undefined>( return useSyncExternalStore<MetadataCache | undefined>(
h => { h => {
if (pubKey) { if (pubKey) {

View File

@ -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<Array<string>> {
// 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;
}

View File

@ -1,6 +1,6 @@
{ {
"name": "@snort/system", "name": "@snort/system",
"version": "1.0.16", "version": "1.0.17",
"description": "Snort nostr system package", "description": "Snort nostr system package",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@ -30,10 +30,12 @@
"@noble/curves": "^1.0.0", "@noble/curves": "^1.0.0",
"@noble/hashes": "^1.3.1", "@noble/hashes": "^1.3.1",
"@scure/base": "^1.1.1", "@scure/base": "^1.1.1",
"@snort/shared": "^1.0.4",
"@stablelib/xchacha20": "^1.0.1", "@stablelib/xchacha20": "^1.0.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"dexie": "^3.2.4", "dexie": "^3.2.4",
"uuid": "^9.0.0" "uuid": "^9.0.0"
},
"peerDependencies": {
"@snort/shared": "^1.0.4"
} }
} }

View File

@ -3,6 +3,7 @@ import { RequestBuilder } from "./request-builder";
import { NoteStore } from "./note-collection"; import { NoteStore } from "./note-collection";
import { Query } from "./query"; import { Query } from "./query";
import { NostrEvent, ReqFilter } from "./nostr"; import { NostrEvent, ReqFilter } from "./nostr";
import { ProfileLoaderService } from "./profile-cache";
export * from "./nostr-system"; export * from "./nostr-system";
export { default as EventKind } from "./event-kind"; export { default as EventKind } from "./event-kind";
@ -46,6 +47,7 @@ export interface SystemInterface {
DisconnectRelay(address: string): void; DisconnectRelay(address: string): void;
BroadcastEvent(ev: NostrEvent): void; BroadcastEvent(ev: NostrEvent): void;
WriteOnceToRelay(relay: string, ev: NostrEvent): Promise<void>; WriteOnceToRelay(relay: string, ev: NostrEvent): Promise<void>;
get ProfileLoader(): ProfileLoaderService
} }
export interface SystemSnapshot { export interface SystemSnapshot {

View File

@ -1,6 +1,6 @@
import { ExternalStore } from "@snort/shared"; import { ExternalStore } from "@snort/shared";
import { SystemSnapshot, SystemInterface } from "."; import { SystemSnapshot, SystemInterface, ProfileLoaderService } from ".";
import { AuthHandler, ConnectionStateSnapshot, RelaySettings } from "./connection"; import { AuthHandler, ConnectionStateSnapshot, RelaySettings } from "./connection";
import { NostrEvent } from "./nostr"; import { NostrEvent } from "./nostr";
import { NoteStore } from "./note-collection"; import { NoteStore } from "./note-collection";
@ -20,6 +20,10 @@ export class SystemWorker extends ExternalStore<SystemSnapshot> implements Syste
throw new Error("SharedWorker is not supported"); throw new Error("SharedWorker is not supported");
} }
} }
get ProfileLoader(): ProfileLoaderService {
throw new Error("Method not implemented.");
}
HandleAuth?: AuthHandler; HandleAuth?: AuthHandler;