Note to Self
diff --git a/src/Element/ProfileImage.tsx b/src/Element/ProfileImage.tsx
index 542e6d6d..30af6866 100644
--- a/src/Element/ProfileImage.tsx
+++ b/src/Element/ProfileImage.tsx
@@ -1,13 +1,13 @@
import "./ProfileImage.css";
import { useMemo } from "react";
-import { useNavigate, Link } from "react-router-dom";
-import useProfile from "Feed/ProfileFeed";
+import { Link, useNavigate } from "react-router-dom";
+import { useUserProfile } from "Feed/ProfileFeed";
import { hexToBech32, profileLink } from "Util";
import Avatar from "Element/Avatar"
import Nip05 from "Element/Nip05";
import { HexKey } from "Nostr";
-import { MetadataCache } from "Db/User";
+import { MetadataCache } from "State/Users";
export interface ProfileImageProps {
pubkey: HexKey,
@@ -19,7 +19,7 @@ export interface ProfileImageProps {
export default function ProfileImage({ pubkey, subHeader, showUsername = true, className, link }: ProfileImageProps) {
const navigate = useNavigate();
- const user = useProfile(pubkey)?.get(pubkey);
+ const user = useUserProfile(pubkey);
const name = useMemo(() => {
return getDisplayName(user, pubkey);
diff --git a/src/Element/ProfilePreview.tsx b/src/Element/ProfilePreview.tsx
index d00aacd5..83e9c49c 100644
--- a/src/Element/ProfilePreview.tsx
+++ b/src/Element/ProfilePreview.tsx
@@ -3,7 +3,7 @@ import { ReactNode } from "react";
import ProfileImage from "Element/ProfileImage";
import FollowButton from "Element/FollowButton";
-import useProfile from "Feed/ProfileFeed";
+import { useUserProfile } from "Feed/ProfileFeed";
import { HexKey } from "Nostr";
import { useInView } from "react-intersection-observer";
@@ -12,11 +12,12 @@ export interface ProfilePreviewProps {
options?: {
about?: boolean
},
- actions?: ReactNode
+ actions?: ReactNode,
+ className?: string
}
export default function ProfilePreview(props: ProfilePreviewProps) {
const pubkey = props.pubkey;
- const user = useProfile(pubkey)?.get(pubkey);
+ const user = useUserProfile(pubkey);
const { ref, inView } = useInView({ triggerOnce: true });
const options = {
about: true,
@@ -24,7 +25,7 @@ export default function ProfilePreview(props: ProfilePreviewProps) {
};
return (
-
+
{inView && <>
@@ -34,4 +35,4 @@ export default function ProfilePreview(props: ProfilePreviewProps) {
>}
)
-}
\ No newline at end of file
+}
diff --git a/src/Element/Relay.tsx b/src/Element/Relay.tsx
index 4a00c1db..3fdc656b 100644
--- a/src/Element/Relay.tsx
+++ b/src/Element/Relay.tsx
@@ -63,7 +63,7 @@ export default function Relay(props: RelayProps) {
{state?.disconnects}
- navigate(name)}>
+ navigate(state!.id)}>
diff --git a/src/Element/Text.css b/src/Element/Text.css
index 8903a429..06204bd9 100644
--- a/src/Element/Text.css
+++ b/src/Element/Text.css
@@ -57,7 +57,7 @@
margin: 20px;
}
-.text img, .text video, .text iframe {
+.text img, .text video, .text iframe, .text audio {
max-width: 100%;
max-height: 500px;
margin: 10px auto;
diff --git a/src/Element/Text.tsx b/src/Element/Text.tsx
index e74a5cef..e7200980 100644
--- a/src/Element/Text.tsx
+++ b/src/Element/Text.tsx
@@ -1,22 +1,24 @@
import './Text.css'
-import { useMemo } from "react";
+import { useMemo, useCallback } from "react";
import { Link } from "react-router-dom";
import ReactMarkdown from "react-markdown";
+import { visit, SKIP } from "unist-util-visit";
import { TwitterTweetEmbed } from "react-twitter-embed";
-import { UrlRegex, FileExtensionRegex, MentionRegex, InvoiceRegex, YoutubeUrlRegex, TweetUrlRegex, HashtagRegex, TidalRegex, SoundCloudRegex } from "Const";
+import { UrlRegex, FileExtensionRegex, MentionRegex, InvoiceRegex, YoutubeUrlRegex, TweetUrlRegex, HashtagRegex, TidalRegex, SoundCloudRegex, MixCloudRegex } from "Const";
import { eventLink, hexToBech32 } from "Util";
import Invoice from "Element/Invoice";
import Hashtag from "Element/Hashtag";
import Tag from "Nostr/Tag";
-import { MetadataCache } from "Db/User";
+import { MetadataCache } from "State/Users";
import Mention from "Element/Mention";
import TidalEmbed from "Element/TidalEmbed";
import { useSelector } from 'react-redux';
import { RootState } from 'State/Store';
import { UserPreferences } from 'State/Login';
import SoundCloudEmbed from 'Element/SoundCloudEmded'
+import MixCloudEmbed from './MixCloudEmbed';
function transformHttpLink(a: string, pref: UserPreferences) {
try {
@@ -28,6 +30,7 @@ function transformHttpLink(a: string, pref: UserPreferences) {
const tweetId = TweetUrlRegex.test(a) && RegExp.$2;
const tidalId = TidalRegex.test(a) && RegExp.$1;
const soundcloundId = SoundCloudRegex.test(a) && RegExp.$1;
+ const mixcloudId = MixCloudRegex.test(a) && RegExp.$1;
const extension = FileExtensionRegex.test(url.pathname.toLowerCase()) && RegExp.$1;
if (extension) {
switch (extension) {
@@ -39,6 +42,11 @@ function transformHttpLink(a: string, pref: UserPreferences) {
case "webp": {
return
;
}
+ case "wav":
+ case "mp3":
+ case "ogg": {
+ return
+ }
case "mp4":
case "mov":
case "mkv":
@@ -75,6 +83,8 @@ function transformHttpLink(a: string, pref: UserPreferences) {
return
} else if (soundcloundId){
return
+ } else if (mixcloudId){
+ return
} else {
return
e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">{a}
}
@@ -207,5 +217,26 @@ export default function Text({ content, tags, users }: TextProps) {
li: (x: any) => transformLi({ body: x.children ?? [], tags, users, pref }),
};
}, [content]);
- return
{content}
+ const disableMarkdownLinks = useCallback(() => (tree: any) => {
+ visit(tree, (node, index, parent) => {
+ if (
+ parent &&
+ typeof index === 'number' &&
+ (node.type === 'link' ||
+ node.type === 'linkReference' ||
+ node.type === 'image' ||
+ node.type === 'imageReference' ||
+ node.type === 'definition')
+ ) {
+ node.type = 'text';
+ node.value = content.slice(node.position.start.offset, node.position.end.offset).replace(/\)$/, ' )');
+ return SKIP;
+ }
+ })
+ }, [content]);
+ return
{content}
}
diff --git a/src/Element/Textarea.tsx b/src/Element/Textarea.tsx
index 79397df2..c71a92fc 100644
--- a/src/Element/Textarea.tsx
+++ b/src/Element/Textarea.tsx
@@ -2,7 +2,6 @@ import "@webscopeio/react-textarea-autocomplete/style.css";
import "./Textarea.css";
import { useState } from "react";
-import { useLiveQuery } from "dexie-react-hooks";
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
import emoji from "@jukben/emoji-search";
import TextareaAutosize from "react-textarea-autosize";
@@ -10,8 +9,8 @@ import TextareaAutosize from "react-textarea-autosize";
import Avatar from "Element/Avatar";
import Nip05 from "Element/Nip05";
import { hexToBech32 } from "Util";
-import { db } from "Db";
-import { MetadataCache } from "Db/User";
+import { MetadataCache } from "State/Users";
+import { useQuery } from "State/Users/Hooks";
interface EmojiItemProps {
name: string
@@ -45,16 +44,7 @@ const UserItem = (metadata: MetadataCache) => {
const Textarea = ({ users, onChange, ...rest }: any) => {
const [query, setQuery] = useState('')
- const allUsers = useLiveQuery(
- () => db.users
- .where("npub").startsWithIgnoreCase(query)
- .or("name").startsWithIgnoreCase(query)
- .or("display_name").startsWithIgnoreCase(query)
- .or("nip05").startsWithIgnoreCase(query)
- .limit(5)
- .toArray(),
- [query],
- );
+ const allUsers = useQuery(query)
const userDataProvider = (token: string) => {
setQuery(token)
diff --git a/src/Element/Timeline.tsx b/src/Element/Timeline.tsx
index 0324309f..e9e1a88f 100644
--- a/src/Element/Timeline.tsx
+++ b/src/Element/Timeline.tsx
@@ -10,6 +10,7 @@ import LoadMore from "Element/LoadMore";
import Note from "Element/Note";
import NoteReaction from "Element/NoteReaction";
import useModeration from "Hooks/useModeration";
+import ProfilePreview from "./ProfilePreview";
export interface TimelineProps {
postsOnly: boolean,
@@ -41,6 +42,9 @@ export default function Timeline({ subject, postsOnly = false, method, ignoreMod
function eventElement(e: TaggedRawEvent) {
switch (e.kind) {
+ case EventKind.SetMetadata: {
+ return
+ }
case EventKind.TextNote: {
return
}
diff --git a/src/Element/ZapButton.tsx b/src/Element/ZapButton.tsx
index 147b1871..8ab5b1f6 100644
--- a/src/Element/ZapButton.tsx
+++ b/src/Element/ZapButton.tsx
@@ -2,12 +2,13 @@ import "./ZapButton.css";
import { faBolt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useState } from "react";
-import useProfile from "Feed/ProfileFeed";
+import { useUserProfile } from "Feed/ProfileFeed";
import { HexKey } from "Nostr";
import LNURLTip from "Element/LNURLTip";
+
const ZapButton = ({ pubkey, svc }: { pubkey?: HexKey, svc?: string }) => {
- const profile = useProfile(pubkey)?.get(pubkey ?? "");
+ const profile = useUserProfile(pubkey!)
const [zap, setZap] = useState(false);
const service = svc ?? (profile?.lud16 || profile?.lud06);
diff --git a/src/Feed/EventPublisher.ts b/src/Feed/EventPublisher.ts
index c4d44a36..9d46ae4b 100644
--- a/src/Feed/EventPublisher.ts
+++ b/src/Feed/EventPublisher.ts
@@ -14,7 +14,7 @@ declare global {
nostr: {
getPublicKey: () => Promise
,
signEvent: (event: RawEvent) => Promise,
- getRelays: () => Promise<[[string, { read: boolean, write: boolean }]]>,
+ getRelays: () => Promise>,
nip04: {
encrypt: (pubkey: HexKey, content: string) => Promise,
decrypt: (pubkey: HexKey, content: string) => Promise
@@ -72,12 +72,22 @@ export default function useEventPublisher() {
return match;
}
const content = msg.replace(/@npub[a-z0-9]+/g, replaceNpub)
- .replace(/note[a-z0-9]+/g, replaceNoteId)
- .replace(HashtagRegex, replaceHashtag);
+ .replace(/note[a-z0-9]+/g, replaceNoteId)
+ .replace(HashtagRegex, replaceHashtag);
ev.Content = content;
}
return {
+ nip42Auth: async (challenge: string, relay:string) => {
+ if(pubKey) {
+ const ev = NEvent.ForPubKey(pubKey);
+ ev.Kind = EventKind.Auth;
+ ev.Content = "";
+ ev.Tags.push(new Tag(["relay", relay], 0));
+ ev.Tags.push(new Tag(["challenge", challenge], 1));
+ return await signEvent(ev);
+ }
+ },
broadcast: (ev: NEvent | undefined) => {
if (ev) {
console.debug("Sending event: ", ev);
@@ -90,8 +100,8 @@ export default function useEventPublisher() {
* When they open the site again we wont see that updated relay list and so it will appear to reset back to the previous state
*/
broadcastForBootstrap: (ev: NEvent | undefined) => {
- if(ev) {
- for(let [k, _] of DefaultRelays) {
+ if (ev) {
+ for (let [k, _] of DefaultRelays) {
System.WriteOnceToRelay(k, ev);
}
}
@@ -205,6 +215,9 @@ export default function useEventPublisher() {
temp.add(pkAdd);
}
for (let pk of temp) {
+ if (pk.length !== 64) {
+ continue;
+ }
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
}
@@ -217,7 +230,7 @@ export default function useEventPublisher() {
ev.Kind = EventKind.ContactList;
ev.Content = JSON.stringify(relays);
for (let pk of follows) {
- if (pk === pkRemove) {
+ if (pk === pkRemove || pk.length !== 64) {
continue;
}
ev.Tags.push(new Tag(["p", pk], ev.Tags.length));
diff --git a/src/Feed/LoginFeed.ts b/src/Feed/LoginFeed.ts
index cfc8a2a5..d7b5e46a 100644
--- a/src/Feed/LoginFeed.ts
+++ b/src/Feed/LoginFeed.ts
@@ -7,12 +7,14 @@ import EventKind from "Nostr/EventKind";
import Event from "Nostr/Event";
import { Subscriptions } from "Nostr/Subscriptions";
import { addDirectMessage, setFollows, setRelays, setMuted, setBlocked, sendNotification } from "State/Login";
-import type { RootState } from "State/Store";
-import { db } from "Db";
-import { barierNip07 } from "Feed/EventPublisher";
+import { RootState } from "State/Store";
+import { mapEventToProfile, MetadataCache } from "State/Users";
+import { getDb } from "State/Users/Db";
import useSubscription from "Feed/Subscription";
+import { getDisplayName } from "Element/ProfileImage";
+import { barierNip07 } from "Feed/EventPublisher";
import { getMutedKeys, getNewest } from "Feed/MuteList";
-import { mapEventToProfile, MetadataCache } from "Db/User";
+import { MentionRegex } from "Const";
import useModeration from "Hooks/useModeration";
/**
@@ -105,9 +107,10 @@ export default function useLoginFeed() {
return acc;
}, { created: 0, profile: null as MetadataCache | null });
if (maxProfile.profile) {
- let existing = await db.users.get(maxProfile.profile.pubkey);
+ const db = getDb()
+ let existing = await db.find(maxProfile.profile.pubkey);
if ((existing?.created ?? 0) < maxProfile.created) {
- await db.users.put(maxProfile.profile);
+ await db.put(maxProfile.profile);
}
}
})().catch(console.warn);
@@ -152,6 +155,7 @@ export default function useLoginFeed() {
}, [dispatch, dmsFeed.store]);
}
+
async function decryptBlocked(raw: TaggedRawEvent, pubKey: HexKey, privKey?: HexKey) {
const ev = new Event(raw)
if (pubKey && privKey) {
diff --git a/src/Feed/ProfileFeed.ts b/src/Feed/ProfileFeed.ts
index b439f814..c8669410 100644
--- a/src/Feed/ProfileFeed.ts
+++ b/src/Feed/ProfileFeed.ts
@@ -1,27 +1,13 @@
import { useLiveQuery } from "dexie-react-hooks";
-import { useEffect } from "react";
-import { db } from "Db";
-import { MetadataCache } from "Db/User";
+import { useEffect, useMemo } from "react";
+import { RootState } from "State/Store";
+import { MetadataCache } from "State/Users";
+import { useKey, useKeys } from "State/Users/Hooks";
import { HexKey } from "Nostr";
import { System } from "Nostr/System";
-export default function useProfile(pubKey?: HexKey | Array | undefined): Map | undefined {
- const user = useLiveQuery(async () => {
- let userList = new Map();
- if (pubKey) {
- if (Array.isArray(pubKey)) {
- let ret = await db.users.bulkGet(pubKey);
- let filtered = ret.filter(a => a !== undefined).map(a => a!);
- return new Map(filtered.map(a => [a.pubkey, a]))
- } else {
- let ret = await db.users.get(pubKey);
- if (ret) {
- userList.set(ret.pubkey, ret);
- }
- }
- }
- return userList;
- }, [pubKey]);
+export function useUserProfile(pubKey: HexKey): MetadataCache | undefined {
+ const users = useKey(pubKey);
useEffect(() => {
if (pubKey) {
@@ -30,5 +16,19 @@ export default function useProfile(pubKey?: HexKey | Array | undefined):
}
}, [pubKey]);
- return user;
-}
\ No newline at end of file
+ return users;
+}
+
+
+export function useUserProfiles(pubKeys: Array): Map | undefined {
+ const users = useKeys(pubKeys);
+
+ useEffect(() => {
+ if (pubKeys) {
+ System.TrackMetadata(pubKeys);
+ return () => System.UntrackMetadata(pubKeys);
+ }
+ }, [pubKeys]);
+
+ return users;
+}
diff --git a/src/Feed/TimelineFeed.ts b/src/Feed/TimelineFeed.ts
index fe259421..0fc9c67a 100644
--- a/src/Feed/TimelineFeed.ts
+++ b/src/Feed/TimelineFeed.ts
@@ -13,7 +13,7 @@ export interface TimelineFeedOptions {
}
export interface TimelineSubject {
- type: "pubkey" | "hashtag" | "global" | "ptag",
+ type: "pubkey" | "hashtag" | "global" | "ptag" | "keyword",
items: string[]
}
@@ -47,6 +47,11 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
sub.PTags = new Set(subject.items);
break;
}
+ case "keyword": {
+ sub.Kinds.add(EventKind.SetMetadata);
+ sub.Search = subject.items[0];
+ break;
+ }
}
return sub;
}, [subject.type, subject.items]);
@@ -72,6 +77,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
latestSub.Authors = sub.Authors;
latestSub.HashTags = sub.HashTags;
latestSub.Kinds = sub.Kinds;
+ latestSub.Search = sub.Search;
latestSub.Limit = 1;
latestSub.Since = Math.floor(new Date().getTime() / 1000);
sub.AddSubscription(latestSub);
@@ -123,7 +129,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
if (main.store.notes.length > 0) {
setTrackingEvent(s => {
let ids = main.store.notes.map(a => a.id);
- if(ids.some(a => !s.includes(a))) {
+ if (ids.some(a => !s.includes(a))) {
return Array.from(new Set([...s, ...ids]));
}
return s;
@@ -165,4 +171,4 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
latest.clear();
}
};
-}
\ No newline at end of file
+}
diff --git a/src/Feed/VoidUpload.ts b/src/Feed/VoidUpload.ts
index 8bb2ae4b..53c4aeb0 100644
--- a/src/Feed/VoidUpload.ts
+++ b/src/Feed/VoidUpload.ts
@@ -1,4 +1,5 @@
import * as secp from "@noble/secp256k1";
+import { VoidCatHost } from "Const";
/**
* Upload file to void.cat
@@ -8,7 +9,7 @@ export default async function VoidUpload(file: File | Blob, filename: string) {
const buf = await file.arrayBuffer();
const digest = await crypto.subtle.digest("SHA-256", buf);
- let req = await fetch(`https://void.cat/upload`, {
+ let req = await fetch(`${VoidCatHost}/upload`, {
mode: "cors",
method: "POST",
body: buf,
@@ -17,7 +18,8 @@ export default async function VoidUpload(file: File | Blob, filename: string) {
"V-Content-Type": file.type,
"V-Filename": filename,
"V-Full-Digest": secp.utils.bytesToHex(new Uint8Array(digest)),
- "V-Description": "Upload from https://snort.social"
+ "V-Description": "Upload from https://snort.social",
+ "V-Strip-Metadata": "true"
}
});
diff --git a/src/Icons/Envelope.tsx b/src/Icons/Envelope.tsx
index 8e4d0723..7ed1bc5f 100644
--- a/src/Icons/Envelope.tsx
+++ b/src/Icons/Envelope.tsx
@@ -1,6 +1,8 @@
-const Envelope = () => {
+import type IconProps from './IconProps'
+
+const Envelope = (props: IconProps) => {
return (
-
+ {today && (